性能优化

一、关于性能优化的几处问题
1.是否应该手动调用System.gc()?
不应该。在Android 2.2 (API level 8)以及之前,当垃圾回收发生时,应用的线程是会被暂停的,这会导致一个延迟滞后,并降低系统效率。 从Android 2.3开始,添加了并发垃圾回收的机制,但是在GC开始和结束的时候仍然会阻塞主线程。
2.是否有必要调用Bitmap.recycle()方法?
Android3.0之前需要,Android3.0以及之后不需要。
在Android 2.3.3 (API level 10)以及之前, 一个Bitmap的像素级数据(pixel data)是存放在Native内存空间中的。 这些数据与Bitmap本身是隔离的,Bitmap本身被存放在Dalvik堆中。我们无法预测在Native内存中的像素级数据何时会被释放,这意味着程序容易超过它的内存限制并且崩溃。 自Android 3.0 (API Level 11)开始, 像素级数据则是与Bitmap本身一起存放在Dalvik堆中。
在Android 2.3.3 (API level 10) 以及更低版本上,推荐使用recycle()方法。 如果在应用中显示了大量的Bitmap数据,我们很可能会遇到OutOfMemoryError的错误。 recycle()方法可以使得程序更快的释放内存。在Android3.0(API level 11)之后,由于像素级数据与Bitmap一起存放到了堆中,其内存由GC管理,就不需要额外进行释放了。
3.所谓的最省内存读取Bitmap是否有效?

public static Bitmap readBitMap(Context context, int resId)  {  
     BitmapFactory.Options opt = new BitmapFactory.Options(); 
     opt.inPreferredConfig = Bitmap.Config.RGB_565; 
     opt.inPurgeable = true; 
     opt.inInputShareable = true; 
     //获取资源图片  
     InputStream is = context.getResources().openRawResource(resId); 
     return BitmapFactory.decodeStream(is, null, opt);      
 } 

据说这段代码是读取Bitmap最省内存的方法,自己经过测试之后发现确实省内存。比如,同一张图片直接使用imageView.setImageResource()可能需要3M,但用此方法可能只要2M内存就够了。但这个方法有一个弊端:对同一张图片使用ImageView本身的API去加载,系统本身会有缓存策略,比如我第一次加载需要3M,但第二次只需要0.2M就够了;而使用此方法的话第一次加载需要3M,第二次加载还是需要3M,想解决此问题必须手动的为其增加缓存策略。
二、提升性能的小技巧
1.使用优化的数据容器
利用Android Framework里面优化过的容器类,例如SparseArray, SparseBooleanArray, 与 LongSparseArray。 通常的HashMap的实现方式更加消耗内存,因为它需要一个额外的实例对象来记录Mapping操作。另外,SparseArray更加高效在于他们避免了对key与value的autobox自动装箱,并且避免了装箱后的解箱。
2.注意内存开销
对你所使用的语言与库的成本与开销有所了解,从开始到结束,在设计你的app时谨记这些信息。通常,表面上看起来无关痛痒的事情也许实际上会导致大量的开销。 例如:
* Enums的内存消耗通常是static constants的2倍。你应该尽量避免在Android上使用enums。
* 在Java中的每一个类(包括匿名内部类)都会使用大概500 bytes。
* 每一个类的实例花销是12-16 bytes。
* 往HashMap添加一个entry需要额一个额外占用的32 bytes的entry对象。
3.避免创建不必要的对象
* 当从已经存在的数据集中抽取出String的时候,尝试返回原数据的substring对象,而不是创建一个重复的对象。使用substring的方式,你将会得到一个新的String对象,但是这个string对象是和原string共享内部char[]空间的。
* 一组int数据要比一组Integer对象要好很多。可以得知,两组一维数组要比一个二维数组更加的有效率。同样的,这个道理可以推广至其他原始数据类型。
* 如果你需要实现一个数组用来存放(Foo,Bar)的对象,记住使用Foo[]与Bar[]要比(Foo,Bar)好很多。(例外的是,为了某些好的API的设计,可以适当做一些妥协。但是在自己的代码内部,你应该多多使用分解后的容易)。
4.选择static而不是Virtual
如果你不需要访问一个对象的值,请保证这个方法是static类型的,这样方法调用将快15%-20%。这是一个好的习惯,因为你可以从方法声明中得知调用无法改变这个对象的状态。
5.常量声明为Static Final
考虑下面这种声明的方式

static int intVal = 42;
static String strVal = "Hello, world!";

编译器会使用一个初始化类的函数,然后当类第一次被使用的时候执行。这个函数将42存入intVal,还从class文件的常量表中提取了strVal的引用。当之后使用intVal或strVal的时候,他们会直接被查询到。

 static final int intVal = 42;
 static final String strVal = "Hello, world!";

这时再也不需要上面的方法了,因为final声明的常量进入了静态dex文件的域初始化部分。调用intVal的代码会直接使用42,调用strVal的代码也会使用一个相对廉价的“字符串常量”指令,而不是查表。Notes:这个优化方法只对原始类型和String类型有效,而不是任意引用类型。不过,在必要时使用static final是个很好的习惯。
6.避免内部的Getters/Setters
像C++等native language,通常使用getters(i = getCount())而不是直接访问变量(i = mCount)。这是编写C++的一种优秀习惯,而且通常也被其他面向对象的语言所采用,例如C#与Java,因为编译器通常会做inline访问,而且你需要限制或者调试变量,你可以在任何时候在getter/setter里面添加代码。
然而,在Android上,这不是一个好的写法。虚函数的调用比起直接访问变量要耗费更多。在面向对象编程中,将getter和setting暴露给公用接口是合理的,但在类内部应该仅仅使用域直接访问。
在没有JIT(Just In Time Compiler)时,直接访问变量的速度是调用getter的3倍。有JIT时,直接访问变量的速度是通过getter访问的7倍。
7.使用增强for循环
增强型for循环(也被称为for-each循环)可以用于去遍历实现Iterable接口的集合以及数组,这是jdk 1.5中新增的一种循环模式。当然除了这种新增的循环模式之外,我们仍然还可以使用原有的普通循环模式,只不过它们之间是有效率区别的,我们来看下面一段代码:

static class Counter { 
    int mCount; 
} 

Counter[] mArray = ... 

public void zero() { 
    int sum = 0; 
    for (int i = 0; i < mArray.length; ++i) { 
        sum += mArray[i].mCount; 
    } 
} 

public void one() { 
    int sum = 0; 
    Counter[] localArray = mArray; 
    int len = localArray.length; 
    for (int i = 0; i < len; ++i) { 
        sum += localArray[i].mCount; 
    } 
} 

public void two() { 
    int sum = 0; 
    for (Counter a : mArray) { 
        sum += a.mCount; 
    } 
}  

可以看到,上述代码当中我们使用了三种不同的循环方式来对mArray中的所有元素进行求和。其中zero()方法是最慢的一种,因为它是把mArray.length写在循环当中的,也就是说每循环一次都需要重新计算一次mArray的长度。而one()方法则相对快得多,因为它使用了一个局部变量len来记录数组的长度,这样就省去了每次循环时字段搜寻的时间。two()方法在没有JIT(Just In Time Compiler)的设备上是运行最快的,而在有JIT的设备上运行效率和one()方法不相上下,唯一需要注意的是这种写法需要JDK 1.5之后才支持。
但是这里要跟大家提一个特殊情况,对于ArrayList这种集合,自己手写的循环要比增强型for循环更快,而其他的集合就没有这种情况。因此,对于我们来说,默认情况下可以都使用增强型for循环,而遍历ArrayList时就还是使用传统的循环方式吧。
8.尽量采用int类型
Android系统中float类型的数据存取速度是int类型的一半,尽量优先采用int类型。
9.使用库函数
除了那些常见的让你多使用自带库函数的理由以外,记得系统函数有时可以替代第三方库,并且还有汇编级别的优化,他们通常比带有JIT的Java编译出来的代码更高效。典型的例子是:Android API 中的 String.indexOf(),Dalvik出于内联性能考虑将其替换。同样 System.arraycopy()函数也被替换,这样的性能在Nexus One测试,比手写的for循环并使用JIT还快9倍。
10.避免过度绘制
Overdraw,多度绘制会浪费很多的CPU、GPU资源,例如系统默认会绘制Activity的背景,而如果再给布局绘制了重叠的背景,那么默认Activity的背景就属于无效的过度绘制。
11.优化布局层级
在Android中,系统对View进行测量、布局和绘制时,都是通过View树的遍历来进行操作的。如果一个View树的高度太高,就会严重影响测量、布局和绘制的速度,因此在布局时尽量降级View树的高度。Google也在其API文档中建议View树的高度不宜超过10层。
12.RelativeLayout和LinearLayout
在性能方面LinearLayout会比RelativeLayout要好一点,但对于复杂布局使用LinearLayout容易造成View层级过深,并且LinearLayout中如果使用了weight属性性能也会降低。因此要结合实际情况来决定使用什么布局。
13.include、merge和ViewStub
include标签可以重用布局。
include标签在重用布局的时候有可能产生多余的布局嵌套,此时就可以使用merge标签。merge标签是作为include标签的一种辅助扩展来使用的,它的主要作用是为了防止在引用布局文件时产生多余的布局嵌套。
ViewStub是一个轻量的视图,可以实现按需加载视图的功能。有的时候我们会遇到这样的场景,就是某个布局当中的元素非常多,但并不是所有元素都一起显示出来的,而是普通情况下只显示部分常用的元素,而那些不常用的元素只有在用户进行特定操作的情况下才会显示出来,此时就可以使用ViewStub动态加载那些不常用的元素。不过需要注意的是ViewStub所加载的布局是不可以使用merge标签的。
14.AsyncTask和子线程
对于某些耗时操作(比如对本地文件、数据库和网络的操作)应该放到子线程中去做。
15.谨慎导入第三方类库
很多开源的library代码都不是为移动网络环境而编写的,如果运用在移动设备上,这样的效率并不高。当你决定使用一个第三方library的时候,你应该针对移动网络做繁琐的迁移与维护的工作。
即使是针对Android而设计的library,也可能是很危险的,因为每一个library所做的事情都是不一样的。例如,其中一个lib使用的是nano protobufs, 而另外一个使用的是micro protobufs。那么这样,在你的app里面就有2种protobuf的实现方式。这样的冲突同样可能发生在输出日志,加载图片,缓存等等模块里面(我们的框架里目前就有此问题,导入的Picasso和Volley框架都有自己的LruCache,导致浪费大量的内存)。
同样不要陷入为了1个或者2个功能而导入整个library的陷阱。如果没有一个合适的库与你的需求相吻合,你应该考虑自己去实现,而不是导入一个大而全的解决方案。

参考:
1.官方文档:高效显示BitmapAndroid性能优化
2.书籍:Android开发艺术探索、Android群英传
3.Android最佳性能实践三——高性能编码优化
4.Android最佳性能实践四——布局优化

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值