(译) Android 多进程应用

Android 官网上只是简单地提到了多进程,但实际开发中使用多进程会遇到各种各样的问题。本文讨论其中一些问题,或许能带来一些问题解决思路。

原文:Making Multi-process Android applications
译文:译文

Android 应用的组件可以运行在不同的进程中。有时这种做法是必要的,可以提升应用性能。但我们必须意识到,官方网站上并没有太多关于Android多进程应用的文档,而且多进程应用也不容易理解。我们来看看多进程何时会很有用,又会有哪些挑战以及如何应对这些挑战。

缺省情况下 Android 应用中所有的组件都运行在同一个 Linux 进程中,我们也可以让不同组件运行在不同的进程。有些场景中多进程很有用。

Android 会不时杀死进程回收内存,以提供给更重要的进程。Android 使用重要性等级来决定该杀死和保留哪个进程。占用内存高的后台进程更可能被 Android 杀死。如果你将后台组件(service, content provider)和前台组件(activity)放在同一个进程,该进程会使用更多的内存。当应用进入后台,它更可能被杀死。Android 杀死进程时会杀死其中所有的组件。为了将后台组件跟UI的生命周期解耦,最好将其独立到一个独立的进程

举个例子,某个应用要在客户端和服务器端之间同步数据时,它可以在独立进程中的 service 中启动同步任务。这可以让后台进程独立于 UI 所在的主进程的生命周期。另外,UI 进程的崩溃和异常对后台进程没有任何影响。反之亦然。

使用多进程的另一个原因是多个特性完全不同且独立。比如,如果某个应用支持 Email, Calendar, Contacts, Notes 以及 Tasks 等不同的特性,可以考虑将每个特性放在独立进程中运行以保持各自的生命周期独立。

如何运行多进程

<activity>, <service>, <receiver> 以及<provider> 都支持 <android:process> 属性,该属性可以指定组件运行的进程。多个组件也可以共享同一个进程,指定多少进程就能启动多少进程。

挑战

当 Android 应用使用多进程时,需要处理进程间的数据一致性。即便代码结构良好,在运行时也不太容易知道哪段代码在哪个进程运行。

共享数据

尽管可以在多个独立进程中运行不同独立逻辑,某些时候它们仍然需要共享数据。

  • 如果你使用文件,数据库或者shared preferences,你可能遇到跨进程数据不一致问题
  • 如果使用文件,你需要监听其他进程对文件进行的修改
  • 如果从多个进程访问数据库,你可能会遇到非常难以调试的数据库冲突或死锁问题
  • 如果使用shared preferences,必须使用多进程模式打开它,并且使用OnSharedPreferenceChangeListener监听其他进程导致的变化

单例

单例是在 Android 应用中跨组件共享状态信息和数据的简单办法。在单进程应用中,可以使用 synchronized 关键字保证单例的方法是线程安全的。不过,在多进程应用中使用单例,生成的单例对象数量跟进程数量一样多。因为进程并不共享地址空间,所以一个进程中的单例对象对其他进程并不可见。如果使用shared preferences,数据库或文件中的数据初始化单例,要让(不同进程中的)每个单例对象保持一致的数据非常困难,而且很可能它们在运行时有不同的数据状态。synchronized 对这种情况不起作用。

假设我们有一个单例类,不妨称之为 SubscriptionManager,它的功能是跟踪用户订阅。这个类更新了订阅内容,并且将数据保存到本地文件以供离线访问。如果本地数据文件存在的话,由这些文件初始化单例。另外,应用中有主进程和后台进程两个进程。

假设有个工具类检查用户是否订阅了某个内容,该类会被主进程和后台进程调用。用户调用 SubscriptionManager.getInstance(context).updateSubscription(feature, subscription); 从主进程订阅。这个调用更新了订阅内容,发送请求到后台并且更新了文件。

但问题是,后台进程中的 SubscriptionManager 并不知道主进程中发生的这次订阅,除非你在数据文件中设置了 FileObserver,监听数据变化更进行相应更新。或者使用了广播机制来通知这次订阅。

所以说,在进程间共享数据非常复杂,这里的做法并不是最佳方案。

解决办法

Android 通过 Binder 接口提供进程间通信(IPC)。Content providersBound services 使用 binder 接口来跨进程通信。所以本方案使用这种方式来维护多进程间数据和状态的一致性。

Content Provider

ContentProvider 用于管理对结构化数据的访问。它们封装数据并且提供数据安全机制。ContentProvider 是在当前进程的数据跟其他进程的代码之间建立联系的标准接口。(Content providers are the standard interface that connects data in one process with code running in another process)

尽管 ContentProvider 本是用于应用间共享数据,它也可以用于在多进程之间共享数据。Android 保证跨进程时 ContentProvider 的单一性。ContentResolver 提供易用的接口,所以应用代码不必担心 IPC 细节。

注意:如果不想在应用外共享数据, 不要 在 manifest 中对外暴露 ContentProvider。

尽管 ContentProvider 和 ContentResolver 的 “CURD” API 主要被设计为共享 SQL 数据库中的数据,也可以将他们扩展成更通用的 call(android.net.Uri, java.lang.String, java.lang.String, android.os.Bundle) API 以实现应用特定的目的。

比如:在上述例子的 SubscriptionManager,可以提供如下 API 来检查和更新订阅:

1
2
ContentResolver resolver = getContentResolver(); 
Bundle result = resolver.call(“SubcriptionCheck”, arg, extras);

Bound Service

Bound service 是客户端-服务器接口中的服务器端。Bound service 允许组件(比如activity)绑定到service,发送请求,接收响应,甚至执行IPC。

有两种方式实现 IPC:Messengers 和 AIDL 。Messenger 方式更简单一些。两种方式中 Android 框架都做好了 IPC 底层工作(marshalling, unmarshalling, RPC),IPC 对调用方是透明的。跟 service 通信的步骤如下:

  • bindService
  • 连接到 service 时收到回调
  • 使用 binder 接口发送请求,或调用 service API
  • 从 service 接收响应

Service

可以使用 intent 在另一个进程中启动 service,或者从代码中的任何地方发送广播,并且在另外的进程中接收该广播并启动一个 service 来处理它。

比如你有一个测量系统用于捕获应用中的某些事件,可以将每次测量发送到 service,让 service 将其写入数据库或发送到后台,而不是让代码中到处都是数据库或网络操作。

Application 类的坑

你可以继承 Application,并且在AndroidManifest.xml<application> 标签的 name 属性中指定子类的全名。当应用进程启动时,子类会最先被初始化。

要注意的是,应用中启动的每一个进程都会执行上述初始化。不幸的是,并不能为不同的进程指定不同的Application 类。

如果你使用 Application 类,很可能你会在它的onCreate() 方法中做一些初始化工作。 请确保这些初始化工作对进程是恰当且必要的 也在 Application.onCreate() 中使用下面的代码确定进程上下文:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int pid = android.os.Process.myPid(); 
ActivityManager manager = (ActivityManager) this.getSystemService(Context.ACTIVITY_SERVICE);
for (ActivityManager.RunningAppProcessInfo processInfo : manager.getRunningAppProcesses()) {
if (processInfo.pid == pid) {
String currentProcName = processInfo.processName;
if (!TextUtils.isEmpty(currentProcName) && currentProcName.equals(":background")) {
//Rest of the initializations are not needed for the background
//process
return;
}
}
}

/* Initializations for the UI process */

通过这种方式你可以降低内存占用,避免某些大块内存分配,甚至有可能加快应用启动速度。这里提到初始化包括:加载专有字体,初始化 Google Maps,及以初始化第三方 SDK。通常只需要在主进程中做这些初始化。

总结

希望这篇博客能帮助你理解多进程应用,知道使用多进程中会遇到的问题,以及如何去解决这些问题。

注意

  • 如果不必在应用外访问 provider 和 service,就不要暴露它们
  • 限制广播的只对自己的应用可见