Android内存泄露场景分析

原文地址:http://www.cnblogs.com/qianxudetianxia/p/3645106.html

大部分内容来自以上原文,有的内容从别的博客中整理而来

Context作为最基本的上下文,承载着Activity,Service等最基本组件。当有对象引用到Activity,并不能被回收释放,必将造成大范围的对象无法被回收释放,进而造成内存泄漏。

下面针对一些常用场景逐一分析。

总之就是任何生命周期超过activity生命周期的对象持有了context都会在activity退出后造成内存泄露

匿名内部类的对象都默认持有外部类的强引用。如果匿名内部类的对象生命周期长与activity的生命周期就会内存泄露。比如保存到了静态变量中,比如保存到了回调中还没有执行,像定时器

下面的1-9都是这种场景。

1. CallBack对象的引用

场景: CallBack对象持有context的引用

1
2
3
4
5
6
7
8
9
@Override
protectedvoid onCreate(Bundle state){
   super .onCreate(state);
   
   TextView label = new  TextView( this );
   label.setText( "Leaks are bad" );
   
   setContentView(label);
}

    没问题是吧,继续看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private  static  Drawable sBackground;
   
@Override
protected  void  onCreate(Bundle state){
   super .onCreate(state);
   
   TextView label = new  TextView( this );
   label.setText( "Leaks are bad" );
   
   if (sBackground == null ){
     sBackground = getDrawable(R.drawable.large_bitmap);
   }
   label.setBackgroundDrawable(sBackground);
   
   setContentView(label);
}

    有问题吗?

    首先,查看setBackgroundDrawable(Drawable background)方法源码里面有一行代码引起我们的注意:

1
2
3
4
5
public  void  setBackgroundDrawable(Drawable background) {
     // ... ...
     background.setCallback( this );
     // ... ...
}

    所以sBackground对view保持了一个引用,view对activity保持了一个引用。

    当退出当前Activity时,当前Activity本该释放,但是因为sBackground是静态变量,它的生命周期并没有结束,而sBackground间接保持对Activity的引用,导致当前Activity对象不能被释放,进而导致内存泄露。

    所以结论是:有内存泄露!

    这是Android官方文档的例子:http://android-developers.blogspot.com/2009/01/avoiding-memory-leaks.html

   Android官方文档的这篇文章是写于2009年1月的,当时的Android Source至少是Froyo之前的。

    Froyo的Drawable的setCallback()方法的实现是这样的:

1
2
3
public  final  void  setCallback(Callback cb) {
         mCallback = cb;
}

    在GingerBread的代码还是如此的。

    但是当进入HoneyComb,也就是3.0之后的代码我们发现Drawable的setCallback()方法的实现变成了:

1
2
3
public  final  void  setCallback(Callback cb) {
         mCallback =  new  WeakReference<Callback>(cb);
}

    也就是说3.0之后,Drawable使用了软引用,把这个泄露的例子问题修复了。(至于软引用怎么解决了以后有机会再分析吧)

    所以最终结论是,在android3.0之前是有内存泄露,在3.0之后无内存泄露!

    如果认真比较代码的话,Android3.0前后的代码改进了大量类似代码,前面的Cursor篇里的例子也是在3.0之后修复了。

    从这个例子中,我们很好的发现了内存是怎么通过回调泄露的,同时通过官方代码的update也了解到了怎么修复类似的内存泄露。

2. System Service对象

场景: 创建系统服务时持有context的引用

    通过各种系统服务,我们能够做一些系统设计好的底层功能:

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
//ContextImpl.java
@Override
public  Object getSystemService(String name) {
     ServiceFetcher fetcher = SYSTEM_SERVICE_MAP.get(name);
     return  fetcher ==  null  null  : fetcher.getService( this );
}
 
static  {
     registerService(ACCESSIBILITY_SERVICE,  new  ServiceFetcher() {
             public  Object getService(ContextImpl ctx) {
             return  AccessibilityManager.getInstance(ctx);
             }});
 
     registerService(CAPTIONING_SERVICE,  new  ServiceFetcher() {
             public  Object getService(ContextImpl ctx) {
             return  new  CaptioningManager(ctx);
             }});
 
     registerService(ACCOUNT_SERVICE,  new  ServiceFetcher() {
             public  Object createService(ContextImpl ctx) {
             IBinder b = ServiceManager.getService(ACCOUNT_SERVICE);
             IAccountManager service = IAccountManager.Stub.asInterface(b);
             return  new  AccountManager(ctx, service);
             }});
     // ... ...
}

  这些其实就是定义在Context里的,按理说这些都是系统的服务,应该都没问题,但是代码到了各家厂商一改,事情发生了一些变化。

      一些厂商定义的服务,或者厂商自己修改了一些新的代码导致系统服务引用了Context对象不能及时释放,我曾经碰到过Wifi,Storage服务都有内存泄露。

     我们改不了这些系统级应用,我们只能修改自己的应用。

     解决方案就是:使用ApplicationContext代替Context。

     举个例子吧:

1
2
3
4
// For example
mStorageManager = (StorageManager) getSystemService(Context.STORAGE_SERVICE);
改成:
mStorageManager = (StorageManager) getApplicationContext().getSystemService(Context.STORAGE_SERVICE);

3. Handler对象

场景: activity中创建内部类handler实例

首先我们来看一段代码

这样创建的handler是内部类。默认是强引用

1
2
3
4
5
6
public  class  MainActivity extends Activity {
         // lint tip: This Handler class should be static or leaks might occur
     class  MyHandler extends Handler {
         ... ...
     }
}

在handler对象创建的时候却会报警告:This Handler class should be static or leaks might occur

Handler类应该为static类型,否则可能会造成内存泄漏。

为什么会造成这种情况呢?这种情况就是由于android的特殊机制造成的:当一个android主线程被创

建的时候,同时会有一个Looper对象被创建,而这个Looper对象会实现一个MessageQueue(消息队列),当我们创建一个handler对象时,而handler的作用就是放入和取出消息从这个消息队列中,每当我们通过handler将一个msg放入消息队列时,这个msg就会持有一个handler对象的引用。因此当Activity被结束后,这个msg在被取出来之前,这msg会继续存活,但是这个msg持有handler的引用,而handler在Activity中创建,会持有Activity的引用,因而当Activity结束后,Activity对象并不能够被gc回收,因而出现内存泄漏。

        这个根本原因就是:Activity在被结束之后,MessageQueue并不会随之被结束,如果这个消息队列中存在msg,则导致持有handler的引用,但是又由于Activity被结束了,msg无法被处理,从而导致永久持有handler对象,handler永久持有Activity对象,于是发生内存泄漏。但是为什么为static类型就会解决这个问题呢?因为在java中所有非静态的对象都会持有当前类的强引用,而静态对象则只会持有当前类的弱引用。声明为静态后,handler将会持有一个Activity的弱引用,而弱引用会很容易被gc回收,这样就能解决Activity结束后,gc却无法回收的情况。

    Handler泄露的关键点有两个:

    1). 内部类

    2). 生命周期和Activity不一定一致  (activity退出的时候handler还有未处理的消息)

    第一点,Handler使用的比较多,经常需要在Activity中创建内部类,所以这种场景还是很多的。

    内部类持有外部类Activity的引用,当Handler对象有Message在排队,则无法释放,进而导致Activity对象不能释放。

    如果是声明为static,则该内部类不持有外部Acitivity的引用,则不会阻塞Activity对象的释放。

    如果声明为static后,可在其内部声明一个弱引用(WeakReference)引用外部类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public  class  MainActivity  extends  Activity {
     private  CustomHandler mHandler;
 
     @Override
     protected  void  onCreate(Bundle savedInstanceState) {
         super .onCreate(savedInstanceState);
         mHandler =  new  CustomHandler( this );
     }
 
     static  class  CustomHandlerextends Handler {
         // 内部声明一个弱引用,引用外部类
         private  WeakReference<MainActivity > activityWeakReference;
         public  MyHandler(MyActivity activity) {
             activityWeakReference=  new  WeakReference<MainActivity >(activity);
         }
                 // ... ...   
     }
}

    第二点,其实不单指内部类,而是所有Handler对象,如何解决上面说的Handler对象有Message在排队,而不阻塞Activity对象释放?

    解决方案也很简单,在Activity onStop或者onDestroy的时候,取消掉该Handler对象的Message和Runnable。

    通过查看Handler的API,它有几个方法:removeCallbacks(Runnable r)和removeMessages(int what)等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 一切都是为了不要让mHandler拖泥带水
@Override
public  void  onDestroy() {
     mHandler.removeMessages(MESSAGE_1);
     mHandler.removeMessages(MESSAGE_2);
     mHandler.removeMessages(MESSAGE_3);
     mHandler.removeMessages(MESSAGE_4);
 
     // ... ...
 
     mHandler.removeCallbacks(mRunnable);
 
     // ... ...
}

    上面的代码太长?好吧,出大招:

1
2
3
4
5
@Override
public  void  onDestroy() {
     //  If null, all callbacks and messages will be removed.
     mHandler.removeCallbacksAndMessages( null );
}

    有人会问,当Activity退出的时候,我还有好多事情要做,怎么办?我想一定有办法的,比如用Service等等.

4. Thread对象

场景:activity中创建了thread

    创建thread对象的时候虽然没有传递context引用,但是因为是匿名内部类。都会默认持有外部类的引用。

    同Handler对象可能造成内存泄露的原理一样,Thread的生命周期不一定是和Activity生命周期一致。

    而且因为Thread主要面向多任务,往往会造成大量的Thread实例。

    据此,Thread对象有2个需要注意的泄漏点:

    1). 创建过多的Thread对象

    2). Thread对象在Activity退出后依然在后台执行

    解决方案是:

    1). 使用ThreadPoolExecutor,在同时做很多异步事件的时候是很常用的,这个不细说。

    2). 当Activity退出的时候,退出Thread

    第一点,例子太多,建议大家参考一下afinal中AsyncTask的实现学习。

    第二点,如何正常退出Thread,我在之前的博文中也提到过。示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ref http://docs.oracle.com/javase/1.5.0/docs/guide/misc/threadPrimitiveDeprecation.html
private  volatile  Thread blinker;
 
public  void  stop() {
     blinker =  null ;
}
 
public  void  run() {
     Thread thisThread = Thread.currentThread();
     while  (blinker == thisThread) {
         try  {
             thisThread.sleep(interval);
         catch  (InterruptedException e){
         }
         repaint();
     }
}

    有人会问,当Activity退出的时候,我还有好多事情要做,怎么办?请看上面Handler的分析最后一行。

5. AsyncTask对象

场景: acticity中创建了AsyncTask对象

它的泄露原理和前面Handler,Thread泄露的原理差不多,它的生命周期和Activity不一定一致。

即activity退出的时候异步任务还没有执行

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

但是,问题是如何终止?

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

但是这个方法并不可靠

如果正在运行,它可能会中断后台任务

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


3
4
5
6
7
8
9
10
11
12
13

private  class  DownloadFilesTask  extends  AsyncTask<URL, Integer, Long> {
      protected  Long doInBackground(URL... urls) {
          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;
      }
  }

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

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

 6. BroadcastReceiver对象

场景: acticity中创建了BroadcastReceiver对象

泄露原因:没有调用到unregister()方法。

解决方法:在activity退出的时候确保调用到unregister()的方法即可

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

有两种解决方案:

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

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

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

7. TimerTask对象

TimerTask对象在和Timer的schedule()方法配合使用的时候极容易造成内存泄露。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private  void  startTimer(){ 
     if  (mTimer ==  null ) { 
        // 这样的非静态类对象是否也持有外部类的引用呢?
         mTimer =  new  Timer(); 
    
 
     if  (mTimerTask ==  null ) { 
        //  匿名内部类对象默认就持有外部类activity的引用。虽然构造参数没有context.mTimerTask的声明周期比activity长就会造成泄露。
         mTimerTask =  new  TimerTask() { 
             @Override 
             public  void  run() { 
                 // todo
            
         }; 
    
 
     if (mTimer !=  null  && mTimerTask !=  null 
         mTimer.schedule(mTimerTask,  1000 1000 ); 
 
}

泄露的点是,忘记cancel掉Timer和TimerTask实例。cancel的时机同cursor篇说的,在合适的时候cancel。

1
2
3
4
5
6
7
8
9
10
private void cancelTimer(){ 
        if (mTimer != null) { 
            mTimer.cancel(); 
            mTimer = null
        
        if (mTimerTask != null) { 
            mTimerTask.cancel(); 
            mTimerTask = null
        }
    }
8. Dialog对象

 一般发生于Handler的MESSAGE在排队,Activity已退出,然后Handler才开始处理Dialog相关事情。

 因为创建Dialog对象的时候持有context的引用

 关键点就是,怎么判断Activity是退出了,有人说,在onDestroy中设置一个FLAG。我很遗憾的告诉你,这个错误很有可能还会出来。

解决方案是:使用isFinishing()判断Activity是否退出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Handler handler =  new  Handler() {
     public  void  handleMessage(Message msg) {
         switch  (msg.what) {
         case  MESSAGE_1:
             // isFinishing == true, 则不处理,尽快结束
             if  (!isFinishing()) {
                 // 不退出
                 // removeDialog()
                 // showDialog()
             }  
             break ;
         default :
             break ;
         }  
         super .handleMessage(msg);
     }  
};

  早完早释放!

9. Observer对象

Observer对象的泄露,也是一种常见、易发现、易解决的泄露类型。

    先看一段正常的代码:

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
// 其实也非常简单,只不过ContentObserver是系统的例子,有必要单独拿出来提示一下大家,不可掉以轻心
private  final  ContentObserver mSettingsObserver =  new  ContentObserver( new  Handler()) {
     @Override
     public  void  onChange( boolean  selfChange, Uri uri) {
         // todo
     }
};
 
@Override
public  void  onStart() {
     super .onStart();
 
     // register the observer
     getContentResolver().registerContentObserver(Settings.Global.getUriFor(
             xxx),  false , mSettingsObserver);
}
 
@Override
public  void  onStop() {
     super .onStop();
 
     // unregister it when stoping
     getContentResolver().unregisterContentObserver(mSettingsObserver);
 
}

  看完示例,我们来看看病例:

1
2
3
4
5
6
7
8
private  final  class  SettingsObserver  implements  Observer {
     public  void  update(Observable o, Object arg) {
         // todo ...
     }  
}
 
  mContentQueryMap =  new  ContentQueryMap(mCursor, Settings.System.XXX,  true null );
  mContentQueryMap.addObserver( new  SettingsObserver());

    靠,谁这么偷懒,把SettingObserver搞个匿名对象传进去,这可如何是好?

    所以,有些懒是不能偷的,有些语法糖是不能吃的。

    解决方案就是, 在不需要或退出的时候delete这个Observer。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private  Observer mSettingsObserver;
@Override
public  void  onResume() {
     super .onResume();
     if  (mSettingsObserver ==  null ) {
         mSettingsObserver =  new  SettingsObserver();
     }  
     mContentQueryMap.addObserver(mSettingsObserver);
}
 
@Override
public  void  onStop() {
     super .onStop();
     if  (mSettingsObserver !=  null ) {
         mContentQueryMap.deleteObserver(mSettingsObserver);
     }  
     mContentQueryMap.close();
}

  注意一点,不同的注册方法,不同的反注册方法。

1
2
3
4
5
6
7
8
// 只是参考,不必死板
/*
addCallback             <==>     removeCallback
registerReceiver        <==>     unregisterReceiver
addObserver             <==>     deleteObserver
registerContentObserver <==>     unregisterContentObserver
... ...
*/

 10. 单例模式

前面的9种情况都是一个根本原因,的是因为在activity中创建了匿名内部类,默认持有了activity的强引用。

单例模式下是因为静态变量持有了context

单例的生命周期和应用(Application)的生命周期一样长

单例一般都是下面的设计


构造函数传入的是个context

如果传入的是activity的context就会内存泄露

解决方法

我们不管传入的是Context还是ApplicationContext,我们都将它转换为ApplicationContext,这样这个单例就不会持有Activity或其他Context的强引用了。

private GlobalManager(Context context) {
        this.mContext = context.getApplicationContext();
    }











评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值