第二十四章 深入学习intent和任务
本章将使用隐式intent创建一个替换Android默认启动器的应用。新建应用名为NerdLauncher。NerdLauncher应用能列出设备上的其他应用。点选任意列表项会启动相应应用。完成该应用能帮你深入理解intent、intent过滤器,搞清楚Android应用间是如何交互的。
一、创建 NerdLauncher 项目
二、解析隐式intent
NerdLaucher应用会列出设备上的可启动应用。(可启动应用是指点击主屏幕或启动器界面上的图标就能打开的应用)。要实现该功能,它会使用PackageManager获取所有可启动主activity。可启动主 activity 都带有包含 MAIN 操作和 LAUNCHER 类别的 intent 过滤器。
private void setupAdapter() {
Intent startupIntent = new Intent(Intent.ACTION_MAIN);
startupIntent.addCategory(Intent.CATEGORY_LAUNCHER);
PackageManager pm = getActivity().getPackageManager();
List<ResolveInfo> activities = pm.queryIntentActivities(startupIntent, 0);
Log.i(TAG, "Found " + activities.size() + " activities.");
}
调用上面的 setupAdapter() 方法,可以看到 PackageManager 返回多少个Activity。
注意:MAIN/LAUNCHER intent 过滤器可能无法与通过 startActivity(...) 方法发送的 MAIN/LAUNCHER 隐式intent相匹配。事实上,startActivity(Intent) 方法意味着“启动匹配隐式 intent 的默认 activity”,而不是想当然的“启动匹配隐式 intent 的 activity”。调用startActivity(Intent) 方法(或 startActivityForResult(...) 方法)发送隐式 intent 时,操作系统会悄悄为目标 intent 添加 Intent.CATEGORY_DEFAULT 类别。
因此,如果希望intent过滤器匹配startActivity(...)方法发送的隐式intent,就必须在对应的intent过滤器中包含DEFAULT类别。
定义了 MAIN/LAUNCHER intent 过滤器的activity是应用的主要入口点。它只负责做好作为应用主要入口点要处理的工作。它通常不关心自己是否为默认的主要入口点,所以可以不包含CATEGORY_DEFAULT类别。
前面说过,MAIN/LAUNCHER intent过滤器并不一定包含CATEGORY_DEFAULT类别,因此不能保证可以与startActivity(...)方法发送的隐式intent匹配。所以,我们转而使用intent直接向PackageManager查询带有MAIN/LAUNCHER intent过滤器的activity。
在PackageManager返回的ResolveInfo对象中,可以获取activity标签和其他一些元数据。
使用 ResolveInfo.loadLabel(PackageManager) 方法,对 ResolveInfo 对象中的 activity 标签按首字母排序:
/**
* activities:获取到的activity的List
*/
Collections.sort(activities, new Comparator<ResolveInfo>() {
@Override
public int compare(ResolveInfo o1, ResolveInfo o2) {
PackageManager pm = getActivity().getPackageManager();
return String.CASE_INSENSITIVE_ORDER.compare(
o1.loadLabel(pm).toString(),
o2.loadLabel(pm).toString()
);
}
});
三、在运行时创建显示 Intent
要创建启动 activity 的显式 intent,需要从 ResolveInfo 对象中获取 activity 的包名与类名。这些信息可以从 ResolveInfo 对象的 ActivityInfo 中获取。
ActivityInfo activityInfo = mResolveInfo.activityInfo;
Intent i = new Intent(Intent.ACTION_MAIN)
.setClassName(activityInfo.applicationInfo.packageName,
activityInfo.name);
startActivity(i);
四、任务与回退栈
任务是一个activity栈。栈底部的activity通常称为基activity。栈顶的activity用户能看得到。如果按后退键,栈顶activity会弹出栈外。如果用户看到的是基activity,按后退键,系统就会回到主屏幕。
默认情况下,新activity都在当前任务中启动。在当前任务中启动activity的好处是,用户可以在任务内而不是在应用层级间导航返回。
1、在任务间切换
比如,用户先打开微信,之后按Home键返回主屏幕,再打开淘宝,然后长按Home键(每个手机操作不一样,不一定都是长按Home键),会出现微信和淘宝的类似缩略图一样的卡片,点击微信就会回到之前打开的微信页面。
手机上每个应用显示项(就是Lollipop系统所说的卡片)就代表着一个应用任务。当前任务显示的是处于回退栈顶部activity的快照。你可以点击任意显示项切换至对应应用的当前activity。要清除应用任务,用户只需滑动移除卡片即可。清除任务会从应用回退栈中清除所有activity。
2、启动新任务
有时你需要在当前任务中启动activity,而有时又需要在新任务中启动activity。当前,从 NerdLauncher 项目启动的任何activity都会添加到 NerdLauncher 任务中。
我们需要 NerdLauncher 在新任务中启动activity,如下图。这样,点击 NerdLauncher 启动器中的应用项可以让应用拥有自己的任务,用户就可以在运行的应用间自由切换了。
为了在启动新activity时启动新任务,需要为intent添加一个标志:
这样就会看到 CriminalIntent 应用处于一个单独的任务中。
如果从 NerdLauncher 应用中再次启动 CriminalIntent 应用,也不会创建第二个 CriminalIntent 任务。FLAG_ACTIVITY_NEW_TASK 标志控制每个 activity 仅创建一个任务。已经有了一个运行的任务之后,Android 会自动切换到原来的任务,而不是创建新的任务。
五、使用 NerdLauncher 应用作为设备主屏幕
打开 NerdLauncher 项目的 AndroidManifest. xml,向 intent 主过滤器添加以下节点定义:
<category android:name="android.intent.category.HOME" />
<category android:name="android.intent.category.DEFAULT" />
添加 HOME 和 DEFAULT 类别定义后,NerdLauncher 应用的 activity 会成为可选的主界面。按主屏幕键可以看到,在弹出的对话框中,NerdLauncher 变成了主界面可选项。
六、深入学习进程与任务
对象需要内存和虚拟机的支持才能生存。进程是操作系统创建的、供应用对象生存以及应用运行的地方。
进程通常会占用由操作系统管理着的系统资源,如内存、网络端口以及打开的文件等。进程还拥有至少一个(可能多个)执行线程。在Android系统中,每个进程都需要一个虚拟机来运行。
尽管存在未知的异常情况,但总的来说,Android世界里的每个应用组件都仅与一个进程相关联。应用伴随着自己的进程一起完成创建,该进程同时也是应用中所有组件的默认进程。
每一个activity实例都仅存在于一个进程之中,同一个任务关联。这也是进程与任务的唯一相似之处。任务只包含 activity,这些 activity 通常来自于不同的应用进程;而进程则包含了应用的全部运行代码和对象。
进程与任务很容易让人混淆,主要原因在于它们不仅在概念上有某种重叠,而且通常会被人以应用名提及。例如,从NerdLauncher 启动器中启动 CriminalIntent 应用时,操作系统创建了一个 CriminalIntent 进程以及一个以 CrimeListActivity 为基栈 activity 的新任务。在 overview screen(前面提到的长按 Home 键出现的应用卡片) 中,可以看到这个任务就被标名为CriminalIntent。
包含 activity 的任务和它赖以生存的进程有可能会不同。例如:
打开 CriminalIntent 应用,选择任意 crime 项(或添加一条crime记录),然后点击 CHOOSE SUSPECT 按钮。这会打开联系人应用让你选择目标联系人。随即,联系人列表 activity 会被加入 CriminalIntent 应用任务中。如果此时按后退键在不同 activity 间切换的话,用户可能意识不到他们正在进程间切换。
然而,联系人 activity 实例实际是在联系人应用进程的内存空间创建的,而且也是在该应用进程里的虚拟机上运行的,下图:
为进一步了解进程和任务的概念,让 CriminalIntent 应用运行着,进入联系人列表界面。(继续之前,请确保在overview screen里看不到联系人应用。)按主屏幕键回到主屏幕,从中启动联系人应用。然后从联系人列表选取任意联系人,或添加新联系人。
在这个操作过程中,系统会在联系人应用进程中创建新的联系人列表 activity 和联系人明细界面实例。也会创建联系人应用新任务。这个新任务会引用联系人列表和联系人明细界面activity实例,下图:
总结来说,就是:一个任务中的 activity 可能不都是同一个进程,一个进程中的 Activity 实例可能不都是同一个任务创建的。
七、并发文档
有了并发文档,就可以为运行的应用动态创建任意数目的任务。在 Lollipop(5.0) 之前,应用任务只能预先定义好,而且还要在manifest文件中指明。
如果需要应用启动多个任务,比如打开编辑多份文档,可采用两种方式:给 intent 打上 Intent. FLAG_ACTIVITY_NEW_DOCUMENT 标签,再调用startActivity(...) 方法;或者在 manifest 文件中,为 activity 设置如下 documentLaunchMode:
android:documentLaunchMode="intoExisting"
使用上述方法,一份文档只会对应一个任务。(如果发送带有和已存在任务相同数据的 intent,系统就不会再创建新任务。)如果无论如何都想创建新任务,那就给 intent 同时打上 Intent.FLAG_ACTIVITY_NEW_DOCUMENT 和Intent.FLAG_ACTIVITY_MULTIPLE_TASK 标签,或者把 manifest 文件中的 documentLaunchMode 属性值改为 always。