這段時間想到一個有趣的功能,就是在Android的代碼編譯期間進行一些騷操作,來達到一些日常情境下難以實現(xiàn)的功能,比如監(jiān)聽應用中的所有onClick點擊時間,或者監(jiān)聽某些方法的運行耗時,如果在代碼中一個方法一個方法修改會很蛋疼,所以想通過Gradle插件來實現(xiàn)在應用的編譯期間進行代碼插入的功能。
1、AOP的概念
其實這已經涉及到AOP(Aspect Oriented Programming),即面向切面編程,在編譯期間對代碼進行動態(tài)管理,以達到統(tǒng)一維護的目的。

舉個栗子,Android開發(fā)我們都知道,在項目越來越大的時候,應用可能被分解為多個模塊,如果你要往所有模塊的方法里頭加一句‘我是大傻叼’的Toast,那是不是得跪。所以最好的方式是想辦法在編譯的時候拿到所有方法,往方法里頭懟一個Toast,這樣還不會影響到運行期間性能。
2、Transform

如圖所示是Android打包流程,.java文件->.class文件->.dex文件,只要在紅圈處攔截住,拿到所有方法進行修改完再放生就可以了,而做到這一步也不難,Google官方在Android Gradle的1.5.0 版本以后提供了 Transfrom API, 允許第三方 Plugin 在打包 dex 文件之前的編譯過程中操作 .class 文件,我們做的就是實現(xiàn)Transform進行.class文件遍歷拿到所有方法,修改完成對原文件進行替換。
/**
* 自動埋點追蹤,遍歷所有文件更換字節(jié)碼
*/
public class AutoTransform extends Transform {
@Override
String getName() {
return "AutoTrack"
}
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS
}
@Override
Set<QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT
}
@Override
boolean isIncremental() {
return false
}
@Override
public void transform(
@NonNull Context context,
@NonNull Collection<TransformInput> inputs,
@NonNull Collection<TransformInput> referencedInputs,
@Nullable TransformOutputProvider outputProvider,
boolean isIncremental) throws IOException, TransformException, InterruptedException {
//此處會遍歷所有文件
/**遍歷輸入文件*/
inputs.each { TransformInput input ->
/**
* 遍歷jar
*/
input.jarInputs.each { JarInput jarInput ->
...
}
/**
* 遍歷目錄
*/
input.directoryInputs.each { DirectoryInput directoryInput ->
...
}
}
}
3、Gradle插件實現(xiàn)
通過Transform提供的api可以遍歷所有文件,但是要實現(xiàn)Transform的遍歷操作,得通過Gradle插件來實現(xiàn),關于Gradle插件的知識可以看相關博客,也可以直接看博主的項目Luffy。編寫Gradle插件可能需要一點Goovy知識,具體編寫直接用java語言寫也可以,Goovy是完全兼容java的,只截取插件入口部分實現(xiàn)PluginEntry.groovy
class PluginEntry implements Plugin<Project> {
@Override
void apply(Project project) {
...
//使用Transform實行遍歷
def android = project.extensions.getByType(AppExtension)
registerTransform(android)
...
}
def static registerTransform(BaseExtension android) {
AutoTransform transform = new AutoTransform()
android.registerTransform(transform)
}
4、字節(jié)碼編寫
完成上面的操作以后就剩下一件事了,那就是拿到.class文件了,大家都知道.class文件是字節(jié)碼格式的,操作起來難度是相當于大的,所以需要一個字節(jié)碼操作庫來減輕難度,那就是ASM了。
4.1、ASM簡介
ASM 可以直接產生二進制的class 文件,也可以在增強既有類的功能。Java class 被存儲在嚴格格式定義的 .class文件里,這些類文件擁有足夠的元數據來解析類中的所有元素:類名稱、方法、屬性以及 Java 字節(jié)碼(指令)。
4.2、具體使用ASM
ASM框架中的核心類有以下幾個:
- ClassReader:該類用來解析編譯過的class字節(jié)碼文件。
- ClassWriter:該類用來重新構建編譯后的類,比如說修改類名、屬性以及方法,甚至可以生成新的類的字節(jié)碼文件。
- ClassVisitor:主要負責 “拜訪” 類成員信息。其中包括標記在類上的注解,類的構造方法,類的字段,類的方法,靜態(tài)代碼塊。
- AdviceAdapter:實現(xiàn)了MethodVisitor接口,主要負責 “拜訪” 方法的信息,用來進行具體的方法字節(jié)碼操作。
ClassVisitor的全部方法如下,按一定的次序來遍歷類中的成員。

在ClassVisitor中根據你的條件進行判斷,滿足條件的類才會修改其中方法,比如要統(tǒng)計點擊事件的話,需要實現(xiàn)View$OnClickListener接口的類才會遍歷其中的方法進行操作。
class AutoClassVisitor extends ClassVisitor {
AutoClassVisitor(final ClassVisitor cv) {
super(Opcodes.ASM4, cv)
}
@Override
void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
//進行需要滿足類的條件過濾
...
super.visit(version, access, name, signature, superName, interfaces)
}
@Override
void visitInnerClass(String name, String outerName, String innerName, int access) {
// 內部類信息
...
super.visitInnerClass(name, outerName, innerName, access)
}
@Override
MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
// 拿到需要修改的方法,執(zhí)行修改操作
MethodVisitor methodVisitor = cv.visitMethod(access, name, desc, signature, exceptions)
MethodVisitor adapter = null
...
adapter = new AutoMethodVisitor(methodVisitor, access, name, desc)
...
return methodVisitor
}
@Override
void visitEnd() {
//類中成員信息遍歷介紹
...
super.visitEnd()
}
}
在MethodVisitor中根據對已經拿到的方法進行修改了。
MethodVisitor adapter = new AutoMethodVisitor(methodVisitor, access, name, desc) {
boolean isAnnotation = false
@Override
protected void onMethodEnter() {
super.onMethodEnter()
//進入方法時可以插入字節(jié)碼
...
}
@Override
protected void onMethodExit(int opcode) {
super.onMethodExit(opcode)
//退出方法前可以插入字節(jié)碼
...
}
/**
* 需要通過注解的方式加字節(jié)碼才會重寫這個方法來進行條件過濾
*/
@Override
AnnotationVisitor visitAnnotation(String des, boolean visible) {
...
return super.visitAnnotation(des, visible)
}
}
5、實戰(zhàn)演練
以上就是總體的思路了,現(xiàn)在就通過Luffy根據具體需求實戰(zhàn)一下,比如說在onClick方法點擊的耗時(自動埋點也是一樣的道理,只不過換了插樁的方法)。
5.1、插件配置
先打包一下插件到本地倉庫進行引用,在項目的根build.gradle加入插件的依賴
dependencies {
classpath 'com.xixi.plugin:plugin:1.0.1-SNAPSHOT'
}
在app的build.gradle中
apply plugin: 'apk.move.plugin'
xiaoqingwa{
name = "小傻逼"
isDebug = true
//具體配置
matchData = [
//是否使用注解來找對應方法
'isAnotation': false,
//方法的匹配,可以通過類名或者實現(xiàn)的接口名匹配
'ClassFilter': [
['ClassName': null, 'InterfaceName':null,
'MethodName':null, 'MethodDes':null]
],
//插入的字節(jié)碼,方法的執(zhí)行順序visitAnnotation->onMethodEnter->onMethodExit
'MethodVisitor':{
MethodVisitor methodVisitor, int access, String name, String desc ->
MethodVisitor adapter = new AutoMethodVisitor(methodVisitor, access, name, desc) {
boolean isAnnotation = false
@Override
protected void onMethodEnter() {
super.onMethodEnter()
//使用注解找對應方法的時候得加這個判斷
}
@Override
protected void onMethodExit(int opcode) {
super.onMethodExit(opcode)
//使用注解找對應方法的時候得加這個判斷
}
/**
* 需要通過注解的方式加字節(jié)碼才會重寫這個方法來進行條件過濾
*/
@Override
AnnotationVisitor visitAnnotation(String des, boolean visible) {
return super.visitAnnotation(des, visible)
}
}
return adapter
}
]
}
要是使用演示的話,因為還沒上傳到jcenter庫,所以只能本地倉庫打包插件,記得要先把依賴都注釋掉,插件打包完成后再啟用,不然會編譯不過去的。
xiaoqingwa{}里頭的配置信息先不用管,等會會講到,主要是為了能夠不修改插件進行動態(tài)更換插樁的方法。
5.2、應用測試
插件配置好了之后就可以測試一下效果了,先寫一個耗時統(tǒng)計的工具類
TimeCache.java
/**
* Author:xishuang
* Date:2018.01.10
* Des:計時類,編譯器加入指定方法中
*/
public class TimeCache {
public static Map<String, Long> sStartTime = new HashMap<>();
public static Map<String, Long> sEndTime = new HashMap<>();
public static void setStartTime(String methodName, long time) {
sStartTime.put(methodName, time);
}
public static void setEndTime(String methodName, long time) {
sEndTime.put(methodName, time);
}
public static String getCostTime(String methodName) {
long start = sStartTime.get(methodName);
long end = sEndTime.get(methodName);
long dex = end - start;
return "method: " + methodName + " cost " + dex + " ns";
}
}
大概思路就是使用HashMap來臨時保存對應方法的時間,退出方法時獲取時間差。
在一個方法的前后插入時間統(tǒng)計的方法,這個具體的過程要怎么操作呢,因為class文件是字節(jié)碼格式的,ASM也是進行字節(jié)碼操作,所以必須先把插入的代碼轉換成字節(jié)碼先。這里推薦一個字節(jié)碼查看工具Java Bytecode Editor,導入.class文件就可以看到對應字節(jié)碼了。
比如我們要插入的代碼如下:
private void countTime() {
TimeCache.setStartTime("newFunc", System.currentTimeMillis());
TimeCache.setEndTime("newFunc", System.currentTimeMillis());
Log.d("耗時", TimeCache.getCostTime("newFunc"));
}
先把.java文件編譯成.class文件,用Java Bytecode Editor打開

然后根據其用ASM提供的Api一一對應的把代碼填進來加到onMethodEnter和onMethodExit中。
//方法前加入
methodVisitor.visitMethodInsn
methodVisitor.visitLdcInsn(name)
methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false)
methodVisitor.visitMethodInsn(INVOKESTATIC, "com/xishuang/plugintest/TimeCache", "setStartTime", "(Ljava/lang/String;J)V", false)
//方法后加入
methodVisitor.visitLdcInsn(name)
methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false)
methodVisitor.visitMethodInsn(INVOKESTATIC, "com/xishuang/plugintest/TimeCache", "setEndTime", "(Ljava/lang/String;J)V", false)
methodVisitor.visitLdcInsn("耗時")
methodVisitor.visitLdcInsn(name)
methodVisitor.visitMethodInsn(INVOKESTATIC, "com/xishuang/plugintest/TimeCache", "getCostTime", "(Ljava/lang/String;)Ljava/lang/String;", false)
methodVisitor.visitMethodInsn(INVOKESTATIC, "android/util/Log", "d", "(Ljava/lang/String;Ljava/lang/String;)I", false)
在app的build.gradle中配置得到的字節(jié)碼,最后設置一下過濾條件,最終的代碼如下:
build.gradle
xiaoqingwa{
name = "小傻逼"
isDebug = true
//具體配置
matchData = [
//是否使用注解來找對應方法
'isAnotation': false,
//方法的匹配,可以通過類名或者實現(xiàn)的接口名匹配
'ClassFilter': [
['ClassName': 'com.xishuang.plugintest.MainActivity', 'InterfaceName': 'android/view/View$OnClickListener',
'MethodName':'onClick', 'MethodDes':'(Landroid/view/View;)V']
],
//插入的字節(jié)碼,方法的執(zhí)行順序visitAnnotation->onMethodEnter->onMethodExit
'MethodVisitor':{
MethodVisitor methodVisitor, int access, String name, String desc ->
MethodVisitor adapter = new AutoMethodVisitor(methodVisitor, access, name, desc) {
boolean isAnnotation = false
@Override
protected void onMethodEnter() {
super.onMethodEnter()
//使用注解找對應方法的時候得加這個判斷
// if (!isAnnotation){
// return
// }
methodVisitor.visitMethodInsn(INVOKESTATIC, "com/xishuang/plugintest/MainActivity", "notifyInsert", "()V", false)
methodVisitor.visitLdcInsn(name)
methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false)
methodVisitor.visitMethodInsn(INVOKESTATIC, "com/xishuang/plugintest/TimeCache", "setStartTime", "(Ljava/lang/String;J)V", false)
}
@Override
protected void onMethodExit(int opcode) {
super.onMethodExit(opcode)
//使用注解找對應方法的時候得加這個判斷
// if (!isAnnotation){
// return
// }
methodVisitor.visitLdcInsn(name)
methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false)
methodVisitor.visitMethodInsn(INVOKESTATIC, "com/xishuang/plugintest/TimeCache", "setEndTime", "(Ljava/lang/String;J)V", false)
methodVisitor.visitLdcInsn("耗時")
methodVisitor.visitLdcInsn(name)
methodVisitor.visitMethodInsn(INVOKESTATIC, "com/xishuang/plugintest/TimeCache", "getCostTime", "(Ljava/lang/String;)Ljava/lang/String;", false)
methodVisitor.visitMethodInsn(INVOKESTATIC, "android/util/Log", "d", "(Ljava/lang/String;Ljava/lang/String;)I", false)
}
/**
* 需要通過注解的方式加字節(jié)碼才會重寫這個方法來進行條件過濾
*/
@Override
AnnotationVisitor visitAnnotation(String des, boolean visible) {
// if (des.equals("Lcom/xishuang/annotation/AutoCount;")) {
// println "注解匹配:" + des
// isAnnotation = true
// }
return super.visitAnnotation(des, visible)
}
}
return adapter
}
]
}
'isAnotation'表示是否使用注解的方式找到對應方法,這里false,因為我們現(xiàn)在是通過具體類信息來判斷的。
'ClassFilter'表示過濾條件,其中'ClassName'和'InterfaceName'用于判斷哪些類中的方法可以遍歷其中的方法進行匹配修改,不滿足的話就不會進行方法名匹配了,這些感興趣的童鞋都可以改插件自定義擴展。
'MethodName'和'MethodDes'是方法名和方法描述符,可以唯一確定一個方法名,滿足類過濾條件的就會進行方法匹配,例如我們要統(tǒng)計的點擊事件onClick(View v)。
意思就是繼承自android/view/View$OnClickListener的類或者類名是'com.xishuang.plugintest.MainActivity'就可以進行方法的遍歷,然后方法滿足onClick(View v)就會進行代碼插入操作。
設置完之后rebuild一下就可以了,可以通過日志看下具體信息,isDebug = true可以開啟日志打印。

通過日志可以看到我們設置的字節(jié)碼確實插樁成功,現(xiàn)在再看一下編譯后的文件驗證一下,具體位置是:app\build\intermediates\transforms\AutoTrack\debug\folders

其中的notifyInsert()是我用來彈Toast額外調試用的,請忽略。在手機上點擊一下按鈕測試一下,發(fā)現(xiàn)確實記錄下點擊的耗時時間,完成。

5.3、注解匹配
除了以上的方式來查找修改的方法之外,還可以通過注解來查找,切換很簡單,只需要改一下app的build.gradle文件就可以了,項目中也有栗子,添加了一個注解類。
/**
* Author:xishuang
* Date:2018.1.9
* Des:時間統(tǒng)計注解
*/
@Target(ElementType.METHOD)
public @interface AutoCount {
}
然后在對應的方法上添加你自定義的注解
@AutoCount
private void onClick() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@AutoCount
@Override
public void onClick(View v) {
if (v.getId() == R.id.button) {
Toast.makeText(this, "我是按鈕", Toast.LENGTH_SHORT).show();
}
}
修改一下build.gradle中的配置文件
xiaoqingwa{
name = "小傻逼"
isDebug = true
//具體配置
matchData = [
//是否使用注解來找對應方法
'isAnotation': true,
//方法的匹配,可以通過類名或者實現(xiàn)的接口名匹配
'ClassFilter': [
['ClassName': 'com.xishuang.plugintest.MainActivity', 'InterfaceName': 'android/view/View$OnClickListener',
'MethodName':'onClick', 'MethodDes':'(Landroid/view/View;)V']
],
//插入的字節(jié)碼,方法的執(zhí)行順序visitAnnotation->onMethodEnter->onMethodExit
'MethodVisitor':{
MethodVisitor methodVisitor, int access, String name, String desc ->
MethodVisitor adapter = new AutoMethodVisitor(methodVisitor, access, name, desc) {
boolean isAnnotation = false
@Override
protected void onMethodEnter() {
super.onMethodEnter()
//使用注解找對應方法的時候得加這個判斷
if (!isAnnotation){
return
}
methodVisitor.visitMethodInsn(INVOKESTATIC, "com/xishuang/plugintest/MainActivity", "notifyInsert", "()V", false)
methodVisitor.visitLdcInsn(name)
methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false)
methodVisitor.visitMethodInsn(INVOKESTATIC, "com/xishuang/plugintest/TimeCache", "setStartTime", "(Ljava/lang/String;J)V", false)
}
@Override
protected void onMethodExit(int opcode) {
super.onMethodExit(opcode)
//使用注解找對應方法的時候得加這個判斷
if (!isAnnotation){
return
}
methodVisitor.visitLdcInsn(name)
methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false)
methodVisitor.visitMethodInsn(INVOKESTATIC, "com/xishuang/plugintest/TimeCache", "setEndTime", "(Ljava/lang/String;J)V", false)
methodVisitor.visitLdcInsn("耗時")
methodVisitor.visitLdcInsn(name)
methodVisitor.visitMethodInsn(INVOKESTATIC, "com/xishuang/plugintest/TimeCache", "getCostTime", "(Ljava/lang/String;)Ljava/lang/String;", false)
methodVisitor.visitMethodInsn(INVOKESTATIC, "android/util/Log", "d", "(Ljava/lang/String;Ljava/lang/String;)I", false)
}
/**
* 需要通過注解的方式加字節(jié)碼才會重寫這個方法來進行條件過濾
*/
@Override
AnnotationVisitor visitAnnotation(String des, boolean visible) {
if (des.equals("Lcom/xishuang/annotation/AutoCount;")) {
println "注解匹配:" + des
isAnnotation = true
}
return super.visitAnnotation(des, visible)
}
}
return adapter
}
]
}
關鍵代碼在于把'isAnotation'設為true,然后在visitAnnotation方法中添加你的注解類匹配,也就是這句des.equals("Lcom/xishuang/annotation/AutoCount;")代碼,注解類的描述符,運行效果和上面差不多,但是不會打印日志,因為通過注解來查找方法會遍歷每個方法,打印信息太多電腦會爆炸。
6、插件擴展自動埋點功能
針對以下的方法進行埋點監(jiān)聽,并實現(xiàn)了View的唯一區(qū)別鏈
- 1、View的onClick(View v)方法
- 2、Fragment的onResume()方法
- 3、Fragment的onPause()方法
- 4、Fragment的setUserVisibleHint(boolean b)方法
- 5、Fragment的onHiddenChanged(boolean b)方法
- 6、在app的module中手動設置的監(jiān)聽條件:指定方法或注解方法
插件的增加自動埋點處理類主要是ChoiceUtil
App中對監(jiān)聽的方法處理的類是AutoHelper.java
/**
* Author:xishuang
* Date:2018.03.01
* Des:自動埋點幫助類
*/
public class AutoHelper {
private static final String TAG = AutoHelper.class.getSimpleName();
private static Context context = AutoApplication.getInstance().getApplicationContext();
/**
* 實現(xiàn)onClick點擊時間的自動注入處理
*/
public static void onClick(View view) {
String path = AutoUtil.getPath(context, view);
String activityName = AutoUtil.getActivityName(view);
path = activityName + ":onClick:" + path;
Log.d(TAG, path);
}
/**
* 實現(xiàn)onClick點擊時間的自動注入處理
*/
public static void onClick() {
Log.d(TAG, "onClick()");
}
public static void onFragmentResume(Fragment fragment) {
Log.d(TAG, "onFragmentResume" + fragment.getClass().getSimpleName());
}
public static void onFragmentPause(Fragment fragment) {
Log.d(TAG, "onFragmentPause" + fragment.getClass().getSimpleName());
}
public static void setFragmentUserVisibleHint(Fragment fragment, boolean isVisibleToUser) {
Log.d(TAG, "setFragmentUserVisibleHint->" + isVisibleToUser + "->" + fragment.getClass().getSimpleName());
}
public static void onFragmentHiddenChanged(Fragment fragment, boolean hidden) {
Log.d(TAG, "onFragmentHiddenChanged->" + hidden + "->" + fragment.getClass().getSimpleName());
}

具體的信息可以看下源碼,已共享到github上,在這里講了下大概的思路和代碼框架,博主已經初步擴展完成自動埋點的基礎功能,更有趣的玩法大家可以自己修改一下插件來實現(xiàn)。
github地址:Luffy。