加快apk的構建速度,如何把編譯時間從130秒降到17秒

_typ0520發表於2017-09-28

公司的專案程式碼比較多,每次除錯改動java檔案後要將近2分鐘才能跑起來,實在受不了。在網上找了一大堆配置引數也沒有很明顯的效果, 嘗試使用instant run效果也不怎麼樣,然後又嘗試使用freeline編譯速度還可以但是不穩定,每次失敗後全量編譯很耗費時間,既然沒有好的方案就自己嘗試做。

專案地址: github.com/typ0520/fas…

注: 本文對gradle task做的說明都建立在關閉instant run的前提下
注: 本文所有的程式碼、gradle任務名、任務輸出路徑、全部使用debug這個buildType作說明

優化構建速度首先需要找到那些環節導致構建速度這麼慢,把下面的程式碼放進app/build.gradle裡把時間花費超過50ms的任務時間列印出來

 public class BuildTimeListener implements TaskExecutionListener, BuildListener {
    private Clock clock
    private times = []

    @Override
    void beforeExecute(Task task) {
        clock = new org.gradle.util.Clock()
    }

    @Override
    void afterExecute(Task task, TaskState taskState) {
        def ms = clock.timeInMs
        times.add([ms, task.path])

        //task.project.logger.warn "${task.path} spend ${ms}ms"
    }

    @Override
    void buildFinished(BuildResult result) {
        println "Task spend time:"
        for (time in times) {
            if (time[0] >= 50) {
                printf "%7sms  %s\n", time
            }
        }
    }

    ......
}

project.gradle.addListener(new BuildTimeListener())複製程式碼

執行./gradlew assembleDebug,經過漫長的等待得到以下輸出

Total time: 1 mins 39.566 secs
Task spend time:
     69ms  :app:prepareComAndroidSupportAnimatedVectorDrawable2340Library
    448ms  :app:prepareComAndroidSupportAppcompatV72340Library
     57ms  :app:prepareComAndroidSupportDesign2340Library
     55ms  :app:prepareComAndroidSupportSupportV42340Library
     84ms  :app:prepareComFacebookFrescoImagepipeline110Library
     69ms  :app:prepareComSquareupLeakcanaryLeakcanaryAndroid14Beta2Library
     60ms  :app:prepareOrgXutilsXutils3336Library
     68ms  :app:compileDebugRenderscript
    265ms  :app:processDebugManifest
   1517ms  :app:mergeDebugResources
    766ms  :app:processDebugResources
   2897ms  :app:compileDebugJavaWithJavac
   3117ms  :app:transformClassesWithJarMergingForDebug
   7899ms  :app:transformClassesWithMultidexlistForDebug
  65327ms  :app:transformClassesWithDexForDebug
    151ms  :app:transformNative_libsWithMergeJniLibsForDebug
    442ms  :app:transformResourcesWithMergeJavaResForDebug
   2616ms  :app:packageDebug
    123ms  :app:zipalignDebug複製程式碼

從上面的輸出可以發現總的構建時間為100秒左右(上面的輸出不是按照真正的執行順序輸出的),transformClassesWithDexForDebug任務是最慢的耗費了65秒,它就是我們需要重點優化的任務,首先講下構建過程中主要任務的作用,方便理解後面的hook點

mergeDebugResources任務的作用是解壓所有的aar包輸出到app/build/intermediates/exploded-aar,並且把所有的資原始檔合併到app/build/intermediates/res/merged/debug目錄裡

processDebugManifest任務是把所有aar包裡的AndroidManifest.xml中的節點,合併到專案的AndroidManifest.xml中,並根據app/build.gradle中當前buildType的manifestPlaceholders配置內容替換manifest檔案中的佔位符,最後輸出到app/build/intermediates/manifests/full/debug/AndroidManifest.xml

processDebugResources的作用

  • 1、呼叫aapt生成專案和所有aar依賴的R.java,輸出到app/build/generated/source/r/debug目錄
  • 2、生成資源索引檔案app/build/intermediates/res/resources-debug.ap_
  • 3、把符號表輸出到app/build/intermediates/symbols/debug/R.txt

compileDebugJavaWithJavac這個任務是用來把java檔案編譯成class檔案,輸出的路徑是app/build/intermediates/classes/debug
編譯的輸入目錄有

  • 1、專案原始碼目錄,預設路徑是app/src/main/java,可以通過sourceSets的dsl配置,允許有多個(列印project.android.sourceSets.main.java.srcDirs可以檢視當前所有的原始碼路徑,具體配置可以參考android-doc
  • 2、app/build/generated/source/aidl
  • 3、app/build/generated/source/buildConfig
  • 4、app/build/generated/source/apt(繼承javax.annotation.processing.AbstractProcessor做動態程式碼生成的一些庫,輸出在這個目錄,具體可以參考ButterknifeTinker)的程式碼

transformClassesWithJarMergingForDebug的作用是把compileDebugJavaWithJavac任務的輸出app/build/intermediates/classes/debug,和app/build/intermediates/exploded-aar中所有的classes.jar和libs裡的jar包作為輸入,合併起來輸出到app/build/intermediates/transforms/jarMerging/debug/jars/1/1f/combined.jar,我們在開發中依賴第三方庫的時候有時候報duplicate entry:xxx 的錯誤,就是因為在合併的過程中在不同jar包裡發現了相同路徑的類

transformClassesWithMultidexlistForDebug這個任務花費的時間也很長將近8秒,它有兩個作用

  • 1、掃描專案的AndroidManifest.xml檔案和分析類之間的依賴關係,計算出那些類必須放在第一個dex裡面,最後把分析的結果寫到app/build/intermediates/multi-dex/debug/maindexlist.txt檔案裡面
  • 2、生成混淆配置項輸出到app/build/intermediates/multi-dex/debug/manifest_keep.txt檔案裡

專案裡的程式碼入口是manifest中application節點的屬性android.name配置的繼承自Application的類,在android5.0以前的版本系統只會載入一個dex(classes.dex),classes2.dex .......classesN.dex 一般是使用android.support.multidex.MultiDex載入的,所以如果入口的Application類不在classes.dex裡5.0以下肯定會掛掉,另外當入口Application依賴的類不在classes.dex時初始化的時候也會因為類找不到而掛掉,還有如果混淆的時候類名變掉了也會因為對應不了而掛掉,綜上所述就是這個任務的作用

transformClassesWithDexForDebug這個任務的作用是把包含所有class檔案的jar包轉換為dex,class檔案越多轉換的越慢
輸入的jar包路徑是app/build/intermediates/transforms/jarMerging/debug/jars/1/1f/combined.jar
輸出dex的目錄是build/intermediates/transforms/dex/debug/folders/1000/1f/main

*注意編寫gradle外掛時如果需要使用上面這些路徑不要硬編碼的方式寫死,最好從Android gradle api中去獲取路徑,防止以後發生變化

結合上面的這些資訊重點需要優化的是transformClassesWithDexForDebug這個任務,我的思路是第一次全量打包執行完transformClassesWithDexForDebug任務後把生成的dex快取下來,並且在執行這個任務前對當前所有的java原始檔做快照,以後補丁打包的時候通過當前所有的java檔案資訊和之前的快照做對比,找出變化的java檔案進而得到那些class檔案發生變化,然後把app/build/intermediates/transforms/jarMerging/debug/jars/1/1f/combined.jar中沒有變化的class移除掉,僅把變化class送去生成dex,然後選擇一種熱修復方案把這個dex當做補丁dex載入進來,有思路了後面就是攻克各個技術點

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

####如何拿到transformClassesWithDexForDebug任務執行前後的生命週期
參考了Tinker專案的程式碼,找到下面的實現

public class ImmutableDexTransform extends Transform {
    Project project
    DexTransform dexTransform
    def variant

    ......

    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, IOException, InterruptedException {
        def outputProvider = transformInvocation.getOutputProvider()
        //dex的輸出目錄
        File outputDir = outputProvider.getContentLocation("main", dexTransform.getOutputTypes(), dexTransform.getScopes(), Format.DIRECTORY);
        if (outputDir.exists()) {
            outputDir.delete()
        }
        println("===執行transform前清空dex輸出目錄: ${project.projectDir.toPath().relativize(outputDir.toPath())}")
        dexTransform.transform(transformInvocation)
        if (outputDir.exists()) {
            println("===執行transform後dex輸出目錄不是空的: ${project.projectDir.toPath().relativize(outputDir.toPath())}")
            outputDir.listFiles().each {
                println("===執行transform後: ${it.name}")
            }
        }
    }
}

project.getGradle().getTaskGraph().addTaskExecutionGraphListener(new TaskExecutionGraphListener() {
    @Override
    public void graphPopulated(TaskExecutionGraph taskGraph) {
        for (Task task : taskGraph.getAllTasks()) {
            if (task instanceof TransformTask && task.name.toLowerCase().contains(variant.name.toLowerCase())) {

                if (((TransformTask) task).getTransform() instanceof DexTransform && !(((TransformTask) task).getTransform() instanceof ImmutableDexTransform)) {
                    project.logger.warn("find dex transform. transform class: " + task.transform.getClass() + " . task name: " + task.name)

                    DexTransform dexTransform = task.transform
                    ImmutableDexTransform hookDexTransform = new ImmutableDexTransform(project,
                            variant, dexTransform)
                    project.logger.info("variant name: " + variant.name)

                    Field field = TransformTask.class.getDeclaredField("transform")
                    field.setAccessible(true)
                    field.set(task, hookDexTransform)
                    project.logger.warn("transform class after hook: " + task.transform.getClass())
                    break;
                }
            }
        }
    }
});複製程式碼

把上面的程式碼放進app/build.gradle執行./gradlew assembleDebug

:app:transformClassesWithMultidexlistForDebug
ProGuard, version 5.2.1
Reading program jar [/Users/tong/Projects/fastdex/app/build/intermediates/transforms/jarMerging/debug/jars/1/1f/combined.jar]
Reading library jar [/Users/tong/Applications/android-sdk-macosx/build-tools/23.0.1/lib/shrinkedAndroid.jar]
Preparing output jar [/Users/tong/Projects/fastdex/app/build/intermediates/multi-dex/debug/componentClasses.jar]
  Copying resources from program jar [/Users/tong/Projects/fastdex/app/build/intermediates/transforms/jarMerging/debug/jars/1/1f/combined.jar]
:app:transformClassesWithDexForDebug
===執行transform前清空dex輸出目錄: build/intermediates/transforms/dex/debug/folders/1000/1f/main
......
===執行transform後dex輸出目錄不是空的: build/intermediates/transforms/dex/debug/folders/1000/1f/main
===執行transform後: classes.dex複製程式碼

從上面的日誌輸出證明這個hook點是有效的,在全量打包時執行transform前可以對java原始碼做快照,執行完以後把dex快取下來;在補丁打包執行transform之前對比快照移除沒有變化的class,執行完以後合併快取的dex放進dex輸出目錄

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

####如何做快照與對比快照並拿到變化的class列表
執行下面的程式碼可以獲取所有的專案原始碼目錄

project.android.sourceSets.main.java.srcDirs.each { srcDir->
    println("==srcDir: ${srcDir}")
}複製程式碼

sample工程沒有配置sourceSets,因此輸出的是app/src/main/java

給原始碼目錄做快照,直接通過檔案複製的方式,把所有的srcDir目錄下的java檔案複製到快照目錄下(這裡有個坑,不要使用project.copy {}它會使檔案的lastModified值發生變化,直接使用流copy並且要用原始檔的lastModified覆蓋目標檔案的lastModified)

通過java檔案的長度和上次修改時間兩個要素對比可以得知同一個檔案是否發生變化,通過快照目錄沒有某個檔案而當前目錄有某個檔案可以得知增加了檔案,通過快照目錄有某個檔案但是當前目錄沒有可以得知刪除檔案(為了效率可以不處理刪除,僅造成快取裡有某些用不到的類而已)
舉個例子來說假如專案原始碼的路徑為/Users/tong/fastdex/app/src/main/java,做快照時把這個目錄複製到/Users/tong/fastdex/app/build/fastdex/snapshoot下,當前快照裡的檔案樹為

└── com
    └── dx168
        └── fastdex
            └── sample
                ├── CustomView.java
                ├── MainActivity.java
                └── SampleApplication.java複製程式碼

如果當前原始碼路徑的內容發生變化,當前的檔案樹為

└── com
    └── dx168
        └── fastdex
            └── sample
                ├── CustomView.java
                ├── MainActivity.java(內容已經被修改)
                ├── New.java
                └── SampleApplication.java複製程式碼

通過檔案遍歷對比可以得到這個變化的相對路徑列表

  • com/dx168/fastdex/sample/MainActivity.java
  • com/dx168/fastdex/sample/New.java

通過這個列表進而可以得知變化的class有

  • com/dx168/fastdex/sample/MainActivity.class
  • com/dx168/fastdex/sample/New.class

但是java檔案編譯的時候如果有內部類還會有其它的一些class輸出,比如拿R檔案做下編譯,它的編譯輸出如下

➜  sample git:(master) ls
R.java
➜  sample git:(master) javac R.java 
➜  sample git:(master) ls
R$attr.class      R$dimen.class     R$id.class        R$layout.class    R$string.class    R$styleable.class R.java
R$color.class     R$drawable.class  R$integer.class   R$mipmap.class    R$style.class     R.class
➜  sample git:(master)複製程式碼

另外如果使用了butterknife,還會生成binder類,比如編譯MainActivity.java時生成了
com/dx168/fastdex/sample/MainActivity$$ViewBinder.class

結合上面幾點可以獲取所有變化class的匹配模式

  • com/dx168/fastdex/sample/MainActivity.class
  • com/dx168/fastdex/sample/MainActivity$*.class
  • com/dx168/fastdex/sample/New.class
  • com/dx168/fastdex/sample/New$*.class

有了上面的匹配模式就可以在補丁打包執行transform前把app/build/intermediates/transforms/jarMerging/debug/jars/1/1f/combined.jar中沒有變化的class全部移除掉

project.copy {
    from project.zipTree(combinedJar)
        for (String pattern : patterns) {
            include pattern
        }
    }
    into tmpDir
}
project.ant.zip(baseDir: tmpDir, destFile: patchJar)複製程式碼

然後就可以使用patchJar作為輸入jar生成補丁dex

注: 這種對映方案如果開啟了混淆就對應不上了,需要解析混淆以後產生的mapping檔案才能解決,不過我們也沒有必要在開啟混淆的buildType下做開發開發除錯,所以暫時可以不做這個事情

==============================
有了補丁dex,就可以選擇一種熱修復方案把補丁dex載入進來,這裡方案有好幾種,為了簡單直接選擇android.support.multidex.MultiDex以dex插樁的方式來載入,只需要把dex按照google標準(classes.dex、classes2.dex、classesN.dex)排列好就行了,這裡有兩個技術點

由於patch.dex和快取下來dex裡面有重複的類,當載入引用了重複類的類時會造成pre-verify的錯誤,具體請參考QQ空間團隊寫的安卓App熱補丁動態修復技術介紹
,這篇文章詳細分析了造成pre-verify錯誤的原因,文章裡給的解決方案是往所有引用被修復類的類中插入一段程式碼,並且被插入的這段程式碼所在的類的dex必須是一個單獨的dex,這個dex我們事先準備好,叫做fastdex-runtime.dex,它的程式碼結構是

└── com
    └── dx168
        └── fastdex
            └── runtime
                ├── FastdexApplication.java
                ├── antilazyload
                │   └── AntilazyLoad.java
                └── multidex
                    ├── MultiDex.java
                    ├── MultiDexApplication.java
                    ├── MultiDexExtractor.java
                    └── ZipUtil.java複製程式碼

AntilazyLoad.java就是在注入時被引用的類
MultiDex.java是用來載入classes2.dex - classesN.dex的包,為了防止專案沒有依賴MultiDex,所以把MultiDex的程式碼copy到了我們的package下
FastdexApplication.java的作用後面在說

結合我們的專案需要在全量打包前把app/build/intermediates/transforms/jarMerging/debug/jars/1/1f/combined.jar中所有的專案程式碼的class全部動態插入程式碼(第三方庫由於不在我們的修復範圍內所以為了效率忽略掉),具體的做法是往所有的構造方法中新增對com.dx168.fastdex.runtime.antilazyload.AntilazyLoad的依賴,如下面的程式碼所示

//source class:
public class MainActivity {
}

==>

//dest class:
import com.dx168.fastdex.runtime.antilazyload.AntilazyLoad;
public class MainActivity {
    public MainActivity() {
        System.out.println(Antilazyload.str);
    }
}複製程式碼

動態往class檔案中插入程式碼使用的是asm,我把做測試的時候找到的一些相關資料和程式碼都放到了github上面點我檢視,程式碼比較多隻貼出來一部分,具體請檢視ClassInject.groovy

 private static class MyClassVisitor extends ClassVisitor {
    public MyClassVisitor(ClassVisitor classVisitor) {
        super(Opcodes.ASM5, classVisitor);
    }

    @Override
    public MethodVisitor visitMethod(int access,
                                     String name,
                                     String desc,
                                     String signature,
                                     String[] exceptions) {
        //判斷是否是構造方法
        if ("<init>".equals(name)) {
            MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
            MethodVisitor newMethod = new AsmMethodVisit(mv);
            return newMethod;
        } else {
            return super.visitMethod(access, name, desc, signature, exceptions);
        }
    }
}

static class AsmMethodVisit extends MethodVisitor {
    public AsmMethodVisit(MethodVisitor mv) {
        super(Opcodes.ASM5, mv);
    }

    @Override
    public void visitInsn(int opcode) {
        if (opcode == Opcodes.RETURN) {
            //訪問java/lang/System的靜態常量out
            mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            //訪問AntilazyLoad的靜態變數
            mv.visitFieldInsn(GETSTATIC, "com/dx168/fastdex/runtime/antilazyload/AntilazyLoad", "str", "Ljava/lang/String;");
            //呼叫out的println列印AntilazyLoad.str的值
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
        }
        super.visitInsn(opcode);
    }
}複製程式碼

===============
處理完pre-verify問題,接下來又出現坑了,當補丁dex打好後假如快取的dex有兩個(classes.dex classes2.dex),那麼合併dex後的順序就是
fastdex-runtime.dex 、patch.dex、classes.dex 、classes2.dex (patch.dex必須放在快取的dex之前才能被修復)

fastdex-runtime.dex  => classes.dex
patch.dex            => classes2.dex
classes.dex          => classes3.dex
classes2.dex         => classes4.dex複製程式碼

在講解transformClassesWithMultidexlistForDebug任務時有說過程式入口Application的問題,假如patch.dex中不包含入口Application,apk啟動的時候肯定會報類找不到的錯誤,那麼怎麼解決這個問題呢

    1. 第一個方案:
      transformClassesWithMultidexlistForDebug任務中輸出的maindexlist.txt中所有的class都參與patch.dex的生成
    1. 第二種方案:
      對專案的入口Application做代理,並把這個代理類放在第一個dex裡面,專案的dex按照順序放在後面

第一種方案方案由於必須讓maindexlist.txt中大量的類參與了補丁的生成,與之前儘量減少class檔案參與dex生成的思想是相沖突的,效率相對於第二個方案比較低,另外一個原因是無法保證專案的Application中使用了MultiDex;

第二種方案沒有上述問題,但是如果專案程式碼中有使用getApplication()做強轉就會出問題(參考issue#2),instant run也會有同樣的問題,它的做法是hook系統的api執行期把Application還原回來,所以強轉就不會有問題了,請參考MonkeyPatcher.java(需要翻牆才能開啟,如果看不了就參考FastdexApplication.java的monkeyPatchApplication方法)

綜上所述最終選擇了第二種方案以下是fastdex-runtime.dex中代理Application的程式碼

public class FastdexApplication extends Application {
    public static final String LOG_TAG = "Fastdex";
    private Application realApplication;

    //從manifest檔案的meta_data中獲取真正的專案Application類
    private String getOriginApplicationName(Context context) {
        ApplicationInfo appInfo = null;
        try {
            appInfo = context.getPackageManager().getApplicationInfo(getPackageName(), PackageManager.GET_META_DATA);
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        String msg = appInfo.metaData.getString("FASTDEX_ORIGIN_APPLICATION_CLASSNAME");
        return msg;
    }

    private void createRealApplication(Context context) {
        String applicationClass = getOriginApplicationName(context);
        if (applicationClass != null) {
            Log.d(LOG_TAG, new StringBuilder().append("About to create real application of class name = ").append(applicationClass).toString());

            try {
                Class realClass = Class.forName(applicationClass);
                Constructor constructor = realClass.getConstructor(new Class[0]);
                this.realApplication = ((Application) constructor.newInstance(new Object[0]));
                Log.v(LOG_TAG, new StringBuilder().append("Created real app instance successfully :").append(this.realApplication).toString());
            } catch (Exception e) {
                throw new IllegalStateException(e);
            }
        } else {
            this.realApplication = new Application();
        }
    }

    protected void attachBaseContext(Context context) {
        super.attachBaseContext(context);
        MultiDex.install(context);
        createRealApplication(context);

        if (this.realApplication != null)
            try {
                Method attachBaseContext = ContextWrapper.class
                        .getDeclaredMethod("attachBaseContext", new Class[]{Context.class});

                attachBaseContext.setAccessible(true);
                attachBaseContext.invoke(this.realApplication, new Object[]{context});
            } catch (Exception e) {
                throw new IllegalStateException(e);
            }
    }

    public void onCreate() {
        super.onCreate();

        if (this.realApplication != null) {
            this.realApplication.onCreate();
        }
    }
    ......
}複製程式碼

根據之前的任務說明生成manifest檔案的任務是processDebugManifest,我們只需要在這個任務執行完以後做處理,建立一個實現類為FastdexManifestTask的任務,核心程式碼如下

def ns = new Namespace("http://schemas.android.com/apk/res/android", "android")
def xml = new XmlParser().parse(new InputStreamReader(new FileInputStream(manifestPath), "utf-8"))
def application = xml.application[0]
if (application) {
    QName nameAttr = new QName("http://schemas.android.com/apk/res/android", 'name', 'android');
    def applicationName = application.attribute(nameAttr)
    if (applicationName == null || applicationName.isEmpty()) {
        applicationName = "android.app.Application"
    }
    //替換application的android.name節點
    application.attributes().put(nameAttr, "com.dx168.fastdex.runtime.FastdexApplication")
    def metaDataTags = application['meta-data']
    // remove any old FASTDEX_ORIGIN_APPLICATION_CLASSNAME elements
    def originApplicationName = metaDataTags.findAll {
        it.attributes()[ns.name].equals(FASTDEX_ORIGIN_APPLICATION_CLASSNAME)
    }.each {
        it.parent().remove(it)
    }
    // Add the new FASTDEX_ORIGIN_APPLICATION_CLASSNAME element
    //把原來的Application寫入到meta-data中
    application.appendNode('meta-data', [(ns.name): FASTDEX_ORIGIN_APPLICATION_CLASSNAME, (ns.value): applicationName])
    // Write the manifest file
    def printer = new XmlNodePrinter(new PrintWriter(manifestPath, "utf-8"))
    printer.preserveWhitespace = true
    printer.print(xml)
}
File manifestFile = new File(manifestPath)
if (manifestFile.exists()) {
    File buildDir = FastdexUtils.getBuildDir(project,variantName)
    FileUtils.copyFileUsingStream(manifestFile, new File(buildDir,MANIFEST_XML))
    project.logger.error("fastdex gen AndroidManifest.xml in ${MANIFEST_XML}")
}複製程式碼

使用下面的程式碼把這個任務加進去並保證在processDebugManifest任務執行完畢後執行

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

        //替換專案的Application為com.dx168.fastdex.runtime.FastdexApplication
        FastdexManifestTask manifestTask = project.tasks.create("fastdexProcess${variantName}Manifest", FastdexManifestTask)
        manifestTask.manifestPath = variantOutput.processManifest.manifestOutputFile
        manifestTask.variantName = variantName
        manifestTask.mustRunAfter variantOutput.processManifest

        variantOutput.processResources.dependsOn manifestTask
    }
}複製程式碼

處理完以後manifest檔案application節點android.name屬性的值就變成了com.dx168.fastdex.runtime.FastdexApplication,並且把原來專案的Application的名字寫入到meta-data中,用來執行期給FastdexApplication去讀取

<meta-data android:name="FASTDEX_ORIGIN_APPLICATION_CLASSNAME" android:value="com.dx168.fastdex.sample.SampleApplication"/>複製程式碼

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

#####開發完以上功能後做下面的四次打包做時間對比(其實只做一次並不是太準確,做幾十次測試取時間的平均值這樣才最準)

  • 1、刪除build目錄第一次全量打包(不開啟fastdex)

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

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

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

      BUILD SUCCESSFUL
    
        Total time: 16.5 secs
        Task spend time:
          142ms  :app:processDebugManifest
         1339ms  :app:compileDebugJavaWithJavac
         3291ms  :app:transformClassesWithJarMergingForDebug
         4865ms  :app:transformClassesWithMultidexlistForDebug
         1005ms  :app:transformClassesWithDexForDebug
         2112ms  :app:packageDebug
           76ms  :app:zipalignDebug複製程式碼
打包編號 總時間 transform時間
1 1 mins 46.678s 61770 ms
2 1 mins 57.764s 78990 ms
3 1 mins 05.394s 60718 ms
4 16.5s 1005 ms

通過1和2對比發現,開啟fastdex進行第一次全量的打包時的時間花費比不開啟多了10秒左右,這個主要是注入程式碼和IO上的開銷

通過2和3對比發現,開啟fastdex進行補丁打包時的時間花費比不開啟快了60秒左右,這就是期待已久的構建速度啊^_^

==============================
剛激動一會就尼瑪報了一個錯誤,當修改activity_main.xml時往裡面增加一個控制元件

<TextView
    android:id="@+id/tv2"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />複製程式碼

打出來的包啟動的時候就直接crash掉了

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

錯誤資訊裡的意思是為CustomView的tv1欄位,尋找id=2131493007的view時沒有找到,先反編譯報錯的apk,找到報錯的地方CustomView$$ViewBinder.bind

public class CustomView$$ViewBinder<T extends CustomView>
        implements ViewBinder<T>
{
    public CustomView$$ViewBinder()
    {
        System.out.println(AntilazyLoad.str);
    }

    public Unbinder bind(Finder paramFinder, T paramT, Object paramObject)
    {
        InnerUnbinder localInnerUnbinder = createUnbinder(paramT);
        paramT.tv1 = ((TextView)paramFinder.castView((View)paramFinder
                .findRequiredView(paramObject, 2131493007, "field 'tv1'"), 2131493007, "field 'tv1'"));
        paramT.tv3 = ((TextView)paramFinder.castView((View)paramFinder
                .findRequiredView(paramObject, 2131493008, "field 'tv3'"), 2131493008, "field 'tv3'"));
        return localInnerUnbinder;
    }
    ......
}複製程式碼

CustomView$$ViewBinder這個類是ButterKnife動態生成的,這個值的來源是CustomView的tv1欄位上面的註解,CustomView.class反編譯後如下

public class CustomView extends LinearLayout 
{
    @BindView(2131493007)
    TextView tv1;
    @BindView(2131493008)
    TextView tv3;

    public CustomView(Context paramContext, AttributeSet paramAttributeSet)
    {
        super(paramContext, paramAttributeSet);
        inflate(paramContext, 2130968632, this);
        ButterKnife.bind(this);
        this.tv3.setText(2131099697);
        MainActivity.aa();
        System.out.println(AntilazyLoad.str);
    }
}複製程式碼

看到這裡是不是覺得奇怪,CustomView的原始碼明明是

public class CustomView extends LinearLayout {
    @BindView(R.id.tv1)  TextView tv1;
    @BindView(R.id.tv3)  TextView tv3;

    public CustomView(Context context, AttributeSet attrs) {
        super(context, attrs);
        inflate(context,R.layout.view_custom,this);
        ButterKnife.bind(this);

        tv3.setText(R.string.s3);
        MainActivity.aa();
    }
}複製程式碼

在編譯以後R.id.tv1怎麼就變成數字2131493007了呢,原因是java編譯器做了一個效能優化,如果發現原始檔引用的是一個帶有final描述符的常量,會直接做值copy

反編譯最後一次編譯成功時的R.class結果如下(
app/build/intermediates/classes/debug/com/dx168/fastdex/sample/R.class)

public static final R {
    public static final class id {
        ......

        public static final int tv1 = 2131493008;
        public static final int tv2 = 2131492977;
        public static final int tv3 = 2131493009;

        ......

        public id() {
        }
    }
}複製程式碼

經過分析,當全量打包時R.id.tv1 = 2131493007,由於R檔案中的id都是final的,所以引用R.id.tv1的地方都被替換為它對應的值2131493007了;當在activity_layout.xml中新增名字為tv2的控制元件,然後進行補丁打包時R.id.tv1的值變成了2131493008,而快取的dex對應節點的值還是2131493007,所以在尋找id為2131493007對應的控制元件時因為找不到而掛掉

我的第一個想法是如果在執行完processDebugResources任務後,把R檔案裡id類的所有欄位的final描述符去掉就可以把值copy這個編譯優化繞過去 =>

public static final R {
    public static final class id {
        ......

        public static int tv1 = 2131493008;
        public static int tv2 = 2131492977;
        public static int tv3 = 2131493009;

        ......

        public id() {
        }
    }
}複製程式碼

去掉以後在執行compileDebugJavaWithJavac時編譯出錯了

2.png
2.png

出錯的原因是註解只能引用帶final描述符的常量,除此之外switch語句的case也必須引用常量,具體請檢視oracle對常量表示式的說明

如果採取這個方案,對id的引用就不能使用常量表示式,像ButterKnife這樣的view依賴注入的框架都不能用了,限制性太大這個想法就放棄了

還有一個思路就是修改aapt的原始碼,使多次打包時名字相同id的值保持一致,這個肯定能解決不過工作量太大了就沒有這樣做,之後採用了一個折中的辦法,就是每次把專案中的所有類(除去第三方庫)都參與dex的生成,雖然解決了這個問題但效率一下子降低好多,需要將近40秒才能跑起來還是很慢

==============================
這個問題困擾了好久,直到tinker開源後閱讀它的原始碼TinkerResourceIdTask.groovy時,發現它們也碰到了同樣的問題,並有了一個解決方案,我們的場景和tinker場景在這個問題上是一模一樣的,直接照抄程式碼就解決了這個問題,重要的事情說三遍,感謝tinker、感謝tinker、感謝tinker!!

tinker的解決方案是,打補丁時根據使用者配置的resourceMapping檔案(每次構建成功後輸出的app/build/intermediates/symbols/debug/R.txt),生成public.xml和ids.xml然後放進app/build/intermediates/res/merged/debug/values目錄裡,aapt在處理的時候會根據檔案裡的配置規則去生成,具體這塊的原理請看老羅的文章Android應用程式資源的編譯和打包過程分析(在裡面搜尋public.xml)這裡面有詳細的說明

同上並結合我們的場景,第一次全量打包成功以後把app/build/intermediates/symbols/debug/R.txt快取下來,補丁打包在執行processResources任務前,根據快取的符號表R.txt去生成public.xml和ids.xml然後放進app/build/intermediates/res/merged/debug/values目錄裡,這樣相同名字的id前後的兩次構建值就能保持一致了,程式碼如下FastdexResourceIdTask.groovy

public class FastdexResourceIdTask extends DefaultTask {
    static final String RESOURCE_PUBLIC_XML = "public.xml"
    static final String RESOURCE_IDX_XML = "idx.xml"

    String resDir
    String variantName

    @TaskAction
    def applyResourceId() {
        File buildDir = FastdexUtils.getBuildDir(project,variantName)
        String resourceMappingFile = new File(buildDir,Constant.R_TXT)
        // Parse the public.xml and ids.xml
        if (!FileUtils.isLegalFile(resourceMappingFile)) {
            project.logger.error("==fastdex apply resource mapping file ${resourceMappingFile} is illegal, just ignore")
            return
        }
        File idsXmlFile = new File(buildDir,RESOURCE_IDX_XML)
        File publicXmlFile = new File(buildDir,RESOURCE_PUBLIC_XML)
        if (FileUtils.isLegalFile(idsXmlFile) && FileUtils.isLegalFile(publicXmlFile)) {
            project.logger.error("==fastdex public xml file and ids xml file already exist, just ignore")
            return
        }
        String idsXml = resDir + "/values/ids.xml";
        String publicXml = resDir + "/values/public.xml";
        FileUtils.deleteFile(idsXml);
        FileUtils.deleteFile(publicXml);
        List<String> resourceDirectoryList = new ArrayList<String>()
        resourceDirectoryList.add(resDir)

        project.logger.error("==fastdex we build ${project.getName()} apk with apply resource mapping file ${resourceMappingFile}")
        Map<RDotTxtEntry.RType, Set<RDotTxtEntry>> rTypeResourceMap = PatchUtil.readRTxt(resourceMappingFile)

        AaptResourceCollector aaptResourceCollector = AaptUtil.collectResource(resourceDirectoryList, rTypeResourceMap)
        PatchUtil.generatePublicResourceXml(aaptResourceCollector, idsXml, publicXml)
        File publicFile = new File(publicXml)

        if (publicFile.exists()) {
            FileUtils.copyFileUsingStream(publicFile, publicXmlFile)
            project.logger.error("==fastdex gen resource public.xml in ${RESOURCE_PUBLIC_XML}")
        }
        File idxFile = new File(idsXml)
        if (idxFile.exists()) {
            FileUtils.copyFileUsingStream(idxFile, idsXmlFile)
            project.logger.error("==fastdex gen resource idx.xml in ${RESOURCE_IDX_XML}")
        }
    }
}

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

        //保持補丁打包時R檔案中相同的節點和第一次打包時的值保持一致
        FastdexResourceIdTask applyResourceTask = project.tasks.create("fastdexProcess${variantName}ResourceId", com.dx168.fastdex.build.task.FastdexResourceIdTask)
        applyResourceTask.resDir = variantOutput.processResources.resDir
        applyResourceTask.variantName = variantName
        variantOutput.processResources.dependsOn applyResourceTask
    }
}複製程式碼

如果專案中的資源特別多,第一次補丁打包生成public.xml和ids.xml時會佔用一些時間,最好做一次快取,以後的補丁打包直接使用快取的public.xml和ids.xml**

==============================
解決了上面的原理性問題後,接下來繼續做優化,上面有講到 transformClassesWithMultidexlistForDebug任務的作用,由於採用了隔離Application的做法,所有的專案程式碼都不在classes.dex中,這個用來分析那些專案中的類需要放在classes.dex的任務就沒有意義了,直接禁掉它

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

        def multidexlistTask = null
        try {
            multidexlistTask = project.tasks.getByName("transformClassesWithMultidexlistFor${variantName}")
        } catch (Throwable e) {
            //沒有開啟multiDexEnabled的情況下,會報這個任務找不到的異常
        }
        if (multidexlistTask != null) {
            multidexlistTask.enabled = false
        }
    }
}複製程式碼

禁掉以後,執行./gradle assembleDebug,在構建過程中掛掉了

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

FAILURE: Build failed with an exception.
......
BUILD FAILED複製程式碼

從上面的日誌的第一行發現transformClassesWithMultidexlistForDebug任務確實禁止掉了,後面跟著一個SKIPPED的輸出,但是執行transformClassesWithDexForDebug任務時報app/build/intermediates/multi-dex/debug/maindexlist.txt (No such file or directory)
,原因是transformClassesWithDexForDebug任務會檢查這個檔案是否存在,既然這樣就在執行transformClassesWithDexForDebug任務前建立一個空檔案,看是否還會報錯,程式碼如下

public class FastdexCreateMaindexlistFileTask extends DefaultTask {
    def applicationVariant

    @TaskAction
    void createFile() {
        if (applicationVariant != null) {
            File maindexlistFile = applicationVariant.getVariantData().getScope().getMainDexListFile()
            File parentFile = maindexlistFile.getParentFile()
            if (!parentFile.exists()) {
                parentFile.mkdirs()
            }

            if (!maindexlistFile.exists() || maindexlistFile.isDirectory()) {
                maindexlistFile.createNewFile()
            }
        }
    }
}

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

        def multidexlistTask = null
        try {
            multidexlistTask = project.tasks.getByName("transformClassesWithMultidexlistFor${variantName}")
        } catch (Throwable e) {
            //沒有開啟multiDexEnabled的情況下,會報這個任務找不到的異常
        }
        if (multidexlistTask != null) {
            FastdexCreateMaindexlistFileTask createFileTask = project.tasks.create("fastdexCreate${variantName}MaindexlistFileTask", FastdexCreateMaindexlistFileTask)
            createFileTask.applicationVariant = variant

            multidexlistTask.dependsOn createFileTask
            multidexlistTask.enabled = false
        }
    }
}複製程式碼

再次執行./gradle assembleDebug

:app:transformClassesWithJarMergingForDebug UP-TO-DATE
:app:collectDebugMultiDexComponents UP-TO-DATE
:app:fastdexCreateDebugMaindexlistFileTask
:app:transformClassesWithMultidexlistForDebug SKIPPED
:app:transformClassesWithDexForDebug UP-TO-DATE
:app:mergeDebugJniLibFolders UP-TO-DATE
:app:transformNative_libsWithMergeJniLibsForDebug UP-TO-DATE
:app:processDebugJavaRes UP-TO-DATE
:app:transformResourcesWithMergeJavaResForDebug UP-TO-DATE
:app:validateConfigSigning
:app:packageDebug UP-TO-DATE
:app:zipalignDebug UP-TO-DATE
:app:assembleDebug UP-TO-DATE

BUILD SUCCESSFUL

Total time: 16.201 secs複製程式碼

這次構建成功說明建立空檔案的這種方式可行

=========

我們公司的專案在使用的過程中,發現補丁打包時雖然只改了一個java類,但構建時執行compileDebugJavaWithJavac任務還是花了13秒

BUILD SUCCESSFUL

Total time: 28.222 secs
Task spend time:
    554ms  :app:processDebugManifest
    127ms  :app:mergeDebugResources
   3266ms  :app:processDebugResources
  13621ms  :app:compileDebugJavaWithJavac
   3654ms  :app:transformClassesWithJarMergingForDebug
   1354ms  :app:transformClassesWithDexForDebug
    315ms  :app:transformNative_libsWithMergeJniLibsForDebug
    220ms  :app:transformResourcesWithMergeJavaResForDebug
   2684ms  :app:packageDebug複製程式碼

經過分析由於我們使用了butterknife和tinker,這兩個裡面都用到了javax.annotation.processing.AbstractProcessor這個介面做程式碼動態生成,所以專案中的java檔案如果很多,挨個掃描所有的java檔案並且做操作會造成大量的時間浪費,其實他們每次生成的程式碼幾乎都是一樣的,因此如果補丁打包時能把這個任務換成自己的實現,僅編譯和快照對比變化的java檔案,並把結果輸出到app/build/intermediates/classes/debug,覆蓋原來的class,能大大提高效率,部分程式碼如下,詳情看FastdexCustomJavacTask.groovy

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

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

執行./gradlew assembleDebug ,再來一次

BUILD SUCCESSFUL

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

一下子快了10秒左右,good

=========
既然有快取,就有快取過期的問題,假如我們新增了某個第三方庫的依賴(依賴關係發生變化),並且在專案程式碼中引用了它,如果不清除快取打出來的包執行起來後肯定會包類找不到,所以需要處理這個事情。
首先怎麼拿到依賴關係呢?通過以下程式碼可以獲取一個依賴列表

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

輸入如下

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

可以在第一次全量打包時,和生成專案原始碼目錄快照的同一個時間點,獲取一份當前的依賴列表並儲存下來,當補丁打包時在獲取一份當前的依賴列表,與之前儲存的作對比,如果發生變化就把快取清除掉

另外最好提供一個主動清除快取的任務

public class FastdexCleanTask extends DefaultTask {
    String variantName

    @TaskAction
    void clean() {
        if (variantName == null) {
            FastdexUtils.cleanAllCache()
        }
        else {
            FastdexUtils.cleanCache(project,variantName)
        }
    }
}複製程式碼

先來一個清除所有快取的任務

project.tasks.create("fastdexCleanAll", FastdexCleanTask)複製程式碼

然後在根據buildType、flavor建立對應的清除任務

android.applicationVariants.all { variant ->
    def variantName = variant.name.capitalize()
    //建立清理指定variantName快取的任務(使用者觸發)
    FastdexCleanTask cleanTask = project.tasks.create("fastdexCleanFor${variantName}", FastdexCleanTask)
    cleanTask.variantName = variantName
}複製程式碼

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

###後續的優化計劃

  • 1、提高穩定性和容錯性,這個是最關鍵的
  • 2、目前補丁打包的時候,是把沒有變化的類從app/build/intermediates/transforms/jarMerging/debug/jars/1/1f/combined.jar中移除,如果能hook掉transformClassesWithJarMergingForDebug這個任務,僅把發生變化的class參與combined.jar的生成,能夠在IO上省出很多的時間
  • 3、目前給專案原始碼目錄做快照,使用的是檔案copy的方式,如果能僅僅只把需要的資訊寫在文字檔案裡,能夠在IO上省出一些時間
  • 4、目前還沒有對libs目錄中發生變化做監控,後續需要補上這一塊
  • 5、apk的安裝速度比較慢(尤其是ART下由於在安裝時對應用做AOT編譯,所以造成安裝速度特別慢,具體請參考張邵文大神的文章Android N混合編譯與對熱補丁影響解析),通過socket把程式碼補丁和資源補丁傳送給app,做到免安裝

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

###這裡對打包的流程做下總結

打包流程

全量打包時的流程:
  • 1、合併所有的class檔案生成一個jar包
  • 2、掃描所有的專案程式碼並且在構造方法裡新增對fastdex.runtime.antilazyload.AntilazyLoad類的依賴
    這樣做的目的是為了解決class verify的問題,
    詳情請看 安卓App熱補丁動態修復技術介紹
  • 3、對專案程式碼做快照,為了以後補丁打包時對比那些java檔案發生了變化
  • 4、對當前專案的所以依賴做快照,為了以後補丁打包時對比依賴是否發生了變化,如果變化需要清除快取
  • 5、呼叫真正的transform生成dex
  • 6、快取生成的dex,並且把fastdex-runtime.dex插入到dex列表中,假如生成了兩個dex,classes.dex classes2.dex 需要做一下操作
    fastdex-runtime.dex => classes.dex
    classes.dex => classes2.dex
    classes2.dex => classes3.dex
    然後執行期在入口Application(fastdex.runtime.FastdexApplication)使用MultiDex把所有的dex載入進來
  • @see fastdex.build.transform.FastdexTransform
  • 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

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

整個專案的程式碼目前已經開源了 github.com/typ0520/fas…

如果你喜歡本文就來給我們star吧

=============
加快apk的構建速度,如何把編譯時間從130秒降到17秒
加快apk的構建速度,如何把編譯時間從130秒降到17秒(二)

參考的專案與文章

Instant Run

Tinker

安卓App熱補丁動態修復技術介紹

Android應用程式資源的編譯和打包過程分析

關鍵字:
加快apk編譯速度
加快app編譯速度
加快android編譯速度
加快android studio 編譯速度
android 加快編譯速度
android studio編譯慢
android studio編譯速度優化
android studio gradle 編譯慢

本文出自typ0520的簡書部落格www.jianshu.com/p/53923d8f2…

相關文章