1 概述
本篇博客是Android App性能优化专题的第一篇文章,该专题会在渲染、计算、内存、电量方面进行讲解Android App的性能优化。
为了更加高效的优化App性能,Android Studio提供了一个Android Monitor工具,它位于Android Studio主窗口的下方,Android Monitor提供了实时记录和观察App以下信息的工具:
- 系统或用户定义的Log信息
- Memory,CPU和GPU使用率
- Network流量(仅限硬件设备)
2 预备知识
2.1 Android App的内存结构
Random Access Memory(RAM)在任何软件开发环境中都是一个很宝贵的资源。这一点在物理内存通常很有限的移动操作系统上,显得尤为突出。系统会在RAM上为App进程分配固定的内存空间,然后该App进程就会运行在该内存空间上。该内存空间会被分成Stack内存空间和Heap内存空间,其中Stack内存空间里存放对象的引用,Heap内存空间里存放对象数据。
在Android的高级系统版本里面针对Heap内存空间有一个3级的Generational Heap Memory的模型,它包括Young Generation,Old Generation,Permanent Generation三个区域。最新分配的对象会存放在Young Generation区域,当这个对象在这个区域停留的时间超过某个值的时候,会被移动到Old Generation,最后到Permanent Generation区域。整个结构如下图所示:
这3个区域都有固定的大小,随着新的对象陆续被分配到此区域,当这些对象总的大小快达到该区域的大小时,会触发GC的操作,以便腾出空间来存放其他新的对象,如下图所示:
最近刚分配的对象会放在Young Generation区域,这个区域的GC操作速度也是比Old Generation区域的GC操作速度更快的,如下图所示:
通常情况下,当GC线程运行时,其他线程会暂停工作(包括UI线程),直到GC完成,如下图所示:
虽然单个的GC操作并不会占用太多时间,但是频繁的GC操作有可能会影响到帧率,导致卡顿。
2.2 GC root and Dominator tree
Java中有以下几种GC root:
- references on the stack
- Java Native Interface (JNI) native objects and memory
- static variables and functions
- threads and objects that can be referenced
- classes loaded by the bootstrap loader
- finalizers and unfinalized objects
- busy monitor objects
如果从GC Root到达Y的的所有path都经过X,那么我们称X dominates Y,或者X是Y的Dominator tree。当优化内存时,可以通过释放一个dominator对象来释放其所有下级对象。 例如,在下图中,如果要删除对象B,那么也会释放其所主导的对象所使用的内存,即对象C,D,E和F,实际上,如果对象C,D, E和F被标记为删除,但对象B仍然指向它们,这可能是它们未被释放的原因。
3 Memory Monitor
Android Monitor提供了Memory Monitor工具,以便更轻松地实时监听App的性能和内存使用情况,通过该工具可以:
- 显示空闲和已分配的Java内存随时间变化的图表。
- 随着时间的推移显示垃圾回收(GC)事件。
- 启动GC事件。
- 快速测试UI线程卡顿是否与频繁GC事件有关。
当GC线程运行时,其他线程都会暂停(包括UI线程),直到GC完成。频繁GC操作有可能会影响到帧率,导致卡顿,特别是性能比较差的手机上,尤为明显。 - 快速测试app崩溃是否与内存不足(内存溢出或者内存泄漏)有关。
Memory Monitor的工作流程
为了分析和优化内存使用,典型的工作流程是运行app并执行以下操作:
- 使用Memory Monitor来分析是否由于不良GC事件模式导致的app性能问题。
- 如果在短时间内产生频繁的GC事件,就通过Dump Java Heap操作来查看当前内存快照,继而查找哪些类型的对象有可能发生了内存泄漏或者占用了太大内存。
- 最后通过Start allocation tracking操作来追踪对象分配内存时对应的方法调用。
在Memory Monitor中执行Dump Java Heap操作时,会创建一个Android-specific Heap/CPU Profiling (HPROF)文件,HPROF文件中保存了app该时刻内存中的GC root列表,文件创建完成后会自动在HPROF Viewer中打开, HPROF Viewer使用
图标标示GC root(深度为零)以及使用
4 常见内存性能问题模拟及优化
4.1 内存抖动现象模拟及优化
内存抖动是因为在短时间内大量的对象被创建又马上被释放导致的。因此下面的例子中我通过一个for循环来不断的创建和释放对象来模拟内存抖动的现象。
举个例子:
public class TestLeakActivity1 extends AppCompatActivity implements View.OnClickListener {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Button click = new Button(this);
click.setOnClickListener(this);
click.setText("模拟内存抖动");
setContentView(click);
}
@Override
public void onClick(View v) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
Bitmap result1;
result1 = BitmapFactory.decodeResource(getResources(), R.drawable.noah_silliman);
}
}
}).start();
}
}
上面代码很简单,当点击模拟内存抖动按钮时,通过Memory Monitor工具可以看到出现了非常明显的内存抖动情况,如下图所示:
当内存抖动的峰值快达到Young Generation区域的容量时就会触发GC操作,因此为了触发GC操作,就在代码中加载来一张非常大图片(5184*3456),对应的GC log如下:
08-22 10:53:51.579 11758-11758/com.cytmxk.test I/art: Alloc partial concurrent mark sweep GC freed 14(608B) AllocSpace objects, 1(68MB) LOS objects, 39% free, 17MB/29MB, paused 492us total 52.970ms
08-22 10:53:51.988 11758-11758/com.cytmxk.test I/art: Alloc partial concurrent mark sweep GC freed 14(608B) AllocSpace objects, 1(68MB) LOS objects, 40% free, 17MB/29MB, paused 370us total 36.902ms
08-22 10:53:52.329 11758-11758/com.cytmxk.test I/art: Alloc partial concurrent mark sweep GC freed 14(608B) AllocSpace objects, 1(68MB) LOS objects, 40% free, 17MB/29MB, paused 365us total 36.754ms
08-22 10:53:52.664 11758-11758/com.cytmxk.test I/art: Alloc partial concurrent mark sweep GC freed 14(608B) AllocSpace objects, 1(68MB) LOS objects, 40% free, 17MB/29MB, paused 305us total 32.072ms
08-22 10:53:52.952 11758-11758/com.cytmxk.test I/art: WaitForGcToComplete blocked for 8.791ms for cause Alloc
08-22 10:53:52.988 11758-11758/com.cytmxk.test I/art: Alloc partial concurrent mark sweep GC freed 8(304B) AllocSpace objects, 1(68MB) LOS objects, 40% free, 17MB/29MB, paused 305us total 32.178ms
08-22 10:53:53.396 11758-11758/com.cytmxk.test I/art: WaitForGcToComplete blocked for 9.036ms for cause Alloc
08-22 10:53:53.444 11758-11758/com.cytmxk.test I/art: Alloc partial concurrent mark sweep GC freed 8(304B) AllocSpace objects, 1(68MB) LOS objects, 40% free, 17MB/29MB, paused 493us total 43.976ms
08-22 10:53:53.809 11758-11758/com.cytmxk.test I/art: WaitForGcToComplete blocked for 11.791ms for cause Alloc
08-22 10:53:53.853 11758-11758/com.cytmxk.test I/art: Alloc partial concurrent mark sweep GC freed 8(304B) AllocSpace objects, 1(68MB) LOS objects, 40% free, 17MB/29MB, paused 373us total 38.598ms
08-22 10:53:54.181 11758-11758/com.cytmxk.test I/art: Alloc partial concurrent mark sweep GC freed 14(608B) AllocSpace objects, 1(68MB) LOS objects, 40% free, 17MB/29MB, paused 311us total 32.794ms
08-22 10:53:54.617 11758-11758/com.cytmxk.test I/art: Alloc partial concurrent mark sweep GC freed 18(736B) AllocSpace objects, 1(68MB) LOS objects, 40% free, 17MB/29MB, paused 481us total 46.280ms
通过上面的log中的时间点证明了发生了频繁的GC, 由于导致GC的原因是Alloc(可以参考调查 RAM 使用情况来理解GC Log),因此可能会在不久的将来会发生OOM异常;当我点击两次按钮时,确实引发OOM异常;频繁GC操作有可能会影响到帧率,导致卡顿。
Allocation Tracker功能(可以参考Allocation Tracker)对于识别和优化内存抖动是非常有效的,接下来就通过这个功能来定位上面发生内存抖动的位置:
点击右下角红色框中的按钮,即开始执行Allocation Tracker,等一段时间,再点击一下右下角红色框中的按钮就会停止Allocation Tracker,此时下面的波形图中矩形区域就是Allocation Tracker执行的周期,并且会生成和打开一个alloc格式的文件,通过上图可知分配内存最多的是Thread 22线程,打开Thread 22线程的调用stack,定位到TestLeakActivity1的34行就是分配内存的位置,接下去的问题修复也就显得相对简单了,尽量避免在for循环里面分配对象,尝试把对象的创建移到循环体之外,对于那些无法避免需要创建对象的情况,我们可以考虑对象池模型,通过对象池来解决频繁创建与销毁的问题,注意在对象池没用时需要手动释放对象池中的对象。
4.2 内存泄漏现象模拟及优化
内存泄漏是指不再用到的对象由于被错误引用而无法被GC回收,这样就导致这个对象一直留在内存当中,占用了宝贵的内存空间。显然会导致每级Generation的内存区域可用空间变小,GC就会更容易被触发,从而引起性能问题。
举个例子:
public class TestLeakActivity2 extends AppCompatActivity implements View.OnClickListener {
private Button testLeakBtn = null;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test_leak2);
testLeakBtn = (Button) findViewById(R.id.button_test_leak);
testLeakBtn.setOnClickListener(this);
}
@Override
public void onClick(View v) {
Intent intent = new Intent(this, TestLeakActivity3.class);
startActivity(intent);
}
}
public class TestLeakActivity3 extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ImageView imageView = new ImageView(this);
imageView.setImageResource(R.drawable.noah_silliman);
setContentView(imageView);
handler.sendEmptyMessageDelayed(0, 60000);
}
private Handler handler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
};
}
上面的代码很简单,运行App,多次并且快速执行操作(从TestLeakActivity2跳转到TestLeakActivity3,然后再回到TestLeakActivity2),接着利用Memory Monitor工具的Dump Java Heap功能(可以参考HPROF Viewer and Analyzer)列举此时Heap中各种类型对象的多少和大小:
点击右上角的箭头,可以解析出当前泄漏的activity,然后选中instance窗口中的第一个泄漏的实例,下面的reference tree窗口就会立即显示该实例对应的reference tree,可以看出:
1> TestLeakActivity3$1@316570880是TestLeakActivity3@315071952的Dominator tree。
2> TestLeakActivity3$1@316570880的类型是Message中target的类型,即Handler类型。
3> 由于TestLeakActivity3$1@316570880通过this$0引用TestLeakActivity3@315071952,因此TestLeakActivity3$1是TestLeakActivity3的内部类。
4> target和handler是同一个实例(TestLeakActivity3$1@316570880),并且handler是TestLeakActivity3@315071952的一个属性。
由上面的4条信息可以得出只要TestLeakActivity3中handler的生命周期在TestLeakActivity3生命周期之内,就可以避免TestLeakActivity3实例的泄漏,接下去的问题修复也就显得相对简单了,就不在赘叙了。