本文已授權微信公眾號:鴻洋(hongyangAndroid)原創首發
公司的項目代碼比較多,每次調試改動java文件后要將近2分鐘才能跑起來,實在受不了。在網上找了一大堆配置參數也沒有很明顯的效果, 嘗試使用instant run效果也不怎么樣,然后又嘗試使用freeline編譯速度還可以但是不穩定,每次失敗后全量編譯很耗費時間,既然沒有好的方案就自己嘗試做。
項目地址: https://github.com/typ0520/fastdex
注: 本文對gradle task做的說明都建立在關閉instant run的前提下
注: 本文所有的代碼、gradle任務名、任務輸出路徑、全部使用debug這個buildType作說明
優化構建速度首先需要找到那些環節導致構建速度這么慢,把下面的代碼放進app/build.gradle里把時間花費超過50ms的任務時間打印出來
public class BuildTimeListener implements TaskExecutionListener, BuildListener {
private Clock clock
private times = []
@Override
void beforeExecute(Task task) {
clock = new org.gradle.util.Clock()
}
@Override
void afterExecute(Task task, TaskState taskState) {
def ms = clock.timeInMs
times.add([ms, task.path])
//task.project.logger.warn "${task.path} spend ${ms}ms"
}
@Override
void buildFinished(BuildResult result) {
println "Task spend time:"
for (time in times) {
if (time[0] >= 50) {
printf "%7sms %s\n", time
}
}
}
......
}
project.gradle.addListener(new BuildTimeListener())
執行./gradlew assembleDebug,經過漫長的等待得到以下輸出
Total time: 1 mins 39.566 secs
Task spend time:
69ms :app:prepareComAndroidSupportAnimatedVectorDrawable2340Library
448ms :app:prepareComAndroidSupportAppcompatV72340Library
57ms :app:prepareComAndroidSupportDesign2340Library
55ms :app:prepareComAndroidSupportSupportV42340Library
84ms :app:prepareComFacebookFrescoImagepipeline110Library
69ms :app:prepareComSquareupLeakcanaryLeakcanaryAndroid14Beta2Library
60ms :app:prepareOrgXutilsXutils3336Library
68ms :app:compileDebugRenderscript
265ms :app:processDebugManifest
1517ms :app:mergeDebugResources
766ms :app:processDebugResources
2897ms :app:compileDebugJavaWithJavac
3117ms :app:transformClassesWithJarMergingForDebug
7899ms :app:transformClassesWithMultidexlistForDebug
65327ms :app:transformClassesWithDexForDebug
151ms :app:transformNative_libsWithMergeJniLibsForDebug
442ms :app:transformResourcesWithMergeJavaResForDebug
2616ms :app:packageDebug
123ms :app:zipalignDebug
從上面的輸出可以發現總的構建時間為100秒左右(上面的輸出不是按照真正的執行順序輸出的),transformClassesWithDexForDebug任務是最慢的耗費了65秒,它就是我們需要重點優化的任務,首先講下構建過程中主要任務的作用,方便理解后面的hook點
mergeDebugResources任務的作用是解壓所有的aar包輸出到app/build/intermediates/exploded-aar,并且把所有的資源文件合并到app/build/intermediates/res/merged/debug目錄里
processDebugManifest任務是把所有aar包里的AndroidManifest.xml中的節點,合并到項目的AndroidManifest.xml中,并根據app/build.gradle中當前buildType的manifestPlaceholders配置內容替換manifest文件中的占位符,最后輸出到app/build/intermediates/manifests/full/debug/AndroidManifest.xml
processDebugResources的作用
- 1、調用aapt生成項目和所有aar依賴的R.java,輸出到app/build/generated/source/r/debug目錄
- 2、生成資源索引文件app/build/intermediates/res/resources-debug.ap_
- 3、把符號表輸出到app/build/intermediates/symbols/debug/R.txt
compileDebugJavaWithJavac這個任務是用來把java文件編譯成class文件,輸出的路徑是app/build/intermediates/classes/debug
編譯的輸入目錄有
- 1、項目源碼目錄,默認路徑是app/src/main/java,可以通過sourceSets的dsl配置,允許有多個(打印project.android.sourceSets.main.java.srcDirs可以查看當前所有的源碼路徑,具體配置可以參考android-doc
- 2、app/build/generated/source/aidl
- 3、app/build/generated/source/buildConfig
- 4、app/build/generated/source/apt(繼承javax.annotation.processing.AbstractProcessor做動態代碼生成的一些庫,輸出在這個目錄,具體可以參考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加載進來,有思路了后面就是攻克各個技術點
==============================
如何拿到transformClassesWithDexForDebug任務執行前后的生命周期
參考了Tinker項目的代碼,找到下面的實現
public class ImmutableDexTransform extends Transform {
Project project
DexTransform dexTransform
def variant
......
@Override
void transform(TransformInvocation transformInvocation) throws TransformException, IOException, InterruptedException {
def outputProvider = transformInvocation.getOutputProvider()
//dex的輸出目錄
File outputDir = outputProvider.getContentLocation("main", dexTransform.getOutputTypes(), dexTransform.getScopes(), Format.DIRECTORY);
if (outputDir.exists()) {
outputDir.delete()
}
println("===執行transform前清空dex輸出目錄: ${project.projectDir.toPath().relativize(outputDir.toPath())}")
dexTransform.transform(transformInvocation)
if (outputDir.exists()) {
println("===執行transform后dex輸出目錄不是空的: ${project.projectDir.toPath().relativize(outputDir.toPath())}")
outputDir.listFiles().each {
println("===執行transform后: ${it.name}")
}
}
}
}
project.getGradle().getTaskGraph().addTaskExecutionGraphListener(new TaskExecutionGraphListener() {
@Override
public void graphPopulated(TaskExecutionGraph taskGraph) {
for (Task task : taskGraph.getAllTasks()) {
if (task instanceof TransformTask && task.name.toLowerCase().contains(variant.name.toLowerCase())) {
if (((TransformTask) task).getTransform() instanceof DexTransform && !(((TransformTask) task).getTransform() instanceof ImmutableDexTransform)) {
project.logger.warn("find dex transform. transform class: " + task.transform.getClass() + " . task name: " + task.name)
DexTransform dexTransform = task.transform
ImmutableDexTransform hookDexTransform = new ImmutableDexTransform(project,
variant, dexTransform)
project.logger.info("variant name: " + variant.name)
Field field = TransformTask.class.getDeclaredField("transform")
field.setAccessible(true)
field.set(task, hookDexTransform)
project.logger.warn("transform class after hook: " + task.transform.getClass())
break;
}
}
}
}
});
把上面的代碼放進app/build.gradle執行./gradlew assembleDebug
:app:transformClassesWithMultidexlistForDebug
ProGuard, version 5.2.1
Reading program jar [/Users/tong/Projects/fastdex/app/build/intermediates/transforms/jarMerging/debug/jars/1/1f/combined.jar]
Reading library jar [/Users/tong/Applications/android-sdk-macosx/build-tools/23.0.1/lib/shrinkedAndroid.jar]
Preparing output jar [/Users/tong/Projects/fastdex/app/build/intermediates/multi-dex/debug/componentClasses.jar]
Copying resources from program jar [/Users/tong/Projects/fastdex/app/build/intermediates/transforms/jarMerging/debug/jars/1/1f/combined.jar]
:app:transformClassesWithDexForDebug
===執行transform前清空dex輸出目錄: build/intermediates/transforms/dex/debug/folders/1000/1f/main
......
===執行transform后dex輸出目錄不是空的: build/intermediates/transforms/dex/debug/folders/1000/1f/main
===執行transform后: classes.dex
從上面的日志輸出證明這個hook點是有效的,在全量打包時執行transform前可以對java源碼做快照,執行完以后把dex緩存下來;在補丁打包執行transform之前對比快照移除沒有變化的class,執行完以后合并緩存的dex放進dex輸出目錄
==============================
如何做快照與對比快照并拿到變化的class列表
執行下面的代碼可以獲取所有的項目源碼目錄
project.android.sourceSets.main.java.srcDirs.each { srcDir->
println("==srcDir: ${srcDir}")
}
sample工程沒有配置sourceSets,因此輸出的是app/src/main/java
給源碼目錄做快照,直接通過文件復制的方式,把所有的srcDir目錄下的java文件復制到快照目錄下(這里有個坑,不要使用project.copy {}它會使文件的lastModified值發生變化,直接使用流copy并且要用源文件的lastModified覆蓋目標文件的lastModified)
通過java文件的長度和上次修改時間兩個要素對比可以得知同一個文件是否發生變化,通過快照目錄沒有某個文件而當前目錄有某個文件可以得知增加了文件,通過快照目錄有某個文件但是當前目錄沒有可以得知刪除文件(為了效率可以不處理刪除,僅造成緩存里有某些用不到的類而已)
舉個例子來說假如項目源碼的路徑為/Users/tong/fastdex/app/src/main/java,做快照時把這個目錄復制到/Users/tong/fastdex/app/build/fastdex/snapshoot下,當前快照里的文件樹為
└── com
└── dx168
└── fastdex
└── sample
├── CustomView.java
├── MainActivity.java
└── SampleApplication.java
如果當前源碼路徑的內容發生變化,當前的文件樹為
└── com
└── dx168
└── fastdex
└── sample
├── CustomView.java
├── MainActivity.java(內容已經被修改)
├── New.java
└── SampleApplication.java
通過文件遍歷對比可以得到這個變化的相對路徑列表
- com/dx168/fastdex/sample/MainActivity.java
- com/dx168/fastdex/sample/New.java
通過這個列表進而可以得知變化的class有
- com/dx168/fastdex/sample/MainActivity.class
- com/dx168/fastdex/sample/New.class
但是java文件編譯的時候如果有內部類還會有其它的一些class輸出,比如拿R文件做下編譯,它的編譯輸出如下
? sample git:(master) ls
R.java
? sample git:(master) javac R.java
? sample git:(master) ls
R$attr.class R$dimen.class R$id.class R$layout.class R$string.class R$styleable.class R.java
R$color.class R$drawable.class R$integer.class R$mipmap.class R$style.class R.class
? sample git:(master)
另外如果使用了butterknife,還會生成binder類,比如編譯MainActivity.java時生成了
com/dx168/fastdex/sample/MainActivity$$ViewBinder.class
結合上面幾點可以獲取所有變化class的匹配模式
- com/dx168/fastdex/sample/MainActivity.class
- com/dx168/fastdex/sample/MainActivity$*.class
- com/dx168/fastdex/sample/New.class
- com/dx168/fastdex/sample/New$*.class
有了上面的匹配模式就可以在補丁打包執行transform前把app/build/intermediates/transforms/jarMerging/debug/jars/1/1f/combined.jar中沒有變化的class全部移除掉
project.copy {
from project.zipTree(combinedJar)
for (String pattern : patterns) {
include pattern
}
}
into tmpDir
}
project.ant.zip(baseDir: tmpDir, destFile: patchJar)
然后就可以使用patchJar作為輸入jar生成補丁dex
注: 這種映射方案如果開啟了混淆就對應不上了,需要解析混淆以后產生的mapping文件才能解決,不過我們也沒有必要在開啟混淆的buildType下做開發開發調試,所以暫時可以不做這個事情
==============================
有了補丁dex,就可以選擇一種熱修復方案把補丁dex加載進來,這里方案有好幾種,為了簡單直接選擇android.support.multidex.MultiDex以dex插樁的方式來加載,只需要把dex按照google標準(classes.dex、classes2.dex、classesN.dex)排列好就行了,這里有兩個技術點
由于patch.dex和緩存下來dex里面有重復的類,當加載引用了重復類的類時會造成pre-verify的錯誤,具體請參考QQ空間團隊寫的安卓App熱補丁動態修復技術介紹
,這篇文章詳細分析了造成pre-verify錯誤的原因,文章里給的解決方案是往所有引用被修復類的類中插入一段代碼,并且被插入的這段代碼所在的類的dex必須是一個單獨的dex,這個dex我們事先準備好,叫做fastdex-runtime.dex,它的代碼結構是
└── com
└── dx168
└── fastdex
└── runtime
├── FastdexApplication.java
├── antilazyload
│ └── AntilazyLoad.java
└── multidex
├── MultiDex.java
├── MultiDexApplication.java
├── MultiDexExtractor.java
└── ZipUtil.java
AntilazyLoad.java就是在注入時被引用的類
MultiDex.java是用來加載classes2.dex - classesN.dex的包,為了防止項目沒有依賴MultiDex,所以把MultiDex的代碼copy到了我們的package下
FastdexApplication.java的作用后面在說
結合我們的項目需要在全量打包前把app/build/intermediates/transforms/jarMerging/debug/jars/1/1f/combined.jar中所有的項目代碼的class全部動態插入代碼(第三方庫由于不在我們的修復范圍內所以為了效率忽略掉),具體的做法是往所有的構造方法中添加對com.dx168.fastdex.runtime.antilazyload.AntilazyLoad的依賴,如下面的代碼所示
//source class:
public class MainActivity {
}
==>
//dest class:
import com.dx168.fastdex.runtime.antilazyload.AntilazyLoad;
public class MainActivity {
public MainActivity() {
System.out.println(Antilazyload.str);
}
}
動態往class文件中插入代碼使用的是asm,我把做測試的時候找到的一些相關資料和代碼都放到了github上面點我查看,代碼比較多只貼出來一部分,具體請查看ClassInject.groovy
private static class MyClassVisitor extends ClassVisitor {
public MyClassVisitor(ClassVisitor classVisitor) {
super(Opcodes.ASM5, classVisitor);
}
@Override
public MethodVisitor visitMethod(int access,
String name,
String desc,
String signature,
String[] exceptions) {
//判斷是否是構造方法
if ("<init>".equals(name)) {
MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
MethodVisitor newMethod = new AsmMethodVisit(mv);
return newMethod;
} else {
return super.visitMethod(access, name, desc, signature, exceptions);
}
}
}
static class AsmMethodVisit extends MethodVisitor {
public AsmMethodVisit(MethodVisitor mv) {
super(Opcodes.ASM5, mv);
}
@Override
public void visitInsn(int opcode) {
if (opcode == Opcodes.RETURN) {
//訪問java/lang/System的靜態常量out
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
//訪問AntilazyLoad的靜態變量
mv.visitFieldInsn(GETSTATIC, "com/dx168/fastdex/runtime/antilazyload/AntilazyLoad", "str", "Ljava/lang/String;");
//調用out的println打印AntilazyLoad.str的值
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
super.visitInsn(opcode);
}
}
===============
處理完pre-verify問題,接下來又出現坑了,當補丁dex打好后假如緩存的dex有兩個(classes.dex classes2.dex),那么合并dex后的順序就是
fastdex-runtime.dex 、patch.dex、classes.dex 、classes2.dex (patch.dex必須放在緩存的dex之前才能被修復)
fastdex-runtime.dex => classes.dex
patch.dex => classes2.dex
classes.dex => classes3.dex
classes2.dex => classes4.dex
在講解transformClassesWithMultidexlistForDebug任務時有說過程序入口Application的問題,假如patch.dex中不包含入口Application,apk啟動的時候肯定會報類找不到的錯誤,那么怎么解決這個問題呢
- 第一個方案:
把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 elements
def originApplicationName = metaDataTags.findAll {
it.attributes()[ns.name].equals(FASTDEX_ORIGIN_APPLICATION_CLASSNAME)
}.each {
it.parent().remove(it)
}
// Add the new FASTDEX_ORIGIN_APPLICATION_CLASSNAME element
//把原來的Application寫入到meta-data中
application.appendNode('meta-data', [(ns.name): FASTDEX_ORIGIN_APPLICATION_CLASSNAME, (ns.value): applicationName])
// Write the manifest file
def printer = new XmlNodePrinter(new PrintWriter(manifestPath, "utf-8"))
printer.preserveWhitespace = true
printer.print(xml)
}
File manifestFile = new File(manifestPath)
if (manifestFile.exists()) {
File buildDir = FastdexUtils.getBuildDir(project,variantName)
FileUtils.copyFileUsingStream(manifestFile, new File(buildDir,MANIFEST_XML))
project.logger.error("fastdex gen AndroidManifest.xml in ${MANIFEST_XML}")
}
使用下面的代碼把這個任務加進去并保證在processDebugManifest任務執行完畢后執行
project.afterEvaluate {
android.applicationVariants.all { variant ->
def variantOutput = variant.outputs.first()
def variantName = variant.name.capitalize()
//替換項目的Application為com.dx168.fastdex.runtime.FastdexApplication
FastdexManifestTask manifestTask = project.tasks.create("fastdexProcess${variantName}Manifest", FastdexManifestTask)
manifestTask.manifestPath = variantOutput.processManifest.manifestOutputFile
manifestTask.variantName = variantName
manifestTask.mustRunAfter variantOutput.processManifest
variantOutput.processResources.dependsOn manifestTask
}
}
處理完以后manifest文件application節點android.name屬性的值就變成了com.dx168.fastdex.runtime.FastdexApplication,并且把原來項目的Application的名字寫入到meta-data中,用來運行期給FastdexApplication去讀取
<meta-data android:name="FASTDEX_ORIGIN_APPLICATION_CLASSNAME" android:value="com.dx168.fastdex.sample.SampleApplication"/>
==============================
開發完以上功能后做下面的四次打包做時間對比(其實只做一次并不是太準確,做幾十次測試取時間的平均值這樣才最準)
-
1、刪除build目錄第一次全量打包(不開啟fastdex)
BUILD SUCCESSFUL Total time: 1 mins 46.678 secs Task spend time: 437ms :app:prepareComAndroidSupportAppcompatV72340Library 50ms :app:prepareComAndroidSupportDesign2340Library 66ms :app:prepareComAndroidSupportSupportV42340Library 75ms :app:prepareComFacebookFrescoImagepipeline110Library 56ms :app:prepareOrgXutilsXutils3336Library 870ms :app:mergeDebugResources 93ms :app:processDebugManifest 777ms :app:processDebugResources 1200ms :app:compileDebugJavaWithJavac 3643ms :app:transformClassesWithJarMergingForDebug 5520ms :app:transformClassesWithMultidexlistForDebug 61770ms :app:transformClassesWithDexForDebug 99ms :app:transformNative_libsWithMergeJniLibsForDebug 332ms :app:transformResourcesWithMergeJavaResForDebug 2083ms :app:packageDebug 202ms :app:zipalignDebug
-
2、刪除build目錄第一次全量打包(開啟fastdex)
BUILD SUCCESSFUL Total time: 1 mins 57.764 secs Task spend time: 106ms :app:prepareComAndroidSupportAnimatedVectorDrawable2340Library 107ms :runtime:transformClassesAndResourcesWithSyncLibJarsForDebug 416ms :app:prepareComAndroidSupportAppcompatV72340Library 67ms :app:prepareComAndroidSupportSupportV42340Library 76ms :app:prepareComFacebookFrescoImagepipeline110Library 53ms :app:prepareOrgXutilsXutils3336Library 111ms :app:processDebugManifest 929ms :app:mergeDebugResources 697ms :app:processDebugResources 1227ms :app:compileDebugJavaWithJavac 3237ms :app:transformClassesWithJarMergingForDebug 6225ms :app:transformClassesWithMultidexlistForDebug 78990ms :app:transformClassesWithDexForDebug 122ms :app:transformNative_libsWithMergeJniLibsForDebug 379ms :app:transformResourcesWithMergeJavaResForDebug 2050ms :app:packageDebug 77ms :app:zipalignDebug
-
3、在開啟fastdex第一次全量打包完成后,關掉fastdex修改sample工程的MainActivity.java
BUILD SUCCESSFUL Total time: 1 mins 05.394 secs Task spend time: 52ms :app:mergeDebugResources 2583ms :app:compileDebugJavaWithJavac 60718ms :app:transformClassesWithDexForDebug 101ms :app:transformNative_libsWithMergeJniLibsForDebug 369ms :app:transformResourcesWithMergeJavaResForDebug 2057ms :app:packageDebug 75ms :app:zipalignDebug
-
4、在開啟fastdex第一次全量打包完成后,仍然開啟fastdex修改sample工程的MainActivity.java
BUILD SUCCESSFUL Total time: 16.5 secs Task spend time: 142ms :app:processDebugManifest 1339ms :app:compileDebugJavaWithJavac 3291ms :app:transformClassesWithJarMergingForDebug 4865ms :app:transformClassesWithMultidexlistForDebug 1005ms :app:transformClassesWithDexForDebug 2112ms :app:packageDebug 76ms :app:zipalignDebug
打包編號 | 總時間 | transform時間 |
---|---|---|
1 | 1 mins 46.678s | 61770 ms |
2 | 1 mins 57.764s | 78990 ms |
3 | 1 mins 05.394s | 60718 ms |
4 | 16.5s | 1005 ms |
通過1和2對比發現,開啟fastdex進行第一次全量的打包時的時間花費比不開啟多了10秒左右,這個主要是注入代碼和IO上的開銷
通過2和3對比發現,開啟fastdex進行補丁打包時的時間花費比不開啟快了60秒左右,這就是期待已久的構建速度啊_
==============================
剛激動一會就尼瑪報了一個錯誤,當修改activity_main.xml時往里面增加一個控件
<TextView
android:id="@+id/tv2"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
打出來的包啟動的時候就直接crash掉了
Caused by: java.lang.IllegalStateException:
Required view 'end_padder' with ID 2131493007 for field 'tv1' was not found.
If this view is optional add '@Nullable' (fields) or '@Optional' (methods) annotation.
at butterknife.internal.Finder.findRequiredView(Finder.java:51)
at com.dx168.fastdex.sample.CustomView$$ViewBinder.bind(CustomView$$ViewBinder.java:17)
at com.dx168.fastdex.sample.CustomView$$ViewBinder.bind(CustomView$$ViewBinder.java:12)
at butterknife.ButterKnife.bind(ButterKnife.java:187)
at butterknife.ButterKnife.bind(ButterKnife.java:133)
at com.dx168.fastdex.sample.CustomView.<init>(CustomView.java:20)
......
at dalvik.system.NativeStart.main(Native Method)
錯誤信息里的意思是為CustomView的tv1字段,尋找id=2131493007的view時沒有找到,先反編譯報錯的apk,?找到報錯的地方CustomView$$ViewBinder.bind
public class CustomView$$ViewBinder<T extends CustomView>
implements ViewBinder<T>
{
public CustomView$$ViewBinder()
{
System.out.println(AntilazyLoad.str);
}
public Unbinder bind(Finder paramFinder, T paramT, Object paramObject)
{
InnerUnbinder localInnerUnbinder = createUnbinder(paramT);
paramT.tv1 = ((TextView)paramFinder.castView((View)paramFinder
.findRequiredView(paramObject, 2131493007, "field 'tv1'"), 2131493007, "field 'tv1'"));
paramT.tv3 = ((TextView)paramFinder.castView((View)paramFinder
.findRequiredView(paramObject, 2131493008, "field 'tv3'"), 2131493008, "field 'tv3'"));
return localInnerUnbinder;
}
......
}
CustomView$$ViewBinder這個類是ButterKnife動態生成的,這個值的來源是CustomView的tv1字段上面的注解,CustomView.class反編譯后如下
public class CustomView extends LinearLayout
{
@BindView(2131493007)
TextView tv1;
@BindView(2131493008)
TextView tv3;
public CustomView(Context paramContext, AttributeSet paramAttributeSet)
{
super(paramContext, paramAttributeSet);
inflate(paramContext, 2130968632, this);
ButterKnife.bind(this);
this.tv3.setText(2131099697);
MainActivity.aa();
System.out.println(AntilazyLoad.str);
}
}
看到這里是不是覺得奇怪,CustomView的源碼明明是
public class CustomView extends LinearLayout {
@BindView(R.id.tv1) TextView tv1;
@BindView(R.id.tv3) TextView tv3;
public CustomView(Context context, AttributeSet attrs) {
super(context, attrs);
inflate(context,R.layout.view_custom,this);
ButterKnife.bind(this);
tv3.setText(R.string.s3);
MainActivity.aa();
}
}
?在編譯以后R.id.tv1怎么就變成數字2131493007了呢,原因是java編譯器做了一個性能優化,如果發現源文件引用的是一個帶有final描述符的常量,會直接做值copy
反編譯最后一次編譯成功時的R.class結果如下(
app/build/intermediates/classes/debug/com/dx168/fastdex/sample/R.class)
public static final R {
public static final class id {
......
public static final int tv1 = 2131493008;
public static final int tv2 = 2131492977;
public static final int tv3 = 2131493009;
......
public id() {
}
}
}
經過分析,當全量打包時R.id.tv1 = 2131493007,由于R文件中的id都是final的,所以引用R.id.tv1的地方都被替換為它對應的值2131493007了;當在activity_layout.xml中添加名字為tv2的控件,然后進行補丁打包時R.id.tv1的值變成了2131493008,而緩存的dex對應節點的值還是2131493007,所以在尋找id為2131493007對應的控件時因為找不到而掛掉
我的第一個想法是如果在執行完processDebugResources任務后,把R文件里id類的所有字段的final描述符去掉就可以把值copy這個編譯優化繞過去 =>
public static final R {
public static final class id {
......
public static int tv1 = 2131493008;
public static int tv2 = 2131492977;
public static int tv3 = 2131493009;
......
public id() {
}
}
}
去掉以后在執行compileDebugJavaWithJavac時編譯出錯了
出錯的原因是注解只能引用帶final描述符的常量,除此之外switch語句的case也必須引用常量,具體請查看oracle對常量表達式的說明
如果采取這個方案,對id的引用就不能使用常量表達式,像ButterKnife這樣的view依賴注入的框架都不能用了,限制性太大這個想法就放棄了
還有一個思路就是修改aapt的源碼?,使多次打包時名字相同id的值保持一致,這個肯定能解決不過工作量太大了就沒有這樣做,之后采用了一個折中的辦法,就是每次把項目中的所有類(除去第三方庫)都參與dex的生成,雖然解決了這個問題但效率一下子降低好多,需要將近40秒才能跑起來還是很慢
==============================
這個問題困擾了好久,直到tinker開源后閱讀它的源碼TinkerResourceIdTask.groovy時,發現它們也碰到了同樣的問題,并有了一個解決方案,我們的場景和tinker場景在這個問題上是一模一樣的,直接照抄代碼就解決了這個問題,重要的事情說三遍,感謝tinker、感謝tinker、感謝tinker?。?/p>
tinker的解決方案是,打補丁時根據用戶配置的resourceMapping文件(每次構建成功后輸出的app/build/intermediates/symbols/debug/R.txt),生成public.xml和ids.xml然后放進app/build/intermediates/res/merged/debug/values目錄里,aapt在處理的時候會根據文件里的配置規則去生成,具體這塊的原理請看老羅的文章Android應用程序資源的編譯和打包過程分析(在里面搜索public.xml)這里面有詳細的說明
同上并結合我們的場景,第一次全量打包成功以后把app/build/intermediates/symbols/debug/R.txt緩存下來,補丁打包在執行processResources任務前,根據緩存的符號表R.txt去生成public.xml和ids.xml然后放進app/build/intermediates/res/merged/debug/values目錄里,這樣相同名字的id前后的兩次構建值就能保持一致了,代碼如下FastdexResourceIdTask.groovy
public class FastdexResourceIdTask extends DefaultTask {
static final String RESOURCE_PUBLIC_XML = "public.xml"
static final String RESOURCE_IDX_XML = "idx.xml"
String resDir
String variantName
@TaskAction
def applyResourceId() {
File buildDir = FastdexUtils.getBuildDir(project,variantName)
String resourceMappingFile = new File(buildDir,Constant.R_TXT)
// Parse the public.xml and ids.xml
if (!FileUtils.isLegalFile(resourceMappingFile)) {
project.logger.error("==fastdex apply resource mapping file ${resourceMappingFile} is illegal, just ignore")
return
}
File idsXmlFile = new File(buildDir,RESOURCE_IDX_XML)
File publicXmlFile = new File(buildDir,RESOURCE_PUBLIC_XML)
if (FileUtils.isLegalFile(idsXmlFile) && FileUtils.isLegalFile(publicXmlFile)) {
project.logger.error("==fastdex public xml file and ids xml file already exist, just ignore")
return
}
String idsXml = resDir + "/values/ids.xml";
String publicXml = resDir + "/values/public.xml";
FileUtils.deleteFile(idsXml);
FileUtils.deleteFile(publicXml);
List<String> resourceDirectoryList = new ArrayList<String>()
resourceDirectoryList.add(resDir)
project.logger.error("==fastdex we build ${project.getName()} apk with apply resource mapping file ${resourceMappingFile}")
Map<RDotTxtEntry.RType, Set<RDotTxtEntry>> rTypeResourceMap = PatchUtil.readRTxt(resourceMappingFile)
AaptResourceCollector aaptResourceCollector = AaptUtil.collectResource(resourceDirectoryList, rTypeResourceMap)
PatchUtil.generatePublicResourceXml(aaptResourceCollector, idsXml, publicXml)
File publicFile = new File(publicXml)
if (publicFile.exists()) {
FileUtils.copyFileUsingStream(publicFile, publicXmlFile)
project.logger.error("==fastdex gen resource public.xml in ${RESOURCE_PUBLIC_XML}")
}
File idxFile = new File(idsXml)
if (idxFile.exists()) {
FileUtils.copyFileUsingStream(idxFile, idsXmlFile)
project.logger.error("==fastdex gen resource idx.xml in ${RESOURCE_IDX_XML}")
}
}
}
project.afterEvaluate {
android.applicationVariants.all { variant ->
def variantOutput = variant.outputs.first()
def variantName = variant.name.capitalize()
//保持補丁打包時R文件中相同的節點和第一次打包時的值保持一致
FastdexResourceIdTask applyResourceTask = project.tasks.create("fastdexProcess${variantName}ResourceId", com.dx168.fastdex.build.task.FastdexResourceIdTask)
applyResourceTask.resDir = variantOutput.processResources.resDir
applyResourceTask.variantName = variantName
variantOutput.processResources.dependsOn applyResourceTask
}
}
如果項目中的資源特別多,第一次補丁打包生成public.xml和ids.xml時會占用一些時間,最好做一次緩存,以后的補丁打包直接使用緩存的public.xml和ids.xml**
==============================
解決了上面的原理性問題后,接下來繼續做優化,上面有講到* transformClassesWithMultidexlistForDebug*任務的作用,由于采用了隔離Application的做法,所有的項目代碼都不在classes.dex中,這個用來分析那些項目中的類需要放在classes.dex的任務就沒有意義了,直接禁掉它
project.afterEvaluate {
android.applicationVariants.all { variant ->
def variantName = variant.name.capitalize()
def multidexlistTask = null
try {
multidexlistTask = project.tasks.getByName("transformClassesWithMultidexlistFor${variantName}")
} catch (Throwable e) {
//沒有開啟multiDexEnabled的情況下,會報這個任務找不到的異常
}
if (multidexlistTask != null) {
multidexlistTask.enabled = false
}
}
}
禁掉以后,執行./gradle assembleDebug,在構建過程中掛掉了
:app:transformClassesWithMultidexlistForDebug SKIPPED
:app:transformClassesWithDexForDebug
Running dex in-process requires build tools 23.0.2.
For faster builds update this project to use the latest build tools.
UNEXPECTED TOP-LEVEL ERROR:
java.io.FileNotFoundException: /Users/tong/Projects/fastdex/app/build/intermediates/multi-dex/debug/maindexlist.txt (No such file or directory)
at java.io.FileInputStream.open0(Native Method)
at java.io.FileInputStream.open(FileInputStream.java:195)
at java.io.FileInputStream.<init>(FileInputStream.java:138)
at java.io.FileInputStream.<init>(FileInputStream.java:93)
at java.io.FileReader.<init>(FileReader.java:58)
at com.android.dx.command.dexer.Main.readPathsFromFile(Main.java:436)
at com.android.dx.command.dexer.Main.runMultiDex(Main.java:361)
at com.android.dx.command.dexer.Main.run(Main.java:275)
at com.android.dx.command.dexer.Main.main(Main.java:245)
at com.android.dx.command.Main.main(Main.java:106)
:app:transformClassesWithDexForDebug FAILED
FAILURE: Build failed with an exception.
......
BUILD FAILED
從上面的日志的第一行發現transformClassesWithMultidexlistForDebug任務確實禁止掉了,后面跟著一個SKIPPED的輸出,但是執行transformClassesWithDexForDebug任務時報app/build/intermediates/multi-dex/debug/maindexlist.txt (No such file or directory)
,原因是transformClassesWithDexForDebug任務會檢查這個文件是否存在,既然這樣就在執行transformClassesWithDexForDebug任務前創建一個空文件,看是否還會報錯,代碼如下
public class FastdexCreateMaindexlistFileTask extends DefaultTask {
def applicationVariant
@TaskAction
void createFile() {
if (applicationVariant != null) {
File maindexlistFile = applicationVariant.getVariantData().getScope().getMainDexListFile()
File parentFile = maindexlistFile.getParentFile()
if (!parentFile.exists()) {
parentFile.mkdirs()
}
if (!maindexlistFile.exists() || maindexlistFile.isDirectory()) {
maindexlistFile.createNewFile()
}
}
}
}
project.afterEvaluate {
android.applicationVariants.all { variant ->
def variantName = variant.name.capitalize()
def multidexlistTask = null
try {
multidexlistTask = project.tasks.getByName("transformClassesWithMultidexlistFor${variantName}")
} catch (Throwable e) {
//沒有開啟multiDexEnabled的情況下,會報這個任務找不到的異常
}
if (multidexlistTask != null) {
FastdexCreateMaindexlistFileTask createFileTask = project.tasks.create("fastdexCreate${variantName}MaindexlistFileTask", FastdexCreateMaindexlistFileTask)
createFileTask.applicationVariant = variant
multidexlistTask.dependsOn createFileTask
multidexlistTask.enabled = false
}
}
}
再次執行./gradle assembleDebug
:app:transformClassesWithJarMergingForDebug UP-TO-DATE
:app:collectDebugMultiDexComponents UP-TO-DATE
:app:fastdexCreateDebugMaindexlistFileTask
:app:transformClassesWithMultidexlistForDebug SKIPPED
:app:transformClassesWithDexForDebug UP-TO-DATE
:app:mergeDebugJniLibFolders UP-TO-DATE
:app:transformNative_libsWithMergeJniLibsForDebug UP-TO-DATE
:app:processDebugJavaRes UP-TO-DATE
:app:transformResourcesWithMergeJavaResForDebug UP-TO-DATE
:app:validateConfigSigning
:app:packageDebug UP-TO-DATE
:app:zipalignDebug UP-TO-DATE
:app:assembleDebug UP-TO-DATE
BUILD SUCCESSFUL
Total time: 16.201 secs
這次構建成功說明創建空文件的這種方式可行
=========
我們公司的項目在使用的過程中,發現補丁打包時雖然只改了一個java類,但構建時執行compileDebugJavaWithJavac任務還是花了13秒
BUILD SUCCESSFUL
Total time: 28.222 secs
Task spend time:
554ms :app:processDebugManifest
127ms :app:mergeDebugResources
3266ms :app:processDebugResources
13621ms :app:compileDebugJavaWithJavac
3654ms :app:transformClassesWithJarMergingForDebug
1354ms :app:transformClassesWithDexForDebug
315ms :app:transformNative_libsWithMergeJniLibsForDebug
220ms :app:transformResourcesWithMergeJavaResForDebug
2684ms :app:packageDebug
經過分析由于我們使用了butterknife和tinker,這兩個里面都用到了javax.annotation.processing.AbstractProcessor這個接口做代碼動態生成,所以項目中的java文件如果很多,挨個掃描所有的java文件并且做操作會造成大量的時間浪費,其實他們每次生成的代碼幾乎都是一樣的,因此如果補丁打包時能把這個任務換成自己的實現,僅編譯和快照對比變化的java文件,并把結果輸出到app/build/intermediates/classes/debug,覆蓋原來的class,能大大提高效率,部分代碼如下,詳情看FastdexCustomJavacTask.groovy
public class FastdexCustomJavacTask extends DefaultTask {
......
@TaskAction
void compile() {
......
File androidJar = new File("${project.android.getSdkDirectory()}/platforms/${project.android.getCompileSdkVersion()}/android.jar")
File classpathJar = FastdexUtils.getInjectedJarFile(project,variantName)
project.logger.error("==fastdex androidJar: ${androidJar}")
project.logger.error("==fastdex classpath: ${classpathJar}")
project.ant.javac(
srcdir: patchJavaFileDir,
source: '1.7',
target: '1.7',
encoding: 'UTF-8',
destdir: patchClassesFileDir,
bootclasspath: androidJar,
classpath: classpathJar
)
compileTask.enabled = false
File classesDir = applicationVariant.getVariantData().getScope().getJavaOutputDir()
Files.walkFileTree(patchClassesFileDir.toPath(),new SimpleFileVisitor<Path>(){
@Override
FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Path relativePath = patchClassesFileDir.toPath().relativize(file)
File destFile = new File(classesDir,relativePath.toString())
FileUtils.copyFileUsingStream(file.toFile(),destFile)
return FileVisitResult.CONTINUE
}
})
}
}
project.afterEvaluate {
android.applicationVariants.all { variant ->
def variantName = variant.name.capitalize()
Task compileTask = project.tasks.getByName("compile${variantName}JavaWithJavac")
Task customJavacTask = project.tasks.create("fastdexCustomCompile${variantName}JavaWithJavac", com.dx168.fastdex.build.task.FastdexCustomJavacTask)
customJavacTask.applicationVariant = variant
customJavacTask.variantName = variantName
customJavacTask.compileTask = compileTask
compileTask.dependsOn customJavacTask
}
}
執行./gradlew assembleDebug ,再來一次
BUILD SUCCESSFUL
Total time: 17.555 secs
Task spend time:
1142ms :app:fastdexCustomCompileDebugJavaWithJavac
59ms :app:generateDebugBuildConfig
825ms :app:processDebugManifest
196ms :app:mergeDebugResources
3540ms :app:processDebugResources
3045ms :app:transformClassesWithJarMergingForDebug
1505ms :app:transformClassesWithDexForDebug
391ms :app:transformNative_libsWithMergeJniLibsForDebug
253ms :app:transformResourcesWithMergeJavaResForDebug
3413ms :app:packageDebug
一下子快了10秒左右,good
=========
既然有緩存,就有緩存過期的問題,假如我們添加了某個第三方庫的依賴(依賴關系發生變化),并且在項目代碼中引用了它,如果不清除緩存打出來的包運行起來后肯定會包類找不到,所以需要處理這個事情。
首先怎么拿到依賴關系呢?通過以下代碼可以獲取一個依賴列表
project.afterEvaluate {
project.configurations.all.findAll { !it.allDependencies.empty }.each { c ->
if (c.name.toString().equals("compile")
|| c.name.toString().equals("apt")
|| c.name.toString().equals("_debugCompile".toString())) {
c.allDependencies.each { dep ->
String depStr = "$dep.group:$dep.name:$dep.version"
println("${depStr}")
}
}
}
}
輸入如下
com.dialonce:dialonce-android:2.3.1
com.facebook.fresco:fresco:1.1.0
com.google.guava:guava:18.0
......
com.android.support:design:23.4.0
com.bigkoo:alertview:1.0.2
com.bigkoo:pickerview:2.0.8
可以在第一次全量打包時,和生成項目源碼目錄快照的同一個時間點,獲取一份當前的依賴列表并保存下來,當補丁打包時在獲取一份當前的依賴列表,與之前保存的作對比,如果發生變化就把緩存清除掉
另外最好提供一個主動清除緩存的任務
public class FastdexCleanTask extends DefaultTask {
String variantName
@TaskAction
void clean() {
if (variantName == null) {
FastdexUtils.cleanAllCache()
}
else {
FastdexUtils.cleanCache(project,variantName)
}
}
}
先來一個清除所有緩存的任務
project.tasks.create("fastdexCleanAll", FastdexCleanTask)
然后在根據buildType、flavor創建對應的清除任務
android.applicationVariants.all { variant ->
def variantName = variant.name.capitalize()
//創建清理指定variantName緩存的任務(用戶觸發)
FastdexCleanTask cleanTask = project.tasks.create("fastdexCleanFor${variantName}", FastdexCleanTask)
cleanTask.variantName = variantName
}
==============================
后續的優化計劃
- 1、提高穩定性和容錯性,這個是最關鍵的
- 2、目前補丁打包的時候,是把沒有變化的類從app/build/intermediates/transforms/jarMerging/debug/jars/1/1f/combined.jar中移除,如果能hook掉transformClassesWithJarMergingForDebug這個任務,僅把發生變化的class參與combined.jar的生成,能夠在IO上省出很多的時間
- 3、目前給項目源碼目錄做快照,使用的是文件copy的方式,如果能僅僅只把需要的信息寫在文本文件里,能夠在IO上省出一些時間
- 4、目前還沒有對libs目錄中發生變化做監控,后續需要補上這一塊
- 5、apk的安裝速度比較慢(尤其是ART下由于在安裝時對應用做AOT編譯,所以造成安裝速度特別慢,具體請參考張邵文大神的文章Android N混合編譯與對熱補丁影響解析),通過socket把代碼補丁和資源補丁發送給app,做到免安裝
==============================
這里對打包的流程做下總結
打包流程
全量打包時的流程:
- 1、合并所有的class文件生成一個jar包
- 2、掃描所有的項目代碼并且在構造方法里添加對fastdex.runtime.antilazyload.AntilazyLoad類的依賴
這樣做的目的是為了解決class verify的問題,
詳情請看 安卓App熱補丁動態修復技術介紹 - 3、對項目代碼做快照,為了以后補丁打包時對比那些java文件發生了變化
- 4、對當前項目的所以依賴做快照,為了以后補丁打包時對比依賴是否發生了變化,如果變化需要清除緩存
- 5、調用真正的transform生成dex
- 6、緩存生成的dex,并且把fastdex-runtime.dex插入到dex列表中,假如生成了兩個dex,classes.dex classes2.dex 需要做一下操作
fastdex-runtime.dex => classes.dex
classes.dex => classes2.dex
classes2.dex => classes3.dex
然后運行期在入口Application(fastdex.runtime.FastdexApplication)使用MultiDex把所有的dex加載進來 - @see fastdex.build.transform.FastdexDexTransform
- 7、保存資源映射表,為了保持id的值一致,詳情看
- @see fastdex.build.task.FastdexResourceIdTask
補丁打包時的流程
- 1、檢查緩存的有效性
- @see fastdex.build.variant.FastdexVariant 的prepareEnv方法說明
- 2、掃描所有變化的java文件并編譯成class
- @see fastdex.build.task.FastdexCustomJavacTask
- 3、合并所有變化的class并生成jar包
- 4、生成補丁dex
- 5、把所有的dex按照一定規律放在transformClassesWithMultidexlistFor${variantName}任務的輸出目錄
fastdex-runtime.dex => classes.dex
patch.dex => classes2.dex
dex_cache.classes.dex => classes3.dex
dex_cache.classes2.dex => classes4.dex
dex_cache.classesN.dex => classes(N + 2).dex
=============
整個項目的代碼目前已經開源了 https://github.com/typ0520/fastdex
如果你喜歡本文就來給我們star吧
=============
加快apk的構建速度,如何把編譯時間從130秒降到17秒
加快apk的構建速度,如何把編譯時間從130秒降到17秒(二)
參考的項目與文章
關鍵字:
加快apk編譯速度
加快app編譯速度
加快android編譯速度
加快android studio 編譯速度
android 加快編譯速度
android studio編譯慢
android studio編譯速度優化
android studio gradle 編譯慢