Activity作为Android开发中最常用的一个组件,是Android开发人员必须熟悉且掌握的重要内容。同时Activity也是在面试中经常被问到的一个方向。因此,掌握Activity的重要性也不言而喻。这或许也是为什么任大神会在《Android开发艺术探索》这本书中把Activity作为第一章的用意吧。而在本章的博客中,除了对书中的内容做笔记之外,也会相应地增加一些内容,希望能够对Activity有一个较为全面的介绍。如果在阅读过程中发现讲述的内容中有什么疏忽没有记录下来或者是错误的地方的话,欢迎在下方留言指出。下面开始进入正题吧:
栈是一种常见的数据结构,具有先进后出,后进先出的特点。从数据形式上来说,它可以用下面这一张图来表示:
在这个栈中,我们每一个新放进栈的数据,都会放在栈头的位置,而其他的在之前如果已经放进来的数据,则会被逐渐按顺序往栈底下面移动,看起来就像是前面先被进来的数据被后面新放进来的数据往栈底下面压一样。这种情况叫做压栈。就好像一把手枪的弹夹,当我们把里面装子弹的时候,后面放进去的子弹会被逐渐往底部挤压下去。而当我们需要取出数据时,因为栈就规定了一个进出口,就是栈头(也叫栈顶),所以我们取出数据的时候,也是要按照数据进来的顺序,从栈顶开始取出,每取出一个顺序,栈中的其他数据就会忘上移,直到最后一个数据从栈顶被取出而结束。这种情况叫做弹栈。而如果我们不想要其他的数据,值需要栈中倒数第二个数据的话,那也没办法,只能把这个数据前面的数据全部弹出栈,你才能取到这个数据。否则的话一切皆是免谈。而在Android的设计中,是使用栈这种数据结构来存放Activity的,它有一个名字,叫做任务栈,也叫回退栈。关于栈的知识,到这里已经基本可以用来介绍Activity了,至于其他方面的内容,可参考其他博客或书籍。这里推荐的是:程杰的《大话数据结构》一书。
Activity,中文“活动”。在任大神的书中,把它理解成界面。实际上我非常同意这种说法,因为Activity实际上就是我们在应用中展现出来的一个个用户界面,它会加载指定的布局文件来显示各种UI元素,并为这些元素设置事件处理函数,从而实现用户与应用的交互。比如我们打开一个手机App时,展现出来的登录界面、注册界面、主界面等,都是一个个不同的Activity。它是用户可以直接在App上用肉眼看得到的界面操作,以及针对用户对App的某些特定操作而进行的事件处理(这个是用户不可见的)。
在上面对Activity的介绍中,用以作为用户可见的部分显示的是一份.xml格式的布局文件,而用以布置事件处理函数的则是一个Activity对象。那么为什么我们把肉眼可见的部分也叫作Activity呢?因为我们可以直接在Activity中对布局文件进行操作,等于布局文件是Activity对象的一个外延,但多数情况下,我们使用Activity的方式依旧为Activity对象+xml布局文件。不过,关于Activity的组成,却并没有那么简单。实际上,一个完整的Activity组成并没有那么简单,在Activity与开发人员可以设置的视图,也就是对用户界面进行布局的范围之上,还包括了其他层次的封装。如图:
我们可以看到,在Activity之下有一个PhoneWindow,这个PhoneWindow实际上是一个叫做Window的类的实现类。说起Window,相信有人试过在取消Activity自带的标题栏的时候,调用了这么一个方法:
//取消系统自带的标题栏
requestWindowFeature(Window.FEATURE_NO_TITLE);
而这个方法的具体模样,是这样的:
public final boolean requestWindowFeature(int featureId) {
return getWindow().requestFeature(featureId);
}
我们发现,它实际回调了一个getWindow().requestFeature(featureId)方法,而这个getWindow()返回的实际上就是一个Window类型的对象mWindow:
public Window getWindow() {
return mWindow;
}
那么,当我们查看这个Window源码的时候,发现它其实是抽象类
public abstract class Window {
/** Flag for the "options panel" feature. This is enabled by default. */
public static final int FEATURE_OPTIONS_PANEL = 0;
/** Flag for the "no title" feature, turning off the title at the top
* of the screen. */
public static final int FEATURE_NO_TITLE = 1;
/** Flag for the progress indicator feature */
public static final int FEATURE_PROGRESS = 2;
/** Flag for having an icon on the left side of the title bar */
.....
}
我们知道,抽象类要想发挥作用,那么需要有一个子类去继承抽象类,而PhoneWinsow这是这么一个实现类。我们所执行的去除标题栏的操作,实际上调用的也是这个PhoneWindow中的方法而已。这也从侧面反映出我们前面所说的,Activity的组成不仅仅是一个Activity对象+xml布局文件那么简单。尽管我们常见的模式就是这样,但实际上这已经是经过封装之后才展现出来的。
要在代码中创建一个Activity,需要经过一下几个步骤:
1.新建一个类,继承自android.app.Activity.类的命名建议为:xxxActivity样式,比如LoginActivity
2.在AndroidManifest.xml文件中的 < application>节点下使用新的< activity>节点注册该Activity,同时为< activity>节点配置android:name属性.取值为Activity类的包名和类名,推荐为每一个< activity>节点配置android:label属性。例如:
<activity
android:name=".LoginActivity"
android:label="@string/title_activity_login" >
activity>
注意:如果存在多个Activity,那么各< activity>节点不区分先后顺序。如果要调整第一个进入软件显示界面的Activity,那么则需在该< activity>节点中配置以下信息:
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
intent-filter>
3.如果需要在Activity中加入xml布局文件,则需要在res\layout目录下创建并设计布局,命名建议为activity_xxx(与xxxActivity匹配),如activity_login。
4.设计完布局文件之后,在Activity类中重写onCreate()方法,并在该方法中通过调用setContentView()方法加载布局。比如:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_registe
}
以上为手动创建Activity的方式,当然我们也可以使用IDE帮助实现。以Eclipse为例:
1.快捷键:ztrl+N。弹出窗口如下:
选择Android目录下-> Android Activity ,点击Next,效果如下:
选择样式,这里选择默认样式Black Activity。点击next,效果如下:
在这里,更改Activity Name栏目,下面Layout栏目也会发生自动变化,点击finish。创建成功。
我们创建了Activity之后,除非把它设置为默认界面,否则都需要被激活才可使用。调用Activity类定义的startActivity(Intent)方法,即可激活新的Activity,其中:参数Intent对象可以直接通过Intent类的构造方法来创建
在使用Intent类的构造方法时,指定2个参数,第1个参数是Context对象
第2个参数是被激活的Activity类,例如SecondActivity.class。例子如下:
//跳转到另一个Activity
startActivity(new Intent(this,SecondActivity.class));
如此,便实现了激活SecondActivity并跳转至SecondActivity界面的操作
上面是激活Activity的第一种方法,而如果你需要在激活第二个Activity后对其设置,并且在该Activity执行finish之后返回数据给第一个Activity的时候,可以用startActivityForResult()方法。用法如下:
Intent mIntent =new Intent(this,SecondActivity.class);
int requestCode = 0;
startActivityForResult(mIntent,requestCode)
同时,在第一个Activity重写数据返回的处理办法onActivityResult();用法如下:
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
}
注意:requestCode可以随便设置,但必须大于或等于0
调用Activity类定义的finish()方法即可销毁当前Activity。
在正常情况下。Activity会经历以下生命周期,每个生命周期都执行着一个声明周期方法:
1.onCreate():在我们前面的手动创建Activity的例子中便已经接触到这个方法,它是Activity生命周期执行的第一个生命周期方法,表示的是Activity正在被创建,在这个生命周期方法里我们可以执行一些初始化的工作。比如加载xml布局文件,初始化元素控件以及加载相应的数据或者设置监听器等。
2.onStart():表示Activity正在启动,并且逐渐从不可见到可见直到Activity展示到前台
3.onRestart():表示Activity正在重启。一般情况下,当当前的Activity由不可见状态重新恢复为可见状态的过程中,就会调用这个方法。
4.onResume():在Activity彻底展示在前台(完全可见)的时候被调用,在执行完这个方法后,Activity会请求ActivityManagerService(下称AMS)对它管理的视图进行渲染,此时的Activity位于栈顶且保持运行状态
5.onPause():该方法在Activity由可见状态逐渐变为不可见的状态的过程中调用。一般在系统准备去启动或者恢复另一个Activity的时候调用。这时可做一些数据存储、停止动画或者释放一些消耗的CPU资源等操作,但注意不要做耗时操作。否则会造成卡顿现象或者ActiviNotResponding异常(ANR)。
6.onStop():表示Activity处于停止,即完全不可见状态。
7.onDestory():在Activity被彻底销毁之前调用,表示着Activity被彻底移出了任务栈。是Activity的最后一个声明周期方法。
关于Activity的生命周期,网上找了一张非常明了的示意图:
在这里作两点补充关于从onPause()–>onStop()的特例:
1.如书中所言,当新的Activity采用的是透明主题是,Activity不回调onStop()方法
2.当新启动的Activity是一个对话框式的Activity,那么onStop()方法同样不会被回调。
关于书中提出的两个问题,结合大神的解释和自己的一些想法,归纳如下:
问题1:onStart和onResume、onPause和onStop从描述上差不多,对我们来说有什么区别呢?
大神的解释:onStart和onStop是从Activity是否可见的角度进行回调的。而onResume和onPause是否位于前台这个角度来回调的。在实际开发中没有其他明显区别。
我的想法:onStart和onPause对应的是一种动态的变化,前者是从不可见到逐渐可见再到完全可见(onResume)的变化。而后者则是从完全可见到逐渐不可见再到完全不可见的状态(onStop)。相应的,onResume和onStop对应的就是一个静态改变。一旦完全可见,立刻调用onResume,一旦不可见,立刻调用onStop。这也是为什么当新建的 Activity为透明主题时,onStop方法不会被回调。因为从前台,也就是显示界面的窗口来看,Activity并没有从窗口中消除,依旧为可见状态,只是这个Activity不能控制而已。
问题2:假设当前Activity为A,如果这时用户打开一个新的ActivityB,那么B的onResume和A的onPause哪个先执行呢?
大神的解释:从源码分析角度来看,当我们要启动一个Activity时,启动Activity的请求会由Instrumentation来处理,然后它通过Bilder像AMS发送一个请求,而AMS内部维护着一个ActivityStack(任务栈),负责的是栈内的Activity的状态同步,AMS通过ActivityThread去同步Activity的状态从而完成生命周期的调用。在ActivityStack的源码中,确定了新的Activity启动之前,栈顶的Activity要先onPause后,在ActivityStackSupervisor中的realStartActivityLocked方法调用scheduleLauchActivity方法来完成新Activity的onCreate、onStart、onResume的调用过程。
我的想法:从生命周期的作用描述来看,当我们要启动一个ActivityB并且在它由不可见到逐渐可见再到完全可见的过程中,无可避免的是会占用前台显示的空间,也就是说在这个过程中,会使得ActivityA失去完全可见的状态,变成局部可见或者逐渐不可见的状态,而在这时候,就已经调用了A的onPause方法,而当B的onResume方法被调用时,如果B不是透明主题或者对话框式的Activity的话,这个时候A的onStop也会开始被调用。即:先调用A的onPause,再调用B的onResume.
ps:自主想法,未经推敲,如果有发现不妥的地方,可在下方评论指出,以便及时更改。
关于异常情况下的生命周期分析,大神主要讲了两种情况:
1.资源相关的系统配置发生改变导致Activity被杀死并重新创建
在发生因为系统配置突然发生改变,需要立即杀死当前的Activity并且重新创建,就如手机突然旋转屏幕,需要重新加载适应屏幕变化的Activity的时候。Activity的生命周期如图所示:
在Activity被异常关闭并且需要重新恢复的时候,Activity会先被彻底销毁,其onPause、onStop、onDestroy方法都会被调用。之后才再重新开始调用onCreate方法。但因为是异常关闭,在恢复Activity时我们也不希望看到数据丢失的情况。于是在Activity的onStop方法被调用前,会执行onSaveInstanceState方法保存Activity内的数据,在新恢复的Activity调用onRestoreInstanceState方法,并且把Activity销毁前调用onSaveInstanceState方法所保存的Bundle对象作为参数同时传递给onCreate和onRestoreInstanceState方法,通过两者的信息对比来判断是否重建,如果确定是重建了,则取出之前保存的数据并且恢复,这个过程出现在onStart之前。
而关于数据恢复的过程,工作流程如下:
1.Activity调用onSaveInstanceState保存数据
2.Activity委托Window去帮忙保存数据,就好像自己死前把信物交给信得过的人一般
3.Window在委托它的上层顶级容器取保存数据,这个容器是一个ViewGroup,在Activity中可能就是DecorView
4.顶层容器通知它的子元素来保存相关数据。
5.完成
注:当Activity正常销毁时,系统不会调用onSaveInstanceState方法来保存数据,只有在Activity异常终止的并且有机会重新显示的情况下才会调用onSaveInstanceState方法。
2.资源内存不足导致低优先级的Activity被杀死
这种情况下,数据存储和恢复过程和情况1是一样的。但是这里做个区分,在手机内存不足的时候,有时候我们把应用退出到后台,过了一会再打开的时候,发现它是重新启动了应用,而不是像情况2说的会恢复。因为在这里涉及到的并不是情况2,事实上,情况2针对的是Activity,是指应用在执行过程中处于后台的Activity被kill(杀死)。而上面所说的则是手机在整个应用处于后台的时候,直接把整个应用销毁掉,这属于典型的生命周期调度过程(参见示意图左侧)。
下面记录Activity的优先级情况,也就是当内存不足时,系统会根据Activity的优先级从低到高kill掉一些不需要的Activity以释放资源工其他Activity调用。
1.前台Activity,正在和用户的交互的Activity,一般为用户对可控UI元素具有操作的能力。比如,点击按钮有反应。优先级最高。
2.可见但非前台Actiyity,比如Activity弹出了一个对话框,导致Activity可以可见,但是对控件不具有操作的能力,只能在对话框中进行交互,优先级次之
3.后台Activity,位于后台的不可见Activity,优先级最低,系统首先清理该部分的Activity。
对于情况1的一个拓展:如果想让系统配置发生改变后不重新创建,我们可以通过在 < activity>节点中设置 以下的属性来实现:
android:configChanges="orientation"
在这里,android:configChanges的作用是捕获设备的变化,并回调相应的处理方法而不会重新恢复Activity。当然,关于ConfigChanges的属性值是有很多的,但我们需要定义多个属性的时候,可以用 | 号分割,比如:
android:configChanges="orientation|keyboardHidden"
而关于configChages的属性值及其含义,可以参考如下:
关于Activity的启动模式,就要回顾我们的预备知识——任务栈了。在Android中,系统是利用任务栈来存储每次创建的Activity,这就意味着只要有多次重复调用同一个Activity的现象,那么我们就要创建多个Activity并且把他们存储到Activity中,这样不仅浪费存储空间,还使得Activity的回退机制变得过于死板,不符合Android灵活开发的需求。于是,Android对任务栈就行了一定的功能封装,形成了四种启动模式:
1.standard(标准模式):这是系统默认的启动模式,每创建一个新的Activity,都会产生一个新的Activity实例并且放入相应的任务栈中。和典型的栈调用数据类似没多大区别。
2.singleTop(栈顶复用模式,也叫栈顶唯一模式):在这种模式下,如果要新建的Activity本身已经有一个Activity实例位于栈顶时,那么这个Activity不会被重新创建,而是会回调onNewIntent方法取出当前请求的信息,而这个新建的Activity不会被系统调用onCreate、onStart方法。注意的是,该模式只使用于新Activity已经位于栈顶。否则的话还是会创建新的Activity并且进行压栈操作。
3.singTask(栈内复用模式,也叫任务唯一模式):在这个模式下,只要Activity在任务栈中存在,那么当我们新建该Activity时,会将栈内存在的Activity做置顶操作。也就是说,除非要创建的Activity已经位于栈顶,否则系统会在栈内将位于该Activity之上的所有Activity做弹栈操作,直到该Activity位于栈顶。而如果要创建的Activity在栈内不存在时,会直接创建并压栈。
4.singleInstance(单实例模式):这是一种加强的singleTask模式,它除了具有singTask的一切特性之外,还加强了一点,就是具有此模式的Activity只能单独的位于一个任务栈中。也就是说,如果ActivityA是singleInstance模式,在它启动的时候,系统会为它分配一个新的任务栈。由于singleTask的复用性,在其他需要创建Activity的时候,都不会创建新的Activity。而注意的是,因为singleInsyance模式所创建的实例是位于一个独立的任务栈里,所以当我们销毁Activity的时候,会先把栈顶Activity所在的任务栈里面的Activity清理完毕再来清理该Activity。例如,Activity的启动顺序如下:
A(栈顶)
B(singleInstance)
C
D(栈底)
那么当我们连续按back键时,销毁顺序为 A -> C -> D -> B
askAffinity与Activity的关系
在讲述两者关系的时候,首先可以声明一下:在Android系统中,并不是只有一个任务栈,很多时候,在每一个应有程序之间都会存有一个任务栈,在这个应用中所创建出来的Activity,在一般情况下会放入同应用下的任务栈中,而这个任务栈的名字为应用的包名。那么,有没有特殊情况,我们创建的Activity会进入到其他的任务栈之中呢?如果可以,它又会跳转到哪个任务栈中呢?我们可以通过TaskAffinity属性给出答案。
每一个Activity都会有一个TaskAffinity参数,这个参数标识了Activity所需要进入的任务栈的名字。如果我们不对其进行设置,那么它默认为当前应用的包名,也就是当前应用下的任务栈。而如果我们对其设置了其他应用的包名的话,那么在一些特定情况下,该Activity则会出现转移的情况,书中介绍了两种情况:
1.当TaskAffinity和singleTask启动模式配对使用的时候
如果应用要加载singleTask模式的Activity时,首先该Activity会检查是否存在与它的taskAffinity相同的任务栈。如果存在,那么检查栈内是否有已经实例化的Activity,如果有,那么销毁在该Activity以上的Activity并调用onNewIntent。如果没有,那么该Activity实例化并入栈。而如果任务栈不存在,那么将重新创建出一个任务栈,并入栈。
2.当TaskAffinity与allowTaskReparenting结合的时候
在这种情况下,如果该Activity的allowTaskReparenting设置为true的时候,这个Activity会直接进入后台,直到当和TaskAffinity名字相同的任务栈进入前台的时候,此时的Activity会转移到该任务栈中并处于栈顶位置。
书中例子:先有应用A、B。当在A中启动B的一个ActivityC,然后按Home键回到桌面,打开B应用。此时你会发现显示出来的是ActivityC。
给Activity制定启动模式的方法
1.通过AndroidMenifest.xml为Activity指定启动模式,在相应的节点中添加属性:
android:launchMode="launchName"
这里的launchName为四种启动模式任意一种,注意为英文!
2.在Intent中设置标志位来指定启动模式。比如:
Intent intent =new Intent();
intent.setClass(MainActivity.this,SecondActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
注:当两种方法同时应用时,以第二种方法为准。
在前面介绍Activity的激活方法时,我们已经介绍了如何去用Intent去启动一个Activity,但我们所讲的启动Activity是有两种办法,前面说的是显式启动,意即是可以清楚知道Activity下一个跳转的Activity是什么。(从Intent的参数对象就可以看出)。而关于启动Activity的另一种办法–隐式启动,则更多的是一举通过Intent与IntentFilter的匹配来实现的。下面就介绍一下Activity中Intent与IntentFilter的匹配规则。
1.IntentFilter如何设定
在AndroidManifest.xml文件中,找到< application>,在指定的< activity>标签内添加,如例:
<activity
android:name=".RegisterActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
intent-filter>
activity>
2.Intent与IntentFilter的联合工作
Intent与IntentFilter的联合工作表现为:开发者在需要启动一个Activity时,通过Intent向传递每个Activity的IntentFilter发送一则消息,如果有某个Activity的预定义信息和接收到的Activity相匹配,则表示下一个要启动的Activity为该Activity。因为我们在开发者中并不能直接透过代码看到下一个Activity是什么,只是透过这种信息匹配的方法去完成的Activity启动过程,就是隐式启动。
3.IntentFilter的过滤信息:
IntentFilter的过滤信息主要包括三种:action、category、data,三种过滤信息都有相应的功能。如果一个Intent传递的信息同时匹配了IntentFilter设定的过滤信息,那么才能成功启动目标Activity,否则就算失败。不过注意的是,一个Activity可以设定多个IntentFilter,只要有其中一组IntentFilter完全匹配,同样可以开启该Activity。
下面分别介绍三种过滤信息的作用:
action的匹配规则
action的本质是一个字符串,其作用是描述Intent所触发的动作的名称,我们知道,一个人可以有多种称呼,当其他人要找这个人时,只需要叫他其中一个名字就可以了。同样,在IntentFilter中,我们可以定义多个action,只要有一个action和Intent传递的信息匹配,那么就算配合成功。注意的是,系统本身预定义了一些action,代表可启动的一些预定义的Activity,比如拨号界面等这些预定义的action集中放在android.intent.action下,调用的时候从里面选取,比如:
android.intent.action.SEND
action在IntentFilter中的定义格式如下:
//actionName表示你需要加入的action信息
"actionName"/>
action在Intent中的调用格式如下:
String action="actionName";
Intent intent =new Intent(action);
category匹配规则
category和action的本质是一致的,但代表的意义不同,category描述的是目标组件的类别信息,表明这个目标可以干些什么,比如系统预定义中的:
CATEGORY_GADGET:表示目标Activity是可以嵌入到其他Activity中的
当然,我们也可以给它进行自定义的设置。
而关于category的匹配规则,大致如下:如果Intent中含有category,那么不管你有几个,都需要和目标Activity在IntentFilter中设定的category匹配。哪怕有一个是不匹配的,都将报下面这个异常:
android.content.ActivityNotFoundException:No Activity found to handle Intent {act=actionName cat=[categoryName]}
category在IntentFilter中的定义格式如下:
<category android:name="android.intent.category.LAUNCHER" />
category在Intent中的添加category调用格式如下:
String category ="categoryName";
intent.addCategory(category);
data匹配规则
data的匹配规则和action相似,如果IntentFilter中定义了data,那么Intent中必须也要定义可匹配的data,但是因为data的结构与action不一样,所以会有一些变化的地方。
data的组成
data由两部分组成:mimeType 和URI。其中,mimeType指的媒体类型,可以表示图片image/jpeg,文本text/html ,音频audio/mpeg4-generic 和视频video/*等。而URI表示统一资源标识符(Uniform Resource Identifier),用以制定所需资源的存储路径。其结构如下:
:// : :/[||]
结构说明如下:
scheme:URI的模式,比如http、file等
host:URI的主机名,即当前资源所在的主机的IP地址,可以用域名表示,如www.baidu.com
port:URI的端口号,比如80,指获得资源的窗口路径。
path、pathPrefix、pathPattern:表示路径信息
data在IntentFilter的定义格式:
<data
android:mimeType="mimeName"
android:scheme="schemeName"
android:host="hostName"
android:port="portName"
android:path="pathName"
android:pathPrefix="pathPrefixName"
android:pathPattern="pathPatternName"
/>
或者:
<data android:mimeType="mimeName"/>
<data android:scheme="schemeName"/>
......
<data android:pathPattern="pathPatternName"/>
data在Intent中的调用方法有:
intent.setdata(Uri data);
intent.setDataAndNormalize(Uri data);
intent.setDataAndType(Uri data, String type);
intent.setDataAndType(Uri data, String type);
intent.setDataAndTypeAndNormalize(Uri data, String type);
至此,关于《Android开发艺术探索》第一章的读书笔记已经完成。其中参考的数目有:
何红辉《Android开发进阶:从小兵到专家》
郭霖《第一行代码》
零点起飞学编程系列《零点起飞学Android开发》
在看书过程中,发现关于一个知识点的内容,每本书都会有一些相同的地方,但也会侧重于另一些不一样的点。恰好也证明了这么一句话:一本书只有20%的内容是有用的额,同时也是只展现了应该展现的那20%,剩下的80%,要么自己积累,要么多多看其他技术数籍。加油吧,罗马不是一天建成的,要看的书还有很多,要走的路也有好长。(完)