项目地址: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 格式后,以下图所示: