在Android的app開發過程中,除了機型適配等問題,常常還會出一些特殊的bug,這些bug往往需要特殊的場景情況下才會發生,這里羅列了一些平時項目中遇到的問題及注意點。
App打包apk安裝后重復啟動根界面的問題
這個問題很特殊,一般情況下很難被發現,是Android系統一直以來的一個Bug。
當我們把app打包成apk安裝程序,通過點擊apk文件進行安裝時,會啟動安裝界面,
并在安裝成功后會跳轉安裝完成界面,
如圖:
我們點擊圖中的 打開按鈕,此時會啟動我們的app
這里為了讓大家更容易理解一些,
我們假設app有兩個界面
- 啟動界面SplashActivity
- 主界面MainActivity
- app啟動后打開SplashActivity,3秒后自動跳轉MainActivity,界面不做強制finish
接下來,我們需要了解下Task任務棧和Back Stack返回棧,
如果有同學對這兩個概念還不熟悉的,
可以看一下官方文檔,講得很詳細:
這里我們引用官方文檔的一句話:
The device Home screen is the starting place for most tasks. When the user touches an icon in the application launcher (or a shortcut on the Home screen), that application's task comes to the foreground. If no task exists for the application (the application has not been used recently), then a new task is created and the "main" activity for that application opens as the root activity in the stack.
當我們點擊home界面的應用啟動圖標時(安裝完成界面點擊打開同理)
如果沒有對應Task任務棧存在,則會創建一個新的任務棧,
并且把應用啟動的首頁面作為根Activity放到任務棧中。
如果存在對應的Task任務棧,則會直接調用對應的Task任務棧到前臺,并將棧頂的界面顯示給用戶,
那么當我們的app啟動后打開SplashActivity并跳轉主界面MainActivity后,我們app的任務棧應該如圖所示:
此時,當我們點擊Home鍵退回到桌面,
app的Task任務棧進入后臺,然后我們點擊桌面上的啟動圖標,
正常情況下,app應該會把它對應的Task任務棧調到前臺,并顯示剛剛棧頂的MainActivity界面,
正常流程:
然而,實際情況是,app會把它的Task任務棧調用到前臺,
并在任務棧上重新創建新的SplashActivity ,再跳轉到MainActivity,
在不重新加載application的情況下,它又重新走了一遍啟動的流程,這個時候,我們會發現任務棧中的Activity重復了,SplashActivity跟MainActivity都變成了兩個
為了更清晰的讓大家理解,這里畫了兩個圖,
- 錯誤的bug流程
- 錯誤狀態下的Task任務棧
bug流程:
新調用的SplashActivity會被置于該app的task棧頂
多出了兩個Activity
當然這個bug一般用戶也很難注意到,它的產生必須滿足下面的條件:
- 點擊apk文件安裝app
- 安裝完成界面點擊打開按鈕
- 點擊Home鍵,進入系統桌面,此時app退到后臺
- 再點擊桌面上啟動圖標
那么對于這種問題我們如何來處理呢?
按照上文的舉例,
在正常流程下啟動app進入MainActivity界面時的任務棧:
bug情況下,會調起任務棧到前臺并添加根Acitivy SplashActivity到棧頂,此時的任務棧:
我們可以看到,在bug情況下啟動app時,SplashActivity(app的根Activity)再次創建并疊加到Task任務棧上了
理應只會出現在棧底的SplashActivity出現在了其他位置,所以這里我們直接判斷了app根Activity SplashActivity的位置
在app的SplashActivity(app的根Activity)的onCreate方法中通過 isTaskRoot() 方法來判斷是否是任務棧中的根Activity,如果是就不做任何處理,如果不是則直接finish掉;
public class SplashActivity extends BaseActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
setTheme(R.style.AppTheme_NoActionBar);
super.onCreate(savedInstanceState);
if (!isTaskRoot()) {
finish();
return;
}
}
}
這樣棧頂的SplashActivity在還未執行其他代碼的情況下就finish()掉了,此時會顯示棧頂的MainActivity。
Android包含Fragment界面的Activity界面,在app被系統釋放后,重新回到前臺時,重建Activity造成Fragment重疊
隨著功能需求的多樣化,Fragment的應用場景也是越來越廣,其中我們的首頁底欄可能是最常見的場景了。
那我們這里說的app在被系統釋放后,重回前臺Activity時,重建造成Fragment重疊又是怎么回事呢?
我們知道,要使用Fragment的Activity必須繼承v7的AppCompatActivity,
而AppCompatActivity繼承自FragmentActivity
當我們的app退到后臺處于容易被系統回收的狀態時,會觸發我們的onSaveInstanceState方法,
而使用Fragment的Activity會調用到父類FragmentActivity的onSaveInstanceState方法,
這里我截取FragmentActivity中onSaveInstanceState的關鍵代碼:
/**
* Save all appropriate fragment state.
*/
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
Parcelable p = mFragments.saveAllState();//獲取FragmentManager保存的所有Fragments
if (p != null) {
outState.putParcelable(FRAGMENTS_TAG, p);//Fragment不為空,執行保存操作
}
...
}
}
我們看到,這里的代碼把Fragment的狀態保存了下來,
而在FragmentActivity的onCreate方法中,又將這些Fragment重建了:
/**
* Perform initialization of all fragments and loaders.
*/
@SuppressWarnings("deprecation")
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
mFragments.attachHost(null /*parent*/);
super.onCreate(savedInstanceState);
...
if (savedInstanceState != null) {
Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG);
mFragments.restoreAllState(p, nc != null ? nc.fragments : null);
...
}
...
}
也就是說,界面因為被系統釋放后重建,重新觸發了Activity的onCreate方法,
如果開發人員沒有判斷onCreate的saveInstance變量調整創建邏輯,直接執行了Fragment的創建代碼,那新建的Fragment就會跟系統恢復的重疊。
這個問題一方面因為內存不足的極端情況下才會觸發(紅米等低端設備屬于常態,經常會釋放app),
另一方面由于部分開發的Fragment界面不是透明的,因此即使疊加了也不一定能發現這個問題。
那對于這樣的問題,我們如何處理呢,這里給出了三種處理方案:
1.在Activity的onCreate中判斷savedInstanceState變量是否為null,
如果savedInstanceState為null說明是界面是新建,則執行完整的fragment tab初始化工作;
如果savedInstanceState不為null,說明Activity是被釋放重建,那就不執行Fragment的創建,執行相關邏輯代碼,
代碼如下:
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
if (savedInstanceState == null) {
//界面正常情況下create時的邏輯
initTab();
}
else {
//界面在內存不足情況下被強制回收后重新create的邏輯
}
}
2.這個方法我稱之為懶人做法
使用了Fragment的Activity在調用onCreate方法時會首先調用super.onCreate()
而super.onCreate最終又會執行FragmentActivity的onCreate方法,
從上文截取的代碼中,我們看到,FragmentActivity的onCreate方法會判斷saveInstanceState里的Fragment是否為空,不為空就恢復保存的Fragment
if (savedInstanceState != null) {
Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG);
mFragments.restoreAllState(p, nc != null ? nc.fragments : null);
...
}
也就是說,我們在執行到這段代碼前把FRAGMENTS_TAG對應的值清空,那樣就不會觸發系統重建的恢復了
那么我們只需要在使用Fragment的Activity的onCreate方法添加以下代碼就可以了:
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
if (savedInstanceState != null) {
savedInstanceState.putParcelable("android:support:fragments", null);//清空保存Fragment的狀態數據
}
super.onCreate(savedInstanceState);
}
這樣,在執行到FragmentActivity的onCreate前,FRAGMENTS_TAG對應的數據就已經清空了。
3.同樣是懶人方法,直接重寫onSaveInstanceState方法,注釋掉super.onSaveInstanceState,這樣就不會保存Fragment的數據了,不過副作用也是非常明顯,就是onSaveInstanceState就完全失去作用了,
所以并不太推薦大家這么去做,僅做參考:
@Override
public void onSaveInstanceState(Bundle outState, PersistableBundle outPersistentState) {
// super.onSaveInstanceState(outState, outPersistentState);
}
關于模擬app被釋放的場景,這里介紹個小方法,就是在app運行之后,按home鍵退到后臺,然后打開電腦命令行工具,運行:
adb shell am kill 包名packagename
此時app就會被釋放,接著通過任務管理器或者啟動圖標打開app,這個時候剛剛的界面就會重建走onRestoreInstanceState了。
app調用系統相機后,拍照返回崩潰
一般情況下,我們大部分情況是通過傳遞uri的方式來調用系統相機的:
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
mTakePhotoUri = FileUtils.getOutputMediaFileUri(FileUtils.MEDIA_TYPE_IMAGE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, mTakePhotoUri);
startActivityForResult(intent, CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE);
這種通過指定uri存儲路徑的方式調用系統相機的方式
在onActivityResult的時候,返回的intent會沒有數據
因此我們一般都是在onActivityResult里獲取之前保留的uri(例子中的mTakePhotoUri,這個變量是個全局變量)變量來獲取具體圖片文件。
正式因為這個問題,導致不管調用系統相機導致app退到后臺被釋放
還是三星之類的手機調用相機時的自動旋轉
都會導致調用相機的界面被釋放并重建,從而使得Activity界面的全局變量值丟失。
如果沒有在onSaveInstanceState里保存這個全局變量,在onRestoreInstanceState取回mTakePhotoUri的值,那重建之后的界面變量就丟失了,因此onActivityResult中取到的mTakePhotoUri就為null了,從而導致獲取圖片路徑變量的時候報null。
經過測試,經過這樣的處理后,大部分相機的崩潰問題都得以解決。
其實不僅是相機,很多功能在實際開發過程中都可能遇到因界面被釋放導致變量數據丟失的情況,所以我們需要在onSaveInstanceState方法中根據實際情況來保存需要的變量,在onRestoreInstanceState方法中取回變量。
當然如果覺得太麻煩,這里給大家推薦一個懶人庫,可以自動保存我們的變量,非常方便
https://github.com/frankiesardo/icepick
在Android 4.1等設備上使用EventBus報caused by: java.lang.ClassNotFoundException: Didn’t find class “android.os.PersistableBundle” on path: DexPathList
這個問題我只在Android 4.1的設備上發生過,在其他設備上均未報錯
而造成這個錯誤的原因是我在無意中重寫了 onSaveInstanceState(Bundle outState, PersistableBundle outPersistentState) 這個方法
(正常情況下應該重寫 onSaveInstanceState(Bundle outState))
如果你的手頭沒有4.1的設備,這個問題可能一直發現不了
引入圖片框架fresco后,出現is 32-bit instead of 64-bit的錯誤
這個問題主要由于Android系統對于so文件的加載機制造成的
不同CPU架構的手機加載時會在libs下找自己對應的目錄,從對應的目錄下尋找需要的.so文件;如果沒有對應的目錄,就會去armeabi下去尋找,如果已經有對應的目錄,但是如果沒有找到對應的.so文件,也不會去armeabi下去尋找了。
我的項目只引用armeabi和 x86架構的so文件,這里我們假設為lib.so文件
當我使用一臺arm64-v8架構的手機時,因為找不到arm64-v8對應的目錄,因此系統會降級到armeabi中去查找lib.so文件。
而fresco圖片框架因為考慮到了so的兼容性,compile引入編譯的時候自帶了arm64-v8的so文件,因此產生了一個arm64-v8的目錄。
當項目打包編譯安裝后,arm64-v8架構的手機因為查找到了arm64-v8的目錄,因此所有的so文件都會到arm64-v8的目錄下查找,不會再去查找armeabi目錄,而在arm64-v8的目錄下,我并沒有配置對應的lib.so文件,所以找不到lib.so文件,隨即拋出is 32-bit instead of 64-bit的錯誤。
那我們如何解決了,這里介紹三種方法:
為項目已經引用的so庫添加對應arm64-v8架構的so庫,對于沒有源碼的情況下很難去配置編譯對應版本的so文件;
刪除引用的庫的arm64-v8目錄的so文件;
在gradle的defaultConfig中設置
ndk {
// 設置支持的 SO 庫構架,注意這里要根據你的實際情況來設置
abiFilters 'armeabi' , 'x86'
}
這樣就固定只會打包armeabi和x86目錄的so文件了,這么做可以防止在使用不熟悉的庫的時候不小心引入了其他目錄的so文件造成app報錯
Android app的實際開發過程中還有各種各樣奇怪的問題,如果你也遇到了一些特殊或者奇葩的bug,歡迎進行補充