在网上搜安卓教程的时候听到好几个地方推荐这本书,说是比较适合入门之后的菜鸟们学习。那我就二话不说下了个PDF看了一下,发现确实有些地方介绍的挺不错的,尤其是干货比较多。因此可能会写的比较长或者啰嗦,希望大家谅解。虽然这是一本android进阶书,但我希望我写的笔记能给更多的人看懂。特地也开了这本书的学习笔记连载,希望能对大家有所帮助。
(有些内容书上没有讲的很仔细,针对一些我自己想到的情况我会自己尽可能做一下实验然后把结果分享给大家,所以不可避免有时候会有错误。欢迎大家指正~)。
第一章 Activity的生命周期和启动模式
正如书中所说的Activity作为安卓的四大组件之首,其重要程度不言而喻。那么这一章的侧重点介绍的是Activity在使用过程中的一些不容易搞清楚的概念,主要包括生命周期和启动模式以及IntentFilter的匹配规则。
Activity的生命周期
典型情况下的生命周期
相信大家也都知道。一个Activity从开始到创建经历的一般流程是:onCreate,onStart,onResume,onPause,onStop,onDestroy。下面我们就来细说一下其中究竟在每个环节都做什么什么样的事情。
- onCreate:相信写过app的童鞋们都会发现里面总是做一些初始化工作。比如setContentView来加载布局资源,还有一些控件的绑定。在onCreate做这些能大概率的避免因没有绑定组件而产生的空指针异常。
- onStart:书中原话为:表示Activity已经可见了,但没有出现在前台,还无法和用户交互。可以理解为Activity已经显示出来了,但我们还看不到。(读到这里表示有点不懂,但是我们至少可以知道可见和前台这是两个不同的概念)。
- onResume:Activity已可见了。(onStart的时候也已经可见了,但那时还在后台,onResume的时候才显示到前台)。
- onPause:表示Activity正在停止。一般情况下会立马调用onStop方法。如果这个时候快速回到当前Activity则会调用onResume方法。(???就是说我从A切换到B然后突然反悔啦?搞不懂)。作者也说上述情况比较极端,用户很难模拟这种情况。在onPause里面可以放一些数据存储,停止动画等操作,但是不能很耗时。为什么呢?因为这个会影响下个Activity的产生。因为顺序是当前的Activity的onPause执行完之后才能执行下一个Activity的onResume。(这个大家感兴趣的可以自己去试一下,毕竟亲身体验过才更有印象)
- onStop:表示Activity**即将停止**(有点文字游戏的赶脚。。)书中说是可以做一些稍微重量级的操作,但也不能很耗时。
- onDestroy:表示Activity**即将被销毁**,这里通常放一些资源回收。
- onRestart:这个书中没有写,不过相信大家都知道。就是A跳转到B,而且A没有被销毁掉的情况下,B跳转到A,那么会执行A的onRestart而不是onCreate。
我自己实验的时候发现两个我自己之前还不知道的情况:
(1)假设从A跳转到B,那么执行的顺序一定是AonPause->BonCreate/BonRestart->BonStart->BonResume->AonStop。大家可以自己试试。无论在这五个环节的任意一个写一个耗时操作(我用的是直接线程睡眠1s),顺序都是不变的。
(2)在(1)的5个执行序列里面,除了在AonStop里面放耗时操作不会影响Activity跳转的流畅性,其他四个都会。那么这个是不是可以说明:A到B的跳转实质是从AonPause到BonResume执行完。可能大家会说onDestroy呢?因为onDestroy肯定是在onStop之后的,所以就不需要考虑了。
这里说明书中提到的几点,可能大家都知道了,但我这里还是啰嗦一下:
1. 一个Activity的启动是onCreate->onStart->onResume
2. 用户打开新的Activity或者切换到桌面时原来的Activity会执行:onPause->onStop。如果用户采用透明主题那不会回调onStop。
3. 用户回到原Activity时,onRestart->onStart->onResume。
4. 用户按back回退时,onPause->onStop->onDestroy
5. Activity被系统回收以后,再次打开和1一样。只是生命周期一样,并不是所有过程都一样。
6. 从生命周期来看,onCreate和onDestroy是一对,表示Activity的创建和销毁并且只能被调用一次;onStart和onStop是一对,表示Activity是否可见,可以被调用多次;onResume和onPause是一对,表示Activity是否在前台,可以被调用多次。
作者提出的两个问题:
- onStart和onResume,onPause和onStop的不同点。
- A->B,A的onPause和B的onResume哪个先执行。
第一个问题我相信我在前面也写的比较详细了,第二问题的话根据我自己试验的结果也能得出答案(A先执行onPause)。从源码角度分析原因的话太复杂啦,我就不写啦~(偷个懒,其实书上也就贴了一段代码。大家只要记住这个结论就行了额)
异常情况下的生命周期
书中分析了两种情况:
1. 资源相关的系统配置发生改变导致Activity被杀死并重新创建
这说的是什么意思呢?比如一个Activity允许旋转屏幕的话,这个Activity就会被杀死并重新创建。在这种情况下,Activity会调用onSaveInstanceState来保存当前Activity的状态(只有在异常终止的时候会自动回调)。这个方法调用在onStop之前,但是跟onPause没有关系。在Activity重建的时候会调用onRestoreInstanceState来恢复Activity的状态。(可以通过监听onRestoreInstanceState来判断Activity是否被重建了)onRestoreInstanceState在onStart之后被调用。
上述的状态恢复技术,能够为我们恢复View层次的信息,如输入框里面的内容。具体流程为:Activity被终止时会调用onSaveInstanceState保存数据,然后Acitvity会委托Window去保存数据,接着Window再委托它的顶层容器来保存。这个顶层容器是个ViewGroup,一般情况是个DecorView,最后顶层容器再去一一通知它的子元素来保存相应的数据。(不知大家看明白没,斜眼笑~。在我的理解上就是这样子:一个官员得到消息说我们这里要地震,然后就上报消息给他的上级直到最后到皇上,然后皇上就下令让手下的官员准备好一切工作,官员再告知他们手下的官员直到最后做实事的人。嘿嘿嘿不知道这样子描述大家能不能听懂~)
2. 资源内存不足导致低优先级的Activity被杀死
Activity按优先级从高到低可以分为:(1)前台Activity:正在和用户交互的Activity,(2)可见但非前台Activity:比如弹出对话框以后,被对话框挡住的Activity,(3)后台Activity:A->B,那么A就是后台Activity。其实这个也挺好理解的,作为用户的角度,用户永远只关心当前操作的界面,后台的(不执行下载任务的那种)谁管它死没死,死了更好还省内存呢!!说到这里,有必要提一下那种在后台执行四大组件的Activity优先级并不是很低,如我刚刚说的在执行某个下载任务。所以当系统内存不足时会优先杀死优先级低的Activity。这个也是属于异常情况杀死Activity的所以自然也可以通过onSaveInstanceState和onRestoreInstanceState来恢复数据。
如果要指定系统配置改变以后不重新创建Activity则需要在Manifiest里面配置一个叫做configChanges的属性。如我们这个旋转屏幕的例子,就只需要加一句android:configChanges=”orientation|screenSize”就可以阻止Activity重建。(没有重建自然不会有恢复机制了)。
Activity的启动模式
作为新手写app的时候肯定没有在意过Activity的启动模式,因为标准的启动模式完全足够了,而且一般来说自己写一些app玩玩的话,只用标准的启动模式也不怎么会出问题。但有时候如果要开发一些项目,我们却又不得不使用其他的启动模式。
Activity的LaunchMode
首先介绍一下任务栈这个概念:所谓任务栈其实也就是Activity栈。既然是栈就会遵循先入后出的规则。而且系统不只有一个任务栈,当一个任务栈里面没有东西的时候系统就会回收这个任务栈。
- standard:标准模式。也就是我们平时什么都不加的系统默认启动模式。每次启动一个Activity都会产生一个新的实例并压入任务栈中。比如A->A->A->A->A那么这个任务栈里面就会有5个A实例,如果要用back键退出程序则需要按5下。再如:A->B(B是标准模式),则B会进入A的任务栈。
这里我有一个小问题:作者说使用ApplicationContext去启动standard模式的Activity会报错,原因是standard模式启动的Activity会默认进入启动它的任务栈里面,如果用ApplicationContext则丢失所谓的任务栈。startActivity(new Intent(getApplicationContext(),FirstActivity.class));我使用这句话实验的时候,但好像没有出现错误。此处确实有点不解,如果大家知道为什么的话恳请指点一下小弟的迷津~ - singleTop:栈顶复用模式。如果要启动的Activity已经位于栈顶了,则不会创建新的实例。
如:A->A->A->A->A还是只有一个A实例。**这里要注意一点:**A->A以后并不会执行onCreate和onStart,而是执行onNewIntent(Intent),大家看到这个方法参数是Intent型,说明可以接受消息并处理。还有一点是如果要启动的Activity不在栈顶,则仍然会新建一个Activity。singleTop也跟standard一样会进入启动它的Activity所在的栈里面。
如果A->A:则生命周期为onPause->onNewIntent->onResume。 singleTask:栈内复用模式。这个就是singleTop的进化版,只要栈里面有实例的则就直接把那个实例放到栈顶,所有本来在它上面的Activity直接从栈里移除。(所谓的clearTop性质)。和singleTop一样,如果本来有实例,调用的也是onNewIntent。不同的是它在刚发出启动的请求的时候,系统会先找是否存在我这个Activity期望呆的任务栈,没有就新建一个任务栈然后新建一个Activity实例放进去。
如果A->A:则生命周期为onPause->onNewIntent->onResume。
如果A->B(不为singleInstance)->A:则B->A的生命周期为:BonPause->AonNewIntent->AonRestart->AonStart->AonResume->BonStop->BonDestroysingleInstance:单实例模式。这个是终极single。就是说他具有singleTask的一切特性,还包括一个:这个Activity只能位于一个独立的任务栈。除非这个任务栈被销毁了,否则再也不会创建这个Activity的新的实例。这个有什么用呢?我在另外一本《第一行代码》中找到了实用例子:多个程序要共享一个Activity实例的时候就要使用singleInstance(我觉得原因是因为singleInstance确保自己的实例是放在一个单独的返回栈里面的)。
如果A->B->A:则B->A的生命周期为:BonPause->AonNewIntent->AonRestart->AonStart->AonResume->BonStop
总结:通过上面的描述以及我做的几个实验大致可以得出以下一些结论:如果一个Activity位于栈顶,并且是single家族的,那么启动自己就不会调用onRestart,如果不位于栈顶,并且是singleTask则会执行onRestart;singleTask默认没有独立的任务栈,所以上面会执行BonDestroy(clearTop性质还记得吗?);反之singleInstance则是位于独立的任务栈。
这里作者还指出一种情况:如果AB在前台任务栈,CD在后台任务栈,并且CD为singleTask,此时启动ActivityD的话会把后台的所有Activity移至前台任务栈。即前台任务栈为ABCD;但如果启动ActivityC的话前台任务栈为ABC。(书中说原因在后面分析,但这个难道不是clearTop吗???)
关于任务栈:这里我就把作者说的话写一遍吧,感觉如果用自己说的话会有偏差。先说一个参数:TaskAffinity(任务相关性)。这个参数标识了一个Activity所需要的任务栈的名字,默认情况下所有任务栈的名字是包名,如果TaskAffinity指定为包名就相当于没有指定。TaskAffinity属性主要和singleTask启动模式或者allowTaskParenting属性配对使用,在其他情况下没有意义。另外,任务栈分为前台任务栈和后台任务栈,后台任务栈中的Activity位于暂停状态,用户可以通过切换将后台任务栈调到前台。
当TaskAffinity和singleTask配对使用的时候:TaskAffinity 是 具有该模式的Activity的目前任务栈的名字,待启动的Activity会运行在名字和TaskAffinity相同的任务栈。
当TaskAffinity和allowTaskParenting结合的时候:*allowTaskParenting为true时,假设有两个应用 A B 。A启动的属于B的Activity C,C会运行在A的任务栈里面。此时按下home键回到桌面(C还在运行),按下B的启动按钮,会把C重新拉回到B的任务栈。因为是新启动B,B的任务栈里面还是空的,而C已经被创建了,所以无论B的MainActivity是不是C都会显示C。大家可以这么理解*:B原来养了一只狗C,但是B有事出门了。A作为B的邻居想要把C叫到自己家里来看门。因为C也认识A,所以就过去了,但是呆的肯定没有在B家里舒服。有一天B回家了,那么C看到以后直接屁颠屁颠跑回来了。
TaskAffinity在Manifest里面可以指定,如:android:taskAffinity=”com.example.test”
allowTaskParenting在Manifest里面可以指定,如:android:allowTaskParenting=”true”
那么如何指定启动模式呢?
- 通过Menifest。如:android:launchMode=”singleTask”
通过Java代码:如Intent intent = new Intent();
intent.setClass(this,FirstActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
这两种方式都可以指定但仍有区别:优先级第二种高于第一种;
- 两种方式各有限定范围:1不能指定clearTop标识,2不能指定singleInstance。
这里作者出了一道挺有意思的题目,我写出来跟大家分享下,看看自己的答案是否与结果相同:A是standard,BC是singleTask而且BC的taskAffinity相同但与A不同,。打开应用进入A,执行如下界面跳转A->B->C->A->B,此时按下back键会是什么界面,再按下back键呢?
答案:按下第一下的时候回到A界面,再次按下back键回到桌面。
原因:A为standard,所以A的任务栈是包名,A启动B,B的任务栈与A不同,所以会新建一个任务栈压入B,B启动C,由于BC的任务栈相同,则把C压入B的栈顶。C启动A,由于A是standard所以会新建一个A的实例并把这个实例压入C的上面。即目前的栈情况为:后台栈:A,前台栈:BCA。此时A启动B,由于B是singleTask,所以由clearTop的性质,那么后台栈就只有B。按下返回键,B出栈,前台栈销毁,此时调用后台栈放到前台,并显示栈顶Activity也就是A,再按back,A出栈。所有任务栈销毁,退出程序。
Activity的Flags
这里就直接把一些书中提及的标志位罗列出来:
- FLAGS_ACTIVITY_NEW_TASK:指定“singleTask”启动模式
- FLAGS_ACTIVITY_SINGLE_TOP:指定“singleTop”启动模式
- FLAGS_ACTIVITY_CLEAR_TOP:指定clearTop属性,一般与FLAGS_ACTIVITY_NEW_TASK配合使用。如果被启动的Activity属性是standard,会把之前的Activity和它之前的所有Activity都出栈,然后创建一个新的实例压入栈中。比如ABC都是standard,A启动B具有clearTop属性则经过A->B>C>A>B之后,栈里面还剩下的是A->B只需要按两下back键就会退出程序。再举个复杂的例子A->A->A->A,前两个A->A如果没有指定clearTop,最后一个指定了clearTop,则执行完序列后栈中为AAA。这说明了clearTop属性只会找出和自己一样的最靠近栈顶的出栈。(可能我这描述比较拗口,大家看看我之前的分析自己理解一下。
- FLAGS_ACTIVITY_EXCLUDE_FROM_RECENTS:具有这个标记的Activity不会出现在历史Activity列表(recent列表)中。
IntentFilter的匹配规则
大家都知道启动Activity分为显示调用和隐式调用。其实就我而言基本不用隐式调用的,感觉太麻烦。(不知道多少人跟我一样呢?)作者在这里主要介绍的就是隐式调用,刚好可以弥补一下自己对这方面知识的空缺。
隐式调用主要就是在Java代码里面写一个intent,然后里面的一长串参数和在Manifest里面定义的IntentFilter的信息匹配上就可以启动Activity,我感觉比较类似于广播。IntentFilter的过滤信息主要分为:action,category,data。
- action:在
<intent-filter>
里面通过<action android:name="" />
标签调用。
匹配规则:Intent里面必须要有(好像也只能指定一个,在构造intent的时候传入一个,在后面调用intent.addAction()好像会把之前的action覆盖掉),但是在Intent-filter里面可以定义多个action过滤器,Intent里面定义的那个只要和其中之一个过滤器对上就算匹配成功。action过滤区分大小写。 - category:在
<intent-filter>
里面通过<category android:name="" />
标签调用。
匹配规则:Intent里面可以没有,但是如果有的话必须要与Intent-filter里面其中一个对应上。(我感觉好像和action匹配规则差不多,只是catagory可以不写,但是action必须写)此外还有一点比较重要:系统默认会给每一个Intent加上一个android.intent.category.DEFAULT的category(所以category可以不设置),所以我们在每个intent-filter里面必须加上<category android:name="android.intent.category.DEFAULT" />
,原因已经说明了。
其实到了现在 使用action和category就已经可以使用隐式调用启动Activity了。因为这两个必须在intent-filter里面指明,action用来指明大方向,category必须要加一个DEFAULT+其他自己定义的过滤规则。我们在Intent里面只要适配intent-filter里面的过滤信息就可以隐式启动Activity。
3.data: 如果你在intent-filter定义了data,那么你也必须要在intent里面定义data,而且要适配上。以上是我实验得出的结论。data比较复杂,其结构分为:mimeType和URI。
(1)mimeType指的是媒体类型,如:image/jpeg、audio/mpeg4-generic、video/*等。
(2)URI学名叫做统一资源标识符(Uniform Resource Identifier):表示的意思类似于路径。
URI的结构又可以这样子表示:<scheme>://<host>:<port>/[<path>|<pathPrefix>|<pathPattern>]
,
如content://com.example.project:200/folder/subfolder/etc;
http://www.baidu.com:80/search/info
- scheme:URI模式,如:file,http,content等。如果没有scheme,则整个URI失效。
- host:主机名,如:www.baidu.com。如果没有host,则整个URI失效。
- port:端口号。如:80。只有指定scheme和host的时候port才是有意义的。
- path,pathPattern,pathPrefix:表示路径信息。path是完整路径,pathPattern也是完整路径但是里面可以有通配符*,*表示0个或者多个任意字符;pathPrefix表示路径的前缀信息(我猜想应该是完整路径减文件名吧)。
data在intent-filter里面的字段表示:
<data android:scheme="string"/>
<data android:host="string"/>
<data android:port="string "/>
<data android:path="string"/>
<data android:pathPattern="string"/>
<data android:pathPrefix="string"/>
<data android:mimeType="string"/>
匹配规则:和action差不多,即data可以有很多很多的mimeType,scheme等等属性。只要Intent里面写的有一个能匹配上就可以了。
这里注意一点:如果你在intent-filter中没有指定URI,那么系统默认URI的scheme部分为content或者file,也就是说你在Intent里面指定scheme是http是不能匹配的。
还有几点书上没有提及的:如果intent是image/*,intent-filter是image/png可以成功,反之也可以。但如果intent是image/png,intent-filter是image/jpeg,则匹配失败;在intent-filter里面指定scheme就只需scheme=“content”,而在intent里面就需要setData(Uri.parse(“content://”)),或者setData(Uri.parse(“content:/”)),或者setData(Uri.parse(“content:”))。
这里介绍3个方法:setData(Uri data),我们看参数就知道这个是来设置URI的(不要被它的名字迷惑),这个方法会把mimeType置为null;setType(String mimeType),设置mimeType,同样也会把URI置null;所以在写完整的data的时候要用setDataAndType()来设置。
最后作者介绍了几种避免因为匹配不成功出异常的方法:
(我都亲自试验过,亲测可用~)
1. intent.resolveActivity(packManager),如果匹配不到则会返回null,匹配到了则返回下一个Activity的ComponentName。
2. packManager.queryIntentActivities(intent, int flags);这个返回的是一个List<ResolveInfo>
的数组。里面放的是所有匹配成功的Activity的信息。intent参数不用多说了,flags书上说我们都要用PackageManager.MATCH_DEFAULT_ONLY这个参数。简单一想大家也能明白为什么:若要使一个intent-filter有用,则肯定有DEFAULT这个category。我们就找这个参数,找出来的肯定是满足基本要求的。如果不用这个,虽然满足其他category的要求,但是找出来的Intent-filter不能保证有DEFAULT这个参数(即无效的)。
3. packManager.resolveActivity(intent, int flags);据说返回的是最适配的Activity,但我自己试的时候如果有两个Activity都能适配的话,他返回的值都不是这两个Activity的信息,而是另外一个奇奇怪怪的东西。这个就很怪,求大神指导咯~~~
特殊的intent-filter
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
这个大家肯定是不会陌生,这二者的共同作用是标明这是个入口Activity。其实还有许许多多其他的针对系统组件的intent-filter。我在最后TIPS那里贴几个链接,大家感兴趣的可以看看。
终于码完了,我本来以为只要一点点就可以把第一章给写完。但发现有很多东西如果直接把书抄进来你们会看不懂,而且书上讲的也很模糊。我不放心就自己一项一项去试,毕竟我不确定的东西也不能很肯定的写出来~~
TIPS
- decorView:http://blog.csdn.net/guxiao1201/article/details/41744107
http://www.cnblogs.com/yogin/p/4061050.html - configChanges:http://blog.csdn.net/zhaokaiqiang1992/article/details/19921703
- recent列表:就是手机上显示最近使用过哪些应用的列表。一般都是在桌面下滑都会出来。或者说按手机的下面的正方形的按钮,不会出现这个应用。
- intent-filter:http://blog.csdn.net/playboyanta123/article/details/7913679
http://blog.csdn.net/aminfo/article/details/7623231#comments