Android Transform API 從原理到實(shí)戰(zhàn)

Transform API

從 1.5.0-beta1 開始,Gradle 插件包含一個 Transform API,允許第三方插件在將已編譯的類文件轉(zhuǎn)換為 dex 文件之前對其進(jìn)行操作。(該 API 已存在于 1.4.0-beta2 中,但已在 1.5.0-beta1 中進(jìn)行了徹底修改)

Transform API 的目標(biāo)是簡化注入自定義類的操作而不必處理任務(wù),并為操作內(nèi)容提供更大的靈活性。內(nèi)部代碼處理(jacoco,progard,multi-dex)已經(jīng)在 1.5.0-beta1 中轉(zhuǎn)移到了這一新機(jī)制。

Transform 的注冊和使用非常簡單,在我們自定義的 Gradle 插件中,只需創(chuàng)建一個實(shí)現(xiàn) Transform 接口的類,然后將其注冊到 android.registerTransform(theTransform) 或 android.registerTransform(theTransform, dependencies) 中即可。

Transform 是一個鏈?zhǔn)浇Y(jié)構(gòu),每個 Transform 都是一個 Gradle 的 Task,Android 編譯器通過 TaskManager 將每個 Transform 串聯(lián)起來。

Gradle Transform 是 Android 官方提供給開發(fā)者在項(xiàng)目構(gòu)建階段,即由 .class 到 .dex 轉(zhuǎn)換期間修改 .class 文件的一套 API。目前比較經(jīng)典的應(yīng)用是字節(jié)碼插樁、代碼注入技術(shù)。

主要方法
Transform.java 是一個抽象類,我們在使用是,需要實(shí)現(xiàn)它,它的主要方法有哪些呢?

getName()
用于指定 Transform 的名字,對應(yīng)了該 Transform 的 Task 的名稱。

isIncremental()
該方法指明是否支持增量編譯,增量編譯用于加快編譯速度。

getInputTypes()
指定 Transform 要處理的數(shù)據(jù)類型,可以作為輸入過濾的一種手段。

在 TransformManager 中定義了很多類型:

CONTENT_CLASS // 代表 javac 編譯成的 class 文件,可能是 jar 也可能是目錄。
CONTENT_JARS
CONTENT_RESOURCES // 表示處理標(biāo)準(zhǔn)的 java 資源
CONTENT_NATIVE_LIBS
CONTENT_DEX
CONTENT_DEX_WITH_RESOURCES
DATA_BINDING_BASE_CLASS_LOG_ARTIFACT
getScopes()
用于指定 Transform 的作用域。同樣在 TransformManager 中定義了很多類型。

常見的作用域有:

PROJECT:只處理當(dāng)前項(xiàng)目。
SUB_PROJECT:只處理子項(xiàng)目。
PROJECT_LOCAL_DEPS:只處理當(dāng)前項(xiàng)目的本地依賴,例如:jar、aar。
SUB_PROJECT_LOCAL_DEPS:只處理子項(xiàng)目的本地依賴,例如:jar、aar。
EXTERNAL_LIBRARIES:只處理外部依賴庫。
PROVIDED_ONLY:只處理本地或遠(yuǎn)程以 provided 形式引入的依賴庫。
TESTED_CODE:測試代碼。
SCOPE_FULL_PROJECT:即代表所有 Project。

確定了 ContentType 和 Scope 后就確定了該自定義 Transform 需要處理的資源流。
例如,上面提到的常用輸入類型(CONTENT_CLASS)和常用作用域(SCOPE_FULL_PROJECT)表示的就是 所有項(xiàng)目中 java 編譯成的 class 組成的資源流。

transform()
transform 方法來處理中間轉(zhuǎn)換過程,主要邏輯在該方法中實(shí)現(xiàn)。

它的定義:

public void transform(@NonNull TransformInvocation transformInvocation)

該方法的參數(shù)是 TransformInvocation。

我們可以在 transform 方法中,實(shí)現(xiàn)對字節(jié)碼的修改、處理等操作。

TransformInvocation
我們可以通過 TransformInvocation 來獲取輸入,同時也獲得了輸出的功能。
TransformInvocation 接口定義如下:

public interface TransformInvocation {
    @NonNull Context getContext();
    //TransformInput 是輸入文件的抽象,包括 jar 和目錄格式。
    @NonNull Collection<TransformInput> getInputs();
    @NonNull Collection<TransformInput> getReferencedInputs();
    @NonNull Collection<SecondaryInput> getSecondaryInputs();
    // Transform 的輸出,通過它可以獲取輸出路徑。
    @Nullable TransformOutputProvider getOutputProvider();
    boolean isIncremental();
}

TransformInvocation 的使用,我們舉例說明一下:

    @Override
    void transform(@NonNull TransformInvocation transformInvocation) {
        def startTime = System.currentTimeMillis()
        //如果是非增量編譯,則刪除之前的輸出
        if (!transformInvocation.isIncremental) {
            transformInvocation.outputProvider.deleteAll()
        }
        //TransformInvocation 來獲取輸入
        Collection<TransformInput> inputs = transformInvocation.inputs
        //TransformInvocation 來獲取輸出
        TransformOutputProvider outputProvider = transformInvocation.outputProvider
        //遍歷inputs
        inputs.each { TransformInput input ->
            //遍歷directoryInputs
            input.directoryInputs.each { DirectoryInput directoryInput ->
                handleDirectoryInput(directoryInput, outputProvider)
            }
            //遍歷jarInputs
            input.jarInputs.each { JarInput jarInput ->
                handleJarInputs(jarInput, outputProvider)
            }
        }
        def cost = (System.currentTimeMillis() - startTime) / 1000
    }

TransformInput
TransformInput 是指這些輸入文件的抽象。它包括兩部分:

  1. DirectoryInput 集合
    是指以源碼方式參與項(xiàng)目編譯的所有目錄結(jié)構(gòu)及其目錄下 的源碼文件。

  2. JarInput 集合
    是指以 jar 包方式參與項(xiàng)目編譯的所有本地 jar 包和遠(yuǎn)程 jar 包。

TransformOutputProvider
是 Transform 的輸出的抽象,通過它可以獲取輸出路徑。TransformOutputProvider 通過調(diào)用 getContentLocation 來獲取輸出目錄:

    @NonNull
    File getContentLocation(
            @NonNull String name,
            @NonNull Set<QualifiedContent.ContentType> types,
            @NonNull Set<? super QualifiedContent.Scope> scopes,
            @NonNull Format format);

實(shí)戰(zhàn)
Transform 的注冊和使用非常易懂, 在我們自定義的 plugin 內(nèi), 我們可以通過 android.registerTransform(theTransform) 或者 android.registerTransform(theTransform, dependencies) 就可以完成注冊。

使用 Transform API 主要是寫一個類繼承 Transform,并把該 Transform 注入到打包過程中。
注入 Transform 很簡單,先獲取 com.android.build.gradle.AppExtension 對象,然后調(diào)用它的registerTransform() 方法。

這個方法實(shí)際上是屬于 BaseExtension 的,AppExtension 繼承自 BaseExtension:

#com.android.build.gradle.BaseExtension
public void registerTransform(@NonNull Transform transform, Object... dependencies) {
    transforms.add(transform);
    transformDependencies.add(Arrays.asList(dependencies));
}

注冊我們自定義的 Transform:

    void apply(Project project) {
        AppExtension android = project.extensions.getByType(AppExtension)
        android.registerTransform(new MethodTimeTransform(project))
    }

通過獲取 module 的 Project 的 AppExtension,通過它的 registerTransform 方法完成 Transform 的注冊。

這里注冊之后,會在編譯過程中的 TransformManager#addTransform 中生成一個 task,然后在執(zhí)行這個 task 的時候會執(zhí)行到我們自定義的 Transform 的 transform 方法。這個 task 的執(zhí)行時機(jī)就是 .class 文件轉(zhuǎn)換成 .dex文 件的時候。

我們來看完整的實(shí)例代碼。

自定義的 Transform 類,AsmTransform:

class AsmTransform extends Transform{
    private Project mProject;
    AsmTransform(Project project){
        mProject = project
    }

    @Override
    String getName() {
        return "budaye_transform"
    }

    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS
    }

    @Override
    Set<? super QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT
    }

    @Override
    boolean isIncremental() {
        return true
    }

    @Override
    void transform(@NonNull TransformInvocation transformInvocation) {
        def startTime = System.currentTimeMillis()
        //如果是非增量編譯,則刪除之前的輸出
        if (!transformInvocation.isIncremental) {
            transformInvocation.outputProvider.deleteAll()
        }
        Collection<TransformInput> inputs = transformInvocation.inputs
        TransformOutputProvider outputProvider = transformInvocation.outputProvider
        //遍歷inputs
        inputs.each { TransformInput input ->
            //遍歷directoryInputs
            input.directoryInputs.each { DirectoryInput directoryInput ->
                handleDirectoryInput(directoryInput, outputProvider)
            }
            //遍歷jarInputs
            input.jarInputs.each { JarInput jarInput ->
                handleJarInputs(jarInput, outputProvider)
            }
        }
        def cost = (System.currentTimeMillis() - startTime) / 1000
    }

    /**
     * 處理文件目錄下的class文件
     */
    static void handleDirectoryInput(DirectoryInput directoryInput, TransformOutputProvider outputProvider) {
        //是否是目錄
        if (directoryInput.file.isDirectory()) {
            //列出目錄所有文件(包含子文件夾,子文件夾內(nèi)文件)
            directoryInput.file.eachFileRecurse { File file ->
                def name = file.name
                if (checkClassFile(name)) {
                    println '----------- deal with "class" file <' + name + '> -----------'
                    ClassReader classReader = new ClassReader(file.bytes)
                    ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
                    ClassVisitor cv = new LifecycleClassVisitor(classWriter)
                    classReader.accept(cv, EXPAND_FRAMES)
                    byte[] code = classWriter.toByteArray()
                    FileOutputStream fos = new FileOutputStream(
                            file.parentFile.absolutePath + File.separator + name)
                    fos.write(code)
                    fos.close()
                }
            }
        }
        //處理完輸入文件之后,要把輸出給下一個任務(wù)
        def dest = outputProvider.getContentLocation(directoryInput.name,
                directoryInput.contentTypes, directoryInput.scopes,
                Format.DIRECTORY)
        FileUtils.copyDirectory(directoryInput.file, dest)
    }

    /**
     * 處理Jar中的class文件
     */
    static void handleJarInputs(JarInput jarInput, TransformOutputProvider outputProvider) {
        if (jarInput.file.getAbsolutePath().endsWith(".jar")) {
            //重名名輸出文件,因?yàn)榭赡芡?會覆蓋
            def jarName = jarInput.name
            def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
            if (jarName.endsWith(".jar")) {
                jarName = jarName.substring(0, jarName.length() - 4)
            }
            JarFile jarFile = new JarFile(jarInput.file)
            Enumeration enumeration = jarFile.entries()
            File tmpFile = new File(jarInput.file.getParent() + File.separator + "classes_temp.jar")
            //避免上次的緩存被重復(fù)插入
            if (tmpFile.exists()) {
                tmpFile.delete()
            }
            JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(tmpFile))
            //用于保存
            while (enumeration.hasMoreElements()) {
                JarEntry jarEntry = (JarEntry) enumeration.nextElement()
                String entryName = jarEntry.getName()
                ZipEntry zipEntry = new ZipEntry(entryName)
                InputStream inputStream = jarFile.getInputStream(jarEntry)
                //插樁class
                if (checkClassFile(entryName)) {
                    //class文件處理
                    println '----------- deal with "jar" class file <' + entryName + '> -----------'
                    jarOutputStream.putNextEntry(zipEntry)
                    ClassReader classReader = new ClassReader(IOUtils.toByteArray(inputStream))
                    ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
                    ClassVisitor cv = new LifecycleClassVisitor(classWriter)
                    classReader.accept(cv, EXPAND_FRAMES)
                    byte[] code = classWriter.toByteArray()
                    jarOutputStream.write(code)
                } else {
                    jarOutputStream.putNextEntry(zipEntry)
                    jarOutputStream.write(IOUtils.toByteArray(inputStream))
                }
                jarOutputStream.closeEntry()
            }
            //結(jié)束
            jarOutputStream.close()
            jarFile.close()
            def dest = outputProvider.getContentLocation(jarName + md5Name,
                    jarInput.contentTypes, jarInput.scopes, Format.JAR)
            FileUtils.copyFile(tmpFile, dest)
            tmpFile.delete()
        }
    }

    /**
     * 檢查class文件是否需要處理
     * @param fileName
     * @return
     */
    static boolean checkClassFile(String name) {
        //只處理需要的class文件
        return (name.endsWith(".class") && !name.startsWith("R\$")
                && !"R.class".equals(name) && !"BuildConfig.class".equals(name)
                && "android/support/v4/app/FragmentActivity.class".equals(name))
    }
}

AsmTransform 的注冊:

    @Override
    void apply(Project project) {
        if (project.plugins.hasPlugin(AppPlugin)){
            //registerTransform
            def android = project.extensions.getByType(AppExtension)
            android.registerTransform(new AsmTransform(project))
        }
    }

到了這里,就實(shí)現(xiàn)了整個 Transform 的定義和注冊過程了。

**PS:更多性能優(yōu)化相關(guān)文章,請查看 --> 《Android 性能優(yōu)化》

————————————————
版權(quán)聲明:本文為CSDN博主「卜大爺」的原創(chuàng)文章,遵循CC 4.0 BY-SA版權(quán)協(xié)議,轉(zhuǎn)載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/u011578734/article/details/114262419

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容