最近在优化公司项目时, 抛出了一个诡异而少见的 DeadObjectException, 错误堆栈最上层还显示出了 RemoteException, 具体的环境情况是下面这样的.
开发的是一个反射透传接口, 用于跟产品 (所开发的产品也是嵌入式设备) 的 USB 口所接入的嵌入式设备通信, 上层 Java 通过反射机制调用 Framework 层的自定义接口 (Framework 层接口用 AIDL 方式实现, 类似各种 XXXManager的实现方式, 嗯, 具体实现不清楚, 同事开发) 将数据传输给底层, 底层接收处理后再传输给 USB 口的设备.
最初由于开发周期短, 只是简单地将所需要传输的数据硬编码, 再封装为 Json 格式透传下去 (反射的实现也只是每一次传输就创建各种 ClassLoader, 各种 Method 等等), 即每次下发的包, 序列号都是一样的, 测试一切正常, 也很稳定地运行了一段时间. 后来项目要求代码重构优化, 也增加了各种不同类型数据包的需求开发, 同时要求每次下发的包的序列号唯一, 用 1 个字节的长度来表示序列号, 从 0 开始, 达到最大值归 0 不断循环, 所以需要建立包模型, 动态产出所需要的类型数据包再下发.
开发完功能后测试, 发现程序跑起来后, 同嵌入式设备的交互时间一旦达到 17 分钟左右 (需求要求每 8 秒左右下发一次数据), 就会报出 RemoteException : android.os.DeadObjectException
的异常, 然后出现了以下两种情况 (交互出现, 出现概率不定) :
程序不断重启, 并且发现引发了程序其他模块的错误;
程序崩溃, 该嵌入式设备直接重启.
见鬼了, 从来没遇见过这种异常, 堆栈还是一些跟 Java 代码无关的信息, 一下子慌了神, 多次测试后, 发现这是必现的 bug. 于是松了一大口气, 毕竟必现的问题解决比较方便, 于是开始一路追查 (追查点很明显, 代码优化前一切正常, 引入 bug 的地方必在修改点上).
由于模型优化, 负责与嵌入式设备交互的类做成了单例类, 大致类似以下这种鱼和熊掌兼得的写法 :
public class XXX {
private XXX(){}
private static class InstanceHolder {
private static final XXX INSTANCE = new XXX();
}
public static XXX getInstance() {
return InstanceHolder.INSTANCE;
}
}
将对象的创建方式还原为优化前的 new 方式 (其他修改不变), 重新跑起程序, 发现 17 分钟后, 依然抛出上述异常, 证明非单例修改引起.
通俗易懂的面向对象设计模式 :
面向对象设计模式, 作者 : 工匠若水.
优化前每次需要下发数据, 都新建了一个 ClassLoader, Class, Constructor, Object 和 Method 等对象, 而优化后只在单例类初始化时创建了一次各个反射对象, 如下 :
public class XXX {
......
private Context mContext;
private ClassLoader mClassLoader;
private Class mClass;
private Constructor mConstructor;
private Object mObject;
private Method mMethod;
public void init(final Context context) {
try {
if (mContext == null) {
mContext = context;
mClassLoader = mContext.getClassLoader();
//CLASS_NAME 为 Framework 的自定义 XXXManager 路径名
mClass = mClassLoader.loadClass(CLASS_NAME);
mConstructor = mClass.getConstructor(Context.class);
mObject = mConstructor.newInstance(mContext);
//METHOD_NAME 为 XXXManager 类中用于接收数据的方法
mMethod = mClass.getMethod(METHOD_NAME, String.class);
}
} catch (Exception e) {
e.printStackTrace();
}
}
......
}
对于反射对象的优化是比较怀疑造成 bug 的一点, 因为这是 Java 层同底层 C/C++ 代码交互数据所依托的手段. 然而, 回退为每次下发数据都重新 new 反射对象的方式 (其他修改不变) 再测试后, 发现 bug 依然存在, 证明非反射优化造成.
反射机制 :
反射机制官方教程, English Version
【Android】 认识反射机制(Reflection), 作者 : 袁永超
嗯, bug 的引入点只能是它了, 修改动态数据模型, 根据程序所需数据, 包装下发的透传数据, 并且还加入了序列号保证包的唯一性.
回退为包数据硬编码的形式 (其他修改不变), 跑起程序, 测试多次, 未出现 17 分钟左右抛出 DeadObjectException
异常的情况, 好吧, 找到点上了, 但是具体问题出在哪里呢 ?
记录下硬编码形式下发的数据包, 恢复修改为新动态数据模型, 加 Log 记录每次下发的数据包, 并将这些 Log 追加输出到设备 SD 卡上的文件里, 跑起程序, 待程序崩溃或设备重启后, 查看收集到的 Log, 发现最后一条记录是序列号为 127 的数据包.
咦 ? 不应该啊, 数据传输规定了序列号用 1 个字节来存, 1 byte = 8 bits, 理论上应该是从 0 到 255 循环才对啊. 带着疑问, 修改动态数据模型的初始化序列号为 127, 清空 Log 收集文件, 跑起程序, 这次等了 10 几秒, 程序就 crash 了, 还是那个异常抛出, 查看 Log, 发现只有一条序列号为 127 的数据记录. 看来老天爷不让下发序列号比 127 大的数据包啊……
看着 127, 感觉有点眼熟, 嗯, 1 个字节, 嗯, 127, 恍然大悟, 回头翻看工具书, 果真是, Java 中 byte 的取值范围是 -128 ~ 127, 再仔细想想, 底层 C/C++ 中对字节的处理相当敏感, 也许就是因为这个字节溢出, 导致内存写错误, 进而引起其他模块的异常或者设备重启 ! 难道同事给的自定义接口没有加容错 ?
修改动态数据模型的序列号一旦达到 127 就归 0 循环, 跑起程序, 经测试, 不再出现 DeadObjectException
异常, 数据交互一切都正常了.
回过头, 找底层开发的同事聊了会, 果然没有加容错, 好吧, 万年好队友……
本文记录了一次追查 android.os.DeadObjectException
异常的过程, 更证明了一个道理, 细节决定成败, 虽然没有多高的技术含量, 仅作为自己 Android 学习路上的总结吧. 难免水平不足说法有所偏颇, 欢迎指正, 谢谢阅读 !