Android啟動模式之singleinstance的坑
Android啟動模式之singleinstance的坑
前言
在實際應用中,使用singleinstance啟動模式時,會遇到一些奇奇怪怪的問題。Android有四種啟動模式,分別是standard,singleTop,singleTask,singleInstance。下面分別簡單的介紹下這四種啟動模式的作用。
standard
Android 預設的一種啟動模式。不需要為activity設定launchMode。這種啟動模式簡單的來說就是當你startActivity的時候,他就建立一個。
singleTop
這種模式模式從字面意思就能看得出來,就是當前的activity處於棧頂的時候,當你startActivity當前的activity的時候,它不會建立新的activity,而是會複用之前的activity。舉個例子,startActivity了一個ActivityA,ActivityA又startActivity了ActivityB,當在ActivityB再次startActivity一個ActivityB的時候,它不會建立一個新的ActivityB,而是複用之前的ActivityB。
singleTask
單一任務。意思就是說當前的activity只有一個例項,無論在任何地方startActivity出來這個activity,它都只存在一個例項。並且,它會將在他之上的所有activity都銷燬。通常這個activity都是用來作為MainActivity。因為主頁只需要存在一個,然後回到主頁的時候可以將所有的activity都銷燬起到退出應用的作用。舉個例子,startActivity了一個ActivityA,ActivityA的啟動模式為singleTask,那麼在ActivityA裡startActivity了一個ActivityB,在ActivityB裡startActivity了一個ActivityC。此時在當前的任務棧中的順序是,ActivityA->ActivityB->ActivityC。然後在ActivityC裡重新startActivity了一個ActivityA,此時ActivityA會將存在於它之上的所有activity都銷燬。所以此時任務棧中就只剩下ActivityA了。
singleInstance
這個模式才是重點,也是比較容易入坑的一種啟動模式。字面上理解為單一例項。它具備所有singleTask的特點,唯一不同的是,它是存在於另一個任務棧中。上面的三種模式都存在於同一個任務棧中,而這種模式則是存在於另一個任務棧中。舉個例子,上面的啟動模式都存在於地球上,而這種模式存在於火星上。整個Android系統就是個宇宙。下面來詳細介紹一下singleInstance的坑。
此時有三個activity,ActivityA,ActivityB,ActivityC,除了ActivityB的啟動模式為singleInstance,其他的啟動模式都為預設的。startActivity了一個ActivityA,在ActivityA裡startActivity了一個ActivityB,在ActivityB裡startActivity了一個ActivityC。此時在當前的任務棧中的順序是,ActivityA->ActivityB->ActivityC。照理來說在當前ActivityC頁面按返回鍵,finish當前介面後應當回到ActivityB介面。但是事與願違,奇蹟出現了,頁面直接回到了ActivityA。這是為什麼呢?其實想想就能明白了,上面已經說過,singleInstance模式是存在於另一個任務棧中的。也就是說ActivityA和ActivityC是處於同一個任務棧中的,ActivityB則是存在另個棧中。所以當關閉了ActivityC的時候,它自然就會去找當前任務棧存在的activity。當前的activity都關閉了之後,才會去找另一個任務棧中的activity。也就是說當在ActivityC中finish之後,會回到ActivityA的介面,在ActivityA裡finish之後會回到ActivityB介面。如果還想回到ActivityB的頁面怎麼辦呢?我的做法是,在ActivityB定義一個全域性變數,public static boolean returnActivityB;介面需要跳轉的時候將returnActivityB=true;然後在ActivityA介面onstart方法裡判斷returnActivityB是否為true,是的話就跳轉到ActivityB,同時將returnActivityB=false;這樣就能解決跳轉的問題了。不過感覺還不是很好,如果有更好的方法,歡迎大家給我留言告訴我一聲。
此時有兩個個activity,ActivityA,ActivityB,ActivityA的啟動模式為預設的,ActivityB的啟動模式為singleInstance。當在ActivityA裡startActivity了ActivityB,當前頁面為ActivityB。按下home鍵。應用退到後臺。此時再點選圖示進入APP,按照天理來說,此時的介面應該是ActivityB,可是奇蹟又出現了,當前顯示的介面是ActivityA。這是因為當重新啟動的時候,系統會先去找主棧(我是這麼叫的)裡的activity,也就是APP中LAUNCHER的activity所處在的棧。檢視是否有存在的activity。沒有的話則會重新啟動LAUNCHER。要解決這個方法則是和一坑的解決辦法一樣,在ActivityB定義一個全域性變數,public static boolean returnActivityB;在oncreat方法將returnActivityB=true;然後在ActivityA介面onstart方法裡判斷returnActivityB是否為true,是的話就跳轉到ActivityB,同時將returnActivityB=false;這樣就能解決跳轉的問題了。
基友留言,確實發現了好多問題,所以在此更新2020/5/21
有許多好基友留言,我也確實發現以上的解決方案不是很好,也存在一些問題,所以我又想了另一種解決思路。程式碼在GitHub上LaunchModeDemo
首先先將每個建立的activity用一個單例類儲存下來,接著再用這個單例類儲存啟動了singleInstance模式的activity。在oncreate()時put,在onDestroy和onBackPressed時remove。為什麼要在這兩個地方都刪除,待會會說明,已經在remove方法裡處理了重複刪除的問題。
先貼上管理activity的類,也就是新增刪除activity的單例類ActivityTaskManager
package com.example.launchmodedemo;
import android.app.Activity;
import java.util.concurrent.CopyOnWriteArrayList;
/** Activity棧管理類,當Activity被建立是壓棧,銷燬時出棧 */
public class ActivityTaskManager {
private final CopyOnWriteArrayList<Activity> ACTIVITY_ARRAY = new CopyOnWriteArrayList<>();
private final CopyOnWriteArrayList<Activity> SINGLE_INSTANCE_ACTIVITY_ARRAY =
new CopyOnWriteArrayList<>();
private static final Singleton<ActivityTaskManager> SINGLETON =
new Singleton<ActivityTaskManager>() {
@Override
protected ActivityTaskManager create() {
return new ActivityTaskManager();
}
};
public static ActivityTaskManager getInstance() {
return SINGLETON.get();
}
public void put(Activity targetActivity) {
boolean hasActivity = false;
for (Activity activity : ACTIVITY_ARRAY) {
if (targetActivity == activity) {
hasActivity = true;
break;
}
}
if (!hasActivity) {
ACTIVITY_ARRAY.add(targetActivity);
}
}
public void remove(Activity targetActivity) {
for (Activity activity : ACTIVITY_ARRAY) {
if (targetActivity == activity) {
ACTIVITY_ARRAY.remove(targetActivity);
break;
}
}
}
public void putSingleInstanceActivity(Activity targetActivity) {
boolean hasActivity = false;
for (Activity activity : SINGLE_INSTANCE_ACTIVITY_ARRAY) {
if (targetActivity == activity) {
hasActivity = true;
break;
}
}
if (!hasActivity) {
SINGLE_INSTANCE_ACTIVITY_ARRAY.add(targetActivity);
}
}
public void removeSingleInstanceActivity(Activity targetActivity) {
SINGLE_INSTANCE_ACTIVITY_ARRAY.remove(targetActivity);
}
public CopyOnWriteArrayList<Activity> getSingleInstanceActivityArray() {
return SINGLE_INSTANCE_ACTIVITY_ARRAY;
}
public Activity getTopActivity() {
if (ACTIVITY_ARRAY.isEmpty()) {
return null;
}
return ACTIVITY_ARRAY.get(0);
}
public Activity getLastActivity() {
if (ACTIVITY_ARRAY.isEmpty()) {
return null;
}
return ACTIVITY_ARRAY.get(ACTIVITY_ARRAY.size() - 1);
}
}
Singleton
package com.example.launchmodedemo;
/**
* 單例構建類
*/
public abstract class Singleton<T> {
private T mInstance;
protected abstract T create();
public final T get() {
synchronized (this) {
if (mInstance == null) {
mInstance = create();
}
return mInstance;
}
}
}
偷個懶,沒什麼註釋,但是各位那麼聰明應該看得懂。
然後貼上我的BaseActivity,基本上都是在這裡處理的了
package com.example.launchmodedemo;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import androidx.appcompat.app.AppCompatActivity;
public class BaseActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityTaskManager.getInstance().put(this);
}
private static final String TAG = "BaseActivity";
@Override
protected void onStart() {
super.onStart();
Log.i(
TAG,
"onStart: " + ActivityTaskManager.getInstance().getLastActivity().getClass().getName());
checkActivityJump();
}
@Override
public void onBackPressed() {
super.onBackPressed();
// 如果不在這裡移除當前activity的話,在啟動另一個介面的onStart的時候,判斷處於棧頂的activity,
// 也就是ActivityTaskManager.getInstance().getLastActivity()的時候,就會出現錯誤。
// 原因是,當一個activity的onPause之後就會啟動另一個activity,還沒經歷過onDestroy,
// 而removeActivity();只放在onDestroy中的話就會在啟動的activity的onStart
// 獲取ActivityTaskManager.getInstance().getLastActivity()返回的是錯誤。
removeActivity();
}
private void checkActivityJump() {
if (ActivityTaskManager.getInstance().getLastActivity() != null) {
Log.i(
TAG,
"onStart: " + ActivityTaskManager.getInstance().getLastActivity().getClass().getName());
// 如果當前的activity跟新增進去的最後一個activity不是同一個的話,那麼這種哦情況就有可能是最後一個activity的啟動模式是SingleInstance,
// 所以這時候就要遍歷新增進去的SingleInstanceActivityArray,看是否有存在,有的話並且跟最後一個新增進去的activity是同一個的話就跳轉
// 這裡設定了跳轉動畫,是因為單例模式的跳轉動畫跟其他的模式不一樣,看起來很難受,設定後看起來舒服些,也可以設定別的動畫,
// 退到後臺再進來會一閃,十分明顯,新增跳轉動畫看起來也會舒服些
if (!ActivityTaskManager.getInstance()
.getLastActivity()
.getClass()
.getName()
.equals(this.getClass().getName())) {
if (ActivityTaskManager.getInstance().getSingleInstanceActivityArray().size() > 0) {
for (Activity activity :
ActivityTaskManager.getInstance().getSingleInstanceActivityArray()) {
if (activity
.getClass()
.getName()
.equals(ActivityTaskManager.getInstance().getLastActivity().getClass().getName())) {
ActivityTaskManager.getInstance().removeSingleInstanceActivity(activity);
startActivity(
new Intent(this, ActivityTaskManager.getInstance().getLastActivity().getClass()));
overridePendingTransition(0, 0);
break;
}
}
}
}
}
}
@Override
protected void onStop() {
super.onStop();
Log.i(TAG, "onStop: " + this.getClass().getName());
}
@Override
protected void onDestroy() {
super.onDestroy();
removeActivity();
}
/** 釋放資源 */
private void removeActivity() {
ActivityTaskManager.getInstance().remove(this);
}
}
在程式碼的註釋已經解釋了我的思路了。我簡要說明一下,在onStart中判斷,如果當前的activity跟新增進去的最後一個activity不是同一個的話,那麼這種哦情況就有可能是最後一個activity的啟動模式是SingleInstance,所以這時候就要遍歷新增進去的SingleInstanceActivityArray,看是否有存在,有的話並且跟最後一個新增進去的activity是同一個的話就跳轉。大家可以看看程式碼,在註釋上寫的很清楚的。最後附上程式碼地址,程式碼在GitHub上LaunchModeDemo
。
總結
Android的啟動模式如果利用的好,還是可以解決很多問題的。啟動模式還是值得好好的研究一下的。歡迎各位指教出錯誤,共同學習。如果有不對的地方,請大家指出,一起快樂的改bug。