首页 > 代码库 > Android 源码系列之<九>从源码的角度深入理解Activity的launchModel特性
Android 源码系列之<九>从源码的角度深入理解Activity的launchModel特性
转载请注明出处:http://blog.csdn.net/llew2011/article/details/52509515
随着公司新业务的起步由于原有APP_A的包已经很大了,所以上边要求另外开发一款APP_B,要求是APP_A和APP_B账号通用且两个APP可以相互打开。账号通用也就是说在APP_A上登录了那么打开APP_B也就默认是登录状态,这个实现也不复杂就不介绍了;APP相互打开本来也不是难事,但是在测试的过程中发现了一个之前没有遇到的问题,现象如下图的demo所示:
运行现象是在APP_A中打开了APP_B后,这时候在APP_B中进行任何操作都是没问题的,在APP_B不退出的情况下若摁了HOME键切换到桌面后此时再点击APP_A的icon图标打开APP_A时,发现界面竟然是APP_B的界面,当时感觉很诡异,是什么原因导致出现这种现象呢?当时就琢磨着可能是APP_B运行在了APP_A的任务栈中了,于是开始排查代码,在APP_B中响应APP_A的代码如下所示:
<activity android:name="com.llew.wb.A" android:label="@string/app_name" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> <intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <data android:scheme="llew" /> </intent-filter> </activity>由于我们APP_A和APP_B约定了相互打开采用scheme的形式,所以响应代码看起来是没有问题的,接着查看在APP_A中打开APP_B的代码,如下所示:
public void openAPP_B1() { Uri uri = Uri.parse("llew://"); Intent intent = new Intent(Intent.ACTION_VIEW, uri); startActivity(intent); }这段就是打开我们APP_B的代码,看上去也没有什么问题,但是为什么会出现上述现象呢?然后我就尝试在openAPP_B中采用另外的方式,代码如下:
public void openAPP_B2() { Intent intent = getPackageManager().getLaunchIntentForPackage("packageName"); if(null != intent) { startActivity(intent); } }
方式二以前使用过并看过这块相关源码,所以首先就想到了通过PackageManager来获取Intent来启动我们的APP_B,运行程序后发现第二种方式是没问题的,那也就是说在第二种中采用PackageManager获取到的Intent肯定是和采用第一种方式获取到的Intent是有区别的,那他们的区别在哪呢?先不说结论我们接着往下看,运行程序通过debug模式分别查看这两种方式获取到的Intent的不同之处:
方式一的intent截图如下所示:
方式二的intent截图如下所示:
通过对比这两种方式的Intent对象可以发现方式二中的intent对象包含了flg属性,而该flg属性的值恰好是Intent.FLAG_ACTIVITY_NEW_TASK的值,这时候豁然开朗了,原来方式二中的Intent添加了FLAG_ACTIVITY_NEW_TASK标记,也就是说采用方式一开打APP_B时的页面是运行在APP_A的任务栈中,而通过方式二打开APP_B的页面运行在了新的任务栈中。为了证明通过方式一打开的APP_B的页面是运行在APP_A的任务栈中,我们可以使用adb shell dumpsys activity activities 命令来查看Activity任务栈的情况,截图如下:
然后我们在方式一中的Intent也添加FLAG_ACTIVITY_NEW_TASK标记在运行一下,使用adb shell dumpsys activity activities 命令查看一下,截图如下:
出现以上问题的原因就是APP_B运行在了APP_A的任务栈中,解决方法也就是在启动APP_B的时候让APP_B运行在新的任务栈中,接下来顺带进入源码看一看通过PackageManager获取到的Intent对象在哪赋值的flag标记吧,在Activity中调用getPackageManager()辗转调用的是其间接父类ContextWrapper的getPackageManager()的方法,源码如下所示:
@Override public PackageManager getPackageManager() { // mBase为Context类型,其实现类为ContextImpl return mBase.getPackageManager(); }ContextWrapper的getPackageManager()方法中调用的是Context的getPackageManager()同名方法,而mBase的实现类为ContextImpl,所以我们直接查看ContextImpl的getPackageManager()方法,源码如下:
@Override public PackageManager getPackageManager() { // 如果mPackageManager非空就直接返回 if (mPackageManager != null) { return mPackageManager; } // 通过ActivityThread获取IPackageManager对象pm IPackageManager pm = ActivityThread.getPackageManager(); if (pm != null) { // 新建ApplicationPackageManager对象并返回 // Doesn't matter if we make more than one instance. return (mPackageManager = new ApplicationPackageManager(this, pm)); } return null; }通过源码我们知道getPackageManger()方法获取的是ApplicationPackageManager对象,获取Intent对象就是调用该对象的getLaunchIntentForPackage()方法,源码如下:
@Override public Intent getLaunchIntentForPackage(String packageName) { // First see if the package has an INFO activity; the existence of // such an activity is implied to be the desired front-door for the // overall package (such as if it has multiple launcher entries). Intent intentToResolve = new Intent(Intent.ACTION_MAIN); intentToResolve.addCategory(Intent.CATEGORY_INFO); intentToResolve.setPackage(packageName); List<ResolveInfo> ris = queryIntentActivities(intentToResolve, 0); // Otherwise, try to find a main launcher activity. if (ris == null || ris.size() <= 0) { // reuse the intent instance intentToResolve.removeCategory(Intent.CATEGORY_INFO); intentToResolve.addCategory(Intent.CATEGORY_LAUNCHER); intentToResolve.setPackage(packageName); ris = queryIntentActivities(intentToResolve, 0); } if (ris == null || ris.size() <= 0) { return null; } // 运行到这里是查找到了符合条件的Intent了,新建Intent Intent intent = new Intent(intentToResolve); // 在这里给Intent添加了我们期待的FLAG_ACTIVITY_NEW_TASK标签 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.setClassName(ris.get(0).activityInfo.packageName, ris.get(0).activityInfo.name); // 返回新建的Intent对象 return intent; }
通过源码我们看到在ApplicationPackageManager的getLaunchIntentForPackage()方法中给符合条件的Intent添加了FLAG_ACTIVITY_NEW_TASK标签,而该标签的作用就是为目标Activity开启新的任务栈并把目标Activity放到栈底。
开始讲解Activity的launchMode之前我们先提一下任务和返回栈的概念,以下部分内容参考自官方文档。
- 任务和返回栈
应用通常包含多个Activity。每个 Activity 均应围绕用户可以执行的特定操作设计,并且能够启动其他 Activity。 例如,电子邮件应用可能有一个 Activity 显示新邮件的列表。用户选择某邮件时,会打开一个新 Activity 以查看该邮件。
一个 Activity 甚至可以启动设备上其他应用中存在的 Activity。例如,如果应用想要发送电子邮件,则可将 Intent 定义为执行“发送”操作并加入一些数据,如电子邮件地址和电子邮件。 然后,系统将打开其他应用中声明自己处理此类 Intent 的 Activity。在这种情况下, Intent 是要发送电子邮件,因此将启动电子邮件应用的“撰写”Activity(如果多个 Activity 支持相同 Intent,则系统会让用户选择要使用的 Activity)。发送电子邮件时,Activity 将恢复,看起来好像电子邮件 Activity 是您的应用的一部分。 即使这两个 Activity 可能来自不同的应用,但是 Android 仍会将 Activity 保留在相同的任务中,以维护这种无缝的用户体验。
任务是指在执行特定作业时与用户交互的一系列 Activity。 这些 Activity 按照各自的打开顺序排列在堆栈(即“返回栈”)中。
设备主屏幕是大多数任务的起点。当用户触摸应用启动器中的图标(或主屏幕上的快捷键)时,该应用的任务将出现在前台。 如果应用不存在任务(应用最近未曾使用),则会创建一个新任务,并且该应用的“主”Activity 将作为堆栈中的根 Activity 打开。
当前 Activity 启动另一个 Activity 时,该新 Activity 会被推送到堆栈顶部,成为焦点所在。 前一个 Activity 仍保留在堆栈中,但是处于停止状态。Activity 停止时,系统会保持其用户界面的当前状态。 用户按“返回”按钮时,当前 Activity 会从堆栈顶部弹出(Activity 被销毁),而前一个 Activity 恢复执行(恢复其 UI 的前一状态)。 堆栈中的 Activity 永远不会重新排列,仅推入和弹出堆栈:由当前 Activity 启动时推入堆栈;用户使用“返回”按钮退出时弹出堆栈。 因此,返回栈以“后进先出”对象结构运行。 图 1 通过时间线显示 Activity 之间的进度以及每个时间点的当前返回栈,直观呈现了这种行为。
如果用户继续按“返回”,堆栈中的相应 Activity 就会弹出,以显示前一个 Activity,直到用户返回主屏幕为止(或者,返回任务开始时正在运行的任意 Activity)。 当所有 Activity 均从堆栈中删除后,任务即不复存在。
由于返回栈中的 Activity 永远不会重新排列,因此如果应用允许用户从多个 Activity 中启动特定 Activity,则会创建该 Activity 的新实例并推入堆栈中(而不是将 Activity 的任一先前实例置于顶部)。 因此,应用中的一个 Activity 可能会多次实例化(即使 Activity 来自不同的任务)。
【注意:】后台可以同时运行多个任务。但是,如果用户同时运行多个后台任务,则系统可能会开始销毁后台 Activity,以回收内存资源,从而导致 Activity 状态丢失。
好了,用了不小篇幅介绍了任务和返回栈的概念,若要改变返回栈的默认行为,可通过Activity的launchMode以及Intent的Flag标签,我们今天主要讲解的是通过launchMode来改变任务栈的默认行为,Android系统为launchMode提供了四种机制,分别是standard,singleTop,singleTask,singleInstance,为了方便查看任务栈的相关信息,这里给大家说一个命令:adb shell dumpsys activity,如果有对该命令不熟悉的,请自行查阅并掌握。下面我们来逐一讲解launchMode的各个属性值。
- standard
该属性是Activity默认情况下的启动模式,也就是说我们如果没有在manifest.xml中声明Activity的launchMode属性,系统会默认为Activity配置成standard,每次启动该Activity时系统都会在当前的任务栈中新建一个该Activity的实例并加入任务栈中。
【例如:A和B都是standard】打开顺序为:A→B→B→B,则任务栈中的顺序如下所示: - singleTop
1、如果Activity的launchMode属性定义成了singleTop,若在当前任务栈中已经存在该Activity的实例并且在栈顶位置,那再次打开该Activity都不会新建该Activity的实例,此时会回调该Activity的onNewIntent()方法。【注意:】如果Activity的launchMode属性为singleTop,则taskAffinity属性无效。
【例如:B为singleTop,其它为默认】打开顺序为:A→B→B→B,则任务栈中的顺序如下所示:
2、如果Activity的launchMode属性定义成了singleTop,若在当前任务栈中已经存在该Activity的实例且不在栈顶,那此时会继续新建该Activity的实例
【例如:B为singleTop,其它为默认】打开顺序为:A→B→C→B→C→B→C→B - singleTask
1、如果Activity的launchMode属性定义成了singleTask,如果此时没有声明taskAffinity属性(当不声明taskAffinity属性,那么Activity就会以包名作为其默认值)
【例如:B为singleTask,其它为默认】打开顺序为:A→B→C→D→B
2、如果Activity的launchMode属性定义成了singleTask,如果此时声明了taskAffinity属性且该属性不同于包名,则
【例如:B为singleTask,其它为默认】打开顺序为:A→B
【例如:B为singleTask,其它为默认】打开顺序为:A→B→C
【例如:B为singleTask,其它为默认】打开顺序为:A→B→C→D
【例如:B为singleTask,其它为默认】打开顺序为:A→B→C→D→B
根据运行结果我们发现,当Activity的launchMode设置成singleTask,singTask保证了当前任务栈中只有一个该Activity的实例,若该Activity不在栈顶,则会清除该Activity之上的所有的Activity并回调该Activity的onNewIntent()方法,singleTask的使用小结如下所示:if( 发现一个 Task 的 affinity == Activity 的 affinity ){ if(此 Activity 的实例已经在这个 Task 中){ 这个 Activity 启动并且清除顶部的 Acitivity ,通过标识 CLEAR_TOP } else { 在这个 Task 中新建这个 Activity 实例 } } else { // Task 的 affinity 属性值与 Activity 不一样 新建一个 affinity 属性值与之相等的 Task 新建一个 Activity 的实例并且将其放入这个 Task 之中 }
- singleInstance
1、singleInstance稍微比singleTask好理解,singleInstance的Activity只能在一个新的Task中并且这个Task中有且只能有这一个Activity,举个栗子
【例如:B为singleInstance,其它为默认】打开顺序为:A→B
【例如:B为singleInstance,其它为默认】打开顺序为:A→B→C→D→C
根据以上结果我们已经大致掌握了launchMode的各种特性了,为了深刻理解需要小伙伴们自己动手实验尝试各种情况下的Activity的打开方式。本篇文章到此就结束了,感谢观看(*^__^*) ……
【参考文章:】
1、https://developer.android.com/guide/components/tasks-and-back-stack.htm
2、http://www.songzhw.com/2016/08/09/explain-activity-launch-mode-with-examples/
Android 源码系列之<九>从源码的角度深入理解Activity的launchModel特性