• 避免Android Context引起的内存泄露

    Context的使用细节
    服务器君一共花费 34.710 ms 进行了 4 次数据库查询,努力地为您提供了这个页面。
    广告很萌的

    Android的应用最开始被限制为最多占用16m的内存,至少在T-Mobile G1上是这样的。它包括电话本身占用的和开发者可以使用的两部分。即使你没有占用全部内存的打算,你也应该尽量少的使用内存,以免别的应用在运行的时候关闭你的应用。Android能在内存中保持的应用越多,用户在切换应用的时候就越快。

    我仔细研究了Android应用的内存泄露问题,大多数情况下它们是由同一个错误引起的,那就是对一个上下文(Context)保持了长时间的引用。

    在Android中,上下文(Context)被用作很多操作中,但是大部分是载入和访问资源。这就是所有的widget都会在它们的构造函数中接受一个上下文(Context)参数。在一个合格的Android应用中,你通常能够用到两种上下文(Context):活动(Activity)和应用(Application)。活动(Activity)通常被传递给需要上下文(Context)参数的类或者方法。

    通常我们在各种类和方法间传递的是activity context,比如一个activity的onCreate:

    protected void onCreate(Bundle state) {
    	super.onCreate(state);
    	//传递context给view control
    	TextView label = new TextView(this); 
    	label.setText("Leaks are bad");
    	setContentView(label);
    }
    

    这就意味着那个View有一个对整个活动(Activity)的引用并且对这个活动(Activity)中保持的所有对象有保持了引用;通常它们包括整个View的层次和它的所有资源。因此,如果你“泄露”了上下文(Context)(这里“泄露”的意思是你保持了一个引用并且组织GC收集它),你将造成大量的内存泄露。如果你不够小心的话,“泄露”一整个活动(Activity)是件非常简单的事情。

    把activity context传递给view,意味着view拥有一个指向activity的引用,进而引用activity占有的资源:view hierachy, resource等。

    如果context发生内存泄露的话,就会泄露很多内存。这里泄露的意思是gc没有办法回收activity的内存。

    为什么GC没有办法回收相应的内存,个人感觉是因为传递Context会增加对象指针的引用计数,所以基于智能指针技术的GC无法释放相应的内存。

    当屏幕的方向改变时系统会默认的销毁当前的活动(Activity)并且创建一个新的并且保持了它的状态。这样的结果就是Android会从资源中重新载入应用的UI。现在想象一下,你写了一个应用,有一个非常大的位图,并且你并不想在每次旋转时都重新载入。保留它并且每次旋转不重新加载的最简单的办法就是把它保存在一个静态字段上。

    当屏幕旋转的时候,系统会销毁当前的activity,保存状态信息,再创建一个新的。比如我们写了一个应用程序,它需要加载一个很大的图片,我们不希望每次旋转屏幕的时候都销毁这个图片,重新加载。实现这个要求的简单想法就是定义一个静态的Drawable,这样Activity 类创建销毁它始终保存在内存中。实现类似:

    public class myactivity extends Activity {
    	private static Drawable sBackground;
    	
    	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);//drawable attached to a view
    		
    		setContentView(label);
    	}
    }
    

    这段代码非常快,同时也错的够离谱。它泄露了当第一次屏幕角度改变时创建的第一个活动(Activity)。当一个Drawable被附加到一个View,这个View被设置为drawable的一个回调。在上面的代码片断中,这意味着这个Drawable对TextView有一个引用,同时这个TextView对Activity(Context对象)保持着引用,同时这个Activity对很多对象又有引用(这个多少还要看你的代码了)。

    这个例子是造成Context泄露的最简单的一个原因,你可以看一下我们在主屏幕源码(查看unbindDrawables()方法)中是通过在Activity销毁时设置保存过的Drawable的回调为空来解决这个问题的。更为有趣的是,你可以创建一个context泄露的链,当然这非常的糟糕。它们可以让你飞快的用光所有的内存。

    当屏幕旋转的时候会有leak(即gc没法销毁activity)。我们刚才说过,屏幕旋转的时候系统会销毁当前的activity。但是当drawable和view关联后,drawable保存了view的 reference,即sBackground保存了label的引用,而label保存了activity的引用。既然drawable不能销毁,它所引用和间接引用的都不能销毁,这样系统就没有办法销毁当前的activity,于是造成了内存泄露。gc对这种类型的内存泄露是无能为力的。避免这种内存泄露的方法是避免activity中的任何对象的生命周期长过activity,避免由于对象对 activity的引用导致activity不能正常被销毁。

    有两种简单的方法可以避免与context相关的内存泄露。最明显的一个就是避免在context的自身的范围外使用它。上面的例子展示了在类内部的一个静态的引用和它们对外部类的间接引用是非常危险的。第二个解决方案就是使用Application Context。这个context会伴随你的应用而存在,并且不依赖Activity的的生命周期。如果你计划保持一个需要context的长生命周期的对象,请记得考虑Application对象。你可以非常方便的通过调用Context.getApplicationContext() 或者 Activity.getApplication()获取它。

    总之,为了避免涉及到context的内存泄露,请记住如下几点:

    1. 不要让生命周期长的对象引用activity context,即保证引用activity的对象要与activity本身生命周期是一样的
    2. 对于生命周期长的对象,可以使用application context
    3. 避免非静态的内部类,尽量使用静态类,避免生命周期问题,注意内部类对外部对象引用导致的生命周期变化。如果你不能控制它们的生命周期,在活动(Activity)中避免使用不是静态的内部类,使用静态类并且使用弱引用到活动(Activity)的内部。对于这个问题的解决方法是使用静态的内部类与一个弱引用(WeakReference)的外部类。就像ViewRoot和它的W内部类那么实现的。
    4. 垃圾回收器对于内存泄露来说并不是百分百保险的。
更多 推荐条目

Welcome to NowaMagic Academy!

现代魔法 推荐于 2013-02-27 10:23   

本章最新发布
随机专题
  1. [JavaScript程序设计] jQuery与表单操作 2 个条目
  2. [移动开发] ListView 使用相关问题集 1 个条目
  3. [软件工程与项目管理] 呈现树的构建 13 个条目
  4. [PHP程序设计] 声明式编程范式 12 个条目
  5. [PHP程序设计] fsockopen,curl与file_get_contents 12 个条目
  6. [PHP程序设计] PHP数组的遍历 7 个条目
  7. [PHP程序设计] PHP数组探索 4 个条目
  8. [智力开发与知识管理] 整体性学习策略 9 个条目
  9. [PHP程序设计] PHP里的引用 5 个条目
  10. [Python程序设计] 标准库:urllib/urllib2 14 个条目
  11. [运维管理] 路由器与交换机 4 个条目
  12. [PHP程序设计] PHP中的Hash算法 3 个条目
窗口 -- [博客]