前言
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)大同小異污筷。
看到這里,我們該插播一下調試方案的整體原理了:
- replugin-host-lib 的DebuggerReceivers類中乍赫,注冊了一系列用于快速調試的廣播瓣蛀,而replugin-host-lib是會內置在宿主應用中的。
- replugin-plugin-gradle 中創(chuàng)建了一系列gradle task雷厂,用于啟動停止重啟宿主應用惋增,安裝卸載運行插件應用。這些gradle task都是被動型task改鲫,需要通過命令行主動的運行這些task诈皿。
- 打開命令行終端,執(zhí)行replugin插件項目的某個gradle task钩杰,以實現(xiàn)快速調試功能纫塌。比如:gradlew.bat rpInstallPluginDebug,最終就會將宿主和插件運行起來讲弄。
- 這些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()
,即指定剛才提到的那個插入的transformtransformClassesWith{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方法中的參數
inputs
和outputProvider
一定程度上反映了Transform的工作流殿较,接受輸入->處理輸入->輸出數據耸峭。 -
initClassPool(...)
方法主要的工作:添加編譯時引用到的類
到ClassPool
,同時記錄要修改的 jar
到includeJars
淋纲。方便后續(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提供了一些特殊的變量來代表特定含義:
全部的類遍歷完后载城,將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
方法,逐個掃描每個方法體內每一行代碼葬毫,并交由LocalBroadcastExprEditor
的edit()
處理對方法體代碼的修改镇辉。
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_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, $$);}'
抛腕,傳入調用方芋绸,全部參數,以及把返回值賦給特殊變量$_担敌。
到這里廣播注入器的工作就完成了摔敛。接下來看看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
方法敢伸,逐個掃描每個方法體內每一行代碼,并交由ProviderExprEditor
的edit()
處理對方法體代碼的修改恒削。
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(...)
中池颈,替換方法體: - 替換
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方法的明確要求琢歇。
到這里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
方法味榛,逐個掃描每個方法體內每一行代碼,并交由ProviderExprEditor2
的edit()
處理對方法體代碼的修改予跌。
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(...)
中搏色,替換方法體: - 替換
query
和update
:
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
方法吃既,逐個掃描每個方法體內每一行代碼考榨,并交由GetIdentifierExprEditor
的edit()
處理對方法體代碼的修改。
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插件。