Android|Android跨进程传大图思考及实现——附上原理分析

1.抛一个问题 这一天,法海想锻炼小青的定力,由于Bitmap也是一个Parcelable类型的数据,法海想通过Intent小青传个特别大的图片

intent.putExtra("myBitmap",fhBitmap)

如果“法海”(Activity)使用Intent去传递一个大的Bitmap“小青”(Activity),如果你的图片够大,会出现类似下面这样的错误,请继续往下看:
Caused by: android.os.TransactionTooLargeException: data parcel size 8294952 bytes at android.os.BinderProxy.transactNative(Native Method) at android.os.BinderProxy.transact(BinderProxy.java:535) at android.app.IActivityTaskManager$Stub$Proxy.startActivity(IActivityTaskManager.java:3904) at android.app.Instrumentation.execStartActivity(Instrumentation.java:1738)

至于是什么样的大图,这个只有法海知道了(小青:好羞涩啊)
Android|Android跨进程传大图思考及实现——附上原理分析
文章图片

所以TransactionTooLargeException这玩意爆出来的地方在哪呢?
2.问题定位分析 我们可以看到错误的日志信息里面看到调用了BinderProxy.transactNative,这个transactNative是一个native方法
//android.os.BinderProxy /** * Native implementation of transact() for proxies */ public native boolean transactNative(int code, Parcel data, Parcel reply, int flags) throws RemoteException;

在Android Code Search,全局搜索一下:android_os_BinderProxy_transact
//frameworks/base/core/jni/android_util_Binder.cppstatic jboolean android_os_BinderProxy_transact(JNIEnv* env, jobject obj, jint code, jobject dataObj, jobject replyObj, jint flags) // throws RemoteException { ...... status_t err = target->transact(code, *data, reply, flags); ...... if (err == NO_ERROR) { //如果匹配成功直接拦截不往下面执行了 return JNI_TRUE; } else if (err == UNKNOWN_TRANSACTION) { return JNI_FALSE; } signalExceptionForError(env, obj, err, true /*canThrowRemoteException*/, data->dataSize()); return JNI_FALSE; }

我们打开signalExceptionForError方法看看里面的内容
//frameworks/base/core/jni/android_util_Binder.cpp //处理异常的方法 void signalExceptionForError(JNIEnv* env, jobject obj, status_t err, bool canThrowRemoteException, int parcelSize) { switch (err) { //其他异常,大家可以自行阅读了解; //如:没有权限异常,文件太大,错误的文件描述符,等等; ........ case FAILED_TRANSACTION: { const char* exceptionToThrow; char msg[128]; //官方在FIXME中写道:事务过大是FAILED_TRANSACTION最常见的原因 //但它不是唯一的原因,Binder驱动可以返回 BR_FAILED_REPLY //也有其他原因可能是:事务格式不正确,文件描述符FD已经被关闭等等//parcelSize大于200K就会报错,canThrowRemoteException传递进来的是true if (canThrowRemoteException && parcelSize > 200*1024) { // bona fide large payload exceptionToThrow = "android/os/TransactionTooLargeException"; snprintf(msg, sizeof(msg)-1, "data parcel size %d bytes", parcelSize); } else { .......... } //使用指定的类和消息内容抛出异常 jniThrowException(env, exceptionToThrow, msg); } break; ........ } }

此时我们看到: parcelSize大于200K就会报错,难道一定是200K以内?先别着急着下结论,继续往下看
3.提出疑问 法海:我有个疑问,我看到文档写的1M大小啊;
许仙:别急,妹夫,来先看一下文档的解释,看一下使用说明:
官方TransactionTooLargeException的文档中描述到:Binder 事务缓冲区有一个有限的固定大小,目前为 1MB,由进程所有正在进行的事务共享
可以看到写的是:共享事务的缓冲区
如来佛祖:汝等别急,我们简单测试一下,Intent传递201*1024个字节数组,我们发现可以正常传递过去,Logcat仅仅输出了一个Error提示的日志信息,还是可以正常传递的
E/ActivityTaskManager: Transaction too large, intent: Intent { cmp=com.melody.test/.SecondActivity (has extras) }, extras size: 205848, icicle size: 0

我们再测试一个值,intent传递800*1024个字节数组,我们发现会崩溃
android.os.TransactionTooLargeException: data parcel size 821976 bytes at android.os.BinderProxy.transactNative(Native Method) at android.os.BinderProxy.transact(BinderProxy.java:540) at android.app.IApplicationThread$Stub$Proxy.scheduleTransaction(IApplicationThread.java:2504) at android.app.servertransaction.ClientTransaction.schedule(ClientTransaction.java:136)

不要着急,我们继续往下看分析
4.解答疑问 我们来看一下,下面两行代码
//frameworks/base/core/jni/android_util_Binder.cpp //这个方法android_os_BinderProxy_transact里面的 IBinder* target = getBPNativeData(env, obj)->mObject.get(); status_t err = target->transact(code, *data, reply, flags);

从上面的分析和测试结果,我们从target->transact这里来找err返回值, 先根据头文件,搜索对应的cpp类,我们看一下这几个cpp类:BpBinder.cpp、 IPCThreadState.cpp、ProcessState.cpp
//frameworks/native/libs/binder/ProcessState.cpp// (1 * 1024 * 1024) - (4096 *2) #define BINDER_VM_SIZE ((1 * 1024 * 1024) - sysconf(_SC_PAGE_SIZE) * 2) #define DEFAULT_MAX_BINDER_THREADS 15//下面两个注释 //引用自官方文档:https://source.android.google.cn/devices/architecture/hidl/binder-ipc #ifdef __ANDROID_VNDK__ //供应商/供应商进程之间的IPC,使用 AIDL 接口 const char* kDefaultDriver = "/dev/vndbinder"; #else // "/dev/binder" 设备节点成为框架进程的专有节点 const char* kDefaultDriver = "/dev/binder"; #endif//构造函数:初始化一些变量,Binder最大线程数等 ProcessState::ProcessState(const char* driver) : mDriverName(String8(driver)), mDriverFD(-1), mVMStart(MAP_FAILED), ...... mMaxThreads(DEFAULT_MAX_BINDER_THREADS), mStarvationStartTimeMs(0), mThreadPoolStarted(false), mThreadPoolSeq(1), mCallRestriction(CallRestriction::NONE) { ...... //打开驱动 base::Result opened = open_driver(driver); if (opened.ok()) { //映射(1M-8k)的mmap空间 mVMStart = mmap(nullptr, BINDER_VM_SIZE, PROT_READ, MAP_PRIVATE | MAP_NORESERVE, opened.value(), 0); ...... } ...... }

点击查看sysconf.cpp
getauxval(AT_PAGESZ) = 4096,可以得出Binder内存限制,BINDER_VM_SIZE = 1M-8kb
这里为什么不是1M,而是1M-8K?
最开始的时候,官方写的是1M,后来他们内部自己优化了;
来看这里官方提交的ProcessState.cpp提交的log日志:允许内核更有效地利用其虚拟地址空间
我们知道:微信的MMKV美团的Logan的日志组件,都是基于mmap来实现的;
binder驱动的注册逻辑在Binder.c中,我们看一下binder_mmap方法
//kernel/msm/drivers/android/binder.c static int binder_mmap(struct file *filp, struct vm_area_struct *vma) { int ret; struct binder_proc *proc = filp->private_data; const char *failure_string; if (proc->tsk != current->group_leader) return -EINVAL; //这里可以看到:映射空间最多4M if ((vma->vm_end - vma->vm_start) > SZ_4M) vma->vm_end = vma->vm_start + SZ_4M; ...... //初始化指定的空间vma用于分配绑定缓冲区 ret = binder_alloc_mmap_handler(&proc->alloc, vma); ...... }

这里能看到映射空间最多4M,我们再来看一下binder_alloc_mmap_handler这个方法,点击查看binder_alloc.c
//kernel/msm/drivers/android/binder_alloc.c //由binder_mmap()调用来初始化指定的空间vma用于分配绑定缓冲区 int binder_alloc_mmap_handler(struct binder_alloc *alloc, struct vm_area_struct *vma) { ...... //buffer_size最大4M alloc->buffer_size = vma->vm_end - vma->vm_start; ...... //异步事务的空闲缓冲区大小最大2M alloc->free_async_space = alloc->buffer_size / 2; ...... }

从上面的分析得出结论:
1.Binder驱动给每个进程最多分配4M的buffer空间大小;
2.异步事务的空闲缓冲区空间大小最多为2M;
3.Binder内核内存上限为1M-8k;
4.异步事务缓冲区空间大小等于buffer_size/2,具体值取决于buffer_size;
同步、异步是定义在AIDL文件中的,我们看上面测试的两个例子,其中有一个传了800*1024个字节数组崩溃如下:
android.os.TransactionTooLargeException: data parcel size 821976 bytes at android.os.BinderProxy.transactNative(Native Method) at android.os.BinderProxy.transact(BinderProxy.java:540) at android.app.IApplicationThread$Stub$Proxy.scheduleTransaction(IApplicationThread.java:2504)

点击查看IApplicationThread.aidl 查看AIDL里面的内容,我们看到scheduleTransaction是一个异步的方法;
因为oneway修饰在interface之前,会让interface内所有的方法都隐式地带上oneway;
由于oneway异步调用,我们这个时候修改一下,传递(1M-8k)/2大小之内的数据测试一下
// ((1024 * 1024 - 8 * 1024)/2)-1 E/ActivityTaskManager: Transaction too large, intent: Intent { cmp=com.melody.test/.SecondActivity (has extras) }, extras size: 520236, icicle size: 0Exception when starting activity com.melody.test/.SecondActivity android.os.TransactionTooLargeException: data parcel size 522968 bytes at android.os.BinderProxy.transactNative(Native Method) at android.os.BinderProxy.transact(BinderProxy.java:540) at android.app.IApplicationThread$Stub$Proxy.scheduleTransaction(IApplicationThread.java:2504)

可以看到还是会报错,说明异步事务的可用空间不够,仔细看一下为什么不够,细心的同学可能发现了:
警告的日志打印:extras size: 520236
崩溃的日志打印:data parcel size: 522968
大小相差:2732 约等于 2.7k
如果这个时候我们用Intent传递一个ByteArray,比之前的大小减去3k
ByteArray((1024*1024 - (8 * 1024))/2 - 3 * 1024)
startActivity(Intent(this,SecondActivity::class.java).apply {putExtra("KEY",ByteArray((1024*1024 - (8 * 1024))/2 - 3 * 1024)) })

这个时候发现(1M-8k)/2 -3k,可以成功传递数据,说明有其他数据占用了这部分空间。
我们上面写了,不要忘记:共享事务的缓冲区,这里减去3k仅测试用的,我们继续往下分析;
找一下:异步事务的空闲缓冲区空间大小比较的地方,打开binder_alloc.c,找到binder_alloc_new_buf方法
//kernel/msm/drivers/android/binder_alloc.c //分配一个新缓冲区 struct binder_buffer *binder_alloc_new_buf(struct binder_alloc *alloc, size_t data_size, size_t offsets_size, size_t extra_buffers_size, int is_async, int pid) { ...... buffer = binder_alloc_new_buf_locked(alloc, data_size, offsets_size,extra_buffers_size, is_async, pid); ....... }

我们来看一下binder_alloc_new_buf_locked方法
//kernel/msm/drivers/android/binder_alloc.c static struct binder_buffer *binder_alloc_new_buf_locked( struct binder_alloc *alloc, size_t data_size, size_t offsets_size, size_t extra_buffers_size, int is_async, int pid) { ...... //如果是异步事务,检查所需的大小是否在异步事务的空闲缓冲区区间内 if (is_async && alloc->free_async_space < size + sizeof(struct binder_buffer)) { return ERR_PTR(-ENOSPC); } }

分析了这么多,不论是同步还是异步,都是共享事务的缓冲区,如果有大量数据需要通过Activity的Intent传递,数据大小最好维持在200k以内;
上面测试的时候,超出200k数据传递的时候,LogCat已经给我们打印提示“Transaction too large”了,但是只要没有超出异步事务空闲的缓冲区大小,就不会崩溃
如果Intent传递大量的数据完全可以使用别的方式方法;
5.Intent设置Bitmap发生了什么? 5.1-Intent.writeToParcel Intent数据写入到parcel中,在writeToParcel方法里面,Intent把Bundle写入到Parcel中
//android.content.Intentpublic void writeToParcel(Parcel out, int flags) {...... //把Bundle写入到Parcel中 out.writeBundle(mExtras); }

打开out.writeBundle方法
//android.os.Parcel#writeBundle public final void writeBundle(@Nullable Bundle val) {if (val == null) {writeInt(-1); return; } //执行Bundle自身的writeToParcel方法 val.writeToParcel(this, 0); }

5.2-Bundle.writeToParcel
//android.os.Bundlepublic void writeToParcel(Parcel parcel, int flags) {final boolean oldAllowFds = parcel.pushAllowFds((mFlags & FLAG_ALLOW_FDS) != 0); try {//这里官方注释已经写的很详细了: //将Bundle内容写入Parcel,通常是为了让它通过IBinder连接传递 super.writeToParcelInner(parcel, flags); } finally {//把mAllowFds值设置回来 parcel.restoreAllowFds(oldAllowFds); } }

点击查看Parcel.cpp,我们看一下里面的pushAllowFds方法
//frameworks/native/libs/binder/Parcel.cpp bool Parcel::pushAllowFds(bool allowFds) {const bool origValue = https://www.it610.com/article/mAllowFds; if (!allowFds) {mAllowFds = false; } return origValue; }

如果Bundle设置了不允许带描述符,当调用pushAllowFds之后Parcel中的内容也不带描述符;
在文章开头,我们举的例子中:通过Intent去传递一个Bitmap,在执行到Instrumentation#execStartActivity的时候,我们发现Intent有个prepareToLeaveProcess方法,在此方法里面调用了Bundle#setAllowFds(false)
//android.app.Instrumentation public ActivityResult execStartActivity( Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode, Bundle options) {try {...... intent.prepareToLeaveProcess(who); ...... } catch (RemoteException e) {throw new RuntimeException("Failure from system", e); } return null; }

5.3-Parcel.writeArrayMapInternal 刚刚上面Bundle.writeToParcel方法里面super.writeToParcelInner触发下面方法
//android.os.BaseBundle void writeToParcelInner(Parcel parcel, int flags) {...... parcel.writeArrayMapInternal(map); ...... }

我们看一下writeArrayMapInternal方法
void writeArrayMapInternal(@Nullable ArrayMap val) {...... for (int i=0; i

5.4-writeValue 文章一开头我们使用的是intent.putExtra("bmp",法海bitmap)
//android.os.Parcel public final void writeValue(@Nullable Object v) {...... if (v instanceof Parcelable) {writeInt(VAL_PARCELABLE); writeParcelable((Parcelable) v, 0); } ...... } public final void writeParcelable(@Nullable Parcelable p, int parcelableFlags) {...... writeParcelableCreator(p); p.writeToParcel(this, parcelableFlags); }

因为传入的是Bitmap,我们看Bitmap.writeToParcel
5.5-Bitmap.writeToParcel
//android.graphics.Bitmap public void writeToParcel(Parcel p, int flags) {noteHardwareBitmapSlowCall(); //打开Bitmap.cpp找对应的native方法 if (!nativeWriteToParcel(mNativePtr, mDensity, p)) {throw new RuntimeException("native writeToParcel failed"); } }

点击打开Bitmap.cpp,查看Bitmap_writeToParcel方法
//frameworks/base/libs/hwui/jni/Bitmap.cppstatic jboolean Bitmap_writeToParcel(JNIEnv* env, jobject, jlong bitmapHandle, jint density, jobject parcel) { ...... //获得Native层的对象 android::Parcel* p = parcelForJavaObject(env, parcel); SkBitmap bitmap; auto bitmapWrapper = reinterpret_cast(bitmapHandle); //获取SkBitmap bitmapWrapper->getSkBitmap(&bitmap); //写入parcel p->writeInt32(!bitmap.isImmutable()); ...... p->writeInt32(bitmap.width()); p->writeInt32(bitmap.height()); p->writeInt32(bitmap.rowBytes()); p->writeInt32(density); // Transfer the underlying ashmem region if we have one and it's immutable. android::status_t status; int fd = bitmapWrapper->bitmap().getAshmemFd(); if (fd >= 0 && bitmap.isImmutable() && p->allowFds()) { //AshmemFd大于等于0 && bitmap不可变 && parcel允许带Fd //符合上述条件,将fd写入到parcel中 status = p->writeDupImmutableBlobFileDescriptor(fd); if (status) { doThrowRE(env, "Could not write bitmap blob file descriptor."); return JNI_FALSE; } return JNI_TRUE; }//mutableCopy=true:表示bitmap是可变的 const bool mutableCopy = !bitmap.isImmutable(); //返回像素存储所需的最小内存 size_t size = bitmap.computeByteSize(); android::Parcel::WritableBlob blob; //获取到一块blob缓冲区,往下翻有代码分析 status = p->writeBlob(size, mutableCopy, &blob); ...... }

我们来看看writeBlob里面做了什么事情
5.6-Parcel::writeBlob
//frameworks/native/libs/binder/Parcel.cppstatic const size_t BLOB_INPLACE_LIMIT = 16 * 1024; // 16kstatus_t Parcel::writeBlob(size_t len, bool mutableCopy, WritableBlob* outBlob) {status_t status; if (!mAllowFds || len <= BLOB_INPLACE_LIMIT) {//如果不允许带FD 或者 数据小于等于16k,则直接将图片写入到parcel中 status = writeInt32(BLOB_INPLACE); if (status) return status; void* ptr = writeInplace(len); if (!ptr) return NO_MEMORY; outBlob->init(-1, ptr, len, false); return NO_ERROR; } //不满足上面的条件,即(允许Fd && len > 16k): //创建一个新的ashmem区域并返回文件描述符FD //ashmem-dev.cpp里面有注释说明: //https://cs.android.com/android/platform/superproject/+/master:system/core/libcutils/ashmem-dev.cpp int fd = ashmem_create_region("Parcel Blob", len); if (fd < 0) return NO_MEMORY; //设置ashmem这块区域是“可读可写” int result = ashmem_set_prot_region(fd, PROT_READ | PROT_WRITE); if (result < 0) {status = result; } else {//根据fd,映射 “len大小” 的mmap的空间 void* ptr = ::mmap(nullptr, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); ...... if (!status) {//将fd写入到parcel中 status = writeFileDescriptor(fd, true /*takeOwnership*/); if (!status) {outBlob->init(fd, ptr, len, mutableCopy); return NO_ERROR; } } ...... } ...... }

看到这里,大家应该知道我们为什么先分析Intent传递数据大小的上限了吧;
在目录5下面的 5.2-Bundle.writeToParcel已经说明清楚了,Intent启动Activity的时候,禁用掉了文件描述符;
所以: 在执行writeBlob方法只能执行到第一个分支,直接将图片写入到parcel中,我们在目录4给出Intent传递数据大小限制的结论;
那么如何不受Intent禁用文件描述符和数据大小的限制?
6.跨进程传大图 在Parcel类中看到writeValue方法里面有个分支,判断当前value是不是IBinder,如果是IBinder类型的会调用writeStrongBinder把这个对象写入到Parcel中;
所以我们可以使用Bundle的putBinder来把IBinder对象写入到Parcel中,通过putBinder不会受Intent禁用文件描述符的影响,数据大小也没有限制,Bitmap写入到parcel中默认是true,可以使用匿名共享内存(Ashmem);
6.1-单进程下putBinder用法
//定义一个IntentBinder,此方法仅在『同一个进程』下有效哦,切记切记!!!! class IntentBinder(val imageBmp:Bitmap? = null): Binder()//------------------------使用如下--------------------------// //com.xxx.xxx.MainActivity val bitmap = BitmapFactory.decodeStream(...) startActivity(Intent(this,SecondActivity::class.java).putExtras(Bundle().apply {putBinder("myBinder",IntentBinder(bitmap)) }))//------------------------获取Bitmap并显示如下--------------------------// //com.xxx.xxx.SecondActivity val bundle: Bundle? = intent.extras val imageBinder:IntentBinder? = bundle?.getBinder("myBinder") as IntentBinder? //拿到Binder中的Bitmap val bitmap = imageBinder?.imageBmp //自行压缩后显示到ImageView上.....

注意: 这个用法不能跨进程,喜欢动手的同学,可以试一试,给SecondActivity配置一个android:process=":remote",你会发现会报一个强制转换的异常错误
//错误的用在多进程场景下,报错如下: java.lang.ClassCastException: android.os.BinderProxy cannot be cast to com.xxx.xxx.IntentBinder

为什么可以通过这种方式传递对象?
Binder会为我们的对象创建一个全局的JNI引用,点击查看android_util_Binder.cpp
//frameworks/base/core/jni/android_util_Binder.cpp ...... static struct bindernative_offsets_t { // Class state. jclass mClass; jmethodID mExecTransact; jmethodID mGetInterfaceDescriptor; // Object state. jfieldID mObject; } gBinderOffsets; ...... static const JNINativeMethod gBinderMethods[] = { /* name, signature, funcPtr */ // @CriticalNative { "getCallingPid", "()I", (void*)android_os_Binder_getCallingPid }, // @CriticalNative { "getCallingUid", "()I", (void*)android_os_Binder_getCallingUid }, ...... { "getExtension", "()Landroid/os/IBinder; ", (void*)android_os_Binder_getExtension }, { "setExtension", "(Landroid/os/IBinder; )V", (void*)android_os_Binder_setExtension }, }; const char* const kBinderPathName = "android/os/Binder"; //调用下面这个方法,完成Binder类的注册 static int int_register_android_os_Binder(JNIEnv* env) { //获取Binder的class对象 jclass clazz = FindClassOrDie(env, kBinderPathName); //内部创建全局引用,并将clazz保存到全局变量中 gBinderOffsets.mClass = MakeGlobalRefOrDie(env, clazz); //获取Java层的Binder的成员方法execTransact gBinderOffsets.mExecTransact = GetMethodIDOrDie(env, clazz, "execTransact", "(IJJI)Z"); //获取Java层的Binder的成员方法getInterfaceDescriptor gBinderOffsets.mGetInterfaceDescriptor = GetMethodIDOrDie(env, clazz, "getInterfaceDescriptor", "()Ljava/lang/String; "); //获取Java层的Binder的成员变量mObject gBinderOffsets.mObject = GetFieldIDOrDie(env, clazz, "mObject", "J"); //注册gBinderMethods中定义的函数 return RegisterMethodsOrDie( env, kBinderPathName, gBinderMethods, NELEM(gBinderMethods)); } ......

6.2-多进程下putBinder用法
//先定义一个IGetBitmapService.aidl package com.xxx.aidl; interface IGetBitmapService {Bitmap getIntentBitmap(); }//------------------------使用如下--------------------------// //com.xxx.xxx.MainActivity进程A val bitmap = BitmapFactory.decodeStream(...) startActivity(Intent(this,SecondActivity::class.java).putExtras(Bundle().apply {putBinder("myBinder",object: IGetBitmapService.Stub() {override fun getIntentBitmap(): Bitmap {return bitmap } }) }))//------------------------获取Bitmap并显示如下--------------------------// //com.xxx.xxx.SecondActivity进程B val bundle: Bundle? = intent.extras //返回IGetBitmapService类型 val getBitmapService = IGetBitmapService.Stub.asInterface(bundle?.getBinder("myBinder")) val bitmap = getBitmapService.intentBitmap //自行压缩后显示到ImageView上.....

推荐 从零开始打造自定义图片加载框架
即学即用的Android高级技能大长图加载原理及手写实现
Android App开发——如何从打造一款可商用的图片加载框架?
【Android|Android跨进程传大图思考及实现——附上原理分析】Android进阶之如何打造一个图片加载框架
更多Android知识点在下方小卡片中

    推荐阅读