在上一篇博客中,我讲了初次开发安卓必须知道的 6件事(6 THINGS I WISH I KNEW BEFORE I WROTE MY FIRST ANDROID APP)。其中一条就是:不要有一个Context的静态引用。我这么警告的原因是一个Context的静态引用可能引发内存泄露。但是一位读者指出:一个Application Context的静态引用不会造成内存泄露,因为只要程序还在运行,Application Context的生命周期就不会结束。我则反驳到:技术上来说,你可以拥有一个Application
Context的静态引用而不造成内存泄露,但是我推荐你这样做。
在这篇博客中,我想解释一下为什么拥有和使用一个Application Context的静态引用不是一个理想的选择。之所以强调“理想的选择”,因为我并不是说使用Application Context的静态引用每次都会造成程序崩溃。相对的,我这篇博客所说的是一些使用Context静态引用的缺陷,以至于说这并不是开发安卓应用最简洁的方式。
1. 对象/方法 使用Application Context静态引用都是“欺骗”
这一点是出自谷歌对于编写可测试代码的指南(Google’s Guide to Writing testable code)。在这个指南中,他们指出了:
静态的获取全局变量并没有将它们(全局变量)构造函数和方法的依赖关系告诉给阅读代码的人。全局变量和单例通过API掩盖了它们真实的依赖关系。如果想要真正理解依赖关系,开发人员必须逐行阅读代码。(Accessing global state statically doesn’t clarify those shared dependencies to readers of the constructors and methods
that use the Global State. Global State and Singletons make APIs lie about their true dependencies. To really understand the dependencies, developers must read every line of code.)
一个Application Context的全局静态引用也正如这点所说:读这个对象的人无法知道,这个对象依赖Context只是为了使用它的API。当一个对象拥有一个清晰真实的API来表达它的依赖,就能够更容易的理解类或者方法的功能以及它将如何实现这个功能。
下面用一个简单的例子进行阐述。假设你当你阅读代码时,遇到了一个这样的方法名称:
public void displayString(String stringToDisplay)
当你遇到这个名称的时候,你没有办法知道这个方法会怎样显示参数传入的字符串。现在,假设你阅读到的是这样的方法名:
public void displayString(Context context, String stringToDisplay)
对于这样的方法名,你有一个线索(译者:线索指Context参数):这个方法也许是用Toast显示字符串。因为Context是一个“万能类”,对于一个特定的对象或者方法使用了它并不总是能够显示这个对象/方法的功能或者它如何实现这个功能的。但是,一点点的提示也比没有任何提示要强。
(译者:可能翻译的有些不通顺了,这里总的解释一下。就是说使用ApplicationContext的静态引用去使用一些方法的话,你是无法判断这个方法是不是需要用到Context的。而如果不使用ApplicationContext的静态引用的话,当一个方法需要用到Context对象时(如Toast),就必须多一个Context参数。而不需要时,则不会有Context参数。阅读代码的时候就可以根据这一点对方法的实现上有一定的估计)。
2. 使用了Application Context静态引用的对象不是封装的
封装虽然经常被提及,但是并没有一个准确的定义。我也不打算使这个定义更复杂。当说“封装”时,我指的是Steve Freeman和Nat Pryce在基于测试的面向对象编程(Growing Object Oriented Software Guided by Tests)所提到的概念:
(封装)保障了一个对象的行为只能被它的API所影响。它保障了无关对象之间不会产生未知的引用,从而使得我们可以控制一个对象的修改对系统其它部分的影响。([It] Ensures that the behavior of an object can only be affected through its API. It lets us control how much a change to one object will impact
other parts of the system by ensuring that there are no unexpected dependencies between unrelated components. -Pg. 92)
因为使用ApplicationContext静态引用的对象关联的是一个全局依赖,这些对象的行为可能会被全局共享的Application Context所影响。因为Application Context并不是这些对象API的一部分,这就意味着对象行为的改变可能不是被该对象的API所影响的。换一句话说,这就意味着使用Application Context静态引用会破坏封装。
在大多数情况下,以这样一种形式破坏封装不会产生太大影响。事实上,我仅可以想象的几个可能产生问题的例子也看起来像是故意编造的。但是,我仍然认为,在其他条件相等的情况下,我们应当选择能100%在全部情况下适用的结构,而不是99%都适用的结构。再一次的,使用ApplicationContext的静态引用和对封装的破坏并不会使你的程序崩溃,但是这并不是最稳定的结构。
3. 使用了Application Context静态引用的对象可能难以进行单元测试
如果你的一个对象调用了Application Context里的一个方法,并且你想要验证这个方法在单元测试中被调用了,使用一个静态引用不会让你好受。正如我在为什么Android单元测试这么困难中所说,你会在一些情况下想要进行这样一个操作。假设你已经有一个启动安卓Service的ServiceLauncher对象。如果你使用了依赖注入来在ServiceLaucher被引用的时候传入一个Context对象,单元测试就很简单:
public class ServiceLauncherTests { @Mock Context mContext; @Test public void launchesSessionCalendarService() { ServiceLauncher serviceLauncher = new ServiceLauncher(mContext); serviceLauncher.launchSessionCalendarService(); verify(mContext).startService(any(Intent.class)); } }
如果这个ServiceLaucher使用了Application Context静态引用,这个对象就很难进行单元测试了。在这个例子当中,你可以使用测试支持库的UI测试来验证Intent被发送了,但是UI测试比单元测试要慢。并且,你也可能需要验证Context中也一些不使用Intent的方法。所以,注入一个Context到目标对象中相比全局静态变量更加灵活,即使你可以使用测试支持库来帮助你验证Intent的发送。
4. 使用了Application Context静态引用的对象更可能违背迪米特法则(最少知道法则、最少引用法则)
我们经常使用Context去获取一个我们需要的对象的引用。一个特定的对象可能会需要Resoureces, SharePreferences或者 PackageManager去实现它的功能。当我们有一个全局Application Context引用时,我们可能会尝试去通过这样一种方式去获取这些对象的引用:
public class SmellySessionColorResolver { public SmellySessionColorResolver() { } public int resolveSessionColor(int sessionColor) { if (sessionColor == 0) { // no color -- use default sessionColor = IOApplication.getContext().getResources().getColor(R.color.default_session_color); } else { // make sure it's opaque sessionColor = UIUtils.setColorAlpha(sessionColor, 255); } return sessionColor; } }
这就违背了迪米特法则。我实际上也抱怨过违背迪米特法则使得一个程序难以进行单元测试。但是即使你不关心单元测试,违背迪米特法则通常被视为一种恶心的代码风格。
结论
我不认为我讲得东西存在太大的争议。我认为我只是把从更聪明的人身上学到的通用编程课程给运用上了而已。当然,欢迎批评和指正。
如果你确信了你需要避免Application Context静态引用的使用,向必要的对象和方法中注入Context应当不是一件难事。你甚至可能发现你在重构过程中能够消除一大堆违背迪米特法则的代码。Android Studio的推测和重构功能使得这项工作更加轻松,哪怕有点无聊。
译者结论
这篇文章基本列举了使用Application Context几个公认的弊端。对于将Application Context当作全局变量的使用,可能是因为这是安卓独特的方法,一度被认为是最合理的方法。然而谷歌官方文档却指出了,重写Application类其实是一种不推荐的做法,因为并没有任何理由去说明它比传统的JAVA全局变量要好。而且这样做还容易使得Application变得又臭又长,引用的时候也需要添加很长的一句话。因此,我还是赞同不要使用Application
Context的。