图片来自:unsplash.com/photos/_2mL… 本文作者:ZZG

前言

作为 APP 体会的重要环节,发动速度是各个技能团队注重的要点。几百毫秒发动耗时的增减都会影响用户的体会,并直接反响在留存上。心遇 APP 作为一款用于满意中青年市场用户交际诉求的运用,对各个功用层次的手机类型,都要求有良好的发动体会。因而,随着用户量快速增长,发动优化作为一个功用专项被提上了日程。

发动优化,顾名思义,便是优化用户从点击 icon 到主页彻底可见这一进程的时长。为了能更好地对发动时长进行衡量,咱们将它分为发动阶段和主页首刷这两部分。发动阶段即是从点击 icon 到主页首帧展现为止。主页首刷阶段则是记载从主页首帧可见到主页彻底可见的时长。

经过 5 个月的优化实践,心遇线上均匀发动时长从 8 秒多下降到 4 秒左右,发动时长减幅超越 50%,其中发动阶段下降 3.7秒,首帧首刷时长下降了 0.4秒。发动优化作为功用优化项意图重要组成部分,现已成功达到了预期的基线方针。

优化实践

本文将介绍心遇团队在发动优化上所做的作业,以及在优化实践中所取得一些感悟。

运用有三种发动状态:冷发动,温发动和热发动。本文首要注重冷发动的耗时。首要咱们要了解,发动优化优化的是哪几个进程:

心遇 Android 启动优化实践:将启动时间降低 50%

在冷发动开始时,体系进程首要会履行一系列操作,并终究创立出运用进程,然后由运用进程履行主线程发动,页面创立等使命。这个流程其实涉及到的点有许多,但依据削减从发动到主页展现这段主链路的时长这个方针来讲,咱们能够将作业聚集于三个阶段: Application 创立,主线程使命, Activity 页面烘托。在后续的优化中,咱们也是着重优化这三个阶段的耗时点。

为了能够更好地阐述各个优化办法的完成和带来的收益,咱们以 oppo A5 手机为例进行阐明。

这是优化前的 App 在 oppo A5上 的各个阶段的耗时。

心遇 Android 启动优化实践:将启动时间降低 50%

从点击 icon 到主页彻底可交互,这个阶段的耗时达到了 19 秒。oppo A5 作为一款功用较差的手机,固然会对发动时长有必定影响,可是 App 发动进程中各种不合理的逻辑和代码完成才是整个冗长发动流程的首要原因。经过一系列的优化作业,App 各个发动流程的耗时如下所示:

心遇 Android 启动优化实践:将启动时间降低 50%

整个发动耗时缩短至 9 秒,优化作业收益在10秒左右。接下来,咱们会分阶段阐明这10秒的收益是怎么完成的。

Application 优化

Application 阶段一般用于初始化比较中心的事务库。在运用开发早期,咱们并没有对这个阶段的发动使命进行管控,导致这儿往往会堆积有很多的强事务相关的代码。在接手这个优化项现在,整个 Application 中履行的使命有 90 多个。在后续的优化中,咱们对整个使命流程进行了精简,依据的原则是:

  • Application 中的使命应当是大局根底使命
  • Application 创立时应当尽量削减网络恳求操作
  • Application 创立时不允许有强事务相关的使命
  • Application 创立时尽量削减有 Json 解析处理和 IO 操作的作业

优化后,Application 中的发动使命被削减到了 60 多个,首要分为根底库初始化,功用装备和大局装备这三大类。根底类库首要是对网络库,日志库等根底库进行初始化装备,除了主进程外,其他的进程也依靠这些使命,移除它们会对大局的稳定性形成影响。它们也是发动使命中占比最大的,耗时最多的一类使命,因而下降它们的耗时也是后边继续优化的要点。功用装备首要是对一些大局相关的事务功用的前置装备,例如对事务缓存的预加载,特定事务前置等,移除它们会形成事务有损,在这种状况下,咱们需求找到事务诉求和功用装备之间的平衡点。大局装备首要是关于大局 UI 装备,文件路径的处理操作,它们占比少,耗时少,是主页创立的前置使命,因而暂不处理。

使命排布的中心是处理好使命的前后依靠问题,这个需求开发者对事务逻辑有着比较深的了解,由于每个运用都不相同,因而这儿不做打开。咱们首要对心遇在使命排布和优化中一些细节进行介绍:

  • 依据进程进行使命排布。心遇在运行中会发动多个进程,它们用于完成特定的使命,便利模块阻隔。许多进程,例如 IM 进程,一般只需求发动 Crash SDK 和网络 SDK 等极少数中心 SDK 初始化作业。关于这类进程,假如依照主进程的流程履行一切的主链路代码,会形成不必要的资源糟蹋。为此,咱们会对 Application 的使命进行详尽区分,将使命的运行精密到进程级,防止在非主进程中履行了不必要的使命。
  • 懒加载。这儿首要是对一些根底使命进行改造,将使命初始化和使命发动拆分开来,将发动作业移出 Application 创立流程,一起对其进行精简,去除冗余逻辑。在创立方针时,能够推延其成员方针的创立,灵敏运用 by lazy 等要害字,使方针轻量化。
  • 进程收敛。多进程能够完成模块阻隔,一起防止单个进程内存占比过高导致的内存上限约束,其下风在于多进程会导致运用全体内存占用过多,触发低内存的概率更高。此外,假如运用发动时内存占比过高,或许会导致手机进行内存收回,占用很多的CPU资源,这反响在用户的体会上便是发动慢,运用卡。比照多进程的优劣,咱们现在选用的战略是尽量拖延主进程以外的进程发动,一起经过进程兼并来削减进程的数量。经过这些战略,咱们终究有用地将发动时的进程数下降至两个。结合使命排布作业,咱们为每一个进程都筛选了最简的使命集,防止它们履行不必要的使命,形成资源糟蹋,这些作业终究使得进程发动占用的内存大大削减。
  • 线程收敛。关于多核CPU来说,恰当的线程数量能够提高功率,可是假如线程泛滥则会导致 CPU 负载过重。多线程并发,本质上便是多个线程轮番获取 CPU 运用权的进程。在负载超重的状况下,过多的线程争抢时刻片,除了下降发动速度外,也会导致主线程卡顿,影响用户体会。在做这方面优化时,需求保证大局运用一致的线程池。一起许多二方和三方 SDK 也是创立子线程的大户,这时候需求和相关的技能部门进行沟通,去除不合理的线程创立。另一方面,防止在发动阶段进行网络恳求,也是削减线程数的要害所在。

Application 优化是整个发动流程的要害,合理的使命编列不只能够下降 Application 的创立时长,对后续的主页创立也有非常大的优化作用。现在,在 oppo A5 手机上,心遇 Application 的创立时刻从 5 秒下降到了 2.5 秒左右,而且还有很大的优化空间。

发动链路

履行完 Application 创立之后,运用进程的首要作业便是创立 Activity。这儿需求留意的是,从 Application 到 Activity 里还藏有许多 post 到主线程中的使命,以及注册的 ActivityLifecycleCallbacks 的回调监听,它们会偷偷添加从 Application 到 Activity 的时刻空隙。ActivityLifecycleCallbacks 注册一般和事务相关,它的注册比较隐蔽。在之前事务开发中,咱们确实存在着必定的 ActivityLifecycleCallbacks 滥用的状况,这需求咱们注重起来。

关于主线程音讯耗时,咱们在运用 Profiler 和 Systrace 对发动流程进行耗时定位时,就发现了许多这样的问题。由于各种原因,Application 中使命会将耗时的作业 post 到主线程中来,表面上看 Application 的创立时刻缩短了,可是总体上发动时刻却被扩展了。关于耗时点应该定位到根本原因进行处理,而不是一味地 post 出去,这样治标不治本。

其次,缩短发动到主页的链路,是咱们优化的要点。

心遇 Android 启动优化实践:将启动时间降低 50%

在原有的发动流程中,loading 页面作为心遇的发动页面,承当有路由和权限恳求两个使命。

  1. 在一般状况下,用户发动 App,在 loading 页面判别是否登录,假如未登录,则进入登录页面
  2. 假如用户现已登录,则判别是否需求展现开屏页,假如需求则进入开屏页,比及开屏完毕,就跳回 loading 界面,再进入主页。

从上可知,即使没有开屏页,用户发动 APP 到展现主页,最起码要经过两个 Activity 的发动。发动链路缩短的中心在于将 loading,main 和开屏页兼并为一个页面。这样做不只能够最起码削减一次 Activity 的发动,一起也能够在展现开屏页时并行处理其他的使命。

心遇 Android 启动优化实践:将启动时间降低 50%

这儿的主页更像是一块画布,主页界面的创立和烘托是其中的一笔。

完成这个功用的代码逻辑比较简略,便是将 main 页面设置为发动页,主页和开屏页封装为两个 fragment,依据事务逻辑进行展现。用户点击 icon 进入到主页,假如判别已登录,则履行主页前置使命和主页 UI 烘托,一起判别是否加载开屏页 fragment。值得留意的是,咱们并没有去除 loading 页,当判别用户未登录的状况下,就会进入 loading 页面,进行原有的打点和登录路由的作业。由于在绝大多数状况下,用户都是在已登录的状况下运用运用,所以这么做的收益最大而且批改本钱最小。

为了完成这个流程,咱们需求处理好主页实例和主页使命编列的问题:

主页实例

主页原有的 launchMode 是 singleTask,这么做的意图是为了保证大局只有一个主页实例。可是咱们在改造后将主页设置为发动页,假如继续将主页设置为 singleTask,会形成事务 bug:当咱们从二级页面退到后台,在点击图标时回到前台时,会跳到主页,而不是原先的二级页面。这儿的原因能够简略了解为,点击图标时,体系会调用 launcher 特点的主页。由于栈中存在主页实例以及它的 singleTask 特点,导致体系会运用这个已有实例,并将它上面的 activity 都 pop 出栈,终究形成这个反常状况呈现。处理的计划是选择 singleTop 作为主页的 launchMode。singleTop 并不能保证主页实例的大局唯一性。好在心遇 APP 完成了 router 跳转的功用,能够经过一致的 url 打开主页。咱们在发动主页 Activity 的终究一步,在 intent 中添加 FLAG_ACTIVITY_NEW_TASK 和 FLAG_ACTIVITY_CLEAR_TOP 的 flag,以完成了 singleTask 的作用。这个计划根本能够满意咱们的需求,可是也不排除在特定状况下,呈现直接发动主页的操作。为此咱们注册了 Application.ActivityLifecycleCallbacks,对栈里的 activity 实例进行监控,当栈中呈现多个主页实例时,对新的主页实例进行清除,并进行提示。

使命编列

经过改造后的主页并不是一个传统意义上的页面 Activity,而是承载了一连串使命履行的容器。包含在后续的改造中,咱们会将主页的数据恳求和 UI 烘托抽离开来,此外一部分高优的事务使命也被从 Application 中抽出放到了主页中,为了有用办理这些使命的前后依靠联系,咱们需求一个有向无环图对这些使命进行办理。

心遇 Android 启动优化实践:将启动时间降低 50%

使命编列的中心思维便是使命打散,错峰加载,将一个低优先级而且高耗时的使命拖延,或许放在更往后的闲时作业流中去进行履行,一起也要保证使命之间的前后依靠联系,保证不要犯错。合理地区分事务使命的颗粒度而且对它们进行排序是决议图运行速度的要害,也比较检测开发对事务的了解程度。现在业内现已开源的关于有向无环图的计划有许多,比方 alpha 等,为了适配心遇特定的事务需求,团队内部也开发一套发动结构用于完成对主页使命的编列。

关于主页使命的加载,咱们初步提出了作业流的概念。咱们将发动使命区分为了三个作业流阶段,包含根底作业流,中心作业流和闲时作业流。整个发动流程中的事务逻辑都被咱们拆成了相对独立的使命,并依照优先级和依靠联系分配到了这三个作业流中。

  • 根底作业流:这个阶段首要是履行 Application 的创立,用于放置网络库,监控等根底 SDK。 这个作业流的使命要求尽量得少,而且都是后续作业流的前置使命。
  • 中心作业流:在这个阶段中,会放置中心的事务作业,除了一些中心事务的初始化作业之外,还包含主页 UI 的烘托,事务数据的恳求以及开屏页的展现。这些使命依据有向无环图进行摆放和办理,从主页创立时开始履行。由于这个阶段现已进入到了主页,因而为了让用户能尽早看到第一帧,咱们需求尽或许地将事务数据获取和主页烘托的使命提早。
  • 闲时作业流:这个阶段首要适用于放置一些优先低,耗时长而且对完成时刻不做要求的使命。关于对闲机遇遇的判别有好几种办法,心遇这边做了简略处理,即在中心作业流完毕 10 秒后,在 IdleHandler 中进行履行的。假如希望比较精确地断定闲机遇遇,能够经过往主线程中 post 音讯,统计主线程中音讯间隔时长和运用内存水位监控相结合的计划进行断定。

运用发动结构来办理发动使命的优点在于能够使得中心事务提早加载完成,一起也能够将使命细粒度化。例如为了使得主页更快地展现,咱们将主页的数据恳求和 UI 烘托相剥离。将主页的数据恳求提早到了 Application 中。这个低端机上优化作用显著,数据恳求的 call 方针创立和 Json 解析在低端机上耗时严峻,经过运用 Application 和 Activity 创立的时刻一起进行接口恳求操作,使得在低端机上,主页的 loading 时刻从原有的 3 秒缩短到了 1 秒以内。

在后续的作业中,使命编列始终是咱们发动优化的要点方向。尤其是经过对每个使命的耗时点定位和对整个使命流程的梳理,然后将发动时长下降到极致,是咱们发动优化的长时刻方针之一。

发动优化做到了现在这个境地,整个发动流程的代码也是大翻新,可是线下评测数据显现,整个发动耗时仅仅缩短了 3 秒左右,甚至于首刷时长还有必定程度的劣化。这是由于发动流程是一个全体,发动和主页不能割裂开来。前面关于发动的使命编列也势必会影响到主页的创立。此外,咱们的优化作业还不行细腻,对一些细节掌握还不足,尤其是锁的处理方面,后边会对这个进行介绍。

主页优化

处理完发动链路的梳理,接下来,咱们需求将目光转向主页。主页是整个 APP 中最中心的页面。它的事务逻辑繁复而且 UI 层级复杂。

懒加载

经过前面的改造,咱们的主页大致如图所示:

心遇 Android 启动优化实践:将启动时间降低 50%
在打开主页后,APP 会加载主页的五个 TabFragment,这个是极为耗时的。

咱们测算了 App在 oppo A5 上各个 fragment 的创立时长,大致数据如下:

心遇 Android 启动优化实践:将启动时间降低 50%

假如能够推延动态等别的四个 fragment 的创立和加载,理论上能够削减 2 秒左右的发动耗时。

考虑到主页展现时只有第一个 fragment 是可见的。为此咱们对主页完成了懒加载。主页运用的是通用的 ViewPager2+tabLayout 的架构办法。ViewPager2 天然支撑懒加载的操作,为了防止在页面切换时,已有的fragment 被收回,咱们增大了 viewPager2 内部的 recyclerView 的缓存池巨细。

 ((RecyclerView)mViewPager.getChildAt(0)).setItemViewCacheSize(mFragments.size());

尽管这个计划能极大地加速主页的烘托速度,可是本质上它是将其他页面的创立和烘托推延到了切换时,假如页面比较重而且手机功用比较差的话,在切换时会有显着的卡顿和白屏状况,这也是无法接受的。

为此咱们对主页和各个fragment进行了改造。

页面插件化

View 的创立是主页烘托耗时的大户。一般状况下,咱们运用 LayoutInflater 去加载 xml 文件,这儿面涉及到了 xml 解析,然后进行反射生成实例的进程,总体上是比较耗时的。咱们对其中比较简略的 xml,运用了代码进行构建,可是关于复杂的布局文件,运用代码构建耗时巨大,而且不可保护。

为了让主页”轻”起来,咱们依据事务角度对 view 进行组件化切割。这儿中心的一个思路便是让用户看到最根底的界面,运用到最中心的功用。举个比方,比方一款视频播映运用,用户第一眼想看到的是它的播映界面,想运用的是视频播映功用。至于其他的顶部图标等等,却是不会在乎的。依据这个思维,咱们应当首要将播映组件和播映功用优先创立,并展现出来,而其他的事务模块能够经过 ViewStub 的办法拖延加载。

那么心遇的中心页面是什么?是主页的缘分列表,而中心功用则是对缘分列表的操作。了解了这一点,主页复杂的逻辑整个就明晰了起来,咱们了解了用户最中心的需求是什么。

这儿介绍一下 Plugin,Plugin 是团队内部沉积出的一套 UI 组件化计划,本质上是一套升级版的 ViewStub,可是又具备 fragment 的才干,天然适配 mvvm。Plugin 是一个功用强大的组件库,关于 Plugin 的详细完成,咱们在经过细细打磨之后,有机会会在后续文章里进行介绍,这儿暂时不做打开。经过 Plugin, 咱们将复杂的 view 依据事务层次切割成一块块独立的事务功用组件,依照优先级进行加载。这样能够保证用户能够更快地看到主页,并运用到最中心的功用。

心遇 Android 启动优化实践:将启动时间降低 50%

缘分的 plugin 在主页创立时就优先展现,而其他的plugin能够比及缘分 plugin 彻底展现,而且相关的数据回来时再进行烘托和加载,这样大大减轻了主页的负载。

Json解析处理

Json 解析操作也是需求进行优化的点。优化前,依据测试同学的测试数据,在低端手机上,主接口的 Json 解析时刻高达 3 秒,这个时长是无法被接受的。

Json 解析耗时的原因本质上是在解析时,从 Json 数据到方针的创立是经过反射操作进行方针生成和赋值的,方针越复杂,那么耗时就越长。关于主页的主接口来说,回来方针的解析在低端机上的耗时现已超越 UI 的烘托的耗时,是咱们必须要克服的点。

咱们现在采取的计划是将主页的数据方针运用 Kotlin 进行重构,并对相关方针运用 @JsonClass(generateAdapter = true) 进行标示,它会在编译期间对标示的方针生成对应的解析适配器,然后缩短解析时刻。

XML 解析优化

测试数据显现,在功用较差的手机上,xml inflate 的时刻在 200 到 500 毫秒之间。自定义控件和较深的 UI 层级会加剧这个解析耗时。

为了下降 xml 解析的时刻。咱们对主页的各个 UI 模块的 xml 进行了优化,尽量减小 xml 层级,而且防止不必要的自定义控件的运用。在心遇 App 上,对强事务相关的自定义控件必定程度的滥用也是导致 xml 加载耗时的重要原因。

除此之外,咱们也考虑过其他计划来下降解析的时刻,比方将 xml 解析的操作放在子线程中,并提早到 Application 中进行履行。这个计划简略且有用,并取得了必定的收益。

一个详细的比方是,主页缘分页面的 item 的 xml 解析耗时在 200ms 左右,咱们将它放置于子线程中并提早预处理,解析成功后的 view 存入缓存中,在进入到主页创立 item 时,从缓存中获取 view 进行烘托。结果是成功将 item 创立的时长下降到了 50ms 以内。

异步解析的计划看起来很有作用,可是假如指望将一切 xml 都经过异步解析的计划进行预处理,那么注定是要绝望的。由于它其实有很大的局限性。

首要要留意的 view 的锁的问题,它会使 xml 的解析从异步变成同步,导致解析反而变慢。关于锁的解说和处理,咱们会在下一章进行详细阐明。在上述优化比方中,咱们经过复制 LayoutInflater 实例的计划来部分绕过了锁的约束。

   LayoutInflater inflater = LayoutInflater.from(context).cloneInContext(context);
   View view = inflater.inflate(R.layout.item_view, null, false);

view的锁现实上并不只LayoutInflater一处,在resource,assets内部也存在有锁,因而上述的计划并不能彻底达到同步的作用。

第二点,子线程优先级低,尤其在负载重的状况下,子线程解析 xml 会导致整个解析流程拉长。这很容易呈现在实践需求用到 view 时,xml 解析还没履行完毕,导致走降级计划,重新主线程解析 xml,这反而导致了资源糟蹋,终究使得烘托时长变长。因而异步解析 xml 只能用于极少的中心 xml 的预处理,而且 xml 的层级不能太复杂。

关于 xml 的解析优化一直是咱们探索的要点。现在,咱们尝试运用 compose 的计划来作为处理 xml 解析耗时问题的首要方向,现在还在实验和沉积阶段,信任不久就会有符合预期的作用。

主页优化的作业带来了很大的收益,线下评测显现,在 oppo A5 手机上,不只首帧展现的时长比较于优化前下降了 3 秒左右,整个主页烘托的时刻也达到了 1 秒以内。主页的数据能够更快地展现,用户的运用体会也会大大提高。

锁对发动优化时带来的费事是如此让人印象深入,以至于咱们需求单独开一节来讲。

在进行发动优化时,假如遇到耗时的使命,咱们一般会将它置于子线程中处理,理论上讲假如资源满足,这个耗时使命的时刻会被彻底优化掉。可是现实往往并不是如此,这种操作或许作用不佳,甚至反而会恶化。这儿面的原因便是锁。这儿咱们挑几个有代表性的锁来讲一讲。

Retrofit

咱们都知道 Retrofit 是经过动态署理的办法生成恳求时的 Call 实例,可是咱们往往忽视了其中锁的存在。

心遇 Android 启动优化实践:将启动时间降低 50%

假如在主页呈现这种很多接口一起建议恳求的状况,多个接口创立竞赛这把锁,这会无形中把接口恳求,从并行变成了串行,这在功用较差的手机上尤为显着。

所以在实践发动进程中,咱们能够看到,一个 api 恳求的耗时,往往是恳求自身只需求 300ms,但等 Retrofit 的这把锁就要 200ms。

心遇 Android 启动优化实践:将启动时间降低 50%

此外剖析 Retrofit 通用的写法,咱们能够看到这部分耗时是在履行 create 时发生的。糟糕的是,这种写法常常会让咱们误以为,这仅仅创立了一个方针,并不是一个耗时的操作,然后将它轻易地暴露在主线程中。

   GitHubService service = retrofit.create(GitHubService.class);

咱们经过对主页的代码进行整改,经过切换线程的办法,将这部分代码切到子线程中。关于锁的问题,经过排查,根本上是由于解析接口回来数据时耗时过长所导致的。这部分涉及到 Json 解析优化的问题,能够看上文处理计划。

反射

咱们知道反射是耗时操作,尤其关于 Kotlin 的反射而言。由于 Kotlin 各种语法糖的原因,反射操作需求从 Metadata 中读取类信息,所以 Kotlin 的反射功率自身就比 Java 低不少。

一起,由于 kotlin_builtins 的存在,Kotlin 中的许多内建信息(比方根底类型 Int, String, Enum, Annotation, Collection 都是以文件的办法保存在 apk 中的,一起也包含协程、多平台等 Kotlin 会用到的信息),在反射进程中会隐式触发类加载和对这些文件的 IO 操作。

   static {
        Iterator<ModuleVisibilityHelper> iterator = ServiceLoader.load(ModuleVisibilityHelper.class, ModuleVisibilityHelper.class.getClassLoader()).iterator();
        MODULE_VISIBILITY_HELPER = iterator.hasNext() ? iterator.next() : ModuleVisibilityHelper.EMPTY.INSTANCE;
        }

关于类加载而言,自身便是有锁 + IO 操作,所以线上经常会有 ANR 呈现。

而 IO 操作更不必说,受限于体系全体文件体系的负载,IO 操作自身的耗时便是不可控的,一起在锁内 IO 又无形中加剧了这种耗时。

所以,反射操作尤其是 Kotlin 的反射能够简略了解成一个潜在的带锁 IO 操作(尤其是在 APP 发动进程中)。

这个或许会导致各种古怪的问题。在优化中咱们就曾碰到过这么一个比方,咱们自身希望经过本地缓存的办法让用户更早得看到 UI,可是由于各种锁的加持,终究不只拖慢了各个 api 恳求的速度,还蝴蝶效应般的把这部分预加载的耗时又转换回了 UI 线程,导致线程卡死。咱们在经过一系列的排查作业之后,终究定位到原因是在发动进程中,Kotlin 首次加载 buildins 带来的耗时。咱们能够在必要时手动触发第一次反射,来规避掉这个问题。

View的锁

前面说到了 view 的创立是耗时大户,正向优化会比较困难,咱们也会想到,是不是能够把一部分 UI 扔到 IO 线程中去 inflate。上文中也有异步解析 xml 的计划。一起咱们也提到了 view 的 inflate 部分进程也是带锁的。

心遇 Android 启动优化实践:将启动时间降低 50%

这把锁是跟着 LayoutInflater 实例走的,换言之一般状况下是跟 Context 走的。在咱们遇到的比方中,view 的加载放在子线程中,由于锁的存在,导致其他的 view 的加载时刻被拖长了,而且由于 CPU 负载高,IO 线程优先级低,这一系列原因反而导致发动流程恶化。

Moshi

Moshi 深度遍历一个 Class 并生成 JsonAdapter 涉及很多反射,耗时时刻不可控。不过比较科学的是,Moshi 内部缓存尽管也有用到锁,但借助 ThreadLocal 能够把耗时操作放在了锁外,后续类似场景也能够参阅这种写法。

心遇 Android 启动优化实践:将启动时间降低 50%

最佳实践

经过对上述一系列问题的剖析,咱们能够总结出以下最佳实践:

  1. 不要在主线程进行任何 Moshi 解析;
  2. 经过 Moshi 解析 Kotlin 类时,要运用 JsonClass 注解;
  3. 不要在主线程进行任何 Retrofit 相关的操作;
  4. 异步 inflate xml 需求留意多线程竞赛的问题。

实践状况上,问题千变万化,硬套公式是不行的,最佳实践更不是银弹。在遇到耗时的使命时,不想着去查找原因,而是粗犷地将它放在子线程中,不论是由于同步锁仍是其他机制,被消耗的 CPU 时刻总会以另一种办法影响 UI 线程的作业功率。

防劣化

发动优化是一个长时刻的优化项目,其时效之长能够说是贯穿一款产品的生命周期。所以并不是说在一段时刻要点攻坚之后,发动时长降下来了就万事大吉了。假如没有防劣化办法,在经过几个迭代之后,发动时长又将上升,尤其是当发动优化来到了深水区之后,各方面的改动都会对发动速度有着千丝万缕的影响。所以发动优化不仅仅一个攻坚战,更是一个长时刻的拉锯战。

因而,咱们需求线上和线下的监控数据作为咱们发动优化作业的辅导。

线上数据

中心的节点如下:

心遇 Android 启动优化实践:将启动时间降低 50%

在线上数据中,咱们首要收集了 Application 的 attachBaseContext 办法作为发动的起始点,主页的 onWindowFocusChanged 作为主页可见的节点,onViewAttachedToWindow 作为主页数据上屏的节点。

心遇现在运用的监控节点更偏向于作为横向比较,假如希望愈加精确的丈量数据,发动的起始点能够运用进程的创立时刻,首帧数据收集能够定位到 dispatchDraw 时调用。可是考虑到易用性,而且希望不受事务影响,心遇运用了现在的监控计划,首要是用于比照历史版别的优化和劣化。

值得一提的是,线上数据收集需求注重噪音的影响。部分机型会在后台杀死进程并重启,可是在重启进程中由于省电战略又会强制中止重启进程,导致这次计时反常,呈现噪音。

心遇这边选用的计划是比照发动时长和线程的发动时长的比照,假如相差超越阈值,则放弃这次记载。

   val intervalTime =
        abs(System.currentTimeMillis() - attachStartTime - SystemClock.currentThreadTimeMillis())

依据实践,20 秒是比较合适的阈值数字。

线下数据

经过打点进行数据统计,尽管能够在必定意义上反映出当时的发动状态,可是总归和用户实践的体会会有差异。因而在线下数据收集时,咱们比较引荐运用各个功用层级的手机对运用进行发动录屏,终究丈量出发动耗时。

办法

关于防劣化的作业,咱们现在还在探索阶段。现在在每个版别发布之后,测试同学会给出一份当时版别的功用测评报告,咱们会结合线上的发动数据进行综合剖析,判别当时版别的发动时长是否劣化。假如数据劣化,咱们会对整个发动流程进行剖析,找出反常点并批改。

这种计划比较低效,由于许多状况下劣化程度低,不易在数据上展现出来。在比及能从数据上反响出来时,或许现已累计有许多的反常点了。

为此,咱们对Application和主页的代码改动会进行特别的 Code Review。咱们不鼓励在 Application 中添加代码,假如需求添加代码,那么会对其必要性进行评价。此外,咱们对发动结构设置了发动耗时报警,假如发动耗时超越阈值,则会在开发阶段就提醒开发者代码或许有反常。咱们认为一切的优化作业归根结底都要靠开发者自身,因而每个团队成员都要有这方面的优化意识才是最重要的。咱们现在也在计划制定相关的标准办法来标准团队成员在这方面的开发作业。

总结

发动优化做到现在,心遇的发动速度和首屏烘托时长都已进入到基线。可是正如上文中说的,发动优化是一个需求长时刻注重的专项,咱们关于心遇的发动时长的优化也不会仅仅限于此。在这次优化项目中,咱们遇到过许多问题,也总结出了许多的最佳实践计划,其中最大的收获便是深入了解了一点:没有平白无故的耗时,假如有,那么就肯定是哪里出问题了。面临耗时,不想着去处理,仅仅将它放在子线程中,然后不予理睬,这个问题必然会在下个路口等你。咱们有时也考虑过运用一些黑科技去优化发动速度,可是往往作用不尽人意,后来想想,其实大道至简,往往最简略的计划才是最好的,对症下药,才干拿到最佳作用。一味追求高大上的技能去优化,往往陷入了大炮打蚊子的窘境。

在后续的作业中,咱们仍然会对发动进行继续的迭代和打磨。比较于之前的作业,咱们会愈加精密化,由浅入深,结合详细事务进行技能计划定制,完成速度提高,在探索出一套比较合适的计划后,再对计划进行泛化,运用到其他事务中。然后回过头来看看,这样由外入内,再由内入外,咱们或许会对整个发动流程有崭新的认识。

参阅资料

  • 运用发动时刻
  • 抖音 Android 功用优化系列:发动优化实践

本文发布自网易云音乐技能团队,文章未经授权制止任何办法的转载。咱们终年招收各类技能岗位,假如你预备换作业,又恰好喜爱云音乐,那就加入咱们 grp.music-fe(at)corp.netease.com!