这是我第三遍看《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语法如下:
具体说一下:
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/
大家无聊可以来看看。。。下次见