Android應用運行在后臺的時候,經常被系統的LowMemoryKiller殺掉,當用戶再次點擊icon或者從最近的任務列表啟動的時候,進程會被重建,并且恢復被殺之前的現場。什么意思呢?假如APP在被殺之前的Activity堆棧是這樣的,A<B<C,C位于最上層
APP被后臺殺死后,APP端進程被銷毀了,也就不存在什么Activity了,也就沒有什么Activity堆棧,不過AMS的卻是被保留了下來:
當用戶再次啟動APP時候會怎么樣呢?這個時候,首先看到其實C,而不是棧底部的A,也就是說往往被殺死后,恢復看到的第一個界面是用戶最后見到的那個界面。
而用戶點擊返回,看到的就是上一個界面B,其次是A
之所以這樣是因為APP端Activity的創建其實都是由AMS管理的,AMS嚴格維護這APP端Activity對應的ActivityRecord棧,可以看做當前APP的場景,不過,APP端Activity的銷毀同AMS端ActivityRecord的銷毀并不一定是同步的,最明顯的就是后臺殺死這種場景。Android為了能夠讓用戶無感知后臺殺死,就做了這種恢復邏輯,不過,在開發中,這種邏輯帶了的問題確實多種多樣,甚至有些產品就不希望走恢復流程,本文就說說如何避免走恢復流程。結合常見的開發場景,這里分為兩種,一種是針對推送喚起APP,一種是針對從最近任務列表喚起APP(或者icon)。
正常啟動
從最近任務列表跟Laucher是有區別的,區別在與根Intent 會影響從最近的任務列表的表現
從最近的任務列表或者Lancher如何重新走閃屏
前提:對于后臺殺死的狀況,不考慮路由。
首先,APP端必須知道當前Activity的啟動是不是在走恢復流程,Activity有一個onCreate方法,在ActivityThread新建Activity之后,會回調該函數,如果是從后臺殺死恢復來的,回調onCreate的時候會傳遞一個非空的Bundle savedInstanceState給當前Activity,只要判斷這個非空就能知道是否是恢復流程。
做法一 所有Activity一視同仁
public abstract class BaseActivity<T extends BasePresenter>
extends AppCompatActivity
{
@Override
protected void onCreate(Bundle savedInstanceState) {
if (savedInstanceState != null) {
savedInstanceState.clear();
<!--重新走閃屏邏輯-->
SplashActivity.startClearLastTask(this);
isValidActivity = false;
}
super.onCreate(savedInstanceState);
}
}
知道恢復流程之后,如何處理呢?其實很簡單,直接吊起閃屏頁就可以了,不過這里有一點要注意的是,在啟動閃屏頁面的時候,必須要設置其IntentFlag:Intent.FLAG_ACTIVITY_NEW_TASK|Intent.FLAG_ACTIVITY_CLEAR_TASK,這樣做的理由是為了清理之前的場景,不然之前的ActivityRecord棧仍然保留在ActivityManagerService中,返回上一頁可能會返回到被殺死前的界面(除非有特殊需求,一般模擬全新冷啟動):
public static void startClearLastTask(Activity restoreActivity) {
Intent intent = new Intent(restoreActivity, SplashActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
restoreActivity.startActivity(intent);
restoreActivity.finish();
}
設置Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK主要是為了獲取一個干凈的新環境。當然如果你不設置也是有不設置的做法的。比如單獨將首頁路由界面過濾出來,對于首頁的不finish。但是對于首頁打開的閃屏頁就不應該再start MainActivity了,不然會接著調用onNewIntent,不是多余嗎。對于MainActivity而言,被殺死的場景無需處理route邏輯。
public class MainActivity extends BaseActivity {
<!--為存活處理-->
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState == null) {
route(getIntent());
}
}
<!--存活處理-->
@Override
public void onNewIntent(Intent intent) {
super.onNewIntent(intent);
route(getIntent());
}
如果MainActivity是通過推送或者其他方式直接啟動的,那么intent中必定設置了數據,這種情況就需要先打開閃屏頁,然后由閃屏頁Finish后,再跳轉推送目標頁面。
做法二 單獨處理MainActivity(路由Activity)
public class BaseActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
LogUtil.v("lishang", "onCreate --" + this);
// savedInstanceState 非空就說明被殺了
if (savedInstanceState != null) {
savedInstanceState.clear();
if (this instanceof MainPageActivity) {
SplashActivity.startFormRestoreMainActivity(this);
} else {
SplashActivity.startClearLastTask(this);
}
isValidActivity = false;
}
// 推送或者其他的界面借助MainPage路由跳板,需要額外處理
super.onCreate(savedInstanceState);
}
}
做的特殊處理就是對于后臺殺死的時候是只有MainActivity存活的情況下,則允許MainActivity走恢復流程,只不過再onCreate中先走一遍閃屏,同時由于后臺殺死不走路由,那么這個情況下我們是可以做到少啟動一次MainActivity。
// 殺死走恢復流程時候,主動截斷
public static void startFormRestoreMainActivity(Activity restoreActivity) {
Intent intent = new Intent(restoreActivity, SplashActivity.class);
<!--標識從其他頁面啟動的,這樣就不需要主動startMainActivity-->
intent.putExtra("FromResoreMainPage",true);
restoreActivity.startActivity(intent);
}
這樣也能達到預期效果,同時也不會創建兩個MainActivity,如果從閃屏頁跳轉其他界面,則需要額外處理,不過要特別注意的是:MainActivity在恢復的時候會先走完onCreate跟onResume可能會降低閃屏頁的可見速度
從推送或者DeepLink喚起被殺APP時,如何走閃屏邏輯
首先思考個問題:推送如何路由?,可以通過啟動一個Service,然后在我們APP中優雅的處理路由跳轉,這算是個不錯的方案。因為存在冷啟動任然需要啟動首頁場景,一般而言推送跟DeepLink會統一通過MainActivity路由。
發送一個可以啟動Service的通知
void notify() {
final NotificationCompat.Builder builder = new NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID);
builder.setContentIntent(PendingIntent.getService(this, (int) System.currentTimeMillis(),
new Intent(this, PushService.class),
PendingIntent.FLAG_UPDATE_CURRENT))
.setContentText("content")
.setContentTitle("title")
.setContentInfo("content")
.setWhen(System.currentTimeMillis())
.setShowWhen(true)
.setSmallIcon(R.mipmap.ic_launcher)
.setAutoCancel(true)
.setOngoing(false);
final NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID,
"Channel human readable title",
NotificationManager.IMPORTANCE_DEFAULT);
if (nm != null) {
nm.createNotificationChannel(channel);
}
}
nm.notify((int) System.currentTimeMillis(), builder.build());
}
在PushService中處理路由
public class PushService extends Service {
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if(hasLiveActivity){
Intent intent=getRouterActivityIntent();
intent.addFlags( Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent)
}else{
Intent intent=getMainActivityIntent();
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent)
}
return START_STICKY ;
}
}
對于路由的處理分兩種:在MainActivity存活的情況下,可以直接跳轉target頁面;如果不存在則需要喚起MainActivity,并由其作為路由跳板,因為只有這么處理,關閉推送target界面后才能返回主界面。這里只看MainActivity不存活的情況:推送分三步:
- 跳轉MainActivity
- startActivityForResult打開閃屏頁
- 跳轉推送目標頁面
如何判斷呢,正產流程下,啟動后,rootActivity就是MainActivity,其實只需要判斷是否有Activity存活即可,也就是查查APP的topActivity是否為null,注意不要去向AMS查詢,而是在本地進程中查詢,因為AMS在后臺殺敵的場景下是有堆棧保存的。可以通過反射查詢ActivityThread的mActivities,也可以根據自己維護的Activity堆棧來判斷,判斷沒有存活Activity的前提下,就跳轉主頁面去路由。 在MainActivity的路由中,需要準確區分是否是推送跳轉進來的,如果不是推送跳轉進來,就不需要什么特殊處理,如果是推送跳轉進來一定會攜帶跳轉scheme數據,可以根據是否攜帶數據做區分下,當然也有其他處理方式,如果是MainActivity不存活,路由啟動的場景,則需要打開閃屏。看一下MainActivity的代碼:
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
protected void onNewIntent(Intent intent) {
Uri uri= intent.getData();
intent.setData(null);
router(uri);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if(requestCode==JUMP_TO_TARGET && requestCode == RESULT_OK){
router(getIntent().getData());
<!--閃屏消失后,需要跳轉target頁面-->
getIntent().setData(null);
}
}
private void router(Uri uri) {
Uri uri= getIntent().getData();
<!--只有在intent被設置了跳轉數據的時候才去跳轉,一般是推送就來,如果冷啟動,是沒有數據的-->
if(uri!=null){
SplashActivity.startActivityForResult(this,JUMP_TO_TARGET)
}
}
通過上面兩部分的處理,基本能夠滿足APP“死亡”的情況下,先跳轉閃屏的需求。
作者:看書的小蝸牛
原文鏈接:被后臺殺死后,Android應用如何重新走閃屏邏輯
僅供參考,歡迎指正