2013年11月24日

Android性能优化

最近学习了一下Android开发中的性能优化,文中列出的大部分优化技术都来自于对Best Practices for Performance《Android应用性能优化》的学习总结。

大家可能对服务器端的Java的性能优化都有比较丰富的经验了,但是由于移动设备的硬件性能还是远远落后于服务器的,因此很多在开发服务器上运行的应用时不需要考虑的问题到了移动设备上就变成了必须要关注的首要问题。今天主要介绍一下使用Java开发Android应用时所需要关注的一些性能方面的问题。

先来比较一下服务器和移动设备在硬件上的差异:
现在一台普通的服务器配置可能是:16核CPU,2.7G主频,48G内存
而前不久刚刚发布的Nexus 5的配置是:4核CPU,2.2G主频,2G内存,2300mAh电池

Nexus 5是目前相对很高端的Android设备了,但是和服务器的CPU和内存比起来就弱爆了。服务器CPU核心数是Nexus 5的4倍, 内存更是Nexus 5的24倍。因此一些在服务器上可能会碰到的问题比如因为heap太大导致GC过慢,在移动设备上是不太可能发生的。

再来看一下电池,Nexus 5配备了一块2300mAh的电池,可以有17小时的通话时间。尽管移动设备配备的电源容量越来越大,但终归是有限的容量,屏幕显示、网络传输都是耗电大户,如果APP太耗电,用户可能就直接卸载你的APP了。而服务器呢?一般情况下我们都不需要担心服务器的电源问题,有时可能需要考虑服务器断电,但是基本不会去想着如何为机房省电,而这在开发移动应用是必须要考虑的一个因素。

再来看看移动应用的运行环境。 Android设备的操作系统是Linux,因此可以使用各种语言来开发Android应用,今天我们主要关注使用Java开发的APP的运行环境。
对于使用Java开发的APP来说,程序也是运行在jvm中运行的,这个jvm是google为Android平台定制的,叫做dalvik。
当然从Android 4.4开始,引入了ART,在不久的将来预计会替代dalvik。ART会在应用安装时就把字节码编译成机器码,成为一个真正的本地应用,完全不需要虚拟机了。

不过今天还是先来了解一下dalvik,在4.4中,它依旧还是默认的运行环境。它有以下几个主要特点

  • 加载dex文件,java文件会首先编译成class,然后把所有class文件编译成一个dex文件,dex还可以进一步优化成odex文件
  • 由Zygote统一管理,多个dalvik进程可以共享一些framework的代码和资源
  • 支持并发GC(2.3+)
  • 支持JIT(2.2+)

和通常的jvm相比,dalvik指令集是基于寄存器的,而通常的jvm是基于栈的,因此dalvik具有以下优点

  • 编译时提前优化,dex整合了多个class文件中的重复信息,对冗余的部分做全局优化,使得dex体积更小
  • 执行更快

当然jvm也有自己的优点

  • 更好的可移植性,基于寄存器的指令集需要特定的硬件支持
  • 指令集简单

可以来看一下class和dex字节码的比较:

java:
public long add(long a, long b) {
  return a + b;
}

class:
0: lload_1
1: lload_3
2: ladd
3: lretuen

dex:
0df438:9b00 0305  |0000: add-long v0, v3, v5
0df43c:1000       |0002: return-wide v0

在开发应用时,大致可以从代码、内存、电源和布局这四个方面来对应用做相应的优化,代码和内存使用的优化,在服务器端开发时应该已经接触过不少了,而电源和布局优化则是在开发移动应用所特有的。

代码优化

  • 减少不必要的对象创建
    越少的对象创建,越少的GC。
    特别要避免Java的自动装箱,能使用基本类型完成的计算就不要使用对象。
  • 减少虚方法调用, 特别要避免类内部的getter/setter调用
    在类内部直接访问实例变量
    使用ProGuard可以在build时自动inline getter/setter调用
  • 用package访问级别代替private

    public class Foo {
    private class Inner {
        void a() {
            Foo.this.b(Foo.this.v);
        }
    }
    private int v;
    public void run() {
        Inner in = new Inner();
        v = 1;
        in.a();
    }
    private void b(int v) {}
    }

    在上面这个类中,尽管Java的语法允许在Foo的内部类Inner中直接引用Foo.this.bFoo.this.v的,但是在jvm中直接访问private属性或方法是非法的,因此编译生成的字节码会把直接引用替换成方法调用。
    首先Foo.classs会多出两个静态方法用于访问private属性和方法

    static int access$000(com.example.android_demo.Foo);
    Code:
    0:   aload_0
    1:   getfield        #2; //Field v:I
    4:   ireturn
    
    static void access$100(com.example.android_demo.Foo, int);
    Code:
    0:   aload_0
    1:   iload_1
    2:   invokespecial   #1; //Method b:(I)V
    5:   return

    然后在Inner中对private属性的访问和方法调用被替换成对以上两个方法的调用

    void a();
    Code:
    0:   aload_0
    1:   getfield        #2; //Field this$0:Lcom/example/android_demo/Foo;
    4:   aload_0
    5:   getfield        #2; //Field this$0:Lcom/example/android_demo/Foo;
    8:   invokestatic    #4; //Method com/example/android_demo/Foo.access$000:(Lcom/example/android_demo/Foo;)I
    11:  invokestatic    #5; //Method com/example/android_demo/Foo.access$100:(Lcom/example/android_demo/Foo;I)V
    14:  return

    这种代码即使编译成dex,也不能得到优化,会依然保留这些方法调用,从而降低性能。

    000794:                 |[000794] com.example.android_demo.Foo.Inner.a:()V
    0007a4: 5420 0100       |0000: iget-object v0, v2, Lcom/example/android_demo/Foo$Inner;.this$0:Lcom/example/android_demo/Foo; // field@0001
    0007a8: 5421 0100       |0002: iget-object v1, v2, Lcom/example/android_demo/Foo$Inner;.this$0:Lcom/example/android_demo/Foo; // field@0001
    0007ac: 7110 0b00 0100  |0004: invoke-static {v1}, Lcom/example/android_demo/Foo;.access$000:(Lcom/example/android_demo/Foo;)I // method@000b
    0007b2: 0a01            |0007: move-result v1
    0007b4: 7120 0c00 1000  |0008: invoke-static {v0, v1}, Lcom/example/android_demo/Foo;.access$100:(Lcom/example/android_demo/Foo;I)V // method@000c
    0007ba: 0e00            |000b: return-void

    对于以上这种情况,只要把private访问级别改成package访问就可以减少两次方法调用了。

    void a();
    Code:
    0:   aload_0
    1:   getfield        #2; //Field this$0:Lcom/example/android_demo/Foo;
    4:   aload_0
    5:   getfield        #2; //Field this$0:Lcom/example/android_demo/Foo;
    8:   getfield        #4; //Field com/example/android_demo/Foo.v:I
    11:  invokevirtual   #5; //Method com/example/android_demo/Foo.b:(I)V
    14:  return
  • 利用多个核心
    现在很多移动设备的CPU核心数都已经是两个甚至四个了,对于一些计算工作可以让多个核心来完成,使UI线程响应更快。

内存管理

很多Android设备的内存已经是2G了,和个人电脑比起来也差不了多少,但是Android是没有虚拟内存的,这意味着应用更加容易导致内存不足,因此在使用内存时需要更加小心。

  • 监听onTrimMemory事件(API 14+)
    通过监听onTrimMemory或onLowMemory,可以在内存紧张时释放非关键性的资源比如缓存等,降低应用被kill的几率
  • 优化图片加载
    从Android 3.0开始,图片需要的内存是在虚拟机的heap里分配的,从而能够被更好的被垃圾收集器管理,提高了性能。
    但是在显示图片时还是需要对图片做一些处理,比如裁减图片到合适尺寸用于显示,使用LRU缓存最近显示的图片,使用引用计数管理图片的使用情况等。
    具体可以参考Displaying Bitmaps Efficiently了解如何在Android中更好的显示图片。
  • 使用优化过的容器类
    在key是int或long型的时候,使用SparseArray和LongSparseArray替代HashMap,因为HashMap的key必须是对象,Java的自动装箱会产生大量对象。
  • 使用恰当的数据类型
    目前大部分Android设备的CPU都是32位的,使用int产生的指令会比使用short和long的更少。
    short类型的数组排序会比其他类型的快一个数量级,因为使用了计数排序
  • 减少抽象
    抽象可以带来灵活性和可维护性,但是抽象会额外的增加类层次和代码量,程序代码也是要加载到内存中占用内存的。因此代码越精简,占用的内存就越少。
  • 不要使用依赖注入
    依赖注入在web框架中使用的非常广泛,但是使用了依赖注入的框架需要更久的初始化,在初始化时需要扫描更多的类,即使这些类并未被使用,在扫描时也会被载入到内存中,从而导致更多的内存占用。
  • 使用StrictMode(2.3+)在开发时检查内存泄漏,耗时调用等
  • 使用ProGuard在build时清理、混肴代码,减少代码体积

电源管理

  • 获取电源信息
    SDK提供了多种方法用来获取电源信息,比如是否正在充电,充电方式是USB还是AC适配器,以及当前电量等。
    应用可以监听自己感兴趣的事件,比如当连接上外部电源的时候,可以加大刷新数据的频率、加大传输速率等;当电量很少的时候,减少数据更新的频率。
  • 管理广播接收器
    应用不在前台的时候,很多事件都不需要处理,应该及时禁用广播接收器,重新回到前台时再重新打开接收器
  • 检查网络
    不同网络环境的传输速率差别非常大,GPRS可能只有100Kb每秒,而Wi-Fi可以达到几十M每秒。
    同样一个文件,在GPRS下下载会比在Wi-Fi下下载耗费更多的电量,因此在显示图片时,可以根据网络环境的不同下载不同压缩比的图片。
    对于实时的搜索请求,也可以根据网络的不同调整请求的频率。
  • 定位服务,传感器使用
    使用恰当的通知频率,权衡用户体验和省电
  • WakeLock
    播放视频时使用带超时的WakeLock,防止WakeLock没有被关闭

布局优化

  • 减少布局层次
    过深的布局层次会严重影响应用的性能,在低版本的系统上可能就直接崩溃了。可以从几个方面来减少布局层次

    • 嵌套的LinearLayout会深化布局层次,特别是LinearLayout只是用来起定位的话就更加没有意义,应该合理的使用RelativeLayout取代LinearLayout。
    • 使用Merge合并布局,使用merge取代顶层的Layout,可以减少一层布局

    下面来看一个例子,对于如下如下的界面

    先来看一下使用LinearLayout实现的布局

    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
            android:layout_width="match_parent"
            android:layout_height="wrap_content">
    <LinearLayout android:layout_width="match_parent"
                  android:layout_height="match_parent"
                  android:orientation="vertical">
            <TextView android:text="title2"
                      android:gravity="center"
                      android:layout_width="match_parent"
                      android:layout_height="wrap_content"></TextView>
            <LinearLayout android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:orientation="horizontal">
                <EditText android:id="@+id/edit"
                          android:layout_width="0px"
                          android:layout_height="wrap_content"
                          android:layout_weight="0.82"></EditText>
                <Button android:text="btn"
                        android:layout_width="0px"
                        android:layout_height="wrap_content"
                        android:layout_weight="0.18"></Button>
            </LinearLayout>
    </LinearLayout>
    </RelativeLayout>

    直观的布局层次可以看下图

    现在来看一下使用RelativeLayout和Merge优化后的布局:

    <merge xmlns:android="http://schemas.android.com/apk/res/android">
    <RelativeLayout android:layout_width="match_parent"
                  android:layout_height="wrap_content">
        <TextView android:text="title1"
                  android:gravity="center"
                  android:id="@+id/text"
                  android:layout_width="match_parent"
                  android:layout_height="wrap_content"></TextView>
        <Button android:id="@+id/btn"
                android:text="btn"
                android:layout_width="60dp"
                android:layout_height="wrap_content"
                android:layout_below="@id/text"
                android:layout_alignParentRight="true"></Button>
        <EditText android:id="@+id/edit"
                  android:layout_width="match_parent"
                  android:layout_height="wrap_content"
                  android:layout_below="@id/text"
                  android:layout_toLeftOf="@id/btn"></EditText>
    </RelativeLayout>
    </merge>

    可以看到布局从原来的四层减少到了两层

    再来比较一下这两个布局的渲染时间

    可以看到Measure和Layout的性能有了显著提升,Measure的时间从原来的6.2ms减少到了0.8ms,可见在使用LinearLayout时,用于Measure的时间是相当可观的,Layout的时间也从0.2ms减少到了0.06ms。

  • 重用布局 和代码一样,对于可以重用的布局,也应该尽量使用include重用这些布局,减少冗余的布局代码

  • ViewStub
    使用ViewStub可以实现view的延迟加载,ViewStub引入的布局既不会显示也不会占用位置,可以在解析layout时节省cpu和内存从而提高性能

  • ViewHolder
    在ListView中,系统会重用每个item的view,在ViewHolder中保持对控件的引用可以避免每次都去查找

以上列出的这些优化技巧,虽然大部分都只能带来一些微小的性能提升,但是如果用在日常的实践中,积少成多,也会对性能有很大的影响。

但是由于业务逻辑的复杂性,应用往往不可避免的还会出现性能瓶颈。此时就需要一些额外的工具来帮助我们分析应用的性能瓶颈, 再来做针对性的优化。
目前Android平台主要提供了如下3个工具用于分析应用的性能瓶颈:
1. Traceview
2. Systrace
3. Oprofile
Android系统性能调优工具介绍这篇文章中比较全面的介绍了这三个工具的基本用法。

以上主要介绍的是一些通用的性能优化点,由于Android的硬件环境非常多样,拿屏幕尺寸来说,现在的各种盒子主要是连接到电视机上的,屏幕可以大到四、五十寸,将来可能非常流行的各种可穿戴设备如手表、眼镜等,屏幕可能不到1英寸,因此针对不同的设备,也有必要需要做一些特殊的优化。比如开发盒子应用时就可以参考Android Developers:针对电视优化布局