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/

时间: 2024-10-05 12:27:48

Android 源码系列之<九>从源码的角度深入理解Activity的launchModel特性的相关文章

Android 源码系列之&lt;十三&gt;从源码的角度深入理解LeakCanary的内存泄露检测机制(中)

转载请注明出处:http://blog.csdn.net/llew2011/article/details/52958563 在上篇文章Android 源码系列之<十二>从源码的角度深入理解LeakCanary的内存泄露检测机制(上)中主要介绍了Java内存分配相关的知识以及在Android开发中可能遇见的各种内存泄露情况并给出了相对应的解决方案,如果你还没有看过上篇文章,建议点击这里阅读一下,这篇文章我将要向大家介绍如何在我们的应用中使用square开源的LeakCanary库来检测应用中出

Android 源码系列之&lt;十一&gt;从源码的角度深入理解AccessibilityService,打造自己的APP小外挂(下)

转载请注明出处:http://blog.csdn.net/llew2011/article/details/52843637 在上篇文章Android 源码系列之<十>从源码的角度深入理解AccessibilityService,打造自己的APP小外挂(上)中我们讲解了通过AccessibilityService实现自动安装APK小外挂的操作流程,如果你还没有看过上篇文章请点击这里.在这篇文章中我将带领小伙伴从源码的角度来深入学习一下AccessibilityServie的技术实现原理,希望这

Android 源码系列之&lt;十&gt;从源码的角度深入理解AccessibilityService,打造自己的APP小外挂(上)

转载请注明出处:http://blog.csdn.net/llew2011/article/details/52822148 说起外挂特别是玩游戏的小伙伴估计对它很熟悉,肯定有部分小伙伴使用过,至于为什么使用它,你懂得(*^__^*) --我最早接触外挂是在大二的时候,那时候盛行玩QQ农场,早上一睁眼就是打开电脑先把自己的菜收了,收完之后再去偷别人的:后来童靴说非凡软件上有一个偷菜外挂,于是赶紧整了一个,有了外挂之后就告别了体力时代,省时又省力--既然在PC上有外挂,那在智能手机上可以做外挂呢?

我的Android 4 学习系列之开始入手:配置开发环境与理解Hello World!

p { padding-left: 10px; } 目录 如何安装Android SDK.创建开发环境和调试项目 移动设计中一些注意事项 使用Android虚拟设备.模拟器和其他开发工具 如何安装Android SDK.创建开发环境和调试项目 下载和安装Android SDK : 我的是window7系统,当然下载 SDK starter package 最合适了: http://developer.android.com/sdk/index.html 下载完打开压缩包如下: 然后把这个包解压到

Android 源码系列之&lt;四&gt;从源码的角度深入理解LayoutInflater.Factory之主题切换(上)

转载请注明出处:http://blog.csdn.net/llew2011/article/details/51252401 现在越来越多的APP都加入了主题切换功能或者是日间模式和夜间模式功能切换等,这些功能不仅增加了用户体验也增强了用户好感,众所周知QQ和网易新闻的APP做的用户体验都非常好,它们也都有日间模式和夜间模式的主题切换功能.体验过它们的主题切换后你会发现大部分效果是更换相关背景图片.背景颜色.字体颜色等来完成的,网上这篇文章对主题切换讲解的比较不错,今天我们从源码的角度来学习一下

Android 源码系列之&lt;一&gt;从源码的角度深入理解ImageView的ScaleType属性

做Android开发的童靴们肯定对系统自带的控件使用的都非常熟悉,比如Button.TextView.ImageView等.如果你问我具体使用,我会给说:拿ImageView来说吧,首先创建一个新的项目,在项目布局文件中应用ImageView控件,代码如下: <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.

Android 源码系列之&lt;七&gt;从源码的角度深入理解IntentService及HandlerThread

转载请注明出处:http://blog.csdn.net/llew2011/article/details/51373243 提起Service大家都很熟悉,它乃Android四(si)大(da)组(jing)件(gang)之一.但是说起IntentService有童靴或许有点陌生,看名字感觉和Service有关连.不错,不仅有关联而且关系还不一般,IntentService是Service的子类,所以它也是正宗的Service,由于IntentService借助了HandlerThread,我

Android 源码系列之&lt;五&gt;从源码的角度深入理解LayoutInflater.Factory之主题切换(中)

转载请注明出处:http://blog.csdn.net/llew2011/article/details/51287391 在上篇文章中我们主要讲解了LayoutInflater渲染xml布局文件的流程,文中讲到如果在渲染之前为LayoutInflater设置了Factory,那么在渲染每一个View视图时都会调用Factory的onCreateView()方法,因此可以拿onCreateView()方法做切入口实现主题切换功能.如果你不清楚LayoutInflater的渲染流程,请点击这里.

Spark源码系列(九)Spark SQL初体验之解析过程详解

好久没更新博客了,之前学了一些R语言和机器学习的内容,做了一些笔记,之后也会放到博客上面来给大家共享.一个月前就打算更新Spark Sql的内容了,因为一些别的事情耽误了,今天就简单写点,Spark1.2马上就要出来了,不知道变动会不会很大,据说添加了很多的新功能呢,期待中... 首先声明一下这个版本的代码是1.1的,之前讲的都是1.0的. Spark支持两种模式,一种是在spark里面直接写sql,可以通过sql来查询对象,类似.net的LINQ一样,另外一种支持hive的HQL.不管是哪种方