Android应用优化

Android应用优化主要从两方面来考虑,其一是针对内存的优化,Android设备的内存相比较而言是比较珍贵,应及时回收不再使用的内存,防止内存泄露;其二是针对性能的优化,防止用户使用是出现卡顿,响应慢或ANR。

性能调优Android官方有指导性的文档,以及相关的调试工具,可参考Android Developer

另外这里有一篇文章总结Android应用性能调优方案的专题,写得不错。

本文主要介绍如何使用MAT与Hierarchy View工具来进行内存优化以及如何使用Trace View进行性能调优。

内存优化

在进行内存优化前,需要明确的几个问题:

  • 如何确认内存占用量
  • 如何确认内存瓶颈或内存泄露
  • 如何优化内存占用

如果顺利解决了以上问题,则优化工作就取得比较理想的成果。

如何确认内存占用量

确认内存占用量有两种方式,

  • 通过dumpsys meminfo,可以查询系统内存使用情况,包括各个进程的内存占用量
    如:
37150 kB: Foreground
           37150 kB: com.yunos.tv.launcherdemo (pid 14884)
  • 通过Android Studio的Android面板中的Memory监控数据,可以查看所选进程的内存变化情况,如图:

如何确认内存泄露或内存瓶颈

内存是十分紧俏的资源,内存耗尽会导致应用体验差甚至出现OOM,因此需要以最优的方式来使用内存。

内存泄露指的是内存中残留一些对象,而这些对象在应用的后续流程中不再需要使用,但是由于被其它存活对象引用,所以其所占用的内存不能被回收,随着这种不可回收的无用对象的积累,内存会被慢慢吞噬,最终导致OutOfMemory。

确定是否存在内存泄露的方法有两种:

  • Memory监控——粗略判断应用运行时,通过Android Studio的Android-Memory面板可以监控内存的变化情况,如果反复在应用中执行操作会导致内存持续上涨,则基本可以断定存在内存泄露。
    内存正常的应用在使用过程中,内存不会持续高涨,在进入某个页面时,内存上涨,但是退出这个页面时,内存会回落,因此会出现锯齿状的内存监控图。
  • MAT内存分析——精确分析内存泄露点
    MAT可以很方便的分析内存情况,用于定位内存泄露点经常事半功倍。
  • MAT工具的使用(MAT插件安装请自行google)
MAT是Memory Analysis Tools的简称,该工具用于分析hprof文件,该文件可以通过SDK工具生成,其生成方式如下:

a) 在Android Studio的Android-Memory面板中,点击“Dump Java Heap”按钮,可能需要dump多次,dump成功后,会再Capture视图中生成相应的Heap Snapshot文件

b) 这个文件不能直接被MAT工具识别,需要使用SDK提供的工具将其转换成标准的hprof文件,转换方式:

    . ${SDK_DIR}/tools/hprof-conv xxx/captures/Snapshot_2015.09.16_11.58.03.hprof yyy/targetfile.hprof

c) 使用MAT工具导入转换后的hprof文件
  • Memory Leak Suspects
a) 在导入Heap Dump文件时,可以选择Memory Leak Report选项
![LeakSuspects](http://img1.tbcdn.cn/L1/461/1/d499f073a42eae73f2288f58a521d29d93dc1f18)

b) 加载完后,会进入可疑泄露点汇总,其展现形式如图:
          ![leakoverview](http://img2.tbcdn.cn/L1/461/1/9bd24d7f69026763b4c08b9074cdd5d5a4fb51b0)

该图会列出几个嫌疑最大的类,包括其实例的数量以及所占用内存大小,根据这些信息,可以分析这些对象包含其他那些对象以及各自的大小是多少、从该对象出发,到GC Root的路径是什么、该对象被那些对象引用或者引用了哪些对象、该对象所属类引用了其他哪些类或别其他哪些类引用,具体含义见下表:

选项

含义

List objects-with outgoing references

以该对象为起点,向外的引用,指的是该对象引用的其他对象列表

List objects-with incoming references

以该对象为起点,向内的引用,指的是该对象被哪些对象引用

Show objects by class-with outgoing references

以该对象所属类为起点,向外的引用,指的是该对象所属类引用了哪些类

Show objects by class-with incoming references

以该对象所属类为起点,向内的引用,指的是该对象所属类类被哪些类引用

Path to GC Roots-with all references

从GC Roots节点到该对象的引用路径,包含所有引用类型

Path to GC Roots-exclude weak references

从GC Roots节点到该对象的引用路径,去除所有弱引用,其他类似

Merge Shortest Paths to GC Roots-with all references

从GC Roots节点到该对象的最短引用路径,包含所有引用类型

Merge Shortest Paths to GC Roots-exclude weak references

从GC Roots节点到该对象的最短引用路径,去除所有弱引用,其他类似

Show Retained Set

列出该对象直接或间接占用的空间具体包含那些对象

  • OQL(Object Query Language)
通过上述方案,怀疑某个对象存在泄露后,可以通过OQL来进一步证明。OQL可以通过查询语法,来查询具体的对象信息,主要使用的是查询某个类的实例信息。

语法为:

select * from instanceof Class
例如,查询类com.yunos.tv.launchersdk.view.component.ScreenContainer的所有实例:
select * from instanceof com.yunos.tv.launchersdk.view.component.ScreenContainer

得到的结果如图:
![OQL](http://img4.tbcdn.cn/L1/461/1/5813a3e473cb3a8ade09d17eeb1b3510bd0e1f89)

| Class Name | Shallow Heap | Retained Heap |
| ------------ | ------------- | ------------- |
| com.yunos.tv.launchersdk.view.component.ScreenContainer @ 0x443108e8| 680 | 2,712 |
| com.yunos.tv.launchersdk.view.component.ScreenContainer @ 0x42817648| 680 | 16,289,472 |

图中说明com.yunos.tv.launchersdk.view.component.ScreenContainer这个类有两个实例,两个实例占用的内存大小分别为2,712和16,289,472。

如过实例的数量超出预期,说明存在内存泄露,如果实例所占内存的大小超出预期,说明这个实例内部的引用存在问题,可以进一步分析这个实例的具体信息。

如何优化内存占用

通过上述步骤确定哪个类的实例存在泄露或者哪个类占用的内存过多,则基本可以通过代码来排查内存泄露或内存瓶颈的位置。

内存泄露的最常见的方式是:在View中定义了非static的内部类,并将该内部类注册到了第三方接口中或注册到单例中,当View Destory时,由于外部持有该View的引用,导致这个View不能被GC回收。

常见的泄露场景以及解决方案

  • Context泄露
Android开发中,经常需要使用Context对象,当第三方接口请求Context实例时,如果不假思索的传入当前Activity或Service,则有可能导致内存泄露

解决方案:统一使用ApplicationContext

1、方式一,所有使用Context的地方,都调用context.getApplicationContext()

2、方式二,自定义Application,在Application中提供getContext静态方法,并将Application注册到Manifest中替换默认的Application,这样有个好处是:内部定义的接口可以不需要Context参数,直接从Application获取,另外,可以保证使用的context都是与Application的生命周期相同,不会出现泄露。
  • 非static内部类泄露
Java在定义内部类时,如果该内部类不是static,则会默认携带外部类的引用。

如:

public class MainActivity extends Activity implements ActionBar.TabListener {

static Leaky leak = null;
class Leaky {
    void doSomething() {
        System.out.println("Wheee!!!");
    }
}
@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    if (leak == null) {
        leak = new Leaky();
    }
...

}

其中Leak是MainActivity的内部类,由于是非static的,默认会持有MainActivity的引用,如图:

android app 优化 安卓应用优化_android app 优化

static Leaky leak = null;

leak是MainActivity的静态变量,只要MainActivity一执行OnCreate就会被初始化,初始化时,该Leaky对象持有第一次执行onCreate方法时的MainActivity对象的引用,导致横竖屏切换,第二次进入onCreate时,MainActivity的第一个对象仍然不会被GC回收掉。

这种类型的泄露修改方式为:将Leaky定义为static。

static class Leaky {

void doSomething() {
    System.out.println("Wheee!!!");
}

}

但是在大多数情况下,内部类会调用外部类的方法,因此需要使用外部内的引用,为了避免泄露,可以使用弱引用的方式,修复方案如下:

public class MainActivity extends Activity implements ActionBar.TabListener {

static Leaky leak = null;
static class Leaky {
    private WeakReference<MainActivity> mainActivityRef;

    public Leaky(MainActivity mainActivity) {
        mainActivityRef = new WeakReference<MainActivity>(mainActivity);
    }
    void doSomething() {
        System.out.println("Wheee!!!");
        MainActivity mainActivity = mainActivityRef.get();
        if(mainActivity != null) {
            mainActivity.doSomethingElse();
        }
    }

    public boolean statusOK() {
        return mainActivityRef.get() != null;
    }
}
@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    if (leak == null || !leak.statusOK()) {
        leak = new Leaky();
    }
    ...
}

public void doSomethingElse() {
    ...
}
...

}

非static内部类(包括匿名内部类)泄露常见于Handler,Listener,Runnable,Timer,Thread,AsyncTask,TimerTask,Callback等等
  • 内存瓶颈
主要是占用内存的大对象,主要的优化手段为压缩算法,比如图片的压缩。

性能优化

这里主要指CPU的性能优化,也就是Application占用CPU时间的优化,APP的CPU性能差主要体现在用户操作时卡顿,比如进入应用时时间长,加载数据时导致主线程阻塞,刷新图片时阻塞主线程等。

主要从以下三个方面来介绍:

  • 优化工具TraceView介绍
  • 如何确定性能瓶颈
  • 如何优化

优化工具TraceView介绍

工欲善其事,必先利其器。选择一个好的工具往往能做到事半功倍。TraceView工具很强大,能快速定位出某个操作过程中,好时的操作在哪里。

在Android Studio中,使用TraceView的步骤如下:

  • 打开Android Device Monitor
Tools -> Android -> Android Device Monitor
  • 连接ADB
  • 连接成功后,devices面板会列出系统中所有的进程列表
  • 选中需要监测的进程
  • 点击Start Method Profiling
  • 在应用中执行需要监测的操作
  • 操作完成后,点击Stop Method Profiling
  • 会自动生成并打开.trace文件,如图所示:

详细使用方法可以参考[正确使用Android性能分析工具——TraceView
](http://bxbxbai.github.io/2014/10/25/use-trace-view/)Android性能调优工具TraceView介绍

如何确定性能瓶颈

Android Monitor Device打开TraceView后,就可以分析应用在执行这个操作时,各个函数所花费的时间。如图

android app 优化 安卓应用优化_内存泄露_02

TraceView分为上下两个主要部分,上面部分列出了应用中运行的所有线程在这段时间内的图形化运行情况,下面部分通过函数的树状调用,显示了函数的执行时间。

下半部分的表格中,标题栏含义如下表:

序号

名称

含义

1

Incl Cpu Time %

当前函数及其调用的子函数执行时所占CPU时间百分比

2

Incl Cpu Time

当前函数及其调用的子函数一共执行时所占CPU时间

3

Excl Cpu Time %

当前函数执行时所占CPU时间百分比(不包含调用子函数的执行时间)

4

Excl Cpu Time

当前函数执行时所占CPU时间(不包含调用子函数的执行时间)

5

Incl/Excl Real Time %/NA

对比1~4,Real Time指的是实际执行时间,不包括CPU上下文切换等

6

Calls + Recur Calls / Total

Call表示这个方法调用的次数,Recur Call表示递归调用次数

7

Cpu Time / Call

该函数平均执行时间

8

Real Time / Call

该函数平均执行时间,不包括CPU上下文切换等

在了解上述指标后,可以比较直观的看出各个函数执行时所占用的时间,而且能快速定位到时间最长的函数——也就是性能瓶颈。

一般可以抓住从三个方面切入:

  • 从top_level来看,其children所占用的时间最长的TOP5
  • 一类是调用次数不多,但每次调用却需要花费很长时间的函数。可以通过CPU Time / Call的排序来排查,关注调用时间最长的前几个方法的调用情况。
  • 一类是那些自身占用时间不长,但调用却非常频繁的函数。可以通过Calls + Recur Time / Total排序,可以根据其调用链来追踪调用量异常的原因。

如何优化

找到性能瓶颈后,需要分析程序的执行流程,包括正常流程和异常流程。

可能的优化措施:

  • 如果应用出现ANR,将耗时的操作放到异步线程中去完成
  • 如果数据处理过慢,考虑使用线程池
  • 如果出现CPU占用持续高涨,且某些函数调用量很高,排查是否陷入死循环