四大组件之Activity

参考:Android 四大组件

参考:开发艺术探索书籍

Activity 生命周期

正常启动流程:

onCreate() —— onStart() —— onResume()

正常销毁流程:

onPause() —— onStop() —— onDestroy()

操作情况:

1.处于 onResume() 的时候,用户按 Home 键或锁屏或打开新界面时,导致界面不可见,会回调 onPause() —— onStop() ,这里有一种特殊情况,如果新打开的界面采用了透明主题,那么当前的 Activity 只回调 onPause() 不回调 onStop()

2.处于 onPause() 的时候,用户操作使界面回到前台时,直接回调 onResume()

3.处于 onStop() 的时候,用户操作使界面重新可见时,回调 onRestart() —— onStart() —— onResume()

4.处于 onPause() 或 onStop() 的时候 ,界面被回收了,会重新走周期流程 onCreate() —— onStart() —— onResume()

异常销毁流程:

当系统资源配置发生改变(屏幕旋转)以及系统内存不足时,activity 被杀死。

情况1(系统资源配置发生改变(屏幕旋转)):

onCreate() —— onStart() —— onResume() ,此时屏幕旋转 ,会回调 onPause() —— onSaveInstanceState() —— onStop() —— onDestroy() —— onCreate() —— onStart() —— onRestoreInstanceState() ——onResume()

由回调流程可知,系统配置改变时,界面会先销毁,而且会回调一个 onSaveInstanceState() 来保存当前的界面状态,这个方法一事实上在 onStop() 之前,但是与 onPause() 没既定顺序(但是测试的时候一直都在 onPause() 之后调用),界面销毁后会重新创建,并会在 onResume() 之前回调一个 onRestoreInstanceState() 方法,用于恢复异常时保存的数据。

情况2(内存不足导致低优先级的Activity被杀死):

流程跟情况1是一样的。Activity 优先级从高到低如下:
1.处于 onResume()时
2.处于 onPause() 时
3.处于 onStop()时

使 Activity 处于各种生命周期的情况多变,我们只关注此时 Activity 正处于什么状态,就能知道此时的优先级了。

注意:

1.旧 Activity 先 onPause() ,然后新 Activity 再启动,在新 Activity 回调到 onResume() 时,旧 Activity 才会回调 onStop(),所以在 onPause() 里可以做一些回收资源的操作,但是不能耗时,不然会影响到新 Activity 显示。在onStop()里也可以做一些回收资源的操作,同样也不能太耗时。最好在 onStop()或 onDestroy() 做资源回收,这样会使新界面尽快显示。

Activity缓存方法。

场景1: 从 A 界面 打开 B 界面,此时 A 界面处于 onStop() ,很长时间后 A 界面可能因为优先级过低而被回收掉,此时从 B 界面返回到 A 界面时,将不会出现 onRestart() —— onStart() —— onResume() 这样的生命周期,而是 onCreate() —— onStart() —— onResume() 的正常生命周期,那么这会导致 A 界面数据和状态的丢失。

场景2: 在处于 A界面的时候,按下 Home 键或锁屏 ,些时 A 界面处于 onStop() ,此时恢复情况和场景1一样,其实原理一样,只是不同的操作导致界面处于优先级低的 onStop() 状态。

解决办法:

在上面两种场景下,界面从 onResume() 到 onStop() 中 ,会调用 onSaveInstanceState() 方法,即 A 界面会经历 onPause() —— onSaveInstanceState() —— onStop() ,我们可以利用 onSaveInstanceState() 回调方法保存临时数据和状态 ,之后我们可以通过 onCreate() 或 onRestoreInstanceState() 方法获取到之前保存的数据。至于要在哪个方法里做恢复数据处理,这取决于你要在什么时机, onRestoreInstanceState() 是在 onStart() 之后 ,onResume() 之前回调的。

@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    outState.putString("key","恢复数据");
    Logger.d("onSaveInstanceState"+ "===" +TAG);
}

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    //如果不是因为异常情况导致重新创建,则为Null
    if (savedInstanceState != null) {
        mNavigation.setText(savedInstanceState.getString("key"));
    }
}

@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
    super.onRestoreInstanceState(savedInstanceState);
     //这里不用判空,如果回调了这个方法,一定不为Null
     mNavigation.setText(savedInstanceState.getString("key"));
}

onSaveInstanceState (Bundle outState)

不是每次回调 onStop() 之前都会回调这个方法的,是当 Activity 变得“容易”被系统销毁时,该 Activity 的 onSaveInstanceState 才会被回调。

不被回调的情况:
1.当用户主动按下返回键或通过点击界面的操作使 Activity 销毁了,不会回调该方法。

被回调的情况:
1.按下 HOME 键或锁屏,即之前的场景2
2.长按 HOME 键,选择运行其他的程序
3.从旧的 Activity 中启动一个新的 Activity 时,即之前的场景1
4.屏幕旋转,一定会回调

总结:除非你主动销毁界面,否则都会回调该方法,这是系统的责任,因为它必须要提供一个机会让你保存你的数据(当然你不保存那就随便你了)。

注意:
1.布局中的每一个 View 默认实现了 onSaveInstanceState 和 onRestoreInstanceState 方法,View 会自动地存储和在 Activity 重新创建的时候自动地恢复。但是这种情况只有在你为这个View提供了唯一的ID之后才起作用。

2.虽然 View 中实现了该方法,但是如果你的自定义 View 需要存储额外的状态信息,那么你应该覆盖该方法。

3.不要在该方法中存储持久化的数据,因为这个方法在用户主动销毁时不会调用,应该在 onPause()方法中存储持久化数据。

二、onRestoreInstanceState (Bundle outState)

onSaveInstanceState() 方法和 onRestoreInstanceState() 方法“不一定”是成对的被调用的,因为 onSaveInstanceState() 是在可能发生界面被回收时给你保存数据用的,这是系统的责任,上面已经说得很清楚,而如果界面没有被回收,那么 onRestoreInstanceState() 也就不会出现了。

onConfigurationChanged(Configuration newConfig) 方法

当系统的配置信息发生改变时,系统会调用此方法。注意,只有在清单文件 AndroidManifest 中处理了 configChanges 属性对应的设备配置,该方法才会被调用。如果发生设备配置与在清单文件中设置的不一致,则Activity会被销毁并使用新的配置重建。

上面说的旋转屏幕时,Activity 会先销毁,然后重新创建,但是如果想防止重新创建 Activity ,可以配置 configChange 属性,比如:

<activity 
       android:name=".MainActivity" 
       android:configChanges="orientation|screenSize">

此时 Activity 不会被销毁重建,而是调用 onConfigurationChanged 方法。如果 configChanges 只设置了orientation ,则当其他设备配置信息改变时,Activity 依然会销毁重建,且不会调用 onConfigurationChanged 。比如语言改变了,Acitivyt 就会销毁重建,且不会调用 onConfigurationChanged 方法。

@Override
public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);
    Logger.d("onConfigurationChanged"+ "===" +TAG);
}

注意:
1.横竖屏切换的属性是 orientation 。如果 targetSdkVersion 的值大于等于13,则如上配置才会回调 onConfigurationChanged 方法,要带上 screenSize 属性。

2.当我们需要在界面旋转时,可以在该方法做一些特殊处理,比如视频播放横竖屏切换时,可以通过 newConfig.orientation 根据方向做界面处理。

Activity与Fragment生命周期关系

Activity启动模式

任务栈

是一种后进先出的结构。位于栈顶的 Activity 处于焦点状态,当按下 back 按钮的时候,栈内的 Activity 会一个一个的出栈,并且调用其 onDestory() 方法。如果栈内没有 Activity ,那么系统就会回收这个栈,每个应用默认只有一个栈,以应用的包名来命名。

standard 模式(默认)

系统默认的启动模式,每次启动一个 Activity 都会重新创建一个新的实例,不管这个实例存在与否,这种模式下,谁启动了该模式的 Activity ,该 Activity 就属于启动它的 Activity 的任务栈中。 Activity 的启动三回调(onCreate()->onStart()->onResume())都会执行。

清单文件配置如下:

<activity android:name=".activity.StandardModeActivity"  android:launchMode="standard"/>

使用案例:从一个主界面,打开一个 Standard 模式的界面,在该界面再次打开该界面。

案例打印的日志如下:

日志分析:

系统先创建了 MainLaunchModeActivity ,该界面的 taskAffinity 是 zeng.fanda.com.fourmoduledemo ,taskId 是 753 ,然后再创建了两次 StandardModeActivity ,而且 taskAffinity 和 taskId 都跟 MainLaunchModeActivity 的一样。结合上述 standard 模式的描述,由于 MainLaunchModeActivity 启动了 StandardModeActivity ,所以 StandardModeActivity 就属于 MainLaunchModeActivity 的任务栈,再次打开的 StandardModeActivity 就属于原 StandardModeActivity 的任务栈,所以三个界面的任务栈是一样的。而且每个 Activity 的 hashCode 都是不一样,说明他们都是不同的实例,即每次启动都会重新创建一个新实例。onCreate()->onStart()->onResume()都会执行,日志里没做打印而已。

singleTop 模式

这个模式分3种情况。第一,Activity 没创建过,此时打开该模式的 Activity 就跟 Standard 模式一模一样。第二,Activity 创建了,但是不在栈顶,也跟 Standard 模式一模一样。第三,当 Activity 处于栈顶,此时再次打开该模式的 Activity ,该 Activity 不会重新创建实例,直接复用,而且不会调用 onCreate() 和 onStart() ,但是会执行 onNewIntent(Intent intent) 和 onResume() 方法。

清单文件配置如下:

<activity android:name=".activity.SingleTopModeActivity"  android:launchMode="singleTop"/>

使用案例:从一个主界面,打开一个 singleTop 模式的界面,在该界面多次打开该界面。

案例打印的日志如下:

日志分析:

因为上述的第一种情况包括了第二种情况,所以这里只分析第一和第三种情况。系统先创建了 MainLaunchModeActivity ,该界面的 taskAffinity 是 zeng.fanda.com.fourmoduledemo ,taskId 是 760 ,然后再创建了SingleTopModeActivity ,而且 taskAffinity 和 taskId 都跟 MainLaunchModeActivity 的一样。结合上述 singleTop 模式的描述,该模式还是属于打开它的 Activity 的任务栈,因为第一次创建时 SingleTopModeActivity 不存在,所以onCreate()->onStart()->onResume()都会执行, hashCode 不一样,证实是创建了一个新实例,这跟 Standard 模式一样。之后多次打开该界面时,走了 onNewIntent()->onResume() 回调,而且 taskId 和 hashCode 都没变,证明还是处于同一个栈内且没创建新实例,直接复用原有的实例。

总结一下:只要该模式的界面不在栈顶,流程都跟 Standard 模式一样,不管实例存在与否,只有处于栈顶时,流程才会不一样。

taskAffinity 属性

这里先说明一下 taskAffinity 这个属性。 taskAffinity 可以简单的理解为任务相关性。

  • 这个参数标识了一个 Activity 所需任务栈的名字,默认情况下,所有 Activity 所需的任务栈的名字为应用的包名。上述日志打印的 taskAffinity 的值 (zeng.fanda.com.fourmoduledemo) 就是我的包名。

  • 我们可以单独指定每一个 Activity 的 taskAffinity 属性来覆盖默认值

  • 在概念上,具有相同的 affinity 的 activity(即设置了相同 taskAffinity 属性的activity)属于同一个任务栈

  • 为一个 activity 的 taskAffinity 设置一个空字符串,表明这个 activity 不属于任何任务栈

注意: taskAffinity 属性主要和 singleTask 启动模式或者 allowTaskReparenting 属性配对使用,在其他情况下没有意义。所以即使上述两种模式下设置了该属性,也不会创建新的任务栈,只是名字不同罢了。指定方式如下:

<activity android:name=".activity.MainLaunchModeActivity" 
    android:configChanges="orientation|screenSize" 
    android:taskAffinity="zeng.fanda.com.fourmoduledemo.launch">

singleTask 模式

栈内复用模式,这是一种栈内单例模式,不同的栈内可以存在不同的实例。创建这种模式的 Activity 存在的时候,系统会先确认它所需任务栈是否已经创建,否则先创建任务栈(任务栈通过 taskAffinity 属性指定).然后放入 Activity ,如果所需的栈已经存在而且已经有一个 Activity 实例,那么这个 Activity 就会被复用,会将当前 Activity 上面所有的 Activity 出栈,并且会回调 onNewIntent()->onResume() 。

清单文件配置如下:

<activity android:name=".activity.SingleTaskModeActivity"  android:launchMode="singleTask"/>

使用案例一:从一个主界面,打开一个 singleTask 模式的界面,在该界面再次打开该界面。

案例打印的日志如下:

这种情况跟 SingleTop 模式一样的流程,无需过多分析。

使用案例二:从一个主界面,打开一个 singleTask 模式的界面 SingleTaskModeActivity ,在该界面再打开另一个 singleTask 模式的界面 OtherTaskModeActivity ,最后在 OtherTaskModeActivity 打开之前的 SingleTaskModeActivity 。

案例打印的日志如下:

这里我们只关注 OtherTaskModeActivity 跳回 SingleTaskModeActivity 时的情况,由图可知,SingleTaskModeActivity 回调了 onNewIntent()->onStart()->onResume() 。由于是从不可见到可见,所以这里比 SingleTop 模式多回调 onStart()。此时栈内应该只有两个实例,因为 OtherTaskModeActivity 会被出栈,我们使用命令 adb shell dumpsys activity activities 查看一下:

当没跳回到 SingleTaskModeActivity 时,栈内信息如下:

跳回到 SingleTaskModeActivity 时,栈内信息如下:

使用案例:在案例二的基础上,指定 SingleTaskModeActivity 的 taskAffinity 值。

<activity android:name=".activity.SingleTaskModeActivity"  
    android:launchMode="singleTask" 
    android:taskAffinity="zeng.fanda.com.fourmoduledemo.singletop"/>

案例打印的日志如下:

当没跳回到 SingleTaskModeActivity 时,日志信息和栈内信息如下:

日志分析: SingleTaskModeActivity 的 TaskId 和 taskAffinity 跟 MainLaunchModeActivity 的不一样了,说明新开启了一个任务栈 SingleTaskModeActivity 放在了一个新栈上面。因为 OtherTaskModeActivity 没指定 taskAffinity 属性,所以OtherTaskModeActivity 的 TaskId 和 taskAffinity 跟 MainLaunchModeActivity 的一样,说明没建新的任务栈,放在了 MainLaunchModeActivity 所处的栈上面了。栈内信息充分说明了这一点。

当跳回到 SingleTaskModeActivity 时,日志信息和栈内信息如下:

日志分析:跳回到 SingleTaskModeActivity 时,回调了 SingleTaskModeActivity 的 onNewIntent() 方法,但是 OtherTaskModeActivity 没有出栈 ,因为 SingleTaskModeActivity 是在一个新栈上面的,且栈内只有它一个实例。即 793 栈 有 MainLaunchModeActivity 和 OtherTaskModeActivity ,794 栈有 SingleTaskModeActivity ,这个操作只是从 793 的 OtherTaskModeActivity 切换到了794栈的 SingleTaskModeActivity 。

singleInstance 模式

该模式具备 singleTask 模式的所有特性外,与它的区别就是,这种模式下的 Activity 会单独占用一个任务栈,具有全局唯一性,即整个系统中就这么一个实例,由于栈内复用的特性,后续的请求均不会创建新的 Activity 实例,除非这个特殊的任务栈被销毁了。以 singleInstance 模式启动的 Activity 在整个系统中是单例的,如果在启动这样的 Activiyt 时,已经存在了一个实例,那么会把它所在的任务调度到前台,重用这个实例。

使用案例::从 A 应用的一个主界面,打开一个 singleInstance 模式的界面 SingleTaskInstanceActivity ,在 B 应用再打开该界面。即分别在两个应用打开同一个 singleInstance 模式的界面 。

配置信息:

<activity android:name=".activity.SingleInstanceModeActivity"  android:launchMode="singleInstance">
    <intent-filter>
        <action android:name="zeng.fanda.com.fourmoduledemo.singleinstance"/>
        <category android:name="android.intent.category.DEFAULT"/>

日志信息如下:

日志分析:

A应用的主界面的 TaskId 是 809 , SingleInstanceModeActivity 的 TaskId 是 810 ,证明为SingleInstanceModeActivity 新建了一个任务栈 。B应用的主界面的 TaskId 是 811 ,在B应用打开 SingleInstanceModeActivity 时,回调了 onNewIntent ,且 TaskId 是 810 ,证明SingleInstanceModeActivity 被调到前台重用了,没有新建任务栈和新建实例,具有系统全局唯一性。

配合 allowTaskReparenting 使用

android:allowTaskReparenting="true"

当应用A启动应用B的某个 Activity 后,如果这个 Activity 设置了该属性,那么当B应用启动后,该 Activity 会从应用A的任务栈转移到应用B的任务栈中。具体一点,A应用启动了B的界面,B的界面此时应该放在启动它的应用A的任务栈里,当B应用启动后,B的任务栈就生成了,界面检测到了之后,就不放在A应用了,转移到自己的应用的任务栈里,所以打开B应用后,会看到从A应用打开的界面。这个效果比较特殊。

前后台任务栈

前台任务栈表示该任务栈正在前台,比如前台任务栈里有 A-B 两个界面。后台任务栈中的界面位于暂停状态,用户可以通过切换将后台任务栈再次调到前台,比如后台任务栈里有 C-D 两个界面 。现在从前台任务栈请求D界面,此时整个后台任务栈将切换到前台,如果此时返回,将从D返回到C 界面,C再返回B,B再返回A。即前台任务栈的界面全部出栈后,后台任务栈再切到前台继续出栈。

Activity 启动模式的使用场景

  1. SingleTask 模式的运用场景,最常见的应用场景就是保持我们应用开启后仅仅有一个Activity 的实例。最典型的案例就是应用中展示的主页(Home页)。

  2. SingleTop模式的运用场景,假设你在当前的 Activity 中又要启动同类型的 Activity,此时建议将此类型 Activity 的启动模式指定为 SingleTop,能够降低 Activity 的创建,节省内存!

注意:由于当一个 Activity 设置了 SingleTop 或者 SingleTask 模式后,跳转此 Activity 出现复用原有 Activity 的情况时,此 Activity 的 onCreate 方法将不会再次运行。 onCreate 方法仅仅会在第一次创建 Activity 时被运行。这时我们要在另外一个回调 onNewIntent(Intent intent)方法中做处理。

@Override
protected void onNewIntent(Intent intent) {
    super.onNewIntent(intent);
    setIntent(intent);
    //数据处理
}

Activity 的 Flags

  • FLAG_ACTIVITY_NEW_TASK

作用是为 Activity 指定 “SingleTask” 启动模式。效果和在 AndroidMainfest.xml 指定相同。

  • FLAG_ACTIVITY_SINGLE_TOP

作用是为 Activity 指定 “SingleTop” 启动模式,效果和在 AndroidMainfest.xml 指定相同。

  • FLAG_ACTIVITY_CLEAR_TOP

具有此标记位的 Activity ,启动时会将与该 Activity 在同一任务栈的其他 Activity 出栈。一般与 SingleTask 启动模式一起出现。 SingleTask 启动模式默认具有此标记位的作用。

总结:Flags 方便动态打开 Activity 时实现这些效果,比如我们打开第三方 Activity 时,我们可以加上这些 Flags 来达到我们想要的效果。

IntentFilter 的匹配规则

启动 Activity 分为显式调用和隐式调用,隐式调用需要 Intent 能够匹配目标组件的 IntentFilter 中所设置的过滤信息,如果不匹配将无法启动目标 Activity。 IntentFilter 中的过滤信息有 action 、 category 和 data 。

为了匹配过滤列表,需要同时匹配列表中的 action 、 category 、data信息,否则匹配失败。 action 、 category 、data 都可以有多个,IntentFilter 也可以有多个,一个 Intent 只要能匹配任何一组 intent-filter 即可成功启动对应的 Activity 。

action 匹配规则

Intent 中的 action 存在且必须和过滤规则中的其中一个 action 相同即可匹配成功。action 区分大小写,大小写不同会导致匹配失败。虽然过滤规则中可以有多个 action ,但是 Intent 中的设置的 action 只有一个,方法是 intent.setAction()。Intent中 没有指定 action ,也会匹配失败。

category 匹配规则

Intent 中可以不添加 category ,系统会默认为 Intent 添加 上 “android.intent.category.DEFAULT” 这个 category。Intent 中可以添加多个 category ,方法是 intent.addCategory(),添加的 category 在过滤规则中都必须存在,有一个不存在或不相同,都会匹配失败

data 匹配规则

data 的结构如下:

<data
    android:host="string"
    android:mimeType="string"
    android:path="string"
    android:pathPattern="string"
    android:pathPrefix="string"
    android:port="string"
    android:scheme="string" />

data 由两部分组成,mimeType 和 URL ,mineType 指媒体类型,比如 image/jpeg 、 video/* 等,可以表示图片、视频等不同的媒体格式,而URI 中包含的数据比较多,URI结构如下:

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

比如:

content://zeng.fanda.com.fourmoduledemo.provider.PersonProvider/person

http://www.baidu.com:80/search/info

scheme: URI 的模式,比如 http、file、content 等,如果没有指定,整个URI都是无效了。

host: URI 的主机名,比如 www.baidu.com ,如果没有指定,整个URI都是无效了。

port: URI 的端口号,比如 80 ,仅当有 scheme 和 host 的情况下 ,才有意义。

path、pathPrefix、pathPattern: 表述路径信息,path 表示完整路径,pathPrefix 表示路径的前缀信息,pathPattern 也表示完整的路径信息,但是可以包含通配符*

匹配规则和 action 类似 ,Intent 中的 data 存在且必须和过滤规则中的其中一个 data 相同即可匹配成功。虽然过滤规则中可以有多个 data ,但是 Intent 中的设置的 data 只有一个,方法是 intent.setData()。如果过滤规则中有,但是Intent中没有指定 data ,也会匹配失败。

如果我们没有指定 URI ,系统会有默认值,URI 的默认值为 content 和file ,也就是说,Intent 中的 URI 部分的 scheme 必须为 content 或 file 才能匹配。比如 :

<data android:mimeType="image/*" />

如下调用方式会失败:

intent.setDataAndType(Uri.parse("http://abc"), "image/jpg");

正确的调用方式为:

intent.setDataAndType(Uri.parse("content://abc"), "image/jpg");

注意:

  • 如果为 Intent 指定完整的 data ,必须调用 setDataAndType 方法,不能先调用 setData 再调用 setType ,这两个方法会清除对方的值。

  • 当隐式调用时,可能会找不到相应的组件,这时候会报错,所以我们可以先做一下判断,看是否有组件能够匹配到,匹配到才做相应处理。Intent 的 resolveActivity() 方法可以做到,方法返回 null 则表示没找到匹配的,代码示例如下:

    Intent intent = new Intent();
    intent.setAction("abc");
    intent.setDataAndType(Uri.parse("content://abc"), "image/jpg");
    if (intent.resolveActivity(getPackageManager()) != null) {
        startActivity(intent);
    }
    
  • 有一类 action 和 category 比较特殊,是用来标明这是一个入口 Activity 并且会出现在系统的应用列表中,少了任何一个都没有意义,他们是:

    <action android:name="android.intent.action.MAIN" />
    
    <category android:name="android.intent.category.LAUNCHER" />