探究intent傳遞大小限制

大頭呆發表於2019-03-03

前言

當我們用Intent傳輸大資料時,有可能會出現錯誤:

        val intent = Intent(this@MainActivity, Main2Activity::class.java)
        val data = ByteArray(1024 * 1024)
        intent.putExtra("111", data)
        startActivity(intent)
複製程式碼

如上我們傳遞了1M大小的資料時,結果程式就一直反覆報如下TransactionTooLargeException錯誤:

探究intent傳遞大小限制

但我們平時傳遞少量資料的時候是沒問題的。由此得知,通過intent在頁面間傳遞資料是有大小限制的。本文我們就來分析下為什麼頁面資料傳輸會有這個量的限制以及這個限制的大小具體是多少。

startActivity流程探究

首先我們知道Context和Activity都含有startActivity,但兩者最終都呼叫了Activity中的startActivity:

      @Override
    public void startActivity(Intent intent, @Nullable Bundle options) {
        if (options != null) {
            startActivityForResult(intent, -1, options);
        } else {
            // Note we want to go through this call for compatibility with
            // applications that may have overridden the method.
            startActivityForResult(intent, -1);
        }
    }
複製程式碼

而startActivity最終會呼叫自身的startActivityForResult,省略了巢狀activity的程式碼:

    public void startActivityForResult(@RequiresPermission Intent intent, int requestCode,
            @Nullable Bundle options) {
      options = transferSpringboardActivityOptions(options);
            Instrumentation.ActivityResult ar =
                mInstrumentation.execStartActivity(
                    this, mMainThread.getApplicationThread(), mToken, this,
                    intent, requestCode, options);
            if (ar != null) {
                mMainThread.sendActivityResult(
                    mToken, mEmbeddedID, requestCode, ar.getResultCode(),
                    ar.getResultData());
            }
            if (requestCode >= 0) {
                // If this start is requesting a result, we can avoid making
                // the activity visible until the result is received.  Setting
                // this code during onCreate(Bundle savedInstanceState) or onResume() will keep the
                // activity hidden during this time, to avoid flickering.
                // This can only be done when a result is requested because
                // that guarantees we will get information back when the
                // activity is finished, no matter what happens to it.
                mStartedActivity = true;
            }

            cancelInputsAndStartExitTransition(options);
    }
複製程式碼

然後系統會呼叫Instrumentation中的execStartActivity方法:

   public ActivityResult execStartActivity(
            Context who, IBinder contextThread, IBinder token, Activity target,
            Intent intent, int requestCode, Bundle options) {
        IApplicationThread whoThread = (IApplicationThread) contextThread;
       ...
        try {
            intent.migrateExtraStreamToClipData();
            intent.prepareToLeaveProcess(who);
            int result = ActivityManager.getService()
                .startActivity(whoThread, who.getBasePackageName(), intent,
                        intent.resolveTypeIfNeeded(who.getContentResolver()),
                        token, target != null ? target.mEmbeddedID : null,
                        requestCode, 0, null, options);
            checkStartActivityResult(result, intent);
        } catch (RemoteException e) {
            throw new RuntimeException("Failure from system", e);
        }
        return null;
    }
複製程式碼

接著呼叫了ActivityManger.getService().startActivity ,getService返回的是系統程式中的AMS在app程式中的binder代理:

	/**
     * @hide
     */
    public static IActivityManager getService() {
        return IActivityManagerSingleton.get();
    }

    private static final Singleton<IActivityManager> IActivityManagerSingleton =
            new Singleton<IActivityManager>() {
                @Override
                protected IActivityManager create() {
                    final IBinder b = ServiceManager.getService(Context.ACTIVITY_SERVICE);
                    final IActivityManager am = IActivityManager.Stub.asInterface(b);
                    return am;
                }
            };
複製程式碼

接下來就是App程式呼叫AMS程式中的方法了。簡單來說,系統程式中的AMS集中負責管理所有程式中的Activity。app程式與系統程式需要進行雙向通訊。比如開啟一個新的Activity,則需要呼叫系統程式AMS中的方法進行實現,AMS等實現完畢需要回撥app程式中的相關方法進行具體activity生命週期的回撥。

所以我們在intent中攜帶的資料也要從APP程式傳輸到AMS程式,再由AMS程式傳輸到目標Activity所在程式。有同學可能由疑問了,目標Acitivity所在程式不就是APP程式嗎?其實不是的,我們可以在Manifest.xml中設定android:process屬性來為Activity, Service等指定單獨的程式,所以Activity的startActivity方法是原生支援跨程式通訊的。

接下來簡單分析下binder機制。

binder資料傳輸

img

普通的由Zygote孵化而來的使用者程式,所對映的Binder記憶體大小是不到1M的,準確說是 110241024) - (4096 *2) :這個限制定義在frameworks/native/libs/binder/processState.cpp類中,如果傳輸說句超過這個大小,系統就會報錯,因為Binder本身就是為了程式間頻繁而靈活的通訊所設計的,並不是為了拷貝大資料而使用的:

#define BINDER_VM_SIZE ((1*1024*1024) - (4096 *2))
複製程式碼

並可以通過cat proc/[pid]/maps命令檢視到。

而在核心中,其實也有個限制,是4M,不過由於APP中已經限制了不到1M,這裡的限制似乎也沒多大用途:

static int binder_mmap(struct file *filp, struct vm_area_struct *vma)
{
    int ret;
    struct vm_struct *area;
    struct binder_proc *proc = filp->private_data;
    const char *failure_string;
    struct binder_buffer *buffer;
    //限制不能超過4M
    if ((vma->vm_end - vma->vm_start) > SZ_4M)
        vma->vm_end = vma->vm_start + SZ_4M;
    。。。
    }
複製程式碼

其實在TransactionTooLargeException中也提到了這個:

The Binder transaction buffer has a limited fixed size, currently 1Mb, which
is shared by all transactions in progress for the process.  Consequently this
exception can be thrown when there are many transactions in progress even when
most of the individual transactions are of moderate size.
複製程式碼

只不過不是正好1MB,而是比1MB略小的值。

小結

至此我們來解答開頭提出的問題,startActivity攜帶的資料會經過BInder核心再傳遞到目標Activity中去,因為binder對映記憶體的限制,所以startActivity也就會這個限制了。

替代方案

一、寫入臨時檔案或者資料庫,通過FileProvider將該檔案或者資料庫通過Uri傳送至目標。一般適用於不同程式,比如分離程式的UI和後臺服務,或不同的App之間。之所以採用FileProvider是因為7.0以後,對分享本App檔案存在著嚴格的許可權檢查。

二、通過設定靜態類中的靜態變數進行資料交換。一般適用於同一程式內,這樣本質上資料在記憶體中只存在一份,通過靜態類進行傳遞。需要注意的是進行資料校對,以防多執行緒操作導致的資料顯示混亂。

參考資料

聽說你Binder機制學的不錯,來面試下這幾個問題(一)

原始碼分析:startActivity流程

相關文章