作者:大頭獃
連結:https://www.jianshu.com/p/4537270be897
當我們用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 IActivityManagerSingleton =
new Singleton() {
@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類中
很多同學可能不知道去哪裡看這個類,這裡跟大家分享一個非常快速便捷的檢視方式,比較適合偶爾查詢一兩個類:
直接進入搜尋就好了,包含各個版本的原始碼:
如果你指定了某個具體的版本,還有非常便利的提示: