美文网首页
安卓代码修复框架Patch(2)

安卓代码修复框架Patch(2)

作者: 呵呵_9e25 | 来源:发表于2019-07-28 14:33 被阅读0次

安卓代码修复框架Patch

只支持程序重启修复,不支持热修复

markdown

上一篇文章自定义了gradle插件,但是这并没有啥用,因为我们要做代码修复,然而我们的插件没有做任何东西


当然我们必须知道我们的插件要做代码修复,具体要明确我们修复的目的,那我们开始列出我们的几个目标,然后一一实现它

插件目标

  • 1.怎么在插件里面打印日志
  • 2.要知道哪些类需要修复,那些类不需要修复
  • 3.我们要在哪个任务进行代码修复
  • 4.创建我们的补丁任务
  • 5.我们怎么区别上个版本代码和我们修复之后的代码类,我们需要用什么工具进行代码注入
  • 6.我们要处理的都是一些什么文件
  • 7.找到修复的代码类之后怎么把它打包到patch.jar包里面

实现目标

  • 1.怎么在插件里面打印日志
    • 其实要在插件里面打印日志很简单,我们上篇文章就提到过println ("我是插件"),这是这个代码,其实调用的是javaSystem.out.println("我是插件");的简写groovy默认提供System.out的引入
    • 但是我们都知道我们打印日志的时候,最好有一个TAG,而我们这里希望TAG是我们构建变体的名称Variant,这里我们新建一个类BLogger代码实现如下
class BLogger{
    private static String TAG

    static void init(BaseVariant variant){
        //构建变体的名称
        TAG=">PatchPlugin-${variant.name.capitalize()}"
    }

    static void i(String log){
        println("${TAG}:${log}")
    }
}

这里我们传递了一个BaseVariant类型参数进来,这样每个构建变体构建时都能区分开来

  • 然后我们看看怎么用
 @Override
    void apply(Project project) {
        //afterEvaluate是一般比较常见的一个配置参数的回调方式,只要project配置成功均会调用,
        project.afterEvaluate {
            project.android.applicationVariants.each {
                variant ->
                        //初始化日志打印器
                        BLogger.init(variant)
                        BLogger.i("Hello World 中国")
            }
        }
    }

我们在project.afterEvaluate项目配置之后,可以通过project.android.applicationVariants遍历获取到所有的构建变体,然后我们把我们开始配置我们的日志类,然后打印一下日志

我们点击上传一下这个插件 ,然后点击app的assemble任务看看构建日志

image.png
我们可以看到,他打印了两个变体的日志,因为我们现在没有自定义productFlavors { xiaomi{} },所以并没有出现小米的变体,但是还是会有默认的DebugRelease这两个构建变体
  • 2.要知道哪些类需要修复,那些类不需要修复

我们分析一下这个问题,第三库和安卓提供的代码库我们都不需要管,我们只想修复我们需要修复的代码,另外还有一个问题是,我们有一些业务比较稳定,也没有出现bug的可能性了,我们是不是应该区分一下,就不需要做代码注入了

基于上面的分析,我们先做一个准备,就是我们必须让开发者可以自己配置哪些业务代码是不需要被注入代码的,同时哪些类是需要注入的,这些都需要动态配置,需要怎么做呢?
我们可以参考android插件的配置,我们自定义一个扩展。
下面我们来实现一下 ,然后我们验证一下有没有用

 @Override
    void apply(Project project) {
       //第一步创建扩展
        project.extensions.create("bigman", BigmanExtension, project)

        //afterEvaluate是一般比较常见的一个配置参数的回调方式,只要project配置成功均会调用,
        project.afterEvaluate {
            //通过名字找到配置里面的bigman扩展
            extension = project.extensions.findByName("bigman") as BigmanExtension

            project.android.applicationVariants.each {
                variant ->
                    BLogger.init(variant)
                    extension.excludeClass.each {
                        name ->
                            BLogger.i("排除类名称:${name}")
                    }
            }
    }
  • 第一步.在这里我们创建了一个名字为bigman的插件,然后我们在上面代码的基础上加入上面的代码
    然后我们点击上传,上传成功之后,我们在我们的app模块的build.gralde加以下代码
apply plugin: 'com.android.application'
apply plugin: 'com.bigman.tech'
bigman{
    includePackage = ['com/example/administrator/patchdemo']
    excludeClass = ['com/example/administrator/patchdemo/patchlib/SignaChecker','me/wcy/cfix/sample/Test']
}
  • 第二步这里我们声明了两个数组一个includePackage我们用来指定需要插入代码的包路径,另一个excludeClass使我们不希望插入代码的类路径,这里的分割符号要注意一下。

然后我们对比一下上面的代码

 extension.excludeClass.each {
                        name ->
                            BLogger.i("排除类名称:${name}")
                    }
  • 第三步 我们这里循环打印我们这个数组里面的值,我们点击gradle里的任务我这里选择assembleDebug

    image.png
    我们可以看到即使我选择了这个任务,另一个在android扩展里配置了的构建变体同样会输出我们的排除数组。
    我们这里已经顺利的打印出我们需要配置的数组,另一个数组大家可以通过同样的方法获取

    这是我们通过自定义扩展实现自定义配置,但是这两个东西具体有啥用,我们讲到代码注入的时候会说到

这里如果我们配置了includePackage,那其实已经可以很好地排除第三方库,但是假如我们这个位置不做任何配置,那我们还需要做额外的事情,比如android/support/,R,BuildConfig这些不应该修改的类文件,怎么做,这个就比较简单我们这里会提供一个方法,这里后面会用到,代码先放出来

private static boolean shouldProcessClass(File classFile,BigmanExtension extension){
        if (!classFile.exists()||!classFile.name.endsWith(SdkConstants.DOT_CLASS)){
            return false
        }

        FileInputStream inputStream=new FileInputStream(classFile)
        ClassReader cr=new ClassReader(inputStream)
        String className=cr.className
        inputStream.close()
        return !className.startsWith("com/bigman/tech/lib")&&
                !className.contains("android/support/")&&
                !className.contains("/R\$")&&
                !className.endsWith("/R")&&
                !className.endsWith("/BuildConfig")&&
                PatchSetUtils.isIncluded(className,extension.includePackage)&&
                !PatchSetUtils.isExcluded(className,extension.excludeClass)
    }

其实上面的代码就能解决上面说的问题,好了目标2已经完成


  • 3.我们要在哪个任务进行代码修复
    这个其实我们之前提到过,就是在dex任务之前进行,这个我们可以通过以下代码进行判别
//Gradle版本为1.5-2.x
   Task dexTaskLower = project.tasks.findByName("transformClassesWithDexFor${variant.name.capitalize()}")
    //Gradle版本为3.0+
  Task dexTaskHigner = project.tasks.findByName("transformClassesWithDexBuilderFor${variant.name.capitalize()}")
   //获取生成dex的任务
   Task dexTask
 if (dexTaskLower != null) {
        dexTask = dexTaskLower
  } else if (dexTaskHigner != null) {
        dexTask = dexTaskHigner
  } else {
    BLogger.i("Gradle 版本暂不支持")
   return
 }

这里任务的名称其实是固定的 我们兼容了1.5-3.0+版本,通过project.tasks.findByName就能获取到任务名称
这里其实我们可以抽出来写成一个方法,看起来简洁一点

我们说要在dex任务之前进行,但是具体要怎么做,大家应该是懵逼的

     //补丁任务将在dex任务获取完依赖之后再进行
     patchJarBeforeDexTask.dependsOn 
                         dexTask.taskDependencies.getDependencies(dexTask)
      //dex任务将在补丁任务执行完之后再执行
     dexTask.dependsOn patchJarBeforeDexTask

patchJarBeforeDexTask这里是我们将要进行代码注入的任务,这个任务我们等一下会讲到,大家知道它是一个任务就行
上面两句代码的意思就是我们这个代码注入任务patchJarBeforeDexTask是在dexTask任务获取完所有的依赖文件之后执行,这样能保证class文件的完整性,然后dexTask任务必须在我们代码注入任务patchJarBeforeDexTask完成之后执行
我们在这里改变了打包任务的执行次序,而且创建了一个patchJarBeforeDexTask的任务,当然这个任务使我们的核心


  • 4.创建我们的补丁任务
    创建一个任务其实很简单,首先任务需要一个名字,我们需要和构建变体绑定在一起,我们看一下代码实现
     String patchJarBeforeDex = "patchJarBeforeDex${variant.name.capitalize()}"
     project.task(patchJarBeforeDex) << {
                            Set<File> inputFiles = dexTask.inputs.files.files
                            inputFiles.each {
                                file ->
                                    //打印dex任务返回的输入文件
                                    BLogger.i("transformClassesTask input:${file.absolutePath}")
                            }
                        }

可以看到我们通过project.task(patchJarBeforeDex) << {}创建了一个任务,这个任务就是我们的核心任务,我们可以看到我们在任务里面获取到了dexTask的一些输入文件,其实它们就是我们的class文件,我们上传一下,然后看看效果,操作和上面的一样。

image.png
可以看到已经获取到了所有的class文件和jar文件,这些文件我将对它们进行过滤和代码插入
我们可以简要看看哪些类应该过滤,比如上图的R.class,R$**.class,android\support\这几个安卓内置的类咱们都不需要去植入代码,那么大家就能理解我们上面写的那几句代码的意思了
  !className.contains("android/support/")&&
                !className.contains("/R\$")&&
                !className.endsWith("/R")&&

这里就是过滤安卓内部代码的语句


  • 5.我们怎么区别上个版本代码和我们修复之后的代码类,我们需要用什么工具进行代码注入
    其实很简单,因为class文件生成hash码是唯一的,所以我们只要对比两次生成的hash码是否相等,就能判断是不是需要修复的代码类。
    我们具体来实现以下:
    * 第一步 我们需要为我们需要插入代码的类,生成hash码,然后保存到一个hash.txt的文件

    File hashFile = new File(outputDir, "hash.txt")
    String patchJarBeforeDex = "patchJarBeforeDex${variant.name.capitalize()}"
    project.task(patchJarBeforeDex) << {
                           Set<File> inputFiles = dexTask.inputs.files.files
                           inputFiles.each {
                               file ->
                                   //打印dex任务返回的输入文件
                                   BLogger.i("transformClassesTask input:${file.absolutePath}")
                           }
                                //取到所有的类文件
                         
            Set<File> files=[]
            for (File file :inputFiles){
               if (file.directory) {
               file.eachFileRecurse {
                   f ->
                       if (f.file) {
                           files.add(f)
                       }
               }
                }else{
                     files.add(file)
                }
                }
    
    
                           //---核心逻辑---循环进行代码注入
                           files.each {
                               file ->
                                   //如果是jar文件需要解压后再处理
                                   if (file.name.endsWith(SdkConstants.DOT_JAR)) {
                                       PatchProcessor.processJar(file, hashFile, hashMap, patchDir, extension)
                                   } else if (file.name.endsWith(SdkConstants.DOT_CLASS)) {
                                       PatchProcessor.processClass(file, hashFile, hashMap, patchDir, extension)
                                   }
                           }
                       }
    
其他代码看源码 不说了

      
      
     

















相关文章

网友评论

      本文标题:安卓代码修复框架Patch(2)

      本文链接:https://www.haomeiwen.com/subject/joxdrctx.html