Android跨进程传大图思考及实现——附上原理分析_android微信 跨进程打开图片

51 阅读7分钟
    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)

至于是什么样的大图,这个只有法海知道了`(小青:好羞涩啊)`🙈🙈🙈


![](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/da918eb1beec4494806f998622791906~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55So5oi3MDY4Mzc0MTQxMzQ0:q75.awebp?rk3s=f64ab15b&x-expires=1771286377&x-signature=CfeypXE1%2F5%2FRz%2FZGLyjnXSlY4W0%3D)


所以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.cpp

static 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.IApplicationThreadStubStubProxy.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

//下面两个注释 //引用自官方文档:source.android.google.cn/devices/arc… #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.IApplicationThreadStubStubProxy.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: 0

Exception 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.IApplicationThreadStubStubProxy.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.Intent

public 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.Bundle

public 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 = 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<String, Object> val) { ...... for (int i=0; i<N; i++) { writeString(val.keyAt(i)); //根据不同数据类型调用不同的write方法 writeValue(val.valueAt(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.cpp

static 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<BitmapWrapper*>(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);
......