從Zygote孵化frameworks進程,分析StartActivity流程中intent傳遞數據的最大值。

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

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

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

但我們平時傳遞少量數據的時候是沒問題的。由此得知,通過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介紹

普通的由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文件存在着嚴格的權限檢查。

二、通過設置靜態類中的靜態變量進行數據交換。一般適用於同一進程內,這樣本質上數據在內存中只存在一份,通過靜態類進行傳遞。需要注意的是進行數據校對,以防多線程Data Racer出現導致的數據顯示混亂。

參考資料

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

https://www.jianshu.com/p/adaa1a39a274

源碼分析:startActivity流程

https://www.jianshu.com/p/dc6b0ead30aa


文中提到一個重點類

這個限制定義在frameworks/native/libs/binder/processState.cpp類中

很多開發者可能不知道去哪裏看這個類,這裏跟大家分享一個非常快速便捷的查看方式,比較適合偶爾查找一兩個類:

直接進入搜索就好了,包含各個版本的源碼:

如果你指定了某個具體的版本,還有非常便利的提示:


生活中信息交流的核心是數據,Android提供了多種數據存儲的方式,文件存儲數據,SharedPreferences存儲數據,SQLite數據庫存儲數據,ContentProvider存儲數據,根據不同的數據文件選擇合適的存儲方式是開發者所具備的基本技能之一。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章