前言

上一篇文章分享了宿主的gradle插件的源码分析,本文将分析插件项目的gradle插件的源码,360的插件apk是支持独立安装的,这点和其他插件化框架有不小的区别,很显然插件程序肯定做了不少事情。

一、源码结构

repo 拉去android源码 replugin源码_ide

显然光看这代码量就知道比宿主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如下:

repo 拉去android源码 replugin源码_Replugin_02


这部分代码主要实现还是通过adb pushadb 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)
   }

这里初始化了一个ClassPoolClassPool是Jboos开源的Java字节码操作工具Javassist的一个类,负责管理CtClass对象,具体相关的知识,可以前往Github仓库了解。这里的initClassPool方法将编译产生的jar包进行了解压,并加载到了ClassPool中,如下图所示,该方法把所有的jar包都解压了,便于后面注入的时候直接操纵某个class文件,然后注入完毕后重新zip成jar包。

repo 拉去android源码 replugin源码_android_03


核心的方法就在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,分别如下:
LoaderActivityInjectorLocalBroadcastInjectorProviderInjectorProviderInjector2GetIdentifierInjector 下面单独分析每个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的调用。

repo 拉去android源码 replugin源码_Replugin_04


看到没有,把AppCompatActivity替换成了他自己的PluginAppCompatActivit,然后替换了super.onCreateView()调用,为什么要替换super调用?因为在编译成字节码后,会变成全路径的调用,如果不替换的话,调用会出错。 下图是编译成字节码后的内容,很显然,这里的super.xxx调用是全路径的,如果不替换super.xxxPluginActivity里的方法不会被调用到。

repo 拉去android源码 replugin源码_ide_05

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.ContentResolverandroid.content.ContentProviderClient这两个类的调用的。替换方式也是一样的,直接换成了Replugin的调用方法。

d.GetIdentifierInjector

repo 拉去android源码 replugin源码_repo 拉去android源码_06


这里只是把第三个参数替换成了对应的包名,这里做的这个处理应该是为了在找资源的时候防止找错了。

三、流程分析

repo 拉去android源码 replugin源码_插件化_07

四、总结

整个过程都是进行了class文件的处理,但从整体代码来看,可以优化的地方还有很多,反复扫描类文件,缺乏增量编译机制,每次都需要处理所有的class,导致整个编译非常的耗时,相比非Replugin模式编译,编译时间至少长了一倍。我猜测360团队可能在编码上就行成了约束,因此不需要借助插件进行编译替换,所以没有对编译速度做过多的优化。