前言
当我们用 Intent 传输大数据时, 有可能会出现错误:
val intent = Intent(this@MainActivity, Main2Activity::class.java)val data = ByteArray(1024 * 1024)intent.putExtra("111", data)startActivity(intent)
如上我们传递了 1M 大小的数据时, 结果程序就一直反复报如下 TransactionTooLargeException 错误:
image.PNG
但我们平时传递少量数据的时候是没问题的. 由此得知, 通过 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 数据传输
image.PNG
普通的由 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, whichis shared by all transactions in progress for the process. Consequently thisexception can be thrown when there are many transactions in progress even whenmost of the individual transactions are of moderate size.
只不过不是正好 1MB, 而是比 1MB 略小的值.
小结
至此我们来解答开头提出的问题, startActivity 携带的数据会经过 BInder 内核再传递到目标 Activity 中去, 因为 binder 映射内存的限制, 所以 startActivity 也就会这个限制了.
替代方案
一, 写入临时文件或者数据库, 通过 FileProvider 将该文件或者数据库通过 Uri 发送至目标. 一般适用于不同进程, 比如分离进程的 UI 和后台服务, 或不同的 App 之间. 之所以采用 FileProvider 是因为 7.0 以后, 对分享本 App 文件存在着严格的权限检查.
二, 通过设置静态类中的静态变量进行数据交换. 一般适用于同一进程内, 这样本质上数据在内存中只存在一份, 通过静态类进行传递. 需要注意的是进行数据校对, 以防多线程操作导致的数据显示混乱.
[附] 相关架构视频资料
image.PNG
image
资料领取
关注 + 点赞 + 加群: 185873940 免费获取!
点击链接加入群聊[Android IoC 架构设计] :https://jq.qq.com/?_wv=1027&k=5tIZkaU
领取获取往期 Android 高级架构资料, 源码, 笔记, 视频. 高级 UI, 性能优化, 架构师课程, NDK, 混合式开发 (ReactNative+Weex) 微信小程序, Flutter 全方面的 Android 进阶实践技术
来源: http://www.jianshu.com/p/c0eed3b2a473