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實現對字節碼文件的注入。
目錄概覽
| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354 | \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 依賴:123456 buildscript {dependencies {classpath 'com.qihoo360.replugin:replugin-plugin-gradle:2.1.5'...}}
在項目的app模塊中的build.gradle應用插件:
| 1 | apply plugin: 'replugin-plugin-gradle' |
replugin-plugin-gradle的源碼解析
我們在開始閱讀源碼前,要思考下,replugin-plugin-gradle是什么?
A:replugin-plugin-gradle是一個自定義的gradle插件。
這個清楚了,沒上車的上車,上車了的別動!
replugin-plugin-gradle.properties文件
| 1 | implementation-class=com.qihoo360.replugin.gradle.plugin.ReClassPlugin |
在開發自定義gradle插件時,都會先定義這么個文件。這里有 2 個知識點:
- 文件中的implementation-class用來指定插件實現類。
- 文件名用來指定插件名,即在插件中使用gradle插件時的apply plugin: 'replugin-plugin-gradle'中的replugin-plugin-gradle.
我們到插件實現類看看這個插件是如何工作的。
ReClassPlugin.groovy文件
| 1234567 | public class ReClassPlugin implements Plugin<Project> { public void apply(Project project) {println "${AppConstant.TAG} Welcome to replugin world ! "...}} |
定義了一個類ReClassPlugin,繼承自gradle-api 庫中的接口類 Plugin ,實現了apply接口方法,apply方法會在 build.gradle 中執行 apply plugin: 'replugin-plugin-gradle'時被調用。
接下來解讀下 apply 方法的具體實現。
用于快速調試的gradle task
| 12345678910111213141516171819202122232425262728293031323334353637383940 | 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中定義的閉包:
123 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卡上做準備。
12 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的任務內容:
1234567 installPluginTask.doLast {pluginDebugger.startHostApp()pluginDebugger.uninstall()pluginDebugger.forceStopHostApp()pluginDebugger.startHostApp()pluginDebugger.install()}
- 流程:啟動宿主 -> 卸載插件 -> 強制停止宿主 -> 啟動宿主 -> 安裝插件
- pluginDebugger 內的方法實現:基于adb shell + am 命令,實現 發送廣播,push apk 等功能。,比如:pluginDebugger.startHostApp()
123456789101112 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分別有不同的調試目的,可以去分別了解下,細節實現大同小異。
看到這里,我們該插播一下調試方案的整體原理了:
Tips.調試模式開啟方法:插件調試
Debug階段建議開啟,Release階段建議關閉,默認為關閉狀態
繼續看apply()方法中的源碼。
##Transform:動態編譯方案實現
| 12345678910111213 | public void apply(Project project) {... if (isApp) {... def transform = new ReClassTransform(project) // 將 transform 注冊到 androidandroid.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 類的核心實現。
| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849 | public class ReClassTransform extends Transform { String getName() { return '___ReClass___'} 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.appModuleCommonData.ignoredActivities = config.ignoredActivities def injectors = includedInjectors(config, variantDir) if (injectors.isEmpty()) {copyResult(inputs, outputProvider) // 跳過 reclass} else {doTransform(inputs, outputProvider, config, injectors) // 執行 reclass}} Set<QualifiedContent.ContentType> getInputTypes() { return TransformManager.CONTENT_CLASS} Set<QualifiedContent.Scope> getScopes() { return TransformManager.SCOPE_FULL_PROJECT} 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_RESOURCES,CONTENT_NATIVE_LIBS等。在replugin-plugin-gradle中是使用Transform來做代碼插樁,所以選用CONTENT_CLASS類型。
getScopes() 配置當前Transform的作用域,實際使用中可以根據需求配置多種Scope。
doTransform()方法是執行reclass的關鍵
| 123456789 | def doTransform(Collection<TransformInput> inputs,TransformOutputProvider outputProvider,Object config, def injectors) { /* 初始化 ClassPool */Object pool = initClassPool(inputs)...} |
- Transform方法中的參數inputs和outputProvider一定程度上反映了Transform的工作流,接受輸入->處理輸入->輸出數據。
- initClassPool(...)方法主要的工作:添加編譯時引用到的類到 ClassPool,同時記錄要修改的 jar 到 includeJars。方便后續拿到這些class文件去修改。比如Sample中會添加的class路徑:
1234 >>> 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類或者接口。
| 12345678910111213141516171819202122 | 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(...)方法實現。
| 123456789101112131415161718 | /*** 執行注入操作*/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
| 1234 | 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父類。看看如何實現的。
| 1234567891011121314 | 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。
12345678910 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(...)的實現細節。
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970 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 = superClssuperCls = 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() { 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父類是不是神奇的變了。或者直接apktool工具反編譯插件apk,看smali文件的改變。
2.可以基于命令行的方式gradlew.bat build編譯你的插件應用,然后查看命令行中的編譯日志,會有助于你更好的理解。
##LocalBroadcastInjector
LocalBroadcastInjector,實現了替換插件中的 LocalBroadcastManager的方法調用 為 插件庫的PluginLocalBroadcastManager中的方法調用。
直接看injectClass的實現,遍歷class目錄并訪問到文件時,執行以下這段邏輯。
| 12345678910111213141516171819202122232425262728293031 | 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方法,逐個掃描每個方法體內每一行代碼,并交由LocalBroadcastExprEditor的edit()處理對方法體代碼的修改。
LocalBroadcastExprEditor.groovy
| 1234567891011121314151617181920212223242526272829303132333435363738394041 | 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']... 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_CLASS和PROXY_CLASS分別指定了需要處理的目標類和對應的代理類
- static def includeMethodCall中定義了需要處理的目標方法名
- replaceStatement(...)中,替換方法體:
- 替換getInstance:
1)調用原型:PluginLocalBroadcastManager.getInstance(context);
2)replace statement:'{$_ = ' + PROXY_CLASS + '.' + method + '($$);}',$$表示全部參數的簡寫。$_表示resulting value即返回值。 - 替換registerReceiver unregisterReceiver sendBroadcastSync(returnType == 'void'):
1)調用原型:PluginLocalBroadcastManager.registerReceiver(instance, receiver, filter);
2)replace statement:'{' + PROXY_CLASS + '.' + method + '($0, $$);}',$0在這里就不代表this了,而是表示方法的調用方(參見:javassist tutorial),即PluginLocalBroadcastManager。因為調用原型中需要入參instance(要求是PluginLocalBroadcastManager類型),所以這里必須傳入$0。
注:unregisterReceiver和sendBroadcastSync同上,調用原型請參見replugin-plugin-lib插件庫中的PluginLocalBroadcastManager.java文件。 - 替換sendBroadcast (returnType != 'void'):
1)調用原型:PluginLocalBroadcastManager.sendBroadcast(instance, intent);
2)replace statement:'{$_ = ' + PROXY_CLASS + '.' + method + '($0, $$);}',傳入調用方,全部參數,以及把返回值賦給特殊變量$_。
- 替換getInstance:
到這里廣播注入器的工作就完成了。接下來看看ProviderInjector。
##ProviderInjector
ProviderInjector,主要用來替換 插件中的 ContentResolver相關的方法調用 為 插件庫的PluginProviderClient中的對應方法調用。
| 1234567891011121314 | // 處理以下方法public static def includeMethodCall = ['query', 'getType', 'insert', 'bulkInsert', 'delete', 'update', 'openInputStream', 'openOutputStream', 'openFileDescriptor', 'registerContentObserver', 'acquireContentProviderClient', 'notifyChange',] |
- static def includeMethodCall中定義了需要處理的目標方法名
直接看injectClass的實現,遍歷class目錄并訪問到文件時,執行以下邏輯。
| 12345678910111213141516171819 | 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方法,逐個掃描每個方法體內每一行代碼,并交由ProviderExprEditor的edit()處理對方法體代碼的修改。
ProviderExprEditor.groovy
| 1234567891011121314151617181920 | public class ProviderExprEditor extends ExprEditor { static def PROVIDER_CLASS = 'com.qihoo360.replugin.loader.p.PluginProviderClient' 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(...)中,替換方法體:
- 替換registerContentObserver或notifyChange :
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方法的明確要求。
- 替換registerContentObserver或notifyChange :
到這里Provider注入器的工作就完成了。接下來看看ProviderInjector2。
##ProviderInjector2
ProviderInjector2,主要用來替換 插件中的 ContentProviderClient 相關的方法調用。
| 12 | // 處理以下方法public static def includeMethodCall = ['query', 'update'] |
- static def includeMethodCall中定義了需要處理的目標方法名
看下injectClass的實現,遍歷class目錄并訪問到文件時,執行以下這段邏輯。
| 12345678910111213141516171819 | 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方法,逐個掃描每個方法體內每一行代碼,并交由ProviderExprEditor2的edit()處理對方法體代碼的修改。
ProviderExprEditor2.groovy
| 12345678910111213141516 | public class ProviderExprEditor2 extends ExprEditor { static def PROVIDER_CLASS = 'com.qihoo360.loader2.mgr.PluginProviderClient2' 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(...)中,替換方法體:
- 替換query和update:
replace statement:'{$_ = ' + PROVIDER_CLASS + '.' + method + '(com.qihoo360.replugin.RePlugin.getPluginContext(), $$);}',因為方法調用是有返回值的,所以statement必須將返回值賦值給特殊變量$_,這是javassist.expr.MethodCall方法的明確要求。
- 替換query和update:
到這里ProviderInjector2注入器的工作就完成了。接下來看看GetIdentifierInjector。
##GetIdentifierInjector
GetIdentifierInjector,主要用來替換 插件中的 Resource.getIdentifier 方法調用的參數 為 動態適配的參數。
看下injectClass的實現,遍歷class目錄并訪問到文件時,執行以下這段邏輯。
| 12345678910111213141516171819 | 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方法,逐個掃描每個方法體內每一行代碼,并交由GetIdentifierExprEditor的edit()處理對方法體代碼的修改。
GetIdentifierExprEditor.groovy
| 1234567891011121314151617181920212223 | public class GetIdentifierExprEditor extends ExprEditor { public def filePath 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。
| 1234567891011121314 | 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插件。
https://wangfuda.github.io/2017/07/20/replugin-plugin-gradle/
總結
以上是生活随笔為你收集整理的replugin源码解析之replugin-plugin-gradle(插件的gradle插件)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [Android]用架构师角度看插件化(
- 下一篇: replugin源码解析之replugi