然后又尝试采纳freeline编译速度还足以只是不稳定,经过一段时间的优化品质和稳定性都有很大的进步

本文已授权微信公众号:鸿洋(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的作用

  • 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做动态代码生成的一对库,输出在这几个目录,具体可以参见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秒,它有八个成效

  • 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加载进来,有思路了后面就是打下各类技术点

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

品类地址:
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文件的长度和上次修改时间多个要素比较可以识破同一个文书是还是不是暴发变化,通过快照目录没有某个文件而当前目录有某个文件可以得知增添了文本,通过快照目录有某个文件不过当前目录没有可以识破删除文件(为了作用可以不处理删除,仅造成缓存里有一些用不到的类而已)
举个例子来说假诺项目源码的路径为/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中,用来运行期给法斯特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中移除,那样的做法有七个难点

  • 1、combined.jar那几个文件是*
    transformClassesWithJarMergingForDebug职责输出的,存在这么些义务的前提是开启了multidex,假使没有开启那么执行到
    transformClassesWithDexForDebug*义务时输入就不在是combined.jar,而是项目标classes目录(app/build/intermediates/classes/debug)和依赖性的library输出的jar以及第三方库的jar;
  • 2、假设存在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做注入就行了,效能会比此前的做法有很大提高

付出完以上效能后做上边的两回打包做时间相比较(其实只做一次并不是太准确,做几十次测试取时间的平均值这样才最准)
  • 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
}

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

二、对直接敬爱的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);
}

持续的优化安顿

  • 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,做到免安装

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

三、 全新的快照相比较模块

fastdex方今内需比较的地方有三处

  • 全量打包时对眼前凭借的库做快照,补丁打包时相比是或不是暴发变化
  • 检测app工程和有着看重的android
    library工程中具备AndroidManifest.xml是上次打包比较是不是暴发变化(免安装模块要用到manifest文件爆发变化,必要求双重安装app)
  • 全量打包时对富有的java文件和kotlin文件做快照,补丁打包时比较那么些源文件发生变化

以率先种现象为例,说下相比较的规律,全量打包时生成一个文书文件把当前的看重写进去以换行符分割

/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前边),并且更新下源代码快照,那样做有八个坏处

  • 1、每便补丁打包时都必须对class文件做注入,为了化解上篇小说中提到的pre-verify错误
  • 2、每一次补丁打包都亟需缓存patch.dex,会促成上边那些目录的dex越来越多

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

解决第四个难点的方案是把patch.dex中的class合并到缓存的dex中,那样就不需求保留所有的patch.dex了,一个相比费力的标题是只要缓存的dex的方法数已经有65535个了,在往里面加新增的class,肯定会爆掉了,末了fastdex选择的方案是率先次触发dex
merge时直接把patch.dex扔进缓存(merged-patch.dex),将来在触发dex
merge时就拿patch.dex和merged-patch.dex做联合(这样做也存在潜在的题材,假如生成的class越发多也有可能引致合并dex时出现65535的谬误)

化解第四个难题是加了一个可安插选项,默许是3个以上的源文件发生变化时触发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中写了一个编译职责方便大家知道那么些参数都是怎么拼接的,代码太多了此间就不贴出来了
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

全量打包时的流水线:
  • 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.法斯特dexApplication)使用MultiDex把持有的dex加载进来
  • @see
    fastdex.build.transform.FastdexDexTransform
  • 7、保存资源映射表,为了保持id的值一致,详情看
  • @see
    fastdex.build.task.FastdexResourceIdTask

六、填过的坑

焚薮而田的bug那块本来是不准备说的,因为那块最有价值的事物不是化解难题我,而是怎么发现和复发难题的,那块确实不太好描述V_V,应简友的渴求仍旧挑了一些对峙相比有营养的难题说下,首要照旧说解决的章程,至于标题是何等定位和复发的只可以全力描述了。

补丁打包时的流水线
  • 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

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

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,此前就发现改了一个文书名字的大大小小写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

。。。。。。

说其实的实在希望我们对开源项目多或多或少保护,觉得对团结有赞助就用。假诺觉得糟糕,能够挑选提提议,也可以选取默默离开,假设有时间有力量可以涉足进去优化,解决自己工作难点的同时也服务了豪门。在这一个快节奏的社会我们的大运都不菲,你觉得测试一下荒废了光阴就开首吐槽,有没有想到开源项目标小编捐躯了大气的私房时光在缓解一个一个难点、为明白决新职能的技术点一个一个方案的做测试做相比吧?

注:
要是项目标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服务器上从不那么坚苦。我们团队的品类都是唯有一个根本的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时当然指望只有一个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]

应用方面的那些参数一个一个做测试,发现是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 编译慢

相关文章