2021-05-11

startActivity时可能遇到的问题


目录

activity任务栈及其调配

任务栈的作用

任务栈是什么

启动模式(launchMode)

亲属关系和新的任务(taskAffinity与allowTaskReparenting)

清理任务栈

启动任务栈

Android5.0之前LaunchMode与StartActivityForResult

通过Binder传递数据的限制

多进程应用可能会造成的问题

后台启动activity的限制


activity任务栈及其调配

以往基于应用(application)的程序开发中,程序具有明确的边界,一个程序就是一个应用,一个应用为了实现功能可以采用开辟新线程甚至新进程来辅助,但是应用与应用之间不能复用资源和功能

而Android引入了基于组件开发的软件架构,虽然我们开发android程序,仍然使用一个apk工程一个Application的开发形式,但是对于Aplication的开发就用到了Activity、service等四大组件,其中的每一个组件,都是可以被跨应用复用的,这就是android的神奇之处。

任务栈的作用

Android系统需要把不同应用的组件能无缝结合起来。

比如你的应用希望去发送一封邮件,你就可以定义一个具有”send”动作的Intent,并且传入一些数据,如对方邮箱地址、邮件内容等。这样,如果另外一个应用程序中的某个Activity声明自己是可以响应这种Intent的,那么这个Activity就会被打开。当邮件发送之后,按下返回键仍然还是会回到你的应用程序当中,这让用户看起来好像刚才那个编写邮件的Activity就是你的应用程序当中的一部分。

虽然组件可以跨应用被调用,但是一个组件所在的进程必须是在组件所在的Aplication进程中。由于android强化了组件概念,弱化了Aplication的概念,所以在android程序开发中,A应用的A组件想要使用拍照或录像的功能就可以不用去针对Camera类进行开发,直接调用系统自带的摄像头应用(称其B应用)中的组件(称其B组件)就可以了,

但是这就引发了一个新问题,A组件运行在A应用中,B组件运行在B应用中,自然都不在同一个进程中,那么从B组件中返回的时候,如何实现正确返回到A组件呢?Task就是来负责实现这个功能的,它是从用户角度来理解应用而建立的一个抽象概念。因为用户所能看到的组件就是Activity,所以Task可以理解为实现一个功能而负责管理所有用到的Activity实例的栈。

任务栈是什么

任务栈Task,是一种用来放置Activity实例的容器,他是以栈的形式进行盛放,也就是所谓的先进后出,主要有2个基本操作:压栈和出栈,其所存放的Activity是不支持重新排序的,只能根据压栈和出栈操作更改Activity的顺序。

启动一个Application的时候,系统会为它默认创建一个对应的Task,用来放置根Activity。默认启动Activity会放在同一个Task中,新启动的Activity会被压入启动它的那个Activity的栈中,并且显示它。当用户按下回退键时,这个Activity就会被弹出栈,按下Home键回到桌面,再启动另一个应用,这时候之前那个Task就被移到后台,成为后台任务栈,而刚启动的那个Task就被调到前台,成为前台任务栈,Android系统显示的就是前台任务栈中的Top实例Activity。

栈是一个先进后出的线性表,根据Activity在当前栈结构中的位置,来决定该Activity的状态。正常情况下,当一个Activity启动了另一个Activity的时候,新启动的Activity就会置于任务栈的顶端,并处于活动状态,而启动它的Activity虽然成功身退,但依然保留在任务栈中,处于停止状态,当用户按下返回键或者调用finish()方法时,系统会移除顶部Activity,让后面的Activity恢复活动状态。当然,世界不可能一直这么“和谐”,可以给Activity设置一些“特权”,来打破这种“和谐”的模式,这种特权,就是通过在AndroidManifest文件中的属性andorid:launchMode来设置或者通过Intent的flag来设置的,下面就先介绍下Activity的几种启动模式。

启动模式(launchMode)

一、基本描述:

1.standard:标准模式:如果在mainfest中不设置就默认standard;standard就是新建一个Activity就在栈中新建一个activity实例;
2.singleTop:栈顶复用模式:与standard相比栈顶复用可以有效减少activity重复创建对资源的消耗,但是这要根据具体情况而定,不能一概而论;
3.singleTask:栈内复用模式,栈内只有一个activity实例,栈内已存activity实例,在其他activity中start这个activity,Android直接把这个实例上面其他activity实例踢出栈GC掉;
4.singleInstance :堆内单例:整个手机操作系统里面只有一个实例存在就是内存单例;

在singleTop、singleTask、singleInstance 中如果在应用内存在Activity实例,并且再次发生startActivity(Intent intent)回到Activity后,由于并不是重新创建Activity而是复用栈中的实例,因此Activity再获取焦点后并没调用onCreate、onStart,而是直接调用了onNewIntent(Intent intent)函数;

二、Intent中标志位设置启动模式

在上文中的四种模式都是在mainfest的xml文件中进行配置的,GoogleAndroid团队同时提供另种级别更高的设置方式,即通过Intent.setFlags(int flags)设置启动模式;

1.FLAG_ACTIVITY_CLEAR_TOP : 等同于mainfest中配置的singleTask,没啥好讲的;
2.FLAG_ACTIVITY_SINGLE_TOP: 同样等同于mainfest中配置的singleTop;
3.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS: 其对应在AndroidManifest中的属性为android:excludeFromRecents=“true”,当用户按了“最近任务列表”时候,该Task不会出现在最近任务列表中,可达到隐藏应用的目的。
4.FLAG_ACTIVITY_NO_HISTORY: 对应在AndroidManifest中的属性为:android:noHistory=“true”,这个FLAG启动的Activity,一旦退出,它不会存在于栈中。
5.FLAG_ACTIVITY_NEW_TASK: 这个属性需要在被start的目标Activity在AndroidManifest.xml文件配置taskAffinity的值【必须和startActivity发其者Activity的包名不一样,如果是跳转另一个App的话可以taskAffinity可以省略】,则会在新标记的Affinity所存在的taskAffinity中压入这个Activity。

个人认为在上述Flag中FLAG_ACTIVITY_NEW_TASK是最为重要的一个flag,同时也需要注意的是网上有很多是瞎说的;而且也是个人唯一一个在实际开发中应用过的属性;先说说个人应用示例:
1.在Service中启动Activity;
2.App为系统Launcher时,跳转到微信无法退出时用到;
(四)、startActivity场景
Activity的启动模式的应用的设置是和它的开发场景有关系的,在App中打开新的Activity的基本上分为两种情况:

三、startActivity场景

目标Activity是本应用中的Activity,即它的启动模式是可以直接在fanifest中配置或者默认为standard,任务栈也可以自己随意设置;
目标Activity是第三方App中的Activity,这个时候就需要先考虑打开新Activity的是和自己App放在同一任务栈中还是新的task中【这个是很重要的因为在Android的机制中:同一个任务栈中的activity的生命周期是和这个task相关联的[具体实例见下文]】,然后考虑Activity的启动模式; 所以Android提供了优先级更高的设置方式在Intent.setFlags(int flags),通过这setFlags就可以为打开第三方的App中Activity设置任务栈和启动模式了,具体设置就自己去看源码了。

四、Activity四种启动模式常见使用场景:

LauchModeInstance
standard邮件、mainfest中没有配置就默认标准模式
singleTop登录页面、WXPayEntryActivity、WXEntryActivity 、推送通知栏、落地页防快速点击
singleTask程序模块逻辑入口:主页面(Fragment的containerActivity)、WebView页面、扫一扫页面、电商中:购物界面,确认订单界面,付款界面
singleInstance系统Launcher、锁屏键、来电显示等系统应用、路由过渡页

你最通常使用的模式是singleTop(除了默认为standard模式)。这不会对任务产生什么影响;仅仅是防止在栈顶多次启动同一个活动。

singleTask模式对任务有一些影响:它能使得活动总是在新的任务里被打开(或者将已经打开的任务切换到前台来)。使用这个模式需要加倍小心该进程是如何和系统其他部分交互的,它可能影响所有的活动。这个模式最好被用于应用程序入口活动的标记中。(支持MAIN活动和LAUNCHER分类)。

singleInstance启动模式更加特殊,该模式只能当整个应用只有一个活动时使用。

有一种情况你会经常遇到,其它实体(如搜索管理器SearchManager 或者 通知管理器NotificationManager)会启动你的活动。这种情况下,你需要使用 Intent.FLAG_ACTIVITY_NEW_TASK 标记,因为活动在任务(这个应用/任务还没有被启动)之外被启动。就像之前描述的一样, 这种情况下标准特性就是当前和任务和新的活动的亲和性匹配的任务将会切换到前台,然后在最顶端启动一个新的活动。当然,你也可以实现其它类型的特性。

一个常用的做法就是将Intent.FLAG_ACTIVITY_CLEAR_TOP NEW_TASK一起使用。这样做,如果你的任务已经处于运行中,任务将会被切换到前台来, 在栈里的所有的活动除了根活动,都将被清空,根活动的onNewIntent(Intent) 方法传入意图参数后被调用。当使用这种方法的时候 singleTop 或者 singleTask启动模式经常被使用,这样当前实例会被置入一个新的意图,而不是销毁原先的任务然后启动一个新的实例。

另外你可以使用的一个方法是设置活动的任务亲和力为空字串(表示没有亲和力),然后设置finishOnTaskLaunch属性。 如果你想让用户给你提供一个单独的活动描述的通知,倒不如返回到应用的任务里,这个比较管用。要指定这个属性,不管用户使用BACK还是HOME,活动都会结束;如果这个属性没有指定,按HOME键将会导致活动以及任务还留在系统里,并且没有办法返回到该任务里。

四种模式的演示其他许多文章有详细比较简单这里不多演示,需要注意的是singleTask和singleInstance,singleInstance会单独创建一个Task,而singleTask默认情况不会,要想singleTask也另外创建一个task需要配合属性taskAffinity使用,接下来我们看看taskAffinity属性

亲属关系和新的任务(taskAffinity与allowTaskReparenting)

taskAffinity

对于 Activity 的启动模式,每一个 Android 工程师都非常熟悉。通过设置不同的启动模式可以实现调配不同的 Task。但是 taskAffinity 在一定程度上也会影响任务栈的调配流程。

每一个 Activity 都有一个 Affinity 属性,如果不在清单文件中指定,默认为当前应用的包名。taskAffinity 主要有以下几点需要注意:

taskAffinity 会默认使 Activity 在新的栈中分配吗?

可以通过一个例子来验证一下,在一个 Android 项目 LagouTaskAffinity 中,创建两个 Activity:First 和 Second,它们的具体配置如下:

 

除了 Activity 类名之外,其他都是默认配置。这种情况下,点击 First 中的 Button,从 First 页面跳转到 Second 页面。

然后在命令行执行以下命令:

adb shell dumpsys activity activities

上述命令会将系统中所有存活中的 Activity 信息打印到控制台,具体结果如下:

上图中的 TaskRecord 代表一个任务栈,在这个栈中存在两个 Activity 实例:First 和 Second,并且 Second 处于栈顶。

接下来将 Second 的 taskAffinity 修改一下,如下所示:

我将 Second 的 taskAffinity 修改为 ”lagou.affinity“,使它和 First 的 taskAffinity 不同。重新运行代码,并再次查看任务栈中的情况,结果如下:

可以看出,虽然 First 和 Second 的 taskAffinity 不同,但是它们都被创建在一个任务栈中。

但如果我再将 Second 的 launchMode 改为 singleTask,再次重新运行则会发现两个 Activity 会被分配到不同的任务栈中,如下所示:

结论:单纯使用 taskAffinity 不能导致 Activity 被创建在新的任务栈中,需要配合 singleTask 或者 singleInstance!或者Intent对象包含FLAG_ACTIVITY_NEW_TASK标志

taskAffinity + allowTaskReparenting

allowTaskReparenting 赋予 Activity 在各个 Task 中间转移的特性。一个在后台任务栈中的 Activity A,当有其他任务进入前台,并且 taskAffinity 与 A 相同,则会自动将 A 添加到当前启动的任务栈中。这么说比较抽象,举一个生活中的场景:

  1. 在某外卖 App 中下好订单后,跳转到支付宝进行支付。当在支付宝中支付成功之后,页面停留在支付宝支付成功页面。
  2. 按 Home 键,在主页面重新打开支付宝,页面上显示的并不是支付宝主页面,而是之前的支付成功页面。
  3. 再次进入外卖 App,可以发现支付宝成功页面已经消失。(支付成功页面回到宿主)

造成上面现象的原因就是 allowTaskReparenting 属性,还是通过代码案例来演示。

分别创建 2 个 Android 工程:First 和 TaskAffinityReparent:

  • 在 First 中有 3 个 Activity:FirstA、FirstB、FirstC。打开顺序依次是 FirstA -> FirstB -> FirstC。其中 FirstC 的 taskAffinity 为”lagou.affinity“,且 allowTaskReparenting 属性设置为true。FirstA 和 FirstB 为默认值;
  • TaskAffinityReparent 中只有一个 Activity--ReparentActivity,并且其 TaskAffinity 也等于”lagou.affinity“。

将这两个项目分别安装到手机上之后,打开 First App,并从 FirstA 开始跳转到 FirstB,再进入 FirstC 页面。然后按 Home 键,使其进入后台任务。此时系统中的 Activity 信息如下:

接下来,打开 TaskAffinityReparent 项目,屏幕上本应显示 ReparentActivity 的页面内容,但是实际上显示的却是 FirstC 中的页面内容,并且系统中 Activity 信息如下:

 

可以看出,FirstC 被移动到与 ReparentActivity 处在一个任务栈中。此时 FirstC 位于栈顶位置,再次点击返回键,才会显示 ReparentActivity 页面。

清理任务栈

    如果一个任务栈在很长的一段时间都被用户保持在后台的,那么系统就会将这个任务栈中除了根activity以外的其它所有activity全部清除掉。从这之后,当用户再将任务栈切换到前台,则只能显示根activity了。
以上说的是默认模式,可以通过<activity>标签的一些属性来更改:
    1)alwaysRetainTaskState属性
    如果将根activity的alwaysRetainTaskState属性设置为“true”,则即便一个任务栈在很长的一段时间都被用户保持在后台的,系统也不会对这个任务栈进行清理。
    2)clearTaskOnLaunch属性
    如果将根activity的clearTaskOnLaunch属性设置为“true”,那么只有这个任务栈切换到了后台,那么系统就会将这个任务栈中除了根activity以外的其它所有activity全部清除掉。即和alwaysRetainTaskState的行为完全相反。
    3) finishOnTaskLaunch属性
    这个属性的行为类似于clearTaskOnLaunch,但是此属性作用于单个的activity对象,而不是整个任务栈。当这个任务栈切换到了后台,这个属性可以使任务栈清理包括根activity在内的任何activity对象。

    这里也有另一种方法来使activity对象从任务栈中被移除。若Intent对象包含FLAG_ACTIVITY_CLEAR_TOP标志,并且在目标任务栈中已经存在了 用于处理这个Intent对象的activity类型的一个实例,那么在任务栈中这个实例之上的所有activity实例会被移除。 从而用于处理这个Intent对象的activity类型的那个实例会位于任务栈的栈顶,并用来处理那个Intent对象。若那个匹合的activity类型的启动模式是“standard”,则这个已经存在于任务栈中的匹合的activity类型的实例也会被移除,并且一个新的此类型activity的实例被创建并压栈来处理这个Intent对象。

    FLAG_ACTIVITY_CLEAR_TOP这个标志经常和FLAG_ACTIVITY_NEW_TASK标志结合使用,这样结合使用的意思是在另一个任务栈中定位已经存在的匹合的activity类型的实例,并且让此实例位于栈顶。


启动任务栈

    通过将一个activity类型的intent-filter的动作设置为“android.intent.action.MAIN”,类别设置为“android.intent.category.LAUNCHER”可以使这个activity实例称为一个任务栈的入口。拥有这种类型的intent-filter的activity类型的图表和名字也会显示在application launcher中。

    第二个能力是很重要的:用户必须能够使一个任务栈切换到后台,也可以随时将其切换到前台。出于这个原因,使activity在启动时新开任务栈的启动模式(即“singleTask”和“singleInstance”模式)只应该被利用在拥有拥有“android.intent.action.MAIN”动作和“android.intent.category.LAUNCHER”类别的intent-filter的activity类型上。
    类似的限制同样体现在FLAG_ACTIVITY_NEW_TASK标志上。如果这个标志使一个activity开始了一个新的任务栈,并且用户点击“HOME”键将其切换到了后台,则必须有某种方式使用户可以重新将那个任务栈切换到前台。一些实例(比如通知管理器),总是在外部的任务栈中开启一个activity,而不是其自身的任务栈,所以它们总是将FLAG_ACTIVITY_NEW_TASK标志放入Intent对象中,并将Intent对象传入startActivity()方法中。
    对于在某些情况下,你不希望用户能够返回到某一个activity,那么可以通过设置<activity>标签的“finishOnTaskLaunch”属性为“true”来实现。

Android5.0之前LaunchMode与StartActivityForResult

我们在开发过程中经常会用到StartActivityForResult方法启动一个Activity,然后在onActivityResult()方法中可以接收到上个页面的回传值,但你有可能遇到过拿不到返回值的情况,那有可能是因为Activity的LaunchMode设置为了singleTask。5.0之后,android的LaunchMode与StartActivityForResult的关系发生了一些改变。两个Activity,A和B,现在由A页面跳转到B页面,看一下LaunchMode与StartActivityForResult之间的关系:


这是为什么呢?

这是因为ActivityStackSupervisor类中的startActivityUncheckedLocked方法在5.0中进行了修改。在5.0之前,当启动一个Activity时,系统将首先检查Activity的launchMode,如果为A页面设置为SingleInstance或者B页面设置为singleTask或者singleInstance,则会在LaunchFlags中加入FLAG_ACTIVITY_NEW_TASK标志,而如果含有FLAG_ACTIVITY_NEW_TASK标志的话,onActivityResult将会立即接收到一个cancle的信息,而5.0之后这个方法做了修改,修改之后即便启动的页面设置launchMode为singleTask或singleInstance,onActivityResult依旧可以正常工作,也就是说无论设置哪种启动方式,StartActivityForResult和onActivityResult()这一组合都是有效的。所以如果你目前正好基于5.0做相关开发,不要忘了向下兼容,这里有个坑请注意避让。

关于Task,Activity栈的参考文章如下:

Activity的四种启动模式应用场景

彻底弄懂Activity的启动模式和任务栈

activity之栈管理

通过Binder传递数据的限制

Binder 传递数据限制

Activity 界面跳转时,使用 Intent 传递数据是最常用的操作了。但是 Intent 传值偶尔也会导程序崩溃,比如以下代码:

在 startFirstB 方法中,跳转 FirstB 页面,并通过 Intent 传递 Bean 类中的数据。但是执行上述代码会报如下错误:

上面 log 日志的意思是 Intent 传递数据过大,最终原因是 Android 系统对使用 Binder 传数据进行了限制。通常情况为 1M,但是根据不同版本、不同厂商,这个值会有区别。

解决办法:

  1. 减少通过 Intent 传递的数据,将非必须字段使用 transient 关键字修饰。

比如上述 Bean 类中,假如 byte[] data 并非必须使用的数据,则需要避免将其序列化,如下所示:

添加 transient 修饰之后,再次运行代码则不会再报异常。

  1. 将对象转化为 JSON 字符串,减少数据体积。

因为 JVM 加载类通常会伴随额外的空间来保存类相关信息,将类中数据转化为 JSON 字符串可以减少数据大小。比如使用 Gson.toJson 方法。

大多时候,将类转化为 JSON 字符串之后,还是会超出 Binder 限制,说明实际需要传递的数据是很大的。这种情况则需要考虑使用本地持久化来实现数据共享,或者使用 EventBus 来实现数据传递。

关于 Binder 机制的原理分析。可以参考网上以下两篇文章:

多进程应用可能会造成的问题

process 造成多个 Application

一直以来,我们经常会在自定义的 Application 中做一些初始化的操作。比如 App 分包、推送初始化、图片加载库的全局配置等,如下所示:

但实际上,Activity 可以在不同的进程中启动,而每一个不同的进程都会创建出一个 Application,因此有可能造成 Application 的 onCreate 方法被执行多次。比如以下代码:

 

RemoteActivity 的 process 为“lagou.process”,这将导致它会在一个新的进程中创建。当在 MainActivity 中跳转到 RemoteActivity 时,LagouApplication 会被再次创建,其代码如下:

 

最终打印日志如下:

 

可以看出 LagouApplication 的 onCreate 方法被创建了 2 次,因此各种初始化的操作也会被执行 2 遍。

针对这个问题,目前有两种比较好的处理方式:

  • onCreate 方法中判断进程的名称,只有在符合要求的进程里,才执行初始化操作;
  • 抽象出一个与 Application 生命周期同步的类,并根据不同的进程创建相应的 Application 实例。

更多详细介绍可以参考这篇文章:解决 Android 多进程导致 Application 重复创建问题

后台启动activity的限制

后台启动 Activity 失效

试想一下,如果我们正在玩着游戏,此时手机后台可能有个下载某 App 的任务在执行。当 App 下载完之后突然弹出安装界面,中断了游戏界面的交互,这种情况会造成用户体验极差,而最终用户的吐槽对象都会转移到 Android 手机或者 Android 系统本身。

为了避免这种情况的发生,从 Android10(API 29)开始,Android 系统对后台进程启动 Activity 做了一定的限制。官网对其介绍如下:

 

主要目的就是尽可能的避免当前前台用户的交互被打断,保证当前屏幕上展示的内容不受影响。

但是这也造成了很多实际问题,在我们项目中有 Force Update 功能,当用户选择升级之后会在后台进行新的安装包下载任务。正常情况下下载成功需要弹出 apk 安装界面,但是在某一版升级时突然很多用户反馈无法弹出下载界面。经过查看抓取的 log 信息,最终发现有个特点就是都发生在 Android 10 版本,因此怀疑应该是版本兼容问题,最终谷歌搜索,发现果然如此。

解决办法:
Android 官方建议我们使用通知来替代直接启动 Activity 操作:

 

也就是当后台执行的任务执行完毕之后,并不会直接调用 startActivity 来启动新的界面,而是通过 NotificationManager 来发送 Notification 到状态栏。这样既不会影响当前使用的交互操作,用户也能及时获取后台任务的进展情况,后续的操作由用户自己决定。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值