天天看點

Android元件化開發實踐(九):自定義Gradle插件

本文緊接着前一章 Android元件化開發實踐(八):元件生命周期如何實作自動注冊管理 ,主要講解怎麼通過自定義插件來實作元件生命周期的自動注冊管理。

1. 采用groovy建立插件

建立一個Java Library module,命名為lifecycle-plugin,删除 src->main 下面的java目錄,建立一個groovy目錄,在groovy目錄下建立類似java的package,在 src->main 下面建立一個 resources 目錄,在resources目錄下依次建立 META-INF/gradle-plugins 目錄,最後在該目錄下建立一個名為 com.hm.plugin.lifecycle.properties的文本檔案,檔案名是你要定義的插件名,按需自定義即可。最後的工程結構如圖所示:

Android元件化開發實踐(九):自定義Gradle插件

修改module的build.gradle檔案,引入groovy插件等:

apply plugin: 'java-library'
apply plugin: 'groovy'
apply plugin: 'maven'

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    compile gradleApi()
    compile localGroovy()
    compile 'com.android.tools.build:transform-api:1.5.0'
    compile 'com.android.tools.build:gradle:3.0.1'
}

sourceCompatibility = "1.7"
targetCompatibility = "1.7"

//通過maven将插件釋出到本地的腳本配置,根據自己的要求來修改
uploadArchives {
    repositories.mavenDeployer {
        pom.version = '1.0.0'
        pom.artifactId = 'hmlifecyclepluginlocal'
        pom.groupId = 'com.heima.iou'
        repository(url: "file:///Users/hjy/.m2/repository/")
    }
}
           

這裡有幾點需要說明的是:

  1. 通常都是采用groovy語言來建立gradle plugin的,groovy是相容java的,你完全可以采用java來編寫插件。關于groovy語言,了解一些基礎文法就足夠支撐我們去編寫插件了。
  2. 在 src/main/resources/META-INF/gradle-plugins目錄下定義插件聲明,*.properties檔案的檔案名就是插件名稱。

2. 實作Plugin接口

要編寫一個插件是很簡單的,隻需實作Plugin接口即可。

package com.hm.iou.lifecycle.plugin

import com.android.build.gradle.AppExtension
import org.gradle.api.Plugin
import org.gradle.api.Project

class LifeCyclePlugin implements Plugin<Project>{
    @Override
    void apply(Project project) {
        println "------LifeCycle plugin entrance-------"
    }
}
           

接着在com.hm.plugin.lifecycle.properties檔案裡增加配置:

implementation-class=com.hm.iou.lifecycle.plugin.LifeCyclePlugin
           

其中implementation-class的值為Plugin接口的實作類的全限定類名,至此為止一個最簡單的插件編寫好了,它的功能很簡單,僅僅是在控制台列印一句文本而已。

我們通過maven将該插件釋出到本地的maven倉庫裡,釋出成功後,我們在app module裡引入該插件,修改app module目錄下的build.gradle檔案,增加如下配置:

apply plugin: 'com.android.application'
//引入自定義插件,插件名與前面的*.properties檔案的檔案名是一緻的
apply plugin: 'com.hm.plugin.lifecycle'
buildscript {
    repositories {
        google()
        jcenter()
        //自定義插件maven位址,替換成你自己的maven位址
        maven { url 'file:///Users/hjy/.m2/repository/' }
    }
    dependencies {
        //通過maven加載自定義插件
        classpath 'com.heima.iou:hmlifecyclepluginlocal:1.0.0'
    }
}
           

我們build一下工程,在Gradle Console裡會列印出"------LifeCycle plugin entrance-------"來,這說明我們的自定義插件成功了。

講到這裡可以看到,按這個步驟實作一個gradle插件是很簡單的,它并沒有我們想象中那麼高深莫測,你也可以自豪地說我會制作gradle插件了。

3. Gradle Transform

然而前面這個插件并沒有什麼卵用,它僅僅隻是在編譯時,在控制台列印一句話而已。那麼怎麼通過插件在打包前去掃描所有的class檔案呢,幸運的是官方給我們提供了 Gradle Transform技術,簡單來說就是能夠讓開發者在項目建構階段即由class到dex轉換期間修改class檔案,Transform階段會掃描所有的class檔案和資源檔案,具體技術我這裡不詳細展開,下面通過僞代碼部分說下我的思路。

//隻需要繼承Transform類即可
class LifeCycleTransform extends Transform {

    Project project

    LifeCycleTransform(Project project) {
        this.project = project
    }

    //該Transform的名稱,自定義即可,隻是一個辨別
    @Override
    String getName() {
        return "LifeCycleTransform"
    }

    //該Transform支援掃描的檔案類型,分為class檔案和資源檔案,我們這裡隻處理class檔案的掃描
    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS
    }

    //Transfrom的掃描範圍,我這裡掃描整個工程,包括目前module以及其他jar包、aar檔案等所有的class
    @Override
    Set<? super QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT
    }

    //是否增量掃描
    @Override
    boolean isIncremental() {
        return true
    }

    @Override
    void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs,
                   TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {
        println "\nstart to transform-------------->>>>>>>"

        def appLikeProxyClassList = []
        //inputs就是所有掃描到的class檔案或者是jar包,一共2種類型
        inputs.each { TransformInput input ->
            //1.周遊所有的class檔案目錄
            input.directoryInputs.each { DirectoryInput directoryInput ->
                //遞歸掃描該目錄下所有的class檔案
                if (directoryInput.file.isDirectory()) {
                    directoryInput.file.eachFileRecurse {File file ->
                        //形如 Heima$$****$$Proxy.class 的類,是我們要找的目标class,直接通過class的名稱來判斷,也可以再加上包名的判斷,會更嚴謹點
                        if (ScanUtil.isTargetProxyClass(file)) {
                            //如果是我們自己生産的代理類,儲存該類的類名
                            appLikeProxyClassList.add(file.name)
                        }
                    }
                }

                //Transform掃描的class檔案是輸入檔案(input),有輸入必然會有輸出(output),處理完成後需要将輸入檔案拷貝到一個輸出目錄下去,
                //後面打包将class檔案轉換成dex檔案時,直接采用的就是輸出目錄下的class檔案了。
                //必須這樣擷取輸出路徑的目錄名稱
                def dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
                FileUtils.copyDirectory(directoryInput.file, dest)
            }

            //2.周遊查找所有的jar包
            input.jarInputs.each { JarInput jarInput ->
                println "\njarInput = ${jarInput}"

                //與處理class檔案一樣,處理jar包也是一樣,最後要将inputs轉換為outputs
                def jarName = jarInput.name
                def md5 = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
                if (jarName.endsWith(".jar")) {
                    jarName = jarName.substring(0, jarName.length() - 4)
                }
                //擷取輸出路徑下的jar包名稱,必須這樣擷取,得到的輸出路徑名不能重複,否則會被覆寫
                def dest = outputProvider.getContentLocation(jarName + md5, jarInput.contentTypes, jarInput.scopes, Format.JAR)
                if (jarInput.file.getAbsolutePath().endsWith(".jar")) {
                    File src = jarInput.file
                    //先簡單過濾掉 support-v4 之類的jar包,隻處理有我們業務邏輯的jar包
                    if (ScanUtil.shouldProcessPreDexJar(src.absolutePath)) {
                        //掃描jar包的核心代碼在這裡,主要做2件事情:
                        //1.掃描該jar包裡有沒有實作IAppLike接口的代理類;
                        //2.掃描AppLifeCycleManager這個類在哪個jar包裡,并記錄下來,後面需要在該類裡動态注入位元組碼;
                        List<String> list = ScanUtil.scanJar(src, dest)
                        if (list != null) {
                            appLikeProxyClassList.addAll(list)
                        }
                    }
                }
                //将輸入檔案拷貝到輸出目錄下
                FileUtils.copyFile(jarInput.file, dest)
            }
        }

        println ""
        appLikeProxyClassList.forEach({fileName ->
            println "file name = " + fileName
        })
        println "\n包含AppLifeCycleManager類的jar檔案"
        println ScanUtil.FILE_CONTAINS_INIT_CLASS.getAbsolutePath()
        println "開始自動注冊"

        //1.通過前面的步驟,我們已經掃描到所有實作了 IAppLike接口的代理類;
        //2.後面需要在 AppLifeCycleManager 這個類的初始化方法裡,動态注入位元組碼;
        //3.将所有 IAppLike 接口的代理類,通過類名進行反射調用執行個體化
        //這樣最終生成的apk包裡,AppLifeCycleManager調用init()方法時,已經可以加載所有元件的生命周期類了
        new AppLikeCodeInjector(appLikeProxyClassList).execute()

        println "transform finish----------------<<<<<<<\n"
    }
}
           

我們來看看ScanUtil類裡的代碼邏輯:

class ScanUtil {

    static final PROXY_CLASS_PREFIX = "Heima\$\$"
    static final PROXY_CLASS_SUFFIX = "\$\$Proxy.class"
    //注意class檔案名中的包名是以“/”分隔開,而不是“.”分隔的,這個包名是我們通過APT生成的所有 IAppLike 代理類的包名
    static final PROXY_CLASS_PACKAGE_NAME = "com/hm/iou/lifecycle/apt/proxy"
    //AppLifeCycleManager是應用生命周期架構初始化方法調用類
    static final REGISTER_CLASS_FILE_NAME = "com/hm/lifecycle/api/AppLifeCycleManager.class"

    //包含生命周期管理初始化類的檔案,即包含 com.hm.lifecycle.api.AppLifeCycleManager 類的class檔案或者jar檔案
    static File FILE_CONTAINS_INIT_CLASS

    /**
     * 判斷該class是否是我們的目标類
     *
     * @param file
     * @return
     */
    static boolean isTargetProxyClass(File file) {
        if (file.name.endsWith(PROXY_CLASS_SUFFIX) && file.name.startsWith(PROXY_CLASS_PREFIX)) {
            return true
        }
        return false
    }

    /**
     * 掃描jar包裡的所有class檔案:
     * 1.通過包名識别所有需要注入的類名
     * 2.找到AppLifeCycleManager類所在的jar包,後面我們會在該jar包裡進行代碼注入
     *
     * @param jarFile
     * @param destFile
     * @return
     */
    static List<String> scanJar(File jarFile, File destFile) {
        def file = new JarFile(jarFile)
        Enumeration<JarEntry> enumeration = file.entries()
        List<String> list = null
        while (enumeration.hasMoreElements()) {
            //周遊這個jar包裡的所有class檔案項
            JarEntry jarEntry = enumeration.nextElement()
            //class檔案的名稱,這裡是全路徑類名,包名之間以"/"分隔
            String entryName = jarEntry.getName()
            if (entryName == REGISTER_CLASS_FILE_NAME) {
                //标記這個jar包包含 AppLifeCycleManager.class
                //掃描結束後,我們會生成注冊代碼到這個檔案裡
                FILE_CONTAINS_INIT_CLASS = destFile
            } else {
                //通過包名來判斷,嚴謹點還可以加上類名字首、字尾判斷
                //通過APT生成的類,都有統一的字首、字尾
                if (entryName.startsWith(PROXY_CLASS_PACKAGE_NAME)) {
                    if (list == null) {
                        list = new ArrayList<>()
                    }
                    list.addAll(entryName.substring(entryName.lastIndexOf("/") + 1))
                }
            }
        }
        return list
    }

    static boolean shouldProcessPreDexJar(String path) {
        return !path.contains("com.android.support") && !path.contains("/android/m2repository")
    }

}
           

修改Plugin接口實作類,在插件中注冊該Transfrom:

class LifeCyclePlugin implements Plugin<Project>{
    @Override
    void apply(Project project) {
        println "------LifeCycle plugin entrance-------"
        def android = project.extensions.getByType(AppExtension)
        android.registerTransform(new LifeCycleTransform(project))
    }
}
           

前面的代碼裡,先注釋掉LifeCycleTransform類裡的AppLikeCodeInjector相關代碼,這塊我們後面再講。我們再建立一個Android Library module,在該module裡建立 ModuleCAppLike、ModuleDAppLike,同樣都實作IAppLike接口并采用@AppLifeCycle作為注解。最後采用最新的插件重新build一下工程,看看Gradle Console裡的輸出資訊。

file name = Heima$$ModuleCAppLike$$Proxy.class
file name = Heima$$ModuleDAppLike$$Proxy.class
file name = Heima$$ModuleAAppLike$$Proxy.class
file name = Heima$$ModuleBAppLike$$Proxy.class

包含AppLifeCycleManager類的jar檔案
/Users/hjy/Desktop/heima/code/gitlab/HM-AppLifeCycleMgr/app/build/intermediates/transforms/LifeCycleTransform/debug/17.jar
           

可以看到,在Transform過程中,我們找到了ModuleAAppLike、ModuleBAppLike、ModuleCAppLike、ModuleDAppLike這4個類的代理類,以及AppLifeCycleManager這個class檔案所在的jar包。

Android元件化開發實踐(九):自定義Gradle插件

在app->build->intermediates->transforms中,可以看到所有的Transform,包括我們剛才自定義的Transform。從上圖中可以看到,這裡的0.jar、1.jar、2.jar等等,都是通過outputProvider.getContentLocation()方法來生成的,這個Transform目錄下的class檔案、jar包等,會當做下一個Transform的inputs傳遞過去。

4. 通過ASM動态修改位元組碼

到現在,我們隻剩下最後一步了,那就是如何注入代碼了。ASM 是一個 Java 位元組碼操控架構,它能被用來動态生成類或者增強既有類的功能。我這裡對ASM不做詳細介紹了,主要是介紹使用ASM動态注入代碼的思路。

首先,我們修改一下AppLifeCycleManager類,增加動态注入位元組碼的入口方法:

/**
     * 通過插件加載 IAppLike 類
     */
    private static void loadAppLike() {
    }

    //通過反射去加載 IAppLike 類的執行個體
    private static void registerAppLike(String className) {
        if (TextUtils.isEmpty(className))
            return;
        try {
            Object obj = Class.forName(className).getConstructor().newInstance();
            if (obj instanceof IAppLike) {
                APP_LIKE_LIST.add((IAppLike) obj);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 初始化
     *
     * @param context
     */
    public static void init(Context context) {
        //通過插件加載 IAppLike 類
        loadAppLike();
        Collections.sort(APP_LIKE_LIST, new AppLikeComparator());
        for (IAppLike appLike : APP_LIKE_LIST) {
            appLike.onCreate(context);
        }
    }
           

相比之前,這裡增加了一個loadAppLike()方法,在init()方法調用時會先執行。通過前面Transform步驟之後,我們現在的目标是把代碼動态插入到loadAppLike()方法裡,下面這段代碼是我們期望插入後的結果:

private static void loadAppLike() {
  registerAppLike("com.hm.iou.lifecycle.apt.proxy.Heima$$ModuleAAppLike$$Proxy");
  registerAppLike("com.hm.iou.lifecycle.apt.proxy.Heima$$ModuleBAppLike$$Proxy");
  registerAppLike("com.hm.iou.lifecycle.apt.proxy.Heima$$ModuleCAppLike$$Proxy");
  registerAppLike("com.hm.iou.lifecycle.apt.proxy.Heima$$ModuleDAppLike$$Proxy");
}
           

這樣在初始化時,就已經知道要加載哪些生命周期類,來看看具體實作方法,關于ASM不了解的地方,需要先搞清楚其使用方法再來閱讀:

class AppLikeCodeInjector {

    //掃描出來的所有 IAppLike 類
    List<String> proxyAppLikeClassList

    AppLikeCodeInjector(List<String> list) {
        proxyAppLikeClassList = list
    }

    void execute() {
        println("開始執行ASM方法======>>>>>>>>")

        File srcFile = ScanUtil.FILE_CONTAINS_INIT_CLASS
        //建立一個臨時jar檔案,要修改注入的位元組碼會先寫入該檔案裡
        def optJar = new File(srcFile.getParent(), srcFile.name + ".opt")
        if (optJar.exists())
            optJar.delete()
        def file = new JarFile(srcFile)
        Enumeration<JarEntry> enumeration = file.entries()
        JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(optJar))
        while (enumeration.hasMoreElements()) {
            JarEntry jarEntry = enumeration.nextElement()
            String entryName = jarEntry.getName()
            ZipEntry zipEntry = new ZipEntry(entryName)
            InputStream inputStream = file.getInputStream(jarEntry)
            jarOutputStream.putNextEntry(zipEntry)

            //找到需要插入代碼的class,通過ASM動态注入位元組碼
            if (ScanUtil.REGISTER_CLASS_FILE_NAME == entryName) {
                println "insert register code to class >> " + entryName

                ClassReader classReader = new ClassReader(inputStream)
                // 建構一個ClassWriter對象,并設定讓系統自動計算棧和本地變量大小
                ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS)
                ClassVisitor classVisitor = new AppLikeClassVisitor(classWriter)
                //開始掃描class檔案
                classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES)

                byte[] bytes = classWriter.toByteArray()
                //将注入過位元組碼的class,寫入臨時jar檔案裡
                jarOutputStream.write(bytes)
            } else {
                //不需要修改的class,原樣寫入臨時jar檔案裡
                jarOutputStream.write(IOUtils.toByteArray(inputStream))
            }
            inputStream.close()
            jarOutputStream.closeEntry()
        }

        jarOutputStream.close()
        file.close()

        //删除原來的jar檔案
        if (srcFile.exists()) {
            srcFile.delete()
        }
        //重新命名臨時jar檔案,新的jar包裡已經包含了我們注入的位元組碼了
        optJar.renameTo(srcFile)
    }

    //插入位元組碼的邏輯,都在這個類裡面
    class AppLikeClassVisitor extends ClassVisitor {
        AppLikeClassVisitor(ClassVisitor classVisitor) {
            super(Opcodes.ASM5, classVisitor)
        }

        @Override
        MethodVisitor visitMethod(int access, String name,
                                  String desc, String signature,
                                  String[] exception) {
            println "visit method: " + name
            MethodVisitor mv = super.visitMethod(access, name, desc, signature, exception)
            //找到 AppLifeCycleManager裡的loadAppLike()方法,我們在這個方法裡插入位元組碼
            if ("loadAppLike" == name) {
                mv = new LoadAppLikeMethodAdapter(mv, access, name, desc)
            }
            return mv
        }
    }

    class LoadAppLikeMethodAdapter extends AdviceAdapter {

        LoadAppLikeMethodAdapter(MethodVisitor mv, int access, String name, String desc) {
            super(Opcodes.ASM5, mv, access, name, desc)
        }

        @Override
        protected void onMethodEnter() {
            super.onMethodEnter()
            println "-------onMethodEnter------"
            //周遊插入位元組碼,其實就是在 loadAppLike() 方法裡插入類似registerAppLike("");的位元組碼
            proxyAppLikeClassList.forEach({proxyClassName ->
                println "開始注入代碼:${proxyClassName}"
                def fullName = ScanUtil.PROXY_CLASS_PACKAGE_NAME.replace("/", ".") + "." + proxyClassName.substring(0, proxyClassName.length() - 6)
                println "full classname = ${fullName}"
                mv.visitLdcInsn(fullName)
                mv.visitMethodInsn(INVOKESTATIC, "com/hm/lifecycle/api/AppLifeCycleManager", "registerAppLike", "(Ljava/lang/String;)V", false);
            })
        }

        @Override
        protected void onMethodExit(int opcode) {
            super.onMethodExit(opcode)
            println "-------onMethodEnter------"
        }
    }

}
           

最後重新編譯插件再運作,驗證結果。

這裡有個比較困難的地方,就是需要使用ASM編寫class位元組碼。我這裡推薦一個比較好用的方法:

  1. 将要注入的java源碼先寫出來;
  2. 通過javac編譯出class檔案;
  3. 通過 asm-all.jar

    反編譯該class檔案,可得到所需的ASM注入代碼;

    執行指令如下:

java -classpath "asm-all.jar" org.objectweb.asm.util.ASMifier com/hm/lifecycle/api/AppLifeCycleManager.class
           

從中找到loadAppLike()方法位元組碼處,這樣通過ASM注入代碼就比較簡單了:

{
mv = cw.visitMethod(ACC_PRIVATE + ACC_STATIC, "loadAppLike", "()V", null, null);
mv.visitCode();
mv.visitLdcInsn("com.hm.iou.lifecycle.apt.proxy.Heima$$ModuleAAppLike$$Proxy");
mv.visitMethodInsn(INVOKESTATIC, "com/hm/lifecycle/api/AppLifeCycleManager", "registerAppLike", "(Ljava/lang/String;)V", false);
mv.visitInsn(RETURN);
mv.visitMaxs(1, 0);
mv.visitEnd();
}
           

5. 小結

結合前一章我們基本上實作了自動注冊加載元件的生命周期管理類,做到了無侵入式的服務注冊,離我們的徹底元件化解耦更近一步了。本文有些地方借鑒了阿裡的路由架構ARouter,其基本思路是一緻的,弄懂了這些也基本上就弄懂了ARouter的實作原理 ,原理弄清楚了之後,在此基礎上咱們寫出自己的架構也不是什麼難事了。

源碼位址:

https://github.com/houjinyun/Android-AppLifecycleMgr

源碼已經托管到github上了, 有興趣的可以跟我留言,互相交流學習進步。

系列文章

Android元件化開發實踐(一):為什麼要進行元件化開發? Android元件化開發實踐(二):元件化架構設計 Android元件化開發實踐(三):元件開發規範 Android元件化開發實踐(四):元件間通信問題 Android元件化開發實踐(五):元件生命周期管理 Android元件化開發實踐(六):老項目實施元件化 Android元件化開發實踐(七):開發常見問題及解決方案 Android元件化開發實踐(九):自定義Gradle插件

繼續閱讀