美文网首页
Replugin源码解析之replugin-plugin-gra

Replugin源码解析之replugin-plugin-gra

作者: PeytonWu | 来源:发表于2017-11-03 17:15 被阅读0次

    概述

    该部分基础知识在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}"
            }
    */
        }
    

    相关文章

      网友评论

          本文标题:Replugin源码解析之replugin-plugin-gra

          本文链接:https://www.haomeiwen.com/subject/iovtmxtx.html