Activity全面解析

来源:互联网 发布:半月板损伤 知乎 编辑:程序博客网 时间:2024/06/05 07:36

Activity是Android的四大组件之一,它的使用频率是最高的,因此想要了解android开发,那么必须要了解Activity。

一、典型情况下的生命周期

正常情况下,Activity会经历如下声明周期。

  1. onCreate():Activity的第一个生命周期,主要是通过setContentView()加载布局。
  2. onStart():表示Activity已经被启动了,但是还没有到前台,无法与用户进行交互。
  3. onResume():表示Activity已经到了前台,开始活动了,可以与用户交互 。
  4. onPause():表示Activity正在停止。此时可以做一些保存数据、停止动画等操作,但是不能太耗时。
  5. onStop():表示Activity即将停止,可以做一些稍微重量级的回收工作,但是不能太耗时
  6. onDestroy():Activity的最后一个生命周期,主要做一些回收和释放工作。
  7. onRestart():这是一个特殊的生命周期,当用户按下home键或者被覆盖之后,重新将Activity显示出来的时候,执行onRestart()->onStart()->onResume()。
    这里写图片描述

二、异常情况下的生命周期

异常情况下是指Activity被系统回收或者是当前设备的Configuration发生改变从而导致Activity被销毁重建。

在销毁之前会调用onSaveInstanceState方法保存数据,该方法执行在onStop之前,但是与onPause没有时序关系。在Activity重建的时候会调用onRestoreInstanceState方法恢复数据,它执行在onStart之后。
这里写图片描述

情况一:资源相关的系统配置发生改变造成的Activity的销毁与重建

例如旋转屏幕来造成系统配合发生变化:
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按照优先级从高到低,可以分为如下三种:

  1. 前台Activity——正在和用户交互的Activity,它的优先级最高
  2. 可见但非前台Activity——比如Activity中弹出一个对话框,或者打开一个透明的Activity,导致Activity可见,但是位于后台无法和用户直接交互。
  3. 后台Activity——已经被暂停的Activity,比如执行了onStop(),优先级最低。

当系统内存不足的时候,系统就会按照上述优先级去杀死目标Activity所在的进程,并在后续通过onSaveInstance和onRestoreInstanceState来存储和恢复数据。

如果一个进程中没有四大组件在执行,那么这个进程将会很快被系统杀死,因此后台工作不适合脱离四大组件单独存在,比较好的方法就是将这些工作放在service中从而保证进程具有一定的优先级,这样不容易被杀死。

情况三:系统配置发生改变的时候,怎样才能不让Activity销毁重建

如果当系统配置发生改变后,Activity不想被销毁重建,那么应该就在资源配置文件中给Activity制定configChanges属性。

android:configChanges="orientation"

如果想要指定多个值,可以用“|”链接起来,比如:

android:configChanges="orientation|keyboardHidden"

当Activity如果没有被销毁重建,就不会执行onSaveInstanceState和onRestoreInstanceState方法,但是会执行onConfigurationChanged方法。

configChanges的属性含义:

  • locale:设备的位置发生改变,一般指切换系统语言。
  • orientation:屏幕方向发生改变,这个是最常用的,比如旋转手机屏幕。
  • keyboardHidden:键盘的可访问性发生了改变,比如用户调出了键盘 。
  • screenSize:当屏幕的尺寸信息发生了改变,当旋转设备屏幕时,屏幕尺寸会发生改变,这个选项比较特殊,他和编译选项有关,当编译选项中的minSdkVersion和targetSdkVersion均低于13时,此选项不会导致Activity重启,否则就会。
  • smallestScreenSize:与screenSize类似,只是当minSdkVersion和targetSdkVersion有小于13时,Activity会重启,否则不会。

以上几个比较常用的configurationChange属性。

Activity的启动模式

一、Activity的启动模式

我们知道,在默认情况下,当我们多次启动同一个Activity的时候,系统会创建多个实例并把他们一一放入任务栈中,当我们单击back键,会发现这些Activity会一一回退。任务栈就是一种“后进先出”的栈结构。
Activity有四种启动模式:standard,singleTop,singleTask和singleInstance。

1、standard:标准启动模式

这是系统默认的启动模式,每次启动一个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标记位,这样启动的时候就会为它创建一个新的任务栈。

2、singleTop:栈顶复用模式

在这种模式下,如果新的Activity已经位于栈顶,那么,新的Actiity就不会在创建,同时这个Activity就会执行onNewIntent的方法来更新数据,
需要知道的就是这个Activity的生命周期以及onNewIntent是这样执行的onPause()->onNewIntent()->onResume()。

3、singleTask:栈内复用模式

例如需要启动一个任务栈是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显示出来。

4、singleInstance:单例模式

具有此种模式的Activity只能单独位于一个任务栈中,例如Activity A启动singleInstance模式的Activity B,系统首先查找是否存在该任务栈的Activity B,如果有那么就将B在启动A的上面。按back键的时候,退到B界面,在按back键,退出应用。

这里有两个需要注意的地方:

  1. 标准启动模式的Activity A,启动singleInstance模式的B,然后再B中启动A。按下back键,又退到A,然后再按下back键,退到B,再按退出应用。
  2. 前台任务栈中有Activity A,后台singleTask模式任务栈中有CD,如果启动的是C,那么时序结构是AC,如果启动的是D,那么时序结构是ACD。

最后:
TaskAffinity属性主要和singleTask启动模式或者allowTaskReparenting属性配对使用。当与singleTask启动模式结合时,TaskAffinity用来指定任务栈名称。
当与allowTaskReparenting结合的时候比较复杂,例如应用A启动应用B中的allowTaskReparenting属性为true的Activity C,因为是两个不同的应用,所以C的任务栈肯定与A中的不同,那么在按Home键隐藏Activity之后,在打开应用B,B在创建任务栈的时候发现已经存在任务栈了,那么就会直接将C所在的任务栈拿来直接使用,即直接显示应用A打开的Activity C。

二、Activity的标记位

Activity的Flags有很多,这里主要介绍一些常用的标记位。

  1. FLAG_ACTIVITY_NEW_TAK:这个标记位指定Activity为singleTask启动模式
  2. FLAG_ACTIVITY_SINGLE_TOP:这个标记位指定Activity为singleTop启动模式
  3. FLAG_ACTIVITY_CLEAR_TOP:这个标记位与singleTask启动模式一起使用的时候,因为实例已经存在,所以系统会将它之上的所以Activity清空,然后调用它的onNewIntent更新数据。如果这个标记位与standard启动模式一起使用的时候,系统会连同该Activity与它之上的所有Activity一起清空,然后再重新创建该Activity。
  4. FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS:这个标记位的作用于android:excludeFromRecents="true"作用相同,具有这个标记位的Activity不会出现在历史Activity列表中。例如许多人有个习惯,就是长按android的home键,然后清除刚才看的应用,这个属性的作用恰恰就是让你在长按home键的时候在弹出的应用列表中隐藏你的应用,达到隐藏应用程序进行的目的。

Activitiy的启动方式

我们知道启动Activity有两种方式,一种是显示启动和隐式启动。我们平时使用,明确启动对象组件信息,包括包名和类名,这种是显示启动,这里就不需要多做介绍了,还有一种是隐式启动,这种启动方式比较少见。

隐式启动需要Intent能够匹配目标组件的IntentFilter中所设置的过滤信息,否则无法启动,这些信息包括action、category、data。一个Activity中可以有很多个intent-filter,一个Intent只要能匹配任何一组intent-filter即可成功启动Activity。

action匹配规则

action是一个字符串,系统预定义了一些action,同时我们也可以在应用中定义自己的action。一个过滤规则中可以有多个action,那么只要Intent中的action能够和过滤规则中的任何一个action相同即可匹配成功,这就要求Intent中定义的action不能为空。

category匹配规则

category是一个字符串,系统预定义了一些category,同时我们也可以在应用中定义自己的category。规定你的代码中可以没有category,但是XML中要加上”android.intent.category.DEFAULT”这句。如果你在代码中定义了一个或者多个category,那么你必须跟XML文件中定义的一样。比如你定义了一个category,那么要在XML文件中匹配到一个,,如果你定义了多个category,那么要在XML文件中全部匹配,一一对应。

data匹配规则

data的匹配规则和action类似,如果过滤规则中定义了data,那么Intent中必须也要定义可匹配的data。
data由两部分组成,mimeType和URI。mimeType指媒体类型,比如text/plain(纯文本)、image/jpeg(JPEG图像)、video/mpeg(MPEG动画)。URI中包含的数据比较多,下面是URI的结构:

<scheme>://<host>:<port>/[<path>|<pathPrefix>|<pathPattern>]

例如:

http://www.baidu.com:80/search/info
  1. Scheme:URI模式,比如http,file,content等,如果URI中没有指定scheme,那么URI无效
  2. Host:URI主机名,比如www.baidu.com,如果host未指定,那么host后面的参数无效,导致URI无效
  3. Port:URI中的端口号,比如80,仅当URI中指定scheme和host参数的时候port才有意义
  4. Path:完整的路径信息
  5. pathPattern:完整的路径信息,但是里面可以使用通配符“ * ”和“ \ ”,但是根据正则表达式的规则,需要写成” \\* ” 、“ \\\\ ”
  6. 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是否存在。

  1. 使用PackageManager的resolveActivity或者Intent的resolveActivity方法
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);        }
  1. 通过PackageManager的queryIntentActivities方法,这个方法提供所有成功匹配的Activity信息。
List<ResolveInfo> resolveInfos = getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);

最后:针对Service和BroadcastReceiver,PackageManager同样提供了类似的方法去获取成功匹配的组件信息,但是系统对于Service的建议还是尽量使用显示调用方式来启动服务。

0 0