Android 使用 intent 传递数据时的大小限制

2 篇文章 0 订阅
2 篇文章 0 订阅

Intent 究竟能传递多大的数据?

1. Intent 传递不同大小数据时的问题

2. 问题分析

2.1 intent 传递数据测试

2.2 从 crash log 角度分析

2.3 startActivity 流程探究

3. Binder 传输数据的 大小限制

4. intent 传递数据大小限制问题的小结

4.1 一句话概括这个问题

4.2 分析

5. 为什么 Binder 要限制传输数据的大小

6. 从 Intent 传递数据的原理分析

7. 解决方法

8. 参考链接


我们知道可以通过 Intent 和 bundle 在 activity 或 fragment 间进行通信,在sendBroadcast,我们也会用到Intent。但是 Intent 传递数据时,如果数据太大,可能会出现异常。

1. Intent 传递不同大小数据时的问题

  • 传 512K 以下的数据的数据可以正常传递。
  • 传 512K~1024K 的数据会出错,闪退。
  • 传 1024K 以上的数据会报错:TransactionTooLargeException
  • 考虑到 Intent 还包括要启动的 Activity 等信息,实际可以传的数据略小于 512K

2. 问题分析

2.1 intent 传递数据测试
val intent = Intent(this, MainActivity2::class.java).apply {
    val data2 = ByteArray(1024 * 1024)
    putExtra("111", data2)
}
startActivity(intent)

error:

Caused by: android.os.TransactionTooLargeException: data parcel size 1049012 bytes
   at android.os.BinderProxy.transactNative(Native Method)
   at android.os.BinderProxy.transact(BinderProxy.java:511)
   at android.app.IActivityTaskManager$Stub$Proxy.startActivity(IActivityTaskManager.java:3969)
   at android.app.Instrumentation.execStartActivity(Instrumentation.java:1716)
   at android.app.Activity.startActivityForResult(Activity.java:5260) 
   at androidx.fragment.app.FragmentActivity.startActivityForResult(FragmentActivity.java:675) 
   at android.app.Activity.startActivityForResult(Activity.java:5218) 
   at androidx.fragment.app.FragmentActivity.startActivityForResult(FragmentActivity.java:662) 
   at android.app.Activity.startActivity(Activity.java:5589) 
   at android.app.Activity.startActivity(Activity.java:5557) 
2.2 从 crash log 角度分析

Intent 中传入一个 Parcelable 对象, 例如传入一个 bitmap 对象。Bitmap 实现了 Parcelable 接口,并且可以通过 getByteCount() 得知所占内存大小。测试时候报出如下信息

 V/ActivityManager: Broadcast: Intent { act=intent_bi flg=0x10 (has extras) } ordered=false userid=0 callerApp=ProcessRecord{27aeaaf5 31217:com.rustfisher.basic4/u0a113}
 E/JavaBinder: !!! FAILED BINDER TRANSACTION !!!
 W/BroadcastQueue: Failure sending broadcast Intent { act=intent_bi flg=0x10 (has extras) }
        android.os.TransactionTooLargeException
            at android.os.BinderProxy.transactNative(Native Method)
            at android.os.BinderProxy.transact(Binder.java:504)
            at android.app.ApplicationThreadProxy.scheduleRegisteredReceiver(ApplicationThreadNative.java:1170)
            at com.android.server.am.BroadcastQueue.performReceiveLocked(BroadcastQueue.java:576)
            at com.android.server.am.BroadcastQueue.deliverToRegisteredReceiverLocked(BroadcastQueue.java:848)
            at com.android.server.am.BroadcastQueue.processNextBroadcast(BroadcastQueue.java:917)
            at com.android.server.am.BroadcastQueue$BroadcastHandler.handleMessage(BroadcastQueue.java:254)
            at android.os.Handler.dispatchMessage(Handler.java:111)
            at android.os.Looper.loop(Looper.java:194)
            at android.os.HandlerThread.run(HandlerThread.java:61)
            at com.android.server.ServiceThread.run(ServiceThread.java:46)

查看异常类 TransactionTooLargeException,它继承了 RemoteExceptio:

package android.os;
public class TransactionTooLargeException extends RemoteException {
    public TransactionTooLargeException() {
        super();
    }

    public TransactionTooLargeException(String msg) {
        super(msg);
    }
}

追踪到 Binder,它的 transactNative 方法会报出 RemoteException:

public native boolean transactNative(int code, Parcel data, Parcel reply,
            int flags) throws RemoteException;

由此可以看出,抛出异常与 Binder 有关。

2.3 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) {
        if (mParent == null) {
            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);
            // TODO Consider clearing/flushing other event sources and events for child windows.
        } else {
            if (options != null) {
                mParent.startActivityFromChild(this, intent, requestCode, options);
            } else {
                // Note we want to go through this method for compatibility with
                // existing applications that may have overridden it.
                mParent.startActivityFromChild(this, intent, requestCode);
            }
        }
    }

然后系统会调用 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(who);
            intent.prepareToLeaveProcess(who);
            int result = ActivityTaskManager.getService().startActivity(whoThread,
                    who.getBasePackageName(), who.getAttributionTag(), 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 IActivityTaskManager getService() {
    return IActivityTaskManagerSingleton.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 方法是原生支持跨进程通信的。

3. Binder 传输数据的 大小限制

普通的由 Zygote 孵化而来的用户进程,所映射的Binder内存大小是不到1M的,准确说是

#define BINDER_VM_SIZE ((1*1024*1024) - (4096 *2))

这个限制定义在 frameworks/native/+/4d2f4bb/libs/binder/ProcessState.cpp类中,如果传输数据超过这个大小,系统就会报错,因为Binder本身就是为了进程间频繁而灵活的通信所设计的,并不是为了拷贝大数据而使用的.

其实在 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.

4. intent 传递数据大小限制问题的小结

4.1 一句话概括这个问题

startActivity携带的数据会经过 Binder内核再传递到目标Activity中去,因为binder映射内存的限制,所以startActivity也就会这个限制了。

4.2 分析

Intent 携带信息的大小其实是受 Binder 限制。本文标题也可以改为 Binder传递数据大小限制。

数据以 Parcel 对象的形式存放在 Binder传递缓存中。如果数据或返回值比传递 buffer 大,则此次传递调用失败并抛出 TransactionTooLargeException 异常。

Binder 传递缓存有一个限定大小,通常是 1Mb

但同一个进程中所有的传输共享缓存空间。多个地方在进行传输时,即时它们各自传输的数据不超出大小限制,TransactionTooLargeException异常也可能会被抛出

在使用 Intent 传递数据时,1Mb 并不是安全上限因为 Binder 中可能正在处理其它的传输工作
不同的机型和系统版本,这个上限值也可能会不同

在其它地方,例如 onSaveInstanceState(@NonNull Bundle outState),也可能会遇到与 Binder 有关的类似问题。

5. 为什么 Binder 要限制传输数据的大小

个人推测,作为一种 IPC 的方式Binder 并不是为传输大量数据而设计传输大量数据,可以考虑URL 之类的方法( 只传递索引,不传递具体的数据。或者其他类似的做法)

6. 从 Intent 传递数据的原理分析

通过 intent bundle 的源码可以看到它们都是实现了 Parcelable ,其实就是通过序列化来实现通信的。提到 Parcelable 就不得不提 Serializable,这里引用一段网上的总结:

介绍Parcelable不得不先提一下Serializable接口,Serializable是Java为我们提供的一个标准化的序列化接口,那什么是序列化呢? —- 简单来说就是将对象转换为可以传输的二进制流(二进制序列)的过程,这样我们就可以通过序列化,转化为可以在网络传输或者保存到本地的流(序列),从而进行传输数据 ,那反序列化就是从二进制流(序列)转化为对象的过程.

Parcelable 是Android为我们提供的序列化的接口, Parcelable 相对于 Serializable 的使用相对复杂一些,但 Parcelable 的效率相对 Serializable 也高很多,这一直是 Google工程师引以为傲的,有时间的可以看一下 Parcelable 和 Serializable 的效率对比 Parcelable vs Serializable 号称快10倍的效率

Yet another post on Serializable vs Parcelable

Parcelable 的底层使用了 Parcel 机制。传递实际上是使用了 binder 机制,binder 机制会将 Parcel序列化的数据写入到一个共享内存中,读取时也是 binder 从共享内存中读出字节流,然后 Parcel 反序列化后使用。这就是 Intent Bundle 能够在 activity或者跨进程通信的原理。

关于 Parcelable Serializable 的区别,同样引用一段网上的总结:

Parcelable Serializable 都是实现序列化并且都可以用于 Intent 间传递数据, Serializable Java 的实现方式,可能会频繁的 IO 操作,所以消耗比较大,但是实现方式简单。Parcelable Android 提供的方式, 效率比较高,但是实现起来复杂一些 , 二者的选取规则是:

  • 内存序列化上选择Parcelable
  • 存储到设备或者网络传输上选择Serializable(当然Parcelable也可以但是稍显复杂)

当我们用Intent传输过大数据时,一般logcat会报出 TransactionTooLargeException 错误,谷歌官方对这个错误的描述如下:

The Binder transaction failed because it was too large.
During a remote procedure call, the arguments and the return value of the call are transferred as
Parcel objects stored in the Binder transaction buffer. If the arguments or the return value are too
large to fit in the transaction buffer, then the call will fail and TransactionTooLargeException
will be thrown.

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.

There are two possible outcomes when a remote procedure call throws TransactionTooLargeException.
Either the client was unable to send its request to the service (most likely if the arguments were
too large to fit in the transaction buffer), or the service was unable to send its response back to
the client (most likely if the return value was too large to fit in the transaction buffer). It is
not possible to tell which of these outcomes actually occurred. The client should assume that a
partial failure occurred.

The key to avoiding TransactionTooLargeException is to keep all transactions relatively small.
Try to minimize the amount of memory needed to create a Parcel for the arguments and the return
value of the remote procedure call. Avoid transferring huge arrays of strings or large bitmaps.
If possible, try to break up big requests into smaller pieces.

If you are implementing a service, it may help to impose size or complexity contraints on the
queries that clients can perform. For example, if the result set could become large, then don't
allow the client to request more than a few records at a time. Alternately, instead of returning
all of the available data all at once, return the essential information first and make the client
ask for additional information later as needed.

简单来说,我们上面提到了 Parcel 机制使用了一个共享内存,这个共享内存就叫 Binder transaction buffer,这块内存有一个大小限制,目前是 1MB,而且共用的,当超过了这个大小就会报错。

也就是说不仅仅是一次性传递大数据会出问题,当同时传递很多数据,尽管每个都不超过1MB,但是总大小超过 1MB也会出错。

7. 解决方法

Activity之间传递数据的方式及常见问题总结

  • 限制传递数据量
  • 改变数据传输方式(参见Activity之间传递数据的方式)
  • 静态static
  • 单例
  • Application
  • 持久化
  • 使用容器的单例模式

8. 参考链接



————————————————
原文链接:https://blog.csdn.net/u011033906/article/details/89316543

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Android Intent是一种用于在应用程序组件之间传递数据的机制。通过Intent,您可以将数据从一个Activity传递到另一个Activity,或者从一个Activity传递到Service或BroadcastReceiverIntent可以携带不同类型的数据,例如字符串、整数、布尔值、数组等。您可以使用putExtra()方法将数据添加到Intent中,并使用getExtra()方法从Intent中获取数据。Intent还可以用于启动其他应用程序中的Activity或Service。 ### 回答2: Android中,Intent是一种非常常见的机制,用于在应用程序中传递数据Intent可以将数据从一个Activity传递到另一个Activity,也可以被广播到整个系统。在本文中,我们将讨论Android中的Intent传递数据Android中的Intent有两种类型:显式Intent和隐式Intent。显式Intent用来启动一个特定的组件(Activity、Service或Receiver),而隐式Intent会匹配最佳的组件来处理传递数据的请求。为了传递数据,我们需要将数据添加到Intent中,并通过putExtra()方法传递。 我们可以在Intent中附加多种数据类型,例如字符串、布尔值、整数、浮点数、对象等。您传递的数据类型取决于您的业务需求。以字符串为例,Intent可以通过传递键值对的方式在Activity之间传递字符串数据。下面是一个示例: ```java Intent intent = new Intent(MainActivity.this, SecondActivity.class); intent.putExtra("name", "Mike"); startActivity(intent); ``` 上面的代码中,我们创建了一个名为intentIntent对象,将当前Activity的上下文和目标Activity的类作为参数传递给Intent构造函数。我们还通过putExtra()方法将一个名为“name”的字符串键值对附加到intent中。 在第二个Activity中,我们使用getXXXExtra()方法从Intent中获取数据。在这个例子中,我们使用getStringExtra()方法来检索名为“name”的字符串: ```java String name = getIntent().getStringExtra("name"); ... ``` 通过这种方式,我们可以将数据从一个Activity传递到另一个Activity,为后续操作提供方便。 在Android开发中,Intent传递数据是一项非常重要的工作。学会如何使用Intents和传递数据,将大大提高您的应用程序的交互性和实用性。 ### 回答3: Android中,我们可以使用intent传递数据,也就是将数据从一个Activity传递到另一个Activity。intent是一种可以在应用内部或应用之间交换数据的机制,它可以在两个不同的组件间传递数据,并且可以携带各种类型的数据,如字符串、整型、布尔型、对象等等。 首先,我们需要定义一个intent对象,然后使用它来传递数据。通过调用putExtra()方法,我们可以将要传递的数据放入intent中。这些数据可以是基本数据类型,也可以是自定义对象。例如,我们可以将一个字符串传递给另一个Activity,如下所示: ``` Intent intent = new Intent(this, AnotherActivity.class); String data = "Hello, Another Activity!"; intent.putExtra("key", data); startActivity(intent); ``` 在接收Activity中,我们需要使用getIntent()方法获取发送intent的数据。我们可以使用getStringExtra()方法来获取在intent中传递的字符串数据,如下所示: ``` Intent intent = getIntent(); String data = intent.getStringExtra("key"); ``` 我们还可以使用类似的方法来获取其他类型的数据,如getIntExtra()、getBooleanExtra()、getParcelableExtra()等等,取决于在intent中放置了什么类型的数据。 还有一种常用的情况是,我们需要在两个Activity之间传递对象。在这种情况下,需要确保对象是可序列化的。可以通过实现Serializable接口或Parcelable接口来实现对象的序列化。一旦对象被序列化,可以通过putExtra()方法将它放入intent中,然后在接收Activity中使用getSerializableExtra()或getParcelableExtra()方法来获取对象。 综上所述,intentAndroid中非常强大的一个概念,它使得不同组件之间可以传递各种数据类型,为Android应用程序的开发提供了很多便利。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值