1. 程式人生 > 其它 >Android啟動模式之singleinstance的坑

Android啟動模式之singleinstance的坑

技術標籤:Androidjavaandroid

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。

這裡需要注意的是,只有當前的activity處於棧頂的時候才管用。舉個例子:startActivity了一個ActivityA,ActivityA又startActivity了ActivityB,ActivityB又startActivity了ActivityA,那麼ActivityA還是會重新建立,而不是複用之前的ActivityA。

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的坑。

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;這樣就能解決跳轉的問題了。不過感覺還不是很好,如果有更好的方法,歡迎大家給我留言告訴我一聲。

singleInstance之二坑

此時有兩個個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。