Android 的启动模式是在我们日常开发中经常用使用到, 这个也是在面试用经常问到的一个问题. 虽然我们对他很熟悉, 但也会有些地方了解的太全面, 因此写篇文章来来总结这方面的知识. 文章主要内容来自《Android 开发艺术探讨》这本书, 在文章的最后这本书的网页版本可供查看.
项目源码 https://github.com/hdd1024/AndroidReView
目录
四中启动模式
什么是任务栈
Activity 如何指定需要的任务栈
TaskAffinity 使用场景
Activity 的 Flags
IntentFilter 的匹配规则
如何判断隐式启动是否成功
1. 四中启动模式
standard: 标准启动模式
每启动一个 Activity 都会重新创建, 不管这个实例是否存在.
- <!-- 系统默认启动方式, 不需要指定 launchMode 值 -->
- <activity Android:name=".StandardActivity" />
下面内容摘自《Android 开发艺术探讨》第一章 16 页底部
在 standard 模式下, 谁启动了这个 Activity 那么这个 Activity 就运行在它的任务栈中. 例如: ActivityA 启动了 ActivityB(B 为标准模式), 那么 ActivityB 就会进入 ActivityA 的任务栈中.
启动 Activity 的时候传入的 Context 不要是 ApplicationContext. 如果一定要传, 那么一定要设置 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 否则回报下面异常:
singleTop: 栈顶复用模式
新启动 Activity 栈顶已经存在, 不会重新创建, 同时会调用 onNewIntent 方法
Activity 如果存在, 但是不再栈顶, 则会重新创建, 并将新的 Activity 压入栈顶.
- <activity Android:name=".SingleTopActivity" Android:launchMode="singleTop"
- />
singleTask: 栈内复用模式
只要 Activity 在栈内存在, 多次启动此 Activity 也不会创建新的实例, 并且系统会调用 onNewIntent 方法
如果 Activity 在栈内存在, 但是没有在栈顶, 系统会将该 Activity 之上的 Activity 全部挤出栈顶, 使该 Activity 位于栈顶.
- <activity Android:name=".SingleTaskActivity" Android:launchMode="singleTask"
- />
singleInstance: 单实例模式
该 Activity 只能单独位于一个任务栈中
- <activity Android:name=".SingleInstanceActivity" Android:launchMode="singleInstance">
生命周期执行:
singleTask,singleInstance,singleInstance 模式下, 如果启动该 Activity 正好在顶部. 那么他的生命周期执行为:
onPause-->onNewIntent-->onResume
singleTask,singleInstance 模式下, 如果栈内有 Activity 实例, 但不在栈顶. 那么生命周期执行如下
onNewIntent-->onRestart-->onStart
2. 什么是任务栈
查看 activity 在栈中的情况, 可在控制台输入: adb shell dumpsys activity activities 通过搜索关键字 most recent first 快速定位 留意包名
任务栈(Task):
Task 特点:
Android 的任务栈主要用于存放 Activity, 遵循先进后出的原则.
Android 的任务栈是一个包含了 Activity 的集合, 我们每次打开新的 Activity 或者关闭一个 Activity 任务栈中就会增加或减少一个 Activity 组件.
任务栈在没有 Activity 或者 App 退出的时候都会被销毁.
一个 App 不止有一个任务栈, 任务栈的 Activity 可以来自不同的 App, 同一个 App 的 Activity 也可以不再一个任务栈中.
3. Activity 如何指定需要的任务栈
Activity 指定需要启动的任务栈可以用过在配置文件中添加 taskAffinity 属性来实现.
TaskAffinity 特点:
默认情况下, Activity 启动的任务栈名称为应用包名.
如果自己指定该属性值, 不能与包名相同, 否则相当于没指定.
该属于一般配合 singleTask 或者 allowTaskReparenting 属性结合使用, 在其他情况下没有实际意义.
- <activity Android:name=".TestActivity" Android:launchMode="singleTask"
- Android:taskAffinity="com.test.singleTask.affinity" />
- <activity Android:name=".Test2ActivityC" Android:exported="true" Android:allowTaskReparenting="true"
- />
4. TaskAffinity 使用场景
下面这段内容摘自 Activity 启动模式与任务栈 (Task) 全面深入记录 (下) 这篇文章.
TaskAffinity 与 singleTask 应用场景
假如现在有这么一个需求, 我们的客户端 App 正处于后台运行, 此时我们因为某些需要, 让微信调用自己客户端 App 的某个页面, 用户完成相关操作后, 我们不做任何处理, 按下回退或者当前 Activity.finish(), 页面都会停留在自己的客户端(此时我们的 App 回退栈不为空), 这显然不符合逻辑的, 用户体验也是相当出问题的. 我们要求是, 回退必须回到微信客户端, 而且要保证不杀死自己的 App. 这时候我们的处理方案就是, 设置当前被调起 Activity 的属性为: LaunchMode=""SingleTask" taskAffinity="com.tencent.mm" 其中 com.tencent.mm 是借助于工具找到的微信包名, 就是把自己的 Activity 放到微信默认的 Task 栈里面, 这样回退时就会遵循 "Task 只要有 Activity 一定从本 Task 剩余 Activity 回退" 的原则, 不会回到自己的客户端; 而且也不会影响自己客户端本来的 Activity 和 Task 逻辑.
TaskAffinity 与 allowTaskReparenting 应用场景
一个 e-mail 应用消息包含一个网页链接, 点击这个链接将出发一个 activity 来显示这个页面, 虽然这个 activity 是浏览器应用定义的, 但是 activity 由于 e-mail 应用程序加载的, 所以在这个时候该 activity 也属于 e-mail 这个 task. 如果 e-mail 应用切换到后台, 浏览器在下次打开时由于 allowTaskReparenting 值为 true, 此时浏览器就会显示该 activity 而不显示浏览器主界面, 同时 actvity 也将从 e-mail 的任务栈迁移到浏览器的任务栈, 下次打开 e - 买了时并不会再显示该 activity.
Taskffinity 与 singleTask 实例:
注: 如果使用我在 GitHub 上建立的项目测这个功能的时候, 请将 TestTaskffinityOrAllowTaskRep.zip 这个压缩包解压, 并导入 AS 中. 这个压缩包是我在测试的时候写的用于跳转 androidreview 应用的 testTask 应用.
testTask 应用 (简称 T 应用)MainActivity 中有一个按钮 A, 点击按钮会调用 androidreview 应用(简称 A 应用) 的 SingleTaskActivity. 下面是两个应用的主要代码.
testTask 应用代码:
- switch (v.getId()) {
- case R.id.mBnt_ForSingleTask:
- // 跳转 androidreview 应用 SingleToak 页面的按钮方法
- ComponentName cnForSingleTask = new ComponentName(
- "com.hdd.androidreview",
- "com.hdd.androidreview.Patterm.SingleTaskActivity");
- intent.setComponent(cnForSingleTask);
- startActivity(intent);
- break;
- }
androidreview 应用代码:
- <activity
- Android:name=".Patterm.SingleTaskActivity"
- Android:exported="true"
- Android:launchMode="singleTask"
- Android:taskAffinity="cmom.han.testbt.testTask">
- </activity>
从上面的 A 应用代码配置信息中可以看到 taskAffinity 属性配置的是 T 应用的包名. 因此 SingleTaskActivity 会在 T 的任务中被创建. 假如 MainActivity 的按钮 A 点击事件中启动了 SingleTaskActivity. 那么 cmom.han.testbt.testTask 任务栈中会存在 SingleTaskActivity 和 MainActivity 两个 Activity. 下面是 Activity 在栈中的信息.
这时如果按了 hone 键返回桌面 SingleTaskActivity 进入后台, 然后点击 A 应用的图标启动的却是 A 应用的 MainActivity. 出现这种情况是因为 SingleTaskActivity 的 taskAffinity 属性指定的是 T 应用包名. 在 T 应用的 MainActivity 启动的 SingleTaskActivy 是在 T 应用的任务栈中.
TaskAffinity 与 allowTaskReparenting 实例 T 应用的 B 按钮点击事件代码:
- case R.id.mBnt_ForAllowTaskRep:
- //allowTaskReparenting 模式跳转 PattermActivity
- intent.setAction("com.hdd.androidreview.PattermActivity");
- ComponentName cnForAllowTaskRep = new ComponentName(
- "com.hdd.androidreview",
- "com.hdd.androidreview.Patterm.PattermActivity");
- intent.setComponent(cnForAllowTaskRep);
- break;
A 应用的 PattermActivity(简称 PActivity)的配置信息:
- <activity
- Android:name=".Patterm.PattermActivity"
- Android:allowTaskReparenting="true"
- Android:theme="@style/AppTheme.NoActionBar">
- <intent-filter>
- <action Android:name="com.hdd.androidreview.PattermActivity"/>
- </intent-filter>
- </activity>
点击 T 用于的 B 按钮, 会启动 A 用的 PActivity. 该 Activity 的 allowTaskReparenting 属性为 true, 那么 Activity 会被移动到 T 应用的任务中站创建. 当 T 应用按 home 返回桌面, 再点击 A 应用; PActivity 会被移回 A 的任务栈中.
T 应用调用 A 应用的 PActivity 栈内信息
T 应用按 home 返回桌面, 在启动 A 应用栈内信息:
allowTaskReparenting 仅限于以 standard 和 singleTop 启动的 activity 使用
5. Activity 的 Flags
指定 Activity 的启动模式有两种, 一种是在 AndroidMenifest.xml 中指定
- <activity
- Android:name=".Patterm.SingleInstanceActivity"
- Android:launchMode="singleInstance">
另外一种是通过 Intent 来指定
- Intent intent = new Intent();
- intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- //intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK|Intent.FLAG_ACTIVITY_CLEAR_TOP);
- intent.setClass(context, CycleActivity.class);
- context.startActivity(intent);
注: intent 指定的优先级大于 xml 中指定的优先级; 如果两个方式都指定了启动方式, 那么系统会以 intent 指定的启动方式为准.
FLAG_ACTIVITY_NEW_TASK
等同于在 xml 中配置了 singleTask 启动码模式
FLAG_ACTIVITY_SINGLE_TOP
等同于在 xml 中配置了 singleTop 启动码模式
FLAG_ACTIVITY_CLEAR_TOP
singTask 自带该标记. 这个标记会清除同一个任务栈中目标 Activity 之上的 Activity. 如果目标 Activity 采用了 standard 启动模式, 但是任务栈中已经存在了 Activity 的实例. 那么系统会清除任务中该实例以及它上面的 Activity, 并且会重新创建一个目标 Activity 实例放入栈顶.
6. IntentFilter 的匹配规则
启动 Activity 有两种, 一种为显示调用
- Intent intent = new Intent(MainActivity.this, TestActivity.class);
- startActivity(intent);
另一种为隐式调用要配置清单文件
- <!-- 隐式调用 -->
- <activity
- Android:name=".Patterm.PattermActivity"
- Android:theme="@style/AppTheme.NoActionBar">
- <intent-filter>
- <action Android:name="com.hdd.androidreview.asdf" />
- <category Android:name="com.hdd.123456" />
- <!-- 比就加上, 否则会报错 -->
- <category Android:name="android.intent.category.DEFAULT" />
- </intent-filter>
- </activity>
Activity 跳转代码
- Intent intent = new Intent();
- intent.setAction("com.hdd.androidreview.asdf");
- //category 非必须指定. 如果要指定, 一点要和清单文件中填写的一至
- // intent.addCategory("com.hdd.123456");
- context.startActivity(intent);
1. action
可以再清单文件中配置多个
intent 指定的 action 值只要和清单文件中的其中一个字符串值一样, 即可匹配成功.
如果清单文件中配置了 action, 那么在 intent 跳转中必须至少指定且配对成功一个.
- <activity
- Android:name=".Patterm.PattermActivity"
- Android:theme="@style/AppTheme.NoActionBar">
- <intent-filter>
- <action Android:name="com.hdd.androidreview.asdf" />
- <action Android:name="com.hdd.androidreview.qwer" />
- <action Android:name="12345678" />
- <!-- 必须加上, 否则会报错 -->
- <category Android:name="android.intent.category.DEFAULT" />
- </intent-filter>
- </activity>
- //java 代码
- Intent intent = new Intent();
- intent.setAction("12345678");
- context.startActivity(intent);
2. category
category 可以再清单文件中添加多个
intent 的 addCategory()字符串有一个相同就可以匹配成功.
他和 action 区别是, action 是要在 intent 必须要指定的, 切至少和一个匹配成功. category 可以不用指定. 如果指定也是至少和一个匹配成功.
- Intent intent = new Intent();
- // 必须指定一个并匹配成功
- intent.setAction("com.hdd.androidreview.asdf");
- //category 非必须指定. 如果要指定, 一点要和清单文件中填写的一至
- // intent.addCategory("com.hdd.123456");
- context.startActivity(intent);
3. data
data 的匹配规则和 action 类似, 如果过滤规则中定义了 data, 那么 intent 中必须要匹配 data.
data 有 mimeType 和 URL 两部分组成
mimeType 为媒体类型: image/jpeg,audio/mpeg4-generic 以及 video/* 等.
URL 数据结构为:
<scheme>://<host>:<prot>/[<path>|<pathPrefix>|<pathPattern>]
例如:
- content://com.example.project:200/folder/subfolder/etc
- http://www.baidu.com:80/search/info
Scheme:URL 的模式, 比如 http,file,content 等; 如果 URL 没有指定 scheme, 这个 URL 是无效的.
Host:URL 主机名, 比如 www.baidu.com http://xn--www-eo8er22f.baidu.com , 如果未指定, URL 无效.
Port:RUL 端口号, 比如 80, 只有 scheme 和 host 指定了 port 才有意义.
Path: 表示完整的路径信息. PathPattern: 表示完整路径信息, 里面可以包含通配符. PathPrefix: 表示路径的前缀信息
data 在定义的时候有两种情况需要注意
第一种情况
非完整写法, 及只指定了 mimeType 或者只指定了 URL.
注: 如果只指定了 URL, 系统会默认设置 mimeType 的值为 content 和 file
- <!-- 隐式调用, 只配置 URL -->
- <activity Android:name=".RegulationActivity">
- <intent-filter>
- <action Android:name="12345678" />
- <!-- 必须加上, 否则会报错 -->
- <category Android:name="android.intent.category.DEFAULT" />
- <data
- Android:host="www.baidu.com"
- Android:scheme="http" />
- </intent-filter>
- </activity>
- <!-- 隐式调用, 只配置 mimeType. -->
- <activity Android:name=".RegulationActivity">
- <intent-filter>
- <action Android:name="12345678" />
- <!-- 必须加上, 否则会报错 -->
- <category Android:name="android.intent.category.DEFAULT" />
- <data Android:mimeType="audio/mpeg" />
- </intent-filter>
- </activity>
java 代码:
- //intent 指定只配置了 URL 的 data
- intent.setAction("12345678");
- intent.setData(Uri.parse("http://www.baidu.com"));
- context.startActivity(intent);
- //intent 指定只配置了 mimeType 的 data
- intent.setAction("12345678");
- intent.setType("audio/mpeg");
- context.startActivity(intent);
第二种情况
完整写法
注: 如果 intent 指定的为完整的 data, 必须要使用 setDataAndType(), 因为 setData()和 setType()会彼此清楚对方的值.
- <!-- 隐式调用 -->
- <activity Android:name=".RegulationActivity">
- <intent-filter>
- <action Android:name="12345678" />
- <!-- 必须加上, 否则会报错 -->
- <category Android:name="android.intent.category.DEFAULT" />
- <data
- Android:mimeType="audio/mpeg"
- Android:host="www.baidu.com"
- Android:scheme="http" />
- </intent-filter>
- </activity>
java 代码:
- //intent 完整的 data
- intent.setAction("12345678");
- intent.setDataAndType(Uri.parse("http://www.baidu.com"), "audio/mpeg");
- context.startActivity(intent);
还有一种特殊写法:
- // 写法 1
- <intent-filter>
- <data
- Android:mimeType="audio/mpeg"
- Android:host="www.baidu.com"
- Android:scheme="http" />
- </intent-filter>
- // 写法 2
- <intent-filter>
- <data Android:mimeType="audio/mpeg" />
- <data Android:scheme="http" />
- <data Android:host="www.baidu.com" />
- </intent-filter>
上面的两个写法上在使用效果上是一样的.
7. 如何判断隐式启动是否成功
第一种, 使用 intent 的 resolveActivity
ComponentName componentName = intent.resolveActivity(context.getPackageManager()); if (componentName != null) context.startActivity(intent); else Toast.makeText(context, "匹配不成功", Toast.LENGTH_SHORT).show();
第二种, 使用 PackageManager 的 resolveActivity
PackageManager packageManager=context.getPackageManager(); ResolveInfo resolveInfo = packageManager.resolveActivity(intent,PackageManager.MATCH_DEFAULT_ONLY); // 判断是否匹配成功 if (resolveInfo != null) context.startActivity(intent); else Toast.makeText(context, "匹配不成功", Toast.LENGTH_SHORT).show();
上面的代码中返回 ResolveInfo 不是最佳的 Activity 信息, 而是所有匹配成功的 Activity 信息. resolveActivity()填写的第二个参数必须是 MATCH_DEFAULT_ONLY. 具体原因可以查看《Android 开发艺术探索》第一章 34 页, 这里就不解释了.
终结:
用了 3 天终于写完了! 其实写这篇文章是为了复习 Android 的基础知识, 在复习过程中增加了对 Activity 启动模式以及 Activity 任务栈中的状态的了解. 文章中四个启动模式中最麻烦的模式个人认为是 singTask, 最有意思的为 Taskffinerty 这个标签的使用, 自己可以写个 demo 或者使用我在 GitHub 上的项目测试.
参考
Android 开发艺术探索完结篇 -- 天道酬勤
Activity 启动模式与任务栈 (Task) 全面深入记录(下)
Activity 启动模式与任务栈 (Task) 全面深入记录(上)
来源: https://juejin.im/post/5c5d85da6fb9a049fd104d8f