前言
成爲一名優秀的Android開發,需要一份完備的知識體系,在這裏,讓我們一起成長爲自己所想的那樣~。
現如今,編譯插樁技術已經深入 Android
開發中的各個領域,而 AOP
技術正是一種高效實現插樁的模式,它的出現正好給處於黑暗中的我們帶來了光明,極大地解決了傳統開發過程中的一些痛點,而 AspectJ
作爲一套基於 Java
語言面向切面的擴展設計規範,能夠賦予我們新的能力。在這篇文章我們將來學習如何使用 AspectJ
來進行插樁。本篇內容如下所示:
- 1)、編譯插樁技術的分類與應用場景。
- 2)、AspectJ 的優勢與侷限性。
- 3)、AspectJ 核心語法簡介。
- 4)、AspectJX 實戰。
- 5)、使用 AspectJX 打造自己的性能監控框架。
- 6)、總結。
面向切面的程序設計 (aspect-oriented programming (AOP))
吸引了很多開發者的目光, 但是如何在編碼中有效地實現這一套設計概念卻並不簡單,幸運的是,早在 2003 年,一套基於 Java
語言面向切面的擴展設計:AspectJ
誕生了。
不同與傳統的 OOP 編程,AspectJ (即 AOP)
的獨特之處在於 發現那些使用傳統編程方法無法處理得很好的問題。
例如一個要在某些應用中實施安全策略的問題。安全性是貫穿於系統所有模塊間的問題,而且每一個模塊都必須要添加安全性才能保證整個應用的安全性,並且安全性模塊自身也需要安全性,很明顯這裏的 安全策略的實施問題就是一個橫切關注點,使用傳統的編程解決此問題非常的困難而且容易產生差錯,這正是 AOP
發揮作用的時候了。
傳統的面向對象編程中,每個單元就是一個類,而 類似於安全性這方面的問題,它們通 常不能集中在一個類中處理,因爲它們橫跨多個類,這就導致了代碼無法重用,它們是不可靠和不可繼承的,這樣的編程方式使得可維護性差而且產生了大量的代碼冗餘,這是我們所不願意看到的。
而面向切面編程的出現正好給處於黑暗中的我們帶來了光明,它針對於這些橫切關注點進行處理,就似面向對象編程處理一般的關注點一樣。
在我繼續講解 AOP
編程之前,我們有必要先來看看當前編譯插樁技術的分類與應用場景。這樣能讓我們 從更高的緯度上去理解各個技術點之間的關聯與作用。
一、編譯插樁技術的分類與應用場景
編譯插樁技術具體可以分爲兩類,如下所示:
- 1)、
APT(Annotation Process Tools)
:用於生成 Java 代碼。 - 2)、
AOP(Aspect Oriented Programming)
:用於操作字節碼。
下面👇,我們分別來詳細介紹下它們的作用。
1、APT(Annotation Process Tools)
總所周知,ButterKnife、Dagger、GreenDao、Protocol Buffers
這些常用的註解生成框架都會在編譯過程中生成代碼。而 這種使用 AndroidAnnotation 結合 APT 技術 來生成代碼的時機,是在編譯最開始的時候介入的。而 AOP 是在編譯完成後生成 dex 文件之前的時候,直接通過修改 .class 文件的方式,來直接添加或者修改代碼邏輯的。
使用 APT
技術生成 Java
代碼的方式具有如下 兩方面 的優勢:
- 1)、隔離了框架複雜的內部實現,使得開發更加地簡單高效。
- 2)、大大減少了手工重複的工作量,降低了開發時出錯的機率。
2、AOP(Aspect Oriented Programming)
而對於操作字節碼的方式來說,一般都在 代碼監控、代碼修改、代碼分析 這三個場景有着很廣泛的應用。
相對於 Java
代碼生成的方式,操作字節碼的方式有如下 特點:
- 1)、應用場景更廣。
- 2)、功能更加強大。
- 3)、使用複雜度較高。
此外,我們不僅可以操作 .class
文件的 Java
字節碼,也可以操作 .dex
文件的 Dalvik
字節碼。下面我們就來大致瞭解下在以上三類場景中編譯插樁技術具體是如何應用的。
1、代碼監控
編譯插樁技術除了 不能夠實現耗電監控,它能夠實現各式各樣的性能監控,例如:網絡數據監控、耗時方法監控、大圖監控、線程監控 等等。
譬如 網絡數據監控 的實現,就是在 網絡層通過 hook 網絡庫方法 和 自動化注入攔截器的形式,實現網絡請求的全過程監控,包括獲取握手時長,首包時間,DNS 耗時,網絡耗時等各個網絡階段的信息。
實現了對網絡請求過程的監控之後,我們便可以 對整個網絡過程的數據表現進行詳細地分析,找到網絡層面性能的問題點,並做出針對性地優化措施。例如針對於 網絡錯誤率偏高
的問題,我們可以採取以下幾方面的措施,如下所示:
- 1)、使用 HttpDNS。
- 2)、將錯誤數據同步 CDN。
- 3)、CDN 調度鏈路優化。
2、代碼修改
用編譯插樁技術來實現代碼修改的場景非常之多,而使用最爲頻繁的場景具體可細分爲爲如下四種:
- 1)、
實現無痕埋點
:如網易HubbleData之Android無埋點實踐、51 信用卡 Android 自動埋點實踐
。 - 2)、
統一處理點擊抖動
:編譯階段統一 hook android.view.View.OnClickListener#onClick() 方法,來實現一個快速點擊無效的防抖動效果,這樣便能高效、無侵入性地統一解決客戶端快速點擊多次導致頻繁響應的問題。 - 3)、
第三方 SDK 的容災處理
:我們可以在上線前臨時修改或者 hook 第三方 SDK 的方法,做到快速容災上線。 - 4)、
實現熱修復框架
:我們可以在 Gradle 進行自動化構建的時候,即在 Java 源碼編譯完成之後,生成 dex 文件之前進行插樁,而插樁的作用是在每個方法執行時先去根據自己方法的簽名尋找是否有自己對應的 patch 方法,如果有,執行 patch 方法;如果沒有,則執行自己原有的邏輯。
3、代碼分析
例如 Findbugs
等三方的代碼檢查工具裏面的 自定義代碼檢查 也使用了編譯插樁技術,利用它我們可以找出 不合理的 Hanlder 使用、new Thread 調用、敏感權限調用 等等一系列編碼問題。
二、AspectJ 的優勢與侷限性
最常用的字節碼處理框架有 AspectJ、ASM
等等,它們的相同之處在於輸入輸出都是 Class
文件。並且,它們都是 在 Java 文件編譯成 .class 文件之後,生成 Dalvik 字節碼之前執行。
而 AspectJ 作爲 Java 中流行的 AOP(aspect-oriented programming) 編程擴展框架,其內部使用的是 BCEL框架 來完成其功能。下面,我們就來了解下 AspectJ 具備哪些優勢。
1、AspectJ 的優勢
它的優勢有兩點:成熟穩定、使用非常簡單。
1、成熟穩定
字節碼的處理並不簡單,特別是 針對於字節碼的格式和各種指令規則,如果處理出錯,就會導致程序編譯或者運行過程中出現問題。而 AspectJ 作爲從 2001 年發展至今的框架,它已經發展地非常成熟,通常不用考慮插入的字節碼發生正確性相關的問題。
2、使用非常簡單
AspectJ
的使用非常簡單,並且它的功能非常強大,我們完全不需要理解任何 Java
字節碼相關的知識,就可以在很多情況下對字節碼進行操控。例如,它可以在如下五個位置插入自定義的代碼:
- 1)、在方法(包括構造方法)被調用的位置。
- 2)、在方法體(包括構造方法)的內部。
- 3)、在讀寫變量的位置。
- 4)、在靜態代碼塊內部。
- 5)、在異常處理的位置的前後。
此外,它也可以 直接將原位置的代碼替換爲自定義的代碼。
2、AspectJ 的缺陷
而 AspectJ
的缺點可以歸結爲如下 三點:
1、切入點固定
AspectJ 只能在一些固定的切入點來進行操作,如果想要進行更細緻的操作則很難實現,它無法針對一些特定規則的字節碼序列做操作。
2、正則表達式的侷限性
AspectJ
的匹配規則採用了類似正則表達式的規則,比如 匹配 Activity 生命週期的 onXXX 方法,如果有自定義的其他以 on 開頭的方法也會匹配到,這樣匹配的正確性就無法滿足。
3、性能較低
AspectJ
在實現時會包裝自己一些特定的類,它並不會直接把 Trace
函數直接插入到代碼中,而是經過一系列自己的封裝。這樣不僅生成的字節碼比較大,而且對原函數的性能會有不小的影響。如果想對 App 中所有的函數都進行插樁,性能影響肯定會比較大。如果你只插樁一小部分函數,那麼 AspectJ 帶來的性能損耗幾乎可以忽略不計。
三、AspectJ 核心語法簡介
AspectJ
其實就是一種 AOP 框架,AOP 是實現程序功能統一維護的一種技術。利用 AOP
可以對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合性降低,提高程序的可重用性,同時大大提高了開發效率。因此 AOP
的優勢可總結爲如下 兩點:
- 1)、無侵入性。
- 2)、修改方便。
此外,AOP 不同於 OOP 將問題劃分到單個模塊之中,它把 涉及到衆多模塊的同一類問題進行了統一處理。比如我們可以設計兩個切面,一個是用於處理 App 中所有模塊的日誌輸出功能,另外一個則是用於處理 App 中一些特殊函數調用的權限檢查。
下面👇,我們就來看看要掌握 AspectJ 的使用,我們需要了解的一些 核心概念。
1、橫切關注點
對哪些方法進行攔截,攔截後怎麼處理。
2、切面(Aspect)
類是對物體特徵的抽象,切面就是對橫切關注點的抽象。
3、連接點(JoinPoint)
JPoint 是一個程序的關鍵執行點,也是我們關注的重點。它就是指被攔截到的點(如方法、字段、構造器等等)。
4、切入點(PointCut)
對 JoinPoint 進行攔截的定義。PointCut 的目的就是提供一種方法使得開發者能夠選擇自己感興趣的 JoinPoint。
5、通知(Advice)
切入點僅用於捕捉連接點集合,但是,除了捕捉連接點集合以外什麼事情都沒有做。事實上實現橫切行爲我們要使用通知。它 一般指攔截到 JoinPoint 後要執行的代碼,分爲 前置、後置、環繞 三種類型。這裏,我們需要注意 Advice Precedence(優先權) 的情況,比如我們對同一個切面方法同時使用了 @Before 和 @Around 時就會報錯,此時會提示需要設置 Advice 的優先級。
AspectJ 作爲一種基於 Java 語言實現的一套面向切面程序設計規範。它向 Java
中加入了 連接點(Join Point)
這個新概念,其實它也只是現存的一個 Java
概 唸的名稱而已。它向 Java
語言中加入了少許新結構,譬如 切入點(pointcut)、通知(Advice)、類型間聲明(Inter-type declaration) 和 切面(Aspect)
。切入點和通知動態地影響程序流程,類型間聲明則是 靜態的影響程序的類等級結構,而切面則是對所有這些新結構的封裝。
對於 AsepctJ 中的各個核心概念來說,其 連接點就恰如程序流中適當的一點。而切入點收集特定的連接點集合和在這些點中的值。一個通知則是當一個連接點到達時執行的代碼,這些都是 AspectJ 的動態部分。其實連接點就好比是 程序中那一條一條的語句,而切入點就是特定一條語句處設置的一個斷點,它收集了斷點處程序棧的信息,而通知就是在這個斷點前後想要加入的程序代碼
。
此外,AspectJ
中也有許多不同種類的類型間聲明,這就允許程序員修改程序的靜態結構、名稱、類的成員以及類之間的關係。 AspectJ
中的切面是橫切關注點的模塊單元。它們的行爲與 Java
語言中的類很象,但是切面 還封裝了切入點、通知以及類型間聲明。
在 Android
平臺上要使用 AspectJ
還是有點麻煩的,這裏我們可以直接使用滬江的 AspectJX 框架。下面,我們就來使用 AspectJX
進行 AOP
切面編程。
四、AspectJX 實戰
首先,爲了在 Android
使用 AOP
埋點需要引入 AspectJX
,在項目根目錄的 build.gradle
下加入:
classpath 'com.hujiang.aspectjx:gradle-android-plugin- aspectjx:2.0.0'
然後,在 app
目錄下的 build.gradle
下加入:
apply plugin: 'android-aspectjx'
implement 'org.aspectj:aspectjrt:1.8.+'
JoinPoint
一般定位在如下位置:
- 1)、函數調用。
- 2)、獲取、設置變量。
- 3)、類初始化。
使用 PointCut 對我們指定的連接點進行攔截,通過 Advice,就可以攔截到 JoinPoint 後要執行的代碼。Advice 通常有以下 三種類型:
- 1)、Before:PointCut 之前執行。
- 2)、After:PointCut 之後執行。
- 3)、Around:PointCut 之前、之後分別執行。
1、最簡單的 AspectJ 示例
首先,我們舉一個 小栗子
🌰:
@Before("execution(* android.app.Activity.on**(..))")
public void onActivityCalled(JoinPoint joinPoint) throws Throwable {
Log.d(...)
}
其中,在 execution 中的是一個匹配規則,第一個 * 代表匹配任意的方法返回值,後面的語法代碼匹配所有 Activity 中以 on 開頭的方法。這樣,我們就可以 在 App 中所有 Activity 中以 on 開頭的方法中輸出一句 log。
上面的 execution 就是處理 Join Point 的類型,通常有如下兩種類型:
- 1)、call:代表調用方法的位置,插入在函數體外面。
- 2)、execution:代表方法執行的位置,插入在函數體內部。
2、統計 Application 中所有方法的耗時
那麼,我們如何利用它統計 Application
中的所有方法耗時呢?
@Aspect
public class ApplicationAop {
@Around("call (* com.json.chao.application.BaseApplication.**(..))")
public void getTime(ProceedingJoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
String name = signature.toShortString();
long time = System.currentTimeMillis();
try {
joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
Log.i(TAG, name + " cost" + (System.currentTimeMillis() - time));
}
}
需要注意的是,當 Action 爲 Before、After 時,方法入參爲 JoinPoint。當 Action 爲 Around 時,方法入參爲 ProceedingPoint。
而 Around 和 Before、After 的最大區別就是 ProceedingPoint 不同於 JoinPoint,其提供了 proceed 方法執行目標方法。
3、對 App 中所有的方法進行 Systrace 函數插樁
在 《深入探索 Android 啓動速度優化》 一文中我講到了使用 Systrace
對函數進行插樁,從而能夠查看應用中方法的耗時與 CPU
情況。學習了 AspectJ
之後,我們就可以利用它實現對 App
中所有的方法進行 Systrace
函數插樁了,代碼如下所示:
@Aspect
public class SystraceTraceAspectj {
private static final String TAG = "SystraceTraceAspectj";
@Before("execution(* **(..))")
public void before(JoinPoint joinPoint) {
TraceCompat.beginSection(joinPoint.getSignature().toString());
}
@After("execution(* **(..))")
public void after() {
TraceCompat.endSection();
}
}
瞭解了 AspectJX
的基本使用之後,接下來我們就會使用它和 AspectJ
去打造一個簡易版的 APM(性能監控框架)
。
五、使用 AspectJ 打造自己的性能監控框架
現在,我們將以奇虎360的 ArgusAPM 性能監控框架來全面分析下 AOP 技術在性能監控方面的應用。主要分爲 三個部分:
- 1)、監控應用冷熱啓動耗時與生命週期耗時。
- 2)、監控 OKHttp3 的每一次網絡請求。
- 3)、監控 HttpConnection 的每一次網絡請求。
1、監控應用冷熱啓動耗時與生命週期耗時
在 ArgusAPM
中,實現了 Activity
切面文件 TraceActivity
, 它被用來監控應用冷熱啓動耗時與生命週期耗時,TraceActivity
的實現代碼如下所示:
@Aspect
public class TraceActivity {
// 1、定義一個切入點方法 baseCondition,用於排除 argusapm 中相應的類。
@Pointcut("!within(com.argusapm.android.aop.*) && !within(com.argusapm.android.core.job.activity.*)")
public void baseCondition() {
}
// 2、定義一個切入點 applicationOnCreate,用於執行 Application 的 onCreate方法。
@Pointcut("execution(* android.app.Application.onCreate(android.content.Context)) && args(context)")
public void applicationOnCreate(Context context) {
}
// 3、定義一個後置通知 applicationOnCreateAdvice,用於在 application 的 onCreate 方法執行完之後插入 AH.applicationOnCreate(context) 這行代碼。
@After("applicationOnCreate(context)")
public void applicationOnCreateAdvice(Context context) {
AH.applicationOnCreate(context);
}
// 4、定義一個切入點,用於執行 Application 的 attachBaseContext 方法。
@Pointcut("execution(* android.app.Application.attachBaseContext(android.content.Context)) && args(context)")
public void applicationAttachBaseContext(Context context) {
}
// 5、定義一個前置通知,用於在 application 的 onAttachBaseContext 方法之前插入 AH.applicationAttachBaseContext(context) 這行代碼。
@Before("applicationAttachBaseContext(context)")
public void applicationAttachBaseContextAdvice(Context context) {
AH.applicationAttachBaseContext(context);
}
// 6、定義一個切入點,用於執行所有 Activity 中以 on 開頭的方法,後面的 ”&& baseCondition()“ 是爲了排除 ArgusAPM 中的類。
@Pointcut("execution(* android.app.Activity.on**(..)) && baseCondition()")
public void activityOnXXX() {
}
// 7、定義一個環繞通知,用於在所有 Activity 的 on 開頭的方法中的開始和結束處插入相應的代碼。(排除了 ArgusAPM 中的類)
@Around("activityOnXXX()")
public Object activityOnXXXAdvice(ProceedingJoinPoint proceedingJoinPoint) {
Object result = null;
try {
Activity activity = (Activity) proceedingJoinPoint.getTarget();
// Log.d("AJAOP", "Aop Info" + activity.getClass().getCanonicalName() +
// "\r\nkind : " + thisJoinPoint.getKind() +
// "\r\nargs : " + thisJoinPoint.getArgs() +
// "\r\nClass : " + thisJoinPoint.getClass() +
// "\r\nsign : " + thisJoinPoint.getSignature() +
// "\r\nsource : " + thisJoinPoint.getSourceLocation() +
// "\r\nthis : " + thisJoinPoint.getThis()
// );
long startTime = System.currentTimeMillis();
result = proceedingJoinPoint.proceed();
String activityName = activity.getClass().getCanonicalName();
Signature signature = proceedingJoinPoint.getSignature();
String sign = "";
String methodName = "";
if (signature != null) {
sign = signature.toString();
methodName = signature.getName();
}
if (!TextUtils.isEmpty(activityName) && !TextUtils.isEmpty(sign) && sign.contains(activityName)) {
invoke(activity, startTime, methodName, sign);
}
} catch (Exception e) {
e.printStackTrace();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return result;
}
public void invoke(Activity activity, long startTime, String methodName, String sign) {
AH.invoke(activity, startTime, methodName, sign);
}
}
我們注意到,在註釋4、5這兩處代碼是用於 在 application 的 onAttachBaseContext 方法之前插入 AH.applicationAttachBaseContext(context) 這行代碼。此外,註釋2、3兩處的代碼是用於 在 application 的 onCreate 方法執行完之後插入 AH.applicationOnCreate(context) 這行代碼。下面,我們再看看 AH
類中這兩個方法的實現,代碼如下所示:
public static void applicationAttachBaseContext(Context context) {
ActivityCore.appAttachTime = System.currentTimeMillis();
if (Env.DEBUG) {
LogX.d(Env.TAG, SUB_TAG, "applicationAttachBaseContext time : " + ActivityCore.appAttachTime);
}
}
public static void applicationOnCreate(Context context) {
if (Env.DEBUG) {
LogX.d(Env.TAG, SUB_TAG, "applicationOnCreate");
}
}
可以看到,在 AH 類的 applicationAttachBaseContext 方法中將啓動時間 appAttachTime 記錄到了 ActivityCore 實例中。而 applicationOnCreate 基本上什麼也沒有實現。
然後,我們再回到切面文件 TraceActivity 中,看到註釋6、7處的代碼,這裏用於 在所有 Activity 的 on 開頭的方法中的開始和結束處插入相應的代碼。需要注意的是,這裏 排除了 ArgusAPM 中的類。
下面,我們來分析下 activityOnXXXAdvice
方法中的操作。首先,在目標方法執行前獲取了 startTime。然後,調用了 proceedingJoinPoint.proceed() 用於執行目標方法;最後,調用了 AH 類的 invoke 方法。我們看看 invoke
方法的處理,代碼如下所示:
public static void invoke(Activity activity, long startTime, String lifeCycle, Object... extars) {
// 1
boolean isRunning = isActivityTaskRunning();
if (Env.DEBUG) {
LogX.d(Env.TAG, SUB_TAG, lifeCycle + " isRunning : " + isRunning);
}
if (!isRunning) {
return;
}
// 2
if (TextUtils.equals(lifeCycle, ActivityInfo.TYPE_STR_ONCREATE)) {
ActivityCore.onCreateInfo(activity, startTime);
} else {
// 3
int lc = ActivityInfo.ofLifeCycleString(lifeCycle);
if (lc <= ActivityInfo.TYPE_UNKNOWN || lc > ActivityInfo.TYPE_DESTROY) {
return;
}
ActivityCore.saveActivityInfo(activity, ActivityInfo.HOT_START, System.currentTimeMillis() - startTime, lc);
}
}
首先,在註釋1處,我們會先去查看當前應用的 Activity
耗時統計任務是否打開了。如果打開了,然後就會走到註釋2處,這裏 會先判斷目標方法名稱是否是 “onCreate”,如果是 onCreate 方法,就會執行 ActivityCore 的 onCreateInfo 方法,代碼如下所示:
// 是否是第一次啓動
public static boolean isFirst = true;
public static long appAttachTime = 0;
// 啓動類型
public static int startType;
public static void onCreateInfo(Activity activity, long startTime) {
// 1
startType = isFirst ? ActivityInfo.COLD_START : ActivityInfo.HOT_START;
// 2
activity.getWindow().getDecorView().post(new FirstFrameRunnable(activity, startType, startTime));
//onCreate 時間
long curTime = System.currentTimeMillis();
// 3
saveActivityInfo(activity, startType, curTime - startTime, ActivityInfo.TYPE_CREATE);
}
首先,在註釋1處,會 記錄此時的啓動類型,第一次默認是冷啓動。然後在註釋2處,當第一幀顯示時會 post 一個 Runnable。最後,在註釋3處,會 調用 saveActivityInfo 將目標方法相關的信息保存起來。這裏我們先看看這個 FirstFrameRunnable
的 run
方法的實現代碼,如下所示:
@Override
public void run() {
if (DEBUG) {
LogX.d(TAG, SUB_TAG, "FirstFrameRunnable time:" + (System.currentTimeMillis() - startTime));
}
// 1
if ((System.currentTimeMillis() - startTime) >= ArgusApmConfigManager.getInstance().getArgusApmConfigData().funcControl.activityFirstMinTime) {
saveActivityInfo(activity, startType, System.currentTimeMillis() - startTime, ActivityInfo.TYPE_FIRST_FRAME);
}
if (DEBUG) {
LogX.d(TAG, SUB_TAG, "FirstFrameRunnable time:" + String.format("[%s, %s]", ActivityCore.isFirst, ActivityCore.appAttachTime));
}
if (ActivityCore.isFirst) {
ActivityCore.isFirst = false;
if (ActivityCore.appAttachTime <= 0) {
return;
}
// 2
int t = (int) (System.currentTimeMillis() - ActivityCore.appAttachTime);
AppStartInfo info = new AppStartInfo(t);
ITask task = Manager.getInstance().getTaskManager().getTask(ApmTask.TASK_APP_START);
if (task != null) {
// 3
task.save(info);
if (AnalyzeManager.getInstance().isDebugMode()) {
// 4
AnalyzeManager.getInstance().getParseTask(ApmTask.TASK_APP_START).parse(info);
}
} else {
if (DEBUG) {
LogX.d(TAG, SUB_TAG, "AppStartInfo task == null");
}
}
}
}
}
首先,在註釋1處,會計算出當前的 第一幀的時間,即 當前 Activity 的冷啓動時間,將它與 activityFirstMinTime 這個值作比較(activityFirstMinTime 的值默認爲300ms),如果 Activity 的冷啓動時間大於300ms的話,就會將冷啓動時間調用 saveActivityInfo 方法保存起來。
然後,在註釋2處,我們會 記錄 App 的啓動時間 並在註釋3處將它 保存到 AppStartTask 這個任務實例中。最後,在註釋4處,如果是 debug 模式,則會調用 AnalyzeManager 這個數據分析管理單例類的 getParseTask 方法獲取 AppStartParseTask 這個實例,關鍵代碼如下所示:
private Map<String, IParser> mParsers;
private AnalyzeManager() {
mParsers = new HashMap<String, IParser>(3);
mParsers.put(ApmTask.TASK_ACTIVITY, new ActivityParseTask());
mParsers.put(ApmTask.TASK_NET, new NetParseTask());
mParsers.put(ApmTask.TASK_FPS, new FpsParseTask());
mParsers.put(ApmTask.TASK_APP_START, new AppStartParseTask());
mParsers.put(ApmTask.TASK_MEM, new MemoryParseTask());
this.isUiProcess = Manager.getContext().getPackageName().equals(ProcessUtils.getCurrentProcessName());
}
public IParser getParseTask(String name) {
if (TextUtils.isEmpty(name)) {
return null;
}
return mParsers.get(name);
}
接着,就會調用 AppStartParseTask
類的 parse
方法,可以看出,它是一個 專門用於在 Debug 模式下的應用啓動時間分析類。parse
方法的代碼如下所示:
/**
* app啓動
*
* @param info
*/
@Override
public boolean parse(IInfo info) {
if (info instanceof AppStartInfo) {
AppStartInfo aInfo = (AppStartInfo) info;
if (aInfo == null) {
return false;
}
try {
JSONObject obj = aInfo.toJson();
obj.put("taskName", ApmTask.TASK_APP_START);
// 1
OutputProxy.output("啓動時間:" + aInfo.getStartTime(), obj.toString());
} catch (JSONException e) {
e.printStackTrace();
}
DebugFloatWindowUtls.sendBroadcast(aInfo);
}
return true;
}
在註釋1處,parse
方法中僅僅是繼續調用了 OutputProxy
的 output
方法 將啓動時間和記錄啓動信息的字符串傳入。我們再看看 OutputProxy
的 output
方法,如下所示:
/**
* 警報信息輸出
*
* @param showMsg
*/
public static void output(String showMsg) {
if (!AnalyzeManager.getInstance().isDebugMode()) {
return;
}
if (TextUtils.isEmpty(showMsg)) {
return;
}
// 1、存儲在本地
StorageManager.saveToFile(showMsg);
}
註釋1處,在 output
方法中又繼續調用了 StorageManager
的 saveToFile
方法 將啓動信息存儲在本地,saveToFile
的實現代碼如下所示:
/**
* 按行保存到文本文件
*
* @param line
*/
public static void saveToFile(String line) {
TraceWriter.log(Env.TAG, line);
}
這裏又調用了 TraceWriter
的 log
方法 將啓動信息按行保存到文本文件中,關鍵代碼如下所示:
public static void log(String tagName, String content) {
log(tagName, content, true);
}
private synchronized static void log(String tagName, String content, boolean forceFlush) {
if (Env.DEBUG) {
LogX.d(Env.TAG, SUB_TAG, "tagName = " + tagName + " content = " + content);
}
if (sWriteThread == null) {
// 1
sWriteThread = new WriteFileRun();
Thread t = new Thread(sWriteThread);
t.setName("ApmTrace.Thread");
t.setDaemon(true);
t.setPriority(Thread.MIN_PRIORITY);
t.start();
String initContent = "---- Phone=" + Build.BRAND + "/" + Build.MODEL + "/verName:" + " ----";
// 2
sQueuePool.offer(new Object[]{tagName, initContent, Boolean.valueOf(forceFlush)});
if (Env.DEBUG) {
LogX.d(Env.TAG, SUB_TAG, "init offer content = " + content);
}
}
if (Env.DEBUG) {
LogX.d(Env.TAG, SUB_TAG, "offer content = " + content);
}
// 3
sQueuePool.offer(new Object[]{tagName, content, Boolean.valueOf(forceFlush)});
synchronized (LOCKER_WRITE_THREAD) {
LOCKER_WRITE_THREAD.notify();
}
}
在註釋1處,如果 sWriteThread 這個負責寫入 log 信息的 Runnable 不存在,就會新建並啓動這個寫入 log 信息的低優先級守護線程。
然後,會在註釋2處,調用 sQueuePool 的 offer 方法將相關的信息保存,它的類型爲 ConcurrentLinkedQueue,說明它是一個專用於併發環境下的隊列。如果 Runnable 已經存在了的話,就直接會在註釋3處將 log 信息入隊。最終,會在 sWriteThread 的 run 方法中調用 sQueuePool 的 poll() 方法將 log 信息拿出並通過 BufferWriter 封裝的 FileWriter 將信息保存在本地。
到此,我們就分析完了 onCreate
方法的處理,接着我們再回到 invoke
方法的註釋3處來分析不是 onCreate
方法的情況。如果方法名不是 onCreate 方法的話,就會調用 ActivityInfo 的 ofLifeCycleString 方法,我們看看它的實現,如下所示:
/**
* 生命週期字符串轉換成數值
*
* @param lcStr
* @return
*/
public static int ofLifeCycleString(String lcStr) {
int lc = 0;
if (TextUtils.equals(lcStr, TYPE_STR_FIRSTFRAME)) {
lc = TYPE_FIRST_FRAME;
} else if (TextUtils.equals(lcStr, TYPE_STR_ONCREATE)) {
lc = TYPE_CREATE;
} else if (TextUtils.equals(lcStr, TYPE_STR_ONSTART)) {
lc = TYPE_START;
} else if (TextUtils.equals(lcStr, TYPE_STR_ONRESUME)) {
lc = TYPE_RESUME;
} else if (TextUtils.equals(lcStr, TYPE_STR_ONPAUSE)) {
lc = TYPE_PAUSE;
} else if (TextUtils.equals(lcStr, TYPE_STR_ONSTOP)) {
lc = TYPE_STOP;
} else if (TextUtils.equals(lcStr, TYPE_STR_ONDESTROY)) {
lc = TYPE_DESTROY;
}
return lc;
}
可以看到,ofLifeCycleString 的作用就是將生命週期字符串轉換成相應的數值,下面是它們的定義代碼:
/**
* Activity 生命週期類型枚舉
*/
public static final int TYPE_UNKNOWN = 0;
public static final int TYPE_FIRST_FRAME = 1;
public static final int TYPE_CREATE = 2;
public static final int TYPE_START = 3;
public static final int TYPE_RESUME = 4;
public static final int TYPE_PAUSE = 5;
public static final int TYPE_STOP = 6;
public static final int TYPE_DESTROY = 7;
/**
* Activity 生命週期類型值對應的名稱
*/
public static final String TYPE_STR_FIRSTFRAME = "firstFrame";
public static final String TYPE_STR_ONCREATE = "onCreate";
public static final String TYPE_STR_ONSTART = "onStart";
public static final String TYPE_STR_ONRESUME = "onResume";
public static final String TYPE_STR_ONPAUSE = "onPause";
public static final String TYPE_STR_ONSTOP = "onStop";
public static final String TYPE_STR_ONDESTROY = "onDestroy";
public static final String TYPE_STR_UNKNOWN = "unKnown";
然後,我們再回到 AH
類的 invoke
方法的註釋3處,僅僅當方法名是上述定義的方法,也就是 Acitivity 的生命週期方法或第一幀的方法時,纔會調用 ActivityCore 的 saveActivityInfo 方法。該方法的實現代碼如下所示:
public static void saveActivityInfo(Activity activity, int startType, long time, int lifeCycle) {
if (activity == null) {
if (DEBUG) {
LogX.d(TAG, SUB_TAG, "saveActivityInfo activity == null");
}
return;
}
if (time < ArgusApmConfigManager.getInstance().getArgusApmConfigData().funcControl.activityLifecycleMinTime) {
return;
}
String pluginName = ExtraInfoHelper.getPluginName(activity);
String activityName = activity.getClass().getCanonicalName();
activityInfo.resetData();
activityInfo.activityName = activityName;
activityInfo.startType = startType;
activityInfo.time = time;
activityInfo.lifeCycle = lifeCycle;
activityInfo.pluginName = pluginName;
activityInfo.pluginVer = ExtraInfoHelper.getPluginVersion(pluginName);
if (DEBUG) {
LogX.d(TAG, SUB_TAG, "apmins saveActivityInfo activity:" + activity.getClass().getCanonicalName() + " | lifecycle : " + activityInfo.getLifeCycleString() + " | time : " + time);
}
ITask task = Manager.getInstance().getTaskManager().getTask(ApmTask.TASK_ACTIVITY);
boolean result = false;
if (task != null) {
result = task.save(activityInfo);
} else {
if (DEBUG) {
LogX.d(TAG, SUB_TAG, "saveActivityInfo task == null");
}
}
if (DEBUG) {
LogX.d(TAG, SUB_TAG, "activity info:" + activityInfo.toString());
}
if (AnalyzeManager.getInstance().isDebugMode()) {
AnalyzeManager.getInstance().getActivityTask().parse(activityInfo);
}
if (Env.DEBUG) {
LogX.d(TAG, SUB_TAG, "saveActivityInfo result:" + result);
}
}
可以看到,這裏的邏輯很簡單,僅僅是 將 log 信息保存在 ActivityInfo 這個實例中,並將 ActivityInfo 實例保存在了 ActivityTask 中,需要注意的是,在調用 ArgusAPM.init() 這句初始化代碼時就已經將 ActivityTask 實例保存在了 taskMap 這個 HashMap 對象中 了,關鍵代碼如下所示:
/**
* 註冊 task:每添加一個task都要進行註冊,也就是把
* 相應的 xxxTask 實例放入 taskMap 集合中。
*/
public void registerTask() {
if (Env.DEBUG) {
LogX.d(Env.TAG, "TaskManager", "registerTask " + getClass().getClassLoader());
}
if (Build.VERSION.SDK_INT >= 16) {
taskMap.put(ApmTask.TASK_FPS, new FpsTask());
}
taskMap.put(ApmTask.TASK_MEM, new MemoryTask());
taskMap.put(ApmTask.TASK_ACTIVITY, new ActivityTask());
taskMap.put(ApmTask.TASK_NET, new NetTask());
taskMap.put(ApmTask.TASK_APP_START, new AppStartTask());
taskMap.put(ApmTask.TASK_ANR, new AnrLoopTask(Manager.getContext()));
taskMap.put(ApmTask.TASK_FILE_INFO, new FileInfoTask());
taskMap.put(ApmTask.TASK_PROCESS_INFO, new ProcessInfoTask());
taskMap.put(ApmTask.TASK_BLOCK, new BlockTask());
taskMap.put(ApmTask.TASK_WATCHDOG, new WatchDogTask());
}
接着,我們再看看 ActivityTask
類的實現,如下所示:
public class ActivityTask extends BaseTask {
@Override
protected IStorage getStorage() {
return new ActivityStorage();
}
@Override
public String getTaskName() {
return ApmTask.TASK_ACTIVITY;
}
@Override
public void start() {
super.start();
if (Manager.getInstance().getConfig().isEnabled(ApmTask.FLAG_COLLECT_ACTIVITY_INSTRUMENTATION) && !InstrumentationHooker.isHookSucceed()) {//hook失敗
if (DEBUG) {
LogX.d(TAG, "ActivityTask", "canWork hook : hook失敗");
}
mIsCanWork = false;
}
}
@Override
public boolean isCanWork() {
return mIsCanWork;
}
}
可以看到,這裏並沒有看到 save
方法,說明是在基類 BaseTask
類中,繼續看到 BaseTask
類的實現代碼:
/**
* ArgusAPM任務基類
*
* @author ArgusAPM Team
*/
public abstract class BaseTask implements ITask {
...
@Override
public boolean save(IInfo info) {
if (DEBUG) {
LogX.d(TAG, SUB_TAG, "save task :" + getTaskName());
}
// 1
return info != null && mStorage != null && mStorage.save(info);
}
...
}
在註釋1處,繼續調用了 mStorage 的 save 方法,它是一個接口 IStorage,很顯然,這裏的實現類是在 ActivityTask 的 getStorage() 方法中返回的 ActivityStorage 實例,它是一個 Activity 存儲類,專門負責處理 Activity 的信息。到此,監控應用冷熱啓動耗時與生命週期耗時的部分就分析完畢了。
下面,我們再看看如何使用 AspectJ
監控 OKHttp3
的每一次網絡請求。
2、監控 OKHttp3 的每一次網絡請求
首先,我們看到 OKHttp3
的切面文件,代碼如下所示:
/**
* OKHTTP3 切面文件
*
* @author ArgusAPM Team
*/
@Aspect
public class OkHttp3Aspect {
// 1、定義一個切入點,用於直接調用 OkHttpClient 的 build 方法。
@Pointcut("call(public okhttp3.OkHttpClient build())")
public void build() {
}
// 2、使用環繞通知在 build 方法執行前添加一個 NetWokrInterceptor。
@Around("build()")
public Object aroundBuild(ProceedingJoinPoint joinPoint) throws Throwable {
Object target = joinPoint.getTarget();
if (target instanceof OkHttpClient.Builder && Client.isTaskRunning(ApmTask.TASK_NET)) {
OkHttpClient.Builder builder = (OkHttpClient.Builder) target;
builder.addInterceptor(new NetWorkInterceptor());
}
return joinPoint.proceed();
}
}
在註釋1、2處,在調用 OkHttpClient 的 build 方法之前添加了一個 NetWokrInterceptor。我們看看它的實現代碼,如下所示:
@Override
public Response intercept(Chain chain) throws IOException {
// 1、獲取每一個 OkHttp 請求的開始時間
long startNs = System.currentTimeMillis();
mOkHttpData = new OkHttpData();
mOkHttpData.startTime = startNs;
if (Env.DEBUG) {
Log.d(TAG, "okhttp request 開始時間:" + mOkHttpData.startTime);
}
Request request = chain.request();
// 2、記錄當前請求的請求 url 和請求數據大小
recordRequest(request);
Response response;
try {
response = chain.proceed(request);
} catch (IOException e) {
if (Env.DEBUG) {
e.printStackTrace();
Log.e(TAG, "HTTP FAILED: " + e);
}
throw e;
}
// 3、記錄這次請求花費的時間
mOkHttpData.costTime = System.currentTimeMillis() - startNs;
if (Env.DEBUG) {
Log.d(TAG, "okhttp chain.proceed 耗時:" + mOkHttpData.costTime);
}
// 4、記錄當前請求返回的響應碼和響應數據大小
recordResponse(response);
if (Env.DEBUG) {
Log.d(TAG, "okhttp chain.proceed end.");
}
// 5、記錄 OkHttp 的請求數據
DataRecordUtils.recordUrlRequest(mOkHttpData);
return response;
}
首先,在註釋1處,獲取了每一個 OkHttp 請求的開始時間。接着,在註釋2處,通過 recordRequest 方法記錄了當前請求的請求 url 和請求數據大小。然後,註釋3處,記錄了這次 請求所花費的時間。
接下來,在註釋4處,通過 recordResponse 方法記錄了當前請求返回的響應碼和響應數據大小。最後,在註釋5處,調用了 DataRecordUtils 的 recordUrlRequest 方法記錄了 mOkHttpData 中保存好的數據。我們繼續看到 recordUrlRequest
方法,代碼如下所示:
/**
* recordUrlRequest
*
* @param okHttpData
*/
public static void recordUrlRequest(OkHttpData okHttpData) {
if (okHttpData == null || TextUtils.isEmpty(okHttpData.url)) {
return;
}
QOKHttp.recordUrlRequest(okHttpData.url, okHttpData.code, okHttpData.requestSize,
okHttpData.responseSize, okHttpData.startTime, okHttpData.costTime);
if (Env.DEBUG) {
Log.d(Env.TAG, "存儲okkHttp請求數據,結束。");
}
}
可以看到,這裏調用了 QOKHttp 的 recordUrlRequest 方法用於記錄網絡請求信息。我們再看到 QOKHttp
的 recordUrlRequest
方法,如下所示:
/**
* 記錄一次網絡請求
*
* @param url 請求url
* @param code 狀態碼
* @param requestSize 發送的數據大小
* @param responseSize 接收的數據大小
* @param startTime 發起時間
* @param costTime 耗時
*/
public static void recordUrlRequest(String url, int code, long requestSize, long responseSize,
long startTime, long costTime) {
NetInfo netInfo = new NetInfo();
netInfo.setStartTime(startTime);
netInfo.setURL(url);
netInfo.setStatusCode(code);
netInfo.setSendBytes(requestSize);
netInfo.setRecordTime(System.currentTimeMillis());
netInfo.setReceivedBytes(responseSize);
netInfo.setCostTime(costTime);
netInfo.end();
}
可以看到,這裏 將網絡請求信息保存在了 NetInfo 中,並最終調用了 netInfo 的 end 方法,代碼如下所示:
/**
* 爲什存儲的操作要寫到這裏呢?
* 歷史原因
*/
public void end() {
if (DEBUG) {
LogX.d(TAG, SUB_TAG, "end :");
}
this.isWifi = SystemUtils.isWifiConnected();
this.costTime = System.currentTimeMillis() - startTime;
if (AnalyzeManager.getInstance().isDebugMode()) {
AnalyzeManager.getInstance().getNetTask().parse(this);
}
ITask task = Manager.getInstance().getTaskManager().getTask(ApmTask.TASK_NET);
if (task != null) {
// 1
task.save(this);
} else {
if (DEBUG) {
LogX.d(TAG, SUB_TAG, "task == null");
}
}
}
可以看到,這裏 最終還是調用了 NetTask 實例的 save 方法保存網絡請求的信息。而 NetTask 肯定是使用了與之對應的 NetStorage 實例將信息保存在了 ContentProvider 中。至此,OkHttp3
這部分的分析就結束了。
對於使用 OkHttp3
的應用來說,上述的實現可以有效地獲取網絡請求的信息,但是如果應用沒有使用 OkHttp3
呢?這個時候,我們就只能去監控 HttpConnection
的每一次網絡請求。下面,我們就看看如何去實現它。
3、監控 HttpConnection 和 HttPClient 的每一次網絡請求
在 ArgusAPM
中,使用的是 TraceNetTrafficMonitor
這個切面類對 HttpConnection
的每一次網絡請求進行監控。關鍵代碼如下所示:
@Aspect
public class TraceNetTrafficMonitor {
// 1
@Pointcut("(!within(com.argusapm.android.aop.*) && ((!within(com.argusapm.android.**) && (!within(com.argusapm.android.core.job.net.i.*) && (!within(com.argusapm.android.core.job.net.impl.*) && (!within(com.qihoo360.mobilesafe.mms.transaction.MmsHttpClient) && !target(com.qihoo360.mobilesafe.mms.transaction.MmsHttpClient)))))))")
public void baseCondition() {
}
// 2
@Pointcut("call(org.apache.http.HttpResponse org.apache.http.client.HttpClient.execute(org.apache.http.client.methods.HttpUriRequest)) && (target(httpClient) && (args(request) && baseCondition()))")
public void httpClientExecuteOne(HttpClient httpClient, HttpUriRequest request) {
}
// 3
@Around("httpClientExecuteOne(httpClient, request)")
public HttpResponse httpClientExecuteOneAdvice(HttpClient httpClient, HttpUriRequest request) throws IOException {
return QHC.execute(httpClient, request);
}
// 排查一些處理異常的切面代碼
// 4
@Pointcut("call(java.net.URLConnection openConnection()) && (target(url) && baseCondition())")
public void URLOpenConnectionOne(URL url) {
}
// 5
@Around("URLOpenConnectionOne(url)")
public URLConnection URLOpenConnectionOneAdvice(URL url) throws IOException {
return QURL.openConnection(url);
}
// 排查一些處理異常的切面代碼
}
TraceNetTrafficMonitor
裏面的操作分爲 兩類,一類是用於切 HttpClient 的 execute 方法,即註釋1、2、3處所示的切面代碼;一類是用於切 HttpConnection 的 openConnection 方法,對應的切面代碼爲註釋4、5處。我們首先分析 HttpClient
的情況,這裏最終 調用了 QHC 的 execute 方法進行處理,如下所示:
public static HttpResponse execute(HttpClient client, HttpUriRequest request) throws IOException {
return isTaskRunning()
? AopHttpClient.execute(client, request)
: client.execute(request);
}
這裏又 繼續調用了 AopHttpClient 的 execute 方法,代碼如下所示:
public static HttpResponse execute(HttpClient httpClient, HttpUriRequest request) throws IOException {
NetInfo data = new NetInfo();
// 1
HttpResponse response = httpClient.execute(handleRequest(request, data));
// 2
handleResponse(response, data);
return response;
}
首先,在註釋1處,調用了 handleRequest 處理請求數據,如下所示:
private static HttpUriRequest handleRequest(HttpUriRequest request, NetInfo data) {
data.setURL(request.getURI().toString());
if (request instanceof HttpEntityEnclosingRequest) {
HttpEntityEnclosingRequest entityRequest = (HttpEntityEnclosingRequest) request;
if (entityRequest.getEntity() != null) {
// 1、將請求實體使用 AopHttpRequestEntity 進行了封裝
entityRequest.setEntity(new AopHttpRequestEntity(entityRequest.getEntity(), data));
}
return (HttpUriRequest) entityRequest;
}
return request;
}
可以看到,在註釋1處,使用 AopHttpRequestEntity 對請求實體進行了封裝,這裏的目的主要是爲了 便於使用封裝實體中的 NetInfo 進行數據操作。接着,在註釋2處,將得到的響應信息進行了處理,這裏的實現很簡單,就是 使用 NetInfo 這個實體類將響應信息保存在了 ContentProvider 中。至此,HttpClient
的處理部分我們就分析完畢了。
下面,我們接着分析下 HTTPConnection
的切面部分代碼,如下所示:
// 4
@Pointcut("call(java.net.URLConnection openConnection()) && (target(url) && baseCondition())")
public void URLOpenConnectionOne(URL url) {
}
// 5
@Around("URLOpenConnectionOne(url)")
public URLConnection URLOpenConnectionOneAdvice(URL url) throws IOException {
return QURL.openConnection(url);
}
可以看到,這裏是 調用了 QURL 的 openConnection 方法進行處理。我們來看看它的實現代碼:
public static URLConnection openConnection(URL url) throws IOException {
return isNetTaskRunning() ? AopURL.openConnection(url) : url.openConnection();
}
這裏 又調用了 AopURL 的 openConnection 方法,繼續
看看它的實現:
public static URLConnection openConnection(URL url) throws IOException {
if (url == null) {
return null;
}
return getAopConnection(url.openConnection());
}
private static URLConnection getAopConnection(URLConnection con) {
if (con == null) {
return null;
}
if (Env.DEBUG) {
LogX.d(TAG, "AopURL", "getAopConnection in AopURL");
}
// 1
if ((con instanceof HttpsURLConnection)) {
return new AopHttpsURLConnection((HttpsURLConnection) con);
}
// 2
if ((con instanceof HttpURLConnection)) {
return new AopHttpURLConnection((HttpURLConnection) con);
}
return con;
}
最終,在註釋1處,會判斷如果是 https 請求,則會使用 AopHttpsURLConnection 封裝 con,如果是 http 請求,則使用 AopHttpURLConnection 進行封裝。AopHttpsURLConnection
的實現與它類似,僅僅是多加了 SSL
證書驗證的部分。所以這裏我們就直接分析一下 AopHttpURLConnection
的實現,這裏面的代碼非常多,就不貼出來了,但是,它的 核心的處理 可以簡述爲如下 兩點:
- 1)、在回調 getHeaderFields()、getInputStream()、getLastModified() 等一系列方法時會調用 inspectAndInstrumentResponse 方法把響應大小和狀態碼保存在 NetInfo 中。
- 2)、在回調 onInputstreamComplete()、onInputstreamError()等方法時,即請求完成或失敗時,此時會直接調用 myData 的 end 方法將網絡響應信息保存在 ContentProvider 中。
至此,ArgusAPM
的 AOP
實現部分就已經全部分析完畢了。
六、總結
最後,我們再來回顧一下本篇文章中我們所學到的知識,如下所示:
- 1、編譯插樁技術的分類與應用場景。
- 1)、APT。
- 2)、AOP。
- 2、AspectJ 的優勢與侷限性。
- 3、AspectJ 核心語法簡介。
- 4、AspectJX 實戰。
- 1)、最簡單的 AspectJ 示例。
- 2)、統計 Application 中所有方法的耗時。
- 3)、對 App 中所有的方法進行 Systrace 函數插樁。
- 5、使用 AspectJ 打造自己的性能監控框架。
- 1)、監控應用冷熱啓動耗時與生命週期耗時。
- 2)、監控 OKHttp3 的每一次網絡請求。
- 3)、監控 HttpConnection 和 HttpClient 的每一次網絡請求。
可以看到,AOP
技術的確很強大,使用 AspectJ
我們能做很多事情,但是,它也有一系列的缺點,比如切入點固定、正則表達式固有的缺陷導致的使用不靈活,此外,它還生成了比較多的包裝代碼。那麼,有沒有更好地實現方式,既能夠在使用上更加地靈活,也能夠避免生成包裝代碼,以減少插樁所帶來的性能損耗呢?沒錯,就是 ASM
,但是它 需要通過操作 JVM 字節碼的方式來進行代碼插樁,入手難度比較大,所以,下篇文章我們將會先深入學習 JVM
字節碼的知識,敬請期待~
參考鏈接:
1、極客時間之Android開發高手課 編譯插樁的三種方法:AspectJ、ASM、ReDex
3、The AspectJ 5 Development Kit Developer’s Notebook
7、AspectJX
8、BCEL框架
9、360 的性能監控框架ArgusAPM中AspectJ的使用
Contanct Me
● 微信:
歡迎關注我的微信:
bcce5360
● 微信羣:
微信羣如果不能掃碼加入,麻煩大家想進微信羣的朋友們,加我微信拉你進羣。
● QQ羣:
2千人QQ羣,Awesome-Android學習交流羣,QQ羣號:959936182, 歡迎大家加入~
About me
-
Email: [email protected]
-
Blog: https://jsonchao.github.io/
-
掘金: https://juejin.im/user/5a3ba9375188252bca050ade