美文网首页移动开发Android 知识Android Tech
加快apk的构建速度,如何把编译时间从130秒降到17秒

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

作者: typ0520 | 来源:发表于2017-03-16 20:52 被阅读22070次

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

    公司的项目代码比较多,每次调试改动java文件后要将近2分钟才能跑起来,实在受不了。在网上找了一大堆配置参数也没有很明显的效果, 尝试使用instant run效果也不怎么样,然后又尝试使用freeline编译速度还可以但是不稳定,每次失败后全量编译很耗费时间,既然没有好的方案就自己尝试做。

    项目地址: https://github.com/typ0520/fastdex

    注: 本文对gradle task做的说明都建立在关闭instant run的前提下
    注: 本文所有的代码、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的作用

    • 1、调用aapt生成项目和所有aar依赖的R.java,输出到app/build/generated/source/r/debug目录
    • 2、生成资源索引文件app/build/intermediates/res/resources-debug.ap_
    • 3、把符号表输出到app/build/intermediates/symbols/debug/R.txt

    compileDebugJavaWithJavac这个任务是用来把java文件编译成class文件,输出的路径是app/build/intermediates/classes/debug
    编译的输入目录有

    • 1、项目源码目录,默认路径是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
    • 4、app/build/generated/source/apt(继承javax.annotation.processing.AbstractProcessor做动态代码生成的一些库,输出在这个目录,具体可以参考ButterknifeTinker)的代码

    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秒,它有两个作用

    • 1、扫描项目的AndroidManifest.xml文件和分析类之间的依赖关系,计算出那些类必须放在第一个dex里面,最后把分析的结果写到app/build/intermediates/multi-dex/debug/maindexlist.txt文件里面
    • 2、生成混淆配置项输出到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/1000/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加载进来,有思路了后面就是攻克各个技术点

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

    如何拿到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输出目录

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

    如何做快照与对比快照并拿到变化的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输出,比如拿R文件做下编译,它的编译输出如下

    ➜  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下
    FastdexApplication.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生成的思想是相冲突的,效率相对于第二个方案比较低,另外一个原因是无法保证项目的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中,用来运行期给FastdexApplication去读取

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

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

    开发完以上功能后做下面的四次打包做时间对比(其实只做一次并不是太准确,做几十次测试取时间的平均值这样才最准)
    • 1、删除build目录第一次全量打包(不开启fastdex)

      BUILD SUCCESSFUL
      
        Total time: 1 mins 46.678 secs
        Task spend time:
          437ms  :app:prepareComAndroidSupportAppcompatV72340Library
           50ms  :app:prepareComAndroidSupportDesign2340Library
           66ms  :app:prepareComAndroidSupportSupportV42340Library
           75ms  :app:prepareComFacebookFrescoImagepipeline110Library
           56ms  :app:prepareOrgXutilsXutils3336Library
          870ms  :app:mergeDebugResources
           93ms  :app:processDebugManifest
          777ms  :app:processDebugResources
         1200ms  :app:compileDebugJavaWithJavac
         3643ms  :app:transformClassesWithJarMergingForDebug
         5520ms  :app:transformClassesWithMultidexlistForDebug
        61770ms  :app:transformClassesWithDexForDebug
           99ms  :app:transformNative_libsWithMergeJniLibsForDebug
          332ms  :app:transformResourcesWithMergeJavaResForDebug
         2083ms  :app:packageDebug
          202ms  :app:zipalignDebug
          
      
    • 2、删除build目录第一次全量打包(开启fastdex)

      BUILD SUCCESSFUL
      
        Total time: 1 mins 57.764 secs
        Task spend time:
          106ms  :app:prepareComAndroidSupportAnimatedVectorDrawable2340Library
          107ms  :runtime:transformClassesAndResourcesWithSyncLibJarsForDebug
          416ms  :app:prepareComAndroidSupportAppcompatV72340Library
           67ms  :app:prepareComAndroidSupportSupportV42340Library
           76ms  :app:prepareComFacebookFrescoImagepipeline110Library
           53ms  :app:prepareOrgXutilsXutils3336Library
          111ms  :app:processDebugManifest
          929ms  :app:mergeDebugResources
          697ms  :app:processDebugResources
         1227ms  :app:compileDebugJavaWithJavac
         3237ms  :app:transformClassesWithJarMergingForDebug
         6225ms  :app:transformClassesWithMultidexlistForDebug
        78990ms  :app:transformClassesWithDexForDebug
          122ms  :app:transformNative_libsWithMergeJniLibsForDebug
          379ms  :app:transformResourcesWithMergeJavaResForDebug
         2050ms  :app:packageDebug
           77ms  :app:zipalignDebug
           
      
    • 3、在开启fastdex第一次全量打包完成后,关掉fastdex修改sample工程的MainActivity.java

      BUILD SUCCESSFUL
      
      Total time: 1 mins 05.394 secs
      Task spend time:
         52ms  :app:mergeDebugResources
       2583ms  :app:compileDebugJavaWithJavac
      60718ms  :app:transformClassesWithDexForDebug
        101ms  :app:transformNative_libsWithMergeJniLibsForDebug
        369ms  :app:transformResourcesWithMergeJavaResForDebug
       2057ms  :app:packageDebug
         75ms  :app:zipalignDebug
      
    • 4、在开启fastdex第一次全量打包完成后,仍然开启fastdex修改sample工程的MainActivity.java

      BUILD SUCCESSFUL
      
        Total time: 16.5 secs
        Task spend time:
          142ms  :app:processDebugManifest
         1339ms  :app:compileDebugJavaWithJavac
         3291ms  :app:transformClassesWithJarMergingForDebug
         4865ms  :app:transformClassesWithMultidexlistForDebug
         1005ms  :app:transformClassesWithDexForDebug
         2112ms  :app:packageDebug
           76ms  :app:zipalignDebug
      
    打包编号 总时间 transform时间
    1 1 mins 46.678s 61770 ms
    2 1 mins 57.764s 78990 ms
    3 1 mins 05.394s 60718 ms
    4 16.5s 1005 ms

    通过1和2对比发现,开启fastdex进行第一次全量的打包时的时间花费比不开启多了10秒左右,这个主要是注入代码和IO上的开销

    通过2和3对比发现,开启fastdex进行补丁打包时的时间花费比不开启快了60秒左右,这就是期待已久的构建速度啊_

    ==============================
    刚激动一会就尼玛报了一个错误,当修改activity_main.xml时往里面增加一个控件

    <TextView
        android:id="@+id/tv2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
    

    打出来的包启动的时候就直接crash掉了

    Caused by: java.lang.IllegalStateException: 
    Required view 'end_padder' with ID 2131493007 for field 'tv1' was not found.
    If this view is optional add '@Nullable' (fields) or '@Optional' (methods) annotation.
         at butterknife.internal.Finder.findRequiredView(Finder.java:51)
         at com.dx168.fastdex.sample.CustomView$$ViewBinder.bind(CustomView$$ViewBinder.java:17)
         at com.dx168.fastdex.sample.CustomView$$ViewBinder.bind(CustomView$$ViewBinder.java:12)
         at butterknife.ButterKnife.bind(ButterKnife.java:187)
         at butterknife.ButterKnife.bind(ButterKnife.java:133) 
         at com.dx168.fastdex.sample.CustomView.<init>(CustomView.java:20) 
         ......
         at dalvik.system.NativeStart.main(Native Method) 
    

    错误信息里的意思是为CustomView的tv1字段,寻找id=2131493007的view时没有找到,先反编译报错的apk,�找到报错的地方CustomView$$ViewBinder.bind

    public class CustomView$$ViewBinder<T extends CustomView>
            implements ViewBinder<T>
    {
        public CustomView$$ViewBinder()
        {
            System.out.println(AntilazyLoad.str);
        }
    
        public Unbinder bind(Finder paramFinder, T paramT, Object paramObject)
        {
            InnerUnbinder localInnerUnbinder = createUnbinder(paramT);
            paramT.tv1 = ((TextView)paramFinder.castView((View)paramFinder
                    .findRequiredView(paramObject, 2131493007, "field 'tv1'"), 2131493007, "field 'tv1'"));
            paramT.tv3 = ((TextView)paramFinder.castView((View)paramFinder
                    .findRequiredView(paramObject, 2131493008, "field 'tv3'"), 2131493008, "field 'tv3'"));
            return localInnerUnbinder;
        }
        ......
    }
    

    CustomView$$ViewBinder这个类是ButterKnife动态生成的,这个值的来源是CustomView的tv1字段上面的注解,CustomView.class反编译后如下

    public class CustomView extends LinearLayout 
    {
        @BindView(2131493007)
        TextView tv1;
        @BindView(2131493008)
        TextView tv3;
    
        public CustomView(Context paramContext, AttributeSet paramAttributeSet)
        {
            super(paramContext, paramAttributeSet);
            inflate(paramContext, 2130968632, this);
            ButterKnife.bind(this);
            this.tv3.setText(2131099697);
            MainActivity.aa();
            System.out.println(AntilazyLoad.str);
        }
    }
    
    

    看到这里是不是觉得奇怪,CustomView的源码明明是

    public class CustomView extends LinearLayout {
        @BindView(R.id.tv1)  TextView tv1;
        @BindView(R.id.tv3)  TextView tv3;
    
        public CustomView(Context context, AttributeSet attrs) {
            super(context, attrs);
            inflate(context,R.layout.view_custom,this);
            ButterKnife.bind(this);
    
            tv3.setText(R.string.s3);
            MainActivity.aa();
        }
    }
    

    �在编译以后R.id.tv1怎么就变成数字2131493007了呢,原因是java编译器做了一个性能优化,如果发现源文件引用的是一个带有final描述符的常量,会直接做值copy

    反编译最后一次编译成功时的R.class结果如下(
    app/build/intermediates/classes/debug/com/dx168/fastdex/sample/R.class)

    public static final R {
        public static final class id {
            ......
    
            public static final int tv1 = 2131493008;
            public static final int tv2 = 2131492977;
            public static final int tv3 = 2131493009;
    
            ......
    
            public id() {
            }
        }
    }
    

    经过分析,当全量打包时R.id.tv1 = 2131493007,由于R文件中的id都是final的,所以引用R.id.tv1的地方都被替换为它对应的值2131493007了;当在activity_layout.xml中添加名字为tv2的控件,然后进行补丁打包时R.id.tv1的值变成了2131493008,而缓存的dex对应节点的值还是2131493007,所以在寻找id为2131493007对应的控件时因为找不到而挂掉

    我的第一个想法是如果在执行完processDebugResources任务后,把R文件里id类的所有字段的final描述符去掉就可以把值copy这个编译优化绕过去 =>

    public static final R {
        public static final class id {
            ......
    
            public static int tv1 = 2131493008;
            public static int tv2 = 2131492977;
            public static int tv3 = 2131493009;
    
            ......
    
            public id() {
            }
        }
    }
    

    去掉以后在执行compileDebugJavaWithJavac时编译出错了

    2.png

    出错的原因是注解只能引用带final描述符的常量,除此之外switch语句的case也必须引用常量,具体请查看oracle对常量表达式的说明

    如果采取这个方案,对id的引用就不能使用常量表达式,像ButterKnife这样的view依赖注入的框架都不能用了,限制性太大这个想法就放弃了

    还有一个思路就是修改aapt的源码�,使多次打包时名字相同id的值保持一致,这个肯定能解决不过工作量太大了就没有这样做,之后采用了一个折中的办法,就是每次把项目中的所有类(除去第三方库)都参与dex的生成,虽然解决了这个问题但效率一下子降低好多,需要将近40秒才能跑起来还是很慢

    ==============================
    这个问题困扰了好久,直到tinker开源后阅读它的源码TinkerResourceIdTask.groovy时,发现它们也碰到了同样的问题,并有了一个解决方案,我们的场景和tinker场景在这个问题上是一模一样的,直接照抄代码就解决了这个问题,重要的事情说三遍,感谢tinker、感谢tinker、感谢tinker!!

    tinker的解决方案是,打补丁时根据用户配置的resourceMapping文件(每次构建成功后输出的app/build/intermediates/symbols/debug/R.txt),生成public.xml和ids.xml然后放进app/build/intermediates/res/merged/debug/values目录里,aapt在处理的时候会根据文件里的配置规则去生成,具体这块的原理请看老罗的文章Android应用程序资源的编译和打包过程分析(在里面搜索public.xml)这里面有详细的说明

    同上并结合我们的场景,第一次全量打包成功以后把app/build/intermediates/symbols/debug/R.txt缓存下来,补丁打包在执行processResources任务前,根据缓存的符号表R.txt去生成public.xml和ids.xml然后放进app/build/intermediates/res/merged/debug/values目录里,这样相同名字的id前后的两次构建值就能保持一致了,代码如下FastdexResourceIdTask.groovy

    public class FastdexResourceIdTask extends DefaultTask {
        static final String RESOURCE_PUBLIC_XML = "public.xml"
        static final String RESOURCE_IDX_XML = "idx.xml"
    
        String resDir
        String variantName
    
        @TaskAction
        def applyResourceId() {
            File buildDir = FastdexUtils.getBuildDir(project,variantName)
            String resourceMappingFile = new File(buildDir,Constant.R_TXT)
            // Parse the public.xml and ids.xml
            if (!FileUtils.isLegalFile(resourceMappingFile)) {
                project.logger.error("==fastdex apply resource mapping file ${resourceMappingFile} is illegal, just ignore")
                return
            }
            File idsXmlFile = new File(buildDir,RESOURCE_IDX_XML)
            File publicXmlFile = new File(buildDir,RESOURCE_PUBLIC_XML)
            if (FileUtils.isLegalFile(idsXmlFile) && FileUtils.isLegalFile(publicXmlFile)) {
                project.logger.error("==fastdex public xml file and ids xml file already exist, just ignore")
                return
            }
            String idsXml = resDir + "/values/ids.xml";
            String publicXml = resDir + "/values/public.xml";
            FileUtils.deleteFile(idsXml);
            FileUtils.deleteFile(publicXml);
            List<String> resourceDirectoryList = new ArrayList<String>()
            resourceDirectoryList.add(resDir)
    
            project.logger.error("==fastdex we build ${project.getName()} apk with apply resource mapping file ${resourceMappingFile}")
            Map<RDotTxtEntry.RType, Set<RDotTxtEntry>> rTypeResourceMap = PatchUtil.readRTxt(resourceMappingFile)
    
            AaptResourceCollector aaptResourceCollector = AaptUtil.collectResource(resourceDirectoryList, rTypeResourceMap)
            PatchUtil.generatePublicResourceXml(aaptResourceCollector, idsXml, publicXml)
            File publicFile = new File(publicXml)
    
            if (publicFile.exists()) {
                FileUtils.copyFileUsingStream(publicFile, publicXmlFile)
                project.logger.error("==fastdex gen resource public.xml in ${RESOURCE_PUBLIC_XML}")
            }
            File idxFile = new File(idsXml)
            if (idxFile.exists()) {
                FileUtils.copyFileUsingStream(idxFile, idsXmlFile)
                project.logger.error("==fastdex gen resource idx.xml in ${RESOURCE_IDX_XML}")
            }
        }
    }
    
    project.afterEvaluate {
        android.applicationVariants.all { variant ->
            def variantOutput = variant.outputs.first()
            def variantName = variant.name.capitalize()
    
            //保持补丁打包时R文件中相同的节点和第一次打包时的值保持一致
            FastdexResourceIdTask applyResourceTask = project.tasks.create("fastdexProcess${variantName}ResourceId", com.dx168.fastdex.build.task.FastdexResourceIdTask)
            applyResourceTask.resDir = variantOutput.processResources.resDir
            applyResourceTask.variantName = variantName
            variantOutput.processResources.dependsOn applyResourceTask
        }
    }
    

    如果项目中的资源特别多,第一次补丁打包生成public.xml和ids.xml时会占用一些时间,最好做一次缓存,以后的补丁打包直接使用缓存的public.xml和ids.xml**

    ==============================
    解决了上面的原理性问题后,接下来继续做优化,上面有讲到* transformClassesWithMultidexlistForDebug*任务的作用,由于采用了隔离Application的做法,所有的项目代码都不在classes.dex中,这个用来分析那些项目中的类需要放在classes.dex的任务就没有意义了,直接禁掉它

    project.afterEvaluate {
        android.applicationVariants.all { variant ->
            def variantName = variant.name.capitalize()
    
            def multidexlistTask = null
            try {
                multidexlistTask = project.tasks.getByName("transformClassesWithMultidexlistFor${variantName}")
            } catch (Throwable e) {
                //没有开启multiDexEnabled的情况下,会报这个任务找不到的异常
            }
            if (multidexlistTask != null) {
                multidexlistTask.enabled = false
            }
        }
    }
    
    

    禁掉以后,执行./gradle assembleDebug,在构建过程中挂掉了

    :app:transformClassesWithMultidexlistForDebug SKIPPED
    :app:transformClassesWithDexForDebug
    Running dex in-process requires build tools 23.0.2.
    For faster builds update this project to use the latest build tools.
    UNEXPECTED TOP-LEVEL ERROR:
    java.io.FileNotFoundException: /Users/tong/Projects/fastdex/app/build/intermediates/multi-dex/debug/maindexlist.txt (No such file or directory)
          at java.io.FileInputStream.open0(Native Method)
          at java.io.FileInputStream.open(FileInputStream.java:195)
          at java.io.FileInputStream.<init>(FileInputStream.java:138)
          at java.io.FileInputStream.<init>(FileInputStream.java:93)
          at java.io.FileReader.<init>(FileReader.java:58)
          at com.android.dx.command.dexer.Main.readPathsFromFile(Main.java:436)
          at com.android.dx.command.dexer.Main.runMultiDex(Main.java:361)
          at com.android.dx.command.dexer.Main.run(Main.java:275)
          at com.android.dx.command.dexer.Main.main(Main.java:245)
          at com.android.dx.command.Main.main(Main.java:106)
    :app:transformClassesWithDexForDebug FAILED
    
    FAILURE: Build failed with an exception.
    ......
    BUILD FAILED
    
    

    从上面的日志的第一行发现transformClassesWithMultidexlistForDebug任务确实禁止掉了,后面跟着一个SKIPPED的输出,但是执行transformClassesWithDexForDebug任务时报app/build/intermediates/multi-dex/debug/maindexlist.txt (No such file or directory)
    ,原因是transformClassesWithDexForDebug任务会检查这个文件是否存在,既然这样就在执行transformClassesWithDexForDebug任务前创建一个空文件,看是否还会报错,代码如下

    public class FastdexCreateMaindexlistFileTask extends DefaultTask {
        def applicationVariant
    
        @TaskAction
        void createFile() {
            if (applicationVariant != null) {
                File maindexlistFile = applicationVariant.getVariantData().getScope().getMainDexListFile()
                File parentFile = maindexlistFile.getParentFile()
                if (!parentFile.exists()) {
                    parentFile.mkdirs()
                }
    
                if (!maindexlistFile.exists() || maindexlistFile.isDirectory()) {
                    maindexlistFile.createNewFile()
                }
            }
        }
    }
    
    project.afterEvaluate {
        android.applicationVariants.all { variant ->
            def variantName = variant.name.capitalize()
    
            def multidexlistTask = null
            try {
                multidexlistTask = project.tasks.getByName("transformClassesWithMultidexlistFor${variantName}")
            } catch (Throwable e) {
                //没有开启multiDexEnabled的情况下,会报这个任务找不到的异常
            }
            if (multidexlistTask != null) {
                FastdexCreateMaindexlistFileTask createFileTask = project.tasks.create("fastdexCreate${variantName}MaindexlistFileTask", FastdexCreateMaindexlistFileTask)
                createFileTask.applicationVariant = variant
    
                multidexlistTask.dependsOn createFileTask
                multidexlistTask.enabled = false
            }
        }
    }
    

    再次执行./gradle assembleDebug

    :app:transformClassesWithJarMergingForDebug UP-TO-DATE
    :app:collectDebugMultiDexComponents UP-TO-DATE
    :app:fastdexCreateDebugMaindexlistFileTask
    :app:transformClassesWithMultidexlistForDebug SKIPPED
    :app:transformClassesWithDexForDebug UP-TO-DATE
    :app:mergeDebugJniLibFolders UP-TO-DATE
    :app:transformNative_libsWithMergeJniLibsForDebug UP-TO-DATE
    :app:processDebugJavaRes UP-TO-DATE
    :app:transformResourcesWithMergeJavaResForDebug UP-TO-DATE
    :app:validateConfigSigning
    :app:packageDebug UP-TO-DATE
    :app:zipalignDebug UP-TO-DATE
    :app:assembleDebug UP-TO-DATE
    
    BUILD SUCCESSFUL
    
    Total time: 16.201 secs
    
    

    这次构建成功说明创建空文件的这种方式可行

    =========

    我们公司的项目在使用的过程中,发现补丁打包时虽然只改了一个java类,但构建时执行compileDebugJavaWithJavac任务还是花了13秒

    BUILD SUCCESSFUL
    
    Total time: 28.222 secs
    Task spend time:
        554ms  :app:processDebugManifest
        127ms  :app:mergeDebugResources
       3266ms  :app:processDebugResources
      13621ms  :app:compileDebugJavaWithJavac
       3654ms  :app:transformClassesWithJarMergingForDebug
       1354ms  :app:transformClassesWithDexForDebug
        315ms  :app:transformNative_libsWithMergeJniLibsForDebug
        220ms  :app:transformResourcesWithMergeJavaResForDebug
       2684ms  :app:packageDebug
    

    经过分析由于我们使用了butterknife和tinker,这两个里面都用到了javax.annotation.processing.AbstractProcessor这个接口做代码动态生成,所以项目中的java文件如果很多,挨个扫描所有的java文件并且做操作会造成大量的时间浪费,其实他们每次生成的代码几乎都是一样的,因此如果补丁打包时能把这个任务换成自己的实现,仅编译和快照对比变化的java文件,并把结果输出到app/build/intermediates/classes/debug,覆盖原来的class,能大大提高效率,部分代码如下,详情看FastdexCustomJavacTask.groovy

    public class FastdexCustomJavacTask extends DefaultTask {
        ......
    
        @TaskAction
        void compile() {
            ......
            File androidJar = new File("${project.android.getSdkDirectory()}/platforms/${project.android.getCompileSdkVersion()}/android.jar")
            File classpathJar = FastdexUtils.getInjectedJarFile(project,variantName)
            project.logger.error("==fastdex androidJar: ${androidJar}")
            project.logger.error("==fastdex classpath: ${classpathJar}")
            project.ant.javac(
                    srcdir: patchJavaFileDir,
                    source: '1.7',
                    target: '1.7',
                    encoding: 'UTF-8',
                    destdir: patchClassesFileDir,
                    bootclasspath: androidJar,
                    classpath: classpathJar
            )
            compileTask.enabled = false
            File classesDir = applicationVariant.getVariantData().getScope().getJavaOutputDir()
            Files.walkFileTree(patchClassesFileDir.toPath(),new SimpleFileVisitor<Path>(){
                @Override
                FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                    Path relativePath = patchClassesFileDir.toPath().relativize(file)
                    File destFile = new File(classesDir,relativePath.toString())
                    FileUtils.copyFileUsingStream(file.toFile(),destFile)
                    return FileVisitResult.CONTINUE
                }
            })
        }
    }
    project.afterEvaluate {
        android.applicationVariants.all { variant ->
            def variantName = variant.name.capitalize()
            Task compileTask = project.tasks.getByName("compile${variantName}JavaWithJavac")
            Task customJavacTask = project.tasks.create("fastdexCustomCompile${variantName}JavaWithJavac", com.dx168.fastdex.build.task.FastdexCustomJavacTask)
            customJavacTask.applicationVariant = variant
            customJavacTask.variantName = variantName
            customJavacTask.compileTask = compileTask
            compileTask.dependsOn customJavacTask
        }
    }
    
    

    执行./gradlew assembleDebug ,再来一次

    BUILD SUCCESSFUL
    
    Total time: 17.555 secs
    Task spend time:
       1142ms  :app:fastdexCustomCompileDebugJavaWithJavac
         59ms  :app:generateDebugBuildConfig
        825ms  :app:processDebugManifest
        196ms  :app:mergeDebugResources
       3540ms  :app:processDebugResources
       3045ms  :app:transformClassesWithJarMergingForDebug
       1505ms  :app:transformClassesWithDexForDebug
        391ms  :app:transformNative_libsWithMergeJniLibsForDebug
        253ms  :app:transformResourcesWithMergeJavaResForDebug
       3413ms  :app:packageDebug
    

    一下子快了10秒左右,good

    =========
    既然有缓存,就有缓存过期的问题,假如我们添加了某个第三方库的依赖(依赖关系发生变化),并且在项目代码中引用了它,如果不清除缓存打出来的包运行起来后肯定会包类找不到,所以需要处理这个事情。
    首先怎么拿到依赖关系呢?通过以下代码可以获取一个依赖列表

    project.afterEvaluate {
        project.configurations.all.findAll { !it.allDependencies.empty }.each { c ->
            if (c.name.toString().equals("compile")
                    || c.name.toString().equals("apt")
                    || c.name.toString().equals("_debugCompile".toString())) {
                c.allDependencies.each { dep ->
                    String depStr =  "$dep.group:$dep.name:$dep.version"
                    println("${depStr}")
                }
            }
        }
    }
    

    输入如下

    com.dialonce:dialonce-android:2.3.1
    com.facebook.fresco:fresco:1.1.0
    com.google.guava:guava:18.0
    ......
    com.android.support:design:23.4.0
    com.bigkoo:alertview:1.0.2
    com.bigkoo:pickerview:2.0.8
    

    可以在第一次全量打包时,和生成项目源码目录快照的同一个时间点,获取一份当前的依赖列表并保存下来,当补丁打包时在获取一份当前的依赖列表,与之前保存的作对比,如果发生变化就把缓存清除掉

    另外最好提供一个主动清除缓存的任务

    public class FastdexCleanTask extends DefaultTask {
        String variantName
    
        @TaskAction
        void clean() {
            if (variantName == null) {
                FastdexUtils.cleanAllCache()
            }
            else {
                FastdexUtils.cleanCache(project,variantName)
            }
        }
    }
    
    

    先来一个清除所有缓存的任务

    project.tasks.create("fastdexCleanAll", FastdexCleanTask)
    

    然后在根据buildType、flavor创建对应的清除任务

    android.applicationVariants.all { variant ->
        def variantName = variant.name.capitalize()
        //创建清理指定variantName缓存的任务(用户触发)
        FastdexCleanTask cleanTask = project.tasks.create("fastdexCleanFor${variantName}", FastdexCleanTask)
        cleanTask.variantName = variantName
    }
    

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

    后续的优化计划

    • 1、提高稳定性和容错性,这个是最关键的
    • 2、目前补丁打包的时候,是把没有变化的类从app/build/intermediates/transforms/jarMerging/debug/jars/1/1f/combined.jar中移除,如果能hook掉transformClassesWithJarMergingForDebug这个任务,仅把发生变化的class参与combined.jar的生成,能够在IO上省出很多的时间
    • 3、目前给项目源码目录做快照,使用的是文件copy的方式,如果能仅仅只把需要的信息写在文本文件里,能够在IO上省出一些时间
    • 4、目前还没有对libs目录中发生变化做监控,后续需要补上这一块
    • 5、apk的安装速度比较慢(尤其是ART下由于在安装时对应用做AOT编译,所以造成安装速度特别慢,具体请参考张邵文大神的文章Android N混合编译与对热补丁影响解析),通过socket把代码补丁和资源补丁发送给app,做到免安装

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

    这里对打包的流程做下总结

    打包流程

    全量打包时的流程:
    • 1、合并所有的class文件生成一个jar包
    • 2、扫描所有的项目代码并且在构造方法里添加对fastdex.runtime.antilazyload.AntilazyLoad类的依赖
      这样做的目的是为了解决class verify的问题,
      详情请看 安卓App热补丁动态修复技术介绍
    • 3、对项目代码做快照,为了以后补丁打包时对比那些java文件发生了变化
    • 4、对当前项目的所以依赖做快照,为了以后补丁打包时对比依赖是否发生了变化,如果变化需要清除缓存
    • 5、调用真正的transform生成dex
    • 6、缓存生成的dex,并且把fastdex-runtime.dex插入到dex列表中,假如生成了两个dex,classes.dex classes2.dex 需要做一下操作
      fastdex-runtime.dex => classes.dex
      classes.dex => classes2.dex
      classes2.dex => classes3.dex
      然后运行期在入口Application(fastdex.runtime.FastdexApplication)使用MultiDex把所有的dex加载进来
    • @see fastdex.build.transform.FastdexDexTransform
    • 7、保存资源映射表,为了保持id的值一致,详情看
    • @see fastdex.build.task.FastdexResourceIdTask
    补丁打包时的流程
    • 1、检查缓存的有效性
    • @see fastdex.build.variant.FastdexVariant 的prepareEnv方法说明
    • 2、扫描所有变化的java文件并编译成class
    • @see fastdex.build.task.FastdexCustomJavacTask
    • 3、合并所有变化的class并生成jar包
    • 4、生成补丁dex
    • 5、把所有的dex按照一定规律放在transformClassesWithMultidexlistFor${variantName}任务的输出目录
      fastdex-runtime.dex => classes.dex
      patch.dex => classes2.dex
      dex_cache.classes.dex => classes3.dex
      dex_cache.classes2.dex => classes4.dex
      dex_cache.classesN.dex => classes(N + 2).dex

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

    整个项目的代码目前已经开源了 https://github.com/typ0520/fastdex

    如果你喜欢本文就来给我们star吧

    =============
    加快apk的构建速度,如何把编译时间从130秒降到17秒
    加快apk的构建速度,如何把编译时间从130秒降到17秒(二)

    参考的项目与文章

    Instant Run

    Tinker

    安卓App热补丁动态修复技术介绍

    Android应用程序资源的编译和打包过程分析

    关键字:
    加快apk编译速度
    加快app编译速度
    加快android编译速度
    加快android studio 编译速度
    android 加快编译速度
    android studio编译慢
    android studio编译速度优化
    android studio gradle 编译慢

    相关文章

      网友评论

      • 海边的卡夫卡Fu:请问这个项目现在还在维护么
      • var_rain:既来之则安之,为什么谷歌不这么去操作?引人深思
      • 84a057d17c6d:楼主,想请问下:mergeDebugResources等这些任务具体是干啥的 大神是从哪知道的
      • 我的橘子分你一瓣:大佬,发了封邮件,有空看看
      • xiaodouyaer:赶紧登录账号赞一个,牛人就是通过各种源码分析解决问题,牛
      • 3Zero:大大大神。。
      • GoogleCrypto:请问这个是什么错?
        A problem occurred configuring project ':app'.
        > Failed to notify project evaluation listener.
        > Could not resolve all dependencies for configuration ':app:_debugApk'.
        > A problem occurred configuring project ':sliding_menu'.
        > Could not resolve all dependencies for configuration ':sliding_menu:classpath'.
        > Could not find com.android.tools.build:gradle:2.3.3.
        Searched in the following locations:
        https://repo1.maven.org/maven2/com/android/tools/build/gradle/2.3.3/gradle-2.3.3.pom
        https://repo1.maven.org/maven2/com/android/tools/build/gradle/2.3.3/gradle-2.3.3.jar
        Required by:
        project :sliding_menu
        > Could not resolve all dependencies for configuration ':app:apt'.
        > Project :app declares a dependency from configuration 'compile' to configuration 'default' which is not declared in the descriptor for project :sliding_menu.

        * Try:
        Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.
      • 53d0f98260b6:这不就是又做了遍tinker么,如果是dex这步时间长,可以通过拆分module的方法提速,效果很明显,跟你这个差不多,而且简单很多,这个微信团队也有文章介绍,可以看看。
        typ0520:用了tinker的一些技术,做的事情是不一样的;fastdex除了优化dex的生成还在其它的节点做了处理,比如免安装,把生成的dex补丁和资源补丁直接通过socket传到正在运行的app里这样就省去安装的开销了,另外在android gradle2.x版本上经过hook在所有的buildtype以及忽略minSdkVersion去触发dex缓存; 微信团队的组件化方案其实和这个是不冲突的
      • 路人葵:登录只为点赞
        typ0520:@路人葵 谢谢支持:smile:
      • SMSM:大牛,代码是增量加载的,资源也是增量加载的么?
        typ0520:资源这块目前和instant-run一样,是推送全量的资源包,这块有时间的时候会处理(aapt增量编译、推送增量的资源包)
      • RetroX:有没有测试过多模块过程的构建加速效果
      • RetroX:哈哈哈看到了Freeline代码的影子 这个方案很棒
        typ0520:@RetroX 是的,freeline与gradle交互成本比较高,只能靠插件写文件python去读取,不过这样也有比较大的优势避开了gradle冗余的任务,gradle启动的时候创建以及初始化project还是挺慢的,尤其是library工程比较多的时候,我觉得fastdex最大的优势是与assemble是兼容的,免安装和走assemble都是共用缓存的,由于是base在gradle基础上再交互上几乎是0成本的;另外一点是能更好的使用android gradle新版本的特性,比如2.3的build-cache,fastdex0.6版本在2.3上能在所有的build type,minsdkversion稳定的触发dex缓存,总的来说我觉得freeline适合大型的项目,fastdex适合中性项目
        RetroX:@typ0520 Freeline的增量逻辑更加激进 但是目前也有python层无法复用gradle相关代码的问题 还有其他零零碎碎的兼容问题
        typ0520:@RetroX fastdex插件就是拿freeline插件代码改的,也参考了一部分gradle插件的代码
      • MISSGY_:大神我怎么觉得谷歌官方的Instant Run还是挺快的呢,修改的地方不多还是半分钟就能跑起来的。
        MISSGY_:@typ0520 对。没分包。项目不算特别大
        typ0520:你的这个项目是只有一个dex吧
      • f075283aac73:写的真好!
      • 81c17c76d562:希望博主有时间修改一下文章,里面讲的有些和开源的代码有区别,如果能再讲讲解决Github 相关 Issue 的经验就更好了。
        typ0520:@Mr_Wrong丶 第二篇里有解决issue相关的说明http://www.jianshu.com/p/6785ddb4383f
        typ0520:@Mr_Wrong丶 很好的建议,下一篇中把之前解决的部分issue讲一下
      • GitViolet:有bug,项目自定义application被插件替换了,导致APP报错
      • emial:大神
      • 完美落地:请问下BuildTimeListener 怎么添加到build.gradle文件里?
        完美落地:@typ0520 直接加进去报错的。之前你给了一个链接,能再提供下吗
        typ0520:@完美落地 直接把代码复制进去啊,当成java文件弄
      • c29d593fdbc6:相当于自己实现了一套 Instant Run?
        3925e0b0b726:那跟install run比有什么不一样吗
        typ0520:@梁山boy 是的
      • 7650769b4b02: > 找出变化的java文件进而得到那些class文件发生变化,然后把app/build/intermediates/transforms/jarMerging/debug/jars/1/1f/combined.jar中没有变化的class移除掉,仅把变化class送去生成dex

        为什么还要把combined.jar中没有变化的class移除? 既然已经得到哪些是变化的class,为什么不直接拿这些class去生成dex?
        typ0520:@70kg 你用命令行调用gradle这样可以打印出来
        7650769b4b02:hi ,我在build-lib中使用System.out.println但是并不会在Gradle console中输出,请问如何在build-lib中输出打印信息呢?谢谢
        typ0520:@70kg 目前已经是这种做法了,之前没有hook jarmerge任务,到生成dex的步骤时已经是完整的jar包了
      • chuwe1:这是真大神啊
        typ0520:@chuwe1 大神不敢当,只是平时经常看开源项目源码,自己尝试倒腾一些东西
      • 大大大大大大的大大:加依赖后 run app怎么跑不起来
        大大大大大大的大大:@typ0520 我是mac
        typ0520:你用的是windows吗,之前windows上跑有问题,昨天开了个虚拟机把windows上跑出的问题全修掉了,试试这个版本0.0.3-beta4
        typ0520:如果有错误把堆栈信息贴到issue里面,执行./gradlew assembleDebug --stacktrace,https://github.com/typ0520/fastdex/issues
      • 158d5844be13:编译报错
        Error:Error converting bytecode to dex:
        Cause: PARSE ERROR:
        class name (com/baidu/platform/comapi/map/a) does not match path (com/baidu/platform/comapi/map/A.class)
        ...while parsing com/baidu/platform/comapi/map/A.class

        Error:Execution failed for task ':app:transformClassesWithDexForDebug'.
        > com.android.build.api.transform.TransformException: com.android.ide.common.process.ProcessException: java.util.concurrent.ExecutionException: com.android.ide.common.process.ProcessException: Error while executing java process with main class com.android.dx.command.Main with arguments {--dex --num-threads=4 --multi-dex --main-dex-list

        项目中用了百度sdk, 不用fastdex插件是正常的.
        typ0520:已经在0.0.3-beta4上修掉了,详情看https://github.com/typ0520/fastdex/issues/6
        158d5844be13:@typ0520 好的 谢谢
        typ0520:github上已经有人提了这个问题的issue,https://github.com/typ0520/fastdex/issues/6,如果修复了会在上面说的
      • 大空ts翼:Error:Execution failed for task ':client:transformClassesWithDexForDevelopDebug'.
        > D:\android_projects\kfx\client\build\fastdex\DevelopDebug\patch-classes does not exist.
        第二次编译的时候报这个错误,是补丁没生成吗
        typ0520:@大空ts翼 这个错误是自定义的编译任务执行过程中出的错误,能把详细的堆栈信息在github上提一个issue吗,./gradlew assembleDebug --stacktrace
      • 3916db3b5d83:大牛!!
      • 开发者头条_程序员必装的App:感谢分享!已推荐到《开发者头条》:https://toutiao.io/posts/lsnfoc 欢迎点赞支持!
        欢迎订阅《345933的独家号》https://toutiao.io/subjects/222197
        typ0520:@开发者头条_程序员必装的App 多谢推荐:stuck_out_tongue_winking_eye:
      • 06785e590422:大牛
      • 李亦然:厉害。研究下
        typ0520:@李亦然 谢谢,可以跑demo试试看
      • 糖醋大排:牛.......

      本文标题:加快apk的构建速度,如何把编译时间从130秒降到17秒

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