文章

多进程

多进程

Android 中的多进程

Android 进程概述

进程是进程资源分配和调度的基本单位。在 Android 中,一个应用默认有一个主进程(正常情况下,一个 apk 启动后只会运行在一个进程中,其进程名为 apk 的包名,所有的组件都会在这个进程中运行),我们也可以通过配置实现一个应用对应多个进程。

多进程模式中,不同进程间的组件会拥有独立的虚拟机,Application 以及内存空间

android:process 属性

  • 实现单个应用多个进程,需要用 android:process 属性(默认值为包名)
  • 作用于:ApplicationActivityServiceBroadcastReceiverContentProvider
  • : 开头的,需要在当前进程名前面加上当前包名,表示这个进程是应用私有的,无法在跨应用之间共用
  • 以小写字母开头,完整的命名,表示这个进程为全局进程,可以被多个应用共用,其他应用可以通过 ShareUID 方式可以和它跑在同一个进程中

进程名不能以数字开头,并且要符合命名规范,必须要有 .(android:process 值一定要有个点号 .)否则将会出现这种错误:Invalid process name simon in package com.wind.check: must have at least one '.'.

私有进程和公有进程的区别

  1. : 开头 以冒号开头,冒号后面的字符串原则上是可以随意指定的,这种设置形式表示该进程为当前应用的私有进程,其他应用的组件不可以和它跑在同一个进程中。

如果我们的包名为 “me.hacket.multiprocess”,则实际的进程名 为 “me.hacket.multiprocess:remote”。

  1. 以字母开头 小写字母开头,表示运行在一个以这个名字命名的全局进程中,其他应用通过设置相同的 ShareUID 可以和它跑在同一个进程。

如 android:process=”com.secondProcess”

ShareUID ShareUserId,在 Android 里面每个 app 都有一个唯一的 linux user ID,则这样权限就被设置成该应用程序的文件只对该用户可见,只对该应用程序自身可见,而我们可以使他们对其他的应用程序可见,这会使我们用到 SharedUserId,也就是让两个 apk 使用相同的 userID,这样它们就可以看到对方的文件

进程生命周期与优先级

Android 系统将尽量长时间地保持应用进程,但为了新建进程或运行更重要的进程,最终需要移除旧进程来回收内存。 为了确定保留或终止哪些进程,系统会根据进程中正在运行的组件以及这些组件的状态,将每个进程放入 “ 重要性层次结构 “ 中。 必要时,系统会首先消除重要性最低的进程,然后是重要性略逊的进程,依此类推,以回收系统资源。

进程优先级

重要性层次结构一共有 5 级。以下列表按照重要程度列出了各类进程(第一个进程最重要,将是最后一个被终止的进程):

前台进程 foreground process

用户当前操作所必需的进程。如果一个进程满足以下任一条件,即视为前台进程。

  1. 托管用户正在交互的 Activity(已调用 Activity 的 onResume() 方法)
  2. 托管某个 Service,后者绑定到用户正在交互的 Activity
  3. 托管正在 “ 前台 “ 运行的 Service(服务已调用 startForeground())
  4. 托管正执行一个生命周期回调的 Service(onCreate()、onStart() 或 onDestroy())
  5. 托管正执行其 onReceive() 方法的 BroadcastReceiver
可见进程

没有任何前台组件、但仍会影响用户在屏幕上所见内容的进程。 如果一个进程满足以下任一条件,即视为可见进程

  • 托管不在前台、但仍对用户可见的 Activity(已调用其 onPause() 方法)。例如,如果前台 Activity 启动了一个对话框,允许在其后显示上一 Activity,则有可能会发生这种情况。
  • 托管绑定到可见(或前台)Activity 的 Service。

可见进程被视为是极其重要的进程,除非为了维持所有前台进程同时运行而必须终止,否则系统不会终止这些进程。

服务进程

正在运行已使用 startService() 方法启动的服务且不属于上述两个更高类别进程的进程。尽管服务进程与用户所见内容没有直接关联,但是它们通常在执行一些用户关心的操作(例如,在后台播放音乐或从网络下载数据)。因此,除非内存不足以维持所有前台进程和可见进程同时运行,否则系统会让服务进程保持运行状态。

后台进程

这些进程对用户体验没有直接影响,系统可能随时终止它们,以回收内存供前台进程、可见进程或服务进程使用。

包含目前对用户不可见的 Activity 的进程(已调用 Activity 的 onStop() 方法)。

通常会有很多后台进程在运行,因此它们会保存在 LRU (最近最少使用)列表中,以确保包含用户最近查看的 Activity 的进程最后一个被终止。如果某个 Activity 正确实现了生命周期方法,并保存了其当前状态,则终止其进程不会对用户体验产生明显影响,因为当用户导航回该 Activity 时,Activity 会恢复其所有可见状态。

空进程

不含任何活动应用组件的进程。保留这种进程的的唯一目的是用作缓存,以缩短下次在其中运行组件所需的启动时间。为使总体系统资源在进程缓存和底层内核缓存之间保持平衡,系统往往会终止这些进程。


由于运行服务的进程其级别高于托管后台 Activity 的进程,因此启动长时间运行操作的 Activity 最好为该操作启动服务,而不是简单地创建工作线程,当操作有可能比 Activity 更加持久时尤要如此。例如,正在将图片上传到网站的 Activity 应该启动服务来执行上传,这样一来,即使用户退出 Activity,仍可在后台继续执行上传操作。使用服务可以保证,无论 Activity 发生什么情况,该操作至少具备 “ 服务进程 “ 优先级。 同理,广播接收器也应使用服务,而不是简单地将耗时冗长的操作放入线程中。

多进程好处

1、增加 App 可用内存

在 Android 中,默认情况下系统会为每个 App 分配一定大小的内存。比如从最早的 16M 到后面的 32M 或者 48M 等。具体的内存大小取决于硬件和系统版本。

这些有限的内存对于普通的 App 还算是够用,但是对于展示大量图片的应用来说,显得实在是捉襟见肘。

仔细研究一下,你会发现原来系统的这个限制是作用于进程的 (毕竟进程是作为资源分配的基本单位)。意思就是说,如果一个应用实现多个进程,那么这个应用可以获得更多的内存。

于是,增加 App 可用内存成了应用多进程的重要原因

2、独立于主进程,子进程崩溃不会影响主进程

除了增加 App 可用内存之外,确保使用多进程,可以独立于主进程,确保某些任务的执行和完成。

举一个简单的例子,之前的一个项目存在退出的功能,其具体实现为杀掉进程。为了保证某些统计数据上报正常,不受当前进程退出的影响,我们可以使用独立的进程来完成。

如果子进程因为某种原因崩溃了,不会直接导致主程序的崩溃,可以降低我们程序的崩溃率。

3、实现守护进程

如果主线程中的服务要从开机起持续运行,若由于内存等原因被系统 kill 掉,守护进程可以重新启动主线程的服务。

多进程的缺点

数据共享问题

Android 为每个应用分配了独立的虚拟机,或者说为每个进程都分配了一个独立的虚拟机,不同的虚拟机在内存分配上有不同的地址空间,导致在不同的虚拟机中访问同一个类的对象会产生多份副本,

静态成员的失效

由于处于不同的进程导致了数据无法共享内容,无论是 static 变量还是单例模式的实现。多进程存在多份内存副本。

按照正常的逻辑,静态变量是可以在应用的所有地方共享的,但是设置了 process 属性后,产生了两个隔离的内存空间,一个内存空间里值的修改并不会影响到另外一个内存空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class ProcessTestActivity extends Activity {
    public final static String TAG = "viclee";
    public static boolean processFlag = false;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_process_test);
        processFlag = true;
        Log.i(TAG, "ProcessTestActivity onCreate");
        this.startService(new Intent(this, ProcessTestService.class));
    }
}
public class ProcessTestService extends Service {
    public static final String TAG = "viclee";
    @Override
    public void onCreate() {
        Log.i(TAG, "ProcessTestService onCreate");
        Log.i(TAG, "ProcessTestActivity.processFlag is " + ProcessTestActivity.processFlag);
    }
    @Override
    public IBinder onBind(Intent arg0) {
        return null;
    }
}

线程同步机制完全失效

内存地址不一致,不管是锁对象还是锁全局类都无法保证线程同步,因为不同进程锁的不是同一个对象

文件共享问题

多进程情况下会出现两个进程在同一时刻访问同一个数据库文件的情况。这就可能造成资源的竞争访问,导致诸如数据库损坏、数据丢失等。

  • SharedPreferences 还没有增加对多进程的支持。SP 底层是通过读写 XML 文件来实现的,并发写显示是可能出问题的,甚至并发读/写都有可能出问题
  • 跨进程共享数据可以通过 Intent, Messenger,AIDL 等。

SQLite 容易被锁

  • 由于每个进程可能会使用各自的 SQLOpenHelper 实例,如果两个进程同时对数据库操作,则会发生 SQLiteDatabaseLockedException 等异常。
  • 解决方法:可以使用 ContentProvider 来实现或者使用其他存储方式。

Application 的多次重建

  • 多进程之后,每个进程在创建的时候,都会执行自己的 Application.onCreate 方法。
  • 通常情况下,onCreate 中包含了我们很多业务相关的初始化,更重要的这其中没有做按照进程按需初始化,即每个进程都会执行全部的初始化。
  • 按需初始化需要根据当前进程名称,进行最小需要的业务初始化。
  • 按需初始化可以选择简单的 if else 判断,也可以结合工厂模式

多进程设计

  • 小组件在多进程
  • WebView 在多进程
  • WorKManager 在多进程 参考:[[03 .WorkManager多进程支持]]
  • Google Cubes 在多进程

一个案例

AndroidManifest.xml 文件中配置:

1
2
3
4
5
6
7
8
<application
            android:name="me.hacket.demo.base.BaseApplication"
            android:icon="@mipmap/ic_launcher"
            android:process="com.baidu.hacket">
    <service android:name="me.hacket.demo.multiapp.MultiProcessServiceA"
         android:process=":service"/>
    <service android:name="me.hacket.demo.multiapp.MultiProcessServiceB"/>
</application>

在 Application 中 onCreate() 初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private String getCurrentProcessName() {
    String currentProcName = "";
    int pid = android.os.Process.myPid();
    ActivityManager manager = (ActivityManager) this.getSystemService(Context.ACTIVITY_SERVICE);
    for (ActivityManager.RunningAppProcessInfo processInfo : manager.getRunningAppProcesses()) {
        if (processInfo.pid == pid) {
            currentProcName = processInfo.processName;
            break;
        }
    }
    return currentProcName;
}
private void initMultiProcess() {
    String currentProcessName = getCurrentProcessName();
    Log.e(TAG, "currentPrcessName:" + currentProcessName + ",pid:" + Process.myPid());
    startService(new Intent(this, MultiProcessServiceA.class));
    startService(new Intent(this, MultiProcessServiceB.class));
}

log 输出:(https://cdn.nlark.com/yuque/0/2023/png/694278/1687971572185-96acdfbc-9fa7-482e-8c41-89aa98a74a46.png#averageHue=%23474040&clientId=u958d25b4-bc3c-4&from=paste&height=120&id=u68b14d73&originHeight=180&originWidth=1350&originalType=binary&ratio=1.5&rotation=0&showTitle=false&size=150172&status=done&style=none&taskId=ufcefd08a-3fe2-447d-b628-efe30d1c692&title=&width=900)

log 输出可以看出,ServiceA 和 ServiceB 都初始化了 2 遍,这是由于本身 Application 创建会执行 1 遍,然后遇到启动 ServiceA,由于其在一个新的进程,又会将 Application 的 onCreate 方法走一遍,导致执行了 2 遍。

Android 在遇到需要放在新进程的组件时,首先创建新的进程,此时当前进程的当前线程是阻塞的,直到新进程创建完毕。
启动单独进程组件时,进程的创建会影响继承了 Application 的实例,里面的方法会完全再执行一遍,尽管进程由于处于不同的虚拟机,里面的所有内存私有,但一些影响文件,UI 等无进程概念的问题会出现。

关于 Android 应用多进程的整理 http://droidyue.com/blog/2017/01/15/android-multiple-processes-summary/index.html

多进程通信

多进程问题

两个进程对应的是不同的内存区域

  • Application 对象会创建多次
  • 静态成员不共用
  • 同步锁失效
  • 单例模式失效
  • 数据传递的对象必须可序列化

多进程通信的方式

  • Intent 原理其实也是对于 Binder 的封装,但是他只能做到单向的数据传递
  • 文件
  • 广播
  • Messenger
  • AIDL

Binder 注意

DeadObjectException

原因

在使用 aidl 进行进程间通信时,有时候在客户端调用服务端的接口会抛出 DeadObjectException 异常,原因一般是由于某种原因服务端程序崩溃重启或者服务对象由于内存紧张被回收导致的

解决:Binder 死亡监听

  1. 在调用服务端接口的时候先进行判断 bind 是否还活着
1
2
3
4
5
6
7
8
if (mIMyAidlInterface != null && mIMyAidlInterface.asBinder().isBinderAlive()) {
    try {
        mIMyAidlInterface.startRecord();
    } catch (Exception e) {
        Log.e(TAG, "Exception");
        e.printStackTrace();
    }
}
  1. 注册死亡代理
1
2
3
4
5
6
7
8
9
10
11
12
private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() {

    @Override
    public void binderDied() {                           
        // 当绑定的service异常断开连接后,自动执行此方法
        Log.e(TAG,"binderDied " );
        if (mIMyAidlInterface != null){
        // 当前绑定由于异常断开时,将当前死亡代理进行解绑        mIMyAidlInterface.asBinder().unlinkToDeath(mDeathRecipient, 0);
        // 重新绑定服务端的service
        bindService(new Intent("com.service.bind"),mMyServiceConnection,BIND_AUTO_CREATE);      
    }
};
  1. 在 service 绑定成功后,调用 linkToDeath()注册进 service,当 service 发生异常断开连接后会自动调用 binderDied()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void onServiceConnected(ComponentName name, IBinder service) {          
    // 绑定成功回调
    Log.d(TAG, "onServiceConnected");
    mIMyAidlInterface = IMyAidlInterface.Stub.asInterface(service);     
    // 获取服务端提供的接口
    try {
        // 注册死亡代理
        if(mIMyAidlInterface != null){
        Log.d(TAG, mIMyAidlInterface.getName());
        service.linkToDeath(mDeathRecipient, 0); 
    } catch (RemoteException e) {
        e.printStackTrace();
    }
}
本文由作者按照 CC BY 4.0 进行授权