一、试验背景和方针
我想做一个Android平台的跨进程数据通道,经过这个通道支撑若干App之间的数据传输。我想到了一些传输计划,可是缺乏在计划中做出选型的点评依据。本试验会根据若干计划完结数据传输通道,在模仿的业务场景中进行试验,从功能性目标和非功能性目标对各计划做出点评。
i. 数据通道的功能性目标
- 数据由A进程发生,从A进程传输到B进程。
- 数据是在一段时刻内接连发生的,数据的长度事前不确定。
- 每发生一段数据就要立即从A进程发送到B进程,不能比及数据发生完了再一次性传输。
- B进程需求及时接纳数据,接纳数据不能有太大推迟,更不能比及A完结了数据发送才一次性接纳。换句话说,B进程要能及时感知A进程发送了数据。
- B进程接纳的数据有必要是完好的。
- B进程要能感知A进程完毕了数据发送。
- 假如A进程逝世了,B进程要能够感知到。
ii. 非功能性目标
- 比照各传输计划的传输功率和呼应推迟
- 传输功率=传输数据量max(A进程完结发送的耗时, B进程完结接纳的耗时)
- 呼应推迟=B进程收到数据的时刻-A进程发送数据的时刻
- 在相同传输场景下比照各传输计划的CPU占用
- 比照各传输计划的功耗
二、试验计划
我完结了一个包含如下界面的测验工程,对若干传输计划进行了测验。
测验界面
测验中首先选择用例和通道,然后点击“开端测验”,等待测验完毕。下方两个文本框别离显现A进程和B进程的状况。
i. 测验计划
测验计划
测验场景
主进程:
- 由
MainActivity
供给:1)测验用例和通道的选项;2)操作按钮;3)展现:provider
进程和:consumer
进程回传的测验状况。 - 开端测验时先发动
:consumer
进程,准备好接纳数据。然后发动:provider
进程。
:provider
进程:
- 界说
ProviderService
,以前台服务身份运转,确保进程具有较高的优先级。 - 经过
XferFactory
创立通道实例,将通道实例注入到IUsecase
的实例目标。 - 经过
UsecaseFactory
创立用例,经过用例发生数据。 - 依照固定时刻距离周期性的把测验状况发给
MainActivity
。
:consumer
进程:
- 界说
ConsumerService
,以前台服务身份运转,确保进程具有较高的优先级。 - 经过
XferFactory
创立通道实例,接纳从:provider
进程发来的数据。 - 依照固定时刻距离周期性的把测验状况发给
MainActivity
。
用例计划
用例的责任:
- 发生待发送的数据,不同用例支撑不同的数据规划。
- 调用注入的
IXfer
实例目标发送数据。 - 完结不同的数据发送节奏。
- 调用
IXfer
的接口封闭通道,或杀死:provider
进程。 - 把发送的数据回调给
ProviderService
,异步计算被发送数据的MD5,用于跟:consumer
进程比照。
测验数据:
- 数据取自一段中文文本,文本以txt(UTF8)格式存放于
assets
目录。 - 文本读取到内存中,按300个字符分割,每次发送一个分片。
- 各用例能够发送其间部分文本,能够循环发送多次文本。
- 为防止IO影响测验过程的功能,由用例工厂在进程发动的时分准备好测验数据,各用例实例仅从内存中取出数据来发送。
用例实例:
- 用例1:周期性发送
- 按固定周期发送数据。
- 能够验证数据通道是否支撑
:consumer
进程及时接纳数据。
- 用例2:极速发送
- 在一个循环内不断的发送数据。
- 数据通道应当同步发送数据,这样才干验证数据通道的传输功率。
- 用例4:封闭通道
- 由数据通道供给封闭才干,用例仅负责调用数据通道的接口。
- 验证数据通道是否支撑让
:consumer
进程感知通道封闭。
- 用例5:杀进程
- 直接杀掉
:provider
进程。 - 验证数据通道是否支撑让
:consumer
进程感知:provider
进程的状况。
- 直接杀掉
通道工厂和数据通道
- 经过工厂创立详细的数据通道,做到数据通道和用例解耦。
- 由一个类完结一个数据通道计划,
:provider
进程和:consumer
进程使用同一个数据通道类,两个进程别离调用不同的接口来发动数据发送和数据接纳。 -
:consumer
进程的数据通道把收到的数据按顺序回调给ConsumerService
,异步计算MD5,跟:provider
进程做比照,验证数据完好性。
ii. 被测验的通道计划
本试验的通道计划包含如下约束:
- 仅经过Java代码编写,调用Android或JDK接口,不涉及JNI。
- 不能确保这些计划之外没有其他更好的计划。
- 尽量同步发送、同步接纳,以表现收发行为本身对传输功率的影响。
下面记载本试验各数据通道的要害计划。
经过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()
时假如捕获到SocketException
或EOFException
,就以为链接已经断开了。在数据收发中假如发生了上述反常之外的反常,也视为链接断开。
经过Pipe传输
树立衔接:
- 经过
ParcelFileDescriptor.createPipe()
创立成对的Pipe目标,以FD的方式回来。 - 经过IPC把第零个FD发送到
:consumer
进程。注意FD不能直接经过Intent
发送,这儿存在一些技巧。
断开链接:把FD给close()
掉。
发送数据/接纳数据:经过FD构造DataOutputStream
和DataInputStream
,别离用于数据发送和接纳。
感知链接断开/感知进程退出:假如捕获到EOFException
,就视为链接断开。假如捕获到其他反常,则视为:provider
进程反常,链接已不可用。
经过SockerPipe传输
跟经过Pipe传输基本相同,要害差异在于创立FD对的接口改用ParcelFileDescriptor.createSocketPair()
。
经过ShareMemory传输
树立衔接:ShareMemory
是Parcelable
,能够跨进程传输。但由于内部是FD,不能直接经过Intent
传输,需求一些技巧。
断开链接/感知链接断开/感知进程退出:ShareMemory
本身没有衔接特点。要凭借其他辅佐计划,比如凭借AIDL、或在内存中约好一个flag。
发送数据/接纳数据:
-
ShareMemory
需求指定地址空间的长度,本身并不能无约束的写入数据。为了支撑恣意长度的传输容量,这儿把有限的内存空间做成一个环形缓冲区,内存中保存若干字节作为缓冲区的读写指针。 - 发送数据的时分,依照写指针的方位写入数据,然后移动写指针。注意写的时分不要覆盖了读指针指向的方位。假如没有满足的空间写入数据,要等待&轮询,直到读指针发生移动,留出了能够写数据的空间。
- 读数据的时分,轮询检测写指针和读指针的距离,发现有能够读的数据,就立马读出来,然后更新读指针到写指针的方位。
- 轮询的时刻距离关乎功能和功率的平衡。
iii. 测验数据收集计划
收集CPU占用
-
重启测验app进程
-
在
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 }
-
对一个通道计划进行测验,测验以“用例1:周期性发送”为测验场景(这样才干确保计算数据没有搅扰要素)
-
经过
Ctrl+C
终止CPU数据收集 -
从
sdcard
导出CPU数据,计算:provider
和:consumer
进程的CPU占用(本试验不计算主进程的CPU占用)
收集电量消耗
-
衔接手机,重启测验app,重置电量计算
dumpsys batterystats --reset dumpsys batterystats --enable full-wake-history
-
断开手机,履行测验
-
从头衔接手机,打印电量数据
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"
收集耗时、推迟等
在代码中精确记载数据收发的起止时刻点,在测验完毕后以日志方式打印出来。
三、试验成果
本试验的数据肯定存在误差,不确保能经受住高标准的挑战。本试验经过以下办法确保了测验成果具有必定的可信度:
- 事前对一切待测验的通讯计划做了验证,确保接纳的数据的MD5和发送的数据的MD5是持平的。即使说,数据通道在测验中能确保数据完好性。
- 一切测验在同一部手机上测验完结。手机上没有安装过多使用,防止出现各类使用在后台运转影响测验数据。
- 一切测验尽量确保在接连的两三个小时内完结,防止时刻段隐含的环境差异性影响测验数据。
- 每一轮测验都重启使用,防止潜在的技术问题影响测验数据。
- 测验数据按10次测验的均匀值算,尽量抹平随机要素的搅扰。
- 一些接连操作采用adb命令模仿点击完结,防止手动操作引进误差。
- 用release版别的apk做测验,防止构建中保存的调试代码影响测验数据。
i. 功能性目标比照
传输计划的可用性:
- 试验中一切传输计划都有才干确保数据完好性。
- 各传输计划都有才干支撑不确定长度的数据传输。
- 各传输计划都有才干支撑无限长的数据传输。
传输计划的通道特性:
-
对发送方来说,能够随时发送数据。数据长度或许有约束,但只要不超过长度约束,就能够发送恣意长度的数据。
-
对接纳方来说,都有才干感知到有数据发送,都能够及时接纳数据。
-
各传输计划本身都有才干支撑衔接自动封闭或通知数据发送完毕(不凭借其他辅佐计划,单纯根据本传输计划的特性)
传输计划 自动封闭的计划比照 Intent 经过 action
区分数据和信令,其间能够界说数据发送完毕的信令AIDL 在接口界说中约好通知数据发送完毕的方法 Pipe 封闭FD SocketPipe 封闭FD SocketChannel 封闭 Socket
SharedMemory 约好某些字节表明传输状况,其间包含一个flag,标志数据传输是否完毕 根据SharedMemory的传输通道也能够凭借Intent或AIDL直接完结自动封闭计划。
-
各传输计划中,有些计划无法在没有其他辅佐计划的前提下感知通道的被迫断开(如接纳进程能否感知到发送进程的逝世)
传输计划 直接感知通道被迫断开的才干 Intent 通道本身是无衔接的,无直接感知才干。 AIDL 假如是接纳方向发送方 bindService()
,则经过onServiceDisconnected()
完结感知才干。假如是发送方向接纳方bindService()
,则需求发送方把一个IBinder
目标传给接纳方,接纳方经过DeathRecipient
完结感知才干。Pipe 经过读写反常感知。 SocketPipe 经过读写反常感知。 SocketChannel 经过读写反常感知。 SharedMemory 通道本身是无衔接的,无直接感知才干。 对于不具有直接感知才干的通道计划,能够凭借
DeathRecipient
直接完结感知才干。
ii. 非功能性目标比照
要害定论先行:
- 性价比(功率/功能)方面,SharedMemory是最高的计划,而Intent是最低的(AIDL紧随其后),其他计划居中基本持平。
- 上述性价比点评,没有包含开发难度的维度。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 |
各传输计划的优劣在数据上能够直观的表现出来,这儿不做过多解读,但有几个数据需求做出说明:
- AIDL的发送耗时高于接纳耗时,其底层原因是AIDL默许是同步调用,被调用方先回来了,主调方才干回来。
- AIDL接纳最终一段数据的推迟是负数,其底层原因同上。
- SharedMemory的推迟取决于轮询的计划中对轮询距离的规划。距离大则推迟大,距离小则或许引进CPU和功耗风险。
- 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老皮!!!欢迎大家来找我讨论交流