990.am:如何把编译时间从130秒降到17秒,加快apk的构建速度

990.am:如何把编译时间从130秒降到17秒,加快apk的构建速度。正文已授权微信公众号:鸿洋(hongyangAndroid)原创头阵

990.am 1

前日要聊的就跟android有关了,然而在介绍的时候会引用到前两篇的学问,所以并未看过前两篇文章的朋友,提议先看一下近期提到的概念:
Gradle插件学习笔记(一)
Gradle插件学习笔记(二)

商厦的档次代码相比较多,每趟调节和测试改动java文件后要将近2分钟才能跑起来,实在受不住。在网上找了一大堆配置参数也尚无很显然的功用,
尝试使用instant
run效果也不过尔尔,然后又尝试利用freeline编写翻译速度仍是能够只是不平静,每一趟退步后全量编写翻译很耗时,既然没有好的方案就和好尝尝做。

fastdex.png


类型地址:
https://github.com/typ0520/fastdex

本文已授权微信公众号:鸿洋(hongyangAndroid)原创首发

android编写翻译职分

要讲到android的编写翻译,肯定要先看看android在实践assemble都进行了什么,这几个大家能够打字与印刷一下职分(怎么着打字与印刷任务?那个我们前边再说,也是个groovy插件):

app工程的preBuild任务开始执行
app工程的preDebugBuild任务开始执行
app工程的checkDebugManifest任务开始执行
app工程的preReleaseBuild任务开始执行
app工程的prepareComAndroidSupportAnimatedVectorDrawable2600Alpha1Library任务开始执行
app工程的prepareComAndroidSupportAppcompatV72600Alpha1Library任务开始执行
app工程的prepareComAndroidSupportConstraintConstraintLayout102Library任务开始执行
app工程的prepareComAndroidSupportSupportCompat2600Alpha1Library任务开始执行
app工程的prepareComAndroidSupportSupportCoreUi2600Alpha1Library任务开始执行
app工程的prepareComAndroidSupportSupportCoreUtils2600Alpha1Library任务开始执行
app工程的prepareComAndroidSupportSupportFragment2600Alpha1Library任务开始执行
app工程的prepareComAndroidSupportSupportMediaCompat2600Alpha1Library任务开始执行
app工程的prepareComAndroidSupportSupportV42600Alpha1Library任务开始执行
app工程的prepareComAndroidSupportSupportVectorDrawable2600Alpha1Library任务开始执行
app工程的prepareDebugDependencies任务开始执行
app工程的compileDebugAidl任务开始执行
app工程的compileDebugRenderscript任务开始执行
app工程的generateDebugBuildConfig任务开始执行
app工程的generateDebugResValues任务开始执行
app工程的generateDebugResources任务开始执行
app工程的mergeDebugResources任务开始执行
app工程的processDebugManifest任务开始执行
app工程的processDebugResources任务开始执行
app工程的generateDebugSources任务开始执行
app工程的incrementalDebugJavaCompilationSafeguard任务开始执行
app工程的javaPreCompileDebug任务开始执行
app工程的compileDebugJavaWithJavac任务开始执行
app工程的compileDebugNdk任务开始执行
app工程的compileDebugSources任务开始执行
app工程的mergeDebugShaders任务开始执行
app工程的compileDebugShaders任务开始执行
app工程的generateDebugAssets任务开始执行
app工程的mergeDebugAssets任务开始执行
app工程的transformClassesWithDexForDebug任务开始执行
app工程的mergeDebugJniLibFolders任务开始执行
app工程的transformNativeLibsWithMergeJniLibsForDebug任务开始执行
app工程的transformNativeLibsWithStripDebugSymbolForDebug任务开始执行
app工程的processDebugJavaRes任务开始执行
app工程的transformResourcesWithMergeJavaResForDebug任务开始执行
app工程的validateSigningDebug任务开始执行
app工程的packageDebug任务开始执行
app工程的assembleDebug任务开始执行

那是举行assembleDebug打字与印刷的具备职责,当然你要推行assembleRelease任务一定是平等的。
那接下去介绍一下常用的多少个职分:

  • mergeDebugResources 义务的功用是将拥有正视的aar或library
    module中的资源集合到app/build/intermediates/res/merged/debug目录里

  • processDebugManifest任务是将富有依赖的aar或library
    module中AndroidManifest.xml中的节点,合并到花色的AndroidManifest.xml中

  • processDebugResources的机能是调用aapt生成项目和全部aar依赖的本田CR-V.java,同时生成能源索引文件,把符号表输出到app/build/intermediates/symbols/debug/奥德赛.txt

  • compileDebugJavaWithJavac这些职分是用来把java文件编写翻译成class文件,输出的路子是app/build/intermediates/classes/debug
    编写翻译的输入目录有

  • transformClassesWithJarMergingForDebug的效果是把compileDebugJavaWithJavac任务的输出app/build/intermediates/classes/debug,和app/build/intermediates/exploded-aar中具备的classes.jar和libs里的jar包作为输入,合并起来输出到app/build/intermediates/transforms/jarMerging/debug/jars/1/1f/combined.jar,大家在付出中凭借第叁方库的时候偶然报duplicate
    entry:xxx
    的不当,正是因为在统一的进度中在分裂jar包里发现了同样路线的类

  • transformClassesWithDexForDebug本条职责的坚守是把带有全数class文件的jar包转换为dex,class文件愈多变换的越慢

注: 本文对gradle task做的辨证都创立在闭馆instant run的前提下

在上一篇小说加快apk的创设速度,怎样把编写翻译时间从130秒降到17秒中讲了优化的笔触与开始的落到实处,经过一段时间的优化质量和稳定性都有极大的增加,那里要多谢我们提的建议以及github990.am:如何把编译时间从130秒降到17秒,加快apk的构建速度。上的issue,那篇小说就把主要优化的点和新效用以及填的坑介绍下。

选择android提供的插件

万一想在温馨的插件中生出干预android编写翻译的作为,肯定要重视android的gradle插件。那里要注解两种情形:

  • 要是使用buildSrc(不精晓是何许的,请查看前两篇作品)的不二法门,不须求做额外的信赖。
  • 假使是新建java Library的款型要求丰盛依赖:

dependencies {
    compile gradleApi()
    compile localGroovy()
    compile 'com.android.tools.build:gradle:2.3.3'
}
注: 本文全数的代码、gradle职分名、职分输出路径、全部施用debug那么些buildType作表明

优化营造速度首先必要找到那多少个环节造成创设速度如此慢,把下部的代码放进app/build.gradle里把日子费用超越50ms的天职时间打字与印刷出来

 public class BuildTimeListener implements TaskExecutionListener, BuildListener {
    private Clock clock
    private times = []

    @Override
    void beforeExecute(Task task) {
        clock = new org.gradle.util.Clock()
    }

    @Override
    void afterExecute(Task task, TaskState taskState) {
        def ms = clock.timeInMs
        times.add([ms, task.path])

        //task.project.logger.warn "${task.path} spend ${ms}ms"
    }

    @Override
    void buildFinished(BuildResult result) {
        println "Task spend time:"
        for (time in times) {
            if (time[0] >= 50) {
                printf "%7sms  %s\n", time
            }
        }
    }

    ......
}

project.gradle.addListener(new BuildTimeListener())

执行./gradlew assembleDebug,经过漫长的等候获得以下输出

Total time: 1 mins 39.566 secs
Task spend time:
     69ms  :app:prepareComAndroidSupportAnimatedVectorDrawable2340Library
    448ms  :app:prepareComAndroidSupportAppcompatV72340Library
     57ms  :app:prepareComAndroidSupportDesign2340Library
     55ms  :app:prepareComAndroidSupportSupportV42340Library
     84ms  :app:prepareComFacebookFrescoImagepipeline110Library
     69ms  :app:prepareComSquareupLeakcanaryLeakcanaryAndroid14Beta2Library
     60ms  :app:prepareOrgXutilsXutils3336Library
     68ms  :app:compileDebugRenderscript
    265ms  :app:processDebugManifest
   1517ms  :app:mergeDebugResources
    766ms  :app:processDebugResources
   2897ms  :app:compileDebugJavaWithJavac
   3117ms  :app:transformClassesWithJarMergingForDebug
   7899ms  :app:transformClassesWithMultidexlistForDebug
  65327ms  :app:transformClassesWithDexForDebug
    151ms  :app:transformNative_libsWithMergeJniLibsForDebug
    442ms  :app:transformResourcesWithMergeJavaResForDebug
   2616ms  :app:packageDebug
    123ms  :app:zipalignDebug

从地点的出口能够发现总的创设时间为100秒左右(上面包车型地铁出口不是依据真的的实施各种输出的),transformClassesWithDexForDebug任务是最慢的消耗了65秒,它正是我们须求重点优化的职责,首先讲下创设进度中最首要职务的功能,方便理解前面包车型大巴hook点

mergeDebugResources职分的功用是解压全数的aar包输出到app/build/intermediates/exploded-aar,并且把持有的能源文件合并到app/build/intermediates/res/merged/debug目录里

processDebugManifest任务是把全体aar包里的AndroidManifest.xml中的节点,合并到花色的AndroidManifest.xml中,并基于app/build.gradle中当前buildType的manifestPlaceholders配置内容替换manifest文件中的占位符,最后输出到app/build/intermediates/manifests/full/debug/AndroidManifest.xml

processDebugResources的作用

  • 一 、调用aapt生成项目和全数aar信赖的Rubicon.java,输出到app/build/generated/source/r/debug目录
  • 二 、生成能源索引文件app/build/intermediates/res/resources-debug.ap_
  • 990.am:如何把编译时间从130秒降到17秒,加快apk的构建速度。叁 、把符号表输出到app/build/intermediates/symbols/debug/翼虎.txt

compileDebugJavaWithJavac以此职分是用来把java文件编写翻译成class文件,输出的门道是app/build/intermediates/classes/debug
990.am:如何把编译时间从130秒降到17秒,加快apk的构建速度。编写翻译的输入目录有

  • 壹 、项目源码目录,暗许路径是app/src/main/java,能够透过sourceSets的dsl配置,允许有四个(打字与印刷project.android.sourceSets.main.java.srcDirs能够查看当前抱有的源码路径,具体计划能够参见android-doc
  • 2、app/build/generated/source/aidl
  • 3、app/build/generated/source/buildConfig
  • 四 、app/build/generated/source/apt(继承javax.annotation.processing.AbstractProcessor做动态代码生成的有个别库,输出在那一个目录,具体能够参考Butterknife

    Tinker)的代码

transformClassesWithJarMergingForDebug的效益是把compileDebugJavaWithJavac职务的输出app/build/intermediates/classes/debug,和app/build/intermediates/exploded-aar中负有的classes.jar和libs里的jar包作为输入,合并起来输出到app/build/intermediates/transforms/jarMerging/debug/jars/1/1f/combined.jar,大家在开发中凭借第②方库的时候偶然报duplicate
entry:xxx 的失实,正是因为在统一的长河中在差别jar包里发现了相同路线的类

transformClassesWithMultidexlistForDebug以此职分开销的小时也不长将近8秒,它有多少个效益

  • 壹 、扫描项指标AndroidManifest.xml文件和剖析类之间的依靠关系,总计出那么些类必须放在第①个dex里面,最终把分析的结果写到app/build/intermediates/multi-dex/debug/maindexlist.txt文件之中
  • ② 、生成混淆配置项输出到app/build/intermediates/multi-dex/debug/manifest_keep.txt文件里

花色里的代码入口是manifest中application节点的性情android.name配置的继承自Application的类,在android5.0在先的版本系统只会加载三个dex(classes.dex),classes2.dex
…….classesN.dex
一般是使用android.support.multidex.MultiDex加载的,所以假使输入的Application类不在classes.dex里5.0之下肯定会挂掉,其它当入口Application依赖的类不在classes.dex时初叶化的时候也会因为类找不到而挂掉,还有若是混淆的时候类名变掉了也会因为对应持续而挂掉,综上所述正是这一个职务的效用

transformClassesWithDexForDebug以此职分的效应是把带有全部class文件的jar包转换为dex,class文件更加多变换的越慢
输入的jar包路径是app/build/intermediates/transforms/jarMerging/debug/jars/1/1f/combined.jar
出口dex的目录是build/intermediates/transforms/dex/debug/folders/一千/1f/main

***瞩目编写gradle插件时假使急需利用方面那一个途径不要硬编码的方式写死,最好从Android
gradle api中去获取路径,幸免现在产生变化

重组方面包车型客车那一个信息根本须要优化的是transformClassesWithDexForDebug其一职务,我的思绪是率先次全量打包举办完transformClassesWithDexForDebug任务后把变化的dex缓存下来,并且在实践这几个任务前对方今具备的java源文件做快速照相,现在补丁打包的时候经过当前怀有的java文件音讯和事先的快速照相做比较,找出转变的java文件进而赢得这一个class文件产生变化,然后把app/build/intermediates/transforms/jarMerging/debug/jars/1/1f/combined.jar中从未成形的class移除掉,仅把变化class送去生成dex,然后选拔一种热修复方案把那几个dex当做补丁dex加载进来,有思路了后头正是夺取各种技术点

==============================

体系地址:
https://github.com/typ0520/fastdex
对应tag:
https://github.com/typ0520/fastdex/releases/tag/v.0.5.1
demo代码:
https://github.com/typ0520/fastdex-test-project

探望源码

上一篇文章说到了足以因此android的额外属性名访问Project的android属性,从而访问android的编写翻译。
笔者们后天会由此源码看一下,android到底是何许兑现的。
首先是AppPlugin.java文件(即插件的主文件),里面有那般一行代码:

project.getExtensions()
                .create(
                        "android",
                        AppExtension.class,
                        project,
                        instantiator,
                        androidBuilder,
                        sdkHandler,
                        buildTypeContainer,
                        productFlavorContainer,
                        signingConfigContainer,
                        extraModelInfo);

能够看看AppExtension正是android拓展属性的公文,那里要评释的是借使是编写翻译Library,使用的就是LibraryPlugin插件,对应的展开文件为LibraryExtension。好了小编们照旧一连说多少个文件比较一下信手拈来发现,八个文本都以基本上的,皆未来续TestedExtension,只是属性别变化量不太相同。后天大家依旧只说AppExtension。
在那几个文件中有二个如此的数组ApplicationVariant:

990.am 2

随后再看看ApplicationVariant是何等,ApplicationVariant只是贰个接口,同时有一连了别的类,然后其他类有独家继承了其他类,那里的后续关系有个别复杂,小编画多个图或者相比较好评释。

990.am 3

图某些大了,可是应该能够看掌握BaseVariant中的方法比较多,小编概括了一晃,省略的片段首若是获得上一节打印的Task的点子。一时半刻应该不会用到。
此地有个十分重要的措施BaseVariantOutput,大家常用的轮换占位符(多渠道打包等)都会用到,那里做个简单的介绍:

990.am 4

以上介绍的法门和类都很要紧,想更深理解的心上人不妨仿照上面包车型地铁demo,多打字与印刷些log看看输出的内容都以何许,

什么样获得transformClassesWithDexForDebug职务履行前后的生命周期

参考了Tinker种类的代码,找到下边包车型客车落到实处

public class ImmutableDexTransform extends Transform {
    Project project
    DexTransform dexTransform
    def variant

    ......

    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, IOException, InterruptedException {
        def outputProvider = transformInvocation.getOutputProvider()
        //dex的输出目录
        File outputDir = outputProvider.getContentLocation("main", dexTransform.getOutputTypes(), dexTransform.getScopes(), Format.DIRECTORY);
        if (outputDir.exists()) {
            outputDir.delete()
        }
        println("===执行transform前清空dex输出目录: ${project.projectDir.toPath().relativize(outputDir.toPath())}")
        dexTransform.transform(transformInvocation)
        if (outputDir.exists()) {
            println("===执行transform后dex输出目录不是空的: ${project.projectDir.toPath().relativize(outputDir.toPath())}")
            outputDir.listFiles().each {
                println("===执行transform后: ${it.name}")
            }
        }
    }
}

project.getGradle().getTaskGraph().addTaskExecutionGraphListener(new TaskExecutionGraphListener() {
    @Override
    public void graphPopulated(TaskExecutionGraph taskGraph) {
        for (Task task : taskGraph.getAllTasks()) {
            if (task instanceof TransformTask && task.name.toLowerCase().contains(variant.name.toLowerCase())) {

                if (((TransformTask) task).getTransform() instanceof DexTransform && !(((TransformTask) task).getTransform() instanceof ImmutableDexTransform)) {
                    project.logger.warn("find dex transform. transform class: " + task.transform.getClass() + " . task name: " + task.name)

                    DexTransform dexTransform = task.transform
                    ImmutableDexTransform hookDexTransform = new ImmutableDexTransform(project,
                            variant, dexTransform)
                    project.logger.info("variant name: " + variant.name)

                    Field field = TransformTask.class.getDeclaredField("transform")
                    field.setAccessible(true)
                    field.set(task, hookDexTransform)
                    project.logger.warn("transform class after hook: " + task.transform.getClass())
                    break;
                }
            }
        }
    }
});

把地方的代码放进app/build.gradle执行./gradlew assembleDebug

:app:transformClassesWithMultidexlistForDebug
ProGuard, version 5.2.1
Reading program jar [/Users/tong/Projects/fastdex/app/build/intermediates/transforms/jarMerging/debug/jars/1/1f/combined.jar]
Reading library jar [/Users/tong/Applications/android-sdk-macosx/build-tools/23.0.1/lib/shrinkedAndroid.jar]
Preparing output jar [/Users/tong/Projects/fastdex/app/build/intermediates/multi-dex/debug/componentClasses.jar]
  Copying resources from program jar [/Users/tong/Projects/fastdex/app/build/intermediates/transforms/jarMerging/debug/jars/1/1f/combined.jar]
:app:transformClassesWithDexForDebug
===执行transform前清空dex输出目录: build/intermediates/transforms/dex/debug/folders/1000/1f/main
......
===执行transform后dex输出目录不是空的: build/intermediates/transforms/dex/debug/folders/1000/1f/main
===执行transform后: classes.dex

从地方的日志输出申明这几个hook点是有效的,在全量打包时实施transform前能够对java源码做快速照相,执行完之后把dex缓存下来;在补丁打包进行transform从前比较快速照相移除没有转变的class,执行完之后合并缓存的dex放进dex输出目录

==============================

注:
提出把fastdex的代码和demo代码拉下来,本文中的绝一大半例子在demo工程中得以一贯跑

注: 本文对gradle task做的证实都创设在闭馆instant run的前提下
注:
本文全部的代码、gradle职责名、职务输出路径、全体应用debug这么些buildType作表达

注: 本文使用./gradlew执行职分是在mac下,若是是windows换到gradlew.bat

写个小demo

只要上面包车型客车图认真的看一下,就足以了然variant中包括了android编写翻译的要紧功效参数,所以要初阶,只好从那边动手了。
在此间咱们先举个大致的小例子(在前边的demo中做个修改,不纯熟的情人请参见前两章):

 void apply(Project project) {
        project.extensions.create("deep", MyExtension)
        project.afterEvaluate {
            MyExtension extension = project['deep'];
            String a = extension.aaa
            String b = extension.bbb
            println("deep:${a},${b}")
            project.tasks.getByName("preDebugBuild") {
                it.doFirst {
                    project.android.applicationVariants.all { variant ->
                        variant.outputs.each { output ->
                            output.processManifest.doLast {
                                def manifestFile = "${project.getProjectDir().absolutePath}/build/intermediates/manifests/full/${variant.dirName}/AndroidManifest.xml"
                                def updatedContent = new File(manifestFile).getText('UTF-8').replaceAll("vvvvv", "ccccc")
                                new File(manifestFile).write(updatedContent, 'UTF-8')
                            }
                        }
                    }
                }
            }
        }
    }

那段代码的意义很肯定是修改AndroidManifest中的字符串vvvvv,改成ccccc。说白了作用正是替换字符串,跟替换占位符是三个功效。看看上边有个至关心注重要的写法:

output ->
                            output.processManifest.doLast {}

那是怎么样意思呢,依据下面我们介绍的源码,可以掌握那么些正是在processManifest
Task之后再去履行,再由地点介绍到的内容processManifest是统一AndroidManifest的天职,在集合之后,立刻去修改AndroidManifest文件,保证了占位符的轮换。
再修改一下那个文件,大家看看output的file是什么样:

project.tasks.getByName("preDebugBuild") {
                it.doFirst {
                    project.android.applicationVariants.all { variant ->
                        variant.outputs.each { output ->
                            println("out put="+output.getOutputFile())
                        }
                    }
                }
            }

看看输出:

990.am 5

有时候大家大概需求修改一下apk的名字,依据地点的源码,大家找到了1个方法正是setOutputFile,来我们在打打log,看一下:

  project.tasks.getByName("preDebugBuild") {
                it.doFirst {
                    project.android.applicationVariants.all { variant ->
                        variant.outputs.each { output ->
                                println("getDirName:"+output.getDirName())
                                output.setOutputFile(new File("${project.getProjectDir().absolutePath}/build/${output.getName()}_aaa.apk"))
                                println("out put=" + output.getOutputFile())
                        }
                    }
                }
            }

看望结果:

990.am 6

再有变化的apk:

990.am 7

如何做快速照相与对待快速照相并得到变化的class列表

进行上面包车型地铁代码能够赢得具有的种类源码目录

project.android.sourceSets.main.java.srcDirs.each { srcDir->
    println("==srcDir: ${srcDir}")
}

sample工程没有安排sourceSets,由此输出的是app/src/main/java

给源码目录做快速照相,直接通过文件复制的办法,把富有的srcDir目录下的java文件复制到快速照相目录下(那里有个坑,不要选用project.copy
{}它会使文件的lastModified值发生变化,直接使用流copy并且要用源文件的lastModified覆盖指标文件的lastModified)

因此java文件的长短和上次修改时间五个成分相比能够摸清同二个文件是不是产生变化,通过快速照相目录没有有些文件而当前目录有某些文件能够查出增添了文件,通过快速照相目录有有些文件不过当前目录没有得以摸清删除文件(为了成效能够不处理删除,仅造成缓存里有一些用不到的类而已)
举个例子来说假若项目源码的路径为/Users/tong/fastdex/app/src/main/java,做快速照相时把这些目录复制到/Users/tong/fastdex/app/build/fastdex/snapshoot下,当前快速照相里的公文树为

└── com
    └── dx168
        └── fastdex
            └── sample
                ├── CustomView.java
                ├── MainActivity.java
                └── SampleApplication.java

借使当前源码路径的始末发生变化,当前的公文树为

└── com
    └── dx168
        └── fastdex
            └── sample
                ├── CustomView.java
                ├── MainActivity.java(内容已经被修改)
                ├── New.java
                └── SampleApplication.java

经过文件遍历相比较能够收获那一个变化的相对路径列表

  • com/dx168/fastdex/sample/MainActivity.java
  • com/dx168/fastdex/sample/New.java

通过这么些列表进而能够摸清变化的class有

  • com/dx168/fastdex/sample/MainActivity.class
  • com/dx168/fastdex/sample/New.class

只是java文件编译的时候假设有内部类还会有此外的一对class输出,比如拿君越文件做下编写翻译,它的编写翻译输出如下

➜  sample git:(master) ls
R.java
➜  sample git:(master) javac R.java 
➜  sample git:(master) ls
R$attr.class      R$dimen.class     R$id.class        R$layout.class    R$string.class    R$styleable.class R.java
R$color.class     R$drawable.class  R$integer.class   R$mipmap.class    R$style.class     R.class
➜  sample git:(master) 

除此以外假诺利用了butterknife,还会生成binder类,比如编译MainActivity.java时生成了
com/dx168/fastdex/sample/MainActivity$$ViewBinder.class

结缘方面几点可以取得具有变化class的匹配情势

  • com/dx168/fastdex/sample/MainActivity.class
  • com/dx168/fastdex/sample/MainActivity$*.class
  • com/dx168/fastdex/sample/New.class
  • com/dx168/fastdex/sample/New$*.class

有了地方的极度形式就能够在补丁打包举行transform前把app/build/intermediates/transforms/jarMerging/debug/jars/1/1f/combined.jar中没有变化的class全体移除掉

project.copy {
    from project.zipTree(combinedJar)
        for (String pattern : patterns) {
            include pattern
        }
    }
    into tmpDir
}
project.ant.zip(baseDir: tmpDir, destFile: patchJar)

接下来就足以行使patchJar作为输入jar生成补丁dex

注:
那种映射方案一经打开了歪曲就对应不上了,需求分析混淆以往产生的mapping文件才能化解,可是咱们也远非要求在打开混淆的buildType下做开发支出调试,所以一时可以不做这些工作

==============================
有了补丁dex,就能够挑选一种热修复方案把补丁dex加载进来,那里方案有好两种,为了不难直接选用android.support.multidex.MultiDex以dex插桩的不二法门来加载,只需求把dex根据google标准(classes.dex、classes2.dex、classesN.dex)排列好就行了,那里有三个技术点

是因为patch.dex和缓存下来dex里面有再一次的类,当加载引用了再也类的类时会招致pre-verify的错误,具体请参考QQ空间组织写的安卓App热补丁动态修复技术介绍
,那篇小说详细分析了造成pre-verify错误的来头,文章里给的消除方案是往全体引用被修复类的类中插入一段代码,并且被插入的那段代码所在的类的dex必须是二个独门的dex,那几个dex我们事先准备好,叫做fastdex-runtime.dex,它的代码结构是

└── com
    └── dx168
        └── fastdex
            └── runtime
                ├── FastdexApplication.java
                ├── antilazyload
                │   └── AntilazyLoad.java
                └── multidex
                    ├── MultiDex.java
                    ├── MultiDexApplication.java
                    ├── MultiDexExtractor.java
                    └── ZipUtil.java

AntilazyLoad.java正是在注入时被引述的类
MultiDex.java是用来加载classes2.dex –
classesN.dex的包,为了预防项目尚未注重MultiDex,所以把MultiDex的代码copy到了小编们的package下
法斯特dexApplication.java的效用后边在说

重组大家的门类必要在全量打包前把app/build/intermediates/transforms/jarMerging/debug/jars/1/1f/combined.jar中有所的项目代码的class全部动态插入代码(第二方库由于不在大家的修补范围内之所以为了效用忽略掉),具体的做法是往全体的构造方法中添加对com.dx168.fastdex.runtime.antilazyload.AntilazyLoad的依靠,如上边的代码所示

//source class:
public class MainActivity {
}

==>

//dest class:
import com.dx168.fastdex.runtime.antilazyload.AntilazyLoad;
public class MainActivity {
    public MainActivity() {
        System.out.println(Antilazyload.str);
    }
}

动态往class文件中插入代码应用的是asm,笔者把做测试的时候找到的一部分有关材料和代码都放到了github上边点自身查看,代码比较四只贴出来一部分,具体请查看ClassInject.groovy

 private static class MyClassVisitor extends ClassVisitor {
    public MyClassVisitor(ClassVisitor classVisitor) {
        super(Opcodes.ASM5, classVisitor);
    }

    @Override
    public MethodVisitor visitMethod(int access,
                                     String name,
                                     String desc,
                                     String signature,
                                     String[] exceptions) {
        //判断是否是构造方法
        if ("<init>".equals(name)) {
            MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
            MethodVisitor newMethod = new AsmMethodVisit(mv);
            return newMethod;
        } else {
            return super.visitMethod(access, name, desc, signature, exceptions);
        }
    }
}

static class AsmMethodVisit extends MethodVisitor {
    public AsmMethodVisit(MethodVisitor mv) {
        super(Opcodes.ASM5, mv);
    }

    @Override
    public void visitInsn(int opcode) {
        if (opcode == Opcodes.RETURN) {
            //访问java/lang/System的静态常量out
            mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            //访问AntilazyLoad的静态变量
            mv.visitFieldInsn(GETSTATIC, "com/dx168/fastdex/runtime/antilazyload/AntilazyLoad", "str", "Ljava/lang/String;");
            //调用out的println打印AntilazyLoad.str的值
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
        }
        super.visitInsn(opcode);
    }
}

===============
拍卖完pre-verify难点,接下去又出新坑了,当补丁dex打好后倘若缓存的dex有八个(classes.dex
classes2.dex),那么合并dex后的次第正是
fastdex-runtime.dex 、patch.dex、classes.dex 、classes2.dex
(patch.dex必须放在缓存的dex此前才能被修复)

fastdex-runtime.dex  => classes.dex
patch.dex            => classes2.dex
classes.dex          => classes3.dex
classes2.dex         => classes4.dex

在讲解transformClassesWithMultidexlistForDebug义务时有说经过序入口Application的标题,假使patch.dex中不包涵入口Application,apk运维的时候势必会报类找不到的荒唐,那么怎么化解这一个标题啊

    1. 第贰个方案:
      transformClassesWithMultidexlistForDebug任务中输出的maindexlist.txt中颇具的class都踏足patch.dex的成形
    1. 第两种方案:
      对品种的入口Application做代理,并把这几个代理类放在第③个dex里面,项目标dex根据顺序放在后边

第③种方案方案由于必须让maindexlist.txt中山大学量的类参预了补丁的转变,与事先尽量减弱class文件参与dex生成的思索是相冲突的,功能相对于首个方案比较低,其它2个原因是不可能保险项指标Application中应用了MultiDex;

第二种方案尚未上述难点,可是借使项目代码中有接纳getApplication()做强转就会出标题(参考issue#2),instant
run也会有一样的题材,它的做法是hook系统的api运转期把Application还原回来,所以强转就不会有题目了,请参考MonkeyPatcher.java(须求翻墙才能开拓,要是看不住就参照FastdexApplication.java的monkeyPatchApplication方法)

总结最终甄选了第两种方案以下是fastdex-runtime.dex中代理Application的代码

public class FastdexApplication extends Application {
    public static final String LOG_TAG = "Fastdex";
    private Application realApplication;

    //从manifest文件的meta_data中获取真正的项目Application类
    private String getOriginApplicationName(Context context) {
        ApplicationInfo appInfo = null;
        try {
            appInfo = context.getPackageManager().getApplicationInfo(getPackageName(), PackageManager.GET_META_DATA);
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        String msg = appInfo.metaData.getString("FASTDEX_ORIGIN_APPLICATION_CLASSNAME");
        return msg;
    }

    private void createRealApplication(Context context) {
        String applicationClass = getOriginApplicationName(context);
        if (applicationClass != null) {
            Log.d(LOG_TAG, new StringBuilder().append("About to create real application of class name = ").append(applicationClass).toString());

            try {
                Class realClass = Class.forName(applicationClass);
                Constructor constructor = realClass.getConstructor(new Class[0]);
                this.realApplication = ((Application) constructor.newInstance(new Object[0]));
                Log.v(LOG_TAG, new StringBuilder().append("Created real app instance successfully :").append(this.realApplication).toString());
            } catch (Exception e) {
                throw new IllegalStateException(e);
            }
        } else {
            this.realApplication = new Application();
        }
    }

    protected void attachBaseContext(Context context) {
        super.attachBaseContext(context);
        MultiDex.install(context);
        createRealApplication(context);

        if (this.realApplication != null)
            try {
                Method attachBaseContext = ContextWrapper.class
                        .getDeclaredMethod("attachBaseContext", new Class[]{Context.class});

                attachBaseContext.setAccessible(true);
                attachBaseContext.invoke(this.realApplication, new Object[]{context});
            } catch (Exception e) {
                throw new IllegalStateException(e);
            }
    }

    public void onCreate() {
        super.onCreate();

        if (this.realApplication != null) {
            this.realApplication.onCreate();
        }
    }
    ......
}

依照在此之前的职分表达生成manifest文件的天职是processDebugManifest,大家只要求在那些职务履行完事后做处理,创制一个兑现类为FastdexManifestTask的义务,主旨代码如下

def ns = new Namespace("http://schemas.android.com/apk/res/android", "android")
def xml = new XmlParser().parse(new InputStreamReader(new FileInputStream(manifestPath), "utf-8"))
def application = xml.application[0]
if (application) {
    QName nameAttr = new QName("http://schemas.android.com/apk/res/android", 'name', 'android');
    def applicationName = application.attribute(nameAttr)
    if (applicationName == null || applicationName.isEmpty()) {
        applicationName = "android.app.Application"
    }
    //替换application的android.name节点
    application.attributes().put(nameAttr, "com.dx168.fastdex.runtime.FastdexApplication")
    def metaDataTags = application['meta-data']
    // remove any old FASTDEX_ORIGIN_APPLICATION_CLASSNAME elements
    def originApplicationName = metaDataTags.findAll {
        it.attributes()[ns.name].equals(FASTDEX_ORIGIN_APPLICATION_CLASSNAME)
    }.each {
        it.parent().remove(it)
    }
    // Add the new FASTDEX_ORIGIN_APPLICATION_CLASSNAME element
    //把原来的Application写入到meta-data中
    application.appendNode('meta-data', [(ns.name): FASTDEX_ORIGIN_APPLICATION_CLASSNAME, (ns.value): applicationName])
    // Write the manifest file
    def printer = new XmlNodePrinter(new PrintWriter(manifestPath, "utf-8"))
    printer.preserveWhitespace = true
    printer.print(xml)
}
File manifestFile = new File(manifestPath)
if (manifestFile.exists()) {
    File buildDir = FastdexUtils.getBuildDir(project,variantName)
    FileUtils.copyFileUsingStream(manifestFile, new File(buildDir,MANIFEST_XML))
    project.logger.error("fastdex gen AndroidManifest.xml in ${MANIFEST_XML}")
}

利用下边包车型地铁代码把这一个任务加进去并确定保障在processDebugManifest职责执行完结后实施

project.afterEvaluate {
    android.applicationVariants.all { variant ->
        def variantOutput = variant.outputs.first()
        def variantName = variant.name.capitalize()

        //替换项目的Application为com.dx168.fastdex.runtime.FastdexApplication
        FastdexManifestTask manifestTask = project.tasks.create("fastdexProcess${variantName}Manifest", FastdexManifestTask)
        manifestTask.manifestPath = variantOutput.processManifest.manifestOutputFile
        manifestTask.variantName = variantName
        manifestTask.mustRunAfter variantOutput.processManifest

        variantOutput.processResources.dependsOn manifestTask
    }
}

拍卖完之后manifest文件application节点android.name属性的值就成为了com.dx168.fastdex.runtime.FastdexApplication,并且把原先项指标Application的名字写入到meta-data中,用来运转期给法斯特dexApplication去读取

<meta-data android:name="FASTDEX_ORIGIN_APPLICATION_CLASSNAME" android:value="com.dx168.fastdex.sample.SampleApplication"/>

==============================

一、拦截transformClassesWithJarMergingForDebug任务

事先补丁打包的时候,是把尚未成形的类从app/build/intermediates/transforms/jarMerging/debug/jars/1/1f/combined.jar中移除,那样的做法有三个难点

  • 一 、combined.jar这么些文件是*
    transformClassesWithJarMergingForDebug任务输出的,存在这么些义务的前提是打开了multidex,假设没有打开那么执行到
    transformClassesWithDexForDebug*职责时输入就不在是combined.jar,而是项目标classes目录(app/build/intermediates/classes/debug)和注重的library输出的jar以及第③方库的jar;
  • 二 、借使存在transformClassesWithJarMergingForDebug职分,先开销多量年华合成combined.jar,然后在把尚未成形的类从combined.jar中移除,那样功用太低了,若是绕过combined.jar的合成间接拿变化class去生成dex对功用会有不小的增长

到现在先是供给得到transformClassesWithJarMergingForDebug义务履行前后的生命周期,完毕的主意和拦阻transformClassesWithDexForDebug时用的方案差不离,完整的测试代码地址
https://github.com/typ0520/fastdex-test-project/tree/master/jarmerging-test

public class MyJarMergingTransform extends Transform {
    Transform base

    MyJarMergingTransform(Transform base) {
        this.base = base
    }

    @Override
    void transform(TransformInvocation invocation) throws TransformException, IOException, InterruptedException {
        List<JarInput> jarInputs = Lists.newArrayList();
        List<DirectoryInput> dirInputs = Lists.newArrayList();
        for (TransformInput input : invocation.getInputs()) {
            jarInputs.addAll(input.getJarInputs());
        }
        for (TransformInput input : invocation.getInputs()) {
            dirInputs.addAll(input.getDirectoryInputs());
        }
        for (JarInput jarInput : jarInputs) {
            println("==jarmerge jar      : ${jarInput.file}")
        }
        for (DirectoryInput directoryInput : dirInputs) {
            println("==jarmerge directory: ${directoryInput.file}")
        }
        File combinedJar = invocation.outputProvider.getContentLocation("combined", base.getOutputTypes(), base.getScopes(), Format.JAR);
        println("==combinedJar exists ${combinedJar.exists()} ${combinedJar}")
        base.transform(invocation)
        println("==combinedJar exists ${combinedJar.exists()} ${combinedJar}")
    }
}

public class MyDexTransform extends Transform {
    Transform base

    MyDexTransform(Transform base) {
        this.base = base
    }

    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, IOException, InterruptedException {
        List<JarInput> jarInputs = Lists.newArrayList();
        List<DirectoryInput> dirInputs = Lists.newArrayList();
        for (TransformInput input : transformInvocation.getInputs()) {
            jarInputs.addAll(input.getJarInputs());
        }
        for (TransformInput input : transformInvocation.getInputs()) {
            dirInputs.addAll(input.getDirectoryInputs());
        }
        for (JarInput jarInput : jarInputs) {
            println("==dex jar      : ${jarInput.file}")
        }
        for (DirectoryInput directoryInput : dirInputs) {
            println("==dex directory: ${directoryInput.file}")
        }
        base.transform(transformInvocation)
    }
}

project.afterEvaluate {
    android.applicationVariants.all { variant ->
        project.getGradle().getTaskGraph().addTaskExecutionGraphListener(new TaskExecutionGraphListener() {
            @Override
            public void graphPopulated(TaskExecutionGraph taskGraph) {
                for (Task task : taskGraph.getAllTasks()) {
                    if (task.getProject().equals(project) && task instanceof TransformTask && task.name.toLowerCase().contains(variant.name.toLowerCase())) {
                        Transform transform = ((TransformTask) task).getTransform()
                        //如果开启了multidex有这个任务
                        if ((((transform instanceof JarMergingTransform)) && !(transform instanceof MyJarMergingTransform))) {
                            project.logger.error("==fastdex find jarmerging transform. transform class: " + task.transform.getClass() + " . task name: " + task.name)

                            MyJarMergingTransform jarMergingTransform = new MyJarMergingTransform(transform)
                            Field field = getFieldByName(task.getClass(),'transform')
                            field.setAccessible(true)
                            field.set(task,jarMergingTransform)
                        }

                        if ((((transform instanceof DexTransform)) && !(transform instanceof MyDexTransform))) {
                            project.logger.error("==fastdex find dex transform. transform class: " + task.transform.getClass() + " . task name: " + task.name)

                            //代理DexTransform,实现自定义的转换
                            MyDexTransform fastdexTransform = new MyDexTransform(transform)
                            Field field = getFieldByName(task.getClass(),'transform')
                            field.setAccessible(true)
                            field.set(task,fastdexTransform)
                        }
                    }
                }
            }
        });
    }
}

把地点的代码放进app/build.gradle执行./gradlew assembleDebug

  • 打开multidex(multiDexEnabled true)时的日志输出**

:app:mergeDebugAssets
:app:transformClassesWithJarMergingForDebug
==jarmerge jar      : /Users/tong/Projects/fastdex-test-project/jarmerging-test/app/libs/exist-in-app-libs-2.1.2.jar
==jarmerge jar      : /Users/tong/Projects/fastdex-test-project/jarmerging-test/app/build/intermediates/exploded-aar/com.android.support/multidex/1.0.1/jars/classes.jar
==jarmerge jar      : /Users/tong/Applications/android-sdk-macosx/extras/android/m2repository/com/android/support/support-annotations/23.3.0/support-annotations-23.3.0.jar
==jarmerge jar      : /Users/tong/Projects/fastdex-test-project/jarmerging-test/app/build/intermediates/exploded-aar/com.jakewharton/butterknife/8.0.1/jars/classes.jar
==jarmerge jar      : /Users/tong/.gradle/caches/modules-2/files-2.1/com.jakewharton/butterknife-annotations/8.0.1/345b89f45d02d8b09400b472fab7b7e38f4ede1f/butterknife-annotations-8.0.1.jar
==jarmerge jar      : /Users/tong/Projects/fastdex-test-project/jarmerging-test/javalib/build/libs/javalib.jar
==jarmerge jar      : /Users/tong/Projects/fastdex-test-project/jarmerging-test/app/build/intermediates/exploded-aar/jarmerging-test/aarlib/unspecified/jars/classes.jar
==jarmerge directory: /Users/tong/Projects/fastdex-test-project/jarmerging-test/app/build/intermediates/classes/debug
==combinedJar exists false /Users/tong/Projects/fastdex-test-project/jarmerging-test/app/build/intermediates/transforms/jarMerging/debug/jars/1/1f/combined.jar
==combinedJar exists true /Users/tong/Projects/fastdex-test-project/jarmerging-test/app/build/intermediates/transforms/jarMerging/debug/jars/1/1f/combined.jar
:app:transformClassesWithMultidexlistForDebug
:app:transformClassesWithDexForDebug
===dex jar      : /Users/tong/Projects/fastdex-test-project/jarmerging-test/app/build/intermediates/transforms/jarMerging/debug/jars/1/1f/combined.jar
:app:mergeDebugJniLibFolders
  • 关门multidex(multiDexEnabled false)时的日志输出**

:app:mergeDebugAssets
:app:transformClassesWithDexForDebug
===dex jar      : /Users/tong/Projects/fastdex-test-project/jarmerging-test/app/libs/exist-in-app-libs-2.1.2.jar
===dex jar      : /Users/tong/Applications/android-sdk-macosx/extras/android/m2repository/com/android/support/support-annotations/23.3.0/support-annotations-23.3.0.jar
===dex jar      : /Users/tong/Projects/fastdex-test-project/jarmerging-test/app/build/intermediates/exploded-aar/com.jakewharton/butterknife/8.0.1/jars/classes.jar
===dex jar      : /Users/tong/.gradle/caches/modules-2/files-2.1/com.jakewharton/butterknife-annotations/8.0.1/345b89f45d02d8b09400b472fab7b7e38f4ede1f/butterknife-annotations-8.0.1.jar
===dex jar      : /Users/tong/Projects/fastdex-test-project/jarmerging-test/javalib/build/libs/javalib.jar
===dex jar      : /Users/tong/Projects/fastdex-test-project/jarmerging-test/app/build/intermediates/exploded-aar/jarmerging-test/aarlib/unspecified/jars/classes.jar
===dex directory: /Users/tong/Projects/fastdex-test-project/jarmerging-test/app/build/intermediates/classes/debug
:app:mergeDebugJniLibFolders

从地点的日记输出能够看到,只必要在下图中湖蓝箭头指的地点做patch.jar的生成就能够了

990.am 8

flow.png

别的在此之前全量打包做asm
code注入的时候是遍历combined.jar要是entry对应的是种类代码就做注入,反之认为是第3方库跳过注入(第1方库不在修复之列,为了节省注入开支的小运因故忽略);未来阻碍了jarmerge职务,直接扫描全数的DirectoryInput对应目录下的有着class做注入就行了,功效会比从前的做法有十分的大升高

总结

本次介绍的剧情比较多,要想多精晓,或然要求多调节和测试五回,多打打log才能整个驾驭,喜欢的用户能够依照本人上边提到的不二法门,多试几遍。
接下去的稿子恐怕会青睐介绍groovy的语法,因为有用户微信公众号给自个儿留言说写的代码,瞅着某些别扭。还有大概会介绍一些别的gradle的插件,比如上面提到的打字与印刷职分日志等等。
爱好的情侣能够关切本身的公众号,第暂时间看到作品:

990.am 9

相关文章