概述
对于安卓工程师来说,startActivity就如同初恋一般,熟悉又陌生。在写startActivity代码时,有一些比较重要的问题,同时也是可能的面试点。
骚气的TaskAffinity
回顾一下,activity的四种启动模式,一个标准模式(standard ),3个复用模式(SingleTop,SingleTask,SingleIntance),都是在不同的场景下尽可能减少activity的创建,以及尽可能回收不再需要的activity,以保证内存的最大利用率。
而在 启动模式之外,还有一个重要参数,也能影响到activity栈结构。每一个Activity都有一个affinity属性。如果不在 manifest中静态注册activity时指定 affinity,那么就默认是当前 packageName.
单词 Affinity,本意是:亲密感、吸引力或相似性。它可以描述人之间或事物之间的紧密联系或吸引力。例如,你可能会说你有某人的亲密感或有一种对某个活动的亲近感。
在安卓中,taskAffinity 可以理解为 一个 改变任务栈之间关联关系
的属性。
理论上来说,只要我们改变了一个Activity的taskAffinity属性,那么它的任务栈就有可能发生变化
实验阶段一 原始状态
创建 MainActivity 和 MainActivity2 ,这两个Activity,做一个简单的startActivity跳转,从 MainActivity 到 MainActivity2,没有其他任何特别设定。
完成跳转之后,使用命令 adb shell dumpsys activity activities
可以看到,这两个Activity处于同一个任务栈中。
实验阶段二 仅将MainActivity2的taskAffinity改变为my.affinity
xml
<activity
android:name=".MainActivity2"
android:theme="@style/Theme.AppCompat"
android:taskAffinity="my.affinity"
/>
重复从MainActivity跳转到MainActivity2的操作,完成跳转之后,使用命令 adb shell dumpsys activity activities
注意黄色标识,两个Activity的taskAffinity虽然已经不同,但是MainActivity2仍然和MainActivity在同一个任务栈之下。 MainActivity的taskAffinity是默认的包名,MainActivity2的taskAffinity现在变成了 my.affinity
由此可见,仅变更 taskAffinity 达不到变更任务栈结构的目的。
实验阶段3 再将 MainActivity的launchMode改成 singleTask或者SingleInstance
同样的,完成跳转之后,使用命令 adb shell dumpsys activity activities
此时,才看到,两个Activity分别处在两个不同的activity中。
结论:taskAffinity单独使用不会改变任务栈结构,必须结合SingleTask/SingleInstance,才能达成单独启动一个任务栈的效果。
从手机上的Recent中也能看出来:
实验阶段4 taskAffinity allowTaskReparenting
allowTaskReparenting是Android中的一个属性,用于控制活动(Activity)与任务(Task)之间的关联性和重组。当设置为true时,允许活动从一个任务移动到另一个任务,这在某些多任务场景下非常有用。
以下是一些具体的场景和用法:
- 分离活动到新的任务:当用户从一个任务转到另一个任务时,可以使用allowTaskReparenting属性将活动从当前任务分离出来,并放置到一个新的任务中。这对于应用程序内的导航、多窗口模式或分屏模式下的应用场景非常有用。
- 合并任务中的活动:在某些情况下,您可能需要将两个任务中的活动合并为一个。通过设置allowTaskReparenting属性为true,您可以将一个活动从一个任务移动到另一个任务,并在一个任务中汇总显示。
- 多窗口模式下的任务管理:在支持多窗口模式的设备上,使用allowTaskReparenting属性可以更好地管理任务和活动。您可以在不同的窗口或任务中显示和移动活动,以提供更灵活的用户体验。
受限的Binder传递数据
activity之间通信,通常是使用的Intent。
以下代码, 在运行时会崩溃。
原因是,安卓系统中,对用Binder传递数据有大小限制。通常情况下是1M,但是国内厂商对安卓系统进行了定制,这个值就不一定是1M了。
解决办法
-
将不必要的数据通过 transient 修饰。使之不会被序列化。
-
将对象转化成json字符串,减少部分体积 JVM保存一个class,往往会使用额外空间来保存类相关信息。将类中数据转化成json字符串可以减少数据大小。 比如,使用Gson.toJson
-
使用本地数据持久化来实现数据共享 或者 引入EventBus这种组件 在转化成json仍然超出binder大小的情况下,我们就得自建一个参数仓库,让两个Activity到仓库中去读写,并控制参数对象的生命周期,尽量保持内存干净,不要有冗余对象。
容易忽略的多Process
我们在自定义Application中,会做很多启动动作。比如,网络模块初始化,读取本地配置文件,同步网络配置文件,推送SDK初始化,图片加载器参数初始化。
但是实际上,我们的app是支持多进程的。我们可以手动指定一个activity在子进程中运行。而当我们启动一个独立子进程的activity时,它也会创建出一个application来执行我们所有的启动动作。这显然不是我们要的结果,子进程不需要这么多初始化的东西。
解决方案
- 在onCreate中判断当前进程名称,只对主进程执行必要的初始化动作
- 抽象出一个与Application同生命周期的类,根据不同的进程创建出相应的Application类,使得不同的进程执行不同的初始化过程。
注意:对于安卓应用中的主进程和子进程,在动态权限申请方面是相互独立的,它们不会自动共享权限。
当您在应用的主进程中申请一个动态权限时,该权限只适用于主进程的上下文和功能。如果您的应用中存在子进程(通过android:process
属性指定),子进程默认是不具备与主进程相同的权限的。
如果您希望子进程也能够使用某个动态权限,您需要在子进程中重新申请该权限。这意味着,每个进程(主进程和子进程)的权限申请都是独立的。
需要注意的是,根据安卓系统的版本和权限模型的不同,某些权限可能会自动被分配给应用的所有进程,例如网络访问权限。但是,对于敏感权限(如相机、联系人等),每个进程仍然需要独立申请。
如果您希望在主进程和子进程之间共享权限信息,您可以考虑使用进程间通信(IPC)机制,将权限信息从主进程传递给子进程。例如,您可以使用Binder、AIDL或广播等方式进行通信,并在子进程中重新申请相应的权限。
综上所述,主进程和子进程在动态权限申请方面是相互独立的,它们不会自动共享权限。您需要在每个进程中单独申请所需的权限,并可以通过进程间通信机制将权限信息共享给子进程。
后台启动Activity失效的解决方案
从安卓10(sdk 29)开始,安卓系统对后台进程启动Activity做了一定的限制。 比如,我们的app有内部更新机制,我们把app退到后台,后台有个service在定期检索新版本,当发现新版本时,会开启一个线程去下载apk,并且在下载完毕之后,调用 installer 去安装apk,弹出安装界面,它实际上是利用我们的app进程去打开新的Activity。注意,此时我们的app是在后台的。
在安卓10(含)以后,这种搞法行不通了,现在的解决方案是:
利用通知的方式来代替启动Activity的操作。
具体做法是,在下载完成之后,通过NotificationManager发送一个通知到状态栏,这样,用户既不会被打扰到当前正在进行的操作,又可以感知到后台应用的更新情况。