Android多进程

多进程

Posted by chchaooo on January 19, 2019

多进程的好处

  • 可以获得更多内存。进程是系统分配资源和调度的基本单位,进程越多得到的资源就越多。Android对内存的限制是针对于进程的。设置为多个进程就可以获取更多的资源。
  • 子进程奔溃,主进程可以继续工作。所以可以把一些非常重要的进程(比如upgrade进程等)放到独立进程中保证其可靠性。这个常用的有下面几点:
    • 非常重要的进程,比如热更新进程,无论如何也应该保证热更新进程的正常,所以我们可以将热更新逻辑独立为一个进程,保证其绝对稳定
    • 内存占用比较大的进程,比如大图预览
  • 主进程退出,子进程可以继续工作,这个最常见的的情况是,主进程启动了推送服务,在主进程结束之后,用户仍然能一直收到推送服务
  • 多进程相互保活

Android多进程

  • 通过修改mainifest中的android:process来达到多进程目的
    • 以:开头,则该进程为私有进程(android:process=”:upgrade”)
    • 以packagename:remote开头则该进程为公有进程(拥有相同ShareUID的不同应用可以跑在同一个进程里)(android:process=”hdc.socket.server”)
  • 通过JNI,调用fork()方法来生成子进程,这种方法一般用以启动保活机制里面的daemon进程 下面是按照不同方式来进行多进程间通信的一些阐述。

进程的优先级

android系统将尽量长时间的保持应用进程,但为了新建进程或运行更重要的进程,在内存不足时,可能会移除旧进程来回收内存。回收的时所参考的进程优先级

  • 前台进程:用户当前操作所必需的进程
    • 进程托管用户正在交互的Activity
    • 进程托管某个Service,而该Service绑定到用户正在交互的activity上
    • 进程托管正在“前台”运行的Service(服务已调用startForeground())
    • 进程托管证执行一个生命周期回调的Service(onCreate(),onStart()或onDestroy())
    • 进程托管正执行其onReceive()的BroadcastReceiver
  • 可见进程
    • 托管不在前台,但仍然用户可见的activity
    • 托管绑定到前台Activity的Service
  • 服务进程
    • 正在运行已使用 startService() 方法启动的服务且不属于上述两个更高类别进程的进程。尽管服务进程与用户所见内容没有直接关联,但是它们通常在执行一些用户关心的操作(例如,在后台播放音乐或从网络下载数据)。
  • 后台进程
    • 包含目前对用户不可见的 Activity 的进程(已调用 Activity 的 onStop() 方法)
  • 空进程
    • 不含任何活动应用组件的进程。保留这种进程的的唯一目的是用作缓存,以缩短下次在其中运行组件所需的启动时间

android开启多线程之后可能出现的问题

对于使用多进程最重要的需要的注意是:

进程之间的内存空间是相互不可见的

单例失效/静态变量失效

每个进程中保持有独立的一份单例,单例退化为某进程中的单例,而不是整个app运行期间的单例 静态变量相对于类仍然只有一份,但是不同进程中同一个类会加载两次,所以一个类中的静态变量每个进程都独立有一个值,之间没有任何关联

线程同步机制可能失效

例如原本一个线程池中我们最多只希望有2个线程在运行,如果这个线程池同时在两个进程中均存在,那么就可以有4个线程同时运行

Application多次创建

不同进程跑在不同虚拟机上,每个虚拟机上会创建自己的Application 所以在Application中的只存放尽可能少的逻辑,有些sdk要求一定在Application中,此时可以判断当前是否在主进程中,只有主进程中才启动相应逻辑。

public static String getProcessName() {
    try {
        File file = new File("/proc/" + android.os.Process.myPid() + "/" + "cmdline");
        BufferedReader mBufferedReader = new BufferedReader(new FileReader(file));
        String processName = mBufferedReader.readLine().trim();
        mBufferedReader.close();
        return processName;
    } catch (Exception e) {
        e.printStackTrace();
        return null;
    }
}
  • 数据持久化过程中容易出问题:SharedPreference和DB多进程之间同时写时,均会产生脏数据

下面主要讨论进程间的通信的方式

使用Preference

Preference中早期存在MODE_MULTI_PROCESS

SharedPreferences preferences = mContext.getSharedPreferences(SP_NAME, Context.MODE_MULTI_PROCESS);

但下面是Context中,对于MODE_MULTI_PROCESS的说明

 /*
  * @deprecated MODE_MULTI_PROCESS does not work reliably in
  * some versions of Android, and furthermore does not provide any
  * mechanism for reconciling concurrent modifications across
  * processes.  Applications should not attempt to use it.  Instead,
  * they should use an explicit cross-process data management
  * approach such as {@link android.content.ContentProvider ContentProvider}.
  */
  @Deprecated
  public static final int MODE_MULTI_PROCESS = 0x0004;

总结下官方注释的意思:不要用这个模式了,这个不好使,虽然不好使但我们也不管了,你们用ContentProvider那货吧。

使用数据库

ObjectBox

objectbox在多进程中使用时有坑,项目在单线程时运行很稳定,但配置成多进程之后。Buggly上存在很多Objectbox上报的crash,我们不知道其中错误的细节,但是坑是肯定存在的。

SqlLite

android API 26 Platform中包含sqllite。一下是一段关于Sqllite的官网介绍:

SQLite是一个很快的数据库,但"快"这个词本身是一个主观的和模糊不清的词。坦白地讲,对于有些事情,SQLite比其他数据库做得快,也有些事情比不上其他数据库。利用SQLite提供的配置参数,SQLite是足够快速和高效的。与大多数数据库一样,SQLite使用B-tree做索引,使用B+-tree处理表。因此,在对单表进行查询时,平均而言,SQLite与其他数据库一样快(至少不慢于)。简单的SELECT、INSERT和UPDATE语句是相当快速的--几乎与内存(如果是内存数据库)或者磁盘同速。SQLite通常要快于其他数据库,因为它在处理一个事务开始,或者一个查询计划的产生方面开销较小,并且没有调用服务器的网络或认证以及权限协商的开销。它的简单性使它更快。

但是随着查询变大变复杂,查询时间使得网络调用或者事务处理开销相形见绌,SQLite将会与其他数据库一样。这时一些大型的设计复杂的数据库开始发挥作用了。虽然SQLite也能处理复杂的查询,但是它没有精密的优化器或者查询计划器。SQLite知道如何使用索引,但是它没有保存详细的表统计信息。假如执行17路join,SQLite也会连接表并给您结果,并不像您在Oracle或者PostgreSQL中期望的那样,SQLite没有通过计算各种替代查询计划并选择最快的候选计划来尝试判断优化路径。因此,假如您在大型数据集合上运行复杂的查询,SQLite与那些有复杂设计的查询计划器的数据库运行一样快的机会是非常小的。

一些情况下,SQLite可能不如大型数据库快,但大多数的这些情况是预期的。SQLite是为中小规模的应用程序设计的一个嵌入式的数据库,这些限制是设计目的预见到的。许多新用户错误地认为使用SQLite可以代替大型关系数据库。事实是有时可以这样做,有时不行,这完全取决于您准备用SQLite来做什么。

一般情况下,SQLite的局限性主要有以下两方面:

并发。SQLite的锁机制是粗粒度的,它允许多个读,但是一次只允许一个写。写锁会在写期间排他地锁定数据库,其他人在此期间不能访问数据库。SQLite已经采取措施以最小化排它锁所占用的时间。通常来讲,SQLite中的锁只保持几毫秒。但是按照一般经验,如果您的应用程序有很高的写并发(许多连接竞争向同一数据库写),并且是时间关键型,您可能需要其他数据库。需要实际测试您的应用程序才能知道能获得怎样的性能。作者曾见过一个简单的网络应用程序中,SQLite用100个并发连接每秒处理500多个事务。您的事务所修改的记录数以及查询所涉及的复杂性可能有所不同。什么样的并发性是可接受的,这取决于具体的应用程序,只能通过直接测试来判断。总之,对任何数据库都是真理:直到您做了实际测试才能知道应用程序能获得怎样的性能。

网络。虽然SQLite数据库可以通过网络文件系统共享,但是与这种文件系统相关的潜在延时会导致性能受损。更糟的是,网络文件系统实现中的一些缺陷也使得打开和修改远程文件--SQLite或其他--很容易出错。如果文件系统的锁实现不当,可能允许两个客户端同时修改同一个数据库文件,这样必然会导致数据库出错。实际上,并不是因为SQLite的实现导致SQLite不能在网络文件系统上工作。相反,SQLite在底层文件系统和有线协议下工作,但是这些技术有时并不完善。例如,许多NFS使用有缺陷的fcntl(),这意味着锁可能并不像设想的那样工作。一些较新版本的NFS,如Solaris NFSV4就可以工作正常,SQLite需要的锁机制也是相当可靠的。然而,SQLite开发者既没有时间,也没有资源去验证任何给定的网络文件系统在所有的情况下都可以无缺陷工作。

再次申明,绝大部分限制是有意设计的--它们是SQLite设计的结果。例如,支持高度的写并发会带来很大的复杂性,这将使SQLite的简单性无法保持。同样,作为嵌入式数据库,SQLite是有意不支持网络的。这一点并不奇怪。总之,SQLite不能做的都是它所能做的直接结果。它开始的设计就是一款模块化、简单、紧凑的以及易于使用的嵌入式关系数据库,它的基础代码都在使用它的程序员的掌握范围内。从多方面看,它能完成许多其他数据库不能做的事情,例如,运行在电力消耗实际上是一项制约因素的嵌入式环境中。

当Android系统中存在多个进程时,会有多个SQLiteDatabase对象被创建出来,每个对象都去试图连接同一个数据文件,这个没有什么问题。我们的项目中使用的是“一写多读”的模式,一个进程写,多个进程读取,这个是OK的。

但是如果同时有多个进程进行写操作。个人认为大概率会存在问题,多进程中每个SQLiteDatabase中的锁必然是相互独立,彼此不关联的,那么这个锁必然拦截不住多个进程同时写同一张表的情况。

其他DB

总体来说,db在多进程间使用时,需要遵照一写多读的方式,并且这个db中没有缓存机制的情况下,还是可用的。但是如果像greendao或者其他本身包含缓存机制的db,那么多进程间即使使用一写多读的工作模式,同样存在风险,因为此时还存在内存与db文件之间不一致的风险

使用文件

使用文件同使用数据库本质上一样,全读 / 一写多读没有问题,但是存在多个写时,如何对多个写操作之间进行同步,这个是个大问题。目前看,没有找到什么工具内部处理好了这件事情,都需要业务层自己来做好互斥、管理。

Broadcaster

以上均是多进程之间对于持久化信息之间的共享,Android上多进程之间非持久化通信莫过于Broadcaster了。可以基于Broadcaster来封装一个好用一点的通信组件。

socket

这个方式比较原始。相对于前面所有提到的通信方式,它最大的优点是:不需要context。看起来这不算什么有用的优点。不过特殊情况下,比如我们项目中,由于历史原因获取不到能够支持Service的Context,这个反而成了唯一的选项

Messenger

Messenger是基于AIDL实现的,服务端(被动方)提供一个Service来处理客户端(主动方)连接,维护一个Handler来创建Messenger,在onBind时返回Messenger的binder。

双方用Messenger来发送数据,用Handler来处理数据。Messenger处理数据依靠Handler,所以是串行的,也就是说,Handler接到多个message时,就要排队依次处理。

AIDL

AIDL通过定义服务端暴露的接口,以提供给客户端来调用,AIDL使服务器可以并行处理,而Messenger封装了AIDL之后只能串行运行

使用ContentProvider

参考下面的后续看看: https://www.jianshu.com/p/875d13458538 https://juejin.im/entry/590833711b69e60058eb34b9

总结

进程间的通信在实际场景中频次一般有限,总体来说,先想明白业务的需求。在确定通信需求不是很大的情况下,使用普通db + broadcast基本能满足需求了。