在查看同事写的代码时,发现各种方法调用有点混乱,不易快速梳理逻辑.所以想着是否可以通过打印方法日志,从而观察各种方法间的调用逻辑.
需求
- 打印方法名称、入参、耗时、返回值
- 对项目所有方法生效
- 可以自定义排除不打印的类
- 可作为三方库
实现
本项目使用 Javassist 实现了以下需求.
AspectJ
这个功能让我想起了 JakeWharton 大神的 hugo 库,通过 AspectJ 对添加注解的方法插入 Log 在调用的时候就可以观察调用顺序及参数.但是这个库不能满足所有方法的打印,只能对添加了注解的方法打印,手动给方法添加注解工程量太大.但是可以通过改造切入点 @Pointcut("execution(* " + BuildConfig.PACKAGE_NAME + "..*.*(..)) BuildConfig.PACKAGE_NAME 为项目包名
使其切入包内所有方法,这样便可以打印所有方法,但是这样又不能满足作为第三方库的问题,因为注解值必须是静态常量,不能动态修改.本项目借鉴了 hugo 库部分思想.在Github 中有使用 AspectJ 实现部分需求,并且可以切入 Kotlin.
Aspectj 本身语法很简单,应该是最容易上手的 AOP 框架,但是对 Kotlin 支持有问题,关于这个可以参考沪江网校对 AspectJ 改造的开源项目,其支持了更多功能,文后参考中有给出相关链接.
Javassist
Javassist 是一个开源的分析、编辑和创建 Java 字节码 的类库 .性能较 ASM 差,跟 cglib 差不多,但是使用相对简单.
为了方便引用所以采用 gradle plugin 方式,方便调用者快速添加到原有项目中去.关于编写插件的教程网络有很多,不做过多介绍.
TraceExtension 自定义 plugin 配置
1 2 3 4 5 6 7 8 9 10 11 12
| class TraceExtension { List<String> classExcludes = new ArrayList<>() List<String> packageExcludes = new ArrayList<>() def enabled = true def packageName def logLevel = "d" }
|
TraceExtension 定义自定义配置类,方便排除一些简单的工具类调用.
Trace
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| class Trace implements Plugin<Project> {
@Override void apply(Project project) {
def hasApp = project.plugins.withType(AppPlugin) def hasLib = project.plugins.withType(LibraryPlugin)
if (!hasApp && !hasLib) { throw new IllegalStateException("'android' or 'android-library' plugin required.") }
final def variants if (hasApp) { variants = project.android.applicationVariants } else { variants = project.android.libraryVariants }
final def log = project.logger variants.all { variant -> if (!variant.buildType.isDebuggable()) { log.debug("Skipping non-debuggable build type '${variant.buildType.name}'.") return } else if (!project.Trace.enabled) { log.debug("Trace is not disabled.") return } }
project.extensions.create('Trace', TraceExtension)
println("=== Trace Plugin ===") def android = project.extensions.findByType(AppExtension) android.registerTransform(new TraceTransform(project)) }
}
|
Trace 为自定义的插件,在 release 环境下不需要插入 Log 以免影响性能.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
| class TraceTransform extends Transform {
Project project
TraceTransform(Project project) { this.project = project }
@Override String getName() { return "TraceTransform" }
@Override Set<QualifiedContent.ContentType> getInputTypes() { return TransformManager.CONTENT_CLASS }
@Override Set<? super QualifiedContent.Scope> getScopes() { return TransformManager.SCOPE_FULL_PROJECT }
@Override boolean isIncremental() { return false }
@Override void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException { super.transform(transformInvocation) if (!project.Trace.enabled) { return }
def outputProvider = transformInvocation.outputProvider
transformInvocation.inputs.each { TransformInput input -> input.directoryInputs.each { DirectoryInput directoryInput -> TraceInject.injectDirCode(directoryInput.file.absolutePath, project) def dest = outputProvider.getContentLocation( directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY) FileUtils.copyDirectory(directoryInput.file, dest) }
input.jarInputs.each { JarInput jarInput -> 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) } }
} }
|
我们为了在方法中添加 Log 就需要赶在 class 文件被转化为 dex 文件之前去修改类,Tranfrom 注册到插件中便会自动添加到 Task 执行序列中,并且正好是项目被打包成dex之前,不需要像 AspectJ 那用手动添加到最后一个 Task 中去.
TraceInject 插入 Log
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113
| class TraceInject {
static String TAG = "Trace" static ClassPool POOL = ClassPool.getDefault()
static void injectDirCode(String path, Project project) { POOL.appendClassPath(path) POOL.appendClassPath(project.android.bootClasspath[0].toString()) File dir = new File(path) if (dir.isDirectory()) { dir.eachFileRecurse { File file -> String filePath = file.absolutePath if (filePath.endsWith(".class") && !filePath.contains('R$') && !filePath.contains('R.class') && !filePath.contains("BuildConfig.class")) { modifyClass(project, path, filePath) } } } } static void modifyClass(Project project, String path, String filePath) { String classPath def packageName = project.Trace.packageName filePath = filePath.replace("/", ".")
if (classExcludes(project, filePath)) { return } if (packageExcludes(project, filePath)) { return }
if (filePath.contains(packageName)) { int index = filePath.indexOf(packageName) classPath = filePath.substring(index, filePath.length()) } else { println("project can not inject") return } String className = classPath.substring(0, classPath.length() - 6) .replace('/', '.') .replace('/', '.') CtClass c = POOL.getCtClass(className) if (c.isFrozen()) { c.defrost() } CtMethod[] methods = c.getDeclaredMethods() int i = 0 for (CtMethod method : methods) { if (method.isEmpty() || Modifier.isNative(method.getModifiers())) { return } i++ insertTime(project.Trace.logLevel, className.replace(packageName + ".", ""), method) } c.writeFile(path) c.detach() }
static void insertTime(String logLevel, String className, CtMethod method) { try { int pos = 1
CodeAttribute codeAttribute = method.getMethodInfo().getCodeAttribute() LocalVariableAttribute attribute = (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag) int size = method.getParameterTypes().length String[] paramTypes = new String[size] if (attribute == null) { return } for (int i = 0; i < size; i++) { paramTypes[i] = method.getParameterTypes()[i].name }
def stringType = POOL.getCtClass("java.lang.String") def objType = POOL.getCtClass("java.lang.Object") method.addLocalVariable("startTime", CtClass.longType) method.addLocalVariable("endTime", CtClass.longType) method.addLocalVariable("className", stringType) method.addLocalVariable("methodName", stringType) method.addLocalVariable("lineNumber", CtClass.intType) method.addLocalVariable("returnObj", objType)
StringBuilder startInjectSB = new StringBuilder()
method.insertBefore(startInjectSB.toString())
StringBuilder endInjectSB = new StringBuilder()
method.insertAfter(endInjectSB.toString())
} catch (Exception e) { e.printStackTrace() } } }
|
Javassit 语法相对简单,在看过参考文档和一些简单的资料就可以直接上手.
用法及项目地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 项目 gradle classpath 'cn.libery.analysis:trace:1.0.1' module gradle apply plugin: 'analysis-trace' Trace { enabled true packageName "cn.libery.analysis.sample" logLevel "i" classExcludes = ["cn.libery.analysis.sample.Logger", "cn.libery.analysis.sample.Logger2"] packageExcludes = ["cn.libery.analysis.sample.test"] }
|
可以参考 Github 中 sample 配置
Github
-> 打印入参
<- 打印耗时及返回值
参考
沪江网校 AspectJ
Javassist 使用指南
AspectJ 和 Javassist 对比
AspectJ 语法
hugo
AspectJ 适配 Kotlin