Android 启动优化

854 阅读12分钟

目录

  • 优化工具methodtrace
  • 启动起点和终点
  • appliation 初始化异步任务 延迟任务
  • splash页面,主页面布局优化
  • 主页面数据预加载
  • 耗时方法排查和优化
  • gc抑制
  • 类重排优化
  • webview优化
  • 监控防裂化

启动优化的基本思想

  1. 抓各个环节
  2. 针对启动流程的各阶段,寻找可优化点
  3. 整理系统化优化方案,再来实操
  4. 明确成本收益比和风险收益
  5. 明确指标以及形成一套监控防劣化体系
  6. 把秒开率这个指标刻在脑子里

用户启动app的耗时如何定义?

我们这里的定义是:进程启动 -> 用户看见Feed流卡片/内容

业内常见的app启动过程阶段一般分为「启动阶段」和「首刷阶段」。

  • 启动阶段:指用户点击icon到见到app的首页
  • 首刷阶段:指用户见到app的首页到首页列表内容展现

image.png

根据业务流程,我们想要优化启动速度,需要进行如下考虑:

  • 开屏广告接口尽量早的发出请求
  • 等待开屏接口过程中,尽量完成更多的对启动流程有 block 的启动任务
  • feed列表的第一屏数据尽量走缓存

从系统角度看启动过程

从系统角度来看自家App的启动路径,与大多数App是类似的。整体分为 Application阶段Activity阶段

Application阶段

Application阶段中,需要我们重点关注:

  1. Application#onCreate,一般来说,项目本身模块的初始化、各种三方库初始化、业务前置环境初始化都会在 Application#onCreate 这个生命周期里干,往往这个生命周期里的任务是非常臃肿的,我们优化Application流程的大部分目光也集中在这里,也是我们通过异步、按需、预加载等各种手段做优化的主要时机。
Activity阶段

Activity阶段的起点来自于 ActivityThread#performLaunchActivity 的调用,在 performLaunchActivity 方法中,将会创建Activity的上下文,并且反射创建Activity实例,如果是App的冷启动(即 Application 并未创建),则会先创建Application并调用ApplicationonCreate方法,再初始化Activity,创建Window对象(PhoneWindow)并实现ActivityWindow相关联,最终调用到ActivityonCreate生命周期方法。

在启动优化的专项中,Activity阶段最关键的生命周期是 Activity#onCreate,这个阶段中包含了大量的 UI 构建、首页相关业务初始化等耗时任务,是我们在优化启动过程中非常重要的一环,我们可以通过异步、预加载、延迟执行等手段做各方面的优化。

启动时间统计
点击icon开始到首页第一帧显示出来的时长

我们统计方式只能相对靠谱,无法做到绝对精确,也不需要绝对精确,比如大家比较认可的 onAttachBaseContext做起点,onPreDraw 做终点就可以,启动优化绝大部分情况都是自己跟自己比,有个相对靠谱的统计口径即可

首帧显示出来的时机

feed流列表的子item装载数据attch到window来算,onViewAttachedToWindow

5.2 Method Trace

在开发环境下我们想要去优化启动时长,必须得有方法知道瓶颈在哪儿,是哪个方法太耗时,还是哪些逻辑不合理,哪些能优化,哪些没法优化。Method Trace就是其中手段之一,我们通过 Method Trace能看到每个线程的运行情况,每个方法、方法栈耗时情况如何。

Debug.startMethodTracingSamping

我们也可以通过代码去抓取 method trace:

Application#onCreateFile file = new File(FileUtils.getCacheDir(application), "trace_" + System.currentTimeMillis()); 
Debug.startMethodTracing(file.getAbsolutePath(), 200000000); 
Debug.startMethodTracingSamping(file.getAbsolutePath(), 200000000, 100); 
StartupFlow#afterStartupDebug.stopMethodTracing();
Perfetto

将trace文件导入到Perfetto查看进程、方法执行情况

Perfetto 是 Android 10 中引入的全新平台级跟踪工具。这是适用于 Android、Linux 和 Chrome 的更加通用和复杂的开源跟踪项目。它提供数据源超集,可让你以 protobuf 编码的二进制流形式记录任意长度的跟踪记录。你可以在 Perfetto 界面中打开这些跟踪记录,可以理解成如果开发机器是 Android 10 以下,就用 Systrace,如果是 Android 10 及以上,就用 Perfetto,但是 Perfetto跟Systrace一样

image.png

所以大致的思路可以总结为:
  1. 前期低成本低风险快速降低大盘启动耗时
  2. 后期高成本突破各个瓶颈
  3. 全期加强监控,做好防劣化

 Application流程

 启动任务删减与重排

在App的启动流程中,有非常多的启动任务全部在Application的onCreate里被执行,有主线程的有非主线程的,但是不可避免的是,二者都会对启动的性能带来损耗。所以我们需要做的第一件重要的事情就是 减少启动任务。 我们通过逐个排查启动任务,同时将他们分为几类:

  • 刚需任务:不可延迟,必须第一时间最高优先级执行完成,比如网络库、存储库等基础库的初始化。如果不在启动阶段初始化完成,根本无法进入到后续流程。
  • 非刚需高优任务:这类任务的特征就是高优,但是并非刚需,并不是说不初始化完成后续首页就没法进没法用,比如拉取ab实验配置、ip直连、push、长链接相关非刚需基础建设项,这类可以高优在启动阶段执行,但是没必要放在 UI 线程 block 执行,就可以放到启动阶段的后台工作线程中去跑。
  • 非刚需低优任务:这类任务常见的特征就是对业务能否运作无决定性影响或者业务本身流程靠后,完全可以放在我们认为的启动阶段结束之后再后台执行,比如 x5内核初始化、在线客服sdk预初始化 之类的。
  • 可删除任务:这类任务完全不需要存在于启动流程,可能是任务本身无意义,也可能是任务本身可以懒加载,即在用到的时候再初始化都不迟。

将任务分类之后,我们就能大概知道如何去进行优化。

  • 拉高刚需任务优先级

  • 非刚需高优 异步化

  • 非刚需低优任务 异步化+延迟化

  • 可删除任务 删除

~~# 启动窗口优化

启动应用后会显示空白启动窗口,可给这个窗口这是一张背景图代替白屏(通过theme设置)。

image.png ~~

任务排布框架

为了更加方便的对启动任务进行排布,我们自己实现了一套用于启动过程的任务排布框架TaskManager。TaskManager具有以下几个特性:

  1. 支持优先级

  2. 支持依赖关系

  3. 提供超时、失败机制以供 fallback

  4. 支持在关键时间阶段运行任务,如MainActivity某个生命周期、启动流程结束后

TaskManager.getInstance().beginWith(A) .then(B) .then(C, D) .then(E) .enqueue(); TaskManager.getInstance().runAfterStartup({ xxx; })


TaskManager.getInstance().runAfterStartup({ xxx; })

实现runAfterStartup机制 + idleHandler

这玩意儿十分重要,我通过昏天黑地的梳理业务,将启动流程中原先可能超过一半的代码任务非常方便的放到了启动流程之后

我们通过提供 runAfterStartup 的API,用于更加容易的支持各种场景、各种业务把自己的启动过程任务或者非启动过程任务放在启动流程结束之后运行,这也有助于我们自己在优化的过程中,更加轻松的将上面的非刚需低优任务进行排布。 runAfterStartup的那些任务,应该在什么时候去执行呢? 这里我们认定的启动流程结束是有几个关键点的:

  1. 首页tab的feed流渲染完成
  2. 首页tab加载、渲染失败
  3. 用户进入了二级页面
  4. 用户退后台
  5. 用户在首页有 tab 切换操作

通过TaskManager的使用以及我们对各业务的逐一排查分析,我们将原先在启动阶段一股脑无脑运行的任务进行了拆解和细化,该延后的延后,该异步的异步,该按需的按需。

异步任务

image.png

1,初始化逻辑抽象成一个个Task
2,根据任务的依赖关系,使用 BFS(广度优先) 构建出有向无环图,并得到拓扑排序,获取任务的执行顺序。

1.1,首先找出所有入度为 0 的队列,用 queue 变量存储

1.2,当队列不为空,进行循环判断。

  • 从队列 pop 出,添加到结果队列

  • 遍历当前任务的子任务,通知他们的入度减一(其实是遍历 taskChildMap),如果入度为 0,添加到队列 queue 里面

1.3,当结果队列和 list size 不相等试,证明有环

3,在多线程执行过程中,通过任务的依赖和 CounDownLatch 确保先后执行关系的

假设a是b的一个前置任务,当一个任务b执行时:

2.1,如果CounDownLatch>0,说明前置任务a没有执行完毕,任务b等待;

2.2,前置任务a执行完毕,通知任务b,b任务的CounDownLatch减一;

2.3,任务b的CounDownLatch如果为0,执行b任务。

任务的CounDownLatch为前置任务的数量。初始化任务时会设置当前任务的前置任务列表,所以可以通过当前任务获取到前置任务列表及其数量。

真正的任务是DispatchRunnable,上述任务指DispatchRunnable,它持有Task并在其run方法中调用了Task的run方法。

await方法实现原理,也是通过将使用CounDownLatch记录NeedWait的任务数,NeedWait任务执行完就减1,CounDownLatch如果为0,才执行后续任务(拓扑图任务后面的任务)

注意:异步任务必须使用线程池,避免线程过多,cpu过度切换

延迟任务

public class DelayInitDispatcher {

    private Queue<Task> mDelayTasks = new LinkedList<>();

    private MessageQueue.IdleHandler mIdleHandler = new MessageQueue.IdleHandler() {
        @Override
        public boolean queueIdle() {
            if(mDelayTasks.size()>0){
                Task task = mDelayTasks.poll();
                new DispatchRunnable(task).run();
            }
            return !mDelayTasks.isEmpty();
        }
    };

    public DelayInitDispatcher addTask(Task task){
        mDelayTasks.add(task);
        return this;
    }

    public void start(){
        Looper.myQueue().addIdleHandler(mIdleHandler);
    }
}

Handler().postDelayed()的痛点:

1,时机不容易控制:handler postDelayed指定的延迟时间不好估计。

2,导致界面UI卡顿:此时用户可能还在滑动列表。

使用IdleHandler,Handler 空闲的时候才会被调用,如果返回 true, 则会一直执行,如果返回 false,执行完一次后就会被移除消息队列。

当CPU空闲时,去执行延迟初始化的task,一个一个地拿出来并执行。这种分批执行的好处在于每一个task占用主线程的时间相对来说很短暂,并且此时CPU是空闲的,这样能更有效地避免UI卡顿。

需要注意的是,能异步的task(或者是必须在Application的onCreate方法完成前必须执行完的非异task务),优先使用异步启动器在Application的onCreate方法中加载,对于不能异步的task,我们可以利用延迟启动器进行加载。如果任务可以到用时再加载,可以使用懒加载的方式。

问题:用户使用某个库时,如果还没有初始化怎么处理?

可以看到IdleHandler确实是在消息队列为空或者需要执行的消息还未到时间时,即消息队列空闲时才去执行的。

闪屏页与主页的绘制优化

开发者模式->显示布局边界分析

1、分析布局,减少布局嵌套或者替换消耗性能少的布局(FrameLayout,constraintlayout)

2、使用include+merge减少布局层级

3、使用viewstub提供按需加载

4、复用系统自带的资源 winow background

主页面数据预加载

splash页面的时候使用异步任务提前将主页面的数据请求下来,主页面加载的时候可以直接从内存读取。 另外可以网络优化 Httpdns 连接复用 http2.0 精简合并借口

内存优化避免gc占用cpu时间

启动过程中减少 GC 的次数

1,避免进行大量的字符串操作,特别是序列化和反序列化

2,频繁创建的对象需要考虑复用

3,转移到 Native 实现

耗时方法排除和优化

排查高频方法可以通过 method trace + 插桩记录函数调用来做

  • 1,获取耗时长的方法进行优化
  • 2,binder获取数据(判断网络,获取电量,加密),进行缓存
  • 3,反序列化的对象,进行异步读取,缓存

webview优化

如果主页面打开后有webview显示广告。

1,webview提前初始化及复用

2,使用离线包

3,使用数据预加载

类重排

通过 ReDex 的 Interdex 调整类在 Dex 中的排列顺序,把启动时需要加载的类按顺序放在主 dex 里。具体实现可以参考 Redex 初探与 Interdex:Andorid 冷启动优化。

线上监控

90 分位 App的启动耗时从 2800 左右 下降到 1500 左右。降幅47%

Android 主版本秒开率由原先的约 17% 提升到 76%

Android 主版本两秒打开率由原先的 75% 提升到了 93% 

参考

深入探索Android启动速度优化 Android 优化之 App 启动优化

Android 启动优化(四)- AnchorTask 是怎么实现的

www.androidperformance.com/2019/11/18/…