一、试验背景和方针

我想做一个Android平台的跨进程数据通道,经过这个通道支撑若干App之间的数据传输。我想到了一些传输计划,可是缺乏在计划中做出选型的点评依据。本试验会根据若干计划完结数据传输通道,在模仿的业务场景中进行试验,从功能性目标和非功能性目标对各计划做出点评。

i. 数据通道的功能性目标

  1. 数据由A进程发生,从A进程传输到B进程。
  2. 数据是在一段时刻内接连发生的,数据的长度事前不确定。
  3. 每发生一段数据就要立即从A进程发送到B进程,不能比及数据发生完了再一次性传输。
  4. B进程需求及时接纳数据,接纳数据不能有太大推迟,更不能比及A完结了数据发送才一次性接纳。换句话说,B进程要能及时感知A进程发送了数据。
  5. B进程接纳的数据有必要是完好的。
  6. B进程要能感知A进程完毕了数据发送。
  7. 假如A进程逝世了,B进程要能够感知到。

ii. 非功能性目标

  1. 比照各传输计划的传输功率和呼应推迟
    1. 传输功率=传输数据量max(A进程完结发送的耗时, B进程完结接纳的耗时)
    2. 呼应推迟=B进程收到数据的时刻-A进程发送数据的时刻
  2. 在相同传输场景下比照各传输计划的CPU占用
  3. 比照各传输计划的功耗

二、试验计划

我完结了一个包含如下界面的测验工程,对若干传输计划进行了测验。

Android跨进程数据通道若干方案的实验笔记

测验界面

测验中首先选择用例和通道,然后点击“开端测验”,等待测验完毕。下方两个文本框别离显现A进程和B进程的状况。

i. 测验计划

Android跨进程数据通道若干方案的实验笔记

测验计划

测验场景

主进程:

  1. MainActivity供给:1)测验用例和通道的选项;2)操作按钮;3)展现:provider进程和:consumer进程回传的测验状况。
  2. 开端测验时先发动:consumer进程,准备好接纳数据。然后发动:provider进程。

:provider进程:

  1. 界说ProviderService,以前台服务身份运转,确保进程具有较高的优先级。
  2. 经过XferFactory创立通道实例,将通道实例注入到IUsecase的实例目标。
  3. 经过UsecaseFactory创立用例,经过用例发生数据。
  4. 依照固定时刻距离周期性的把测验状况发给MainActivity

:consumer进程:

  1. 界说ConsumerService,以前台服务身份运转,确保进程具有较高的优先级。
  2. 经过XferFactory创立通道实例,接纳从:provider进程发来的数据。
  3. 依照固定时刻距离周期性的把测验状况发给MainActivity

用例计划

用例的责任:

  1. 发生待发送的数据,不同用例支撑不同的数据规划。
  2. 调用注入的IXfer实例目标发送数据。
  3. 完结不同的数据发送节奏。
  4. 调用IXfer的接口封闭通道,或杀死:provider进程。
  5. 把发送的数据回调给ProviderService,异步计算被发送数据的MD5,用于跟:consumer进程比照。

测验数据:

  1. 数据取自一段中文文本,文本以txt(UTF8)格式存放于assets目录。
  2. 文本读取到内存中,按300个字符分割,每次发送一个分片。
  3. 各用例能够发送其间部分文本,能够循环发送多次文本。
  4. 为防止IO影响测验过程的功能,由用例工厂在进程发动的时分准备好测验数据,各用例实例仅从内存中取出数据来发送。

用例实例:

  1. 用例1:周期性发送
    1. 按固定周期发送数据。
    2. 能够验证数据通道是否支撑:consumer进程及时接纳数据。
  2. 用例2:极速发送
    1. 在一个循环内不断的发送数据。
    2. 数据通道应当同步发送数据,这样才干验证数据通道的传输功率。
  3. 用例4:封闭通道
    1. 由数据通道供给封闭才干,用例仅负责调用数据通道的接口。
    2. 验证数据通道是否支撑让:consumer进程感知通道封闭。
  4. 用例5:杀进程
    1. 直接杀掉:provider进程。
    2. 验证数据通道是否支撑让:consumer进程感知:provider进程的状况。

通道工厂和数据通道

  1. 经过工厂创立详细的数据通道,做到数据通道和用例解耦。
  2. 由一个类完结一个数据通道计划,:provider进程和:consumer进程使用同一个数据通道类,两个进程别离调用不同的接口来发动数据发送和数据接纳。
  3. :consumer进程的数据通道把收到的数据按顺序回调给ConsumerService,异步计算MD5,跟:provider进程做比照,验证数据完好性。

ii. 被测验的通道计划

本试验的通道计划包含如下约束:

  1. 仅经过Java代码编写,调用Android或JDK接口,不涉及JNI。
  2. 不能确保这些计划之外没有其他更好的计划。
  3. 尽量同步发送、同步接纳,以表现收发行为本身对传输功率的影响。

下面记载本试验各数据通道的要害计划。

经过Intent传输

发送数据:把数据放到Intent中,调用startService()。最终发送一个Intent奉告数据传输完毕。经过Intent中的action区分数据和信令。

接纳数据:呼应onStartCommand(),从Intent中取出数据。依据Intent中的信令判断数据是否接纳完毕。

经过‘根据AIDL的IPC‘传输

树立衔接:bindService()/onBind()

断开链接:unbindService()

AIDL界说:

interface IAidlXferInterface {
    void reverseBind(IBinder binder);
    void doTransfer(String data);
    void close(); // 奉告数据发送完毕
}

感知链接断开或进程退出:经过reverseBind():consumer进程持有:provider进程的一个目标,经过DeathRecipient感知对方的状况。

@Override
public void reverseBind(IBinder binder) throws RemoteException {
    binder.linkToDeath(() -> {
        //:provider进程死了
    }, 0);
}

经过SocketChannel传输

树立衔接/断开链接::consumer作为服务端,:provider作为客户端,树立和断开链接遵循SocketChannel的惯例用法,其间IP使用127.0.0.1

发送数据/接纳数据:经过DataOutputStream.writeUTF()DataInputStream.readUTF()收发数据,

感知链接断开/感知进程退出:DataInputStream.readUTF()时假如捕获到SocketExceptionEOFException,就以为链接已经断开了。在数据收发中假如发生了上述反常之外的反常,也视为链接断开。

经过Pipe传输

树立衔接:

  1. 经过ParcelFileDescriptor.createPipe()创立成对的Pipe目标,以FD的方式回来。
  2. 经过IPC把第零个FD发送到:consumer进程。注意FD不能直接经过Intent发送,这儿存在一些技巧。

断开链接:把FD给close()掉。

发送数据/接纳数据:经过FD构造DataOutputStreamDataInputStream,别离用于数据发送和接纳。

感知链接断开/感知进程退出:假如捕获到EOFException,就视为链接断开。假如捕获到其他反常,则视为:provider进程反常,链接已不可用。

经过SockerPipe传输

跟经过Pipe传输基本相同,要害差异在于创立FD对的接口改用ParcelFileDescriptor.createSocketPair()

经过ShareMemory传输

树立衔接:ShareMemoryParcelable,能够跨进程传输。但由于内部是FD,不能直接经过Intent传输,需求一些技巧。

断开链接/感知链接断开/感知进程退出:ShareMemory本身没有衔接特点。要凭借其他辅佐计划,比如凭借AIDL、或在内存中约好一个flag。

发送数据/接纳数据:

  1. ShareMemory需求指定地址空间的长度,本身并不能无约束的写入数据。为了支撑恣意长度的传输容量,这儿把有限的内存空间做成一个环形缓冲区,内存中保存若干字节作为缓冲区的读写指针。
  2. 发送数据的时分,依照写指针的方位写入数据,然后移动写指针。注意写的时分不要覆盖了读指针指向的方位。假如没有满足的空间写入数据,要等待&轮询,直到读指针发生移动,留出了能够写数据的空间。
  3. 读数据的时分,轮询检测写指针和读指针的距离,发现有能够读的数据,就立马读出来,然后更新读指针到写指针的方位。
  4. 轮询的时刻距离关乎功能和功率的平衡。

iii. 测验数据收集计划

收集CPU占用

  1. 重启测验app进程

  2. adb shell中发动CPU数据收集

    function record_cpu() {
        xfer=$1 # 通道计划名称
        pkg=$2 # 测验app的packageName
        uid=$(top -o PID,USER,%CPU,CMDLINE -b -n 1 | grep $pkg | grep -v 'shell' | awk '{print $2}')
        echo "uid=$uid"
        top -o PID,USER,%CPU,CMDLINE -b -d 0.015 -u $uid | grep $pkg | tee /sdcard/temp/cpu_${xfer}_$(date +%Y%m%dT%H%M%S).txt
    }
    
  3. 对一个通道计划进行测验,测验以“用例1:周期性发送”为测验场景(这样才干确保计算数据没有搅扰要素)

  4. 经过Ctrl+C终止CPU数据收集

  5. sdcard导出CPU数据,计算:provider:consumer进程的CPU占用(本试验不计算主进程的CPU占用)

收集电量消耗

  1. 衔接手机,重启测验app,重置电量计算

    dumpsys batterystats --reset
    dumpsys batterystats --enable full-wake-history
    
  2. 断开手机,履行测验

  3. 从头衔接手机,打印电量数据

    pkg= # 填写测验app的packageName
    uid=$(dumpsys batterystats | grep $pkg | head -1 | grep -oE 'u[0-9a-f]*a[0-9a-f]*')
    echo "uid=$uid"
    dumpsys batterystats | awk 'BEGIN{c=0} /Estimated power use/{c=1} {if(c==1){print $0}}' | grep "Uid $uid"
    

收集耗时、推迟等

在代码中精确记载数据收发的起止时刻点,在测验完毕后以日志方式打印出来。

三、试验成果

本试验的数据肯定存在误差,不确保能经受住高标准的挑战。本试验经过以下办法确保了测验成果具有必定的可信度:

  1. 事前对一切待测验的通讯计划做了验证,确保接纳的数据的MD5和发送的数据的MD5是持平的。即使说,数据通道在测验中能确保数据完好性。
  2. 一切测验在同一部手机上测验完结。手机上没有安装过多使用,防止出现各类使用在后台运转影响测验数据。
  3. 一切测验尽量确保在接连的两三个小时内完结,防止时刻段隐含的环境差异性影响测验数据。
  4. 每一轮测验都重启使用,防止潜在的技术问题影响测验数据。
  5. 测验数据按10次测验的均匀值算,尽量抹平随机要素的搅扰。
  6. 一些接连操作采用adb命令模仿点击完结,防止手动操作引进误差。
  7. 用release版别的apk做测验,防止构建中保存的调试代码影响测验数据。

i. 功能性目标比照

传输计划的可用性:

  1. 试验中一切传输计划都有才干确保数据完好性。
  2. 各传输计划都有才干支撑不确定长度的数据传输。
  3. 各传输计划都有才干支撑无限长的数据传输。

传输计划的通道特性:

  1. 对发送方来说,能够随时发送数据。数据长度或许有约束,但只要不超过长度约束,就能够发送恣意长度的数据。

  2. 对接纳方来说,都有才干感知到有数据发送,都能够及时接纳数据。

  3. 各传输计划本身都有才干支撑衔接自动封闭或通知数据发送完毕(不凭借其他辅佐计划,单纯根据本传输计划的特性)

    传输计划 自动封闭的计划比照
    Intent 经过action区分数据和信令,其间能够界说数据发送完毕的信令
    AIDL 在接口界说中约好通知数据发送完毕的方法
    Pipe 封闭FD
    SocketPipe 封闭FD
    SocketChannel 封闭Socket
    SharedMemory 约好某些字节表明传输状况,其间包含一个flag,标志数据传输是否完毕

    根据SharedMemory的传输通道也能够凭借Intent或AIDL直接完结自动封闭计划。

  4. 各传输计划中,有些计划无法在没有其他辅佐计划的前提下感知通道的被迫断开(如接纳进程能否感知到发送进程的逝世)

    传输计划 直接感知通道被迫断开的才干
    Intent 通道本身是无衔接的,无直接感知才干。
    AIDL 假如是接纳方向发送方bindService(),则经过onServiceDisconnected()完结感知才干。假如是发送方向接纳方bindService(),则需求发送方把一个IBinder目标传给接纳方,接纳方经过DeathRecipient完结感知才干。
    Pipe 经过读写反常感知。
    SocketPipe 经过读写反常感知。
    SocketChannel 经过读写反常感知。
    SharedMemory 通道本身是无衔接的,无直接感知才干。

    对于不具有直接感知才干的通道计划,能够凭借DeathRecipient直接完结感知才干。

ii. 非功能性目标比照

要害定论先行:

  1. 性价比(功率/功能)方面,SharedMemory是最高的计划,而Intent是最低的(AIDL紧随其后),其他计划居中基本持平。
  2. 上述性价比点评,没有包含开发难度的维度。Intent和AIDL的开发难度是最低的,SharedMemory是最高的,其他计划居中基本持平。

总的来说,这儿不存在单一的优劣点评,详细业务/项目需求依据本身状况归纳决议计划。

传输功率:

Intent AIDL Pipe SocketPipe SocketChannel SharedMemory
每秒可传输的数据量 94 KB 469 KB 5.36 MB 3.89 MB 3.77 MB 6.72 MB
数据发送总耗时(秒) 15.560 3.126 0.261 0.369 0.205 0.168
数据接纳总耗时(秒) 15.560 3.116 0.267 0.369 0.380 0.213
接纳到第一段数据的推迟(毫秒 1.9 0.4 1.450 1.650 0.450 2
接纳到最终一段数据的推迟(毫秒) 0.95 -9.8 8.050 1.550 175.850 46.9

各传输计划的优劣在数据上能够直观的表现出来,这儿不做过多解读,但有几个数据需求做出说明:

  1. AIDL的发送耗时高于接纳耗时,其底层原因是AIDL默许是同步调用,被调用方先回来了,主调方才干回来。
  2. AIDL接纳最终一段数据的推迟是负数,其底层原因同上。
  3. SharedMemory的推迟取决于轮询的计划中对轮询距离的规划。距离大则推迟大,距离小则或许引进CPU和功耗风险。
  4. SocketChannel接纳最终一段数据的推迟看起来似乎太大了,这个问题本试验未深入调查原因,本文无法做出过多点评,权且以为就是现实状况。

均匀CPU占用:

Intent AIDL Pipe SocketPipe SocketChannel SharedMemory
发送进程(%) 25 71 168 141 207 145
接纳进程(%) 49 80 174 158 166 34

均匀能耗:

Intent AIDL Pipe SocketPipe SocketChannel SharedMemory
均匀能耗(mAh) 0.2893 0.1944 0.0347 0.04 0.05086 0.0179

重视公众号:Android老皮!!!欢迎大家来找我讨论交流