現現在,編譯插樁技術已經深刻 Android
開發中的各個領域,而 AOP
技術正是一種高效實現插樁的模式,它的出現正好給處於黑暗中的咱們帶來了光明,極大地解決了傳統開發過程當中的一些痛點,而 AspectJ
做爲一套基於 Java
語言面向切面的擴展設計規範,可以賦予咱們新的能力。在這篇文章中咱們未來學習如何使用 AspectJ
來進行插樁。本篇內容以下所示:html
面向切面的程序設計 (aspect-oriented programming (AOP))
吸引了不少開發者的目光, 可是如何在編碼中有效地實現這一套設計概念卻並不簡單,幸運的是,早在 2003 年,一套基於 Java
語言面向切面的擴展設計:AspectJ
誕生了。java
不一樣與傳統的 OOP 編程,AspectJ (即 AOP)
的獨特之處在於 發現那些使用傳統編程方法沒法處理得很好的問題。 例如一個要在某些應用中實施安全策略的問題。安全性是貫穿於系統全部模塊間的問題,並且每個模塊都必需要添加安全性才能保證整個應用的安全性,而且安全性模塊自身也須要安全性,很明顯這裏的 安全策略的實施問題就是一個橫切關注點,使用傳統的編程解決此問題很是的困難並且容易產生差錯,這正是 AOP
發揮做用的時候了。android
傳統的面向對象編程中,每一個單元就是一個類,而 相似於安全性這方面的問題,它們通 常不能集中在一個類中處理,由於它們橫跨多個類,這就致使了代碼沒法重用,它們是不可靠和不可繼承的,這樣的編程方式使得可維護性差並且產生了大量的代碼冗餘,這是咱們所不肯意看到的。git
而面向切面編程的出現正好給處於黑暗中的咱們帶來了光明,它針對於這些橫切關注點進行處理,就似面向對象編程處理通常的關注點同樣。程序員
在咱們繼續深刻 AOP
編程以前,咱們有必要先來看看當前編譯插樁技術的分類與應用場景。這樣能讓咱們 從更高的緯度上去理解各個技術點之間的關聯與做用。github
編譯插樁技術具體能夠分爲兩類,以下所示:正則表達式
APT(Annotation Process Tools)
:用於生成 Java 代碼。AOP(Aspect Oriented Programming)
:用於操做字節碼。下面👇,咱們分別來詳細介紹下它們的做用。apache
總所周知,ButterKnife、Dagger、GreenDao、Protocol Buffers
這些經常使用的註解生成框架都會在編譯過程當中生成代碼。而 這種使用 AndroidAnnotation 結合 APT 技術 來生成代碼的時機,是在編譯最開始的時候介入的。而 AOP 是在編譯完成後生成 dex 文件以前的時候,直接經過修改 .class 文件的方式,來直接添加或者修改代碼邏輯的。編程
使用 APT
技術生成 Java
代碼的方式具備以下 兩方面 的優點:json
而對於操做字節碼的方式來講,通常都在 代碼監控、代碼修改、代碼分析 這三個場景有着很普遍的應用。
相對於 Java
代碼生成的方式,操做字節碼的方式有以下 特色:
此外,咱們不只能夠操做 .class
文件的 Java
字節碼,也能夠操做 .dex
文件的 Dalvik
字節碼。下面咱們就來大體瞭解下在以上三類場景中編譯插樁技術具體是如何應用的。
編譯插樁技術除了 不可以實現耗電監控,它可以實現各式各樣的性能監控,例如:網絡數據監控、耗時方法監控、大圖監控、線程監控 等等。
譬如 網絡數據監控 的實現,就是在 網絡層經過 hook 網絡庫方法 和 自動化注入攔截器的形式,實現網絡請求的全過程監控,包括獲取握手時長,首包時間,DNS 耗時,網絡耗時等各個網絡階段的信息。
實現了對網絡請求過程的監控以後,咱們即可以 對整個網絡過程的數據表現進行詳細地分析,找到網絡層面性能的問題點,並作出針對性地優化措施。例如針對於 網絡錯誤率偏高
的問題,咱們能夠採起如下幾方面的措施,以下所示:
用編譯插樁技術來實現代碼修改的場景很是之多,而使用最爲頻繁的場景具體可細分爲爲以下四種:
實現無痕埋點
:如網易HubbleData之Android無埋點實踐、51 信用卡 Android 自動埋點實踐 。統一處理點擊抖動
:編譯階段統一 hook android.view.View.OnClickListener#onClick() 方法,來實現一個快速點擊無效的防抖動效果,這樣便能高效、無侵入性地統一解決客戶端快速點擊屢次致使頻繁響應的問題。第三方 SDK 的容災處理
:咱們能夠在上線前臨時修改或者 hook 第三方 SDK 的方法,作到快速容災上線。實現熱修復框架
:咱們能夠在 Gradle 進行自動化構建的時候,即在 Java 源碼編譯完成以後,生成 dex 文件以前進行插樁,而插樁的做用是在每一個方法執行時先去根據本身方法的簽名尋找是否有本身對應的 patch 方法,若是有,執行 patch 方法;若是沒有,則執行本身原有的邏輯。例如 Findbugs
等三方的代碼檢查工具裏面的 自定義代碼檢查 也使用了編譯插樁技術,利用它咱們能夠找出 不合理的 Hanlder 使用、new Thread 調用、敏感權限調用 等等一系列編碼問題。
最經常使用的字節碼處理框架有 AspectJ、ASM
等等,它們的相同之處在於輸入輸出都是 Class
文件。而且,它們都是 在 Java 文件編譯成 .class 文件以後,生成 Dalvik 字節碼以前執行。
而 AspectJ 做爲 Java 中流行的 AOP(aspect-oriented programming) 編程擴展框架,其內部使用的是 BCEL框架 來完成其功能。下面,咱們就來了解下 AspectJ
具有哪些優點。
它的優點有兩點:成熟穩定、使用很是簡單。
字節碼的處理並不簡單,特別是 針對於字節碼的格式和各類指令規則,若是處理出錯,就會致使程序編譯或者運行過程當中出現問題。而 AspectJ
做爲從 2001 年發展至今的框架,它已經發展地很是成熟,一般不用考慮插入的字節碼發生正確性相關的問題。
AspectJ
的使用很是簡單,而且它的功能很是強大,咱們徹底不須要理解任何 Java
字節碼相關的知識,就能夠在不少狀況下對字節碼進行操控。例如,它能夠在以下五個位置插入自定義的代碼:
此外,它也能夠 直接將原位置的代碼替換爲自定義的代碼。
而 AspectJ
的缺點能夠歸結爲以下 三點:
AspectJ 只能在一些固定的切入點來進行操做,若是想要進行更細緻的操做則很難實現,它沒法針對一些特定規則的字節碼序列作操做。
AspectJ
的匹配規則採用了相似正則表達式的規則,好比 匹配 Activity 生命週期的 onXXX 方法,若是有自定義的其餘以 on 開頭的方法也會匹配到,這樣匹配的正確性就沒法知足。
AspectJ
在實現時會包裝本身一些特定的類,它並不會直接把 Trace
函數直接插入到代碼中,而是通過一系列本身的封裝。這樣不只生成的字節碼比較大,並且對原函數的性能會有不小的影響。若是想對 App 中全部的函數都進行插樁,性能影響確定會比較大。若是你只插樁一小部分函數,那麼 AspectJ 帶來的性能損耗幾乎能夠忽略不計。
AspectJ
其實就是一種 AOP 框架,AOP 是實現程序功能統一維護的一種技術。利用 AOP
能夠對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合性下降,提升程序的可重用性,同時大大提升了開發效率。所以 AOP
的優點可總結爲以下 兩點:
此外,AOP 不一樣於 OOP 將問題劃分到單個模塊之中,它把 涉及到衆多模塊的同一類問題進行了統一處理。好比咱們能夠設計兩個切面,一個是用於處理 App 中全部模塊的日誌輸出功能,另一個則是用於處理 App 中一些特殊函數調用的權限檢查。
下面👇,咱們就來看看要掌握 AspectJ 的使用,咱們須要瞭解的一些 核心概念。
對哪些方法進行攔截,攔截後怎麼處理。
類是對物體特徵的抽象,切面就是對橫切關注點的抽象。
JPoint 是一個程序的關鍵執行點,也是咱們關注的重點。它就是指被攔截到的點(如方法、字段、構造器等等)。
對 JoinPoint 進行攔截的定義。PointCut 的目的就是提供一種方法使得開發者可以選擇本身感興趣的 JoinPoint。
切入點僅用於捕捉鏈接點集合,可是,除了捕捉鏈接點集合之外什麼事情都沒有作。事實上實現橫切行爲咱們要使用通知。它 通常指攔截到 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
切面編程。
首先,爲了在 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
通常定位在以下位置:
使用 PointCut 對咱們指定的鏈接點進行攔截,經過 Advice,就能夠攔截到 JoinPoint 後要執行的代碼。Advice 一般有如下 三種類型:
首先,咱們舉一個 小栗子
🌰:
@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 的類型,一般有以下兩種類型:
那麼,咱們如何利用它統計 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 方法執行目標方法。
在 《深刻探索 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(性能監控框架)
。
如今,咱們將以奇虎360的 ArgusAPM 性能監控框架來全面分析下 AOP 技術在性能監控方面的應用。主要分爲以下 三個部分:
在 ArgusAPM
中,實現了 Activity
切面文件 TraceActivity
, 它被用來監控應用冷熱啓動耗時與生命週期耗時,TraceActivity
的實現代碼以下所示:
@Aspect
public class TraceActivity {
// 一、定義一個切入點方法 baseCondition,用於排除 argusapm 中相應的類。
@Pointcut("!within(com.argusapm.android.aop.*) && !within(com.argusapm.android.core.job.activity.*)")
public void baseCondition() {
}
// 二、定義一個切入點 applicationOnCreate,用於執行 Application 的 onCreate方法。
@Pointcut("execution(* android.app.Application.onCreate(android.content.Context)) && args(context)")
public void applicationOnCreate(Context context) {
}
// 三、定義一個後置通知 applicationOnCreateAdvice,用於在 application 的 onCreate 方法執行完以後插入 AH.applicationOnCreate(context) 這行代碼。
@After("applicationOnCreate(context)")
public void applicationOnCreateAdvice(Context context) {
AH.applicationOnCreate(context);
}
// 四、定義一個切入點,用於執行 Application 的 attachBaseContext 方法。
@Pointcut("execution(* android.app.Application.attachBaseContext(android.content.Context)) && args(context)")
public void applicationAttachBaseContext(Context context) {
}
// 五、定義一個前置通知,用於在 application 的 onAttachBaseContext 方法以前插入 AH.applicationAttachBaseContext(context) 這行代碼。
@Before("applicationAttachBaseContext(context)")
public void applicationAttachBaseContextAdvice(Context context) {
AH.applicationAttachBaseContext(context);
}
// 六、定義一個切入點,用於執行全部 Activity 中以 on 開頭的方法,後面的 」&& baseCondition()「 是爲了排除 ArgusAPM 中的類。
@Pointcut("execution(* android.app.Activity.on**(..)) && baseCondition()")
public void activityOnXXX() {
}
// 七、定義一個環繞通知,用於在全部 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);
}
}
複製代碼
咱們注意到,在註釋四、5這兩處代碼是用於 在 application 的 onAttachBaseContext 方法以前插入 AH.applicationAttachBaseContext(context) 這行代碼。此外,註釋二、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 中,看到註釋六、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;
}
// 一、存儲在本地
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
的每一次網絡請求。
首先,咱們看到 OKHttp3
的切面文件,代碼以下所示:
/**
* OKHTTP3 切面文件
*
* @author ArgusAPM Team
*/
@Aspect
public class OkHttp3Aspect {
// 一、定義一個切入點,用於直接調用 OkHttpClient 的 build 方法。
@Pointcut("call(public okhttp3.OkHttpClient build())")
public void build() {
}
// 二、使用環繞通知在 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();
}
}
複製代碼
在註釋一、2處,在調用 OkHttpClient 的 build 方法以前添加了一個 NetWokrInterceptor。咱們看看它的實現代碼,以下所示:
@Override
public Response intercept(Chain chain) throws IOException {
// 一、獲取每個 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();
// 二、記錄當前請求的請求 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;
}
// 三、記錄此次請求花費的時間
mOkHttpData.costTime = System.currentTimeMillis() - startNs;
if (Env.DEBUG) {
Log.d(TAG, "okhttp chain.proceed 耗時:" + mOkHttpData.costTime);
}
// 四、記錄當前請求返回的響應碼和響應數據大小
recordResponse(response);
if (Env.DEBUG) {
Log.d(TAG, "okhttp chain.proceed end.");
}
// 五、記錄 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
的每一次網絡請求。下面,咱們就看看如何去實現它。
在 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 方法,即註釋一、二、3處所示的切面代碼;一類是用於切 HttpConnection 的 openConnection 方法,對應的切面代碼爲註釋四、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) {
// 一、將請求實體使用 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
的實現,這裏面的代碼很是多,就不貼出來了,可是,它的 核心的處理 能夠簡述爲以下 兩點:
至此,ArgusAPM
的 AOP
實現部分就已經所有分析完畢了。
最後,咱們再來回顧一下本篇文章中咱們所學到的知識,以下所示:
能夠看到,AOP
技術的確很強大,使用 AspectJ
咱們能作不少事情,可是,它也有一系列的缺點,好比切入點固定、正則表達式固有的缺陷致使的使用不靈活,此外,它還生成了比較多的包裝代碼。那麼,有沒有更好地實現方式,既可以在使用上更加地靈活,也可以避免生成包裝代碼,以減小插樁所帶來的性能損耗呢?沒錯,就是 ASM
,可是它 須要經過操做 JVM 字節碼的方式來進行代碼插樁,入手難度比較大,因此,下篇文章咱們將會先深刻學習 JVM
字節碼的知識,敬請期待~
一、極客時間之Android開發高手課 編譯插樁的三種方法:AspectJ、ASM、ReDex
三、The AspectJ 5 Development Kit Developer's Notebook
七、AspectJX
八、BCEL框架
九、360 的性能監控框架ArgusAPM中AspectJ的使用
歡迎關注個人微信:
bcce5360
微信羣若是不能掃碼加入,麻煩你們想進微信羣的朋友們,加我微信拉你進羣。
2千人QQ羣,Awesome-Android學習交流羣,QQ羣號:959936182, 歡迎你們加入~