前言
前段时间,收集到一条关于App的反馈,具体是这样描述的:安装完我们的应用,打开App,按Home键回到手机桌面,操作一段时间其他App后,再次进入我们的应用,总是会重新启动App回到主页。听到这样的描述,我的第一反应是:是不是我们的App占用内存过大或者其他什么原因被系统回收,再次打开会重新启动应用?然而拿到测试MM的手机重现问题之后,我否认了自己的猜测。因为这重现频率也太高了,系统是不会这么频繁的回收应用的。
究其原因,原来是这样。细化重现步骤:1. 系统安装器安装APK(含覆盖安装)、第三方平台(豌豆荚、360手机助手等等)安装,安装成功后,选择打开App。2. 我们的应用正常启动顺序为:SplashActivity -- MainActivity。3. 接着你可以正常操作App,不停的进入下级子页面。4. 点击Home键,将程序置于后台,回到手机主页面。5. 其实此时无需再去操作其他App,直接点击桌面上的程序Logo,在Launcher上打开App。6. 神奇的发现,程序重新启动,又回到了MainActivity。其他的页面都被无情的pop掉了。经过以上细化的步骤,最终问题重现了。这里多说两句,我们在开发过程中,遇到任何问题,都需要耐心究其根本,最终将它解决。切忌,在表层做太多的无用功。
细化步骤是如何分析出来的呢?和大家分享一下:我们的App,启动后的基本步骤是先进入SplashActivity,再进入MainActivity。SplashActivity就是一个纯纯的普通的Activity,启动模式为Standard,在其中会有一系列初始化操作和判断。一般情况下是紧接着进入MainActivity,主页Activity也不是什么另类的Activity,最大的不同是启动模式为SingleTask,基本和其他App一样。点击Home键不做任何拦截处理,按照系统默认逻辑返回Lanuch桌面。
换句话说,App从安装到启动到置于后台,整体交互逻辑并没有任何特殊的地方。所以首先排除业务逻辑导致的Bug。细细的分析,你会发现唯一的不同点是:启动App的入口不同。第一次是通过系统安装器或者第三方平台安装完成后打开,第二次是Launcher桌面应用图标启动。两次的启动入口不一样呀。我们都知道,桌面启动的话是通过startActivity这个api通过特定的Intent向ActivityManagerServer发起启动任务,最终打开App。那么第三方平台这类方式也是通过Intent启动对应的App。那么接下来我们需要研究的是这两个入口启动为什么会导致App的重启启动?
分析原因
以上已经定位了为题重现的步骤,那么接下来我们需要继续往下分析问题原因所在了。目前的初步发现是第三方平台安装完成后打开会出现重复启动的问题。那么我们在Android Studio中,打印出SplashActivity的action、category、flags、taskId、pid等信息比对分析。这里我就不重新打印了,直接拿来主义。Android应用Home键后Launcher重复启动问题(Thanks to 火山石)
第三方平台安装App完成打开后的Log信息 Home键触发后点击Launcher上App图标再次打开App的Log信息重复以上操作,多次点击Home键,使得App置于后台,打开其他App进行使用,再次打开我们的App。你会发现Log信息与第二幅图中的日志一致。所以可以得出结论:新建的SplashActivity是被压入了之前创建的任务栈中。
接下来我们完全退出当前的App。Kill掉当前App的进程。再次打开App,来观察此时App中SplashActivity的Log信息。有什么不同?
完全退出应用再次打开App的Log信息通过以上的日志分析我们可以得出以下结论:1. 用第三方应用打开App,flag为FLAG_ACTIVITY_NEW_TASK,launcher打开应用比第三方应用打开多了FLAG_ACTIVITY_RESET_TASK_IF_NEEDED。2. 用FLAG_ACTIVITY_NEW_TASK创建的任务栈,再用FLAG_ACTIVITY_RESET_TASK_IF_NEEDED(launcher方式)打开,会在SplashActivity多了FLAG_ACTIVITY_BROUGHT_TO_FRONT。3. 重复启动过程中,新建SplashActivity的TaskId与Pid保持一致,可确定还是在原有任务栈中创建Activity,如果打开多次MainActivity(standard启动模式)会在原有任务栈中叠加。
从上面的例子中我们可以分析出,我们编写任何一个Activity的时候,都可以在AndroidManifest里面显式指定一个taskAffinity的属性,也就是说该Activity归属于对应taskAffinity的栈。如果没有指定任何taskAffinity,那么该Activity将会直接归属于包名所在的Task之下。而我们启动一个Activity时(这里只讨论standard启动模式),那么回去先搜寻对应的Task是否存在,如果不存在,新建一个Task并将Activity入栈,如果已经存在对应的Task,那么直接在对应Task入栈即可。如果对taskAffinity属性理解还不够的童鞋,可以移步Taskaffinity属性使用小结。
此处我相信不少童鞋会有一个疑问:为什么新建的SplashActivity是被压入了之前创建的任务栈中?
首先我们来看一看AndroidManifest配置清单文件
SplashActivity在配置清单中的呈现这个用红色框框圈出来的部分其实就是跟桌面约定好的启动拦截过滤器,我相信大家App的启动页配置清单中肯定都有这几行代码。为什么会有这个约定呢?原因在于桌面有一个明显的需求是再次点击已经置于后台的App时,要将后台任务置于前台,而不是再次启动该App程序,这就是为什么新建的SplashActivity是被压入了之前创建的任务栈中。
从柯元旦所著的《android内核剖析》一书中有记录如下规则:每次启动Intent导致新创建Task的时候,该Task会记录导致其创建的Intent。而如果后续需要有一个新的与创建Intent完全一致(完全一致定位为:启动类,action、category等等全部一样,不可多项也不可缺少),那么该Intent并不会触发Activity的新建启动,而只会将已经存在的对应Task移到前台。这也就是为什么桌面会在再次点击图标时将后台任务挪到前台而不是重新启动App的实现。
解决问题
如何解决以上出现的问题,我相信有的童鞋可能自己已经有思路了。如果时多余SplashView的页面入口的话是不可能位于Task的根部的,如果正常启动的话,SplashView的Activity肯定位于任务战Task的根部位置。那么我们可以就从这个入口来解决问题。在SplashView的Activity中加入以下代码:
代码至此,问题解决。如果还有疑问的童鞋请留言。另外此处需要感谢Android Bug分析系列:第三方平台安装app启动后,home键回到桌面后点击app启动时会再次启动入口类bug的原因剖析
大眼杰克 jxiaolee@aliyun.com
对于攀登者来说,失掉往昔的足迹并不可惜,迷失了继续前时的方向却很危险。平凡的脚步也可以走完伟大的行程。当你觉的累的时候就看看那些还在努力的人。
网友评论