App启动优化最佳实践

App启动优化最佳实践

优化启动的意义

启动流程是用户对我们App的第一体验,打开应用后才能去使用其提供的强大功能,就算我们应用的内部界面设计的再精美,功能再强大,如果启动速度过慢,用户第一印象就会很差。更有甚者,如果用户点击App后,半天都打不开,用户就可能失去耐心卸载应用。一款App,启动流程承载的不止是用户体验的第一场景,更是一款产品技术形象的初始印象。

我们遇到的问题

借款App很早就开始重视并着手对启动流程做优化,早期借款App的启动项散落在整个启动流程各处,启动项之间又有依赖关系,如需增删启动项,或者调整启动项顺序,就会牵一发而动全身,整个启动流程稳定性欠佳,启动速度慢,测试回归成本高,启动卡住的情况偶有发生。启动流程治理已经迫在眉睫,自此PPDFrame1.0诞生,1.0版本将启动任务与业务流程做了分离,并对启动流程做了物理隔离,将整个启动流程分为Application,Splash闪屏页,首页三个区域。以不影响启动速度为前提,将各启动任务梳理清楚后分别放置在对应区域的各生命周期模块里,后续的迭代过程如需修改启动项,经评估后决定该启动项应该放在哪里。相当长的一段时间里PPDFrame1.0版本有效的保证了启动流程的健壮与稳定。然而随着业务的快速迭代,启动项迅速从之前的20个左右增加到近40个,加之合规需求对启动流程的不断冲击,整个启动流程又出现了新的问题:

1:闪屏页跳转至首页导致的割裂感,用户体验不连贯。
2:首页加载慢,用户已经到了首页,内容还没有渲染完成
3:启动速度慢,白屏时间长。

为了将上述问题一网打尽,是时候祭出PPDFrame2.0了。2.0完全抛弃了1.0的思路与实现,针对目前的问题做通盘考虑,各个击破。

闪屏页跳转至首页导致的割裂感,用户体验不连贯。

因为闪屏页是个Activity,即SplashActivity,首页也是Activity,Activity之间的跳转在App开发中其实是比较重的操作,给用户的感觉就是割裂,不连贯。同时Activity的创建销毁也都是有开销的,基于此首先想到的是抛弃闪屏Activity,应用直接启动首页Activity,闪屏页作为Fragment(SplashFragment)挂载到首页。而实际情况是因为借款App已经迭代多年,即便业务需求可以平移至SplashFragment中,但SplashActivity作为push,deeplink的入口,轻易改动影响的业务方较多,其次SplashActivity还牵扯到复杂的Activity栈管理。基于此方案上仍然保留SplashActivity,但它是个空白闪屏不承载业务,也没有UI渲染,只作为一个跳板存在,创建即跳转首页并销毁。流程图大致如下:

11.jpg

基于上述修改,闪屏页摇身一变成为了首页的Fragment,闪屏页的倒计时3...2...1...结束后,首页的视图早已利用倒计时的间隙在闪屏页下默默的渲染好,用户视感受验连贯不割裂,体验得到了提升。

首页加载慢

虽然调整完启动流程,用户体验着实有了改善,但假如在闪屏页用户直接点了跳过,这个时候留给首页准备的时间还是捉襟见肘。首页是采用Hybrid实现,上半部分是Native,下半部分是H5,H5时常来不及渲染,首页的统一弹框有时候出来也比较慢。所以我们又启动了关于首页优化的项目,Native侧的优化措施有:

1.首页接口请求BFF聚合,减轻客户端网络请求压力。

首页因为业务较复杂,一打开光Native就请求了七八个接口,客户端的网络请求压力可想而知,所以我们考虑对还款卡片,消息通知,品牌信息,滚动栏,异形广告位这五个接口做BFF聚合,这样就避免了因为网络请求队列阻塞而导致接口请求慢引起的白屏问题。

2.首页布局采用渐进式加载策略,提升首屏展示速度。

首页的布局本身就比较复杂,加之新的优化策略中把SplashFragment也挂载到了首页,首页的复杂度进一步提升,最开始首页是一次性加载的,这就导致首页的渲染耗时达到了近500ms,所以我们又对首页的加载策略做了优化,采用渐进式加载,先加载SplashFragment,SplashFragment展示后再去加载真正的首页,这里的具体实现细节不表。经过渐进式加载优化后首页的渲染时间降低了一半。

3.优化首页布局层级,提升首页渲染速度。

虽然经过渐进式加载优化后首页渲染速度已经快了不少,但是首页原本的布局复杂,嵌套层级较深,影响了渲染速度,最后又硬生生的把首页的布局层级减少了2层,布局压力得到缓解。

4.预加载统一弹窗,提升弹窗速度。

针对首页的弹窗弹出比较慢的情况,也对弹窗做了预加载操作。首页的统一弹窗本质上是一个H5页面,以往的方案都是首页加载完成后再去请求url,然后loadurl,预加载的思路也比较容易理解,即在首页创建之前先去请求url并load。

经过上诉的优化后首页视觉以及体验上连贯丝滑,用户体验大幅提升。然而首页下半部分的H5部分因为架构设计以及业务牵扯问题,目前还没有去做预加载操作,但整体效果已经好了不少。

启动速度优化

从用户手指点击桌面上的应用图标到屏幕上显示出应用主 Activity 界面而完成应用启动,快的话往往不到一秒,但是这整个过程却是十分复杂的,其中涉及了 Android 系统的几乎所有核心知识点。同时应用的启动速度也绝对是系统的核心用户体验指标之一,多年来,无论是谷歌或是手机系统厂商们还是各Android应用开发者,都在为实现应用打开速度更快一点的目标而不断努力。但是要想真正做好应用启动速度优化这件事情,须要对应用启动的整个流程有充分的认识和理解。首先应用的启动类型分为三种:

1:冷启动

2:热启动

3:温启动

其中冷启动是指从点击应用图标到UI界面完全显示且用户可操作的全部过程,可以简单的理解为一个应用从未被启动到完全启动的整个流程。

热启动是当我们按了Home键或其它情况app被切换到后台,再次启动app的过程。
热启动时,系统将activity带回前台。如果应用程序的所有activity存在内存中,则应用程序可以避免重复对象初始化、渲染、绘制操作。
如果由于内存不足导致对象被回收,则需要在热启动时重建对象,此时与冷启动时将界面显示到手机屏幕上一样。

温启动包含了冷启动的一些操作,由于app进程依然在,温启动只会重走Activity的生命周期,而不会重走进程的创建,Application的创建与生命周期等,这代表着它比热启动有更多的开销。
温启动有很多场景,例如:
用户按连续按返回退出了app,然后重新启动app;
由于系统收回了app的内存,然后重新启动app。

限于内容与篇幅的问题,本文则主要聚焦在冷启动流程的优化上,本文所述启动也皆指冷启动。

冷启动流程

1643354818710.jpg

启动流程:

1:点击桌面App图标,Launcher进程采用Binder IPC向system_server进程发起startActivity请求;

2:system_server进程接收到请求后,向zygote进程发送创建进程的请求;

3:Zygote进程fork出新的子进程,即App进程;

4:App进程,通过Binder IPC向sytem_server进程发起attachApplication请求;

5:system_server进程在收到请求后,进行一系列准备工作后,再通过binder IPC向App进程发送scheduleLaunchActivity请求;

6:App进程的binder线程(ApplicationThread)在收到请求后,通过handler向主线程发送LAUNCH_ACTIVITY消息;

7:主线程在收到Message后,通过发射机制创建目标Activity,并回调Activity.onCreate()等方法。

8:到此,App便正式启动,开始进入Activity生命周期,执行完onCreate/onStart/onResume方法,UI渲染结束后便可以看到App的主界面。

通常到了界面首帧绘制完成后,我们就可以认为启动已经结束了。然而这些都是系统行为,一般情况下我们是无法直接干预的。我们对启动速度的优化方向就是 Application和Activity的生命周期,因为这个阶段的时机对于我们来说是可控的。如下图是早期借款App对于启动项的管理,基本上是完全堆砌在主线程上的,随着业务的快速迭代,启动项也逐渐增多,启动速度濒于失控。我们对启动速度的优化,对启动流程的治理,很大一部分工作就是对启动任务做高效的管理。

b1a8addc-403b-4ad5-aab9-debbe087960a.png

启动速度优化

说到启动任务管理,第一时间可能会想到异步加载。将耗时任务放到子线程加载,等到所有加载任务加载完成之后,再进入首页。多线程异步加载方案确实是ok 的。但如果遇到前后依赖的关系呢。比如任务2 依赖于任务 1,这时候要怎么解决呢。最简单的方案是将任务1 丢到主线程加载,然后再启动多线程异步加载。
如果遇到更复杂的依赖呢?任务3 依赖于任务 2, 任务 2 依赖于任务 1 呢,这时候你要怎么解决。更复杂的依赖关系呢?总不能将任务 2,任务 3 都放到主线程加载吧,这样多线程加载的意义就不大了。有没有更好的方案呢?答案肯定是有的,使用有向无环图。它可以完美解决先后依赖关系。

有向无环图(Directed Acyclic Graph, DAG)是有向图的一种,字面意思的理解就是图中没有环。常常被用来表示事件之间的驱动依赖关系,管理任务之间的调度。有向无环图的原来以及具体实现过程,本文不再赘述。

自定义预设 2.jpg

如上图,想要执行任务5,必须是3和4都执行过,同理1执行完才能执行2和4,4执行完才能执行3,那么上述这个依赖关系的正确执行顺序即为12435。

2.jpg

前置任务:任务3依赖于任务 0,1,那么任务3的前置任务是任务 0,1。
子任务:任务0执行完之后,任务3才能执行,那么称呼任务3为任务0的子任务。

多线程中,任务的执行是随机的,那如何保证任务被依赖的任务先于任务执行呢?
首先我们要解决一个问题,它有哪些前置任务,这个可以用队列存储,代表它依赖的任务队列。当它所依赖的任务队列没有执行完毕,当前任务需要等待。当前依赖的任务队列为空,即代表改任务无前置任务或前置任务全都执行完毕,可以立刻执行。

具体实现

首先我们定义一个ITask的接口,再实现一个Task的任务包装类,将启动的任务项全都包装成Task

public interface ITask {

    @Priority
    int getPriority();

    ThreadType threadType();

    void run(@NonNull Remote remote) throws Exception;

    void onStart();

    void onError(Throwable throwable);

    void onComplete();


    interface LifecycleListener {

        void onComplete(Task task);

        void onStart(Task task);

        void onError(Task task, Throwable throwable);

    }
}

task流转与调度上,

 private void doPromoteRunner() {
        if (!startFlag.get() /*|| runningTask.size() >= concurrency*/) {
            return;
        }
        synchronized (this) {
            if (startTime == 0) {
                startTime = System.currentTimeMillis();
            }
            if (completed()) {
                startTime = 0;
                notifyRunCompleteTask();
            } else {
                for (Iterator<TaskWrapper> i = queue.iterator(); i.hasNext(); ) {
                    TaskWrapper call = i.next();
                    if (!call.isReady()) {
                        continue;
                    }
                    i.remove();
                    if (call.getRealTask().isExecutable()) {
                        runningTask.add(call);          findExecutorByThreadType(call.threadType()).execute(call);
                    }
                }
            }
        }
    }

做了上面的实现,已经可以保证任务之间的依赖关系以及执行顺序不会出错,然而
Java线程调度是抢占式的,线程优先级比较重要,需要区分。没有区分IO和CPU密集型任务,有可能导致主线程抢不到CPU于是在线程池设计上,我们提供了3个线程池:主线程,iO密集型线程池,CPU密集型线程池。我们在把启动相关做task包装的时候就要知道并定义好当前任务需要进入的线程池类型。

IO密集型任务:IO密集型任务不消耗CPU,核心池可以很大。常见的IO密集型任务如文件读取、写入,网络请求等等。
CPU密集型任务:核心池大小和CPU核心数相关。常见的CPU密集型任务如比较复杂的计算操作,此时需要使用大量的CPU计算单元。

任务集 (1).jpg

为了更好的监视启动任务的执行状况,我们还做了个小工具可以直观的展示启动任务的执行情况。通过对任务执行情况的分析灵活调整任务的调度流程,对于比较耗时的任务也做了专项的优化。通过一些列优化后,debug情况下,启动主线程耗时从1.5s降低到了300ms,启动速度得到了大幅提升。
启动任务执行情况见下图:


e0d224a9f31dcead2df7109bcf22485b.png

优化前借款App线上的平均启动速度近4s。


b1a8addc-403b-4ad5-aab9-debbe087960a.png

优化后线上的平均启动速度降低到1.3s。


1643354802224.jpg

未来规划

以上就是借款App在启动流程上的主要优化工作。虽然现在的启动速度比较快了,启动流程的体验也好了很多,但是我们追求极致性能与体验的脚步并没有停止,我们的启动速度目标是1s以内,也即所谓的秒开。当然我们也还有一些规划与手段还没来得及落地,比如前文提到的首页的webview还没有做预加载,空闪屏页也因为历史负债问题没有去掉,我们还可以对首页接口预请求,对布局做预渲染。当然我们还可以从系统层面考虑做MultiDex优化等等。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 197,814评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,124评论 2 375
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 144,814评论 0 327
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,924评论 1 268
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,815评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,562评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,944评论 3 388
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,582评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,859评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,881评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,700评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,493评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,943评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,115评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,413评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,978评论 2 343
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,182评论 2 339