每個方法增加日誌輸出,記錄呼叫時間
使用Gradle自定義一個外掛,並且在程式碼編譯階段,使用ASM在Transform中進行程式碼插入。
一、gradle外掛
1. 建立module和groovy目錄
2. 定義build.gradle
apply plugin: 'groovy'
apply plugin: 'maven'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
//gradle和groovy的依賴
implementation gradleApi()
implementation localGroovy()
}
dependencies {
//直接使用build工具的asm類庫,不再額外引入asm的依賴
implementation 'com.android.tools.build:gradle:3.5.3'
}
repositories {
mavenCentral()
}
//本地配置的引數,需要上傳maven時配置使用者名稱和密碼等資訊
Properties props = new Properties()
props.load(project.rootProject.file('local.properties').newDataInputStream())
def artifactory_user = props.getProperty('user')
def pwd = props.getProperty('password')
def contextUrl = props.getProperty('contextUrl')
def repoKey = props.getProperty('repoKey')
def releaseRepoKey = props.getProperty('release_repoKey')
def snapshot = true
def log_version = '1.0.0'
def log_group = 'com.example.autolog'
def log_id = 'auto-log'
group = log_group
version = log_version
def debug = true
uploadArchives {
repositories {
mavenDeployer {
if (debug) {
//本地倉庫,生成的jar和pom放在根目錄下的repo-local目錄中
repository(url: "file://$projectDir/../repo-local")
} else {
def repokey = snapshot ? repoKey : releaseRepoKey
repository(url: "${contextUrl}/${repokey}") {
authentication(username: artifactory_user, password: pwd)
}
}
pom.groupId = log_group
pom.artifactId = log_id
pom.version = log_version
pom.project {
licenses {
license {
name 'The Apache Software License, Version 2.0'
url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
}
}
}
}
}
}
複製程式碼
3. 建立Plugin
AutoLogPlugin.groovy:
package com.example.autolog
import com.android.build.gradle.AppExtension
import com.android.build.gradle.AppPlugin
import org.gradle.api.Plugin
import org.gradle.api.Project
//實現Plugin介面
public class AutoLogPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
//判斷專案使用了com.android.application外掛
def isApp = project.plugins.hasPlugin(AppPlugin)
if(isApp){
println 'project('+project.name+') apply auto-log plugin'
def android = project.extensions.getByType(AppExtension)
def transform = new LogTransform(project)
//註冊Transform
android.registerTransform(transform)
project.afterEvaluate {
//do init
}
}
}
}
複製程式碼
4. 建立LogTransform
package com.example.autolog
import com.android.build.api.transform.*
import com.android.build.gradle.internal.pipeline.TransformManager
import org.gradle.api.Project
import org.objectweb.asm.*
import org.objectweb.asm.util.TraceClassVisitor
public class LogTransform extends Transform {
Project project;
LogTransform(Project project) {
this.project = project
}
@Override
String getName() {
return 'auto-log'
}
//處理型別:java位元組碼會被處理
@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 {
project.logger.warn('start auto-log transform..')
if (!transformInvocation.incremental) {
transformInvocation.outputProvider.deleteAll()
}
transformInvocation.inputs.each { TransformInput input ->
input.jarInputs.each { JarInput jarInput ->
//遍歷jar檔案
scanJar(jarInput, transformInvocation.outputProvider)
}
input.directoryInputs.each { DirectoryInput directoryInput ->
//遍歷原始碼目錄
project.logger.warn("directoryInput : " + directoryInput.name)
directoryInput.file.eachFileRecurse { File file ->
project.logger.warn("directory file:"+file.name)
if (file.isFile()) {
//如果是位元組碼檔案,進行處理
scanClass(file)
}
}
}
}
}
void scanJar(JarInput jarInput, TransformOutputProvider outputFileProvider) {
}
void scanClass(File file) {
project.logger.warn("scanClass " + file.name)
def optclass = new File(file.getParent(), file.name + ".opt")
FileInputStream is = new FileInputStream(file)
FileOutputStream os = new FileOutputStream(optclass)
//返回處理後的位元組碼,寫入檔案
def bytes = generateCode(is,file.name)
os.write(bytes)
is.close()
os.close()
//刪除原有的編譯後的class,將新生成的class重新命名
if (file.exists()) {
file.delete()
}
optclass.renameTo(file)
}
byte[] generateCode(InputStream is, String name) {
ClassReader cr = new ClassReader(is)
ClassWriter cw = new ClassWriter(cr, 0)
//ASM進行方法掃描的類
ClassVisitor cv = new LogClassVisitor(Opcodes.ASM5, cw, name)
//TraceClassVisitor可以將掃描過的類的位元組碼儲存到本地
TraceClassVisitor tcv = new TraceClassVisitor(cv, new PrintWriter(new File("E:/ASM/" + name + ".txt")))
cr.accept(tcv, ClassReader.EXPAND_FRAMES)
return cw.toByteArray()
}
class LogClassVisitor extends ClassVisitor {
String cn
boolean isInterface
LogClassVisitor(int api) {
super(api)
}
LogClassVisitor(int api, ClassVisitor cv, String className) {
super(api, cv)
this.cn = className
}
@Override
void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces)
//類是否是介面
isInterface = access == Opcodes.ACC_INTERFACE
}
@Override
MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions)
//如果不是建構函式,且不是介面,則進行方法處理
if(mv != null && name !="<init>" && !isInterface){
return new LogMethodVisitor(Opcodes.ASM5, mv, name, cn)
}
return mv
}
}
class LogMethodVisitor extends MethodVisitor {
String mn
String cn
LogMethodVisitor(int api) {
super(api)
}
LogMethodVisitor(int api, MethodVisitor mv, String methodName, String className) {
super(api, mv)
this.mn = methodName
this.cn = className
}
@Override
void visitCode() {
//使用ASM介面進行方法注入
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false)
mv.visitVarInsn(Opcodes.LSTORE, 1)
mv.visitLdcInsn(cn)
mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder")
mv.visitInsn(Opcodes.DUP)
mv.visitMethodInsn(Opcodes.INVOKESPECIAL,"java/lang/StringBuilder","<init>","()V", false)
mv.visitLdcInsn(mn + " start")
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,"java/lang/StringBuilder","append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false)
mv.visitVarInsn(Opcodes.LLOAD, 1)
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false)
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;",false)
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "v", "(Ljava/lang/String;Ljava/lang/String;)I", false)
mv.visitInsn(Opcodes.POP)
super.visitCode()
}
@Override
void visitMaxs(int maxStack, int maxLocals) {
//幀的最大棧長度,和本地變數的數量,可以檢視位元組碼進行比對
super.visitMaxs(maxStack + 4, maxLocals + 2)
}
}
}
複製程式碼
5. 執行uploadArchives的task,結果:
二、ASM工具使用
1. 如何檢視位元組碼
-
Android Studio有一個外掛——ASM Bytecode Outline,可以用來看類的位元組碼。
(未執行成功,可能跟kotlin有關...)
-
使用ASM提供的TraceClassVisitor,將類的位元組碼儲存到本地,然後再分析。
2. 使用ASM提供的API進行方法修改
-
未新增程式碼的方法:
package com.example.autolog; public class Test { void test(){ } } 複製程式碼
-
使用TraceClassVisitor將對應的位元組碼儲存到對應的檔案,如下所示:
// class version 51.0 (51) // access flags 0x21 public class com/example/autolog/Test { // compiled from: Test.java // access flags 0x1 public <init>()V L0 LINENUMBER 3 L0 ALOAD 0 INVOKESPECIAL java/lang/Object.<init> ()V RETURN L1 LOCALVARIABLE this Lcom/example/autolog/Test; L0 L1 0 MAXSTACK = 1 MAXLOCALS = 1 // access flags 0x0 test()V L0 LINENUMBER 6 L0 RETURN L1 LOCALVARIABLE this Lcom/example/autolog/Test; L0 L1 0 MAXSTACK = 0 MAXLOCALS = 1 } 複製程式碼
-
新增程式碼的方法:
package com.example.autolog; public class Test { void test(){ } } 複製程式碼
-
使用TraceClassVisitor將對應的位元組碼儲存到對應的檔案,如下所示:
// class version 51.0 (51) // access flags 0x21 public class com/example/autolog/Test { // compiled from: Test.java // access flags 0x1 public <init>()V L0 LINENUMBER 5 L0 ALOAD 0 INVOKESPECIAL java/lang/Object.<init> ()V RETURN L1 LOCALVARIABLE this Lcom/example/autolog/Test; L0 L1 0 MAXSTACK = 1 MAXLOCALS = 1 // access flags 0x0 test()V L0 LINENUMBER 8 L0 INVOKESTATIC java/lang/System.currentTimeMillis ()J LSTORE 1 L1 LINENUMBER 9 L1 LDC "Test" NEW java/lang/StringBuilder DUP INVOKESPECIAL java/lang/StringBuilder.<init> ()V LDC "test start:" INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; LLOAD 1 INVOKEVIRTUAL java/lang/StringBuilder.append (J)Ljava/lang/StringBuilder; INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String; INVOKESTATIC android/util/Log.v (Ljava/lang/String;Ljava/lang/String;)I POP L2 LINENUMBER 10 L2 RETURN L3 LOCALVARIABLE this Lcom/example/autolog/Test; L0 L3 0 LOCALVARIABLE start J L1 L3 1 MAXSTACK = 4 MAXLOCALS = 3 } 複製程式碼
-
將位元組碼中test()中多出來的內容用ASM的api新增即可,在LogMethodVisitor類的visitCode()方法中