性能编程

众所周知,java是面向对象编程语言,正因为其动态性和灵活性,可以构建出低耦合系统,但是其性能却不如面向过程的编程的方式,一套设计原则的实现需要创建很多过程对象,这些对象在创建,和销毁都比较耗费性能,另外对象过多占用内存也是一种性能上负担。尽量做好在满足架构设计高内聚低耦合的特性下,节约性能。

由于Android系统运行于资源和内存有限的手机上,应用程序为了保证体验的流畅性,应当从点点滴滴的性能节约上做起,九层之台,起于累土。在编程中有一些通用的原则可以有益于提升性能,大体上分为两个层面,基于编程语言的提升,以及基于平台的API提升。

基于语言的:实现一个功能的算法,尽可能的降低代码运行时的时间复杂度和空间复杂度,使用合适的数据结构以及策略,坚决去除无用的步骤。

基于平台的:Android 平台有很多API,只要使用得当就能有很好的效果,要遵循平台原则性的编程指导。纵然有时候不遵循,也不会出现严重的bug,但是小毛病累计多了,日积月累性能底下,经常发生莫名其妙的ANR。

1.数据容器

下面是一个常见的集合遍历方式:

List datas =newArrayList();

for(inti =; i

AccessControlContext context =getContext();

context.checkPermission(newRuntimePermission(datas.get(i)));

}

在这里datas.size()和getContext()在每次循环中都会使用并且每次结果都是一样的,如果getContext()方法是一个很复杂的耗费资源的方法,那么每次循环将会损耗很多性能。这种情况,可以将不变的对象移出循环,在输入量非常大的时候,上面的修改带来的提升是显著的。

AccessControlContext context =getContext();

intsize = datas.size();

for(inti =; i

context.checkPermission(newRuntimePermission(datas.get(i)));

}

以下是迭代器,while循环和普通for循环遍历数据的实验对比:

纵轴为消耗时间,横轴为输入规模,可见在这三种循环中普通for循环速度最快,Andorid系统中只要是循环编码,大多使用for循环,很少使用其他,比如消息队列中的循环就很典型。另外增加for循环比一般for循环又更快些。

若在数据容器的选择中,满足功能需求的情况下,既可以使用数组,也可以使用集合,优先推荐使用数组,因为可变灵活的集合底层实现仍然是数组,集合的扩容功能是建立子数组复制和扩容算法的基础上,性能相比较数组低很多。 在某些情况下不得不适用集合,对将要使用的数据结构在常态下可能的元素个数有着预期,建议在初始化集合时带上容量参数。集合合内部的数据结构实现是数组,集合拥有动态扩容的特性,原理是在每次添加集合或者元素的时候去检测内部的数组集合能否装下这些元素,若无法装下这些新元素,则会创建一个新的更大容量的数组,然后将旧数组的元素复制过来。以下是集合的部分源码摘取:

public booleanadd(Ee) {

ensureCapacityInternal(size+1);//每次添加元素都会检测数组容量

elementData[size++] = e;

return true;

}

private voidensureExplicitCapacity(intminCapacity) {

modCount++;

// 若超出容量则进行扩容

if(minCapacity -elementData.length>)

grow(minCapacity);

}

private voidgrow(intminCapacity) {

// overflow-conscious code

intoldCapacity =elementData.length;

intnewCapacity = oldCapacity + (oldCapacity >>1);

if(newCapacity - minCapacity

newCapacity = minCapacity;

if(newCapacity -MAX_ARRAY_SIZE>)

newCapacity =hugeCapacity(minCapacity);

// minCapacity is usually close to size, so this is a win:

elementData= Arrays.copyOf(elementData, newCapacity);

}

扩容是个耗费资源的操作,尽可能的减少这样的操作可以提高性能,这就要求在编程的时候,尽可能的构建明确容量的集合。

另外数组的复制底层用的时c方法 :

@FastNative

public static native voidarraycopy(Object src,intsrcPos,

Object dest,intdestPos,

intlength);

这个方法比一般的数组遍历然后添加元素的方式来复制快的多,在考虑数组复制的时候也可以用这样的方法。

在Android平台中使用集合时,Sparse家族集合比java原生提供的数据容器效率要高,将整形key值单独存储,这样后面获取是通过二分搜索的方式获取,能使用sparseArray,就使用sparseArray,该数据集合比jdk提供的集合效率要高。

在不是特别强调数据检索的情况下,更为灵活的数据容器是链式存储结构,将对象数据用引用连接的方式进行关联,不存在数据的扩容和复制问题,扩展性强,Android系统中消息类Message就是这种设计方式。

2.Java语言

使用位移操作符来代替标量计算,在Android系统的源码中非常普遍,位移操作符有很多好处。第一点是数据空间大,一个整形32位二进制,可操作空间大;第二点是运算效率高,据一些资料给出的数据,能提高50%的效率。基本运算方式是将整形数字转化为32位二进制,左右移动增0删0,逻辑与或真假变成1或0。

左移运算符

右移运算符>>:若值为正,则在高位插入0;若值为负,则在高位插入1。如13的二进制形式为:0000 0000 0000 0000 0000 0000 0000 1101 ,则13>>2的结果为 00 0000 0000 0000 0000 0000 0000 0000 11 结果为3 。可见右移有时候不一定是除以2。

与运算符&:两个操作数中位都为1,结果才为1,否则结果为0。

或运算符|:两个位只要有一个为1,那么结果就是1,否则就为0。

非运算符~:如果位为0,结果是1,如果位为1,结果是0。

常量字段尽量使用static final修饰,final不会带来性能上的提升,但是会帮组编译器优化代码。

避免使用浮点数,在android设备中,浮点型运算比整形慢两倍。

避免基本类型频繁自动装箱拆箱,如下的代码:

inal inti =newInteger(1) +1;

对基本类型1进行了装箱,执行加法操作时又进行了拆箱,复制给i时又进行了装箱,这样的操作时非常的耗费性能,尤其是在可以大数据量的循环中这样操作。使用基本类型完成操作将是好的选择。

对象字段访问速度。对象的字段直接访问的方式要比通过setter,getter方法访问块三倍,当然大多数时候,面向对象编程,为了保证对象功能的封闭性和不可更改性,又或者满足某些框架的需求不得不适用getter setter方法,但是在能直接访问对象字段的时候尽量直接访问,比如在对象内部,访问对象的字段,直接使用字段即可避免通过getter方法去访问。

switch语法比if else语法块,因为switch选择值之后直接跳转到那个特定的分支,但是if else要进行顺次遍历if else比前者使用的类型更多,更灵活。

在流的操作上尽量使用缓存流来进行操作。缓存流的速度要比最原始的流快很多,原因是每次进行IO操作,都要从用户态转入内核态,由内核把数据从磁盘中读到内核缓冲区,再由内核缓冲区到用户缓冲区,如果没有缓冲区,无法预加载,读取都需要从用户态到内核态切换。比缓冲流更快的是内存映射的方式mmp,其速度读取文件的速度与从内存中访问对象的速度相当,java的NIO编程接口中有其相关的上层接口类,淡然也可以直接操作C++的mmp函数来实现。

3.多线程

尽可能少的使用同步机制,多线程访问共同资源会阻塞等待,比如当一个同步方法在同时被一个异步线程和UI线程访问时,有一定几率会导致UI线程阻塞等待,如果异步线程执行任务因为某些原因长时间不释放锁,甚至产生死锁,很容易导致ANR异常。尽量避免UI线程参与共同资源的竞争。同步锁的使用本身非常耗资源,有时候会比锁定范围里的代码执行时间长,因为线程的同步最终要从用户态转换到内核态进行操作。一次状态的转换需要耗费很多处理器时间。

线程同步锁定的代码范围尽可能的小。线程同步本质上是对主内存该字段资源的独占,其他线程在独占期间无法问,处于阻塞状态,独占的范围越小越好。

并发编程中,为了保证对共享资源操作的原子性,如果满足一个资源只有一个线程对其修改,而其他线程都是读取的操作的条件,那么可以使用vilatile字段来修饰,该字段具有线程间可见性和禁止指令重排序的特性,每次获取值是都会从主内存刷新最新值回来,并且性能比同步锁高,但是如果涉及到多个线程修改值,还是只能用同步字段来控制。

尽可能使用原子引用来解决,比如AtomicIntegerAtomicBoolean等类来实现,这些来该系列方法的操作是处理器的cas指令无条件内联,相比同步关键字,快很多。

字符串的复杂拼凑一般用Stringbuffer或者Stringbuilder,前者是线程安全的,后者是线程不安全的,因此后者速度更快,单线程环境中使用后者更好。

4.内存

内存分配很昂贵,而且释放也很昂贵,当应用的内存快达到上限,整个应用程序的性能会变得非常低,用户体验的滞后。应当尽可能少的创建使用频繁的对象,减少很少使用的对象的创建。一些常见的设计模式可以帮助我们节约内存。

单利模式,对象池设计模式以及享源模式都是复用对象的设计模式。复用对象的例子在安卓系统中,最为典型的就是message类的复用,该类使用了对象池的设计模式,在事件驱动系统中,消息的创建非常的频繁,如果不断的创建新的消息对象不加以复用,那么内存负担压力降很大频繁发生GC也会导致卡顿。

使用共享内存文件MemoryFile,该类是android系统提供的匿名共享内存接口,底层使用mmp内存文件映射,使用的内存是内核控件的内存,并且在Android 4.4以下,这里应用内存不计入app。一些图片库在内存处理上使用了该文件。在binder通信的编程中,一个应用通过binder代理传递的数据,包括系统的的数据传递总和不能超过IM,否则会抛异常。除了使用分批传递数据的策略外,还可以使用MemoryFile来传递数据,但是要小心内存上限。

不要频繁GC,频繁GC垃圾线程做回收,会使进程内的线程停滞导致卡顿。

android开发中尽量少使用枚举,使用@IntDef注释和整形常量结合来代替枚举,枚举比整形占据更多的内存。

String string = new String("example”);

以上的代码中,创建了两个对象 string 和”example”,直接使用字符串来初始化更合适。

尽量不要用静态变量持有一些大对象,因为静态变量的生命周期与应用的生命周期一样长,容易造成内存泄漏。如果不得不用静态,我们需要在相关组件生命周期结束时将对象置空,但是此处处理有时候可能会造成空指针异常。

此外非静态内部类或者匿名内部类非常容易引起内存泄漏,我们经常使用它们是因为可以很方面的使用外部的的成员方法和属性,但也正因为如此,造成内存泄漏,一个比较常见的例子是AsyncTask内的使用,在有些时候当Activity结束时Asynctask任务并没结束,因此Activity对象没有被回收,直到任务结束;同样的类似比如handler的任务执行,无论是匿名内部类runnable还是handlermessage方法都会造成短暂内存Activity对象短暂泄漏。如果在onDestroy方法中进行了成员变量的置空回收,那么当这些延时回调回来的时候可能会造成空指针异常。又比如当设备配置发生变化屏幕旋转,Activity会销毁重新创建,视图不存在,极有可能发生空指针异常。为避免这种情况,我们需要将内部类声明为静态类,如果需要使用到Activity实例,使用弱引用来包裹Activity实例,利用弱引用在gc回收时,弱引用里的对象会被直接回收。因此在时候用弱引用对象时,每次都需要进行判空。

单例对象的回调一定要进行释放,比如单例对象的回调实现类是Acitivty,在onDestroy中一定要进行注销,因为单例对象本质是个静态对象,生命周期与应用周期一样长。同理在onDestroy时,handler需要移除之前发送的任务,异步任务需要取消。

service使用完要手动关闭,因为service不会自动关闭,无用的service占用内存是不合理的,开发者可以调用stopSelf 或者stopService来停止service,service的过多存在也会影响系统分配资源,系统对进程的资源分配遵循一定的优先级,service会提升进程的优先级。推荐使用Intentservice,任务的执行不仅是异步队列不会阻塞主线程,而且任务执行完毕后自动关闭。

若应用的内存已经相当吃紧,可以使用多个私有进程的方式变相的增加应用内存。

5.ANR异常

ANR异常见之前发的文章

优化ANR异常

6.平台相关

流畅的UI交互,每帧的渲染时间是16毫秒,60FPS,如果一帧的绘制时间过长,系统会强制跳跳帧,用户会感觉界面卡顿,因此布局的优化十分重要,除了避免过渡渲染问题,在布局时层级扁平化,不要生层次嵌套,尽量使用相对布局或者约束布局实现UI,因为深层次的布局,在代码加载视图资源时要经过非常多层的遍历才能找到相应的控件。慢渲染如果出现一帧的渲染时间超过700毫秒,便是冻帧现象。

电量损耗是个不容忽视的问题。客户端相隔一定时间主动轮询某些服务,轮训的弊端在于,不仅增加看了服务端的负载,客户端电量的消耗,流量的消耗,而且很多次轮训都是无效的操作。可以改为其他方式,比如推送,因为这是个耗费资源耗费电量的操作,在客户端有长连接心跳的情况下,心跳包的间隔尽可能的大,如果条件允许可以使用系统级别的推送,google的GCM来完成推送的功能。

只有在应用需求的情况下,才使用局部唤醒锁,不需要的时候及时关闭局部唤醒锁。局部唤醒锁的作用是在屏幕关闭后或者是设备关闭后继续保持CPU运行的的机制,长期持有局部唤醒锁,会阻止系统进入低功耗状态。同样的AlarmManager的定时机制锁也要在不需要时及时释放。高版本的系统,定时条件调度任务可以使用JobScheduler,该方案对不同的系统版本做了不同的兼容,并且对局部唤醒锁做了处理。如果一定要使用局部唤醒锁,请一定确保锁在不需要时释放。

voiddoSomethingAndRelease()throwsMyException{

try{

mWakeLock.acquire();

doSomethingThatThrows();

}finally{

mWakeLock.release();

}

}

序列化的对象parcelable对象比serialiable对象的效率高很多,与Android的共享内存有关,因此在intent之间传递数据时,尽量用前者。

图片的处理尽量使用标准的三级缓存库,若要单独处理图片,比如缩放与压缩,要在异步线程中操作避免阻塞主线程。

APK的大小有一半是由资源大小决定的,尤其是图片,因此图片要尽可能的小,并且能压缩尽量压缩,可以换用轻量级的图片,比如webpy格式或者vector格式,前者webpy格式比一般格式的图片要小很多,并且随着图片越大,差距也越大,但显示效果基本一样。vecotr本质是图片描述文件,运行时渲染,因此不存在设备适配问题,不需要配置多个文件,但是不能显示实景图,这是局限性。

数据的序列化问题。主流的数据传输格式主要有xml和json,由于xml标签冗余,相比json,传输同样的数据量要偏大,因此现在主流公司里基本都用Json。而json的解析工具市面也有多种,性能也各有不同,一般fastjson的性能比gson要好,编解码块,jackson的注解支持非常棒,性能也不错。但是在同样的数据传输中,无论是从数据量的大小还是从编解码的速度protobuffer都是最快的,下面是测试数据对比:

但是protobuffer也有自身的局限性,数据无法体现描述模型语意,除非能拿到定义的proto文件,但在我看不来这点也有好处,给数据抓包增加了难度。

列表视图刷新是一个常见的操作,recyclerview相比较listview特性众多,其中最具特色的便是局部刷新系列api。基本的原则是,若每个item所对应的数据没有发生不改变,那么对应的视图将不刷新,这个不刷新包含两个方面,一是视图不再重新渲染,二是数据与视图不需重新绑定。如果在数据与视图绑定是做了非常多的操作,那么局部刷新将会提高很多性能。新版的androidx包中AsyncPagedListDiffer工具类在异步线程中比较新老数据的异同,然后汇总一个最终的集合进行局部刷新,使用这个工具可以方便的实现自动刷新功能。

以上是笔者的一些经验,由于水平有限,性能的提上方面还有很多值得琢磨的地方,尽管这些东西非常零碎,但是只要我们做好这些,力求细节完美,做出来的应用一定非常流畅,bug少。

  • 发表于:
  • 原文链接:https://kuaibao.qq.com/s/20181110G0ZC6900?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。

扫码关注云+社区

领取腾讯云代金券