最常见的Android内存优化方式及防止泄漏造成OOM总结篇

本文讨论了Android开发中如何避免内存泄漏,重点涉及静态内部类、匿名内部类、Handler、AsyncTask、BroadcastReceiver等对象的生命周期管理和最佳实践,强调了正确处理Context、引用管理以及资源清理的重要性。
摘要由CSDN通过智能技术生成

正确的做法为:将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,如果需要使用Context,请按照上面推荐的使用Application 的Context。当然,Application 的 context 不是万能的,所以也不能随便乱用,对于有些地方则必须使用 Activity 的 Context,对于Application,Service,Activity三者的Context的应用场景如下:Application 和 Service 可以启动一个 Activity,不过需要创建一个新的 task 任务队列。而对于 Dialog 而言,只有在 Activity 中才能创建。

####4、匿名内部类线程异步导致泄漏
在继承实现Activity/Fragment/View时,此时如果你使用了匿名类,并被异步线程持有了,那要小心了,没有任何措施一定会导致泄露。
举个栗子:

public class MainActivity extends Activity {
…{
Runnable re1 = new MyRunable();
Runnable re2 = new Runnable() {
@Override
public void run() {
}
};
}

re1和re2的区别是,re2使用了匿名内部类。运行时这两个引用的内存可以看到,re1没什么特别的。

但ref2这个匿名类的实现对象里面多了一个引用:this$0这个引用指向MainActivity.this。

也就是说当前的MainActivity实例会被re2持有,如果将这个引用再传入一个异步线程,此线程和此Acitivity生命周期不一致的时候,就造成了Activity的泄露。

####5、Handler 造成的内存泄漏
handler为了避免ANR而不在主线程进行耗时操作,去处理网络任务或者封装一些请求回调等api等。我们知道 Handler、Message 和 MessageQueue 都是相互关联在一起的,万一 Handler 发送的 Message 尚未被处理,则该 Message 及发送它的 Handler 对象将被线程 MessageQueue 一直持有造成内存泄漏。 
由于 Handler 属于 TLS(Thread Local Storage) 变量, 生命周期和 Activity 是不一致的。因此这种实现方式一般很难保证跟 View 或者 Activity 的生命周期保持一致,故很容易导致无法正确释放。

知识点:在java里 ,非静态内部类 和匿名类都会潜在的引用它们所属的外部类。但是静态内部类却不会。

接下里看个案例:

public class SampleActivity extends Activity {

private final Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
// …
}
}

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

// Post a message and delay its execution for 10 minutes.
mHandler.postDelayed(new Runnable() {
@Override
public void run() { … }}, 1000 * 60 * 10);

// Go back to the previous Activity.
finish();
}
}

分析:
当activity结束(finish)时,里面的延时消息在得到处理前,会一直保存在主线程的消息队列里持续10分钟。而且,由上文可知这个message持有handler引用,而handler又持有对其外部类(activity)的潜引用。这条引用关系会一直保持直到消息得到处理,从而阻止了acitivty被垃圾回收器回收,造成应用程序的泄漏。另外非静态匿名类Runnable同样持有外部类,导致泄漏。总结2条原因:

小结:

  • 只要有未处理的消息,那么消息会引用handler,非静态的handler又会引用外部类,即Activity,导致Activity无法被回收,造成泄漏;
  • Runnable类属于非静态匿名类,同样会引用外部类。

解决方案:

  • 我们可以把handler类放在单独的类文件中,或者使用静态内部类便可以避免泄漏。另外,如果想要在handler内部去调用所在的外部类Activity,那么可以在handler内部使用弱引用的方式指向所在Activity,这样统一不会导致内存泄漏。
  • 对于匿名类Runnable,同样可以将其设置为静态类。因为静态的匿名类不会持有对外部类的引用。

再看源码:

public class SampleActivity extends Activity {

private static class MyHandler extends Handler {
private final WeakReference mActivity;

public MyHandler(SampleActivity activity) {
mActivity = new WeakReference(activity); //弱引用
}

@Override
public void handleMessage(Message msg) {
SampleActivity activity = mActivity.get();
if (activity != null) {
// …
}
}
}

private final MyHandler mHandler = new MyHandler(this);

private static final Runnable sRunnable = new Runnable() { //静态匿名类
@Override
public void run() { /* … */ }
};

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

// Post a message and delay its execution for 10 minutes.
mHandler.postDelayed(sRunnable, 1000 * 60 * 10);

// Go back to the previous Activity.
finish();
}
}

如果一个内部类实例的生命周期比Activity更长,那么我们千万不要使用非静态的内部类。最好的做法是,使用静态内部类,然后在该类里使用弱引用来指向所在的Activity。综述,推荐使用静态内部类 + WeakReference 这种方式。每次使用前注意判空。

知识点:

前面提到了 WeakReference,所以这里就简单的说一下 Java 对象的几种引用类型。

Java对引用的分类有强(Strong reference),软(SoftReference),弱 (WeakReference),虚 PhatomReference 四种。

在Android应用的开发中,为了防止内存溢出,在处理一些占用内存大而且声明周期较长的对象时候,可以尽量应用软引用和弱引用技术。

软/弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。利用这个队列可以得知被回收的软/弱引用的对象列表,从而为缓冲器清除已失效的软/弱引用。

假设我们的应用会用到大量的默认图片,比如应用中有默认的头像,默认游戏图标等等,这些图片很多地方会用到。如果每次都去读取图片,由于读取文件需要硬件操作,速度较慢,会导致性能较低。所以我们考虑将图片缓存起来,需要的时候直接从内存中读取。但是,由于图片占用内存空间比较大,缓存很多图片需要很多的内存,就可能比较容易发生OutOfMemory异常。这时,我们可以考虑使用软/弱引用技术来避免这个问题发生。以下就是高速缓冲器的雏形:首先定义一个HashMap,保存软引用对象——private Map。

####6、尽量避免使用 static 成员变量
如果成员变量被声明为 static,那我们都知道其生命周期将与整个app进程生命周期一样。

这会导致一系列问题,如果你的app进程设计上是长驻内存的,那即使app切到后台,这部分内存也不会被释放。按照现在手机app内存管理机制,占内存较大的后台进程将优先回收,如果此app做过进程互保保活,那会造成app在后台频繁重启。当手机安装了你参与开发的app以后一夜时间手机被消耗空了电量、流量,你的app不得不被用户卸载或者静默。

修复的方法:

  • 不要在类初始时初始化静态成员。可以考虑lazy初始化。

####7、AsyncTask对象造成的泄漏

AsyncTask确实需要额外注意一下。它的泄露原理和前面Handler,Thread泄露的原理差不多,它的生命周期和Activity不一定一致。

解决方案:在activity退出的时候,终止AsyncTask中的后台任务。

但是,问题是如何终止?

AsyncTask提供了对应的API:public final boolean cancel (boolean mayInterruptIfRunning)。

它的说明有这么一句话:

// Attempts to cancel execution of this task. This attempt will fail if the task has already completed, already been cancelled, or could not be cancelled for some other reason.
// If successful, and this task has not started when cancel is called, this task should never run. If the task has already started, then the mayInterruptIfRunning parameter determines whether the thread executing this task should be interrupted in an attempt to stop the task.

cancel是不一定成功的,如果正在运行,它可能会中断后台任务。怎么感觉这话说的这么不靠谱呢?

是的,就是不靠谱。

那么,怎么才能靠谱点呢?我们看看官方的示例:

private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> {
protected Long doInBackground(URL… urls) {
int count = urls.length;
long totalSize = 0;
for (int i = 0; i < count; i++) {
totalSize += Downloader.downloadFile(urls[i]);
publishProgress((int) ((i / (float) count) * 100));
// Escape early if cancel() is called
// 注意下面这行,如果检测到cancel,则及时退出
if (isCancelled()) break;
}
return totalSize;
}

protected void onProgressUpdate(Integer… progress) {
setProgressPercent(progress[0]);
}

protected void onPostExecute(Long result) {
showDialog(“Downloaded " + result + " bytes”);
}
}

官方的例子是很好的,在后台循环中时刻监听cancel状态,防止没有及时退出。

为了提醒大家,google特意在AsyncTask的说明中撂下了一大段英文:

// AsyncTask is designed to be a helper class around Thread and Handler and does not constitute a generic threading framework. AsyncTasks should ideally be used for short operations (a few seconds at the most.) If you need to keep threads running for long periods of time, it is highly recommended you use the various APIs provided by the java.util.concurrent pacakge such as Executor, ThreadPoolExecutor and FutureTask.

可怜我神州大陆幅员辽阔,地大物博,什么都不缺,就是缺对英语阅读的敏感。

AsyncTask适用于短耗时操作,最多几秒钟。如果你想长时间耗时操作,请使用其他java.util.concurrent包下的API,比如Executor, ThreadPoolExecutor 和 FutureTask.

学好英语,避免踩坑!

####8、 BroadcastReceiver对象
种种原因没有调用到unregister()方法。

解决方法很简单,就是确保调用到unregister()方法。

顺带说一下,我在工作中碰到一种相反的情况,receiver对象没有registerReceiver()成功(没有调用到),于是unregister的时候提示出错:

// java.lang.IllegalArgumentException: Receiver not registered …

解决方案:

方案一:在registerReceiver()后设置一个FLAG,根据FLAG判断是否unregister()。网上搜到的文章几乎都这么写,我以前碰到这种bug,也是一直都这么解。但是不可否认,这种代码看上去确实有点丑陋。

方案二:我后来无意中听到某大牛提醒,在Android源码中看到一种更通用的写法:

// just sample, 可以写入工具类
// 第一眼我看到这段代码,靠,太粗暴了,但是回头一想,要的就是这么简单粗暴,不要把一些简单的东西搞的那么复杂。
private void unregisterReceiverSafe(BroadcastReceiver receiver) {
try {
getContext().unregisterReceiver(receiver);
} catch (IllegalArgumentException e) {
// ignore
}
}

####9、BitMap对象造成的泄漏
Bitmap 对象不用的时候最好调用一下recycle 方法再赋值null,清空资源的直接或间接引用,但是有人要问,android源码里面好多地方也没有调用啊?

是的,我这里说的是最好,如果不调用的话,只能依赖于Java GC 执行的时候,调用Bitmap 的 finalize方法,

这里面会执行navtive的方法 nativeDestructor() 去释放资源,其实查看一下那个函数,就是一句 delete bitmap。

###总结
1.对 Activity 等组件的引用应该控制在 Activity 的生命周期之内; 如果不能就考虑使用 getApplicationContext 或者 getApplication,以避免 Activity 被外部长生命周期的对象引用而泄露。

2.尽量不要在静态变量或者静态内部类中使用非静态外部成员变量(包括context ),即使要使用,也要考虑适时把外部成员变量置空;也可以在内部类中使用弱引用来引用外部类的变量。

3.对于生命周期比Activity长的内部类对象,并且内部类中使用了外部类的成员变量,可以将内部类改为静态内部类、静态内部类中使用弱引用来引用外部类的成员变量 。

4.Handler 的持有的引用对象最好使用弱引用,资源释放时也可以清空 Handler 里面的消息。比如在 Activity onStop 或者 onDestroy 的时候,取消掉该 Handler 对象的 Message和 Runnable。

5.在 Java 的实现过程中,也要考虑其对象释放,最好的方法是在不使用某对象时,显式地将此对象赋值为 null,比如使用完Bitmap 后先调用 recycle(),再赋为null,清空对图片等资源有直接引用或者间接引用的数组(使用 array.clear() ; array = null)等,最好遵循谁创建谁释放的原则。

6.正确关闭资源,对于使用了BraodcastReceiver,ContentObserver,File,游标 Cursor,Stream,Bitmap等资源的使用,应该在Activity销毁时及时关闭或者注销。

7.保持对对象生命周期的敏感,特别注意单例、静态对象、全局性集合等的生命周期。

好了,文章到这里就结束了,如果你觉得文章写得不错就给个赞呗?如果你觉得那里值得改进的,请给我留言。一定会认真查询,修正不足。谢谢。

最后这里是关于我自己的Android 学习,面试文档,视频收集大整理,有兴趣的伙伴们可以看看~

如果你觉得还算有用的话,不妨把它们推荐给你的朋友。
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

最后

题外话,我在一线互联网企业工作十余年里,指导过不少同行后辈。帮助很多人得到了学习和成长。

我意识到有很多经验和知识值得分享给大家,也可以通过我们的能力和经验解答大家在IT学习中的很多困惑,所以在工作繁忙的情况下还是坚持各种整理和分享。但苦于知识传播途径有限,很多程序员朋友无法获得正确的资料得到学习提升,故此将并将重要的Android进阶资料包括自定义view、性能优化、MVC与MVP与MVVM三大框架的区别、NDK技术、阿里面试题精编汇总、常见源码分析等学习资料。

【Android思维脑图(技能树)】

知识不体系?这里还有整理出来的Android进阶学习的思维脑图,给大家参考一个方向。

Android开发8年,阿里、百度一面惨被吊打!我是否应该转行了?

【Android进阶学习视频】、【全套Android面试秘籍】

希望我能够用我的力量帮助更多迷茫、困惑的朋友们,帮助大家在IT道路上学习和发展

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!

树)】**

知识不体系?这里还有整理出来的Android进阶学习的思维脑图,给大家参考一个方向。

[外链图片转存中…(img-SP58qAbI-1712459766034)]

【Android进阶学习视频】、【全套Android面试秘籍】

希望我能够用我的力量帮助更多迷茫、困惑的朋友们,帮助大家在IT道路上学习和发展

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!
  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值