安卓开发通过自定义Gradle插件实现自动化埋点

一 埋点现状:

公司APP在实际开发中埋点是一个很碎片化的问题,我总结了以下几点:

1.变化快,埋点文档经常变化,开发人员不得不修改代码,造成了一定的风险

2.页面埋点之前是通过把握Activity和Fragment的生命周期实现大部分的统一配置,但是这里面又牵扯到

Fragment嵌套和ViewPager的加入,引起生命周期的难以精确把控,况且随着代码的变化这些生命周期

可能又会发生变化,造成了埋点的错误

3.漏埋多埋,很多店可能已经过期不用或者开发人员少埋了部分点,造成大数据的数据误差

4.事件埋点分散性大,难以做到统一把控

5.埋点代码散布在业务代码中,一定程度上干扰了开发人员,造成不必要的麻烦


二:未来自动化埋点的期望

1.埋点零代码,我不希望在我们的项目中看到任何关于埋点的代码

2.通过在一个外部配置文件进行简单的配置就能实现自动插入需要的埋点

3.当我们的埋点发生变化时,打包时候自动发出提醒,告诉我什么点没有埋,

什么点已经在程序中没有合适的埋点位置了

4.可以精准的按照配置文件指定的位置埋点

三:解决办法思考

最近在尝试使用第三方监控oneApm和听云,发现他们只需要一行代码就能监控那么多的数据,甚是感觉神奇,后来得知对class

文件进行代码插入.也就是说当我们只想gradle assembleRelease的时候,首先我们的JAVA代码会被gradle 编译生成class文件,

然后再组装为DEX文件,最终生成APK文件,那么我们就是要在CLASS文件转换为DEX过程中,对CLASS文件进行修改,插入

我们想要的埋点代码,要实现这一点我使用了两个技术:gradle官方提供的Transform和JAVASSIST技术

Transform:一个gradle包,通过它我们可以输入打包过程中的class文件,然后输出我们修改后的class文件,

简单点说就是个输入输出的东西

JAVASSIST:主要同于字节码修改,另外还有其他类似的字节码修改的框架,例如ASM等

原则上通过上面两个技术就可以实现我们定向修改class文件的目的了


四:具体实现

1.技术准备:

A:gradle插件如何开发,

B:gradle的Transform使用(输入输出CLASS)

C"JAVASSIST(改写class)

D:groovy语法

E:POI(读取EXCEl)


微笑实现第一步:输入输出CLASS,下面一段代码是输入gradle打包时候的编译代码,并且改装后输出的逻辑

  @Override
    public  void transform(Context context, Collection<TransformInput> inputs,
                   Collection<TransformInput> referencedInputs,
                   TransformOutputProvider outputProvider, boolean isIncremental)
            throws IOException, TransformException, InterruptedException {
        // Transform的inputs有两种类型,一种是目录,一种是jar包,要分开遍历
        inputs.each { TransformInput input ->
            //对类型为“文件夹”的input进行遍历
            input.directoryInputs.each { DirectoryInput directoryInput ->
                //文件夹里面包含的是我们手写的类以及R.class、BuildConfig.class以及R$XXX.class等
                <span style="color:#FF0000;"><strong>BuryInject.injectDir(directoryInput.file.absolutePath,"com\\sasas\\dsdsd")
                // 获取output目录</strong></span>
                def dest = outputProvider.getContentLocation(directoryInput.name,
                        directoryInput.contentTypes, directoryInput.scopes,
                        Format.DIRECTORY)

                // 将input的目录复制到output指定目录
                FileUtils.copyDirectory(directoryInput.file, dest)
            }
            //对类型为jar文件的input进行遍历
            input.jarInputs.each { JarInput jarInput ->

                //jar文件一般是第三方依赖库jar文件

                // 重命名输出文件(同目录copyFile会冲突)
                def jarName = jarInput.name
                def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
                if (jarName.endsWith(".jar")) {
                    jarName = jarName.substring(0, jarName.length() - 4)
                }
                //生成输出路径
                def dest = outputProvider.getContentLocation(jarName + md5Name,
                        jarInput.contentTypes, jarInput.scopes, Format.JAR)
                //将输入内容复制到输出
                FileUtils.copyFile(jarInput.file, dest)
            }
        }
    }

微笑 实现第二步:通过JAVASSIST改写CLASS,分别判断不同的行为插入不同的代码逻辑

 public static void injectDir(String path, String packageName) {
        if(cellModelList == null){
            cellModelList = new ArrayList<CellModel>();
            System.out.println("构建Cell模型数组")
            new Test("aa").salute()
       //     cellModelList = JSON.parseArray(modelData,CellModel.class);
            System.out.println("结束构建Cell模型数组:"+cellModelList.size())
        }
        /**设置类搜索路径**/
        pool.appendClassPath(path)
        pool.insertClassPath("D:\\adt\\sdk\\platforms\\android-23\\android.jar");
        File dir = new File(path)
        if (dir.isDirectory()) {
            dir.eachFileRecurse { File file ->

                String filePath = file.absolutePath
                System.out.println("路径:"+filePath);
                //D:\androidprogram\rkapp\trunk\app\build\intermediates\transforms\MyTrans\custom\release\jars\1\10\
                //确保当前文件是class文件,并且不是系统自动生成的class文件
                if (filePath.endsWith(".class")
                        && !filePath.contains('R$')
                        && !filePath.contains('R.class')
                        && !filePath.contains("BuildConfig.class")) {
                    // 判断当前目录是否是在我们的应用包里面
                    int index = filePath.indexOf(packageName);
                    boolean isMyPackage = index != -1;
                    if (isMyPackage) {
                        int end = filePath.length() - 6 // .class = 6
                        String className = filePath.substring(index, end).replace('\\', '.').replace('/', '.')
                        List<CellModel> cellFilterModelList =  getCellModelListByClassName(className);
                        if(cellFilterModelList == null || cellFilterModelList.size() == 0){
                            println className +"对应模型数:无";
                            return ;
                        }else {
                            println className +"对应模型数: "+cellFilterModelList.size();
                        }
                        //开始修改class文件
                        CtClass c = pool.getCtClass(className)

                        if (c.isFrozen()) {
                            c.defrost()
                        }
                        pool.importPackage("com.xxxxx.xx.report.api.ReportPoint");
                            pool.importPackage("com.xxxxx.xx.report.api.ReportClient");
                        for (int i = 0;i<cellFilterModelList.size();i++){
                            try {
                                CellModel cellModel = cellFilterModelList.get(i);
                                System.out.println("方法名:"+cellModel.getMethodName()+" 行为:"+cellModel.getAction());
                                if(cellModel.getAction().equals("点击事件")){
                                    // 获取String类型参数集合
                                //   CtClass[] paramTypes = {pool.get(View.class.getName())};
                                    String methodName = cellModel.getMethodName();
                                    CtMethod   method4 = c.getDeclaredMethod(cellModel.getMethodName());
                                    if(method4 != null){
                                        println methodName +" method is not null"
                                    }else {
                                        println methodName+"method is  null"
                                    }
                                    method4.insertBefore(ExecuteCode.createReportOnClick(cellModel.getEventId()))
                                }else {
                                    CtMethod  method4 = c.getDeclaredMethod(cellModel.getMethodName());
                                    if(cellModel.getAction().equals("页面进入")){//待完善,用方法名不合适,可以构建类型来区别
                                        println "页面进入 method start insert code"
                                        String code = ExecuteCode.createReportOnResume(cellModel.getPageId(),cellModel.getBussinessType(),cellModel.getEventId());
                                        println "页面进入 method start insert code is:${code}"
                                        method4.insertBefore(code)
                                    }else if(cellModel.getAction().equals("页面离开")){
                                        println "页面离开 start insert code"
                                        String code = ExecuteCode.createReportOnPause(cellModel.getPageId(),cellModel.getBussinessType(),cellModel.getEventId());
                                        println "页面离开 method start insert code is:${code}"
                                        method4.insertBefore(code)
                                    }
                                }
                            }catch (Throwable e){
                                System.out.println(e)
                                System.out.println("改造异常"+e.getMessage());
                            }
                        }
                        c.writeFile(path)
                        c.detach()/**从类池中移除该对象,避免加载过多造成内存溢出问题*/
                    }
                }
            }
        }


以上就是实现自动化埋点的核心代码了,最终的代码结构截图



五:最终如何使用:


所有点人员维护一个EXCEL文档即可,我利用上面的插件读取你指定的EXCEL文件和插入相关埋点代码




反编译APK展示







六:在项目中使用自动化埋点插件

 1.在项目根目录下的build.gradle中依赖自动化埋点插件

   dependencies {
        classpath 'com.xxxxx.bury:buryplugin:1.0.0'
    }
2.在APP主模块目录下的build.gradle应用该插件
apply plugin: 'com.xxxxx.bury'

3.运行打包命令gradle assembleRelease或者graldew assembleDebug插件即可得到执行,
埋点代码得到插入


七:开发遇到的其他问题

1.插件源码中libs中JAR包引用带不到生成的插件的问题

   我把jar包拷贝到安卓的plugin文件夹居然好了,这要感谢oneAPM的使用,完全是猜的,百度都找不到解决办法,困扰了一天啊

2016年111月17日补充:今天在项目根build文件中看到

buildscript {
    repositories {
        maven {
            url uri('file:D:/androidprogram/library_20161024_v25/Nuwa/trunk/repo')//nuwa插件库位置
        }
     /*   maven {
            url uri('repo')
        }*/
        jcenter()
        mavenCentral()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.1.2'
        classpath fileTree(dir: 'plugin', include: ['*.jar'])    }
}
原来我们是可以通过这样的方式指示插件的问题的哦


2.onClick带有View参数获取该方法修改造成编译失败问题

插件类池中缺少安卓的View对象,

   pool.insertClassPath("D:\\adt\\sdk\\platforms\\android-23\\android.jar");

居然这要就能找到了,也是试了很久才可以,我是从http://blog.csdn.net/liuwei063608/article/details/38020203

文章知道的,因为人家要修改JAR包中的类,哎试了好久

3.导包只能导入一个问题的解决

  pool.importPackage(
  pool.importPackage(

导入两个实际只有一个,可能是源码中已经导入了一个,我调换了下两行代码,两个包都被正常导入了,原因不详!!!








  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 13
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值