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实现对字节码文件的注入。
目录概览
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
|
\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 依赖:123456buildscript {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
文件
1
2
3
4
5
6
7
|
public class ReClassPlugin implements Plugin<Project> {
@Override
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
@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中定义的闭包:123android {...}android.applicationVariants.all
,遍历android extension的Application variants 组合。android gradle 插件,会对最终的包以多个维度进行组合。ApplicationVariant的组合 = {ProductFlavor} x {BuildType} 种组合.new PluginDebugger(project, config, variant)
,初始化PluginDebugger类实例,主要配置了最终生成的插件应用的文件路径,以及adb文件的路径,是为了后续基于adb命令做push apk到SD卡上做准备。12apkFile = 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的任务内容:
1234567installPluginTask.doLast {pluginDebugger.startHostApp()pluginDebugger.uninstall()pluginDebugger.forceStopHostApp()pluginDebugger.startHostApp()pluginDebugger.install()}
- 流程:启动宿主 -> 卸载插件 -> 强制停止宿主 -> 启动宿主 -> 安装插件
- pluginDebugger 内的方法实现:基于adb shell + am 命令,实现 发送广播,push apk 等功能。,比如:
pluginDebugger.startHostApp()
123456789101112public 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分别有不同的调试目的,可以去分别了解下,细节实现大同小异。
看到这里,我们该插播一下调试方案的整体原理了:
- replugin-host-lib 的DebuggerReceivers类中,注册了一系列用于快速调试的广播,而replugin-host-lib是会内置在宿主应用中的。
- replugin-plugin-gradle 中创建了一系列gradle task,用于启动停止重启宿主应用,安装卸载运行插件应用。这些gradle task都是被动型task,需要通过命令行主动的运行这些task。
- 打开命令行终端,执行replugin插件项目的某个gradle task,以实现快速调试功能。比如:gradlew.bat rpInstallPluginDebug,最终就会将宿主和插件运行起来。
- 这些gradle task被手动执行后,task会执行一系列任务,比如通过adb push 插件到sdcard,或通过am命令发送广播,启动activity等。当发送一系列步骤1中注册的广播后,宿主应用收到广播后会执行对应的操作,比如启动插件的activity等。
Tips.调试模式开启方法:插件调试
Debug阶段建议开启,Release阶段建议关闭,默认为关闭状态
继续看apply()
方法中的源码。
##Transform:动态编译方案实现
1
2
3
4
5
6
7
8
9
10
11
12
13
|
@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 是 Android Gradle API ,允许第三方插件在class文件转为dex文件前操作编译完成的class文件,这个API的引入是为了简化class文件的自定义操作而无需对Task进行处理。在做代码插桩时,本质上是在
如何使用 Transform?
- 实现一个继承自
Transform
的自定义 Transform 类。 - 通过
registerTransform(@NonNull Transform transform, Object... dependencies)
注册自定义 Transform 类。
- 实现一个继承自
去看看 ReClassTransform 类的核心实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
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()
,即指定刚才提到的那个插入的transformtransformClassesWith{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的关键
1
2
3
4
5
6
7
8
9
|
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类或者接口。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
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(...)
方法实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
/**
* 执行注入操作
*/
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
1
2
3
4
|
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父类。看看如何实现的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
@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。12345678910def 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(...)
的实现细节。12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970private def handleActivity(ClassPool pool, String activity, String classesDir) {def clsFilePath = classesDir + File.separatorChar + activity.replaceAll('\\.', '/') + '.class'...def stream, ctClstry {stream = new FileInputStream(clsFilePath)ctCls = pool.makeClass(stream);// ctCls 之前的父类def originSuperCls = ctCls.superclass/* 从当前 Activity 往上回溯,直到找到需要替换的 Activity */def superCls = originSuperClswhile (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() {@Overridevoid 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目录并访问到文件时,执行以下这段逻辑。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
@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
方法,逐个扫描每个方法体内每一行代码,并交由LocalBroadcastExprEditor
的edit()
处理对方法体代码的修改。
LocalBroadcastExprEditor.groovy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
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中的对应方法调用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// 处理以下方法
public static def includeMethodCall = ['query',
'getType',
'insert',
'bulkInsert',
'delete',
'update',
'openInputStream',
'openOutputStream',
'openFileDescriptor',
'registerContentObserver',
'acquireContentProviderClient',
'notifyChange',
]
|
static def includeMethodCall
中定义了需要处理的目标方法名
直接看injectClass
的实现,遍历class目录并访问到文件时,执行以下逻辑。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
@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
方法,逐个扫描每个方法体内每一行代码,并交由ProviderExprEditor
的edit()
处理对方法体代码的修改。
ProviderExprEditor.groovy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
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(...)
中,替换方法体:- 替换
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 相关的方法调用。
1
2
|
// 处理以下方法
public static def includeMethodCall = ['query', 'update']
|
static def includeMethodCall
中定义了需要处理的目标方法名
看下injectClass
的实现,遍历class目录并访问到文件时,执行以下这段逻辑。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
@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
方法,逐个扫描每个方法体内每一行代码,并交由ProviderExprEditor2
的edit()
处理对方法体代码的修改。
ProviderExprEditor2.groovy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
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(...)
中,替换方法体:- 替换
query
和update
:
replace statement:'{$_ = ' + PROVIDER_CLASS + '.' + method + '(com.qihoo360.replugin.RePlugin.getPluginContext(), $$);}'
,因为方法调用是有返回值的,所以statement必须将返回值赋值给特殊变量$_,这是javassist.expr.MethodCall方法的明确要求。
- 替换
到这里ProviderInjector2注入器的工作就完成了。接下来看看GetIdentifierInjector。
##GetIdentifierInjector
GetIdentifierInjector
,主要用来替换 插件中的 Resource.getIdentifier 方法调用的参数 为 动态适配的参数。
看下injectClass
的实现,遍历class目录并访问到文件时,执行以下这段逻辑。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
@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
方法,逐个扫描每个方法体内每一行代码,并交由GetIdentifierExprEditor
的edit()
处理对方法体代码的修改。
GetIdentifierExprEditor.groovy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
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。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
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插件)相关推荐
- replugin源码解析之replugin-host-gradle(宿主的gradle插件)
前言 replugin-host-gradle 是 RePlugin 插件框架中的宿主gradle插件,主要用于在宿主应用的编译期常规构建任务流中,插入一些定制化的构建任务,以便实现自动化编译期修改宿 ...
- babel源码解析之(@babel/preset-env)
前言 还记得之前写过一篇文章:babel源码解析一,里面把babel的整个流程跑了一遍,最后还自定义了一个插件用来转换"箭头函数",通过前面的源码解析我们知道,preset其实就是 ...
- 唯一插件化Replugin源码及原理深度剖析--插件的安装、加载原理
上一篇 唯一插件化Replugin源码及原理深度剖析–唯一Hook点原理 在Replugin的初始化过程中,我将他们分成了比较重要3个模块,整体框架的初始化.hook系统ClassLoader.插件的 ...
- Kubernetes学习笔记之Calico CNI Plugin源码解析(二)
女主宣言 今天小编继续为大家分享Kubernetes Calico CNI Plugin学习笔记,希望能对大家有所帮助. PS:丰富的一线技术.多元化的表现形式,尽在"360云计算" ...
- 全网最全Skywalking8.9.1源码解析系列文章
1.本系列文档简介 本系列文章为研究Skywalking-OAP8.9.1版本, 探针Skywalking-java8.9.0时所著,文章内容来源有博客.官网.自己的体会.源代码剖析.测试所得.专业性 ...
- ARouter 源码解析(1.5.2 版本)
文章目录 1.简介 2.ARouter 配置与基本用法 2.1 依赖引入与配置 2.2 基本用法 3.ARouter 编译时原理分析 4.ARouter 源码解析 4.1 ARouter 源码主要代码 ...
- Laravel5.2之Filesystem源码解析(下)
2019独角兽企业重金招聘Python工程师标准>>> 说明:本文主要学习下\League\Flysystem这个Filesystem Abstract Layer,学习下这个pac ...
- dayjs 源码解析(一)(api)
前言 作为一个程序员,阅读别人优秀代码是提升自己技术能力的一个很好的方法.下面,我将自己阅读 dayjs(v1.6.10)的源码的过程记录下来. 阅读库的代码,首先先要知道这个库的作用 dayjs 是 ...
- JVM-白话聊一聊JVM类加载和双亲委派机制源码解析
文章目录 Java 执行代码的大致流程 类加载loadClass的步骤 类加载器和双亲委派机制 sun.misc.Launcher源码解析 Launcher实例化 Launcher 构造函数 双亲委派 ...
最新文章
- ZBrush关于遮罩的一些操作
- VTK:简单操作之PerspectiveTransform
- E-SKILL网络工程师考试认证必备
- php 中文转成url,php字符转URL地址的函数
- Bootstrap固定导航条
- java springmvc 后台读取文件,springMVC
- 【OpenCV】腐蚀膨胀
- UCOS内核结构学习笔记
- oracle循环语句小结
- python博弈树_博弈方法
- C语言的自动关机程序和一个用来整人的小程序
- 中北计算机组成原理期末,中北大学《计算机组成原理》选择题和填空题试题大集合(含答案).pdf...
- 计算机符号mi,在线特殊符号大全
- Android Studio-开发少年强国App(一)
- PHP 调用百度翻译api翻译数据
- 25 匹马 5 条赛道,最快需要几轮求出前 3 名?
- java实现md5签名实现对外调用接口
- Entity Framework Core 之Modeling Relationships
- Windows下获取usb视频设备vendor id和product id的4种方法
- tts百度系统api-字符转语音
热门文章
- VS.NET2005中的WEBPART初步(一)
- python的二维数组操作
- linux库函数mmap()原理及用法详解
- 计算机类专业综合理论模拟试卷1,山东省2011年高等职业教育对口招生计算机类专业理论综合模拟试题(一)...
- [云炬创业基础笔记]第七张创业团队测试9
- 科大星云诗社动态20210406
- 云炬60s看世界20211117
- linux hive创建数据库失败,Hive本地模式安装及遇到的问题和解决方案
- 3DSlicer26:Add Extension/Module filepath,no module named ...
- 有监督回归:最小二乘学习法