03 | Android 高級進階(源碼剖析篇) 便于性能分析的日志框架 hugo

作者簡介:ASCE1885, 《Android 高級進階》作者。
本文由于潛在的商業目的,未經授權不開放全文轉載許可,謝謝!
本文分析的源碼版本已經 fork 到我的 Github。

2d1e92443570a341f4383473ffc0bdca8df3jacobpostuma409826jpg.jpg

在 Android 性能調優中,通常存在需要對方法的執行時間進行統計的需求,這樣就可以看出哪些方法耗時多,是系統的瓶頸。最容易想到的方案是在每個方法的開頭處獲取系統時間,在方法的結尾處再次獲取系統時間,前后兩個時間戳的差值就是這個方法執行所消耗的總時間。這個方案雖然簡單易懂,但實際操作起來要寫很多樣板代碼,同時對原有的代碼浸入性太高。那么有沒有更好的方案實現方法的性能監控呢?當然是有的,它就是本文的主角:hugo。

hugo 也是 Android 平臺著名的日志框架,跟 timber 一樣出自 JakeWharton 之手。在《Android 高級進階》一書的《面向切面編程及其在 Android 中的應用》一節中其實已經介紹過 hugo 相關內容,本文會再做拓展,對 hugo 源碼做更詳細的剖析。

基本用法

在介紹 hugo 的核心原理前,有必要先了解其基本用法。hugo 以 gradle 插件的形式供開發者集成和使用,分為兩步:

  • 在項目全局添加對 hugo 插件的依賴
  • 在需要使用 hugo 的 module 中應用 hugo 插件
buildscript {
  repositories {
    mavenCentral()
  }

  dependencies {
    classpath 'com.jakewharton.hugo:hugo-plugin:1.2.1' // 添加 Hugo 的 Gradle 插件依賴
  }
}

apply plugin: 'com.jakewharton.hugo' // 應用 Hugo 插件

就這么簡單,之后這個插件會幫我們下載一些依賴庫,分別是:

  • aspectjrt.jar:aspectJ 運行時的依賴庫,想要使用 aspectJ 的功能都需要引入這個庫
  • hugo-annotations:hugo 的注解庫,定義了 DebugLog 這個注解,后面會介紹到
  • hugo-runtime:hugo 的運行時庫,是實現 hugo 日志功能的核心庫

hugo 的使用很簡單,在需要進行日志記錄的類名或者方法名處使用 @DebugLog 注解標記即可,之后 hugo 就會在編譯時織入(weaving)打印日志的代碼,從而省去了開發者手動編寫日志代碼的繁瑣。例如下面這個方法使用 @DebugLog 注解:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    printArgs("The", "Quick", "Brown", "Fox");
}

@DebugLog
private void printArgs(String... args) {
    for (String arg : args) {
        Log.i("Args", arg);
    }
}

在程序運行的時候會打印出下面的日志信息,其中 ? printArgs(args=["The", "Quick", "Brown", "Fox"])? printArgs [0ms] 是 Hugo 這個函數庫為我們自動添加的日志信息。

com.asce1885.hugodemo V/MainActivity: ? printArgs(args=["The", "Quick", "Brown", "Fox"])
com.asce1885.hugodemo I/Args: The
com.asce1885.hugodemo I/Args: Quick
com.asce1885.hugodemo I/Args: Brown
com.asce1885.hugodemo I/Args: Fox
com.asce1885.hugodemo V/MainActivity: ? printArgs [0ms]

通過查看編譯后生成的 .class 文件(位于 build/intermediates/classes/類所在包名 中),可以看到 printArgs 方法經過 AspectJ 框架的代碼織入后,已經面目全非了:

@DebugLog
private void printArgs(String... args) {
    JoinPoint var7 = Factory.makeJP(ajc$tjp_0, this, this, args);
    Hugo var10000 = Hugo.aspectOf();
    Object[] var8 = new Object[]{this, args, var7};
    var10000.logAndExecute((new HugoActivity$AjcClosure1(var8)).linkClosureAndJoinPoint(69648));
}

核心知識點

hugo 這個框架麻雀雖小但五臟俱全,它使用了很多 Android 開發中流行的技術,例如注解,AOP,AspectJ,Gradle 插件等。在進行 hugo 源碼解讀之前,你需要首先對這些知識點有一定的了解。

注解

注解是 Java 語言的特性之一,它是在源代碼中插入的標簽,這些標簽在后面的編譯或者運行過程中起到某種作用,每個注解都必須通過注解接口 @interface 進行聲明,接口的方法對應著注解的元素。

元注解,注解的一種類型,顧名思義,就是用來定義和實現注解的注解,總共有如下五種,在 hugo 中會用到 @Target@Retention 這兩個元注解,我們來做個簡單的介紹。

  • @Target:這個注解的取值是一個 ElementType 類型的數組,用來指定注解所適用的對象范圍,總共有十種不同的類型,根據定義的注解進行靈活的組合,如下所示(加粗的三個元素類型是 hugo 用到的,可重點關注):
元素類型 適用于
ANNOTATION_TYPE 注解類型聲明
CONSTRUCTOR 構造函數
FIELD 實例變量
LOCAL_VARIABLE 局部變量
METHOD 方法
PACKAGE
PARAMETER 方法參數或者構造函數的參數
TYPE 類(包含enum)和接口(包含注解類型)
TYPE_PARAMETER 類型參數
TYPE_USE 類型的用途
  • @Retention:用來指明注解的訪問范圍,也就是在什么級別保留注解,有如下三種選擇:

    • 源碼級注解:在定義注解接口時,使用 @Retention(RetentionPolicy.SOURCE) 修飾的注解,該類型的注解信息只會保留在 .java 源碼里,源碼經過編譯后,注解信息會被丟棄,不會保留在編譯好的.class 文件中
    • 編譯時注解:在定義注解接口時,使用 @Retention(RetentionPolicy.CLASS) 修飾的注解,該類型的注解信息會保留在 .java 源碼里和 .class 文件里,在執行的時候,會被 Java 虛擬機丟棄,不會加載到虛擬機中,hugo 就是用的這一種
    • 運行時注解:在定義注解接口時,使用 @Retention(RetentionPolicy.RUNTIME) 修飾的注解,Java 虛擬機在運行期也保留注解信息,可以通過反射機制讀取注解的信息(.java 源碼、.class 文件和執行的時候都有注解的信息)

未指定類型時,默認是 CLASS 類型。

更多關于注解的相關知識點,可以參考《Android 高級進階》中的《注解在 Android 中的應用》一節。

AOP

AOP,全稱為 Aspect Oriented Programming,即面向切面編程。AOP 是軟件開發中的一個編程范式,通過預編譯方式或者運行期動態代理等實現程序功能的統一維護的一種技術,它是 OOP(面向對象編程)的延續,利用 AOP 開發者可以實現對業務邏輯中的不同部分進行隔離,從而進一步降低耦合,提高程序的可復用性,進而提高開發的效率。AOP 能夠實現將日志紀錄,性能統計,埋點統計,安全控制,異常處理等代碼從具體的業務邏輯代碼中抽取出來,放到統一的地方進行處理。AOP 涉及到的基本概念有:

  • 橫切關注點(Cross-cutting concerns):在面向對象編程中,經常需要在不同的模塊代碼中添加一些類似的代碼,例如在函數入口處打印日志,在 View 的點擊處添加點擊事件的埋點統計,或者對一個函數進行性能監控,查看它的執行耗時等等,在 AOP 中把軟件系統分成兩個部分:核心關注點和橫切關注點,核心關注點就是業務邏輯處理的主要流程,而橫切關注點就是上面所說的經常發生在核心關注點的多個地方,且基本相似的日志紀錄,埋點統計等等。
  • 連接點(Joint point):在核心關注點中可能會存在橫切關注點的地方,例如方法調用的入口,View 的點擊處理等地方,在 AOP 中習慣稱為連接點。
  • 增強(Advice):特定連接點處所執行的動作,也就是 AOP 織入的代碼,目的是對原有代碼進行功能的增強,典型的有:
    • before:在目標方法執行之前的動作
    • around:在目標方法之前前后的動作
    • after:在目標方法執行之后的動作
  • 切入點(Pointcut):連接點的集合,這些連接點可以確定什么時機會觸發一個通知。切入點通常使用正則表達式或者通配符語法表示,可以指定執行某個方法,也可以指定多個方法,例如指定標記了某個注解的所有方法。
  • 切面(Aspect):切入點和通知可以組合成一個切面。
  • 織入(Weaving):將通知注入到連接點的過程。

AOP 中代碼的織入根據類型的不同,主要可以分為三類:

  • 編譯時織入:在 Java 類文件編譯的時候進行織入,這需要通過特定的編譯器來實現,例如使用 AspectJ 的織入編譯器。
  • 類加載時織入:通過自定義類加載器 ClassLoader 的方式在目標類被加載到虛擬機之前進行類的字節代碼的增強。
  • 運行時織入:切面在運行的某個時刻被動態織入,基本原理是使用 Java 的動態代理技術。

hugo 使用到的代碼織入屬于編譯時織入,用到了 AspectJ 這樣一個面向切面的框架,它擴展了 Java 語言,定義了一套 AOP 語法,實現了一個專門的編譯器來在編譯期生成遵守 Java 字節碼規范的 .class 文件。

AspectJ

AspectJ 框架主要包含三部分內容:

  • aspectjrt:運行時函數庫,AOP 所需要用到的
  • aspectjtools:工具庫
  • aspectjweaver:實現織入功能的函數庫

AspectJ 涉及的知識點比較多,可以獨立成書,這里我們只介紹 hugo 使用到的相關知識點,主要包括切點表達式,類型匹配通配符,邏輯運算符,增強類型等。

切點表達式

AspectJ 的切點表達式由關鍵字和操作參數組成,以切點表達式 execution(* helloWorld(..)) 為例,其中 execution 是關鍵字,為了便于理解,通常也稱為函數,而 * helloWorld(..) 是操作參數,通常也稱為函數的入參。切點表達式函數的類型很多,例如方法切點函數,方法入參切點函數,目標類切點函數等,hugo 用到的有兩種類型:

  • 方法切點函數之一 execution()
  • 目標類切點函數之一 within()

具體涵義如下表所示:

函數名 入參 說明
execution() 方法匹配模式字符串 表示所有目標類中滿足某個匹配模式的方法連接點,例如 execution(* helloWorld(..)) 表示所有目標類中的 helloWorld 方法,返回值和參數任意
within() 類名匹配模式字符串 表示滿足某個匹配模式的特定域中的類的所有連接點,例如 within(com.asce1885.debug.*) 表示 com.asce1885.debug 中的所有類的所有方法

接下來我們來介紹這兩個切入點函數入參的語法格式,先來看 execution() 的入參語法格式:

execution([注解] [修飾符] 返回值類型 方法名(參數列表) [異常列表])

其中,[] 號中的簽名組件是可選的。

對于 execution(* *(..)) 這個切入點而言,

  • 第一個 * 號對應方法的返回值,* 號表示方法返回值是任意的;
  • 第二個 * 號對應方法名,* 表示可以匹配該類中的所有方法;
  • (..) 表示方法的參數是任意的

within() 函數入參語法格式如下:

within(類匹配模式)

可以看出,execution()within() 兩者的主要區別是 within() 所指定的連接點最小范圍只能到類,而 execution() 所指定的連接點可以實現包,類,方法,方法入參范圍全覆蓋。

類型匹配通配符

AspectJ 切點表達式中的操作參數支持通配符,有三種類型的通配符可供選擇,具體的涵義如下表所示:

通配符 涵義
* 匹配任意字符,但只能匹配上下文中的一個元素
.. 匹配任意字符,可以匹配上下文中多個元素,比如在目標類模式的匹配中,表示匹配任意數量的子包;在方法參數模式的匹配中,表示匹配任意數量的參數
+ 匹配指定類型的子類型,只能作為后綴放在類名后面
還有 54% 的精彩內容
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
支付 ¥5.20 繼續閱讀
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,622評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,716評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,746評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,991評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,706評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,036評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,029評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,203評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,725評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,451評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,677評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,161評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,857評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,266評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,606評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,407評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,643評論 2 380

推薦閱讀更多精彩內容