市面上已經有不少實現方法插樁的框架了,爲何咱們還要重複造輪子呢?因爲業務拓展時刻在變化,咱們不得不去造一個適應業務的輪子,在造輪子以前,咱們先列幾個比較熱門的框架進行比較,列出優缺點,而後再總結本身實現的輪子
java
框架 | 性能 | 技術實現 | 方法參數獲取 | 混淆 | 範圍 |
---|---|---|---|---|---|
hugo | 差 | aspectJ | 支持 | 不支持 | java 類 |
costTime | 好 | asm | 不支持 | 支持 | app 模塊 |
matrix | 好 | asm | 不支持 | 支持 | 全部模塊 |
Mamba | 好 | asm | 支持 | 支持 | 全部模塊 |
hugo 是利用 aspectJ 實現的方法插樁,使用很簡單,只須要給方法添加 @DebugLog 註解便可獲取方法的執行耗時。因爲使用的是 aspectJ,只能做用於 java 文件,對於 aar 文件不起做用,而且,獲取方法和參數的整個過程很是耗時,具體能夠看 enterMethod 和 exitMethod 方法。hugo 也不支持混淆,codeSignature.getName 拿到的是混淆後的方法,沒法拿到原函數名,這也就沒法作收集統計。可能 hugo 的定位僅僅只是 debug 階段的統計,從註解 DebugLog 就能夠看出。
android
這是巴神寫的一個統計方法耗時的框架,使用的是 asm 進行方法插樁,使用也很簡單,只須要給方法添加 @Cost 註解便可,插件會掃描類下面的全部方法是否有添加 @Cost 註解,若是有的話,則對方法進行插樁,插樁效果以下:git
System.out.println("========start=========");
TimeUtil.setsStartTime("newFunc", System.nanoTime());
// 原方法執行體
TimeUtil.setEndTime("newFunc", System.nanoTime());
System.out.println(TimeCache.getCostTime("newFunc"));
System.out.println("========end=========");
複製代碼
但探究了一下源碼,costTime 僅支持對當前 app module 有效,具體能夠看 transfrom 的部分,對於 library 生成的 jar 部分是不作插樁的。
github
Matrix 是騰訊的一款 APM 框架,在 matrix-gradle-plugin 模塊中實現了對方法的插樁,具體原理能夠參考個人文章《Matrix 之 TraceCanary 源碼分析》。Matrix 並不會記錄方法的名稱,而是給每一個插樁的方法生成惟一的 methodId,而且生成一份方法與 methodId 映射的配置文件,在作數據上報的時候,只須要上傳 methodId 便可,雲端只須要經過配置文件解析出 methodId 對應的方法名,便可查看到整個方法調用鏈,優勢固然是不言而喻,數據大小和內存優化作的很是極致,缺點也有,methodId 會隨着版本的變化而變化,須要維護每一個版本的配置文件,在作數據分析時,須要根據版本號來調整。
數組
Mamba 的實現相似於 Matrix,但插樁的內容不是 methodId,而是當前的類、方法名和方法參數,插樁效果以下:app
public void test() {
long start = System.currentTimeMillis();
Class<Test> cls = Test.class;
Mamba.i(cls, "test");
// 原方法體
Mamba.i(cls, "test");
}
複製代碼
Mamba 自己不作邏輯,只將方法體的開始和結束交給實現 IMambaLoader 類來實現。Mamba 還提供了使用 @Track 註解來捕捉方法信息的功能,用於輔助無痕埋點方案參數值的獲取功能,插樁效果以下:框架
// 原方法
@Track
private void open(String t, float a, double b, long c) {
Toast.makeText(this, "What can I say?Mamba out", Toast.LENGTH_SHORT).show();
}
// 插樁以後效果
private void open(String str, float f, double d, long j) {
Class<TrackActivity> cls = TrackActivity.class;
Mamba.i(cls, "open", str, Float.valueOf(f), Double.valueOf(d), Long.valueOf(j));
Toast.makeText(this, "What can I say?Mamba out", 0).show();
}
複製代碼
缺點就是,基礎類型須要裝箱成引用類型,存儲到 Object 數組中
ide
Mamba 採用 gradle-plugin 和 asm 實現的方法插樁,Mamba 會遍歷 full project 的 class,並利用 asm 在方法的開始和結束插入字節碼。
Mamba 插入的字節碼爲何是 Class、MethodName、Method Params 呢?函數
在業務實踐中,想要作到無痕埋點方案是不可能的,有的埋點部分會依賴上下文環境,而且還要記錄當前的變量值,因此,咱們不得不在業務代碼中進行硬編埋點。源碼分析
爲了解決硬編問題,我想到的一個解決方案就是:將須要埋點的地方寫成函數調用,而後將須要記錄的變量做爲函數的參數,而後給函數標記 @Track,而後 Mamba 會根據 @Track 註解自動去實現方法和參數的插樁,咱們只須要在 Mamba 的實現類中進行埋點的數據便可。
下面給一份操做示例,需求是:在點擊事件中記錄 userName 變量
public class MyActivity{
public void onClick(View view){
String userName = editUserName.getText().toString();
updateUser(userName);
// 通常來講,咱們可能會直接進行硬編,好比 TrackManager.get().logEvent("獲取用戶名稱",userName)
}
/* * 更新用戶名稱 */
private void updateUser(String userName){
...
}
}
複製代碼
咱們來改造一下:
public class MyActivity{
public void onClick(View view){
String userName = editUserName.getText().toString();
updateUser(userName)
}
// 給更新用戶添加一個 Track 註解便可
@Track
private void updateUser(String userName){
...
}
}
複製代碼
生成字節碼後的結果爲:
public class MyActivity{
public void onClick(View view){
String userName = editUserName.getText().toString();
updateUser(userName)
}
@Track
private void updateUser(String userName){
Class<MyActivity> cls = MyActivity.class;
Mamba.i(cls, "updateUser", userName);
...
}
}
複製代碼
咱們只須要在 Mamba 的實現類中對 class 爲 MyActivity,method 爲 updateUser 的方法進行判斷,並取出 params 值便可,例如:
override fun methodEnter(clazz: Class<*>?, methodName: String?, args: Array<out Any>?) {
when (clazz) {
MyActivity::class.java -> {
trackMyActivity(methodName, args)
}
}
}
private fun trackMyActivity(methodName: String?, args: Array<out Any>?){
when(methodName){
"updateUser"->{
// 獲取 userName 值
val userName = args!![0] as String
// 使用 TrackManager 進行埋點操做
}
}
}
複製代碼
雖然 @Track 仍然須要在業務代碼中進行編輯,但已是儘可能小的侵入業務代碼,即便之後不須要記錄用戶名,咱們也無需去刪除 @Track 註解,只須要移除 Mamba 實現類中對 updateUser 的判斷便可。
那麼讀者可能會問了,爲啥你不直接作自動化收集方法參數,而是使用註解的方式侵入業務?其實,我也想過這種方案,但對於基礎類型參數很是的不友好,若是我想統一收集方法參數,就必須使用一個你們都有的父類容器來存,因此,這裏定義了 Object 數組來存儲參數,但問題又來了,基礎類型沒有父類你怎麼辦,只能將基礎類型包裝成引用類型,也就是將 float 包裝成 Float.valueOf() 存進 Object 數組,這種包裝會消耗內存,試想,若是對全部的方法參數都進行包裝收集,性能就成了問題。因此,這裏採用 @Track 註解本身認爲要收集的方法。
你們可能會比較關心插樁後的性能問題,我這裏列一下測試用例和結果:
一、方法插樁,屢次測試耗時爲 0 毫秒
二、方法參數插樁,屢次測試,耗時大約在 2 毫秒
總的來講,各個方案實現都差很少,略微的差別在於業務的不一樣實現。
Mamba 也提供了兩個默認的實現類:
具體使用能夠查看 Mamba README:github.com/MRwangqi/Ma…