前言
上一篇文章分享了宿主的gradle
插件的源码分析,本文将分析插件项目的gradle
插件的源码,360的插件apk
是支持独立安装的,这点和其他插件化框架有不小的区别,很显然插件程序肯定做了不少事情。
一、源码结构
显然光看这代码量就知道比宿主
gradle
插件干的事情多。
二、源码分析
插件入口类:com.qihoo360.replugin.gradle.plugin.ReClassPlugin
@Override
public void apply(Project project) {
/* 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)
//省略了创建Debug任务的代码...
CommonData.appPackage = android.defaultConfig.applicationId
println ">>> APP_PACKAGE " + CommonData.appPackage
def transform = new ReClassTransform(project)
// 将 transform 注册到 android
android.registerTransform(transform)
}
}
1、Debug任务生成
上面略过了部分代码,这部分代码主要是生成gradle Task
的代码,比较臃肿,这些任务用于辅助插件安装测试等,task
如下:
这部分代码主要实现还是通过adb push
、adb pm
两个命令来完成的,installAndRun
其实执行的是如下几个命令:
源码路径:com.qihoo360.replugin.gradle.plugin.debugger.PluginDebugger
//推送apk文件到手机
pushCmd = "${adbFile.absolutePath} push ${apkFile.absolutePath} ${config.phoneStorageDir}"
//发送安装广播
installBrCmd = "${adbFile.absolutePath} shell am broadcast -a ${config.hostApplicationId}.replugin.install -e path ${apkPath} -e immediately true "
//启动对应plugin
runBrCmd = "${adbFile.absolutePath} shell am broadcast -a ${config.hostApplicationId}.replugin.start_activity -e plugin ${config.pluginName}"
从上面的命令可以看出来,
RePlugin
借助adb
工具实现了一些简单的插件安装、启动脚本。通过adb
工具实现发送广播,通知主程序加载插件,这种形式,也多用于自动化测试里。
2、注册ReClassTransform
整个apply
方法里最为关键的一句代码就是注册class
处理器了:
def transform = new ReClassTransform(project)
// 将 transform 注册到 android
android.registerTransform(transform)
android编译脚本提供方法来操作编译后的
.class
文件,这儿相当于注册了一个。
3、ReClassTransform分析
源码位置:com.qihoo360.replugin.gradle.plugin.inner.ReClassTransform
@Override
void transform(Context context,
Collection<TransformInput> inputs,
Collection<TransformInput> referencedInputs,
TransformOutputProvider outputProvider,
boolean isIncremental) throws IOException, TransformException, InterruptedException {
/* 读取用户配置 */
def config = project.extensions.getByName('repluginPluginConfig')
File rootLocation = null
try {
rootLocation = outputProvider.rootLocation
} catch (Throwable e) {
//android gradle plugin 3.0.0+ 修改了私有变量,将其移动到了IntermediateFolderUtils中去
rootLocation = outputProvider.folderUtils.getRootFolder()
}
def variantDir = rootLocation.absolutePath.split(getName() + Pattern.quote(File.separator))[1]
CommonData.appModule = config.appModule
//要忽略哪些Activity不处理
CommonData.ignoredActivities = config.ignoredActivities
def injectors = includedInjectors(config, variantDir)
if (injectors.isEmpty()) {
copyResult(inputs, outputProvider) // 跳过 reclass
} else {
doTransform(inputs, outputProvider, config, injectors) // 执行 reclass
}
}
transform()
方法是Android编译器调用该类处理的入口,他有5个传入参数:
类型 | 参数名 | 描述 |
Contex | contex | 任务上下文 |
Collection<TransformInput> | inputs | 最终要打包进APK的class和jar路径 |
Collection<TransformInput> | referencedInputs | 引用的class和jar的路径 |
TransformOutputProvider | outputProvider | 文件输出适配器 |
boolean | isIncremental | 是否增量,不过从代码来看,360并没有做增量编译 |
如果在项目中配置忽略了所有的注入器、Replugin
会跳过class
注入。
接下来让我们看看doTransform
里做了什么?
def doTransform(Collection<TransformInput> inputs,TransformOutputProvider outputProvider,Object config, def injectors) {
/* 初始化 ClassPool */
Object pool = initClassPool(inputs)
/* 进行注入操作 */
Injectors.values().each {
if (it.nickName in injectors) {
println ">>> Do: ${it.nickName}"
// 将 NickName 的第 0 个字符转换成小写,用作对应配置的名称
def configPre = Util.lowerCaseAtIndex(it.nickName, 0)
doInject(inputs, pool, it.injector, config.properties["${configPre}Config"])
} else {
println ">>> Skip: ${it.nickName}"
}
}
if (config.customInjectors != null) {
config.customInjectors.each {
doInject(inputs, pool, it)
}
}
/* 重打包 */
repackage()
/* 拷贝 class 和 jar 包 */
copyResult(inputs, outputProvider)
}
这里初始化了一个ClassPool
,ClassPool
是Jboos开源的Java字节码操作工具Javassist的一个类,负责管理CtClass
对象,具体相关的知识,可以前往Github仓库了解。这里的initClassPool
方法将编译产生的jar包进行了解压,并加载到了ClassPool
中,如下图所示,该方法把所有的jar包都解压了,便于后面注入的时候直接操纵某个class文件,然后注入完毕后重新zip成jar包。
核心的方法就在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 文件
*/
def handleDir(ClassPool pool, DirectoryInput input, IClassInjector injector, Object config) {
println ">>> Handle Dir: ${input.file.absolutePath}"
injector.injectClass(pool, input.file.absolutePath, config)
}
这里一种是dir
类型的输入源,还有一种是处理jar
包的,这里的jar
包是指放在libs
里面的那些jar包,在编译的时候,Replugin
会把libs
里的jar包解压,其内在逻辑都是替换class字节码,其内在逻辑都封装到Injector里。
4、Injector分析
从源码的分包来看,一共有五类Injector
,分别如下:LoaderActivityInjector
LocalBroadcastInjector
ProviderInjector
ProviderInjector2
GetIdentifierInjector
下面单独分析每个Injector
做了哪些事情,由于替换使用的第三方框架javassist,相关知识这里就不说了,主要是我也没花时间弄清楚他是怎么完成替换的。哈哈哈哈哈…
a. LoaderActivityInjector
源码文件:com.qihoo360.replugin.gradle.plugin.injector.loaderactivity.LoaderActivityInjector
/* LoaderActivity 替换规则 */
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'
]
@Override
def injectClass(ClassPool pool, String dir, Map config) {
init()
/* 遍历程序中声明的所有 Activity */
//每次都new一下,否则多个variant一起构建时只会获取到首个manifest
new ManifestAPI().getActivities(project, variantDir).each {
// 处理没有被忽略的 Activity
if (!(it in CommonData.ignoredActivities)) {
handleActivity(pool, it, dir)
}
}
}
handleActivity
里面做了两件事情,首先找到当前Activity
的父类,根据规则替换成对应的PluginActivity
,然后将所有super.xxx
替换成PluginActivity
的调用。
看到没有,把AppCompatActivity
替换成了他自己的PluginAppCompatActivit
,然后替换了super.onCreateView()
调用,为什么要替换super
调用?因为在编译成字节码后,会变成全路径的调用,如果不替换的话,调用会出错。 下图是编译成字节码后的内容,很显然,这里的super.xxx
调用是全路径的,如果不替换super.xxx
,PluginActivity
里的方法不会被调用到。
b.LocalBroadcastInjector
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']
干的事情都差不多,替换了系统的广播管理类为Replugin
的广播管理类。就不多说了。
c.ProviderInjector、ProviderInjector2
这两个注入器主要是替换android.content.ContentResolver
、android.content.ContentProviderClient
这两个类的调用的。替换方式也是一样的,直接换成了Replugin
的调用方法。
d.GetIdentifierInjector
这里只是把第三个参数替换成了对应的包名,这里做的这个处理应该是为了在找资源的时候防止找错了。
三、流程分析
四、总结
整个过程都是进行了class
文件的处理,但从整体代码来看,可以优化的地方还有很多,反复扫描类文件,缺乏增量编译机制,每次都需要处理所有的class
,导致整个编译非常的耗时,相比非Replugin
模式编译,编译时间至少长了一倍。我猜测360团队可能在编码上就行成了约束,因此不需要借助插件进行编译替换,所以没有对编译速度做过多的优化。