写点什么

RingCentral Android 启动优化实践

  • 2022 年 7 月 05 日
  • 本文字数:3094 字

    阅读完需:约 10 分钟

RingCentral Android启动优化实践

背景

背景

RingCentral Android 在 Playstore 控制台收集到了大量 ANR 记录,最普遍的 ANR 场景是收到 FCM 推送导致的启动 ANR,如下图:


上面就涉及到 Android 传统启动流程中典型的问题:如何权衡复杂的启动任务和性能之间的平衡,如果偏向于性能而采取大量并发操作,就会使得逻辑过于混乱,后期难以维护;反之如果偏向于逻辑结构而放弃并发优势就会使得启动耗时过长从而导致一系列体验和 ANR 问题。

 

因此启动问题一直是每个应用要时刻面对的问题,下面就来简单分析一下现状以及解决方案。


现状分析

目前比较常用的启动方案有两种:

1、Application onCreate 阻塞 UI 线程直到启动任务全部结束才继续往下走

优点:

能够保证功能使用前所依赖的初始化全部完成,这样就能正常使用


缺点:

阻塞 UI 线程在正常启动流程中一般没什么问题,但是像 BroadcastReceiver 和 ContentProvider 这种在应用未启动情况下仍可以接收消息的组件,一旦接收到消息就会触发应用启动,并且等待 Application onCreate 结束,在等待过程中就有可能发生 ANR。(例如:对系统进程(AMS) 而言, 如果从发出启动 receiver 命令到收到 receiver 完成任务的回调之间的时间间隔超过 10s, 系统就会判断 app 发生了 ANR)


2、Applicaiton onCreate 不阻塞 UI 线程,将全部初始化任务抛到一个异步线程,依赖启动初始化的地方基于事件驱动方式设置回调任务

优点:

不阻塞 UI 意味着第一种方案中

BroadcastReceiver 和 ContentProvider 的问题就可以避免,不会出现等待 ANR


缺点:

同样带来的问题也很明显:

  • 不阻塞 UI 意味着后面所有依赖启动初始化的地方都要预先判断启动是否结束才能开始执行任务,也就是要引入同步机制,并且包括 Activity 在内的 Android 四大组件都需要增加判断,范围比较广。


  • 由于担心任务之间依赖的复杂关系,主要抛到一个异步线程,并没有充分利用并发的优势,所以对启动耗时并没有太大的改善


Ringcentral (RNG) Android 启动

RNG 主要功能模块包括:

  • Message

  • Video

  • Phone

由于功能比较多,初始化时需要依赖包括 coreLib、pal、thirdPartySdk、account 等多个组件初始化,起初的方案是按照传统方式在 Application->onCreate 阻塞 UI 线程,如下为 FCM 唤醒后的处理流程:


这种启动方式主要遇到两个问题:

  • 初始化任务多,但是每个任务都是必须的,无法进行延迟优化


  • 由于 UI 线程阻塞过久,正常启动可能影响还小一点,但是通过系统方式唤醒(比如被 FCM 消息唤醒)来处理消息就会受到 Android 四大组件 ANR 限时影响,从而更容易出现 ANR


面对以上问题,我们引出了下面的方案思考和实践。


解决方案

目前网上有很多启动优化相关的解决方案,大多分为以下四种:

  • 延迟

  • 预加载

  • 方法耗时优化

  • 异步化

从上面可以看出基本是针对启动速度的优化,但是有一个问题我们可以考虑一下:我们针对启动的优化基本都是后期出现问题然后再去想办法解决,那有没有可能我们从导致问题根源上进行优化,这样就能防患于未然,岂不是更好?

针对上面的思考,这里引入了一个方案,即通过框架的束缚来从根源上减少启动耗时的劣化,接下来让我们开始这次 RingCentral 启动优化实战的介绍。


RingCentral 如何解决之前提到的两种启动方案问题呢?


1、针对 Android 四大组件启动带来的 ANR 问题, RNG 引入了 aync launch 机制(内部自研框架),如下:


通过 async launch 机制来将启动任务抛到异步线程,等待任务结束再唤醒所有依赖的它的任务,这样在不阻塞 UI 线程的同时也能保证启动任务正常结束。

注意:Android 依赖启动的四大组件在执行前都需要根据 LaunchWaiter 的状态来走不同流程,LaunchWaiter 提供了状态判断以及 runAfterLaunch 方法给接入方使用,需要在启动任务全部结束才能执行的 Task 可以注册到 runAfterLaunch 里。

举个例子,比如接收消息的 Service 此刻就可以按照下面这种方式来实现:


LaunchWaiter.runAfterLaunch 其实就是基于事件驱动方式添加回调任务,等初始化任务结束后就会执行回调。


补充:LaunchWaiter 使用了一个后台线程执行 app 中核心的启动逻辑(包含三方库和 so 文件的加载、初始化逻辑)。但是在 app 以后台方式启动的情况下(比如被 FCM 消息唤醒),它的进程运行是非常慢的,即使是主线程,线程优先级也比前台启动情况下低,因此 LaunchWaiter 还会把所使用的后台线程的优先级设置到最大(Thead.MAX_PRIORITY),从而加快启动任务的处理。


2、针对启动任务在异步线程无法充分利用并发优势的问题,RNG 引入了 Anchor 框架来基于任务链的方式执行任务,如下:


通过像 CountDownLatch 这种等待机制,我们可以在异步线程里面等待 anchor task 执行完毕,比如上图的 TaskE 和 TaskF 执行结束就可以触发 async launch 唤醒机制。


3、最终通过结合 async launch 机制和改造后的 Anchor,我们既能解决启动 ANR 问题,又能充分利用并发优势,同时梳理了任务依赖链,有利于后期的扩展维护。下面是 RNG 优化数据,可以看出启动耗时优化了 40%多。


Anchors 框架详解

1.总体架构图


可以看到 Anchors 框架主要分成三大部分,分别是

  • 应用层

  • Anchor Core

  • Business Task

其中最关键的在于 Anchor Core,我们先来看一下它的具体内容

1)AnchorManager

AnchorManager 作为整个 Anchors 框架对外暴露的接口,主要发挥对外接收配置,对内容器作用,可以看一下它的使用方式:


可以看到使用很简单,主要设置包括

  • TaskFactory: 作为 Anchor 内部根据 TaskId 创建具体 Task 的工厂

  • anchors:设置 anchor task,需要阻塞 UI 线程就必须设置

  • graphics:Task 依赖链, 采取图结构,TaskA.sons(TaskB) 代表 TaskA 执行结束是 TaskB 任务开始的一个必要不充分条件


2)AnchorRuntime

AnchorRuntime 作为 Anchor 框架内部真正处理任务的核心类,负责了所有任务的调度以及依赖链的处理,可以来看一下它的核心逻辑:


上面是 AnchorRuntime 阻塞 UI 线程等待所有 anchor task 执行结束的核心逻辑,可以看到只要还有 anchor task 存在,这里就会不断循环,同时执行需要在 UI 线程跑的任务,这就是 "钩子” 的来源。


3)Task

Task 在 Anchors 框架里面构建基于图的依赖关系发挥了重大作用,它拥有两个核心变量:

  • behindTasks(依赖它的任务列表)

  • dependTasks (它依赖的任务列表)

让我们来看一下这两个变量如何处理任务的依赖关系


首先看看一开始提到的 sons 函数:


可以看到 TaskA.sons(TaskB) 主要作用是调用了 TaskB 的 dependOn,我们继续看一下这个函数:


dependOn 作用其实就是把 TaskA 加入到 TaskB 的 dependTasks 列表,相当于说 TaskB 依赖了 TaskA,同时把 TaskB 加入到 TaskA 的 behindTasks 列表,相当于说 TaskB 是 TaskA 的后置任务。


Anchors 框架能够保证按照任务依赖链正常有序的执行任务,核心逻辑主要就是:

  • 初始化时每个任务确定各自的 behindTasks 和 dependTasks 

  • 每个任务结束时通知它所有的 behindTasks 从其 dependTasks 中删除当前任务,同时检查 dependTasks 是否为空,为空代表所依赖的任务都已经执行完毕,这时候就能启动自身任务

  • 当任务链上面所有任务执行结束就意味着 Anchors 任务已经完成,这时候就可以结束 UI 线程的阻塞


R/整合后的主要流程

1、RNG 启动流程


RNG 这里结合了 LaunchWaiter 和 Anchor 两个启动框架,其中 Anchor 不使用自身的 anchor task 机制阻塞 UI 线程,而是通过 CountDownLatch 来替代原先的阻塞机制,这样做的好处就是能够使得异步线程监听 anchor task 执行情况,实现了”异步阻塞版 Anchor”


2、任务依赖 & 执行流程


任务的依赖和执行在上面也已经介绍过了,这里面的重要角色就是 behindTasks 和 dependTasks,每个任务触发的时机是依据它的 dependTasks 是否全部执行完毕。


总结

RNG 结合了 async launch 机制和 Anchor 机制,建立了基于图的任务链结构,梳理了启动任务之间的依赖,后续如果新增启动任务就可以根据当前任务链放到合适的位置,由于每个任务的依赖关系比较清晰,选择位置时就能更好的利用空余时间片,提高启动效率!


发布于: 刚刚阅读数: 3
用户头像

RingCentral铃盛中国研发中心 2021.11.24 加入

全球云商务通信与协作解决方案领导者,连续七年荣膺Gartner UCaaS(统一通信即服务)魔力象限全球领导者。与你分享各种技术专家的文章、公开课,各种好玩有趣的活动与福利,以及最新的招聘机会。

评论

发布
暂无评论
RingCentral Android启动优化实践_android_RingCentral铃盛_InfoQ写作社区