0%

性能优化-:(04)2022.1.20-Android面试必备Application启动过程与耗时分析

一、概述

分为三个步骤:

  1. 通过源码分析,知道启动流程(便于启动监控——哪些地方耗时)

  2. 阿里的启动优化方案

  3. 如何实现高效的 SplashActivity

二、启动流程

分为三个流程,如下图所示:

App启动的3个流程

在第二个阶段中,Application 的启动很关键,其中 Application 中的 attachBaseContext 方法比较关键,很多时候我们会在里面做一些比较前置的任务,诸如:

  • 对于加固的 App ,这里会做解密的操作

  • 对于热修复而言,第一时间将修复的 dex 给生效

只需要在执行类之前,dex 中包括这个类。所以,理论上我们可以无需一次性将所有的 dex 文件在 attachBaseContext 的时候加载进去。第一个 Activity 启动之后,再将其他的 dex 加载进去。attachBaseContext 阶段可以使用字节的 BoostMultiDex 方案去优化。

只有 window 才能展示出来看到,而目前在 Android 里面就一个 PhoneWindow ,所以,我们看到的第一屏肯定只能是 第一个 Activity 的 PhoneWindow。

那么,在用户点击Launcher 到第一个 Activity 显示出来之前,我们要显示内容,显示什么内容呢?其实显示的是Application 的主题,这个主题里面有个属性:

1
2
3
4
5
6
7
8
9
10
11
12
<application
android:theme="@style/Theme.AndroidDemo">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

application 的 theme 是 @style/Theme.AndroidDemo ,然后我们可以在theme 里面设置 android:windowSplashscreenContent 值:

1
2
3
4
5
6
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.AndroidDemo" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<item name="android:windowSplashscreenContent">@drawable/ic_launcher_background</item>
</style>
</resources>

当然,以上操作要求 API 版本是 26 ,如果要兼容低的版本,就要设置 background 属性,但是这二者还是有区别的:

  • background 只能是图片

  • windowSplashscreenContent 可以做动画

这是怎么实现的呢?Application 里面压根就没有 Window 啊,其实在startActivity 的时候,Activity 真正启动之前,add 了一个 SplashWindow,WindowManager 通过 addView 的方式将 window 交给 WMS 去展示这个闪屏页了,这个闪屏window 会去解析 windowSplashscreenContent 字段。在第一个 Activity 初始化完,Activity 的 window 也通过 vm.addView 展示的时候,就会覆盖 SplashWindow 。

以上就是黑白屏优化,这其实就是欺骗用户,实际上也没加快启动速度。

所以,我们优化启动速度,优化在哪里呢?其实就是 vm.addView 添加 SplashWindow 和 vm.addView 第一个 Activity 的 window 之间的过程,将这个过程尽量缩短。并且,这个过程里面会执行 Application.onCreate(),所以我们需要减少 onCreate 的耗时。

优化方法

线程数,有些 SDK 会启动线程,你自己可能压根没创建线程。所以如何控制这个线程数呢?后续再讲,基本上可以通过阿里的 Alpha 。(老师说Android 的 startUp 比较垃圾),这就是 Application 的 onCreate 方法优化方法。

上述的过程还会经历第一个 Activity 的onCreate、onStart、onResume 这些流程

perfomStartActivity 的过程中, Activity.attach 的时候会为Activity创建 Context 、创建 window ,并未 window 设置 WindowManager。为什么这么设计?因为单一职责问题,Activity 是管理生命周期的,Window 是管理 View 的。

我们在 Activity 的 onCreate 中只应该去做 view 的操作,其他的诸如数据库初始化之类的应该放在子线程中异步操作。

装饰模式是功能增强;代理模式只是代理,没有增强。

有可能面试官会问,为什么采用 inflater 解析的view ,要比自定义的 view 效率低?这里我们可以看 setContentView 方法,最底层就是使用 inflater 实现的,如果能看到后面就会发现,大量使用反射创建出来 View 。而反射又是比较耗时的,所以总结如下:

  • 解析 xml 耗时

  • 根据 xml 反射 view 的反射操作耗时

所以,Activity 的 onCreate 怎么优化?如果采用的还是 xml 就没法优化;只能通过减少 xml 中的布局的层级,减少 view 个数,就减少时间;还有就是从上面的原理来说,可以自己去 new 这些view 。不过也可以用 compose ,它没有 xml 了,通过 new 之类的创建出来的。固定测量方式,避免过度渲染,多次测量问题。

onCreate 执行完成后,只是解析完成了 xml ,在 setContentView 的时候会创建 decorView,然后创建了 window ,但是还没有 显示出来,还要在 wm.addView() 才能显示在硬件上。而 handleResume 的时候,先执行了 onResume 回调之后,然后再执行Activity 的wm.addView() 。

所以,Activity 怎么优化,就得onResume() 以及之前的所有方法(onCreate、onStart) 。所以,在 onResume 这里面如果有耗时操作,就要上前面说的阿里的策略了(Alpha)。以及相应的懒加载思想(ViewStub、Viewpaget+fragment 等)。

层级,深度遍历,多一层 2的次幂

尽量使用 ConstraintLayout 而不是 Linelayout 这些,会增加嵌套。从观法的数据来看,其渲染速度比 RelativeLayout 要提升 40% 。

Activity 中所有的回调都是在 handler 里面执行的。handler 机制体系里面会有MessageQueue ,message 按照时间排序,如果最开始的那个msg 都要 10ms 后执行,那么线程就会通过 epoll 机制睡眠了。所以我们还是利用 IdelHandler 去让主线程空闲的时候去做。比如说,后台 Service 有些内容是没有必要那么及时的,可以考虑在IdleHandler 中执行。

GC 会 STW ,所以,我们也能在 IdleHandler 中取做 GC ,这样就不会影响主线程,LeakCanary里面就是这么做的(老师展示的代码是这样的,AndroidWatchExecutor.waitForIdle() 方法。还需要自己确认下)。要注意一点,IdleHandler 中不要做耗时操作,因为它也是个 msg

当 App 第一个 Activity 的 window 展示将 SplashWindow 覆盖的时候,是在 windowFocusChange 中实现切换的。所以我们一般用 windowFocusChange 来标记新的 window 启动。

每一个 Activity 都会有一个独一无二的 Window

总结

启动优化分为几个点:

  • 黑白屏阶段,使用 manifest 中 theme 的 windowSplashscreenContent 属性

  • Application 的 attachBaseContext() 回调中,可以用 字节的 BoostMultiDex 方案

  • Application 的 onCreate 阶段,采用图论原理,将各个任务依次执行,例如阿里的 Alpha 框架

  • 接着就到了Activity 阶段,由于要遭 onResume 之后才会执行 vm.addView 最终展示 window ,所以在 onCreate、onStart 、onResume 这些回调里面都不要执行耗时操作

谢谢你的鼓励