Activity的生命周期和启动模式 -- 重读《Android 开发艺术探索》

318 阅读5分钟

重读《Android 开发艺术探索》,本篇是书中的第一章内容的总结。

一、Activity 生命周期

首先看下官方提供的 Activity 生命周期图

image.png

1. 正常情况下的生命周期分析

a)首次启动一个 mainActivty 时的调用情况:
main onCreate
main onStart
main onResume
b)从 mainActivty 启动 childActivity 时的调用情况:
main onPause
child onCreate
child onStart
child onResume
main onStop
c)从 childActivity 回退到 mainActivty 时的调用情况:
child onPause
main onRestart
main onStart
main onResume
child onStop
child onDestroy
d)在 mainActivty 按 home 键回到桌面或在当前页面锁屏的调用情况:
main onPause
main onStop
e)接着上一步再打开 mainActivty 或解锁屏幕时的调用情况:
main onRestart
main onStart
main onResume
f)显示 Toast、Dialog

显示 Taost 不会调用任何生命周期

显示 Dialog 不会调用任何生命周期

再延伸一下,在 dialog 中有个按钮,点击这个按钮之后打开 childActivity,此时的生命周期调用同直接在 mainActivity 中打开 childActivity 是一样的。

g)特殊情况,childActivity 设置了透明主题

透明主题设置,在 AndroidManifest.xml 中设置

<activity
        android:name=".ChildActivity"
        android:theme="@style/TranslateTheme"
        android:exported="false" />

TranslateThemethemes.xml 中定义

<style name="Translate" parent="Theme.AppCompat">
    <item name="android:windowBackground">@android:color/transparent</item>
    <item name="android:colorBackgroundCacheHint">@null</item>
    <item name="android:windowIsTranslucent">true</item>
</style>

从 mainActivty 启动 childActivity 时的调用情况:

main onPause
child onCreate
child onStart
child onResume

在 childActivity 锁屏或按 Home 键回到桌面,调用情况:

child onPause
child onStop
main onStop

解锁屏幕,调用情况:

main onRestart
main onStart
child onRestart
child onStart
child onResume

从 childActivity 回退到 mainActivty 时的调用情况:

child onPause
main onResume
child onStop
child onDestroy

可以看到同上面 b、c 中相比较不会调用 onStop、onRestar、onStart,这种情况可以更好的理解:onStart 的时候就可见了,但是不能同用户交互,onStop 之后就不可见了,因为透明主题两个 Activity 都是可见的,所以不会调用 onStop,也就不会调用 onRestar、onStart。

2. 异常情况下生命周期分析

a)不做额外设置的前提下,横竖屏切换,生命周期调用情况:
main onPause
main onStop
main onSaveInstanceState // 存储数据
main onDestroy
main onCreate
main onStart
main onRestoreInstanceState // 恢复 onSaveInstanceState 中保存的数据
main onResume

onSaveInstanceStateonRestoreInstanceState 为当 Activity 被销毁并且有机会再次显示(常见系统配置发送改变如屏幕旋转、切换了系统语言、插入外接键盘)的情况时才会被调用,正常调用 finish()或按返回键退出并不会调用这两个方法。

Editext 需要设置 id 才能在横竖屏切换后自动恢复输入的内容

b)设置 Activity 当设备配置发送改变后不重建

通过在 AndroidManifest.xml 文件中对指定 activity 做出如下设置,则可以在屏幕旋转后不重建。

<activity
        android:name=".MainActivity"
        android:configChanges="orientation|screenSize" // 设置当屏幕旋转不发生重建
        android:exported="false">
</activity>

二、Activity 的启动模式

1. 启动模式

包含:stadard、singleTop、singleTask、singleInstance 四种。 解释见下图:

image.png

在模式为 singleTask 的 activityA 中打开模式为 standard 的 ActivityB,则 ActivityB 也处于 ActivityA 的任务栈中。

TaskAffinity 用来标识一个 Activity 所需要的任务栈的名字(名字必须包含至少一个 .),默认为应用的包名。该属性结合 singelTask 启动模式或者 allowTaskReparenenting 配对使用。

设置启动模式及任务栈的方式

<activity
        android:name=".ChildActivity"
        android:exported="false"
        // 设置启动模式
        android:launchMode="singleTask"
        // 设置任务栈名称至少包括一个 . 
        android:taskAffinity="hyh.task_h" />

查看 activity 堆栈信息 adb shell dumpsys activity activities 查看堆栈相关信息

...

 Application tokens in top down Z order:
      // hyh.task_h 为堆栈名,即 taskAffinity 属性的值
      * Task{63ac7df #625 visible=true type=standard mode=fullscreen translucent=false A=10128:hyh.task_h U=0 StackId=625 sz=2}
        mLastOrientationSource=ActivityRecord{b27edcd u0 com.hyh.okhttpdemo/.ThirdActivity t625}
        bounds=[0,0][1440,2560]
        // 该任务栈中的 activity 信息
        * ActivityRecord{b27edcd u0 com.hyh.okhttpdemo/.ThirdActivity t625}
        * ActivityRecord{18e2a58 u0 com.hyh.okhttpdemo/.ChildActivity t625}
      * Task{3944544 #1 visible=false type=home mode=fullscreen translucent=true I=com.android.launcher3/.uioverrides.QuickstepLauncher U=0 StackId=1 sz=1}
        mLastOrientationSource=Task{2a97ab0 #5 visible=true type=home mode=fullscreen translucent=true I=com.android.launcher3/.uioverrides.QuickstepLauncher U=0 StackId=1 sz=1}
        bounds=[0,0][1440,2560]
        * Task{2a97ab0 #5 visible=true type=home mode=fullscreen translucent=true I=com.android.launcher3/.uioverrides.QuickstepLauncher U=0 StackId=1 sz=1}
          mLastOrientationSource=ActivityRecord{7adeb2d u0 com.android.launcher3/.uioverrides.QuickstepLauncher t5}
          bounds=[0,0][1440,2560]
          * ActivityRecord{7adeb2d u0 com.android.launcher3/.uioverrides.QuickstepLauncher t5}
      * Task{dec510e #624 visible=false type=standard mode=fullscreen translucent=true A=10128:com.hyh.okhttpdemo U=0 StackId=624 sz=1}
        mLastOrientationSource=ActivityRecord{e977410 u0 com.hyh.okhttpdemo/.MainActivity t624}
        bounds=[0,0][1440,2560]
        * ActivityRecord{e977410 u0 com.hyh.okhttpdemo/.MainActivity t624}
      * Task{6c48fa8 #3 visible=false type=undefined mode=split-screen-primary translucent=true ?? U=0 StackId=3 sz=0}
        bounds=[0,0][1440,1221]
      * Task{9668ac1 #4 visible=false type=undefined mode=split-screen-secondary translucent=true ?? U=0 StackId=4 sz=0}
        bounds=[0,1255][1440,2560]
        
...

2. Flags

标记位,作用:设置 activity 的启动模式,影响 activity 的运行状态。

常用标记位 FLAG_ACTIVITY_NEW_TASK:指定为 singleTask 启动模式,同在 xml 中设置一样 FLAG_ACTIVITY_SINGLE_TOP:指定为 singleTop 启动模式,同在 xml 中设置一样 FLAG_ACTIVITY_CLEAR_TOP:当具有该标记位的 activity 启动时,在同一任务栈中所有位于它上面的 activity 都要被出栈 FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS:具有该标记的 activity,不会出现在历史列表中。

3. IntentFilter 匹配规则

启动 Activity 分为显式调用隐式调用

如果 Intent 同时存在显式调用和隐式调用,以显式调用为主。

显式调用:明确指定对象的组件信息,包括包名、类名。

// 显式调用例子
startActivity(Intent(this, ChildActivity::class.java))

隐式调用通过匹配 IntentFilter 中所设置的过滤信息来启动 Activity。

IntentFilter 包括: action、category、data。

详细说明见下图

image.png

使用列子

定义 IntentFilter 的代码:

<activity
        android:name=".ThirdActivity"
        android:exported="false" >
    <intent-filter>
        <action android:name="com.hyh.third"/>
        <category android:name="hyh.third"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <data android:mimeType="text/plain"/>
    </intent-filter>
</activity>

启动 ThirdActivity 部分代码:

val intent = Intent("com.hyh.third")
intent.addCategory("hyh.third") // 可以省略不写
intent.setDataAndType(Uri.parse("content://abc"), "text/plain") // 因为只设置了mimeType ,所以等同与:intent.type = "text/plain"
// 校验是否可以匹配到指定 activity
packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY)?.let {
    startActivity(intent)
}

App 默认启动 Activity 的 intent-filter 配置

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

另外:Servie、BroadcastRecivier 也是同样的匹配规则。