Android OOM/MemoryLeak
各位读者可能都有拿的出手的github或者APP实战项目,但是会使用现成的XX开源组件并不代表你的基础就很好。本文将带你补习Android基础 -- Android中内存泄露实例,分享给大家。
1. 基础
在阅读本文前,请了解如下基础
本文属于java语言上的分析,不涉及到GC,虚拟机,native底层细节的实现。
2. 什么是内存泄漏
- 当你不再需要某个实例后,但是这个对象却仍然被引用,防止被垃圾回收(Prevent from being bargage collected)。这个情况就叫做内存泄露(Memory Leak)。
- 内存泄漏潜在危害非常大,比如无意泄漏了一个Drawable,它可能只有几百K的占用,但是由于它一般会引用View,就意味着同时泄漏了View,Context,Activity 以及 Activity中的resource,这个内存的泄漏就非常可观了。而且Android设备作为嵌入式设备,内存非常有限,泄漏后的卡顿或者崩溃也非常影响用户体验。
3. 常见内存泄露与解决方法
Activity中防止内存的关键只有一个:及时回收没有使用的项目。
3.1. 需要手动关闭的对象没有关闭
3.1.1. try/catch/finally中网络文件等流的手动关闭
- HTTP
- File
- ContendProvider
- Bitmap
- Uri
这些都是java基础啦,就不一一介绍了。
3.1.2. onDestroy()
或者 onPause()
中未及时关闭对象
泄露实例:
- 线程泄漏:当你执行耗时任务,在
onDestroy()
的时候考虑调用Thread.close()
- Handler泄露:当退出activity时,要注意所在Handler消息队列中的Message是否全部处理完成,可以考虑
removeCallbacksAndMessages()
手动关闭 - 广播泄露:手动注册广播时,记住退出的时候要
unregisterReceiver()
- 第三方SDK/开源框架泄露:ShareSDK, JPush等第三方SDK需要按照文档控制生命周期,它们有时候要求你继承它们的activity,其实也为了帮你控制生命周期
- 各种callBack/Listener的泄露
- EventBus等观察者模式的框架需要手动解除注册
比如常见的ButterKnife
@Override public void onDestroyView() {
super.onDestroyView();
ButterKnife.reset(this);
}
再比如ShareSDK
protected void onDestroy() {
ShareSDK.stopSDK(this);
super.onDestroy();
}
使用开源的框架可能会帮助你快速的解决这个问题,但是知其然并知其所以然,也要了解它们的生命周期
3.2. Static的使用
3.2.1 static class/method/variable 的区别,你真的懂了吗?
(1). Static inner class 与 non static inner class 的区别
static inner class 即静态内部类
,它只会出现在类的内部,在某个类中写一个静态内部类其实同你在IDE里新建一个.java
文件是完全一样的。
以下为它们的对比
class | static inner class | non-static inner class` |
---|---|---|
与外部class引用关系 | 如果没有传入参数,就无引用关系 | 自动获得强引用(implicit reference) |
被调用时需要外部实例 | 不需要(比如Bulider类) | 需要 |
能否调用外部class中的变量与方法 | 不能 | 能 |
生命周期 | 自主的生命周期 | 依赖于外部类,甚至比外部类更长 |
可以看到,在生命周期中,埋下了内存泄漏的隐患,如果它的生命周期比activity更长,那么可能会发生泄露,更可怕的是,有可能会产生难以预防的空指针问题。
这个泄露的例子,详见内存管理(2)的文章。
(2). static inner method
静态内部方法:可以被直接调用,而不用去依赖它所在的类,比如你需要随机数,只用调用Math.random()
即可,而不用实例化Math
这个对象。在工具类中,建议用static修饰方法。
(3). static inner variable
慎重使用静态变量,静态变量是被分配给当前的Class的,而不是一个独立的实例,当ClassLoader停止加载这个Class时,它才会回收,在许多的程序中,这样的事情永远不会发生。
static 变量称为静态变量或者类变量,它由类的所有实例共享。
这段谷歌博客上的著名代码演示了一次内存泄露的,当你旋转屏幕后,Drawable就会泄露。
private static Drawable sBackground;
@Override
protected void onCreate(Bundle state) {
super.onCreate(state);
TextView label = new TextView(this);
label.setText("Leaks are bad");
if (sBackground == null) {
sBackground = getDrawable(R.drawable.large_bitmap);
}
label.setBackgroundDrawable(sBackground);
setContentView(label);
}
注意,我在实际试验(CM12)中,没有GC时,导出的堆是有泄露的,而手动GC后,是不会发生内存泄露的,希望各位自己做实验,发一下反馈。
总的来说,还是少用,就算可能是新的设备支持更加先进的GC,但是还是要注意控制内存。
3.2.2. 使用内部匿名类要注意什么?
匿名内部类实际上就是non-static inner class,建议单独写出来,并加入static修饰。
3.2.4. 单例模式(Singleton)是不是内存泄漏?
在单例模式中,只有一个对象被产生,看起来一直占用了内存,但是这个不意味就是浪费了内存,内存本来就是用来装东西的,只要这个对象一直被高效的利用就不能叫做泄露。但是也不要偷懒,一个劲的全整成了单例,越多的单例会让内存占用过多,而且软件维护起来也变得复杂。
3.2.5. 为什么大神喜欢用static final来修饰常数?
static由于是所有实例共享的,万一某个实例更改它后,其它的实例也会受到影响,所以加入final
以防止常数被修改。
这个在面试中经常遇到,问你经过多次计算后,static的值是多少。
3.2.6. 顺便说下final吧
- final 变量:是只读的;
- final 方法:是不能继承或者重写的。
- final 对象:引用不能修改,但是对象本事可以修改
final MyObject o = new MyObject();
o.setValue("foo"); // Works just fine
o = new MyObject(); // Doesn't work.
虚拟机并不会知道你的变量是否是final的,所以final与内存泄露无关。
3.3. Bitmap的使用
- 使用前注意配置Bitmap的Config,比如长宽,参数,格式;
- 使用中注意缓存;
- 使用后注意recycle以清理native层的内存。
2.3以后的bitmap不需要手动recycle了,内存已经在java层了。同时,Bitmap还有别人做好的轮子,比如PhotoView,就可以方便的解决问题。
3.4. 多线程
线程泄露可能是最严重的泄露问题了,第一它可能与Handler一样,转一转手机内存就没了,第二是当回调的时候,它极可能弹出NullPointException
个人在实际使用的一个失败实例
上传图片时退出Activity,等到图片完成后,Toast就会抛出空指针异常。
RestAdapter adapter = new RestAdapter.Builder().setEndpoint(HeadlineService.END_POINT)
.setLogLevel(RestAdapter.LogLevel.FULL)
.build();
adapter.create(ImageService.class)
.updateImage(new TypedFile("image/*", file), new TypedString(nickname),
new TypedString(Build.MODEL), new TypedString(avatar),
new Callback<UploadResult>() {
@Override public void success(UploadResult uploadResult, Response response) {
if (uploadResult.getStatus() == 1) {
Log.d(TAG, "upload successfully!");
Toast.makeText(getActivity(), "上传成功!", Toast.LENGTH_SHORT)
.show();
} else {
Log.e(TAG, "upload failed!");
Toast.makeText(getActivity(), "上传失败!", Toast.LENGTH_SHORT)
.show();
}
bmp.recycle();
}
@Override public void failure(RetrofitError error) {
bmp.recycle();
}
});
我是使用Retrofit框架进行上传的,retrofit内部自己维护它的线程与生命周期,当我退出Activity时,Retrofit内部的网络线程并没有停止;当图片上传成功回调的时候,却发现activity已经没了,这样就会抛出异常。
解决方法:在Activity中使用耗时任务本来就不合适,使用Service可以更好的控制回调问题。
3.5. Context与ApplicationContext
class | Context | ApplicationContext |
---|---|---|
生命周期 | 短 | 非常长,几乎就是单例 |
适用场景 | Activity中需要UI/素材资源的地方 | 数据库,包管理,偏好设置,以及Picasso/Retrofit/ShareSDK等单例框架 |
Context的生命周期是一个Activiy,而ApplicationContext的生命周期是整个程序。我们最要注意的就是Context的内存泄露。
在Activiy的UI中要使用Context,而在其他的地方比如数据库、网络、系统服务的需要频繁调用Context的情况时,要使用ApplicationContext,以防止内存泄露。
其他的小技巧
以下为各类小问题,就不多介绍了,我会尽量写全所有的泄露。
Listview的item泄露
这个是入门问题了,加入ViewHolder可以减少findViewById
的时间,或者使用RecyclerView,来解决“滑动很卡”的问题。
StringBuffer
尽量使用StringBuffer,而不用String来累加字符串,你现在还记得String,StringBuilder,StringBuffer的区别吗?
StringBuffer其实是给StringBuilder加了同步锁;其实你使用
Log.d(TAG,"xx" + "yy")
这类写法后,编译器生成的代码已经自动帮你变成StringBuffer了
多用基本类型
使用int而不用Integer,较少的对象花销。在Android中使用sparseArray取代HashMap就是把key变成了int,而一定程度上减小了内存占用。
Native代码不受GC控制
Native层面的代码不受到GC控制,又是长篇大论了,一笔带过吧
使用弱引用
使用弱引用可以防止一定程度的无意引用造成的泄露,比如在Handler中使用弱引用作为参数,当销毁的时候就有可能不会发生泄露
弱引用随时可能为null,使用前需要判断是否为空。
后记
本文在写作中查询了很多中英文资料,整理代码示例非常累,如果你认为我的免费劳动有价值,请分享知识到微博上或者赞一下吧。
References
- http://www.cnblogs.com/mythou/p/3203536.html
- https://blogs.oracle.com/olaf/entry/memory_leaks_made_easy
- http://www.androiddesignpatterns.com/2013/04/activitys-threads-memory-leaks.html
- http://stackoverflow.com/questions/28595547/android-does-my-app-have-a-memory-leak
- http://ivyhz.lofter.com/post/9ccb6_682e74
- http://beginnersbook.com/2013/04/java-static-class-block-methods-variables/
- http://t18android-developers8blogspot2jp.c.xn--o1q148n.net/2009/01/avoiding-memory-leaks.html