推广

埋点的探索,自动注入的方案

iseeyu2年前 (2024-02-22)推广130

img1.png

(2)在resources 文件夹下创建 xxxx.properties 文件并设置implementation-class ,properties 文件名为 插件对外引用名称既主项目引用插件名,implementation-class 定义插件 主文件。

implementation-class=com.awarmisland.plugin.CusPlugin

(3) 设置buildSrc 的 build 文件,引入groovy ,和 gradle api 同步下项目

apply plugin: 'groovy'  //必须
apply plugin: 'maven'
dependencies {
    implementation gradleApi() //必须
    implementation localGroovy() //必须
    //如果要使用android的API,需要引用这个,实现Transform的时候会用到
    implementation 'com.android.tools.build:gradle:3.1.3'
    implementation 'com.android.tools.build:gradle-api:3.1.3'
}
repositories {
    google()
    jcenter()
    mavenCentral() //必须
}

(4)主项目引入buildSrc插件

apply plugin: 'com.awarmisland.plugin'

(5) CusPlugin 继承 PluginProject, 通过apply 添加需要执行的task,Transform 就是我们需要编写的 自定义编译class task 可以引入多个task, 当我们执行build project时候,在AS build窗口会看到我们自定义的task。

def android = project.extensions.getByType(AppExtension)
 //注册Transform
android.registerTransform(new ActivityLifecycleTransform(project),Collections.EMPTY_LIST)
android.registerTransform(new FragmentLifecycleTransform(project),Collections.EMPTY_LIST)
android.registerTransform(new RecordTransform(project),Collections.EMPTY_LIST)

(6)定义BaseTransform 主要设计目的是 为了抽离编译过程的代码,统筹分类处理。Transform task 任务是依次执行,所以当我们读取了class 文件修改处理后,需要覆盖原来文件,交给下一个task 执行。p.s. 理论大概就是这样,这里需要小心处理,不然很容易编译不通过。

abstract class BaseTransform extends Transform implements TransformInterface{
@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        def transformName = getName();
        println '--------------- '+transformName+' visit start --------------- '
        def startTime = System.currentTimeMillis()
        Collection<TransformInput> inputs = transformInvocation.inputs
        TransformOutputProvider outputProvider = transformInvocation.outputProvider
        //删除之前的输出
        if (outputProvider != null)
            outputProvider.deleteAll()
        //遍历inputs
        inputs.each { TransformInput input ->
            //遍历directoryInputs
            input.directoryInputs.each { DirectoryInput directoryInput ->
                //处理directoryInputs
                handleDirectoryInput(directoryInput, outputProvider)
            }

            //遍历jarInputs
            input.jarInputs.each { JarInput jarInput ->
                //处理jarInputs
                handleJarInputs(jarInput, outputProvider)
            }
        }
        def cost = (System.currentTimeMillis() - startTime) / 1000
        println '--------------- '+transformName+' visit end --------------- '
        println transformName+" cost : $cost s"
}

 /**
     * 遍历sdk 中的class
     * 处理Jar中的class文件
     */
     void handleJarInputs(JarInput jarInput, TransformOutputProvider outputProvider) {
        if (jarInput.file.getAbsolutePath().endsWith(".jar")) {
            //重名名输出文件,因为可能同名,会覆盖
            def jarName = jarInput.name
            def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
            if (jarName.endsWith(".jar")) {
                jarName = jarName.substring(0, jarName.length() - 4)
            }

            JarFile jarFile = new JarFile(jarInput.file)
            Enumeration enumeration = jarFile.entries()
            File tmpFile = new File(jarInput.file.getParent() + File.separator + "classes_temp.jar")
            //避免上次的缓存被重复插入
            if (tmpFile.exists()) {
                tmpFile.delete()
            }
            JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(tmpFile))
            //用于保存
            while (enumeration.hasMoreElements()) {
                JarEntry jarEntry = (JarEntry) enumeration.nextElement()
                String entryName = jarEntry.getName()
                ZipEntry zipEntry = new ZipEntry(entryName)
                InputStream inputStream = jarFile.getInputStream(jarEntry)
//                println("className: "+entryName)
                jarOutputStream.putNextEntry(zipEntry)
                //处理 插桩class
                if(isModifyClass(entryName)&&entryName.endsWith(".class")){
                    byte[] code = modifyClass(entryName, IOUtils.toByteArray(inputStream))
                    if(code){
                        jarOutputStream.write(code)
                    }else{
                        jarOutputStream.write(IOUtils.toByteArray(inputStream))
                    }
                }else{
                    jarOutputStream.write(IOUtils.toByteArray(inputStream))
                }
                jarOutputStream.closeEntry()
            }
            //结束
            jarOutputStream.close()
            jarFile.close()

            def dest = outputProvider.getContentLocation(jarName + md5Name,
                    jarInput.contentTypes, jarInput.scopes, Format.JAR)
            FileUtils.copyFile(tmpFile, dest)
            tmpFile.delete()
        }
    }

(7)现在来看看实践,我们需要在 Activity 生命周期中埋点,记录Activity 生命周期事件。这个时候我们就得在基类FragmentActivity 中 注入我们的埋点代码。
    前面的BaseTransform 基类 已经封装好 遍历class 的调度方法。我们继承它 定义一个ActivityLifecycleTransform,isModifyClass 用于过滤需要修改的class 文件,modifyClass为主要处理 注入代码逻辑方法。
    重点来了,如何实现代码注入呢?代码注入就是需要 修改class 文件,ASM 帮到你。(其实还有其它库,比如Javassist)
    ASM是字节码处理库,常用处理元素ClassVisitor MethodVisitor 对应 类访,方法访问。在modifyClass 方法中,我们通过ClassReader 读取 class 文件,再通过ClassWrite 授权修改class 文件。

class ActivityLifecycleTransform extends BaseTransform {

    ActivityLifecycleTransform(Project p) {
        super(p)
    }

    @Override
    String getName() {
        return "ActivityLifecycleTransform"
    }

    @Override
    boolean isModifyClass(String className) {
        if ("android/support/v4/app/FragmentActivity.class".equals(className)) {
            return true
        }
        return false
    }

    byte[] modifyClass(String className, byte[] classBytes) {
        println '----------- deal with "class" file <' + className + '> -----------'
        ClassReader classReader = new ClassReader(classBytes)
        ClassWriter classWriter = new ClassWriter(classReader,ClassWriter.COMPUTE_MAXS)
        ClassVisitor cv = new LifecycleClassVisitor(classWriter)
        classReader.accept(cv,EXPAND_FRAMES)
        return classWriter.toByteArray()
    }
}

(8)自定义LifecycleClassVisitor 集成ClassVisitor , ClassVisitor 对类 内部元素读取也是有规律的,我们暂时不研究,对方法的读取回调在visitMethod ,我们可以获取到 方法名name, 通过方法名过滤出需要埋点的方法。

public class LifecycleClassVisitor extends ClassVisitor {
    private String mClassName;

    public LifecycleClassVisitor(ClassVisitor cv) {
        super(Opcodes.ASM5,cv);
    }

    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
//        System.out.println("LifecycleClassVisitor:visit----->started"+name);
        this.mClassName = name;
        super.visit(version, access, name, signature, superName, interfaces);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { ;
        MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
        LifecycleMethodVisitor method  = new LifecycleMethodVisitor(mv);
        //匹配FragmentActivity
        if ("onResume".equals(name)
                ||"onStop".equals(name)) {
            //处理onCreate
            method.setLifecycleName(name);
            return method;
        }
        return mv;
    }
}

(9) 和ClassVistor 一样,MethodVisitor 用于 访问 method 中代码,也是有其访问规律,可以说是访问的生命周期。 visitCode 开始访问代码,此时,我们开始在这里注入字节代码。mv.xxxx 6行代码 其实代表着 DotComponent.getInstance().recordLifecycle(this.getClass().getName(), lifecycleName); 这一句埋点 执行逻辑代码。java在编译成class 文件前,会先转化成 机器可识别的字节码 ,然后再编译成二进制码。 现在我们就用ASM 语法手动创建了 需要注入的逻辑代码的字节码。这个时候肯定有人问,那注入代码 岂不是需要另外学习字节码的语法规则? 其实总得来说,如果你需要深入定制,就有必要学习了,但是我们只是简单使用的话,知道一点皮毛就ok ,而且我们是可以通过工具生成字节码的。

public class LifecycleMethodVisitor extends MethodVisitor {

    private String lifecycleName;

    public LifecycleMethodVisitor(MethodVisitor mv){
        super(Opcodes.ASM5, mv);
    }

    @Override
    public void visitCode() {
        super.visitCode();
        //方法执行后插
        //  DotComponent.getInstance().recordLifecycle(this.getClass().getName(), lifecycleName);
        mv.visitMethodInsn(Opcodes.INVOKESTATIC, DOT_PATH, "getInstance", "()L"+DOT_PATH+";", false);
        mv.visitVarInsn(Opcodes.ALOAD, 0);
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Object", "getClass", "()Ljava/lang/Class;", false);
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Class", "getName", "()Ljava/lang/String;", false);
        mv.visitLdcInsn(lifecycleName);
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, DOT_PATH, "recordLifecycle", "(Ljava/lang/String;Ljava/lang/String;)V", false);
    }
}

Plugins 搜索 ASM 找到ASM Bytecode Outline 安装,然后就可以在需要 注入的java 文件 右键 生成字节码,具体的方法可以找度娘,很多介绍的

img2.png

最后~ build project 就会将代码注入到FragmentActivity onResume 方法中

img3.png

img4.jpg

4.Github地址

https://github.com/awarmisland/BuryingPoint

5.参考文章

http://www.woshipm.com/data-analysis/450268.html
https://blog.csdn.net/jiang547860818/article/details/64121698?utm_source=blogkpcl0
https://www.jianshu.com/p/a1e6b3abd789
关于plugin debug 可参考这个文章
https://www.jianshu.com/p/99c8e953654e

扫描二维码推送至手机访问。

版权声明:本文由西安泽虎代运营发布,如需转载请注明出处。

转载请注明出处https://www.0291.com.cn/post/56231.html

相关文章

教你关于网站推广的六个方式分享。

教你关于网站推广的六个方式分享。

如何做?如今,许多公司刚刚开始接触互联网技术。根据互联网推广的,很多公司都从中获得了高额利润。网站推广的期限很长,为让更多的人知道自己网站建成后应该如何做。接下来我将向大家介绍网站的六点网站推广方法,以达到更好的网站推广效果。 网站全是应用场景百度搜索引擎,如果不是百度搜索引擎我们都是浏...

企业如何开展软文营销,优质的软文营销从哪些方面入手 ...

企业如何开展软文营销,优质的软文营销从哪些方面入手 ...

  随着互联网的发展,信息迅速传播,软文在网络中很受欢迎,软文的写作很重要,优质的文章带来的效果很高的。那么企业做工作可以从哪些方面入手呢?  一、适合受众群体  软文基本上都是通过网上平台来发布,因此软文的主要受众体可能是在80后到90后,这两代之间。因此,他们都具备一些...

如何定好财富目标,这几点真的很重要

如何定好财富目标,这几点真的很重要

“昔日龌龊不足夸,今朝放荡思无涯。春风得意马蹄疾,一日看尽长安花。”唐朝诗人孟郊的这首《登科后》,讲述的是他在描绘登榜提名之后的快意人生,其实对于我们追求财富也是一样,需要在心中确定好自己为之奋斗的财富目标,然后一步一步去实现它,但是最重要的是,怎么去确定自己的财富目标呢,...

什么是网络推广带你了解新上线的网站该如何提升关键词排名?

什么是网络之在做网站优化时,关键词优化也是整个优化工作的重中之重,因为关键词排名对网站的推广以及用户认可等都具有重要的作用。尤其是新上线的网站来说,更要注重关键词优化方法,下面什么是网络推广就带大家一起来了解一下。1、提前做好词库规划布局尤其对新网站来说,在整个网站关键词布...

为什么你的活动能吸到粉,却留不住粉?

为什么你的活动能吸到粉,却留不住粉?

做活动不是消耗这群人,而是让这群人觉得你做了一件对我有用的事。 做活动是企业最常用的涨粉手段,尤其是在微信后红利时代,爆款内容越来越难打造,策划一场好的涨粉活动成为了很多企业的营销首选。完整的涨粉活动一般可分为 2 个阶段:第一阶段吸引粉丝,简称“涨粉”;第二阶段留住粉丝,简称“留存”。...

为实体企业输入流量“原油”,百度营销解码创新增长独家心法 ...

为实体企业输入流量“原油”,百度营销解码创新增长独家心法 ...

在流量见顶、竞争激烈、增长受限的大环境下,企业如何找到破局之道,找到驱动增长的确定性?12月21日,2022百度热AI峰会在上海召开。百度集团资深副总裁、百度移动生态事业群组(MEG)总经理何俊杰表示,百度营销的独家心法是“创新驱动增长”。 (百度集团资深副总裁、百度移动生...

现在,非常期待与您的又一次邂逅

我们努力让每一部企业宣传片和抖音短视频成为商业大片