Android Gradle Plugin插件开发——进阶

结合现有开源项目讲述 GradlePluginDemo

<1>Android Gradle Plugin插件开发——基础

<2>Android Gradle Plugin插件开发——进阶

<3>Android Gradle Plugin插件开发——出师

1.通过上篇讲解,已经学会了如何创建一个简单的plugin,当然这不是一个HelloWord,只是打印了另外一句话。

当使用插件com.android.application的时候,需要配置一些参数如:android,defaultConfig等,那么想给自己的插件配置参数应该如何操作了,别急,下面一一道来。
1》建立一个参数配置类

class MethodTimerExtension {
    String exclude = ""//"android,uis.com.
    boolean isMain = true//主线程
    int timeout = 100//超时时间
    boolean enableJar = true//jar包是否处理
    boolean enableLog = true
}
2》然后在上文的MethodTimerPlugin.applay中加入,此时通过project.methodTimer获取配置的值,在applay函数中暂时不能使用,在transform中就可以直接使用了
project.extensions.create("methodTimer",MethodTimerExtension)
3》在使用插件的地方配置
apply plugin: 'com.uis.methodtimer'
methodTimer{
    exclude 'interprocess.uis.com.web_demo.PermissionProxy'
    isMain false
    enableJar true
    enableLog true
    timeout 50
}
//此插件的参数配置完成,那么如何使用呢?
在上文的MethodTimerTransform.transform方法中,可以通过以下语句打印出来

println(project.methodTimer)

也可以在MethodTimerPlugin.applay中加入一个任务打印

       project.task("paramTask").doLast {
            println(project.methodTimer)
        }
2.参数获取了,那么如何进行代码注入呢?代码注入需要考虑到class文件和jar文件,对于class文件直接通过遍历目录即可;对于jar文件java提供了一个JarFile来进行遍历,此时遍历把JarEntity逐个解压到文件夹下,之后的注入的方式和之前一样,jar文件注入完成后,需要重新打包,此时使用JarOutputStream。对于没有注入的jar文件,无需重复打包,直接传递出去。以下是jar文件的解压,和目录压缩成jar的方法。
class JarZipUtil {

    /**
     * 将该jar包解压到指定目录
     * @param jarPath jar包路径
     * @param destDirPath jar包解压后路径
     * @return isExclude
     */
    static boolean unzipJar(String jarPath, String destDirPath) {
        if (jarPath.endsWith('.jar')) {
            JarFile jarFile = new JarFile(jarPath)
            jarFile.entries().each {jarEntry->
                if(!jarEntry.directory) {
                    def outFile = new File(destDirPath, jarEntry.name)
                    outFile.getParentFile().mkdirs()
                    def inputStream = jarFile.getInputStream(jarEntry)
                    def outputStream = new FileOutputStream(outFile)
                    outputStream << inputStream//这是groovy特有写法把右边输入到左右,比java简洁
                    outputStream.close()
                    inputStream.close()
                }
            }
            jarFile.close()
        }
        return false
    }

    /**
     * 重新打包jar
     * @param packagePath 目录文件
     * @param destPath jar包路径
     */
    static void zipJar(String packagePath, String destPath) {
        File dir = new File(packagePath)
        JarOutputStream outputStream = new JarOutputStream(new FileOutputStream(destPath))
        outputStream.setLevel(Deflater.BEST_SPEED)
        dir.eachFileRecurse { file ->
            if(!file.directory) {//此处需要把windows系统下的\\替换成/,否则在后续打包遍历jar会找不到文件
                def entryName = file.getAbsolutePath().substring(packagePath.length() + 1).replace("\\","/")
                outputStream.putNextEntry(new ZipEntry(entryName))
                InputStream inputStream = new FileInputStream(file)
                outputStream << inputStream
                inputStream.close()
            }
        }
        outputStream.close()
    }
}
3.接下来要做的就是代码注入了,此时选择的是javassist进行代码注入,它是java api用起来没有障碍。

1》先要创建一个ClassPool,把transform中所有的class目录和jar解压后的目录加入进去,Javassist有几点需要注意,注入方法里不能使用泛型,虽然java支持,但是注入的代码避免,其次里面使用的类型是强类型,如果调用的方法里出现泛型,此处需要转换,下面的代码中会出现这种情况。

2》这些目录加入进来,还要加上android sdk目录,就可以进行代码注入了,否则会出现找不到类的错误

ClassPool pool = new ClassPool(true)//初始化一个ClassPool
pool.appendClassPath(project.android.bootClasspath[0].toString())//加入android sdk
pool.appendClassPath(path)//需要注入代码的class路径

//此处注入一个耗时统计的entity,保存函数开始时的时间start,一个键值key和函数信息info
CtClass ctClass = pool.makeClass("com.uis.MethodTimerEntity")//创建一个类
        CtClass string = pool.get("java.lang.String")
        ctClass.addField(CtField.make("public long start;",ctClass))//创建一个变量field
        ctClass.addField(CtField.make("public String info;",ctClass))//
        CtConstructor constructor = new CtConstructor([CtClass.longType, string, string] as CtClass[],ctClass)//定义入参
        constructor.setBody("{start = \$1;\n info = \$2+\"-\"+\$3+\" cost time = %s ms\";}")//创建构造函数
        ctClass.addConstructor(constructor)//$1$2$3表示函数的第一个,第二个,第三个入参,$0表示this指针
        ctClass.writeFile(path)//保存MethodTimerEntity到目录下,之后通过全包名使用
//此处注入2个方法,函数执行时调用startCount,结束调用endCount
//javassist不支持范型,HashMap不能加中括号,对一些类最好使用全包名引用的方式
ctClass = pool.makeClass(TAG)//TAG = com.uis.MethodTimer
        ctClass.addField(CtField.make("private static boolean isMain = ${ext.isMain};",ctClass))//创建一个变量
        ctClass.addField(CtField.make("private static int timeout = ${ext.timeout};",ctClass))
        ctClass.addField(CtField.make("private static java.util.HashMap countMap = new java.util.HashMap();",ctClass))
        ctClass.addMethod(CtNewMethod.make("public static void startCount(String key,String calssName,String method){\n" +
                "       if(!isMain || android.os.Looper.myLooper() == android.os.Looper.getMainLooper()){\n" +
                "           countMap.put(key,new com.uis.MethodTimerEntity(System.currentTimeMillis(),calssName,method));\n" +
                "       }\n"+
                "    }",ctClass))//创建一个方法method
        //fixed java.lang.VerifyError: Verifier rejected class
        //java是强类型,同时javassist不支持范型
        ctClass.addMethod(CtNewMethod.make("public static void endCount(String key){\n" +
                "        Object entity = countMap.remove(key);\n" +
                "        if((!isMain || android.os.Looper.myLooper() == android.os.Looper.getMainLooper()) && entity != null && entity instanceof com.uis.MethodTimerEntity){\n" +
                "           com.uis.MethodTimerEntity v = (com.uis.MethodTimerEntity)entity;\n" +//强制类型转换
                "           long costtime = System.currentTimeMillis() - v.start;\n"+
                "           if(costtime > timeout){\n" +
                "               Object[] v1 = new Object[]{String.valueOf(costtime)};\n"+//String.valueOf(String,Object...)是一个泛型,String.valueOf(String,Object[]),此出需要用Object[]
"               android.util.Log.e(\"MethodTimer\",String.format(v.info,v1));\n" +
                "           }\n"+
                "       }\n"+
                "    }",ctClass))
        ctClass.writeFile(path)

3》上面通过自动生成耗时统计代码,避免使用外部的方法,让插件使用更简单。如何把代码插入到函数开始和函数结束的地方呢?请看以下内容:

void insertTimerCode(String path,String className){
//path是directory,className format is: android.app.*
        try {
            CtClass ctClass = pool.getCtClass(className)
            if (ctClass.isFrozen()) {
                ctClass.defrost()
            }
            ctClass.getDeclaredMethods().each { method ->//遍历class下的所有method
                try {//过滤空方法,和access方法
                    if (method.methodInfo.codeAttribute != null && !method.name.startsWith("access\$")) {
                        insertMehodTimer(ctClass, method)//代码注入
                    }
                } catch (Exception ex) {
                    println(TAG + "###insertMethodTimer###" + ex.message)
                }
            }
            ctClass.writeFile(path)//保存注入后的代码
            ctClass.detach()
        }catch (Exception ex){
            println(TAG + "===insertTimerCode###" + ex.message)
        }
    }

void insertMehodTimer(CtClass clas, CtMethod method) throws Exception {
        if(0 == AccessFlag.ABSTRACT.and(method.modifiers).intValue()) {//抽象方法不注入
            String className = clas.name
            String methodName = method.name
            String key = DigestUtils.md5Hex(className + methodName + method.hashCode())//给每个类的函数生成唯一键值
            String methodInfo = methodName + "(" + method.methodInfo.getLineNumber(0) + ")"
            method.insertBefore("com.uis.MethodTimer.startCount(\"${key}\",\"${className}\",\"${methodInfo}\");\n")//插入函数执行前,构造函数super后
            method.insertAfter("com.uis.MethodTimer.endCount(\"${key}\");\n")//插入到return之前
        }
    }

至此代码注入的准备工作完成,剩下的就是遍历当前目录和解压后的jar包目录注入代码,下篇继续讲解

你可能感兴趣的:(Android)