关于singleton:如何在android中声明全局变量?

How to declare global variables in Android?

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

在主要活动onCreate方法中,我增加了以下条件:

1
2
3
4
5
6
7
8
9
10
11
12
13
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    ...

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

当登录表单终止时执行的onActivityResult方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@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

是否有人知道如何设置变量global以避免登录表单在用户成功验证后出现?


我在09年写下了这个答案,当时Android是一个相对较新的版本,在Android开发中有许多不成熟的领域。我在这篇文章的底部添加了一个很长的附录,解决了一些批评,并详细说明了我在哲学上对单例应用程序的使用而不是子类应用程序的分歧。阅读它的风险由你自己承担。好的。

原始答案:好的。

您遇到的更普遍的问题是如何跨多个活动和应用程序的所有部分保存状态。静态变量(例如,单变量)是实现这一点的常用Java方式。不过,我发现,Android中更优雅的方法是将您的状态与应用程序上下文关联起来。好的。

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

这样做的方法是创建自己的android.app.application子类,然后在清单中的application标记中指定该类。现在,Android将自动创建该类的一个实例,并使其可用于整个应用程序。您可以使用Context.getApplicationContext()方法从任何context访问它(Activity还提供了具有完全相同效果的getApplication()方法)。以下是一个非常简单的例子,需要注意以下几点:好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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框架中。请注意,这不会跨进程工作(如果您的应用程序是少数几个具有多个进程的应用程序之一)。好的。

从上面的例子中可以注意到;假设我们做了如下的事情:好的。

1
2
3
4
5
6
7
8
class MyApp extends Application {

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

  public String getState(){
    return myState;
  }
}

现在,每次应用程序被实例化时,都会执行这个缓慢的初始化(如命中磁盘、命中网络、任何阻塞等)!你可能会想,嗯,这个过程只有一次,无论如何,我都得支付费用,对吗?例如,正如DianneHackborn在下面提到的,您的进程完全有可能被实例化——只是——处理一个后台广播事件。如果您的广播处理不需要这种状态,那么您就可能白做了一系列复杂而缓慢的操作。懒惰的实例化就是这里的游戏名称。以下是一种稍微复杂一些的应用程序使用方法,除了最简单的用法外,它对任何事物都更有意义:好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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:正如Antiafe所评论的,为了正确地将应用程序覆盖绑定到应用程序,清单文件中必须有一个标记。同样,请参阅Android文档了解更多信息。一个例子:好的。

1
2
3
4
5
<application
     android:name="my.application.MyApp"
     android:icon="..."
     android:label="...">
</application>

注2:user608578在下面询问如何管理本机对象生命周期。我一点也没有跟上在Android上使用本机代码的速度,我也没有资格回答这将如何与我的解决方案交互。如果有人对此有答案,我愿意相信他们,并把信息放在这篇文章中,以获得最大的可见性。好的。

附录:好的。

正如一些人所指出的,这不是持久状态的解决方案,我也许应该在最初的答案中更强调一些。也就是说,这并不是一个保存用户或其他信息的解决方案,而这些信息将在应用程序的整个生命周期中被持久化。因此,我认为下面的大多数批评都与在任何时候杀死应用程序有关,等等,没有意义,因为任何需要持久化到磁盘的东西都不应该通过应用程序子类存储。它是用于存储临时的、易于重新创建的应用程序状态(例如用户是否登录)和单实例组件(例如应用程序网络管理器)(而不是单实例)的解决方案。本质上。好的。

Dayerman非常友好地指出了与RetoMeier和DianneHackborn的一个有趣的对话,在这个对话中,不鼓励使用应用程序子类,而赞成使用单例模式。索马蒂克早些时候也指出了这种性质,尽管当时我没有看到。由于reto和dianne在维护Android平台方面的作用,我不能真诚地建议忽略他们的建议。他们说的,去吧。我真的不同意关于优先使用单例子类而不是应用程序子类的意见。在我的分歧中,我将使用在这个单例设计模式的stackexchange解释中最好解释的概念,这样我就不必在这个答案中定义术语。我强烈建议在继续之前浏览链接。逐点:好的。

Dianne说,"没有理由从应用程序中进行子类划分。这和制作一个单件没有什么不同……"第一个声明是错误的。这主要有两个原因。1)应用程序类为应用程序开发人员提供了更好的生存期保证;它保证有应用程序的生存期。单例并不明确地与应用程序的生命周期联系在一起(尽管它是有效的)。对于一般的应用程序开发人员来说,这可能不是问题,但我认为这正是Android API应该提供的合同类型,而且通过最小化相关数据的生命周期,它为Android系统提供了更大的灵活性。2)应用程序类为应用程序开发人员提供了一个单实例状态持有者,这与单实例状态持有者非常不同。有关差异的列表,请参阅上面的singleton解释链接。好的。

Dianne继续说,"…当你发现你的应用程序对象变得如此混乱,应该是独立的应用程序逻辑时,你很可能会后悔。"这当然不是不正确的,但这不是选择单例而不是应用程序子类的原因。Diane的所有论点都没有提供一个理由,说明使用singleton比应用程序子类更好,她试图建立的只是使用singleton并不比应用程序子类差,我认为这是错误的。好的。

她继续说,"这会更自然地导致您应该如何管理这些东西——按需初始化它们。"这忽略了这样一个事实,即您没有理由也不能使用应用程序子类按需初始化它们。同样没有区别。好的。

Dianne以"框架本身对于它为应用程序维护的所有小共享数据都有大量的单例数据,比如加载的资源的缓存、对象池等。它工作得很好。"我不认为使用单例可以很好地工作,或者不是一个合法的选择。我的观点是,单件技术不能像使用应用子类那样与Android系统提供强有力的合同,而且使用单件技术通常指向不灵活的设计,这种设计不容易修改,并且会导致很多问题。imho,android api为开发人员应用程序提供的强大合同是android编程最吸引人和最令人愉快的方面之一,并有助于促使早期开发人员采用,从而推动android平台取得今天的成功。建议使用singletons隐式地脱离了强大的API契约,在我看来,这会削弱Android框架。好的。

Dianne在下面也发表了评论,提到了使用应用程序子类的另一个缺点,它们可能会鼓励或使编写性能代码更少变得更容易。这是非常正确的,我已经编辑了这个答案,以强调在这里考虑性能的重要性,并且在使用应用程序子类化时采用正确的方法。正如Dianne所说,重要的是要记住,每次加载进程时都会实例化应用程序类(如果应用程序在多个进程中运行,则一次可以实例化多次!)即使只为后台广播事件加载进程。因此,更重要的是将应用程序类用作指向应用程序共享组件的指针的存储库,而不是用作进行任何处理的位置!好的。

我给您留下了以下单件产品的缺点列表,这是从先前的stackexchange链接中被盗的:好的。

  • 无法使用抽象类或接口类;
  • 无子类能力;
  • 跨应用的高耦合(难以修改);
  • 难以测试(不能在单元测试中伪造/模拟);
  • 在可变状态下难以并行(需要广泛的锁定);

加上我自己的:好的。

  • 不清楚和不可管理的终身合同不适合Android(或大多数其他)开发;

好啊。


创建此子类

1
2
3
public class MyApp extends Application {
  String foo;
}

在androidmanifest.xml中添加android:name

例子

1
2
3
<application android:name=".MyApp"
       android:icon="@drawable/icon"
       android:label="@string/app_name">


Soonil所建议的保持应用程序状态的方法是好的,但是它有一个弱点——有时操作系统会破坏整个应用程序进程。这里是关于这个过程和生命周期的文档。

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

afaik,确保状态安全的唯一方法是使用任何类型的持久化状态,例如对应用程序文件使用private或SharedPrefernces(它最终对内部文件系统中的应用程序文件使用private)。


只是一张便条。

添加:

1
android:name=".Globals"

或者您将您的子类命名为现有的标记。我一直试图在舱单上再加上一个标签,结果会得到一个例外。


我也找不到如何指定应用程序标签,但是在大量的谷歌搜索之后,从清单文件docs:use android:name中可以明显看出,除了应用程序节中的默认图标和标签之外。

Android:名字为应用程序实现的应用程序子类的完全限定名。当应用程序进程启动时,这个类在应用程序的任何组件之前被实例化。

子类是可选的;大多数应用程序不需要。在没有子类的情况下,Android使用基本应用程序类的实例。


那么如何确保使用这种全局结构收集本机内存呢?

活动有一个在销毁时调用的onPause/onDestroy()方法,但应用程序类没有等价的方法。当应用程序被终止或任务堆栈被置于后台时,建议采用什么机制来确保适当地垃圾收集全局结构(尤其是那些包含对本机内存的引用的结构)?


只需定义一个应用程序名,如下所示:

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

就像上面讨论过的那样,操作系统可以在没有任何通知的情况下终止应用程序(没有OnDestroy事件),因此无法保存这些全局变量。

sharedreferences可以是一个解决方案,除非您有复杂的结构化变量(在我的例子中,我有整数数组来存储用户已经处理过的ID)。sharedreferences的问题在于,每次需要的值都很难存储和检索这些结构。

在我的例子中,我有一个后台服务,这样我就可以将这些变量移动到那里,并且因为该服务有OnDestroy事件,所以我可以很容易地保存这些值。


如果某些变量存储在sqlite中,并且您必须在应用程序的大多数活动中使用它们。那么应用程序也许是实现它的最佳方法。在应用程序启动时从数据库中查询变量,并将它们存储在字段中。然后您可以在活动中使用这些变量。

所以找到正确的方法,没有最好的方法。


您可以有一个静态字段来存储这种状态。或者将其放到资源包中,然后在oncreate(bundle savedinstancestate)上从中恢复。只需确保您完全理解Android应用程序管理的生命周期(例如,为什么在键盘方向更改时调用login())。


不要在清单文件中使用另一个标记。只需对现有的标记进行一次更改,添加这行android:name=".ApplicationName",其中,ApplicationName将是您要创建的子类(用于存储全局)的名称。

最后,清单文件中唯一的标记应该如下所示:

1
2
3
4
5
6
7
<application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/Theme.AppCompat.NoActionBar"
        android:name=".ApplicationName"
        >


您可以使用两种方法来实现这一点:

  • 使用应用程序类
  • 使用共享首选项

  • 使用应用程序类

  • 例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class SessionManager extends Application{

      String sessionKey;

      setSessionKey(String key){
        this.sessionKey=key;
      }

      String getSessisonKey(){
        return this.sessionKey;
      }
    }

    您可以使用上面的类在MainActivity中实现登录,如下所示。代码将如下所示:

    1
    2
    3
    4
    5
    6
    7
    @override
    public void onCreate (Bundle savedInstanceState){
      // you will this key when first time login is successful.
      SessionManager session= (SessionManager)getApplicationContext();
      String key=getSessisonKey.getKey();
      //Use this key to identify whether session is alive or not.
    }

    此方法适用于临时存储。由于内存不足,您真的不知道什么时候操作系统会杀死应用程序。当您的应用程序处于后台,而用户正在浏览其他需要更多内存才能运行的应用程序时,由于操作系统对前台进程的优先级高于后台进程,因此您的应用程序将被终止。因此,在用户注销之前,您的应用程序对象将为空。因此,我建议使用上面指定的第二种方法。

  • 使用共享首选项。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    String MYPREF="com.your.application.session"

    SharedPreferences pref= context.getSharedPreferences(MyPREF,MODE_PRIVATE);

    //Insert key as below:

    Editot editor= pref.edit();

    editor.putString("key","value");

    editor.commit();

    //Get key as below.

    SharedPreferences sharedPref = getActivity().getPreferences(Context.MODE_PRIVATE);

    String key= getResources().getString("key");

  • 您可以使用意图、sqlite或共享首选项。当涉及到媒体存储时,如文档、照片和视频,您可以创建新的文件。


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    class GlobaleVariableDemo extends Application {

        private String myGlobalState;

        public String getGlobalState(){
         return myGlobalState;
        }
        public void setGlobalState(String s){
         myGlobalState = s;
        }
    }

    class Demo extends Activity {

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

    on activity result在resume之前调用。因此,将您的登录检查移动到"继续",一旦secomd活动返回了一个积极的结果,您的第二个登录就可以被阻止。在简历上,每次都要打电话,所以不必担心没有第一次打电话。


    Baracus框架也使用了子类化方法。从我的角度来看,子类化应用程序的目的是与Android的生命周期一起工作;这是任何应用程序容器都能做到的。我将bean注册到这个上下文中,让它们被注入到上下文可管理的任何类中,而不是使用globals。每个注入的bean实例实际上都是一个单例实例。

    有关详细信息,请参阅此示例

    如果你能有这么多,为什么要用手工?


    您可以创建一个扩展Application类的类,然后将变量声明为该类的一个字段,并为它提供getter方法。

    1
    2
    3
    4
    5
    6
    7
    public class MyApplication extends Application {
        private String str ="My String";

        synchronized public String getMyString {
            return str;
        }
    }

    然后要访问活动中的该变量,请使用以下命令:

    1
    2
    MyApplication application = (MyApplication) getApplication();
    String myVar = application.getMyString();