今年安卓開發中碰到的幾個稀奇古怪的問題
如果你也遇到了,請保持淡定~
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開發群;701740775,可免費領取一份最新Android高階架構技術體系大綱和視訊資料,以及這些年年積累整理的所有面試資源筆記。加群請備註csdn領取xx資料
相關文章
- azkaban 安裝中的幾個問題
- 無依賴開發中的碰到的問題——封裝DOM操作封裝
- 開發以太坊遇到的幾個問題
- 微信小程式開發中遇到的幾個小問題微信小程式
- webpack碰到的問題Web
- Typora 使用中的幾個問題
- Hodoop碰到的問題628OdooOOP
- C#開發中,學習整理的 New 的幾個常見問題C#
- 安卓 Surfaceview 的截圖的問題安卓View
- 加殼上碰到的問題
- iOS 越獄後碰到的問題iOS
- 安卓開發第一步:安卓面試題安卓面試題
- 求職Python開發,面試官最喜歡問的幾個問題求職Python面試
- 安卓開發中空指標問題 怎麼解決呢安卓指標
- 安卓stdio中新建activity遇到的問題安卓
- 移動 web 開發幾個明顯的相容性問題Web
- 開發網校原始碼時應該注意的幾個問題原始碼
- React小知識(3) - 國際化中碰到的問題React
- 工作中碰到的Java問題整理及解決方案Java
- 安裝oracle11g碰到“無法訪問臨時位置”的問題Oracle
- 安卓開發小組的反思安卓
- RHEL 7.X 或CentOS 7 安裝 11.2.0.4 RAC碰到的問題CentOS
- 【學習】分享幾個學習中的小問題
- 發現幾個小問題安裝時,模型檢視時模型
- grub常見的幾個問題
- vs2015cordova環境安裝【個人遇到的幾個問題】
- 安裝 laraBBS 原始碼包可能會碰到的問題及解決方法原始碼
- 使用vue-server-render時碰到的問題VueServer
- 皮膚開發過程中遇到的3個問題
- 安卓開發——WebView+Recyclerview文章詳情頁,解決高度問題安卓WebView
- 面試官常問的Nginx的幾個問題面試Nginx
- 安卓開發:安卓底部選單欄的實現,RadioGroup 和Fragment安卓Fragment
- 通過幾個問題深入分析Vue中的keyVue
- 技術人溝通中的幾個常見問題
- 分享Java面試中的幾個重要基礎問題!Java面試
- [併發程式設計]-關於 CAS 的幾個問題程式設計
- 入行 AI 的幾個常見問題AI
- Redis學習的幾個小問題Redis