被后臺殺死后,Android應用如何重新走閃屏邏輯

Android應用運行在后臺的時候,經常被系統的LowMemoryKiller殺掉,當用戶再次點擊icon或者從最近的任務列表啟動的時候,進程會被重建,并且恢復被殺之前的現場。什么意思呢?假如APP在被殺之前的Activity堆棧是這樣的,A<B<C,C位于最上層

后臺殺死與恢復的堆棧.jpg

APP被后臺殺死后,APP端進程被銷毀了,也就不存在什么Activity了,也就沒有什么Activity堆棧,不過AMS的卻是被保留了下來:

后臺殺死與恢復的堆棧-殺后.jpg

當用戶再次啟動APP時候會怎么樣呢?這個時候,首先看到其實C,而不是棧底部的A,也就是說往往被殺死后,恢復看到的第一個界面是用戶最后見到的那個界面。

后臺殺死與恢復的堆棧-恢復.jpg

而用戶點擊返回,看到的就是上一個界面B,其次是A

后臺殺死與恢復的堆棧-恢復b.jpg

之所以這樣是因為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不存活的情況:推送分三步:

  1. 跳轉MainActivity
  2. startActivityForResult打開閃屏頁
  3. 跳轉推送目標頁面

如何判斷呢,正產流程下,啟動后,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“死亡”的情況下,先跳轉閃屏的需求。

image.png

作者:看書的小蝸牛
原文鏈接:被后臺殺死后,Android應用如何重新走閃屏邏輯

僅供參考,歡迎指正

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容