问题描述:
开发者打包发布一个release版本app,将app包通过电脑QQ传送到手机QQ上面,点击安装,安装后选择打开app,页面结构如下:
1、闪屏页SplashActivity---> 登录页LoginActivity---> 主页MainActivity
2、用户下载app到手机,通过文件管理器找到并安装这个apk,安装后提示:“安装完成,你可以打开xxx应用了”,
3、用户打开app,输入账号密码跳转到了主页MainActivity。
4、用户按下Home键,然后在程序列表点击app,
先后显示:闪屏页SplashActivity---> 登录页LoginActivity,APP重启了!亮瞎了24K钛合金狗眼的我们觉得这玩法不对吧?
期待页面:显示原先的主页MainActivity。
奇怪的是:真机在debug开发调试时不会出现这个问题???
问题原因:
debug版本是通过adb安装启动或平常的桌面Icon图标启动,
release版本是安装这类第三方平台启动。
两者的启动intent不相同!(相同是指:启动类,action、category等等全部一样,不可多项也不可缺少)
在解决问题前,先了解一下相关:
1、Home主界面其实也是一个Activity。
当从APP界面按下Home键盘,实际是启动APP跳转到Home主界面,这样我们的程序就被置于后台,被这个Home主界面Activity覆盖。
2、Activity的Task管理
Android系统的App启动与切换管理依赖于相关Activity的Task的管理。一个Task之中可能含有若干个Activity,为了简便起见,我们这里记录
【Task A】的Activity分别为 【A1】 、【A2】等,
【Task B】的Activity分别为 【B1】 、【B2】。
那么我们来分析下App之间是怎么切换的。假设应用都是单Task应用(相对于大部分的普通App来说,都是采用单一Task来管理的)
桌面程序App:【TaskA】 ---- 存在Activity有【A1】 ---- 其栈的结构为 A1
应用程序B:【TaskB】 ---- 存在Activity有【B1】【B2】 ---- 其栈的结构为 B1_B2
应用程序C: 【TaskC】 ---- 存在Activity有【C1】【C2】 ---- 其栈的结构为 C1_C2
a、那么我们进入桌面时:Task之间的结构是 A1 ---- 也就是只有一个【TaskA】栈(桌面Task),并且位于最前端(这里表现为最后添加的末端)
b、然后我们点击应用程序B的图标,启动B :Task之间的结构是 A1B1B2 ---- 添加了一个【TaskB】,而且【TaskB】也是位于最前端,现在显示的是【TaskB】的B2的Activity的界面
c、接着点击home键: Android对于home做了特殊默认处理,就是会把桌面Task挪到所以Task最前端,Task结构应该变成 B1_B2_A1 ---- 【TaskA】挪到队列最前端,现在显示的是【TaskA】的A1的Activity的界面,也就是桌面
d、我们再在桌面点击应用程序C的图标,启动C : Task之间的结构变成 B1B2A1C1C2 ---- 添加了一个【TaskC】,而且【TaskC】也是位于最前端,现在显示的是【TaskC】的C2的Activity的界面
从上面的例子,我们可以知道:
我们编写任何一个Activity的时候,都可以在AndroidManifest里面显式指定一个taskAffinity的属性,也就是说该Activity归属于对应taskAffinity的栈;如果没有指定任何taskAffinity,那么该Activity将会直接归属于包名所在的Task之下。而我们启动一个Activity时(这里只讨论standard启动模式),那么回去先搜寻对应的Task是否存在,如果不存在,新建一个Task并将Activity入栈,如果已经存在对应的Task,那么直接在对应Task入栈即可。
那么问题来了:如果我们在上面第d步点击的图片并不是程序C的图标,而是重新点击了程序B的图标,此时【TaskB】是已经存在的了,那么为了不会讲B的入口activity(B1)直接在【TaskB】入栈,而是将【TaskB】挪到前台并不做任何Activity启动的操作呢?
3、桌面的启动管理:
回头研究下AndroidManifest这个文件,我们轻而易举发现,但凡是App入口Activity,那么一定会包含
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
这几行代码。这里到底有什么玄机呢?其实这个就是跟桌面约定好的启动拦截过滤器。因为桌面有一个很明显的需求就是,如果我们再次点击已经在后台的App图标时,是应该将该后台任务挪到前台而不是再次启动该App程序。
而从柯元旦所著的《android内核剖析》一书中有记录如下规则:
每次启动Intent导致新创建Task的时候,该Task会记录导致其创建的Intent;而如果后续需要有一个新的与创建Intent完全一致(完全一致定位为:启动类,action、category等等全部一样,不可多项也不可缺少),那么该Intent并不会触发Activity的新建启动,而只会将已经存在的对应Task移到前台;这也就是为什么桌面会在再次点击图标时将后台任务挪到前台而不是重新启动App的实现。
那么为啥要指定入口Activity特定的action和category呢,那就是为了让桌面启动app所用的Intent具有特殊性,也就是添加了特别的拦截器,避免其他应用内或者应用间的Intent对于这个启动方式的干扰。
原理剖析:
文件管理器虽然使用Intent来启动刚刚安装的那个App,但:它的启动Intent并没有跟桌面的启动Intent完全一致!
我们将桌面的Task记为【TaskDesktop】,文件管理器的Task记为【TaskFile】,我们应用的Task记为【TaskApp】,分析如下:
进入桌面: D1 ---- D1是单纯的桌面
打开文件管理器: D1_F1_F2 ---- F2是安装完毕后询问是否启动对应程序的Activity
点击打开: D1_F1_F2_A1_A2 ---- A1是入口闪屏页,A2是登录Activity
返回桌面: F1_F2_A1_A2_D1 ---- 回到桌面页,也就是D1前置
点击A的图标: F1_F2_D1_A1_A2_A1 ---- 找到【TaskA】,挪到前台,由于比对Intent并不是完全一致,所以该请求是新启动Activity,那么把A1添加到对应的【TaskA】中
所以bug出现了,出现了再一次的闪屏页【A1】,问题定位成功!
PS:这里我稍微变种一下,因为一般我们闪屏页都是在启动登录页后finish的,而登录页一般是singleTask模式
打开文件管理器: D1_F1_F2 ---- F2是安装完毕后询问是否启动对应程序的Activity
点击打开: D1_F1_F2_A2 ---- A1是入口闪屏页,A2是登录Activity,启动后A1业务逻辑应该finish掉,所以从【TaskA】中挪去
返回桌面: F1_F2_A2_D1 ---- 回到桌面页,也就是D1前置
点击A的图标: F1_F2_D1_A2_A1 -> 找到【TaskA】,挪到前台,由于比对启动的Intent不完全一致,所以新创建一个A1 Activity,那么把A1添加到对应的【TaskA】中,然后A1所再一次触发启动登录页 A2,但是登录页是singleTask模式,所以又回到了上次对应的A2登录页,所以现象为再一次出现闪屏页,然后回到原先的登录页界面。
解决方法
1、让腾讯那些第三方平台修正其启动Intent的设置,使其与原声桌面启动Intent保持完全一致。(PS:基本不可能)
2、正常启动的闪屏页Activity必定在【TaskA】的最底部(实际已finish掉被登录页取代),而第二次闪屏Activity不可能位于Task的最底部,所以在闪屏页Activity的onCreate代码:
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 避免从桌面启动程序后,会重新实例化入口类的activity
if (!this.isTaskRoot()) {
Intent intent = getIntent();
if (intent != null) {
String action = intent.getAction();
if (intent.hasCategory(Intent.CATEGORY_LAUNCHER) && Intent.ACTION_MAIN.equals(action)) {
finish();
return;
}
}
}
}
或者
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//首次启动 Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT 为 0,再次点击图标启动时就不为零了
if ((getIntent().getFlags() & Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT) != 0) {
finish();
return;
}
}
读到这里,细心的读者一定会问:你上面说的情形只适用闪屏页和登录页,如果登录进去主页MainActivity按Home键,如何处理呢?
1、闪屏页的OnCreate方法根据登录状态判断跳转:
if ((getIntent().getFlags() & Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT) != 0) {
if (UserInfo.getInstance().isLogined) { // 若已登录,跳转到主页
readyGoThenKill(MainActivity.class);
} else { // 若未登录,引导用户登录
readyGoThenKill(UserLoginActivity.class);
}
return;
}
2、设定MainActivity的launchMode="singleTask",在AndroidManifest.xml修改
<activity
android:name="com.emp.frame.MainActivity"
android:launchMode="singleTask" />