Booster 系列之——性能瓶頸檢測

項目地址:github.com/didi/booste…html

對於一款 APP 來講,卡頓率、ANR 率是衡量這個 APP 質量的兩個重要指標,目前已經有不少成熟的 APM 工具和平臺來統計 APP 的運行時性能,可是對於實行敏捷開發的產品來講,從 APP 開發,到灰度發佈,再到全量,要經歷一個漫長的過程,等到收集到上報的卡頓和 ANR,再去修復,又要經歷灰度、全量這一漫長的過程。java

若是能在上線以前就能發現代碼中的性能問題並進行修復,將大大的加速了產品迭代的效率,通常來講,實現的方式可能有如下幾種:android

  1. 代碼審查
  2. 代碼掃描
  3. 靜態分析

而 Booster 選擇了靜態分析,之因此採用靜態分析的方案,緣由是由於前兩種方案都沒法解決無代碼訪問權限的狀況[1]git

性能測量

Android 官方提供了不少 Profiling 工具,儘管這些工具很是強大,可是對於開發者來講,都須要太多的人工介入,並且門檻比較高,如:github

  • Method Tracingmarkdown

    啓用 Method Tracing 須要在想要測量的代碼段中加上這兩行代碼:網絡

    Debug.startMethodTracing("booster")
    ...
    Debug.stopMethodTracing()
    複製代碼

    並且,Method Tracing 嚴重損耗運行時性能,若是測量的範圍過大,使用起來卡到不能忍受。架構

  • systraceoracle

    啓用 systrace 須要啓動 adb 連上設備進入 debug 模式,並在代碼段中加上這兩行代碼:app

    Trace.beginSection("Activity.onCreate()")
    ...
    Trace.endSection()
    複製代碼

    雖然性能開銷比 Method Tracing 少了許多,可是測量的範圍受 buffer 的限制,只能測量一段代碼的性能。

  • Android Profiler

    Android Studio 3.0 雖然提供了強大的 Android Profiler 來幫助開發者定位分析問題,可是隻有 debug 覆蓋到的代碼分支才能被檢測到,並且範圍有限。

  • Beanchmark

    Android 提供的 Jetpack Benchmark Library 能夠經過寫單元測試來測量代碼的性能,對於快速迭代的產品來講,無疑是個擺設。

爲了可以在上線以前快速的發現全部代碼中潛在的性能問題,咱們提出了經過靜態分析來檢測代碼中存在的性能瓶頸。

如何肯定性能瓶頸?

主線程

對 APP 來講,ANR 和卡頓問題的根源在於主線程被阻塞,所以,對於基於 event-loop 的系統來講,任何阻塞主線程的方法調用[2]均可以認爲是性能瓶頸。除此以外,還有其它影響運行時性能和穩定性的因素,好比:線程過載[3]、使用 finalizer[4]等等。

基於靜態分析的性能瓶頸檢測的關鍵在於肯定方法運行的線程是不是主線程。幾乎全部基於 event-loop 的GUI 系統,操做 UI 都是在主線程/UI 線程中進行,這就意味着:

  • 只要能找到跟 UI 相關的方法調用,就能夠認爲該方法是在主線程中運行;
  • 只要一條調用鏈路中的任意一個方法在主線程中調用,就能夠認爲該鏈路是在主線程中運行。

主線程入口

通過分析,最終咱們肯定了以下規則:

  • Application 的模板方法爲起點的調用鏈路,詳見:Application Entry Points
  • Activity 的模板方法爲起點的調用鏈路,詳見:Activity Entry Points
  • Service 的模板方法爲起點的調用鏈路,說見:Service Entry Points
  • BroadcastReceiver 的模板方法爲起點的調用鏈路,詳見:Receiver Entry Points
  • ContentProvider 的模板方法爲起點的調用鏈路,詳見:Provider Entry Points
  • 以參數列表及返回值中包含下列類型的方法爲起點的調用鏈路
    • Fragment
    • Dialog
    • View
    • Widget
    • Layout
  • 經過 Main Handler 提交的 Runnablerun() 方法

以上規則雖然不能命中全部的主線程入口,但至少解決了 80% 的問題,並且,每一個 APP 的架構不同,若是要作到更加精準,須要針對地性的對 Booster 進行擴展了。

方法調用鏈路

通過前面的分析,咱們可以從整個 Call Graph 中分離出全部在主線程中的調用鏈路了,可是,如何肯定哪些調用鏈路是存在性能瓶頸的呢?

在通過大量的統計分析以後,咱們肯定了會阻塞主線程的方法列表,因爲篇幅緣由,如下只列舉了一部分 API,詳細列表請參見:LINT_APIS

"java/lang/Object.wait()V",
"java/lang/Object.wait(J)V",
"java/lang/Object.wait(JI)V",
"java/lang/Thread.start()V",
"java/lang/ClassLoader.getResource(Ljava/lang/String;)Ljava/net/URL;",
"java/lang/ClassLoader.getResources(Ljava/lang/String;)Ljava/util/Enumeration;",
"java/lang/ClassLoader.getResourceAsStream(Ljava/lang/String;)Ljava/io/InputStream;",
"java/lang/ClassLoader.getSystemResource(Ljava/lang/String;)Ljava/net/URL;",
"java/lang/ClassLoader.getSystemResources(Ljava/lang/String;)Ljava/util/Enumeration;",
"java/lang/ClassLoader.getSystemResourceAsStream(Ljava/lang/String;)Ljava/io/InputStream;",
...
"java/util/zip/ZipFile.<init>(Ljava/lang/String;)",
"java/util/zip/ZipFile.getInputStream(Ljava/util/zip/ZipEntry;)",
"java/util/jar/JarFile.<init>(Ljava/lang/String;)",
"java/util/jar/JarFile.getInputStream(Ljava/util/jar/JarEntry;)",
...
"android/content/Context.getSharedPreferences(Ljava/lang/String;I)Landroid/content/SharedPreferences;",
"android/content/SharedPreferences$Editor.apply()V",
"android/content/SharedPreferences$Editor.commit()B",
...
"android/content/res/AssetManager.list(Ljava/lang/String;)[Ljava/lang/String;",
"android/content/res/AssetManager.open(Ljava/lang/String;)Ljava/io/InputStream;",
"android/content/res/AssetManager.open(Ljava/lang/String;I)Ljava/io/InputStream;",
"android/content/res/AssetManager.openFd(Ljava/lang/String;)Landroid/content/res/AssetFileDescriptor;",
"android/content/res/AssetManager.openNonAssetFd(Ljava/lang/String;)Landroid/content/res/AssetFileDescriptor;",
"android/content/res/AssetManager.openNonAssetFd(ILjava/lang/String;)Landroid/content/res/AssetFileDescriptor;",
"android/content/res/AssetManager.openXmlResourceParser(Ljava/lang/String;)Landroid/content/res/XmlResourceParser;",
"android/content/res/AssetManager.openXmlResourceParser(ILjava/lang/String;)Landroid/content/res/XmlResourceParser;",
...
"android/graphics/BitmapFactory.decodeByteArray([BIILandroid/graphics/BitmapFactory$Options;)Landroid/graphics/Bitmap;",
"android/graphics/BitmapFactory.decodeByteArray([BII)Landroid/graphics/Bitmap;",
"android/graphics/BitmapFactory.decodeFile(Ljava/lang/String;Landroid/graphics/BitmapFactory$Options;)Landroid/graphics/Bitmap;",
"android/graphics/BitmapFactory.decodeFile(Ljava/lang/String;)Landroid/graphics/Bitmap;",
"android/graphics/BitmapFactory.decodeFileDescriptor(Ljava/io/FileDescriptor;)Landroid/graphics/Bitmap;",
"android/graphics/BitmapFactory.decodeFileDescriptor(Ljava/io/FileDescriptor;Landroid/graphics/Rect;Landroid/graphics/BitmapFactory$Options;)Landroid/graphics/Bitmap;",
"android/graphics/BitmapFactory.decodeResource(Landroid/content/res/Resources;I)Landroid/graphics/Bitmap;",
"android/graphics/BitmapFactory.decodeResource(Landroid/content/res/Resources;ILandroid/graphics/BitmapFactory$Options;)Landroid/graphics/Bitmap;",
"android/graphics/BitmapFactory.decodeResourceStream(Landroid/content/res/Resources;Landroid/util/TypedValue;Ljava/io/InputStream;Landroid/graphics/Rect;Landroid/graphics/BitmapFactory$Options;)Landroid/graphics/Bitmap;",
"android/graphics/BitmapFactory.decodeStream(Ljava/io/InputStream;)Landroid/graphics/Bitmap;",
"android/graphics/BitmapFactory.decodeStream(Ljava/io/InputStream;Landroid/graphics/Rect;Landroid/graphics/BitmapFactory$Options;)Landroid/graphics/Bitmap;"
複製代碼

根據前面得出的結論,咱們就能夠經過在全部在主線程調用的鏈路中去匹配上面定義的 API 列表來找出有性能瓶頸的鏈路了。

總結

對於性能瓶頸檢測來講,其首要任務是構建 Call Graph[5]Lint Transformer 按以下步驟進行:

  1. 解析 AndroidManifest.xml ,獲得 Application 以及四大組件的類名;

  2. 建立 Globa Call Graph[6]Lint Call Graph[7],以 ROOT 節點做爲全部主線程入口方法的父節點,便於後續分離出主線程的調用鏈路,Global Call Graph 的結構以下圖所示;

    Call Graph

  3. 解析全部的 class 文件,從方法體指令序列中提取 invoke 指令[8],構建 Edge[5:1],並加入到 Call Graph 中;

  4. ROOT 節點的一級子節點爲根,開始遍歷整個 Call Graph 來匹配前面肯定的方法列表,若是匹配成功,則將該鏈路加到 Lint Call Graph

  5. 最後將 Lint Call Graph 以入口類單位分紅更小的 Call Graph,生成 dot 格式的報告,轉換爲 PNG 格式後,以下圖所示:

    Main Activity


  1. 如第三方 SDK,只提供編譯好的二進制 ↩︎

  2. 如 I/O 操做、網絡訪問、SQLite 訪問、Thread.sleep(...)Object.wait(...)↩︎

  3. 過多的線程會致使 OOM ↩︎

  4. Android 官方明確聲明 finalize 方法的調用時機不可靠 ↩︎

  5. callgraph = [edge(f, g), …];其中 fcaller, gcallee. en.wikipedia.org/wiki/Call_g… ↩︎ ↩︎

  6. Global Call Graph 表示整個 APP 的調用關係圖 ↩︎

  7. Lint Call Graph 表示存在性能瓶頸的全部鏈路組成的圖 ↩︎

  8. docs.oracle.com/javase/spec… ↩︎

相關文章
相關標籤/搜索