今年安卓開發中碰到的幾個稀奇古怪的問題

android飛魚發表於2018-12-10

如果你也遇到了,請保持淡定~

1.SIGBUS和SIGSEGV

首先是這兩個名詞的說明:

  1. SIGBUS(Bus error)意味著指標所對應的地址是有效地址,但匯流排不能正常使用該指標。通常是未對齊的資料訪問所致。

  2. SIGSEGV(Segment fault)意味著指標所對應的地址是無效地址,沒有實體記憶體對應該地址。

有人一看,什麼指標不指標的,對於大多數開發人員來說,不涉及NDK這方面的開發。所以可以想到的就是我們使用的so庫。

我這裡碰到的SIGBUS相關問題主要集中在整合的極光推送,在極光社群的這篇帖子和我的問題一樣。我收集到的資訊集中在CPU架構為arm64-v8a,android 5.x 的 OPPO R9MOPPO R7SMOPPO A59MOPPO A59S等OPPO手機。如下圖:

問題起因是這樣,為了瘦身我們的apk檔案,我只新增了armeabi-v7a 架構的相關so檔案。因為現在絕大部分的裝置都已經是 armeabi-v7a 和 arm64-v8a,雖然我也可以使用armeabi,但是效能關係我最終只保留了armeabi-v7a 。

按道理arm64-v8a裝置可以相容arm64-v8aarmeabi-v7aarmeabi。但結果在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。有興趣的可以試試。

那麼解決問題的方法也就來了,我們可以在ApplicationattachBaseContext中呼叫(可以針對問題機型及系統版本去處理,不要矯枉過正):

        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開發群;701740775,可免費領取一份最新Android高階架構技術體系大綱和視訊資料,以及這些年年積累整理的所有面試資源筆記。加群請備註csdn領取xx資料


相關文章