Android Bug分析系列:第三方平台安装app启动后,home键回到桌面后点击app启动时会再次启动入口类bug的缘由剖析

前言前端

  前些天,测试MM发现了一个比较奇怪的bug。android

  具体表现是:api

  一、将app包经过电脑QQ传送到手机QQ上面,点击安装,安装后选择打开app (此间的应用逻辑应该是要触发 【闪屏页Activity】, 而后跳转 【主页Activity】)app

  二、而后MM在 【主页Activity】 时按下了 【Home键】,回到桌面测试

  三、再点击app的icon图标,原谅耿直的咱们都是以为应该直接回到【主页Activity】,可是结果倒是又一次触发 【闪屏页Activity】,亮瞎了24K钛合金狗眼的咱们以为这玩法不对吧?this

  四、而后,收拾收拾心情开始定位之路吧~spa

 

现象分析code

  先说说项目结构吧,咱们这边的项目需求逻辑是 先进入 【闪屏页Activity】(普通的Activity,启动模式为standard),而后根据一堆初始化操做和判断,通常是接着进入【主页Activity】(Activity的启动模式为singleTask);点击home键不作任何拦截处理,按照系统默认逻辑返回Lanuch桌面。blog

  也就是说,app的总体交互逻辑并无特殊之处,并不是业务逻辑致使的bug。那么回顾下不一样的地方,也就是启动App的入口的区别了,一者是日常的桌面Icon图标启动,一者是QQ安装这类第三方平台启动。咱们都知道,桌面启动的话也是经过startActivity这个api经过特定的Intent向ActivityManagerServer发起启动任务;因此咱们能够推导出QQ安装启动这类方式也是经过Intent启动对应的App。队列

  再往下分析的话,可能须要一些前置知识须要了解才能更好的理解。

 

前置知识

一、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】 ---- 其栈的结构为 B1B2

  应用程序C: 【TaskC】 ---- 存在Activity有【C1】【C2】 ---- 其栈的结构为 C1C2

 

a、那么咱们进入桌面时:Task之间的结构是 A1 ---- 也就是只有一个【TaskA】栈(桌面Task),而且位于最前端(这里表现为最后添加的末端)

b、而后咱们点击应用程序B的图标,启动B :Task之间的结构是 A1B1B2  ---- 添加了一个【TaskB】,并且【TaskB】也是位于最前端,如今显示的是【TaskB】的B2的Activity的界面

c、接着点击home键: Android对于home作了特殊默认处理,就是会把桌面Task挪到因此Task最前端,Task结构应该变成  B1B2A1 ---- 【TaskA】挪到队列最前端,如今显示的是【TaskA】的A1的Activity的界面,也就是桌面

d、咱们再在桌面点击应用程序C的图标,启动C : Task之间的结构变成 B1B2A1C1C2 ---- 添加了一个【TaskC】,并且【TaskC】也是位于最前端,如今显示的是【TaskC】的C2的Activity的界面

 

从上面的例子,咱们能够大体了解到Android是怎么管理不一样app之间切换的逻辑:

  咱们编写任何一个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启动的操做呢?

 

二、桌面的启动管理:

  回头研究下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对于这个启动方式的干扰。

说了这么多,咱们能够着手分析上续bug的产生缘由了。

 

原理剖析

  今后咱们能够知道QQ安装器其实也就是使用Intent来启动其刚刚安装的那个App,可是问题所在的是:他们的启动Intent并无跟桌面的启动Intent彻底一致!

咱们将桌面的Task记为【TaskL】,QQ安装器的Task记为【TaskQ】,咱们应用的Task记为【TaskA】,那么分析以下:

 

进入桌面: L1 ---- L1是单纯的桌面

打开QQ: L1Q1Q2 ---- Q2是安装完毕后询问是否启动对应程序的Activity

点击打开: L1Q1Q2A1A2 ---- A1是入口闪屏页,A2是主页Activity

返回桌面: Q1Q2A1A2L1 ---- 回到桌面页,也就是L1前置

点击A的图标: Q1Q2L1A1A2A1 ---- 找到【TaskA】,挪到前台,因为比对Intent并非彻底一致,因此该请求是新启动Activity,那么把A1添加到对应的【TaskA】中

因此bug出现了,出现了再一次的闪屏页【A1】,问题定位成功!

 

PS:这里我稍微变种一下,由于通常咱们闪屏页都是在启动主页后finish的,而主页通常是singleTask模式

 

打开QQ: L1Q1Q2 ---- Q2是安装完毕后询问是否启动对应程序的Activity

点击打开: L1Q1Q2A2 ---- A1是入口闪屏页,A2是主页Activity,启动后A1业务逻辑应该finish掉,因此从【TaskA】中挪去

返回桌面: Q1Q2A2L1 ---- 回到桌面页,也就是L1前置

点击A的图标: Q1Q2L1A2A1 -> Q1Q2L1A2A1 ---- 找到【TaskA】,挪到前台,因为比对Intent并非彻底一致,因此该请求是新启动Activity,那么把A1添加到对应的【TaskA】中,而后A1所再一次触发启动主页,可是主页是singleTask模式,因此又回到了上次对应的A2主页,因此现象为再一次出现闪屏页,而后回到原先的主页界面。

 

解决思路

  一、让腾讯那些第三方平台修正其启动Intent的设置,使其与原声桌面启动Intent保持彻底一致。(PS:基本不可能)

  二、自身业务代码规避,咱们能够知道,若是是多余的闪屏页入口Activity的话,其基本不可能位于Task的根部,而若是正常启动的话,闪屏页入口Activity一定在多对应的Task的根部位置,那么咱们能够从这个地方对于这个bug进行规避,方法就是在闪屏页入口Activity的onCreate代码加入以下一段代码:

// 避免从桌面启动程序后,会从新实例化入口类的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;
        }
    }
}

 问题解决!

 

做者:码农叔叔(enjoy风铃)
出处:http://www.cnblogs.com/net168/
本文版权归做者和博客园共有,欢迎转载,但未经做者赞成必须保留此段声明,且在文章页面明显位置给出原文链接,不然下次不给你转载了

相关文章
相关标签/搜索