Android跨进程通信

总结一下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基本架构

Android跨进程通信_第1张图片
image.png

前身

  • 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进位编号]


    Android跨进程通信_第2张图片
    image.png

binder查看命令:

ps -A  -T -Z | grep system_server | grep Binder

举例分析

以ServiceManager.getService(“power”)的调用流程为例:

Android跨进程通信_第3张图片
image.png

以上需要注意的两点:

  • 系统服务注册在servicemanager
  • 应用服务注册在ActivityManagerService
Android跨进程通信_第4张图片
image.png

两种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,先产生进程

Android跨进程通信_第5张图片
image.png

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

Android跨进程通信_第6张图片
image.png

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 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();
    }
// ...
}

你可能感兴趣的:(Android跨进程通信)