非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的任务栈设计思想。

© 版权声明
THE END
喜欢就支持以下吧
点赞1 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容