最近友盟总是收到这样的异常:
java.lang.RuntimeException: Could not read input channel file descriptors from parcel.
at android.view.InputChannel.nativeReadFromParcel(Native Method)
at android.view.InputChannel.readFromParcel(InputChannel.java:148)
at android.view.IWindowSession$Stub$Proxy.addToDisplay(IWindowSession.java:759)
at android.view.ViewRootImpl.setView(ViewRootImpl.java:531)
at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:310)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:86)
at android.widget.Toast$TN.handleShow(Toast.java:425)
at android.widget.Toast$TN$1.run(Toast.java:331)
at android.os.Handler.handleCallback(Handler.java:739)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:148)
at android.app.ActivityThread.main(ActivityThread.java:5417)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)
根据经验,单纯的Toast.show()很难引发异常,由于刚开始频次较低没有留意
但是后来发现该异常逐渐频繁起来,于是研究一下
首先看这个异常堆栈,里面没有任何我们自己的代码,很难确定异常代码位置
但是里面可以看到,有调用Toast$TN.handleShow()方法
该方法是应用中调用Toast.show()时,内部进行aidl调用,最终在内部类TN.handleShow()时,向WindowManager添加View
最终执行到nativeReadFromParcel方法:
static void android_view_InputChannel_nativeReadFromParcel(JNIEnv* env, jobject obj,
jobject parcelObj) {
if (android_view_InputChannel_getNativeInputChannel(env, obj) != NULL) {
jniThrowException(env, "java/lang/IllegalStateException",
"This object already has a native input channel.");
return;
}
Parcel* parcel = parcelForJavaObject(env, parcelObj);
if (parcel) {
bool isInitialized = parcel->readInt32();
if (isInitialized) {
String8 name = parcel->readString8();
int rawFd = parcel->readFileDescriptor();
int dupFd = dup(rawFd);
if (dupFd < 0) {
ALOGE("Error %d dup channel fd %d.", errno, rawFd);//此处还会打印错误的原因
jniThrowRuntimeException(env,
"Could not read input channel file descriptors from parcel.");
return;
}//异常就是从这里抛出的。
InputChannel* inputChannel = new InputChannel(name, dupFd);
NativeInputChannel* nativeInputChannel = new NativeInputChannel(inputChannel);
android_view_InputChannel_setNativeInputChannel(env, obj, nativeInputChannel);
}
}
}
发现是文件句柄超出了最大数量导致的(安卓软件最大文件句柄是1024)
于是推测异常原因可能是:某处读写文件/打开连接等操作未及时释放资源,导致file descriptors 泄漏,最终在弹出toast时报出该异常
结合之前有用户反映过,在使用我们应用的打印标价签功能时,发生闪退
查看打印模块,发现在向usb设备发送数据时有一段这样的代码:
UsbDeviceConnection usbDeviceConnection = usbManager.openDevice(usbDevice); if (usbDeviceConnection == null) { return false; } else { usbDeviceConnection.claimInterface(usbInterface, true); if (usbDeviceConnection.bulkTransfer(UsbEndpoint, command, command.length, 10000) >= 0) { return true; } else { return false; } }
UsbManager.openDevice()内实现:
public UsbDeviceConnection openDevice(UsbDevice device) { try { String deviceName = device.getDeviceName(); ParcelFileDescriptor pfd = mService.openDevice(deviceName); if (pfd != null) { UsbDeviceConnection connection = new UsbDeviceConnection(device); boolean result = connection.open(deviceName, pfd); pfd.close(); if (result) { return connection; } } } catch (Exception e) { Log.e(TAG, "exception in UsbManager.openDevice", e); } return null; }
可以看到使用完连接后没有及时关闭,而是直接return,导致连接泄漏
我们假设此时file descriptors 已达上限,那么openDevice()方法不会直接抛出异常,而是内部做了拦截,然后返回null
那么外部得到null之后,继续返回给应用层,应用层会弹出Toast提示用户打印失败
而此时,由于Toast.show()最终也需要打开file descriptors,但此时file descriptors已达上限
所以最终报出异常的位置是Toast,而不是真正引发异常的usb连接泄漏
整个引发异常真的是逻辑严密,环环紧扣,安卓源码中捕获了openDevice的异常,导致我们上层应用直接就不晓得具体异常原因
这一点我觉得底层源码中的UsbManager.openDevice()方法写的有点问题,不应该直接内部捕获异常,而是把异常抛出给上层
这样应用层就可以直接接触到该异常进行上报,就不需要绕这么一大圈子才能发现原因