項目地址:github.com/didi/booste…html
對於一款 APP 來講,卡頓率、ANR 率是衡量這個 APP 質量的兩個重要指標,目前已經有不少成熟的 APM 工具和平臺來統計 APP 的運行時性能,可是對於實行敏捷開發的產品來講,從 APP 開發,到灰度發佈,再到全量,要經歷一個漫長的過程,等到收集到上報的卡頓和 ANR,再去修復,又要經歷灰度、全量這一漫長的過程。java
若是能在上線以前就能發現代碼中的性能問題並進行修復,將大大的加速了產品迭代的效率,通常來講,實現的方式可能有如下幾種:android
而 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 Studio 3.0 雖然提供了強大的 Android Profiler 來幫助開發者定位分析問題,可是隻有 debug 覆蓋到的代碼分支才能被檢測到,並且範圍有限。
Android 提供的 Jetpack Benchmark Library 能夠經過寫單元測試來測量代碼的性能,對於快速迭代的產品來講,無疑是個擺設。
爲了可以在上線以前快速的發現全部代碼中潛在的性能問題,咱們提出了經過靜態分析來檢測代碼中存在的性能瓶頸。
對 APP 來講,ANR 和卡頓問題的根源在於主線程被阻塞,所以,對於基於 event-loop 的系統來講,任何阻塞主線程的方法調用[2]均可以認爲是性能瓶頸。除此以外,還有其它影響運行時性能和穩定性的因素,好比:線程過載[3]、使用 finalizer
[4]等等。
基於靜態分析的性能瓶頸檢測的關鍵在於肯定方法運行的線程是不是主線程。幾乎全部基於 event-loop 的GUI 系統,操做 UI 都是在主線程/UI 線程中進行,這就意味着:
通過分析,最終咱們肯定了以下規則:
Application
的模板方法爲起點的調用鏈路,詳見:Application Entry PointsActivity
的模板方法爲起點的調用鏈路,詳見:Activity Entry PointsService
的模板方法爲起點的調用鏈路,說見:Service Entry PointsBroadcastReceiver
的模板方法爲起點的調用鏈路,詳見:Receiver Entry PointsContentProvider
的模板方法爲起點的調用鏈路,詳見:Provider Entry PointsFragment
Dialog
View
Widget
Layout
以上規則雖然不能命中全部的主線程入口,但至少解決了 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 按以下步驟進行:
解析 AndroidManifest.xml ,獲得 Application
以及四大組件的類名;
建立 Globa Call Graph[6] 和 Lint Call Graph[7],以 ROOT 節點做爲全部主線程入口方法的父節點,便於後續分離出主線程的調用鏈路,Global Call Graph 的結構以下圖所示;
解析全部的 class 文件,從方法體指令序列中提取 invoke 指令[8],構建 Edge[5:1],並加入到 Call Graph 中;
以 ROOT 節點的一級子節點爲根,開始遍歷整個 Call Graph 來匹配前面肯定的方法列表,若是匹配成功,則將該鏈路加到 Lint Call Graph 中
最後將 Lint Call Graph 以入口類單位分紅更小的 Call Graph,生成 dot 格式的報告,轉換爲 PNG 格式後,以下圖所示: