深入探索編譯插樁技術(二、AspectJ)

jsonchao發表於2020-04-02

前言

成為一名優秀的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 將目標方法相關的資訊儲存起來。這裡我們先看看這個 FirstFrameRunnablerun 方法的實現程式碼,如下所示:

 @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 方法中僅僅是繼續呼叫了 OutputProxyoutput 方法 將啟動時間和記錄啟動資訊的字串傳入。我們再看看 OutputProxyoutput 方法,如下所示:

/**
 * 警報資訊輸出
 *
 * @param showMsg
 */
public static void output(String showMsg) {
    if (!AnalyzeManager.getInstance().isDebugMode()) {
        return;
    }
    if (TextUtils.isEmpty(showMsg)) {
        return;
    }
    // 1、儲存在本地
    StorageManager.saveToFile(showMsg);
}
複製程式碼

註釋1處,在 output 方法中又繼續呼叫了 StorageManagersaveToFile 方法 將啟動資訊儲存在本地saveToFile 的實現程式碼如下所示:

/**
 * 按行儲存到文字檔案
 *
 * @param line
 */
public static void saveToFile(String line) {
    TraceWriter.log(Env.TAG, line);
}
複製程式碼

這裡又呼叫了 TraceWriterlog 方法 將啟動資訊按行儲存到文字檔案中,關鍵程式碼如下所示:

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 方法用於記錄網路請求資訊。我們再看到 QOKHttprecordUrlRequest 方法,如下所示:

/**
 * 記錄一次網路請求
 *
 * @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 中

至此,ArgusAPMAOP 實現部分就已經全部分析完畢了。

六、總結

最後,我們再來回顧一下本篇文章中我們所學到的知識,如下所示:

  • 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

2、《AspectJ 程式設計指南》PDF

3、The AspectJ 5 Development Kit Developer's Notebook

4、深入理解Android之AOP

5、AOP技術在客戶端的應用與實踐

6、注意Advice Precedence的情況

7、AspectJX

8、BCEL框架

9、360 的效能監控框架ArgusAPM中AspectJ的使用

10、利用AspectJ實現插樁的例子

Contanct Me

● 微信:

歡迎關注我的微信:bcce5360

● 微信群:

微信群如果不能掃碼加入,麻煩大家想進微信群的朋友們,加我微信拉你進群。

深入探索編譯插樁技術(二、AspectJ)

● QQ群:

2千人QQ群,Awesome-Android學習交流群,QQ群號:959936182, 歡迎大家加入~

About me

很感謝您閱讀這篇文章,希望您能將它分享給您的朋友或技術群,這對我意義重大。

希望我們能成為朋友,在 Github掘金上一起分享知識。

相關文章