Android 常见内存泄露 & 解决方案
前言
内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃 (OOM) 等严重后果。
那什么情况下不能被回收呢?
目前 java 垃圾回收主流算法是虚拟机采用 GC Roots Tracing 算法。算法的基本思路是:通过一系列的名为 GC Roots (GC 根节点)的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径,当一个对象到GC Roots没有任何引用链相连(图论说:从GC Roots 到这个对象不可达)时, 证明此对象是不可用的。
关于可达性的对象,便是能与 GC Roots 构成连通图的对象,如下图:
根搜索算法的基本思路就是通过一系列名为 "GC Roots" 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链 ( Reference Chain),当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的。
从上图,reference1、reference2、reference3 都是 GC Roots,可以看出:
reference1-> 对象实例1;
reference2-> 对象实例2;
reference3-> 对象实例4;
reference3-> 对象实例4 -> 对象实例6;
可以得出对象实例1、2、4、6都具有 GC Roots 可达性,也就是存活对象,不能被 GC 回收的对象。
而对于对象实例3、5直接虽然连通,但并没有任何一个 GC Roots 与之相连,这便是 GC Roots 不可达的对象,这就是 GC 需要回收的垃圾对象。
在了解 GC 之后,开始去了解 Android 的内存泄露情况了。
Android 内存泄露场景
下面会详细介绍一些常见的内存泄露场景,以及对应的修复办法。
非静态内部类的静态实例
比如我们在 Activity 内部定义了一个内部类 InnerClass,同时定义了一个静态变量 inner,并给予赋值。假设你在 onDestory 的时候没有将 inner 置 null;那么就会引起内存泄露。原因是静态变量持有了内部类的实例,内部类会对外部类有个引用,从而导致 Activity 得不到释放。
private static Object inner; void createInnerClass() {
class InnerClass {
}
inner = new InnerClass();
} View icButton = findViewById(R.id.ic_button);
icButton.setOnClickListener(new View.OnClickListener() {
@Override public void onClick(View v) {
createInnerClass();
nextActivity();
}
});
记得在生命周期结束的时候,将不需要的静态变量置 null。
多线程相关的匿名内部类/非静态内部类
和非静态内部类一样,匿名内部类也会持有外部类实例的引用。多线程相关的类有 AsyncTask 类,Thread 类和 Runnable 接口的类等,它们的匿名内部类如果做耗时操作
就可能发生内存泄露,这里以 AsyncTask 的匿名内部类举例,如下所示:
void startAsyncTask() {
new AsyncTask<Void, Void, Void>() {
@Override protected Void doInBackground(Void... params) {
while(true);
}
}.execute();
} super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
View aicButton = findViewById(R.id.at_button);
aicButton.setOnClickListener(new View.OnClickListener() {
@Override public void onClick(View v) {
startAsyncTask();
nextActivity();
}
});
当异步任务在后台执行耗时任务期间,Activity 不幸被销毁了(比如:用户退出,系统回收),这个被 AsyncTask 持有的 Activity 实例就不会被垃圾回收器回收,直到异步任务结束。
解决方法是继承 AsyncTask 新建一个静态内部类,用静态内部类创建实例就不会存在对外部实例的引用了。
Handler 内存泄露
同样道理,Handler 的 message 被传递到消息队列 MessageQueue
中,在 Message
消息没有被处理之前,handler 的实例也不无法被回收,如果 handler 实例不是静态的,就会导致引用它的 activity 或者 service 不能被回收,于是就会发生内存泄漏。
void createHandler() {
new Handler() {
@Override public void handleMessage(Message message) {
super.handleMessage(message);
}
}.sendMessageDelayed(Message.obtain(), 60000);
} View hButton = findViewById(R.id.h_button);
hButton.setOnClickListener(new View.OnClickListener() {
@Override public void onClick(View v) {
createHandler();
nextActivity();
}
});
对于上述问题,有两种解决办法,一种是使用一个静态的 handler 内部类,并且其持有的对象都改成弱引用形式进行引用。还有一种是在销毁 activity 的时候,将发送的消息进行移除。
myHandler.removeCallbackAndMessages(null);
这种有个问题就是 Handler 中的消息可能无法全部被处理完。
另外还有一个要注意的是,最好不要直接使用 View#post 来做一些操作。如果要用,确保要用的话,确保 view 已经被 attach 到了 window。
具体可以参考:View的post方法导致的内存泄漏分析
静态 Activity 或 View
Activity
变量,把当前运行的Activity
实例赋值于这个静态变量。如果这个静态变量在
Activity
生命周期结束后没有清空,就导致内存泄漏。因为 static 变量是贯穿这个应用的生命周期的,所以被泄漏的 Activity
就会一直存在于应用的进程中,不会被垃圾回收器回收。static Activity activity; void setStaticActivity() {
activity = this;
} View saButton = findViewById(R.id.sa_button);
saButton.setOnClickListener(new View.OnClickListener() {
@Override public void onClick(View v) {
setStaticActivity();
nextActivity();
}
});
为了能够被回收,需要在不需要使用的时候进行置 null 操作。比如销毁当前 activity 的时候。
特殊情况:如果一个 View 初始化耗费大量资源,而且在一个 Activity
生命周期内保持不变,那可以把它变成 static,加载到视图树上 (View Hierachy),像这样,当 Activity
被销毁时,应当释放资源。
static view; void setStaticView() {
view = findViewById(R.id.sv_button);
} View svButton = findViewById(R.id.sv_button);
svButton.setOnClickListener(new View.OnClickListener() {
@Override public void onClick(View v) {
setStaticView();
nextActivity();
}
});
同样的,为了解决内存泄露的问题,在 Activity 销毁的时候把这个 static view 置 null 即可,但是还是不建议用这个 static view的方法。
Eventbus 等注册监听造成的内存泄露
相信很多同学都在项目里面会用到 Eventbus。对于一些没有经验的同学在使用的时候经常会出现一些问题。比如说在 onCreate 的时候进行注册,却忘了反注册,或者说,在onStop的时候进行反注册,这些都会导致 Eventbus 的内存泄露。
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EventBus.getDefault().register(this);// 注意在onCreate()方法中注册
} @Override
public void onDestroy() {
EventBus.getDefault().unregister(this);// 注意在onDestory()方法中注册
super.onDestroy();
}
注册和反注册(取消注册)是对应的,必须要添加,否则会引起组件的内存泄漏。因为注册的时候组件是被 EventBus 内部的单例队列所持有引用的。
如果你是在 View 里面注册 Eventbus 的,记得是在 View 的生命周期 onAttachedToWindow 和 onDetachedFromWindow 的时候进行注册和反注册。
最近跟我的同事进行聊天的时候发现,他们为了解决 eventbus 导致的内存泄露问题(已经成对注册和反注册还是存在内存泄露问题),于是打算创建一个 object 的实例,用这个来进行注册与反注册,这样即使发生内存泄露也只会占用很小的内存空间。
单例引起的内存泄露
项目中,经常会存在很多单例。有时候需要我们将当前 Activity 实例传给单例,然后去做一些事情。如下面的代码:
public class SingleInstance {
private Context mContext;
private static SingleInstance instance; private SingleInstance(Context context) {
this.mContext = context;
} public static SingleInstance getInstance(Context context) {
if (instance == null) {
instance = new SingleInstance(context);
}
return instance;
}
}
上述单例中传入一个 context ,就会导致 context 的生命时长和应用的生命时长一样。就会造成内存泄露。
对于这种有三种解决办法:
1、采用弱引用的方式进行引用,确保能够被回收;
2、在对应的 context 要被销毁的时候,进行置 null;确保不会长于原本的生命时长;
3、看是否能够使用 APP context;这样就不会存在内存泄露的问题了。
资源对象没关闭造成内存泄漏
当我们打开资源时,一般都会使用缓存。比如读写文件资源、打开数据库资源、使用 Bitmap 资源等等。当我们不再使用时,应该关闭它们,使得缓存内存区域及时回收。虽然有些对象,如果我们不去关闭,它自己在 finalize() 函数中会自行关闭。但是这得等到 GC 回收时才关闭,这样会导致缓存驻留一段时间。如果我们频繁的打开资源,内存泄漏带来的影响就比较明显了。
解决办法:及时关闭资源
WebView
不同的Android 版本的 webView 会有差异,加上不同的厂商定制的 ROM 的 webView 差异,这就导致 webView 存在很大的兼容性问题。weView 都会存在内存泄露问题,在应用中只要使用一次,内存就不会被释放。通常的做法是为 webView 单独开一个进程,使用 AIDL 与应用的主进程进程通信。webView 进程可以根据业务的需求,在合适的时机进行销毁。
参考文献:
1、《Android进阶解密》
Android 常见内存泄露 & 解决方案的更多相关文章
- Android常见内存泄露,学会这六招优化APP性能
很多开发者都知道,在面试的时候会经常被问到内存泄露和内存溢出的问题. 1.内存溢出(Out Of Memory,简称 OOM),通俗理解就是内存不够,即内存占用超出内存的空间大小. 2.内存泄漏(Me ...
- Android常见内存泄露
前言 对于内存泄漏,我想大家在开发中肯定都遇到过,只不过内存泄漏对我们来说并不是可见的,因为它是在堆中活动,而要想检测程序中是否有内存泄漏的产生,通常我们可以借助LeakCanary.MAT等工具来检 ...
- Android性能优化:手把手带你全面了解 内存泄露 & 解决方案
. 简介 即 ML (Memory Leak)指 程序在申请内存后,当该内存不需再使用 但 却无法被释放 & 归还给 程序的现象2. 对应用程序的影响 容易使得应用程序发生内存溢出,即 OOM ...
- Android 的内存泄露和内存限制
转载自 https://blog.csdn.net/goodlixueyong/article/details/40716779 https://blog.csdn.net/vshuang/artic ...
- android 常见内存泄漏原因及解决办法
android常见内存泄漏主要有以下几类: 一.Handler 引起的内存泄漏. 在Android开发中,我们经常会使用Handler来控制主线程UI程序的界面变化,使用非常简单方便,但是稍不注意,很 ...
- Android 防内存泄露handler
Android 防内存泄露handler 1.使用弱引用 WeakRefHander /** * 作者: allen on 15/11/24.感谢开源作者https://coding.net/u/co ...
- Android App 内存泄露之调试工具(1)
Android App 内存泄露之工具(1) 使用内存监測工具 DDMS –> Heap 操作步骤 启动eclipse后,切换到DDMS透视图,并确认Devices视图.Heap视图都是打开的, ...
- 关于Android 的内存泄露及分析
一. Android的内存机制Android的程序由Java语言编写,所以Android的内存管理与Java的内存管理相似.程序员通过new为对象分配内存,所有对象在java堆内分配空间:然而对象的释 ...
- 【转】.. Android应用内存泄露分析、改善经验总结
原文网址:http://wetest.qq.com/lab/view/107.html?from=ads_test2_qqtips&sessionUserType=BFT.PARAMS.194 ...
随机推荐
- 使用 chart 部署 skywalking
使用 chart 部署 skywalking 本文主要讲述的是如何使用 Helm Charts 将 SkyWalking 部署到 Kubernetes 集群中,相关文档可以参考skywalking-k ...
- FFmpeg(四) 像素转换相关函数理解
一.基本流程 1.sws_getCachedContext();//得到像素转换的上下文 2.sws_scale()://进行转换 二.函数说明 1.SwsContext *vctx = NULL; ...
- Redis面试篇 -- Redis主从复制原理
Redis一般是用来支撑读高并发的,为了分担读压力,Redis支持主从复制.架构是主从架构,一主多从, 主负责写,并且将数据复制到其它的 slave 节点,从节点负责读. 所有的读请求全部走从 ...
- 注册中心nacos完整部署及与eureka区别
1. 场景描述 nacos最近用的比较多,介绍下nacos及部署吧,刚看了下以前写过类似的,不过没写如何部署及与eureka区别,只展示了效果,补补吧. 2.解决方案 2.1 nacos与eureka ...
- netty源码解解析(4.0)-24 ByteBuf基于内存池的内存管理
io.netty.buffer.PooledByteBuf<T>使用内存池中的一块内存作为自己的数据内存,这个块内存是PoolChunk<T>的一部分.PooledByteBu ...
- 浅析十大常见排序(含C++代码)
首先声明一下,本文只对十种排序算法做简单总结,并参照一些资料给出自己的代码实现,并没有对某种算法理论讲解,更详细的 了解可以参考以下资料: 1.<data structure and algor ...
- 【LeetCode刷题】——两数之和.1
---恢复内容开始--- 一直想在leetcode上面刷题,但是java刚刚摸了一下门,所以迟迟没有动手,今天做了第一道题,感觉自己实在菜的不行,但是还是学到了很多东西, 就记录一下遇到的问题. 首先 ...
- webshell检测方法归纳
背景 webshell就是以asp.php.jsp或者cgi等网页文件形式存在的一种命令执行环境,也可以将其称做为一种网页后门.黑客在入侵了一个网站后,通常会将asp或php后门文件与网站服务器WEB ...
- phpstorm 设置换行符的格式
菜单 > 文件 > 设置
- vue——同一局域网内访问项目
1.想要在手机上访问本地的vue项目,首先要保证手机和电脑处在同一局域网内(连着同一个无线网) 2.将你电脑的ip设置为固定ip(ipconfig查找本地的ip,然后修改它,改为你想变的数字) 3.在 ...