• 作者:老汪软件技巧
  • 发表时间:2024-09-12 07:01
  • 浏览量:

前言

本章主要围绕 App 的启动流程如何优化进行讲解;

将启动优化,首先要了解的就是 app 的启动流程,只有清晰并完善的了解了 启动流程 才能更好的进行优化;

App 启动流程

在将 AMS 的时候,其实已经讲解了 App 的启动流程,感兴趣的可以翻看下我之前的文章;

这里我们贴一张启动流程图;

整体流程就是当我们点击桌面图标启动某一个应用层的时候,首先会在 Launcher 进程中通过 ActivityManagerProxy 跨进程通信发送 startActivtity 并代理到 system_server(AMS) 进程, AMS 发现这个 Activity 所在的进程已经存在,则直接启动这个 Activity,(这就是所谓的热启动),如果不存在,则通知 Zygote 进程 fork 出一个进程给目标 App 使用,并通知 AMS,由 AMS 来启动目标 Activity;而启动目标 Activity 之前,先启动 Application,在 Application 启动之后才会启动 Activity;

整体可以分为三个大的阶段

点击桌面 Launcher 的应用图标,通过与 AMS 通信,启动应用的过程;应用 Application 执行过程;启动 MainActivity 执行过程;

所以这里的启动其实是有三种状态的:冷启动、温启动、热启动

启动方式冷启动

冷启动是指应用从头开始启动:系统进程在冷启动后才创建应用进程。发生冷启动的情况包括应用自设备启动后或系统终止应用后首次启动

热启动

在热启动中,系统的所有工作就是将 Activity 带到前台。只要应用的所有 Activity 仍驻留在内存中,应用就不必重复执行对象初始化、布局加载和绘制

温启动

包含了在冷启动期间发生的部分操作;同时,它的开销要比热启动高。有许多潜在状态可视为温启动。例如:

用户在退出应用后又重新启动应用。进程可能未被销毁,继续运行,但应用需要执行onCreate() 从头开始重新创建 Activity;

系统将应用从内存中释放,然后用户又重新启动它。进程和 Activity 需要重启,但传递到 onCreate() 的已保存的实例 state bundle 对于完成此任务有一定助益;

启动时长统计Displayed

app启动完成之后,ActivityManager 会打印一个 Displayed 展示启动时间;

adb 命令

adb shell am start -s -w 「packageName/ .activityName」

插桩 + systrace

通过插桩,我们可以看到应用主线程和其他线程的函数调用流程;

CPU Profile

AS 之后,我们一般都是通过 CPU Profile 对 App 的启动耗时进行采样优化;

当我们需要对一个 app 进行采样的时候,我们需要在 Edit Configuration 中进行相关配置信息的打开

选择 Method Trace 之后,使用 profile 的方式启动 App

开启 trace 之后,采集一段时间(跳转到第一个页面之后)可以点击 stop 停止采集;

之后生成对应的 trace 信息;

其中橙色表示系统方法执行时间,绿色表示 app 方法执行时间,蓝色表示三方 sdk 执行时间;

每一个方法,x 轴越大,表示花费的时间越多,通过放大,可以看到我们每一个方法的执行时间,包括 Application 类加载等的创建时间

重点关注绿色区域,逐个的分支查看 app 中耗时的方法;

右侧区域分为四种分析方式:Summary、Top Down、Flame Chat、Bottom Up

Summary 并不是很方便的查看方法的细节;

我们切换到 Flame Chat(火焰图) 来看下:

这个就是和 Summary 反向的分析图,从下往上分析;

我们切换到 Top Down(主要分析耗时) 来看下:

Top Down 比较直观的看到每个方法的执行耗时,以及内部方法的执行耗时;

_优化流程完善制度_优化流程的重要性和意义

App 启动优化方式

根据启动流程的三大阶段,对应的三个阶段的优化方式;

第一阶段的优化 主要是桌面 Launcher 应用与 AMS 的交互,以及 AMS 启动应用的过程,这个阶段主要是系统 Framework 层在做,优化空间基本没有;

第二阶段的优化,对于 Application 的优化,主要包括三部分的优化,一是 attachBaseContext 的优化,二是 onCreate 回调方法的优化,三是应用执行到 MainActivity 之前的白屏处理;

第三阶段的优化,主要是第一个 Activity 运行的阶段,直到 Activity 执行完成 onResume 函数,对应的就是 onCreate、onStart、onResume 的优化;

应用执行到 MainActivity 之前的白屏处理

Activity 真正展示的是 Window,对应的唯一实例就是 PhoneWindow,也就是到 PhoneWindow 展示之后,用户才能看到实际的内容;

这个流程其实是:点击 Launcher 桌面图图标到第一个 Activity 的 PhoneWindow 展示之前,这个期间内应该展示什么来规避黑屏或者白屏的 case;

产生这个黑白屏的 case 其实跟我们设置的启动的第一个 Activity 的主题有关系,也就是 windowBackground 依赖这个 theme,如果 theme 设置的是白色主题,那么 windowBackground 默认就是白色,启动就会白屏;

根据流程图,我们进入 PhoneWindowManager 的 addSplashScreen 方法看下,黑屏开始的地方在哪里?

public StartingSurface addSplashScreen(IBinder appToken, int userId, String packageName,
        int theme, CompatibilityInfo compatInfo, CharSequence nonLocalizedLabel, int labelRes,
        int icon, int logo, int windowFlags, Configuration overrideConfig, int displayId) {
      
    // 省略部分代码
    // 黑白屏开始的地方,就是 addView 添加要显示的 View 的时候;
    wm.addView(view, params);
    return view.getParent() != null ? new SplashScreenSurface(view, appToken) : null;
}

那么,怎么优化这个黑白屏的问题,我们可以通过 PhoneWindowManager 的源码来看下:

private void addSplashscreenContent(PhoneWindow win, Context ctx) {
    final TypedArray a = ctx.obtainStyledAttributes(R.styleable.Window);
    final int resId = a.getResourceId(R.styleable.Window_windowSplashscreenContent, 0);
    a.recycle();
    if (resId == 0) {
        return;
    }
    final Drawable drawable = ctx.getDrawable(resId);
    if (drawable == null) {
        return;
    }
    // We wrap this into a view so the system insets get applied to the drawable.
    final View v = new View(ctx);
    v.setBackground(drawable);
    win.setContentView(v);
}

可以看到,PhoneWindowManager 通过获取 theme 中定义的 windowSplashscreenContent 来获取一个 drawable 设置给 PhoneWindow;

PS:这个 API 需要 API >= 26,如果最低版本小于 26 则还是通过 windowBackground 来设置;

那么,可能会有人有疑问了,这个 windowSplashscreenContent 属性比 windowBackground 强大在了哪些地方呢?

windowBackground 只能设置一张图片,而 windowSplashscreenContent 借助 Jetpack 的 SplashScreen 可以展示一个开屏动画;

attachBaseContext 优化

可以参考字节的 MutilDex 优化启动速度,核心是:去掉了 dex 转 zip 的操作,优化了启动速度,而不是多进程;

onCreate 优化

onCreate 方法中,如果使用 setContentView 方式,目前只能通过减少 xml 层级的方式来降低启动耗时,因为 setContentView 中充斥着大量的反射逻辑来创建 View;

所以,如果 xml 的绘制比较简单,建议使用 new View 的方式,通过 addView 来实现 View 的创建和绘制;

onResume 优化

如果页面布局是 ViewPager + Fragment 的方式,通常采用懒加载的方式,来进行页面渲染的优化;

布局层级的优化

使用 约束布局 替换普通的布局,优化渲染层级,减少绘制时长;

使用布局的异步加载 AsynLayoutInflater

AsyncLayoutInflater(this).inflate(R.layout.activity_debug, null, object : OnInflateFinishedListener{
    override fun onInflateFinished(view: View, p1: Int, p2: ViewGroup?) {
        setContentView(view)
    }
})

但是 AsyncLayoutInflater 的使用也是有一些限制的,我们可以先尝试下能不能在实际的项目中使用它;

延迟任务优化

通过 handler 的 addIdleHandler 方法添加延迟任务,会在主线程空闲的时候执行;

线程优化

线程的优化主要在于减少 CPU 调度带来的波动,让应用的启动时间更加稳定;

GC 优化

// GC使用的总耗时,单位是毫秒
Debug.getRuntimeStat("art.gc.gc-time");
// 阻塞式GC的总耗时
Debug.getRuntimeStat("art.gc.blocking-gc-time");

系统调优优化

在启动过程,我们尽量不要做系统调用,例如 PackageManagerService 操作、Binder 调用等待;

I/O 优化数据重排

原理:Dex 文件用的到的类和安装包 APK 里面各种资源文件一般都比较小,但是读取非常频繁。我们可以利用系统这个机制将它们按照读取顺序重新排列,减少真实的磁盘 I/O 次数;

资源文件重排类加载优化

通过 Hook 的方式 去掉 类加载的过程中 verify class 的步骤;

下一张预告

Android StartUp 原理解析

欢迎三连

来都来了,点个关注,点个赞吧,你的支持是我最大的动力~