Android坑坑之旅插件化Android知识

replugin源码解析之replugin-plugin-gra

2017-07-20  本文已影响2056人  osan

前言

replugin-plugin-gradle 是 RePlugin 插件框架中提供给replugin插件用的gradle插件,是一种动态编译方案实现。
主要在插件应用的编译期,基于Transform api 注入到编译流程中, 再通过Java字节码类库对编译中间环节的 Java 字节码文件进行修改,以便实现编译期动态修改插件应用的目的。
RePlugin 是一套完整的、稳定的、适合全面使用的,占坑类插件化方案,由360手机卫士的RePlugin Team研发,也是业内首个提出”全面插件化“(全面特性、全面兼容、全面使用)的方案。

注 :文件会提及两种插件,请阅读本文时注意提及插件的上下文情景,避免混淆概念:

结构概览

replugin-plugin-gradle,针对插件应用编译期的注入任务:
动态修改插件中的调用代码,改为调用replugin-plugin-library中的代码(如Activity的继承、Provider的重定向等)

目录概览

\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的基本用法

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

在开发自定义gradle插件时,都会先定义这么个文件。这里有 2 个知识点:

我们到插件实现类看看这个插件是如何工作的。

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> ,实现了apply接口方法,apply方法会在 build.gradle 中执行 apply plugin: 'replugin-plugin-gradle'时被调用。

接下来解读下 apply 方法的具体实现。

用于快速调试的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
                ...
            }
        }
    }
android {
    ...
}
apkFile = new File(apkDir, apkName)
adbFile = globalScope.androidBuilder.sdkInfo.adb;
installPluginTask.doLast {
    pluginDebugger.startHostApp()
    pluginDebugger.uninstall()
    pluginDebugger.forceStopHostApp()
    pluginDebugger.startHostApp()
    pluginDebugger.install()
}
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命令。

以上task分别有不同的调试目的,可以去分别了解下,细节实现大同小异。
看到这里,我们该插播一下调试方案的整体原理了:

  1. replugin-host-lib 的DebuggerReceivers类中,注册了一系列用于快速调试的广播,而replugin-host-lib是会内置在宿主应用中的。
  2. replugin-plugin-gradle 中创建了一系列gradle task,用于启动停止重启宿主应用,安装卸载运行插件应用。这些gradle task都是被动型task,需要通过命令行主动的运行这些task。
  3. 打开命令行终端,执行replugin插件项目的某个gradle task,以实现快速调试功能。比如:gradlew.bat rpInstallPluginDebug,最终就会将宿主和插件运行起来。
  4. 这些gradle task被手动执行后,task会执行一系列任务,比如通过adb push 插件到sdcard,或通过am命令发送广播,启动activity等。当发送一系列步骤1中注册的广播后,宿主应用收到广播后会执行对应的操作,比如启动插件的activity等。

Tips.调试模式开启方法:插件调试
Debug阶段建议开启,Release阶段建议关闭,默认为关闭状态

继续看apply()方法中的源码。

Transform:动态编译方案实现

@Override
    public void apply(Project project) {
        ...
        if (isApp) {

            ...

            def transform = new ReClassTransform(project)
            // 将 transform 注册到 android
            android.registerTransform(transform)
            ...
        }
    }

重点来了,这里就是动态编译方案的实现入口。
在详细解读动态编译实现之前,先了解2个概念:

去看看 ReClassTransform 类的核心实现。

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
    }
}
    def doTransform(Collection<TransformInput> inputs,
                    TransformOutputProvider outputProvider,
                    Object config,
                    def injectors) {

        /* 初始化 ClassPool */
        Object pool = initClassPool(inputs)
        ...
    }
>>> 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类或者接口。

    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(...)方法实现。

    /**
     * 执行注入操作
     */
    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

第一个被执行的就是 LoaderActivityInjector,用来修改插件中XXActivity类中的顶级XXActivity父类 为 XXPluginActivity父类。看看如何实现的。

@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)
            }
        }
    }
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'
    ]
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()
            }
        }
    }

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目录并访问到文件时,执行以下这段逻辑。

@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)
    }
    ...
}

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, $$);}')
            }
        }
    }
}

到这里广播注入器的工作就完成了。接下来看看ProviderInjector。

ProviderInjector

ProviderInjector,主要用来替换 插件中的 ContentResolver相关的方法调用 为 插件库的PluginProviderClient中的对应方法调用。

// 处理以下方法
public static def includeMethodCall = ['query',
                                       'getType',
                                       'insert',
                                       'bulkInsert',
                                       'delete',
                                       'update',
                                       'openInputStream',
                                       'openOutputStream',
                                       'openFileDescriptor',
                                       'registerContentObserver',
                                       'acquireContentProviderClient',
                                       'notifyChange',
]

直接看injectClass的实现,遍历class目录并访问到文件时,执行以下逻辑。

@Override
def injectClass(ClassPool pool, String dir, Map config) {
    ...
    try {
        ...

        /* 检查方法列表 */
        ctCls.getDeclaredMethods().each {
            it.instrument(editor)
        }

        ctCls.getMethods().each {
            it.instrument(editor)
        }

        ...
    }
    ...
}

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注入器的工作就完成了。接下来看看ProviderInjector2。

ProviderInjector2

ProviderInjector2,主要用来替换 插件中的 ContentProviderClient 相关的方法调用。

    // 处理以下方法
    public static def includeMethodCall = ['query', 'update']

看下injectClass的实现,遍历class目录并访问到文件时,执行以下这段逻辑。

@Override
def injectClass(ClassPool pool, String dir, Map config) {
    ...
    try {
        ...

        /* 检查方法列表 */
        ctCls.getDeclaredMethods().each {
            it.instrument(editor)
        }

        ctCls.getMethods().each {
            it.instrument(editor)
        }

        ...
    }
    ...
}

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}"
    }
}

到这里ProviderInjector2注入器的工作就完成了。接下来看看GetIdentifierInjector。

GetIdentifierInjector

GetIdentifierInjector,主要用来替换 插件中的 Resource.getIdentifier 方法调用的参数 为 动态适配的参数。

看下injectClass的实现,遍历class目录并访问到文件时,执行以下这段逻辑。

@Override
def injectClass(ClassPool pool, String dir, Map config) {
    ...
    try {
        ...

        /* 检查方法列表 */
        ctCls.getDeclaredMethods().each {
            it.instrument(editor)
        }

        ctCls.getMethods().each {
            it.instrument(editor)
        }

        ...
    }
    ...
}

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}"
            }
        }
    }
}

到此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)
        ...
    }

ReclassTansfrom任务完成,将会把输出继续传递给下一个TransfromtransformClassesWithDexFor{ProductFlavor}{BuildType},把处理权交还给android gradle插件。至此,replugin-plugin-gradle 插件的工作就全部结束了。

End

replugin-plugin-gradle 插件是一个compile-time gradle plugin,基于两大核心技术Transform + Javassist,完成了编译期对class文件的动态注入,进而实现动态修改构建目标文件的为replugin插件服务的gradle插件。

上一篇 下一篇

猜你喜欢

热点阅读