当一个程序第一次启动的时候,Android会启动一个LINUX进程和一个主线程。默认的情况下,所有该程序的组件都将在该进程和线程中运行。 同时,Android会为每个应用程序分配一个单独的LINUX用户。Android会尽量保留一个正在运行进程,只在内存资源出现不足时,Android会尝试停止一些进程从而释放足够的资源给其他新的进程使用, 也能保证用户正在访问的当前进程有足够的资源去及时地响应用户的事件。这也是之后要讲的如何在进程在后台运行时被杀死之后,怎么更好的去处理重新打开。
那每个activity又是什么?相当于是一个任务task,就比如说你正在微信聊天,打开的聊天界面activity,就是一个任务task,那所有的activity就组成了一个task栈。而每个activity所设置的启动模式又将改变它自己身在栈中的地位。
问题又来了,什么是activity的启动模式?其实就是在activity打开的时候根据你在配置文件(Manifest)配置的launchmode属性来添加到栈中去。启动模式又分为四种:"standard", "singleTop", "singleTask", "singleInstance",分别来简单介绍下这个四种不同的模式。
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完整的生命周期:
不多介绍这个生命周期了。
接下来是我们的重点:程序如果在后台被杀死之后,我们怎么去处理?是立刻恢复还是重新启动?哪个方法更适合我们?
首先,我们得知道,为什么程序会在后台被干掉的?我们又没有手动关闭程序。
我们先跳出来看看android的app运行原理。
app在后台被强杀,是在内存不足的情况下被强制释放了,也有一些恶心的rom会强制杀掉那些后台进程以释放缓存以提高所谓的用户体验。
我们都觉得android rom很恶心,但同时还是用些更恶心的手法去绕开这些瓶颈。乱,是因为在最上层没有一个很好的约束,这也是开源的弊端。anyway。我们还是得想破脑袋来解决这些问题,否则饭碗就没了。
我们先来重现这个bug:
假设: App A -> B -> C -> D
在D activity中点Home键后台运行,打开ddms,选中该App进程,强杀。
然后从“最近打开的应用”中选中该App,回到的界面是D activity,假设App中没有静态变量,这个时候是不会crash的,点击返回到C,这个时候也只是短暂黑屏后显示C界面。但如果C中有引用静态变量,并想要获取静态变量中的某个值时,就NullPointer了。
以上复现的流程就几个点,我们展开说下:
当应用被强杀,整个App进程都是被杀掉了,所有变量全都被清空了。包括Application实例。更别提那些静态变量了。
虽然变量被清空了,但Android给了一些补救措施。activity栈没有被清空,也就是说A -> B -> C -> D这个栈还保存了,只是ABCD这几个activity实例没有了。所以回到App时,显示的还是D页面
另外当activity被强杀时,系统会调用onSaveInstance去让你保存一些变量,但我个人觉得面对海量的静态变量,这个根本不够用。
返回到C会黑屏,是因为C要重绘,重走onCreate流程,渲染上需要点时间,所以会黑屏。
大概是以上这些点。如果App中没有静态变量的引用,那就不用出现NullPointer这个crash,也就不需要解决。一旦你有静态变量,或者有些Application的全局变量,那就很危险了。比如登录状态,user profile等等。这些值都是空了。
肯定会有人说,这没关系啊,所有的静态变量都改到单例去不就好了吗?然后附加上一些持久化cache,空了再取缓存就ok了嘛。嗯,这肯定也是一个办法,但是这样的束手束脚对开发来说也是痛苦,至少需要多50%的编码时间才能全部cover。另外,还有那么多帮你挖坑的队友,难省心啊。
既然App都被强杀了,干嘛不重新走第一次启动的流程呢,别让App回到D而是启动A,这样所有的变量都是按正常的流程去初始化,也就不会空指针了,对吧?有人说这方案用户体验一点都不好呀。但哪有十全十美的事呢,是重走流程好,还是一点一个NullPointer好?好好去沟通,相信产品也不会为难你的。当然你也可以拿iOS来举例,iOS在最近打开的应用里杀了某个App,重新点击那个App,还是会重走流程的啊。
那且想想如何让它不回到D而是重走流程呢?也就是说中断D的初始化而回到A,并且按back键,不会回到D,C,B。考虑一下。
我们先实例化这个场景吧。
A 为App的启动页
B 为首页
C 为二级页面
D为三级页面
简单说下解决方案,剩下的自己思考。
把首页launchMode设置为singleTask,具体为什么我就不说了,自己google。
在BaseActivity中onCreate中判断App是否被强杀,强杀就不往下走,直接重走App流程。
首页起一个承接或者中转的作用,所有跨级跳转都需要通过首页来完成。
再给个提示,以上场景的解决方案也可以用于解决其它相关问题:
在任意页面退出App
在任意页面返回到首页
其实最重要的知识点就是launchMode
当我第一次碰到这种问题的时候就在想,为啥Android非得这么来实现,既然都已经把应用强杀了,为什么还把栈信息保存下来了。既然把栈信息保存下来,为什么不把整个App变量都cache到硬盘上呢。这样还能节省ram,每个当前运行的App分到的最大内存也不用再加限制了啊。这样的话Bitmap的OOM也很难发生了。
有很多bug都是系统级的限制,虽说没有解决不了的技术,但是偏要钻牛角尖偏要用自以为的方式去解决问题,那么就是坑自己,并且也坑了队友。
附代码:
AppStatusConstant:
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=6; //默认值
public static final int ACTION_RESTART_APP=9;//被强杀
AppStatusManager:
public int appStatus= AppStatusConstant.STATUS_FORCE_KILLED; //APP状态 初始值为没启动 不在前台状态
public static AppStatusManager appStatusManager;
private AppStatusManager() {
}
public static AppStatusManager getInstance() {
if (appStatusManager == null) {
appStatusManager = new AppStatusManager();
}
return appStatusManager;
}
public int getAppStatus() {
return appStatus;
}
public void setAppStatus(int appStatus) {
this.appStatus = appStatus;
}
BaseActivity(BaseActivity)
switch (AppStatusManager.getInstance().getAppStatus()) {
/**
* 应用被强杀
*/
case AppStatusConstant.STATUS_FORCE_KILLED:
//跳到主页,主页lauchmode SINGLETASK
protectApp();
break;
case AppStatusConstant.STATUS_NORMAL:
onInitActivityAnim();
if (mIsAnimEnabled) {
overridePendingTransition(mActivityEnterAnim, mActivityHoldAnim);
}
setUpViewAndData();
break;
}
protected abstract void setUpViewAndData();
protected void protectApp() {
Intent intent = new Intent(this, MainActivity.class);
intent.putExtra(AppStatusConstant.KEY_HOME_ACTION, AppStatusConstant.ACTION_RESTART_APP);
startActivity(intent);
}
每一个继承于父activity的都不要在oncreat中实现界面初始化和数据的初始化,因为如果被杀死之后,回来会走一次正常的生命流程的。
在StartPageActivity配置:
AppStatusManager.getInstance().setAppStatus(AppStatusConstant.STATUS_NORMAL); //进入应用初始化设置成未登录状态
MainActivity(配置了singleTask的主界面)
@Override
protected void protectApp() {
Toast.makeText(getApplicationContext(),"应用被回收重启",Toast.LENGTH_LONG).show();
startActivity(new Intent(this, StartPageActivity.class));
finish();
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
getIntentData(intent);
int action = intent.getIntExtra(AppStatusConstant.KEY_HOME_ACTION, AppStatusConstant.ACTION_BACK_TO_HOME);
switch (action) {
case AppStatusConstant.ACTION_RESTART_APP:
protectApp();
break;
case AppStatusConstant.ACTION_BACK_TO_HOME:
break;
}
}