如何在Android中声明全局变量?

608

我正在创建一个需要登录的应用程序。我创建了主页面和登录页面。

在主页面的onCreate方法中,我添加了以下条件:

public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    ...

    loadSettings();
    if(strSessionString == null)
    {
        login();
    }
    ...
}

当登录表单结束时执行的onActivityResult方法看起来像这样:

@Override
public void onActivityResult(int requestCode,
                             int resultCode,
                             Intent data)
{
    super.onActivityResult(requestCode, resultCode, data);
    switch(requestCode)
    {
        case(SHOW_SUBACTICITY_LOGIN):
        {
            if(resultCode == Activity.RESULT_OK)
            {

                strSessionString = data.getStringExtra(Login.SESSIONSTRING);
                connectionAvailable = true;
                strUsername = data.getStringExtra(Login.USERNAME);
            }
        }
    }

问题在于登录表单有时会出现两次(login() 方法被调用了两次),当手机键盘滑动时,登录表单又会再次出现,我猜测问题出在变量strSessionString上。

有没有人知道如何将该变量设置为全局变量,以避免用户成功认证后再次出现登录表单?


如何使用“saved instance state bundle”处理 Activity 状态的好教程:http://www.quicktips.in/handling-activity-state-using-saved-instance-state-bundle/ - Deepak Swami
17个回答

967

我在'09年写下了这个答案,当时Android相对较新,在Android开发的许多不太成熟的领域中存在着许多问题。在此帖子底部添加了一个长的附录,解决了一些批评,并详细说明了我与使用Singletons而不是子类化Application存在的哲学分歧。阅读风险自负。

原始答案:

你遇到的更普遍的问题是如何跨多个Activity和应用程序所有部分保存状态。静态变量(例如单例)是实现这一目标的常见Java方式。然而,我发现在Android中更优雅的方法是将状态与应用程序上下文相关联。

正如您所知,每个Activity也是一个Context,它是关于其执行环境的最广泛的信息。您的应用程序也有一个上下文,并且Android保证它将作为单个实例存在于整个应用程序中。

要做到这一点,您需要创建自己的android.app.Application子类,然后在清单文件中的应用程序标记中指定该类。现在,Android将自动创建该类的实例并使其在整个应用程序中可用。您可以使用Context.getApplicationContext()方法从任何context访问它(Activity还提供了一个具有完全相同效果的getApplication()方法)。以下是一个极其简化的示例,附带警告:

class MyApp extends Application {

  private String myState;

  public String getState(){
    return myState;
  }
  public void setState(String s){
    myState = s;
  }
}

class Blah extends Activity {

  @Override
  public void onCreate(Bundle b){
    ...
    MyApp appState = ((MyApp)getApplicationContext());
    String state = appState.getState();
    ...
  }
}

这与使用静态变量或单例模式基本相同,但非常好地集成到现有的Android框架中。请注意,如果您的应用程序是具有多个进程的罕见应用程序之一,则此方法将无法跨进程工作。

从上面的示例中需要注意的一点是:假设我们改为执行以下操作:

class MyApp extends Application {

  private String myState = /* complicated and slow initialization */;

  public String getState(){
    return myState;
  }
}

现在,每次实例化应用程序时都会执行这种缓慢的初始化操作(如磁盘读取、网络连接等),这可能会阻塞进程。您可能认为,这只需要为进程付出一次代价,无论如何我都必须支付代价,对吗?例如,正如Dianne Hackborn在下面提到的那样,完全有可能仅为处理后台广播事件而实例化您的进程。如果您的广播处理不需要此状态,则可能仅为无意义地执行了一系列复杂和缓慢的操作。在这里,懒惰实例化是关键。以下是一种稍微复杂一些的使用Application的方式,适用于除最简单用途以外的任何情况:
class MyApp extends Application {

  private MyStateManager myStateManager = new MyStateManager();

  public MyStateManager getStateManager(){
    return myStateManager ;
  }
}

class MyStateManager {

  MyStateManager() {
    /* this should be fast */
  }

  String getState() {
    /* if necessary, perform blocking calls here */
    /* make sure to deal with any multithreading/synchronicity issues */

    ...

    return state;
  }
}

class Blah extends Activity {

  @Override
  public void onCreate(Bundle b){
    ...
    MyStateManager stateManager = ((MyApp)getApplicationContext()).getStateManager();
    String state = stateManager.getState();
    ...
  }
}

虽然我更喜欢使用应用程序子类化作为更优雅的解决方案,但如果开发人员真的需要将状态与应用程序子类相关联,则我宁愿他们使用单例模式而不是完全不考虑性能和多线程影响。

注意1:正如anticafe所评论的那样,在将您的应用程序覆盖关联到应用程序时,manifest文件中需要一个<application>标签。再次参见Android文档获取更多信息。这是一个示例:

<application
     android:name="my.application.MyApp" 
     android:icon="..."
     android:label="...">
</application>

注意2: 用户user608578在下方询问如何管理本地对象的生命周期。我对在Android中使用本地代码一无所知,因此不能回答它与我的解决方案如何交互的问题。如果有人有答案,我愿意给他们信用并将信息放在这篇文章中以获得最大的可见性。

附录:

正如一些人所指出的,这不是一个持久化状态的解决方案,这是我在原始答案中可能应该更加强调的。即,这不是为了保存用户或其他需要在应用程序生命周期内保持的信息而设计的解决方案。因此,我认为大多数批评都与应用程序随时被杀死等相关的问题无关,因为任何需要存储到磁盘的东西都不应该通过Application子类来存储。它旨在为存储临时、易于重新创建的应用程序状态(例如用户是否已登录)和单实例组件(例如应用程序网络管理器)(不是单例!)提供解决方案。

Dayerman很友好地指出了一个有趣的与Reto Meier和Dianne Hackborn的对话,其中不鼓励使用Application子类而更喜欢Singleton模式。Somatik也早些时候指出了这样的事情,尽管我当时没有看到。由于Reto和Dianne在维护Android平台方面的角色,我不能诚信地建议忽略他们的建议。他们说什么就是什么。我确实希望反对有关更喜欢Singleton而不是Application子类的观点。在我的反对中,我将利用最好在Singleton设计模式的这个StackExchange解释中解释的概念,以便我不必在这个答案中定义术语。我强烈建议在继续之前浏览该链接。逐点说明:

Dianne表示:“没有理由从Application进行子类化。它与创建Singleton没有区别...”这个第一个声明是不正确的。这有两个主要原因。1)Application类为应用程序开发人员提供了更好的生命周期保证;它保证具有应用程序的生命周期。Singleton没有EXPLICITLY与应用程序的生命周期绑定(尽管它是有效的)。这可能对您的普通应用程序开发人员来说不是问题,但我会认为这正是Android API应该提供的类型的合同,并且它还为Android系统提供了更大的灵活性,通过最小化相关数据的生命周期。2)Application类为应用程序开发人员提供了一个状态的单实例持有者,这与Singleton状态的持有者非常不同。有关差异的列表,请参见上面的Singleton解释链接。

Dianne说,“...这可能会让你在未来后悔,因为你的Application对象会变成一个混乱的东西,应该是独立的应用逻辑。” 这当然不是错误的,但这并不是选择Singleton而不是Application子类的原因。 Diane的论点没有提供使用Singleton比Application子类更好的理由,她试图建立的只是使用Singleton并不比Application子类更差,我认为这是错误的。
她接着说,“这更自然地引导你如何管理这些东西——按需初始化。” 这忽略了一个事实,即你也可以使用Application子类按需初始化。再次,没有区别。
Dianne最后说:“框架本身有大量单例,用于维护应用程序的所有共享数据,例如缓存加载的资源、对象池等等。它运行得很好。” 我并不是在争论使用Singleton不能很好地工作或不是一个合法的替代方案。我争论的是,与使用Application子类相比,Singletons没有提供与Android系统一样强的契约,并且使用Singletons通常指向不灵活的设计,这些设计不容易修改,并且会在以后引起许多问题。在我看来,Android API为开发者应用程序提供的强大契约是使用Android编程中最吸引人和令人愉悦的方面之一,这帮助推动了Android平台的早期开发者采用,从而推动了今天Android平台的成功。建议使用Singletons隐含地远离了强大的API契约,而且在我看来,削弱了Android框架。
Dianne还在下面发表了评论,提到了使用Application子类的另一个缺点,它可能鼓励或使编写性能较差的代码更容易。这是非常正确的,我已经编辑了这个答案,以强调考虑性能的重要性,并采取正确的方法,如果你正在使用Application子类。正如Dianne所说,重要的是要记住,每次加载进程时都会实例化你的Application类(如果你的应用程序在多个进程中运行,则可能同时多次加载!),即使进程仅被加载用于后台广播事件。因此,更重要的是将Application类用作指向应用程序共享组件的指针的存储库,而不是进行任何处理的地方!
我给你留下以下关于Singletons的缺点列表,从之前的StackExchange链接中窃取:
  • 无法使用抽象或接口类;
  • 无法子类化;
  • 应用程序耦合度高(难以修改);
  • 难以测试(无法在单元测试中伪造/模拟);
  • 在可变状态的情况下难以并行化(需要广泛的锁定);
我自己加上了:
  • 不清楚和无法管理的生命周期合同不适用于Android(或大多数其他)开发;

5
对于任何想知道如何在AndroidManifest文件的应用程序标签中“指定类”的人,截至本篇写作时,有两个其他答案描述了如何实现(使用android:name),一个是ebuprofen,另一个是Mike Brown。 - Tyler Collier
9
Soonil,你的答案是正确的,但你是否注意到我们应该将 <application android:name=".MyApp" ... /> 添加到Android清单文件中? - anticafe
13
让我再重复一遍,你不应该在全局范围内使用Application。它没有任何作用,与单例相比没有任何好处,并且可能会对启动进程的性能造成负面影响。在创建Application时,你并不知道进程被创建的目的是什么。通过需要时惰性初始化单例,你只需要做必要的工作。例如,如果你的进程被启动来处理有关某个后台事件的广播,那么没有理由去初始化UI所需的任何全局状态。 - hackbod
5
此外,如果您的应用程序正在使用多个进程,那么一个 Application 对象意味着您需要在所有进程中进行全局初始化(时间和内存消耗)。这真是让人头疼。而且,在某些情况下,特别是在恢复期间,您的 Application 对象将不会被创建,这可能会使您陷入麻烦。 - hackbod
15
让我们非常清楚地表明,当我们在讨论选择单例和其他不是全局的方法之间时,你反对单例的所有论点都是完全有效的;单例是全局变量,适用于全局变量的所有注意事项同样适用于单例。但是,“Application”也是一个单例。通过切换到继承“Application”的方式,你并没有摆脱这些问题,因为“Application”与单例完全相同(甚至更糟),只是让你自欺欺人认为你正在做某些更干净的事情。但实际上并不是这样。 - hackbod
显示剩余26条评论

155
创建这个子类。
public class MyApp extends Application {
  String foo;
}
在AndroidManifest.xml中添加android:name属性。 示例
<application android:name=".MyApp" 
       android:icon="@drawable/icon" 
       android:label="@string/app_name">

3
为了让它对我起作用,我必须删除“.MyApp”中的“.”。 - Someone Somewhere
3
请在主Activity之后声明它,否则可能无法安装/部署。 - sami
11
我想说的是,这应该放在已经存在的主要应用标签中...这不是第二个标签 :) 我曾经吃过亏。 - bwoogie
java.lang.IllegalAccessException: access to class is not allowed - Raptor

142

Soonil提出的应用程序状态保持方式是不错的,但它有一个弱点——有些情况下操作系统会杀掉整个应用程序进程。这里有关于此的说明 - 进程和生命周期

考虑一个情况——当你的应用程序进入后台因为有人在打电话(电话应用程序现在在前台)时。在这种情况下,并且在一些其他条件下(请查看上面的链接了解可能是什么条件),操作系统可能会杀死您的应用程序进程,包括Application子类实例。结果状态就会丢失。当您之后返回应用程序时,操作系统将恢复其活动堆栈和Application子类实例,但myState字段将为null。

据我所知,保证状态的安全性的唯一方法是使用任何形式的状态持久化,例如使用应用程序文件的私有存储或SharedPrefernces(它最终使用内部文件系统中的应用程序文件的私有存储)。


10
坚持使用“SharedPreferences”,加个赞!这就是我看到的做法。虽然我觉得滥用偏好设置来保存状态有点奇怪,但它运行得非常好,问题只是一个术语问题。 - Cheezmeister
1
请问您能否发布代码(或提供解释链接),说明如何使用SharedPreferences解决Arhimed所描述的问题? - Someone Somewhere
2
偏好设置、数据库、文件序列化等。如果每个活动使用 onSaveInstanceState 来维护状态,那么它们可以保持状态,但如果用户退出活动并将其从历史堆栈中删除、强制关闭或关闭设备,则无法帮助。 - Darren Hinderer
2
在我看来,这是正确的答案。依赖于相同的应用程序实例存在于各个活动中是一个错误。根据我的经验,在 Android 中,当您处于后台时,完全拆除并重新创建整个进程是非常常见的。处于后台可能只意味着启动相机意图、浏览器意图或接收电话。 - Jared Kells
1
如果持久化的数据仅在该进程实例中使用,您无需滥用共享首选项。当Android终止一个Activity时,它会调用onSaveInstanceState(Bundle)方法。Bundle对象中的数据存储在系统内存中,只要您的进程在内存中存在,这部分内存就不会消失。稍后可以在onCreate(Bundle)或onRestoreInstanceState(Bundle)方法中使用这些数据。除非有必须在进程之外持久化的数据,否则应使用Bundle。虽然可以忽略不计,但Bundle比磁盘上的数据更快,因为它位于主内存中。 - Ryhan
显示剩余5条评论

25

只是一句话提醒..

添加:

android:name=".Globals"

将你的子类命名为现有的 <application> 标签名称,或者任何其他名称。我曾经尝试向清单文件添加另一个 <application> 标签,但遇到了异常。


嗨,Gimbl。 我也遇到了同样的问题。我有自己的<application>标签,当我尝试添加另一个<application>标签时,就像你一样遇到了相同的问题(异常消息)。但是我按照你提到的方式做了,却没有起作用。我在<application>标签中添加了android:name = ".GlobalClass",但它不起作用。你可以详细说明一下你是如何解决这个问题的吗? - Sonhja
3
好的:<manifest><application android:name=".GlobalData"></application></manifest>不好的:<manifest><application></application> <application android:name=".GlobalData"> </application></manifest> - Gimbl

13

如何确保这些全局结构的本地内存被收集?

Activities有一个onPause/onDestroy()方法在销毁时被调用,但Application类没有相应的方法。当应用程序被杀死或任务栈被放入后台时,推荐使用什么机制以确保全局结构(特别是包含对本地内存的引用的结构)适当地进行垃圾收集?


1
明显的解决方案是为负责本地资源的对象实现Closeable接口,并确保它们由try-with-resources语句或其他方式管理。最坏的情况下,您总是可以使用对象终结器。 - sooniln

12

我也找不到如何指定应用程序标签,但经过大量搜索后,从清单文件文档中变得明显:使用android:name来指定应用程序子类的完全限定名称,以及在应用程序块中默认的图标和标签。

android:name 实现应用程序的Application子类的完全限定名称。当应用程序进程启动时,在任何应用程序组件之前都会实例化该类。

子类是可选的;大多数应用程序不需要这个。如果没有子类,Android将使用基础Application类的一个实例。


5

您只需要像下面这样定义一个应用程序名称,它就可以工作:

<application
  android:name="ApplicationName" android:icon="@drawable/icon">
</application>

4
如果一些变量存储在sqlite中,并且您必须在应用程序的大多数活动中使用它们,则可能使用Application是最好的方法。 当应用程序启动时从数据库查询这些变量并将它们存储在一个字段中。 然后您可以在您的活动中使用这些变量。
所以找到正确的方法,没有最好的方法。

4

正如之前讨论的那样,操作系统可能会在没有任何通知的情况下杀掉应用程序(没有onDestroy事件),因此无法保存这些全局变量。

SharedPreferences可能是一个解决方案,但如果您有复杂结构的变量(在我的情况下,我有整数数组来存储用户已处理的ID),则会出现问题。 SharedPreferences的问题是每次需要值时都很难存储和检索这些结构。

在我的情况下,我有一个后台服务,所以我可以将这些变量移到那里,因为服务具有onDestroy事件,所以我可以轻松保存这些值。


即使是服务,onDestroy() 方法也不能保证被调用。 - Learn OpenGL ES
是的,这种情况可能发生,但只有在紧急情况下才会出现。 - Adorjan Princz

3
您可以使用静态字段来存储此类状态。或者将其放入资源包中,并在onCreate(Bundle savedInstanceState)中从那里恢复。只需确保您完全了解Android应用程序管理的生命周期(例如,为什么键盘方向更改时会调用login())。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接