在活动之间共享数据的最佳方法是什么?

262

我有一个活动是应用程序中使用的主要活动,它有许多变量。我有另外两个活动,我希望能够使用第一个活动的数据。 现在我知道我可以像这样做:

GlobalState gs = (GlobalState) getApplication();
String s = gs.getTestMe();

然而我想要共享很多变量,其中一些可能相当大,所以我不想像上面那样创建它们的副本。

有没有一种方法可以直接获取和更改变量,而不使用get和set方法?我记得在Google开发者网站上读到过一篇文章,称这在Android上不推荐使用,原因是性能问题。


2
从Android 2.3(姜饼)开始,Dalvik会自动执行get/set的优化;这仅在您针对旧版本的Android时才相关。 - StellarVortex
请注意,示例代码并未复制字符串数据,它只是创建了一个指向同一字符串对象的引用。 - Code-Apprentice
1
很难相信,为什么没有可能从另一个活动开始并将任何复杂对象从第一个传递到第二个?没有序列化,保存对象和所有这些努力。这是安全漏洞还是其他原因反对简单地传递对象引用,如果两个活动在同一个应用程序中?(我理解如果它们在不同的应用程序中,则情况有所不同) - Droidum
请查看此链接:https://dev59.com/LrTma4cB1Zd3GeqP52UR#56521970 - Levon Petrosyan
LiveData是最好的、最新的解决方案。请查看下面我的回答。 - Amir Uval
依赖注入 - k4dima
14个回答

511

以下是一些实现此目的最常见的方法:

  • 将数据放入意图中
  • 静态字段
  • WeakReferences哈希映射
  • 持久化对象(例如sqlite、共享首选项、文件等)

简而言之:有两种共享数据的方式:将数据作为意图额外信息传递或将其保存在其他位置。如果数据是原始类型、字符串或用户定义的对象,请将其作为意图额外信息的一部分发送(用户定义的对象必须实现Parcelable接口)。如果要传递复杂对象,请在其他地方的单例中保存其实例,并从启动的活动中访问它们。

以下是每种方法的一些示例及其实现原因:

将数据放入意图中

Intent intent = new Intent(FirstActivity.this, SecondActivity.class);
intent.putExtra("some_key", value);
intent.putExtra("some_other_key", "a value");
startActivity(intent);

在第二个活动中:

Bundle bundle = getIntent().getExtras();
int value = bundle.getInt("some_key");
String value2 = bundle.getString("some_other_key");

如果您要传递基本数据类型或字符串,请使用此方法。您也可以传递实现Serializable接口的对象。

尽管很诱人,但在使用Serializable之前应三思而后行:它容易出错且速度非常慢。因此,通常情况下,如可能请避免使用Serializable。如果您想传递复杂的用户定义的对象,请查看Parcelable接口。虽然实现更难,但与Serializable相比,它具有相当大的速度优势。

在不持久化到磁盘的情况下共享数据

通过将数据保存在内存中,可以在活动之间共享数据,因为在大多数情况下,这两个活动运行在同一个进程中。

注意:有时,当用户离开你的活动(未退出)时,Android可能会决定终止你的应用程序。在这种情况下,我曾经遇到过一些情况,即Android试图使用在应用程序被终止之前提供的意图来启动最后一个活动。在这种情况下,存储在单例中的数据(无论是你的还是Application的)都将消失,可能会发生不良事件。为了避免这种情况,您可以将对象持久化到磁盘上,或在使用数据之前检查数据以确保其有效。

使用单例类

创建一个类来保存数据:

public class DataHolder {
  private String data;
  public String getData() {return data;}
  public void setData(String data) {this.data = data;}

  private static final DataHolder holder = new DataHolder();
  public static DataHolder getInstance() {return holder;}
}

从已启动的活动中:

String data = DataHolder.getInstance().getData();

使用应用程序单例

应用程序单例是在应用启动时创建的android.app.Application实例。您可以通过扩展Application来提供自定义实例:

import android.app.Application;
public class MyApplication extends Application {
  private String data;
  public String getData() {return data;}
  public void setData(String data) {this.data = data;}
}

启动活动之前:

MyApplication app = (MyApplication) getApplicationContext();
app.setData(someData);

然后,从启动的活动中:

MyApplication app = (MyApplication) getApplicationContext();
String data = app.getData();

静态字段

这个思路基本上与单例模式相同,但在这种情况下,您提供对数据的静态访问:

public class DataHolder {
  private static String data;
  public static String getData() {return data;}
  public static void setData(String data) {DataHolder.data = data;}
}

从启动的活动中:

String data = DataHolder.getData();

使用WeakReferences的HashMap

这个想法与之前的相同,但允许垃圾收集器移除无引用的对象(例如当用户退出活动时):

public class DataHolder {
  Map<String, WeakReference<Object>> data = new HashMap<String, WeakReference<Object>>();

  void save(String id, Object object) {
    data.put(id, new WeakReference<Object>(object));
  }

  Object retrieve(String id) {
    WeakReference<Object> objectWeakReference = data.get(id);
    return objectWeakReference.get();
  }
}

启动活动之前:

DataHolder.getInstance().save(someId, someObject);

从启动的活动中:

DataHolder.getInstance().retrieve(someId);

根据你的具体问题,你可能需要使用Intent的extras传递对象ID,也可能不需要。

将对象持久化到磁盘

这个想法是在启动其他活动之前将数据保存到磁盘上。

优点:您可以从其他位置启动活动,并且如果数据已经持久化,它应该能够正常工作。

缺点:它很麻烦,实现需要更多时间。需要更多的代码,因此引入错误的机会更大。它还会变慢。

一些将对象持久化的方法包括:


11
我认为,这并不是处理更大、更复杂数据的“常规”方式。使用静态单例或Application对象更容易,而且效果很好。话虽如此,OP在示例中确实使用了一个字符串,在这种情况下,使用Intent是完美且首选的。 - Charlie Collins
11
Serializable在Android进程模型中存在严重的性能问题。这就是为什么他们引入了Parcelable。在上面的答案中,阅读Parcelable而不是Serializable - C--
3
这是通过使用setResult方法完成的。此外,在这种情况下,必须使用startActivityForResult方法来调用次要活动。 - Cristian
2
很好的总结!关于单例被销毁的问题,对于有许多活动和对象的应用程序,有一个简单的解决方案:在整个应用程序中使用Activity的子类,并在其onCreate()方法中检查您在启动应用程序时填充的单例的任何静态字段。如果该字段为空,则返回到起始活动并使用FLAG_ACTIVITY_CLEAR_TASKBroadcastReceiver来终止其他活动。 - Janosch
1
我不建议在应用程序类中保存数据。在这里阅读更多信息:http://www.developerphil.com/dont-store-data-in-the-application-object/ - Shayan_Aryan
显示剩余9条评论

24

你可以使用以下方式:

  1. 在活动之间传递数据(如Cristian所说)
  2. 使用带有许多静态变量的类(这样您可以在不使用类的实例和getter / setter的情况下调用它们)
  3. 使用数据库
  4. 共享首选项

您选择什么取决于您的需求。当您需要“很多”时,可能会同时使用多种方法。


1
请注意,静态变量在进程结束时会被清除。 - EpicPandaForce
当然是@EpicPandaForce,还有设备关闭时。 - WarrenFaith
1
但是如果设备关闭,应用程序将从“MAIN”操作重新启动。在进程死亡后,您将重新启动上次打开的任何活动,这可能是应用程序中某个深处的详细页面。 - EpicPandaForce

16

遵从Google的指引!在这里:http://developer.android.com/resources/faq/framework.html#3

  • 基本数据类型
  • 非持久化对象
  • 单例模式 - 我最喜欢的:D
  • 公共静态字段/方法
  • 弱引用对象的HashMap
  • 持久化对象(应用程序首选项、文件、ContentProvider、SQLite数据库)

1
Google 持久对象链接: http://developer.android.com/guide/topics/data/data-storage.html - NicoMinsk
只有反模式,单例类是第一个设计模式,具有静态字段/方法的类行为很像单例,并且为持久化业务对象创建数据库有时并不是一件好事,当然这不是你的错,你正在列出可能实现它的方式,但我想知道为什么Google会把这样一个非常愚蠢的事情复杂化,是性能问题还是其他原因??!!或者我没有理解Android的方式??!! - La VloZ Merrill
链接已损坏 :( - Shayan_Aryan

14
"然而,我想分享很多变量,其中一些可能相当大,因此我不想像上面那样创建它们的副本。"这并不会创建副本(特别是使用 String,但即使是对象也是按引用传递值,而不是对象本身,像这样的 getter 是可以使用的 - 可以说比其他方法更好,因为它们是常见的且被广泛理解的)。旧的“性能神话”,例如不使用 getter 和 setter,仍然有一定价值,但已在文档中更新。但是,如果您不想这样做,您也可以在 GlobalState 中将变量设置为 public 或 protected 并直接访问它们。而且,您可以像 Application object JavaDoc 指示的那样 创建静态单例:
“通常不需要子类化 Application。在大多数情况下,静态单例可以以更模块化的方式提供相同的功能。如果您的单例需要全局上下文(例如注册广播接收器),则检索它的函数可以给定一个 Context,在首次构建单例时内部使用 Context.getApplicationContext()。”
使用Intent数据,正如其他答案所述,是传递数据的另一种方式,但通常用于较小的数据和简单类型。您可以传递更大/更复杂的数据,但这比仅使用静态singleon更为复杂。Application对象仍然是我个人最喜欢的在Android应用程序组件之间共享更大/更复杂的非持久性数据的方法(因为它在Android应用程序中具有明确定义的生命周期)。
此外,正如其他人指出的那样,如果数据变得非常复杂并且需要持久化,则还可以使用SQLite或文件系统。

1
其实,我最近在文档中偶然发现了这个:http://developer.android.com/guide/appendix/faq/framework.html#3。对于“非持久性复杂对象”,它建议使用Application类来创建和拆除一个静态单例!这样你就可以获得Application提供的良好定义的生命周期和静态单例的易用性。 - Charlie Collins
2
那个常见问题解答的部分现在似乎已经被删除了(我只看到“非持久对象”,没有提到Application类)。你能详细说明一下吗? - Tony Chan
1
目前在http://developer.android.com/guide/faq/framework.html#3,关于“如何在单个应用程序中的活动/服务之间传递数据?”的问题,没有提到Application类。 - Jerry101
我喜欢使用Application对象,遵循你在《Android in Action》中设定的先例。但是很多潜在雇主不喜欢在代码挑战中看到这个。他们可能是错的,但是他们有话语权。顺便说一下:那个“framework.html#3”链接已经失效了。 - Matt J.

8
有一种新的、更好的方法可以在活动之间共享数据,它就是LiveData。特别要注意的是来自Android开发者页面的这句话:
“LiveData对象具有生命周期感知能力,这意味着您可以在多个活动、片段和服务之间共享它们。为了保持示例简单,您可以将LiveData类实现为单例。”
这意味着任何模型数据都可以在一个公共的单例类中使用LiveData包装器进行共享。它可以从活动中注入到它们各自的ViewModel中,以便进行测试。而且您不再需要担心弱引用以防止内存泄漏。

1
这个答案应该排得更靠前。这是一种更易于维护和合理的数据共享方式,并且与前端和后端框架中的其他状态保持解决方案相符合。观察者模式在数据访问方面强制实现一致性,无论是数据本身还是如何与其交互。 - adammtlx
1
仅使用LiveData并不能解决在不同活动之间共享LiveData对象的问题。为此,Android开发者页面建议使用单例模式以简化操作。 - JCvanDamme
@JCvanDamme 是的,这就是我写的。我不明白你在评论什么。 - Amir Uval
@AmirUval,你写道:“有一种新的更好的方法可以在活动之间共享数据,那就是LiveData”。我的观点是,LiveData并没有解决OPs关于如何在活动之间共享数据的问题。人们可以使用任何数据类型的单例模式,并且本质上使用LiveData与使用任何其他数据类型没有什么优势。如果您想共享String类型,请使用String类型的单例模式。您不会希望仅出于共享String跨越活动而将String制作为LiveData<String> - JCvanDamme
@JCvanDamme 我期望我的回答能够被聪明的读者根据需要进行调整。我写了一个使用LiveData封装单例模型类的方法,在我写下这个答案2.5年后,我仍然推荐这种方法来在活动之间共享数据,因为它解决了内存泄漏的根本问题。但是,如果只是针对单个字符串,我不建议使用它,因为简单的静态变量已经足够了。 - Amir Uval

7
你可以扩展Application类并在那里添加任何对象,然后它们就可以在应用程序的任何地方使用。请参考此处

我不建议在应用程序类中保存数据。在这里阅读更多信息:http://www.developerphil.com/dont-store-data-in-the-application-object/ - Shayan_Aryan

3

有多种方法可以在活动之间共享数据

1:使用 Intent 在活动之间传递数据

Intent intent=new Intent(this, desirableActivity.class);
intent.putExtra("KEY", "Value");
startActivity(intent)

2:使用 static 关键字,将变量定义为 public static,并在项目中任何地方使用

      public static int sInitialValue=0;

使用classname.variableName可以在项目中任何地方使用。

3: 使用数据库

这是一个有点冗长的过程,您需要使用查询来插入数据,并使用游标迭代数据。但是,没有清除缓存的机会会导致数据丢失。

4: 使用共享首选项

比使用数据库更容易。但是,有一些限制,您不能保存ArrayList、List和自定义对象。

5: 在Aplication类中创建getter setter,可以在项目中任何地方访问。

      private String data;
      public String getData() {
          return data;
      }

      public void setData(String data) {
          this.data = data;
      }

在这里从活动中设置和获取

         ((YourApplicationClass)getApplicationContext()).setData("abc"); 

         String data=((YourApplicationClass)getApplicationContext()).getData();  

2
使用上述弱引用哈希映射的方法,在http://developer.android.com/guide/faq/framework.html中描述,对我来说似乎有问题。如何回收整个条目,而不仅仅是映射值?你在哪里分配它的范围?由于框架控制Activity生命周期,使参与的Activity之一拥有它会在所有者提前销毁时产生运行时错误。如果Application拥有它,则必须有某个Activity明确删除条目,以避免哈希映射保留具有有效键和潜在垃圾收集弱引用的条目。此外,当返回给定键的值为null时,客户端应该怎么做?
在我看来,由Application或单例拥有的WeakHashMap是更好的选择。映射中的值通过键对象访问,当没有对键的强引用存在(即所有Activity都完成了对键及其映射的操作)时,GC可以回收映射条目。

1

如果您想使用数据对象,那么这两个实现非常重要:

Serializable vs Parcelable

  • Serializable是一个标记接口,它意味着用户不能按照自己的要求对数据进行编组。因此,当对象实现Serializable时,Java会自动对其进行序列化。
  • Parcelable是Android自己的序列化协议。在Parcelable中,开发人员编写自定义代码进行编组和解组。因此,与Serialization相比,它创建的垃圾对象较少。
  • 与Serializable相比,Parcelable的性能非常高,因为它具有自定义实现。在Android中序列化对象时,强烈建议使用Parcelable实现。

public class User implements Parcelable

这里查看更多信息。


1

我有一些想法,但不确定它们是否符合你的需求。

你可以使用一个保存所有数据的服务,然后只需将你的活动绑定到该服务以进行数据检索。

或者将你的数据打包成可序列化或可包裹对象,并将它们附加到一个 bundle 中,在活动之间传递 bundle。

这个可能完全不是你要找的,但你也可以尝试使用 SharedPreferences 或一般的 preference。

无论如何,请让我知道你决定了什么。


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