Activity是Android的四大组件之一,它的使用频率是最高的,因此想要了解android开发,那么必须要了解Activity。
正常情况下,Activity会经历如下声明周期。
异常情况下是指Activity被系统回收或者是当前设备的Configuration发生改变从而导致Activity被销毁重建。
在销毁之前会调用onSaveInstanceState方法保存数据,该方法执行在onStop之前,但是与onPause没有时序关系。在Activity重建的时候会调用onRestoreInstanceState方法恢复数据,它执行在onStart之后。
例如旋转屏幕来造成系统配合发生变化:
activity_main:
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="cn.centran.zx_mylock.MainActivity">
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/et"/>
RelativeLayout>
MainActivity:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
et = (EditText) findViewById(R.id.et);
}
在Editext中输入文字,切换屏幕后,那么,Activity重建之后,输入的文字也恢复了,大家可以试试。
原因:
查看Editext源码中的onSaveInstanceState()方法:
@Override
public Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
// Save state if we are forced to
final boolean freezesText = getFreezesText();
boolean hasSelection = false;
int start = -1;
int end = -1;
if (mText != null) {
start = getSelectionStart();
end = getSelectionEnd();
if (start >= 0 || end >= 0) {
// Or save state if there is a selection
hasSelection = true;
}
}
if (freezesText || hasSelection) {
SavedState ss = new SavedState(superState);
if (freezesText) {
if (mText instanceof Spanned) {
final Spannable sp = new SpannableStringBuilder(mText);
if (mEditor != null) {
removeMisspelledSpans(sp);
sp.removeSpan(mEditor.mSuggestionRangeSpan);
}
ss.text = sp;
} else {
ss.text = mText.toString();
}
}
if (hasSelection) {
// XXX Should also save the current scroll position!
ss.selStart = start;
ss.selEnd = end;
}
if (isFocused() && start >= 0 && end >= 0) {
ss.frozenWithFocus = true;
}
ss.error = getError();
if (mEditor != null) {
ss.editorState = mEditor.saveInstanceState();
}
return ss;
}
return superState;
}
onRestoreInstanceState():
@Override
public void onRestoreInstanceState(Parcelable state) {
if (!(state instanceof SavedState)) {
super.onRestoreInstanceState(state);
return;
}
SavedState ss = (SavedState)state;
super.onRestoreInstanceState(ss.getSuperState());
// XXX restore buffer type too, as well as lots of other stuff
if (ss.text != null) {
setText(ss.text);
}
if (ss.selStart >= 0 && ss.selEnd >= 0) {
if (mText instanceof Spannable) {
int len = mText.length();
if (ss.selStart > len || ss.selEnd > len) {
String restored = "";
if (ss.text != null) {
restored = "(restored) ";
}
Log.e(LOG_TAG, "Saved cursor position " + ss.selStart +
"/" + ss.selEnd + " out of range for " + restored +
"text " + mText);
} else {
Selection.setSelection((Spannable) mText, ss.selStart, ss.selEnd);
if (ss.frozenWithFocus) {
createEditorIfNeeded();
mEditor.mFrozenWithFocus = true;
}
}
}
}
if (ss.error != null) {
final CharSequence error = ss.error;
// Display the error later, after the first layout pass
post(new Runnable() {
public void run() {
if (mEditor == null || !mEditor.mErrorWasChanged) {
setError(error);
}
}
});
}
if (ss.editorState != null) {
createEditorIfNeeded();
mEditor.restoreInstanceState(ss.editorState);
}
}
不需要细查每一行代码,大致知道当输入数据之后,切换屏幕,那么因为系统配置发生变化,导致该Activity被杀死,但是在onStop()方法之前调用了Editext的onSaveInstanceState()方法,保存了数据,之后重建的时候调用onRestoreInstanceState()恢复了。
关于保存和恢复View层次结构,系统的工作流程是这样的:首先Activity被意外结束的时候,Activity会调用onSaveInstanceState去保存数据,然后Activity会委托Window去保存数据,接着window在委托它上面的顶级容器去保存数据。顶层容器是一个ViewGroup,一般来说,它可能是DecorView。最后顶层容器再去一一通知它的子元素来保存数据,这样整个数据保存过程就完成了。这是一种典型的委托思想。
Activity按照优先级从高到低,可以分为如下三种:
当系统内存不足的时候,系统就会按照上述优先级去杀死目标Activity所在的进程,并在后续通过onSaveInstance和onRestoreInstanceState来存储和恢复数据。
如果一个进程中没有四大组件在执行,那么这个进程将会很快被系统杀死,因此后台工作不适合脱离四大组件单独存在,比较好的方法就是将这些工作放在service中从而保证进程具有一定的优先级,这样不容易被杀死。
如果当系统配置发生改变后,Activity不想被销毁重建,那么应该就在资源配置文件中给Activity制定configChanges属性。
android:configChanges="orientation"
如果想要指定多个值,可以用“|”链接起来,比如:
android:configChanges="orientation|keyboardHidden"
当Activity如果没有被销毁重建,就不会执行onSaveInstanceState和onRestoreInstanceState方法,但是会执行onConfigurationChanged方法。
configChanges的属性含义:
以上几个比较常用的configurationChange属性。
我们知道,在默认情况下,当我们多次启动同一个Activity的时候,系统会创建多个实例并把他们一一放入任务栈中,当我们单击back键,会发现这些Activity会一一回退。任务栈就是一种“后进先出”的栈结构。
Activity有四种启动模式:standard,singleTop,singleTask和singleInstance。
这是系统默认的启动模式,每次启动一个Activity都会重新创建一个新的实例,系统不管这个实例是否存在。在这种模式下,谁启动了Activity,那么这个Activity就运行在启动它的那个Activity所在的栈中。比如Activity A启动了Activity B,那么B就在A所在的任务栈中。所以很容易发现,当我们使用Application去启动这种模式下的Activity会报错。
getApplicationContext().startActivity(new Intent(MainActivity.this,SecondActivity.class));
错误
Caused by: android.util.AndroidRuntimeException: Calling startActivity() from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag. Is this really what you want?
这是因为,谁启动Activity,那么Activity就进入谁的栈中,但是getApplicationContext()没有任务栈。解决这个问题的方法是为待启动的Activity指定FLAG_ACTIVITY_NEW_TASK标记位,这样启动的时候就会为它创建一个新的任务栈。
在这种模式下,如果新的Activity已经位于栈顶,那么,新的Actiity就不会在创建,同时这个Activity就会执行onNewIntent的方法来更新数据,
需要知道的就是这个Activity的生命周期以及onNewIntent是这样执行的onPause()->onNewIntent()->onResume()。
例如需要启动一个任务栈是aa.bb,启动模式是singleTask模式的Activity A,那么系统首先回去查询是否存在任务栈aa.bb,如果存在,那么就会查看该任务栈中是否存在Activity A,如果不存在就会创建一个,如果存在那么就将该Activity上面的Activity全部清除,然后将该Activity显示出来,通过回调onNewIntent方法来更新数据。如果不存在任务栈aa.bb,那么久创建任务栈aa.bb,然后再创建Activity。
例一:
MainActivity和SecondActivity是标准启动模式,但是ThirdActivity是singleTask启动模式,它的任务栈是aa.bb,在MainActivity中启动SecondActivity,然后再SecondActivity中启动ThirdActivity。最后通过adb shell dumpsys activity查询任务栈信息,知道有两个任务栈
Running activities (most recent first):
TaskRecord{2c73b67 #2662 A=aa.bb U=0 sz=1}
Run #2: ActivityRecord{1413334 u0 cn.demo.zx_demo/.ThirdActivity t2662}
TaskRecord{b97dd14 #2661 A=cn.demo.zx_demo U=0 sz=2}
Run #1: ActivityRecord{89dec9c u0 cn.demo.zx_demo/.SecondActivity t2661}
Run #0: ActivityRecord{165ccb0 u0 cn.demo.zx_demo/.MainActivity t2661}
例二:
MainActivity和SecondActivity是标准启动模式,但是ThirdActivity是singleTask启动模式,它的任务栈是默认模式(标准启动模式),在SecondActivity中启动ThirdActivity,它们的任务栈信息是:
Running activities (most recent first):
TaskRecord{97685f5 #2670 A=cn.demo.zx_demo U=0 sz=3}
Run #7: ActivityRecord{836431b u0 cn.demo.zx_demo/.ThirdActivity t2670}
Run #6: ActivityRecord{8fe2d7a u0 cn.demo.zx_demo/.SecondActivity t2670}
Run #5: ActivityRecord{ca92e3e u0 cn.demo.zx_demo/.MainActivity t2670}
例三:
MainActivity和SecondActivity还有ThirdActivity是singleTask启动模式,它的任务栈是同一个,在ThirdActivity中启动SecondActivity,它们的任务栈信息是:
Running activities (most recent first):
TaskRecord{f69a350 #2676 A=cn.demo.zx_demo U=0 sz=2}
Run #6: ActivityRecord{d70ba66 u0 cn.demo.zx_demo/.SecondActivity t2676}
Run #5: ActivityRecord{3c5df4 u0 cn.demo.zx_demo/.MainActivity t2676}
由此可知,如果任务栈中有SecondActivity,那么在启动它的时候,将该Activity之上的所有Activity移除栈,然后将SecondActivity显示出来。
具有此种模式的Activity只能单独位于一个任务栈中,例如Activity A启动singleInstance模式的Activity B,系统首先查找是否存在该任务栈的Activity B,如果有那么就将B在启动A的上面。按back键的时候,退到B界面,在按back键,退出应用。
这里有两个需要注意的地方:
最后:
TaskAffinity属性主要和singleTask启动模式或者allowTaskReparenting属性配对使用。当与singleTask启动模式结合时,TaskAffinity用来指定任务栈名称。
当与allowTaskReparenting结合的时候比较复杂,例如应用A启动应用B中的allowTaskReparenting属性为true的Activity C,因为是两个不同的应用,所以C的任务栈肯定与A中的不同,那么在按Home键隐藏Activity之后,在打开应用B,B在创建任务栈的时候发现已经存在任务栈了,那么就会直接将C所在的任务栈拿来直接使用,即直接显示应用A打开的Activity C。
Activity的Flags有很多,这里主要介绍一些常用的标记位。
android:excludeFromRecents="true"
作用相同,具有这个标记位的Activity不会出现在历史Activity列表中。例如许多人有个习惯,就是长按android的home键,然后清除刚才看的应用,这个属性的作用恰恰就是让你在长按home键的时候在弹出的应用列表中隐藏你的应用,达到隐藏应用程序进行的目的。我们知道启动Activity有两种方式,一种是显示启动和隐式启动。我们平时使用,明确启动对象组件信息,包括包名和类名,这种是显示启动,这里就不需要多做介绍了,还有一种是隐式启动,这种启动方式比较少见。
隐式启动需要Intent能够匹配目标组件的IntentFilter中所设置的过滤信息,否则无法启动,这些信息包括action、category、data。一个Activity中可以有很多个intent-filter,一个Intent只要能匹配任何一组intent-filter即可成功启动Activity。
action是一个字符串,系统预定义了一些action,同时我们也可以在应用中定义自己的action。一个过滤规则中可以有多个action,那么只要Intent中的action能够和过滤规则中的任何一个action相同即可匹配成功,这就要求Intent中定义的action不能为空。
category是一个字符串,系统预定义了一些category,同时我们也可以在应用中定义自己的category。规定你的代码中可以没有category,但是XML中要加上”android.intent.category.DEFAULT”这句。如果你在代码中定义了一个或者多个category,那么你必须跟XML文件中定义的一样。比如你定义了一个category,那么要在XML文件中匹配到一个,,如果你定义了多个category,那么要在XML文件中全部匹配,一一对应。
data的匹配规则和action类似,如果过滤规则中定义了data,那么Intent中必须也要定义可匹配的data。
data由两部分组成,mimeType和URI。mimeType指媒体类型,比如text/plain(纯文本)、image/jpeg(JPEG图像)、video/mpeg(MPEG动画)。URI中包含的数据比较多,下面是URI的结构:
:// :/ [||]
例如:
http://www.baidu.com:80/search/info
pathPrefix:表示路径的前缀信息
注意1:
这里需要注意如果Intent指定完整的data,那么使用setDataAndType方法,不要使用setData和setType方法,因为他们会清除彼此的值。
setData源码:
public Intent setData(Uri data) {
mData = data;
mType = null;
return this;
}
setType源码:
public Intent setType(String type) {
mData = null;
mType = type;
return this;
}
注意2:
虽然data的匹配方法与action类似,但是也有不同:
<Intent-filter>
<action android:scheme="file"
android:host="www.baidu.com"
android:port="80"/>
Intent-filter>
<Intent-filter>
<action android:scheme="file"/>
<action android:host="www.baidu.com"/>
<action android:port="80"/>
Intent-filter>
这两种特殊的写法的作用相同。
注意3:
使用隐式方式启动Activity的时候,可以通过两种方法判断要启动的Activity是否存在。
Intent intent = new Intent ();
intent.setAction("aa.aa");
intent.setDataAndType(Uri.parse("content"),"video");
if(getPackageManager().resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY)==null){
Toast.makeText(this, "没有Activity", Toast.LENGTH_SHORT).show();
}else{
startActivity(intent);
}
List resolveInfos = getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
最后:针对Service和BroadcastReceiver,PackageManager同样提供了类似的方法去获取成功匹配的组件信息,但是系统对于Service的建议还是尽量使用显示调用方式来启动服务。