距离上一篇 安卓开发中遇到的奇奇怪怪的问题 (二) 又过了半年了, 转眼也到年底了, 是时候拿出点干货了. 这篇算是本年度个人印象最深的几个问题, 分享一下.
1.SIGBUS 和 SIGSEGV
首先是这两个名词的说明:
SIGBUS(Bus error)意味着指针所对应的地址是有效地址, 但总线不能正常使用该指针. 通常是未对齐的数据访问所致.
SIGSEGV(Segment fault)意味着指针所对应的地址是无效地址, 没有物理内存对应该地址.
有人一看, 什么指针不指针的, 对于大多数开发人员来说, 不涉及 NDK 这方面的开发. 所以可以想到的就是我们使用的 so 库.
我这里碰到的 SIGBUS 相关问题主要集中在集成的极光推送, 在极光社区的这篇帖子 https://community.jiguang.cn/t/android-crash/26863 和我的问题一样. 我收集到的信息集中在 CPU 架构为 arm64-v8a,Android 5.x 的 OPPO R9M,OPPO R7SM,OPPO A59M,OPPO A59S 等 OPPO 手机. 如下图:
问题起因是这样, 为了瘦身我们的 apk 文件, 我只添加了 armeabi-v7a 架构的相关 so 文件. 因为现在绝大部分的设备都已经是 armeabi-v7a 和 arm64-v8a, 虽然我也可以使用 armeabi, 但是性能关系我最终只保留了 armeabi-v7a .
按道理 arm64-v8a 设备可以兼容 arm64-v8a,armeabi-v7a,armeabi. 但结果在 oppo 的这些手机上没有兼容, 或者说更加的严格, 导致了未对齐的数据访问. 为什么这么说, 因为后来有观察再升级极光的 sdk 后, 发现这类问题有所下降. 当然如果你直接添加上 arm64-v8a, 则不会有这个问题.
导致这个问题有多方面的因素, 有我们使用的三方 sdk 的问题, 也有手机问题. 但在手机不可变的基础上, 只能我们去解决, 所以尽量不要通过这种方法瘦身 APK.(实在不行可以用折中方案, 保留 armeabi-v7a 和 arm64-v8a).
而 SIGSEGV 问题排除掉架构兼容问题, 相对于集中在 5.0 以下及机子. 这块问题相对比较复杂, 我碰到了这样一个问题:
搜索了一下相关问题, 找到一篇解决方法: 三星 Android 4.3 机型上 webview crash 问题 https://www.jianshu.com/p/629fbeb5db47
有兴趣的可以去看看, 这里就不赘述了. 导致这类问题的情况比较多, 只能是经验积累, 碰到一个解决一个. 不涉及 NDK 这方面的开发人员, 很难规避掉此类问题.
2.TimeoutException
这个问题真的 "无法避免". 从 buyly 的统计看主要集中在 oppo 5.0~6.0 及个别华为 5.0 机型. 好吧又是 oppo 手机, oppo 真的是很严格, 我都快成黑粉了... (当然了 7,8,9 看来挺不错的)
反馈上来的远比截图看的多, 我只取了截取了一小部分. 新版本已经 "解决了" 这个问题, 所以现在报上来的主要都是老版本.
bugly 异常信息如下:
错误堆栈信息:
- FinalizerWatchdogDaemon
- java.util.concurrent.TimeoutException
- Android.os.BinderProxy.finalize() timed out after 120 seconds
- Android.os.BinderProxy.destroy(Native Method)
- Android.os.BinderProxy.finalize(Binder.java:547)
- java.lang.Daemons$FinalizerDaemon.doFinalize(Daemons.java:214)
- java.lang.Daemons$FinalizerDaemon.run(Daemons.java:193)
- java.lang.Thread.run(Thread.java:818)
首先来说明一下发生问题的原因, 在 GC 时, 为了减少应用程序的停顿, 会启动四个 GC 相关的守护线程. FinalizerWatchdogDaemon 就是其中之一, 它是用来监控 FinalizerDaemon 线程的执行.
FinalizerDaemon: 析构守护线程. 对于重写了成员函数 finalize 的对象, 它们被 GC 决定回收时, 并没有马上被回收, 而是被放入到一个队列中, 等待 FinalizerDaemon 守护线程去调用它们的成员函数 finalize, 然后再被回收.
一旦检测到执行成员函数 finalize 时超出一定的时间, 那么就会退出 VM. 我们可以理解为 GC 超时了. 这个时间默认为 10s, 我通过翻看 oppo, 华为的 Framework 源码发现这个时间在部分机型被改为了 120s 和 30s.
虽然时间加长了, 但还是一样的超时了, 具体在 oppo 手机上为何这么慢, 暂时无法得知, 但是可以肯定的是 Finalizer 对象过多导致的. 知道了原因, 所以要模拟这个问题也很简单了. 也就是引用一个重写 finalize 方法的实例, 同时这个 finalize 方法有耗时操作, 这时我们手动 GC 就行了. 刚好前几天, 在我订阅的张绍文老师的《Android 开发高手课中》, 老师提到了这个问题, 同时分享了一个模拟问题并解决问题的 Demo. 有兴趣的可以试试.
那么解决问题的方法也就来了, 我们可以在 Application 的 attachBaseContext 中调用(可以针对问题机型及系统版本去处理, 不要矫枉过正):
- try {
- final Class clazz = Class.forName("java.lang.Daemons$FinalizerWatchdogDaemon");
- final Field field = clazz.getDeclaredField("INSTANCE");
- field.setAccessible(true);
- final Object watchdog = field.get(null);
- try {
- final Field thread = clazz.getSuperclass().getDeclaredField("thread");
- thread.setAccessible(true);
- thread.set(watchdog, null);
- } catch (final Throwable t) {
- Log.e(TAG, "stopWatchDog, set null occur error:" + t);
- t.printStackTrace();
- try {
- // 直接调用 stop 方法, 在 Android 6.0 之前会有线程安全问题
- final Method method = clazz.getSuperclass().getDeclaredMethod("stop");
- method.setAccessible(true);
- method.invoke(watchdog);
- } catch (final Throwable e) {
- Log.e(TAG, "stopWatchDog, stop occur error:" + t);
- t.printStackTrace();
- }
- }
- } catch (final Throwable t) {
- Log.e(TAG, "stopWatchDog, get object occur error:" + t);
- t.printStackTrace();
- }
其实我是用的是 Stack Overflow 这篇帖子中提供的方法:
- public static void fix() {
- try {
- Class clazz = Class.forName("java.lang.Daemons$FinalizerWatchdogDaemon");
- Method method = clazz.getSuperclass().getDeclaredMethod("stop");
- method.setAccessible(true);
- Field field = clazz.getDeclaredField("INSTANCE");
- field.setAccessible(true);
- method.invoke(field.get(null));
- }
- catch (Throwable e) {
- e.printStackTrace();
- }
- }
两种方法都是通过反射最终将 FinalizerWatchdogDaemon 中的 thread 置空, 这样也就不会执行此线程, 所以不会再有超时异常发生. 推荐老师的方法, 更加全面完善. 因为在 Android 6.0 之前会有线程安全问题, 如果直接调用 stop 方法, 还是会有几率触发此异常. 5.0 源代码如下:
- private static abstract class Daemon implements Runnable {
- private Thread thread;// 一种是直接置空 thread
- public synchronized void start() {
- if (thread != null) {
- throw new IllegalStateException("already running");
- }
- thread = new Thread(ThreadGroup.systemThreadGroup, this, getClass().getSimpleName());
- thread.setDaemon(true);
- thread.start();
- }
- public abstract void run();
- protected synchronized boolean isRunning() {
- return thread != null;
- }
- public synchronized void interrupt() {
- if (thread == null) {
- throw new IllegalStateException("not running");
- }
- thread.interrupt();
- }
- public void stop() {
- Thread threadToStop;
- synchronized (this) {
- threadToStop = thread;
- thread = null; // 一种是通过调用 stop 置空 thread
- }
- if (threadToStop == null) {
- throw new IllegalStateException("not running");
- }
- threadToStop.interrupt();
- while (true) {
- try {
- threadToStop.join();
- return;
- } catch (InterruptedException ignored) {
- }
- }
- }
- public synchronized StackTraceElement[] getStackTrace() {
- return thread != null ? thread.getStackTrace() : EmptyArray.STACK_TRACE_ELEMENT;
- }
- }
这个所谓的线程安全问题就在 stop 方法中的 threadToStop.interrupt(). 在 6.0 开始, 这里变为了 interrupt(threadToStop), 而 interrupt 方法加了同步锁.
- public synchronized void interrupt(Thread thread) {
- if (thread == null) {
- throw new IllegalStateException("not running");
- }
- thread.interrupt();
- }
虽然崩溃不会出现了, 但是问题依然存在, 可谓治标不治本. 通过这个问题也提醒我们, 尽量避免重写 finalize 方法, 同时不要在其中有耗时操作. 其实我们 Android 中的 View 都有实现 finalize 方法, 那么减少 View 的创建就是一种解决方法.
- static void tryStart(boolean purgeEnabled) {
- if (purgeEnabled) {
- for (;;) { // 一个死循环
- ScheduledExecutorService curr = PURGE_THREAD.get();
- if (curr != null) {
- return;
- }
- ScheduledExecutorService next = Executors.newScheduledThreadPool(1, new RxThreadFactory("RxSchedulerPurge"));
- if (PURGE_THREAD.compareAndSet(curr, next)) {
- // RxSchedulerPurge 线程池, 每隔 1s 清除一次
- next.scheduleAtFixedRate(new ScheduledTask(), PURGE_PERIOD_SECONDS, PURGE_PERIOD_SECONDS, TimeUnit.SECONDS);
- return;
- } else {
- next.shutdownNow();
- }
- }
- }
- }
- static final class ScheduledTask implements Runnable {
- @Override
- public void run() {
- for (ScheduledThreadPoolExecutor e : new ArrayList<ScheduledThreadPoolExecutor>(POOLS.keySet())) {
- if (e.isShutdown()) {
- POOLS.remove(e);
- } else {
- e.purge();// 图中 154 行, purge 方法可用于移除那些已被取消的 Future.
- }
- }
- }
- }
- // 修改周期时间为一小时
- System.setProperty("rx2.purge-period-seconds", "3600");
- static final class PurgeProperties {
- boolean purgeEnable;
- int purgePeriod;
- void load(Properties properties) {
- if (properties.containsKey(PURGE_ENABLED_KEY)) {
- purgeEnable = Boolean.parseBoolean(properties.getProperty(PURGE_ENABLED_KEY));
- } else {
- purgeEnable = true; // 默认是 true
- }
- if (purgeEnable && properties.containsKey(PURGE_PERIOD_SECONDS_KEY)) {
- try {
- // 可以修改周期时间
- purgePeriod = Integer.parseInt(properties.getProperty(PURGE_PERIOD_SECONDS_KEY));
- } catch (NumberFormatException ex) {
- purgePeriod = 1; // 默认是 1s
- }
- } else {
- purgePeriod = 1; // 默认是 1s
- }
- }
- }
来源: https://juejin.im/entry/5c0db7fe5188255a004ca20f