应用启动相关流程与优化
应用启动主要涉及SystemServer进程 和 app进程。
SystemServer进程负责app进程创建和管理、窗口的创建和管理(StartingWindow 和 AppWindow)、应用的启动流程调度等。
App进程被创建后,进行一系列进程初始化、组件初始化(Activity、Service、ContentProvider、Broadcast)、主页面的构建、View加载。
1.应用启动方式
启动方式:热启动、冷启动、温启动三种。
冷启动
冷启动耗时最多,冷启动常见场景APP首次启动或者APP完全被杀死。
温启动
当启动应用时,后台已有该应用的进程,Activity因为内存不足被回收。系统会从已有的进程中来启动这个Activity。
温启动场景:第一种是首先用户按连续按返回退出了app,最后重新启动app,第二种是首先系统收回了app的内存,最后重新启动app。
热启动
热启动时,系统将activity带回前台。如果应用程序的所有activity存在内存中,那么应用程序可以避免重复对象初始化、渲染、绘制操作。
热启动常见的场景如: 当我们按了Home键或其它情况app被切换到后台,再次启动app的过程。
2.应用冷启动关键流程
- 用户Click事件触发IPC操作,会执行Process的start方法来创建进程。
- ActivityThread执行main方法,是App进程的入口,相当于Java进程的main方法,在其中会执行消息循环的创建与主线程Handler的创建。
- Handler创建完成之后,就会执行到 bindApplication 方法,在这里使用了反射去创建 Application以及调用了 Application相关的生命周期。
- Application生命周期之后,便会执行Activity的生命周期,在Activity LifeCycle结束之后,就会执行到 ViewRootImpl,这时才会进行真正的一个页面的绘制。
优化方向
创建Application、启动主线程、创建HomeActivity、加载布局、布置屏幕和界面首帧绘制完成后,启动完成。
APP 层可以优化的相关流程:Application atttachBaseContext、Appication onCreate 和 Activity LifeCycle三个流程。
3.归因分析
程序运行最根本的是需要得到CPU时间片,如果一个任务需要较多的CPU时间执行,那么它将影响其他任务的执行,从而影响整体任务队列的运行;
线程切换涉及到 CPU调度,而CPU调度会有系统资源的开销,所以大量的线程频繁切换也会产生巨大的性能损耗;
IO和锁的等待会直接阻塞任务的执行,不能充分地利用CPU等系统资源。
4.启动指标
第一个是启动开始,启动开始是进程创建的时间
第二个是启动结束,首页首屏渲染完成的时间;
第三个是启动时长,启动时长是指启动结束的时间戳减去启动开始的时间戳;
5.启动优化价值
启动耗时增长可能缩减App用户的留存。
启动性能优化目标是以低端机为重点,辐射中高端机,通过技术和产品上的深度优化,可感知的提升用户体验,实现扩大用户规模、提升留存和提升收入。
6.优化方案整理
1.Application优化
Application 阶段通常用于初始化比较核心的业务库。在应用开发早期,我们并没有对这个阶段的启动任务进行管控,导致这里往往会堆积有大量的强业务相关的代码。精简基于的原则是:
- Application 中的任务应当是全局基础任务
- Application 创建时应当尽量减少网络请求操作
- Application 创建时不允许有强业务相关的任务
- Application 创建时尽量减少有 Json 解析处理和 IO 操作的工作
Application中的启动任务要尽可能的少,主要分为基础库初始化,功能配置和全局配置这三大类。基础类库主要是对网络库,日志库等基础库进行初始化配置,除了主进程外,其余的进程也依赖这些任务,移除它们会对全局的稳定性造成影响。它们也是启动任务中占比最大的,耗时最多的一类任务,因此降低它们的耗时也是后面持续优化的重点。功能配置主要是对一些全局相关的业务功能的前置配置,例如对业务缓存的预加载,特定业务前置等,移除它们会造成业务有损,在这种情况下,我们需要找到业务诉求和功能配置之间的平衡点。全局配置主要是对于全局 UI 配置,文件路径的处理操作,它们占比少,耗时少,是首页创建的前置任务,因此暂不处理。
任务排布的核心是处理好任务的前后依赖问题。
1.要基于进程进行任务排布,对 Application 的任务进行细致划分,将任务的运行精细到进程级,避免在非主进程中执行了不必要的任务。
2.懒加载。这里主要是对一些基础任务进行改造,将任务初始化和任务启动拆分开来,将启动工作移出 Application 创建流程,同时对其进行精简,去除冗余逻辑。在创建对象时,可以延迟其成员对象的创建,灵活使用 by lazy 等关键字,使对象轻量化。
3.进程收敛。多进程可以实现模块隔离,同时避免单个进程内存占比过高导致的内存上限限制,其劣势在于多进程会导致应用整体内存占用过多,触发低内存的概率更高。此外,如果应用启动时内存占比过高,可能会导致手机进行内存回收,占用大量的CPU资源,这反应在用户的体验上就是启动慢,应用卡。对比多进程的优劣,采用的策略是尽量延后主进程以外的进程启动,同时通过进程合并来减少进程的数量。
4.线程收敛。对于多核CPU来说,适当的线程数量能够提升效率,但是如果线程泛滥则会导致 CPU 负载过重。多线程并发,本质上就是多个线程轮流获取 CPU 使用权的过程。在负载超重的情况下,过多的线程争抢时间片,除了降低启动速度外,也会导致主线程卡顿,影响用户体验。在做这方面优化时,需要确保全局使用统一的线程池。同时很多二方和三方 SDK 也是创建子线程的大户,这时候需要和相关的技术部门进行沟通,去除不合理的线程创建。另一方面,避免在启动阶段进行网络请求,也是减少线程数的关键所在。
Application 优化是整个启动流程的关键,合理的任务编排不仅能够降低 Application 的创建时长,对后续的首页创建也有非常大的优化效果。
2.启动链路
执行完 Application 创建之后,应用进程的主要工作就是创建 Activity。这里需要注意的是,从 Application 到 Activity 里还藏有很多 post 到主线程中的任务,以及注册的 ActivityLifecycleCallbacks 的回调监听,它们会偷偷增加从 Application 到 Activity 的时间间隙。ActivityLifecycleCallbacks 注册通常和业务相关,它的注册比较隐蔽。
关于主线程消息耗时,使用 Profiler 和 Systrace 对启动流程进行耗时定位时,可以发现很多问题。因为各种原因,Application 中任务会将耗时的工作 post 到主线程中来,表面上看 Application 的创建时间缩短了,但是总体上启动时间却被扩大了。对于耗时点应该定位到根本原因进行解决,而不是一味地 post 出去,这样治标不治本。
缩短启动到首页的链路,是我们优化的重点。
在一般启动流程中,loading 页面启动页面,承担有路由和权限请求两个任务。
1.在通常情况下,用户启动 App,在 loading 页面判断是否登录,如果未登录,则进入登录页面。
2.如果用户已经登录,则判断是否需要展示开屏页,如果需要则进入开屏页,等到开屏结束,就跳回 loading 界面,再进入首页。
即使没有开屏页,用户启动 APP 到展示首页,最起码要经过两个 Activity 的启动。启动链路缩短的核心在于将 loading,main 和开屏页合并为一个页面。这样做不仅可以最起码减少一次 Activity 的启动,同时也可以在展示开屏页时并行处理其他的任务。
实现这个功能的代码逻辑就是将 main 页面设置为启动页,首页和开屏页封装为两个 fragment,根据业务逻辑进行展示。用户点击 icon 进入到首页,如果判断已登录,则执行首页前置任务和首页 UI 渲染,同时判断是否加载开屏页 fragment。
3.首页优化
懒加载
首页大致情况如下图:
APP 会加载首页的四个 TabFragment,比较耗时。
延迟动态等另外3个Fragment 的创建和加载。考虑到首页展示时只有第一个 fragment 是可见的。对首页进行懒加载。首页使用的是通用的 ViewPager2+tabLayout 的架构形式。ViewPager2 天然支持懒加载的操作,为了避免在页面切换时,已有的fragment 被回收,增大 viewPager2 内部的 recyclerView 的缓存池大小。
((RecyclerView)mViewPager.getChildAt(0)).setItemViewCacheSize(mFragments.size());
该方案是将其他页面的创建和渲染推迟到了切换时,如果页面比较重并且手机性能比较差的话,在切换时会有明显的卡顿和白屏情况,这也是无法接受的。
所以要对首页和各个fragment进行了改造。
4.ViewStub实现懒加载。
View 的创建是首页渲染耗时的大户。使用 LayoutInflater 去加载 xml 文件,这里面涉及到了 xml 解析,然后进行反射生成实例的过程,总体上是比较耗时的。对其中比较简单的 xml,使用了代码进行构建,但是对于复杂的布局文件,使用代码构建耗时巨大,并且不可维护。建议通过 ViewStub 的形式延后加载。
5.Json解析处理
Json 解析操作也是需要进行优化的点。Json 解析耗时的原因本质上是在解析时,从 Json 数据到对象的创建是通过反射操作进行对象生成和赋值的,对象越复杂,那么耗时就越长。对于首页的主接口来说,返回对象的解析在低端机上的耗时可能超过 UI 的渲染的耗时。可以采取的方案是将首页的数据对象使用 Kotlin 进行重构,并对相关对象使用 @JsonClass(generateAdapter = true) 进行标注,它会在编译期间对标注的对象生成对应的解析适配器,从而缩短解析时间。
6.XML 解析优化
1.XML异步解析。
2.compose 的方案。