大厂App 性能分析与优化方案研究
2018-05-02 20:58 阅读(363)

作者:李通Tookie

链接:

https://www.jianshu.com/p/307ba8911799

本文由作者授权发布。


该文章是结合我司产品手机迅雷做的一个全面的性能分析及优化方案。本文篇幅较长,几乎涵盖了所有的性能方面问题,以及给出了如何查找和解决问题的方案,几乎是史上最全最实用的Android性能分析和优化文章。


另外,由于简书对MarkDown的支持问题导致图片格式看着有点乱,还请多多谅解,细心阅读。


结合以下四个部分讲解:



性能问题分类


1、渲染问题:过度绘制、布局冗杂

2、内存问题:内存浪费(内存管理)、内存泄漏

3、功耗问题:耗电


性能优化原则和方法


1、性能优化原则



2、优化方法



性能优化工具


以下优化工具在下面文章中具体介绍使用方法。


1、手机开发者选项:调试GPU过度绘制、启用严格模式、显示CPU使用情况、GPU呈现模式分析、显示所有"应用程序无响应"。(小米手机开发开发者选项中名字)


2、IDE中:Android Studio,比如静态代码检测工具、Memory Monitor、CPU Monitor、NetWork Monitor、GPU Monitor、Layout Inspector、Analyze APK等。


3、SDK中:sdk\tools,比如DDMS、HierarchyViewer、TraceView等。


4、第三方工具:MAT、LeakCanary、GT等。


性能优化指标


1、渲染


2、内存


3、功耗


1
渲染问题


先来看看造成应用UI卡顿的常见原因都有哪些?


1、人为在UI线程中做轻微耗时操作,导致UI线程卡顿;

2、布局Layout过于复杂,无法在16ms内完成渲染;

3、同一时间动画执行的次数过多,导致CPU或GPU负载过重;

4、View过度绘制,导致某些像素在同一帧时间内被绘制多次,从而使CPU或GPU负载过重;

5、View频繁的触发measure、layout,导致measure、layout累计耗时过多及整个View频繁的重新渲染;

6、内存频繁触发GC过多(同一帧中频繁创建内存),导致暂时阻塞渲染操作;

7、冗余资源及逻辑等导致加载和执行缓慢;

8、臭名昭著的ANR;


大多数用户感知到的卡顿等性能问题的最主要根源都是因为渲染性能。(Google官方说的)


Android系统每隔16ms发出VSYNC信号(vertical synchronization --场扫描同步,场同步,垂直同步),触发对UI进行渲染,如果每次渲染都成功,这样就能够达到流畅的画面所需要的60fps,为了能够实现60fps,这意味着程序的大多数操作都必须在16ms(1000/60=16.67ms)内完成。


如果你的某个操作花费时间是24ms,系统在得到VSYNC信号的时候就无法进行正常渲染,这样就发生了丢帧现象。那么用户在32ms内看到的会是同一帧画面。



1、过度绘制


Overdraw(过度绘制)描述的是屏幕上的某个像素在同一帧的时间内被绘制了多次。在多层次的UI结构里面,如果不可见的UI也在做绘制的操作,这就会导致某些像素区域被绘制了多次。这就浪费大量的CPU以及GPU资源,找出界面滑动不流畅、界面启动速度慢、手机发热。


如何查看过度绘制?


设置 — 开发中选项 — 调试GPU过度绘制


来看看手雷里的过度绘制和优化效果(目前手雷还存在很多待优化的页面)



上图中的各种颜色都代表什么意思?



  每个颜色的说明如下:

  原色:没有过度绘制
  紫色:1 次过度绘制
  绿色:2 次过度绘制
  粉色:3 次过度绘制
  红色:4 次及以上过度绘制


造成过度优化的关键是什么?多余的背景(Background)


接下来举例说明:


1、MainTabActivity


在MainTabActivity的Theme中修改背景



去除布局(main_activity_linerlayout.xml)中的background



如果不给当前Activity设置主题,默认主题是什么,默认主题背景是什么?








可以在默认主题中添加通用主题背景
 <item name="android:windowBackground">@drawable/common_layout_content_bkg</item>
 去除背景
 <item name="android:windowBackground">null</item>


2、除了布局中多余背景,还有可能在代码里添加了多余的背景。



查看分享弹窗的布局代码发现只有一个background,但为什么会过度绘制呢?




代码修改(SharePlatformsDialog.java)



3、弹窗底部布局不会导致弹窗本身过度绘制



弹窗的绘制是属于剪切式绘制不是覆盖绘制,蒙层是透明度亮度的调节不是绘制一层灰色


如果我们不想用系统dialog而是自定义一个弹窗view,就需要考虑过度绘制问题。


4、自定义view时,通过Canvas的clipRect方法控制每个视图每次刷新的区域,这样可以避免刷新不必要的区域,从而规避过渡绘制的问题。还可以使用canvas.quickreject()来判断是否和某个矩形相交,从而跳过那些非矩形区域内的绘制操作。


参考:

http://jaeger.itscoder.com/android/2016/09/29/android-performance-overdraw.html


优化方法和步骤关键总结


总结一下,优化步骤如下:


1、移除或修改Window默认的Background

2、移除XML布局文件中非必需的Background

3、按需显示占位背景图片

4、控制绘制区域

2
布局优化


布局太过复杂,层级嵌套太深导致绘制操作耗时,且增加内存的消耗。
我们的目标就是,层级扁平化。


布局优化的建议:


第一个建议:可以使用相对布局减少层级的就使用相对布局,否则使用线性布局。Android中RelativeLayout和LinearLayout性能分析,参考:http://www.jianshu.com/p/8a7d059da746#


第二个建议:用merge标签来合并布局,这可以减少布局层次。


第三个建议:用include标签来重用布局,抽取通用的布局可以让布局的逻辑更清晰明了,

但要避免include乱用。


第四个建议:避免创建不必要的布局层级。(最容易发生的!)


第五个建议:使用惰性控件ViewStub实现布局动态加载


如何借助工具查看代码布局?


Android SDK 工具箱中有一个叫做 Hierarchy Viewer 的工具,能够在程序运行时分析 Layout。
可以用这个工具找到 Layout 的性能瓶颈
该工具的使用条件:模拟器或者Root版真机。
如何开启该功能:AndroidStudio中,Tools — Android — Android Devices Monitor
该工具的缺点:使用起来麻烦。



看看项目中遇到的问题(MainTabAvtivity)。


merge标签的使用



未使用merge,例如:XLTabLayout.java




使用merge,例如:账号信息页的条目UserAccountItem



include标签的使用导致的问题




避免创建不必要的层级(MainTabActivity)



ViewStub的使用


这个标签最大的优点是当你需要时才会加载,使用他并不会影响UI初始化时的性能。


通常情况下我们需要在某个条件下使用某个布局的时候会通过gone或者invisible来隐藏,其实这样的方式虽然隐藏了布局,但是当显示该界面的时候还是将该布局实例化的。使用ViewStub可以避免内存的浪费,加快渲染速度。


其实ViewStub就是一个宽高都为0的一个View,它默认是不可见的,只有通过调用setVisibility函数或者Inflate函数才会将其要装载的目标布局给加载出来,从而达到延迟加载的效果,这个要被加载的布局通过android:layout属性来设置。



当准备inflate ViewStub时,调用inflate()方法即可。还可以设定ViewStub的Visibility为VISIBLE或INVISIBLE,也会触发inflate。注意的是,使用inflate()方法能返回布局文件的根View。

((ViewStub) findViewById(R.id.stub_import)).setVisibility(View.VISIBLE);
 // or
 View importPanel = ((ViewStub) findViewById(R.id.stub_import)).inflate();


setVisibility的时候会触发了inflate



注意:使用ViewStub加载的布局中不能使用merge标签。


看看Space标签(不常用)


space标签可以只在布局文件中占位,不绘制,Space标签有对应的java类Space.java,通过阅读源码可以发现,它继承至View.java,并且复写了draw方法,该方法为空,既没有调用父类的draw方法,也没有执行自己的代码,表示该类是没有绘制操作的,但onMeasure方法正常调用,说明是有宽高的。


主要功能用来设置间距,这个标签不常用,常使用margin或padding。



3
介绍一下查看渲染性能的工具


GPU呈现模式分析(大致定位问题)


开发者选项 — GPU呈现模式分析 — 选择“在屏幕上显示为条形图”




Android开发者选项——Gpu呈现模式分析,参考:http://www.voidcn.com/blog/gjy211/article/p-6210447.html
自动播放的视频停止的时候会有两条很长的柱线,下个视频播放的时候还会有一条。这里有一个明显的卡顿。


播放器操作(DefaultPlayerView.java - doPlay,player_auto_control_layout.xml):



上图的E total time = 68 是播放器停止播放的时候耗费的时间。
total time = 29 是播放器开始播放的时候耗费的时间。
其中,大部分时间耗费在了 5total time = 18 上面,这个是inflate播放器界面的时候耗费的时间。



是不是所有的inflate都很耗费时间,看一下账号信息页:




GPU Monitor



启用严格模式(不止渲染性能)


应用在主线程上执行长时间操作时会闪烁屏幕。
通过代码进行严格模式(StrictMode)调试,参考:
http://www.tuicool.com/articles/ueeM7b6


4
内存浪费


程序内存的管理是否合理高效对应用的性能有着很大的影响。
推荐阅读Android性能优化典范-第3季,参考:
http://hukai.me/android-performance-patterns-season-3/


ArrayMap(我们项目中没有用到,Android源码中很多使用)


Android为移动操作系统特意编写了一些更加高效的容器,例如ArrayMap、SparseArray。

为了解决HashMap更占内存的弊端,Android提供了内存效率更高的ArrayMap。


先来看看HashMap的原理


HashMap的整体结构如下:



存储位置的确定流程:



再看来看看ArrayMap是如何优化内存的


它内部使用两个数组进行工作,其中一个数组记录key hash过后的顺序列表,另外一个数组按key的顺序记录Key-Value值,如下图所示:



当你想获取某个value的时候,ArrayMap会计算输入key转换过后的hash值,然后对hash数组使用二分查找法寻找到对应的index,然后我们可以通过这个index在另外一个数组中直接访问到需要的键值对。



既然ArrayMap中的内存占用是连续不间断的,那么它是如何处理插入与删除操作的呢?它跟HashMap有什么区别?二者之间的删除插入效率有什么差异?请看下图所示,演示了Array的特性:




HashMap与ArrayMap之间的内存占用效率对比图如下:



与HashMap相比,ArrayMap在循环遍历的时候更加高效。



什么时候使用ArrayMap呢?


1、对象个数的数量级最好是千以内,没有频繁的插入删除操作

2、数据组织形式包含Map结构


Autoboxing(避免自动装箱)


Autoboxing的行为还经常发生在类似HashMap这样的容器里面,对HashMap的增删改查操作都会发生了大量的autoboxing的行为。当key是int类型的时候,HashMap和ArrayMap都有Autoboxing行为。


SparseArray(项目中用到较多 -- 后面再说如何利用工具查找该用SparseArray而没有用到的地方)


为了避免Autoboxing行为Android提供了SparseArray,此容器使用于key为int类型。
SparseBooleanMap,SparseIntMap,SparseLongMap等容器,是key为int,value类型相应为boolean、int、long等。


Enum(枚举,项目中较多使用,应尽量避免)


Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.
Android官方强烈建议不要在Android程序里面使用到enum。
关于enum的效率,请看下面的讨论。假设我们有这样一份代码,编译之后的dex大小是2556 bytes,在此基础之上,添加一些如下代码,这些代码使用普通static常量相关作为判断值:



增加上面那段代码之后,编译成dex的大小是2680 bytes,相比起之前的2556 bytes只增加124 bytes。假如换做使用enum,情况如下:



使用enum之后的dex大小是4188 bytes,相比起2556增加了1632 bytes,增长量是使用static int的13倍。不仅仅如此,使用enum,运行时还会产生额外的内存占用,如下图所示:



推荐一些文章:



5
内存泄露


什么是内存泄漏?


一些不用的对象被长期持有,导致内存无法被释放。


可能发生内存泄漏的地方有哪些?


内部类引用导致Activity的泄漏


在Java中,非静态(匿名)内部类会默认隐性引用外部类对象。而静态内部类不会引用外部类对象。


最典型的场景是Handler导致的Activity泄漏,如果Handler中有延迟的任务或者是等待执行的任务队列过长,都有可能因为Handler继续执行而导致Activity发生泄漏。


为了解决这个问题,可以在UI退出之前,执行remove Handler消息队列中的消息与runnable对象。或者是使用Static + WeakReference的方式来达到断开Handler与Activity之间存在引用关系的目的。


举例,MainTabActivity - MainTabHandler:





如何修复?





Android Weak Handler:可以避免内存泄漏的Handler库,参考:http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2014/1123/2047.html


Activity Context被传递到其他实例中,这可能导致自身被引用而发生泄漏。


考虑使用Application Context而不是Activity Context。


例如:全局Dialog或者Context被单例持有。


静态造成的内存泄漏




还有静态变量持有View,例如:

private static View view;
void setStaticView() {
  view = findViewById(R.id.sv_button);
}


注意监听器的注销(稍后利用工具分析一个例子)



使用工具分析定位解决内存泄漏


Memory monitor


通过MemoryMonitor可以看到,启动手雷进入手雷的内存情况如下(为什么没有做任何操作内存一直在增加?):




通过实例分析一处内存泄漏,操作步骤如下:


启动手雷 - 进入首页 - 切换底部tab到我的tab - 点击登录弹窗弹窗 - 登录成功返回首页




MAT(Memory Analyzer Tool)


需要下载MAT独立版。


分析刚才的操作内存情况





进入首页并没有进行任何活动操作,为什么会有那么多bitmap对象呢?



LeakCanary



LeakCanary 中文使用说明,参考:https://www.liaohuqiu.net/cn/posts/leak-canary-read-me/

LeakCanary:让内存泄露无所遁形,参考:https://www.liaohuqiu.net/cn/posts/leak-canary/


TraceView(不做详细分析)


GT(应该更适合测试同学测试APP性能)


利用GT,仅凭一部手机,无需连接电脑,您即可对APP进行快速的性能测试(CPU、内存、流量、电量、帧率/流畅度等等)、开发日志的查看、Crash日志查看、网络数据包的抓取、APP内部参数的调试、真机代码耗时统计等。


GT官网:http://gt.qq.com/index.html


内存使用策略优化




看看下载一个视频加上浏览一下精选页,然后将应用切到后台,内存使用情况





有什么优化内存的策略


文章推荐:


http://hukai.me/android-performance-oom/

性能优化必备神器推荐(Lint)


上面分析的一些项目中的问题,怎么找到的呢?


Lint:静态代码分析工具


如何通过Lint查找项目中的问题,如何使用?












如果只想分析某个文件夹的代码




设置代码分析选项



耗电问题


例如:关闭屏幕时关闭掉登录ping,屏幕亮时再打开。
以上的很多问题都会导致耗电量增加。
如何优化后台下载时的耗电手机发烫问题?
需要具体的硬件工具测试应用耗电量,需要一套耗电量测试标准。