既是没有好的方案就融洽尝尝做,经过一段时间的优化品质和平稳都有很大的加强

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

图片 1

商厦的档次代码比较多,每一趟调试改动java文件后要将近2分钟才能跑起来,实在受持续。在网上找了一大堆配置参数也从不很精通的效果,
尝试使用instant
run效果也不怎么着,然后又尝试运用freeline编译速度还足以只是不安定,每回战败后全量编译很费用时间,既然没有好的方案就融洽尝试做。

fastdex.png

花色地址:
https://github.com/typ0520/fastdex

正文已授权微信公众号:鸿洋(hongyangAndroid)原创头阵

注: 本文对gradle task做的认证都建立在关闭instant run的前提下

在上一篇小说加速apk的创设速度,怎样把编译时间从130秒降到17秒中讲了优化的思路与初步的贯彻,经过一段时间的优化质量和平稳都有很大的滋长,那里要多谢大家提的提出以及github上的issue,那篇小说就把重点优化的点和新效率以及填的坑介绍下。

注: 本文全部的代码、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倚重的奥迪Q5.java,输出到app/build/generated/source/r/debug目录
  • 二 、生成能源索引文件app/build/intermediates/res/resources-debug.ap_
  • 三 、把符号表输出到app/build/intermediates/symbols/debug/奇骏.txt

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

  • 一 、项目源码目录,默许路径是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文件和分析类之间的依赖性关系,总括出那多少个类必须放在第2个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

何以得到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

怎么样做快照与相比快照并拿到变化的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文件的长度和上次修改时间多个要素比较可以摸清同2个文本是或不是发生变化,通过快照目录没有某些文件而当前目录有有些文件可以得知伸张了文件,通过快照目录有有些文件不过当前目录没有得以识破删除文件(为了功能可以不处理删除,仅造成缓存里有几许用不到的类而已)
举个例证来说假设项目源码的路径为/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输出,比如拿景逸SUV文件做下编译,它的编译输出如下

➜  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必须是1个单独的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全部动态插入代码(第2方库由于不在大家的修复范围内为此为了功能忽略掉),具体的做法是往全体的构造方法中添加对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,大家只需求在那些职分履行完将来做处理,创制3个贯彻类为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的生成就可以了

图片 2

flow.png

其它以前全量打包做asm
code注入的时候是遍历combined.jar倘使entry对应的是连串代码就做注入,反之认为是第二方库跳过注入(第贰方库不在修复之列,为了节约注入开销的时间之所以忽略);以往拦截了jarmerge职务,直接扫描全数的DirectoryInput对应目录下的享有class做注入就行了,功用会比以前的做法有很大升级

付出完以上功能后做上面的四回打包做时间相比较(其实只做一次并不是太标准,做几十四次测试取时间的平均值那样才最准)
  • 一 、删除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
    
  • 二 、删除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
    
  • 三 、在开启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时往里面增添2个控件

<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();
    }
}

�在编译未来昂科拉.id.tv1怎么就变成数字2131493007了吧,原因是java编译器做了三个属性优化,如若发现源文件引用的是2个富含final描述符的常量,会直接做值copy

反编译最后一回编译成功时的LX570.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() {
        }
    }
}

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

自家的首先个想法是如若在实施完processDebugResources任务后,把路虎极光文件里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时,发现它们也遇到了相同的标题,并有了3个缓解方案,大家的场景和tinker场景在那么些题材上是一模一样的,直接照抄代码就缓解了这么些难题,紧要的事体说五次,感激tinker、谢谢tinker、谢谢tinker!!

tinker的消除方案是,打补丁时依照用户配置的resourceMapping文件(每趟打造成功后输出的app/build/intermediates/symbols/debug/PAJERO.txt),生成public.xml和ids.xml然后放进app/build/intermediates/res/merged/debug/values目录里,aapt在拍卖的时候会依据文件里的布局规则去变通,具体那块的原理请看锤子科技董事长锤子科技(science and technology)创办者罗永浩的小说Android应用程序能源的编译和包装进度分析(在里头搜索public.xml)这些中有详尽的声明

同上并组成大家的景观,第2遍全量打包成功将来把app/build/intermediates/symbols/debug/Sportage.txt缓存下来,补丁打包在实践processResources任务前,依据缓存的号子表Highlander.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义务真正禁止掉了,前边跟着3个SKIPPED的输出,不过实施transformClassesWithDexForDebug义务时报app/build/intermediates/multi-dex/debug/maindexlist.txt
(No such file or directory)
,原因是transformClassesWithDexForDebug义务会检查这几个文件是或不是存在,既然那样就在实施transformClassesWithDexForDebug义务前创办3个空文件,看是不是还会报错,代码如下

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

可以在第⑥遍全量打包时,和扭转项目源码目录快照的同二个时间点,获取一份当前的借助列表并保留下来,当补丁打包时在取得一份当前的依靠列表,与从前封存的作对照,如若暴发变化就把缓存清除掉

除此以外最好提供1个积极消除缓存的任务

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
}

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

贰 、对平昔注重的library工程做支撑

以上面这一个工程为例
https://github.com/typ0520/fastdex-test-project/tree/master/jarmerging-test

图片 3

project.png

那几个工程包括两个子工程

  • app (android application project)
  • aarlib (android library project)
  • javalib (java project)

app工程着重aarlib和javalib

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.jakewharton:butterknife:8.0.1'
    apt 'com.jakewharton:butterknife-compiler:8.0.1'
    compile project(':javalib')
    compile project(':aarlib')
    compile project(':libgroup:javalib2')
}

对此使用compile
project(‘:xxx’)那种措施倚重的工程,在apk的创设进程中是作为jar处理的,从拦截transformClassesWithJarMergingForDebug任务时的日记输出可以作证

===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

前边修改了library工程的代码补丁打包之所以没有奏效,就是因为补丁打包时只从DirectoryInput中抽离变化的class而尚未对library工程的输出jar做抽离,这几个时候就需求知道JarInput中那多少个属于library工程这么些属于第二方库。最直白的不二法门是经过文件系统路径区分,可是那样需求排除掉library工程中一贯放在libs目录下倚重的jar比如

==jarmerge jar: /Users/tong/Projects/fastdex-test-project/jarmerging-test/app/libs/exist-in-app-libs-2.1.2.jar

其次如若依靠的library目录和app工程不在同壹个索引下还要做容错的判定

图片 4

libgroup.png

==jarmerge jar: /Users/tong/Projects/fastdex-test-project/jarmerging-test/libgroup/javalib2/build/libs/javalib2.jar

终极舍弃了判断路径的方式,转而去找android
gradle的api得到各种library工程的输出jar路径,翻阅了源码发现2.0.02.2.02.3.0相应的api都不平等,通过判断版本的法门得以缓解,代码如下

public class LibDependency {
    public final File jarFile;
    public final Project dependencyProject;
    public final boolean androidLibrary;

    LibDependency(File jarFile, Project dependencyProject, boolean androidLibrary) {
        this.jarFile = jarFile
        this.dependencyProject = dependencyProject
        this.androidLibrary = androidLibrary
    }

    boolean equals(o) {
        if (this.is(o)) return true
        if (getClass() != o.class) return false

        LibDependency that = (LibDependency) o

        if (jarFile != that.jarFile) return false

        return true
    }

    int hashCode() {
        return (jarFile != null ? jarFile.hashCode() : 0)
    }

    @Override
    public String toString() {
        return "LibDependency{" +
                "jarFile=" + jarFile +
                ", dependencyProject=" + dependencyProject +
                ", androidLibrary=" + androidLibrary +
                '}';
    }

    private static Project getProjectByPath(Collection<Project> allprojects, String path) {
        return allprojects.find { it.path.equals(path) }
    }

    /**
     * 扫描依赖(<= 2.3.0)
     * @param library
     * @param libraryDependencies
     */
    private static final void scanDependency(com.android.builder.model.Library library,Set<com.android.builder.model.Library> libraryDependencies) {
        if (library == null) {
            return
        }
        if (library.getProject() == null) {
            return
        }
        if (libraryDependencies.contains(library)) {
            return
        }

        libraryDependencies.add(library)

        if (library instanceof com.android.builder.model.AndroidLibrary) {
            List<com.android.builder.model.Library> libraryList = library.getJavaDependencies()
            if (libraryList != null) {
                for (com.android.builder.model.Library item : libraryList) {
                    scanDependency(item,libraryDependencies)
                }
            }

            libraryList = library.getLibraryDependencies()
            if (libraryList != null) {
                for (com.android.builder.model.Library item : libraryList) {
                    scanDependency(item,libraryDependencies)
                }
            }
        }
        else if (library instanceof com.android.builder.model.JavaLibrary) {
            List<com.android.builder.model.Library> libraryList = library.getDependencies()

            if (libraryList != null) {
                for (com.android.builder.model.Library item : libraryList) {
                    scanDependency(item,libraryDependencies)
                }
            }
        }
    }

    /**
     * 扫描依赖(2.0.0 <= android-build-version <= 2.2.0)
     * @param library
     * @param libraryDependencies
     */
    private static final void scanDependency_2_0_0(Object library,Set<com.android.builder.model.Library> libraryDependencies) {
        if (library == null) {
            return
        }

        if (library.getProject() == null){
            return
        }
        if (libraryDependencies.contains(library)) {
            return
        }

        libraryDependencies.add(library)

        if (library instanceof com.android.builder.model.AndroidLibrary) {
            List<com.android.builder.model.Library> libraryList = library.getLibraryDependencies()
            if (libraryList != null) {
                for (com.android.builder.model.Library item : libraryList) {
                    scanDependency_2_0_0(item,libraryDependencies)
                }
            }
        }
    }

    /**
     * 解析项目的工程依赖  compile project('xxx')
     * @param project
     * @return
     */
    public static final Set<LibDependency> resolveProjectDependency(Project project, ApplicationVariant apkVariant) {
        Set<LibDependency> libraryDependencySet = new HashSet<>()
        VariantDependencies variantDeps = apkVariant.getVariantData().getVariantDependency();
        if (Version.ANDROID_GRADLE_PLUGIN_VERSION.compareTo("2.3.0") >= 0) {
            def allDependencies = new HashSet<>()
            allDependencies.addAll(variantDeps.getCompileDependencies().getAllJavaDependencies())
            allDependencies.addAll(variantDeps.getCompileDependencies().getAllAndroidDependencies())

            for (Object dependency : allDependencies) {
                if (dependency.projectPath != null) {
                    def dependencyProject = getProjectByPath(project.rootProject.allprojects,dependency.projectPath);
                    boolean androidLibrary = dependency.getClass().getName().equals("com.android.builder.dependency.level2.AndroidDependency");
                    File jarFile = null
                    if (androidLibrary) {
                        jarFile = dependency.getJarFile()
                    }
                    else {
                        jarFile = dependency.getArtifactFile()
                    }
                    LibDependency libraryDependency = new LibDependency(jarFile,dependencyProject,androidLibrary)
                    libraryDependencySet.add(libraryDependency)
                }
            }
        }
        else if (Version.ANDROID_GRADLE_PLUGIN_VERSION.compareTo("2.2.0") >= 0) {
            Set<Library> librarySet = new HashSet<>()
            for (Object jarLibrary : variantDeps.getCompileDependencies().getJarDependencies()) {
                scanDependency(jarLibrary,librarySet)
            }
            for (Object androidLibrary : variantDeps.getCompileDependencies().getAndroidDependencies()) {
                scanDependency(androidLibrary,librarySet)
            }

            for (com.android.builder.model.Library library : librarySet) {
                boolean isAndroidLibrary = (library instanceof AndroidLibrary);
                File jarFile = null
                def dependencyProject = getProjectByPath(project.rootProject.allprojects,library.getProject());
                if (isAndroidLibrary) {
                    com.android.builder.dependency.LibraryDependency androidLibrary = library;
                    jarFile = androidLibrary.getJarFile()
                }
                else {
                    jarFile = library.getJarFile();
                }
                LibDependency libraryDependency = new LibDependency(jarFile,dependencyProject,isAndroidLibrary)
                libraryDependencySet.add(libraryDependency)
            }
        }
        else {
            Set librarySet = new HashSet<>()
            for (Object jarLibrary : variantDeps.getJarDependencies()) {
                if (jarLibrary.getProjectPath() != null) {
                    librarySet.add(jarLibrary)
                }
                //scanDependency_2_0_0(jarLibrary,librarySet)
            }
            for (Object androidLibrary : variantDeps.getAndroidDependencies()) {
                scanDependency_2_0_0(androidLibrary,librarySet)
            }

            for (Object library : librarySet) {
                boolean isAndroidLibrary = (library instanceof AndroidLibrary);
                File jarFile = null
                def projectPath = (library instanceof com.android.builder.dependency.JarDependency) ? library.getProjectPath() : library.getProject()
                def dependencyProject = getProjectByPath(project.rootProject.allprojects,projectPath);
                if (isAndroidLibrary) {
                    com.android.builder.dependency.LibraryDependency androidLibrary = library;
                    jarFile = androidLibrary.getJarFile()
                }
                else {
                    jarFile = library.getJarFile();
                }
                LibDependency libraryDependency = new LibDependency(jarFile,dependencyProject,isAndroidLibrary)
                libraryDependencySet.add(libraryDependency)
            }
        }
        return libraryDependencySet
    }
}

把上边的这段代码,和上面的代码都放进build.gradle中

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

        if ("Debug".equals(variantName)) {
            LibDependency.resolveProjectDependency(project,variant).each {
                println("==androidLibrary: " + it.androidLibrary + " ,jarFile: " + it.jarFile)
            }
        }
    }
}

task resolveProjectDependency<< {

}

执行./gradlew resolveProjectDependency 能够收获以下输出

==androidLibrary: true ,jarFile: /Users/tong/Projects/fastdex-test-project/jarmerging-test/app/build/intermediates/exploded-aar/jarmerging-test/aarlib/unspecified/jars/classes.jar
==androidLibrary: false ,jarFile: /Users/tong/Projects/fastdex-test-project/jarmerging-test/javalib/build/libs/javalib.jar
==androidLibrary: false ,jarFile: /Users/tong/Projects/fastdex-test-project/jarmerging-test/libgroup/javalib2/build/libs/javalib2.jar

有了这几个途径大家就可以在遍历JarInput是进行匹配,只要在那么些路子列表中的都属于library工程的输出jar,用到那块有两处地点

public static void injectJarInputFiles(FastdexVariant fastdexVariant, HashSet<File> jarInputFiles) {
    def project = fastdexVariant.project
    long start = System.currentTimeMillis()

    Set<LibDependency> libraryDependencies = fastdexVariant.libraryDependencies
    List<File> projectJarFiles = new ArrayList<>()
    //获取所有依赖工程的输出jar (compile project(':xxx'))
    for (LibDependency dependency : libraryDependencies) {
        projectJarFiles.add(dependency.jarFile)
    }
    if (fastdexVariant.configuration.debug) {
        project.logger.error("==fastdex projectJarFiles : ${projectJarFiles}")
    }
    for (File file : jarInputFiles) {
        if (!projectJarFiles.contains(file)) {
            continue
        }
        project.logger.error("==fastdex ==inject jar: ${file}")
        ClassInject.injectJar(fastdexVariant,file,file)
    }
    long end = System.currentTimeMillis()
    project.logger.error("==fastdex inject complete jar-size: ${projectJarFiles.size()} , use: ${end - start}ms")
}

public static void generatePatchJar(FastdexVariant fastdexVariant, TransformInvocation transformInvocation, File patchJar) throws IOException {
    Set<LibDependency> libraryDependencies = fastdexVariant.libraryDependencies
    Map<String,String> jarAndProjectPathMap = new HashMap<>()
    List<File> projectJarFiles = new ArrayList<>()
    //获取所有依赖工程的输出jar (compile project(':xxx'))
    for (LibDependency dependency : libraryDependencies) {
        projectJarFiles.add(dependency.jarFile)
        jarAndProjectPathMap.put(dependency.jarFile.absolutePath,dependency.dependencyProject.projectDir.absolutePath)
    }

    //所有的class目录
    Set<File> directoryInputFiles = new HashSet<>();
    //所有输入的jar
    Set<File> jarInputFiles = new HashSet<>();
    for (TransformInput input : transformInvocation.getInputs()) {
        Collection<DirectoryInput> directoryInputs = input.getDirectoryInputs()
        if (directoryInputs != null) {
            for (DirectoryInput directoryInput : directoryInputs) {
                directoryInputFiles.add(directoryInput.getFile())
            }
        }

        if (!projectJarFiles.isEmpty()) {
            Collection<JarInput> jarInputs = input.getJarInputs()
            if (jarInputs != null) {
                for (JarInput jarInput : jarInputs) {
                    if (projectJarFiles.contains(jarInput.getFile())) {
                        jarInputFiles.add(jarInput.getFile())
                    }
                }
            }
        }
    }

    def project = fastdexVariant.project
    File tempDir = new File(fastdexVariant.buildDir,"temp")
    FileUtils.deleteDir(tempDir)
    FileUtils.ensumeDir(tempDir)

    Set<File> moudleDirectoryInputFiles = new HashSet<>()
    DiffResultSet diffResultSet = fastdexVariant.projectSnapshoot.diffResultSet
    for (File file : jarInputFiles) {
        String projectPath = jarAndProjectPathMap.get(file.absolutePath)
        List<String> patterns = diffResultSet.addOrModifiedClassesMap.get(projectPath)
        if (patterns != null && !patterns.isEmpty()) {
            File classesDir = new File(tempDir,"${file.name}-${System.currentTimeMillis()}")
            project.copy {
                from project.zipTree(file)
                for (String pattern : patterns) {
                    include pattern
                }
                into classesDir
            }
            moudleDirectoryInputFiles.add(classesDir)
            directoryInputFiles.add(classesDir)
        }
    }
    JarOperation.generatePatchJar(fastdexVariant,directoryInputFiles,moudleDirectoryInputFiles,patchJar);
}

此起彼伏的优化布署

  • ① 、进步稳定性和容错性,这么些是最器重的
  • ② 、近来补丁打包的时候,是把尚未变动的类从app/build/intermediates/transforms/jarMerging/debug/jars/1/1f/combined.jar中移除,要是能hook掉transformClassesWithJarMergingForDebug这几个职责,仅把发生变化的class加入combined.jar的浮动,可以在IO上省出过多的光阴
  • 叁 、近期给品种源码目录做快照,使用的是文件copy的方式,假如能只是只把须要的消息写在文件文件里,可以在IO上省出一些日子
  • ④ 、如今还从未对libs目录中产生变化做监控,后续须求补上这一块
  • 五 、apk的安装速度比较慢(越发是A凯雷德T下是因为在装置时对运用做AOT编译,所以导致安装速度尤其慢,具体请参考张邵文大神的篇章Android
    N混合编译与对热补丁影响解析
    ),通过socket把代码补丁和资源补丁发送给app,做到免安装

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

叁 、 全新的快照比较模块

fastdex方今亟待相比的地点有三处

  • 全量打包时对近年来凭借的库做快照,补丁打包时相比是或不是发生变化
  • 检测app工程和拥有正视的android
    library工程中拥有AndroidManifest.xml是上次打包相比较是不是发生变化(免安装模块要用到manifest文件暴发变化,必须要重复安装app)
  • 全量打包时对具备的java文件和kotlin文件做快照,补丁打包时比较那2个源文件爆发变化

以率先种意况为例,说下相比的原理,全量打包时生成二个文件文件把如今的依靠写进去以换行符分割

/Users/tong/Projects/fastdex/sample/app/libs/fm-sdk-2.1.2.jar
/Users/tong/Projects/fastdex/sample/javalib/build/libs/javalib.jar

补丁打包时先把那些文件文件读取到ArrayList中,然后把当下的依靠列表页放进ArrayList中
,通过以下操作可以收获新增项、删除项,只要发现有删除项和新增项就认为器重发生了变通

ArrayList<String> old = new ArrayList<>();
old.add("/Users/tong/Projects/fastdex/sample/app/libs/fm-sdk-2.1.2.jar");
old.add("/Users/tong/Projects/fastdex/sample/javalib/build/libs/javalib.jar");

ArrayList<String> now = new ArrayList<>();
now.add("/Users/tong/Projects/fastdex/sample/app/libs/fm-sdk-2.1.2.jar");
now.add("/Users/tong/Projects/fastdex/sample/javalib/build/libs/new.jar");

//获取删除项
Set<String> deletedNodes = new HashSet<>();
deletedNodes.addAll(old);
deletedNodes.removeAll(now);

//新增项
Set<String> increasedNodes = new HashSet<>();
increasedNodes.addAll(now);
//如果不用ArrayList套一层有时候会发生移除不掉的情况 why?
increasedNodes.removeAll(old);

//需要检测是否变化的列表
Set<String> needDiffNodes = new HashSet<>();
needDiffNodes.addAll(now);
needDiffNodes.addAll(old);
needDiffNodes.removeAll(deletedNodes);
needDiffNodes.removeAll(increasedNodes);

注: 文本的相比较不存在创新,但是文件相比较是存在这种场地的

拥有的快照比较都以基于上边那段代码的抽象,具体可以参照那里
https://github.com/typ0520/fastdex/tree/master/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot

此间对包裹的流水线做下总计

四、 dex merge

全量打包未来,依据正规的费用节奏爆发变化的源文件会越多,相应的涉企dex生成的class也会进一步多,那样会造成补丁打包速度更是慢。
解决那个题材相比不难的法门是把每一趟变更的patch.dex�放进全量打包时的dex缓存中(必须排在此前的dex后边),并且更新下源代码快照,那样做有多个坏处

  • 壹 、每一次补丁打包时都必须对class文件做注入,为了缓解上篇小说中涉及的pre-verify错误
  • 二 、每一遍补丁打包都急需缓存patch.dex,会造成上边这些目录的dex越多

app/build/intermediates/transforms/dex/debug/folders/1000/1f/main

缓解第四个难题的方案是把patch.dex中的class合并到缓存的dex中,那样就不须要保留全数的patch.dex了,3个比较为难的难题是只要缓存的dex的方法数已经有6553一个了,在往里面加新增的class,肯定会爆掉了,最后fastdex采用的方案是首先次触发dex
merge时直接把patch.dex扔进缓存(merged-patch.dex),以往在触发dex
merge时就拿patch.dex和merged-patch.dex做联合(那样做也存在潜在的难点,如若生成的class越多也有大概引致合并dex时出现65535的荒谬)

消除第多少个难点是加了二个可配置选项,默许是一个以上的源文件发生变化时触发merge,这样即决不每一趟都做代码注入和merge操作,也能在源文件变化多的时候復苏情况

那几个dex
merge工具是从freeline里找到的,感兴趣的话能够把下载下来试着调用下
https://github.com/typ0520/fastdex-test-project/tree/master/dex-merge

java -jar fastdex-dex-merge.jar output.dex patch.dex merged-patch.dex

图片 5

dex-merge.png

打包流程

⑤ 、帮忙注脚生成器

在近年来的Android开发中,申明越来越流行起来,比如ButterKnifeEventBus等等都接纳采取申明来布局。依据拍卖时期,注明又分为两系列型,一种是运作时申明,另一种是编译时申明,运行时评释由于品质难题被部分人所诟病。编译时注脚的主导爱慕APT(Annotation
Processing
Tools)完毕,原理是在有个别代码成分上(如类型、函数、字段等)添加注明,在编译时编译器会检讨AbstractProcessor的子类,并且调用该品种的process函数,然后将添加了诠释的具有因素都传送到process函数中,使得开发人员可以在编译期进行对应的处理,例如,依据表明生成新的Java类,这相当于ButterKnifeEventBus等开源库的基本原理。Java
API已经提供了围观源码并分析声明的框架,你可以一而再AbstractProcessor类来提供落成团结的剖析注明逻辑


引用自http://blog.csdn.net/industriously/article/details/53932425

虽说能增强运营期的频率但也给支付带来一些劳动

  • AbstractProcessor这么些类唯有在编译期才会用到,运行期是用不到的,然则倘使通过compile格局着重的包,会把那么些类都打包进dex中

    以这些项目为例(提出把代码拉下来,前面好多少个地方会用到)
    https://github.com/typ0520/fastdex-test-project/annotation-generators

    app中凭借了butterknife7.0.1

    dependencies {
      compile 'com.jakewharton:butterknife:7.0.1'
    }
    

    butterknife7.0.1中的注明生成器叫ButterKnifeProcessor

图片 6

butterknife.png

执行./gradlew app:assembleDebug

图片 7

app.png

从上图可以看到ButterKnifeProcessor.class被打包进dex中了

  • 为了防止上述的那种景况,可以透过annotationProcessor的不二法门引入,butterknife8.8.1把ButterKnifeProcessor相关的独自成了butterknife-compiler模块,butterknife模块只保留运转期必要拔取的代码

app2中凭借了butterknife8.8.1

apply plugin: 'com.jakewharton.butterknife'

dependencies {
  compile 'com.jakewharton:butterknife:8.8.1'
  annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'
}

执行./gradlew app2:assembleDebug

图片 8

app2.png

从上图能够见见butterknife.compiler包下全部的代码都不曾被打包进dex。纵然经过annotationProcessor倚重AbstractProcessor相关代码有上述好处,但是会招致增量编译不可用,不难地说就是平常的花色实施compileDebugJavaWithJavac义务调用javac的时候只会编译内容发生变化的java源文件,如若使用了annotationProcessor每一遍执行compileDebugJavaWithJavac职务都会把品种中拥有的java文件都参预编译,想象一下只要项目中有诸两个java文件编译起来那酸爽。我们得以做个测试,依然利用那几个连串
https://github.com/typ0520/fastdex-test-project/annotation-generators

annotation-generators包蕴两个子项目

  • app依赖7.0.1

    compile 'com.jakewharton:butterknife:7.0.1'
    
  • app2依赖8.8.1

    dependencies {
      compile 'com.jakewharton:butterknife:8.8.1'
      annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'
    }
    
  • app3不包蕴其余AbstractProcessor

那八个子工程都富含五个java文件
com/github/typ0520/annotation_generators/HAHA.java
com/github/typ0520/annotation_generators/MainActivity.java

测试的思绪是先反省MainActivity.class文件的更新时间,然后修改HAHA.java执行编译,最终在检讨MainActivity.class文件的立异时间是或不是和编译在此以前的均等,借使一致表达增量编译可用,反之不可用

通过increment_compile_test.sh本条shell脚本来做测试(使用windows的同校可以手动做测试V_V)

#!/bin/bash

sh gradlew assembleDebug

test_increment_compile() {
    echo "========测试${1}是否支持增量, ${2}"

    str=$(stat -x ${1}/build/intermediates/classes/debug/com/github/typ0520/annotation_generators/MainActivity.class | grep 'Modify')
    echo $str

    echo 'package com.github.typ0520.annotation_generators;' > ${1}/src/main/java/com/github/typ0520/annotation_generators/HAHA.java
    echo 'public class HAHA {' >> ${1}/src/main/java/com/github/typ0520/annotation_generators/HAHA.java
    echo "    public long millis = $(date +%s);" >> ${1}/src/main/java/com/github/typ0520/annotation_generators/HAHA.java
    echo '}' >> ${1}/src/main/java/com/github/typ0520/annotation_generators/HAHA.java

    sh gradlew ${1}:assembleDebug > /dev/null

    str2=$(stat -x ${1}/build/intermediates/classes/debug/com/github/typ0520/annotation_generators/MainActivity.class  | grep 'Modify')
    echo $str2

    echo ' '
    if [ "$str" == "$str2" ];then
        echo "${1}只修改HAHA.java,MainActivity.class没有发生变化"
    else
        echo "${1}只修改HAHA.java,MainActivity.class发生变化"
    fi
}

test_increment_compile app "compile 'com.jakewharton:butterknife:7.0.1'"
test_increment_compile app2 "annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'"
test_increment_compile app3 "没有用任何AbstractProcessor"

执行sh increment_compile_test.sh

图片 9

increment_compare.png

日记的出口能够表达下边所讲述的

既是原生不辅助那么大家就在自定义的java
compile职责
中来做这一个业务,通过事先的快照模块可以对照出那么些java源文件发出了扭转,那么就足以本身拼接javac命令参数然后调用仅编译变化的java文件

demo中写了1个编译职责方便我们明白那些参数都是怎么拼接的,代码太多了此地就不贴出来了
https://github.com/typ0520/fastdex-test-project/annotation-generators/app/build.gradle
https://github.com/typ0520/fastdex-test-project/annotation-generators/app2/build.gradle

可以调用./gradlew mycompile1 恐怕 ./gradlew
mycompile2看下最后拼接出来的授命

图片 10

mycompile1.png

fastdex中对应模块的代码在
https://github.com/typ0520/fastdex/blob/master/fastdex-gradle/src/main/groovy/fastdex/build/task/FastdexCustomJavacTask.groovy

全量打包时的流程:
  • 壹 、合并全体的class文件生成2个jar包
  • 二 、扫描全数的序列代码并且在构造方法里添加对fastdex.runtime.antilazyload.AntilazyLoad类的珍重
    如此做的目标是为着缓解class verify的题材,
    详情请看
    安卓App热补丁动态修复技术介绍
  • ③ 、对品种代码做快照,为了将来补丁打包时相比较那么些java文件发出了扭转
  • ④ 、对当下项目标之所以正视做快照,为了未来补丁打包时比较依赖是或不是爆发了变更,如若生成须要解决缓存
  • 五 、调用真正的transform生成dex
  • 六 、缓存生成的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.法斯特dexApplication)使用MultiDex把富有的dex加载进来
  • @see
    fastdex.build.transform.FastdexDexTransform
  • 柒 、保存能源映射表,为了保持id的值一致,详情看
  • @see
    fastdex.build.task.FastdexResourceIdTask

六 、填过的坑

化解的bug那块本来是不准备说的,因为这块最有价值的事物不是化解难点本身,而是怎么发现和复发难点的,那块确实不太好描述V_V,应简友的渴求如故挑了有的对立比较有滋养的难点说下,紧要如故说消除的法子,至于题目是怎么定位和再次出现的只好努力描述了。

补丁打包时的流水线
  • ① 、检查缓存的实用
  • @see
    fastdex.build.variant.FastdexVariant
    的prepareEnv方法求证
  • 贰 、扫描全数变化的java文件并编译成class
  • @see
    fastdex.build.task.FastdexCustomJavacTask
  • 三 、合并全体变更的class并生成jar包
  • 肆 、生成补丁dex
  • ⑤ 、把拥有的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

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

1、issues#2

https://github.com/typ0520/fastdex/issues/2
@hexi

导致这一个题材的原委是项目中原来的YtxApplication类被替换到了法斯特dexApplication,当在activity中推行类似于上面的操作时就会报ClassCastException

MyApplication app = (MyApplication) getApplication();

化解的不二法门是在instant-run的源码里找到的,运营期把android
api里具有引用Application的地点把实例替换掉

public static void monkeyPatchApplication( Context context,
                                           Application bootstrap,
                                           Application realApplication,
                                           String externalResourceFile) {

    try {
        // Find the ActivityThread instance for the current thread
        Class<?> activityThread = Class.forName("android.app.ActivityThread");
        Object currentActivityThread = getActivityThread(context, activityThread);

        // Find the mInitialApplication field of the ActivityThread to the real application
        Field mInitialApplication = activityThread.getDeclaredField("mInitialApplication");
        mInitialApplication.setAccessible(true);
        Application initialApplication = (Application) mInitialApplication.get(currentActivityThread);
        if (realApplication != null && initialApplication == bootstrap) {
            mInitialApplication.set(currentActivityThread, realApplication);
        }

        // Replace all instance of the stub application in ActivityThread#mAllApplications with the
        // real one
        if (realApplication != null) {
            Field mAllApplications = activityThread.getDeclaredField("mAllApplications");
            mAllApplications.setAccessible(true);
            List<Application> allApplications = (List<Application>) mAllApplications
                    .get(currentActivityThread);
            for (int i = 0; i < allApplications.size(); i++) {
                if (allApplications.get(i) == bootstrap) {
                    allApplications.set(i, realApplication);
                }
            }
        }

        // Figure out how loaded APKs are stored.

        // API version 8 has PackageInfo, 10 has LoadedApk. 9, I don't know.
        Class<?> loadedApkClass;
        try {
            loadedApkClass = Class.forName("android.app.LoadedApk");
        } catch (ClassNotFoundException e) {
            loadedApkClass = Class.forName("android.app.ActivityThread$PackageInfo");
        }
        Field mApplication = loadedApkClass.getDeclaredField("mApplication");
        mApplication.setAccessible(true);
        Field mResDir = loadedApkClass.getDeclaredField("mResDir");
        mResDir.setAccessible(true);
        Field mLoadedApk = null;
        try {
            mLoadedApk = Application.class.getDeclaredField("mLoadedApk");
        } catch (NoSuchFieldException e) {
            // According to testing, it's okay to ignore this.
        }
        for (String fieldName : new String[]{"mPackages", "mResourcePackages"}) {
            Field field = activityThread.getDeclaredField(fieldName);
            field.setAccessible(true);
            Object value = field.get(currentActivityThread);

            for (Map.Entry<String, WeakReference<?>> entry :
                    ((Map<String, WeakReference<?>>) value).entrySet()) {
                Object loadedApk = entry.getValue().get();
                if (loadedApk == null) {
                    continue;
                }

                if (mApplication.get(loadedApk) == bootstrap) {
                    if (realApplication != null) {
                        mApplication.set(loadedApk, realApplication);
                    }
                    if (externalResourceFile != null) {
                        mResDir.set(loadedApk, externalResourceFile);
                    }

                    if (realApplication != null && mLoadedApk != null) {
                        mLoadedApk.set(realApplication, loadedApk);
                    }
                }
            }
        }
    } catch (Throwable e) {
        throw new IllegalStateException(e);
    }
}

切切实实可以参见测试工程的代码
https://github.com/typ0520/fastdex-test-project/tree/master/replace\_application

全体项目标代码如今早就开源了 https://github.com/typ0520/fastdex

只要你欢腾本文就来给大家star吧

=============
加速apk的打造速度,怎么样把编译时间从130秒降到17秒
加速apk的打造速度,怎么样把编译时间从130秒降到17秒(二)

2、issues#6

https://github.com/typ0520/fastdex/issues/6
@YuJunKui1995

其一荒唐的显现是即使项目里富含baidumapapi_v2_0_0.jar,平常打包是没难点的,只要采纳fastdex就会报上边那么些错误

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

通过分析利用fastdex打包时会有解压jar然后在缩减的操作,使用上边那段代码做测试
https://github.com/typ0520/fastdex-test-project/tree/master/issue%236-desc

task gen_dex2<< {
    File tempDir = project.file('temp')
    tempDir.deleteDir()

    project.copy {
        from project.zipTree(project.file('baidumapapi_v2_0_0.jar'))
        into tempDir
    }

    File baidumapJar = project.file('temp/baidu.jar')
    project.ant.zip(baseDir: tempDir, destFile: baidumapJar)

    ProcessBuilder processBuilder = new ProcessBuilder('dx','--dex',"--output=" + project.file('baidu.dex').absolutePath, baidumapJar.absolutePath)
    def process = processBuilder.start()

    InputStream is = process.getInputStream()
    BufferedReader reader = new BufferedReader(new InputStreamReader(is))
    String line = null
    while ((line = reader.readLine()) != null) {
        println(line)
    }
    reader.close()

    int status = process.waitFor()

    reader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
    while ((line = reader.readLine()) != null) {
        System.out.println(line);
    }
    reader.close();

    try {
        process.destroy()
    } catch (Throwable e) {

    }
}

执行./gradlew gen_dex2

图片 11

dex-error.png

果然再现了那些题材,查了材料发现mac和windows一样文件系统大小写不灵敏,倘若jar包里有A.class,解压后有大概就变成a.class了,所以生成dex的时候会报不协作的错误(类似的难点也会潜移默化git,此前就发现改了3个文书名字的大小写git检测不到变化,当前卫未细想那几个难题,未来看来也是一样的题目)。知道难题是怎么发生的那么消除就大约了,既然在文件系统操作jar会有标题,那就置身内存做,对应java的api就是ZipOutputStream和ZipInputStream。

对于mac下文件系统大小写不灵活可以在极限履行下边那段命令,体会下输出

echo 'a' > a.txt;echo 'A' > A.txt;cat a.txt;cat A.txt

图片 12

echo_a_b.png

参考的品类与作品

Instant
Run

Tinker

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

Android应用程序财富的编译和打包进度分析

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

3、issues#8

https://github.com/typ0520/fastdex/issues/8
@dongzy

Error:Execution failed for task ':app:tinkerSupportProcess_360DebugManifest'.

java.io.FileNotFoundException: E:\newkp\kuaipiandroid\NewKp\app\src\main\java\com\dx168\fastdex\runtime\FastdexApplication.java (系统找不到指定的路径。)

出现这么些荒唐的原委是@dongzy的花色中行使了tinkerpatch的一键接入,tinkerpatch的gradle插件也有Application替换的法力,必须确保fastdexProcess{variantName}Manifest义务在终极执行才行

FastdexManifestTask manifestTask = project.tasks.create("fastdexProcess${variantName}Manifest", FastdexManifestTask)
manifestTask.fastdexVariant = fastdexVariant
manifestTask.mustRunAfter variantOutput.processManifest
variantOutput.processResources.dependsOn manifestTask

//fix issue#8
def tinkerPatchManifestTask = null
try {
    tinkerPatchManifestTask = project.tasks.getByName("tinkerpatchSupportProcess${variantName}Manifest")
} catch (Throwable e) {}

if (tinkerPatchManifestTask != null) {
    manifestTask.mustRunAfter tinkerPatchManifestTask
}

4、issues#xxoo

那段不是斩草除根难题的,
忍不住吐槽下那男人儿,觉得浪费了她的时光,上来就是“亲测无软用,提出大家不用用如何什么的”,搞的本身万分烦躁,果断用腾讯网上的一篇作品回应了千古
https://zhuanlan.zhihu.com/p/25768464
新兴透过联系发现那男人儿在一个好端端打包3秒的连串上做的测试,作者也是无语了

图片 13

。。。。。。

说其实的确实愿意大家对开源项目多或多或少器重,觉得对团结有赞助就用。若是认为不佳,可以挑选提提议,也得以选用默默离开,假诺有时间有力量可以涉足进去优化,化解自个儿干活儿难题的同时也服务了豪门。在这些快节奏的社会我们的时刻都难得,你以为测试一下浪费了时间就从头吐槽,有没有想到开源项目标撰稿人就义了大气的私家时间在解决二个1个难题、为了缓解新职能的技术点一个一个方案的做测试做相比较吧?

注:
尽管项目标dex生成小于10秒,提出并非采纳fastdex,大约是感知不到效用的。

gradle编译速度优化指出

  • 并非选取类似于com.android.tools.build:gradle:2.+的动态尊敬,不然老是运行编译都要求请求maven
    server比较当前是还是不是是新本子

  • 少直接采纳compile
    project(‘:xxx’)依赖library工程,倘诺module相比多编译起先的时候需求遍历module依照build.gradle配置项目,此外每一种library工程都包括大批量的任务各种职务都亟待相比较输入和输出,那几个小职务叠加到一块的年华消耗也是很可观的。
    提议把library工程打成aar包丢到集团的maven服务器上,别和自家说开发阶段library平常改直接着重方便,每一回修改打包到maven服务器上并未那么麻烦。大家公司的档次都以唯有1个根本的application工程,library代码全丢进了maven服务器,dex方法数在12w左右,使用fastdex修改了多少个java文件能平安在8秒左右落成打包、发送补丁和app重启

  • 此外情况都别在library工程里应用flavor

切切实实能够参照@如故范特稀西写的那篇小说
Android 优化APP
打造速度的17条提出

5、issues#17

https://github.com/typ0520/fastdex/issues/17
@junchenChow

[ant:javac] : warning: 'includeantruntime' was not set, defaulting to build.sysclasspath=last; set to false for repeatable builds
[ant:javac] /Users/zhoujunchen/as/xx/app/build/fastdex/DevelopDebug/custom-combind/com/xx/xx/xx/xx/CourseDetailActivity.java:229: 错误: -source 1.7 中不支持 lambda 表达式
[ant:javac] wrapperControlsView.postDelayed(() -> wrapperControlsView.initiativeRefresh(), 500L);
[ant:javac] ^
[ant:javac] (请使用 -source 8 或更高版本以启用 lambda 表达式)
[ant:javac] /Users/zhoujunchen/as/android-donguo/app/build/fastdex/DevelopDebug/custom-combind/com/xx/xx/xx/xx/CourseDetailActivity.java:489: 错误: -source 1.7 中不支持方法引用
[ant:javac] .subscribe(conf -> ShareHelper.share(this, conf), Throwable::printStackTrace);
[ant:javac] ^
[ant:javac] (请使用 -source 8 或更高版本以启用方法引用)
[ant:javac] 2 个错误
:app:fastdexCustomCompileDevelopDebugJavaWithJavac FAILED
有什么选项没开启么 不支持lambda?

那些错误的原因是以前自定义的编译义务写死了使用1.7去编译,查阅gradle-ret松坂庆子mbda的源码找到了这么些代码
https://github.com/evant/gradle-retrolambda

https://github.com/evant/gradle-retrolambda/blob/master/gradle-retrolambda/src/main/groovy/me/tatarka/RetrolambdaPluginAndroid.groovy

private static configureCompileJavaTask(Project project, BaseVariant variant, RetrolambdaTransform transform) {
    variant.javaCompile.doFirst {
        def retrolambda = project.extensions.getByType(RetrolambdaExtension)
        def rt = "$retrolambda.jdk/jre/lib/rt.jar"

        variant.javaCompile.classpath = variant.javaCompile.classpath + project.files(rt)
        ensureCompileOnJava8(retrolambda, variant.javaCompile)
    }

    transform.putVariant(variant)
}

 private static ensureCompileOnJava8(RetrolambdaExtension retrolambda, JavaCompile javaCompile) {
        javaCompile.sourceCompatibility = "1.8"
        javaCompile.targetCompatibility = "1.8"

        if (!retrolambda.onJava8) {
            // Set JDK 8 for the compiler task
            def javac = "${retrolambda.tryGetJdk()}/bin/javac"
            if (!checkIfExecutableExists(javac)) {
                throw new ProjectConfigurationException("Cannot find executable: $javac", null)
            }
            javaCompile.options.fork = true
            javaCompile.options.forkOptions.executable = javac
        }
    }

从那个代码中大家可以识破以下新闻

  • 亟需运用jdk1.8里的javac去编译
  • sourceCompatibility和targetCompatibility必须设置成1.8
  • classpath中要求加上1.8的rt.jar

有了那一个音信就能够在自定义的编译职务做处理了

if (project.plugins.hasPlugin("me.tatarka.retrolambda")) {
    def retrolambda = project.retrolambda
    def rt = "${retrolambda.jdk}${File.separator}jre${File.separator}lib${File.separator}rt.jar"
    classpath.add(rt)

    executable = "${retrolambda.tryGetJdk()}${File.separator}bin${File.separator}javac"

    if (Os.isFamily(Os.FAMILY_WINDOWS)) {
        executable = "${executable}.exe"
    }
}

List<String> cmdArgs = new ArrayList<>()
cmdArgs.add(executable)
cmdArgs.add("-encoding")
cmdArgs.add("UTF-8")
cmdArgs.add("-g")
cmdArgs.add("-target")
cmdArgs.add(javaCompile.targetCompatibility)
cmdArgs.add("-source")
cmdArgs.add(javaCompile.sourceCompatibility)
cmdArgs.add("-cp")
cmdArgs.add(joinClasspath(classpath))

切实可以参照
https://github.com/typ0520/fastdex/blob/master/fastdex-gradle/src/main/groovy/fastdex/build/task/FastdexCustomJavacTask.groovy

6、issues#24 #29 #35 #36

https://github.com/typ0520/fastdex/issues/36
@wsf5918
@ysnows
@jianglei199212
@tianshaokai
@Razhan

Caused by: java.lang.RuntimeException: ==fastdex jar input size is 117, expected is 1
at com.dx168.fastdex.build.transform.FastdexTransform.getCombinedJarFile(FastdexTransform.groovy:173)
at com.dx168.fastdex.build.transform.FastdexTransform$getCombinedJarFile.callCurrent(Unknown Source)
at com.dx168.fastdex.build.transform.FastdexTransform.transform(FastdexTransform.groovy:131)
at com.android.build.gradle.internal.pipeline.TransformTask$2.call(TransformTask.java:185)
at com.android.build.gradle.internal.pipeline.TransformTask$2.call(TransformTask.java:181)
at com.android.builder.profile.ThreadRecorder.record(ThreadRecorder.java:102)
at com.android.build.gradle.internal.pipeline.TransformTask.transform(TransformTask.java:176)
at org.gradle.internal.reflect.JavaMethod.invoke(JavaMethod.java:73)
at org.gradle.api.internal.project.taskfactory.DefaultTaskClassInfoStore$IncrementalTaskAction.doExecute(DefaultTaskClassInfoStore.java:163)
at org.gradle.api.internal.project.taskfactory.DefaultTaskClassInfoStore$StandardTaskAction.execute(DefaultTaskClassInfoStore.java:134)
at org.gradle.api.internal.project.taskfactory.DefaultTaskClassInfoStore$StandardTaskAction.execute(DefaultTaskClassInfoStore.java:123)
at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeAction(ExecuteActionsTaskExecuter.java:95)
at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeActions(ExecuteActionsTaskExecuter.java:76)
... 78 more

好端端状态下打开multidex并且minSdkVersion <
21时会存在transformClassesWithJarMergingForDebug义务,用来归并全数的JarInput和DirectoryInput并且输出到build/intermediates/transforms/jarMerging/debug/jars/1/1f/combined.jar,而以此张冠李戴的变现是不见了jarMerging任务,所以走到dexTransform时当然梦想只有1个combined.jar,可是由于没有统一所以jar
input的个数是117。当时出于直接不能再次出现这些难题,所以就接纳加标示的手段消除的,具体是当走到FastdexJarMergingTransform与此同时实施到位之后就把executedJarMerge设置为true,走到dexTransform时判断假诺翻开了multidex并且executedJarMerge==false就证实是遗失了jarMerge义务,这么些时候调用com.android.build.gradle.internal.transforms.JarMerger手动合并就足以消除了,具体可以参考GradleUtils的executeMerge方法
https://github.com/typ0520/fastdex/blob/master/fastdex-gradle/src/main/groovy/fastdex/build/util/GradleUtils.groovy

后来在支付中发觉了丢失jarMerging任务的法则如下

  • com.android.tools.build:gradle的版本 >= 2.3.0
  • build-type选拔的是debug
  • 只有点studio的run按钮打包时,命令行调用很是
  • 点击run按钮打包时接纳的配备是>=6.0的配备

观看那里第②点的显现是或不是很意外,命令行和studio点击run最后都以走gradle的流水线,既然表现分歧有大概是传的参数不等同,把上边这段代码放进build.gradle中

println "projectProperties: " + project.gradle.startParameter.projectProperties

点击studio的run按钮采纳三个6.0的设备

图片 14

studio_run.png

拿到以下输出

projectProperties: [android.injected.build.density:560dpi, android.injected.build.api:23, android.injected.invoked.from.ide:true, android.injected.build.abi:x86]

行使方面的那个参数1个三个做测试,发现是android.injected.build.api=23以此参数影响的,大家可以用那几个测试项目做下测试
https://github.com/typ0520/fastdex-test-project/tree/master/build-cache-test

执行./gradlew clean assembleDebug -Pandroid.injected.build.api=23
注: gradle传自定义的参数是以-P初阶

图片 15

miss_jar_merge.png

从上面的日志输出中可以看来再现了丢失jarMerge职责,大家再来计算下复出那一个难点的规范

  • com.android.tools.build:gradle的版本 >= 2.3.0
  • build-type采纳的是debug
  • 启航参数包括android.injected.build.api并且>=23

有了定论还没完,之所以2.3.0是这几个行为是因为引入了build-cache机制,不统一是为了做jar级其余dex缓存,那样每趟执行dex
transform时只有首回时第③方库才加入生成,为了升高效用也不会统一dex,若是项目比较大apk中只怕是出现几十一个甚至上百个dex

图片 16

classesN.png

眼下fastdex由于做了jar合并也就是把那个特点禁掉了,前面会考虑不再做联合使之能用dex缓存,那样全量打包时的快慢应该可以升高广大,其它还足以引入到除了debug其余build-type打包中,还有配备必须超出6.0标题也足以拍卖下,理论上5.0从此系统就足以加载三个dex了,不知底怎么那几个阈值设置的是6.0而不是5.0

==========================
理所当然想一气呵成把这多少个月做的效劳和优化全在那篇一并说完的,写着写着简书指示字数快超限了,无奈只可以分篇写了,下一篇主要讲免安装模块和idea插件的完毕。快到上巳节了提前祝大家中秋乐呵呵。未完待续,后会有期。。。。。。

一旦你喜悦本文就来给我们star吧
https://github.com/typ0520/fastdex

加紧apk的创设速度,怎么着把编译时间从130秒降到17秒
加快apk的打造速度,怎么着把编译时间从130秒降到17秒(二)

参考的类型与文章

Instant
Run

Tinker
Freeline
安卓App热补丁动态修复技术介绍
Android应用程序财富的编译和包裹过程分析

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

相关文章