跨進程傳遞大圖,你能想到哪些方案呢?

面試官提了一個問題,咱們來看看 😎、😨 和 🤔️ 三位同窗的表現如何吧java


😎 自認爲無所不知,水平已達應用開發天花板,目前月薪 10kandroid

面試官:如何跨進程傳遞大圖c++

😎:很簡單,把圖片存到 SD 卡,而後把路徑傳過去,在別的進程讀出來這不就完事了嘛。面試

面試官:這個須要文件操做,效率不行,有別的方法嗎?緩存

😎:Bitmap 實現了 Parcelable 接口,能夠經過 Intent.putExtra(String name, Parcelable value) 方法直接放 Intent 裏面傳遞。markdown

面試官:那這個方法有什麼缺點?app

😎:Bitmap 太大就會拋異常,因此我更喜歡傳路徑ide

面試官:爲何會拋異常?學習

😎:.....spa

面試官:好的,回去等通知吧


😨 業餘時間常常打遊戲、追劇、熬夜,目前月薪 15k

面試官:Intent 直接傳 Bitmap 會有什麼問題?

😨:Bitmap 太大會拋 TransactionTooLargeException 異常,緣由是:底層判斷只要 Binder Transaction 失敗,且 Intent 的數據大於 200k 就會拋這個異常了。(見:android_util_Binder.cpp 文件 signalExceptionForError 方法)

面試官:爲何 Intent 傳值會有大小限制。

😨:應用進程在啓動 Binder 機制時會映射一塊 1M 大小的內存,全部正在進行的 Binder 事務共享這 1M 的緩衝區 。當使用 Intent 進行 IPC 時申請的緩存超過 1M - 其餘事務佔用的內存時,就會申請失敗拋 TransactionTooLargeException 異常了。 (哼,不會像上次同樣答不出來了。見:「談談你對 binder 的理解?這樣回答才過關」)

面試官:如何繞開這個限制呢?

😨:經過 AIDL 使用 Binder 進行 IPC 就不受這個限制,具體代碼以下:

Bundle bundle = new Bundle();
bundle.putBinder("binder", new IRemoteGetBitmap.Stub() {
    @Override
    public Bitmap getBitMap() throws RemoteException {
        return mBitmap;
    }
});
intent.putExtras(bundle);
複製代碼

面試官:這是什麼原理呢?

😨:還沒去細看

面試官:好的,回去等通知吧


🤔️ 堅持天天學習、不斷的提高本身,目前月薪 30k

面試官:爲何經過 putBinder 的方式傳 Bitmap 不會拋 TransactionTooLargeException 異常

🤔️:這個問題,咱們先來看下,底層在 IPC 時是怎麼把 Bitmap 寫進 Parcel 的。

Android - 28 Bitmap.cpp static jboolean Bitmap_writeToParcel(JNIEnv* env, jobject, ...) {
    // 拿到 Native 的 Bitmap 
    auto bitmapWrapper = reinterpret_cast<BitmapWrapper*>(bitmapHandle);
    // 拿到其對應的 SkBitmap, 用於獲取 Bitmap 的像素信息
    bitmapWrapper->getSkBitmap(&bitmap);

    int fd = bitmapWrapper->bitmap().getAshmemFd();
    if (fd >= 0 && !isMutable && p->allowFds()) {
   	 		// Bitmap 帶了 ashmemFd && Bitmap 不可修改 && Parcel 容許帶 fd
    		// 就直接把 FD 寫到 Parcel 裏,結束。
        status = p->writeDupImmutableBlobFileDescriptor(fd);
        return JNI_TRUE;
    }

    // 不知足上面的條件就要把 Bitmap 拷貝到一塊新的緩衝區
    android::Parcel::WritableBlob blob;
  	// 經過 writeBlob 拿到一塊緩衝區 blob
    status = p->writeBlob(size, mutableCopy, &blob);

    // 獲取像素信息並寫到緩衝區
    const void* pSrc =  bitmap.getPixels();
    if (pSrc == NULL) {
        memset(blob.data(), 0, size);
    } else {
        memcpy(blob.data(), pSrc, size);
    }
}
複製代碼

接下來咱們看一下 writeBlob 是怎麼獲取緩衝區的(注意雖然方法名寫着 write , 可是實際往緩衝區寫數據是在這個方法執行以後)

Android - 28 Parcel.cpp
// Maximum size of a blob to transfer in-place.
static const size_t BLOB_INPLACE_LIMIT = 16 * 1024;

status_t Parcel::writeBlob(size_t len, bool mutableCopy, WritableBlob* outBlob)
{
    if (!mAllowFds || len <= BLOB_INPLACE_LIMIT) {
    // 若是不容許帶 fd ,或者這個數據小於 16K
    // 就直接在 Parcel 的緩衝區裏分配一塊空間來保存這個數據
        status = writeInt32(BLOB_INPLACE);
        void* ptr = writeInplace(len);
        outBlob->init(-1, ptr, len, false);
        return NO_ERROR;
    }

		// 另外開闢一個 ashmem,映射出一塊內存,後續數據將保存在 ashmem 的內存裏
    int fd = ashmem_create_region("Parcel Blob", len);
    void* ptr = ::mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    ...
  	// parcel 裏只寫個 fd 就行了,這樣就算數據量很大,parcel 本身的緩衝區也不用很大
    status = writeFileDescriptor(fd, true /*takeOwnership*/);
 		outBlob->init(fd, ptr, len, mutableCopy);
    return status;
}
複製代碼

經過上面的分析,咱們能夠看出,同一個 Bitmap 寫入到 Parcel 所佔的緩衝區大小和 Pacel 的 allowFds 有關。

直接經過 Intent 傳 Bitmap 容易拋 TransactionTooLargeException 異常,就是由於 Parcel 的 allowFds = false,直接把 Bitmap 寫入緩衝區佔用了較大的內存。

接下來,咱們來看一下,allowFds 是何時被設置成 false 的呢:

// 啓動 Activity 執行到 Instrumentation.java 的這個方法
public ActivityResult execStartActivity(..., Intent intent, ...){
  ...
  intent.prepareToLeaveProcess(who);
	ActivityManager.getService().startActivity(...,intent,...)
}

// Intent.java
public void prepareToLeaveProcess(boolean leavingPackage) {
 // 這邊一層層傳遞到最後設置 Parcel 的 allowfds
  setAllowFds(false);
  ....
}
複製代碼

面試官:太多了,你懂的。

🤔️:總結一下:較大的 bitmap 直接經過 Intent 傳遞容易拋異常是由於 Intent 啓動組件時,系統禁掉了文件描述符 fd 機制 , bitmap 沒法利用共享內存,只能拷貝到 Binder 映射的緩衝區,致使緩衝區超限, 觸發異常; 而經過 putBinder 的方式,避免了 Intent 禁用描述符的影響,bitmap 寫 parcel 時的 allowFds 默認是 true , 能夠利用共享內存,因此能高效傳輸圖片。

面試官:能夠,咱們再來聊聊別的。


掃一掃關注我,爲你解答 Android 面試的各類問題