Android之點選Home鍵後再次開啟導致APP重啟問題
問題描述:
1、開發者打包釋出一個release版本app,頁面結構如下:閃屏頁SplashActivity---> 登入頁LoginActivity---> 主頁MainActivity
2、使用者下載app到手機,通過檔案管理器找到並安裝這個apk,安裝後提示:“安裝完成,你可以開啟xxx應用了”,
3、使用者開啟app,輸入賬號密碼跳轉到了主頁MainActivity。
4、使用者按下Home鍵,然後在程式列表點選app,
先後顯示:閃屏頁SplashActivity---> 登入頁LoginActivity,APP重啟了!
期待頁面:顯示原先的主頁MainActivity。
奇怪的是:真機在debug開發除錯時不會出現這個問題。
錯誤原因:
debug版本是通過adb安裝啟動或平常的桌面Icon圖示啟動,
release版本是安裝這類第三方平臺啟動。
兩者的啟動intent不相同!(相同是指:啟動類,action、category等等全部一樣,不可多項也不可缺少)
在解決問題前,先了解一下相關知識:
1、Home主介面其實也是一個Activity。當從APP介面按下Home鍵盤,實際是啟動APP跳轉到Home主介面,這樣我們的程式就被置於後臺,被這個Home主介面Activity覆蓋。
2、Activity的Task管理
Android系統的App啟動與切換管理依賴於相關Activity的Task的管理。一個Task之中可能含有若干個Activity,為了簡便起見,我們這裡記錄
【Task A】的Activity分別為 【A1】 、【A2】等,
【Task B】的Activity分別為 【B1】 、【B2】。
那麼我們來分析下App之間是怎麼切換的。假設應用都是單Task應用(相對於大部分的普通App來說,都是採用單一Task來管理的)
桌面程式App:【TaskA】 ---- 存在Activity有【A1】 ---- 其棧的結構為 A1
應用程式B:【TaskB】 ---- 存在Activity有【B1】【B2】 ---- 其棧的結構為 B1_B2
應用程式C: 【TaskC】 ---- 存在Activity有【C1】【C2】 ---- 其棧的結構為 C1_C2
a、那麼我們進入桌面時:Task之間的結構是 A1 ---- 也就是隻有一個【TaskA】棧(桌面Task),並且位於最前端(這裡表現為最後新增的末端)
b、然後我們點選應用程式B的圖示,啟動B :Task之間的結構是 A1B1B2 ---- 新增了一個【TaskB】,而且【TaskB】也是位於最前端,現在顯示的是【TaskB】的B2的Activity的介面
c、接著點選home鍵: Android對於home做了特殊預設處理,就是會把桌面Task挪到所以Task最前端,Task結構應該變成 B1_B2_A1 ---- 【TaskA】挪到佇列最前端,現在顯示的是【TaskA】的A1的Activity的介面,也就是桌面
d、我們再在桌面點選應用程式C的圖示,啟動C : Task之間的結構變成 B1B2A1C1C2 ---- 新增了一個【TaskC】,而且【TaskC】也是位於最前端,現在顯示的是【TaskC】的C2的Activity的介面
從上面的例子,我們可以知道:
我們編寫任何一個Activity的時候,都可以在AndroidManifest裡面顯式指定一個taskAffinity的屬性,也就是說該Activity歸屬於對應taskAffinity的棧;如果沒有指定任何taskAffinity,那麼該Activity將會直接歸屬於包名所在的Task之下。而我們啟動一個Activity時(這裡只討論standard啟動模式),那麼回去先搜尋對應的Task是否存在,如果不存在,新建一個Task並將Activity入棧,如果已經存在對應的Task,那麼直接在對應Task入棧即可。
那麼問題來了:如果我們在上面第d步點選的圖片並不是程式C的圖示,而是重新點選了程式B的圖示,此時【TaskB】是已經存在的了,那麼為了不會講B的入口activity(B1)直接在【TaskB】入棧,而是將【TaskB】挪到前臺並不做任何Activity啟動的操作呢?
3、桌面的啟動管理:
回頭研究下AndroidManifest這個檔案,我們輕而易舉發現,但凡是App入口Activity,那麼一定會包含
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
這幾行程式碼。這裡到底有什麼玄機呢?其實這個就是跟桌面約定好的啟動攔截過濾器。因為桌面有一個很明顯的需求就是,如果我們再次點選已經在後臺的App圖示時,是應該將該後臺任務挪到前臺而不是再次啟動該App程式。
而從柯元旦所著的《android核心剖析》一書中有記錄如下規則:
每次啟動Intent導致新建立Task的時候,該Task會記錄導致其建立的Intent;而如果後續需要有一個新的與建立Intent完全一致(完全一致定位為:啟動類,action、category等等全部一樣,不可多項也不可缺少),那麼該Intent並不會觸發Activity的新建啟動,而只會將已經存在的對應Task移到前臺;這也就是為什麼桌面會在再次點選圖示時將後臺任務挪到前臺而不是重新啟動App的實現。
那麼為啥要指定入口Activity特定的action和category呢,那就是為了讓桌面啟動app所用的Intent具有特殊性,也就是新增了特別的攔截器,避免其他應用內或者應用間的Intent對於這個啟動方式的干擾。
說了這麼多,我們可以著手分析上續bug的產生原因了。
原理剖析
檔案管理器雖然使用Intent來啟動剛剛安裝的那個App,但:它的啟動Intent並沒有跟桌面的啟動Intent完全一致!
我們將桌面的Task記為【TaskDesktop】,檔案管理器的Task記為【TaskFile】,我們應用的Task記為【TaskApp】,分析如下:
進入桌面: D1 ---- D1是單純的桌面
開啟檔案管理器: D1_F1_F2 ---- F2是安裝完畢後詢問是否啟動對應程式的Activity
點選開啟: D1_F1_F2_A1_A2 ---- A1是入口閃屏頁,A2是登入Activity
返回桌面: F1_F2_A1_A2_D1 ---- 回到桌面頁,也就是D1前置
點選A的圖示: F1_F2_D1_A1_A2_A1 ---- 找到【TaskA】,挪到前臺,由於比對Intent並不是完全一致,所以該請求是新啟動Activity,那麼把A1新增到對應的【TaskA】中
所以bug出現了,出現了再一次的閃屏頁【A1】,問題定位成功!
PS:這裡我稍微變種一下,因為一般我們閃屏頁都是在啟動登入頁後finish的,而登入頁一般是singleTask模式
開啟檔案管理器: D1_F1_F2 ---- F2是安裝完畢後詢問是否啟動對應程式的Activity
點選開啟: D1_F1_F2_A2 ---- A1是入口閃屏頁,A2是登入Activity,啟動後A1業務邏輯應該finish掉,所以從【TaskA】中挪去
返回桌面: F1_F2_A2_D1 ---- 回到桌面頁,也就是D1前置
點選A的圖示: F1_F2_D1_A2_A1 -> 找到【TaskA】,挪到前臺,由於比對啟動的Intent不完全一致,所以新建立一個A1 Activity,那麼把A1新增到對應的【TaskA】中,然後A1所再一次觸發啟動登入頁 A2,但是登入頁是singleTask模式,所以又回到了上次對應的A2登入頁,所以現象為再一次出現閃屏頁,然後回到原先的登入頁介面。
解決思路
正常啟動的閃屏頁Activity必定在【TaskA】的最底部(實際已finish掉被登入頁取代),而第二次閃屏Activity不可能位於Task的最底部,所以在閃屏頁Activity的onCreate程式碼:
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 避免從桌面啟動程式後,會重新例項化入口類的activity
if (!this.isTaskRoot()) {
Intent intent = getIntent();
if (intent != null) {
String action = intent.getAction();
if (intent.hasCategory(Intent.CATEGORY_LAUNCHER) && Intent.ACTION_MAIN.equals(action)) {
finish();
return;
}
}
}
}
也可以這樣修改:
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//首次啟動 Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT 為 0,再次點選圖示啟動時就不為零了
if ((getIntent().getFlags() & Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT) != 0) {
finish();
return;
}
}
這兩種方法在 setContentView() 方法之前和之後都可以。
讀到這裡,細心的讀者一定會問:你上面說的情形只適用閃屏頁和登入頁,如果登入進去主頁MainActivity按Home鍵,如何處理呢?
1、閃屏頁的OnCreate方法根據登入狀態判斷跳轉:
if ((getIntent().getFlags() & Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT) != 0) {
if (UserInfo.getInstance().isLogined) { // 若已登入,跳轉到主頁
readyGoThenKill(MainActivity.class);
} else { // 若未登入,引導使用者登入
readyGoThenKill(UserLoginActivity.class);
}
return;
}
2、設定MainActivity的launchMode="singleTask",在AndroidManifest.xml修改
<activity
android:name="com.emp.frame.MainActivity"
android:launchMode="singleTask" />
PS:如果APP頁面複雜、路徑很深,要所有頁面都實現:按Home鍵---再次點選app圖示---恢復原來頁面狀態不重啟,暫時沒有太好的方法,只能像上面一樣逐一指定launchMode=“singleTask”及跳轉判斷,歡迎有其他方法的朋友留言分享,謝謝!
參考:碼農叔叔(enjoy風鈴)出處:http://www.cnblogs.com/net168/
相關文章
- VXFS啟用非同步IO導致的嚴重問題非同步
- 記一次 Mac 意外重啟導致的 Homestead 問題Mac
- 定時重啟tomcat指令碼導致的亂碼問題Tomcat指令碼
- mac 關閉app後,點選dock上的app重新開啟MacAPP
- Android Home鍵之後啟動Activity延遲5sAndroid
- 關於沒有熔斷降級導致服務重啟問題
- Linux,Network manager 導致節點異常重啟Linux
- Android 面向切面程式設計 AOP 解決連續點選開啟重複頁面問題Android程式設計
- Linux主機名修改後導致mysql重啟失敗LinuxMySql
- iptables 重啟後ftp 策略失效的問題FTP
- oracle AS重啟問題Oracle
- 使用Nginx解決IIS繫結域名導致應用程式重啟的問題Nginx
- genymotion中app開啟後螢幕是倒的問題APP
- IP地址被清空導致例項重啟
- API返回延遲,FPM重啟後恢復之後又重現 問題解決方案API
- LightDB/Postgresql 記錄客戶端啟動版本問題導致啟動失敗問題SQL客戶端
- undo表空間出現壞塊導致資料庫重啟問題解決資料庫
- 解決hyper v導致docker無法啟動問題Docker
- 同時開啟節點導致資料DDL操作慢 ??
- service network restart 命令使用時導致叢集該節點重啟REST
- 故障的機器修好後重啟,狂拉主庫binlog,導致網路問題,造成一定影響
- Centos修改DNS重啟或者重啟network服務後丟失問題處理CentOSDNS
- eclipse 異常關閉後, 再開啟時閃退,導致無法再開啟Eclipse
- oracle兩節點RAC,由於gipc導致某節點crs無法啟動問題分析Oracle
- WPF App後臺檔案彈窗導致奇怪的問題APP
- Android 開啟其他 appAndroidAPP
- iPhone8虛擬Home鍵開啟方法 iPhone8小白點怎麼開啟?iPhone
- 歸檔問題導致的資料庫無法啟動資料庫
- 一次oracle 節點重啟問題的定位Oracle
- iOS App從點選到啟動iOSAPP
- 記php-fpm重啟導致的一個bugPHP
- MySQL 5.6因為OOM導致資料庫重啟MySqlOOM資料庫
- 微軟修復了導致 Outlook 啟動時崩潰的問題微軟
- jdk版本導致tomcat,eclipse無法啟動的問題JDKTomcatEclipse
- 滑鼠問題導致筆記本開機點選桌面和工作列無反應筆記
- 記一次dlopen使用問題導致Framework重啟,tombstones、pmap與反彙編分析(上)Framework
- android問題之Button自定義樣式selector後,點選無效果Android
- AIX 5.3 重啟系統後VG PERMISSION被改變導致Oracle10.2.0.5叢集啟動失敗AIOracle