面試官提了一個問題,咱們來看看 😎、😨 和 🤔️ 三位同窗的表現如何吧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 , 能夠利用共享內存,因此能高效傳輸圖片。
面試官:能夠,咱們再來聊聊別的。