写点什么

加快 APK 构建速度,如何一步步把编译时间从 130 秒降到 17 秒以内

用户头像
Android架构
关注
发布于: 53 分钟前

如何做快照与对比快照并拿到变化的 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) lsR.java? sample git:(master) javac R.java? sample git:(master) lsRdimen.class Rlayout.class Rstyleable.class R.javaRdrawable.class Rmipmap.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);}


@Overridepublic 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);}


@Overridepublic void visitInsn(int opcode) {if (opcode == Opcodes.RETURN) {//访问 java/lang/System 的静态常量 outmv.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.dexpatch.dex => classes2.dexclasses.dex => classes3.dexclasses2.dex => classes4.dex


在讲解 transformClassesWithMultidexlistForDebug 任务时有说过程序入口 Application 的问题,假如 patch.dex 中不包含入口 Application,apk 启动的时候肯定会报类找不到的错误,那么怎么解决这个问题呢


  • 第一个方案:把 transformClassesWithMultidexlistForDebug 任务中输出的 maindexlist.txt 中所有的 class 都参与 patch.dex 的生成

  • 第二种方案:对项目的入口 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 elementsdef 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 filedef printer = new XmlNodePrinter(new PrintWriter(manifestPath, "utf-8"))printer.preserveWhitespace = trueprinter.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.FastdexApplicationFastdexManifestTask manifestTask = project.tasks.create("fastdexProcess${variantName}Manifest", FastdexManifestTask)manifestTask.manifestPath = variantOutput.processManifest.manifestOutputFilemanifestTask.variantName = variantNamemanifestTask.mustRunAfter variantOutput.processManifest


variantOutput.processResources.dependsOn manifestTask}}


处理完以后 manifest 文件 application 节点 android.name 属性的值就变成了 com.dx168.fastdex.runtime.FastdexApplication,并且把原来项目的 Application 的名字写入到 meta-data 中,用来运行期给 FastdexApplication 去读取


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


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

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


BUILD SUCCESSFUL


Total time: 1 mins 46.678 secsTask spend time:437ms :app:prepareComAndroidSupportAppcompatV72340Library50ms :app:prepareComAndroidSupportDesign2340Library66ms :app:prepareComAndroidSupportSupportV42340Library75ms :app:prepareComFacebookFrescoImagepipeline110Library56ms :app:prepareOrgXutilsXutils3336Library870ms :app:mergeDebugResources93ms :app:processDebugManifest777ms :app:processDebugResources1200ms :app:compileDebugJavaWithJavac3643ms :app:transformClassesWithJarMergingForDebug5520ms :app:transformClassesWithMultidexlistForDebug61770ms :app:transformClassesWithDexForDebug99ms :app:transformNative_libsWithMergeJniLibsForDebug332ms :app:transformResourcesWithMergeJavaResForDebug2083ms :app:packageDebug202ms :app:zipalignDebug


  • 2、删除 build 目录第一次全量打包(开启 fastdex)


BUILD SUCCESSFUL


Total time: 1 mins 57.764 secsTask spend time:106ms :app:prepareComAndroidSupportAnimatedVectorDrawable2340Library107ms :runtime:transformClassesAndResourcesWithSyncLibJarsForDebug416ms :app:prepareComAndroidSupportAppcompatV72340Library67ms :app:prepareComAndroidSupportSupportV42340Library76ms :app:prepareComFacebookFrescoImagepipeline110Library53ms :app:prepareOrgXutilsXutils3336Library111ms :app:processDebugManifest929ms :app:mergeDebugResources697ms :app:processDebugResources1227ms :app:compileDebugJavaWithJavac3237ms :app:transformClassesWithJarMergingForDebug6225ms :app:transformClassesWithMultidexlistForDebug78990ms :app:transformClassesWithDexForDebug122ms :app:transformNative_libsWithMergeJniLibsForDebug379ms :app:transformResourcesWithMergeJavaResForDebug2050ms :app:packageDebug77ms :app:zipalignDebug


  • 3、在开启 fastdex 第一次全量打包完成后,关掉 fastdex 修改 sample 工程的MainActivity.java


BUILD SUCCESSFUL


Total time: 1 mins 05.394 secsTask spend time:52ms :app:mergeDebugResources2583ms :app:compileDebugJavaWithJavac60718ms :app:transformClassesWithDexForDebug101ms :app:transformNative_libsWithMergeJniLibsForDebug369ms :app:transformResourcesWithMergeJavaResForDebug2057ms :app:packageDebug75ms :app:zipalignDebug


  • 4、在开启 fastdex 第一次全量打包完成后,仍然开启 fastdex 修改 sample 工程的MainActivity.java


BUILD SUCCESSFUL


Total time: 16.5 secsTask spend time:142ms :app:processDebugManifest1339ms :app:compileDebugJavaWithJavac3291ms :app:transformClassesWithJarMergingForDebug4865ms :app:transformClassesWithMultidexlistForDebug1005ms :app:transformClassesWithDexForDebug2112ms :app:packageDebug76ms :app:zipalignDebug



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


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


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


<TextViewandroid: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.java:17)at com.dx168.fastdex.sample.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 时编译出错了



出错的原因是注解只能引用带 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 resDirString variantName


@TaskActiondef applyResourceId() {File buildDir = FastdexUtils.getBuildDir(project,variantName)String resourceMappingFile = new File(buildDir,Constant.R_TXT)// Parse the public.xml and ids.xmlif (!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 {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.resDirapplyResourceTask.variantName = variantNamevariantOutput.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 = nulltry {multidexlistTask = project.tasks.getByName("transformClassesWithMultidexlistFor${variantName}")} catch (Throwable e) {//没有开启 multiDexEnabled 的情况下,会报这个任务找不到的异常}if (multidexlistTask != null) {multidexlistTask.enabled = false}}}


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


:app:transformClassesWithMultidexlistForDebug SKIPPED:app:transformClassesWithDexForDebugRunning 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


@TaskActionvoid 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 = nulltry {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 createFileTaskmultidexlistTask.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 secsTask spend time:554ms :app:processDebugManifest127ms :app:mergeDebugResources3266ms :app:processDebugResources13621ms :app:compileDebugJavaWithJavac3654ms :app:transformClassesWithJarMergingForDebug1354ms :app:transformClassesWithDexForDebug315ms :app:transformNative_libsWithMergeJniLibsForDebug220ms :app:transformResourcesWithMergeJavaResForDebug2684ms :app:packageDebug


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


public class FastdexCustomJavacTask extends DefaultTask {......


@TaskActionvoid compile() {......File androidJar = new File("{project.android.getCompileSdkVersion()}/android.jar")File classpathJar = FastdexUtils.getInjectedJarFile(project,variantName)project.logger.error("==fastdex androidJar: {classpathJar}")project.ant.javac(srcdir: patchJavaFileDir,source: '1.7',target: '1.7',encoding: 'UTF-8',destdir: patchClassesFileDir,bootclasspath: androidJar,classpath: classpathJar)compileTask.enabled = falseFile classesDir = applicationVariant.getVariantData().getScope().getJavaOutputDir()Files.walkFileTree(patchClassesFileDir.toPath(),new SimpleFileVisitor<Path>(){@OverrideFileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {Path relativePath = patchClassesFileDir.toPath().relativize(file)File destFile = new File(classesDir,relativePath.toString())

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
加快APK构建速度,如何一步步把编译时间从130秒降到17秒以内