区块链技术博客
www.b2bchain.cn

Android跨进程传大图思考及实现——附上原理分析求职学习资料

本文介绍了Android跨进程传大图思考及实现——附上原理分析求职学习资料,有助于帮助完成毕业设计以及求职,是一篇很好的资料。

对技术面试,学习经验等有一些体会,在此分享。

基于Android11源码分析,重要的是分析思路

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跨进程传大图思考及实现——附上原理分析

所以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.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<int> 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: 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.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.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

基于Android11源码分析,重要的是分析思路

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跨进程传大图思考及实现——附上原理分析

所以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.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<int> 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: 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.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.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

基于Android11源码分析,重要的是分析思路

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跨进程传大图思考及实现——附上原理分析

所以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.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<int> 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: 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.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.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

部分转自互联网,侵权删除联系

赞(0) 打赏
部分文章转自网络,侵权联系删除b2bchain区块链学习技术社区 » Android跨进程传大图思考及实现——附上原理分析求职学习资料
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

b2b链

联系我们联系我们