Android开发中比较常见的内存泄漏问题
转载自https://www.jianshu.com/p/ab4a7e353076
如果一个无用对象(不需要再使用的对象)仍然被其他对象持有引用,造成该对象无法被系统回收,以致该对象在堆中所占用的内存单元无法被释放而造成内存空间浪费,这中情况就是内存泄露。
在Android开发中,一些不好的编程习惯会导致我们的开发的app存在内存泄露的情况。下面介绍一些在Android开发中常见的内存泄露场景及优化方案。
单例导致内存泄露
单例模式在Android开发中会经常用到,但是如果使用不当就会导致内存泄露。因为单例的静态特性使得它的生命周期同应用的生命周期一样长,如果一个对象已经没有用处了,但是单例还持有它的引用,那么在整个应用程序的生命周期它都不能正常被回收,从而导致内存泄露。
public class AppSettings {
private static AppSettings sInstance;
private Context mContext;
private AppSettings(Context context) {
this.mContext = context;
}
public static AppSettings getInstance(Context context) {
if (sInstance == null) {
sInstance = new AppSettings(context);
}
return sInstance;
}
}
像上面代码中这样的单例,如果我们在调用getInstance(Context context)
方法的时候传入的context
参数是Activity
、Service
等上下文,就会导致内存泄露。
以Activity
为例,当我们启动一个Activity
,并调用getInstance(Context context)
方法去获取AppSettings
的单例,传入Activity.this
作为context
,这样AppSettings
类的单例sInstance
就持有了Activity
的引用,当我们退出Activity
时,该Activity
就没有用了,但是因为sIntance
作为静态单例(在应用程序的整个生命周期中存在)会继续持有这个Activity
的引用,导致这个Activity
对象无法被回收释放,这就造成了内存泄露。
为了避免这样单例导致内存泄露,我们可以将context
参数改为全局的上下文:
private AppSettings(Context context) {
this.mContext = context.getApplicationContext();
}
全局的上下文Application Context
就是应用程序的上下文,和单例的生命周期一样长,这样就避免了内存泄漏。
单例模式对应应用程序的生命周期,所以我们在构造单例的时候尽量避免使用Activity
的上下文,而是使用Application
的上下文。
静态变量导致内存泄露
静态变量存储在方法区,它的生命周期从类加载开始,到整个进程结束。一旦静态变量初始化后,它所持有的引用只有等到进程结束才会释放。
比如下面这样的情况,在Activity
中为了避免重复的创建info
,将sInfo
作为静态变量:
public class MainActivity extends AppCompatActivity {
private static Info sInfo;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (sInfo != null) {
sInfo = new Info(this);
}
}
}
class Info {
public Info(Activity activity) {
}
}
Info
作为Activity
的静态成员,并且持有Activity
的引用,但是sInfo
作为静态变量,生命周期肯定比Activity
长。所以当Activity
退出后,sInfo
仍然引用了Activity
,Activity
不能被回收,这就导致了内存泄露。
在Android开发中,静态持有很多时候都有可能因为其使用的生命周期不一致而导致内存泄露,所以我们在新建静态持有的变量的时候需要多考虑一下各个成员之间的引用关系,并且尽量少地使用静态持有的变量,以避免发生内存泄露。当然,我们也可以在适当的时候讲静态量重置为null,使其不再持有引用,这样也可以避免内存泄露。
非静态内部类导致内存泄露
非静态内部类(包括匿名内部类)默认就会持有外部类的引用,当非静态内部类对象的生命周期比外部类对象的生命周期长时,就会导致内存泄露。
非静态内部类导致的内存泄露在Android开发中有一种典型的场景就是使用Handler
,很多开发者在使用Handler
是这样写的:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
start();
}
private void start() {
Message msg = Message.obtain();
msg.what = 1;
mHandler.sendMessage(msg);
}
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
if (msg.what == 1) {
// 做相应逻辑
}
}
};
}
也许有人会说,mHandler
并未作为静态变量持有Activity
引用,生命周期可能不会比Activity
长,应该不一定会导致内存泄露呢,显然不是这样的!
熟悉Handler
消息机制的都知道,mHandler
会作为成员变量保存在发送的消息msg
中,即msg
持有mHandler
的引用,而mHandler
是Activity
的非静态内部类实例,即mHandler
持有Activity
的引用,那么我们就可以理解为msg
间接持有Activity
的引用。msg
被发送后先放到消息队列MessageQueue
中,然后等待Looper
的轮询处理(MessageQueue
和Looper
都是与线程相关联的,MessageQueue
是Looper
引用的成员变量,而Looper
是保存在ThreadLocal
中的)。那么当Activity
退出后,msg
可能仍然存在于消息对列MessageQueue
中未处理或者正在处理,那么这样就会导致Activity
无法被回收,以致发生Activity
的内存泄露。
通常在Android开发中如果要使用内部类,但又要规避内存泄露,一般都会采用静态内部类+弱引用的方式。
public class MainActivity extends AppCompatActivity {
private Handler mHandler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mHandler = new MyHandler(this);
start();
}
private void start() {
Message msg = Message.obtain();
msg.what = 1;
mHandler.sendMessage(msg);
}
private static class MyHandler extends Handler {
private WeakReference<MainActivity> activityWeakReference;
public MyHandler(MainActivity activity) {
activityWeakReference = new WeakReference<>(activity);
}
@Override
public void handleMessage(Message msg) {
MainActivity activity = activityWeakReference.get();
if (activity != null) {
if (msg.what == 1) {
// 做相应逻辑
}
}
}
}
}
mHandler
通过弱引用的方式持有Activity
,当GC执行垃圾回收时,遇到Activity
就会回收并释放所占据的内存单元。这样就不会发生内存泄露了。
上面的做法确实避免了Activity
导致的内存泄露,发送的msg
不再已经没有持有Activity
的引用了,但是msg
还是有可能存在消息队列MessageQueue
中,所以更好的是在Activity
销毁时就将mHandler
的回调和发送的消息给移除掉。
@Override
protected void onDestroy() {
super.onDestroy();
mHandler.removeCallbacksAndMessages(null);
}
非静态内部类造成内存泄露还有一种情况就是使用Thread
或者AsyncTask
。
比如在Activity中直接new
一个子线程Thread
:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
new Thread(new Runnable() {
@Override
public void run() {
// 模拟相应耗时逻辑
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
或者直接新建AsyncTask
异步任务:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
// 模拟相应耗时逻辑
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return null;
}
}.execute();
}
}
很多初学者都会像上面这样新建线程和异步任务,殊不知这样的写法非常地不友好,这种方式新建的子线程Thread
和AsyncTask
都是匿名内部类对象,默认就隐式的持有外部Activity
的引用,导致Activity
内存泄露。要避免内存泄露的话还是需要像上面Handler
一样使用静态内部类+弱应用的方式(代码就不列了,参考上面Hanlder
的正确写法)。
未取消注册或回调导致内存泄露
比如我们在Activity
中注册广播,如果在Activity
销毁后不取消注册,那么这个刚播会一直存在系统中,同上面所说的非静态内部类一样持有Activity
引用,导致内存泄露。因此注册广播后在Activity
销毁后一定要取消注册。
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
this.registerReceiver(mReceiver, new IntentFilter());
}
private BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
// 接收到广播需要做的逻辑
}
};
@Override
protected void onDestroy() {
super.onDestroy();
this.unregisterReceiver(mReceiver);
}
}
在注册观察则模式的时候,如果不及时取消也会造成内存泄露。比如使用Retrofit+RxJava
注册网络请求的观察者回调,同样作为匿名内部类持有外部引用,所以需要记得在不用或者销毁的时候取消注册。
Timer和TimerTask导致内存泄露
Timer
和TimerTask
在Android中通常会被用来做一些计时或循环任务,比如实现无限轮播的ViewPager
:
public class MainActivity extends AppCompatActivity {
private ViewPager mViewPager;
private PagerAdapter mAdapter;
private Timer mTimer;
private TimerTask mTimerTask;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
init();
mTimer.schedule(mTimerTask, 3000, 3000);
}
private void init() {
mViewPager = (ViewPager) findViewById(R.id.view_pager);
mAdapter = new ViewPagerAdapter();
mViewPager.setAdapter(mAdapter);
mTimer = new Timer();
mTimerTask = new TimerTask() {
@Override
public void run() {
MainActivity.this.runOnUiThread(new Runnable() {
@Override
public void run() {
loopViewpager();
}
});
}
};
}
private void loopViewpager() {
if (mAdapter.getCount() > 0) {
int curPos = mViewPager.getCurrentItem();
curPos = (++curPos) % mAdapter.getCount();
mViewPager.setCurrentItem(curPos);
}
}
private void stopLoopViewPager() {
if (mTimer != null) {
mTimer.cancel();
mTimer.purge();
mTimer = null;
}
if (mTimerTask != null) {
mTimerTask.cancel();
mTimerTask = null;
}
}
@Override
protected void onDestroy() {
super.onDestroy();
stopLoopViewPager();
}
}
当我们Activity
销毁的时,有可能Timer
还在继续等待执行TimerTask
,它持有Activity的引用不能被回收,因此当我们Activity销毁的时候要立即cancel
掉Timer
和TimerTask
,以避免发生内存泄漏。
集合中的对象未清理造成内存泄露
这个比较好理解,如果一个对象放入到ArrayList
、HashMap
等集合中,这个集合就会持有该对象的引用。当我们不再需要这个对象时,也并没有将它从集合中移除,这样只要集合还在使用(而此对象已经无用了),这个对象就造成了内存泄露。并且如果集合被静态引用的话,集合里面那些没有用的对象更会造成内存泄露了。所以在使用集合时要及时将不用的对象从集合remove
,或者clear
集合,以避免内存泄漏。
资源未关闭或释放导致内存泄露
在使用IO
、File
流或者Sqlite
、Cursor
等资源时要及时关闭。这些资源在进行读写操作时通常都使用了缓冲,如果及时不关闭,这些缓冲对象就会一直被占用而得不到释放,以致发生内存泄露。因此我们在不需要使用它们的时候就及时关闭,以便缓冲能及时得到释放,从而避免内存泄露。
属性动画造成内存泄露
动画同样是一个耗时任务,比如在Activity
中启动了属性动画(ObjectAnimator
),但是在销毁的时候,没有调用cancle
方法,虽然我们看不到动画了,但是这个动画依然会不断地播放下去,动画引用所在的控件,所在的控件引用Activity
,这就造成Activity
无法正常释放。因此同样要在Activity
销毁的时候cancel
掉属性动画,避免发生内存泄漏。
@Override
protected void onDestroy() {
super.onDestroy();
mAnimator.cancel();
}
WebView造成内存泄露
关于WebView的内存泄露,因为WebView在加载网页后会长期占用内存而不能被释放,因此我们在Activity销毁后要调用它的destory()
方法来销毁它以释放内存。
另外在查阅WebView
内存泄露相关资料时看到这种情况:
Webview
下面的Callback
持有Activity
引用,造成Webview
内存无法释放,即使是调用了Webview.destory()
等方法都无法解决问题(Android5.1之后)。
最终的解决方案是:在销毁WebView
之前需要先将WebView从
父容器中移除,然后在销毁WebView
。详细分析过程请参考这篇文章:WebView内存泄漏解决方法。
@Override
protected void onDestroy() {
super.onDestroy();
// 先从父控件中移除WebView
mWebViewContainer.removeView(mWebView);
mWebView.stopLoading();
mWebView.getSettings().setJavaScriptEnabled(false);
mWebView.clearHistory();
mWebView.removeAllViews();
mWebView.destroy();
}
总结
内存泄露在Android内存优化是一个比较重要的一个方面,很多时候程序中发生了内存泄露我们不一定就能注意到,所有在编码的过程要养成良好的习惯。总结下来只要做到以下这几点就能避免大多数情况的内存泄漏:
构造单例的时候尽量别用Activity
的引用;
静态引用时注意应用对象的置空或者少用静态引用;
使用静态内部类+软引用代替非静态内部类;
及时取消广播或者观察者注册;
耗时任务、属性动画在Activity
销毁时记得cancel
;
文件流、Cursor
等资源及时关闭;
Activity
销毁时WebView
的移除和销毁。
作者:xiaoyanger
链接:https://www.jianshu.com/p/ab4a7e353076
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。
象持有它的引用从而导致它不能被回收,这就导致本该被回收的对象不能被回收而停留在堆内存中,内存泄漏就产生了。
内存泄漏有什么影响呢?它是造成应用程序OOM的主要原因之一。由于Android系统为每个应用程序分配的内存有限,当一个应用中产生的内存泄漏比较多时,就难免会导致应用所需要的内存超过这个系统分配的内存限额,这就造成了内存溢出而导致应用Crash。
了解了内存泄漏的原因及影响后,我们需要做的就是掌握常见的内存泄漏,并在以后的Android程序开发中,尽量避免它。下面小编搜罗了5个Android开发中比较常见的内存泄漏问题及解决办法,分享给大家,一起来看看吧。
一、单例造成的内存泄漏
Android的单例模式非常受开发者的喜爱,不过使用的不恰当的话也会造成内存泄漏。因为单例的静态特性使得单例的生命周期和应用的生命周期一样长,这就说明了如果一个对象已经不需要使用了,而单例对象还持有该对象的引用,那么这个对象将不能被正常回收,这就导致了内存泄漏。
如下这个典例:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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,所以这个Context的生命周期的长短至关重要:
1、传入的是Application的Context:这将没有任何问题,因为单例的生命周期和Application的一样长 ;
2、传入的是Activity的Context:当这个Context所对应的Activity退出时,由于该Context和Activity的生命周期一样长(Activity间接继承于Context),所以当前Activity退出时它的内存并不会被回收,因为单例对象持有该Activity的引用。
所以正确的单例应该修改为下面这种方式:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public class AppManager { private static AppManager instance; private Context context; private AppManager(Context context) { this .context = context.getApplicationContext(); } public static AppManager getInstance(Context context) { if (instance != null ) { instance = new AppManager(context); } return instance; } } |
这样不管传入什么Context最终将使用Application的Context,而单例的生命周期和应用的一样长,这样就防止了内存泄漏。
二、非静态内部类创建静态实例造成的内存泄漏
有的时候我们可能会在启动频繁的Activity中,为了避免重复创建相同的数据资源,会出现这种写法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
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 (mManager == null ){ mManager = new TestResource(); } //... } class TestResource { //... } } |
这样就在Activity内部创建了一个非静态内部类的单例,每次启动Activity时都会使用该单例的数据,这样虽然避免了资源的重复创建,不过这种写法却会造成内存泄漏,因为非静态内部类默认会持有外部类的引用,而又使用了该非静态内部类创建了一个静态的实例,该实例的生命周期和应用的一样长,这就导致了该静态实例一直会持有该Activity的引用,导致Activity的内存资源不能正常回收。正确的做法为:
将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,如果需要使用Context,请使用ApplicationContext 。
三、Handler造成的内存泄漏
Handler的使用造成的内存泄漏问题应该说最为常见了,平时在处理网络任务或者封装一些请求回调等api都应该会借助Handler来处理,对于Handler的使用代码编写一不规范即有可能造成内存泄漏,如下示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
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的内存资源无法及时回收,引发内存泄漏,所以另外一种做法为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
public class MainActivity extends AppCompatActivity { private MyHandler mHandler = new MyHandler( this ); private TextView mTextView ; private static class MyHandler extends Handler { private WeakReference<Context> 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时应该移除消息队列中的消息,更准确的做法如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
public class MainActivity extends AppCompatActivity { private MyHandler mHandler = new MyHandler( this ); private TextView mTextView ; private static class MyHandler extends Handler { private WeakReference<Context> 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。
四、线程造成的内存泄漏
对于线程造成的内存泄漏,也是平时比较常见的,如下这两个示例可能每个人都这样写过:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
//——————test1 new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... params) { SystemClock.sleep( 10000 ); return null ; } }.execute(); //——————test2 new Thread( new Runnable() { @Override public void run() { SystemClock.sleep( 10000 ); } }).start(); |
上面的异步任务和Runnable都是一个匿名内部类,因此它们对当前Activity都有一个隐式引用。如果Activity在销毁之前,任务还未完成, 那么将导致Activity的内存资源无法回收,造成内存泄漏。正确的做法还是使用静态内部类的方式,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
static class MyAsyncTask extends AsyncTask<Void, Void, Void> { private WeakReference<Context> 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销毁时及时关闭或者注销,否则这些资源将不会被回收,造成内存泄漏。
以上就是android编程中,常见的5大内存泄漏问题及相应的解决办法,如果大家在编程中遇到了上述泄漏问题,不妨可以试试对应的方法。如果大家还有什么疑问,可以去“学习问答”版块直接提出。