写点什么

最后再说一次!!不要在你的 App 启动界面设置 SingleTask-SingleInstance

用户头像
Android架构
关注
发布于: 5 小时前

</layer-list>


AndroidManifest.xml


<activityandroid:name=".ui.main.MainActivity"android:theme="@style/AppWelcomeTheme"


这样一个 MainActivity 启动的时候,就会先显示一个预览窗口,给用户快速响应的体验。当 activity 想要恢复原来 theme,可以通过在调用super.onCreate()setContentView()之前调用 setTheme(R.style.AppTheme),如下:


public class MyMainActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {// Make sure this is before calling super.onCreatesetTheme(R.style.AppTheme);super.onCreate(savedInstanceState);// ...}}


但是却优化出了问题,我们的 MainActivity 使用的启动模式是 SingleTask,我将闪屏页去掉后,无论打开多少页面,将应用推至后台再启动就回到了主页(MainActivity),这是个很严重的问题,还好发现的及时。

问题排查

排查问题的时候,先看看之前的版本有没有该问题(并没有发现问题),再查看我的代码提交记录,发现 AndroidManifest.xml 中我主要做的修改去移除了闪屏界面,点击 App 直接启动的是主页 MainActivity,但是坑爹的是我还以为是引入 dynamic link 带来的问题,还以为动态链需要从启动界面依次传递,等我移除所有的动态链后,发现该问题依旧存在,排除该问题。


结果我却又陷入了自我怀疑中,做了好几年的 Android 开发,什么时候(MainActivity)设置为 SingleTask 会有这种改变,为什么我一直没发现?难道是最新的 api 版本的变化带来的修改,为什么新的修改这么坑爹?然后我又开始用不同版本的虚拟机进行测试,或者设置不同的 targetSdkVersion 进行测试,结果都一样,每次都是 MainActiivy。我又陷入了沉思,这么多年 MainActiviy 都是用的 SingleTask 难道都是错觉吗?可是为什么之前的 app 都没有这个问题。(其实是之前都有闪屏页 SplashActivity,现在没有闪屏页 SplashActivity 了)


后面又仔细确定了提交记录的内容,发现可能影响的就是我移除了闪屏界面,恢复闪屏页面后果然没这个问题,确定问题后,就是有无闪屏页照成的问题,或者说是启动界面设置为 SingleTask 造成的问题。后面网上看了一些解决方案,主要是通过设置启动模式为 standard 或者 SingleTop,然后添加 Flag 为 Intent.FLAG_ACTIVITY_CLEAR_TOP 来解决的,或者说达到这 SingleTask 类似的清栈效果,同时又不会造成每次启动都是 MainActivity。

深入分析 SingleTask 相关源码

但网上清一色的文章并没有仔细分析为什么造成该问题,我又看了一些 Activity 的启动流程源码分析,也只是一笔带过某个方法名,并没有分析到该流程,没办法只能自己动手了。

启动流程图


可以看到图中的 Activity.startActivity 中的启动模块,然后大概看一下流程,很容易就能看出大致的方法出现在哪里,这就是熟悉启动流程的好处,也是画图的好处。

startActivityUnchecked

先说一下 startActivityUnchecked 相关代码的大致逻辑,从 getReusableIntentActivity 中获取一个 reusedActivity,因为这个时候是热启动,我们的 Activity 之前已经创建了,并没有新的 Activity 要插入栈中,所以返回不为空;


进入if (reusedActivity != null) {判断逻辑,下面isLaunchModeOneOf(LAUNCH_SINGLE_INSTANCE, LAUNCH_SINGLE_TASK)条件也成立,又进入下一个逻辑判断,然后判断是否为根 Activity,设置启动的 Activity 为我们的 mStartActivity(MainActivity),所以当 APP 的启动 Activity 为 MainActivity 时,同时设置启动模式为 SingleTask 或者 SingleInstance,每次点击 app 图标看到的界面就是 MainActivity。


private int startActivityUnchecked(final ActivityRecord r, ActivityRecord sourceRecord,IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor,int startFlags, boolean doResume, ActivityOptions options, TaskRecord inTask,ActivityRecord[] outActivity, boolean restrictedBgActivity) {···//从 getReusableIntentActivity 中获取


ActivityRecord reusedActivity = getReusableIntentActivity();


···//不为空时进入该循环


if (reusedActivity != null) {// When the flags NEW_TASK and CLEAR_TASK are set, then the task gets reused but// still needs to be a lock task mode violation since the task gets cleared out and// the devi


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


ce would otherwise leave the locked task.······


// This code path leads to delivering a new intent, we want to make sure we schedule it// as the first operation, in case the activity will be resumed as a result of later// operations.//isLaunchModeOneOf(LAUNCH_SINGLE_INSTANCE, LAUNCH_SINGLE_TASK)表示启动模式为或者 SingleInstance 或者 SingleTask 时,进入该判断


if ((mLaunchFlags & FLAG_ACTIVITY_CLEAR_TOP) != 0|| isDocumentLaunchesIntoExisting(mLaunchFlags)|| isLaunchModeOneOf(LAUNCH_SINGLE_INSTANCE, LAUNCH_SINGLE_TASK)) {final TaskRecord task = reusedActivity.getTaskRecord();


// In this situation we want to remove all activities from the task up to the one// being started. In most cases this means we are resetting the task to its initial// state.//大多数情况下我们可能准备清空当前 task 或者回到 task 的初始状态 final ActivityRecord top = task.performClearTaskForReuseLocked(mStartActivity,mLaunchFlags);


// The above code can remove {@code reusedActivity} from the task, leading to the// the {@code ActivityRecord} removing its reference to the {@code TaskRecord}. The// task reference is needed in the call below to// {@link setTargetStackAndMoveToFrontIfNeeded}.if (reusedActivity.getTaskRecord() == null) {reusedActivity.setTask(task);}


if (top != null) {//是否为根 activity


//boolean frontOfTask; // is this the root activity of its task?if (top.frontOfTask) {// Activity aliases may mean we use different intents for the top activity,// so make sure the task now has the identity of the new intent. //设置启动 Activity 为根 Activitytop.getTaskRecord().setIntent(mStartActivity);}//将会调用该 Activity 的 onNewIntent,一旦调用了 mStartActivity,因为我们也设置了 SingleTask 或者 SingleInstance,所以我们每次看到的都是 mStartActivitydeliverNewIntent(top);}}}······}


先来看看 getReusableIntentActivity 方法,看看该方法的注释,很快就明白作用了,所以返回的不是 null,所以会进入上面的判断逻辑中


/**


  • Decide whether the new activity should be inserted into an existing task. Returns null

  • if not or an ActivityRecord with the task into which the new activity should be added.*/private ActivityRecord getReusableIntentActivity() {// We may want to try to place the new activity in to an existing task. We always// do this if the target activity is singleTask or singleInstance; we will also do// this if NEW_TASK has been requested, and there is not an additional qualifier telling// us to still place it in a new task: multi task, always doc mode, or being asked to// launch this as a new task behind the current one.


再来看看 deliverNewIntent 中被调用到的 deliverNewIntentLocked,最终决定哪个 Activity 的 onNewIntent 会被调用到,也就是我们的 mStartActivity


/**


  • Deliver a new Intent to an existing activity, so that its onNewIntent()

  • method will be called at the proper time.*/final void deliverNewIntentLocked(int callingUid, Intent intent, String referrer) {// The activity now gets access to the data associated with this Intent.

首次安装可能带来的问题

在开发过程中,安装完成一个 app 时,在安装界面直接点击打开。我们进入了 app 的首页,这时我们按 home 键返回桌面,再点击应用图标,会发现没有直接进入首页,而是先进入了 app 的闪屏页,在进入首页。重复这一步一直如此。这时我们按 back 键返回,发现没有直接退回桌面,而是返回到之前打开的多个首页。但是如果一开始安装完我们不是直接打开,而是在桌面点击应用进入就不会这样了。

解决方案

在你的闪屏界面,或者没有闪屏界面,像我上面启动界面直接就是 MainActivity 的话,那你就在该界面的 onCreate 方法中直接添加下面这段代码。具体的分析可以见下面这篇文章


if (!this.isTaskRoot()) { // 当前类不是该 Task 的根部,那么之前启动 Intent intent = getIntent();if (intent != null) {String action = intent.getAction();if (intent.hasCategory(Intent.CATEGORY_LAUNCHER) && Intent.ACTION_MAIN.equals(action)) { // 当前类是从桌面启动的 finish(); // finish 掉该类,直接打开该 Task 中现存的 Activityreturn;}}}

总结

千万要注意,不要在你的启动界面(如果你想把 MainActivity 的 windowbackground 设置为闪屏界面,移除闪屏页,直接启动 MainActivity 给用户造成快速启动的感觉)设置启动模式为 SingleTask 或者 SingleInstance,一旦设置后,不管软启动或者热启动都是从该启动界面开始启动 App,除非特殊的需求,否则千万不要这么设置。如果想要实现类似 SingleTask 的清栈效果,可以使用 standard 或者 singleTop 结合对应的 Flag 进行实现。

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
最后再说一次!!不要在你的App启动界面设置SingleTask-SingleInstance