App内存泄露分析之绝世高手篇

来源:互联网 发布:淘宝货源软件 编辑:程序博客网 时间:2024/04/30 15:58

转载自:http://www.cnblogs.com/qianxudetianxia/p/3645106.html

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

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

1. CallBack对象的引用

    先看一段代码:

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);
}

    有问题吗?

    哈哈,先Hold住一下,先来说一下android各版本发布的历史:

1
2
3
4
5
6
/*
2.2        2010-3-20,Froyo
2.3        2010-12-6, Gingerbread
3.0        2011-2-22, Honeycomb
4.0        2011-10-11 Ice Cream Sandwich
*/

    了解源码的历史,是很有益于我们分析android代码的。

    好,开始分析代码。

    首先,查看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初学者不断的转载学习。

    但是经过本人深入分析Drawable源码,事情发生了一些变化。

    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对象

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

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对象

    先看一段代码:

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

    Handler泄露的关键点有两个:

    1). 内部类

    2). 生命周期和Activity不一定一致

    第一点,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对象

    同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对象

    我N年前去盛大面过一次试,当时面试官极力推荐我使用AsyncTask等系统自带类去做事情,当然无可厚非。

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

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

    但是,问题是如何终止?

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

    它的说明有这么一句话:

1
2
// 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是不一定成功的,如果正在运行,它可能会中断后台任务。怎么感觉这话说的这么不靠谱呢?

    是的,就是不靠谱。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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的说明中撂下了一大段英文:

1
// 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.

    学好英语,避免踩坑!

 

6. BroadcastReceiver对象

    ... has leaked IntentReceiver ... Are you missing a call to unregisterReceiver()?

    这个直接说了,种种原因没有调用到unregister()方法。

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

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

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

    有两种解决方案:

    方案一:在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) { 
        mTimerTask = new TimerTask() { 
            @Override 
            public void run() { 
                // todo
            
        }; 
    
 
    if(mTimer != null && mTimerTask != null 
        mTimer.schedule(mTimerTask, 10001000); 
 
}

  泄露的点是,忘记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. 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, truenull);
 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
... ...
*/

 

9. Dialog对象

    android.view.WindowManager$BadTokenException: Unable to add window -- token android.os.BinderProxy@438afa60 is not valid; is your activity running?

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

    关键点就是,怎么判断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);
    }  
};

  早完早释放!

 

10. 其它对象

    以Listener对象为主,"把自己搭进去了,切记一定要及时把自己放出来"。

 

11. 小结

     结合本文Context篇和前面Cursor篇,我们枚举了大量的泄露实例,大部分根本原因都是相似的。

     通过分析这些例子后,我们应该能理解APP层90%的内存泄露情况了。

     至于怎么发现和定位内存泄露,这是另外一个有意思的话题,现在只能说,有方法有工具。



原创粉丝点击