文章目录
- 跨进程传递大图有哪些方案
- TransactionTooLargeException
- Bitmap 是怎么传输的
- 为什么 intent 带大图异常,但是 binder 传输就没有问题呢?
需要了解各种跨进程传输数据的优缺点
了解 android.os.TransactionTooLargeException 触发的原因和底层机制
了解 Bitmap 底层传输原理
跨进程传递大图有哪些方案
- 通过将图片保存到一个地方,将 key 进行跨进程传递。
缓存内存适合同一个进程;如果存到本地,需要写文件再读文件,大数据会性能差
- 通过 IPC 的方式跨进程传递
- Binder:性能不错,使用方便,但是有大小限制。
- Socket 、管道:需要两次拷贝,而且也有大小限制。
- 共享内存:性能不错,可以考虑,不过比单独 binder 用起来麻烦一些
TransactionTooLargeException
跨进程通信的时候是需要传递 buffer 的(Activity启动就是跨进程通信),buffer是需要申请空间的,如果申请不到空间就会出错。而且一个进程在启动的时候会分配一个binder的缓存空间,所有和该进程通信都是共享这一个空间,并且只有在通信结束之后才会释放空间,所有如果同时进行通信,一个进程占用太多就会导致其他进程没有办法分配空间了。官方给的建议大数据分批发送。否则就会报下面的错误。
- 当用 Intent 启动一个 Activity 时,传递大的对象会报错如下(bitmap 是实现了 Parcelable ,所以可以传输)
val intent = Intent(this, SecondActivity::class.java)
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.ic_large_img)
intent.putExtra("image", bitmap)
startActivity(intent)
Caused by: android.os.TransactionTooLargeException: data parcel size 221276756 bytes
at android.os.BinderProxy.transactNative(Native Method)
at android.os.BinderProxy.transact(BinderProxy.java:575)
at android.app.IActivityTaskManager$Stub$Proxy.startActivity(IActivityTaskManager.java:4450)
at android.app.Instrumentation.execStartActivity(Instrumentation.java:1716)
看报错的栈信息,startActivity 后,执行到了native 层的 BinderProxy 的 transactNative 后报错了,接下来看看代码
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);
signalExceptionForError(env, obj, err, true /*canThrowRemoteException*/, data->dataSize());
return JNI_FALSE;
}
调用完 transact() 之后调用了 signalExceptionForError() 函数如下:
void signalExceptionForError(JNIEnv* env, jobject obj, status_t err,
bool canThrowRemoteException, int parcelSize)
{
switch (err) {
// ...
case FAILED_TRANSACTION: {
const char* exceptionToThrow;
std::string msg;
if (canThrowRemoteException && parcelSize > 200*1024) {
// bona fide large payload
exceptionToThrow = "android/os/TransactionTooLargeException";
msg = base::StringPrintf("data parcel size %d bytes", parcelSize);
} else {
// ...
}
jniThrowException(env, exceptionToThrow, msg.c_str());
}
FAILED_TRANSACTION 就是事物失败了,如果 parcelSize > 200k 就是失败了,其实不一定图片 > 200k 就一定失败。只是如果失败,并且size>200k 大概率是因为太大了。
接下来,发起 IPC 通信以后,那么返回以后是在哪里接收的呢?、,有提到过,是在 status_t IPCThreadState::transact() 里面调用了 waitForResponse() 和驱动就行交互
status_t IPCThreadState::waitForResponse(Parcel *reply, status_t *acquireResult)
{
uint32_t cmd;
int32_t err;
while (1) {
case BR_FAILED_REPLY:
err = FAILED_TRANSACTION;
goto finish;
}
}
那么这个 BR_FAILED_REPLY: 是在哪里设置的呢?是在驱动分配内存的时候
// drivers/android/binder.c
static void binder_transaction(struct binder_proc *proc,
struct binder_thread *thread,
struct binder_transaction_data *tr, int reply)
{
...
// 通过 binder_alloc_buf 申请 data_size 大小的空间
t->buffer = binder_alloc_buf(target_proc, tr->data_size,
tr->offsets_size, !reply && (t->flags & TF_ONE_WAY));
if (t->buffer == NULL) {
// 如果失败 错误就是 BR_FAILED_REPLY
return_error = BR_FAILED_REPLY;
goto err_binder_alloc_buf_failed;
}
Bitmap 是怎么传输的
val intent = Intent(this, SecondActivity::class.java)
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.ic_large_img)
val bundle = Bundle()
bundle.putParcelable("image", bitmap)
intent.putExtras(bundle)
startActivity(intent)
通过上面传输的方式来传递大图会抛 TransactionTooLargeException 异常,如果使用下面的方式则不会有问题
val intent = Intent(this, SecondActivity::class.java)
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.ic_large_img)
val bundle = Bundle()
// 传输的是 aidl 接口
bundle.putBinder("binder", object : IMyAidlInterface.Stub() {
override fun getBitmap(): Bitmap {
return bitmap
}
})
intent.putExtras(bundle)
startActivity(intent)
那么为什么这种情况就不会抛异常?
- intent 传递会将数据写进去
frameworks/base/core/java/android/os/Parcel.java
先通过 public void writeToParcel(Parcel out, int flags) 将数据写到 Parcel 里,然后调用 transact() 出去
public void writeToParcel(Parcel out, int flags) {
// ...
// 把 bundle 写到 out Parcel 中
out.writeBundle(mExtras);
}
- out.writeBundle(mExtras); 调用了 Parcel 的 writeBundle()
public final void writeBundle(@Nullable Bundle val) {
if (val == null) {
writeInt(-1);
return;
}
// this 就是 Parcel
val.writeToParcel(this, 0);
}
- val.writeToParcel(this, 0);
@Override
public void writeToParcel(Parcel parcel, int flags) {
// AllowFds 是否允许传输描述符的意思
// bundle 里面有一个 AllowFds ,如果 bundle 里面的 AllowFds 为 false 则 parcel 里面也必须是 false
// 如果 bundle 里面 AllowFds 是true 则不影响 Parcel 里面的 AllowFds
final boolean oldAllowFds = parcel.pushAllowFds((mFlags & FLAG_ALLOW_FDS) != 0);
try {
super.writeToParcelInner(parcel, flags);
} finally {
parcel.restoreAllowFds(oldAllowFds);
}
}
writeToParcel() 首先如果 bundler 里面的 AllowFds 属性是false 则 Parcel 里面也需要是 false ,如果 bundle 内部的属性 true 则不影响 Parcel 里面的属性。(AllowFds: 是否允许传输描述符)。然后调用了 super.writeToParcelInner(parcel, flags);
- super.writeToParcelInner(parcel, flags);
void writeToParcelInner(Parcel parcel, int flags) {
// frameworks/native/libs/binder/PersistableBundle.cpp.
final ArrayMap<String, Object> map;
// ...
parcel.writeArrayMapInternal(map);
}
parcel.writeArrayMapInternal(map); 把 bundle 中的 ArrayMap 写到 Parcel 里面去。之前往 Bundle put 的数据时以 map 形式保存的。下面看看是怎么写到 Parcel 里面的
void writeArrayMapInternal(@Nullable ArrayMap<String, Object> val) {
final int N = val.size();
writeInt(N);
int startPos;
for (int i=0; i<N; i++) {
writeString(val.keyAt(i));
writeValue(val.valueAt(i));
}
}
通过 for 循环依次将 key 和 value 写到 Parcel 中,key 是固定的 String ,value 是分为各种类型,下面看一下 Parcelable 类型的数据,bitmap 就是 Parcelable 类型的数据。
public final void writeValue(@Nullable Object v) {
// ...
} else if (v instanceof Parcelable) {
writeInt(VAL_PARCELABLE);
writeParcelable((Parcelable) v, 0);
}
- writeParcelable()
public final void writeParcelable(@Nullable Parcelable p, int parcelableFlags) {
if (p == null) {
writeString(null);
return;
}
writeParcelableCreator(p);
p.writeToParcel(this, parcelableFlags);
}
- p.writeToParcel(this, parcelableFlags); Parcelable 是一个接口,如果想看 bitmap 则去看 bitmap 是怎么实现 writeToParcel 的。
- Bitmap : writeToParcel()
public void writeToParcel(Parcel p, int flags) {
checkRecycled("Can't parcel a recycled bitmap");
noteHardwareBitmapSlowCall();
if (!nativeWriteToParcel(mNativePtr, mDensity, p)) {
throw new RuntimeException("native writeToParcel failed");
}
}
调用到了 nativeWriteToParcel(mNativePtr, mDensity, p)
// frameworks/base/libs/hwui/jni/Bitmap.cpp
static jboolean Bitmap_writeToParcel(JNIEnv* env, jobject,
jlong bitmapHandle, jint density, jobject parcel) {
#ifdef __ANDROID__ // Layoutlib does not support parcel
ScopedParcel p(env, parcel);
SkBitmap bitmap;
// 先获取 native 层的 Bitmap 对象
auto bitmapWrapper = reinterpret_cast<BitmapWrapper*>(bitmapHandle);
// 获取 SkBitmap 对象
bitmapWrapper->getSkBitmap(&bitmap);
// ... 往 parcel 写 bitmap 的参数
p.writeInt32(bitmap.width());
p.writeInt32(bitmap.height());
// ....
binder_status_t status;
// 获取 getAshmemFd
int fd = bitmapWrapper->bitmap().getAshmemFd();
// 如果 fd 满足条件 则直接返回 fd 文件描述符就可以
if (fd >= 0 && p.allowFds() && bitmap.isImmutable()) {
status = writeBlobFromFd(p.get(), bitmapWrapper->bitmap().getAllocationByteCount(), fd);
return JNI_TRUE;
}
// 如果 获取不到 fd 则 先计算bitmap 的大小
size_t size = bitmap.computeByteSize();
// 然后根据大小申请一块缓冲区 然后内部通过 memcpy(dest, data, size); 将数据拷贝到缓冲区
status = writeBlob(p.get(), size, bitmap.getPixels(), bitmap.isImmutable());
if (status) {
doThrowRE(env, "Could not copy bitmap to parcel blob.");
return JNI_FALSE;
}
return JNI_TRUE;
}
Bitmap_writeToParcel() 就是找一个地方,开辟缓冲区,然后将数据存储到缓冲区
- writeBlob() 是如何开辟缓冲区的
static binder_status_t writeBlob(AParcel* parcel, const int32_t size, const void* data, bool immutable) {
if (size <= 0 || data == nullptr) {
return STATUS_NOT_ENOUGH_DATA;
}
binder_status_t error = STATUS_OK;
// shouldUseAshmem 会判断Parcel 中的 AllowFds 是否允许 并且判断大小,如果不允许传递描述符或者文件大小< 12 * 1024 也就是 12k shouldUseAshmem() 返回 false,走 else
if (shouldUseAshmem(parcel, size)) {
// 如果满足条件通过 ashmem 创建一块共享内存
base::unique_fd fd(ashmem_create_region("bitmap", size));
if (fd.get() < 0) {
return STATUS_NO_MEMORY;
}
{
// 然后通过 mmap 开辟共享内存的空间
void* dest = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd.get(), 0);
if (dest == MAP_FAILED) {
return STATUS_NO_MEMORY;
}
// 将数据拷贝到共享内存中
memcpy(dest, data, size);
munmap(dest, size);
}
if (immutable && ashmem_set_prot_region(fd.get(), PROT_READ) < 0) {
return STATUS_UNKNOWN_ERROR;
}
// 将文件描述符写到 parcel 传输过去
int rawFd = fd.release();
error = writeBlobFromFd(parcel, size, rawFd);
close(rawFd);
return error;
} else {
// 这个判断了一下最大值以防 AllowFds 为 false 但是文件还 > 1 * 1024 * 1024; 1M 就报错
if (size > BLOB_MAX_INPLACE_LIMIT) {
return STATUS_FAILED_TRANSACTION;
}
// 把数据直接写入 parcel 中传递
ON_ERROR_RETURN(AParcel_writeInt32(parcel, static_cast<int32_t>(BlobType::IN_PLACE)));
ON_ERROR_RETURN(AParcel_writeByteArray(parcel, static_cast<const int8_t*>(data), size));
return STATUS_OK;
}
}
static constexpr bool shouldUseAshmem(AParcel* parcel, int32_t size) {
return size > BLOB_INPLACE_LIMIT && AParcel_getAllowFds(parcel);
}
writeBlob() 函数总结就是 判断文件大小是否< 12k 并且允许传输文件描述符,则直接用 Parcel 传输 bitmap 文件,否则,通过 ashmen 和 mmap 开辟一块匿名共享内存,然后将数据拷贝到共享内存,保存共享内存的文件描述符。、
这样就把 bitmap 传输到另外一个进程了。
为什么 intent 带大图异常,但是 binder 传输就没有问题呢?
上面讲过 writeToParcel() 的时候,如果传入的 allowFds 是false的话,Bundle 的allowFds也为false,上一段代码有个判断,如果 allowFds = false的话,会把数据写到 Parcel 中,但是写入 parcel 中的时候会限制大小。如果 allowFds = true 的时候,会为了图片开辟一块共享内存空间,所以不会报错。
启动 Activity 时 frameworks/base/core/java/android/app/Instrumentation.java
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
intent.prepareToLeaveProcess(who);
int result = ActivityTaskManager.getService().startActivity(whoThread,
who.getOpPackageName(), who.getAttributionTag(), intent,
intent.resolveTypeIfNeeded(who.getContentResolver()), token,
target != null ? target.mEmbeddedID : null, requestCode, 0, null, options);
}
intent.prepareToLeaveProcess(who); 最终会调用到 setAllowFds() 所以 intent 传递的话不允许传递大图
public void prepareToLeaveProcess(boolean leavingPackage) {
setAllowFds(false);
}
发送广播也是一样的机制,所以原因就是如此了。跨进程传输大数据,也可以考虑使用 ContentProvider 和 MemoryFile 底层都是使用的共享内存的原理。