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

_typ0520發表於2019-03-04

fastdex.png
fastdex.png

在上一篇文章加快apk的構建速度,如何把編譯時間從130秒降到17秒中講了優化的思路與初步的實現,經過一段時間的優化效能和穩定性都有很大的提高,這裡要感謝大家提的建議以及github上的issue,這篇文章就把主要優化的點和新功能以及填的坑介紹下。

專案地址: github.com/typ0520/fas…
對應tag: github.com/typ0520/fas…
demo程式碼: github.com/typ0520/fas…

注: 建議把fastdex的程式碼和demo程式碼拉下來,本文中的絕大部分例子在demo工程中可以直接跑
注: 本文對gradle task做的說明都建立在關閉instant run的前提下
注: 本文所有的程式碼、gradle任務名、任務輸出路徑、全部使用debug這個buildType作說明
注: 本文使用./gradlew執行任務是在mac下,如果是windows換成gradlew.bat

###一、攔截transformClassesWithJarMergingForDebug任務

之前補丁打包的時候,是把沒有變化的類從app/build/intermediates/transforms/jarMerging/debug/jars/1/1f/combined.jar中移除,這樣的做法有兩個問題

  • 1、combined.jar這個檔案是 transformClassesWithJarMergingForDebug任務輸出的,存在這個任務的前提是開啟了multidex,如果沒有開啟那麼執行到 transformClassesWithDexForDebug任務時輸入就不在是combined.jar,而是專案的classes目錄(app/build/intermediates/classes/debug)和依賴的library輸出的jar以及第三方庫的jar;
  • 2、如果存在transformClassesWithJarMergingForDebug任務,先花費大量時間合成combined.jar,然後在把沒有變化的類從combined.jar中移除,這樣效率太低了,如果繞過combined.jar的合成直接拿變化class去生成dex對效率會有很大的提高

現在首先需要拿到transformClassesWithJarMergingForDebug任務執行前後的生命週期,實現的方式和攔截transformClassesWithDexForDebug時用的方案差不多,完整的測試程式碼地址
github.com/typ0520/fas…

public class MyJarMergingTransform extends Transform {
    Transform base

    MyJarMergingTransform(Transform base) {
        this.base = base
    }

    @Override
    void transform(TransformInvocation invocation) throws TransformException, IOException, InterruptedException {
        List<JarInput> jarInputs = Lists.newArrayList();
        List<DirectoryInput> dirInputs = Lists.newArrayList();
        for (TransformInput input : invocation.getInputs()) {
            jarInputs.addAll(input.getJarInputs());
        }
        for (TransformInput input : invocation.getInputs()) {
            dirInputs.addAll(input.getDirectoryInputs());
        }
        for (JarInput jarInput : jarInputs) {
            println("==jarmerge jar      : ${jarInput.file}")
        }
        for (DirectoryInput directoryInput : dirInputs) {
            println("==jarmerge directory: ${directoryInput.file}")
        }
        File combinedJar = invocation.outputProvider.getContentLocation("combined", base.getOutputTypes(), base.getScopes(), Format.JAR);
        println("==combinedJar exists ${combinedJar.exists()} ${combinedJar}")
        base.transform(invocation)
        println("==combinedJar exists ${combinedJar.exists()} ${combinedJar}")
    }
}

public class MyDexTransform extends Transform {
    Transform base

    MyDexTransform(Transform base) {
        this.base = base
    }

    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, IOException, InterruptedException {
        List<JarInput> jarInputs = Lists.newArrayList();
        List<DirectoryInput> dirInputs = Lists.newArrayList();
        for (TransformInput input : transformInvocation.getInputs()) {
            jarInputs.addAll(input.getJarInputs());
        }
        for (TransformInput input : transformInvocation.getInputs()) {
            dirInputs.addAll(input.getDirectoryInputs());
        }
        for (JarInput jarInput : jarInputs) {
            println("==dex jar      : ${jarInput.file}")
        }
        for (DirectoryInput directoryInput : dirInputs) {
            println("==dex directory: ${directoryInput.file}")
        }
        base.transform(transformInvocation)
    }
}

project.afterEvaluate {
    android.applicationVariants.all { variant ->
        project.getGradle().getTaskGraph().addTaskExecutionGraphListener(new TaskExecutionGraphListener() {
            @Override
            public void graphPopulated(TaskExecutionGraph taskGraph) {
                for (Task task : taskGraph.getAllTasks()) {
                    if (task.getProject().equals(project) && task instanceof TransformTask && task.name.toLowerCase().contains(variant.name.toLowerCase())) {
                        Transform transform = ((TransformTask) task).getTransform()
                        //如果開啟了multidex有這個任務
                        if ((((transform instanceof JarMergingTransform)) && !(transform instanceof MyJarMergingTransform))) {
                            project.logger.error("==fastdex find jarmerging transform. transform class: " + task.transform.getClass() + " . task name: " + task.name)

                            MyJarMergingTransform jarMergingTransform = new MyJarMergingTransform(transform)
                            Field field = getFieldByName(task.getClass(),'transform')
                            field.setAccessible(true)
                            field.set(task,jarMergingTransform)
                        }

                        if ((((transform instanceof DexTransform)) && !(transform instanceof MyDexTransform))) {
                            project.logger.error("==fastdex find dex transform. transform class: " + task.transform.getClass() + " . task name: " + task.name)

                            //代理DexTransform,實現自定義的轉換
                            MyDexTransform fastdexTransform = new MyDexTransform(transform)
                            Field field = getFieldByName(task.getClass(),'transform')
                            field.setAccessible(true)
                            field.set(task,fastdexTransform)
                        }
                    }
                }
            }
        });
    }
}複製程式碼

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

  • 開啟multidex(multiDexEnabled true)時的日誌輸出**

    :app:mergeDebugAssets
    :app:transformClassesWithJarMergingForDebug
    ==jarmerge jar      : /Users/tong/Projects/fastdex-test-project/jarmerging-test/app/libs/exist-in-app-libs-2.1.2.jar
    ==jarmerge jar      : /Users/tong/Projects/fastdex-test-project/jarmerging-test/app/build/intermediates/exploded-aar/com.android.support/multidex/1.0.1/jars/classes.jar
    ==jarmerge jar      : /Users/tong/Applications/android-sdk-macosx/extras/android/m2repository/com/android/support/support-annotations/23.3.0/support-annotations-23.3.0.jar
    ==jarmerge jar      : /Users/tong/Projects/fastdex-test-project/jarmerging-test/app/build/intermediates/exploded-aar/com.jakewharton/butterknife/8.0.1/jars/classes.jar
    ==jarmerge jar      : /Users/tong/.gradle/caches/modules-2/files-2.1/com.jakewharton/butterknife-annotations/8.0.1/345b89f45d02d8b09400b472fab7b7e38f4ede1f/butterknife-annotations-8.0.1.jar
    ==jarmerge jar      : /Users/tong/Projects/fastdex-test-project/jarmerging-test/javalib/build/libs/javalib.jar
    ==jarmerge jar      : /Users/tong/Projects/fastdex-test-project/jarmerging-test/app/build/intermediates/exploded-aar/jarmerging-test/aarlib/unspecified/jars/classes.jar
    ==jarmerge directory: /Users/tong/Projects/fastdex-test-project/jarmerging-test/app/build/intermediates/classes/debug
    ==combinedJar exists false /Users/tong/Projects/fastdex-test-project/jarmerging-test/app/build/intermediates/transforms/jarMerging/debug/jars/1/1f/combined.jar
    ==combinedJar exists true /Users/tong/Projects/fastdex-test-project/jarmerging-test/app/build/intermediates/transforms/jarMerging/debug/jars/1/1f/combined.jar
    :app:transformClassesWithMultidexlistForDebug
    :app:transformClassesWithDexForDebug
    ===dex jar      : /Users/tong/Projects/fastdex-test-project/jarmerging-test/app/build/intermediates/transforms/jarMerging/debug/jars/1/1f/combined.jar
    :app:mergeDebugJniLibFolders複製程式碼
  • 關閉multidex(multiDexEnabled false)時的日誌輸出**

:app:mergeDebugAssets
:app:transformClassesWithDexForDebug
===dex jar      : /Users/tong/Projects/fastdex-test-project/jarmerging-test/app/libs/exist-in-app-libs-2.1.2.jar
===dex jar      : /Users/tong/Applications/android-sdk-macosx/extras/android/m2repository/com/android/support/support-annotations/23.3.0/support-annotations-23.3.0.jar
===dex jar      : /Users/tong/Projects/fastdex-test-project/jarmerging-test/app/build/intermediates/exploded-aar/com.jakewharton/butterknife/8.0.1/jars/classes.jar
===dex jar      : /Users/tong/.gradle/caches/modules-2/files-2.1/com.jakewharton/butterknife-annotations/8.0.1/345b89f45d02d8b09400b472fab7b7e38f4ede1f/butterknife-annotations-8.0.1.jar
===dex jar      : /Users/tong/Projects/fastdex-test-project/jarmerging-test/javalib/build/libs/javalib.jar
===dex jar      : /Users/tong/Projects/fastdex-test-project/jarmerging-test/app/build/intermediates/exploded-aar/jarmerging-test/aarlib/unspecified/jars/classes.jar
===dex directory: /Users/tong/Projects/fastdex-test-project/jarmerging-test/app/build/intermediates/classes/debug
:app:mergeDebugJniLibFolders複製程式碼

從上面的日誌輸出可以看出,只需要在下圖紅色箭頭指的地方做patch.jar的生成就可以了

flow.png
flow.png

另外之前全量打包做asm code注入的時候是遍歷combined.jar如果entry對應的是專案程式碼就做注入,反之認為是第三方庫跳過注入(第三方庫不在修復之列,為了節省注入花費的時間所以忽略);現在攔截了jarmerge任務,直接掃描所有的DirectoryInput對應目錄下的所有class做注入就行了,效率會比之前的做法有很大提升

###二、對直接依賴的library工程做支援

以下面這個工程為例
github.com/typ0520/fas…

project.png
project.png

這個工程包含三個子工程

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

app工程依賴aarlib和javalib

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

對於使用compile project(':xxx')這種方式依賴的工程,在apk的構建過程中是當做jar處理的,從攔截transformClassesWithJarMergingForDebug任務時的日誌輸出可以證明

===dex jar: /Users/tong/Projects/fastdex-test-project/jarmerging-test/javalib/build/libs/javalib.jar
===dex jar: /Users/tong/Projects/fastdex-test-project/jarmerging-test/app/build/intermediates/exploded-aar/jarmerging-test/aarlib/unspecified/jars/classes.jar複製程式碼

之前修改了library工程的程式碼補丁打包之所以沒有生效,就是因為補丁打包時只從DirectoryInput中抽離變化的class而沒有對library工程的輸出jar做抽離,這個時候就需要知道JarInput中那些屬於library工程那些屬於第三方庫。最直接的方式是通過檔案系統路徑區分,但是這樣需要排除掉library工程中直接放在libs目錄下依賴的jar比如

==jarmerge jar: /Users/tong/Projects/fastdex-test-project/jarmerging-test/app/libs/exist-in-app-libs-2.1.2.jar複製程式碼

其次如果依賴的library目錄和app工程不在同一個目錄下還要做容錯的判斷

libgroup.png
libgroup.png

==jarmerge jar: /Users/tong/Projects/fastdex-test-project/jarmerging-test/libgroup/javalib2/build/libs/javalib2.jar複製程式碼

最終放棄了判斷路徑的方式,轉而去找android gradle的api拿到每個library工程的輸出jar路徑,翻閱了原始碼發現2.0.02.2.02.3.0對應的api都不一樣,通過判斷版本的方式可以解決,程式碼如下

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

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

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

        LibDependency that = (LibDependency) o

        if (jarFile != that.jarFile) return false

        return true
    }

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

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

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

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

        libraryDependencies.add(library)

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

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

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

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

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

        libraryDependencies.add(library)

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

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

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

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

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

把上面的這段程式碼,和下面的程式碼都放進build.gradle中

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

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

task resolveProjectDependency<< {

}複製程式碼

執行./gradlew resolveProjectDependency 可以得到以下輸出

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

有了這些路徑我們就可以在遍歷JarInput是進行匹配,只要在這個路徑列表中的都屬於library工程的輸出jar,用到這塊有兩處地方

  • 全量打包時注入library輸出jar ClassInject.groovy

    public static void injectJarInputFiles(FastdexVariant fastdexVariant, HashSet<File> jarInputFiles) {
      def project = fastdexVariant.project
      long start = System.currentTimeMillis()
    
      Set<LibDependency> libraryDependencies = fastdexVariant.libraryDependencies
      List<File> projectJarFiles = new ArrayList<>()
      //獲取所有依賴工程的輸出jar (compile project(':xxx'))
      for (LibDependency dependency : libraryDependencies) {
          projectJarFiles.add(dependency.jarFile)
      }
      if (fastdexVariant.configuration.debug) {
          project.logger.error("==fastdex projectJarFiles : ${projectJarFiles}")
      }
      for (File file : jarInputFiles) {
          if (!projectJarFiles.contains(file)) {
              continue
          }
          project.logger.error("==fastdex ==inject jar: ${file}")
          ClassInject.injectJar(fastdexVariant,file,file)
      }
      long end = System.currentTimeMillis()
      project.logger.error("==fastdex inject complete jar-size: ${projectJarFiles.size()} , use: ${end - start}ms")
    }複製程式碼
  • 補丁打包時從library工程輸出jar中抽離變化的class JarOperation.groovy
public static void generatePatchJar(FastdexVariant fastdexVariant, TransformInvocation transformInvocation, File patchJar) throws IOException {
    Set<LibDependency> libraryDependencies = fastdexVariant.libraryDependencies
    Map<String,String> jarAndProjectPathMap = new HashMap<>()
    List<File> projectJarFiles = new ArrayList<>()
    //獲取所有依賴工程的輸出jar (compile project(':xxx'))
    for (LibDependency dependency : libraryDependencies) {
        projectJarFiles.add(dependency.jarFile)
        jarAndProjectPathMap.put(dependency.jarFile.absolutePath,dependency.dependencyProject.projectDir.absolutePath)
    }

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

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

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

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

三、 全新的快照對比模組

fastdex目前需要對比的地方有三處

  • 全量打包時對當前依賴的庫做快照,補丁打包時對比是否發生變化
  • 檢測app工程和所有依賴的android library工程中所有AndroidManifest.xml是上次打包相比是否發生變化(免安裝模組要用到manifest檔案發生變化,必須要重新安裝app)
  • 全量打包時對所有的java檔案和kotlin檔案做快照,補丁打包時對比那些原始檔發生變化

以第一種場景為例,說下對比的原理,全量打包時生成一個文字檔案把當前的依賴寫進去以換行符分割

/Users/tong/Projects/fastdex/sample/app/libs/fm-sdk-2.1.2.jar
/Users/tong/Projects/fastdex/sample/javalib/build/libs/javalib.jar複製程式碼

補丁打包時先把這個文字檔案讀取到ArrayList中,然後把當前的依賴列表頁放進ArrayList中
,通過以下操作可以獲取新增項、刪除項,只要發現有刪除項和新增項就認為依賴發生了變化

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

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

//獲取刪除項
Set<String> deletedNodes = new HashSet<>();
deletedNodes.addAll(old);
deletedNodes.removeAll(now);

//新增項
Set<String> increasedNodes = new HashSet<>();
increasedNodes.addAll(now);
//如果不用ArrayList套一層有時候會發生移除不掉的情況 why?
increasedNodes.removeAll(old);

//需要檢測是否變化的列表
Set<String> needDiffNodes = new HashSet<>();
needDiffNodes.addAll(now);
needDiffNodes.addAll(old);
needDiffNodes.removeAll(deletedNodes);
needDiffNodes.removeAll(increasedNodes);複製程式碼

注: 文字的對比不存在更新,但是檔案對比是存在這種情況的

所有的快照對比都是基於上面這段程式碼的抽象,具體可以參考這裡
github.com/typ0520/fas…

四、 dex merge

全量打包以後,按照正常的開發節奏發生變化的原始檔會越來越多,相應的參與dex生成的class也會越來越多,這樣會導致補丁打包速度越來越慢。
解決這個問題比較簡單的方式是把每次生成的patch.dex放進全量打包時的dex快取中(必須排在之前的dex前面),並且更新下原始碼快照,這樣做有兩個壞處

  • 1、每次補丁打包時都必須對class檔案做注入,為了解決上篇文章中提到的pre-verify錯誤
  • 2、每次補丁打包都需要快取patch.dex,會導致下面這個目錄的dex越來越多
    app/build/intermediates/transforms/dex/debug/folders/1000/1f/main複製程式碼

解決第二個問題的方案是把patch.dex中的class合併到快取的dex中,這樣就不需要保留所有的patch.dex了,一個比較棘手的問題是如果快取的dex的方法數已經有65535個了,在往裡面加新增的class,肯定會爆掉了,最終fastdex選擇的方案是第一次觸發dex merge時直接把patch.dex扔進快取(merged-patch.dex),以後在觸發dex merge時就拿patch.dex和merged-patch.dex做合併(這樣做也存在潛在的問題,如果變化的class特別多也有可能導致合併dex時出現65535的錯誤)

解決第一個問題是加了一個可配置選項,預設是3個以上的原始檔發生變化時觸發merge,這樣即不用每次都做程式碼注入和merge操作,也能在原始檔變化多的時候恢復狀態

這個dex merge工具是從freeline裡找到的,感興趣的話可以把下載下來試著呼叫下
github.com/typ0520/fas…

java -jar fastdex-dex-merge.jar output.dex patch.dex merged-patch.dex複製程式碼

dex-merge.png
dex-merge.png

五、支援註解生成器

在現階段的Android開發中,註解越來越流行起來,比如ButterKnifeEventBus等等都選擇使用註解來配置。按照處理時期,註解又分為兩種型別,一種是執行時註解,另一種是編譯時註解,執行時註解由於效能問題被一些人所詬病。編譯時註解的核心依賴APT(Annotation Processing Tools)實現,原理是在某些程式碼元素上(如型別、函式、欄位等)新增註解,在編譯時編譯器會檢查AbstractProcessor的子類,並且呼叫該型別的process函式,然後將新增了註解的所有元素都傳遞到process函式中,使得開發人員可以在編譯期進行相應的處理,例如,根據註解生成新的Java類,這也就是ButterKnifeEventBus等開源庫的基本原理。Java API已經提供了掃描原始碼並解析註解的框架,你可以繼承AbstractProcessor類來提供實現自己的解析註解邏輯

-- 引用自blog.csdn.net/industrious…

雖然能提高執行期的效率但也給開發帶來一些麻煩

  • AbstractProcessor這些類只有在編譯期才會用到,執行期是用不到的,但是如果通過compile方式依賴的包,會把這些類都打包進dex中

    以這個專案為例(建議把程式碼拉下來,後面好幾個地方會用到)
    github.com/typ0520/fas…

    app中依賴了butterknife7.0.1

    dependencies {
      compile 'com.jakewharton:butterknife:7.0.1'
    }複製程式碼

    butterknife7.0.1中的註解生成器叫ButterKnifeProcessor

butterknife.png
butterknife.png

執行./gradlew app:assembleDebug

app.png
app.png

從上圖可以看出ButterKnifeProcessor.class被打包進dex中了

  • 為了避免上述的這種情況,可以通過annotationProcessor的方式引入,butterknife8.8.1把ButterKnifeProcessor相關的獨立成了butterknife-compiler模組,butterknife模組只保留執行期需要使用的程式碼

app2中依賴了butterknife8.8.1

apply plugin: 'com.jakewharton.butterknife'

  dependencies {
    compile 'com.jakewharton:butterknife:8.8.1'
    annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'
}複製程式碼

執行./gradlew app2:assembleDebug

app2.png
app2.png

從上圖可以看出butterknife.compiler包下所有的程式碼都沒有被打包進dex。雖然通過annotationProcessor依賴AbstractProcessor相關程式碼有上述好處,但是會造成增量編譯不可用,簡單地說就是正常的專案執行compileDebugJavaWithJavac任務呼叫javac的時候只會編譯內容發生變化的java原始檔,如果使用了annotationProcessor每次執行compileDebugJavaWithJavac任務都會把專案中所有的java檔案都參與編譯,想象一下如果專案中有成百上千個java檔案編譯起來那酸爽。我們可以做個測試,還是使用這個專案
github.com/typ0520/fas…

annotation-generators包含三個子專案

  • app依賴7.0.1

    compile 'com.jakewharton:butterknife:7.0.1'複製程式碼
  • app2依賴8.8.1

    dependencies {
      compile 'com.jakewharton:butterknife:8.8.1'
      annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'
    }複製程式碼
  • app3不包含任何AbstractProcessor

這三個子工程都包含兩個java檔案
com/github/typ0520/annotation_generators/HAHA.java
com/github/typ0520/annotation_generators/MainActivity.java

測試的思路是先檢查MainActivity.class檔案的更新時間,然後修改HAHA.java執行編譯,最後在檢查MainActivity.class檔案的更新時間是否和編譯之前的一致,如果一致說明增量編譯可用,反之不可用

通過increment_compile_test.sh這個shell指令碼來做測試(使用windows的同學可以手動做測試V_V)

#!/bin/bash

sh gradlew assembleDebug

test_increment_compile() {
    echo "========測試${1}是否支援增量, ${2}"

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

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

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

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

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

test_increment_compile app "compile 'com.jakewharton:butterknife:7.0.1'"
test_increment_compile app2 "annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'"
test_increment_compile app3 "沒有用任何AbstractProcessor"複製程式碼

執行sh increment_compile_test.sh

increment_compare.png
increment_compare.png

日誌的輸出可以證明上面所描述的

既然原生不支援那麼我們就在自定義的java compile任務中來做這個事情,通過之前的快照模組可以對比出那些java原始檔發生了變化,那麼就可以自己拼接javac命令引數然後呼叫僅編譯變化的java檔案

demo中寫了一個編譯任務方便大家理解這些引數都是怎麼拼接的,程式碼太多了這裡就不貼出來了
github.com/typ0520/fas…
github.com/typ0520/fas…

可以呼叫./gradlew mycompile1 或者 ./gradlew mycompile2看下最終拼接出來的命令

mycompile1.png
mycompile1.png

fastdex中對應模組的程式碼在
github.com/typ0520/fas…

六、填過的坑

解決的bug這塊本來是不準備說的,因為這塊最有價值的東西不是解決問題本身,而是怎麼發現和重現問題的,這塊確實不太好描述V_V,應簡友的要求還是挑了一些相對比較有營養的問題說下,主要還是說解決的方法,至於問題是怎樣定位和重現的只能盡力描述了。

1、issues#2

github.com/typ0520/fas…
@hexi

導致這個問題的原因是專案中原來的YtxApplication類被替換成了FastdexApplication,當在activity中執行類似於下面的操作時就會報ClassCastException

MyApplication app = (MyApplication) getApplication();複製程式碼

解決的方法是在instant-run的原始碼裡找到的,執行期把android api裡所有引用Application的地方把例項替換掉

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

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

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

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

        // Figure out how loaded APKs are stored.

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

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

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

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

具體可以參考測試工程的程式碼
github.com/typ0520/fas…

2、issues#6

github.com/typ0520/fas…
@YuJunKui1995

這個錯誤的表現是如果專案裡包含baidumapapi_v2_0_0.jar,正常打包是沒問題的,只要使用fastdex就會報下面這個錯誤

Error:Error converting bytecode to dex:
Cause: PARSE ERROR:
class name (com/baidu/platform/comapi/map/a) does not match path (com/baidu/platform/comapi/map/A.class)
...while parsing com/baidu/platform/comapi/map/A.class複製程式碼

經過分析使用fastdex打包時會有解壓jar然後在壓縮的操作,使用下面這段程式碼做測試
github.com/typ0520/fas…

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

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

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

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

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

    int status = process.waitFor()

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

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

    }
}複製程式碼

執行./gradlew gen_dex2

dex-error.png
dex-error.png

果不其然重現了這個問題,查了資料發現mac和windows一樣檔案系統大小寫不敏感,如果jar包裡有A.class,解壓後有可能就變成a.class了,所以生成dex的時候會報不匹配的錯誤(類似的問題也會影響git,之前就發現改了一個檔名字的大小寫git檢測不到變化,當時沒有細想這個問題,現在看來也是同樣的問題)。知道問題是怎麼發生的那麼解決就簡單了,既然在檔案系統操作jar會有問題,那就放在記憶體做,對應java的api就是ZipOutputStream和ZipInputStream。

對於mac下檔案系統大小寫不敏感可以在終端執行下面這段命令,體會下輸出

echo 'a' > a.txt;echo 'A' > A.txt;cat a.txt;cat A.txt複製程式碼

echo_a_b.png
echo_a_b.png

3、issues#8

github.com/typ0520/fas…
@dongzy

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

java.io.FileNotFoundException: E:\newkp\kuaipiandroid\NewKp\app\src\main\java\com\dx168\fastdex\runtime\FastdexApplication.java (系統找不到指定的路徑。)複製程式碼

出現這個錯誤的原因是@dongzy的專案中使用了tinkerpatch的一鍵接入,tinkerpatch的gradle外掛也有Application替換的功能,必須保證fastdexProcess{variantName}Manifest任務在最後執行才行

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

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

if (tinkerPatchManifestTask != null) {
    manifestTask.mustRunAfter tinkerPatchManifestTask
}複製程式碼

4、issues#xxoo

這段不是解決問題的, 忍不住吐槽下這哥們,覺得浪費了他的時間,上來就是“親測無軟用,建議大家不要用什麼什麼的”,搞的我非常鬱悶,果斷用知乎上的一篇文章回應了過去
zhuanlan.zhihu.com/p/25768464
後來經過溝通發現這哥們在一個正常打包3秒的專案上做的測試,我也是無語了

。。。。。。
。。。。。。

說實在的真的希望大家對開源專案多一點尊重,覺得對自己有幫助就用。如果覺得不好,可以選擇提建議,也可以選擇默默離開,如果有時間有能力可以參與進來優化,解決自己工作問題的同時也服務了大家。在這個快節奏的社會大家的時間都寶貴,你覺得測試一下浪費了時間就開始吐槽,有沒有想到開源專案的作者犧牲了大量的個人時間在解決一個一個問題、為了解決新功能的技術點一個一個方案的做測試做對比呢?

注: 如果專案的dex生成小於10秒,建議不要使用fastdex,幾乎是感知不到效果的。

gradle編譯速度優化建議

  • 不要使用類似於com.android.tools.build:gradle:2.+的動態依賴,不然每次啟動編譯都需要請求maven server對比當前是否是新版本
  • 少直接使用compile project(':xxx')依賴library工程,如果module比較多編譯開始的時候需要遍歷module根據build.gradle配置專案,另外每個library工程都包含大量的任務每個任務都需要對比輸入和輸出,這些小任務疊加到一塊的時間消耗也是很可觀的。 建議把library工程打成aar包丟到公司的maven伺服器上,別和我說開發階段library經常改直接依賴方便,每次修改打包到maven伺服器上沒有那麼麻煩。我們團隊的專案都是隻有一個乾淨的application工程,library程式碼全丟進了maven伺服器,dex方法數在12w左右,使用fastdex修改了幾個java檔案能穩定在8秒左右完成打包、傳送補丁和app重啟

  • 任何情況都別在library工程裡使用flavor

具體可以參考@依然範特稀西寫的這篇文章
Android 優化APP 構建速度的17條建議

5、issues#17

github.com/typ0520/fas…
@junchenChow

[ant:javac] : warning: 'includeantruntime' was not set, defaulting to build.sysclasspath=last; set to false for repeatable builds
[ant:javac] /Users/zhoujunchen/as/xx/app/build/fastdex/DevelopDebug/custom-combind/com/xx/xx/xx/xx/CourseDetailActivity.java:229: 錯誤: -source 1.7 中不支援 lambda 表示式
[ant:javac] wrapperControlsView.postDelayed(() -> wrapperControlsView.initiativeRefresh(), 500L);
[ant:javac] ^
[ant:javac] (請使用 -source 8 或更高版本以啟用 lambda 表示式)
[ant:javac] /Users/zhoujunchen/as/android-donguo/app/build/fastdex/DevelopDebug/custom-combind/com/xx/xx/xx/xx/CourseDetailActivity.java:489: 錯誤: -source 1.7 中不支援方法引用
[ant:javac] .subscribe(conf -> ShareHelper.share(this, conf), Throwable::printStackTrace);
[ant:javac] ^
[ant:javac] (請使用 -source 8 或更高版本以啟用方法引用)
[ant:javac] 2 個錯誤
:app:fastdexCustomCompileDevelopDebugJavaWithJavac FAILED
有什麼選項沒開啟麼 不支援lambda?複製程式碼

這個錯誤的原因是之前自定義的編譯任務寫死了使用1.7去編譯,查閱gradle-retrolambda的原始碼找到了這些程式碼
github.com/evant/gradl…

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

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

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

    transform.putVariant(variant)
}

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

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

從這些程式碼中我們可以得知以下資訊

  • 需要使用jdk1.8裡的javac去編譯
  • sourceCompatibility和targetCompatibility必須設定成1.8
  • classpath中需要新增1.8的rt.jar

有了這些資訊就可以在自定義的編譯任務做處理了

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

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

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

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

具體可以參考
github.com/typ0520/fas…

6、issues#24 #29 #35 #36

github.com/typ0520/fas…
@wsf5918 @ysnows @jianglei199212 @tianshaokai @Razhan

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

正常情況下開啟multidex並且minSdkVersion < 21時會存在transformClassesWithJarMergingForDebug任務,用來合併所有的JarInput和DirectoryInput並且輸出到build/intermediates/transforms/jarMerging/debug/jars/1/1f/combined.jar,而這個錯誤的表現是丟失了jarMerging任務,所以走到dexTransform時本來期望只有一個combined.jar,但是由於沒有合併所以jar input的個數是117。當時由於一直無法重現這個問題,所以就採用加標示的手段解決的,具體是當走到FastdexJarMergingTransform並且執行完成以後就把executedJarMerge設定為true,走到dexTransform時判斷如果開啟了multidex並且executedJarMerge==false就說明是丟失了jarMerge任務,這個時候呼叫com.android.build.gradle.internal.transforms.JarMerger手動合併就可以解決了,具體可以參考GradleUtils的executeMerge方法
github.com/typ0520/fas…

後來在開發中發現了丟失jarMerging任務的規律如下

  • com.android.tools.build:gradle的版本 >= 2.3.0
  • build-type選擇的是debug
  • 只有點studio的run按鈕打包時,命令列呼叫不行
  • 點選run按鈕打包時選擇的裝置是>=6.0的裝置

看到這裡第三點的表現是不是很奇怪,命令列和studio點選run最終都是走gradle的流程,既然表現不一樣有可能是傳的引數不一樣,把下面這段程式碼放進build.gradle中

println "projectProperties: " + project.gradle.startParameter.projectProperties複製程式碼

點選studio的run按鈕選擇一個6.0的裝置

studio_run.png
studio_run.png

得到以下輸出

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

使用上面的這些引數一個一個做測試,發現是android.injected.build.api=23這個引數影響的,我們可以用這個測試專案做下測試
github.com/typ0520/fas…

執行./gradlew clean assembleDebug -Pandroid.injected.build.api=23
注: gradle傳自定義的引數是以-P開頭

miss_jar_merge.png
miss_jar_merge.png

從上面的日誌輸出中可以看出重現了丟失jarMerge任務,我們再來總結下重現這個問題的條件

  • com.android.tools.build:gradle的版本 >= 2.3.0
  • build-type選擇的是debug
  • 啟動引數包含android.injected.build.api並且>=23

有了結論還沒完,之所以2.3.0是這個行為是因為引入了build-cache機制,不合並是為了做jar級別的dex快取,這樣每次執行dex transform時只有第一次時第三方庫才參與生成,為了提高效率也不會合並dex,如果專案比較大apk中可能是出現幾十個甚至上百個dex

classesN.png
classesN.png

目前fastdex由於做了jar合併相當於把這個特性禁掉了,後面會考慮不再做合併使之能用dex快取,這樣全量打包時的速度應該可以提高很多,另外還可以引入到除了debug別的build-type打包中,還有裝置必須大於6.0問題也可以處理下,理論上5.0以後系統就可以載入多個dex了,不知道為什麼這個閾值設定的是6.0而不是5.0

==========================
本來想一鼓作氣把這幾個月做的功能和優化全在這篇一併說完的,寫著寫著簡書提示字數快超限了,無奈只能分篇寫了,下一篇主要講免安裝模組和idea外掛的實現。快到中秋節了提前祝大家中秋快樂。未完待續,後會有期。。。。。。

如果你喜歡本文就來給我們star吧
github.com/typ0520/fas…

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

參考的專案與文章

Instant Run
Tinker
Freeline
安卓App熱補丁動態修復技術介紹
Android應用程式資源的編譯和打包過程分析

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

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

相關文章