到底什么是Context?

a19911009 8年前
   <h2><strong>Context类对于做Android开发的同学肯定不陌生,但或许许多同学都没有正确地使用Context实例。</strong></h2>    <p>Context实例非常常见,在许多的情境下(加载资源、启动一个Activity、取得一个系统级的Service、取得应用独有的文件存储路径还有创建View等)都需要用到一个Context实例,但如果不加区分地使用任意的Context实例,很容易会导致一些没意料到的状况发生。</p>    <h2><strong>Context的种类</strong></h2>    <p>并不是所有的Context实例都是一样的构造流程。常见的Context子类如下所列:</p>    <ul>     <li> <p>Application——在你的应用进程中单例存在的一个实例。可以通过Activity或Service的getApplication()方法或者其他任意Context子类的getApplicationContext()方法来取得。不论是在哪里以及何时取得的Application实例,它都是进程唯一的。</p> </li>     <li> <p>Activity/Service——继承自ContextWrapper类,它们实现了与Context类同样的API,但代理了所有的方法到一个对外不可见的Context实例,也就是它们的base Context。每当系统框架创建一个新的Activity或者Service实例时,它同时也会创建一个ContextImpl实例去执行不同的组件所需要做的不同逻辑。每个Activity或Service,以及它们相应的base context,都是实例唯一的。</p> </li>     <li> <p>BroadcastReceiver——这并不是一个Context子类。但每个Receiver都会实现onReceive(Context context, Intent intent)这个回调方法,每次系统发送通知都是调用到这个回调方法,这里就给Receiver传入了一个Context实例。这里传入的Context实例又与其他的Context实例不一样,这里传入的Context实例是不能调用registerReceiver()方法和bindService()方法的。每次发送一个通知的时候,这里传入的Context实例都是不一样的。</p> </li>     <li> <p>ContentProvider——这同样也不是一个Context子类。但它内部持有一个Context实例,这个实例可以通过getContext()方法取得。如果ContentProvider与调用者是运行在同一个进程中,那么它的getContext()方法返回的Context实例其实就是这个进程里的始终单例的Application Context。不过如果ContentProvider与调用者是运行在不同的进程中的,如应用A去调用应用B的ContentProvider,那么这时候ContentProvider的getContext()方法返回的则是应用B里的Application Context。</p> </li>    </ul>    <h2><strong>引用的保存</strong></h2>    <p>呐,我们先来说说非常常见的一种保存Context实例的引用从而导致内存泄漏的情形:一个实例或一个类,它保存了一个生命周期比自己短的Context实例,这就会导致内存泄漏。举个例子,创建一个需要依赖一个Context实例的单例类来进行一些通用操作如加载资源、调用一个ContentProvider,并把当前Activity或者Service作为它依赖的Context实例设置进去。</p>    <p>错误单例的示范</p>    <pre>  <code class="language-java">public class CustomManager {      private static CustomManager sInstance;        public static CustomManager getInstance(Context context) {          if (sInstance == null) {              sInstance = new CustomManager(context);          }          return sInstance;      }        private Context mContext;        private CustomManager(Context context) {          mContext = context;      }  }</code></pre>    <p>这段代码最大的问题是我们并不知道传入的Context参数是啥Context,所以对于我们这个单例来说直接保存这个Context的引用是很危险的(例如这里的Context是一个Activity或者Service的时候)。因为单例里面的对象是静态的,这就会导致它引用的所有资源都不会被系统GC回收掉,假设这里的Context是一个Activity的话,我们这样做就会导致这个Activity相关的View啊还有别的占内存的对象一直不能被系统回收掉,进而导致了内存泄漏。</p>    <p>为了避免这种情况,我们在下面的单例中改为始终是保存Application Context的引用。</p>    <p>正确单例的示范</p>    <pre>  <code class="language-java">public class CustomManager {      private static CustomManager sInstance;        public static CustomManager getInstance(Context context) {          if (sInstance == null) {              //不管什么Context,都改为取Application Context              sInstance = new CustomManager(context.getApplicationContext());          }          return sInstance;      }        private Context mContext;        private CustomManager(Context context) {          mContext = context;      }  }</code></pre>    <p>这样我们就不用关心传入的Context到底是什么了,因为我们现在持有的引用是Application Context。就像前文提到的,Application Context是在整个应用程序中进程单例的,所以哪怕我们在代码中对它持有静态引用也不会导致什么内存泄漏。</p>    <p>那,为什么我们不能 <strong>总是</strong> 使用Application Context来完成各处需要Context的逻辑呢?这样不就可以永不担心Context相关的内存泄漏了吗?原因其实很简单,就像我在一开头就提到的——一个Context实例并不一定能与另一个Context实例等同。</p>    <h2><strong>不同种类的Context的能力区别</strong></h2>    <p>直接参考下表即可:</p>    <table>     <thead>      <tr>       <th> </th>       <th>Application</th>       <th>Activity</th>       <th>Service</th>       <th>ContentProvider</th>       <th>BroadcastReceiver</th>      </tr>     </thead>     <tbody>      <tr>       <td>构造展示一个Dialog</td>       <td>NO</td>       <td>YES</td>       <td>NO</td>       <td>NO</td>       <td>NO</td>      </tr>      <tr>       <td>启动一个Activity</td>       <td>NO <sup>1</sup></td>       <td>YES</td>       <td>NO <sup>1</sup></td>       <td>NO <sup>1</sup></td>       <td>NO <sup>1</sup></td>      </tr>      <tr>       <td>导入布局文件</td>       <td>NO <sup>2</sup></td>       <td>YES</td>       <td>NO <sup>2</sup></td>       <td>NO <sup>2</sup></td>       <td>NO <sup>2</sup></td>      </tr>      <tr>       <td>启动一个Service</td>       <td>YES</td>       <td>YES</td>       <td>YES</td>       <td>YES</td>       <td>YES</td>      </tr>      <tr>       <td>绑定到一个Service</td>       <td>YES</td>       <td>YES</td>       <td>YES</td>       <td>YES</td>       <td>NO</td>      </tr>      <tr>       <td>发送一个广播</td>       <td>YES</td>       <td>YES</td>       <td>YES</td>       <td>YES</td>       <td>YES</td>      </tr>      <tr>       <td>注册一个BroadcastReceiver</td>       <td>YES</td>       <td>YES</td>       <td>YES</td>       <td>YES</td>       <td>NO <sup>3</sup></td>      </tr>      <tr>       <td>加载资源数值</td>       <td>YES</td>       <td>YES</td>       <td>YES</td>       <td>YES</td>       <td>YES</td>      </tr>     </tbody>    </table>    <p>附注:</p>    <ol>     <li>一个非Activity的Context可以用于启动一个Activity,但这样启动的Activity需要新创建一个Activity堆叠栈。这个在某些特定情形下或许会适用,但这种设计一般来说都不太好。</li>     <li>这个其实也是可以的,但是这样导入的布局会用当前系统的默认主题来设置,而不是用你在你的应用程序中设定的主题来设置的。</li>     <li>在Android 4.2及以上的系统里,如果receiver是null,那这也是可以的。这样做是为了取得一个严格广播的当前值。</li>    </ol>    <h2><strong>用户交互界面</strong></h2>    <p>从上表可以看出好些操作不适合使用Application Context来执行,而这些操作无一例外地全都是和用户交互界面直接相关的。适合执行这些与用户交互界面直接相关的操作的Context只有一种,那就是Activity;其他的Context其实和Application Context的功能都差不多。</p>    <p>不过其实这些个与UI相关的操作其实大多数时候都是在Activity中才会有执行的机会。假设使用一个非Activity的Context来调用展示一个Dialog,在调用Dialog实例的show()方法时就会报以下的错误直接崩溃:</p>    <pre>  <code class="language-java">Caused by: android.view.WindowManager$BadTokenException: Unable to add window -- token null is not for an application</code></pre>    <p>又或者使用一个非Activity的Context来启动另一个Activity,同样也会报错崩溃:</p>    <pre>  <code class="language-java">Caused by: android.util.AndroidRuntimeException: Calling startActivity() from outside of an Activity  context requires the FLAG_ACTIVITY_NEW_TASK flag. Is this really what you want?</code></pre>    <p>但如果是使用一个非Activity的Context来导入布局,应用并不会报错崩溃。详细的流程可以参见我之前写的 <a href="/misc/goto?guid=4959715850715578964" rel="nofollow,noindex">《布局的导入》</a> 。此时,Android框架会默默地返回你需要的布局文件对应的View,其中的各个View的层次关系都是正确正常的,只是你在应用程序中设定的主题和样式(在AndroidManifest.xml中设定的值)不会被应用到此时导入布局文件而产生的View中去,而是应用了系统默认的主题。这是因为在Manifest中定义的主题实际上是仅仅绑定到Activity这种Context上的,所以如果使用非Activity的Context实例来导入布局,那就只会应用系统默认的主题,从而导入了一个可能并不是你所期望的布局样式。</p>    <h2><strong>但上述规则是不是有不完善的地方?</strong></h2>    <p>有些同学在开发的时候会发现,依照目前的程序设计,我们的程序就是要长时间的持有一个Context实例,而且这个实例还必须是Activity,因为在这长时间的持有过程中,会涉及到UI相关的操作逻辑。那么假设真的有这种情况,我强烈建议你们重新审视你们的程序的设计,因为这种情形完全就是在 <em>对抗Android系统框架</em> 。</p>    <h2><strong>经验总结</strong></h2>    <p>在大多数情形下,代码是跑在哪类Context内就使用当前可获得的这类Context即可。只要这个Context类引用并不会超脱出它所引用的组件的生命周期,那你完全可以在你的逻辑代码中持有这个引用。但是如果你需要长时持有一个Context引用,这个引用甚至会超脱你的Activity或Service的生命周期,哪怕仅仅是短暂地超脱出生命周期,也务必要把这个Context引用改为Application引用。</p>    <h2><strong>译者说两句</strong></h2>    <p>这段时间断更了抱歉。</p>    <p>这篇文章虽然是2013年的老博文了,但在我看来还是非常有学习价值的。这是我第一次翻译技术类文章,所以可能表述得不太好,我日后会继续努力提升翻译水平的。</p>    <p>依文中所说,在需要Context的时候,直接取能取到的“最近”的Context实例即可,一般情形下是不会导致内存泄漏的。举个例子,在一个Activity A里有个Fragment a,然后Fragment a里面有Adapter View,那这时候就需要透传Context实例来构造Adapter View里面的Item View了,那这时候,其实大胆地在a里面透传A的引用到Adapter中其实是没有问题的, <strong>只要不要把持有的A的引用声明为静态就好</strong> 。</p>    <p>再比如,在后台有个定时任务或者什么的,在特定时机要往SharedPreferences里面写数据啊或者要读取资源文件中的string字符串啥的,这时候就可以在定时任务的代码中长期持有一个Application Context的引用来执行相关的操作,这样也是不会引发内存泄漏的。</p>    <p> </p>    <p>来自:http://www.jianshu.com/p/f7b6611df773</p>    <p> </p>