首页 文章 精选 留言 我的

精选列表

搜索[明略科技],共10000篇文章
优秀的个人博客,低调大师

MVCC 水略深,但是弄懂了真的好爽!

@[toc] 前面写了一篇文章和大家分享了 MySQL 中查询表记录数的问题,里边涉及到一个知识点 MVCC 多版本并发控制。这个问题不搞懂,总感觉缺点什么。因此今天我想花点时间和大家聊一聊 MVCC。 要搞懂 MVCC,最好是要先懂 InnoDB 中事务的隔离级别,不然单纯看概念很难弄明白 MVCC。 1. 隔离级别 1.1 理论 MySQL 中事务的隔离级别一共分为四种,分别如下: 序列化(SERIALIZABLE) 可重复读(REPEATABLE READ) 提交读(READ COMMITTED) 未提交读(READ UNCOMMITTED) 四种不同的隔离级别含义分别如下: SERIALIZABLE > 如果隔离级别为序列化,则用户之间通过一个接一个顺序地执行当前的事务,这种隔离级别提供了事务之间最大限度的隔离。 REPEATABLE READ > 在可重复读在这一隔离级别上,事务不会被看成是一个序列。不过,当前正在执行事务的变化仍然不能被外部看到,也就是说,如果用户在另外一个事务中执行同条 SELECT 语句数次,结果总是相同的。(因为正在执行的事务所产生的数据变化不能被外部看到)。 READ COMMITTED > READ COMMITTED 隔离级别的安全性比 REPEATABLE READ 隔离级别的安全性要差。处于 READ COMMITTED 级别的事务可以看到其他事务对数据的修改。也就是说,在事务处理期间,如果其他事务修改了相应的表,那么同一个事务的多个 SELECT 语句可能返回不同的结果。 READ UNCOMMITTED > READ UNCOMMITTED 提供了事务之间最小限度的隔离。除了容易产生虚幻的读操作和不能重复的读操作外,处于这个隔离级的事务可以读到其他事务还没有提交的数据,如果这个事务使用其他事务不提交的变化作为计算的基础,然后那些未提交的变化被它们的父事务撤销,这就导致了大量的数据变化。 在 MySQL 数据库种,默认的事务隔离级别是 REPEATABLE READ 1.2 SQL 实践 接下来通过几条简单的 SQL 向读者验证上面的理论。 1.2.1 查看隔离级别 通过如下 SQL 可以查看数据库实例默认的全局隔离级别和当前 session 的隔离级别: MySQL8 之前使用如下命令查看 MySQL 隔离级别: SELECT @@GLOBAL.tx_isolation, @@tx_isolation; 查询结果如图: 可以看到,默认的隔离级别为 REPEATABLE-READ,全局隔离级别和当前会话隔离级别皆是如此。 MySQL8 开始,通过如下命令查看 MySQL 默认隔离级别: SELECT @@GLOBAL.transaction_isolation, @@transaction_isolation; 就是关键字变了,其他都一样。 通过如下命令可以修改隔离级别(建议开发者在修改时修改当前 session 隔离级别即可,不用修改全局的隔离级别): SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED 上面这条 SQL 表示将当前 session 的数据库隔离级别设置为 READ UNCOMMITTED,设置成功后,再次查询隔离级别,发现当前 session 的隔离级别已经变了,如图1-2: 注意,如果只是修改了当前 session 的隔离级别,则换一个 session 之后,隔离级别又会恢复到默认的隔离级别,所以我们测试时,修改当前 session 的隔离级别即可。 1.2.2 READ UNCOMMITTED 1.2.2.1 准备测试数据 READ UNCOMMITTED 是最低隔离级别,这种隔离级别中存在脏读、不可重复读以及幻象读问题,所以这里我们先来看这个隔离级别,借此大家可以搞懂这三个问题到底是怎么回事。 下面分别予以介绍。 首先创建一个简单的表,预设两条数据,如下: 表的数据很简单,有 javaboy 和 itboyhub 两个用户,两个人的账户各有 1000 人民币。现在模拟这两个用户之间的一个转账操作。 注意,如果读者使用的是 Navicat 的话,不同的查询窗口就对应了不同的 session,如果读者使用了 SQLyog 的话,不同查询窗口对应同一个 session,因此如果使用 SQLyog,需要读者再开启一个新的连接,在新的连接中进行查询操作。 1.2.2.2 脏读 一个事务读到另外一个事务还没有提交的数据,称之为脏读。具体操作如下: 首先打开两个SQL操作窗口,假设分别为 A 和 B,在 A 窗口中输入如下几条 SQL (输入完成后不用执行): START TRANSACTION; UPDATE account set balance=balance+100 where name='javaboy'; UPDATE account set balance=balance-100 where name='itboyhub'; COMMIT; 在 B 窗口执行如下 SQL,修改默认的事务隔离级别为 READ UNCOMMITTED,如下: SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED 接下来在 B 窗口中输入如下 SQL,输入完成后,首先执行第一行开启事务(注意只需要执行一行即可): START TRANSACTION; SELECT * from account; COMMIT; 接下来执行 A 窗口中的前两条 SQL,即开启事务,给 javaboy 这个账户添加 100 元。 进入到 B 窗口,执行 B 窗口的第二条查询 SQL(SELECT * from user;),结果如下: 可以看到,A 窗口中的事务,虽然还未提交,但是 B 窗口中已经可以查询到数据的相关变化了。 这就是脏读问题。 1.2.2.3 不可重复读 不可重复读是指一个事务先后读取同一条记录,但两次读取的数据不同,称之为不可重复读。具体操作步骤如下(操作之前先将两个账户的钱都恢复为1000): 首先打开两个查询窗口 A 和 B ,并且将 B 的数据库事务隔离级别设置为 READ UNCOMMITTED。具体 SQL 参考上文,这里不赘述。 在 B 窗口中输入如下 SQL,然后只执行前两条 SQL 开启事务并查询 javaboy 的账户: START TRANSACTION; SELECT * from account where name='javaboy'; COMMIT; 前两条 SQL 执行结果如下: 在 A 窗口中执行如下 SQL,给 javaboy 这个账户添加 100 块钱,如下: START TRANSACTION; UPDATE account set balance=balance+100 where name='javaboy'; COMMIT; 4.再次回到 B 窗口,执行 B 窗口的第二条 SQL 查看 javaboy 的账户,结果如下: javaboy 的账户已经发生了变化,即前后两次查看 javaboy 账户,结果不一致,这就是不可重复读。 和脏读的区别在于,脏读是看到了其他事务未提交的数据,而不可重复读是看到了其他事务已经提交的数据(由于当前 SQL 也是在事务中,因此有可能并不想看到其他事务已经提交的数据)。 1.2.2.4 幻象读 幻象读和不可重复读非常像,看名字就是产生幻觉了。 我举一个简单例子。 在 A 窗口中输入如下 SQL: START TRANSACTION; insert into account(name,balance) values('zhangsan',1000); COMMIT; 然后在 B 窗口输入如下 SQL: START TRANSACTION; SELECT * from account; delete from account where name='zhangsan'; COMMIT; 我们执行步骤如下: 首先执行 B 窗口的前两行,开启一个事务,同时查询数据库中的数据,此时查询到的数据只有 javaboy 和 itboyhub。 执行 A 窗口的前两行,向数据库中添加一个名为 zhangsan 的用户,注意不用提交事务。 执行 B 窗口的第二行,由于脏读问题,此时可以查询到 zhangsan 这个用户。 执行 B 窗口的第三行,去删除 name 为 zhangsan 的记录,这个时候删除就会出问题,虽然在 B 窗口中可以查询到 zhangsan,但是这条记录还没有提交,是因为脏读的原因才看到了,所以是没法删除的。此时就产生了幻觉,明明有个 zhangsan,却无法删除。 这就是幻读。 看了上面的案例,大家应该明白了脏读、不可重复读以及幻读各自是什么含义了。 1.2.3 READ COMMITTED 和 READ UNCOMMITTED 相比,READ COMMITTED 主要解决了脏读的问题,对于不可重复读和幻象读则未解决。 将事务的隔离级别改为 READ COMMITTED 之后,重复上面关于脏读案例的测试,发现已经不存在脏读问题了;重复上面关于不可重复读案例的测试,发现不可重复读问题依然存在。 上面那个案例不适用于幻读的测试,我们换一个幻读的测试案例。 还是两个窗口 A 和 B,将 B 窗口的隔离级别改为 READ COMMITTED, 然后在 A 窗口输入如下测试 SQL: START TRANSACTION; insert into account(name,balance) values('zhangsan',1000); COMMIT; 在 B 窗口输入如下测试 SQL: START TRANSACTION; SELECT * from account; insert into account(name,balance) values('zhangsan',1000); COMMIT; 测试方式如下: 首先执行 B 窗口的前两行 SQL,开启事务并查询数据,此时查到的只有 javaboy 和 itboyhub 两个用户。 执行 A 窗口的前两行 SQL,插入一条记录,但是并不提交事务。 执行 B 窗口的第二行 SQL,由于现在已经没有了脏读问题,所以此时查不到 A 窗口中添加的数据。 执行 B 窗口的第三行 SQL,由于 name 字段唯一,因此这里会无法插入。此时就产生幻觉了,明明没有 zhangsan 这个用户,却无法插入 zhangsan。 1.2.4 REPEATABLE READ 和 READ COMMITTED 相比,REPEATABLE READ 进一步解决了不可重复读的问题,但是幻象读则未解决。 REPEATABLE READ 中关于幻读的测试和上一小节基本一致,不同的是第二步中执行完插入 SQL 后记得提交事务。 由于 REPEATABLE READ 已经解决了不可重复读,因此第二步即使提交了事务,第三步也查不到已经提交的数据,第四步继续插入就会出错。 注意,REPEATABLE READ 也是 InnoDB 引擎的默认数据库事务隔离级别 1.2.5 SERIALIZABLE SERIALIZABLE 提供了事务之间最大限度的隔离,在这种隔离级别中,事务一个接一个顺序的执行,不会发生脏读、不可重复读以及幻象读问题,最安全。 如果设置当前事务隔离级别为 SERIALIZABLE,那么此时开启其他事务时,就会阻塞,必须等当前事务提交了,其他事务才能开启成功,因此前面的脏读、不可重复读以及幻象读问题这里都不会发生。 1.3 总结 总的来说,隔离级别和脏读、不可重复读以及幻象读的对应关系如下: 隔离级别 脏读 不可重复读 幻象读 READ UNCOMMITTED 允许 允许 允许 READ COMMITED 不允许 允许 允许 REPEATABLE READ 不允许 不允许 允许 SERIALIZABLE 不允许 不允许 不允许 性能关系如图: 松哥前不久也录过一个隔离级别的视频,大家可以参考下: https://www.bilibili.com/video/BV14L4y1B7mB 2. 快照读与当前读 接下来我们还需要搞明白一个问题:快照读与当前读。 2.1 快照读 快照读(SnapShot Read)是一种一致性不加锁的读,是 InnoDB 存储引擎并发如此之高的核心原因之一。 在可重复读的隔离级别下,事务启动的时候,就会针对当前库拍一个照片(快照),快照读读取到的数据要么就是拍照时的数据,要么就是当前事务自身插入/修改过的数据。 我们日常所用的不加锁的查询,包括本文第一小节中涉及到的所有查询,都属于快照读,这个我就不演示了。 2.2 当前读 与快照读相对应的就是当前读,当前读就是读取最新数据,而不是历史版本的数据,换言之,在可重复读隔离级别下,如果使用了当前读,也可以读到别的事务已提交的数据。 松哥举个例子: MySQL 事务开启两个会话 A 和 B。 首先在 A 会话中开启事务并查询 id 为 1 的记录: 接下来我们在 B 会话中对 id 为 1 的数据进行修改,如下: 注意 B 会话不要开启事务或者开启了及时提交事务,否则 update 语句占用一把排他锁会导致一会在 A 会话中用锁时发生阻塞。 接下来,回到 A 会话中继续做查询操作,如下: 可以看到,A 会话中第一个查询是快照读,读取到的是当前事务开启时的数据状态,后面两个查询则是当前读,读取到了当前最新的数据(B 会话中修改后的数据)。 3. undo log 我们再来稍微了解一下 undo log,这也有助于我们理解后面的 MVCC,这里我们简单介绍一下。 我们知道数据库事务有回滚的能力,既然能够回滚,那么就必须要在数据改变之前先把旧的数据记录下来,作为将来回滚的依据,那么这个记录就是 undo log。 当我们要添加一条记录的时候,就把添加的数据 id 记录到 undo log 中,将来回滚的时候就据此把数据删除;当我们要删除或者修改数据的时候,就把原数据记录到 undo log 中,将来据此恢复数据。查询操作因为不涉及回滚操作,所以就不需要记录到 undo log 中。 4. 行格式 接下来我们再来看一看行格式,这也有助于我们理解 MVCC。 行格式就是 InnoDB 在保存每一行的数据的时候,究竟是以什么样的格式来保存这行数据的。 数据库中的行格式有好几种,例如 COMPACT、REDUNDANT、DYNAMIC、COMPRESSED 等,不过无论是哪种行格式,都绕不开下面几个隐藏的数据列: 上图中的列 1、列 2、列 3 一直到列 N,就是我们数据库中表的列,保存着我们正常的数据,除了这些保存数据的列之外,还有三列额外加进来的数据,这也是我们这里要重点关注的 DB_ROW_ID、DB_TRX_ID、DB_ROLL_PTR 三列: DB_ROW_ID:该列占用 6 个字节,是一个行 ID,用来唯一标识一行数据。如果用户在创建表的时候没有设置主键,那么系统会根据该列建立主键索引。 DB_TRX_ID:该列占用 6 个字节,是一个事务 ID。在 InnoDB 存储引擎中,当我们要开启一个事务的时候,会向 InnoDB 的事务系统申请一个事务 id,这个事务 id 是一个严格递增且唯一的数字,当前数据行是被哪个事务修改的,就会把对应的事务 id 记录在当前行中。 DB_ROLL_PTR:该列占用 7 个字节,是一个回滚指针,这个回滚指针指向一条 undo log 日志的地址,通过这个 undo log 日志可以让这条记录恢复到前一个版本。 好啦,这是关于数据行格式的一些内容。 5. MVCC 有了前面小节的预备知识,接下来我们就来正式看一看 MVCC。 MVCC,英文全称是 Multi-Version Concurrency Control,中文译作多版本并发控制。 MVCC 的核心思路就是保存数据行的历史版本,通过对数据行的多个版本进行管理来实现数据库的并发控制。 简单来说,我们平时看到的一条一条的记录,在数据库中保存的时候,可能不仅仅只有一条记录,而是有多个历史版本。 如下图: 这张图理解到位了,我想大家的 MVCC 也就理解的查不多了。 接下来我结合不同的隔离级别来和大家说这张图。 5.1 REPEATABLE READ 首先,当我们通过 INSERT\DELETE\UPDATE 去操作一行数据的时候,就会产生一个事务 id,这个事务 id 也会同时保存在行记录中(DB_TRX_ID),也就是说,当前数据行是哪个事务修改后得到的,是有记录的。 INSERT\DELETE\UPDATE 操作都会产生对应的 undo log 日志,每一行记录都有一个 DB_ROLL_PTR 指向 undo log 日志,每一行记录,通过执行 undo log 日志,就可以恢复到前一个记录、前前记录、前前前记录... 当我们开启一个事务的时候,首先会向 InnoDB 的事务系统申请一个事务 id,这个 id 是一个严格递增的数字,在当前事务开启的一瞬间系统会创建一个数组,数组中保存了目前所有的活跃事务 id,所谓的活跃事务就是指已开启但是还没有提交的事务。 > 这个数组中的最小值好理解,有的小伙伴可能会误以为数组中的最大值就是的当前事务的 id,其实这个不一定,也有可能更大。因为从申请到 trx_id 到创建数组之间也是需要时间的,这期间可能有其他会话也申请到了 trx_id。 当当前事务想要去查看某一行数据的时候,会先去查看该行数据的 DB_TRX_ID: 如果这个值等于当前事务 id,说明这就是当前事务修改的,那么数据可见。 如果这个值小于数组中的最小值,说明当我们开启当前事务的时候,这行数据修改所涉及到的事务已经提交了,当前数据行是可见的。 如果这个值大于数组中的最大值,说明这行数据是我们在开启事务之后,还没有提交的时候,有另外一个会话也开启了事务,并且修改了这行数据,那么此时这行数据就是不可见的。 如果这个值的大小介于数组中最大值最小值之间(闭区间),且该值不在数组中,说明这也是一个已经提交的事务修改的数据,这是可见的。 如果这个值的大小介于数组中最大值最小值之间(闭区间),且该值在数组中(不等于当前事务 id),说明这是一个未提交的事务修改的数据,不可见。 前三种情况应该很好理解,主要是后面两种,松哥举一个简单例子。 比如我们有 A、B、C、D 四个会话,首先 A、B、C 分别开启一个事务,事务 ID 是 3、4、5,然后 C 会话提交了事务,A、B 未提交。接下来 D 会话也开启了一个事务,事务 ID 是 6,那么当 D 会话开启事务的时候,数组中的值就是 [3,4,6]。现在假设有一行数据的 DB_TRX_ID 是 5(第四种情况),那么该行数据就是可见的(因为当前事务开启的时候它已经提交了);如果有一行数据的 DB_TRX_ID 是 4,那么该行就不可见(因为未提交)。 另外还有一个需要注意的地方,就是如果当前事务中涉及到数据的更新操作,那么更新操作是在当前读的基础上更新的,而不是快照读的基础上更新的,如果是后者则有可能导致数据丢失。 我举一个例子,假设有如下表: 现在有两个会话 A 和 B,首先在 A 中开启事务: 然后在会话 B 中做一次修改操作(不用显式开启事务,更新 SQL 内部会开启事务,更新完成后事务会自动提交): 接下来回到会话 A 中,查询该条记录发现值没变,符合预期(目前隔离级别是可重复读),然后在 A 中做一次修改操作,修改完成后再去查询,如下图: 可以看到,更新其实是在 100 的基础上更新的,这个也好理解,要是在 99 的基础上更新,那么就会丢失掉 100 的那次更新,显然是不对的。 其实 MySQL 中的 update 就是先读再更新,读的时候默认就是当前读,即会加锁。所以在上面的案例中,如果 B 会话中显式的开启了事务并且没有没有提交,那么 A 会话中的 update 语句就会被阻塞。 这就是 MVCC,一行记录存在多个版本。实现了读写并发控制,读写互不阻塞;同时 MVCC 中采用了乐观锁,读数据不加锁,写数据只锁行,降低了死锁的概率;并且还能据此实现快照读。 5.2 READ COMMITTED READ COMMITTED 和 REPEATABLE READ 类似,区别主要是后者在每次事务开始的时候创建一致性视图(创建数组列出活跃事务 id),而前者则每一个语句执行前都会重新算出一个新的视图。 所以 READ COMMITTED 这种隔离级别会看到别的会话已经提交的数据(即使别的会话比当前会话开启的晚)。 6. 小结 MVCC 在一定程度上实现了读写并发,不过它只在 READ COMMITTED 和 REPEATABLE READ 两个隔离级别下有效。 而 READ UNCOMMITTED 总是会读取最新的数据行,SERIALIZABLE 则会对所有读取的行都加锁,这两个都和 MVCC 不兼容。 好啦,不知道小伙伴们看明白没有,有问题欢迎留言讨论。

优秀的个人博客,低调大师

聊聊Gartner 2021战略技术趋势——随处运营

Garnter不久前发布了2021年顶级战略技术趋势,本篇是随处运营(Anywhere Operations)的介绍。 这个趋势目前网上翻译的叫法是随处运营,但是通过Gartner的介绍和说明,感觉更适合成为随处操作/办公,是由于本次全球疫情所催生的一种新的常态和新的需求。Gartner的趋势预测的高明(近几年)之处在于其每年的Top Strategic Technology不是独立的,而是彼此联系,相互协作和促进的。比如这个随处运营,在场景上需要分布式云/边缘计算的支撑,在安全上需要零信任安全/CASB来保护,在体验上需要TX(全面体验)来保证用户和员工满意度及体验等等。所以在看这些趋势的时候,能把这些技术趋势联系到一起,作为一个整体方案或模型去理解和分析(个人观点,仅供参考)。 位置独立 随处运营(Anywhere Operations) Gartner预测,到2023年40%的组织将融合虚拟和实体体验,从而提高生产率和客户覆盖率。 随处运营是一种IT运营模型,旨在支持各地的客户、员工,并管理跨分布式基础设施的业务服务部署,其模式是“数字优先,远程优先”。 随处运营描述了一种业务运营模式,旨在接触任意位置的客户,使员工能够在任何地方工作,并使用数字技术在任何地方交付业务服务。这是对传统模式的挑战,即必须在特定的地点,面对面交流,以实现价值和效率的最大化。它推动了一种新的常态,即员工、服务商、业务合作伙伴、客户和最终消费者将拉开远离。但随处运营的范围远远不止是在家/远程办公,它还包括为团队和客户提供的增值体验。 它意味着提供安全的远程访问,使团队可以像在办公室一样操作,而不会使业务受到影响,它还包括协作工具,使团队可以轻松地肩并肩工作,即使是在不同的位置。其和远程办公之间最大的区别之一是将客户也包含在内(远程办公集中在企业员工上)。这意味着客户可以在任何地点、任何时间完成交互并达到目标——从购买汽车到归还物品,到开设银行账户,再到设计新材料。 数字优先、位置独立的思维模式是随处运营的先决条件。这并不像远程操作那么简单——这种模式必须提供独特的增值体验。提供无缝和可扩展的数字体验需要改变技术基础设施、管理实践、安全和治理政策,以及员工和客户参与模型。 实现无边界数字工作场所的技术为随处运营提供了基础。该技术基础包括五个模块: 协作与生产力。工作流协作、会议解决方案、云办公组合工具、数字面板和智能工作空间; 远程访问安全。无密码及多因素认证、零信任访问、SASE和身份作为新的安全边界。 云与边缘基础设施。分布式云、IoT、API网关、边缘AI、边缘处理。 数字化体验的量化。数字体验监控、工作场所分析、远程支持和无接触交互。 自动化支撑远程运营。AIOps、终端管理、SaaS管理平台,自主服务与无接触操作。 为何是趋势 成功摆脱疫情影响的组织将拥有一个随处运营的基础。组织应对危机的准备和应变能力在很大程度上取决于数字化能力的成熟度和准备情况。Gartner 2020年调查显示,在疫情之前,半数CEO预计会出现衰退。但只有9%的人做好了应对冲击的准备。这场危机加速了数字化转型计划和数字化办公场所的实施,而此前这些计划或被搁置,或被撤资。 大多数基础设施和运营负责人表示,他们恢复了推出云服务的计划,除了零信任的安全功能外,还将推出用于员工协作和数字体验监控的云服务。 在Gartner对317名CFO和财务主管的调查中,近四分之一的受访者表示,他们将把至少20%的现场员工调整为长期远程职位。 由于疫情导致的居家订单和物理距离,消费者对数字服务的需求增加。那些向数字化商业模式转变以支持客户和员工的公司会比其他公司更快恢复活力。 随处运营不仅仅是远程办公,还包括为客户提供全面和富有成效体验(涉及全面体验的运用),即使员工没有通过自动化和云或边缘基础设施远程办公(分布式云技术),也可以应用该模式。边缘计算是随处运营的另一个组成部分,提供了来自多个地点大量数据的收集和处理,以及用于提高效率和降低运营成本的相关信息。 能联想到的 随处运营不仅仅是远程办公,因为在新常态下,对人员和设备的物理距离有了要求。在这种情况下,随处运营使用非接触式交互,但保留了个人交互的独特价值。 员工和客户的最佳体验将取决于特定环境。环境可以分为组织控制或不受控制的空间。例如,工厂和零售商店是组织可以控制的环境。用户的家庭、个人设备和公共空间都是组织无法控制的环境。随处运营都必须将这些环境中的价值交付置于相关环境中。 不管未来的工作场所是知识工作者的办公室还是汽车制造厂,都将随着疫情的发展而变化。这些变化将包括采用移动设备、可穿戴设备和增强现实设备等远程支持技术。使用物联网传感器的非接触式和无密码交互、近源通信支持的智能卡和可穿戴设备将成为常态,比如解锁受限空间的物理通道,以及操作电梯或自动售货机。 现在有几家银行只在手机上开展业务。例如,墨西哥萨瓦德尔银行(Banco Sabadell)和新加坡星展银行(DBS Bank)提供的数字服务不仅包括资金转账和支付账单,还包括开户。这是对服务大众化访问的一个例子,消费者可以在任意地点使用服务。此外,通过流媒体技术大规模开放,在线学习激增,使获取知识和高质量教育变得大众化。 为了支持一线员工,微软Teams和谷歌Meet等数字协作工具使员工能够实时共享信息。例如,微软的Walkie Talkie模式(一种一键通体验),用户可以通过它向自己的频道广播信息。急救人员和护理人员可以通过智能手机使用这一功能在紧急情况下保持联系,无论他们在哪里。随处运营结合了语音控制输入界面和AR眼镜,增强了一线客户服务技术人员的能力。 随处运营对于安全的影响 网络安全是当今每个企业关心的问题。不管经营模式如何,企业都会受到各种各样的威胁——甚至那些没有连接网络的设备的企业也是如此。任何行动都可能增加的风险是正常的。 采用这种模型将扩展组织的暴露面,需要考虑远程用户和功能。但许多企业已经实现远程办公,但未设置网络接入标准和虚拟专用网接入等方式不可避免地增加了所面临的威胁。 作为一种框架方法,随处运营模式将安全远程访问和网络安全优秀实践作为规划和实施的一部分,将其作为整体战略的一部分来降低风险。企业不要盲目跟风,急于求成,并非所有企业和系统都需要采用这种模式,应结合自身情况规划全面的可行性战略方法,考虑各种潜在业务影响因素。随处运营需要IT领域的专业知识,同时,它为组织提供了在不确定时期维持运营所需的灵活性,并能在业务因素发生变化时快速适应转变。 Gartner建议的行动 使用数字体验监控、端点分析、自助服务和远程故障排除工具,增强员工在无边界工作环境中的体验; 通过为一线员工提供远程支持的AR可穿戴设备和语音控制协作工具,提高工作效率; 采用团队结构、流程、技能和工具,采用数字化优先、位置独立战略,推动商业模式创新; 投资分布式云和边缘技术,以构建混合的办公场所,在物理和虚拟地点之间无缝移动办公环境。 【责任编辑:赵宁宁 TEL:(010)68476606】

优秀的个人博客,低调大师

每日一博 | 明修

作者:vivo 互联网大前端团队- Zhao Kaiping 本文从一例业务中遇到的问题出发,以FLAG_ACTIVITY_NEW_TASK这一flag作为切入点,带大家探究Activity启动前的一项重要的工作——栈校验。 文中列举一系列业务中可能遇到的异常状况,详细描述了使用FLAG_ACTIVITY_NEW_TASK时可能遇到的“坑”,并从源码中探究其根源。只有合理使用flag、launchMode,才能避免因为栈机制的特殊性,导致一系列与预期不符的启动问题。 一、问题及背景 应用间相互联动、相互跳转,是实现系统整体性、体验一致性的重要手段,也是最简单的一种方法。 当我们用最常用的方法去startActivity时,竟也会遇到失败的情况。在真实业务中,就遇到了这样一例异常:用户点击某个按钮时,想要“简简单单”跳转另一个应用,却没有任何反应。 经验丰富的你,脑海中是否涌现出了各种猜想:是不是目标Activity甚至目标App不存在?是不是目标Activty没有对外开放?是不是有权限的限制或者跳转的action/uri错了…… 真实的原因被flag、launchMode、Intent等特性层层藏匿,可能超出你此时的思考。 本文将从源码出发,探究前因后果,展开讲讲在startActivity()真正准备启动一个Activity前,需要经过哪些“磨难”,怎样有据可依地解决由栈问题导致的启动异常。 1.1 业务中遇到的问题 业务中的场景是这样的,存在A、B、C三个应用。 (1)从应用A-Activity1跳转至应用B-Activity2; (2)应用B-Activity2继续跳转到应用C-Activity3; (3)C内某个按钮,会再次跳转B-Activity2,但点击后没有任何反应。如果不经过前面A到B的跳转,C直接跳到B是可以的。 1.2 问题代码 3个Activity的Androidmanifest配置如下,均可通过各自的action拉起,launchMode均为标准模式。 <!--应用A--> <activity android:name=".Activity1" android:exported="true"> <intent-filter> <action android:name="com.zkp.task.ACTION_TO_A_PAGE1" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </activity> <!--应用B--> <activity android:name=".Activity2" android:exported="true"> <intent-filter> <action android:name="com.zkp.task.ACTION_TO_B_PAGE2" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </activity> <!--应用C--> <activity android:name=".Activity3" android:exported="true"> <intent-filter> <action android:name="com.zkp.task.ACTION_TO_C_PAGE3" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </activity> A-1到B-2的代码,指定flag为 FLAG_ACTIVITY_NEW_TASK private void jumpTo_B_Activity2_ByAction_NewTask() { Intent intent = new Intent(); intent.setAction("com.zkp.task.ACTION_TO_B_PAGE2"); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent);} B-2到C-3的代码,未指定flag private void jumpTo_C_Activity3_ByAction_NoTask() { Intent intent = new Intent(); intent.setAction("com.zkp.task.ACTION_TO_C_PAGE3"); startActivity(intent);} C-3到B-2的代码,与A-1到B-2的完全一致,指定flag为 FLAG_ACTIVITY_NEW_TASK private void jumpTo_B_Activity2_ByAction_NewTask() { Intent intent = new Intent(); intent.setAction("com.zkp.task.ACTION_TO_B_PAGE2"); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent);} 1.3 代码初步分析 仔细查看问题代码,在实现上非常简单,有两个特征: (1)如果直接通过C-3跳B-2,没有任何问题,但A-1已经跳过B-2后,C-3就失败了。 (2)在A-1和C-3跳到B-2时,都设置了flag为FLAG_ACTIVITY_NEW_TASK。 依据经验,我们推测与栈有关,尝试将跳转前栈的状态打印出来,如下图。 由于A-1跳到B-2时设置了FLAG_ACTIVITY_NEW_TASK,B-2跳到C-3时未设置,所以1在独立栈中,2、3在另一个栈中。示意如下图。 C-3跳转B-2一般有3种可能的预期,如下图:预想1,新建一个Task,在新Task中启动一个B-2;预想2,复用已经存在的B-2;预想3,在已有Task中新建一个实例B-2。 但实际上3种预期都没有实现,所有Activity的任何声明周期都没有变化,界面始终停留在C-3。 看一下FLAG_ACTIVITY_NEW_TASK的官方注释和代码注释,如下图: 重点关注这一段: When using this flag, if a task is already running for the activity you are now starting, then a new activity will not be started; instead, the current task will simply be brought to the front of the screen with the state it was last in. 使用此flag时,如果你正在启动的Activity已经在一个Task中运行,那么一个新Activity不会被启动;相反,当前Task将简单地显示在界面的前面,并显示其最后的状态。 ——显然,官方文档与代码注释的表述与我们的异常现象是一致的,目标Activity2已经在Task中存在,则不会被启动;Task直接显示在前面,并展示最后的状态。由于目标Activty3就是来源Activity3,所以页面没有任何变化。 看起来官方还是很靠谱的,但实际效果真的能一直与官方描述一致吗?我们通过几个场景来看一下。 二、场景拓展与验证 2.1 场景拓展 在笔者依据官方描述进行调整、复现的过程中,发现了几个比较有意思的场景。 PS:上面业务的案例中,B-2和C-3在不同应用内,又在相同的Task内,但实际上是否是同一个应用,对结果的影响并不大。为了避免不同应用和不同Task造成阅读混乱,同一个栈的跳转,我们都在本应用内进行,故业务中的场景等价于下面的【场景0】 【场景0】把业务中B-2到C-3的应用间跳转改为B-2到B-3的应用内跳转 // B-2跳转B-3public static void jumpTo_B_3_ByAction_Null(Context context) { Intent intent = new Intent(); intent.setAction("com.zkp.task.ACTION_TO_B_PAGE3"); context.startActivity(intent);} 如下图,A-1设置NEW_TASK跳转B-2,再跳转B-3,最终设置NEW_TASK想跳转B-2。虽然跳C-3改为了跳B-3,但与之前问题的表现一致,没有反应,停留在B-3。 有的读者会指出这样的问题:如果同一个应用内使用NEW_TASK跳转,而不指定目标的taskAffinity属性,实际是无法在新Task中启动的。请大家忽略该问题,可以认为笔者的操作是已经加了taskAffinity的,这对最终结果并没有影响。 【场景1】如果目标Task和来源Task不是同一个,情况是否会如官方文档所说复用已有的Task并展示最近状态?我们改为B-3启动一个新Task的新Activity C-4,再通过C-4跳回B-2 // B-3跳转C-4public static void jumpTo_C_4_ByAction_New(Context context) { Intent intent = new Intent("com.zkp.task.ACTION_TO_C_PAGE4"); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent);}// C-4跳转B-2public static void jumpTo_B_2_ByAction_New(Context context) { Intent intent = new Intent(); intent.setAction("com.zkp.task.ACTION_TO_B_PAGE2"); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent);} 如下图,A-1设置NEW_TASK跳转B-2,再跳转B-3,再设置NEW_TASK跳转C-4,最终设置NEW_TASK想跳转B-2。 预想的结果是:不会跳到B-2,而是跳到它所在Task的顶层B-3。 实际的结果是:与预期一致,确实是跳到了B-3。 【场景2】把场景1稍做修改:C-4到B-2时,我们不通过action来跳,改为通过setClassName跳转。 // C-4跳转B-2public static void jumpTo_B_2_ByPath_New(Context context) { Intent intent = new Intent(); intent.setClassName("com.zkp.b", "com.zkp.b.Activity2"); // 直接设置classname,不通过action intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent);} 如下图,A-1设置NEW_TASK跳转B-2,再跳转B-3,再设置NEW_TASK跳转C-4,最终设置NEW_TASK想跳转B-2。 预想的结果是:与场景0一致,会跳到B-2所在Task的已有顶层B-3。 实际的结果是:在已有的Task2中,产生了一个新的B-2实例。 仅仅是改变了一下重新跳转B-2的方式,效果就完全不一样了!这与官方文档中提到该flag与"singleTask" launchMode值产生的行为并不一致! 【场景3】把场景1再做修改:这次C-4不跳栈底的B-2,改为跳转B-3,且还是通过action方式。 // C-4跳转B-3public static void jumpTo_B_3_ByAction_New(Context context) { Intent intent = new Intent(); intent.setAction("com.zkp.task.ACTION_TO_B_PAGE3"); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent);} 如下图,A-1设置NEW_TASK跳转B-2,再跳转B-3,再设置NEW_TASK跳转C-4,最终设置NEW_TASK想跳转B-3。 预想的结果是:与场景0一致,会跳到B-2所在Task的顶层B-3。 实际的结果是:在已有的Task2中,产生了一个新的B-3实例。 不是说好的,Activity已经存在时,展示其所在Task的最新状态吗?明明Task2中已经有了B-3,并没有直接展示它,而是生成了新的B-3实例。 【场景4】既然Activity没有被复用,那Task一定会被复用吗?把场景3稍做修改,直接给B-3指定一个单独的affinity。 <activity android:name=".Activity3" android:exported="true" android:taskAffinity="b3.task"><!--指定了亲和性标识--> <intent-filter> <action android:name="com.zkp.task.ACTION_TO_B_PAGE3" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter></activity> 如下图,A-1设置NEW_TASK跳转B-2,再跳转B-3,再设置NEW_TASK跳转C-4,最终设置NEW_TASK想跳转B-3。 ——这次,连Task也不会再被复用了……Activity3在一个新的栈中被实例化了。 再回看官方的注释,就会显得非常不准确,甚至会让开发者对该部分的认知产生严重错误!稍微改变过程中的某个毫无关联的属性(如跳转目标、跳转方式……),就会产生很大差异。 在看flag相关注释时,我们要树立一个意识:Task和Activity跳转的实际效果,是launchMode、taskAffinity、跳转方式、Activity在Task中的层级等属性综合作用的结果,不要相信“一面之词”。 回到问题本身,究竟是哪些原因造就了上面的不同效果呢?只有源码最值得信赖了。 三、场景分析与源码探索 本文以Android 12.0源码为基础,进行探究。上述场景在不同Android版本上的表现是一致的。 3.1 源码调试注意事项 源码的调试方法,许多文章已经有了详细的教学,本文不再赘述。此处只简单总结其中需要注意的事项: 下载模拟器时,不要使用Google Play版本,该版本类似user版本,无法选择system_process进程进行断点。 即使是Google官方模拟器和源码,在断点时,也会有行数严重不对应的情况(比如:模拟器实际会运行到方法A,但在源码中打断点时,实际不能定位到方法A的对应行数),该问题并没有很好的处理方法,只能尽量规避,如使模拟器版本与源码版本保持一致、多打一些断点增加关键行数被定位到的几率。 3.2 初步断点,明确启动结果 以【场景0】为例,我们初步确认一下,为什么B-3跳转B-2会无反应,系统是否告知了原因。 3.2.1 明确启动结果及其来源 在Android源码的断点调试中,常见的有两类进程:应用进程和system_process进程。 在应用进程中,我们能获取到应用启动结果的状态码result,这个result用来告诉我们启动是否成功。涉及堆栈如下图(标记1)所示: Activity类::startActivity() → startActivityForResult() →Instrumentation类::execStartActivity(), 返回值result则是ATMS (ActivityTaskManagerService)执行的结果。 如上图(标记2)标注,ATMS类::startActivity()方法,返回了result=3。 在system_process进程中,我们看一下这个result=3是怎样被赋值的。略去详细断点步骤,实际堆栈如下图(标注1)所示: ATMS类::startActivity() →startActivityAsUser() →ActivityStarter类::execute() →executeRequest() →startActivityUnchecked() → startActivityInner() → recycleTask(),在recycleTask()中返回了结果。 如上图(标注2)所示,result在mMovedToFront=false时被赋值,即result=START_DELIVERED_TO_TOP=3,而START_SUCCESS=0才代表创建成功。 看一下源码中对START_DELIVERED_TO_TOP的说明,如下图: Result for IActivityManaqer.startActivity: activity wasn't really started, but the given Intent was given to the existing top activity. (IActivityManaqer.startActivityActivity的结果:Activity并未真正启动,但给定的Intent已提供给现有的顶层Activity。) “Activity并未真正启动”——是的,因为可以复用 “给定的Intent已提供给现有的顶层Activity”——实际没有,顶层Activity3并没有收到任何回调,onNewIntent()未执行,甚至尝试通过Intent::putExtra()传入新的参数,Activity3也没有收到。官方文档又带给了我们一个疑问点?我们把这个问题记录下来,在后面分析。 满足什么条件,才会造成 START_DELIVERED_TO_TOP的结果呢?笔者的思路是,通过与正常启动流程对比,找出差异点。 3.3 过程断点,探索启动流程 一般来说,在定位问题时,我们习惯通过结果反推原因,但反推的过程只能关注到与问题强关联的代码分支,并不能能使我们很好地了解全貌。 所以,本节内容我们通过顺序阅读的方法,正向介绍startActivity过程中与上述【场景01234】强相关的逻辑。再次简述一下: 【场景0】同一个Task内,从顶部B-3跳转B-2——停留在B-3 【场景1】从另一个Task内的C-4,跳转B-2——跳转到B-3 【场景2】把场景1中,C-4跳转B-2的方式改为setClassName()——创建新B-2实例 【场景3】把场景1中,C-4跳转B-2改为跳转B-3——创建新B-3实例 【场景4】给场景3中的B-3,指定taskAffinity——创建新Task和新B-3实例 3.3.1 流程源码概览 源码中,整个启动流程很长,涉及的方法和逻辑也很多,为了便于大家理清方法调用顺序,方便后续内容的阅读,笔者将本文涉及到的关键类及方法调用关系整理如下。 后续阅读中如果不清楚调用关系,可以返回这里查看: // ActivityStarter.java ActivityStarter::execute() { executeRequest(intent) { startActivityUnchecked() { startActivityInner(); } } ActivityStarter::startActivityInner() { setInitialState(); computeLaunchingTaskFlags(); Task targetTask = getReusableTask(){ findTask(); } ActivityRecord targetTaskTop = targetTask.getTopNonFinishingActivity(); if (targetTaskTop != null) { startResult = recycleTask() { setTargetRootTaskIfNeeded(); complyActivityFlags(); if (mAddingToTask) { return START_SUCCESS; //【场景2】【场景3】从recycleTask()返回 } resumeFocusedTasksTopActivities() return mMovedToFront ? START_TASK_TO_FRONT : START_DELIVERED_TO_TOP;//【场景1】【场景0】从recycleTask()返回 } } else { mAddingToTask = true; } if (startResult != START_SUCCESS) { return startResult;//【场景1】【场景0】从startActivityInner()返回 } deliverToCurrentTopIfNeeded(); resumeFocusedTasksTopActivities(); return startResult; } 3.3.2 关键流程分析 (1)初始化 startActivityInner()是最主要的方法,如下列几张图所示,该方法会率先调用setInitialState(),初始化各类全局变量,并调用reset(),重置ActivityStarter中各种状态。 在此过程中,我们记下两个关键变量mMovedToFront和mAddingToTask,它们均在此被重置为false。 其中,mMovedToFront代表当Task可复用时,是否需要将目标Task移动到前台;mAddingToTask代表是否要将Activity加入到Task中。 (2)计算确认启动时的flag 该步骤会通过computeLaunchingTaskFlags()方法,根据launchMode、来源Activity的属性等进行初步计算,确认LaunchFlags。 此处重点处理来源Activity为空的各类场景,与我们上文中的几种场景无关,故不再展开讲解。 (3)获取可以复用的Task 该步骤通过调用getReusableTask()实现,用来查找有没有可以复用的Task。 先说结论:场景0123中,都能获取到可以复用的Task,而场景4中,未获取到可复用的Task。 为什么场景4不可以复用?我们看一下getReusableTask()的关键实现。 上图(标注1)中,putIntoExistingTask代表是否能放入已经存在的Task。当flag含有NEW_TASK且不含MULTIPLE_TASK时,或指定了singleInstance或singleTask的launchMode等条件,且没有指定Task或要求返回结果 时,场景01234均满足了条件。 然后,上图(标注2)通过findTask()查找可以复用的Task,并将过程中找到的栈顶Activity赋值给intentActivity。最终,上图(标注3)将intentActivity对应的Task作为结果。 findTask()是怎样查找哪个Task可以复用呢? 主要是确认两种结果mIdealRecord——“理想的ActivityRecord” 和 mCandidateRecord——"候选的ActivityRecord",作为intentActivity,并取intentActivity对应的Task作为复用Task。 什么ActivityRecord才是理想或候选的ActivityRecord呢? 在mTmpFindTaskResult.process()中确认。 程序会将当前系统中所有的Task进行遍历,在每个Task中,进行如上图所示的工作——将Task的底部Activity realActivity与目标Activity cls进行对比。 场景012中,我们想跳转Activity2,即cls是Activity2,与Task底部的realActivity2相同,则将该Task顶部的Activity3 r作为“理想的Activity”; 场景3中,我们想跳转Activity3,即cls是Activity3,与Task底部的realActivity2不同,则进一步判断task底部Activity2与目标Activity3的栈亲和行,具有相同亲和性,则将Task的顶部Activity3作为“候选Activity”; 场景4中,所有条件都不满足,最终没能找到可复用的Task。在执行完getReusableTask()后将mAddingToTask赋值为true 由此,我们就能解释【场景4】中,新建了Task的现象。 (4)确定是否需要将目标Task移动到前台 如果存在可复用的Task,场景0123会执行recycleTask(),该方法中会相继进行几个操作:setTargetRootTaskIfNeeded()、 complyActivityFlags()。 首先,程序会执行 setTargetRootTaskIfNeeded(),用来确定是否需要将目标Task移动到前台,使用mMovedToFront作为标识。 在【场景123】中,来源Task和目标Task是不同的,differentTopTask为true,再经过一系列Task属性对比,能够得出mMovedToFront为true; 而场景0中,来源Task和目标Task相同,differentTopTask为false,mMovedToFront保持初始的false。 由此,我们就能解释【场景0】中,Task不会发生切换的现象。 (5)通过对比flag、Intent、Component等确认是否要将Activity加入到Task中 还是在【场景0123】中,recycleTask()会继续执行complyActivityFlags(),用来确认是否要将Activity加入到Task中,使用mAddingToTask作为标识。 该方法会对FLAG_ACTIVITY_NEW_TASK、 FLAG_ACTIVITY_CLEAR_TASK、 FLAG_ACTIVITY_CLEAR_TOP等诸多flag、Intent信息进行一系列判断。 上图(标注1)中,会先判断后续是否需要重置Task,resetTask,判断条件则是FLAG_ACTIVITY_RESET_TASK_IF_NEEDED,显然,场景0123的resetTask都为false。继续执行。 接着,会有多种条件判断按顺序执行。 在【场景3】中,目标Component(mActivityComponent)是B-3,目标Task的realActivity则是B-2,两者不相同,进入了resetTask相关的判断(标注2)。 之前resetTask已经是false,故【场景3】的mAddingToTask脱离原始值,被置为true。 在【场景012】中,相对比的两个Activity都是B-2(标注3),可以进入下一级判断——isSameIntentFilter()。 这一步判断的内容就很明显了,目标Activity2的已有Intent 与 新的Intent做对比。很显然,场景2中由于改为了setClassName跳转,Intent自然不一样了。 故【场景2】的mAddingToTask脱离原始值,被置为true。 总结看一下: 【场景123】的mMovedToFront最先被置为true,而【场景0】经历重重考验,保持初始值为false。 ——这意味着当有可复用Task时,【场景0】不需要把Task切换到前列;【场景123】需要切换到目标Task。 【场景234】的mAddingToTask分别在不同阶段被置为true,而【场景01】,始终保持初始值false。 ——这意味着,【场景234】需要将Activity加入到Task中,而【场景01】不再需要。 (6)实际启动Activity或直接返回结果 被启动的各个Activity会通过resumeFocusedTasksTopActivities()等一系列操作,开始真正的启动与生命周期的调用。 我们关于上述各个场景的探索已经得到答案,后续流程便不再关注。 四、问题修复及遗留问题解答 4.1 问题修复 既然前面总结了这么多必要条件,我们只需要破坏其中的某些条件,就可以修复业务中遇到的问题了,简单列举几个的方案。 方案一:修改flag。B-3跳转B-2时,增加FLAG_ACTIVITY_CLEAR_TASK或FLAG_ACTIVITY_CLEAR_TOP,或者直接不设置flag。经验证可行。 方案二:修改intent属性,即【场景2】。A-1通过action方式隐式跳转B-2,则B-3可以通过setClassName方式,或修改action内属性的方式跳转B-2。经验证可行。 方案三:提前移除B-2。B-2跳转B-3时,finish掉B-2。需要注意的是,finish()要在startActivity()之前执行,以避免遗留的ActivityRecord和Intent信息对后续跳转的影响。尤其是当你把B-2作为自己应用的deeplink分发Activity时,更值得警惕。 4.2 遗留问题 还记得我们在文章开端的某个疑惑吗,为什么没有回调onNewIntent()? onNewIntent() 会通过deliverNewIntent()触发,而deliverNewIntent()仅通过以下两个方法调用。 complyActivityFlags()就是上文3.3.1.5中我们着重探讨的方法,可以发现complyActivityFlags()中所有可能调用deliverNewIntent()的条件均被完美避开了。 而deliverToCurrentTopIfNeeded()方法则如下图所示。 mLaunchFlags和mLaunchMode,无法满足条件,导致dontStart为false,无缘 deliverNewIntent()。 至此,onNewIntent()的问题得到解答。 五、结语 通过一系列场景假设,我们发现了许多出乎意料的现象: 文档提到FLAG_ACTIVITY_NEW_TASK等价于singleTask,与事实并不完全如此,只有与其他flag搭配才能达到相似的效果。这一flag的注释非常片面,甚至会引发误解,单一因素无法决定整体表现。 官方文档提到START_DELIVERED_TO_TOP会将新的Intent传递给顶层Activity,但事实上,并不是每一种START_DELIVERED_TO_TOP都会把新的Intent重新分发。 同一个栈底Activity,前后两次都通过action或都通过setClassName跳转到时,第二次跳转竟然会失败,而两次用不同方式跳转时,则会成功。 单纯使用FLAG_ACTIVITY_NEW_TASK时,跳栈底Activity和跳同栈内其他Activity的效果大相径庭。 业务中遇到的问题,归根结底就是对Android栈机制不够了解造成的。 在面对栈相关的编码时,开发者务必要想清楚,承担新开应用栈的Activty在应用全局承担怎样的使命,要对Task历史、flag属性、launchMode属性、Intent内容等全面评估,谨慎参考官方文档,才能避免栈陷阱,达成理想可靠的效果。 END 猜你喜欢 Android系统服务DropBoxManagerService详解与实践应用 vivo官网App模块化开发方案-ModularDevToo 本文分享自微信公众号 - vivo互联网技术(vivoVMIC)。 如有侵权,请联系 support@oschina.cn 删除。 本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

优秀的个人博客,低调大师

dubbogo 凌烟阁之 何鑫明

dubbogo 项目已进入第六个年头。dubbogo 项目初期的使命就是"bridging the gap between Java and Go" ,目前 dubbogo 已经对齐所有 dubbo 版本,正与 Dubbo 齐头并进,并在云原生方向反哺 Dubbo。 参与过 dubbogo 项目跟着社区一路走来的人,有贡献者100多人,apache dubbo committer 23 人,其中PMC 5 人。社区基础项目在 https://github.com/dubbogo,孵化成熟后即捐献到 https://github.com/apache,已经总体给 apache 组织贡献了 5 个 项目,整体代码有 17 万行之多。 从本期开始,本账号将陆续采访社区的 PMC/committer,回首各位同学加入社区时的初心,回忆在社区成长的朝朝暮暮,以照亮未来在社区的前行路。 1. 首先回忆下是什么契机让你了解到 dubbogo 的? 最初了解到 dubbogo 是在携程的时候需要为公司的 go 语言内部社区寻找一个可以跟 java 生态打通的 go 语言服务框架,于是就全网找到了当时于雨贡献的dubbo-go 项目。dubbo-go 是当时唯一可以通过 hessian2 协议与 java 进行互通的项目,与我们的需求相符。 2. 参与到 dubbogo 的开源贡献是什么样的体验? 我有幸参与到 dubbogo 1.0 版本的重构建设过程,该版本后面正式捐献给apache,感受到了于雨、北纬等这帮技术人们的坚持和热爱。在过程中收获了很多的技术也包括开源认知、协作方面的成长。对我后续从事架构和开源方面的工作有很深的影响。 3. 支撑你持续贡献 dubbogo 最大的动力,以及给 dubbogo 做出的最大贡献是什么? 开源的动力热爱肯定是第一位的,没有热爱就做不成这件事情。当时在参与重构1.0版本的时候,真的是工作时间和休息时间都在写代码,都在思考怎么让程序扩展性更强、性能更优化。我做的最大的贡献就是跟另一位同事,实操了1.0 版本的整个架构重构和服务框架层代码重写,以及在贡献给 apache 之后的一段时间内,参与后续版本的技术规划和组织社区活动。 4. 贡献中遇到最大的挑战是什么,后面社区给你什么帮助? 最大的挑战其实是重构的难度和维护的压力,当时现有的一个版本离 dubbo 完善的 java 语言版本还是有很大的一块距离,dubbo 有完善的服务治理能力、与各个社区兼容的繁荣生态和非常大的用户群体,要实现与java版本的完全兼容和互通还是比较有难度的。开源之后项目有比较大的曝光,有许多公司慢慢用起来了,又有很多坑和 bug 要解决,这段时间是有比较大的压力的。 5. 成为 PMC 后,你对 dubbogo 未来是期待是什么? 希望 dubbogo 后续在 go 语言微服务体系与云原生这条路上能走的更远,也希望有这类需求的企业和感兴趣的同学能够参与进来,希望 dubbogo 项目能对中国的 go 语言开源生态产生影响力。 6. 还有在参与 dubbo/dubbogo 社区或者其他阿里开源社区中的其他开源项目吗? 暂时没有参与阿里开源社区的其他项目。 何鑫铭,前携程基础中台研发部技术专家,现蚂蚁金服支付中台基础资金平台技术组高级技术专家。专注于 Go & Java、中台架构、中间件与区块链等技术 欢迎加入 dubbo-go 社区 钉钉群:23331795

优秀的个人博客,低调大师

明修"栈"道——越过Android启动栈陷阱

作者:vivo 互联网大前端团队- Zhao Kaiping 本文从一例业务中遇到的问题出发,以FLAG_ACTIVITY_NEW_TASK这一flag作为切入点,带大家探究Activity启动前的一项重要的工作——栈校验。 文中列举一系列业务中可能遇到的异常状况,详细描述了使用FLAG_ACTIVITY_NEW_TASK时可能遇到的“坑”,并从源码中探究其根源。只有合理使用flag、launchMode,才能避免因为栈机制的特殊性,导致一系列与预期不符的启动问题。 一、问题及背景 应用间相互联动、相互跳转,是实现系统整体性、体验一致性的重要手段,也是最简单的一种方法。 当我们用最常用的方法去startActivity时,竟也会遇到失败的情况。在真实业务中,就遇到了这样一例异常:用户点击某个按钮时,想要“简简单单”跳转另一个应用,却没有任何反应。 经验丰富的你,脑海中是否涌现出了各种猜想:是不是目标Activity甚至目标App不存在?是不是目标Activty没有对外开放?是不是有权限的限制或者跳转的action/uri错了…… 真实的原因被flag、launchMode、Intent等特性层层藏匿,可能超出你此时的思考。 本文将从源码出发,探究前因后果,展开讲讲在startActivity()真正准备启动一个Activity前,需要经过哪些“磨难”,怎样有据可依地解决由栈问题导致的启动异常。 1.1 业务中遇到的问题 业务中的场景是这样的,存在A、B、C三个应用。 (1)从应用A-Activity1跳转至应用B-Activity2; (2)应用B-Activity2继续跳转到应用C-Activity3; (3)C内某个按钮,会再次跳转B-Activity2,但点击后没有任何反应。如果不经过前面A到B的跳转,C直接跳到B是可以的。 1.2 问题代码 3个Activity的Androidmanifest配置如下,均可通过各自的action拉起,launchMode均为标准模式。 <!--应用A--> <activity android:name=".Activity1" android:exported="true"> <intent-filter> <action android:name="com.zkp.task.ACTION_TO_A_PAGE1" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </activity> <!--应用B--> <activity android:name=".Activity2" android:exported="true"> <intent-filter> <action android:name="com.zkp.task.ACTION_TO_B_PAGE2" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </activity> <!--应用C--> <activity android:name=".Activity3" android:exported="true"> <intent-filter> <action android:name="com.zkp.task.ACTION_TO_C_PAGE3" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </activity> A-1到B-2的代码,指定flag为 FLAG_ACTIVITY_NEW_TASK private void jumpTo_B_Activity2_ByAction_NewTask() { Intent intent = new Intent(); intent.setAction("com.zkp.task.ACTION_TO_B_PAGE2"); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent);} B-2到C-3的代码,未指定flag private void jumpTo_C_Activity3_ByAction_NoTask() { Intent intent = new Intent(); intent.setAction("com.zkp.task.ACTION_TO_C_PAGE3"); startActivity(intent);} C-3到B-2的代码,与A-1到B-2的完全一致,指定flag为 FLAG_ACTIVITY_NEW_TASK private void jumpTo_B_Activity2_ByAction_NewTask() { Intent intent = new Intent(); intent.setAction("com.zkp.task.ACTION_TO_B_PAGE2"); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent);} 1.3 代码初步分析 仔细查看问题代码,在实现上非常简单,有两个特征: (1)如果直接通过C-3跳B-2,没有任何问题,但A-1已经跳过B-2后,C-3就失败了。 (2)在A-1和C-3跳到B-2时,都设置了flag为FLAG_ACTIVITY_NEW_TASK。 依据经验,我们推测与栈有关,尝试将跳转前栈的状态打印出来,如下图。 由于A-1跳到B-2时设置了FLAG_ACTIVITY_NEW_TASK,B-2跳到C-3时未设置,所以1在独立栈中,2、3在另一个栈中。示意如下图。 C-3跳转B-2一般有3种可能的预期,如下图:预想1,新建一个Task,在新Task中启动一个B-2;预想2,复用已经存在的B-2;预想3,在已有Task中新建一个实例B-2。 但实际上3种预期都没有实现,所有Activity的任何声明周期都没有变化,界面始终停留在C-3。 看一下FLAG_ACTIVITY_NEW_TASK的官方注释和代码注释,如下图: 重点关注这一段: When using this flag, if a task is already running for the activity you are now starting, then a new activity will not be started; instead, the current task will simply be brought to the front of the screen with the state it was last in. 使用此flag时,如果你正在启动的Activity已经在一个Task中运行,那么一个新Activity不会被启动;相反,当前Task将简单地显示在界面的前面,并显示其最后的状态。 ——显然,官方文档与代码注释的表述与我们的异常现象是一致的,目标Activity2已经在Task中存在,则不会被启动;Task直接显示在前面,并展示最后的状态。由于目标Activty3就是来源Activity3,所以页面没有任何变化。 看起来官方还是很靠谱的,但实际效果真的能一直与官方描述一致吗?我们通过几个场景来看一下。 二、场景拓展与验证 2.1 场景拓展 在笔者依据官方描述进行调整、复现的过程中,发现了几个比较有意思的场景。 PS:上面业务的案例中,B-2和C-3在不同应用内,又在相同的Task内,但实际上是否是同一个应用,对结果的影响并不大。为了避免不同应用和不同Task造成阅读混乱,同一个栈的跳转,我们都在本应用内进行,故业务中的场景等价于下面的【场景0】 【场景0】把业务中B-2到C-3的应用间跳转改为B-2到B-3的应用内跳转 // B-2跳转B-3public static void jumpTo_B_3_ByAction_Null(Context context) { Intent intent = new Intent(); intent.setAction("com.zkp.task.ACTION_TO_B_PAGE3"); context.startActivity(intent);} 如下图,A-1设置NEW_TASK跳转B-2,再跳转B-3,最终设置NEW_TASK想跳转B-2。虽然跳C-3改为了跳B-3,但与之前问题的表现一致,没有反应,停留在B-3。 有的读者会指出这样的问题:如果同一个应用内使用NEW_TASK跳转,而不指定目标的taskAffinity属性,实际是无法在新Task中启动的。请大家忽略该问题,可以认为笔者的操作是已经加了taskAffinity的,这对最终结果并没有影响。 【场景1】如果目标Task和来源Task不是同一个,情况是否会如官方文档所说复用已有的Task并展示最近状态?我们改为B-3启动一个新Task的新Activity C-4,再通过C-4跳回B-2 // B-3跳转C-4public static void jumpTo_C_4_ByAction_New(Context context) { Intent intent = new Intent("com.zkp.task.ACTION_TO_C_PAGE4"); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent);}// C-4跳转B-2public static void jumpTo_B_2_ByAction_New(Context context) { Intent intent = new Intent(); intent.setAction("com.zkp.task.ACTION_TO_B_PAGE2"); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent);} 如下图,A-1设置NEW_TASK跳转B-2,再跳转B-3,再设置NEW_TASK跳转C-4,最终设置NEW_TASK想跳转B-2。 预想的结果是:不会跳到B-2,而是跳到它所在Task的顶层B-3。 实际的结果是:与预期一致,确实是跳到了B-3。 【场景2】把场景1稍做修改:C-4到B-2时,我们不通过action来跳,改为通过setClassName跳转。 // C-4跳转B-2public static void jumpTo_B_2_ByPath_New(Context context) { Intent intent = new Intent(); intent.setClassName("com.zkp.b", "com.zkp.b.Activity2"); // 直接设置classname,不通过action intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent);} 如下图,A-1设置NEW_TASK跳转B-2,再跳转B-3,再设置NEW_TASK跳转C-4,最终设置NEW_TASK想跳转B-2。 预想的结果是:与场景0一致,会跳到B-2所在Task的已有顶层B-3。 实际的结果是:在已有的Task2中,产生了一个新的B-2实例。 仅仅是改变了一下重新跳转B-2的方式,效果就完全不一样了!这与官方文档中提到该flag与"singleTask" launchMode值产生的行为并不一致! 【场景3】把场景1再做修改:这次C-4不跳栈底的B-2,改为跳转B-3,且还是通过action方式。 // C-4跳转B-3public static void jumpTo_B_3_ByAction_New(Context context) { Intent intent = new Intent(); intent.setAction("com.zkp.task.ACTION_TO_B_PAGE3"); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent);} 如下图,A-1设置NEW_TASK跳转B-2,再跳转B-3,再设置NEW_TASK跳转C-4,最终设置NEW_TASK想跳转B-3。 预想的结果是:与场景0一致,会跳到B-2所在Task的顶层B-3。 实际的结果是:在已有的Task2中,产生了一个新的B-3实例。 不是说好的,Activity已经存在时,展示其所在Task的最新状态吗?明明Task2中已经有了B-3,并没有直接展示它,而是生成了新的B-3实例。 【场景4】既然Activity没有被复用,那Task一定会被复用吗?把场景3稍做修改,直接给B-3指定一个单独的affinity。 <activity android:name=".Activity3" android:exported="true" android:taskAffinity="b3.task"><!--指定了亲和性标识--> <intent-filter> <action android:name="com.zkp.task.ACTION_TO_B_PAGE3" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter></activity> 如下图,A-1设置NEW_TASK跳转B-2,再跳转B-3,再设置NEW_TASK跳转C-4,最终设置NEW_TASK想跳转B-3。 ——这次,连Task也不会再被复用了……Activity3在一个新的栈中被实例化了。 再回看官方的注释,就会显得非常不准确,甚至会让开发者对该部分的认知产生严重错误!稍微改变过程中的某个毫无关联的属性(如跳转目标、跳转方式……),就会产生很大差异。 在看flag相关注释时,我们要树立一个意识:Task和Activity跳转的实际效果,是launchMode、taskAffinity、跳转方式、Activity在Task中的层级等属性综合作用的结果,不要相信“一面之词”。 回到问题本身,究竟是哪些原因造就了上面的不同效果呢?只有源码最值得信赖了。 三、场景分析与源码探索 本文以Android 12.0源码为基础,进行探究。上述场景在不同Android版本上的表现是一致的。 3.1 源码调试注意事项 源码的调试方法,许多文章已经有了详细的教学,本文不再赘述。此处只简单总结其中需要注意的事项: 下载模拟器时,不要使用Google Play版本,该版本类似user版本,无法选择system_process进程进行断点。 即使是Google官方模拟器和源码,在断点时,也会有行数严重不对应的情况(比如:模拟器实际会运行到方法A,但在源码中打断点时,实际不能定位到方法A的对应行数),该问题并没有很好的处理方法,只能尽量规避,如使模拟器版本与源码版本保持一致、多打一些断点增加关键行数被定位到的几率。 3.2 初步断点,明确启动结果 以【场景0】为例,我们初步确认一下,为什么B-3跳转B-2会无反应,系统是否告知了原因。 3.2.1 明确启动结果及其来源 在Android源码的断点调试中,常见的有两类进程:应用进程和system_process进程。 在应用进程中,我们能获取到应用启动结果的状态码result,这个result用来告诉我们启动是否成功。涉及堆栈如下图(标记1)所示: Activity类::startActivity() → startActivityForResult() →Instrumentation类::execStartActivity(), 返回值result则是ATMS (ActivityTaskManagerService)执行的结果。 如上图(标记2)标注,ATMS类::startActivity()方法,返回了result=3。 在system_process进程中,我们看一下这个result=3是怎样被赋值的。略去详细断点步骤,实际堆栈如下图(标注1)所示: ATMS类::startActivity() →startActivityAsUser() →ActivityStarter类::execute() →executeRequest() →startActivityUnchecked() → startActivityInner() → recycleTask(),在recycleTask()中返回了结果。 如上图(标注2)所示,result在mMovedToFront=false时被赋值,即result=START_DELIVERED_TO_TOP=3,而START_SUCCESS=0才代表创建成功。 看一下源码中对START_DELIVERED_TO_TOP的说明,如下图: Result for IActivityManaqer.startActivity: activity wasn't really started, but the given Intent was given to the existing top activity. (IActivityManaqer.startActivityActivity的结果:Activity并未真正启动,但给定的Intent已提供给现有的顶层Activity。) “Activity并未真正启动”——是的,因为可以复用 “给定的Intent已提供给现有的顶层Activity”——实际没有,顶层Activity3并没有收到任何回调,onNewIntent()未执行,甚至尝试通过Intent::putExtra()传入新的参数,Activity3也没有收到。官方文档又带给了我们一个疑问点?我们把这个问题记录下来,在后面分析。 满足什么条件,才会造成 START_DELIVERED_TO_TOP的结果呢?笔者的思路是,通过与正常启动流程对比,找出差异点。 3.3 过程断点,探索启动流程 一般来说,在定位问题时,我们习惯通过结果反推原因,但反推的过程只能关注到与问题强关联的代码分支,并不能能使我们很好地了解全貌。 所以,本节内容我们通过顺序阅读的方法,正向介绍startActivity过程中与上述【场景01234】强相关的逻辑。再次简述一下: 【场景0】同一个Task内,从顶部B-3跳转B-2——停留在B-3 【场景1】从另一个Task内的C-4,跳转B-2——跳转到B-3 【场景2】把场景1中,C-4跳转B-2的方式改为setClassName()——创建新B-2实例 【场景3】把场景1中,C-4跳转B-2改为跳转B-3——创建新B-3实例 【场景4】给场景3中的B-3,指定taskAffinity——创建新Task和新B-3实例 3.3.1 流程源码概览 源码中,整个启动流程很长,涉及的方法和逻辑也很多,为了便于大家理清方法调用顺序,方便后续内容的阅读,笔者将本文涉及到的关键类及方法调用关系整理如下。 后续阅读中如果不清楚调用关系,可以返回这里查看: // ActivityStarter.java ActivityStarter::execute() { executeRequest(intent) { startActivityUnchecked() { startActivityInner(); } } ActivityStarter::startActivityInner() { setInitialState(); computeLaunchingTaskFlags(); Task targetTask = getReusableTask(){ findTask(); } ActivityRecord targetTaskTop = targetTask.getTopNonFinishingActivity(); if (targetTaskTop != null) { startResult = recycleTask() { setTargetRootTaskIfNeeded(); complyActivityFlags(); if (mAddingToTask) { return START_SUCCESS; //【场景2】【场景3】从recycleTask()返回 } resumeFocusedTasksTopActivities() return mMovedToFront ? START_TASK_TO_FRONT : START_DELIVERED_TO_TOP;//【场景1】【场景0】从recycleTask()返回 } } else { mAddingToTask = true; } if (startResult != START_SUCCESS) { return startResult;//【场景1】【场景0】从startActivityInner()返回 } deliverToCurrentTopIfNeeded(); resumeFocusedTasksTopActivities(); return startResult; } 3.3.2 关键流程分析 (1)初始化 startActivityInner()是最主要的方法,如下列几张图所示,该方法会率先调用setInitialState(),初始化各类全局变量,并调用reset(),重置ActivityStarter中各种状态。 在此过程中,我们记下两个关键变量mMovedToFront和mAddingToTask,它们均在此被重置为false。 其中,mMovedToFront代表当Task可复用时,是否需要将目标Task移动到前台;mAddingToTask代表是否要将Activity加入到Task中。 (2)计算确认启动时的flag 该步骤会通过computeLaunchingTaskFlags()方法,根据launchMode、来源Activity的属性等进行初步计算,确认LaunchFlags。 此处重点处理来源Activity为空的各类场景,与我们上文中的几种场景无关,故不再展开讲解。 (3)获取可以复用的Task 该步骤通过调用getReusableTask()实现,用来查找有没有可以复用的Task。 先说结论:场景0123中,都能获取到可以复用的Task,而场景4中,未获取到可复用的Task。 为什么场景4不可以复用?我们看一下getReusableTask()的关键实现。 上图(标注1)中,putIntoExistingTask代表是否能放入已经存在的Task。当flag含有NEW_TASK且不含MULTIPLE_TASK时,或指定了singleInstance或singleTask的launchMode等条件,且没有指定Task或要求返回结果 时,场景01234均满足了条件。 然后,上图(标注2)通过findTask()查找可以复用的Task,并将过程中找到的栈顶Activity赋值给intentActivity。最终,上图(标注3)将intentActivity对应的Task作为结果。 findTask()是怎样查找哪个Task可以复用呢? 主要是确认两种结果mIdealRecord——“理想的ActivityRecord” 和 mCandidateRecord——"候选的ActivityRecord",作为intentActivity,并取intentActivity对应的Task作为复用Task。 什么ActivityRecord才是理想或候选的ActivityRecord呢? 在mTmpFindTaskResult.process()中确认。 程序会将当前系统中所有的Task进行遍历,在每个Task中,进行如上图所示的工作——将Task的底部Activity realActivity与目标Activity cls进行对比。 场景012中,我们想跳转Activity2,即cls是Activity2,与Task底部的realActivity2相同,则将该Task顶部的Activity3 r作为“理想的Activity”; 场景3中,我们想跳转Activity3,即cls是Activity3,与Task底部的realActivity2不同,则进一步判断task底部Activity2与目标Activity3的栈亲和行,具有相同亲和性,则将Task的顶部Activity3作为“候选Activity”; 场景4中,所有条件都不满足,最终没能找到可复用的Task。在执行完getReusableTask()后将mAddingToTask赋值为true 由此,我们就能解释【场景4】中,新建了Task的现象。 (4)确定是否需要将目标Task移动到前台 如果存在可复用的Task,场景0123会执行recycleTask(),该方法中会相继进行几个操作:setTargetRootTaskIfNeeded()、 complyActivityFlags()。 首先,程序会执行 setTargetRootTaskIfNeeded(),用来确定是否需要将目标Task移动到前台,使用mMovedToFront作为标识。 在【场景123】中,来源Task和目标Task是不同的,differentTopTask为true,再经过一系列Task属性对比,能够得出mMovedToFront为true; 而场景0中,来源Task和目标Task相同,differentTopTask为false,mMovedToFront保持初始的false。 由此,我们就能解释【场景0】中,Task不会发生切换的现象。 (5)通过对比flag、Intent、Component等确认是否要将Activity加入到Task中 还是在【场景0123】中,recycleTask()会继续执行complyActivityFlags(),用来确认是否要将Activity加入到Task中,使用mAddingToTask作为标识。 该方法会对FLAG_ACTIVITY_NEW_TASK、 FLAG_ACTIVITY_CLEAR_TASK、 FLAG_ACTIVITY_CLEAR_TOP等诸多flag、Intent信息进行一系列判断。 上图(标注1)中,会先判断后续是否需要重置Task,resetTask,判断条件则是FLAG_ACTIVITY_RESET_TASK_IF_NEEDED,显然,场景0123的resetTask都为false。继续执行。 接着,会有多种条件判断按顺序执行。 在【场景3】中,目标Component(mActivityComponent)是B-3,目标Task的realActivity则是B-2,两者不相同,进入了resetTask相关的判断(标注2)。 之前resetTask已经是false,故【场景3】的mAddingToTask脱离原始值,被置为true。 在【场景012】中,相对比的两个Activity都是B-2(标注3),可以进入下一级判断——isSameIntentFilter()。 这一步判断的内容就很明显了,目标Activity2的已有Intent 与 新的Intent做对比。很显然,场景2中由于改为了setClassName跳转,Intent自然不一样了。 故【场景2】的mAddingToTask脱离原始值,被置为true。 总结看一下: 【场景123】的mMovedToFront最先被置为true,而【场景0】经历重重考验,保持初始值为false。 ——这意味着当有可复用Task时,【场景0】不需要把Task切换到前列;【场景123】需要切换到目标Task。 【场景234】的mAddingToTask分别在不同阶段被置为true,而【场景01】,始终保持初始值false。 ——这意味着,【场景234】需要将Activity加入到Task中,而【场景01】不再需要。 (6)实际启动Activity或直接返回结果 被启动的各个Activity会通过resumeFocusedTasksTopActivities()等一系列操作,开始真正的启动与生命周期的调用。 我们关于上述各个场景的探索已经得到答案,后续流程便不再关注。 四、问题修复及遗留问题解答 4.1 问题修复 既然前面总结了这么多必要条件,我们只需要破坏其中的某些条件,就可以修复业务中遇到的问题了,简单列举几个的方案。 方案一:修改flag。B-3跳转B-2时,增加FLAG_ACTIVITY_CLEAR_TASK或FLAG_ACTIVITY_CLEAR_TOP,或者直接不设置flag。经验证可行。 方案二:修改intent属性,即【场景2】。A-1通过action方式隐式跳转B-2,则B-3可以通过setClassName方式,或修改action内属性的方式跳转B-2。经验证可行。 方案三:提前移除B-2。B-2跳转B-3时,finish掉B-2。需要注意的是,finish()要在startActivity()之前执行,以避免遗留的ActivityRecord和Intent信息对后续跳转的影响。尤其是当你把B-2作为自己应用的deeplink分发Activity时,更值得警惕。 4.2 遗留问题 还记得我们在文章开端的某个疑惑吗,为什么没有回调onNewIntent()? onNewIntent() 会通过deliverNewIntent()触发,而deliverNewIntent()仅通过以下两个方法调用。 complyActivityFlags()就是上文3.3.1.5中我们着重探讨的方法,可以发现complyActivityFlags()中所有可能调用deliverNewIntent()的条件均被完美避开了。 而deliverToCurrentTopIfNeeded()方法则如下图所示。 mLaunchFlags和mLaunchMode,无法满足条件,导致dontStart为false,无缘 deliverNewIntent()。 至此,onNewIntent()的问题得到解答。 五、结语 通过一系列场景假设,我们发现了许多出乎意料的现象: 文档提到FLAG_ACTIVITY_NEW_TASK等价于singleTask,与事实并不完全如此,只有与其他flag搭配才能达到相似的效果。这一flag的注释非常片面,甚至会引发误解,单一因素无法决定整体表现。 官方文档提到START_DELIVERED_TO_TOP会将新的Intent传递给顶层Activity,但事实上,并不是每一种START_DELIVERED_TO_TOP都会把新的Intent重新分发。 同一个栈底Activity,前后两次都通过action或都通过setClassName跳转到时,第二次跳转竟然会失败,而两次用不同方式跳转时,则会成功。 单纯使用FLAG_ACTIVITY_NEW_TASK时,跳栈底Activity和跳同栈内其他Activity的效果大相径庭。 业务中遇到的问题,归根结底就是对Android栈机制不够了解造成的。 在面对栈相关的编码时,开发者务必要想清楚,承担新开应用栈的Activty在应用全局承担怎样的使命,要对Task历史、flag属性、launchMode属性、Intent内容等全面评估,谨慎参考官方文档,才能避免栈陷阱,达成理想可靠的效果。 END 猜你喜欢 Android系统服务DropBoxManagerService详解与实践应用 vivo官网App模块化开发方案-ModularDevToo 本文分享自微信公众号 - vivo互联网技术(vivoVMIC)。 如有侵权,请联系 support@oschina.cn 删除。 本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

用户登录
用户注册