[toc]
运行环境
agp版本 4.1.2 gradle版本 6.5.0 as arctic fox
一.javassist概述
javassist 是 JavaProgrammingAssistant的简称,它本质上是一个java库,用于在不熟悉class字节码结构的情况下,能够修改或者新生成字节码。我们可以通过插入java代码来自动把对应的class字节码插入到class文件中。
相比于apt只能新增java文件,它能够新增或者修改class文件,因此能力更加强大.
二.javassist作用时机
android中javassist一般与transform结合使用,因为我们在javac任务后获得class文件列表,我们需要注入自己写的处理器来进行后续处理
transform作用于class字节码打包成dex的时期,android官方提供了transform任务来方便开发者在class转dex阶段能够插入任务到transfrom pipeline处理class.
通常我们也与javac任务结合起来,比如在javac阶段借助apt记录需要处理的class的信息,然后在transform阶段直接处理class,从而省去遍历class的时间消耗.
三.javassit完整例子
这里例子使用javassit插入代码来统计某些方法的执行时长,使用javassit整体过程包括:
- 定义注解ExecCost,这个注解标记需要计算耗时的方法.与apt不一样的是 这个定义不是必须,javasssit的使用不依赖于注解.
- 定义注解处理器CostProcessor,这个注解处理器将所有标记了ExecCost的类记录到一个新生成的类ExecCostCollect.java文件中
- 定义插件CostPlugin
- 定义CostTransform注册到plugin中,它会在class文件被转换成 dex 之前执行
3.1 定义插件ExecCost
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface ExecCost {
}
注意它的生命周期,虽然我们只需要在source阶段生成java文件,但是在class处理阶段,还需要判断哪些方法需要插入耗时计算代码
3.2 定义CostProcessor
@AutoService(Processor.class)
public class CollectionProcessor extends AbstractProcessor {
Messager mMessage;
Filer filer;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
mMessage = processingEnv.getMessager();
filer = processingEnv.getFiler();
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
for (TypeElement ann:annotations) {
Set<? extends Element> sets = roundEnv.getElementsAnnotatedWith(ann);
TypeSpec spec = buildClass(ann.getSimpleName().toString() , sets);
writeFile(spec);
}
return false;
}
private void writeFile(TypeSpec typeSpec){
JavaFile javaFile = JavaFile.builder("com.hch",typeSpec).build();
try {
javaFile.writeTo(processingEnv.getFiler());
} catch (IOException e) {
e.printStackTrace();
}
}
private TypeSpec buildClass(String annotationName, Set<? extends Element> sets) {
// public final class name ExecCostCollect
TypeSpec.Builder typeSpecBuilder = TypeSpec.classBuilder(annotationName+"Collect")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL);
//public String s_onCreate = "com.hch.MyApp";
int i = 0;
for (Element ele : sets) {
Element enclosingElement = ele.getEnclosingElement();
FieldSpec.Builder builder = FieldSpec.builder(String.class , "s"+i+"_"+ ele.getSimpleName() , Modifier.PUBLIC ,Modifier.FINAL , Modifier.STATIC);
builder.initializer("$S" , enclosingElement.toString());
typeSpecBuilder.addField(builder.build());
i++;
}
return typeSpecBuilder.build();
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> types = new LinkedHashSet<>();
types.add(ExecCost.class.getCanonicalName() );
return types;
}
}
Processor的定义可以参考第一篇,这里的重点在于process方法,获取用ExecCost标记的类名.
Set<? extends Element> sets = roundEnv.getElementsAnnotatedWith(ann);
buildClass方法根据注解的名字生成对应的collect类,如ExecCost注解生成的类名字为ExecCostCollect.class,同时将标记的类写入. ExecCostCollect.class生成的内容如下:
public final class ExecCostCollect {
public static final String s0_onCreate = "com.hch.MyApp";
public static final String s1_onCreate = "com.hch.MainActivity";
public static final String s2_print = "com.hch.Person";
public ExecCostCollect() {
}
}
3.3 定义Costplugin
class CostPlugin implements Plugin<Project>{
@Override
void apply(Project project) {
println("CostPlugin start")
//create extension
project.getExtensions().create("cost" , CostExtension.class)
// //register transform
// AppExtension 必须要在所在的模块依赖 com.android.tools.build:gradle:4.1.2'
AppExtension appExtension = project.getExtensions().findByType(AppExtension.class);
appExtension.registerTransform(new CostTransform(project))
println("CostPlugin end")
}
}
plugin的定义过程在gradle之插件定义中有详细说明,不再细说.
这里的要注意两点:
- appExtension.registerTransform(new CostTransform(project)) 将定义的CostTransform注册到tranform pipeline中.
- AppExtension是在agp中的类,因此我们需要依赖com.android.tools.build:gradle , 也就是在我们定义插件的模块的build.gradle中添加
dependencies {
// 使用javassist必须依赖
implementation 'com.android.tools.build:gradle:4.1.2'
implementation 'org.javassist:javassist:3.24.0-GA'
}
3.4 transform定义
transform的处理过程为
- 读取apt阶段生成的ExecCostCollect.class中写入的需要处理的class的信息
- 将需要处理的class ,找到含有ExecCost注解定义的方法,在方法中插入计算耗时的代码
- 将处理完的代码拷贝到输出中
CostTransform.groovy
public class CostTransform extends Transform {
ClassPool classPool
Project project
public CostTransform(Project p){
classPool = ClassPool.getDefault()
project = p
}
@Override
public String getName() {
return "cost";
}
@Override
public Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS;
}
@Override
public Set<? super QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT;
}
@Override
public boolean isIncremental() {
return false;
}
@Override
public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
println("transform begin .....");
super.transform(transformInvocation);
//project.android.bootClasspath 加入android.jar,不然找不到android相关的所有类
// 提示错误 javassist.CannotCompileException: cannot find android.os.Bundle
classPool.appendClassPath(project.android.bootClasspath[0].toString());
def outputProvider = transformInvocation.outputProvider
for (TransformInput input : transformInvocation.inputs) {
if (null == input) continue
//遍历jar文件 对jar不操作,但是要输出到out路径
for (JarInput jarInput : input.jarInputs) {
// 重命名输出文件
def jarName = jarInput.name
def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
if (jarName.endsWith(".jar")) {
jarName = jarName.substring(0, jarName.length() - 4)
}
def dest = outputProvider.getContentLocation(jarName + md5Name, jarInput.contentTypes, jarInput.scopes, Format.JAR)
FileUtils.copyFile(jarInput.file, dest)
}
//遍历文件夹
for (DirectoryInput directoryInput : input.directoryInputs) {
classPool.appendPathList(directoryInput.file.getAbsolutePath())
// 获取output目录
def dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
TraverseDir.traverse(classPool , directoryInput.file.getAbsolutePath() , directoryInput.file , TraverseDir.action_exec)
// 将input的目录复制到output指定目录
FileUtils.copyDirectory(directoryInput.file, dest)
}
}
//一定要释放,否则执行多次会复用之前导入的class
classPool.clearImportedPackages()
println("transform end .....");
}
}
3.4.1 读取ExecCostCollect.class 的内容
由于定义的注解都在我们自己的代码中,所以只需要处理文件夹中的类
TraverseDir.traverse(classPool , directoryInput.file.getAbsolutePath() , directoryInput.file , TraverseDir.action_exec)
TraverseDir.groovy
public class TraverseDir {
public static int action_exec = 0;
public static int action_nofast = 1
//存放collect中需要处理的class
static Map<String,String> map = new HashMap<>();
static filterAllClasses(ClassPool pool){
CtClass ctClass = pool.getOrNull("com.hch.ExecCostCollect")
if (ctClass != null){
CtField[] fields = ctClass.getDeclaredFields()
fields.each {
if (it.getConstantValue() != null){
map.put(it.getConstantValue(), false)
}
}
}
}
private static String getClassName(String dirPath, String classPath) {
String packagePath = classPath.substring(dirPath.length() + 1);
String clazz = packagePath.replaceAll("/", ".");
String substring = clazz.substring(0, packagePath.length() - ".class".length());
return substring;
}
private static void inject(ClassPool pool , String dirPath, String classPath , String className , int action){
//需要处理的class
boolean needWrite = false;
CtClass ctClass = pool.getCtClass(className)
if (map.containsKey(className)){
// 解冻
if (ctClass.isFrozen()) {
ctClass.defrost()
}
CtMethod[] ctMethods = ctClass.getDeclaredMethods()
ctMethods.each {
//这里显示出用buildSrc的缺陷,它无法依赖到annotation模块,也因此无法直接找到ExecCost的class
if (action == action_exec){
if (it.hasAnnotation("com.hch.annotation.ExecCost")){
needWrite = true;
println("insert start :${dirPath}/${className}.${it.name}");
//只加这一句不会有任何变化
it.addLocalVariable("start", CtClass.longType);
it.insertBefore("start = System.currentTimeMillis();");
String end = "System.out.println(\"" + ctClass.getName() + "." + it.getName() + " exeCost=\" +(System.currentTimeMillis()-start));";
it.insertAfter(end);
println("insert end !");
}
}else if (action == action_nofast){
}
}
if (needWrite){
try {
//保存class , 会自动补全包名
ctClass.writeFile(dirPath);
println("writeFile success:${dirPath}");
} catch (CannotCompileException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
println("detach ctClass");
ctClass.detach();//释放
}
}
static traverse(ClassPool pool , String dirPath , File file ,int action){
filterAllClasses(pool)
File[] fs = file.listFiles();
for (File f : fs) {
if (f.isDirectory()) { //若是目录,则递归
traverse(pool , dirPath, f , action);
} else if (f.isFile()) {
String className = getClassName(dirPath, f.getPath());
//代码插入
inject(pool , dirPath , file.getPath(), className , action);
}
}
}
}
filterAllClasses方法根据包名找到com.hch.ExecCostCollect,并且读取它的所有属性的内容. 因为我们写入的内容是以public static final 写入的属性,因此可以直接用getConstantValue获取属性的值.
static filterAllClasses(ClassPool pool){
CtClass ctClass = pool.getOrNull("com.hch.ExecCostCollect")
if (ctClass != null){
CtField[] fields = ctClass.getDeclaredFields()
fields.each {
if (it.getConstantValue() != null){
map.put(it.getConstantValue(), false)
}
}
}
}
找到需要处理的类后,遍历所有的类名,对需要处理的类中包含ExecCost注解的方法插入代码
if (map.containsKey(className)){
// 解冻
if (ctClass.isFrozen()) {
ctClass.defrost()
}
CtMethod[] ctMethods = ctClass.getDeclaredMethods()
ctMethods.each {
//这里显示出用buildSrc的缺陷,它无法依赖到annotation模块,也因此无法直接找到ExecCost的class
if (action == action_exec){
if (it.hasAnnotation("com.hch.annotation.ExecCost")){
...
}
}else if (action == action_nofast){
}
}
....
}
3.4.2 写入计算耗时代码
if (action == action_exec){
if (it.hasAnnotation("com.hch.annotation.ExecCost")){
needWrite = true;
println("insert start :${dirPath}/${className}.${it.name}");
//只加这一句不会有任何变化
it.addLocalVariable("start", CtClass.longType);
it.insertBefore("start = System.currentTimeMillis();");
String end = "System.out.println(\"" + ctClass.getName() + "." + it.getName() + " exeCost=\" +(System.currentTimeMillis()-start));";
it.insertAfter(end);
println("insert end !");
}
}
写入的时候,要注意insertBefore需要依赖android.jar包,如果不加入,会提示错误 javassist.CannotCompileException: cannot find android.os.Bundle
我们提前在transform方法中加入了android.jar的依赖
classPool.appendClassPath(project.android.bootClasspath[0].toString());
3.4.3 将修改过的文件写到输出
在transform中,我们处理完class之后,将所有的class都写到outpurProvider提供的输出路径中,jar也需要拷贝.
for (JarInput jarInput : input.jarInputs) {
// 重命名输出文件
def jarName = jarInput.name
def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
if (jarName.endsWith(".jar")) {
jarName = jarName.substring(0, jarName.length() - 4)
}
def dest = outputProvider.getContentLocation(jarName + md5Name, jarInput.contentTypes, jarInput.scopes, Format.JAR)
FileUtils.copyFile(jarInput.file, dest)
}
for (DirectoryInput directoryInput : input.directoryInputs) {
classPool.appendPathList(directoryInput.file.getAbsolutePath())
// 获取output目录
def dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
TraverseDir.traverse(classPool , directoryInput.file.getAbsolutePath() , directoryInput.file , TraverseDir.action_exec)
// 将input的目录复制到output指定目录
FileUtils.copyDirectory(directoryInput.file, dest)
}
3.5 踩到的坑记录
3.5.1 ExecCost注解的生命周期定义为RetentionPolicy.source导致class注解阶段找不到注解标记的方法
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface ExecCost {
}
3.5.2 找不到AppExtension
AppExtension在agp中,需要在transform所在模块增加依赖
dependencies {
// 使用javassist必须依赖
implementation 'com.android.tools.build:gradle:4.1.2'
implementation 'org.javassist:javassist:3.24.0-GA'
}
3.5.3 javassist 执行insertBefore时,提示错误 javassist.CannotCompileException: cannot find android.os.Bundle
这是因为classpool在模拟jvm的逻辑,在activity中插入代码需要能够模拟android环境,因此需要依赖android.jar
classPool.appendClassPath(project.android.bootClasspath[0].toString());
3.5.4 ctField.getDeclaredFields 读取class中的常量返回null
原本生成ExecCostCollect.class时,创建的属性为public static s_xxxx = "com.hch.xxx" ,这样读出来的值为null,
需要修改为public static final s_xxxx = "com.hch.xxx"
FieldSpec.Builder builder = FieldSpec.builder(String.class , "s"+i+"_"+ ele.getSimpleName() , Modifier.PUBLIC ,Modifier.FINAL , Modifier.STATIC);