登录

去注册

登录

注册

去登录

注册

每日一问 哪些 Context调用 startActivity 需要设置NEW_TASK,为什么?

xiaoyang   2019-07-18 23:24   收藏

最常见的就是 Application 需要设置 NEW_TASK了,为什么需要呢?


其他的 Context 呢?


能够提取出什么共性吗?


本周2/3

9

我们平时在开发中能直接接触到的Context(临时创建和自定义的除外),无非就三种:Application,Activity还有Service,它们三个都是间接继承自Context的。

有同学可能会问: 喂,BroadcastReceiver和ContentProvider呢?

emmmm,不是滴,这两个组件都没有直接或间接继承Context。ContentProvider的getContext方法得到的是Application的实例,而ContentProvider中onReceive方法的context参数,其实是发广播的那个Context实例。

用Activity去启动另一个Activity不用设置Flags,这是众所周知的。

那为什么除Activity外的其他Context就不行呢?

有同学已经说了:
因为ContextImpl的startActivity方法会对intent的Flags进行检查,如果没有FLAG_ACTIVITY_NEW_TASK标志的话,就会抛出异常。
而Activity则重写了startActivity方法,改了实现方式。
当然了,即便重写了startActivity,最后都是调用同样的方法:Instrumentation的execStartActivity。

那为什么没有NEW_TASK标志会拋异常呢?这样做的目的是什么?

这个问题先搁着,等下再来看。

有同学也说了:

在7.0~8.1之间的系统版本,所有的Context都能像Activity那样,不须设置Flag也能启动Activity,并不会抛出异常,这个是为什么呢?

没有抛异常的原因,是因为在ActivityStarter的startActivityUnchecked方法(startActivity方法始终会走到这里)中,把所有可能发生的情况,都做了处理。

那当发生了这种情况(非Activity启动但没NEW_TASK)的时候,它是怎么处理的呢?

来看看ActivityStarter中的代码就知道了:

private int startActivityUnchecked(){
......
......
if (要启动的Activity当前没有运行在某个任务栈中 并且,设置了FLAG_ACTIVITY_NEW_TASK) {
newTask = true;
result = setTaskFromReuseOrCreateNewTask(taskToAffiliate, topStack);
} else if (是由Activity启动的(即有上一级)) {
result = setTaskFromSourceRecord();
} else if (已经运行在某个任务栈中(即已启动,可能被其他Activity遮挡)) {
result = setTaskFromInTask();
} else {
// This not being started from an existing activity, and not part of a new task...
// just put it in the top task, though these days this case should never happen.
setTaskToCurrentTopOrCreateNewTask();
}
......
......
}

结合上面的代码想一下,如果不是Activity启动而且没加NEW_TASK标识的话,那么可能会走的分支,就只有最后面两个了。
如果这个Activity当前没有运行在某个任务栈中,那就会走最后一个,也就是会调用setTaskToCurrentTopOrCreateNewTask方法了,这个方法,看名字大概可以知道:会把Activity放到当前栈的栈顶,或者放置在一个新的任务栈内。

当前栈,指的是哪个栈?

就是当前能看到的Activity所在的栈。这个方法里面会通过一个canLaunchIntoFocusedStack方法来决定允不允许直接启动在当前栈,而canLaunchIntoFocusedStack方法里面则会根据栈顶Activity的WindowingMode来判断,

它有以下几种情况:

WINDOWING_MODE_FULLSCREEN:

也就是全屏了,在这个窗口模式下,是允许启动在当前栈的

WINDOWING_MODE_SPLIT_SCREEN_PRIMARY和WINDOWING_MODE_SPLIT_SCREEN_SECONDARY:

这两个Mode是7.0后加入分屏功能的标识,前者是主要位置,后者是次要位置。

有两个,那怎么知道究竟哪一个在栈顶?

其实就是当前获取到焦点的那个。
在这两个模式下,它会根据要启动的Activity是否支持分屏来决定能不能启动在当前栈。

WINDOWING_MODE_FREEFORM:

这个好像比较少见,在这个模式下,Activity窗口的尺寸是可以随意调整的。
如果当前栈顶的Activity也是这个模式的话,那么会检查要启动的Activity支不支持自由调整尺寸。

还有最后一个:

当以上条件都不匹配的时候,会允许它启动在当前栈。


如果canLaunchIntoFocusedStack方法返回false(不允许启动在当前栈)的话,那么就会把Activity放到一个新的任务栈里。

好,现在回到上一个问题:

为什么没有NEW_TASK标志要拋异常呢?

可能Android它认为:
如果该Activity是由一个已启动的Activity发起的,那么把它放在这个已启动的Activity的任务栈内,这是合情合理的。
就好像生BB一样,如果是自己亲生的BB,当然是在自己家住了,但如果这个BB是从石头里爆出来的,那应该归谁家养呢?没人收养的话,就只有自己另起门户咯。
所以在这种情况下,就必须要你主动去承认这个Activity是在新的任务栈中,而不是那种妥协的方法:寄生在当前任务栈上面咯。

回复
陈小缘 : @陈小缘 

看了半天的源码, 给自己点个赞~d(* ̄▽ ̄)===b

2019-07-18 01:09 回复
nanchen2251 : @陈小缘 

另一个角度写的很棒。

2019-07-18 09:54 回复
陈小缘 : @陈小缘 

第一个问题 “ 喂,BroadcastReceiver和ContentProvider呢?” 下面的解答打错了: 第二个 “ContentProvider ”应改为“BrocastReceiver  ...查看更多

2019-07-18 12:54 回复
4

熟悉 Context 的小伙伴都知道,Context 是一个抽象类,所以我们可以直接查看它的实现类 ContextImpl.startActivity(),可以看到最终的调用都是 public void startActivity(Intent intent, Bundle options)。


可以看到,在调用 Instrumentation.execStartActivity 执行跳转之前,我们有一个判断条件,当这个条件不成立的时候,会直接抛出运行时异常。


这个条件是什么呢:if ((intent.getFlags() & Intent.FLAG_ACTIVITY_NEW_TASK) == 0

<premenlo';font-size:12.0pt;"> && (targetSdkVersion < Build.VERSION_CODES.N
|| targetSdkVersion >= Build.VERSION_CODES.P)
&& (options == null
|| ActivityOptions.fromBundle(options).getLaunchTaskId() == -1))</premenlo';font-size:12.0pt;">
<premenlo';font-size:12.0pt;">
</premenlo';font-size:12.0pt;">
在 SDK 28 的源码上有这么一串注释。
<premenlo';font-size:12.0pt;">// 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.</premenlo';font-size:12.0pt;">
<premenlo';font-size:12.0pt;">
</premenlo';font-size:12.0pt;">
<premenlo';font-size:12.0pt;">大概意思是,除了 Activity 外,其他的 Context 都必须要设置上 </premenlo';font-size:12.0pt;">FLAG_ACTIVITY_NEW_TASK,否则直接抛出异常。实际上可以看到 & 操作符也证实了这一点。

还比较有意思的一点是,在 N 到 O-MR1 之间,Google 还出现过一个 bug,也就是在这之间的版本上你可以不必设置 
FLAG_ACTIVITY_NEW_TASK,但我想没有开发者会只有这之间的机型,所以我们在开发的时候还是一定得在非 Activity 的 Context 调用 startActivity() 的时候设置上 FLAG_ACTIVITY_NEW_TASK 标志。

可能会有小伙伴想,为什么我们就一定要设置这个标志呢?

熟悉启动模式的小伙伴可能会发现,我们有四种启动模式,而 Activity 有一个 Activity 栈去管理它。如果你用一个非 Activity 的 Context 去启动一个新的 Activity 的话,新的 Activity 并不知道自己应该放到哪个 Activity 栈中。而设置上 FLAG_ACTIVITY_NEW_TASK 标记,就会直接创建一个 Activity 栈来管理它了。实际上,这样的启动方式就是以 singleTask 模式启动的。

实际上,我在前两天公众号(nanchen)上也写了一篇类似的文章:感兴趣的可以去看一看:
https://mp.weixin.qq.com/s/XtWQajY6cR-geHpPCdl-bQ


回复
nanchen2251 : @nanchen2251 

copy 一下源码咋弄出来了字体。这就很尴尬了。不能修改么?@xiaoyang

2019-07-17 11:32 回复
pigmandy : @nanchen2251 

"熟悉启动模式的小伙伴可能会发现,我们有四种启动模式,而 Activity 有一个 Activity 栈去管理它。如果你用一个非 Activity 的 Context 去启动一个新的 Activity  ...查看更多

2019-07-17 19:33 回复
nanchen2251 : @pigmandy 

是 singleTask 哈,可以自己搜一下看看。

2019-07-18 10:21 回复
鸿洋 : @nanchen2251 

哈哈哈 我的锅 暂时不能修改,等我支持一下markdown

2019-07-18 22:16 回复
1

BroadcastReceiver.onReceive(context),Service,Application,ContextProvicder中的Context mBase实现类都是ContextImpl,ContextImpl类中的startActivity方法对intent.flag进行了检查,在这些组件中获得的context启动Activity需要制定新的任务栈,因为这些组件本身无任务栈。

由于activity类覆盖了startActivty实现,允许启动的新act跟当前act在同一个任务栈

一个应用中的context数目等于acitivty数+service数+1(Apllication),非activity.startActivity启动都需要设置NEW_TASK

回复
1

targetSdkVersion 在24,25,26,27这些版本上不设置也可以跳转。

准确是,都不行,只是这些版本上面,google官方的一个bug,在28的时候进行了修改和说明,

@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. 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?");
}
mMainThread.getInstrumentation().execStartActivity(
getOuterContext(), mMainThread.getApplicationThread(), null,
(Activity) null, intent, -1, options);
}

回复
0

在我这边的华为荣耀2上,在广播的onrecive中startActivity发现并不需要设置new_task标志位,在vivo手机上发现会导致直接崩溃。兼容性处理最好还是在非activity调用startActivity中都加上new_task最保险

回复
0

本文是我的context入门文章,有详细的解释

https://possiblemobile.com/2013/06/context/

回复
0

前两天刚看了个文章,在某些版本上Application启动也不需要newTask(bug),这个bug导致的原因是源码里面抛出异常前面的if判断条件写错了,哈哈

回复

删除留言

确认删除留言,会导致相关评论丢失?

取消 确定