一次搞定Process和Task

一次搞定Process和Task

关于进程-Process

影响process的属性

控制组件运行进程的有两个个属性:android:processandroid:multiprocess

关于android:process

  • 可分别指定Application和四大组件运行的进程名
  • 不指定时,使用包名作为进程名
  • 如果进程名是以冒号开头的,则这个进程是应用的私有进程
  • 如果进程名是以字符开头的,且符合包名规范,则这个进程是全局的

只有provider和activity定义了android:multiprocess,但不要在activity中使用。

如果android:multiprocess为true,则每个访问provider的应用都会自己创建一个ContentProvider实例

  • 优势:避免跨进程通信,提高数据访问效率
  • 弊端:多个实例导致系统内存消耗变大,且难以处理多个进程之间的数据同步问题

如果需要两个不同APK中的组件运行于同一个进程,需要以下三个条件:

  • 两个应用程序使用相同的android:sharedUserId
  • 两个应用使用相同的keystore进行签名
  • 为组件设置相同的android:process

不同Process带来的影响

跨进程通信

由于不同的进程使用不同的内存空间,所以不同进程之间的通信本质上只依赖以下四种方式

  • Binder:包括Messenger、AIDL,只能传递基本数据类型或实现了Parcelable的对象,本质是数据的序列化和反序列化
  • Intent:跨进程的组件调用,本质也是数据的序列化和反序列化
  • ASHMEM:匿名共享内存,需要依赖Binder传递共享内存文件描述,使用系统签名的APK才能直接使用
  • 其他通用方式:共享文件、网络接口等

ContentProvider的实现就是使用ASHMEM

  • ContentProvider保存的数据,都是以私有文件存储的,其他进程无法访问
  • 其他程序创建CursorWindow时,同时创建了一块匿名共享内存,并实现了Parcelable
  • 通过Binder将CursorWindow和共享内存文件描述传递给ContentProvider
  • ContentProvider创建自己的共享内存文件描述,并指向共享内存

进程优先级

内存不足时,系统可能杀掉进程,这时进程中的Application和应用组件也会随之销毁。系统如何选择停止的进程,就涉及到进程优先级了。

  1. 前台进程 Activte process
    • 前台响应用户事件的Activity以及与之绑定的Service
    • startForeground的Service
    • 正在执行onStart,onCreate,OnDestroy的Service
    • 正在执行onReceive的BroadcastReceiver
  2. 可见进程 Visible Process
    • onPause但未onStop的Activity
    • 绑定到可见Activity的Service
  3. 服务进程 Service process
    • 普通Service
  4. 背景进程 Background process
    • onStop的Activity
  5. 空进程 Empty process
    • 不包含任何组件的进程

Process的实际使用

  1. 子进程可以分担主进程的内存压力
  2. 在主进程Crash时,子进程中的功能不受影响
  3. 子进程的Application#onCreate中可以跳过一些不必要的初始化

关于任务-Task

关于Task,有太多的参数与之相关,而且他们相互影响,感觉无法穷尽。下面仅从实际的使用出发,明确和Task相关的一些原则。

Task和startActivityForResult

使用startActivityForResult的必要条件是被启动的Activity和原Activity要在同一个Task中。因此,使用startActivityForResult时,会强制将新启动的Activity放在原来的Task中,不论activiy的xml属性和Intent#Flag_XXX

只有一个例外FLAG_ACTIVITY_NEW_TASK:如果使用这个标签,原Activity会立刻收到onActivityResult,并执行和startActivity相同的逻辑。在后面的分析中可以看到,仅凭这个FLAG是无法正真启动新Task的。

多Task有关的参数

可以下载多Task相关的演示代码,修改MultiTaskActivity启动方法的Intent和Manifest中的属性,就可演示这小结的大部分例子。

launchMode

  1. standardsingleTop:可以包含多个Activity实例
  2. singleInstancesingleTask:只有一个Activity实例
  3. singleInstance是一个Task中唯一的Activity
  4. singleTask不一定是Task root(google文档有问题,但使用中建议作为Task root使用)

taskAffinity

  1. 仅有singleInstance启动新Task不依赖taskAffinity
  2. singleTask的Activity只有其taskAffinity和原Activity不一样时才会启动新Task
  3. standardsingleTop启动新Task,不仅要求新的taskAffinity,而且需要FLAG_ACTIVITY_NEW_TASK
  4. 如果有后台Task和要启动的Activity具有相同的taskAffinity,则不会启动新的Task,而是将后台Task切换到前台,并根据其他属性和标签重新安排后台Task中的Activity和新Activity
  5. taskAffinity的值应该是以‘.’开头的字符串

其他多Task属性和标签

只有standardsingleTop类型的Activity才可能出现多个实例。因此,只有这两类Activity才可能出现多个实例,并处于不同的Task中。下面的讨论,也仅限于这两类。

  • FLAG_ACTIVITY_NEW_DOCUMENT
    • 如果已经有Activity符合Intent,则切换到该Activity
    • 否则启动新的Activity
  • FLAG_ACTIVITY_MULTIPLE_TASK
    • 配合FLAG_ACTIVITY_NEW_TASK和FLAG_ACTIVITY_NEW_DOCUMENT使用,在启动时不再寻找匹配的Activity,而是直接启动新任务。
  • documentLaunchMode:与前面两个标签相互限制。
    • always:每次启动时都会启动新的Task。等同于FLAG_ACTIVITY_NEW_DOCUMENT和FLAG_ACTIVITY_MULTIPLE_TASK
    • intoExisting:如果有符合Intent的Activity存在,则不再启动新Task,否则启动新Task。等同于FLAG_ACTIVITY_NEW_DOCUMENT,且不带FLAG_ACTIVITY_MULTIPLE_TASK
    • none:默认值。由其他标签和属性决定是否启动新Task。
    • never:使FLAG_ACTIVITY_NEW_DOCUMENT和FLAG_ACTIVITY_MULTIPLE_TASK失效。

FLAG_ACTIVITY_NEW_DOCUMENT和documentLaunchMode都是5.0的新特性。通过FLAG_ACTIVITY_NEW_DOCUMENT启动一个新的Task,不需要指定taskAffinity。因此,最好不要将taskAffinity与FLAG_ACTIVITY_NEW_DOCUMENT混用。

在实测中发现,never无法使FLAG_ACTIVITY_NEW_DOCUMENT失效,只是使FLAG_ACTIVITY_MULTIPLE_TASK失效。

Task相关的其他参数

这部分的参数主要处理以下两个问题,演示代码不包括这部分的例子。

  • Task在最近任务中的表现
  • 一个新Activity进入已有的Task时,如何重新安排Task中的老Activity和新Activity

其它xml元素

  1. alllowTaskReparenting:默认false,只有standard和singleTop有效,表示Activity实例可以换到其他Task中。(通过应用图标启动程序时会生效,在程序内通过FLAG_ACTIVITY_NEW_TASK切换任务时,不生效)例子:浏览器。
  2. finishOnTaskLaunch:默认false,通过应用图标重新启动程序时,这个Activity会被销毁。
  3. clearTaskOnLaunch:用于Task root,默认false,启动时会清空Task上的其它Activity,只保留root。(通过应用图标启动程序时会生效,通过最近任务启动时不生效)
  4. alwaysRetainTaskState:用于Task root,默认false,在应用切换到后台30分钟后会被系统清理。如果为true则不会被系统清理
  5. noHistory:页面不可见时被自动销毁,不会保存在mHistory

其他Intent.FLAG

  1. FLAG_ACTIVITY_CLEAR_TOP:启动的Activity存在,则不创建新实例,而是使用原有实例,并清空上面的其它Activity。在Activity是singleTask,或者Intent中有FLAG_ACTIVITY_SINGLE_TOP,这个Activity不会重新创建。
  2. FLAG_ACTIVITY_REORDER_TO_FRONT:Activity会重新排序,任何Activity都不会被销毁。
  3. FLAG_ACTIVITY_NO_HISTORY:和xml中的noHistory效果相同
  4. FLAG_ACTIVITY_TASK_ON_HOME:后退时直接回Home,而不会回到之前的Task

Task和Process的关系

之前文章的《怎么处理SaveState》的末尾也提到的Task和Process的关系。这里重复一遍。

我们先看下再ActivityManagerService中进程Process和Task的关系

Task&Process

  1. ActivityManagerService通过一个列表mHistory来管理所有ActivityRecord
  2. 相同TaskRecord中的ActivityRecord在列表中处于连续位置
  3. 同一个TaskRecord中的ActivityRecord可能处于不同的ProcessRecord

由于以下两个因素,使得很难找到Task和进程之间关联的清晰线索。

  • 同一Task中的Activity可能属于不同进程
  • 进程中不仅有Activity,还有Service和BroadcastReceiver

先看Task中Activity销毁

  • 处理的问题:一个进程内部,前后台Task的资源协调
  • 触发时机:进程使用的内存接近上限时(根据机型不同,大约在64M~256M之间)
  • 会调用Activity#onDestroy,后台Task回到前台时会触发Activity#onRestoreInstanceState

再看进程被杀

  • 处理的问题:系统控制中,多进程之间的资源协调
  • 触发时机:整个系统使用的内存接近机器配置的内存上限时
  • 不会调用Activity#onDestroy,后台Task回到前台时不一定会触发Activity#onRestoreInstanceState,和Task的启动方式和启动时间有关。

我们以一个简化的例子讨论两者的关系。假设:

  • 单进程最大可使用内存为100M,进程使用内存超过90M时会触发后台Task销毁。
  • 系统总可用内存为200M,系统使用内存超过190M时会触发后台进程被杀。
  • 系统中运行着3个进程,他们在三个Task中的分布和内存使用如下
  • Task1处于前台运行
Momory Usage Process1 Process2 Process 3
Task1 60M 20M -
Task2 20M 20M -
Task3 - - 40M

如果T1 P1部分消耗的内存由60M上升到75M,由于P1的总内存消耗达到95M,所以会导致P1 T2中的Activity被销毁。

如果T1 P2部分消耗的内存由20M上升到50M,会导致系统总内存消耗达到190M。此时三个Process中,P1和P2和前台Task关联,优先级较高,所以系统会杀掉P3。

这个例子,只是对两者关系的一个简要说明。系统对进程的实际处理方式要复杂得多!