在Android的app開(kāi)發(fā)過(guò)程中,除了機(jī)型適配等問(wèn)題,常常還會(huì)出一些特殊的bug景醇,這些bug往往需要特殊的場(chǎng)景情況下才會(huì)發(fā)生,這里羅列了一些平時(shí)項(xiàng)目中遇到的問(wèn)題及注意點(diǎn)吝岭。
App打包apk安裝后重復(fù)啟動(dòng)根界面的問(wèn)題
這個(gè)問(wèn)題很特殊三痰,一般情況下很難被發(fā)現(xiàn),是Android系統(tǒng)一直以來(lái)的一個(gè)Bug窜管。
當(dāng)我們把a(bǔ)pp打包成apk安裝程序散劫,通過(guò)點(diǎn)擊apk文件進(jìn)行安裝時(shí),會(huì)啟動(dòng)安裝界面幕帆,
并在安裝成功后會(huì)跳轉(zhuǎn)安裝完成界面获搏,
如圖:
我們點(diǎn)擊圖中的 打開(kāi)按鈕,此時(shí)會(huì)啟動(dòng)我們的app
這里為了讓大家更容易理解一些失乾,
我們假設(shè)app有兩個(gè)界面
- 啟動(dòng)界面SplashActivity
- 主界面MainActivity
- app啟動(dòng)后打開(kāi)SplashActivity常熙,3秒后自動(dòng)跳轉(zhuǎn)MainActivity,界面不做強(qiáng)制finish
接下來(lái),我們需要了解下Task任務(wù)棧和Back Stack返回棧碱茁,
如果有同學(xué)對(duì)這兩個(gè)概念還不熟悉的裸卫,
可以看一下官方文檔,講得很詳細(xì):
這里我們引用官方文檔的一句話(huà):
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.
當(dāng)我們點(diǎn)擊home界面的應(yīng)用啟動(dòng)圖標(biāo)時(shí)(安裝完成界面點(diǎn)擊打開(kāi)同理)
如果沒(méi)有對(duì)應(yīng)Task任務(wù)棧存在纽竣,則會(huì)創(chuàng)建一個(gè)新的任務(wù)棧墓贿,
并且把應(yīng)用啟動(dòng)的首頁(yè)面作為根Activity放到任務(wù)棧中。
如果存在對(duì)應(yīng)的Task任務(wù)棧蜓氨,則會(huì)直接調(diào)用對(duì)應(yīng)的Task任務(wù)棧到前臺(tái)募壕,并將棧頂?shù)慕缑骘@示給用戶(hù),
那么當(dāng)我們的app啟動(dòng)后打開(kāi)SplashActivity并跳轉(zhuǎn)主界面MainActivity后,我們app的任務(wù)棧應(yīng)該如圖所示:
此時(shí)语盈,當(dāng)我們點(diǎn)擊Home鍵退回到桌面,
app的Task任務(wù)棧進(jìn)入后臺(tái)缰泡,然后我們點(diǎn)擊桌面上的啟動(dòng)圖標(biāo)刀荒,
正常情況下,app應(yīng)該會(huì)把它對(duì)應(yīng)的Task任務(wù)棧調(diào)到前臺(tái)棘钞,并顯示剛剛棧頂?shù)腗ainActivity界面缠借,
正常流程:
然而,實(shí)際情況是宜猜,app會(huì)把它的Task任務(wù)棧調(diào)用到前臺(tái)泼返,
并在任務(wù)棧上重新創(chuàng)建新的SplashActivity ,再跳轉(zhuǎn)到MainActivity姨拥,
在不重新加載application的情況下绅喉,它又重新走了一遍啟動(dòng)的流程渠鸽,這個(gè)時(shí)候,我們會(huì)發(fā)現(xiàn)任務(wù)棧中的Activity重復(fù)了柴罐,SplashActivity跟MainActivity都變成了兩個(gè)
為了更清晰的讓大家理解徽缚,這里畫(huà)了兩個(gè)圖,
- 錯(cuò)誤的bug流程
- 錯(cuò)誤狀態(tài)下的Task任務(wù)棧
bug流程:
新調(diào)用的SplashActivity會(huì)被置于該app的task棧頂
多出了兩個(gè)Activity
當(dāng)然這個(gè)bug一般用戶(hù)也很難注意到革屠,它的產(chǎn)生必須滿(mǎn)足下面的條件:
- 點(diǎn)擊apk文件安裝app
- 安裝完成界面點(diǎn)擊打開(kāi)按鈕
- 點(diǎn)擊Home鍵凿试,進(jìn)入系統(tǒng)桌面,此時(shí)app退到后臺(tái)
- 再點(diǎn)擊桌面上啟動(dòng)圖標(biāo)
那么對(duì)于這種問(wèn)題我們?nèi)绾蝸?lái)處理呢?
按照上文的舉例似芝,
在正常流程下啟動(dòng)app進(jìn)入MainActivity界面時(shí)的任務(wù)棧:
bug情況下那婉,會(huì)調(diào)起任務(wù)棧到前臺(tái)并添加根Acitivy SplashActivity到棧頂,此時(shí)的任務(wù)棧:
我們可以看到党瓮,在bug情況下啟動(dòng)app時(shí)详炬,SplashActivity(app的根Activity)再次創(chuàng)建并疊加到Task任務(wù)棧上了
理應(yīng)只會(huì)出現(xiàn)在棧底的SplashActivity出現(xiàn)在了其他位置,所以這里我們直接判斷了app根Activity SplashActivity的位置
在app的SplashActivity(app的根Activity)的onCreate方法中通過(guò) isTaskRoot() 方法來(lái)判斷是否是任務(wù)棧中的根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;
}
}
}
這樣棧頂?shù)腟plashActivity在還未執(zhí)行其他代碼的情況下就finish()掉了,此時(shí)會(huì)顯示棧頂?shù)腗ainActivity蝇闭。
Android包含F(xiàn)ragment界面的Activity界面呻率,在app被系統(tǒng)釋放后,重新回到前臺(tái)時(shí)呻引,重建Activity造成Fragment重疊
隨著功能需求的多樣化礼仗,F(xiàn)ragment的應(yīng)用場(chǎng)景也是越來(lái)越廣,其中我們的首頁(yè)底欄可能是最常見(jiàn)的場(chǎng)景了逻悠。
那我們這里說(shuō)的app在被系統(tǒng)釋放后,重回前臺(tái)Activity時(shí)元践,重建造成Fragment重疊又是怎么回事呢?
我們知道童谒,要使用Fragment的Activity必須繼承v7的AppCompatActivity单旁,
而AppCompatActivity繼承自FragmentActivity
當(dāng)我們的app退到后臺(tái)處于容易被系統(tǒng)回收的狀態(tài)時(shí),會(huì)觸發(fā)我們的onSaveInstanceState方法饥伊,
而使用Fragment的Activity會(huì)調(diào)用到父類(lèi)FragmentActivity的onSaveInstanceState方法,
這里我截取FragmentActivity中onSaveInstanceState的關(guān)鍵代碼:
/**
* 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不為空象浑,執(zhí)行保存操作
}
...
}
}
我們看到,這里的代碼把Fragment的狀態(tài)保存了下來(lái)琅豆,
而在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);
...
}
...
}
也就是說(shuō),界面因?yàn)楸幌到y(tǒng)釋放后重建茫因,重新觸發(fā)了Activity的onCreate方法蚪拦,
如果開(kāi)發(fā)人員沒(méi)有判斷onCreate的saveInstance變量調(diào)整創(chuàng)建邏輯,直接執(zhí)行了Fragment的創(chuàng)建代碼,那新建的Fragment就會(huì)跟系統(tǒng)恢復(fù)的重疊驰贷。
這個(gè)問(wèn)題一方面因?yàn)閮?nèi)存不足的極端情況下才會(huì)觸發(fā)(紅米等低端設(shè)備屬于常態(tài)盛嘿,經(jīng)常會(huì)釋放app),
另一方面由于部分開(kāi)發(fā)的Fragment界面不是透明的饱苟,因此即使疊加了也不一定能發(fā)現(xiàn)這個(gè)問(wèn)題孩擂。
那對(duì)于這樣的問(wèn)題,我們?nèi)绾翁幚砟叵浒荆@里給出了三種處理方案:
1.在Activity的onCreate中判斷savedInstanceState變量是否為null类垦,
如果savedInstanceState為null說(shuō)明是界面是新建,則執(zhí)行完整的fragment tab初始化工作城须;
如果savedInstanceState不為null蚤认,說(shuō)明Activity是被釋放重建,那就不執(zhí)行Fragment的創(chuàng)建糕伐,執(zhí)行相關(guān)邏輯代碼砰琢,
代碼如下:
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
if (savedInstanceState == null) {
//界面正常情況下create時(shí)的邏輯
initTab();
}
else {
//界面在內(nèi)存不足情況下被強(qiáng)制回收后重新create的邏輯
}
}
2.這個(gè)方法我稱(chēng)之為懶人做法
使用了Fragment的Activity在調(diào)用onCreate方法時(shí)會(huì)首先調(diào)用super.onCreate()
而super.onCreate最終又會(huì)執(zhí)行FragmentActivity的onCreate方法,
從上文截取的代碼中良瞧,我們看到陪汽,F(xiàn)ragmentActivity的onCreate方法會(huì)判斷saveInstanceState里的Fragment是否為空,不為空就恢復(fù)保存的Fragment
if (savedInstanceState != null) {
Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG);
mFragments.restoreAllState(p, nc != null ? nc.fragments : null);
...
}
也就是說(shuō)褥蚯,我們?cè)趫?zhí)行到這段代碼前把FRAGMENTS_TAG對(duì)應(yīng)的值清空挚冤,那樣就不會(huì)觸發(fā)系統(tǒng)重建的恢復(fù)了
那么我們只需要在使用Fragment的Activity的onCreate方法添加以下代碼就可以了:
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
if (savedInstanceState != null) {
savedInstanceState.putParcelable("android:support:fragments", null);//清空保存Fragment的狀態(tài)數(shù)據(jù)
}
super.onCreate(savedInstanceState);
}
這樣,在執(zhí)行到FragmentActivity的onCreate前赞庶,F(xiàn)RAGMENTS_TAG對(duì)應(yīng)的數(shù)據(jù)就已經(jīng)清空了训挡。
3.同樣是懶人方法,直接重寫(xiě)onSaveInstanceState方法歧强,注釋掉super.onSaveInstanceState澜薄,這樣就不會(huì)保存Fragment的數(shù)據(jù)了,不過(guò)副作用也是非常明顯摊册,就是onSaveInstanceState就完全失去作用了肤京,
所以并不太推薦大家這么去做,僅做參考:
@Override
public void onSaveInstanceState(Bundle outState, PersistableBundle outPersistentState) {
// super.onSaveInstanceState(outState, outPersistentState);
}
關(guān)于模擬app被釋放的場(chǎng)景茅特,這里介紹個(gè)小方法蟆沫,就是在app運(yùn)行之后,按home鍵退到后臺(tái)温治,然后打開(kāi)電腦命令行工具,運(yùn)行:
adb shell am kill 包名packagename
此時(shí)app就會(huì)被釋放,接著通過(guò)任務(wù)管理器或者啟動(dòng)圖標(biāo)打開(kāi)app戒悠,這個(gè)時(shí)候剛剛的界面就會(huì)重建走onRestoreInstanceState了熬荆。
app調(diào)用系統(tǒng)相機(jī)后,拍照返回崩潰
一般情況下绸狐,我們大部分情況是通過(guò)傳遞uri的方式來(lái)調(diào)用系統(tǒng)相機(jī)的:
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);
這種通過(guò)指定uri存儲(chǔ)路徑的方式調(diào)用系統(tǒng)相機(jī)的方式
在onActivityResult的時(shí)候卤恳,返回的intent會(huì)沒(méi)有數(shù)據(jù)
因此我們一般都是在onActivityResult里獲取之前保留的uri(例子中的mTakePhotoUri累盗,這個(gè)變量是個(gè)全局變量)變量來(lái)獲取具體圖片文件。
正式因?yàn)檫@個(gè)問(wèn)題突琳,導(dǎo)致不管調(diào)用系統(tǒng)相機(jī)導(dǎo)致app退到后臺(tái)被釋放
還是三星之類(lèi)的手機(jī)調(diào)用相機(jī)時(shí)的自動(dòng)旋轉(zhuǎn)
都會(huì)導(dǎo)致調(diào)用相機(jī)的界面被釋放并重建若债,從而使得Activity界面的全局變量值丟失。
如果沒(méi)有在onSaveInstanceState里保存這個(gè)全局變量拆融,在onRestoreInstanceState取回mTakePhotoUri的值蠢琳,那重建之后的界面變量就丟失了,因此onActivityResult中取到的mTakePhotoUri就為null了镜豹,從而導(dǎo)致獲取圖片路徑變量的時(shí)候報(bào)null傲须。
經(jīng)過(guò)測(cè)試,經(jīng)過(guò)這樣的處理后趟脂,大部分相機(jī)的崩潰問(wèn)題都得以解決泰讽。
其實(shí)不僅是相機(jī),很多功能在實(shí)際開(kāi)發(fā)過(guò)程中都可能遇到因界面被釋放導(dǎo)致變量數(shù)據(jù)丟失的情況昔期,所以我們需要在onSaveInstanceState方法中根據(jù)實(shí)際情況來(lái)保存需要的變量已卸,在onRestoreInstanceState方法中取回變量。
當(dāng)然如果覺(jué)得太麻煩硼一,這里給大家推薦一個(gè)懶人庫(kù)累澡,可以自動(dòng)保存我們的變量,非常方便
https://github.com/frankiesardo/icepick
在Android 4.1等設(shè)備上使用EventBus報(bào)caused by: java.lang.ClassNotFoundException: Didn’t find class “android.os.PersistableBundle” on path: DexPathList
這個(gè)問(wèn)題我只在Android 4.1的設(shè)備上發(fā)生過(guò)欠动,在其他設(shè)備上均未報(bào)錯(cuò)
而造成這個(gè)錯(cuò)誤的原因是我在無(wú)意中重寫(xiě)了 onSaveInstanceState(Bundle outState, PersistableBundle outPersistentState) 這個(gè)方法
(正常情況下應(yīng)該重寫(xiě) onSaveInstanceState(Bundle outState))
如果你的手頭沒(méi)有4.1的設(shè)備永乌,這個(gè)問(wèn)題可能一直發(fā)現(xiàn)不了
引入圖片框架fresco后,出現(xiàn)is 32-bit instead of 64-bit的錯(cuò)誤
這個(gè)問(wèn)題主要由于Android系統(tǒng)對(duì)于so文件的加載機(jī)制造成的
不同CPU架構(gòu)的手機(jī)加載時(shí)會(huì)在libs下找自己對(duì)應(yīng)的目錄具伍,從對(duì)應(yīng)的目錄下尋找需要的.so文件翅雏;如果沒(méi)有對(duì)應(yīng)的目錄,就會(huì)去armeabi下去尋找人芽,如果已經(jīng)有對(duì)應(yīng)的目錄望几,但是如果沒(méi)有找到對(duì)應(yīng)的.so文件,也不會(huì)去armeabi下去尋找了萤厅。
我的項(xiàng)目只引用armeabi和 x86架構(gòu)的so文件橄抹,這里我們假設(shè)為lib.so文件
當(dāng)我使用一臺(tái)arm64-v8架構(gòu)的手機(jī)時(shí),因?yàn)檎也坏絘rm64-v8對(duì)應(yīng)的目錄惕味,因此系統(tǒng)會(huì)降級(jí)到armeabi中去查找lib.so文件楼誓。
而fresco圖片框架因?yàn)榭紤]到了so的兼容性,compile引入編譯的時(shí)候自帶了arm64-v8的so文件名挥,因此產(chǎn)生了一個(gè)arm64-v8的目錄疟羹。
當(dāng)項(xiàng)目打包編譯安裝后,arm64-v8架構(gòu)的手機(jī)因?yàn)椴檎业搅薬rm64-v8的目錄,因此所有的so文件都會(huì)到arm64-v8的目錄下查找榄融,不會(huì)再去查找armeabi目錄参淫,而在arm64-v8的目錄下,我并沒(méi)有配置對(duì)應(yīng)的lib.so文件愧杯,所以找不到lib.so文件涎才,隨即拋出is 32-bit instead of 64-bit的錯(cuò)誤。
那我們?nèi)绾谓鉀Q了力九,這里介紹三種方法:
為項(xiàng)目已經(jīng)引用的so庫(kù)添加對(duì)應(yīng)arm64-v8架構(gòu)的so庫(kù)耍铜,對(duì)于沒(méi)有源碼的情況下很難去配置編譯對(duì)應(yīng)版本的so文件;
刪除引用的庫(kù)的arm64-v8目錄的so文件畏邢;
在gradle的defaultConfig中設(shè)置
ndk {
// 設(shè)置支持的 SO 庫(kù)構(gòu)架业扒,注意這里要根據(jù)你的實(shí)際情況來(lái)設(shè)置
abiFilters 'armeabi' , 'x86'
}
這樣就固定只會(huì)打包armeabi和x86目錄的so文件了,這么做可以防止在使用不熟悉的庫(kù)的時(shí)候不小心引入了其他目錄的so文件造成app報(bào)錯(cuò)
Android app的實(shí)際開(kāi)發(fā)過(guò)程中還有各種各樣奇怪的問(wèn)題舒萎,如果你也遇到了一些特殊或者奇葩的bug程储,歡迎進(jìn)行補(bǔ)充