安卓開發中碰到的幾個稀奇古怪的問題,你遇到過嗎?

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

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-v7aarm64-v8a,雖然我也可以使用armeabi,但是性能關係我最終只保留了armeabi-v7a

按道理arm64-v8a設備可以兼容arm64-v8aarmeabi-v7aarmeabi。但結果在oppo的這些手機上沒有兼容,或者說更加的嚴格,導致了未對齊的數據訪問。爲什麼這麼說,因爲後來有觀察再升級極光的sdk後,發現這類問題有所下降。當然如果你直接添加上arm64-v8a,則不會有這個問題。

導致這個問題有多方面的因素,有我們使用的三方sdk的問題,也有手機問題。但在手機不可變的基礎上,只能我們去解決,所以儘量不要通過這種方法瘦身APK。(實在不行可以用折中方案,保留armeabi-v7aarm64-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進階技術交流 (895077617 )免費獲取!

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章