跨进程传图片方案
- 直接intent传bitmap
- 使用文件读写
- intent传递自定义binder,binder中传递image
- 使用网络传输
一、直接intent传bitmap
优势
使用简单
劣势
相关代码可能有侵入性,必须在四大组件中接收。
- intent传递数据的总大小是1MB,其中还包括启动四大组件相关的信息。因此使用intent传递的图片不宜超过500KB,甚至应该更小,因为还可能会传递其他数据。
- 如果通过此方案传递大图片,必须先压缩后传输。开发者需要自己评估业务场景是否适用,毕竟很多场景不适合让图片质量下降。
如果intent传递的数据超过1MB时,就会报错TransactionTooLargeException。
二、使用文件读写
优势
- 使用相对简单
- 一定程度上可以避免逻辑耦合的问题,对于单独的模块来说只需要负责“读”或者“写”。
劣势
- 需要自己控制读写的时机。
- 读写操作相比直接传递效率更低,耗时更长。
三、intent传递自定义binder,binder中传递image
优势
- 效率相对最高
- 传递图片没有大小限制
劣势
- 使用相对麻烦,需要自定义aidl
- 相关代码可能有侵入性,必须在四大组件中接收。
四、使用网络传输
这个方案比较特殊,只有特殊场景才会使用。
一般存在两种情况:
- 两个进程都与服务端通信,一个进程传输,一个进程接收。如果是图片上传和下载的场景可以使用,但是效率肯定没有直接传输高。
- 两个进程一个作为服务端,一个作为客户端。 这个方案的关键在于这个“作为服务端的进程”,需要这个进程本身就是某种图片服务的提供者,且通过网络来对其他进程或模块提供服务度。
intent通过binder传递bitmap的Demo
有兴趣的读者可以自行看下Demo:
github地址
https://github.com/Double2hao/ProcessImageTest
intent通过binder传递bitmap的原理
bitmap在native层传递的时候会有两种方案:
1. 直接将图片写入进程的缓冲区。
缓冲区是进程在初始化的时候就已经申请了的,并且大小是一定的。因此如果写入的大小超过了缓冲区的大小,就会报错。
2. 使用共享内存,将共享内存的fd,也就是文件描述符写入缓冲区。
这样的好处就是传递图片的大小不会受限制。
intent直接传递bitmap对应方案1,intent通过binder传递bitmap对应方案2。
为什么intent传递bitmap不默认使用共享内存?
个人理解,缓冲区的大小是进程创建的时候就申请好的,如果能保证不超出缓冲区大小的情况下使用缓冲区,不需要再另外申请共享内存肯定是最好的。
如果默认就使用共享内存,而缓冲区资源又没人用的话,就造成了资源浪费。
因此如果开发者自己认为需要传递大文件的话,就使用共享内存,默认不使用。
Android 基于共享内存跨进程实时传输大量图片或数据
aidl传输文件有大小1M限制,单次传输不适合传递大数据,可以使用aidl传递共享内存引用ParcelFileDescriptor方式传递图片信息,具体实现如下。
一、service端
1.1 aidl文件IIpcService.aidl 定义,这里主要用到pfd参数
interface IIpcService {
/**
* Demonstrates some basic types that you can use as parameters
* and return values in AIDL.
*/
// void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
// double aDouble, String aString);
void register2Server(String packageName,IIpcServiceListener ipcServiceListener);
void unregister2Server(String packageName);
String processClientRequest(String packageName,String clientRequest,inout ParcelFileDescriptor pfd);
}
1.2 service端 处理客户端传递的图片 流 引用ParcelFileDescriptor ,将获取的ParcelFileDescriptor转换成Bitmap 并回调给ui层显示
public String processClientRequest(String packageName, String clientRequest, ParcelFileDescriptor pfd) {
Log.i(TAG, "processClientRequest 11 packageName:" + packageName
+ " clientRequest:" + clientRequest + " pfd:" + pfd);
String ret = clientRequest;
FileDescriptor fileDescriptor = pfd.getFileDescriptor();
FileInputStream fis = null;
try {
fis = new FileInputStream(fileDescriptor);
Bitmap rawBitmap = BitmapFactory.decodeStream(fis);
ret += " process success!";
Log.i(TAG, "processClientRequest 112 rawBitmap:" + rawBitmap + " mUiShow:" + mUiShow);
if (null != mUiShow) {
mUiShow.showBitmap(rawBitmap);
}
} catch (Exception e) {
Log.i(TAG, "processClientRequest 22 error:" + e);
e.printStackTrace();
} finally {
try {
if (fis != null) {
fis.close();
}
} catch (IOException e) {
Log.i(TAG, "processClientRequest 33 error:" + e);
}
}
Log.i(TAG, "processClientRequest 22 end ret:" + ret);
return ret;
}
1.3 也可以处理客户端传递的字节数组 数据引用,处理代码如下
public String processClientRequest(String packageName, String clientRequest, ParcelFileDescriptor pfd) {
Log.i(TAG, "processClientRequest 11 packageName:" + packageName
+ " clientRequest:" + clientRequest + " pfd:" + pfd);
String ret = clientRequest;
FileDescriptor fileDescriptor = pfd.getFileDescriptor();
FileInputStream fis = null;
try {
fis = new FileInputStream(fileDescriptor);
byte[] content = new byte[5];
fis.read(content);
Log.i(TAG, "processClientRequest 111 content:" + content);
for (int i = 0; i < content.length; i++) {
Log.i(TAG, "processClientRequest 113 content[" + i + "]=" + content[i]);
}
}
} catch(
Exception e)
{
Log.i(TAG, "processClientRequest 33 error:" + e);
e.printStackTrace();
} finally
{
try {
if (fis != null) {
fis.close();
}
} catch (IOException e) {
Log.i(TAG, "processClientRequest 44 error:" + e);
}
}
Log.i(TAG,"processClientRequest 55 end ret:"+ret);
return ret;
}
二客户端
2.1 客户端连接到service后,调用接口 传递图片文件引用 ParcelFileDescriptor
String path = "/sdcard/lilei/20230207161749238.jpg";
public ParcelFileDescriptor getPfd() {
ParcelFileDescriptor pfd = null;
try {
pfd = ParcelFileDescriptor.open(new File(path), MODE_READ_WRITE);
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
}
Log.i(TAG, "getPfd() pfd:" + pfd);
return pfd;
}
public String sendFile(String requestJson) {
Log.i(TAG, "sendFile() requestJson:" + requestJson);
if (null != mFtIpcManager) {
return mFtIpcManager.processClientRequest(requestJson, getPfd());
}
return "error";
}
2.2 客户端也可以传递 byte数组
public ParcelFileDescriptor getTextPfd() {
ParcelFileDescriptor pfd = null;
try {
MemoryFile memoryFile = new MemoryFile("test", 1024);
Method method = MemoryFile.class.getDeclaredMethod("getFileDescriptor");
FileDescriptor des = (FileDescriptor) method.invoke(memoryFile);
pfd = ParcelFileDescriptor.dup(des);
//向内存中写入字节数组
memoryFile.getOutputStream().write(new byte[]{1,2,5,4,3});
//关闭流
memoryFile.getOutputStream().close();
memoryFile.close();
} catch (IOException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);a
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
Log.i(TAG, "getTextPfd() pfd:" + pfd);
return pfd;
}
public String sendFile(String requestJson) {
Log.i(TAG, "sendFile() requestJson:" + requestJson);
if (null != mFtIpcManager) {
return mFtIpcManager.processClientRequest(requestJson, getTextPfd());
}
return "error";
}
2.3 客户端也可以传递Bitmap数据,需要先将Bitmap转换成 byte数组,service端接收同1.2
public class test {
public ParcelFileDescriptor getBitmapPfd() {
ParcelFileDescriptor pfd = null;
Bitmap bitmap= BitmapFactory.decodeResource(FtClientApp.getAppContext().getResources(), R.drawable.btn_send);
//将Bitmap转成字节数组
ByteArrayOutputStream stream = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
byte[] byteArray = stream.toByteArray();
try {
MemoryFile memoryFile = new MemoryFile("test", bitmap.getByteCount());
Method method = MemoryFile.class.getDeclaredMethod("getFileDescriptor");
FileDescriptor des = (FileDescriptor) method.invoke(memoryFile);
pfd = ParcelFileDescriptor.dup(des);
//向内存中写入字节数组
memoryFile.getOutputStream().write(byteArray);
//关闭流
memoryFile.getOutputStream().close();
memoryFile.close();
} catch (IOException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
Log.i(TAG, "getPfd() pfd:" + pfd);
return pfd;
}
public String sendFile(String requestJson) {
Log.i(TAG, "sendFile() requestJson:" + requestJson);
if (null != mFtIpcManager) {
return mFtIpcManager.processClientRequest(requestJson, getBitmapPfd());
}
return "error";
}
PS:这里也可以共享内存传递大字符串,只是需要将字符串和字节数组转换一下再传递,转换实现如下。
1.string 字符串转换成 byte[] 数组
String str = "reagan";
byte[] srtbyte = str.getBytes();
2.byte[] 数组转换成 string字符串
String res = new String(srtbyte);
或者
String res = new String(srtbyte,"UTF-8");
System.out.println(res);
Android 跨进程传递大图片
跨进程传大图,有哪些方案?
通过IPC的方式转发图片数据。
- Binder:性能很好,方便使用,但是有大小限制
- Socket,管道:存在多次copy问题,性能差,也有大小限制
- 共享内存:性能好
主要看两个指标
1. 性能,减少copy次数
2. 内存泄露,资源及时关闭
跨进程通信是需要buffer的,发送数据需要buffer,返回数据也需要buffer,buffer只有整个transaction结束时才释放,发送数据占用太多buffer的话,留给返回数据的buffer就很少了。事情buffer是吧就会跑TransactionTooLargeException
进程在启动binder机制时会映射一块内存,大小是1M,也就是说跨进程通信时申请缓冲区大小不大于1M,一个事务用太多的话,其他事务可用空间就变少。甚至事情100K都会跑TransactiionToolargeException。
第三条是官方推荐
binder_alloc_buffer: 分配data_size(parcel)大小的内存空间
Bitmap 是如何传输的
上面代码块,如果使用那个intent启动另外一个进程的Activity,会抛出TransactionTooLargeException, 是因为这个bitmap直接copy到缓冲区了,没有里有ashmem机制,因为allowFd机制没有打开
下面代码块不会抛出TransactionTooLargeException
bitmap超过16K时,使用的是匿名共享内存的方式
setAllowFds(false): 禁用了bundle的fd机制,bundle写入parcel时也会禁用parcel的allowFd机制
这两个底层都用到了共享内存 , 适合跨进程大数据传输
Android 共享内存实现跨进程大文件传输(设计思路和Demo实现绕过Binder传输限制)
项目链接 AndroidSharedMemoryDemo
下图是文件详情:13.7M
项目在客户端最终的显示效果:
本人建议可以下载下来直接查看就可以,对照着代码查看.
项目整体分为三个 部分
1.客户端clientapp:负责调用SDK测试
2.SDKjar包:mylibrary:扶着整体的共享内存的开辟以及读取操作.
3.服务端serverapp:当客户端请求数据时,往共享内存里面写数据.
本文不再对如何提供SDK给第三方项目使用的进行讲解,只针对部代码进行详解,如果想看项目的详解可以查看 Android 应用提供SDK Jar包给第三方使用 (设计思路 以及实现步骤) 和本项目的架构类似。
本项目的整体调用时序图如下:
本项目的类关系图:
MemoryFile简介:
MemoryFile是android在最开始就引入的一套框架,其内部实际上是封装了android特有的内存共享机制Ashmem匿名共享内存,简单来说,Ashmem在Android内核中是被注册成一个特殊的字符设备,Ashmem驱动通过在内核的一个自定义slab缓冲区中初始化一段内存区域,然后通过mmap把申请的内存映射到用户的进程空间中(通过tmpfs),这样子就可以在用户进程中使用这里申请的内存了,另外,Ashmem的一个特性就是可以在系统内存不足的时候,回收掉被标记为"unpin"的内存,这个后面会讲到,另外,MemoryFile也可以通过Binder跨进程调用来让两个进程共享一段内存区域。由于整个申请内存的过程并不再Java层上,可以很明显的看出使用MemoryFile申请的内存实际上是并不会占用Java堆内存的。
MemoryFile.java位置在如下,有兴趣的同学可以翻阅源码看一看
frameworks/base/core/java/android/os/MemoryFile.java
mylibrary简介:
本项目中 mylibrary负责整体的内存开辟以及读操作
MemoryFileHelper.java是开辟空间的具体操作类,具体拿到MemoryFIle用的是反射方法,核心方法如下:
public static MemoryFile openMemoryFile(FileDescriptor fd, int length, int mode) {
MemoryFile memoryFile = null;
try {
memoryFile = new MemoryFile("tem", 1);
memoryFile.close();
if (!Utils.isMoreThanAPI27()) {
Class<?> c = MemoryFile.class;
Method native_mmap = null;
Method[] ms = c.getDeclaredMethods();
for (int i = 0; ms != null && i < ms.length; i++) {
if (ms[i].getName().equals("native_mmap")) {
native_mmap = ms[i];
}
}
ReflectUtils.setField("android.os.MemoryFile", memoryFile, "mFD", fd);
ReflectUtils.setField("android.os.MemoryFile", memoryFile, "mLength", length);
if (Utils.isMoreThanAPI21()) {
long address = (long) ReflectUtils.invokeMethod(null, native_mmap, fd, length, mode);
ReflectUtils.setField("android.os.MemoryFile", memoryFile, "mAddress", address);
} else {
int address = (int) ReflectUtils.invokeMethod(null, native_mmap, fd, length, mode);
ReflectUtils.setField("android.os.MemoryFile", memoryFile, "mAddress", address);
}
} else {
SharedMemory sharedMemory = SharedMemory.create("tem", 1);
sharedMemory.close();
ReflectUtils.setField("android.os.SharedMemory", sharedMemory, "mFileDescriptor", fd);
ReflectUtils.setField("android.os.SharedMemory", sharedMemory, "mSize", length);
ReflectUtils.setField("android.os.MemoryFile", memoryFile, "mSharedMemory", sharedMemory);
ReflectUtils.setField("android.os.MemoryFile", memoryFile, "mMapping", sharedMemory.mapReadWrite());
}
} catch (Exception e) {
e.printStackTrace();
}
return memoryFile;
}
MyControllerImp.java负责开辟共享内存和负责通过Aidl和服务端交互的核心业务类.最核心的方法在链接建立之后,将自己创建的ParcelFileDescriptor对象传递给server这样保证了serverapp拿到的MemoryFile对象是同一个对象
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
Log.d("mysdk", " sdk onServiceConnected ");
if (service == null) {
if (mMyRemoteCtrl != null) {
try {
mMyRemoteCtrl.unlinkToDeath(mFrameDataCallBack.asBinder());
} catch (RemoteException e) {
e.printStackTrace();
}
}
mMyRemoteCtrl = null;
} else {
mMyRemoteCtrl = IMyRemoteCtrl.Stub.asInterface(service);
if (mMyRemoteCtrl != null) {
try {
mMyRemoteCtrl.linkToDeath(mFrameDataCallBack.asBinder());
Log.d("mysdk", " sdk onServiceConnected setBackBufferCallBack ");
if (mCallBack != null) {
mMyRemoteCtrl.setParcelFileDescriptor(mMemoryFile.getParcelFileDescriptor());
mMyRemoteCtrl.registerFrameByteCallBack(mFrameDataCallBack);
mMemoryFile.setReadBufferCallBack(mCallBack);
} else {
mMyRemoteCtrl.unregisterFrameByteCallBack(mFrameDataCallBack);
mMemoryFile.release();
}
Log.d("mysdk", " sdk onServiceConnected setBackBufferCallBack eld ");
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
}
而客户端注册的IReadBufferCallBack.java的对象也被MyControllerImp.java 设置到了MemoryFileImp.java中当,也就是说MemoryFileImp.java持有客户端注册的数据回调对象
mMemoryFile.setReadBufferCallBack(mCallBack);
serverapp简介:
服务端最核心的类ServerClientService.java中的内部类MyRemoteCtrlImpl.java负责和mylibrary 中的MyControllerImp.java通讯,用于接收传递过来的远端ParcelFileDescriptor对象和callBack.最核心的代码如下,因为没有持续的流可以写,就自己准备了一张在草原天路拍色的图片放在服务端的assets文件夹下 13M 绝对超出了Binder限制.
public class MyRemoteCtrlImpl extends IMyRemoteCtrl.Stub {
...........省略代码.......
@Override
public void readFile(String msg) throws RemoteException {
Log.d("mysdk"," mParcelFileDescriptor = null ? " + (mParcelFileDescriptor == null));
if (mParcelFileDescriptor != null) {
memoryFile = MemoryFileHelper.openMemoryFile(mParcelFileDescriptor, MEMORY_SIZE, 0x3);
}
Log.d("mysdk"," memoryFile = null ? " + (memoryFile == null));
try {
InputStream open = getResources().getAssets().open("IMG.JPG");
byte[] buffer = new byte[open.available()];
Log.d("mysdk"," 服务端 buffer " + buffer.length );
open.read(buffer);
readImage(buffer);
open.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
//写共享内存方法
private void readImage(byte[] frame) {
if (memoryFile != null) {
try {
memoryFile.readBytes(isCanRead, 0, 0, 1);
if (isCanRead[0]== 0) {
memoryFile.writeBytes(frame, 0, 1, frame.length);
isCanRead[0] = 1;
memoryFile.writeBytes(isCanRead, 0, 0, 1);
}
Log.d("mysdk"," 服务端 canReadFrameData " );
mIReadDataCallBack.canReadFileData();
} catch (Exception e ) {
Log.d("mysdk"," 服务端 Exception " + e.getMessage() );
e.printStackTrace();
}
}
}
clientapp简介
集成mylibrary的jar包 不知道如何打jar包的可以看 Android 应用提供SDK Jar包给第三方使用 (设计思路 以及实现步骤)
核心代码就是读取数据进行显示MainActivity.java中
public class MainActivity extends AppCompatActivity {
ImageView iv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
iv = findViewById(R.id.iv);
SharedMemoryLibSDK.getInstance().init(this);
SharedMemoryLibSDK.getInstance().setBackBufferCallBack(new IReadBufferCallBack() {
@Override
public void onReadBuffer(final byte[] bytes, int i) {
Log.d("mysdk"," 客户端 读取到客户写到共享内存的大小为: " + bytes.length);
runOnUiThread(new Runnable() {
@Override
public void run() {
Bitmap bitmap = byteToBitmap(bytes);
iv.setImageBitmap(bitmap);
}
});
}
});
}
public static Bitmap byteToBitmap(byte[] imgByte) {
InputStream input = null;
Bitmap bitmap = null;
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 8;
input = new ByteArrayInputStream(imgByte);
SoftReference softRef = new SoftReference(BitmapFactory.decodeStream(
input, null, options));
bitmap = (Bitmap) softRef.get();
if (imgByte != null) {
imgByte = null;
}
try {
if (input != null) {
input.close();
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return bitmap;
}
public void readFIle(View view) {
Log.d("mysdk"," 客户端 调用服务端的 readFIle " );
SharedMemoryLibSDK.getInstance().readFile("我是客户端");
}
}
点击按钮的最后效果:因为数据太大在用byte生成BitMap的时候容易内存溢出,在客户端读取完成数据之后对生成的BitMap使用了中压缩了处理.