性能标准
安卓绿色联盟应用性能标准主要基于主观体验、资源消耗和应用质量三个方面进行制定。
- 主观体验:主观体验主要是对应用启动时间和界面帧率制定标准,要求应用在视觉上足够流畅。其中应用启动时间又分为冷启动时间和热启动时间.安卓绿色联盟性能标准要求,应用冷启动时间需小于1000ms,热启动时间需小于500ms;普通应用帧率应大于55fps,游戏视频帧率应大于25fps。
- 资源消耗:资源消耗主要是要求应用不能占用过高的内存和CPU。安卓绿色联盟性能标准要求应用前台内存占用应小于500M,后台内存占用应小于400M;在CPU占用方面要求应用在后台灭屏5分钟后,CPU占用不超过2%。
- 应用质量:应用质量主要是对应用过度绘制和内存泄露的情况作出要求,规定应用不能存在过度绘制和内存泄露。安卓绿色联盟性能标准要求应用界面任意像素点不存在4x的绘制的情况, 3x绘制的区域不能超过界面面积的1/3,在Strict Mode中不允许有红框闪烁。
性能调试方法
在了解性能调试方法之前,我们可以先通过下图了解安卓应用性能与系统之间的关系。一个应用从应用绘制到最终显示在LCD上经历了一个漫长的路径,在任何一个阶段出现延时都会导致界面上的卡顿。
1、Strict Mode
Strict Mode意思为严格模式,是Android提供的一种运行时检测机制,一般用来检测在主线程发生的耗时动作,比如IO读写、数据库操作、复杂算法等。在手机设置开发者选项把Strict Mode打开,就可以在界面上把它打开了。
严格模式主要有2个策略,一个是线程策略,即ThreadPolicy,主要检测主线程中的一些耗时操作;另一个是虚拟机策略,即VmPolicy,主要检测一些对象的泄漏。
两大策略检测的内容和开启方法可以依据下图中的说明进行使用。 ThreadPolicy:
- 自定义的耗时调用使用detectCustomSlowCalls()开启
- 磁盘读取操作使用detectDiskReads()开启
- 磁盘写入操作使用detectDiskWrites()开启
- 网络操作使用detectNetwork()开启
VmPolicy:
- Activity泄露使用detectActivityLeaks()开启
- 未关闭的Closable对象泄露使用detectLeakedClosableObjects()开启
- 泄露的Sqlite对象,使用detectLeakedSqlLiteObjects()开启
- 检测实例数量,使用setClassInstanceLimit()开启 严格模式有三种惩罚模式:应用崩溃、弹窗警告和打印日志。在性能测试中,我们可以通过APPLogcat抓取Strict Mode的日志,同时利用代码启用Strict Mode,配合我们所需要的策略和惩罚,就可以及时定位应用的违规细节,并及时进行性能优化。
当我们碰到违规的行为时,该如何进行治理呢?建议将文件操作放到工作线程去完成,如果在主线程上提及操作,建议使用Apply和Commit去完成。如果存在对象未关闭的情况,可以通过对应的StackTrace进行关闭。
2、OverDraw DeBugger
Overdraw是指屏幕上的某个像素在同一帧的时间内被绘制了多次,这个工具使用色块来代表不同数量的过度绘制,我们可以使用这个工具来定位由过度绘制引起的用户界面卡顿问题。
在开发者选项中选择开启 Debug GPU Overdraw选项,即可在安卓设备上将过度绘制问题可视化。 左图为正常模式下显示的视图,右图为开启GPU Overdraw后显示的视图
3、Profile GPU Rendering
ProfileGPU Rendering 工具以滚动直方图的形式直观地显示渲染界面窗口帧所花费的相对时间(以每帧 16 毫秒的速度作为对比基准)。这个工具同样也是在安卓设备的开发者选项中开启。每个管线的高度表示时间,管线中各个彩色区段代表不同含义。
下表介绍了使用运行Android 6.0及更高版本的设备时分析器中不同竖条区段的含义。
4、Android Profiler
Android Profiler是一个Android Studio集成的应用性能分析器,可以实时查看CPU、Memory和Network的动态情况。以下重点介绍CPU Profiler:
CPU Profiler 可帮助您实时检查应用的 CPU 使用率和线程 Activity,并记录函数跟踪,方便大家优化和调试应用代码。
当打开 CPU Profiler 时,它将显示应用的 CPU 使用率和线程 Activity。 CPU Profiler可以选择不同的标签,并对应用线程进行跟踪。如:
- Flame Chart标签会提供一个倒置的调用图表,汇总相同的调用堆栈,收集调用顺序完全一致的函数,并在火焰图中用一个较长的横条表示它们。
- Top Down标签能够提供每个函数调用上所花费的CPU时间。Self表示函数调用在执行自己的代码上所花的时间;Children表示函数调用子方法所花费的时间;Total表示Self和Children时间的总和。
5、Systrace
Systrace是我们分析性能最常用的工具之一,它可以分析整机系统性能及动态场景的性能问题。
Systrace 允许您在系统级别收集和检查设备上运行的所有进程的计时信息。它将来自Android内核的数据(例如CPU调度程序,磁盘活动和应用程序线程)组合起来,以生成HTML报告。
上图左部是Systrace的界面,我们可以通过右边的代码抓取Systrace,观察进程的执行时间。在输入抓取命令时,时间参数一般选择5到10秒,因为时间过短可能会抓不到想要的数据,时间过长则可能抓取失败。
一般我们通过Chrome浏览器查看生成的trace文件,也可以通过DDMS图形界面去抓取Systrace。
拿到一个Systrace时主要考察哪些因素?首先看一下CPU的频率,找到对应的进程或者线程,查看相关信息;同时还要观察GPU的频率、Surface Flinger还有绘图的Buffer状态等。
当应用发生卡顿时,我们可以通过Systrace进行分析。在生成的trace文件中,找到主线程UI,每一帧都会标记一个带有F的圆形。当原型为绿色时,代表页面流畅,而黄色和红色则存在超时,我们可以点击去查看具体存在什么问题。
启动时间
应用启动时间是应用性能最重要的指标之一,分冷启动和热启动两种情况:
- 冷启动:当APP启动时,后台没有该app的进程,这时系统会重新创建一个新的进程分配给该app,这个启动方式就叫做冷启动(后台不存在该APP进程)
- 热启动:当APP已经被打开,但是被按下返回键,Home键等按钮时回到桌面或者是其他程序的时候,再重新打开该APP时,这种方式叫做热启动(后台已经存在该APP进程)
测试方法:
将手机root之后,使用adb工具连接手机.使用adb shell am start -S -W 命令获取应用冷启动时间,adb shell am Start -W命令获取应用冷启动时间.
adb shell am start -W -n packagename/packageName.MainActivity
aapt dump badging <apk路径>
:搜package 的 launchable-activity
执行成功后将返回三个测量到的时间:
- ThisTime:一般和TotalTime时间一样,除非在应用启动时开了一个透明的Activity预先处理一些事再显示出主Activity,这样将比TotalTime小。
- TotalTime:应用的启动时间,包括创建进程+Application初始化+Activity初始化到界面显示。
- WaitTime:一般比TotalTime大点,包括系统影响的耗时。
关于ThisTime/TotalTime/WaitTime的区别,下面是其解释:“adb shell am start -W ”的实现在『frameworks\base\cmds\am\src\com\android\commands\am\Am.java』文件中。其实就是跨Binder调用ActivityManagerService.startActivityAndWait() 接口(后面将ActivityManagerService简称为AMS),这个接口返回的结果包含上面打印的ThisTime、TotalTime时间.
- startTime记录的刚准备调用startActivityAndWait()的时间点
- endTime记录的是startActivityAndWait()函数调用返回的时间点
- WaitTime = startActivityAndWait()调用耗时。
ThisTime、TotalTime 的计算在 frameworks\base\services\core\java\com\android\server\am\ActivityRecord.java 文件的 reportLaunchTimeLocked() 函数中。
1 | private void reportLaunchTimeLocked(final long curTime) { |
- curTime表示该函数调用的时间点.
- displayStartTime表示一连串启动Activity中的最后一个Activity的启动时间点.
- mLaunchStartTime表示一连串启动Activity中第一个Activity的启动时间点.
正常情况下点击桌面图标只启动一个有界面的 Activity,此时 displayStartTime 与mLaunchStartTime 便指向同一时间点,此时 ThisTime=TotalTime。另一种情况是点击桌面图标应用会先启动一个无界面的 Activity 做逻辑处理,接着又启动一个有界面的Activity,在这种启动一连串 Activity 的情况下(知乎的启动就是属于这种情况),displayStartTime 便指向最后一个 Activity 的开始启动时间点,mLaunchStartTime 指向第一个无界面Activity的开始启动时间点,此时 ThisTime!=TotalTime。这两种情况如下图:
在上面的图中,我用①②③分别标注了三个时间段,在这三个时间段内分别干了什么事呢?
- 在第①个时间段内,AMS 创建 ActivityRecord 记录块和选择合理的 Task、将当前Resume 的 Activity 进行 pause
- 在第②个时间段内,启动进程、调用无界面 Activity 的 onCreate() 等、 pause/finish 无界面的 Activity
- 在第③个时间段内,调用有界面 Activity 的 onCreate、onResume
看到这里应该清楚 ThisTime、TotalTime、WaitTime 三个时间的关系了吧。WaitTime 就是总的耗时,包括前一个应用 Activity pause 的时间和新应用启动的时间;ThisTime 表示一连串启动 Activity 的最后一个 Activity 的启动耗时;TotalTime 表示新应用启动的耗时,包括新进程的启动和 Activity 的启动,但不包括前一个应用 Activity pause 的耗时。也就是说,开发者一般只要关心 TotalTime 即可,这个时间才是自己应用真正启动的耗时。
Event log中 TAG=am_activity_launch_time 中的两个值分表表示 ThisTime、TotalTime,跟通过 “adb shell am start -W ” 得到的值是一致的。
最后再说下系统根据什么来判断应用启动结束。我们知道应用启动包括进程启动、走 Activity生命周期 onCreate/onResume 等。在第一次 onResume 时添加窗口到WMS中,然后measure/layout/draw,窗口绘制完成后通知 WMS,WMS 在合适的时机控制界面开始显示(夹杂了界面切换动画逻辑)。记住是窗口界面显示出来后,WMS 才调用reportLaunchTimeLocked() 通知 AMS Activity 启动完成。
最后总结一下,如果只关心某个应用自身启动耗时,参考TotalTime;如果关心系统启动应用耗时,参考WaitTime;如果关心应用有界面Activity启动耗时,参考ThisTime。
指标
一般情况下,应用启动时间在1秒以内,用户反馈系统响应很快;1-3秒内完成启动,用户认为反应速度尚可;3-5秒内完成启动,用户会认为系统反应很慢,但是还可以接受;若启动时间超过5秒,则大部分用户会认为系统反应糟糕,甚至卸载应用。
安卓绿色联盟应用体验标准规定,应用冷启动时间应该不超过1000毫秒,热启动时间应该不超过500毫秒,若不满足,则无法获得绿色应用认证。
从上面的测评结果可以看出,参与测评的8款应用冷启动和热启动的时间都是非常快,也都符合安卓绿色联盟对绿色应用启动时间标准。其中QQ音乐和芒果TV的冷启动时间最为优秀,分别只有283毫秒和596毫秒。
Android系统上,APP无进程状态,启动流程见下: Application的构造器方法 ——>attachBaseContext() ——>onCreate() ——>Activity的构造方法 ——>onCreate() ——>配置主题中背景等属性 ——>onStart() ——>onResume() ——>测量布局绘制显示在界面上。
当点击APP的启动图标时,安卓系统会从Zygote进程中fork创建出一个新的进程分配给该应用,之后会依次创建和初始化Application类、创建MainActivity类、加载主题样式Theme中的windowBackground等属性设置给MainActivity以及配置Activity层级上的一些属性、再inflate布局、当onCreate/onStart/onResume方法都走完了后最后才进行contentView的measure/layout/draw显示在界面上,所以直到这里,应用的第一次启动才算完成,这时候我们看到的界面也就是所说的第一帧。
启动时间:
- 冷启动时间:当用户点击目标app图标的 timepoint到显示界面第一帧的时间段(当用户点击你的app那一刻到系统调用Activity.onCreate()之间的时间段),在这个时间段内,WindowManager会先加载app主题样式中的windowBackground做为app的预览元素,然后再真正去加载activity的layout布局。API19 之后,系统会出打印日志输出启动的时间:冷启动时间 = 应用启动(创建进程) —> 完成视图的第一次绘制(Activity内容对用户可见);
- 热启动时间:用户把目标app切换至后台后,点击app图标的timepoint到显示界面第一帧的时间段
优化建议
- 减少Application中过多的三方SDK初始化操作,在真正使用到功能时在进行初始化操作。
- 减少首界面布局文件的层级和嵌套,来减少界面的量测和绘制的时间。
- 减少首界面oncreate()方法中的复杂逻辑和工作量,从而降低启动时间。
- 不要以静态变量的方式在Application中保存数据
流畅度
人为感受的性能不好属于下面两种:
- 响应时间,界面跳转后响应时间
- 流畅度,界面操作时或动画展示的效果
流畅度的衡量指标:
- 帧率fps
- 丢帧SF(Skipped frame)
- 流畅度SM(SMoothness)
帧率计算:
adb shell dumpsys gfxinfo <PACKAGE_NAME>
adb shell dumpsys SurfaceFlinger --latency <window_activity>
https://blog.csdn.net/itfootball/article/details/43084527 https://developer.android.com/training/testing/performance?hl=zh-cn https://testerhome.com/topics/4441 https://testerhome.com/topics/4643
页面响应时间TTLB(Time To Last Byte)
页面响应时间是指从用户发出请求到客户端收到最后一个字节所耗费的时间.
性能案例分析
案例1:界面滑动卡顿
从图中可以看到,这是一个手动滑动事件,当deliverInput事件发生后,第一帧就发生了卡顿。从systrace看UI thread执行draw的时间相当耗时导致丢帧卡顿,而且大部分时间都在做decodeBitmap,共耗时99.045ms。这时,我们打开applog发现,有StrictMode相关的错误提示,从中可以定位到耗时函数。
从上图我们看出有一个网络访问违规,大概可以推测应用在从网络上下载了一个数据流,数据流里可能包含了一些图形,通过decodeBitmap把它解析出来展示在UI界面中。正产情况下,我们应该把网络访问放在工作线程里面去处理,将数据下载完了之后再放到主线程中去展示,避免这种问题的发生。
案例2:Strict Mode错误提示
从上图Strict Mode的日志可以看出:StrictMode policy violation耗时2秒左右。通过最下行蓝色的log,可以知道应用是在某一个目录里面寻找一个文件,判断文件是否存在。
面对这种问题,我们应该把IO操作放到工作线程。正常情况下IO的发生非常快,但是在系统繁忙时,IO放在主线程会产生较大的问题,因为它要等别的程序读写完成之后,才会下发,产生超时。
案例3:GPU调用不当导致的卡顿问题
这是一个GPU的例子,上图主要问题是GPU使用了太长时间处理应用传过来的buffer,例子中Surfaceflinger 使用GPU 做了图像叠加,说明图层比较多。使用GPU做叠加主要会产生功耗和唤醒耗时的问题。大家在做界面设计的时候,尽量不要使用GPU进行叠加。在上面的例子中,GPU叠加之后,导致了大概15ms左右的延时,因为GPU操作完成以后还需要交给Surfaceflinger把图像显示到屏幕上。
案例4:CPU调用不当导致的界面滑动卡顿问题
可以通过上图的红色条块了解messageloop RunTask信息,红色条块上的蓝色bar,表示线程在CPU上的状态。蓝色表示这个线程处于等待CPU调度的状态,可见等待超过8ms的时间,是正常调度周期好几倍。导致这种情况发生的原因有两个:CPU负载过大或CPU调度出现了问题。在上图中我们可以看出,CPU0和CPU1使用率100%,但是CPU2和CPU3是offline的状态,说明系统出现问题,导致CPU2和CPU3未能唤醒,帮助完成系统任务。
性能优化建议
1、避免内存泄露
在应用开发过程中,首先要避免内存泄露的问题,内存泄露是一种比较严重的性能问题,在安卓绿色联盟应用性能标准中也要求应用不允许发生内存泄露。 常见内存泄露:
- 手动关闭try/catch/finally中使用网络文件等流文件的对象,关注对象:HTTP,File, ContentProvider,Bitmap,Uri,Socket
- 注意关闭onDestroy()或者onPause()中未及时关闭对象,防止如下内存泄露:线程泄露,Handler泄露,广播泄露,第三方SDK/开源框架泄露,各种callBack/Listener的泄露
常见内存泄露坚持工具:
- Memory Monitor
- Allocation Tracker
- Heap Viewer
- LeakCanary
2、避免不良设计或程序算法导致CPU占有率持续偏高
- 主要业务处理分散到不同线程,便于后续利用多核处理器的并行处理能力,避免一核累死,7核围观;
- 使用top命令观察应用线程的CPU占有率,找出高负载的进程进行分析,并针对优化。
3、避免OnXXX 回调函数中进行耗时操作,避免主线程卡顿
Android系统中正常情况下所有onXXX类函数均运行在主线程中。 两帧中间有一个因为接收广播处理导致的158ms的卡顿。在这些函数中,我们应该避免网络通信操作、文件读写操作、数据库数据改动的操作、图形处理、文本分析等操作,将这些工作尽可能的移到工作线程中去,从而避免主线程卡顿。
4、合理使用系统资源
合理使用系统资源主要指的是软资源。下图是对广播资源调用的一些建议。
- 避免同一广播在多个不同实例中重复注册
- 对象释放时,必须保证注销广播,避免广播注册泄露
- 尽量不要过度依赖广播机制进行通信,只注册必要的广播
- 尽量不要注册使用频繁放生的系统广播
- 不高频调用系统服务接口,避免引起系统互锁造成阻塞
性能治理
主线程卡顿
主线程卡顿是因为主线程的消息超过阈值,从而导致页面丢帧。手淘通过接管主线程消息的分发机制,获取消息的分发耗时和消息类型,从而定位触发主线程卡顿的具体业务并进行针对性治理。
另外手淘在使用系统的SharedPreferences时,发现页面跳转导致界面ANR的情况。通过阅读系统源码,发现它在做Receiver或者Service时会强制把所有SharedPreferences apply的内容写入文档,导致ANR。针对这个问题,手淘重写SharedPreferences提升性能,减少了这类ANR问题的发生。
内存泄漏
手淘团队投入了大量时间对内存泄露进行治理。一方面通过接管系统底层组件的生命周期,当组件的生命周期销毁时,对它进行一个WeakReference的引用,然后根据GC事件触发情况来判定该对象是否泄漏。另一方面,在Native层通过Hook操作系统底层malloc和free方法,计算每一个so处理内存的情况,根据malloc与free差值大小与白名单进行对比判断是否存在内存泄露。
内存使用不当
何为内存使用不当?举个例子,当开发一个大小为100×100 view,实际却使用了200200甚至更大的bitmap。比如在系统drawable目录下放置一张图片,在高清的设备上展示时,它会根据系统自身的原理对它进行拉伸。这时原本只需要单位1的内存(100100),可能变成单位16的内存(400*400),内存的浪费率达到90%以上。针对这种情况,手淘做了一个内存使用不当排查插件。
视频也同样存这种问题,在低分辨率设备上播放高质量的视频不仅不会给用户带来更好的体验,还可能让设备出现卡顿。还有就是图片持有的问题,当页面已经沉入栈底,最好不要保留之前页面的图片。这样可以保证有足够的内存给前台页面使用,否则随着页面层级的深入,很容易出现OOM。
资源泄露
手淘主要通过接管系统底层的open和close两个Native方法函数对资源泄露进行治理。当open和close没有成对出现,并且该业务并不是伴随整个应用生命周期(伴随整个生命周期的文件有白名单),可以判断该操作可能存在资源泄漏。平台会将该异常行为告知对应的开发同学检查和治理。数据库的治理同样也采用了这套方案。
线程问题
线程问题比较复杂。在线程创建时可能会触发一些意想不到的问题,比如Out Of Memory error。Out Of Memory error可能是由线程创建失败导致的。因此,手淘对线程创建进行了接管。业务在创建时,对它的方法调用栈进行聚类,就可以知道每个业务创建的线程数量,以及线程创建是否合理。建议应用开发者在创建线程时一定规范命名,以便快速定位具体的业务方。
流量监控
手淘主要是通过接管Socket协议,分析协议头部获取请求和回流数据内容的大小信息进行流量监控治理。如发现异常,可以让开发同学定位解决。同时也可以监听后台流量行为,观察APP切到后台以后是否还有大量的网络请求。
设备评级
安卓设备百花齐放,手淘对不同的设备采取了计分的方法进行评级,根据设备分数采用不同的策略,展示相应的图片、视频和业务,给用户带来最佳的性能体验。这个设备评级方案可以给开发同学提供指导建议,更好的展现业务形态。
布局性能
在开发的过程中,常常要通过HierarchyViewer的方法检查布局结构是否合理。手淘写了一套算法,检查页面结构是否合理,页面层级是否过深、页面层级是否还有继续优化的空间。同时还实现了一套OverDraw的算法,给开发同学提示具体哪个层级可以优化,怎么样的降层级,怎么样解决OverDraw的问题。
用户体验优化
手淘很关注用户的体验,包括启动时间、每个页面的打开耗时等。通过监控启动时各个子任务的耗时,以及对这些信息快速的分析,判断每一次发版质量变化的具体原因。
4.X设备体验优化
随着产品功能越来越丰富和产品体积逐渐的壮大,4.X设备出现了multidex越来越慢的情况。基于这个现象,手淘把谷歌的support multidex包进行了重构改造。经过重构以后的support multidex方案,4.X设备的Dex加载随着Dex越来越多,它性能提升越好。
内存容灾
内存容灾手淘一直很关注。当用户使用当前页面时,如果内存不足,手淘期望后台页面可以快速释放内存资源,为前台页面服务。手淘开发了内存容灾插件,监听JVM的GC事件以及轮寻物理页表计算实际使用物理内存,通过这个计算给手淘的业务方发送对应的内存水位事件,如果是属于非常高危的内存事件,就可以让后台快速的释放缓存资源,从而为可视的页面提供更好的服务
稳定性治理
稳定性的治理主要是两部分,Java Crash和Native Crash
Java Crash治理
手淘通过接管UncaughtExceptionHandler,拿到具体的Java Crash的信息及它的堆栈来进行Java Crash治理。Java Crash治理还有一个经常遇到的OOM的情况,一方面可能是虚拟机的内存不足导致的,另一方面可能是线程创建失败导致的,可以使用前面讲到的线程创建插件解决。
Native Crash治理
手淘通过捕获信号量的方式对Native Crash进行治理。当Native发生异常时,创建一个子进程,通过ptrace的方式去dump Native Crash上下文的线程信息。 基于前面讲到的性能治理方式,OOM的Native Crash可以通过malloc和free的接管,定位具体是哪个SO导致这个问题的发生并进行治理。