1.SIGBUS和SIGSEGV
首先是这两个名词的说明:
SIGBUS(Bus error)意味着指针所对应的地址是有效地址,但总线不能正常使用该指针。通常是未对齐的数据访问所致。
SIGSEGV(Segment fault)意味着指针所对应的地址是无效地址,没有物理内存对应该地址。
有人一看,什么指针不指针的,对于大多数开发人员来说,不涉及NDK这方面的开发。所以可以想到的就是我们使用的so库。
我这里碰到的SIGBUS
相关问题主要集中在集成的极光推送,在极光社区的这篇帖子和我的问题一样。我收集到的信息集中在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 问题
有兴趣的可以去看看,这里就不赘述了。导致这类问题的情况比较多,只能是经验积累,碰到一个解决一个。不涉及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(); }
其实我是用的是stackoverflow这篇帖子中提供的方法:
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的创建就是一种解决方法。
强烈推荐阅读:提升Android下内存的使用意识和排查能力、再谈Finalizer对象–大型App中内存与性能的隐性杀手
3.SchedulerPoolFactory
前一阵在用Android Studio的内存分析工具检测App时,发现每隔一秒,都会新分配出20多个实例,跟踪了一下发现是RxJava2中的SchedulerPoolFactory
创建的。
一般来说如果一个页面创建加载好后是不会再有新的内存分配,除非页面有动画、轮播图、EditText的光标闪动等页面变化。当然了在应用退到后台时,或者页面不可见时,我们会停止这些任务。保证不做这些无用的操作。然而我在后台时,这个线程池还在不断运行着,也就是说CPU在周期性负载,自然也会耗电。那么就要想办法优化一下了。
SchedulerPoolFactory
的作用是管理 ScheduledExecutorServices
的创建并清除。
SchedulerPoolFactory
部分源码如下:
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。 } } } }
我查了相关问题,在stackoverflow找到了此问题,同时也给RxJava提了Issue,得到了回复是可以使用:
// 修改周期时间为一小时 System.setProperty("rx2.purge-period-seconds", "3600");
当然你也可以关闭周期清除:
System.setProperty("rx2.purge-enabled", false);
作用范围如下:
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 } } }
1s的清除周期我觉得有点太频繁了,最终我决定将周期时长改为60s。最好在首次使用RxJava前修改,放到Application中最好。
4.其他
适配8.0时注意
Service
的创建。否则会有IllegalStateException
异常:
java.lang.IllegalStateException:Not allowed to start service Intent { xxx.MyService }: app is in background uid null
有些手机(已知oppo)在手机储存空间不足时,当你应用退到后台时会自动清除cache下文件,所以如果你有重要数据存储,避免放在cache下,否则当你再次进入应用时,再次获取数据时会有空指针。例如有使用磁盘缓存
DiskLruCache
来存储数据。
作者:Android高级架构
链接:https://www.jianshu.com/p/c9b636919dbf
共同学习,写下你的评论
评论加载中...
作者其他优质文章