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

fastdex.png

本文已授權(quán)微信公眾號:鴻洋(hongyangAndroid)原創(chuàng)首發(fā)

在上一篇文章加快apk的構(gòu)建速度卸夕,如何把編譯時間從130秒降到17秒中講了優(yōu)化的思路與初步的實現(xiàn)衬浑,經(jīng)過一段時間的優(yōu)化性能和穩(wěn)定性都有很大的提高,這里要感謝大家提的建議以及github上的issue瑟捣,這篇文章就把主要優(yōu)化的點(diǎn)和新功能以及填的坑介紹下馋艺。

項目地址: https://github.com/typ0520/fastdex
對應(yīng)tag: https://github.com/typ0520/fastdex/releases/tag/v.0.5.1
demo代碼: https://github.com/typ0520/fastdex-test-project

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

一踱蛀、攔截transformClassesWithJarMergingForDebug任務(wù)

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

  • 1率拒、combined.jar這個文件是* transformClassesWithJarMergingForDebug任務(wù)輸出的崩泡,存在這個任務(wù)的前提是開啟了multidex,如果沒有開啟那么執(zhí)行到 transformClassesWithDexForDebug*任務(wù)時輸入就不在是combined.jar猬膨,而是項目的classes目錄(app/build/intermediates/classes/debug)和依賴的library輸出的jar以及第三方庫的jar角撞;
  • 2、如果存在transformClassesWithJarMergingForDebug任務(wù)勃痴,先花費(fèi)大量時間合成combined.jar谒所,然后在把沒有變化的類從combined.jar中移除,這樣效率太低了沛申,如果繞過combined.jar的合成直接拿變化class去生成dex對效率會有很大的提高

現(xiàn)在首先需要拿到transformClassesWithJarMergingForDebug任務(wù)執(zhí)行前后的生命周期劣领,實現(xiàn)的方式和攔截transformClassesWithDexForDebug時用的方案差不多,完整的測試代碼地址
https://github.com/typ0520/fastdex-test-project/tree/master/jarmerging-test

public class MyJarMergingTransform extends Transform {
    Transform base

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

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

public class MyDexTransform extends Transform {
    Transform base

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

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

project.afterEvaluate {
    android.applicationVariants.all { variant ->
        project.getGradle().getTaskGraph().addTaskExecutionGraphListener(new TaskExecutionGraphListener() {
            @Override
            public void graphPopulated(TaskExecutionGraph taskGraph) {
                for (Task task : taskGraph.getAllTasks()) {
                    if (task.getProject().equals(project) && task instanceof TransformTask && task.name.toLowerCase().contains(variant.name.toLowerCase())) {
                        Transform transform = ((TransformTask) task).getTransform()
                        //如果開啟了multidex有這個任務(wù)
                        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,實現(xiàn)自定義的轉(zhuǎn)換
                            MyDexTransform fastdexTransform = new MyDexTransform(transform)
                            Field field = getFieldByName(task.getClass(),'transform')
                            field.setAccessible(true)
                            field.set(task,fastdexTransform)
                        }
                    }
                }
            }
        });
    }
}

把上面的代碼放進(jìn)app/build.gradle執(zhí)行./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
  • 關(guān)閉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

另外之前全量打包做asm code注入的時候是遍歷combined.jar如果entry對應(yīng)的是項目代碼就做注入铁材,反之認(rèn)為是第三方庫跳過注入(第三方庫不在修復(fù)之列尖淘,為了節(jié)省注入花費(fèi)的時間所以忽略);現(xiàn)在攔截了jarmerge任務(wù)著觉,直接掃描所有的DirectoryInput對應(yīng)目錄下的所有class做注入就行了村生,效率會比之前的做法有很大提升

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

以下面這個工程為例
https://github.com/typ0520/fastdex-test-project/tree/master/jarmerging-test

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的構(gòu)建過程中是當(dāng)做jar處理的梆造,從攔截transformClassesWithJarMergingForDebug任務(wù)時的日志輸出可以證明

===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工程的代碼補(bǔ)丁打包之所以沒有生效,就是因為補(bǔ)丁打包時只從DirectoryInput中抽離變化的class而沒有對library工程的輸出jar做抽離葬毫,這個時候就需要知道JarInput中那些屬于library工程那些屬于第三方庫。最直接的方式是通過文件系統(tǒng)路徑區(qū)分屡穗,但是這樣需要排除掉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
==jarmerge jar: /Users/tong/Projects/fastdex-test-project/jarmerging-test/libgroup/javalib2/build/libs/javalib2.jar

最終放棄了判斷路徑的方式贴捡,轉(zhuǎn)而去找android gradle的api拿到每個library工程的輸出jar路徑,翻閱了源碼發(fā)現(xiàn)2.0.0村砂、2.2.0烂斋、2.3.0對應(yīng)的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
    }
}

把上面的這段代碼,和下面的代碼都放進(jìn)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<< {

}

執(zhí)行./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是進(jìn)行匹配评腺,只要在這個路徑列表中的都屬于library工程的輸出jar蝶念,用到這塊有兩處地方

public static void injectJarInputFiles(FastdexVariant fastdexVariant, HashSet<File> jarInputFiles) {
    def project = fastdexVariant.project
    long start = System.currentTimeMillis()

    Set<LibDependency> libraryDependencies = fastdexVariant.libraryDependencies
    List<File> projectJarFiles = new ArrayList<>()
    //獲取所有依賴工程的輸出jar (compile project(':xxx'))
    for (LibDependency dependency : libraryDependencies) {
        projectJarFiles.add(dependency.jarFile)
    }
    if (fastdexVariant.configuration.debug) {
        project.logger.error("==fastdex projectJarFiles : ${projectJarFiles}")
    }
    for (File file : jarInputFiles) {
        if (!projectJarFiles.contains(file)) {
            continue
        }
        project.logger.error("==fastdex ==inject jar: ${file}")
        ClassInject.injectJar(fastdexVariant,file,file)
    }
    long end = System.currentTimeMillis()
    project.logger.error("==fastdex inject complete jar-size: ${projectJarFiles.size()} , use: ${end - start}ms")
}
public static void generatePatchJar(FastdexVariant fastdexVariant, TransformInvocation transformInvocation, File patchJar) throws IOException {
    Set<LibDependency> libraryDependencies = fastdexVariant.libraryDependencies
    Map<String,String> jarAndProjectPathMap = new HashMap<>()
    List<File> projectJarFiles = new ArrayList<>()
    //獲取所有依賴工程的輸出jar (compile project(':xxx'))
    for (LibDependency dependency : libraryDependencies) {
        projectJarFiles.add(dependency.jarFile)
        jarAndProjectPathMap.put(dependency.jarFile.absolutePath,dependency.dependencyProject.projectDir.absolutePath)
    }

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

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

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

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

三、 全新的快照對比模塊

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

  • 全量打包時對當(dāng)前依賴的庫做快照媒殉,補(bǔ)丁打包時對比是否發(fā)生變化
  • 檢測app工程和所有依賴的android library工程中所有AndroidManifest.xml是上次打包相比是否發(fā)生變化(免安裝模塊要用到manifest文件發(fā)生變化担敌,必須要重新安裝app)
  • 全量打包時對所有的java文件和kotlin文件做快照,補(bǔ)丁打包時對比那些源文件發(fā)生變化

以第一種場景為例廷蓉,說下對比的原理全封,全量打包時生成一個文本文件把當(dāng)前的依賴寫進(jìn)去以換行符分割

/Users/tong/Projects/fastdex/sample/app/libs/fm-sdk-2.1.2.jar
/Users/tong/Projects/fastdex/sample/javalib/build/libs/javalib.jar

補(bǔ)丁打包時先把這個文本文件讀取到ArrayList中,然后把當(dāng)前的依賴列表頁放進(jìn)ArrayList中
桃犬,通過以下操作可以獲取新增項刹悴、刪除項,只要發(fā)現(xiàn)有刪除項和新增項就認(rèn)為依賴發(fā)生了變化

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套一層有時候會發(fā)生移除不掉的情況 why?
increasedNodes.removeAll(old);

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

注: 文本的對比不存在更新疫萤,但是文件對比是存在這種情況的

所有的快照對比都是基于上面這段代碼的抽象颂跨,具體可以參考這里
https://github.com/typ0520/fastdex/tree/master/fastdex-build-lib/src/main/java/fastdex/build/lib/snapshoot

四、 dex merge

全量打包以后扯饶,按照正常的開發(fā)節(jié)奏發(fā)生變化的源文件會越來越多恒削,相應(yīng)的參與dex生成的class也會越來越多,這樣會導(dǎo)致補(bǔ)丁打包速度越來越慢尾序。
解決這個問題比較簡單的方式是把每次生成的patch.dex?放進(jìn)全量打包時的dex緩存中(必須排在之前的dex前面)钓丰,并且更新下源代碼快照,這樣做有兩個壞處

  • 1每币、每次補(bǔ)丁打包時都必須對class文件做注入携丁,為了解決上篇文章中提到的pre-verify錯誤
  • 2、每次補(bǔ)丁打包都需要緩存patch.dex兰怠,會導(dǎo)致下面這個目錄的dex越來越多
app/build/intermediates/transforms/dex/debug/folders/1000/1f/main

解決第二個問題的方案是把patch.dex中的class合并到緩存的dex中梦鉴,這樣就不需要保留所有的patch.dex了,一個比較棘手的問題是如果緩存的dex的方法數(shù)已經(jīng)有65535個了,在往里面加新增的class揭保,肯定會爆掉了肥橙,最終fastdex選擇的方案是第一次觸發(fā)dex merge時直接把patch.dex扔進(jìn)緩存(merged-patch.dex),以后在觸發(fā)dex merge時就拿patch.dex和merged-patch.dex做合并(這樣做也存在潛在的問題秸侣,如果變化的class特別多也有可能導(dǎo)致合并dex時出現(xiàn)65535的錯誤)

解決第一個問題是加了一個可配置選項存筏,默認(rèn)是3個以上的源文件發(fā)生變化時觸發(fā)merge,這樣即不用每次都做代碼注入和merge操作味榛,也能在源文件變化多的時候恢復(fù)狀態(tài)

這個dex merge工具是從freeline里找到的椭坚,感興趣的話可以把下載下來試著調(diào)用下
https://github.com/typ0520/fastdex-test-project/tree/master/dex-merge

java -jar fastdex-dex-merge.jar output.dex patch.dex merged-patch.dex
dex-merge.png

五、支持注解生成器

在現(xiàn)階段的Android開發(fā)中搏色,注解越來越流行起來善茎,比如ButterKnifeEventBus等等都選擇使用注解來配置频轿。按照處理時期巾表,注解又分為兩種類型汁掠,一種是運(yùn)行時注解,另一種是編譯時注解集币,運(yùn)行時注解由于性能問題被一些人所詬病考阱。編譯時注解的核心依賴APT(Annotation Processing Tools)實現(xiàn),原理是在某些代碼元素上(如類型鞠苟、函數(shù)乞榨、字段等)添加注解,在編譯時編譯器會檢查AbstractProcessor的子類当娱,并且調(diào)用該類型的process函數(shù)吃既,然后將添加了注解的所有元素都傳遞到process函數(shù)中,使得開發(fā)人員可以在編譯期進(jìn)行相應(yīng)的處理跨细,例如鹦倚,根據(jù)注解生成新的Java類,這也就是ButterKnife冀惭,EventBus等開源庫的基本原理震叙。Java API已經(jīng)提供了掃描源碼并解析注解的框架,你可以繼承AbstractProcessor類來提供實現(xiàn)自己的解析注解邏輯

-- 引用自http://blog.csdn.net/industriously/article/details/53932425

雖然能提高運(yùn)行期的效率但也給開發(fā)帶來一些麻煩

  • AbstractProcessor這些類只有在編譯期才會用到散休,運(yùn)行期是用不到的媒楼,但是如果通過compile方式依賴的包,會把這些類都打包進(jìn)dex中

    以這個項目為例(建議把代碼拉下來戚丸,后面好幾個地方會用到)
    https://github.com/typ0520/fastdex-test-project/annotation-generators

    app中依賴了butterknife7.0.1

    dependencies {
      compile 'com.jakewharton:butterknife:7.0.1'
    }
    

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

butterknife.png

執(zhí)行./gradlew app:assembleDebug

app.png

從上圖可以看出ButterKnifeProcessor.class被打包進(jìn)dex中了

  • 為了避免上述的這種情況划址,可以通過annotationProcessor的方式引入,butterknife8.8.1把ButterKnifeProcessor相關(guān)的獨(dú)立成了butterknife-compiler模塊限府,butterknife模塊只保留運(yùn)行期需要使用的代碼

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'
}

執(zhí)行./gradlew app2:assembleDebug

app2.png

從上圖可以看出butterknife.compiler包下所有的代碼都沒有被打包進(jìn)dex夺颤。雖然通過annotationProcessor依賴AbstractProcessor相關(guān)代碼有上述好處,但是會造成增量編譯不可用胁勺,簡單地說就是正常的項目執(zhí)行compileDebugJavaWithJavac任務(wù)調(diào)用javac的時候只會編譯內(nèi)容發(fā)生變化的java源文件拂共,如果使用了annotationProcessor每次執(zhí)行compileDebugJavaWithJavac任務(wù)都會把項目中所有的java文件都參與編譯,想象一下如果項目中有成百上千個java文件編譯起來那酸爽姻几。我們可以做個測試,還是使用這個項目
https://github.com/typ0520/fastdex-test-project/annotation-generators

annotation-generators包含三個子項目

  • app依賴7.0.1

    compile 'com.jakewharton:butterknife:7.0.1'
    
  • app2依賴8.8.1

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

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

測試的思路是先檢查MainActivity.class文件的更新時間势告,然后修改HAHA.java執(zhí)行編譯蛇捌,最后在檢查MainActivity.class文件的更新時間是否和編譯之前的一致,如果一致說明增量編譯可用咱台,反之不可用

通過increment_compile_test.sh這個shell腳本來做測試(使用windows的同學(xué)可以手動做測試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沒有發(fā)生變化"
    else
        echo "${1}只修改HAHA.java,MainActivity.class發(fā)生變化"
    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"

執(zhí)行sh increment_compile_test.sh

increment_compare.png

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

既然原生不支持那么我們就在自定義的java compile任務(wù)中來做這個事情回溺,通過之前的快照模塊可以對比出那些java源文件發(fā)生了變化春贸,那么就可以自己拼接javac命令參數(shù)然后調(diào)用僅編譯變化的java文件

demo中寫了一個編譯任務(wù)方便大家理解這些參數(shù)都是怎么拼接的混萝,代碼太多了這里就不貼出來了
https://github.com/typ0520/fastdex-test-project/annotation-generators/app/build.gradle
https://github.com/typ0520/fastdex-test-project/annotation-generators/app2/build.gradle

可以調(diào)用./gradlew mycompile1 或者 ./gradlew mycompile2看下最終拼接出來的命令

mycompile1.png

fastdex中對應(yīng)模塊的代碼在
https://github.com/typ0520/fastdex/blob/master/fastdex-gradle/src/main/groovy/fastdex/build/task/FastdexCustomJavacTask.groovy

六、填過的坑

解決的bug這塊本來是不準(zhǔn)備說的萍恕,因為這塊最有價值的東西不是解決問題本身逸嘀,而是怎么發(fā)現(xiàn)和重現(xiàn)問題的,這塊確實不太好描述V_V允粤,應(yīng)簡友的要求還是挑了一些相對比較有營養(yǎng)的問題說下崭倘,主要還是說解決的方法,至于問題是怎樣定位和重現(xiàn)的只能盡力描述了类垫。

1司光、issues#2

https://github.com/typ0520/fastdex/issues/2
@hexi

導(dǎo)致這個問題的原因是項目中原來的YtxApplication類被替換成了FastdexApplication,當(dāng)在activity中執(zhí)行類似于下面的操作時就會報ClassCastException

MyApplication app = (MyApplication) getApplication();

解決的方法是在instant-run的源碼里找到的悉患,運(yùn)行期把a(bǔ)ndroid api里所有引用Application的地方把實例替換掉

public static void monkeyPatchApplication( Context context,
                                           Application bootstrap,
                                           Application realApplication,
                                           String externalResourceFile) {
   
    try {
        // Find the ActivityThread instance for the current thread
        Class<?> activityThread = Class.forName("android.app.ActivityThread");
        Object currentActivityThread = getActivityThread(context, activityThread);

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

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

        // Figure out how loaded APKs are stored.

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

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

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

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

具體可以參考測試工程的代碼
https://github.com/typ0520/fastdex-test-project/tree/master/replace_application

2残家、issues#6

https://github.com/typ0520/fastdex/issues/6
@YuJunKui1995

這個錯誤的表現(xiàn)是如果項目里包含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

經(jīng)過分析使用fastdex打包時會有解壓jar然后在壓縮的操作坞淮,使用下面這段代碼做測試
https://github.com/typ0520/fastdex-test-project/tree/master/issue%236-desc

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

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

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

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

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

    int status = process.waitFor()

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

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

    }
}

執(zhí)行./gradlew gen_dex2

dex-error.png

果不其然重現(xiàn)了這個問題,查了資料發(fā)現(xiàn)mac和windows一樣文件系統(tǒng)大小寫不敏感迂求,如果jar包里有A.class碾盐,解壓后有可能就變成a.class了,所以生成dex的時候會報不匹配的錯誤(類似的問題也會影響git揩局,之前就發(fā)現(xiàn)改了一個文件名字的大小寫git檢測不到變化毫玖,當(dāng)時沒有細(xì)想這個問題,現(xiàn)在看來也是同樣的問題)凌盯。知道問題是怎么發(fā)生的那么解決就簡單了付枫,既然在文件系統(tǒng)操作jar會有問題,那就放在內(nèi)存做驰怎,對應(yīng)java的api就是ZipOutputStream和ZipInputStream阐滩。

對于mac下文件系統(tǒng)大小寫不敏感可以在終端執(zhí)行下面這段命令,體會下輸出

echo 'a' > a.txt;echo 'A' > A.txt;cat a.txt;cat A.txt
echo_a_b.png

3县忌、issues#8

https://github.com/typ0520/fastdex/issues/8
@dongzy

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

java.io.FileNotFoundException: E:\newkp\kuaipiandroid\NewKp\app\src\main\java\com\dx168\fastdex\runtime\FastdexApplication.java (系統(tǒng)找不到指定的路徑掂榔。)

出現(xiàn)這個錯誤的原因是@dongzy的項目中使用了tinkerpatch的一鍵接入,tinkerpatch的gradle插件也有Application替換的功能症杏,必須保證fastdexProcess{variantName}Manifest任務(wù)在最后執(zhí)行才行

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

這段不是解決問題的, 忍不住吐槽下這哥們厉颤,覺得浪費(fèi)了他的時間穴豫,上來就是“親測無軟用,建議大家不要用什么什么的”逼友,搞的我非常郁悶精肃,果斷用知乎上的一篇文章回應(yīng)了過去
https://zhuanlan.zhihu.com/p/25768464
后來經(jīng)過溝通發(fā)現(xiàn)這哥們在一個正常打包3秒的項目上做的測試秤涩,我也是無語了

。司抱。筐眷。。状植。浊竟。

說實在的真的希望大家對開源項目多一點(diǎn)尊重,覺得對自己有幫助就用津畸。如果覺得不好振定,可以選擇提建議,也可以選擇默默離開肉拓,如果有時間有能力可以參與進(jìn)來優(yōu)化后频,解決自己工作問題的同時也服務(wù)了大家。在這個快節(jié)奏的社會大家的時間都寶貴暖途,你覺得測試一下浪費(fèi)了時間就開始吐槽卑惜,有沒有想到開源項目的作者犧牲了大量的個人時間在解決一個一個問題、為了解決新功能的技術(shù)點(diǎn)一個一個方案的做測試做對比呢驻售?

注: 如果項目的dex生成小于10秒露久,建議不要使用fastdex,幾乎是感知不到效果的欺栗。

gradle編譯速度優(yōu)化建議

  • 不要使用類似于com.android.tools.build:gradle:2.+的動態(tài)依賴毫痕,不然每次啟動編譯都需要請求maven server對比當(dāng)前是否是新版本

  • 少直接使用compile project(':xxx')依賴library工程,如果module比較多編譯開始的時候需要遍歷module根據(jù)build.gradle配置項目迟几,另外每個library工程都包含大量的任務(wù)每個任務(wù)都需要對比輸入和輸出消请,這些小任務(wù)疊加到一塊的時間消耗也是很可觀的。 建議把library工程打成aar包丟到公司的maven服務(wù)器上类腮,別和我說開發(fā)階段library經(jīng)常改直接依賴方便臊泰,每次修改打包到maven服務(wù)器上沒有那么麻煩。我們團(tuán)隊的項目都是只有一個干凈的application工程蚜枢,library代碼全丟進(jìn)了maven服務(wù)器缸逃,dex方法數(shù)在12w左右,使用fastdex修改了幾個java文件能穩(wěn)定在8秒左右完成打包厂抽、發(fā)送補(bǔ)丁和app重啟

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

具體可以參考@依然范特稀西寫的這篇文章
Android 優(yōu)化APP 構(gòu)建速度的17條建議

5需频、issues#17

https://github.com/typ0520/fastdex/issues/17
@junchenChow

[ant:javac] : warning: 'includeantruntime' was not set, defaulting to build.sysclasspath=last; set to false for repeatable builds
[ant:javac] /Users/zhoujunchen/as/xx/app/build/fastdex/DevelopDebug/custom-combind/com/xx/xx/xx/xx/CourseDetailActivity.java:229: 錯誤: -source 1.7 中不支持 lambda 表達(dá)式
[ant:javac] wrapperControlsView.postDelayed(() -> wrapperControlsView.initiativeRefresh(), 500L);
[ant:javac] ^
[ant:javac] (請使用 -source 8 或更高版本以啟用 lambda 表達(dá)式)
[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?

這個錯誤的原因是之前自定義的編譯任務(wù)寫死了使用1.7去編譯修肠,查閱gradle-retrolambda的源碼找到了這些代碼
https://github.com/evant/gradle-retrolambda

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

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

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

    transform.putVariant(variant)
}

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

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

從這些代碼中我們可以得知以下信息

  • 需要使用jdk1.8里的javac去編譯
  • sourceCompatibility和targetCompatibility必須設(shè)置成1.8
  • classpath中需要添加1.8的rt.jar

有了這些信息就可以在自定義的編譯任務(wù)做處理了

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

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

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

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

具體可以參考
https://github.com/typ0520/fastdex/blob/master/fastdex-gradle/src/main/groovy/fastdex/build/task/FastdexCustomJavacTask.groovy

6、issues#24 #29 #35 #36

https://github.com/typ0520/fastdex/issues/36
@wsf5918 @ysnows @jianglei199212 @tianshaokai @Razhan

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

正常情況下開啟multidex并且minSdkVersion < 21時會存在transformClassesWithJarMergingForDebug任務(wù)户盯,用來合并所有的JarInput和DirectoryInput并且輸出到build/intermediates/transforms/jarMerging/debug/jars/1/1f/combined.jar嵌施,而這個錯誤的表現(xiàn)是丟失了jarMerging任務(wù)饲化,所以走到dexTransform時本來期望只有一個combined.jar,但是由于沒有合并所以jar input的個數(shù)是117吗伤。當(dāng)時由于一直無法重現(xiàn)這個問題吃靠,所以就采用加標(biāo)示的手段解決的,具體是當(dāng)走到FastdexJarMergingTransform并且執(zhí)行完成以后就把executedJarMerge設(shè)置為true足淆,走到dexTransform時判斷如果開啟了multidex并且executedJarMerge==false就說明是丟失了jarMerge任務(wù)巢块,這個時候調(diào)用com.android.build.gradle.internal.transforms.JarMerger手動合并就可以解決了,具體可以參考GradleUtils的executeMerge方法
https://github.com/typ0520/fastdex/blob/master/fastdex-gradle/src/main/groovy/fastdex/build/util/GradleUtils.groovy

后來在開發(fā)中發(fā)現(xiàn)了丟失jarMerging任務(wù)的規(guī)律如下

  • com.android.tools.build:gradle的版本 >= 2.3.0
  • build-type選擇的是debug
  • 只有點(diǎn)studio的run按鈕打包時巧号,命令行調(diào)用不行
  • 點(diǎn)擊run按鈕打包時選擇的設(shè)備是>=6.0的設(shè)備

看到這里第三點(diǎn)的表現(xiàn)是不是很奇怪族奢,命令行和studio點(diǎn)擊run最終都是走gradle的流程,既然表現(xiàn)不一樣有可能是傳的參數(shù)不一樣丹鸿,把下面這段代碼放進(jìn)build.gradle中

println "projectProperties: " + project.gradle.startParameter.projectProperties

點(diǎn)擊studio的run按鈕選擇一個6.0的設(shè)備

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]

使用上面的這些參數(shù)一個一個做測試越走,發(fā)現(xiàn)是android.injected.build.api=23這個參數(shù)影響的,我們可以用這個測試項目做下測試
https://github.com/typ0520/fastdex-test-project/tree/master/build-cache-test

執(zhí)行./gradlew clean assembleDebug -Pandroid.injected.build.api=23
注: gradle傳自定義的參數(shù)是以-P開頭

miss_jar_merge.png

從上面的日志輸出中可以看出重現(xiàn)了丟失jarMerge任務(wù)靠欢,我們再來總結(jié)下重現(xiàn)這個問題的條件

  • com.android.tools.build:gradle的版本 >= 2.3.0
  • build-type選擇的是debug
  • 啟動參數(shù)包含android.injected.build.api并且>=23

有了結(jié)論還沒完廊敌,之所以2.3.0是這個行為是因為引入了build-cache機(jī)制,不合并是為了做jar級別的dex緩存门怪,這樣每次執(zhí)行dex transform時只有第一次時第三方庫才參與生成骡澈,為了提高效率也不會合并dex,如果項目比較大apk中可能是出現(xiàn)幾十個甚至上百個dex

classesN.png

目前fastdex由于做了jar合并相當(dāng)于把這個特性禁掉了掷空,后面會考慮不再做合并使之能用dex緩存肋殴,這樣全量打包時的速度應(yīng)該可以提高很多,另外還可以引入到除了debug別的build-type打包中拣帽,還有設(shè)備必須大于6.0問題也可以處理下疼电,理論上5.0以后系統(tǒng)就可以加載多個dex了,不知道為什么這個閾值設(shè)置的是6.0而不是5.0

==========================
本來想一鼓作氣把這幾個月做的功能和優(yōu)化全在這篇一并說完的减拭,寫著寫著簡書提示字?jǐn)?shù)快超限了蔽豺,無奈只能分篇寫了,下一篇主要講免安裝模塊和idea插件的實現(xiàn)拧粪⌒薅福快到中秋節(jié)了提前祝大家中秋快樂。未完待續(xù)可霎,后會有期魄鸦。。癣朗。拾因。。。

如果你喜歡本文就來給我們star吧
https://github.com/typ0520/fastdex

加快apk的構(gòu)建速度绢记,如何把編譯時間從130秒降到17秒
加快apk的構(gòu)建速度扁达,如何把編譯時間從130秒降到17秒(二)

參考的項目與文章

Instant Run
Tinker
Freeline
安卓App熱補(bǔ)丁動態(tài)修復(fù)技術(shù)介紹
Android應(yīng)用程序資源的編譯和打包過程分析

關(guān)鍵字:
加快apk編譯速度
加快app編譯速度
加快android編譯速度
加快android studio 編譯速度
android 加快編譯速度
android studio編譯慢
android studio編譯速度優(yōu)化
android studio gradle 編譯慢

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市蠢熄,隨后出現(xiàn)的幾起案子跪解,更是在濱河造成了極大的恐慌,老刑警劉巖签孔,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件叉讥,死亡現(xiàn)場離奇詭異,居然都是意外死亡饥追,警方通過查閱死者的電腦和手機(jī)图仓,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來判耕,“玉大人透绩,你說我怎么就攤上這事”谙ǎ” “怎么了帚豪?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長草丧。 經(jīng)常有香客問我狸臣,道長,這世上最難降的妖魔是什么昌执? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任烛亦,我火速辦了婚禮,結(jié)果婚禮上懂拾,老公的妹妹穿的比我還像新娘煤禽。我一直安慰自己,他們只是感情好岖赋,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布檬果。 她就那樣靜靜地躺著,像睡著了一般唐断。 火紅的嫁衣襯著肌膚如雪选脊。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天脸甘,我揣著相機(jī)與錄音恳啥,去河邊找鬼。 笑死丹诀,一個胖子當(dāng)著我的面吹牛钝的,可吹牛的內(nèi)容都是我干的翁垂。 我是一名探鬼主播,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼硝桩,長吁一口氣:“原來是場噩夢啊……” “哼沮峡!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起亿柑,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎棍弄,沒想到半個月后望薄,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡呼畸,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年痕支,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蛮原。...
    茶點(diǎn)故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡卧须,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出儒陨,到底是詐尸還是另有隱情花嘶,我是刑警寧澤,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布蹦漠,位于F島的核電站椭员,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏笛园。R本人自食惡果不足惜隘击,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望研铆。 院中可真熱鬧埋同,春花似錦、人聲如沸棵红。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽窄赋。三九已至哟冬,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間忆绰,已是汗流浹背浩峡。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留错敢,地道東北人翰灾。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓缕粹,卻偏偏與公主長得像,于是被迫代替她去往敵國和親纸淮。 傳聞我的和親對象是個殘疾皇子平斩,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內(nèi)容