我们公司是做车机行业的,最近在项目是基于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后,在没有出现此问题了。( ̄▽ ̄)"。