Activity的生命周期和启动模式

这是我第三遍看《Android开发艺术探索》这本书了,从第一遍看的云里雾里,第二遍略微明白之后,我决定看第三遍,并且将阅读过程中的理解与感悟写在这里,作为我自己知识的梳理,文章的流程会根据我的思路来写,所以可能有些跳跃。心疼你看到了这篇乱七八糟的博客,如果你决定继续看的话,最好带着一颗宽容的心,并且准备好镇定剂,而且最好你得有一本《Android开发艺术探索》

Activity的生命周期

Activity的生命周期分为常规流程和异常流程

常规流程不必说就是, OnCreate—>OnStart->OnResume->OnPause->OnStop->OnDestory ,OnRestart这么一套下来。

而异常流程则是因为Activity意外终止带来的重启等流程,常见的比如Activity被系统回收啦,或者当前设备Configuration改变啦(比如横竖屏切换)之类的

Activity常规流程

先来看看每个生命周期表示什么,以及它们各自的含义

OnCreate:表示Activity正在被创建,生命周期的第一个方法,这个方法里我们可以做一些初始化操作,比如setContentView去加载界面布局,以及初始化各种变量啊数据啊什么的

OnStart:表示Activity正在被启动,即将开始,此时Activity以及可见,但是还没出现在前台,无法和用户交互。

(问题:书上说这个时候可以理解为Activity已经显示出来了,但我们还看不到,我觉得这个有点抽象,为什么显示了我们还看不到呢?以及为什么可见呢?

猜测:我觉得可见是对系统来说的,也就说你可以将Android的界面看做一个个图层,桌面是一个图层,Activity是一个图层,而OnStart方法表示Activity已经变成一个图层,并被放在桌面这个图层的下方)

OnResume: 表示Activity可见,并出现在前台开始活动,注意和OnStart对比,OnStart表示可见但在后台,OnResume表示可见并出现在前台(我个人理解OnResume时Activity出现在桌面这个图层的前面)

OnPause:表示Activity正在停止,正常情况下,紧接着OnStop会被调用,(但是如果你手速够快,再快速回到当前Activity那么OnStop不会调用,而是会调用OnResume方法,不过这个方法很极端,你可以忽略掉)我们可以再OnPause方法里做一些存储数据,停止动画等操作,但是不能太耗时(为什么不能太耗时?因为你在Activity_A里启动Activity_B,那么会先调用Activity_A的OnPause,在调用Activity_B的OnCreate—OnStart—OnResume方法,所以如果在OnPause方法里做耗时操作,会影响新Activity的显示)

OnStop:表示即将停止,可以做一些重量级的回收操作,但依然不能太耗时(对原话不理解,既然重量级为什么不是耗时的??)

OnDestory:表示Activity即将被销毁,这是Activity生命周期的最后一个回调,我们可以在这里做最终的回收工作,和资源释放

OnRestart:表示Activity正在重新启动,一般在Activity从不可见重新变为可见的时候调用,比如用户从Activity_A按HOME键切出去,又切回Activity_A来

附上一张图片让大家更好理解:

这里说明一个特殊情况:
用户打开一个新Activity或者切回桌面时,原本的Activity回调为OnPause->OnStop,但是如果新Activity是透明主题时,原本的Activity是不会回调OnStop的(这个好理解,因为OnStop表示应用被收到了后台,对人眼不可见)

疑问:Android为什么提供OnStop,OnPause,OnStart,OnResume四个方法而不是提供OnStop,OnStart或者OnPause,OnResume这两个之一呢

回答:因为OnStart和OnResume的区分是是否位于前台,OnDestroy,OnStop类似,除此之外没有啥明显区别(我猜测有些操作可能需要Activity位于后台而有些需要位于前台,所以Google提供了两个方法来供我们选择)

(原书还通过源码分析了在Activity_A里启动Activity_B,会先调用Activity_A的OnPause,还是先调用Activity_B的OnResume方法的问题,这个问题等后面讲四大组件分析的时候再说,现在先不讲,我也不懂。。。)

Activity异常流程

异常流程是指用户操作之外的,由操作系统导致的生命周期方法的调度。比如因为系统内存不够,优先级低的应用被杀死了,或者应用切换横竖屏也会引起应用的重启

1:系统配置改变引起的Activity被杀死重建

先列一下系统配置有哪些:

"mcc" 国际移动用户识别码所属国家代号是改变了----- sim被侦测到了,去更新mcc mcc是移动用户所属国家代号

"mnc" 国际移动用户识别码的移动网号码是改变了------ sim被侦测到了,去更新mnc MNC是移动网号码,最多由两位数字组成,用于识别移动用户所归属的移动通信网

"locale" 地址改变了-----一般指切换了系统语言

"touchscreen" 触摸屏是改变了------通常是不会发生的

"keyboard" 键盘发生了改变----例如用户用了外部的键盘

"keyboardHidden" 键盘的可用性发生了改变

"navigation" 导航发生了变化-----通常也不会发生

"screenLayout" 屏幕的显示发生了变化------不同的显示被激活,比如屏幕旋转了(API13新加的)

"fontScale" 字体比例发生了变化----选择了不同的全局字体

"uiMode" 用户的模式发生了变化

"orientation" 屏幕方向改变了

"screenSize" 屏幕大小改变了

"smallestScreenSize" 屏幕的物理大小改变了,如:连接到一个外部的屏幕上(API13新加的)

"layoutDirection" 布局方向发生改变时,用的较少,正常情况下无须改变布局的layoutDirection属性(API17新加的)

以上就是系统属性,上述的系统属性要是改变了,那么Activity就会重建。

例子:如果我们将屏幕旋转,那么orientation属性发生了改变,于是Activity就会重建。Activity的生命周期就会变成这个样子:onSaveInstanceState->OnStop->onCreate->OnStart->onRestoreInstanceState,我没有列出OnPause和OnResume是因为OnPause和onSaveInstanceState以及OnResume和onRestoreInstanceState这两者之间的顺序不是固定的,OnPause可能出现在OnSaveInstanceState之前或之后,OnResume类似。

那么Activity被重建了,我们的数据怎么办呢?不用怕,Activity默认会替我们执行一定的恢复工作,也就是onSaveInstanceState,onRestoreInstanceState。

其中,onSaveInstanceState是用来保存数据,将数据保存在Bundle中,然后在重建时将Bundle传给onRestoreInstanceState以及onCreate方法。所以我们可以通过onCreate和onRestoreInstanceState方法来判断Activity是否重建了,是的话,我们就可以取出数据并恢复。

关于View的恢复:诸如EditText里输入的文本,ListView的滑动位置等各种View的状态,Activity是会为我们保存的,如果我们想了解具体某个View为我们保存了哪些东西,可以看看他们的源码,每个View都会有onSaveInstanceState和onRestoreInstanceState

关于保存和恢复View层次结构:Activity被意外中止时,Activity会调用onSaveInstanceState保存数据,然后Activity会委托Window去保存数据,然后Window再委托它上面的顶层容器去保存数据,顶层容器是一个ViewGroup(一般来说很可能是DecorView),然后顶层容器再一一通知其子元素来保存数据(这个过程和事件分发机制很相似(以后会写到))(这个看不懂没关系,大致知道个过程就可以了)

(问题来了:我写了一个测试,测试EditText控件是否会在旋转屏幕重建之后保存并恢复数据,然而它没有。。输入的数据在旋转屏幕后消失了,但是书上说TextView是可以保存恢复数据的,我看了EditText的源码,它是继承TextView的,并且TextView源码里也是写了onSaveInstanceState和onRestoreInstanceState的。。)

(问题解决:后来我试了各种情况,发现只有在代码里绑定了xml里的控件时,才会保存和恢复数据,其实想想也对。。如果不绑定,那你输入数据也没用。。唉。。我好白痴)

下面我们写个例子,在onSaveInstanceState里面放入一个数据,然后在onRestoreInstanceState里去恢复它 ,代码如下

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    if(savedInstanceState!=null){
        char val = (char) savedInstanceState.get("test");
        Log.d(TAG, "onCreate: "+val);
    }
}

@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    outState.putChar("test",'A');
    Log.d(TAG, "onSaveInstanceState: "+"A");
}

@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
    super.onRestoreInstanceState(savedInstanceState);
    char val = (char) savedInstanceState.get("test");
    Log.d(TAG, "onRestoreInstanceState: "+val);
}

运行截图如下:

可以看到我们的数据在onCreate和onRestoreInstanceState里都恢复成功了。这里说明一下,首先onSaveInstanceState和onRestoreInstanceState只会在Activity即将被销毁并且有机会重新显示时才会调用,比如你横竖屏切换,Activity终止之后会马上重建,这时就会调用。正常退出比如按Back键是不会调用的,因为正常退出Activity就不可见了。

关于如何阻止属性变化时Activity重建:只要在AndroidMenifest.xml里面给Activity加上android:configChanges声明就行,比如声明android:configChanges="orientation|screenSize",那么横竖屏切换时Activity就不会重建了

2:内存不足导致优先级低的Activity被杀死

这种情况不好模拟,但是其数据保存恢复过程与第一种完全一致。

Activity的优先级从高到低分为如下三种:
1.前台Activity:就是正在和我们交互式Activity

2.可见但是非前台:比如Activity中弹出了一个对话框,那么对话框下面那个Activity就是可见但非前台的,此Activity我们看得见,但是不能交互

3.后台Activity:已经被暂停的Activity,比如执行了onStop的Activity

当内存不足时,系统就会按照上面的优先级顺序杀死Activity所在的进程,并在后续通过onSaveInstanceState和onRestoreInstanceState来保存和恢复数据。如果一个进程没有四大组件独立运行是很容易被杀死的,所以我们要在后台做事情,最好将其放进一个Service中来保证其有一定的优先级,不会被轻易杀死。

Activity的启动模式

Activity的LaunchMode

首先,我们来说下为什么需要Activity启动模式。
如果我们不设置启动模式的话,我们多次启动同一个Activity,系统就会多次创建多个实例,并将其压入任务栈内,然后如果我们按Back键,那么栈内的Activity就会一个一个弹出,等到栈空了,系统就会回收这个栈。

看到这个我想你应该明白了,启动相同的Activity还要创建新的实例不是傻嘛,所以,我们就有了启动模式。

启动模式有四个,分别是standard,singleTop,singleTask,singleInstance,我们来一一讲一下

1.standard:标准模式,系统默认模式,也就是启动一个就会创建一个的傻瓜模式。这个模式下,谁启动Activity,Activity就会在谁的任务栈下。

2.singleTop:栈顶复用模式,这个模式下,如果Activity在栈顶,那么Activity不会被创建新实例,同时它的NewInstance方法会被回调,通过此方法的参数,我们可以取出当前请求的信息,并且,这个Activity的onCreate和onStart不会被调用(经测试,onResume会被调用)。如果启动的Activity已存在但不是在栈顶,那么还是会创建新的实例,比如说本来栈内Activity是BC,然后C又启动了B,那么因为B不在栈顶,所以栈内Activity变为BCB

3.singleTask:栈内复用模式,这个模式下,只要有实例存在了,就不会创建实例,并且也会调用NewInstance。具体一点,比如你要创建一个D,那么如果D实例不存在,就会新创建一个任务栈,并创建D实例,然后把D放进去;如果D实例存在,这时要看新启动的D所需的任务栈存不存在,如果任务栈不存在,就会创建这个任务栈,然后把新的D放进去,如果任务栈存在,那么再看D实例是不是存在,如果存在,那么就把它压到栈顶,并且将之前它上面的Activity全都出栈(这个原因是因为singleTask默认具有clearTop效果),如果不存在,则创建然后压入栈。

4.singleInstance:单实例模式,这是一种加强了的singleTask模式,除了具有singleTask的所有特性外,他还有一点,就是此模式的Activity只能单独的位于一个任务栈中。比如说,A启动了,系统就会给他一个任务栈,这个任务栈里只有A一个Activity,并且之后的Activity都不会创建新的实例,除非这个任务栈销毁了。

听了上面的介绍,我们可能会疑惑,Activity所需的任务栈到底是什么?其实就是一个属性,叫TaskAffinity,可以在AndroidMenifest里注册。这个参数表示了Activity所需任务栈的名字,默认情况下,所有Activity所需的任务栈的名字为应用的包名。taskAffinity属性主要和singleTask模式或者和allowTaskReparenting属性配对使用,其他情况下它是没有意义的。另外,任务栈分为前台任务栈和后台任务栈,后台任务栈中的Activity处于暂停状态,用户可以通过切换将其调到前台。

关于如何添加launchMode属性:可以再AndroidMenifest里添加,也可以在Intent.addFlags()里添加。两种的区别在于,第二种优先级要大于第一种,并且两种方法在可添加的范围上也不同,比如第一种无法直接添加FLAG_ACTIVITY_CLEAR_TOP标识,第二种无法为Activity指定singleInstance模式。

关于taskAffinity与singleTask配合使用时:此种情况下,taskAffinity是具有singleTask模式的Activity的目前任务栈的名字,待启动的Activity会运行在名字和taskAffinity相同的任务栈中

关于taskAffinity和allowTaskReparenting配合使用:当应用A启动了应用B的某个Activity,如果这个Activity的allowTaskReparenting属性为true,那么当应用B被启动后,此Activity会直接从应用A的任务栈转移到应用B的任务栈内。具体来说,比如应用A启动了应用B的ActivityC,然后按Home返回桌面,再打开,会发现打开的不是应用B的MainActivity而是ActivityC,因为应用B启动后,C想要的任务栈被创建了,所以移到了应用B里来。

关于Activity的Flags,我们就介绍常用的几种吧:

FLAG_ACTIVITY_NEW_TASK: 指定singletask模式

FLAG_ACTIVITY_SINGLE_TOP: 指定singletop模式

FLAG_ACTIVITY_CLEAR_TOP: 指定此falg的Activity启动后,会将其之前任务栈上面的Activity全都出栈。这个标志位一般和singletask模式一起出现,这种情况下,如果实例以已存在,那么系统就会调用它的NewInstance。如果被启动的Activity是standard模式,那么被启动的Activity本身会和它之上的Activity一起出栈,然后系统会创建新的实例放入栈内

FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS:防止用户通过历史列表返回应用,具体效果就是你呼出任务管理器看不见设置了这个标志位的应用

IntentFilter的匹配规则

我们知道Intent分为显示调用和隐式调用,那么隐式调用就要设置IntentFilter属性。而IntentFilter属性有action,category,data三个。为了匹配IntentFilter,必须同时匹配action,category,data,否则匹配失败。一个IntentFilter里面,可以设置多个action,category,data。一个Activity可以有多个IntentFilter,只要匹配其中一个就可以成功启动对应的Activity。

那我们来分析一下各个属性具体的匹配过程:

1.action:Intent中setAction里的字符串和action完全一样。因为能设置多个action,所以只要能匹配上一个就算匹配成功。如果Intent没有指定action,那么就算匹配失败。还有就是action区分大小写。

2.category:系统预设了一些category,我们也可以设置自己的category。和action不同,category设置了,就必须和任意一个声明的category匹配,也就是说,你不能添加没有声明的category,而action是声明多个只要有一个匹配就行了.如果Intent不设置category,也是能匹配的,因为系统在调用startActivity或者startActivityForResult时会默认加上android.intent.category.DEFAULT这个category,所以我们声明的时候必须加上android.intent.category.DEFAULT这个参数。

3.data:匹配规则和action类似,如果定义了data属性,那么Intent里面也要添加可匹配的data。

data语法如下:

    <data android:mimeType="string"
          android:scheme="string"
          android:host="string"
          android:port="string"
          android:path="string"
          android:pathPattern="string"
          android:pathPrefix="string" />

具体说一下:
Scheme:URI模式,比如http,file,content等,如果没有指定Scheme,那么整个URI无效。

Host:URI的主机名,比如www.baidu.com,如果host未指定,那么整个URI无效。

Port:URI的端口号。

Path,PathPattern和pathPrefix表示都是路径,path表示完整路径,Pathpattern也表示完整路径,但是它里面可以写通配符“*”(得注意转义),pathprefix表示路径的前缀信息(举个例子,完整路径是http://example.com/blog/abc.html,那么pathprefix只要写/blog就能匹配了)。

各种情况:

1.data的scheme如果没有设置,那么默认值为content和file(写任意一个就可以匹配)

2.如果要给Intent指定完整的data,需要用setDataAndType()而不能用setDate和setType,因为上面两个函数执行时都会清空对方的值。

有关如何判断是否有Activity来匹配我们的隐式Intent:因为如果没有Activity可以匹配的话,程序是会出错的,所以我们需要判断一下,判断可以用PackageManager的resolveActivity方法,或者Intent的resolveActivity方法,它们找不到可以匹配的Activity会返回null。packageManager还有一个queryIntentActivities方法可以将所有成功匹配的Activity都返回出来。

结束了,其实我有个自己的博客https://chentiansaber.github.io/
大家无聊可以来看看。。。下次见

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容