Android性能全面分析与优化方案研究—几乎是史上最全最实用的

写在前面,如果面对复杂的动画效果你一筹莫展,不烦看看这篇文章:LottieAndroid使用详解及源码解析—轻而易举实现各种复杂动画

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

  • 滑动流畅度:FPS,即Frame per Second,一秒内的刷新帧数,越接近60帧越好;

  • 过度绘制:单页面的3X(粉红色区域) Overdraw小于25%

  • 启动时间:这里主要说的是Activity界面启动时间,一般低于300ms,需要用高频摄像机计算时间。

2、内存

  • 内存大小:峰值越低越好,需要优化前后做对比

  • 内存泄漏:需要用工具检查对比优化前后

3、功耗

  • 单位时间内的掉电量,掉电量越少越好,业内没有固定标准。华为有专门测试功耗的机器,以及自己的标准。

一、渲染问题

先来看看造成应用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

二、内存问题

1、内存浪费

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

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

    Android为移动操作系统特意编写了一些更加高效的容器,例如ArrayMap、SparseArray。
    为了解决HashMap更占内存的弊端,Android提供了内存效率更高的ArrayMap。

    • 先来看看HashMap的原理

      HashMap的整体结构如下:


      存储位置的确定流程:

      !()[http://img.hb.aicdn.com/16b2993c41a413b24470e1b9b74227674cd16fdb9a09-UTrZIe]


    • 再看来看看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,运行时还会产生额外的内存占用,如下图所示:


  • 推荐一些文章:

2、内存泄漏

  • 什么是内存泄漏?

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

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

    • 内部类引用导致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);
        }
      
    • 注意监听器的注销(稍后利用工具分析一个例子)

      regist就要unregist

    • 注意Cursor对象是否及时关闭(项目中也存在,不再列举)

    • WebView的引起的泄漏(暂时没有研究)

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

    • Memory monitor

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



      通过实例分析一处内存泄漏,操作步骤如下:
      启动手雷 - 进入首页 - 切换底部tab到我的tab - 点击登录弹窗弹窗 - 登录成功返回首页



    • MAT(Memory Analyzer Tool)

      需要下载MAT独立版,可到这里下载解压使用:\\192.168.8.188\上传2\ltsoft
      分析刚才的操作内存情况




      进入首页并没有进行任何活动操作,为什么会有那么多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

  • 内存使用策略优化

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




    • 有什么优化内存的策略

      • onLowMemory():Android系统提供了一些回调来通知当前应用的内存使用情况,通常来说,当所有的background应用都被kill掉的时候,forground应用会收到onLowMemory()的回调。在这种情况下,需要尽快释放当前应用的非必须的内存资源,从而确保系统能够继续稳定运行。

      • onTrimMemory(int):Android系统从4.0开始还提供了onTrimMemory()的回调,当系统内存达到某些条件的时候,所有正在运行的应用都会收到这个回调,同时在这个回调里面会传递参数,代表不同的内存使用情况,收到onTrimMemory()回调的时候,需要根据传递的参数类型进行判断,合理的选择释放自身的一些内存占用,一方面可以提高系统的整体运行流畅度,另外也可以避免自己被系统判断为优先需要杀掉的应用。

  • 文章推荐:

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

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

    Lint:静态代码分析工具

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











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



  • 设置代码分析选项


三、耗电问题

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容