如需转载请评论或简信,并注明出处,未经允许不得转载

目录

前言
当一个短生命周期对象销毁且不会再被使用时,长生命周期对象依然持有了短生命周期的对象的引用,这样这个短生命周期对象就一直不会被GC释放,这样就造成了内存泄漏。内存泄漏显然是对内存的一种浪费,当一个项目中有多处内存泄漏时,就非常容易产生OOM。在Android中,一般内存泄漏都是由于Activity destory后,其引用不能释放导致的,本文主要介绍一些内存泄漏的常见场景及其解决方案
内存泄漏的场景
单例造成内存泄漏
单例模式非常受开发者的喜爱,不过使用的不恰当的话也会造成内存泄漏,由于单例的静态特性使得单例的生命周期和应用的生命周期一样长,这就说明了如果一个对象已经不需要使用了,而单例对象还持有该对象的引用,那么这个对象将不能被正常回收,这就导致了内存泄漏
如下这个典例:
public class AppManager {
private static AppManager instance;
private Context context;
private AppManager(Context context) {
this.context = context;
}
public static AppManager getInstance(Context context) {
if (instance != null) {
instance = new AppManager(context);
}
return instance;
}
}
这样不管传入什么Context
最终将使用Application
的Context
,而单例的生命周期和应用的一样长,这样就防止了内存泄漏
非静态内部类创建静态实例造成内存泄漏
有的时候我们可能会在启动频繁的Activity
中,为了避免重复创建相同的数据资源,可能会出现这种写法:
public class MainActivity extends AppCompatActivity {
private static TestResource mResource = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if(mResource == null){
mResource = new TestResource();
}
//...
}
class TestResource {
//...
}
}
这样就在Activity
内部创建了一个非静态内部类的单例,每次启动Activity
时都会使用该单例的数据,这样虽然避免了资源的重复创建,不过这种写法却会造成内存泄漏,因为非静态内部类默认会持有外部类的引用,而又使用了该非静态内部类创建了一个静态的实例,该实例的生命周期和应用的一样长,这就导致了该静态实例一直会持有该Activity
的引用,导致Activity
的内存资源不能正常回收。正确的做法为:
将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,如果需要使用Context
,请使用ApplicationContext
Handler造成内存泄漏
Handler
的使用造成的内存泄漏问题应该说最为常见了,平时在处理网络任务或者封装一些请求回调等api都应该会借助Handler
来处理,对于Handler
的使用代码编写一不规范即有可能造成内存泄漏,如下示例:
public class MainActivity extends AppCompatActivity {
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
//...
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
loadData();
}
private void loadData(){
//...request
Message message = Message.obtain();
mHandler.sendMessage(message);
}
}
这种创建Handler
的方式会造成内存泄漏,由于mHandler
是Handler
的非静态匿名内部类的实例,所以它持有外部类Activity
的引用,我们知道消息队列是在一个Looper
线程中不断轮询处理消息,那么当这个Activity退出时消息队列中还有未处理的消息或者正在处理消息,而消息队列中的Message
持有mHandler
实例的引用,mHandler
又持有Activity
的引用,所以导致该Activity
的内存资源无法及时回收,引发内存泄漏,所以另外一种做法为:
public class MainActivity extends AppCompatActivity {
private MyHandler mHandler = new MyHandler(this);
private TextView mTextView ;
private static class MyHandler extends Handler {
private WeakReference reference;
public MyHandler(Context context) {
reference = new WeakReference<>(context);
}
@Override
public void handleMessage(Message msg) {
MainActivity activity = (MainActivity) reference.get();
if(activity != null){
activity.mTextView.setText("");
}
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTextView = (TextView)findViewById(R.id.textview);
loadData();
}
private void loadData() {
//...request
Message message = Message.obtain();
mHandler.sendMessage(message);
}
}
创建一个静态Handler
内部类,然后对Handler
持有的对象使用弱引用,这样在回收时也可以回收Handler
持有的对象,这样虽然避免了Activity
泄漏,不过Looper
线程的消息队列中还是可能会有待处理的消息,所以我们在Activity
的Destroy
时或者Stop
时应该移除消息队列中的消息,更准确的做法如下:
public class MainActivity extends AppCompatActivity {
private MyHandler mHandler = new MyHandler(this);
private TextView mTextView ;
private static class MyHandler extends Handler {
private WeakReference reference;
public MyHandler(Context context) {
reference = new WeakReference<>(context);
}
@Override
public void handleMessage(Message msg) {
MainActivity activity = (MainActivity) reference.get();
if(activity != null){
activity.mTextView.setText("");
}
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTextView = (TextView)findViewById(R.id.textview);
loadData();
}
private void loadData() {
//...request
Message message = Message.obtain();
mHandler.sendMessage(message);
}
@Override
protected void onDestroy() {
super.onDestroy();
mHandler.removeCallbacksAndMessages(null);
}
}
使用mHandler.removeCallbacksAndMessages(null);
是移除消息队列中所有消息和所有的Runnable
。当然也可以使用mHandler.removeCallbacks();
或mHandler.removeMessages();
来移除指定的Runnable
和Message
线程造成内存泄漏
对于线程造成的内存泄漏,也是平时比较常见的,异步任务和Runnable
都是一个匿名内部类,因此它们对当前Activity
都有一个隐式引用。如果Activity
在销毁之前,任务还未完成,
那么将导致Activity
的内存资源无法回收,造成内存泄漏。正确的做法还是使用静态内部类的方式,如下:
static class MyAsyncTask extends AsyncTask {
private WeakReference weakReference;
public MyAsyncTask(Context context) {
weakReference = new WeakReference<>(context);
}
@Override
protected Void doInBackground(Void... params) {
SystemClock.sleep(10000);
return null;
}
@Override
protected void onPostExecute(Void aVoid) {
super.onPostExecute(aVoid);
MainActivity activity = (MainActivity) weakReference.get();
if (activity != null) {
//...
}
}
}
static class MyRunnable implements Runnable{
@Override
public void run() {
SystemClock.sleep(10000);
}
}
//——————
new Thread(new MyRunnable()).start();
new MyAsyncTask(this).execute();
这样就避免了Activity
的内存资源泄漏,当然在Activity
销毁时候也应该取消相应的任务AsyncTask::cancel()
,避免任务在后台执行浪费资源。
资源未关闭造成内存泄漏
对于使用了BraodcastReceiver
,ContentObserver
,File
,Cursor
,Stream
,Bitmap
等资源的使用,应该在Activity
销毁时及时关闭或者注销,否则这些资源将不会被回收,造成内存泄漏。
集合类造成内存泄漏
集合类如果仅仅有添加元素的方法,而没有相应的删除机制,导致内存被占用。如果这个集合类是全局性的变量 (比如类中的静态属性,全局性的 map
等即有静态引用或 final
一直指向它),那么没有相应的删除机制,很可能导致集合所占用的内存只增不减。如下面的例子,当然实际上我们在项目中肯定不会这么写,但稍不注意还是很容易出现这种情况,比如我们都喜欢通过 HashMap
做一些缓存之类的事,这种情况就要多留一些心眼
Vector v = new Vector(10);
for (int i = 1; i < 100; i++) {
Object o = new Object();
v.add(o);
o = null;
}
WebView造成的内存泄漏
混合开发时经常用到WebView
加载html等页面,而WebView
的内存泄漏就是最经常遇到的问题,尤其是当项目中需要用Webview
加载的页面比较多时
即使当我退出页面时在我的BrowserActivity
的onDestroy()
方法中进行内存占用回收(如下)但并没有效果:
mWebView.removeAllViews();
mWebView.destroy();
mWebView=null;
- 方法一:
很多文章的给出方法是这样的:不要在布局文件中定义WebView
的节点,而是在需要的时候动态生成。你可以在需要WebView
的布局位置放一个LinearLayout
,需要时在代码中动态生成WebView
并add
进去,然后在Activity onDestory()
的时候销毁WebView
。这个方法的关键点在于动态创建WebView
时传入的是ApplicationContext
,而不是ActivityContext
,但是在很多情况下会报错,这个出错应该是WebView
的某些特殊动作产生由Application
到Activity
的类型转换错误,所以这个方法不是特别推荐
- 方法二:
为加载WebView
的界面开启新进程,在该页面退出之后关闭这个进程
- 方法三:
从根源解决(推荐)。WebView
引起的内存泄漏主要是因为org.chromium.android_webview.AwContents
类中注册了component callbacks
,但是未正常反注册而导致的
org.chromium.android_webview.AwContents
类中有这两个方法 onAttachedToWindow
和 onDetachedFromWindow
,系统会在attach
和detach
处进行注册和反注册component callback
,
在onDetachedFromWindow()
方法的第一行中:
if (isDestroyed()) return;,
如果 isDestroyed()
返回 true
的话,那么后续的逻辑就不能正常走到,所以就不会执行unregister
的操作。我们的activity
退出的时候,都会主动调用 WebView.destroy()
方法,这会导致 isDestroyed()
返回 true
;destroy()
的执行时间又在onDetachedFromWindow
之前,所以就会导致不能正常进行unregister
解决方法就是:让onDetachedFromWindow先走,在主动调用destroy()之前,把webview从它的parent上面移除掉
ViewParent parent = mWebView.getParent();
if (parent != null) {
((ViewGroup) parent).removeView(mWebView);
}
mWebView.destroy();
完整的activity
的onDestroy()
方法如下
@Override
protected void onDestroy() {
if( mWebView!=null) {
// 如果先调用destroy()方法,则会命中if (isDestroyed()) return;这一行代码,需要先onDetachedFromWindow(),再
// destory()
ViewParent parent = mWebView.getParent();
if (parent != null) {
((ViewGroup) parent).removeView(mWebView);
}
mWebView.stopLoading();
// 退出时调用此方法,移除绑定的服务,否则某些特定系统会报错
mWebView.getSettings().setJavaScriptEnabled(false);
mWebView.clearHistory();
mWebView.clearView();
mWebView.removeAllViews();
mWebView.destroy();
}
super.on Destroy();
}
EditText造成内存泄漏
Android 输入法会导致内存泄露,基本原因就是inputMethodManager
持有了EditText
的引用,进而持有了activity
的引用导致的内存泄露,现在提供的基本方法就是通过反射把inputMethodManager
以及相关的持有引用赋值null
,但因为Android平台的多样性,内部代码被修改的乱七八糟,该种方法适用性不高
推荐使用下面这个方法
import android.annotation.SuppressLint;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.EditText;
import java.lang.reflect.Field;
/**
* 防止出现内存泄漏
*/
@SuppressLint("AppCompatCustomView")
public class BaseEditText extends EditText {
private static Field mParent;
static {
try {
mParent = View.class.getDeclaredField("mParent");
mParent.setAccessible(true);
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
public BaseEditText(Context context) {
super(context.getApplicationContext());
}
public BaseEditText(Context context, AttributeSet attrs) {
super(context.getApplicationContext(), attrs);
}
public BaseEditText(Context context, AttributeSet attrs, int defStyleAttr) {
super(context.getApplicationContext(), attrs, defStyleAttr);
}
@Override
protected void onDetachedFromWindow() {
try {
if (mParent != null)
mParent.set(this, null);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
}
super.onDetachedFromWindow();
}
}
属性动画造成内存泄漏
属性动画不用的时候要及时cancel()
if (mUpAnimatorSet != null) {
if (mUpAnimatorSet.isRunning()) {
mUpAnimatorSet.cancel();
}
mUpAnimatorSet.removeAllListeners();
for (Animator animator : mUpAnimatorSet.getChildAnimations()) {
animator.removeAllListeners();
}
mUpAnimatorSet = null;
}
Fragment中使用RecyclerView造成内存泄漏
在Fragemnt
中使用RecyclerView
可能导致内存泄漏,需要小心
@Override
public void onDestroyView() {
super.onDestroyView();
mRecyclerView = null;
mSwipeRefreshLayout = null;
mAdapter = null;
notDataView = null;
errorView = null;
}
内存泄漏的检测工具
- Android Profiler
操作方式:如果想测试某个Activity有没有内存泄漏,可以将当前Activity不断进行横竖屏切换(或不断重复进入Activity又返回的操作),如果结果如下图所示,那基本可以确定这个Activity发生了内存泄漏

具体使用可以参考:https://www.jianshu.com/p/a934df19c42e
- LeakCanary
LeakCanary官方文档:https://github.com/square/leakcanary
LeakCanary工作原理:LeakCanary2.0使用及原理分析 — Kotlin重构版
可以在项目中直接集成LeakCanary,非常方便,一般内存泄漏问题都能够得到及时的监控和解决,如果碰到比较棘手的问题,无法正确跟踪到内存泄漏的位置,可以使用Android Profiler或MAT等工具进行进一步分析
总结
内存泄漏是”内存杀手“,但是往往非常隐蔽,所以个人比较建议集成LeakCanary,同时要重视CodeReview,在开发测试过程中及时发现并解决内存泄漏问题。如果有必要,还可以做一些线上内存监控,但是线上内存监控往往成本比较大(在监控的同时也会有一些性能影响),需要制定针对自己实际项目的更加精细化的方案
网友评论