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

前言

replugin-plugin-gradle 是 RePlugin 插件框架中提供給replugin插件用的gradle插件,是一種動態編譯方案實現。
主要在插件應用的編譯期,基于Transform api 注入到編譯流程中, 再通過Java字節碼類庫對編譯中間環節的 Java 字節碼文件進行修改,以便實現編譯期動態修改插件應用的目的。
RePlugin 是一套完整的、穩定的、適合全面使用的,占坑類插件化方案,由360手機衛士的RePlugin Team研發,也是業內首個提出”全面插件化“(全面特性、全面兼容、全面使用)的方案。

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

  • replugin插件:即replugin插件化框架所指的插件,這個插件指android應用業務拆分出的獨立模塊,是android應用或模塊。
  • gradle插件:即gradle構建所需的構建插件,是gradle應用或模塊。

結構概覽

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

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

目錄概覽

\qihoo\replugin\replugin-plugin-gradle\src
└─main
    ├─groovy
    │  └─com
    │      └─qihoo360
    │          └─replugin
    │              └─gradle
    │                  └─plugin
    │                      │  AppConstant.groovy                      # 程序常量定義區
    │                      │  ReClassPlugin.groovy                    # 插件動態編譯方案入口
    │                      │  
    │                      ├─debugger
    │                      │      PluginDebugger.groovy               # 用于插件調試的gradle task實現
    │                      │      
    │                      ├─injector
    │                      │  │  BaseInjector.groovy                  # 注入器基類
    │                      │  │  IClassInjector.groovy                # 注入器接口類
    │                      │  │  Injectors.groovy                     # 注入器枚舉類,定義了全部注入器
    │                      │  │  
    │                      │  ├─identifier
    │                      │  │      GetIdentifierExprEditor.groovy   # javassist 允許修改方法里的某個表達式,此類為替換 getIdentifier 方法中表達式的實現類
    │                      │  │      GetIdentifierInjector.groovy     # GetIdentifier 方法注入器
    │                      │  │      
    │                      │  ├─loaderactivity
    │                      │  │      LoaderActivityInjector.groovy    # Activity代碼注入器
    │                      │  │      
    │                      │  ├─localbroadcast
    │                      │  │      LocalBroadcastExprEditor.groovy  # 替換幾個廣播相關方法表達式的實現類
    │                      │  │      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 實現動態修改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 插件實現類

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

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

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

我們到插件實現類看看這個插件是如何工作的。

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> ,實現了apply接口方法,apply方法會在 build.gradle 中執行 apply plugin: 'replugin-plugin-gradle'時被調用。

接下來解讀下 apply 方法的具體實現。

用于快速調試的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類中要用到一些參數;另一個是做動態編譯時要用到一些參數;后面邏輯會陸續用到。

  • 判斷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文件的路徑,是為了后續基于adb命令做push apk到SD卡上做準備。
apkFile = new File(apkDir, apkName)
adbFile = globalScope.androidBuilder.sdkInfo.adb;
  • def assembleTask = variant.getAssemble(),獲取assemble task(即打包apk的task),后續的task需要依賴此task,比如安裝插件的task,肯定要等到assemble task打包生成apk后,才能去執行。
  • 生成installPluginTask 的gradle task 名字,并調用project的task()方法創建此Task。然后指定此task的任務內容:
installPluginTask.doLast {
    pluginDebugger.startHostApp()
    pluginDebugger.uninstall()
    pluginDebugger.forceStopHostApp()
    pluginDebugger.startHostApp()
    pluginDebugger.install()
}
  • 流程:啟動宿主 -> 卸載插件 -> 強制停止宿主 -> 啟動宿主 -> 安裝插件
  • pluginDebugger 內的方法實現:基于adb shell + am 命令,實現 發送廣播,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分別有不同的調試目的,可以去分別了解下,細節實現大同小異。
看到這里,我們該插播一下調試方案的整體原理了:

  1. replugin-host-lib 的DebuggerReceivers類中,注冊了一系列用于快速調試的廣播,而replugin-host-lib是會內置在宿主應用中的。
  2. replugin-plugin-gradle 中創建了一系列gradle task,用于啟動停止重啟宿主應用,安裝卸載運行插件應用。這些gradle task都是被動型task,需要通過命令行主動的運行這些task。
  3. 打開命令行終端,執行replugin插件項目的某個gradle task,以實現快速調試功能。比如:gradlew.bat rpInstallPluginDebug,最終就會將宿主和插件運行起來。
  4. 這些gradle task被手動執行后,task會執行一系列任務,比如通過adb push 插件到sdcard,或通過am命令發送廣播,啟動activity等。當發送一系列步驟1中注冊的廣播后,宿主應用收到廣播后會執行對應的操作,比如啟動插件的activity等。

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

繼續看apply()方法中的源碼。

Transform:動態編譯方案實現

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

            ...

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

重點來了,這里就是動態編譯方案的實現入口。
在詳細解讀動態編譯實現之前,先了解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?

  • 實現一個繼承自Transform的自定義 Transform 類。

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

去看看 ReClassTransform 類的核心實現。

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) // 執行 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() 方法會在執行你的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 調用代碼的參數 為 動態適配的參數

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

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

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

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

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

Javassit 是一個處理Java字節碼的類庫。
CtMethod:是一個class文件中的方法的抽象表示。一個CtMethod對象表示一個方法。(Javassit 庫API)
CtClass:是一個class文件的抽象表示。一個CtClass(compile-time class)對象可以用來處理一個class文件。(Javassit 庫API)
ClassPool:是一個CtClass對象的容器類。(Javassit 庫API)
.class文件:.class文件是一種存儲Java字節碼的二進制文件,里面包含一個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)
            }
        }
        ...
    }

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

    /**
     * 執行注入操作
     */
    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

第一個被執行的就是 LoaderActivityInjector,用來修改插件中XXActivity類中的頂級XXActivity父類 為 XXPluginActivity父類。看看如何實現的。

@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 替換規則,只替換那些頂級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文件??聪?code>handleActivity(...)的實現細節。
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文件,創建一個CtClass實例,這個實例表示.class文件對應的類或接口。通過CtClass可以很方便的對.class文件進行自定義操作,比如添加方法,改方法參數,添加類成員,改繼承關系等。

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

  • ctCls.setSuperclass(targetSuperCls),根據初始化中設置的Activity替換規則,修改 此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(),方法體會被逐行進行掃描,從第一行掃描到最后一行。發現有方法調用或表達式時(object creation),edit()會被調用,根據edit()內的replace()方法來修改這一行代碼。

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

  • 遍歷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頂級父類動態注入。

  • 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父類是不是神奇的變了?;蛘咧苯?code>apktool工具反編譯插件apk,看smali文件的改變。




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

LocalBroadcastInjector

LocalBroadcastInjector,實現了替換插件中的 LocalBroadcastManager的方法調用 為 插件庫的PluginLocalBroadcastManager中的方法調用。
直接看injectClass的實現,遍歷class目錄并訪問到文件時,執行以下這段邏輯。

@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包中的源碼實現。
  • pool.makeClass(),創建當前類文件的CtClass實例。
  • ctCls.defrost() 如果CtClass實例被凍結,則執行解凍操作。
  • ctCls.getDeclaredMethods().each { }ctCls.getMethods().each { },遍歷全部方法,并執行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的實現,遍歷class目錄并訪問到文件時,執行以下邏輯。

@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 { },遍歷全部方法,并執行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指定了對應的替代實現類
  • 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的實現,遍歷class目錄并訪問到文件時,執行以下這段邏輯。

@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 { },遍歷全部方法,并執行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指定了對應的替代實現類
  • replaceStatement(...)中,替換方法體:
  • 替換queryupdate:
    replace statement:'{$_ = ' + PROVIDER_CLASS + '.' + method + '(com.qihoo360.replugin.RePlugin.getPluginContext(), $$);}',因為方法調用是有返回值的,所以statement必須將返回值賦值給特殊變量$_,這是javassist.expr.MethodCall方法的明確要求。

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

GetIdentifierInjector

GetIdentifierInjector,主要用來替換 插件中的 Resource.getIdentifier 方法調用的參數 為 動態適配的參數。

看下injectClass的實現,遍歷class目錄并訪問到文件時,執行以下這段邏輯。

@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 { },遍歷全部方法,并執行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,動態適配這些MethodCall中的方法參數:
    1)調用原型: int id = res.getIdentifier("com.qihoo360.replugin.sample.demo2:layout/from_demo1", null, null);
    2)replace statement:'{ $3 = \"' + CommonData.appPackage + '\"; ' +'$_ = $proceed($$);' + ' }',為特殊變量$3賦值,即動態修改參數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任務完成,將會把輸出繼續傳遞給下一個TransfromtransformClassesWithDexFor{ProductFlavor}{BuildType},把處理權交還給android gradle插件。至此,replugin-plugin-gradle 插件的工作就全部結束了。

End

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容