replugin源碼解析之replugin-plugin-gradle(插件的gradle插件)

前言

replugin-plugin-gradle 是 RePlugin 插件框架中提供給replugin插件用的gradle插件,是一種動態(tài)編譯方案實現(xiàn)。
主要在插件應用的編譯期蓬衡,基于Transform api 注入到編譯流程中, 再通過Java字節(jié)碼類庫對編譯中間環(huán)節(jié)的 Java 字節(jié)碼文件進行修改谨读,以便實現(xiàn)編譯期動態(tài)修改插件應用的目的扎谎。
RePlugin 是一套完整的危喉、穩(wěn)定的、適合全面使用的弹灭,占坑類插件化方案督暂,由360手機衛(wèi)士的RePlugin Team研發(fā),也是業(yè)內首個提出”全面插件化“(全面特性鲤屡、全面兼容损痰、全面使用)的方案。

注 :文件會提及兩種插件酒来,請閱讀本文時注意提及插件的上下文情景卢未,避免混淆概念:

  • replugin插件:即replugin插件化框架所指的插件,這個插件指android應用業(yè)務拆分出的獨立模塊堰汉,是android應用或模塊辽社。
  • gradle插件:即gradle構建所需的構建插件,是gradle應用或模塊翘鸭。

結構概覽

replugin-plugin-gradle滴铅,針對插件應用編譯期的注入任務:
動態(tài)修改插件中的調用代碼,改為調用replugin-plugin-library中的代碼(如Activity的繼承就乓、Provider的重定向等)

  • LoaderActivityInjector 動態(tài)將插件中的Activity的繼承相關代碼 修改為 replugin-plugin-library 中的XXPluginActivity父類
  • LocalBroadcastInjector 替換插件中的LocalBroadcastManager調用代碼 為 插件庫的調用代碼汉匙。
  • ProviderInjector 替換 插件中的 ContentResolver 調用代碼 為 插件庫的調用代碼
  • ProviderInjector2 替換 插件中的 ContentProviderClient 調用代碼 為 插件庫的調用代碼
  • GetIdentifierInjector 替換 插件中的 Resource.getIdentifier 調用代碼的參數 為 動態(tài)適配的參數
  • replugin-plugin-gradle插件的工作流:基于Gradle的Transform API,在編譯期的構建任務流中生蚁,class轉為dex之前噩翠,插入一個Transform,并在此Transform流中邦投,基于Javassist實現(xiàn)對字節(jié)碼文件的注入伤锚。

目錄概覽

\qihoo\replugin\replugin-plugin-gradle\src
└─main
    ├─groovy
    │  └─com
    │      └─qihoo360
    │          └─replugin
    │              └─gradle
    │                  └─plugin
    │                      │  AppConstant.groovy                      # 程序常量定義區(qū)
    │                      │  ReClassPlugin.groovy                    # 插件動態(tài)編譯方案入口
    │                      │  
    │                      ├─debugger
    │                      │      PluginDebugger.groovy               # 用于插件調試的gradle task實現(xiàn)
    │                      │      
    │                      ├─injector
    │                      │  │  BaseInjector.groovy                  # 注入器基類
    │                      │  │  IClassInjector.groovy                # 注入器接口類
    │                      │  │  Injectors.groovy                     # 注入器枚舉類,定義了全部注入器
    │                      │  │  
    │                      │  ├─identifier
    │                      │  │      GetIdentifierExprEditor.groovy   # javassist 允許修改方法里的某個表達式志衣,此類為替換 getIdentifier 方法中表達式的實現(xiàn)類
    │                      │  │      GetIdentifierInjector.groovy     # GetIdentifier 方法注入器
    │                      │  │      
    │                      │  ├─loaderactivity
    │                      │  │      LoaderActivityInjector.groovy    # Activity代碼注入器
    │                      │  │      
    │                      │  ├─localbroadcast
    │                      │  │      LocalBroadcastExprEditor.groovy  # 替換幾個廣播相關方法表達式的實現(xiàn)類
    │                      │  │      LocalBroadcastInjector.groovy    # 廣播代碼注入器
    │                      │  │      
    │                      │  └─provider
    │                      │          ProviderExprEditor.groovy       # 替換ContentResolver類的幾個方法表達式
    │                      │          ProviderExprEditor2.groovy      # 替換ContentProviderClient類的幾個方法表達式
    │                      │          ProviderInjector.groovy         # Provider之ContentResolver代碼注入器
    │                      │          ProviderInjector2.groovy        # Provider之ContentProviderClient代碼注入器
    │                      │          
    │                      ├─inner
    │                      │      ClassFileVisitor.groovy             # 類文件遍歷類
    │                      │      CommonData.groovy                   # 實體類
    │                      │      ReClassTransform.groovy             # 核心類屯援,基于 transform api 實現(xiàn)動態(tài)修改class文件的總調度入口
    │                      │      Util.groovy                         # 工具類
    │                      │      
    │                      ├─manifest
    │                      │      IManifest.groovy                    # 接口類
    │                      │      ManifestAPI.groovy                  # 操作Manifest的API類
    │                      │      ManifestReader.groovy               # Manifest讀取工具類
    │                      │      
    │                      └─util
    │                              CmdUtil.groovy                     # 命令行工具類
    │                              
    └─resources
        └─META-INF
            └─gradle-plugins
                    replugin-plugin-gradle.properties                 # 指定 gradle 插件實現(xiàn)類

replugin-plugin-gradle的基本用法

  • 添加 RePlugin Plugin Gradle 依賴
    在項目根目錄的 build.gradle(注意:不是 app/build.gradle) 中添加 replugin-plugin-gradle 依賴:
buildscript {
    dependencies {
        classpath 'com.qihoo360.replugin:replugin-plugin-gradle:2.1.5'
        ...
    }
}

在項目的app模塊中的build.gradle應用插件:

apply plugin: 'replugin-plugin-gradle'

replugin-plugin-gradle的源碼解析

我們在開始閱讀源碼前猛们,要思考下,replugin-plugin-gradle是什么狞洋?
A:replugin-plugin-gradle是一個自定義的gradle插件弯淘。
這個清楚了,沒上車的上車徘铝,上車了的別動耳胎!

replugin-plugin-gradle.properties文件

implementation-class=com.qihoo360.replugin.gradle.plugin.ReClassPlugin

在開發(fā)自定義gradle插件時,都會先定義這么個文件惕它。這里有 2 個知識點:

  • 文件中的implementation-class用來指定插件實現(xiàn)類。
  • 文件名用來指定插件名废登,即在插件中使用gradle插件時的apply plugin: 'replugin-plugin-gradle'中的replugin-plugin-gradle.

我們到插件實現(xiàn)類看看這個插件是如何工作的淹魄。

ReClassPlugin.groovy文件

public class ReClassPlugin implements Plugin<Project> {
    @Override
        public void apply(Project project) {
            println "${AppConstant.TAG} Welcome to replugin world ! "
            ...
    }
}

定義了一個類ReClassPlugin,繼承自gradle-api 庫中的接口類 Plugin<Project> 堡距,實現(xiàn)了apply接口方法甲锡,apply方法會在 build.gradle 中執(zhí)行 apply plugin: 'replugin-plugin-gradle'時被調用。

接下來解讀下 apply 方法的具體實現(xiàn)羽戒。

用于快速調試的gradle task

@Override
    public void apply(Project project) {

        println "${AppConstant.TAG} Welcome to replugin world ! "

        /* Extensions */
        project.extensions.create(AppConstant.USER_CONFIG, ReClassConfig)

        def isApp = project.plugins.hasPlugin(AppPlugin)
        if (isApp) {

            def config = project.extensions.getByName(AppConstant.USER_CONFIG)

            def android = project.extensions.getByType(AppExtension)

            ...

            android.applicationVariants.all { variant ->
                PluginDebugger pluginDebugger = new PluginDebugger(project, config, variant)

                def variantData = variant.variantData
                def scope = variantData.scope

                def assembleTask = variant.getAssemble()

                def installPluginTaskName = scope.getTaskName(AppConstant.TASK_INSTALL_PLUGIN, "")
                def installPluginTask = project.task(installPluginTaskName)

                installPluginTask.doLast {
                    pluginDebugger.startHostApp()
                    pluginDebugger.uninstall()
                    pluginDebugger.forceStopHostApp()
                    pluginDebugger.startHostApp()
                    pluginDebugger.install()
                }
                installPluginTask.group = AppConstant.TASKS_GROUP
                ...
            }
        }
    }
  • 首先向Plugin傳遞參數缤沦,通過project.extensions.create(AppConstant.USER_CONFIG, ReClassConfig),將ReClassConfig類的常量配置信息賦值給AppConstant.USER_CONFIG易稠,后面有兩個地方會用到:一個是PluginDebugger類中要用到一些參數缸废;另一個是做動態(tài)編譯時要用到一些參數;后面邏輯會陸續(xù)用到驶社。

  • 判斷project中是否含有AppPlugin類型插件企量,即是否有'application' projects類型的Gradle plugin。我們在replugin插件項目中是應用了該類型插件的:apply plugin: 'com.android.application'.

  • 獲取project中的AppExtension類型extension亡电,即com.android.application projects的android extension.也就是在你的app模塊的build.gradle中定義的閉包:

android {
    ...
}
  • android.applicationVariants.all届巩,遍歷android extension的Application variants 組合。android gradle 插件份乒,會對最終的包以多個維度進行組合恕汇。ApplicationVariant的組合 = {ProductFlavor} x {BuildType} 種組合.
  • new PluginDebugger(project, config, variant),初始化PluginDebugger類實例或辖,主要配置了最終生成的插件應用的文件路徑瘾英,以及adb文件的路徑,是為了后續(xù)基于adb命令做push apk到SD卡上做準備孝凌。
apkFile = new File(apkDir, apkName)
adbFile = globalScope.androidBuilder.sdkInfo.adb;
  • def assembleTask = variant.getAssemble()方咆,獲取assemble task(即打包apk的task),后續(xù)的task需要依賴此task蟀架,比如安裝插件的task瓣赂,肯定要等到assemble task打包生成apk后榆骚,才能去執(zhí)行。
  • 生成installPluginTask 的gradle task 名字煌集,并調用project的task()方法創(chuàng)建此Task妓肢。然后指定此task的任務內容:
installPluginTask.doLast {
    pluginDebugger.startHostApp()
    pluginDebugger.uninstall()
    pluginDebugger.forceStopHostApp()
    pluginDebugger.startHostApp()
    pluginDebugger.install()
}
  • 流程:啟動宿主 -> 卸載插件 -> 強制停止宿主 -> 啟動宿主 -> 安裝插件
  • pluginDebugger 內的方法實現(xiàn):基于adb shell + am 命令,實現(xiàn) 發(fā)送廣播苫纤,push apk 等功能碉钠。,比如:pluginDebugger.startHostApp()
public boolean startHostApp() {

        if (isConfigNull()) {
            return false
        }

        String cmd = "${adbFile.absolutePath} shell am start -n \"${config.hostApplicationId}/${config.hostAppLauncherActivity}\" -a android.intent.action.MAIN -c android.intent.category.LAUNCHER"
        if (0 != CmdUtil.syncExecute(cmd)) {
            return false
        }
        return true
    }

pluginDebugger類的其他操作應用的方法,基本思路是一致的卷拘,基于adb+am命令喊废。

  • apply()方法中共有如下幾個gradle task(查看task: gradlew.bat task 或 gradlew.bat tasks --all):

以上task分別有不同的調試目的,可以去分別了解下栗弟,細節(jié)實現(xiàn)大同小異污筷。
看到這里,我們該插播一下調試方案的整體原理了:

  1. replugin-host-lib 的DebuggerReceivers類中乍赫,注冊了一系列用于快速調試的廣播瓣蛀,而replugin-host-lib是會內置在宿主應用中的。
  2. replugin-plugin-gradle 中創(chuàng)建了一系列gradle task雷厂,用于啟動停止重啟宿主應用惋增,安裝卸載運行插件應用。這些gradle task都是被動型task改鲫,需要通過命令行主動的運行這些task诈皿。
  3. 打開命令行終端,執(zhí)行replugin插件項目的某個gradle task钩杰,以實現(xiàn)快速調試功能纫塌。比如:gradlew.bat rpInstallPluginDebug,最終就會將宿主和插件運行起來讲弄。
  4. 這些gradle task被手動執(zhí)行后措左,task會執(zhí)行一系列任務,比如通過adb push 插件到sdcard避除,或通過am命令發(fā)送廣播怎披,啟動activity等。當發(fā)送一系列步驟1中注冊的廣播后瓶摆,宿主應用收到廣播后會執(zhí)行對應的操作凉逛,比如啟動插件的activity等。

Tips.調試模式開啟方法:插件調試
Debug階段建議開啟,Release階段建議關閉,默認為關閉狀態(tài)

繼續(xù)看apply()方法中的源碼群井。

Transform:動態(tài)編譯方案實現(xiàn)

@Override
    public void apply(Project project) {
        ...
        if (isApp) {

            ...

            def transform = new ReClassTransform(project)
            // 將 transform 注冊到 android
            android.registerTransform(transform)
            ...
        }
    }

重點來了状飞,這里就是動態(tài)編譯方案的實現(xiàn)入口。
在詳細解讀動態(tài)編譯實現(xiàn)之前,先了解2個概念:

  • 什么是 Transform诬辈?

  • Transform 是 Android Gradle API 酵使,允許第三方插件在class文件轉為dex文件前操作編譯完成的class文件,這個API的引入是為了簡化class文件的自定義操作而無需對Task進行處理焙糟。在做代碼插樁時口渔,本質上是在merge{ProductFlavor}{BuildType}Assets Task 之后,transformClassesWithDexFor{ProductFlavor}{BuildType} Transform 之前,插入一個transformClassesWith{YourTransformName}For{ProductFlavor}{BuildType} Transform穿撮,此Transform中完成對class文件的自定義操作(包括修改父類繼承缺脉,方法中的super方法調用,方法參數替換等等悦穿,這個class交給你攻礼,理論上是可以改到懷疑人生)。

  • 詳細API參見:Transform

  • 如何使用 Transform咧党?

  • 實現(xiàn)一個繼承自Transform的自定義 Transform 類秘蛔。

  • 通過registerTransform(@NonNull Transform transform, Object... dependencies)注冊自定義 Transform 類。

去看看 ReClassTransform 類的核心實現(xiàn)傍衡。

public class ReClassTransform extends Transform {
    @Override
    String getName() {
        return '___ReClass___'
    }

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

        welcome()

        /* 讀取用戶配置 */
        def config = project.extensions.getByName('repluginPluginConfig')

        ...

        // Compatible with path separators for window and Linux, and fit split param based on 'Pattern.quote'
        def variantDir = rootLocation.absolutePath.split(getName() + Pattern.quote(File.separator))[1]

        CommonData.appModule = config.appModule
        CommonData.ignoredActivities = config.ignoredActivities

        def injectors = includedInjectors(config, variantDir)
        if (injectors.isEmpty()) {
            copyResult(inputs, outputProvider) // 跳過 reclass
        } else {
            doTransform(inputs, outputProvider, config, injectors) // 執(zhí)行 reclass
        }
    }

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

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

    @Override
    boolean isIncremental() {
        return false
    }
}
  • getName(),即指定剛才提到的那個插入的transform transformClassesWith{YourTransformName}For{ProductFlavor}{BuildType}中的{YourTransformName}负蠕。

  • transform() 方法會在執(zhí)行你的transform時被調用蛙埂。

  • project.extensions.getByName('repluginPluginConfig')讀取用戶在replugin插件項目的build.gradle中配置的參數,比如設置了需要忽略的注入器ignoredInjectors遮糖、需要忽略替換的ActivityignoredActivities绣的、自定義的代碼注入器customInjectors等。

  • includedInjectors()返回用戶未忽略的注入器的集合

  • LoaderActivityInjector 替換插件中的Activity的繼承相關代碼 為 replugin-plugin-library 中的XXPluginActivity父類

  • LocalBroadcastInjector 替換插件中的LocalBroadcastManager調用代碼 為 插件庫的調用代碼欲账。

  • ProviderInjector 替換 插件中的 ContentResolver 調用代碼 為 插件庫的調用代碼

  • ProviderInjector2 替換 插件中的 ContentProviderClient 調用代碼 為 插件庫的調用代碼

  • GetIdentifierInjector 替換 插件中的 Resource.getIdentifier 調用代碼的參數 為 動態(tài)適配的參數

  • getInputTypes() 指明當前Trasfrom要處理的數據類型,可選類型包括CONTENT_CLASS(代表要處理的數據是編譯過的Java代碼屡江,而這些數據的容器可以是jar包也可以是文件夾),CONTENT_JARS(包括編譯過的Java代碼和標準的Java資源)赛不,CONTENT_RESOURCES惩嘉,CONTENT_NATIVE_LIBS等。在replugin-plugin-gradle中是使用Transform來做代碼插樁,所以選用CONTENT_CLASS類型踢故。

  • getScopes() 配置當前Transform的作用域文黎,實際使用中可以根據需求配置多種Scope

  • doTransform()方法是執(zhí)行reclass的關鍵

    def doTransform(Collection<TransformInput> inputs,
                    TransformOutputProvider outputProvider,
                    Object config,
                    def injectors) {

        /* 初始化 ClassPool */
        Object pool = initClassPool(inputs)
        ...
    }
  • Transform方法中的參數inputsoutputProvider一定程度上反映了Transform的工作流殿较,接受輸入->處理輸入->輸出數據耸峭。
  • initClassPool(...)方法主要的工作:添加編譯時引用到的類ClassPool,同時記錄要修改的 jarincludeJars淋纲。方便后續(xù)拿到這些class文件去修改劳闹。比如Sample中會添加的class路徑:
>>> ClassPath:
...
// 插件項目replugin-sample的class目錄
    E:\opensource\qihoo\RePlugin\replugin-sample\plugin\plugin-demo1\app\build\intermediates\classes\debug

Javassit 是一個處理Java字節(jié)碼的類庫。
CtMethod:是一個class文件中的方法的抽象表示。一個CtMethod對象表示一個方法本涕。(Javassit 庫API)
CtClass:是一個class文件的抽象表示业汰。一個CtClass(compile-time class)對象可以用來處理一個class文件。(Javassit 庫API)
ClassPool:是一個CtClass對象的容器類偏友。(Javassit 庫API)
.class文件:.class文件是一種存儲Java字節(jié)碼的二進制文件蔬胯,里面包含一個Java類或者接口。

    def doTransform(Collection<TransformInput> inputs,
                    TransformOutputProvider outputProvider,
                    Object config,
                    def injectors) {

        ...

        /* 進行注入操作 */
        Injectors.values().each {
            
            ...    
                doInject(inputs, pool, it.injector, config.properties["${configPre}Config"])
            ...
        }

        if (config.customInjectors != null) {
            config.customInjectors.each {
                doInject(inputs, pool, it)
            }
        }
        ...
    }

這里會遍歷除了用戶已忽略過的全部代碼注入器位他,依次執(zhí)行每個注入器的特定注入任務氛濒。
看下doInject(...)方法實現(xiàn)。

    /**
     * 執(zhí)行注入操作
     */
    def doInject(Collection<TransformInput> inputs, ClassPool pool,
                 IClassInjector injector, Object config) {
        try {
            inputs.each { TransformInput input ->
                input.directoryInputs.each {
                    handleDir(pool, it, injector, config)
                }
                input.jarInputs.each {
                    handleJar(pool, it, injector, config)
                }
            }
        } catch (Throwable t) {
            println t.toString()
        }
    }

分別處理目錄中的 class 文件和處理 jar

    def handleDir(ClassPool pool, DirectoryInput input, IClassInjector injector, Object config) {
        println ">>> Handle Dir: ${input.file.absolutePath}"
        injector.injectClass(pool, input.file.absolutePath, config)
    }

接下來就是那些注入器八仙過海鹅髓,各顯神通的時候了舞竿。還記得嗎,前面那句代碼Injectors.values().each {窿冯,這是要用每個注入器都把class們擼一遍骗奖。

LoaderActivityInjector

第一個被執(zhí)行的就是 LoaderActivityInjector,用來修改插件中XXActivity類中的頂級XXActivity父類 為 XXPluginActivity父類醒串≈醋溃看看如何實現(xiàn)的。

@Override
    def injectClass(ClassPool pool, String dir, Map config) {
        println ">>> LoaderActivityInjector dir: $dir"
        init()

        /* 遍歷程序中聲明的所有 Activity */
        //每次都new一下躺率,否則多個variant一起構建時只會獲取到首個manifest
        new ManifestAPI().getActivities(project, variantDir).each {
            // 處理沒有被忽略的 Activity
            if (!(it in CommonData.ignoredActivities)) {
                handleActivity(pool, it, dir)
            }
        }
    }
  • init()指定了 Activity 替換規(guī)則馅精,只替換那些頂級Activity父類為 replugin-plugin-lib 庫中的 XXPluginActivity严嗜。
def private static loaderActivityRules = [
            'android.app.Activity'                    : 'com.qihoo360.replugin.loader.a.PluginActivity',
            'android.app.TabActivity'                 : 'com.qihoo360.replugin.loader.a.PluginTabActivity',
            'android.app.ListActivity'                : 'com.qihoo360.replugin.loader.a.PluginListActivity',
            'android.app.ActivityGroup'               : 'com.qihoo360.replugin.loader.a.PluginActivityGroup',
            'android.support.v4.app.FragmentActivity' : 'com.qihoo360.replugin.loader.a.PluginFragmentActivity',
            'android.support.v7.app.AppCompatActivity': 'com.qihoo360.replugin.loader.a.PluginAppCompatActivity',
            'android.preference.PreferenceActivity'   : 'com.qihoo360.replugin.loader.a.PluginPreferenceActivity',
            'android.app.ExpandableListActivity'      : 'com.qihoo360.replugin.loader.a.PluginExpandableListActivity'
    ]
  • 接下來遍歷插件應用AndroidManifest.xml中聲明的所有 Activity名稱,并在handleActivity(...)方法中處理這些Activity類的.class文件洲敢÷看下handleActivity(...)的實現(xiàn)細節(jié)。
private def handleActivity(ClassPool pool, String activity, String classesDir) {
        def clsFilePath = classesDir + File.separatorChar + activity.replaceAll('\\.', '/') + '.class'
        ...
        def stream, ctCls
        try {
            stream = new FileInputStream(clsFilePath)
            ctCls = pool.makeClass(stream);

            // ctCls 之前的父類
            def originSuperCls = ctCls.superclass

            /* 從當前 Activity 往上回溯压彭,直到找到需要替換的 Activity */
            def superCls = originSuperCls
            while (superCls != null && !(superCls.name in loaderActivityRules.keySet())) {
                // println ">>> 向上查找 $superCls.name"
                ctCls = superCls
                superCls = ctCls.superclass
            }

            // 如果 ctCls 已經是 LoaderActivity睦优,則不修改
            if (ctCls.name in loaderActivityRules.values()) {
                // println "    跳過 ${ctCls.getName()}"
                return
            }

            /* 找到需要替換的 Activity, 修改 Activity 的父類為 LoaderActivity */
            if (superCls != null) {
                def targetSuperClsName = loaderActivityRules.get(superCls.name)
                // println "    ${ctCls.getName()} 的父類 $superCls.name 需要替換為 ${targetSuperClsName}"
                CtClass targetSuperCls = pool.get(targetSuperClsName)

                if (ctCls.isFrozen()) {
                    ctCls.defrost()
                }
                ctCls.setSuperclass(targetSuperCls)

                // 修改聲明的父類后,還需要方法中所有的 super 調用壮不。
                ctCls.getDeclaredMethods().each { outerMethod ->
                    outerMethod.instrument(new ExprEditor() {
                        @Override
                        void edit(MethodCall call) throws CannotCompileException {
                            if (call.isSuper()) {
                                if (call.getMethod().getReturnType().getName() == 'void') {
                                    String statement = '{super.' + call.getMethodName() + '($$);}'
                                    println ">>> ${outerMethod} call.replace 1 to statement ${statement}"
                                    call.replace('{super.' + call.getMethodName() + '($$);}')
                                } else {
                                    String statement = '{super.' + call.getMethodName() + '($$);}'
                                    println ">>> ${outerMethod} call.replace 2 to statement ${statement}"
                                    call.replace('{$_ = super.' + call.getMethodName() + '($$);}')
                                }
                            }
                        }
                    })
                }

                ctCls.writeFile(CommonData.getClassPath(ctCls.name))
                println "    Replace ${ctCls.name}'s SuperClass ${superCls.name} to ${targetSuperCls.name}"
            }
        } catch (Throwable t) {
            println "    [Warning] --> ${t.toString()}"
        } finally {
            if (ctCls != null) {
                ctCls.detach()
            }
            if (stream != null) {
                stream.close()
            }
        }
    }
  • ctCls = pool.makeClass(stream)汗盘,從文件流中加載.class文件,創(chuàng)建一個CtClass實例询一,這個實例表示.class文件對應的類或接口隐孽。通過CtClass可以很方便的對.class文件進行自定義操作,比如添加方法健蕊,改方法參數菱阵,添加類成員,改繼承關系等缩功。

  • while (superCls != null && !(superCls.name in loaderActivityRules.keySet()))送粱,一級級向上遍歷ctCls的父類,找到需要替換的Activity類掂之。

  • ctCls.setSuperclass(targetSuperCls),根據初始化中設置的Activity替換規(guī)則脆丁,修改 此Activity類 的父類為 對應的插件庫中的父類世舰。例:
    public class MainActivity extends Activity {修改為public class MainActivity extends PluginActivity {

  • if (ctCls.isFrozen()) { ctCls.defrost() },如果class被凍結槽卫,則通過defrost()解凍class,以便class重新允許被修改。
    注:當CtClass 調用writeFile()混聊、toClass()季率、toBytecode() 這些方法的時候,Javassist會凍結CtClass Object躲庄,將不允許對CtClass object進行修改查剖。

  • 補充2個 Javassist 知識點:

  • 如何修改方法體?
    1.獲得一個CtMethod實例噪窘,即class中的一個方法笋庄。
    2.調用CtMethod實例的instrument(ExprEditor editor)方法,并傳遞一個ExprEditor實例(A translator of method bodies.)
    3.在ExprEditor實例中覆蓋edit(MethodCall m)方法,這里可以調用MethodCall的replace()方法來更改方法體內的代碼直砂。

  • 修改方法體的原理菌仁?
    調用CtMethod的instrument(),方法體會被逐行進行掃描静暂,從第一行掃描到最后一行济丘。發(fā)現(xiàn)有方法調用或表達式時(object creation),edit()會被調用,根據edit()內的replace()方法來修改這一行代碼洽蛀。

  • ctCls.getDeclaredMethods().each { }摹迷,經過對修改方法體的背景知識的了解,我們再看這段插樁代碼實現(xiàn)就能看懂了:

  • 遍歷class中聲明的全部方法

  • 調用每個方法的instrument方法

  • 掃描方法中的每一行表達式辱士,如果這一行表達式的調用方為此類的super類泪掀,那么就分兩種情況做處理:
    1.返回類型為void時,調用MethodCall的replace方法颂碘,替換這一行代碼為super.' + call.getMethodName() + '($$);异赫,其中$$ 是所有方法參數的簡寫,例如:m($$)等同于m($1,$2,...)头岔。
    2.返回類型非void時塔拳,調用MethodCall的replace方法,替換這一行代碼為$_ = super.' + call.getMethodName() + '($$);峡竣,其中特殊變量$_代表的是方法的返回值靠抑。因為方法調用是有返回值的,所以statement必須將返回值賦值給它适掰,這是javassist.expr.MethodCall方法的明確要求颂碧。

  • Javassist提供了一些特殊的變量來代表特定含義:

    注:在不同的 javassist 方法中使用時,這些特殊變量代表的含義可能會略有不同类浪。參見:javassist tutorial

  • 全部的類遍歷完后载城,將ctCls對象寫回到class文件中。這樣就全部完成了class文件的Activity頂級父類動態(tài)注入费就。

  • CtClass.detach()诉瓦,最后調用detach()方法,把CtClass object 從ClassPool中移除力细,避免當加載過多的CtClass object的時候睬澡,會造成OutOfMemory的異常。因為ClassPool是一個CtClass objects的裝載容器眠蚂。加載CtClass object后煞聪,默認是不釋放的。

  • 關于Jar包中的class注入:在initClassPool時已經把Jar做了unzip河狐,解壓出也是一堆.class文件米绕,其他處理邏輯同上瑟捣。也就是說,你引用的第三方sdk中的jar栅干,以及你依賴的庫中的jar迈套,都會被注入器擼一遍。

1.如果希望看看具體的代碼插樁效果碱鳞,可以基于dex2jar工具+jd-gui工具逆向你的插件apk桑李。先zip工具解壓你的apk,用dex2jar工具從dex拿到完整的jar窿给,然后用jd-gui工具看看jar中的Activity父類是不是神奇的變了贵白。或者直接apktool工具反編譯插件apk崩泡,看smali文件的改變禁荒。




2.可以基于命令行的方式gradlew.bat build編譯你的插件應用,然后查看命令行中的編譯日志角撞,會有助于你更好的理解呛伴。

LocalBroadcastInjector

LocalBroadcastInjector,實現(xiàn)了替換插件中的 LocalBroadcastManager的方法調用 為 插件庫的PluginLocalBroadcastManager中的方法調用谒所。
直接看injectClass的實現(xiàn)热康,遍歷class目錄并訪問到文件時,執(zhí)行以下這段邏輯劣领。

@Override
def injectClass(ClassPool pool, String dir, Map config) {
    ...
    try {
        // 不處理 LocalBroadcastManager.class
        if (filePath.contains('android/support/v4/content/LocalBroadcastManager')) {
            println "Ignore ${filePath}"
            return super.visitFile(file, attrs)
        }

        stream = new FileInputStream(filePath)
        ctCls = pool.makeClass(stream);

        // println ctCls.name
        if (ctCls.isFrozen()) {
            ctCls.defrost()
        }

        /* 檢查方法列表 */
        ctCls.getDeclaredMethods().each {
            it.instrument(editor)
        }

        ctCls.getMethods().each {
            it.instrument(editor)
        }

        ctCls.writeFile(dir)
    }
    ...
}
  • if (filePath.contains('android/support/v4/content/LocalBroadcastManager'))姐军,保護性邏輯,避免替換掉v4包中的源碼實現(xiàn)尖淘。
  • pool.makeClass()奕锌,創(chuàng)建當前類文件的CtClass實例。
  • ctCls.defrost() 如果CtClass實例被凍結村生,則執(zhí)行解凍操作歇攻。
  • ctCls.getDeclaredMethods().each { }ctCls.getMethods().each { },遍歷全部方法梆造,并執(zhí)行instrument方法,逐個掃描每個方法體內每一行代碼葬毫,并交由LocalBroadcastExprEditoredit()處理對方法體代碼的修改镇辉。

LocalBroadcastExprEditor.groovy

public class LocalBroadcastExprEditor extends ExprEditor {

    static def TARGET_CLASS = 'android.support.v4.content.LocalBroadcastManager'
    static def PROXY_CLASS = 'com.qihoo360.replugin.loader.b.PluginLocalBroadcastManager'

    /** 處理以下方法 */
    static def includeMethodCall = ['getInstance',
                                    'registerReceiver',
                                    'unregisterReceiver',
                                    'sendBroadcast',
                                    'sendBroadcastSync']
    ...

    @Override
    void edit(MethodCall call) throws CannotCompileException {
        if (call.getClassName().equalsIgnoreCase(TARGET_CLASS)) {
            if (!(call.getMethodName() in includeMethodCall)) {
                // println "Skip $methodName"
                return
            }

            replaceStatement(call)
        }
    }

    def private replaceStatement(MethodCall call) {
        String method = call.getMethodName()
        if (method == 'getInstance') {
            call.replace('{$_ = ' + PROXY_CLASS + '.' + method + '($$);}')
        } else {

            def returnType = call.method.returnType.getName()
            // getInstance 之外的調用,要增加一個參數贴捡,請參看 i-library 的 LocalBroadcastClient.java
            if (returnType == 'void') {
                call.replace('{' + PROXY_CLASS + '.' + method + '($0, $$);}')
            } else {
                call.replace('{$_ = ' + PROXY_CLASS + '.' + method + '($0, $$);}')
            }
        }
    }
}
  • TARGET_CLASSPROXY_CLASS分別指定了需要處理的目標類和對應的代理類
  • static def includeMethodCall中定義了需要處理的目標方法名
  • replaceStatement(...)中忽肛,替換方法體:
  • 替換getInstance:
    1)調用原型:PluginLocalBroadcastManager.getInstance(context);
    2)replace statement:'{$_ = ' + PROXY_CLASS + '.' + method + '($$);}',$$表示全部參數的簡寫烂斋。$_表示resulting value即返回值屹逛。
  • 替換registerReceiver unregisterReceiver sendBroadcastSyncreturnType == 'void'):
    1)調用原型:PluginLocalBroadcastManager.registerReceiver(instance, receiver, filter);
    2)replace statement:'{' + PROXY_CLASS + '.' + method + '($0, $$);}'础废,$0在這里就不代表this了,而是表示方法的調用方(參見:javassist tutorial)罕模,即PluginLocalBroadcastManager评腺。因為調用原型中需要入參instance(要求是PluginLocalBroadcastManager類型),所以這里必須傳入$0淑掌。
    注:unregisterReceiversendBroadcastSync同上蒿讥,調用原型請參見replugin-plugin-lib插件庫中的PluginLocalBroadcastManager.java文件。
  • 替換sendBroadcastreturnType != 'void'):
    1)調用原型:PluginLocalBroadcastManager.sendBroadcast(instance, intent);
    2)replace statement:'{$_ = ' + PROXY_CLASS + '.' + method + '($0, $$);}'抛腕,傳入調用方芋绸,全部參數,以及把返回值賦給特殊變量$_担敌。

到這里廣播注入器的工作就完成了摔敛。接下來看看ProviderInjector。

ProviderInjector

ProviderInjector全封,主要用來替換 插件中的 ContentResolver相關的方法調用 為 插件庫的PluginProviderClient中的對應方法調用马昙。

// 處理以下方法
public static def includeMethodCall = ['query',
                                       'getType',
                                       'insert',
                                       'bulkInsert',
                                       'delete',
                                       'update',
                                       'openInputStream',
                                       'openOutputStream',
                                       'openFileDescriptor',
                                       'registerContentObserver',
                                       'acquireContentProviderClient',
                                       'notifyChange',
]
  • static def includeMethodCall中定義了需要處理的目標方法名

直接看injectClass的實現(xiàn),遍歷class目錄并訪問到文件時售貌,執(zhí)行以下邏輯给猾。

@Override
def injectClass(ClassPool pool, String dir, Map config) {
    ...
    try {
        ...

        /* 檢查方法列表 */
        ctCls.getDeclaredMethods().each {
            it.instrument(editor)
        }

        ctCls.getMethods().each {
            it.instrument(editor)
        }

        ...
    }
    ...
}
  • ctCls.getDeclaredMethods().each { }ctCls.getMethods().each { },遍歷全部方法颂跨,并執(zhí)行instrument方法敢伸,逐個掃描每個方法體內每一行代碼,并交由ProviderExprEditoredit()處理對方法體代碼的修改恒削。

ProviderExprEditor.groovy

public class ProviderExprEditor extends ExprEditor {

    static def PROVIDER_CLASS = 'com.qihoo360.replugin.loader.p.PluginProviderClient'

    @Override
    void edit(MethodCall m) throws CannotCompileException {
      ...
      replaceStatement(m, methodName, m.lineNumber)
      ...
    }

    def private replaceStatement(MethodCall methodCall, String method, def line) {
        if (methodCall.getMethodName() == 'registerContentObserver' || methodCall.getMethodName() == 'notifyChange') {
            methodCall.replace('{' + PROVIDER_CLASS + '.' + method + '(com.qihoo360.replugin.RePlugin.getPluginContext(), $$);}')
        } else {
            methodCall.replace('{$_ = ' + PROVIDER_CLASS + '.' + method + '(com.qihoo360.replugin.RePlugin.getPluginContext(), $$);}')
        }
        println ">>> Replace: ${filePath} Provider.${method}():${line}"
    }
}
  • PROVIDER_CLASS指定了對應的替代實現(xiàn)類
  • replaceStatement(...)中池颈,替換方法體:
  • 替換registerContentObservernotifyChange :
    replace statement:'{' + PROVIDER_CLASS + '.' + method + '(com.qihoo360.replugin.RePlugin.getPluginContext(), $$);}',唯一特別的地方就是入參中傳入了特定的context钓丰。
  • 替換query 等方法:
    replace statement:'{$_ = ' + PROVIDER_CLASS + '.' + method + '(com.qihoo360.replugin.RePlugin.getPluginContext(), $$);}'躯砰,因為方法調用是有返回值的,所以statement必須將返回值賦值給特殊變量$_携丁,這是javassist.expr.MethodCall方法的明確要求琢歇。

到這里Provider注入器的工作就完成了。接下來看看ProviderInjector2梦鉴。

ProviderInjector2

ProviderInjector2李茫,主要用來替換 插件中的 ContentProviderClient 相關的方法調用。

    // 處理以下方法
    public static def includeMethodCall = ['query', 'update']
  • static def includeMethodCall中定義了需要處理的目標方法名

看下injectClass的實現(xiàn)肥橙,遍歷class目錄并訪問到文件時魄宏,執(zhí)行以下這段邏輯。

@Override
def injectClass(ClassPool pool, String dir, Map config) {
    ...
    try {
        ...

        /* 檢查方法列表 */
        ctCls.getDeclaredMethods().each {
            it.instrument(editor)
        }

        ctCls.getMethods().each {
            it.instrument(editor)
        }

        ...
    }
    ...
}
  • ctCls.getDeclaredMethods().each { }ctCls.getMethods().each { }存筏,遍歷全部方法宠互,并執(zhí)行instrument方法味榛,逐個掃描每個方法體內每一行代碼,并交由ProviderExprEditor2edit()處理對方法體代碼的修改予跌。

ProviderExprEditor2.groovy

public class ProviderExprEditor2 extends ExprEditor {

    static def PROVIDER_CLASS = 'com.qihoo360.loader2.mgr.PluginProviderClient2'

    @Override
    void edit(MethodCall m) throws CannotCompileException {
      ...
      replaceStatement(m, methodName, m.lineNumber)
      ...
    }

    def private replaceStatement(MethodCall methodCall, String method, def line) {
        methodCall.replace('{$_ = ' + PROVIDER_CLASS + '.' + method + '(com.qihoo360.replugin.RePlugin.getPluginContext(), $$);}')
        println ">>> Replace: ${filePath} Provider.${method}():${line}"
    }
}
  • PROVIDER_CLASS指定了對應的替代實現(xiàn)類
  • replaceStatement(...)中搏色,替換方法體:
  • 替換queryupdate:
    replace statement:'{$_ = ' + PROVIDER_CLASS + '.' + method + '(com.qihoo360.replugin.RePlugin.getPluginContext(), $$);}',因為方法調用是有返回值的匕得,所以statement必須將返回值賦值給特殊變量$_继榆,這是javassist.expr.MethodCall方法的明確要求。

到這里ProviderInjector2注入器的工作就完成了汁掠。接下來看看GetIdentifierInjector略吨。

GetIdentifierInjector

GetIdentifierInjector,主要用來替換 插件中的 Resource.getIdentifier 方法調用的參數 為 動態(tài)適配的參數考阱。

看下injectClass的實現(xiàn)翠忠,遍歷class目錄并訪問到文件時,執(zhí)行以下這段邏輯乞榨。

@Override
def injectClass(ClassPool pool, String dir, Map config) {
    ...
    try {
        ...

        /* 檢查方法列表 */
        ctCls.getDeclaredMethods().each {
            it.instrument(editor)
        }

        ctCls.getMethods().each {
            it.instrument(editor)
        }

        ...
    }
    ...
}
  • ctCls.getDeclaredMethods().each { }ctCls.getMethods().each { }秽之,遍歷全部方法,并執(zhí)行instrument方法吃既,逐個掃描每個方法體內每一行代碼考榨,并交由GetIdentifierExprEditoredit()處理對方法體代碼的修改。

GetIdentifierExprEditor.groovy

public class GetIdentifierExprEditor extends ExprEditor {

    public def filePath

    @Override
    void edit(MethodCall m) throws CannotCompileException {
        String clsName = m.getClassName()
        String methodName = m.getMethodName()

        if (clsName.equalsIgnoreCase('android.content.res.Resources')) {
            if (methodName == 'getIdentifier') {
                m.replace('{ $3 = \"' + CommonData.appPackage + '\"; ' +
                        '$_ = $proceed($$);' +
                        ' }')
                println " GetIdentifierCall => " +'{ $3 = \"' + CommonData.appPackage + '\"; ' +
                        '$_ = $proceed($$);' +
                        ' }'
                println " \n";
                println " GetIdentifierCall => ${filePath} ${methodName}():${m.lineNumber}"
            }
        }
    }
}
  • edit(...)中鹦倚,遍歷到調用方為android.content.res.Resources且方法為getIdentifier的MethodCall河质,動態(tài)適配這些MethodCall中的方法參數:
    1)調用原型: int id = res.getIdentifier("com.qihoo360.replugin.sample.demo2:layout/from_demo1", null, null);
    2)replace statement:'{ $3 = \"' + CommonData.appPackage + '\"; ' +'$_ = $proceed($$);' + ' }',為特殊變量$3賦值震叙,即動態(tài)修改參數3的值為插件的包名掀鹅;'$_ = $proceed($$);'表示按原樣調用。

到此GetIdentifierInjector注入器的工作就已完成媒楼,全部的注入器也都遍歷完畢并完成了全部的注入工作乐尊。

伴隨著注入器的遍歷結束,整個replugin-plugin-gradle插件的Tansfrom的注入工作完成了划址,Tansfrom還有一點整理的工作要做扔嵌,用Tansfrom自然要按照Tansfrom的套路,把處理過的數據輸出給下一個Tansfrom夺颤。

def doTransform(Collection<TransformInput> inputs,
                    TransformOutputProvider outputProvider,
                    Object config,
                    def injectors) {

       ...

        /* 重打包 */
        repackage()

        /* 拷貝 class 和 jar 包 */
        copyResult(inputs, outputProvider)
        ...
    }
  • repackage()对人,將解壓的 class 文件重新打包,然后刪除 class 文件
  • copyResult(...)最終會調用output.getContentLocation(...)拂共,按照Tansfrom的API范式,把處理過的數據輸出給下一個Tansfrom姻几。

ReclassTansfrom任務完成宜狐,將會把輸出繼續(xù)傳遞給下一個TransfromtransformClassesWithDexFor{ProductFlavor}{BuildType}势告,把處理權交還給android gradle插件。至此抚恒,replugin-plugin-gradle 插件的工作就全部結束了咱台。

End

replugin-plugin-gradle 插件是一個compile-time gradle plugin,基于兩大核心技術Transform + Javassist俭驮,完成了編譯期對class文件的動態(tài)注入回溺,進而實現(xiàn)動態(tài)修改構建目標文件的為replugin插件服務的gradle插件。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末混萝,一起剝皮案震驚了整個濱河市遗遵,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌逸嘀,老刑警劉巖车要,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異崭倘,居然都是意外死亡翼岁,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門司光,熙熙樓的掌柜王于貴愁眉苦臉地迎上來琅坡,“玉大人,你說我怎么就攤上這事残家∮馨常” “怎么了?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵跪削,是天一觀的道長谴仙。 經常有香客問我,道長碾盐,這世上最難降的妖魔是什么晃跺? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮毫玖,結果婚禮上掀虎,老公的妹妹穿的比我還像新娘。我一直安慰自己付枫,他們只是感情好烹玉,可當我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著阐滩,像睡著了一般二打。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上掂榔,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天继效,我揣著相機與錄音症杏,去河邊找鬼。 笑死瑞信,一個胖子當著我的面吹牛厉颤,可吹牛的內容都是我干的。 我是一名探鬼主播凡简,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼逼友,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了秤涩?” 一聲冷哼從身側響起帜乞,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎溉仑,沒想到半個月后挖函,有當地人在樹林里發(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡浊竟,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年怨喘,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片振定。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡必怜,死狀恐怖,靈堂內的尸體忽然破棺而出后频,到底是詐尸還是另有隱情梳庆,我是刑警寧澤,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布卑惜,位于F島的核電站膏执,受9級特大地震影響,放射性物質發(fā)生泄漏露久。R本人自食惡果不足惜更米,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望毫痕。 院中可真熱鬧征峦,春花似錦、人聲如沸消请。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽臊泰。三九已至蛉加,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背针饥。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工祟偷, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人打厘。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像贺辰,于是被迫代替她去往敵國和親户盯。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內容