Android性能模式 第三季

转自http://hukai.me/android-performance-patterns-season-3

1. Fun with ArrayMaps

程序内存的管理是否合理高效对应用的性能有着很大的影响,有的时候对容器的使用不当也会导致内存管理效率低下。Android为移动操作系统特意编写了一些更加高效的容器,例如SparseArray,今天要介绍的是一个新的容器,叫做ArrayMap。

我们经常会使用到HashMap这个容器,它非常好用,但是却很占用内存。下图演示了HashMap的简要工作原理:

这里写图片描述

为了解决HashMap更占内存的弊端,Android提供了内存效率更高的ArrayMap。它内部使用两个数组进行工作,其中一个数组记录key hash过后的顺序列表,另外一个数组按key的顺序记录Key-Value值,如下图所示:

这里写图片描述

当你想获取某个value的时候,ArrayMap会计算输入key转换过后的hash值,然后对hash数组使用二分查找法寻找到对应的index,然后我们可以通过这个index在另外一个数组中直接访问到需要的键值对。如果在第二个数组键值对中的key和前面输入的查询key不一致,那么就认为是发生了碰撞冲突。为了解决这个问题,我们会以该key为中心点,分别上下展开,逐个去对比查找,直到找到匹配的值。如下图所示:

这里写图片描述

随着数组中的对象越来越多,查找访问单个对象的花费也会跟着增长,这是在内存占用与访问时间之间做权衡交换。

既然ArrayMap中的内存占用是连续不间断的,那么它是如何处理插入与删除操作的呢?请看下图所示,演示了Array的特性

这里写图片描述

这里写图片描述

很明显,ArrayMap的插入与删除的效率是不够高的,但是如果数组的列表只是在一百这个数量级上,则完全不用担心这些插入与删除的效率问题。HashMap与ArrayMap之间的内存占用效率对比图如下:

这里写图片描述

与HashMap相比,ArrayMap在循环遍历的时候也更加简单高效,如下图所示:

    // ArrayMap
    for (int i = 0; i < map.size(); i++) {
        Object keyObj = map.keyAt(i);
        Object valueObj = map.valueAt(i);
        ......
    }

    // HashMap
    for (Iterator<Object> it = map.keySet().iterator(); it.hasNext(); ) {
        Object object = it.hasNext();
        ......
    }

前面演示了很多ArrayMap的优点,但并不是所有情况下都适合使用ArrayMap,我们应该在满足下面2个条件的时候才考虑使用ArrayMap:

  • 对象个数的数量级最好是千以内
  • 数据组织形式包含Map结构

我们需要学会在特定情形下选择相对更加高效的实现方式。

2. Beware Autoboxing

有时候性能问题也可能是因为那些不起眼的小细节引起的,例如在代码中不经意的“自动装箱”。我们知道基础数据类型的大小:boolean(8 bits), int(32 bits), float(32 bits),long(64 bits),为了能够让这些基础数据类型在大多数Java容器中运作,会需要做一个autoboxing的操作,转换成Boolean,Integer,Float等对象,如下演示了循环操作的时候是否发生autoboxing行为的差异:

这里写图片描述

这里写图片描述

Autoboxing的行为还经常发生在类似HashMap这样的容器里面,对HashMap的增删改查操作都会发生了大量的autoboxing的行为。

这里写图片描述

为了避免这些autoboxing带来的效率问题,Android特地提供了一些如下的Map容器用来替代HashMap,不仅避免了autoboxing,还减少了内存占用:

这里写图片描述

注:上图实际上是有问题的,SparseBoolMap,SparseIntMap和SparseLongMap的key的类型都是int。

3. SparseArray Family Ties

为了避免HashMap的autoboxing行为,Android系统提供了SparseBoolMap,SparseIntMap,SparseLongMap,LongSparseMap等容器。关于这些容器的基本原理请参考前面的ArrayMap的介绍,另外这些容器的使用场景也和ArrayMap一致,需要满足数量级在千以内,数据组织形式需要包含Map结构。

4. The price of ENUMs

在StackOverFlow等问答社区常常出现关于在Android系统里面使用枚举类型的性能讨论,关于这一点,Android官方的Training课程里面有下面这样一句话:

Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.

关于enum的效率,请看下面的讨论。假设我们有这样一份代码,编译之后的dex大小是2556 bytes,在此基础之上,添加一些如下代码,这些代码使用普通static常量相关作为判断值:

    public static final int VALUE1 = 1;
    public static final int VALUE2 = 2;
    public static final int VALUE3 = 3;

    int func(int value) {
        switch (value) {
            case VALUE1:
                return -1;
            case VALUE2:
                return -2;
            case VALUE3:
                return -3;
        }
        return 0;
    }

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

    public static enum Value {
        VALUE1,
        VALUE2,
        VALUE3
    }

    int func(Value value) {
        switch (value) {
            case VALUE1:
                return -1;
            case VALUE2:
                return -2;
            case VALUE3:
                return -3;
        }
        return 0;
    }

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

这里写图片描述

Android官方强烈建议不要在Android程序里面使用到enum。

5. Trimming and Sharing Memory

Android系统的一大特色是多任务,用户可以随意在不同的app之间进行快速切换。为了确保你的应用在这种复杂的多任务环境中正常运行,我们需要了解下面的知识。

为了让background的应用能够迅速的切换到forground,每一个background的应用都会占用一定的内存。Android系统会根据当前的系统内存使用情况,决定回收部分background的应用内存。如果background的应用从暂停状态直接被恢复到forground,能够获得较快的恢复体验,如果background应用是从Kill的状态进行恢复,就会显得稍微有点慢。

这里写图片描述

Android系统提供了一些回调来通知应用的内存使用情况,通常来说,当所有的background应用都被kill掉的时候,forground应用会收到onLowMemory()的回调。在这种情况下,需要尽快释放当前应用的非必须内存资源,从而确保系统能够稳定继续运行。Android系统还提供了onTrimMemory()的回调,当系统内存达到某些条件的时候,所有正在运行的应用都会收到这个回调,同时在这个回调里面会传递以下的参数,代表不同的内存使用情况,下表介绍了各种不同的回调参数:

ParameteNote
TRIM_MEMORY_RUNNING_MODERATEthis is your first warning
TRIM_MEMORY_RUNNING_LOWThis is like yellow light. It is your second warning to begin to trim resource to improve performance.
TRIM_MEMORY_RUNNING_CRITICALThis is like red light. If you keep on executing without clearing up memory resource, the system is going to begin killing background processes to get more memory for you. Unfortunately that will lower the performance of your application.
TRIM_MEMORY_UI_HIDDENYour application was just moved off the screen, so this is a good time to release large UI resources. Now your application is on the list off cached applications. If there are memory problems, your processes may be killed.
TRIM_MEMORY_BACKGROUNDBeing a background app – release as much as you can so that your app can resume faster than a pure restart.
TRIM_MEMORY_MODERATEYou are a background app, but in the middle
TRIM_MEMORY_COMPLETEYou are a background app, but about to be killed.

onTrimMemory()的回调可以发生在Application,Activity,Fragment,Service,Content Provider。

从Android 4.4开始,ActivityManager提供了isLowRamDevice()的API,通常指的是Heap Size低于512M或者屏幕大小<=800*480的设备

6. DO NOT LEAK VIEWS

内存泄漏的概念,下面一张图演示下:

这里写图片描述

通常来说,View会保持Activity的引用,Activity同时还和其他内部对象也有可能保持引用关系。当屏幕发生旋转的时候,activity很容易发生泄漏,这样的话,里面的view也会发生泄漏。Activity以及view的泄漏是非常严重的,为了避免出现泄漏,请特别留意以下的规则:

1) 避免使用异步回调

这里写图片描述

2) 避免使用Static对象

因为static的生命周期过长,使用不当很可能导致leak,在Android中应该尽量避免使用static对象。

这里写图片描述

3) 避免把View添加到没有清除机制的容器里面

假如把view添加到WeakHashMap,如果没有执行清除操作,很可能会导致泄漏。

这里写图片描述

7. Location & Battery Drain

开启定位功能是一个相对来说比较耗电的操作,通常来说,我们会使用类似下面这样的代码来发出定位请求:

这里写图片描述

上面演示中有一个方法是setInterval()指的意思是每隔多长的时间获取一次位置更新,时间相隔越短,自然花费的电量就越多,但是时间相隔太长,又无法及时获取到更新的位置信息。其中存在的一个优化点是,我们可以通过判断返回的位置信息是否相同,从而决定设置下次的更新间隔是否增加一倍,通过这种方式可以减少电量的消耗,如下图所示:

这里写图片描述

在位置请求的演示代码中还有一个方法是setFastestInterval(),因为整个系统中很可能存在其他的应用也在请求位置更新,那些应用很有可能设置的更新间隔时间很短,这种情况下,我们就可以通过setFestestInterval的方法来过滤那些过于频繁的更新。

通过GPS定位服务相比起使用网络进行定位更加的耗电,但是也相对更加精准一些,他们的图示关系如下

这里写图片描述

为了提供不同精度的定位需求,同时屏蔽实现位置请求的细节,Android提供了下面4种不同精度与耗电量的参数给应用进行设置调用,应用只需要决定在适当的场景下使用对应的参数就好了,通过LocationRequest.setPriority()方法传递下面的参数就好了。

这里写图片描述

8. Double Layout Taxation

布局中的任何一个View一旦发生一些属性变化,都可能引起很大的连锁反应。例如某个button的大小突然增加一倍,有可能会导致兄弟视图的位置变化,也有可能导致父视图的大小发生改变。当大量的layout()操作被频繁调用执行的时候,就很可能引起丢帧的现象。

这里写图片描述

例如,在RelativeLayout中,我们通常会定义一些类似alignTop,alignBelow等等属性,如图所示:

这里写图片描述

为了获得视图的准确位置,需要经过下面几个阶段。首先子视图会触发计算自身位置的操作,然后RelativeLayout使用前面计算出来的位置信息做边界的调整的操作,如下面两张图所示:

这里写图片描述

这里写图片描述

经历过上面2个步骤,relativeLayout会立即触发第二次layout()的操作来确定所有子视图的最终位置与大小信息。

除了RelativeLayout会发生两次layout操作之外,LinearLayout也有可能触发两次layout操作,通常情况下LinearLayout只会发生一次layout操作,可是一旦调用了measureWithLargetChild()方法就会导致触发两次layout的操作。另外,通常来说,GridLayout会自动预处理子视图的关系来避免两次layout,可是如果GridLayout里面的某些子视图使用了weight等复杂的属性,还是会导致重复的layout操作。

如果只是少量的重复layout本身并不会引起严重的性能问题,但是如果它们发生在布局的根节点,或者是ListView里面的某个ListItem,这样就会引起比较严重的性能问题。如下图所示:

这里写图片描述

我们可以使用Systrace来跟踪特定的某段操作,如果发现了疑似丢帧的现象,可能就是因为重复layout引起的。通常我们无法避免重复layout,在这种情况下,我们应该尽量保持View Hierarchy的层级比较浅,这样即使发生重复layout,也不会因为布局的层级比较深而增大了重复layout的倍数。另外还有一点需要特别注意,在任何时候都请避免调用requestLayout()的方法,因为一旦调用了requestLayout,会导致该layout的所有父节点都发生重新layout的操作。

这里写图片描述

9. Network Performance 101

在性能优化第一季与第二季的课程里面都介绍过,网络请求的操作是非常耗电的,其中在移动蜂窝网络情况下执行网络数据的请求则尤其比较耗电。关于如何减少移动网络下的网络请求的耗电量,有两个重要的原则需要遵守:第一个是减少移动网络被激活的时间与次数,第二个是压缩传输数据。

1) 减少移动网络被激活的时间与次数

通常来说,发生网络行为可以划分为如下图所示的三种类型,一个是用户主动触发的请求,另外被动接收服务器的返回数据,最后一个是数据上报,行为上报,位置更新等等自定义的后台操作。

这里写图片描述

我们绝对坚决肯定不应该使用Polling(轮询)的方式去执行网络请求,这样不仅仅会造成严重的电量消耗,还会浪费许多网络流量,例如:

这里写图片描述

Android官方推荐使用Google Cloud Messaging,这个框架会帮助把更新的数据推送给手机客户端,效率极高!我们应该遵循下面的规则来处理数据同步的问题:

首先,我们应该使用回退机制来避免固定频繁的同步请求,例如,在发现返回数据相同的情况下,推迟下次的请求时间,如下图所示:

这里写图片描述

其次,我们还可以使用Batching(批处理)的方式来集中发出请求,避免频繁的间隔请求,如下图所示:

这里写图片描述

最后,我们还可以使用Prefetching(预取)的技术提前把一些数据拿到,避免后面频繁再次发起网络请求,如下图所示:

这里写图片描述

Google Play Service中提供了一个叫做GCMNetworkManager的类来帮助我们实现上面的那些功能,我们只需要调用对应的API,设置一些简单的参数,其余的工作就都交给Google来帮我们实现了。

2) 压缩传输数据

10. Effective Network Batching

在性能优化课程的第一季与第二季里面,我们都有提到过下面这样一个网络请求与电量消耗的示意图:

这里写图片描述

发起网络请求与接收返回数据都是比较耗电的,在网络硬件模块被激活之后,会继续保持几十秒的电量消耗,直到没有新的网络操作行为之后,才会进入休眠状态。前面一个段落介绍了使用Batching的技术来捆绑网络请求,从而达到减少网络请求的频率。那么如何实现Batching技术呢?通常来说,我们可以会把那些发出的网络请求,先暂存到一个PendingQueue里面,等到条件合适的时候再触发Queue里面的网络请求。

这里写图片描述

可是什么时候才算是条件合适了呢?最简单粗暴的,例如我们可以在Queue大小到10的时候触发任务,也可以是当手机开始充电,或者是手机连接到WiFi等情况下才触发队列中的任务。手动编写代码去实现这些功能会比较复杂繁琐,Google为了解决这个问题,为我们提供了GCMNetworkManager来帮助实现那些功能,仅仅只需要调用API,设置触发条件,然后就OK了。

11. Optimizing Network Request Frequencies

前面的段落已经提到了应该减少网络请求的频率,这是为了减少电量的消耗。我们可以使用Batching,Prefetching的技术来避免频繁的网络请求。Google提供了GCMNetworkManager来帮助开发者实现那些功能,通过提供的API,我们可以选择在接入WiFi,开始充电,等待移动网络被激活等条件下再次激活网络请求。

12. Effective Prefetching

假设我们有这样的一个场景,最开始网络请求了一张图片,隔了10秒需要请求另外一张图片,再隔6秒会请求第三张图片,如下图所示:

这里写图片描述

类似上面的情况会频繁触发网络请求,但是如果我们能够预先请求后续可能会使用到网络资源,避免频繁的触发网络请求,这样就能够显著的减少电量的消耗。可是预先获取多少数据量是很值得考量的,因为如果预取数据量偏少,就起不到减少频繁请求的作用,可是如果预取数据过多,就会造成资源的浪费。

这里写图片描述

我们可以参考在WiFi,4G,3G等不同的网络下设计不同大小的预取数据量,也可以是按照图片数量或者操作时间来作为阀值。这需要我们需要根据特定的场景,不同的网络情况设计合适的方案。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值