(拉钩)Android工程师进阶34讲-13:Android是如何通过Activity进行交互的?

1. taskAffinity

Actiivty的启动模式。通过配置不同的启动模式可以实现调配不同的Task。但是taskAffinity在一定程度上也会影响任务栈的调配流程。

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

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

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

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

然后再执行以下命令:

1
adb shell dumpsys activity activities

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

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

接下来将SecondtaskAffinity修改如下:

SecondtaskAffinity修改成lagou.affinity,使它和FirsttaskAffinity不同。重新运行代码,并再次查看任务栈中的情况,如下:

可以看到,虽然FirstSecondtaskAffinity不同,但是它们都被创建在一个任务栈中。

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

结论:单独使用taskAffinity不能导致Activity被创建在新的任务栈中,需要配合singleTask或者singleInstance

1.2 taskAffinity + allowTaskReparenting

allowTaskReparenting赋予Activity在各个task中间转移的特性。一个后台任务栈中的Activity A,当有其他任务进入前台,并且taskAffinity和A相同,则会自动将A添加到当前启动的任务栈中。举例:

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

造成上述现象的原因就是allowTaskReparenting属性,代码演示。

分别创建两个Android工程:FirstTaskAffinityReparent

  • First中有三个Activity:FirstAFirstBFirstC。打开顺序依次是FirstA -> FirstB -> ``FirstC。其中FirstCtaskAffinity“lagou.affinity”,且allowTaskReparenting属性设置为trueFirstAFirstB`为默认值;
  • TaskAffinityReparent中只有一个Activity——ReparentActivity,并且其TaskAffinity也等于"lagou.affinity"

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

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

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

2. 通过Binder传递数据的限制

2.1 Binder传递数据限制

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

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

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

2.2 解决办法

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

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

添加transient修饰之后,再次运行代码,就不会报错。

  • 2、将对象转化为JSON字符串,减少数据体积。

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

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

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

2.3 process造成多个Application

一直以来,都会在Application中做一些初始化操作。比如App分包、推送初始化、图片加载库的全局配置等,如下图:

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

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

最终打印日志如下:

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

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

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

可以参考:解决Android多进程导致Application重复创建问题

3. 后台启动Activity失效

正在打开某个App,此时手机后台正在下载另一个App。当App下载完成后,突然弹出安装界面,中断了前一个App的界面交互,这种情况会影响用户体验,而最终用户会抱怨Android手机或者Android系统本身。

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

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

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

解决办法:

Android官方建议使用通知来替代直接启动Activity操作:

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

4. 总结

使用startActivity()时可能遇到的问题:

  • taskAffinity实现任务栈的调配;
  • 通过Binder传递数据的限制;
  • 多进程可能造成的问题;
  • 后台启动Activity的限制。
  • Copyrights © 2019-2020 Tyler Liu

请我喝杯咖啡吧~

支付宝
微信