如果你也遇到了,請保持淡定~
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進階技術交流 (895077617 )免費獲取!
裏面可以與大神一起交流並走出迷茫。新手可進羣免費領取學習資料,看看前輩們是如何在編程的世界裏傲然前行!有想學習Android Java的,或是轉行,或是大學生,還有工作中想提升自己能力的,正在學習的小夥伴歡迎加入。(包括Java在Android開發中應用、APP框架知識體系、高級UI、全方位性能調優,ViewPager,Bitmap,組件化架構,四大組件等深入學習視頻資料以及Android、Java全方面面試資料)