Replugin 源码分析------replugin-plugin-gradle插件源码分析

Posted Wastrel_xyz

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Replugin 源码分析------replugin-plugin-gradle插件源码分析相关的知识,希望对你有一定的参考价值。

前言

上一篇文章分享了宿主的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 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个传入参数:

类型参数名描述
Contexcontex任务上下文
Collection<TransformInput>inputs最终要打包进APK的class和jar路径
Collection<TransformInput>referencedInputs引用的class和jar的路径
TransformOutputProvideroutputProvider文件输出适配器
booleanisIncremental是否增量,不过从代码来看,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["$configPreConfig"])
            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包。

核心的方法就在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.xxxPluginActivity里的方法不会被调用到。

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


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

三、流程分析

四、总结

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

以上是关于Replugin 源码分析------replugin-plugin-gradle插件源码分析的主要内容,如果未能解决你的问题,请参考以下文章

Replugin 源码分析------replugin-host-gradle插件源码分析

Replugin 源码分析------replugin-host-gradle插件源码分析

唯一插件化Replugin源码及原理深度剖析--唯一Hook点原理

唯一插件化Replugin源码及原理深度剖析--唯一Hook点原理

唯一插件化Replugin源码及原理深度剖析--初始化之框架核心

唯一插件化Replugin源码及原理深度剖析--插件的安装加载原理