总结一下android平台上的跨进程通讯共有:
- Android 特有的
Binder、ASHMEM
Start activity
Started service
Bind service
Broadcast intent
Content provider
Content observer
Job scheduler
- Socket
- Pipe
- File
本文将针对Binder、ASHMEM、service、provider进行介绍。
一、Binder
binder基本架构
前身
- Binder 前身是Be Inc公司开发的OpenBinder,在BeOS 使用,后来Palm Inc收购了Be Inc,这个项目随之转到了Pam公司下。主开发人员是Dianne Hackborn
- 术语
Binder:有时指整个机制,有时指某个接口的具体实现
Bn(Binder Native)、Stub:实现功能的一端
Bp (Binder Proxy):执行远程调用的一端
Transaction:在Binder 机制中,将一进程的数据,发送给另一进程的过程
Binder token:原本在OpenBinder中是一个整数,代表某个具体实现。在安卓中,是一个型别为IBinder的引用,指向Bn或Bp Binder。通常用在函数的参数设计上
Descriptor:一个字符串,用以代表某个binder 实现。在安卓中,通常是某个类名
- 一般进程,binder 线程最多16 个
- System_server,由于频繁需要与各应用进程通讯,并发量大,binder 线程设成32 个。
-
Binder 线程命名:Binder: [PID]_[16进位编号]
binder查看命令:
ps -A -T -Z | grep system_server | grep Binder
举例分析
以ServiceManager.getService(“power”)的调用流程为例:
以上需要注意的两点:
- 系统服务注册在servicemanager
- 应用服务注册在ActivityManagerService
两种Binder Transaction
- One way:发送binder transaction 后,就能立即执行下去,不需等远程接口执行完
可以将单一方法、或整个类的方法设为one way
One way 方法不能有返回值
- Two way:默认的阻塞式行为,必须等远程接口执行完
可以有返回值
- Parcel:一块连续的内存空间,用以将跨进程通信的数据封装,透过binder transaction 传递到另一个进程。创建parcel 的过程称为marshaling;取得parcel 后,复原成原数据结构的过程称为unmarshaling
- AIDL (Android Interface Definition Language): 将繁杂、冗长的Stub、Proxy、Parcel、binder transaction 使用,用代码产生器产生。使用只需在AIDL 文件定义接口,相关代码即可在编译时自动产生到Java 文件。后续只需撰写代码,继承Stub,实现接口所需做的事即可。
注意事项
- Binder transaction 牵涉数据打包、binder 驱动转移数据到另一进程、另一进程解包数据、调用实现等,需视为耗时操作,在对性能要求高的代码流程、或主线程,应减少调用
- One way binder transaction 并非没有性能疑虑,仍可能在binder 驱动层遇到阻塞,因为binder驱动锁的颗粒较大无法并发。在高并发负载的情况,时间甚至可达数百毫秒
- 由于一个进程的binder transaction,只能共享上限1MB 的内存来进行跨进程通信,故每次封装到Parcel 的数据量须得节制。若进程常有高并发的binder transaction,单个parcel 的数据量尽量不要超过100KB,不然可能会概率遇到1MB 不够用,出现binder transaction 失败的情况。
二、ASHMEM(Anonymous Shared Memory)
- Anonymous: 其并非真的“匿名” 其实是可以命名的。在Linux 中,anonymous 与file-backed 是内存区域的特性描述,所以不可望文生义。
- File-backed 是指磁盘上有对应的文件,此内存是用来做读写缓存,可视系统需要动态增加、减少
- Anonymous 是指这块区域内的数据,内核不会备份,清理这块区域,数据就丢失了
由上文可知Binder可以方便实现跨进程的调用,但是数据量很限,ASHMEM就没有这方面的限制。
与Binder 驱动结合,能透过跨进程传递fd实现内存共享
提供额外内存管理的弹性
常见误解澄清
- 使用ASHMEM 并非总是想跨进程共享内存。由于他可以单独划一块连续的虚拟位置,故也可用来在单应用内进行内存分区管理。安卓系统典型的例子,就是虚拟机使用ASHMEM 管理Java Heap
- 一般的binder 通信,并非使用ASHMEM 进行数据传递。Binder 驱动本来就有配置内存供跨进程传递。Parcel 是透过binder 驱动配置的内存来传递
ASHMEM 经典范例:CursorWindow
- CurorWindow是一块跨进程共享内存,用来存放数据库查询结果
//frameworks/base/libs/androidfw/CursorWindow.cpp
status_tCursorWindow::create(const String8& name, size_tsize, CursorWindow** outCursorWindow) {
String8 ashmemName(“CursorWindow: “);
ashmemName.append(name);
status_tresult;
intashmemFd= ashmem_create_region(ashmemName.string(), size);
result = ashmem_set_prot_region(ashmemFd, PROT_READ | PROT_WRITE);
// MAP_SHARED 代表准备跨进程共享。若不打算共享,可设为MAP_PRIVATE
void* data = ::mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, ashmemFd, 0);
// ...
}
status_tCursowWindow::writeToParcel(Parcel* parcel) {
status_tstatus = parcel->writeString8(mName);
parcel->writeDupFileDescriptor(mAshmemFd); // 传递fd
}
status_tCursorWindow::createFromParcel(Parcel* parcel, CursorWindow** outCursorWindow) {
intashmemFd= parcel->readFileDescriptor(); // 取出跨进程传递的fd
ssize_tsize = ashmem_get_size_region(ashmemFd);
intdupAshmemFd= ::dup(ashmemFd);
void* data = ::mmap(NULL, size, PROT_READ, MAP_SHARED, dupAshmemFd, 0);
}
三、Bind Service
若进程不存在且设定BIND_AUTO_CREATE,先产生进程
Bind Service 适用在需要与另一个服务/应用频繁通信、对调用的反应速度有要求的情况
- Bind Service 可实现高效的callback
- 与OOM_ADJ 相关
- ActivityManager对每个进程,根据其运行情况、以及进程之间的调用关系,指定了OOM_ADJ,代表每个进程的重要性,并决定当内存不足时的回收顺序
- 若进程A bind 住进程B 的service,ActivityManager会将B 的重要性提升至与A 相同。如果A 使用完服务后,忘了调用unbindService(),B 的重要性就降不下来,极端情况是,若A 是常驻的,会导致B 也变成常驻了,引发系统性内存问题
- 若进程A bind 住的service位在同一进程,则OOM_ADJ 不会因此有任何改变
Bind service 不能假设service 进程一定存在,service 进程还是有可能因crash 等因素停止运行。此时调用AIDL 接口会接收到DeadObjectException,需要重新再bind service
Bind service 时,可带以下参数控制行为
类型 | Service进程的 OOM_ADJ | Service进程的CPUscheduling policy | 使用场合 |
---|---|---|---|
默认行为 | 与client 一致。若client 为top,service则为visible | 与client 一致 | |
BIND_NOT_FOREGROUND | 与client 一致。若client 为top,service则为visible | 即使client 进程是foreground scheduling,service进程还是background scheduling | 从system_server、或persistent 进程去bindservice 执行,但不要求执行速度。 |
BIND_ALLOW_OOM_MANAGEMENT | 若service 进程曾经执行过activity、或已经执行了超过30 分钟,则放入LRU list,与其他后台应用一起管理 | 与client 一致 | 从system_server、或persistent 进程去bindservice 执行,预期可能service 执行时间长、内存大,且已做好service 进程被砍的容错处理。 |
四、Started Service
- Started service 目的是通知框架,这个进程退到后台仍在做事,尽量别砍它。
- Started service 与bind service 是两种截然不同的机制,控制方法也大不相同。
1.回传START_STICKY:在service 运行到stopService() 或stopSelf()之前,若所在进程被砍,框架会在后台重新启动进程与该service,重启后onStartCommand()所收到的Intent 为null,必须实现重启的代码,且重启做完事后确保呼叫到stopService()或stopSelf()。
2.回传START_NOT_STICKY:在service 运行到stopService()或stopSelf()之前,若service 所在进程被砍,框架不会重启进程。
3.回传START_REDELIVER_INTENT: 若service 在运行中被砍,service 会重启,且onStartCommand() 会收到之前的intent。
须知
Started Service 会提示Activity Manager,在调用stopService() 或stopSelf() 之前,尽量调高应用的OOM_ADJ,减少被砍的机会,但并非不砍。下面是可能砍的情况:
- 若运行此service 的进程,曾经启动过activity,则AMS 会认为,此进程的内存肯定不小,所以会直接将其OOM_ADJ 降至跟普通后台应用一样,依照LRU 顺序来回收
- 若此service 已经运行了30 分钟,也会放到LRU list,当做普通后台应用
- 若此service 运行的进程内存过大,越有可能早点被放入LRU list
- 若系统的可用内存实在太小,在系统杀完后台应用后,就会开始杀service 进程来腾出内存
- Service 进程被砍前,若没调用过stopService() 或stopSelf(),则Activity Manager 会安排,隔一段时间后,重新启动进行并启动service 运行。然由于系统之前砍service,大多出于内存回收考量,若短时间大量重启service 进程,内存压力可能太大,所以每次砍后,重新启动同一个service 进程的时间会逐渐拉长
- Started Service,必须处理service 重启的情况、且做完还要调用stopSelf(),才是健壮的代码。否则,要嘛事情做不完、要嘛service 没停止,造成内存问题
Foreground Service
Foreground Service 的两层含义
- 所在进程的OOM_ADJ 尽量重要,只比前台应用低一点,保证运行过程尽可能不被砍到。需要的典型案例:音乐、FM 播放,录音
- 所在进程的CPU 调度,与前台应用相同,也就是采用foreground CPU scheduling 策略。当两者竞争时,能分配到同样的运算资源。典型案例:音乐软解码播放
Start/Stop foreground 与start/stop service 是两个概念。应用需要先startService(),再startForeground()。调用stopForeground() 后,service 也仍会运行,必须等到stopService() 才会停止。
public abstract class Serviceextends ContextWrapperimplements ComponentCallbacks2 {
public final void startForeground(intid, Notification notification);
public final void stopForeground(booleanremoveNitification);
}
滥用foreground service 是内存问题的大宗,例如:
- 没有将UI 与service 分类到不同进程运行,导致service 运行时,UI 的内存不被释放,整个应用就如同常驻在内存一样,常驻内存过多
- 代码懒得做容错处理,不想处理service 被砍重启的情况,干脆就写成foreground service
- 流氓应用:不管有无需要,先用此方法让自己常驻在后台
Android O 的改变
- 应用若无法满足下面其中一项,则框架会停止其所有服务
- 具有可见的activity
- 具有foreground service
- 另一个前台应用关联到此应用
- IME、Wallpaper service、Notification listener、Voice
- 只有应用在前台时,才能启动foreground service
- 应用在前台时,首先调用Context.startForegroundService() 启动服务
- 在5 秒内,对应的Service 必须调用startForeground(),否则会产生Service Foreground Timeout ANR
- 若应用采用IntentService,则在Android O 无法正常运行,因为应用无法在后台启动服务。可使用support library 的JobIntentService替代。
五、Content Provider
Content Provider 采用URI 格式,决定其可被其他模块调用的身份、以及可在URI 中包含参数
- Provider 以authority 属性指定其被调用的名称,且一个provider 能对应一个以上的authority
- Content Resolver 透过URI,能从Activity Manager 查询对应的provider 在哪里,并以取得的IContentProvider对象进行跨进程通信
两种Provider 连线
进程A与另一进程B 的provider 建立连线时,有两种方式:
- Stable provider:若使用过程中,B crash 或被砍掉了,则A 立即被ActivityManagerService砍掉,进程A 没有任何容错处理机会
- Unstable provider:若使用过程中,B crash 或被砍掉了,A 会收到DeadObjectException,可进行容错处理
透过ContentResolver操作provider 时,默认用连线方式:
接口 | 连线类型 |
---|---|
ContentResolver.insert() | stable |
ContentResolver.applyBatch() | stable |
ContentResolver.bulkInsert() | stable |
ContentResolver.delete() | stable |
ContentResolver.update() | stable |
ContentResolver.call() | stable |
ContentResolver.openAssetFileDescroptor() | stable |
ContentResolver.query() 与取得的Cursor | stable |
如何使用Unstable Provider
public abstract class ContentResolver{
// 一般使用provider 时,是假设provider 所在进程不会被砍
// 系统在使用provider 时,也会把provider 进程的OOM_ADJ 拉到与呼叫端相同
public final @Nullable ContentProviderClient acquireContentProviderClient(@NonNullUri uri);
public final @Nullable ContentProviderClient acquireContentProviderClient(@NonNullString name);
// 若无法保证provider 进程的稳定性,只能用unstable provider 了
public final @Nullable ContentProviderClient acquireUnstableContentProviderClient(@NonNullUri uri);
public final @Nullable ContentProviderClient acquireUnstableContentProviderClient(@NonNullString name);
}
public class ContentProviderClient{
private final IContentProvidermContentProvider;
/** {@hide} */
ContentProviderClient(ContentResolvercontentResolver, IContentProvidercontentProvider, booleanstable);
// 记得呼叫release() 释放provider 连线
public booleanrelease();
}
private ContentProviderClientmProviderClient= null;
public ArrayList<BluetoothMapAccountItem> parseAccounts(BluetoothMapAccountItemapp) {
mResolver= mContext.getContentResolver();
try{
mProviderClient= mResolver.acquireUnstableContentProviderClient(
Uri.parse(app.mBase_uri_no_account));
if (mProviderClient== null) {
throw new RemoteException("Failed to acquire provider for " + app.getPackageName());
}
mProviderClient.setDetectNotResponding(PROVIDER_ANR_TIMEOUT);
Uri uri= Uri.parse(app.mBase_uri_no_account+ "/" + BluetoothMapContract.TABLE_ACCOUNT);
c = mProviderClient.query(uri, BluetoothMapContract.BT_IM_ACCOUNT_PROJECTION, null, null, BluetoothMapContract.AccountColumns._ID+" DESC");
} catch (RemoteExceptione){
if(D)Log.d(TAG,"Couldnot establish ContentProviderClientfor "+app.getPackageName()+" -returning empty account list" );
return children;
} finally {
mProviderClient.release();
}
// ...
}