第一章 Activity 的生命周期和启动模式

1.1 Activity 生命周期

1.1.1 正常状态下的生命周期

这里正常状态下的生命周期指因用户的操作引起的生命周期改变。具体生命周期可以参考下图:

activity_lifecycle.png

有几种情况需要说明:

  1. 针对一个特定的 Activity ,第一次启动,其回调如下: onCreate -> onStart -> onResume
  2. 当用户打开新的 Activity 或切换到桌面的时候,回调如下:onPause -> onStop ,如果新的 Activity采用透明主题,则原 Activity 的 onStop 不会回调
  3. 当用户再次回到原 Activity 时,回调如下: onRestart -> onStart -> onResume
  4. 当用户按下 back 键时,回调如下:onPause -> onStop -> onDestroy
  5. 当 Activity 被系统回收后再次打开,生命周期回调过程和 1 一样,但只是方法一样,过程可能不一样
  6. 就 Activity 整个生命周期来说, onCreateonDestroy 是配对的;就 Activity 是否可见来说,onStartonStop 是配对的;就 Activity 是否在前台来说,onResumeonPause 是配对的

有两个问题需要回答:

  1. onStartonResumeonPauseonStop 对我们来说有什么实质的不同?
  2. 假设当前处于 Activity A,现在用户打开一个 Activity B,那么请问 A 的 onPause 和 B 的 onResumt 哪一个先执行?
  • 对于问题一,在正常的生命周期中其实没有很大的不同,只不过这两对生命周期表示的意义不同,onStartonStop 是从是否可见角度,onResumeonPause 是从 Activity 是否位于前台的角度,在某些时候这些生命周期有可能会有区别
  • 对于问题二,是 A 的 onPause 先执行,对于为什么要进入到源码来分析,后面会涉及

1.1.2 异常状态下的生命周期

  1. 资源相关配置发生改变导致 Activity 重建

具体可以看下图:

image-20210421213620118.png

在 Activity 发生改变导致重建后,会先调用 onSaveInstanceState方法来保存数据,然后再新的 Activity 创建时,将数据传入 onCreate 方法,以便恢复数据,同时新的 Activity 的 onRestoreInstanceState 方法也会调用。

  • 官方推荐在 onRestoreInstanceState 中恢复数据,因为该方法一定被调用,其参数一定不为空,而对于·onCreate ,其参数可能为空
  • 如果我们不重写 onSaveInstanceStateonRestoreInstanceState 方法,其系统的默认会 通过递归调用布局树中 VeiwonSaveInstanceState 方法来保存数据,恢复数据时也是递归调用 ViewonRestoreInstanceState 方法
  • Activity 的 onSaveInstanceState 方法只会在 Activity 异常中止时才会调用,对于正常调用 finish 方法终止,是不用保存数据的

具体资源配置的项目如下:

当我们不想要 Activity 在相关配置文件改变时重建时,我们可以在 AndroidMenifest.xml 中给 activity 标签加上 configChanges 属性,多个属性用 | 分割,例如我们不想要在 屏幕旋转 (orientation) 和 屏幕布局发生改变 (screenLayout)时重建我们可以这么写:

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

特别的,当用户设置 onfigChanges 时,相应事件发生时系统不会重建 Activity,取而代之的是执行了 onConfigurationChanged 方法

  1. 资源内存不足导致低优先级的 Activity 被杀死

Activity 的优先级:

  1. 前台 Activity
  2. 可见但非前台 Activity
  3. 后台 Activity

当内存不足时,系统会按照以上优先级杀死低优先级的 Activity 并通过 onSaveInstanceStateonRestoreInstanceState 方法来恢复数据

1.2 Activity 启动模式

1.2.1 任务栈

当多个Activity 启动的时候,系统是以 任务栈 的形式来管理,例如我们此时任务栈中只有一个 Activity A,此时启动 Activity B,则会将 Activity B入栈,当用户按 Back 键 finish Activity B 时,此时 Activity B 出栈,回到 Activity A

  • 任务栈是独立于应用的概念,在全局起作用,你的 Activity 可能在别的应用创建的任务栈中
  • 任务栈体现在近期任务中,近期任务页面中一个任务就是一个返回栈
  • 对于默认 Activity ,系统会为其创建一个默认的 任务栈
  • 任务栈有前台任务栈和后台任务栈之分,一般情况下只有一个前台任务栈
  • 前后台任务栈是可以切换的,当你通过近期任务点击一个后台任务栈时,此时的前台任务栈会变为后台,而后台任务栈被唤醒到前台
  • 我们常用的桌面软件就是一个 SingleIntance 启动模式的 Activity ,其拥有一个独立的任务栈,并且不会出现在近期任务中,可见下面 SingleInstance 的特性

1.2.2 启动模式

Activity 的启动模式有四种,他们都可以在 AndroidMenifest.xml 中指定,例如下面表示 MainActivity 的启动模式为 standard

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

四个启动模式的特性如下:

  • standard :当不指定时默认为此类型。每次都会创建新的实例,首先系统会尝试将其放进调用它的 Activity 的返回栈,如果调用它的 Activity 所在返回栈不能放入,则会尝试放入其 想要的任务栈 中,如果还是不能放入,则新建任务栈放入。
  • singleTop:栈顶复用。如果这个 Activity 当前位于当前栈的栈顶,则不会创建新实例,否则创建新,直接调用栈顶 Activity 的 onNewIntent 方法。注意,这里栈顶复用是对于当前前台任务栈来说
  • singleTask:栈内复用。当使用这个模式的 Activity 启动时,系统会找到其想要的任务栈,并在该栈中寻找这个 Activity,如果其这个栈中存在实例,则会将该栈不断出栈知道该栈浮到栈顶然后调用其 onNewIntent方法。否则系统会尝试创建一个新的实例放入这个新的栈中 ,然后将该栈至于前台。
  • singleInstance :单实例。首先该模式满足 simgleTask 模式的行为,只是其想要的任务栈永远为一个新的独立的任务栈,并且当其启动其他的 Activity 也会在其他的栈中,即这个栈是不可进入的

1.2.3 启动 Flags

可以通过启动 Activity 时的 Intent 来指定启动模式,这种指定方式优先级要高于 在 AndroidMenifest.xml中指定的模式

  • FLAG_ACTIVITY_NEW_TASK:与 singleTask 一致
  • FLAG_ACTIVITY_SINGLE_TOP: 与 singleTop 一致
  • FLAG_CLEAR_TOP:如果前台任务栈中拥有该 Activity 实例,则将其顶所有其他 Activity 出栈,然后的行为要看其他指定效果,如果为 standard 则会创建新实例放入。

以上结论部分与书中结论有出入,是我根据实验得出的猜想。

注意:

  1. standard 只有当调用我的 Activity 的任务栈不可进入时才会进入第二步寻找想要的任务栈,如果不存在这样一个返回栈,则会直接抛异常(在安卓7、8两个版本也会进入第二步寻找想要的任务栈,其他版本中会报错),例如用 Application 启动。
  2. singleInstance 的任务栈是特殊的,是不可进入的,当其在后台时,你可以通过返回回到该栈,但当你打开近期任务页面时,处于后台的 singleInstance 创建的栈会被销毁。
  3. singleTask 不会每次都创建新的任务栈,它首先会寻找当前该 应用 的任务栈中是否存在该实例,如存在则直接将该实例通过不断出栈上浮到顶,然后同 onNewIntentIntent 传入。如果不存在,则会将其放到其想要的任务栈中,若想要的任务栈恰巧是调用该 Activity 的任务栈,则和 Standard 一样会放入调用我 的 Activity 的任务栈,如果该栈不可进入,则会创建新任务栈。

1.2.4 TaskAffinity

这个参数可以指定一个 Activity 想要的任务栈的编号,当不指定时,默认为包名。当指定时,字符串不能以包名作为前缀。当新任务栈创建时,其任务栈内所有 Activity 的 TaskAffinity 都可以作为该任务栈的编号。

指定示例:

<activity android:name=".SecondActivity"
            android:taskAffinity="com.heyanle.task"/>

1.3 IntentFilter 匹配规则

1.3.1 IntentIntentFilter

从语义分析,Intent 是意图,表示我们想要实现某个功能,而完成这个功能需要某个组件。而 IntentFilter 是意图过滤器,它可以匹配一个或多个 Intent

简单地说,启动组件的时候,我们需要构造一个 Intent 并发送。而组件在注册的时候可以注册自己的 IntentFilter ,当系统收到 Intent 时,会去寻找有没有已经注册的组件的 IntentFilter 可以匹配这个 Intent,并启动组件,如果有多个匹配,则会弹出一个 窗口 让用户选择,例如用哪个组件打开该文件,用哪个组件来分享图片(将图片分享到哪)。

注册 IntentFilter 的方式是在 AndroidMenifest.xml 中指定,一个组件可以注册多个 IntentFilter

注册示例:

<activity android:name=".ui.search.SearchActivity">
    <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.LAUNCHER"/>
        <data android:mimeType="text\plain"/>
    </intent-filter>
    <intent-filter>
        <action android:name="android.intent.action.ANSWER"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <data android:mimeType="text\plain"/>
    </intent-filter>
</activity>

其实从示例中可以看见,一个 IntentFilteractioncategorydata 三个属性,对于一个 IntentFilter 只有这三个同时匹配,整个 Intent 才能成功匹配,而对于组件来说,只要有一个 IntentFilter 匹配,则整个组件就可以匹配。接下来分析三个属性的匹配规则:

1.3.2 action 匹配规则

action 是一个字符串,其值可以为 Intent 中预定义的一些字符常量,也可以为自定义字符串。从语义来说,这个匹配规则主要是匹配 Intent 的行为。例如我们常用的默认 Activity 中就指定了一个 IntentFilteractionandroid.intent.action.MAIN ,这就是系统预定义的一个常量。

action 匹配遵循以下规则:

  • 一个 IntentFilter 可以指定多个 action ,而只要 Intent 中的 action 可以匹配其中一个,则这个 IntentFilter 的整个 action 属性就可以被匹配。
  • 一个 Intent 只能指定一个 action
  • Intent 指定的 action 必须要和 IntentFilter 中其中一个匹配,一个没有指定 actionIntent 不能匹配没有指定任何 actionIntentFilter
  • action 的匹配区分大小写

1.3.3 category 匹配规则

category 是一个字符串,其值可以为 Intent 中预定义的一些字符串常量,也可以为自定义字符串。从语义来说,这是指定 Intent 的类型。例如默认 Activity 中就指定了一个 android.intent.category.LAUNCHERcategory

category 匹配遵循以下规则:

  • 一个 IntentFilter 中可以指定多个 category ,一个 Intent 中也可以指定多个 category。一个 IntentIntentFilter 匹配的条件是 IntentFilter 中指定的所有 category 要包含 Intent 中指定的所有。
  • 如果 Intent 中没有指定任何 category,那么它能匹配所有 IntentFilter ,因为空集被所有集合包含,包括另一个空集。
  • category 的匹配同样区分大小写
  • 当我们调用 startActivity 或者 startActivityForResult 隐式启动的时候,系统会自动为我们的 Intent 中加上 android.intent.category.DEFAULTcategory,那么按照我们上面的匹配规则,如果你的 Activity 想要被隐式启动,则必须在 IntentFilter 中加入该 category

1.3.4 data 的匹配规则

data 分两部分组成,第一部分是 mineType ,从语义分析是媒体类型。第二部分是 URI ,即统一资源标识符,自然是用于表示一个资源。这里分两部分分析。

  • mineType 是一个字符串,用于指定媒体格式,虽然说是字符串可以自定义,但是其有其一般写法,例如对于所有视频文件为 video/*,对于 jpg 格式的图片为 image/jpeg。并且这里支持通配符匹配 *,即 Intentimage/jpeg 可匹配 image/*IntentType
  • URI 包含的内容较多,以下是一条 URI 的格式:

    <scheme>://<host>:<port>/<path>
    • <scheme> : 模式,比如 httpfilecontent 等。
    • <host>:主机名,可以是 域名 如 heyanle.com,也可以是 IP127.0.0.1
    • <port>:端口。可选。
    • <path>:路径,可以是完整路径,也可包含通配符 *,同时,如果需要 * 字符本身,则需要转转义。

对于 URI ,其各个部分可指定也可不指定,并且具有线性依赖关系,

  • 未指定 scheme,则会忽略 host
  • 未指定 host,则会忽略 port
  • 未指定 hostport,则会忽略 path

注意,这里指的忽略并不是真正将其丢弃,而是匹配的时候将其忽略,例如对于文件,一般为 file:///sdcard/a.jpg,此时没有指定 host ,而最终组件启动后是可以拿到完整的 URI

URI 匹配时,系统会对 IntentFilter 有指定的部分进行比较,如:

  • IntentFilter 有指定 scheme 而未指定其他,则所有包含该 schemeURI 都可匹配
  • IntentFilter 指定了 schemehostport ,则所有包含该 schemehostportURI 均可匹配,无论 path
  • IntentFilter 指定了所有部分,则必须所有部分都一致才可匹配(path 中支持通配符)。

对于部分未指定 mineType 但指定了 URIIntent 而言,系统会尝试从 URI 中分析 mineType,例如对于 schemefile,如果在其 path 中包含文件名,则会从后缀名中分析。

对于整个 data 而言。其匹配遵循以下规则:

  • 如果 Intent 中不包含 data ,则其只可匹配同样不包含 dataIntentFilter,这里隐含了 data 可以为空的规则。
  • IntentFilter 可指定多个 data,而 Intent 只可指定一个 data,并且只要 IntentFilterIntent 其中一个的 data 不为空,则 IntentFilter 中必须存在一个与 Intentdata 完全匹配data 才可匹配(Intent 中若未指定 mineType,则可能会自动分析自动指定)。这里的完全匹配包括未指定的部分,例如 IntentFilter 其中一个 data 指定了 URI 但未指定 mineType,则只有同样未指定 mineType 且指定了 URI 可匹配的 Intent 才可匹配。除了下一点列出的特殊情况。
  • 如果 IntentFilter 其中一个 data 只指定了 mineType 而未指定 URI。此时如果 Intent 指定了可匹配的 mineType,同时指定了 filecontentschemeURI ,则依然可以匹配,此时不需要完全匹配的那种未指定 URI 的条件。

以上是 data 的匹配规则,接下来讲一下 data 的指定

对于 Intent,可以调用其三个方法指定三种情况:setDatasetTypesetDataAndType。值得注意的是,对于前两者会将对方制空,如果你需要同时指定 mineTypeURI ,则只能调用第三个方法,不能通过分别调用两次前两个方法。

对于 IntentFilter,可以参考以下示例:

<activity android:name=".SecondActivity"
    android:launchMode="standard">
    <intent-filter>
        <action android:name="com.heyanle.activitydemoii.t"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <data android:mimeType="video/*" android:scheme="file"/>
    </intent-filter>
</activity>

1.3.5 查找是否有匹配的 Activity

对于隐式启动,如果 Intent 找不到匹配的组件,则会抛出一个运行时异常,因此常规做法是在启动之前查找一下是否有匹配的组件,可以调用以下两个方法:

这两份方法是 PackageManager 中的方法:

public List<ResolveInfo> queryIntentActivities(Intent intent, int flags){}
public ResolveInfo resolveActivity(Intent intent, int flags){}

其中,第一个返回所有可匹配的对象,而第二个返回一个最佳匹配对象。对于第二个参数,可以使用 MATCH_DEFAULT_ONLY,表示只匹配 category 包含 android.intent.category.DEFAULT 的 Activity,即只匹配可以被隐式启动的 Activity。