非Activity環境startActivity的正確姿勢

我們知道非Activity的Context(如:ApplicationContext)對象啓動一個Activity需要添加FLAG_ACTIVITY_NEW_TASK標記才行,否則app會崩潰並報以下錯誤:

android.util.AndroidRuntimeException: Calling startActivity() from outside of an Activity  context requires the FLAG_ACTIVITY_NEW_TASK flag. Is this really what you want?

因爲非Activity的Context不存在放置新活動的現有任務,因此需要將其放置在其自己的單獨任務中。正如官方文檔所述:

Note that if this method is being called from outside of an Activity Context, then the Intent must include the Intent#FLAG_ACTIVITY_NEW_TASK launch flag. This is because, without being started from an existing Activity, there is no existing task in which to place the new activity and thus it needs to be placed in its own separate task.

但是,最近項目中遇到這樣一段類似代碼:

    mContext.getApplicationContext().startActivity(new Intent(mContext,XXXActivity.class));

哦豁,完蛋!!!毫無意外,開發的同事收到了QA的一張紅牌(bug)警告。

同事委屈地告訴我,他的測試機可以跳得過去。emmmm…

愣住

瞬間想到了這些測試機的系統是不是屬於7.x或8.x版本系列。

爲啥呢?走一波源碼看看

以下內容基於不加FLAG_ACTIVITY_NEW_TASK標記的分析

Context的startActivity方法是抽象的,由實現類ContextImpl實現該方法。

  • 直接看Android7.x~Android8.x系列源碼
    @Override
    public void startActivity(Intent intent) {
        warnIfCallingFromSystemProcess();
        startActivity(intent, null);
    }

    @Override
    public void startActivity(Intent intent, Bundle options) {
        warnIfCallingFromSystemProcess();

        // Calling start activity from outside an activity without FLAG_ACTIVITY_NEW_TASK is
        // generally not allowed, except if the caller specifies the task id the activity should
        // be launched in.
        if ((intent.getFlags()&Intent.FLAG_ACTIVITY_NEW_TASK) == 0
                && options != null && ActivityOptions.fromBundle(options).getLaunchTaskId() == -1) {
            throw new AndroidRuntimeException(
                    "Calling startActivity() from outside of an Activity "
                    + " context requires the FLAG_ACTIVITY_NEW_TASK flag."
                    + " Is this really what you want?");
        }
        ......
    }

這幾個版本源碼都一樣。一般情況下我們只調用startActivity(Intent intent)方法,在源碼中實際調用的是startActivity(Intent intent, Bundle options)方法,options爲null值,代碼中有段並集判斷:

        if ((intent.getFlags()&Intent.FLAG_ACTIVITY_NEW_TASK) == 0
                && options != null && ActivityOptions.fromBundle(options).getLaunchTaskId() == -1) {
            throw new AndroidRuntimeException(
                    "Calling startActivity() from outside of an Activity "
                    + " context requires the FLAG_ACTIVITY_NEW_TASK flag."
                    + " Is this really what you want?");
        }

着重來看options != null的判斷,因爲options值爲null,所以結果爲false,整條並集判斷不成立,這樣加不加FLAG_ACTIVITY_NEW_TASK都不會引起程序異常崩潰。

  • 接下來看看Android6.x的源碼
    @Override
    public void startActivity(Intent intent, Bundle options) {
        ......
        if ((intent.getFlags()&Intent.FLAG_ACTIVITY_NEW_TASK) == 0) {
            throw new AndroidRuntimeException(
                    "Calling startActivity() from outside of an Activity "
                    + " context requires the FLAG_ACTIVITY_NEW_TASK flag."
                    + " Is this really what you want?");
        }
        ......
    }

嗯哼~ ~ ~ 沒毛病,不加就會崩潰…

那就意味着android7.0開始就可以不用加FLAG_ACTIVITY_NEW_TASK標記了嗎?
別讓天真迷瞎了你的雙眼

  • 再來看Android9.x的源碼
    @Override
    public void startActivity(Intent intent, Bundle options) {
        ......
        // Calling start activity from outside an activity without FLAG_ACTIVITY_NEW_TASK is
        // generally not allowed, except if the caller specifies the task id the activity should
        // be launched in. A bug was existed between N and O-MR1 which allowed this to work. We
        // maintain this for backwards compatibility.
        final int targetSdkVersion = getApplicationInfo().targetSdkVersion;

        if ((intent.getFlags() & Intent.FLAG_ACTIVITY_NEW_TASK) == 0
                && (targetSdkVersion < Build.VERSION_CODES.N
                        || targetSdkVersion >= Build.VERSION_CODES.P)
                && (options == null
                        || ActivityOptions.fromBundle(options).getLaunchTaskId() == -1)) {
            throw new AndroidRuntimeException(
                    "Calling startActivity() from outside of an Activity "
                            + " context requires the FLAG_ACTIVITY_NEW_TASK flag."
                            + " Is this really what you want?");
        }
        ......
    }

可以看到,若targetSdkVersion < Build.VERSION_CODES.N || targetSdkVersion >= Build.VERSION_CODES.P結果爲true,即當前系統爲Android7.0以下或9.0及以上版本,而options爲null,options == null結果爲true,這樣整條並集判斷爲true,哦豁,程序崩潰!!!

花樣多

targetSdkVersion < Build.VERSION_CODES.N || targetSdkVersion >= Build.VERSION_CODES.P結果爲false,即當前系統爲Android7.0到8.1版本,這樣整條並集判斷爲false,躲過了一劫。

唉?這不正是上述Android7.x~Android8.x系列源碼邏輯嗎?沒錯,google做了版本兼容。

眼神犀利的童鞋應該已經看到了這條註釋:

     A bug was existed between N and O-MR1 which allowed this to work. We maintain this for backwards compatibility.
     翻譯:N和O-MR1版本之間存在一個bug,允許它工作。我們維護這一點是爲了向後兼容。

這一點官方Android9.0變更文檔也有所提及。

在 Android 9 中,您不能從非 Activity 環境中啓動 Activity,除非您傳遞 Intent 標誌 FLAG_ACTIVITY_NEW_TASK。 如果您嘗試在不傳遞此標誌的情況下啓動 Activity,則該 Activity 不會啓動,系統會在日誌中輸出一則消息。

注:在 Android 7.0(API 級別 24)之前,標誌要求一直是期望的行爲並被強制執行。 Android 7.0 中的一個錯誤會臨時阻止實施標誌要求。

你以爲本文水完了?天真再次迷瞎了你的雙眼

有些時候我們也會直接調用startActivity(Intent intent, Bundle options)方法,傳入非空的options對象,例如添加Activity轉場動畫Bundle數據源。在Android7.x~Android8.x的方法中有條這樣的判斷:

    options != null && ActivityOptions.fromBundle(options).getLaunchTaskId() == -1

options不爲null時,執行ActivityOptions.fromBundle(options).getLaunchTaskId() == -1判斷

  • 看源碼吧~ ~ ~
    /** @hide */
    public static ActivityOptions fromBundle(Bundle bOptions) {
        return bOptions != null ? new ActivityOptions(bOptions) : null;
    }
  • 接下來走構造方法
public class ActivityOptions {
    ......
    private int mLaunchTaskId = -1;
    ......
    /** @hide */
    public ActivityOptions(Bundle opts) {
        ......
        mLaunchTaskId = opts.getInt(KEY_LAUNCH_TASK_ID, -1);
        ......
    }
    ......
}

嗯哼~ ~ ~ 問題來了,一般情況下,這個options是我們現場new出來的,或者使用構造工廠創建的(如ActivityOptions.makeCustomAnimation(Context context,int enterResId, int exitResId)),往往沒有主動添加KEY_LAUNCH_TASK_ID值,這樣的話mLaunchTaskId爲默認值-1,則:

    /**
     * @hide
     */
    public int getLaunchTaskId() {
        return mLaunchTaskId;
    }

返回值爲-1,從而導致整條並集判斷爲true,哦豁,程序又崩潰了!!!

這就告訴你即使是Android7.x~Android8.x系統也不能這麼自信地不加FLAG_ACTIVITY_NEW_TASK標記,代碼不規範,親人兩行淚

總結

對於Android7.0以下和9.0及以上,非Activity環境啓動一個Activity時,老老實實加上FLAG_ACTIVITY_NEW_TASK標記吧;對於Android7.0~8.1系統,調用startActivity(Intent intent)可以不用加標記,調用startActivity(Intent intent, Bundle options)時,options有值則需注意加標記或爲options添加上指定的LaunchTaskId

最後建議:對於任何版本的Android系統,都加上標記吧,尊重google的任務棧設計思想。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章