结合现有开源项目讲述 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之前
}
}