被后台杀死后,Android应用如何重新走闪屏逻辑

Android应用运行在后台的时候,经常被系统的LowMemoryKiller杀掉,当用户再次点击icon或者从最近的任务列表启动的时候,进程会被重建,并且恢复被杀之前的现场。什么意思呢?假如APP在被杀之前的Activity堆栈是这样的,A<B<C,C位于最上层

后台杀死与恢复的堆栈.jpg

APP被后台杀死后,APP端进程被销毁了,也就不存在什么Activity了,也就没有什么Activity堆栈,不过AMS的却是被保留了下来:

后台杀死与恢复的堆栈-杀后.jpg

当用户再次启动APP时候会怎么样呢?这个时候,首先看到其实C,而不是栈底部的A,也就是说往往被杀死后,恢复看到的第一个界面是用户最后见到的那个界面。

后台杀死与恢复的堆栈-恢复.jpg

而用户点击返回,看到的就是上一个界面B,其次是A

后台杀死与恢复的堆栈-恢复b.jpg

之所以这样是因为APP端Activity的创建其实都是由AMS管理的,AMS严格维护这APP端Activity对应的ActivityRecord栈,可以看做当前APP的场景,不过,APP端Activity的销毁同AMS端ActivityRecord的销毁并不一定是同步的,最明显的就是后台杀死这种场景。Android为了能够让用户无感知后台杀死,就做了这种恢复逻辑,不过,在开发中,这种逻辑带了的问题确实多种多样,甚至有些产品就不希望走恢复流程,本文就说说如何避免走恢复流程。结合常见的开发场景,这里分为两种,一种是针对推送唤起APP,一种是针对从最近任务列表唤起APP(或者icon)。

从最近的任务列表唤起,不走恢复流程

首先,APP端必须知道当前Activity的启动是不是在走恢复流程,Activity有一个onCreate方法,在ActivityThread新建Activity之后,会回调该函数,如果是从后台杀死恢复来的,回调onCreate的时候会传递一个非空的Bundle savedInstanceState给当前Activity,只要判断这个非空就能知道是否是恢复流程。

1
2
3
4
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}

知道恢复流程之后,如何处理呢?其实很简单,直接吊起闪屏页就可以了,不过这里有一点要注意的是,在启动闪屏页面的时候,必须要设置其IntentFlag:Intent.FLAG_ACTIVITY_NEW_TASK|Intent.FLAG_ACTIVITY_CLEAR_TASK,这样做的理由是为了清理之前的场景,不然之前的ActivityRecord栈仍然保留在ActivityManagerService中,具体实现如下,放在BaseActivity中就可以:

1
2
3
Intent intent = new Intent(this, SplashActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK|Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);

如果不设置会怎么样呢?举个例子,最常见的就是闪屏之后跳转主界面,主界面经常有router逻辑,并且其启动模式一般都是singleTask,处理一些推送,所以其onCreate跟onNewIntent都有相应的处理,如果不设置,在闪屏结束后,在startActivity启动主界面的时候,其实是先走恢复逻辑,然后走singleTask的onNewIntent逻辑,也就是说,onNewIntent跟onCreate是会同时调用的,也可能就会引发重复处理的逻辑,因此最好清理干净。

从推送唤起被杀APP时,如何走闪屏逻辑

对于推送消息的处理,其路由器一般放在MainActivity,并且在onCreate跟onNewIntent都有添加,如果APP存活的情况,可以直接跳转目标页面,如果APP被杀,这个时候,希望先跳转主界面,再跳转目标页面,在效果上来看就是,用户先看到目标页面,点击返回的时候再看到主界面,如果加上闪屏,希望达到的效果是先看到闪屏、点击返回看到目标页,再点击返回看到主页面。如果简单划分一下推送场景,可以看做一下三种

  • 进程存活,Activity存活
  • 进程存活,但是没有Activity存活
  • 进程不存在(无论是否被杀)

其实后面两种完全可以看做一种,这个时候,都是要先start MainActivity,然后让MainActivity在其OnCreate中通过startActivityForResult启动SplashActivity,SplashActivity返回后,在start TargetActivity。下面的讨论都是针对后面两种,需要做的有两件事

  • 一是:检测出后面两种场景,并且在唤起主界面的时候需要添加Intent.FLAG_ACTIVITY_CLEAR_TASK清理之前的现场
  • 二是:在MainActivity的路由系统中,针对这两种场景要,先跳转闪屏,闪屏回来后,再跳转推送页
    如何判断呢,后面两种场景其实只需要判断是否有Activity存活即可,也就是查查APP的topActivity是否为null,注意不要去向AMS查询,而是在本地进程中查询,可以通过反射查询ActivityThread的mActivities,也可以根据自己维护的Activity堆栈来判断,判断没有存活Activity的前提下,就跳转主页面去路由
1
2
3
4
Intent intent = new Intent(this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK|Intent.FLAG_ACTIVITY_CLEAR_TASK);
intent.setDate(跳转的Uri scheme)
startActivity(intent);

在MainActivity的路由中,需要准确区分是否是推送跳转进来的,如果不是推送跳转进来,就不需要什么特殊处理,如果是推送跳转进来一定会携带跳转scheme数据,根据是否携带数据做区分即可,看一下MainActivity的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Uri uri= getIntent().getData();
// 只有在intent被设置了跳转数据的时候才去跳转,一般是推送就来,如果冷启动,是没有数据的
if(uri!=null){
SplashActivity.startActivityForResult(this,JUMP_TO_TARGET)
}
}
//Intent.FLAG_ACTIVITY_CLEAR_TASK保证了onNewIntent被调用的时候,进程一定是正常活着的
@Override
protected void onNewIntent(Intent intent) {
Uri uri= intent.getData();
intent.setData(null);
router(uri);
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if(requestCode==JUMP_TO_TARGET && requestCode == RESULT_OK){
router(getIntent().getData());
getIntent().setData(null);
}
}

private void router(Uri uri) {

}

通过上面两部分的处理,基本能够满足APP“死亡”的情况下,先跳转闪屏的需求。

如何让你的app在后台被干掉后优雅的启动

作为一名Android开发师,肯定在处理用户的体验上下一定的功夫。有这么一个场景,在用户用着你开发的app的时候,突然某个聊天工具来消息了,切换到聊天工具后长时间停留,并且可能做了一些你不知道的操作,比如看视频阿,刷刷消息圈什么的。一般这种情况下都很容易出现手机内存不足的情况,内存不足就会可能被干掉。这种时候用户切换到app准备继续操作时,如果开发师处理不好,就会引起崩溃的情况,肯定会出现返回的时候一瞬间的白屏,对于用户体验的来说,非常不好。

开始

首先要介绍下Android中activity的四种启动模式(就当作复习一下旧知识吧,资料来源于网络总结):

Standard:是默认的也是标准的Task模式,在没有其他因素的影响下,使用此模式的Activity,会构造一个Activity的实例,加入到调用者的Task栈中去,对于使用频度一般开销一般什么都一般的Activity而言,standard模式无疑是最合适的,因为它逻辑简单条理清晰,所以是默认的选择。
singleTop:基本上于standard一致,仅在请求的Activity正好位于栈顶时,有所区别。此时,配置成singleTop的Activity,不再会构造新的实例加入到Task栈中,而是将新来的Intent发送到栈顶Activity中,栈顶的Activity可以通过重载onNewIntent来处理新的Intent(当然,也可以无视…)。这个模式,降低了位于栈顶时的一些重复开销,更避免了一些奇异的行为(想象一下,如果在栈顶连续几个都是同样的Activity,再一级级退出的时候,这是怎么样的用户体验…),很适合一些会有更新的列表Activity展示。一个活生生的实例是,在Android默认提供的应用中,浏览器(Browser)的书签Activity(BrowserBookmarkPage),就用的是singleTop。
singleTask:配置了这个属性的activity,最多仅有一个实例存在,而且,它在根的task中,在之后的被杀死重启的过程中我们会利用到这个配置,也就是我们的主界面MainActivity。
singleInstance:跟上面的singleTask基本上是一样的,但是,singleInstance的Activity,是它所在栈中仅有的一个Activity,如果涉及到的其他Activity,都移交到其他Task中进行,在实际开发中这个是用得比较少的。
这个是activity的生命周期:

就不多介绍这个生命周期了,相信都熟悉不过了,有想了解的自行Google或者百度吧。

重点

接下来是我们的重点:程序如果在后台被杀死之后,我们怎么去处理?是立刻恢复还是重新启动?哪个方法更适合我们?

首先,我们得知道,为什么程序会在后台被干掉的?我们又没有手动关闭程序。

app在后台被强杀,是在内存不足的情况下被强制释放了,也有一些恶心的rom会强制杀掉那些后台进程以释放缓存以提高所谓的用户体验。(注:当你的代码写得混乱、冗余,而且非常消耗内存的时候,那你的app在后台运行时将会比较容易被系统给干掉的,所以从现在开始要约束自己要养成良好的编码习惯和注意内存泄漏的问题)

我们都觉得android rom很恶心,但同时还是用些更恶心的手法去绕开这些瓶颈。乱,是因为在最上层没有一个很好的约束,这也是开源的弊端。anyway。我们还是得想破脑袋来解决这些问题,否则饭碗就没了。

我们现在来重现这个熟悉的一幕:

假设:App A -> B -> C

在C activity中点Home键后台运行,打开ddms,选中该App进程,强杀。

然后从“最近打开的应用”中选中该App,回到的界面是C activity,假设App中没有静态变量,这个时候是不会crash的,点击返回到B,这个时候也只是短暂白屏后显示B界面。但如果B中有引用静态变量,并想要获取静态变量中的某个值时,就NullPointer了。

以上复现的流程就几个点,我们展开说下:

当应用被强杀,整个App进程都是被杀掉了,所有变量全都被清空了。包括Application实例。更别提那些静态变量了。

虽然变量被清空了,但Android给了一些补救措施。activity栈没有被清空,也就是说A -> B -> C这个栈还保存了,只是ABC这几个activity实例没有了。所以回到App时,显示的还是C页面。另外当activity被强杀时,系统会调用onSaveInstance去让你保存一些变量,但我个人觉得面对海量的静态变量,这个根本不够用。返回到B会白屏,是因为B要重绘,重走onCreate流程,渲染上需要点时间,所以会白屏了。

大概是以上这些点。如果App中没有静态变量的引用,那就不用出现NullPointer这个crash,也就不需要解决。一旦你有静态变量,或者有些Application的全局变量,那就很危险了。比如登录状态,user profile等等。这些值都是空了。

肯定会有人说,这没关系啊,所有的静态变量都改到单例去不就好了吗?然后附加上一些持久化cache,空了再取缓存就ok了嘛。嗯,这肯定也是一个办法,但是这样的束手束脚对开发来说也是痛苦,至少需要多50%的编码时间才能全部cover。另外,还有那么多帮你挖坑的队友,难省心啊。

既然App都被强杀了,干嘛不重新走第一次启动的流程呢,别让App回到D而是启动A,这样所有的变量都是按正常的流程去初始化,也就不会空指针了,对吧?有人说这方案用户体验一点都不好呀。但哪有十全十美的事呢,是重走流程好,还是一点一个NullPointer好?好好去沟通,相信产品也不会为难你的。当然你也可以拿来举例,iOS在最近打开的应用里杀了某个App,重新点击那个App,还是会重走流程的啊。

如果你说用户已经打开了C界面,所以重新打开的是是恢复到C界面,这样的用户体验会更好啊,如果你是这样认为的,那你很多时间都是在防止恢复的时候不让你的app crash了,与其这样,还不如让整个app重新走整个流程呢,这样更省时间,而且这样也不用担心随时都会崩溃的情况,难道这样的用户体验不会更好吗?

那且想想如何让它不回到C而是重走流程呢?也就是说中断C的初始化而回到A,并且按back键,不会回到C,B。考虑一下。

我们先实例化这个场景吧。
A为App的启动页
B为首页
C为二级页面

把首页launchMode设置为singleTask,具体为什么上面介绍activity的启动模式的时候已经介绍了singleTask的作用了。

在BaseActivity中onCreate中判断App是否被强杀,强杀就不往下走,直接重走App流程。

首页起一个承接或者中转的作用,所有跨级跳转都需要通过首页来完成。

再给个提示,以上场景的解决方案也可以用于解决其它相关问题:
在任意页面退出App
在任意页面返回到首页
其实最重要的知识点就是launchMode

具体实现

AppStatusConstant

1
2
3
4
5
public static final int STATUS_FORCE_KILLED = -1;//应用放在后台被强杀了
public static final int STATUS_NORMAL = 2; //APP正常态//intent到MainActivity区分跳转目的
public static final String KEY_HOME_ACTION = "key_home_action";//返回到主页面
public static final int ACTION_BACK_TO_HOME = 0;//默认值
public static final int ACTION_RESTART_APP = 1;//被强杀

AppStatusManager

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public int appStatus= AppStatusConstant.STATUS_FORCE_KILLED; //APP状态初始值为没启动不在前台状态
public static AppStatus ManagerappStatusManager;
privateAppStatusManager() {
}
public static AppStatus ManagergetInstance() {
if(appStatusManager==null) {
appStatusManager=newAppStatusManager();
}
return appStatusManager;
}
public int getAppStatus() {
return appStatus;
}
public void setAppStatus(int appStatus) {
this.appStatus= appStatus;
}

BaseActivity(大致内容)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
switch(AppStatusManager.getInstance().getAppStatus()) {
case AppStatusConstant.STATUS_FORCE_KILLED:
restartApp();
break;
case AppStatusConstant.STATUS_NORMAL:
setUpViewAndData();
break;
}
protected abstract void setUpViewAndData();
protected void restartApp() {
Intent intent =newIntent(this,MainActivity.class);
intent.putExtra(AppStatusConstant.KEY_HOME_ACTION,AppStatusConstant.ACTION_RESTART_APP);
startActivity(intent);
}

每一个继承于父activity的都不要在oncreat中实现界面初始化和数据的初始化,因为如果被杀死之后,回来会走一次正常的生命流程的。

StartPageActivity配置(在oncreate()方法配置,并且在super()前):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
AppStatusManager.getInstance().setAppStatus(AppStatusConstant.STATUS_NORMAL);//进入应用初始化设置成未登录状态
MainActivity(配置了singleTask的主界面)

@Override
protected void restartApp() {
Toast.makeText(getApplicationContext(),"应用被回收重启",Toast.LENGTH_LONG).show();
startActivity(newIntent(this,StartPageActivity.class));
finish();
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
intaction = intent.getIntExtra(AppStatusConstant.KEY_HOME_ACTION,AppStatusConstant.ACTION_BACK_TO_HOME);
switch(action) {
case AppStatusConstant.ACTION_RESTART_APP:
restartApp();
break;
}
}

当应用打开的时候,启动StartPageActivity,然后设置app的status为normal状态,记住,一定要设置,因为默认的是被杀死的状态的。

当应用被杀死之后,所有数据都会被回收,所以之前设置的app status也会置于默认状态,即杀死状态,所以再次打开app的时候,status为杀死状态,就会走重启的流程,这里为什么要先跳转到MainActivity呢?就是因为MainActivity配置为了Sing了Task,当跳转到这个界面时,MainActivity就会置于Activity Task的最上层,其他的Activity将会被默认销毁掉,利用这种技巧去销毁其他的Activity,最后才是重新启动StartPageActivity。整个流程就是这样了。

大致的实现就如上所述了,我所倡导的宗旨就是花最少的时间,写最好的代码,实现最好的体验!之前也参考过很多网上大神们的实现方式,但是我觉得以上实现的应该是比较完整的一种了。