前言
内存泄漏,一个说大不大说下不小的瑕疵。作为开发者,我们都很清楚内存泄漏是我们代码问题导致的。但是话说回来,泄漏后果会很严重嘛?这不好说,如果我们不泄漏Bitmap这种大内存的对象,那么修补内存泄漏就像鸡肋一样,“食之无味,弃之可惜”。 就比如说我们项目组,近2000w的DAU,只要不明显影响用户体验,一切以上需求为主…
但是这作为一个996福报码农,不能只挖坑,不填坑,毕竟技术债都是要还的。所以今天咱们来聊一聊Android中的内存泄漏。这篇文章总结翻译了外国友人的一篇文章:原文如下
一、理论
先上一张图:
解释一下这张图,每个Android(或Java)应用程序都有一个起点(GC Root),从这个点中实例化对象、调用方法。。一些对象直接引用GC Root,另一些对象又引用了这些对象。因此,形成了引用链,就像上图一样。因此垃圾收集器从GC Root开始并遍历直接或间接链接到GC Root的对象。在此过程结束时,脱离GC Root的对象/对象链将被回收。
接下来咱们再想另一个问题:
什么是内存泄漏?
有了上图,理解内存泄漏的概念就很简单,说白了就是:长生命周期对象A持有了短生命周期的对象B,那么只要A不脱离GC Root的链,那么B对象永远没有可能被回收,因此B就泄漏了。
有什么危害?
危害的话,如开篇所说。如果泄漏的内存很小,几字节,几kb….对于现在的机器性能,就像星爵打灭霸…“伤害”基本无视。但是如果泄漏的足够多,普通的GC无法回收这些泄漏的内存,那么堆将持续增加,当堆足够大的时候,就会触发“stop-the-world” GC,直接在主线程进行耗时的GC。
主线程进行耗时操作,每一个android开发者都明白这意味着什么….
所以内存泄漏足够严重,其危害还是很严重的。
二、实践
对于我们日常开发来说,有比较多的场景稍不注意就会存在内存泄漏的风险。让我们一起留意一下:
2.1、内部类Inner classes
内部类存在内存泄漏的风险,是一个老生常谈的话题。说白了就是因为我们在new一个内部类时,编译器会在编译时让这个内部类的实例持有外部对象。
这也就是,为啥我们的内部类可以引用到外部类变量、方法的原因。
上段代码:
public class BadActivity extends Activity {
private TextView mMessageView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_bad_activity);
mMessageView = (TextView) findViewById(R.id.messageView);
new LongRunningTask().execute();
}
private class LongRunningTask extends AsyncTask<Void, Void, String> {
@Override
protected String doInBackground(Void... params) {
return "Am finally done!";
}
@Override
protected void onPostExecute(String result) {
mMessageView.setText(result);
}
}
}
大家应该都能看出这里的问题吧。作为非静态内部类的LongRunningTask
,会持有BadActivity
。并且LongRunningTask
是一个长时间任务,也就是说,在这个任务没有完成时,BadActivity
是不会被回收的,因此我们的BadActivity
就被泄漏了。那么怎么改呢?
解决原理
首先我不能让LongRunningTask
持有BadActivity
。那么我们需要使用静态内部类(static class)。这样的确不会持有BadActivity
,但是问题来了,我们LongRunningTask
不持有BadActivity
,也就意味着没办法引用到BadActivity
中的变量,那么我们的更新UI的操作就做不了,也就是说还是要显示的传一个BadActivity
中我们需要的变量进来…但是这样有造成了同样的泄漏问题。
因此,我们需要对传入的变量使用WeakReference
进行包一层。但发生GC的时候,告诉GC收集器“我”可以被回收。
上改造后的代码:
public class GoodActivity extends Activity {
private AsyncTask mLongRunningTask;
private TextView mMessageView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_good_activity);
mMessageView = (TextView) findViewById(R.id.messageView);
mLongRunningTask = new LongRunningTask(mMessageView).execute();
}
@Override
protected void onDestroy() {
super.onDestroy();
mLongRunningTask.cancel(true);
}
private static class LongRunningTask extends AsyncTask<Void, Void, String> {
private final WeakReference<TextView> messageViewReference;
public LongRunningTask(TextView messageView) {
this.messageViewReference = new WeakReference<>(messageView);
}
@Override
protected String doInBackground(Void... params) {
String message = null;
if (!isCancelled()) {
message = "I am finally done!";
}
return message;
}
@Override
protected void onPostExecute(String result) {
TextView view = messageViewReference.get();
if (view != null) {
view.setText(result);
}
}
}
}
2.2、匿名类 Anonymous classes
这一类和2.1很类似。本质都是持有外部对象的引用。
上一段很常见的代码:
public class MoviesActivity extends Activity {
private TextView mNoOfMoviesThisWeek;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_movies_activity);
mNoOfMoviesThisWeek = (TextView) findViewById(R.id.no_of_movies_text_view);
MoviesRepository repository = ((MoviesApp) getApplication()).getRepository();
repository.getMoviesThisWeek()
.enqueue(new Callback<List<Movie>>() {
@Override
public void onResponse(Call<List<Movie>> call,
Response<List<Movie>> response) {
int numberOfMovies = response.body().size();
mNoOfMoviesThisWeek.setText("No of movies this week: " + String.valueOf(numberOfMovies));
}
@Override
public void onFailure(Call<List<Movie>> call, Throwable t) {
// Oops.
}
});
}
}
2.3、注册Listener
SingleInstance.setMemoryLeakListener(new OnMemoryLeakListener(){
//…..
})
这里写了段很常见的伪码,一个单例的对象,register了一个Listener,并且这个Listener被单例的一个成员变量引用。
OK,那么问题很明显了。单例作为静态变量,肯定是一直存在的。而其内部持有了Listener,而Listener作为一个匿名类,有持有了外部对象的引用。因此这条GC链上的所有对象都不会被释放。
解决也很简单,适当的时机,在单例中将Listener的引用置为null。这样,Listener和单例之间的引用关系断了,Listener链上的所有内容就可以被正常释放掉了。也就是咱们常做的在onDestory()
进行unRegisterListener的操作。
类似不注意的内容,还包括Lambda。不过有一点值得注意的,在Kotlin的Lambda中,如果我们没有使用外部对象的变量或者方法,那么Kotlin在编译时,这个Lambda是不会持有外部对象的引用的。也算是Kotlin的一些优化吧
2.4、Contexts
上下文的滥用,也是泄漏的大客户。不过大家针对这类问题应该比较熟悉。
比如:长时间存活的对象,不建议持有Activity的context,而是使用ApplicationContext。如果ApplicationContext没办法完成业务,那么就需要好好考虑一下:这个长时间存活的对象,为什么必须要持有Activity的context。它设计的是否合理,是否它应该是一个长时间存活的对象(比如单例)。
尾声
关于内存泄漏,还是需要咱们平时多注意,对自己写的每一行代码都多思考。毕竟这东西“不是病,但疼起来真要命”。
不管怎么说,大家技术还是要学好的。小编下面给大家分享一份成为高级工程师学习路线,如果想学习高级UI、性能优化、移动架构师、 NDK、混开发等Android高阶开发的朋友可以加下我的Android架构群:887084983,还有免费的学习资料及面试资料领取~
好了,文章到这里就结束了,如果你觉得文章写得不错就给个赞呗?如果你觉得那里值得改进的,请给我留言。一定会认真查询,修正不足。谢谢~
相关推荐
程序员成长路线图:从入门到优秀 程序员成长路线图:从入门到优秀
优秀程序员的十个习惯
word文档关于程序员的习惯 如何成为优秀程序员的方法
程序员里的风景线:程序员因为女孩而美丽.docx
程序员成长路线图 从入门到优秀
为啥成为一个优秀的程序员,作者列数一百条准则
程序员的 AI 启蒙课:ChatGPT 让你 1 人顶 3 人
精通ACE,必读书第第三本:ACE程序员指南:网络与系统编程的实用设计模式
我早就有和他们共语的愿望,希望通过这个主题和他们交流程序员所关注的各种问题,希望我的经验有助于他们的成长,同时我也想谈谈EOM对程序员的真正价值的影响,以及如何实现“成为比尔·盖茨”这个程序员的最高梦想...
对程序员中存在的一些问题进行了生动的讲述。
程序员的十层楼:大家都来测测你的技术层级.pdf
vb程序员的三个境界: (1)所有代码都只放在窗体文件中的,属于菜鸟级,他们只会从面板上拖控件,设置属性,然后再给事件编码 (2)工程中有bas模块的,属于中间级,他们已意识到有大量的代码是重复出现的,...
第1部分:应用程序编程接口(API) – 服务提供程序接口(SPI)–程序员参考 第2部分:服务分类说明–程序员参考 第3部分:打印机和扫描仪设备类接口 – 程序员参考 第4部分:身份证件识别设备类接口 – 程序员参考 ...
WINDOWS程序员指南1--DLL和内存管理
如何做一个有素养的程序员如何做一个有素养的程序员如何做一个有素养的程序员如何做一个有素养的程序员如何做一个有素养的程序员如何做一个有素养的程序员如何做一个有素养的程序员如何做一个有素养的程序员如何做一...
第1章 敏捷——高效软件开发之道 第2章 态度决定一切 1. 做事 ...9.1 只要一个新的习惯 9.2 拯救濒临失败的项目 9.3 引入敏捷:管理者指南 9.4 引入敏捷:程序员指南 9.5 结束了吗 附录A 资源 索引
一个_NET程序员的创业感想:关于创业 想创业的CODER看看吧
这是我转载的一份很不错的心得,如果你想做一个优秀的java程序员,那一定要看
程序员的数学4:图论入门.pptx