Android Studio gradle插件開發----組件註冊插件

組件註冊插件是解決在模塊化化開發中無反射、無新增第三方框架、可混淆的需求。在Android Studio編譯階段根據宿主Module的build.gradle中的配置信息注入組件註冊代碼。html

詳細例子和插件源碼,歡迎star:github.com/trrying/Plu…java

效果:

使用插件前App源碼:android

1559701162143

使用插件後反編譯App:git

1559701477775

使用:

  1. 在項目根目錄 build.gradle添加classpath
buildscript {
    dependencies {
        // 組件註冊插件
        classpath 'com.owm.component:register:1.1.2'
    }
}
複製代碼
  1. 在宿主模塊build.gradle添加配置參數
apply plugin: 'com.android.application'
android {
	...
}
dependencies {
	...
}

apply plugin: 'com.owm.component.register'
componentRegister {
    // 是否開啓debug模式,輸出詳細日誌
    isDebug = false
    // 是否啓動組件註冊
    componentRegisterEnable = true
    // 組件註冊代碼注入類
    componentMain = "com.owm.pluginset.application.App"
    // 註冊代碼注入類的方法
    componentMethod = "instanceModule"
    // 註冊組件容器 HashMap,若是沒有該字段則建立一個 public static final HashMap<String, Object> componentMap = new HashMap<>();
    componentContainer = "componentMap"
    // 註冊組件配置
    componentRegisterList = [
        [
            "componentName": "LoginInterface",
            "instanceClass": "com.owm.module.login.LoginManager",
            "enable"       : true, // 默認爲:true
            "singleton"    : false, // 默認爲:false,是否單例實現,爲true調用Xxx.getInstance(),不然調用new Xxx();
        ],
    ]
}

複製代碼

上述配置表示在com.owm.pluginset.application.App類中instanceModule方法內首部添加 componentMap.put("LoginInterface", new com.owm.module.login.LoginManager());代碼。github

  1. componentMain配置的類中建立instanceModule()方法和componentMap容器。
class App {
    public static final HashMap<String, Object> componentMap = new HashMap<>();
    public void instanceModule() {
    }
}
複製代碼

Gradle同步從新構建項目後出現下列輸入表示注入成功。apache

1559558005181

1. 背景

組件化開發須要動態註冊各個組件服務,解決在模塊化化開發中無需反射、無需第三方框架、可混淆的需求。編程

2. 知識點

  • Android Studio 構建流程;
  • Android Gradle Plugin & Transform;
  • Groovy編程語言;
  • javassist/asm字節碼修改;

使用Android Studio編譯流程以下圖:api

簡單構建流程

若是把整個構建編譯流程當作是河流的話,在java 編譯階段有3條河流入,分別是:緩存

  1. aapt(Android Asset Package Tool)根據資源文件生成的R文件;
  2. app 源碼;
  3. aidl文件生成接口;

上面3條河流聚集後源碼將被編譯成class文件。 如今要作的就是使用 Gralde Plugin 註冊一個Transform,在Java Compileer 以後插入,處理class文件。處理完成後交給下一步流程去繼續構建。bash

3. 構建插件模塊

3.1 建立插件模塊

在項目中建立Android Library Module(其餘模塊也行,只要目錄結構對應下面),建立完成後刪除多餘目錄和文件,只保留src目錄和build.gradle文件。 目錄結構以下:

PluginSet
│
├─ComponentRegister
│  │  .gitignore
│  │  build.gradle
│  │  ComponentRegister.iml
│  │
│  └─src
│      └─main
│          ├─groovy //
│          │  └─com
│          │      └─owm
│          │          └─component
│          │              └─register
│          │                  ├─plugin
│          │                  │      RegisterPlugin.groovy
│          │
│          └─resources
│              └─META-INF
│                  └─gradle-plugins
│                          com.owm.component.register.properties

複製代碼

主要關注有兩個點

  1. src/main/groovy 放置插件代碼

  2. src/main/resources 放置插件配置信息

    src/main/resources下面的 resources/META-INF/gradle-plugins存放配置信息。這裏能夠放置多個配置信息,每一個配置信息是一個插件。 配置文件名就是插件名,例如我這裏是com.owm.component.register.properties,應用時:apply plugin: 'com.owm.component.register'

3.2 建立插件代碼目錄

建立src/main/groovy 目錄,再在該目錄下建立包名路徑和對應groovy類文件。

3.3 建立插件配置文件

src/main/resources目錄下建立 resources/META-INF/gradle-plugins目錄,再建立com.owm.component.register.properties配置文件。 配置文件內容以下:

implementation-class=com.owm.component.register.plugin.RegisterPlugin
複製代碼

這裏是配置org.gradle.api.Plugin 接口的實現類,也就是配置插件的核心入口。

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package org.gradle.api;

public interface Plugin<T> {
    void apply(T t);
}
複製代碼

3.4 配置gradle

ComponentRegister插件模塊build.gradle配置以下:

apply plugin: 'groovy'
apply plugin: 'maven'

dependencies {
    //gradle sdk
    implementation gradleApi()
    //groovy sdk
    implementation localGroovy()

    //noinspection GradleDependency
    implementation "com.android.tools.build:gradle:3.2.0"
    implementation "javassist:javassist:3.12.1.GA"
    implementation "commons-io:commons-io:2.6"
}

// 發佈到 plugins.gradle.org 雙擊Gradle面板 PluginSet:ComponentRegister -> Tasks -> plugin portal -> publishPlugins
apply from: "../script/gradlePlugins.gradle"

// 發佈到本地maven倉庫 雙擊Gradle面板 PluginSet:ComponentRegister -> Tasks -> upload -> uploadArchives
apply from: "../script/localeMaven.gradle"

//發佈 Jcenter 雙擊Gradle面板 PluginSet:ComponentRegister -> Tasks -> publishing -> bintrayUpload
apply from: '../script/bintray.gradle'

複製代碼

gralde sync 後能夠在Android Studio Gradle面板找到uploadArchives Task

uploadArchives Task

當插件編寫完成,雙擊運行uploadArchivesTask會在配置的本地Maven倉庫中生成插件。

gradle plugin

4. 組件註冊插件功能實現

4.1 實現Plugin接口

按照src/main/resources/resources/META-INF/gradle-plugins/com.owm.component.register.properties配置文件中implementation-class的值來建立建立Plugin接口實現類。

在Plugin實現類中完成配置參數獲取和註冊Transform。

注意加載配置項須要延遲一點,例如project.afterEvaluate{}裏面獲取。

class RegisterPlugin implements Plugin<Project> {

    // 定義gradle配置名稱
    static final String EXT_CONFIG_NAME = 'componentRegister'

    @Override
    void apply(Project project) {
        LogUtils.i("RegisterPlugin 1.1.0 $project.name")

        // 註冊Transform
        def transform = registerTransform(project)

        // 建立配置項
        project.extensions.create(EXT_CONFIG_NAME, ComponentRegisterConfig)

        project.afterEvaluate {
            // 獲取配置項
            ComponentRegisterConfig config = project.extensions.findByName(EXT_CONFIG_NAME)
            // 配置項設置設置默認值
            config.setDefaultValue()

            LogUtils.logEnable = config.isDebug
            LogUtils.i("RegisterPlugin apply config = ${config}")

            transform.setConfig(config)

            // 保存配置緩存,判斷改動設置UpToDate狀態
            CacheUtils.handleUpToDate(project, config)
        }
    }

    // 註冊Transform
    static registerTransform(Project project) {
        LogUtils.i("RegisterPlugin-registerTransform :" + " project = " + project)

        // 初始化Transform
        def extension = null, transform = null
        if (project.plugins.hasPlugin(AppPlugin)) {
            extension = project.extensions.getByType(AppExtension)
            transform = new ComponentRegisterAppTransform(project)
        } else if (project.plugins.hasPlugin(LibraryPlugin)) {
            extension = project.extensions.getByType(LibraryExtension)
            transform = new ComponentRegisterLibTransform(project)
        }

        LogUtils.i("extension = ${extension} \ntransform = $transform")

        if (extension != null && transform != null) {
            // 註冊Transform
            extension.registerTransform(transform)
            LogUtils.i("register transform")
        } else {
            throw new RuntimeException("can not register transform")
        }
        return transform
    }

}

複製代碼

4.2 繼承Transform

集成Transform實現抽象方法;

getName():配置名字;

getInputTypes():配置處理內容,例如class內容,jar內容,資源內容等,可多選

getScopes():配置處理範圍,例如當前模塊,子模塊等,可多選;

isIncremental():是否支持增量;

transform(transformInvocation):轉換邏輯處理;

  • 判斷jar是否須要導包,是則加入導包列表
  • 判斷class是否須要導包,是則加入導包列表
  • 根據config配置注入代碼
  • 保存緩存

**注意:**library模塊範圍只能配置爲當前模塊;

class BaseComponentRegisterTransform extends Transform {

    // 組件註冊配置
    protected ComponentRegisterConfig config

    // Project
    protected Project project

    // Transform 顯示名字,只是部分,真實顯示還有前綴和後綴
    protected String name = this.class.simpleName

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

    @Override
    String getName() {
        return name
    }

    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        // 處理的類型,這裏是要處理class文件
        return TransformManager.CONTENT_CLASS
    }

    @Override
    Set<QualifiedContent.Scope> getScopes() {
        // 處理範圍,這裏是整個項目全部資源,library只能處理本模塊
        return TransformManager.SCOPE_FULL_PROJECT
    }

    @Override
    boolean isIncremental() {
        // 是否支持增量
        return true
    }

    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        LogUtils.i("${this.class.name} start ")

        if (!config.componentRegisterEnable) {
            LogUtils.r("componentRegisterEnable = false")
            return
        }

        // 緩存信息,決解UpTpDate緩存沒法控制問題
        ConfigCache configInfo = new ConfigCache()
        configInfo.configString = config.configString()

        // 遍歷輸入文件
        transformInvocation.getInputs().each { TransformInput input ->
            // 遍歷jar
            input.jarInputs.each { JarInput jarInput ->
                File dest = transformInvocation.outputProvider.getContentLocation(jarInput.name, jarInput.contentTypes, jarInput.scopes, Format.JAR)
                // 複製jar到目標目錄
                FileUtils.copyFile(jarInput.file, dest)
                // 查看是否須要導包,是則加入導包列表
                InsertCodeUtils.scanImportClass(dest.toString(), config)
            }

            // 遍歷源碼目錄文件
            input.directoryInputs.each { DirectoryInput directoryInput ->
                // 得到輸出的目錄
                File dest = transformInvocation.outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
                // 複製文件夾到目標目錄
                FileUtils.copyDirectory(directoryInput.file, dest)
                // 查看是否須要導包,是則加入導包列表
                InsertCodeUtils.scanImportClass(dest.toString(), config)
            }
        }

        // 代碼注入
        def result = InsertCodeUtils.insertCode(config)
        LogUtils.i("insertCode result = ${result}")
        LogUtils.r("${result.message}")
        if (!result.state) {
            // 插入代碼異常,終止編譯打包
            throw new Exception(result.message)
        }
        // 緩存-記錄路徑
        configInfo.destList.add(config.mainClassPath)
        // 保存緩存文件
        CacheUtils.saveConfigInfo(project, configInfo)
    }

    ComponentRegisterConfig getConfig() {
        return config
    }

    void setConfig(ComponentRegisterConfig config) {
        this.config = config
    }
}
複製代碼

transform(TransformInvocation transformInvocation)方法中的參數包含須要操做的數據。使用getInputs()獲取輸入的class或者jar內容,遍歷掃描到匹配的類,將的組件實例代碼注入到類中,再將文件複製到getOutputProvider()獲取class或者jar對應的輸出路徑裏面。

package com.android.build.api.transform;

import java.util.Collection;

public interface TransformInvocation {
    Context getContext();

    // 輸入內容
    Collection<TransformInput> getInputs();

    Collection<TransformInput> getReferencedInputs();

    Collection<SecondaryInput> getSecondaryInputs();

    // 輸出內容提供者
    TransformOutputProvider getOutputProvider();

    boolean isIncremental();
}
複製代碼

4.3 使用javassist注入組件註冊代碼

使用javassist來做爲代碼插樁工具,後續熟練字節碼編輯再樁位asm實現;

  • 加載須要的類路徑和jar;
  • 獲取注入代碼的類和方法;
  • 根據配置信息將組件實例代碼注入;
  • 釋放ClassPool佔用資源;
class InsertCodeUtils {

    /** * 注入組件實例代碼 * @param config 組件注入配置 * @return 注入狀態["state":true/false] */
    static insertCode(ComponentRegisterConfig config) {
        def result = ["state": false, "message":"component insert cant insert"]
        def classPathCache = []
        LogUtils.i("InsertCodeUtils config = ${config}")

        // 實例類池
        ClassPool classPool = new ClassPool()
        classPool.appendSystemPath()

        // 添加類路徑
        config.classPathList.each { jarPath ->
            appendClassPath(classPool, classPathCache, jarPath)
        }

        CtClass ctClass = null
        try {
            // 獲取注入註冊代碼的類
            ctClass = classPool.getCtClass(config.componentMain)
            LogUtils.i("ctClass ${ctClass}")

            if (ctClass.isFrozen()) {
                // 若是凍結就解凍
                ctClass.deFrost()
            }

            // 獲取注入方法
            CtMethod ctMethod = ctClass.getDeclaredMethod(config.componentMethod)
            LogUtils.i("ctMethod = $ctMethod")

            // 判斷是否有組件容器
            boolean hasComponentContainer = false
            ctClass.fields.each { field ->
                if (field.name == config.componentContainer) {
                    hasComponentContainer = true
                }
            }
            if (!hasComponentContainer) {
                CtField componentContainerField = new CtField(classPool.get("java.util.HashMap"), config.componentContainer, ctClass)
                componentContainerField.setModifiers(Modifier.PUBLIC | Modifier.STATIC | Modifier.FINAL)
                ctClass.addField(componentContainerField, "new java.util.HashMap();")
            }

            // 注入組件實例代碼
            String insertCode = ""
            // 記錄組件注入狀況,用於日誌輸出
            def componentInsertSuccessList = []
            def errorComponent = config.componentRegisterList.find { component ->
                LogUtils.i("component = ${component}")
                if (component.enable) {
                    String instanceCode = component.singleton ? "${component.instanceClass}.getInstance()" : "new ${component.instanceClass}()"
                    insertCode = """${config.componentContainer}.put("${component.componentName}", ${instanceCode});"""
                    LogUtils.i("insertCode = ${insertCode}")
                    try {
                        ctMethod.insertBefore(insertCode)
                        componentInsertSuccessList.add(component.componentName)
                        return false
                    } catch (Exception e) {
                        if (LogUtils.logEnable) { e.printStackTrace() }
                        result = ["state": false, "message":"""insert "${insertCode}" error : ${e.getMessage()}"""]
                        return true
                    }
                }
            }
            LogUtils.i("errorComponent = ${errorComponent}")
            if (errorComponent == null) {
                File mainClassPathFile = new File(config.mainClassPath)
                if (mainClassPathFile.name.endsWith('.jar')) {
                    // 將修改的類保存到jar中
                    saveToJar(config, mainClassPathFile, ctClass.toBytecode())
                } else {
                    ctClass.writeFile(config.mainClassPath)
                }
                result = ["state": true, "message": "component register ${componentInsertSuccessList}"]
            }
        } catch (Exception e) {
            LogUtils.r("""error : ${e.getMessage()}""")
            if (LogUtils.logEnable) { e.printStackTrace() }
        } finally {
            // 須要釋放資源,不然會io佔用
            if (ctClass != null) {
                ctClass.detach()
            }
            if (classPool != null) {
                classPathCache.each { classPool.removeClassPath(it) }
                classPool = null
            }
        }
        return result
    }

    static saveToJar(ComponentRegisterConfig config, File jarFile, byte[] codeBytes) {
        if (!jarFile) {
            return
        }
        def mainJarFile = null
        JarOutputStream jarOutputStream = null
        InputStream inputStream = null

        try {
            String mainClass = "${config.componentMain.replace(".", "/")}.class"

            def tempJarFile = new File(config.mainJarFilePath)
            if (tempJarFile.exists()) {
                tempJarFile.delete()
            }

            mainJarFile = new JarFile(jarFile)
            jarOutputStream = new JarOutputStream(new FileOutputStream(tempJarFile))
            Enumeration enumeration = mainJarFile.entries()

            while (enumeration.hasMoreElements()) {
                try {
                    JarEntry jarEntry = (JarEntry) enumeration.nextElement()
                    String entryName = jarEntry.getName()
                    ZipEntry zipEntry = new ZipEntry(entryName)
                    inputStream = mainJarFile.getInputStream(jarEntry)
                    jarOutputStream.putNextEntry(zipEntry)
                    if (entryName == mainClass) {
                        jarOutputStream.write(codeBytes)
                    } else {
                        jarOutputStream.write(IOUtils.toByteArray(inputStream))
                    }
                } catch (Exception e) {
                    LogUtils.r("""error : ${e.getMessage()}""")
                    if (LogUtils.logEnable) { e.printStackTrace() }
                } finally {
                    FileUtils.close(inputStream)
                    if (jarOutputStream != null) {
                        jarOutputStream.closeEntry()
                    }
                }
            }
        } catch (Exception e) {
            LogUtils.r("""error : ${e.getMessage()}""")
            if (LogUtils.logEnable) { e.printStackTrace() }
        } finally {
            FileUtils.close(jarOutputStream, mainJarFile)
        }
    }

    /** * 緩存添加類路徑 * @param classPool 類池 * @param classPathCache 類路徑緩存 * @param classPath 類路徑 */
    static void appendClassPath(ClassPool classPool, classPathCache, classPath) {
        classPathCache.add(classPool.appendClassPath(classPath))
    }

    // 檢測classPath是否包含任意一個classList類
    static scanImportClass(String classPath, ComponentRegisterConfig config) {
        ClassPool classPool = null
        def classPathCache = null
        try {
            classPool = new ClassPool()
            classPathCache = classPool.appendClassPath(classPath)
            def clazz = config.classNameList.find {
                classPool.getOrNull(it) != null
            }
            if (clazz != null) {
                config.classPathList.add(classPath)
            }
            if (clazz == config.componentMain) {
                if (classPath.endsWith(".jar")) {
                    File src = new File(classPath)
                    File dest = new File(src.getParent(), "temp_${src.getName()}")
                    org.apache.commons.io.FileUtils.copyFile(src, dest)
                    config.mainClassPath = dest.toString()
                    config.mainJarFilePath = classPath
                } else {
                    config.mainClassPath = classPath
                }
            }
        }  catch (Exception e) {
            LogUtils.r("""error : ${e.getMessage()}""")
            if (LogUtils.logEnable) { e.printStackTrace() }
        } finally {
            if (classPool != null && classPathCache != null) classPool.removeClassPath(classPathCache)
        }
    }
}
複製代碼

4.4 使用緩存決解UpToDate

Transform沒有配置可更新或者強制更新選項,Transform依賴的Task沒法獲取(或者許能經過名字獲取)。

這樣就形成判斷是否須要更新執行Transform的條件是內置定義好的,而Gradle配置改變時沒法改變UpToDate條件,因此會致使修改了Gradle配置選項可是注入代碼沒有改變。

UpToDate

4.4.1Gralde 4.10.1跳過任務執行

這裏來了解下Task緩存判斷條件

  • onlyif

    task.onlyif{ false } // return false 跳過任務執行
    複製代碼
  • StopExecutionException

    task.doFirst{
        throw new StopExecutionException()
    }
    複製代碼
  • enable

    task.enbale = false
    複製代碼
  • input和output

    As part of incremental build, Gradle tests whether any of the task inputs or outputs have changed since the last build. If they haven’t, Gradle can consider the task up to date and therefore skip executing its actions. Also note that incremental build won’t work unless a task has at least one task output, although tasks usually have at least one input as well.

    谷歌翻譯:做爲增量構建的一部分,Gradle會測試自上次構建以來是否有任何任務輸入或輸出已更改。若是他們沒有,Gradle能夠認爲該任務是最新的,所以跳過執行其操做。另請注意,除非任務至少有一個任務輸出,不然增量構建將不起做用,儘管任務一般也至少有一個輸入。

    在第一次執行任務以前,Gradle會對輸入進行快照。此快照包含輸入文件的路徑和每一個文件內容的哈希。Gradle而後執行任務。若是任務成功完成,Gradle將獲取輸出的快照。此快照包含輸出文件集和每一個文件內容的哈希值。Gradle會在下次執行任務時保留兩個快照。

    每次以後,在執行任務以前,Gradle會獲取輸入和輸出的新快照。若是新快照與先前的快照相同,則Gradle會假定輸出是最新的並跳過任務。若是它們不相同,Gradle將執行該任務。Gradle會在下次執行任務時保留兩個快照。

    Task 編譯

決解方法: 基於第4點輸入和輸出快照變化會使Taask執行條件爲true,因此咱們能夠在須要從新注入代碼時,把輸出內容的代碼注入文件刪除便可保證任務正常執行,同時也能夠保證緩存使用加快編譯速度。

class CacheUtils {
    // 緩存文件夾,在構建目錄下
    final static String CACHE_INFO_DIR = "component_register"
    // 緩存文件
    final static String CACHE_CONFIG_FILE_NAME = "config.txt"

    /** * 保存配置信息 * @param project project * @param configInfo 配置信息 */
    static void saveConfigInfo(Project project, ConfigCache configInfo) {
        saveConfigCache(project, new Gson().toJson(configInfo))
    }

    /** * 保存配置信息 * @param project project * @param config 配置信息 */
    static void saveConfigCache(Project project, String config) {
        LogUtils.i("HelperUtils-saveConfigCache :" + " project = " + project + " config = " + config)
        try {
            FileUtils.writeStringToFile(getRegisterInfoCacheFile(project), config, Charset.defaultCharset())
        } catch (Exception e) {
            LogUtils.i("saveConfigCache error ${e.message}")
        }
    }

    /** * 讀取配置緩存信息 * @param project project * @return 配置信息 */
    static String readConfigCache(Project project) {
        try {
            return FileUtils.readFileToString(getRegisterInfoCacheFile(project), Charset.defaultCharset())
        } catch (Exception e) {
            LogUtils.i("readConfigCache error ${e.message}")
        }
        return ""
    }

    /** * 緩存自動註冊配置的文件 * @param project * @return file */
    static File getRegisterInfoCacheFile(Project project) {
        File baseFile = new File(getCacheFileDir(project))
        if (baseFile.exists() || baseFile.mkdirs()) {
            File cacheFile = new File(baseFile, CACHE_CONFIG_FILE_NAME)
            if (!cacheFile.exists()) cacheFile.createNewFile()
            return cacheFile
        } else {
            throw new FileNotFoundException("Not found path:" + baseFile)
        }
    }

    /** * 獲取緩存文件夾路徑 * @param project project * @return 緩存文件夾路徑 */
    static String getCacheFileDir(Project project) {
        return project.getBuildDir().absolutePath + File.separator + AndroidProject.FD_INTERMEDIATES + File.separator + CACHE_INFO_DIR
    }

    /** * 判斷是否須要強制執行Task * @param project project * @param config 配置信息 * @return true:強制執行 */
    static boolean handleUpToDate(Project project, ComponentRegisterConfig config) {
        LogUtils.i("HelperUtils-handleUpToDate :" + " project = " + project + " config = " + config)
        Gson gson = new Gson()
        String configInfoText = getRegisterInfoCacheFile(project).text
        LogUtils.i("configInfoText = ${configInfoText}")
        ConfigCache configInfo = gson.fromJson(configInfoText, ConfigCache.class)
        LogUtils.i("configInfo = ${configInfo}")
        if (configInfo != null && configInfo.configString != config.toString()) {
            configInfo.destList.each {
                LogUtils.i("delete ${it}")
                File handleFile = new File(it)
                if (handleFile.isDirectory()) {
                    FileUtils.deleteDirectory(handleFile)
                } else {
                    handleFile.delete()
                }
            }
        }
    }

}

複製代碼

參考資料

相關文章
相關標籤/搜索