android 怎样跨进程传递大图?Intent 也是采用 binder传递数据,为什么会被限制大小呢?直接使用 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 通信以后,那么返回以后是在哪里接收的呢?这个在之前的文章中 android一次完整的 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 开辟一块匿名共享内存,然后将数据拷贝到共享内存,保存共享内存的文件描述符。匿名共享内存之前了解过有兴趣的可以了解一下。匿名共享内存 ashmem跨进程通信–共享内存(ashmem)实例

这样就把 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 底层都是使用的共享内存的原理。

猜你喜欢

转载自blog.csdn.net/ldxlz224/article/details/129050257
今日推荐