使用ApplicationContext作为全局变量引用的缺陷

在上一篇博客中,我讲了初次开发安卓必须知道的 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 FreemanNat 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的。

时间: 2024-10-12 13:47:03

使用ApplicationContext作为全局变量引用的缺陷的相关文章

php中的全局变量引用

全局变量在函数外部定义,作用域为从变量定义处开始,到本程序文件的末尾.但和其他语言不同,php的全局变量不是自动设为可用的,在php中函数可以视为单独的程序片段,局部变量会覆盖全局变量的能见度,因此,在函数中无法直接调用全局变量. 如下: <?php $one=100; $two=200; //定义全局变量 funcation demo(){ echo "运算结果:".($one+$two)."<br>"; //相当于在函数内部新声明两个没赋初值的

VUE CLI3 less 全局变量引用

方法一 1.添加依赖 style-resources-loader 2.vue.config.js中添加 module.exports = { pluginOptions: { 'style-resources-loader': { preProcessor: 'less', patterns: [], }, }, }; 3.添加全局less引入 module.exports = { pluginOptions: { 'style-resources-loader': { preProcesso

php对引用的简单理解

背景 php语言的高度封装和五花八门的库使这门语言很容易上手,而且开发效率比C/C++高出许多.但是也正是由于php封装度很高,一些在c语言中很简单的概念,让php这么一封装,就变得难以琢磨.比如引用,在c语言中的概念很简答, 就是两个变量名指向同一块内存.而且引用必须要你手动操作,哪个变量引用的哪块内存在编写代码的时候心里是一清二楚的.但是在php中,有很多地方使用了隐式的引用,而写代码的时候并不知道这是引用.这就很容易造成问题,而且难以发现.就比如下面的代码我没有使用引用啊?但是name的值

Lua总结一

值和类型(Values and Types) Lua是一门动态类型语言,这意味着变量没有类型,只有值有类型.语言没有类型定义,所有值携带自己的类型. Lua中所有的值都是一等公民,这意味着所有的值都可以存储在变量中,作为参数传递给其他函数,作为函数的结果返回. 值得注意的是这点对函数也成立,在Java中函数是没这个待遇的.比如对一个列表排序,需要一个比较函数,Lua可以直接传递比较函数,而Java需要为这个比较函数创建一个类型,然后传递这个类型的实例. 来看一个例子,将一个列表从大到小排列: L

[持续交付实践] pipeline:pipeline 使用之快速入门

什么是pipeline 先介绍下什么是Jenkins 2.0,Jenkins 2.0的精髓是Pipeline as Code,是帮助Jenkins实现CI到CD转变的重要角色.什么是Pipeline,简单来说,就是一套运行于Jenkins上的工作流框架,将原本独立运行于单个或者多个节点的任务连接起来,实现单个任务难以完成的复杂发布流程.Pipeline的实现方式是一套Groovy DSL,任何发布流程都可以表述为一段Groovy脚本,并且Jenkins支持从代码库直接读取脚本,从而实现了Pipe

spring学习四

1: InitializingBean  vs  init-method InitializingBean 是一个接口,有一个方法afterPropertiesSet,不建议使用:因为InitializingBean是Spring接口,这样导致 bean和spring耦合到一起了. <bean id="SimpleBean1" class="com.SimpleBean" init-method="init" destroy-method=

Struts2学习第七课 OGNL

关于值栈: helloWorld时,${productName}读取productName值,实际上该属性并不在request等域中,而是从值栈中获取的. ValueStack: 可以从ActionContext中获取值栈对象 值栈分为两个逻辑部分: 1.Map栈:实际上是OgnlCOntext类型,是个Map,也是对ApplicationContext的一个引用,里边保存着各种Map,requestMap,sessionMap,applicationMap,parametersMap,attr

Javascript中this关键字详解

原文出处:http://www.cnblogs.com/justany/archive/2012/11/01/the_keyword_this_in_javascript.html Quiz 请看下面的代码,最后alert出来的是什么呢? 1 var name = "Bob"; 2 var nameObj ={ 3 name : "Tom", 4 showName : function(){ 5 alert(this.name); 6 }, 7 waitShowNa

[Think In Java]基础拾遗1 - 对象初始化、垃圾回收器、继承、组合、代理、接口、抽象类

目录 第一章 对象导论第二章 一切都是对象第三章 操作符第四章 控制执行流程第五章 初始化与清理第六章 访问权限控制第七章 复用类第九章 接口 第一章 对象导论 1. 对象的数据位于何处? 有两种方式在内存中存放对象: (1)为了追求最大的执行速度,对象的存储空间和生命周期可以在编写程序时确定,这可以通过将对象置于堆栈或者静态存储区域内来实现.这种方式牺牲了灵活性. (2)在被称为堆的内存池中动态地创建对象.在这种方式,知道运行时才知道对象需要多少对象,它们的生命周期如何,以及它们的具体类型.