Adding window failed

在基于Android平台的车机系统开发中,遇到窗口管理问题,导致各种界面无法正常屏蔽弹框。尝试通过统一管理 FloatingWindow 解决,但出现了 Adding window failed 的错误,进一步调查发现是 TransactionTooLargeException 引起的。分析源码和日志,发现是由于文件描述符超出限制,与打开的/dev/fastcamera设备文件有关。修复方案是在使用后关闭文件,避免文件描述符过多导致崩溃。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

我们公司是做车机行业的,最近在项目是基于android平台开发的车机系统,项目接手后,发现各种界面窗口无法很好的屏蔽各种弹框,例如向倒车,待机,关屏等界面是不需要各种弹窗信息显示的。所以在这些界面上有了各种逻辑判断 (反正很复杂,至今还未看懂),一直陷入无限解bug的怪圈中。让人感到深深的无力感,为此自我思考了一下,觉得这样不是个办法,必须重构,至少要满足能解决那种乱弹框的问题。在现有的知识面中,我选择了window,使用type的属性来控制window显示的层级关系。我模仿Toast的方式统一管理项目里所有弹出界面(FloatingWindow),合入版本后,刚开始感觉很棒,由于type类型很少,并且有些type有自己的属性会导致,无法触摸,或者无法获取焦点等问题,这些通过更换type类型都可以解决。

然后代码经过一段时间的沉淀,突然有人告诉我音量弹框界面报错。经过查看后报如下信息:

D/AndroidRuntime(  326): Shutting down VM

E/AndroidRuntime(  326): FATAL EXCEPTION: main
E/AndroidRuntime(  326): Process: com.hsae.core, PID: 326
E/AndroidRuntime(  326): java.lang.RuntimeException: Adding window failed
E/AndroidRuntime(  326):        at android.view.ViewRootImpl.setView(ViewRootImpl.java:509)
E/AndroidRuntime(  326):        at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:259)
E/AndroidRuntime(  326):        at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:69)
E/AndroidRuntime(  326):        at com.hsae.autosdk.window.FloatingWindow$FWC.handleShow(FloatingWindow.java:285)
E/AndroidRuntime(  326):        at com.hsae.autosdk.window.FloatingWindow$FWC$1.run(FloatingWindow.java:235)
E/AndroidRuntime(  326):        at android.os.Handler.handleCallback(Handler.java:733)
E/AndroidRuntime(  326):        at android.os.Handler.dispatchMessage(Handler.java:95)
E/AndroidRuntime(  326):        at android.os.Looper.loop(Looper.java:136)
E/AndroidRuntime(  326):        at android.app.ActivityThread.main(ActivityThread.java:5001)
E/AndroidRuntime(  326):        at java.lang.reflect.Method.invokeNative(Native Method)
E/AndroidRuntime(  326):        at java.lang.reflect.Method.invoke(Method.java:515)
E/AndroidRuntime(  326):        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:928)
E/AndroidRuntime(  326):        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:744)
E/AndroidRuntime(  326):        at dalvik.system.NativeStart.main(Native Method)
E/AndroidRuntime(  326): Caused by: android.os.TransactionTooLargeException
E/AndroidRuntime(  326):        at android.os.BinderProxy.transact(Native Method)
E/AndroidRuntime(  326):        at android.view.IWindowSession$Stub$Proxy.addToDisplay(IWindowSession.java:683)
E/AndroidRuntime(  326):        at android.view.ViewRootImpl.setView(ViewRootImpl.java:498)
E/AndroidRuntime(  326):        ... 13 more

自己验证几次,属于概率性问题,反正只要不停的弹音量框,就有可能出现同样的崩溃。

这个错第一次查看,按照惯例先找度娘查找看有没有相关大神的解释,通过半个小时的查看总结了一下原因对策

1 由于binder的传输对象太大导致报TransactionTooLargeException,从而导致添加window失败。

2 binder的Parcelable 自定义的对象读取格式错误比如是个int类型,然后用readDouble来读取也会报错

解决对策

1 减少binder的传输对象大小

2 Parcelable 自定义对象按格式正确的读写。

 

回到我遇到的问题,第二点我就不做验证了,我只是弹个窗,中间没啥自定义参数,关于第一点,我目测我的弹窗没什么大图片或者其他内存操作,就是简单的弹一个小图标,和文言。此时暂时没有什么头绪,怀着报一丝希望在度娘看看,找到一篇和我遇到的情况差不多的现象的博客给我启发很大,《Android TransactionTooLargeException 解析,思考与监控方案》https://blog.youkuaiyun.com/self_study/article/details/60136277

但里面只是给出一个监控方案,利用动态代理以及利用动态代理实现 ServiceHook,意图是检查具体哪块服务的接口通讯异常;但我报错的地方是at android.view.ViewRootImpl.setView(ViewRootImpl.java:509) ;mWindowSession.addToDisplay这个方法报错,我直接修改源码,在这块位置上方加上计算Parcel大小:(红色是我加的代码,是模拟获取data的值,然后在打印data的大小)

              try {

                    mOrigWindowType = mWindowAttributes.type;
                    mAttachInfo.mRecomputeGlobalAttributes = true;
                    collectViewAttributes();
                    _data.writeInterfaceToken("android.view.IWindowSession");
                    _data.writeStrongBinder((((mWindow!=null))?(mWindow.asBinder()):(null)));
                    _data.writeInt(mSeq);
                    if ((mWindowAttributes!=null)) {
                        _data.writeInt(1);
                        mWindowAttributes.writeToParcel(_data, 0);
                    }
                    else {
                        _data.writeInt(0);
                    }
                    _data.writeInt(getHostVisibility());
                    _data.writeInt(mDisplay.getDisplayId());

                    Log.e(TAG, "mWindow:" + mWindow + "addToDisplay transact's parameter size is " + _data.dataSize() + " B");

                    res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(),
                            mAttachInfo.mContentInsets, mInputChannel);
                } catch (RemoteException e) {
                    mAdded = false;
                    mView = null;
                    mAttachInfo.mRootView = null;
                    mInputChannel = null;
                    mFallbackEventHandler.setView(null);
                    unscheduleTraversals();
                    setAccessibilityFocus(null, null);
                    throw new RuntimeException("Adding window failed, size is " + _data.dataSize(), e);
                } finally {
                    if (restore) {
                        attrs.restore();
                    }
                }

通过自测,出现崩溃时,size的大小没有明显的变化,基本上addToDisplay这个方法传递Parcel的大小很稳定。

叹了一口气,先想想这个问题严重性,再次自测一下重现的概率,基本上只要想复现出来,十几分钟就能复现崩溃。感觉概率好高,严重性很高,不解决后面都很难通过测试验证部。

冷静了一会,想想基本上能短期使用的手段都使用过了。只能用最后的大杀器了---《Read the fucking source code》

第一步查看TransactionTooLargeException这个异常从哪儿报的,从刚才看的一篇博客中有讲解到signalExceptionForError 里面报出的异常

我的源码约有差异,走到这块直接报异常,都没有判断大小。

frameworks/base/core/jni/android_util_Binder.cpp

signalExceptionForError 

        case FAILED_TRANSACTION:
            ALOGE("!!! FAILED BINDER TRANSACTION !!!");
            // TransactionTooLargeException is a checked exception, only throw from certain methods.
            // FIXME: Transaction too large is the most common reason for FAILED_TRANSACTION
            //        but it is not the only one.  The Binder driver can return BR_FAILED_REPLY
            //        for other reasons also, such as if the transaction is malformed or
            //        refers to an FD that has been closed.  We should change the driver
            //        to enable us to distinguish these cases in the future.
            jniThrowException(env, canThrowRemoteException
                    ? "android/os/TransactionTooLargeException"
                            : "java/lang/RuntimeException", NULL);
            break;

 

继续查看android_os_BinderProxy_transact方法

有一段


    IBinder* target = (IBinder*)
        env->GetIntField(obj, gBinderProxyOffsets.mObject);

...

    status_t err = target->transact(code, *data, reply, flags);

...
    signalExceptionForError(env, obj, err, true /*canThrowRemoteException*/);

err这个变量决定走signalExceptionForError方法switch的FAILED_TRANSACTION分支。关键是 target->transact返回的结果导致报错,我们来看看transact这个函数是在哪里调用的,我查看相关binder相关资料,binder基本上在java层是一个壳子,主要逻辑是在frameworks的native层完成,路径在frameworks/native/libs/binder,通过查找具体是由BpBinder来消息传递函数transact,我们可以理解target 就是 BpBinder。

具体想详细了解binder机制可参考《Android深入浅出之Binder机制》https://www.cnblogs.com/innost/archive/2011/01/09/1931456.html

好继续看frameworks/native/libs/binder/BpBinder.cpp

status_t BpBinder::transact(
    uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags)
{
    // Once a binder has died, it will never come back to life.
    if (mAlive) {
        status_t status = IPCThreadState::self()->transact(
            mHandle, code, data, reply, flags);
        if (status == DEAD_OBJECT) mAlive = 0;
        return status;
    }

    return DEAD_OBJECT;
}

我们发现是IPCThreadState来调用了一个方法transact,返回了一个status,我继续往下看

frameworks/native/libs/binder/IPCThreadState.cpp

status_t IPCThreadState::transact(int32_t handle,
                                  uint32_t code, const Parcel& data,
                                  Parcel* reply, uint32_t flags)

{
    status_t err = data.errorCheck();

...

    if (err == NO_ERROR) {
        LOG_ONEWAY(">>>> SEND from pid %d uid %d %s", getpid(), getuid(),
            (flags & TF_ONE_WAY) == 0 ? "READ REPLY" : "ONE WAY");
        err = writeTransactionData(BC_TRANSACTION, flags, handle, code, data, NULL);
    }

...

    if ((flags & TF_ONE_WAY) == 0) {
        #if 0
        if (code == 4) { // relayout
            ALOGI(">>>>>> CALLING transaction 4");
        } else {
            ALOGI(">>>>>> CALLING transaction %d", code);
        }
        #endif
        if (reply) {
            err = waitForResponse(reply);
        } else {
            Parcel fakeReply;
            err = waitForResponse(&fakeReply);
        }
        #if 0
        if (code == 4) { // relayout
            ALOGI("<<<<<< RETURNING transaction 4");
        } else {
            ALOGI("<<<<<< RETURNING transaction %d", code);
        }
        #endif
        
        IF_LOG_TRANSACTIONS() {
            TextOutput::Bundle _b(alog);
            alog << "BR_REPLY thr " << (void*)pthread_self() << " / hand "
                << handle << ": ";
            if (reply) alog << indent << *reply << dedent << endl;
            else alog << "(none requested)" << endl;
        }
    } else {
        err = waitForResponse(NULL, NULL);
    }

有三个函数会返回err的值,也是我们想要知道的报错信息。

1  errorCheck

2 writeTransactionData

3  waitForResponse

我经过打log发现最后引起报错的函数是waitForResponse导致的。这个函数是在发送请求发送数据后,以wait形式等待目标服务返回结果。

继续跟进此函数:

status_t IPCThreadState::waitForResponse(Parcel *reply, status_t *acquireResult)
{
...

        switch (cmd) {
...

        case BR_FAILED_REPLY:
            err = FAILED_TRANSACTION;
            goto finish;
        
发现 cmd等于BR_FAILED_REPLY才会返回错误信息FAILED_TRANSACTION,也就是我们frameworks/base/core/jni/android_util_Binder.cpp 里面报错的信息。

BR_FAILED_REPLY是什么鬼。。。通过代码和相关资料知道是kernel层binder的一些协议定义。

参考资料:https://www.cnblogs.com/zhulinhaibao/p/7088339.html

具体协议定义

枚举binder_driver_return_protocol 表示返回结果,枚举都是以BR开头,枚举binder_driver_command_protocol表示发送指令,枚举都是以BC开头。

具体内核kernel层binder来定义一些协议和实现数据传输。

具体路径:kernel_imx/drivers/staging/android/binder.c

我在binder.c搜索BR_FAILED_REPLY,发现有25处地方,在一个函数里

static void binder_transaction(struct binder_proc *proc,
                   struct binder_thread *thread,
                   struct binder_transaction_data *tr, int reply)

对函数不是很了解,简单看了一下,基本返回BR_FAILED_REPLY都会以goto的方式走向相应的错误处理。

通过打log的方式继续验证发现具体报错的位置:

            target_fd = task_get_unused_fd_flags(target_proc, O_CLOEXEC);
            if (target_fd < 0) {
                fput(file);
                return_error = BR_FAILED_REPLY;
                goto err_get_unused_fd_failed;
            }

当task_get_unused_fd_flags函数返回结果target_fd小于0时就会报BR_FAILED_REPLY错误。

task_get_unused_fd_flags 这又是什么东东??

继续参考:https://blog.youkuaiyun.com/lewif/article/details/50668783

知道这个函数是获取目标进程未使用的文件描述符的,如果结果小于0我理解是获取不到文件描述符的意思。

target_proc这个我打印log和在命令行使用ps联合查看 知道target_proc是autocore的一个app进程。

用lsof 查看autocore的进程,发现每次弹出音量弹框都会增加一些/dev/fastcamera的文件描述符。这个我搜索了一下autocore代码,发现在jni部分有open /dev/fastcamera这个文件节点。但没有相关的close操作。通过这个jni函数,我知道具体的调用位置,在每次调整音量的时候都会有判断fastcamera里面的值,但每次都没有做close操作,导致文件描述符持续递增,一直超过1024个文件描述符就崩溃了。 加上close后,在没有出现此问题了。( ̄▽ ̄)"。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值