内存使用总结篇 -- Android内存优化第五弹

前面几弹从Android内存管理, GC机制理论, 到内存分析工具, 内存泄露实例分析等几个方面聊了下Android App中关于内存优化的一些个知识.

本篇作为Android App内存优化的第五弹, 也是最后一弹, 将对Andorid中的内存优化做一个简单的总结.

1, 回顾

系列文链接:

1.GC那些事儿
2.Android的内存管理
3.内存分析工具
4.内存泄露实例分析

几个要点:

  • Android的App运行在Dalvik/ART这种类JVM环境的, 使用的是自动内存管理方式, 也就是通常说的GC机制.
  • 每个App默认单独运行在一个VM进程内, 其内存使用是有上限的.
  • 所谓GC就是回收垃圾对象.
  • 所谓垃圾, 就是GC Roots不可达的对象, 也就是死对象(相对于活对象).
  • 对象占用内存(Retained Size)是其所支配(Dominate)的所有子对象的占用内存之和. 故而我们找内存消耗点, 和内存泄露的时候都是关注对象的Retained Size.
  • 所谓内存分析, 最多是就是使用工具定位是哪个对象支配着某个Retained Size很大的对象, 进而定位出内存消耗或内存泄露点.

回顾之后, 我们再来看下内存问题.

2, 内存问题

从大的分类上来说, Android App中关于内存的问题大致可以分成如下三类:

  • 内存泄露
  • 内存消耗过大
  • 内存抖动

前二者, 内存泄露和内存消耗过大, 最终的结果就是我们常见的OutOfMemoryException, 今天我们的内存使用总结也主要是针对这二者.

关于内存抖动我们在App优化之消除卡顿一文中有描述.

3, 常见的内存泄露及其解决方案

以下关于泄露的名字, 个人根据自己的习惯起的, 并非哪儿的官方称呼, 希望没有误导到吃瓜群众.

3.1 Context泄露

Context使用不当导致的内存泄露.
一般来说是因为某些全部的对象, 理当使用Application级别的Context, 而使用了指定Activity的Context, 导致该Activity无法释放.

例如, 某个单例中需要一个Context, 传入了一个Activity的Context, 导致其被这个单例持续引用而无法回收.

这类泄露的解决方案, 就是根据组件的生命周期来正确使用Context, 全局引用使用Application Context.

关于各种Context的说明和使用请参看这篇译文.
Context泄露的实例还可以看下android dev blog中的这篇, 需翻墙.

3.2 内部类泄露

由于(匿名)内部类隐式地持有一个外部类的引用, 故而当内部类中执行的事情长于外部类的生命周期时, 就会导致外部类的泄露.

常见的此类泄露包括Handler泄露, Thread泄露…, 这些也是我们经常会作为(匿名)内部类在Activity中使用的.

下面以HandlerLeak为例:

如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class HandlerLeakActivity extends AppCompatActivity {
private BigObject mBigObject = new BigObject();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_memory_leak);
mHandler.sendEmptyMessageDelayed(1, 60 * 1000);
}
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
};
}

这段代码我们实际上非常多的使用, 然而如果我们用android lint工具检测的话, 会有一段这样的提示:

也就是说这个Handler类可能会导致内存泄露, 建议我们使用static方式. 点开”more”, 我们来看下官方的建议解决方案:

1
2
3
4
5
6
7
8
9
Since this Handler is declared as an inner class, it may prevent the outer
class from being garbage collected. If the Handler is using a Looper or
MessageQueue for a thread other than the main thread, then there is no issue.
If the Handler is using the Looper or MessageQueue of the main thread, you
need to fix your Handler declaration, as follows: Declare the Handler as a
static class; In the outer class, instantiate a WeakReference to the outer
class and pass this object to your Handler when you instantiate the Handler;
Make all references to members of the outer class using the WeakReference
object.

阅读这段”more”的前半段, 我们分析下泄露是怎么产生的:

因为这个Handler是一个内部类(默认持有一个外部类也就是我们的HandlerLeakActivity的引用), 如果这个Handler的Looper/MQ所在的Thread与Main Thread不同, 则没有问题. 但是如果Handler的Looper/MQ就是Main Thread(本例中就是), 那么问题就来了:

这个Handler发送的message会放到MQ中, 这个message会对Handler有一个引用, 而Handler有HandlerLeakActivity的引用. 当我们进入这个Activity, 然后退出, 理当销毁这个Activity并回收了. 但是因为这个message会延时60s, 故而导致这个mHandler被引用, 从而activity被引用着, 而无法回收释放内存.

GC那些事儿中, 我们就讲到, 运行中的Thread就是GC Root之一, 根据上面的分析, 得出: HandlerLeakActivity到GC Roots可达, 故而无法回收.

我们用LeakCanary来验证下我们的分析:


可以看到, 果然如我们分析的.

那么此类问题怎么解决呢, 可能很多同学也直接使用加上@SuppressLint(“HandlerLeak”)的方式来避免lint提示了, 如下:

1
2
3
4
5
6
7
@SuppressLint("HandlerLeak")
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
};

然而这并非解决之道, 其实这段”more”的后半段也给了我们解决方案 — 使用Static + WeakReference的方式, 具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class HandlerLeakActivity extends AppCompatActivity {
private BigObject mBigObject = new BigObject();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_memory_leak);
new DemoHandler(this).sendEmptyMessageDelayed(1, 60 * 1000);
}
private static class DemoHandler extends Handler {
private final WeakReference<HandlerLeakActivity> mActivity;
private DemoHandler(HandlerLeakActivity activity) {
this.mActivity = new WeakReference<>(activity);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
HandlerLeakActivity activity = mActivity.get();
if (activity != null) {
activity.doSomething();
}
}
}
private void doSomething() {
}
}

留下一个问题, 为什么说这个Handler不在Main Thread的时候不会有问题, 大家可以自行研究下, 有机会就HandlerLeak这个话题我们再深入研究下.

3.3 Register泄露

对于观察者, 广播, Listener等, 注册和注销没有成对出现而导致的内存泄露.

内存泄露实例中那个例子, 就是这种泄露, 在此不在细述了.

解决方案就是编码的时候多注意吧, add/remove, register/unregister, bind/unbind什么的~.

3.4 资源泄露

常见的数据库查询Cursor, 文件读写流等, 用完没有关闭导致的内存泄露.

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class CursorLeakActivity extends AppCompatActivity {
private BigObject mBigObject = new BigObject();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_memory_leak);
Cursor cursor = getContentResolver().query(ContactsContract.Contacts.CONTENT_URI, null, null, null, null);
if (cursor != null) {
cursor.moveToFirst();
// do something.
}
}
}

这个cursor就可能泄露, 实际上android lint也给了我们提示:

此类问题的解决方案, 一般我们使用try-catch-finally的结构, 在finally中关闭并释放资源.
如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class CursorLeakActivity extends AppCompatActivity {
private BigObject mBigObject = new BigObject();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_memory_leak);
Cursor cursor = getContentResolver().query(ContactsContract.Contacts.CONTENT_URI, null, null, null, null);
try {
if (cursor != null) {
cursor.moveToFirst();
// do something.
}
}
catch (Exception e) {
e.printStackTrace();
}
finally {
if (cursor != null) {
cursor.close();
cursor = null;
}
}
}
}

3.5 Bitmap泄露

Bitmap没有及时调用recycle()回收导致的泄露.

对于Bitmap是使用, 一直就是Android开发者的痛, 特别是对大图片的处理. 可以说我们大多数的报出来的OutOfMemory异常基本都是因为要给某个Bitmap分配内存, 而可用内存不够导致的.

3.6 内存泄露小结

对于内存泄露, 我们尽量是以防为主. 根据上面的常见内存泄露, 我们需要注意以下几点:

  • Context的(根据组件生命周期)合理使用.
  • 避免在Activity中使用非静态内部类, 可以静态内部类+WeakReference达成目的.
  • 注意add/remove, register/unregister, bind/unbind的成对使用.
  • 资源及时关闭, 释放.

4, 有效使用内存的建议

本节大部分内容来自官方开发文档.

  • 合理使用Service
    Service的及时关闭可以让我们节省内存消耗, 对于一次性的任务, 建议使用IntentService.

  • 使用优化后的数据容器
    使用Android提供的SparseArray, SparseBooleanArray, LongSparseArray来代替HashMap的使用.
    关于HashMap,ArrayMap,SparseArray, 这篇文章有个比较直观的比较, 可以看下.

  • 少用枚举enum结构
    相比于静态常量(static final), enum会耗费双倍的内存.

  • 避免创建不必要的对象
    诸如一些临时对象, 特别是循环中的.

  • 考虑实现onTrimMemory(), 在此根据当前的内存状态做些处理.

  • Bitmap的合理有效使用.
    对于Bitmap的使用, 建议直接查看官方开发文档中的高效显示Bitmap(需翻墙).

结语

至此, Android App内存优化的5发子弹就打完了, 关于App内存优化的部分, 我们就先到这里了, 可能还有很多遗漏的内容.

再次表明下我写文的思想: 一个是想记录下自己的一个解决问题的思路和经验, 再一个是想传达如何去解决问题的思想. 故而, 文章并不是一开始就说有哪些内存问题, 怎么解. 而是从理论基础到分析工具的使用, 案例的分析去一步步的让大家学会怎么处理这类问题.

希望大家能从中得到一些关于解决问题的启发, 而非被灌输一些强记下的知识.

感谢相随…