Replugin源码解析之replugin-plugin-gra

2017-11-03  本文已影响0人  PeytonWu

概述

该部分基础知识在Gradle学习-----Gradle自定义插件Replugin源码解析之replugin-host-gradle涉及,不再重复累述

源码分析

ReClassTransform入口相关类源码如下

public class ReClassTransform extends Transform {

    private Project project
    private def globalScope

    /* 需要处理的 jar 包 */
    def includeJars = [] as Set
    def map = [:]

    public ReClassTransform(Project p) {
        this.project = p
        AppPlugin appPlugin = project.plugins.getPlugin(AppPlugin)
        // taskManager 在 2.1.3 中为 protected 访问类型的,在之后的版本为 private 访问类型的,
        // 使用反射访问
        def taskManager = BasePlugin.metaClass.getProperty(appPlugin, "taskManager")
        this.globalScope = taskManager.globalScope;
    }

    @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()

        /* 读取用户配置 */
        ReClassConfig config = project.extensions.getByName('repluginPluginConfig')

        File rootLocation = null
        try {
            rootLocation = outputProvider.rootLocation
            println ">>> rootLocation: throw----------->rootLocation=" + (rootLocation == null ? "null" :rootLocation)
        } catch (Throwable e) {
            //android gradle plugin 3.0.0+ 修改了私有变量,将其移动到了IntermediateFolderUtils中去
            rootLocation = outputProvider.folderUtils.getRootFolder()
            println ">>> rootLocation: throw----------->"
        }
        if (rootLocation == null) {
            throw new GradleException("can't get transform root location")
        }
        println ">>> rootLocation: ${rootLocation}"
        // 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]
        println ">>> variantDir: ${variantDir}"

        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
        }
    }
...//省略
 }

查看入口transform 方法
2.1 project.extensions.getByName('repluginPluginConfig')读取用户在replugin插件项目的build.gradle中配置的参数,在下面的代码中会用到,比如获取需要忽略的注入器ignoredInjectors、需要忽略替换的ActivityignoredActivities、自定义的代码注入器customInjectors等。
2.2 includedInjectors返回用户未忽略的注入器,代码如下

/**
     * 返回用户未忽略的注入器的集合
     */
    def includedInjectors(def cfg, String variantDir) {//product\debug
        def injectors = []
        Injectors.values().each {
            //设置project
            it.injector.setProject(project)
            //设置variant关键dir
            it.injector.setVariantDir(variantDir)
            if (!(it.nickName in cfg.ignoredInjectors)) {
                injectors << it.nickName
            }
        }
        injectors
    }

Injectors为枚举,其中定义了5种IClassInjector注入器的封装,被封装的分别对应操作对应组件及resource,源码如下

public enum Injectors {

    LOADER_ACTIVITY_CHECK_INJECTOR('LoaderActivityInjector', new LoaderActivityInjector(), '替换 Activity 为 LoaderActivity'),//替换插件中的Activity的继承相关代码 为 replugin-plugin-library 中的XXPluginActivity父类
    LOCAL_BROADCAST_INJECTOR('LocalBroadcastInjector', new LocalBroadcastInjector(), '替换 LocalBroadcast 调用'),//替换插件中的LocalBroadcastManager调用代码 为 插件库的调用代码。
    PROVIDER_INJECTOR('ProviderInjector', new ProviderInjector(), '替换 Provider 调用'),//替换 插件中的 ContentResolver 调用代码 为 插件库的调用代码
    PROVIDER_INJECTOR2('ProviderInjector2', new ProviderInjector2(), '替换 ContentProviderClient 调用'),//替换 插件中的 ContentProviderClient 调用代码 为 插件库的调用代码
    GET_IDENTIFIER_INJECTOR('GetIdentifierInjector', new GetIdentifierInjector(), '替换 Resource.getIdentifier 调用') //替换 插件中的 Resource.getIdentifier 调用代码的参数 为 动态适配的参数

    IClassInjector injector
    String nickName
    String desc

    Injectors(String nickName, IClassInjector injector, String desc) {
        this.injector = injector
        this.nickName = nickName
        this.desc = desc;
    }
}

2.3 getInputTypes() 指明当前Trasfrom要处理的数据类型,可选类型包括CONTENT_CLASS(代表要处理的数据是编译过的Java代码,而这些数据的容器可以是jar包也可以是文件夹),CONTENT_JARS(包括编译过的Java代码和标准的Java资源),CONTENT_RESOURCES,CONTENT_NATIVE_LIBS等。在replugin-plugin-gradle中是使用Transform来做代码插桩,所以选用CONTENT_CLASS类型。
2.4doTransform方法是执行reclass的关键,代码如下

  * 执行 Transform
     */
    def doTransform(Collection<TransformInput> inputs,
                    TransformOutputProvider outputProvider,
                    Object config,
                    def injectors) {

        /* 初始化 ClassPool */
        Object pool = initClassPool(inputs)

        /* 进行注入操作 */
        Util.newSection()
        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)

        Util.newSection()
    }

2.4.1 initClassPool添加编译时需要引用的到类到 ClassPool。解压对应的jar的路径及名字,将解压后再重新打包的jar的路径及名字作为值,jar的名字作为key存放在map中。includeJars存放的是 android.jar的目录、其它各种引用到的jar进行解压后目录、及class所在目录。

    /**
     * 初始化 ClassPool
     */
    def initClassPool(Collection<TransformInput> inputs) {
        Util.newSection()
        def pool = new ClassPool(true)
        // 添加编译时需要引用的到类到 ClassPool, 同时记录要修改的 jar 到 includeJars
        Util.getClassPaths(project, globalScope, inputs, includeJars, map).each {
            println "    $it"
            pool.insertClassPath(it)
        }
        pool
    }

Util.newSection()为画一条直线长度为50个--,代码如下

    def static newSection() {
        50.times {
            print '--'
        }
        println()
    }

2.4.2

CtMethod:是一个class文件中的方法的抽象表示。一个CtMethod对象表示一个方法。(Javassit 库API)
CtClass:是一个class文件的抽象表示。一个CtClass(compile-time class)对象可以用来处理一个class文件。(Javassit 库API)
ClassPool:是一个CtClass对象的容器类。(Javassit 库API)
.class文件:.class文件是一种存储Java字节码的二进制文件,里面包含一个Java类或者接口。

遍历Injectors枚举中值,与用户未忽略的注入器injectors对比,如果未被忽略则执行doInject否则打印跳过信息

2.4.3 doInject即执行注入操作,分别对目录下的每个class文件执行handleDir及对每个jar文件执行handleJar源码如下

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

2.4.4
handleDir即调用各个注入器的injectClass方法,handleJar为通过jar名称获取map中存放的对应解压目录,然后调用各个注入器的injectClass方法

 /**
     * 处理目录中的 class 文件
     */
    def handleDir(ClassPool pool, DirectoryInput input, IClassInjector injector, Object config) {
        println ">>> Handle Dir: ${input.file.absolutePath}"
        injector.injectClass(pool, input.file.absolutePath, config)
    }

   /**
     * 处理 jar
     */
    def handleJar(ClassPool pool, JarInput input, IClassInjector injector, Object config) {
        File jar = input.file
        if (jar.absolutePath in includeJars) {
            println ">>> Handle Jar: ${jar.absolutePath}"
            String dirAfterUnzip = map.get(jar.getParent() + File.separatorChar + jar.getName()).replace('.jar', '')
            injector.injectClass(pool, dirAfterUnzip, config)
        }
    }

2.4.5 各种注入器的injectClass方法,针对class目录,遍历下面class文件进行处理,以LoaderActivityInjector作为例子分析,源码如下

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

def private init() {
        /* 延迟初始化 loaderActivityRules */
        // todo 从配置中读取,而不是写死在代码中
        if (loaderActivityRules == null) {
            def buildSrcPath = project.project(':buildsrc').projectDir.absolutePath
            def loaderConfigPath = String.join(File.separator, buildSrcPath, 'res', LOADER_PROP_FILE)

            loaderActivityRules = new Properties()
            new File(loaderConfigPath).withInputStream {
                loaderActivityRules.load(it)
            }

            println '\n>>> Activity Rules:'
            loaderActivityRules.each {
                println it
            }
            println()
        }
    }

2.4.6 handleActivity代码如下,主要作用就是更改Activity的最顶层父类,对应关系为loaderActivityRules数组。

 private def handleActivity(ClassPool pool, String activity, String classesDir) {
        def clsFilePath = classesDir + File.separatorChar + activity.replaceAll('\\.', '/') + '.class'
        if (!new File(clsFilePath).exists()) {
            return
        }

        println ">>> Handle $activity  &clsFilePath=-----------> $clsFilePath"

        def stream, ctCls
        try {
            stream = new FileInputStream(clsFilePath)
            ctCls = pool.makeClass(stream);
/*
             // 打印当前 Activity 的所有父类
            CtClass tmpSuper = ctCls.superclass
            while (tmpSuper != null) {
                println(tmpSuper.name)
                tmpSuper = tmpSuper.superclass
            }
*/
            // 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') {
                                    call.replace('{super.' + call.getMethodName() + '($$);}')
                                } else {
                                    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()
            }
        }
    }

2.4.7将解压修改后的jar重新打包,然后删除解压目录,只留jar。源码如下

    /**
     * 将解压的 class 文件重新打包,然后删除 class 文件
     */
    def repackage() {
        Util.newSection()
        println '>>> Repackage...'
        includeJars.each {
            File jar = new File(it)
            String JarAfterzip = map.get(jar.getParent() + File.separatorChar + jar.getName())
            String dirAfterUnzip = JarAfterzip.replace('.jar', '')
            // println ">>> 压缩目录 $dirAfterUnzip"

            Util.zipDir(dirAfterUnzip, JarAfterzip)

            // println ">>> 删除目录 $dirAfterUnzip"
            FileUtils.deleteDirectory(new File(dirAfterUnzip))
        }
    }

2.4.8最后将处理好的class和jar赋值到输出目录,jar改名避免重名,交给下个task处理,至此Transform结束。

 /**
     * 拷贝处理结果
     */
    def copyResult(def inputs, def outputs) {
        // Util.newSection()
        inputs.each { TransformInput input ->
            input.directoryInputs.each { DirectoryInput dirInput ->
                copyDir(outputs, dirInput)
            }
            input.jarInputs.each { JarInput jarInput ->
                copyJar(outputs, jarInput)
            }
        }
    }

    /**
     * 拷贝目录
     */
    def copyDir(TransformOutputProvider output, DirectoryInput input) {
        File dest = output.getContentLocation(input.name, input.contentTypes, input.scopes, Format.DIRECTORY)
        FileUtils.copyDirectory(input.file, dest)
//        println ">>> 拷贝目录 ${input.file.absolutePath} 到 ${dest.absolutePath}"
    }
    /**
     * 拷贝 Jar
     */
    def copyJar(TransformOutputProvider output, JarInput input) {
        File jar = input.file
        String jarPath = map.get(jar.absolutePath);
        if (jarPath != null) {
            jar = new File(jarPath)
        }

        String destName = input.name
        def hexName = DigestUtils.md5Hex(jar.absolutePath)
        if (destName.endsWith('.jar')) {
            destName = destName.substring(0, destName.length() - 4)
        }
        File dest = output.getContentLocation(destName + '_' + hexName, input.contentTypes, input.scopes, Format.JAR)
        FileUtils.copyFile(jar, dest)

/*
        def path = jar.absolutePath
        if (path in CommonData.includeJars) {
            println ">>> 拷贝Jar ${path} 到 ${dest.absolutePath}"
        }
*/
    }
上一篇下一篇

猜你喜欢

热点阅读