背景
长久以来,随着App版本的迭代,各项性能指标都会随着业务的迭代,代码的堆叠而出现逐渐下滑的趋势。以手机天猫Android9.1.0版本为例,整体启动耗时一直在增长。其中50多个启动任务在首页加载之前就需要初始化,再加上首页复杂的业务逻辑,进一步地增加了app启动的耗时,这样就极大地影响到了用户的使用体验。
考虑到用户千人千面的交互行为,每个用户在应用启动初期进行的操作不同,涉及到的启动任务也各有不同,因此一成不变的启动任务初始化势必会导致启动资源的浪费。为此,我们探索了用户个性化行为和启动任务调用之间的关联,通过算法的能力实现启动任务初始化的个性化编排和千人千面,对不同的用户编排不同的启动任务加载策略,在降低应用启动耗时的同时,保障用户交互使用的流畅体验。
目前的问题
首先来分析下目前手猫Android端启动耗时严重,遇到的重要阻碍和问题是什么。
目前有50多个任务在MainActivity初始化之前就必须要执行完成。其中包括了网络sdk,图片sdk,很多中间件及其他一些基础功模块的初始化,同时还包括了很多手猫自身业务的初始化逻辑,这些任务有一些并没有联系但是却因为历史原因牵连在一起导致出现无人敢动的情况。
总结下来主要是下面三种情况,第一,有一些任务和sdk由于历史原因已经不在使用,但是因为相互之间的代码牵连,并没有人能够明确是否可以直接下线。第二,有一些任务并不需要在启动时就必须执行。第三,还有一些任务也是在某些业务场景下才会触发。并不需要直接放到启动时进行。
现有方案
在分析了上面的问题之后,也参考了目前市面上大部分的技术方案,大家都有很多的精髓,从中吸取到很多精华。基于已有的方案我发现,现有的方案基本上都将启动任务进行梳理,剔除不再使用的启动任务。将其他任务按照优先级进行了分块分层,在无法完全确定优先级顺序的任务中,需要与业务同学进行沟通对焦,明确优先级。同时将比较大的任务拆分成多个小任务。将这些任务进行编排执行,一般是分成三个阶段。主线程串行阶段,异步首页前执行阶段,闲时阶段。
目前的这些方案多会遇到一些问题。比如:
- 人为决策而非数据决策
大量的启动任务,有一些业务方提出他们的业务很重要,需要提前到启动首页初始化之前,但是因为我们无法判断这个任务是不是真的这么重要,只能进行人为的拍板。
- 容易形成盲点无人了解,隔段时间就得进行重构
有一些任务可能在某些时间阶段需要提前,其他时间一般不会用到或者可能就是已经下线的业务,但是如果是硬编码加上人员变动,也没人知道某个任务到底是否还在用也不敢进行下线,后续的维护成本就会很高。
- 重构整理时耗费时间需要找各个团队对焦
需要进行业务启动任务调整的时候要找所有初始化任务的业务方,每个每个对焦,确保这个任务延后执行也不会对业务有影响。
- 无法针对不同用户的行为做到区分
目前的任务编排大都是在本地硬编码的,顺序固定,大家都“一视同仁”。而每个用户在使用过程中的行为都是千人千面的,并没有将这些数据很好的利用起来。
新的解决方案
- 启动任务动态化
目前手猫整体的50多个启动任务,都是在本地进行硬编码写死的,每个启动任务都可能有自己的线程池,io操作,在设备硬件条件有限的情况下,对于CPU占用,内存占用都会非常紧张,且每个sdk对于初始化的时机、需要初始化的进程、初始化的线程等都各有要求。现有方案是将任务分成了三个阶段,不同阶段的任务在不同阶段执行。如果需要动态进行编排,基于现有的方案无法通过算法进行动态改变。因此首先我们将50多个启动任务进行抽离,放在统一的管理器里进行管理。同时将原有的写死的任务执行顺序修改成通过动态下发的编排脚本的方式进行任务的编排和启动。
- 设计启动任务编排方式,提供下发变更逻辑
由于任务本身之间的关联关系,C任务可能需要在A、B两个任务执行之后才能执行。整体的任务编排符合有向无环图,因此我们在读取的时候采用反向设计,将每个任务按照链表的思想进行设计,在每个任务之下,将当前任务的父任务都挂在这个任务后面,以此类推。方便后面读取时直接关联父任务。
- 通过AOP插装技术埋点
通过AOP插装技术,统计出每个启动任务在首页启动之后并多长时间才开始第一次使用到。为什么要是用aop,因为目前使用到的一些启动任务,其中分成了两类,一部分在自己的代码中有过一次封装可以直接进行埋点。但是另外一部分是三方库,不同的业务方使用的地方也很分散比较难统计,这时候就需要aop技术来进行统一的埋点了。
我们采用的技术方案是Gradle plugin+Transform+ASM,根据上图可以明白,因为有一些三方库我是没有源码的。只能在打包的过程中在由class打包进dex的过程中修改字节码进行代码插桩。大家知道通过gradle插件通过task执行的时候,上一个task的输出必须正确灌入到下个task的输入中才行。因此我扩展了gradle-plugin中增加了自己的一部分代码插桩逻辑,完成了多个中间件sdk埋点的工作。具体步骤如下图:
另一方面,虽然经过算法编排之后根据相应的算法模型指标,能够解决大部分用户的使用时出现的降级问题,但是仍然会有一些漏网之鱼,为了防止用户在使用时出现降级情况,我们在用户第一次使用某个启动任务之时,如果发现这个任务并未初始化完成,则重新进行一次初始化,减少用户在使用时出现降级的问题。
4.稳定性验证
完成代码插桩工作之后,进行了大量的启动性能测试和整体的稳定性测试,确保启动任务的修改不会对app的使用产生影响,使用的手机是vivo Y67,通过低端机进行monkey等之后开始灰度验证,确保整个性能测试过程中无影响的crash,anr和启动白屏或无法启动等情况出现。
- 启动任务编排策略的千人千面
前文提到了启动任务原本都是在app启动阶段进行初始化加载的,对于那些完成初始化加载,但是并没有很快被用户调用的情况,这样会浪费资源,增加启动耗时;那么如果将所有设备的启动任务全部推迟在用户交互阶段的闲时进行加载呢?这样必然可以降低设备的启动耗时,但是,也会在用户体验方面造成一些负面影响。为了清晰的描述启动任务加载和调用的过程,分析什么样的情况适合将启动任务推迟在闲时加载,什么样的情况应当保持启动任务在启动阶段加载,我们将启动任务的加载调用过程分成了以下3种情况:
启动阶段调用:app启动阶段调用任务,此时app启动尚未完成,如果推迟至闲时加载,调用任务时需等待任务加载。
闲时阶段调用1:app启动完成后,用户交互阶段调用任务,但是调用时间较早,如果推迟至闲时加载,调用任务时,任务没有足够时间加载完成,仍需等待任务加载。
闲时阶段调用2:app启动完成后,用户交互阶段调用任务,但是调用时间较晚甚至不调用任务,如果推迟至闲时加载,不会造成不利影响。
通过上述分析,我们可以确定对于同一个启动任务,全部推迟至闲时加载并不是最好的选择。对应启动阶段调用和闲时阶段调用1这两种情况,如果推迟任务至闲时加载,会在调用任务时出现等待任务加载的情况,对用户造成不好的体验。因此,我们希望利用算法的能力,尽可能的找出符合闲时阶段调用2情况的设备,只将这部分设备的任务推迟,实现启动任务加载的千人千面,不仅有效地降低启动耗时,同时提升用户体验。为此,我们以A启动任务作为试点,开始个性化编排的探索。
为什么要进行个性化的探索,我们认为可以从用户和设备两个维度来思考个性化这件事:
用户维度:每个用户有不同的习惯,有的人打开app会立马搜索商品,有的人会先浏览猜你喜欢等页面,那么可能不同的使用习惯,可能导致任务在不同的时候被调用;
设备维度:不同品牌的设备性能不同,加载客户端的机制和效率存在差异,对于高端机,不需要推迟任务就可以实现快速启动,而低端机需要更快的启动时间来保证用户体验;
综上所述,我们从两个维度构建了特征数据,涵盖了用户的基础信息、行为数据、业务数据、设备性能、A任务的埋点数据等。
对于同一个用户和设备,其在不同时刻使用app的状态不同,启动任务的调用情况也不尽相同,因此理论上应当实现启动任务的实时编排,但是考虑到频繁的变更启动策略可能对客户端稳定性造成影响,我们更多地选择用户和设备的长期数据进行预测。
前文中我们提到,找出闲时阶段2的设备,并将其任务推迟是我们的目标。然而从实际数据中可以看出,每个设备的启动数据并非一成不变,在不同状态下可能出现启动阶段调用、闲时阶段调用1和闲时阶段调用2这三种情况,而且由于用户的活跃度不同,有的用户登录频繁,而有的用户几天只来一次,可利用的数据量不同。因此,相比直接将问题定义为二分类问题,我们综合设备在采集周期内的调用A任务次数all,和调用A任务在不同阶段次数s1,s2,s3对设备打分标注,圈出高分设备推迟A任务,低分设备维持A任务在启动阶段不动:
由于启动任务千人千面的一期工作更多是有效性的探索验证,因为我们采用了集团内高效的PS-SMART算法完成模型训练,并基于模型预测结果对设备进行圈选,区分适合启动阶段加载和闲时阶段加载的设备,最终将配置同步到客户端,在设备初次启动时拉取配置,改变后续启动策略,整体过程如下图:
针对线上实验桶,我们利用算法圈出部分的设备在启动阶段加载的个任务,其余的设备推迟至闲时阶段加载。综合线上数据,设备的启动耗时优化下降了170ms。
遇到的问题和解法
因为本次针对启动优化的改动设计的影响面比较大,一旦出现问题可能就会导致app无法打开或者启动crash的问题,造成的问题也是比较严重。因此我们针对异常的情况采用了一些降级方案。
首先,当监测到用户有三次启动crash时则直接降级到旧版本的代码方案。
第二,因为更新过来的新脚本是保存在sp中的,这样就难免会出现丢失的情况,因此增加了兜底策略,在assets和内存中都保存了一份原始脚本文件。确保最终都有一份启动任务脚本可以执行。
第三,在任务编排之后,难免会出现某些用户成为算法的漏网之鱼,导致编排到启动之后的任务在很早就开始掉用了。为了解决这个问题,我们在启动任务第一次使用时进行判断,如果该任务已经还未进行初始化则直接手动拉起一次。确保不影响业务的正常运转。
第四,为了减少和防止出现较大的问题,增加了一键切换的总开关。
数据成果
经过整体的对启动任务的智能编排。目前有5个任务进行了智能化的延后。目前整体启动时间下降了0.8秒,90%启动分位数下降了1.5秒,低端机下降了1.6s。
总结和展望
在文章完成时,我们已经具备了通过启动任务第一次使用的数据埋点来分析并进行任务顺序调整的能力,改变了以前排查下线启动任务困难,只能通过业务对焦和手动测试变更启动任务的问题。
同时也第一次在应用启动时使用到了算法的能力,使用了几个启动任务作为试点,让大数据和算法能力不仅仅只能局限在内容和商品推荐上,扩展了算法能力再端上应用的边界。后续我们也会在更多的启动任务中进行尝试。
再次,用户体验,特别是启动优化也是一个需要长久投入和分析研究的事情,我们也会进一步思考更好的方式来得到更好的用户体验。
【关于我们】
手猫技术团队目前致力于拓展新零售业务下的新场景,发掘业务的新形态。同时也在持续地探索与AI人工智能结合,一方面扩展算法的使用边界,另一方面通过大数据让App变得更智能。在这里你可以接触到很多无线开发之外的创新技术,扩展技术的宽度。欢迎加入我们:dilun.yx@alibaba-inc.com