由TransactionTooLargeException引发的一些思考

引子

我所在部门的Android app维护时间比较久,一直保持在targetSdkVersion 22。最近oppo、vivo几家应用商店停止targetSdkVersion低于26的app更新,迫不得己,同事升级了targetSdkVersion到26,主要进行了权限处理、FileProvider处理等。新版应用发布后,却出现了一些TransactionTooLargeException崩溃,主要集中在android 8.x和9.x。
我之前处理过一次TransactionTooLargeException的异常。那个异常出现的场景在页面启动时:点击列表单元去开启详情列表页,需要将列表中的数据都传递到详情列表页中,如果通过Intent方式传递且数据量过大,有可能出现TransactionTooLargeException的问题。解决方法是改为静态变量方式传递。由于有可能存在多个相同类型的页面,所以需要存储到静态Map中;每个页面保持一个唯一的key,在页面onDestroy()时,判断是否会重启(isFinish()为true),如果不会重启,则从静态map中将页面的列表数据清除,防止内存泄漏。
但这次的异常都集中在页面onStop()时。

崩溃信息

一次崩溃的机型信息:
系统版本: Android 9,level 28
ROM : HuaWei/EMOTION
CPU架构 : arm64-v8a
堆栈信息:

# main(1)
android.os.TransactionTooLargeException
data parcel size 535580 bytes

解析原始
1 java.lang.RuntimeException:android.os.TransactionTooLargeException: data parcel size 535580 bytes
2 android.app.servertransaction.PendingTransactionActions$StopInfo.run(PendingTransactionActions.java:160)
3 ......
4 Caused by:
5 android.os.TransactionTooLargeException:data parcel size 535580 bytes
6 android.os.BinderProxy.transactNative(Native Method)
7 android.os.BinderProxy.transact(Binder.java:1147)
8 android.app.IActivityManager$Stub$Proxy.activityStopped(IActivityManager.java:4005)
9 android.app.servertransaction.PendingTransactionActions$StopInfo.run(PendingTransactionActions.java:144)
10 android.os.Handler.handleCallback(Handler.java:891)
11 android.os.Handler.dispatchMessage(Handler.java:102)
12 android.os.Looper.loop(Looper.java:207)
13 android.app.ActivityThread.main(ActivityThread.java:7470)
14 java.lang.reflect.Method.invoke(Native Method)
15 com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:524)
16 com.android.internal.os.ZygoteInit.main(ZygoteInit.java:958)

从堆栈第9行可以看到崩溃点在PendingTransactionActions类的内部类StopInfo类的run()方法中,看下api 28的代码:

    /** Reports to server about activity stop. */
    public static class StopInfo implements Runnable {
        @Override
        public void run() {
            // Tell activity manager we have been stopped.
            try {
                // TODO(lifecycler): Use interface callback instead of AMS.
                ActivityManager.getService().activityStopped(
                        mActivity.token, mState, mPersistentState, mDescription);
            } catch (RemoteException ex) {
                if (ex instanceof TransactionTooLargeException
                        && mActivity.packageInfo.getTargetSdkVersion() < Build.VERSION_CODES.N) { // api 24
                    Log.e(TAG, "App sent too much data in instance state, so it was ignored", ex);
                    return;
                }
                throw ex.rethrowFromSystemServer();
            }
        }
    }

根据代码和注释可以看到,跨进程调用AMS(ActivityManagerService)的activityStopped()方法时,如果出现RemoteException,则会进行区分处理,如果是TransactionTooLargeException且targetSdk小于24,则不做处理;否则,直接抛出异常(rethrowFromSystemServer())。

    public RuntimeException rethrowFromSystemServer() {
        if (this instanceof DeadObjectException) {
            throw new RuntimeException(new DeadSystemException());
        } else {
            throw new RuntimeException(this); // 如果不是DeadObjectException,会包一层RuntimeException抛出
        }
    }

看下TransactionTooLargeException类

public class TransactionTooLargeException extends RemoteException { 
    public TransactionTooLargeException() {
        super();
    }

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

它的类说明:

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.

翻译下:
由于数据太大导致的Binder传输失败异常。
在Binder跨进程调用中,参数和返回值会以Prcel对象的形式存储在Binder传输缓存区中。如果Binder调用的参数或返回值过大超过缓存区限制(溢出),那么该次调用会失败,并且抛出TransactionTooLargeException。每个进程的Binder 传输buffer都是由一个固定大小的存储空间(当前为1Mb)构成,并被该进程中所有的Binder调用共有。所以,即使当前进程中所有正在进行的Binder调用的数据占据的内存大小都很小,如果有很多的Binder调用同时进行,也有可能抛出该异常。
避免TransactionTooLargeException的关键在于保持所有的Binder传输数据都比较小。尽量减少Binder调用所需传输的数据量。要避免传输大数据列表或大bitmap。如果可以,将大数据量的跨进程调用分割成多个小数据量的跨进程调用。

几个疑惑
疑惑1. StopInfo中的异步调用做了什么?
ActivityManager.getService().activityStopped(mActivity.token, mState, mPersistentState, mDescription);

先看下ActivityManager.getService()方法

    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;
                }
            };

实际上获取到的是ActivityManagerService的IBinder对象,之后通过它跨进程调用ActivityManagerService的activityStopped方法。

    @Override
    public final void activityStopped(IBinder token, Bundle icicle,
            PersistableBundle persistentState, CharSequence description) {
        synchronized (this) {
            final ActivityRecord r = ActivityRecord.isInStackLocked(token);
            if (r != null) {
                r.activityStoppedLocked(icicle, persistentState, description);
            }
        }

        trimApplications();
    }

主要调用了ActivityRecord的activityStoppedLocked方法

    final void activityStoppedLocked(Bundle newIcicle, PersistableBundle newPersistentState, CharSequence description) {
        final ActivityStack stack = getStack();
        if (mState != STOPPING) {
            stack.mHandler.removeMessages(STOP_TIMEOUT_MSG, this);
            return;
        }
        if (newPersistentState != null) { 
            persistentState = newPersistentState; // ①
            service.notifyTaskPersisterLocked(task, false);  
        }
        if (newIcicle != null) {
            // If icicle is null, this is happening due to a timeout, so we haven't really saved
            // the state.
            icicle = newIcicle;  // ②
            haveState = true;
            launchCount = 0;
            updateTaskDescription(description);
        }
        if (!stopped) {
            stack.mHandler.removeMessages(STOP_TIMEOUT_MSG, this);
            stopped = true;
            setState(STOPPED, "activityStoppedLocked");

            mWindowContainerController.notifyAppStopped();

            if (finishing) {
                clearOptionsLocked();
            } else {
                if (deferRelaunchUntilPaused) {
                    stack.destroyActivityLocked(this, true /* removeFromApp */, "stop-config");  // ③
                    mStackSupervisor.resumeFocusedStackTopActivityLocked();  // ④
                } else {
                    mStackSupervisor.updatePreviousProcessLocked(this); // ⑤
                }
            }
        }
    }

①和②分别将PersistableBundle和Bundle保存到内存变量里,这些值是从哪里来的呢?这个具体见疑惑2,这里先透露一下:Bundle是Activity的onSaveInstanceState(Bundle outState)方法的outState变量,也是页面恢复时onCreate(Bundle savedInstanceState)方法的savedInstanceState变量。这里简单介绍下Android中Activity的销毁和重建:除了正常关闭页面外的Activity销毁,对于处在stopped状态的activity,如果长期未被使用,或者前台的activity需要更多的资源,系统可能会关闭后台的activity,以恢复一些内存。但是系统仍然记得它的存在,当用户返回到它的时候,系统会创建出一个新的实例来代替它,这里需要利用旧实例被销毁时存下来的数据。这些数据被称为“instance state”,是一个存在Bundle对象中的键值对集合,具体可以看下
android进程重启及activity恢复Android 关于Activity的销毁和重建等文章。这些数据是通过Binder跨进程调用的方式存储到ActivityManagerService进程的内存中,等到页面恢复时,再通过Binder跨进程调用重传回页面所在的进程,并不牵扯到IO的操作,所以效率极高。至于PersistableBundle,它不仅在内存中存储,而且会以xml的形式异步存储到文件中,即使关机重启也能恢复,这里由于并未用过,所以不做涉及。

疑惑2.崩溃原因在哪

虽然定位到了具体抛出异常的位置,但仍然不知道是什么操作引发的异常。看下代码中出现StopInfo的地方,在android.app.ActivityThread类中,构造StopInfo类的地方在handleStopActivity中,

    @Override
    public void handleStopActivity(IBinder token, boolean show, int configChanges,
            PendingTransactionActions pendingActions, boolean finalStateRequest, String reason) {
        final ActivityClientRecord r = mActivities.get(token);
        r.activity.mConfigChangeFlags |= configChanges;

        final StopInfo stopInfo = new StopInfo();
        performStopActivityInner(r, stopInfo, show, true /* saveState */, finalStateRequest, reason);

        stopInfo.setActivity(r);
        stopInfo.setState(r.state);
        stopInfo.setPersistentState(r.persistentState);
        pendingActions.setStopInfo(stopInfo);
        mSomeActivitiesChanged = true;
    }

其中调用了performStopActivityInner方法,省略下来就两句话,

    private void performStopActivityInner(ActivityClientRecord r, StopInfo info, boolean keepShown,
            boolean saveState, boolean finalStateRequest, String reason) {
            // One must first be paused before stopped...
            performPauseActivityIfNeeded(r, reason);
            if (!keepShown) {
                callActivityOnStop(r, saveState, reason);
            }
        }
    }

其中,关注下callActivityOnStop方法

    /**
     * Calls {@link Activity#onStop()} and {@link Activity#onSaveInstanceState(Bundle)}, and updates
     * the client record's state.
     * All calls to stop an activity must be done through this method to make sure that
     * {@link Activity#onSaveInstanceState(Bundle)} is also executed in the same call.
     */
    private void callActivityOnStop(ActivityClientRecord r, boolean saveState, String reason) {
        // Before P onSaveInstanceState was called before onStop, starting with P it's
        // called after. Before Honeycomb state was always saved before onPause.
        final boolean shouldSaveState = saveState && !r.activity.mFinished && r.state == null && !r.isPreHoneycomb();
        final boolean isPreP = r.isPreP();
        if (shouldSaveState && isPreP) {
            callActivityOnSaveInstanceState(r); // ①
        }

        try {
            r.activity.performStop(false /*preserveWindow*/, reason);  // ②
        } catch (SuperNotCalledException e) {
            throw e;
        } catch (Exception e) {
            if (!mInstrumentation.onException(r.activity, e)) {
                throw new RuntimeException(
                        "Unable to stop activity "
                                + r.intent.getComponent().toShortString()
                                + ": " + e.toString(), e);
            }
        }
        r.setState(ON_STOP);

        if (shouldSaveState && !isPreP) {
            callActivityOnSaveInstanceState(r);  // ①
        }
    }

主要分成两步,① callActivityOnSaveInstanceState和② activity.performStop。
看下callActivityOnSaveInstanceState,根据名字就知道,主要是调用了ActivityThread的onSaveInstanceState方法。

    private void callActivityOnSaveInstanceState(ActivityClientRecord r) {
        r.state = new Bundle();
        r.state.setAllowFds(false);
        if (r.isPersistable()) {
            r.persistentState = new PersistableBundle();
            mInstrumentation.callActivityOnSaveInstanceState(r.activity, r.state, r.persistentState);
        } else {
            mInstrumentation.callActivityOnSaveInstanceState(r.activity, r.state);
        }
    }

内部构建了Bundle变量,然后调用了Instrumentation对象的callActivityOnSaveInstanceState方法

    public void callActivityOnSaveInstanceState(Activity activity, Bundle outState) {
        activity.performSaveInstanceState(outState);
    }

直接调用了Activity的performSaveInstanceState方法

    final void performSaveInstanceState(Bundle outState) {
        onSaveInstanceState(outState);
        saveManagedDialogs(outState);
        mActivityTransitionState.saveState(outState);
        storeHasCurrentPermissionRequest(outState);
    }

这样就了解到,onSaveInstanceState里的bundle就传递到StopInfo中了。
使用到StopInfo的地方在reportStop中,

    /**
     * Schedule the call to tell the activity manager we have stopped.  We don't do this
     * immediately, because we want to have a chance for any other pending work (in particular
     * memory trim requests) to complete before you tell the activity manager to proceed and allow
     * us to go fully into the background.
     */
    @Override
    public void reportStop(PendingTransactionActions pendingActions) {
        mH.post(pendingActions.getStopInfo());
    }

其中,mH是继承于Handler的变量

class H extends Handler {}
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值