美团热修复Robust源码庖丁解牛(第一篇字节码插桩)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/xiatiandefeiyu/article/details/79020527

如果你想对java编译后的class文件做一些手脚的话,市面上有供你选择的asm、javassist、aspectJ(aop面向切面编程)等等,一般修改class文件的用途有你想统计一些东西,例如app崩溃日志,做为程序员应该要懒,不想写统计的代码,这个时候利用编译器改变class的方式,来选择是否上传错误信息。记得代码中你总想打印一些信息,比如路径、又或者是接口api为你返回的一些信息、又如你判断此段代码是否正常运行、又如打印执行的时间、又如堆栈信息等等,这时你也可以用改变字节码的方式,在debug的时候插入打印代码,在正式版的时候不插入打印代码。如果你想统计方法执行时间,那么也可以通过改变字节码的方式实现在测试版中的测试,以便优化方法代码。总而言之,如果你想对你的代码进行动态的控制(俗称自动化埋点),那么改变字节码的方式非常适合你。


在正式看源码之前,首先你得了解gradle的相关知识,比如它是什么?在这里你可以把它理解为脚本构造框架,额,脚本是什么?如果你用过Liunx的话应该知道shell脚本,它是干什么用的,当然是写编译脚本的了,例如你用gcc命令编译c文件的话,如果你的项目很大了,你不可能为每一个c文件写一个gcc命令进行编译吧,这就是构建脚本的作用了。这里我们开发的android,那么用的就是android studio工具,它已经很好的集成了gradle,并且它还提供了applyplugin:'com.android.application',这他妹的就是一个插件,插件是啥?这个插件咱们可以理解为它承担了java变class,class变dex的工作,也就是说这个插件脚本封装了javac、dx等等的命令,最终它还是调用了谷歌提供的批处理器工具(aapt、aidl、dx.bat等等)。

 

那么怎么用gradle写一个自己的插件呢?好在官方文档介绍详细实现gradle插件,每一个插件都对应一个入口类void apply(Projecttarget),也就是说,你只要定义自己的插件成功后,重新进行编译(这里点击clean project)的话就会执行到这个方法,这里可以把插件理解为一个任务集合,它包括了很多任务,每个任务都承担起自己的工作,比如android plugin就有很多编译的任务,如下图



例如上图中的assembleDebug任务就是你默认运行的时候调用的编译任务,也就是你的apk签名包证书是工具自带的debug.keystore的时候,assembleDebug任务起作用。


认识完了Gradle插件化,也算是找到了入口,接下来就是找到android项目编译完的的字节码路径了,额,看文档,在文档上找到自己想要的东西确实是一件比较让人头疼的事,因为有时候它就藏在一个很隐蔽的角落,翻阅好几天也找不到,所以说那些行业的带头技术人在还未开发的一块技术领域的摸索是多么的不容易,在这里致敬他们。

android plugin1.5版本以后,为我们提供了Transform这个类,官方解释是我们可以利用这个类在class被转化为dex之前进行操作。android官方文档,点击连接,滑动最下面,你就可以找到相关介绍,这里需要记住一点,它是执行在jar转dex和混淆之前的。简单来看一下这个类的实现方法

扫描二维码关注公众号,回复: 3194986 查看本文章

 //设置我们自定义的Transform对应的Task名称
    // 类似:TransformClassesWithPreDexForXXX

    @Override
    String getName() {
        return "robust"
    }
/**
 * 需要操作的文件类型,这里标记为只操作calss类型文件
 * CLASSES 表示要处理编译后的字节码,可能是 jar 包也可能是目录
  RESOURCES 表示处理标准的 java 资源
 * @return
 */
    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS
    }
   /***
   * 处理的作用域
    * Scope.PROJECT	只处理当前项目
    Scope.SUB_PROJECTS	只处理子项目
    Scope.PROJECT_LOCAL_DEPS	只处理当前项目的本地依赖,例如jar, aar
   Scope. EXTERNAL_LIBRARIES	只处理外部的依赖库
    Scope.PROVIDED_ONLY	只处理本地或远程以provided形式引入的依赖库
    Scope.TESTED_CODE	测试代码

     
    @Override
    Set<QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT
    }
    //用于指明是否是增量构建
    @Override
    boolean isIncremental() {
        return false
    }

    //在这里操作字节码文件

    @Override
    void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, 
boolean isIncremental) throws IOException, TransformException, InterruptedException {
	}

继承这几个类之后,只要实现这几个方法就ok了transform的api,注意里的名字,如下

 String getName() {
        return "robust"
    }

如果我们build apk通过的话会在build文件下产生我们操作完字节码后生成的文件,如下截图


下面简单的介绍一下怎么搭建一个本地的插件,这里采用的android studio为3.0.1版本,首先在根目录下新建一个buildSrc的文件夹,然后再建一个src子文件夹,接着创建如图所示的文件夹结构



groovy文件夹下写groovy代码文件,当然java也可以在里面写,java文件夹下写java代码,这里只是为了分开groovy和java代码,也可以不要这个java文件夹,然后是resources文件夹下的东西,如下截图


也就是说在resources文件夹下再建一个META-INF子文件夹和gradle-plugins孙文件夹然后,最后建一个properties文件,注意.之前的名字就是插件的名字,这个文件的内容如下

implementation-class=com.meituan.robust.gradle.plugin.RobustTransform

指定实现插件的那个类,最后在配置当前插件项目的build.gradle,如下配置

apply plugin: 'groovy'
compileGroovy {
    sourceCompatibility = 1.7
    targetCompatibility = 1.7
}
dependencies {
    compile gradleApi()
    compile localGroovy()
    compile 'com.android.tools.build:gradle:3.0.1'
    compile 'org.javassist:javassist:3.20.0-GA'
    compile fileTree(dir: "./src/main/libs", include: ['*.jar'])
}
repositories {
    google()
    jcenter()
}
这里最重要的是这三个配置

compile gradleApi()   使用gradle的api   (Plugin插件类属于它)
compile localGroovy() 使用groovy语法和api
compile 'com.android.tools.build:gradle:3.0.1'   使用com.android.tools.build的api(Transform属于它所以必须配置)

这里注意com.android.tools.build的版本号,要和你当前的android studio匹配的版本号相对应,也就是根目录build.gradle里的它,如下

 dependencies {
        classpath 'com.android.tools.build:gradle:3.0.1'
        

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }


配置完这些之后,最后在setting.gradle中加入

include ':buildSrc'
然后重新编译一下你的项目,这个时候就可以使用本地的gradle插件了,直接在app项目中引用(build.gradle中加入)

apply plugin: 'robust'

ok,现在正式进入源码的探讨,只要你申明了上面的引用插件,那么在gradle解析build.gradle的时候,就会调用下面这个方法,也就是插件的入口类,如下所示

  void apply(Project target) {
        project = target
        robust = new XmlSlurper().parse(new File("${project.projectDir}/${Constants.ROBUST_XML}"))
        logger = project.logger
        initConfig()
        //isForceInsert 是true的话,则强制执行插入
        if (!isForceInsert) {
            def taskNames = project.gradle.startParameter.taskNames
            def isDebugTask = false;
            for (int index = 0; index < taskNames.size(); ++index) {
                def taskName = taskNames[index]
                logger.debug "input start parameter task is ${taskName}"
                //FIXME: assembleRelease下屏蔽Prepare,这里因为还没有执行Task,没法直接通过当前的BuildType来判断,所以直接分析当前的startParameter中的taskname,
                //另外这里有一个小坑task的名字不能是缩写必须是全称 例如assembleDebug不能是任何形式的缩写输入
                if (taskName.endsWith("Debug") && taskName.contains("Debug")) {
//                    logger.warn " Don't register robust transform for debug model !!! task is:${taskName}"
                    isDebugTask = true
                    break;
                }
            }
            if (isDebugTask) {
                project.android.registerTransform(this)
                project.afterEvaluate(new RobustApkHashAction())
                logger.quiet "Register robust transform successful !!!"
            }
            if (null != robust.switch.turnOnRobust && !"true".equals(String.valueOf(robust.switch.turnOnRobust))) {
                return;
            }
        } else {
            project.android.registerTransform(this)
            project.afterEvaluate(new RobustApkHashAction())
        }
    }

这个方法比较简单,首先对robust.xml文件进行解析,就是将xml一些属性映射成对象,然后为下面的判断做铺垫,先来看一下这个文件


<?xml version="1.0" encoding="utf-8"?>
<resources>

    <switch>
        <!--true代表打开Robust,请注意即使这个值为true,Robust也默认只在Release模式下开启-->
        <!--false代表关闭Robust,无论是Debug还是Release模式都不会运行robust-->
        <turnOnRobust>true</turnOnRobust>
        <!--<turnOnRobust>false</turnOnRobust>-->

        <!--是否开启手动模式,手动模式会去寻找配置项patchPackname包名下的所有类,自动的处理混淆,然后把patchPackname包名下的所有类制作成补丁-->
        <!--这个开关只是把配置项patchPackname包名下的所有类制作成补丁,适用于特殊情况,一般不会遇到-->
        <!--<manual>true</manual>-->
        <manual>false</manual>

        <!--是否强制插入插入代码,Robust默认在debug模式下是关闭的,开启这个选项为true会在debug下插入代码-->
        <!--但是当配置项turnOnRobust是false时,这个配置项不会生效-->
        <!--<forceInsert>true</forceInsert>-->
        <forceInsert>false</forceInsert>

        <!--是否捕获补丁中所有异常,建议上线的时候这个开关的值为true,测试的时候为false-->
        <catchReflectException>true</catchReflectException>
        <!--<catchReflectException>false</catchReflectException>-->

        <!--是否在补丁加上log,建议上线的时候这个开关的值为false,测试的时候为true-->
        <!--<patchLog>true</patchLog>-->
        <patchLog>false</patchLog>

        <!--项目是否支持progaurd-->
        <proguard>true</proguard>
        <!--<proguard>false</proguard>-->

        <!--项目是否支持ASM进行插桩,默认使用ASM,推荐使用ASM,Javaassist在容易和其他字节码工具相互干扰-->
        <useAsm>true</useAsm>
        <!--<useAsm>false</useAsm>-->
    </switch>

    <!--需要热补的包名或者类名,这些包名下的所有类都被会插入代码-->
    <!--这个配置项是各个APP需要自行配置,就是你们App里面你们自己代码的包名,
    这些包名下的类会被Robust插入代码,没有被Robust插入代码的类Robust是无法修复的-->
    <packname name="hotfixPackage">
        <name>com.example.administrator.myapplication</name>
        <name>com.sankuai</name>
        <name>com.dianping</name>
    </packname>

    <!--不需要Robust插入代码的包名,Robust库不需要插入代码,如下的配置项请保留,还可以根据各个APP的情况执行添加-->
    <exceptPackname name="exceptPackage">
        <name>com.meituan.robust</name>
        <name>com.meituan.sample.extension</name>
    </exceptPackname>

    <!--补丁的包名,请保持和类PatchManipulateImp中fetchPatchList方法中设置的补丁类名保持一致( setPatchesInfoImplClassFullName("com.meituan.robust.patch.PatchesInfoImpl")),
    各个App可以独立定制,需要确保的是setPatchesInfoImplClassFullName设置的包名是如下的配置项,类名必须是:PatchesInfoImpl-->
    <patchPackname name="patchPackname">
        <name>com.meituan.com.meituan.robust.patch</name>
    </patchPackname>

    <!--自动化补丁中,不需要反射处理的类,这个配置项慎重选择-->
    <noNeedReflectClass name="classes no need to reflect">

    </noNeedReflectClass>
</resources>

美团团队对它的注释很详细,注意这里最重要的三个标签,如下

<!--需要热补的包名或者类名,这些包名下的所有类都被会插入代码-->
    <!--这个配置项是各个APP需要自行配置,就是你们App里面你们自己代码的包名,
    这些包名下的类会被Robust插入代码,没有被Robust插入代码的类Robust是无法修复的-->
    <packname name="hotfixPackage">
        <name>com.example.administrator.myapplication</name>
        <name>com.sankuai</name>
        <name>com.dianping</name>
    </packname>

    <!--不需要Robust插入代码的包名,Robust库不需要插入代码,如下的配置项请保留,还可以根据各个APP的情况执行添加-->
    <exceptPackname name="exceptPackage">
        <name>com.meituan.robust</name>
        <name>com.meituan.sample.extension</name>
    </exceptPackname>

    <!--补丁的包名,请保持和类PatchManipulateImp中fetchPatchList方法中设置的补丁类名保持一致( setPatchesInfoImplClassFullName("com.meituan.robust.patch.PatchesInfoImpl")),
    各个App可以独立定制,需要确保的是setPatchesInfoImplClassFullName设置的包名是如下的配置项,类名必须是:PatchesInfoImpl-->
    <patchPackname name="patchPackname">
        <name>com.meituan.com.meituan.robust.patch</name>
    </patchPackname>

这里的XmlSlurper类是groovy的api提供的专门解析api的类,我们只要调用parse方法传入路径,xml就会帮我们解析好,那么

 def initConfig() {
        hotfixPackageList = new ArrayList<>()
        hotfixMethodList = new ArrayList<>()
        exceptPackageList = new ArrayList<>()
        exceptMethodList = new ArrayList<>()
        isHotfixMethodLevel = false;
        isExceptMethodLevel = false;
        /*对文件进行解析*/
        for (name in robust.packname.name) {
            hotfixPackageList.add(name.text());
        }
        for (name in robust.exceptPackname.name) {
            exceptPackageList.add(name.text());
        }
        for (name in robust.hotfixMethod.name) {
            hotfixMethodList.add(name.text());
        }
        for (name in robust.exceptMethod.name) {
            exceptMethodList.add(name.text());
        }

        if (null != robust.switch.filterMethod && "true".equals(String.valueOf(robust.switch.turnOnHotfixMethod.text()))) {
            isHotfixMethodLevel = true;
        }

        if (null != robust.switch.useAsm && "false".equals(String.valueOf(robust.switch.useAsm.text()))) {
            useASM = false;
        }else {
            //默认使用asm
            useASM = true;
        }

        if (null != robust.switch.filterMethod && "true".equals(String.valueOf(robust.switch.turnOnExceptMethod.text()))) {
            isExceptMethodLevel = true;
        }

        if (robust.switch.forceInsert != null && "true".equals(String.valueOf(robust.switch.forceInsert.text())))
            isForceInsert = true
        else
            isForceInsert = false

    }
用起来也比较方便直接根据标签前缀提取出里面的数据,这里注意美团的这个插件字节码插桩即用了asm又用了javaAssist,这里只探讨javaAssist,因为思想都是一样的,最重要的就是思想,好继续进行插件入口类,话说解析完xml后,遍历当前的任务,看看这些任务中是否包含Debug任务,默认情况下,这个插件只在正式版本中执行插入字节码,例如assembleDebug任务就不会执行插入字节码。也就是说只要是debug模式编译的就不会执行下面这两句代码android的gradle插件介绍

 project.android.registerTransform(this)
                project.afterEvaluate(new RobustApkHashAction())

首先注册Transform(进行字节码处理),然后声明项目编译完成后最后执行的Action。

ok,现在来看Transform处理字节的方法,如下所示

 void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, 
boolean isIncremental) throws IOException, TransformException, InterruptedException {
        logger.quiet '================robust start================'
        def startTime = System.currentTimeMillis()
        outputProvider.deleteAll()
        File jarFile = outputProvider.getContentLocation("h", getOutputTypes(), getScopes(),
                Format.JAR);
        logger.quiet ("jarFile"+jarFile.getAbsolutePath());

        if(!jarFile.getParentFile().exists()){
            jarFile.getParentFile().mkdirs();
        }
        if(jarFile.exists()){
            jarFile.delete();
        }

        ClassPool classPool = new ClassPool()
        //将androidjar包加载进入
        project.android.bootClasspath.each {
            classPool.appendClassPath((String) it.absolutePath)
            logger.quiet ("it.absolutePath:"+it.absolutePath);
    }
        //将所有类名放入box
        def box = ConvertUtils.toCtClasses(inputs, classPool)
        def cost = (System.currentTimeMillis() - startTime) / 1000
//        logger.quiet "check all class cost $cost second, class count: ${box.size()}"
       // if(useASM){
         /*   insertcodeStrategy=new AsmInsertImpl(hotfixPackageList,hotfixMethodList,exceptPackageList,exceptMethodList,isHotfixMethodLevel,isExceptMethodLevel);
        }else {*/
            insertcodeStrategy=new JavaAssistInsertImpl(hotfixPackageList,hotfixMethodList,exceptPackageList,exceptMethodList,isHotfixMethodLevel,isExceptMethodLevel);
        //}
        insertcodeStrategy.insertCode(box, jarFile);
        //将被改变的方法名写入文件中
        writeMap2File(insertcodeStrategy.methodMap, Constants.METHOD_MAP_OUT_PATH)

        logger.quiet "===robust print id start==="
        for (String method : insertcodeStrategy.methodMap.keySet()) {
            int id = insertcodeStrategy.methodMap.get(method);
            System.out.println("key is   " + method + "  value is    " + id);
        }
        logger.quiet "===robust print id end==="

        cost = (System.currentTimeMillis() - startTime) / 1000
        logger.quiet "robust cost $cost second"
        logger.quiet '================robust   end================'
    }

这里注意TransformInput类是持有class和jar完整路径的类,想要操作字节码,必须先知道字节码在那些文件中,所以inputs属性是非常重要的一个属性,TransformInput的api,TransformOutputProvider属性类是用于获取输出路径的类,你操作了字节码之后,要让新的字节码输出到某个jar文件中或文件夹下,如果你注册了Transform,的话,必须指定输出目录,要不后续的编译都没法执行。看一下获取输出路径这个方法

 File jarFile = outputProvider.getContentLocation("h", getOutputTypes(), getScopes(),
                Format.JAR);

这个方法第一个参数可以随便填,第二个参数指定输出类型,第三个参数指出输出格式以jar文件的形式将clas全输出进去,来看一看它最终生成的文件名字是什么?TransformOutputProvider的最终实现类为TransformOutputProviderImpl,如下方法

 public synchronized File getContentLocation(String name, Set<ContentType> types, Set<? super Scope> scopes, Format format) {
        Preconditions.checkNotNull(name);
        Preconditions.checkNotNull(types);
        Preconditions.checkNotNull(scopes);
        Preconditions.checkNotNull(format);
        Preconditions.checkState(!name.isEmpty());
        Preconditions.checkState(!types.isEmpty());
        Preconditions.checkState(!scopes.isEmpty());
        Iterator var5 = this.subStreams.iterator();

        SubStream subStream;
        do {
            if(!var5.hasNext()) {
                SubStream newSubStream = new SubStream(name, this.nextIndex++, scopes, types, format, true);
                this.subStreams.add(newSubStream);
                return new File(this.rootFolder, newSubStream.getFilename());
            }

            subStream = (SubStream)var5.next();
        } while(!name.equals(subStream.getName()) || !types.equals(subStream.getTypes()) || !scopes.equals(subStream.getScopes()) || format != subStream.getFormat());

        return new File(this.rootFolder, subStream.getFilename());
    }
从这个方法可以看到,第一个参数只是为了区分SubStream保存到集合中的不同,和最终生成的文件名字一点关系都没有,最终的文件名字是以nextIndex++,定义的,也就是说第一次调用getContentLocation的时候,文件名字为0,第二次为1依次类推。好,继续字节码修改的流程,获得完输出的文件名字之后,进行清理工作,如果输出文件存在先将文件清理掉,然后创建clas池ClassPool,这个类是javassist的,具体用法小伙伴们请参考一下文档javassist的文档。创建完之后将当前android.jar引入到ClassPool中,也就是下面这个方法

project.android.bootClasspath.each {
            classPool.appendClassPath((String) it.absolutePath)
            logger.quiet ("it.absolutePath:"+it.absolutePath);
    }

这个方法的意思就是,再用ClassPool创建Cclass的时候,可以从路径中找到这个class(android的class),比如我想声明这个方法CeShi(TextView textView),参数textView是属于android.jar里面的,所以说得保证ClassPool能找到TextView.class,当然这也属于javassist的用法。完整路径名如下

D:\studioSdk11\platforms\android-26\android.jar

因为我当前的targetSdkVersion 26,所以编译的时候是用使用的编译classandroid-26\android.jar,接下来就是将所有输入class全部放入集合中,调用这个方法

 //将所有类名放入box
        def box = ConvertUtils.toCtClasses(inputs, classPool)

static List<CtClass> toCtClasses(Collection<TransformInput> inputs, ClassPool classPool) {
        List<String> classNames = new ArrayList<>()
        List<CtClass> allClass = new ArrayList<>();
        def startTime = System.currentTimeMillis()
        inputs.each {
            it.directoryInputs.each {
                def dirPath = it.file.absolutePath
                RobustTransform.logger.quiet("dirPath:"+dirPath);
                // 将项目编译路经圆满设置进去全
                classPool.insertClassPath(it.file.absolutePath);
                org.apache.commons.io.FileUtils.listFiles(it.file, null, true).each {
                    if (it.absolutePath.endsWith(SdkConstants.DOT_CLASS)) {
                        RobustTransform.logger.quiet("it.absolutePath:"+it.absolutePath);
                        //得到 class 的全类名
                        def className = it.absolutePath.substring(dirPath.length() + 1, it.absolutePath.length() - SdkConstants.DOT_CLASS.length()).replaceAll(Matcher.quoteReplacement(File.separator), '.')

                        if(classNames.contains(className)){
                            throw new RuntimeException("You have duplicate classes with the same name : "+className+" please remove duplicate classes ")
                        }
                        classNames.add(className)
                    }
                }
            }

            it.jarInputs.each {
                classPool.insertClassPath(it.file.absolutePath)
                def jarFile = new JarFile(it.file)
                Enumeration<JarEntry> classes = jarFile.entries();
                while (classes.hasMoreElements()) {
                    JarEntry libClass = classes.nextElement();
                    String className = libClass.getName();
                    if (className.endsWith(SdkConstants.DOT_CLASS)) {
                        className = className.substring(0, className.length() - SdkConstants.DOT_CLASS.length()).replaceAll('/', '.')
                        if(classNames.contains(className)){
                            throw new RuntimeException("You have duplicate classes with the same name : "+className+" please remove duplicate classes ")
                        }
                        classNames.add(className)
                    }
                }
            }
        }
        def cost = (System.currentTimeMillis() - startTime) / 1000
        println "read all class file cost $cost second"
        classNames.each {
            try {
                allClass.add(classPool.get(it));
            } catch (javassist.NotFoundException e) {
                println "class not found exception class name:  $it "

            }

        }

        Collections.sort(allClass, new Comparator<CtClass>() {
            @Override
            int compare(CtClass class1, CtClass class2) {
                return class1.getName() <=> class2.getName();
            }
        });
        return allClass;
    }
gradle编译脚本框架可以执行很多的Transform,这里可以把Transform当成通道,往往上一个通道的输出,将作为下一个通道的输入,当然Transform的本质就是Task任务。上面说了TransformInput只有两种类型的文件,如下所示,一种是jar包类型,一种是普通文件夹下的class。例如第三方的jar包为,最后编译完成还是jar包的形式

Collection<DirectoryInput> getDirectoryInputs()
Returns a collection of DirectoryInput.
Collection<JarInput> getJarInputs()
Returns a collection of JarInput.

分别遍历两种文件形式,然后将它们加入集合中,这里注意每种形式的class路径都应放到ClassPool中,如下

  classPool.insertClassPath(it.file.absolutePath);

目的是将class转化为javassist中的CtClass,以便后面的操作,既然已经输入class文件全部提出来了,那么接下来就是操作字节码了,如下方法

protected void insertCode(List<CtClass> box, File jarFile) throws CannotCompileException, IOException, NotFoundException {

        ZipOutputStream outStream = new JarOutputStream(new FileOutputStream(jarFile));
//        new ForkJoinPool().submit {
        for (CtClass ctClass : box) {
            //判断 是否满足要求
            if (isNeedInsertClass(ctClass.getName())) {
                //change class modifier
                ctClass.setModifiers(AccessFlag.setPublic(ctClass.getModifiers()));
                if (ctClass.isInterface() || ctClass.getDeclaredMethods().length < 1) {
                    //skip the unsatisfied class
                    //FileOut(ctClass.toBytecode(), jarFile.getAbsolutePath(), ctClass.getName().replaceAll("\\.", "/") + ".class");

                    zipFile(ctClass.toBytecode(), outStream, ctClass.getName().replaceAll("\\.", "/") + ".class");
                    continue;
                }

                boolean addIncrementalChange = false;
                /**
                 * d
                 */
                for (CtBehavior ctBehavior : ctClass.getDeclaredBehaviors()) {
                    if (!addIncrementalChange) {
                        //insert the field
                        addIncrementalChange = true;
                        ClassPool classPool = ctBehavior.getDeclaringClass().getClassPool();
                        CtClass type = classPool.getOrNull(Constants.INTERFACE_NAME);
                        CtField ctField = new CtField(type, Constants.INSERT_FIELD_NAME, ctClass);
                        ctField.setModifiers(AccessFlag.PUBLIC | AccessFlag.STATIC);
                       // 在每一个class内设changeQuickRedirect
                        ctClass.addField(ctField);
                    }
                    if (!isQualifiedMethod(ctBehavior)) {
                        continue;
                    }
                    //here comes the method will be inserted code
                    RobustTransform.logger.quiet(" ctBehavior.getLongName():"+ ctBehavior.getLongName());
                    methodMap.put(ctBehavior.getLongName(), insertMethodCount.incrementAndGet());
                    try {
                        if (ctBehavior.getMethodInfo().isMethod()) {
                            CtMethod ctMethod = (CtMethod) ctBehavior;
                            boolean isStatic = (ctMethod.getModifiers() & AccessFlag.STATIC) != 0;
                            CtClass returnType = ctMethod.getReturnType();
                            String returnTypeString = returnType.getName();
                            //construct the code will be inserted in string format
                            String body = "Object argThis = null;";
                            if (!isStatic) {
                                //$0 表示当前圂对象
                                body += "argThis = $0;";
                            }
                            //$arg表示当前方法胡所有参数new Object[] { savedInstanceState }
                            String parametersClassType = getParametersClassType(ctMethod);
//                                body += "   if (com.meituan.robust.PatchProxy.isSupport(\$args, argThis, ${Constants.INSERT_FIELD_NAME}, $isStatic, " + methodMap.get(ctBehavior.longName) + ",${parametersClassType},${returnTypeString}.class)) {"
                            body += "   if (com.meituan.robust.PatchProxy.isSupport($args, argThis, " + Constants.INSERT_FIELD_NAME + ", " + isStatic +
                                    ", " + methodMap.get(ctBehavior.getLongName()) + "," + parametersClassType + "," + returnTypeString + ".class)) {";
                            body += getReturnStatement(returnTypeString, isStatic, methodMap.get(ctBehavior.getLongName()), parametersClassType, returnTypeString + ".class");
                            body += "   }";
                            RobustTransform.logger.quiet(" body:"+ body);
                            //finish the insert-code body ,let`s insert it
                            ctBehavior.insertBefore(body);
                        }
                    } catch (Throwable t) {
                        //here we ignore the error
                        t.printStackTrace();
                        System.out.println("ctClass: " + ctClass.getName() + " error: " + t.getMessage());
                    }
                }
            }
            //zip the inserted-classes into output file
            //FileOut(ctClass.toBytecode(), jarFile.getAbsolutePath(), ctClass.getName().replaceAll("\\.", "/") + ".class");

            zipFile(ctClass.toBytecode(), outStream, ctClass.getName().replaceAll("\\.", "/") + ".class");
        }
//        }.get()
        outStream.close();
    }


遍历上面拼装的Ctclass集合,首先判断是不是需要插桩的class,并不是所有的class都需要插桩,如下代码


protected boolean isNeedInsertClass(String className) {

        for (String exceptName : exceptPackageList) {
            if (className.startsWith(exceptName)) {
                return false;
            }
        }
        for (String name : hotfixPackageList) {
            if (className.startsWith(name)) {
                return true;
            }
        }
        return false;
    }

很显然就是我们xml中配置的文件,如果是允许插桩的包名,那么就进行插桩,如果是不需要插桩的包名的话,不插桩。

然后将类的修饰符改为public,最后看它是否是接口,是否没有实现方法,如果满足其中之一,直接将其输出,不需要操作,然后遍历class的所有行为,先为class添加

com.meituan.robust.ChangeQuickRedirect
属性,然后再次判断class是否满足条件

 private boolean isQualifiedMethod(CtBehavior it) throws CannotCompileException {
        /**
         * 是不是静态代码块
         */
        if (it.getMethodInfo().isStaticInitializer()) {
            return false;
        }
//SYNTHETIC由编译器引入的字段、方法、类或其他结构,如果内部类都是private方法的话
        if ((it.getModifiers() & AccessFlag.SYNTHETIC) != 0 && !AccessFlag.isPrivate(it.getModifiers())) {
            return false;
        }
        if (it.getMethodInfo().isConstructor()) {
            return false;
        }
    //抽象方法不行
        if ((it.getModifiers() & AccessFlag.ABSTRACT) != 0) {
            return false;
        }
        /**
         * 本地方法不行
         */
        if ((it.getModifiers() & AccessFlag.NATIVE) != 0) {
            return false;
        }
        /**
         * 接口不行
         */
        if ((it.getModifiers() & AccessFlag.INTERFACE) != 0) {
            return false;
        }

        if (it.getMethodInfo().isMethod()) {
            if (AccessFlag.isPackage(it.getModifiers())) {
                it.setModifiers(AccessFlag.setPublic(it.getModifiers()));
            }
            boolean flag = isMethodWithExpression((CtMethod) it);
            if (!flag) {
                return false;
            }
        }
        //是否在不期望的包下面
        if (isExceptMethodLevel && exceptMethodList != null) {
            for (String exceptMethod : exceptMethodList) {
                if (it.getName().matches(exceptMethod)) {
                    return false;
                }
            }
        }
  //是否在需要修改的包下面
        if (isHotfixMethodLevel && hotfixMethodList != null) {
            for (String name : hotfixMethodList) {
                if (it.getName().matches(name)) {
                    return true;
                }
            }
        }
        return !isHotfixMethodLevel;
    }

这里注意一个SYNTHETIC修饰符,这个修饰符java编译器引入的字段(如果内部类都是private方法的话,编译器会为这个内部类额外的产生一个方法,方便外部类调用内部类的方法,这个方法带有SYNTHETIC标记,所以得过滤掉)。

然后将全方法名放入map集合,最后想字节码中,为每一个类的方法都插入几句代码,如下所示最终插入的代码为


    MainActivity localMainActivity = null;localMainActivity = this;
    if (PatchProxy.isSupport(new Object[] { savedInstanceState }, localMainActivity, changeQuickRedirect, false, 1, new Class[] { Bundle.class }, Void.TYPE))
    {
      PatchProxy.accessDispatchVoid(new Object[] { savedInstanceState }, localMainActivity, changeQuickRedirect, false, 1, new Class[] { Bundle.class }, Void.TYPE);return;
    }
这里涉及到javassist的替代符


$0:代表当前的对象

$args:代表当前方法的参数集合(oncreat的方法的参数为savedInstanceState,所以被包裹为new Object[] { savedInstanceState }


有关替换符的详细用法请参考文档javassist文档,操作完字节码之后,经改变的字节码写入到输出文件中(这里是0.jar,上面有截图)

 protected void zipFile(byte[] classBytesArray, ZipOutputStream zos, String entryName) {
        try {
            ZipEntry entry = new ZipEntry(entryName);
            zos.putNextEntry(entry);
            zos.write(classBytesArray, 0, classBytesArray.length);
            zos.closeEntry();
            zos.flush();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
注意jar需要JarOutputStream操作,最后将全方法名写入到/outputs/robust/methodsMap.robust中

 private void writeMap2File(Map map, String path) {
        logger.quiet ('project.buildDir.path'+project.buildDir.path);

        File file = new File(project.buildDir.path + path);
        if (!file.exists() && (!file.parentFile.mkdirs() || !file.createNewFile())) {
//            logger.error(path + " file create error!!")
        }
        FileOutputStream fileOut = new FileOutputStream(file);

        ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
        ObjectOutputStream objOut = new ObjectOutputStream(byteOut);
        objOut.writeObject(map)
        //gzip压缩
       GZIPOutputStream gzip = new GZIPOutputStream(fileOut);
        gzip.write(byteOut.toByteArray())
        objOut.close();
        gzip.flush();
        gzip.close();
        fileOut.flush()
        fileOut.close()

    }

修改的方法名被保存在文件中,肯定是用在热修复加载class时的检查。好了,到此字节码动态修改就就介绍到这里。










猜你喜欢

转载自blog.csdn.net/xiatiandefeiyu/article/details/79020527