App啟動(dòng)優(yōu)化最佳實(shí)踐

App啟動(dòng)優(yōu)化最佳實(shí)踐

優(yōu)化啟動(dòng)的意義

啟動(dòng)流程是用戶對(duì)我們App的第一體驗(yàn),打開應(yīng)用后才能去使用其提供的強(qiáng)大功能,就算我們應(yīng)用的內(nèi)部界面設(shè)計(jì)的再精美,功能再強(qiáng)大,如果啟動(dòng)速度過慢,用戶第一印象就會(huì)很差。更有甚者,如果用戶點(diǎn)擊App后,半天都打不開,用戶就可能失去耐心卸載應(yīng)用。一款A(yù)pp,啟動(dòng)流程承載的不止是用戶體驗(yàn)的第一場景,更是一款產(chǎn)品技術(shù)形象的初始印象。

我們遇到的問題

借款A(yù)pp很早就開始重視并著手對(duì)啟動(dòng)流程做優(yōu)化,早期借款A(yù)pp的啟動(dòng)項(xiàng)散落在整個(gè)啟動(dòng)流程各處,啟動(dòng)項(xiàng)之間又有依賴關(guān)系,如需增刪啟動(dòng)項(xiàng),或者調(diào)整啟動(dòng)項(xiàng)順序,就會(huì)牽一發(fā)而動(dòng)全身,整個(gè)啟動(dòng)流程穩(wěn)定性欠佳,啟動(dòng)速度慢,測試回歸成本高,啟動(dòng)卡住的情況偶有發(fā)生。啟動(dòng)流程治理已經(jīng)迫在眉睫,自此PPDFrame1.0誕生,1.0版本將啟動(dòng)任務(wù)與業(yè)務(wù)流程做了分離,并對(duì)啟動(dòng)流程做了物理隔離,將整個(gè)啟動(dòng)流程分為Application,Splash閃屏頁,首頁三個(gè)區(qū)域。以不影響啟動(dòng)速度為前提,將各啟動(dòng)任務(wù)梳理清楚后分別放置在對(duì)應(yīng)區(qū)域的各生命周期模塊里,后續(xù)的迭代過程如需修改啟動(dòng)項(xiàng),經(jīng)評(píng)估后決定該啟動(dòng)項(xiàng)應(yīng)該放在哪里。相當(dāng)長的一段時(shí)間里PPDFrame1.0版本有效的保證了啟動(dòng)流程的健壯與穩(wěn)定。然而隨著業(yè)務(wù)的快速迭代,啟動(dòng)項(xiàng)迅速從之前的20個(gè)左右增加到近40個(gè),加之合規(guī)需求對(duì)啟動(dòng)流程的不斷沖擊,整個(gè)啟動(dòng)流程又出現(xiàn)了新的問題:

1:閃屏頁跳轉(zhuǎn)至首頁導(dǎo)致的割裂感,用戶體驗(yàn)不連貫。
2:首頁加載慢,用戶已經(jīng)到了首頁,內(nèi)容還沒有渲染完成
3:啟動(dòng)速度慢,白屏?xí)r間長。

為了將上述問題一網(wǎng)打盡,是時(shí)候祭出PPDFrame2.0了。2.0完全拋棄了1.0的思路與實(shí)現(xiàn),針對(duì)目前的問題做通盤考慮,各個(gè)擊破。

閃屏頁跳轉(zhuǎn)至首頁導(dǎo)致的割裂感,用戶體驗(yàn)不連貫。

因?yàn)殚W屏頁是個(gè)Activity,即SplashActivity,首頁也是Activity,Activity之間的跳轉(zhuǎn)在App開發(fā)中其實(shí)是比較重的操作,給用戶的感覺就是割裂,不連貫。同時(shí)Activity的創(chuàng)建銷毀也都是有開銷的,基于此首先想到的是拋棄閃屏Activity,應(yīng)用直接啟動(dòng)首頁Activity,閃屏頁作為Fragment(SplashFragment)掛載到首頁。而實(shí)際情況是因?yàn)榻杩預(yù)pp已經(jīng)迭代多年,即便業(yè)務(wù)需求可以平移至SplashFragment中,但SplashActivity作為push,deeplink的入口,輕易改動(dòng)影響的業(yè)務(wù)方較多,其次SplashActivity還牽扯到復(fù)雜的Activity棧管理。基于此方案上仍然保留SplashActivity,但它是個(gè)空白閃屏不承載業(yè)務(wù),也沒有UI渲染,只作為一個(gè)跳板存在,創(chuàng)建即跳轉(zhuǎn)首頁并銷毀。流程圖大致如下:

11.jpg

基于上述修改,閃屏頁搖身一變成為了首頁的Fragment,閃屏頁的倒計(jì)時(shí)3...2...1...結(jié)束后,首頁的視圖早已利用倒計(jì)時(shí)的間隙在閃屏頁下默默的渲染好,用戶視感受驗(yàn)連貫不割裂,體驗(yàn)得到了提升。

首頁加載慢

雖然調(diào)整完啟動(dòng)流程,用戶體驗(yàn)著實(shí)有了改善,但假如在閃屏頁用戶直接點(diǎn)了跳過,這個(gè)時(shí)候留給首頁準(zhǔn)備的時(shí)間還是捉襟見肘。首頁是采用Hybrid實(shí)現(xiàn),上半部分是Native,下半部分是H5,H5時(shí)常來不及渲染,首頁的統(tǒng)一彈框有時(shí)候出來也比較慢。所以我們又啟動(dòng)了關(guān)于首頁優(yōu)化的項(xiàng)目,Native側(cè)的優(yōu)化措施有:

1.首頁接口請(qǐng)求BFF聚合,減輕客戶端網(wǎng)絡(luò)請(qǐng)求壓力。

首頁因?yàn)闃I(yè)務(wù)較復(fù)雜,一打開光Native就請(qǐng)求了七八個(gè)接口,客戶端的網(wǎng)絡(luò)請(qǐng)求壓力可想而知,所以我們考慮對(duì)還款卡片,消息通知,品牌信息,滾動(dòng)欄,異形廣告位這五個(gè)接口做BFF聚合,這樣就避免了因?yàn)榫W(wǎng)絡(luò)請(qǐng)求隊(duì)列阻塞而導(dǎo)致接口請(qǐng)求慢引起的白屏問題。

2.首頁布局采用漸進(jìn)式加載策略,提升首屏展示速度。

首頁的布局本身就比較復(fù)雜,加之新的優(yōu)化策略中把SplashFragment也掛載到了首頁,首頁的復(fù)雜度進(jìn)一步提升,最開始首頁是一次性加載的,這就導(dǎo)致首頁的渲染耗時(shí)達(dá)到了近500ms,所以我們又對(duì)首頁的加載策略做了優(yōu)化,采用漸進(jìn)式加載,先加載SplashFragment,SplashFragment展示后再去加載真正的首頁,這里的具體實(shí)現(xiàn)細(xì)節(jié)不表。經(jīng)過漸進(jìn)式加載優(yōu)化后首頁的渲染時(shí)間降低了一半。

3.優(yōu)化首頁布局層級(jí),提升首頁渲染速度。

雖然經(jīng)過漸進(jìn)式加載優(yōu)化后首頁渲染速度已經(jīng)快了不少,但是首頁原本的布局復(fù)雜,嵌套層級(jí)較深,影響了渲染速度,最后又硬生生的把首頁的布局層級(jí)減少了2層,布局壓力得到緩解。

4.預(yù)加載統(tǒng)一彈窗,提升彈窗速度。

針對(duì)首頁的彈窗彈出比較慢的情況,也對(duì)彈窗做了預(yù)加載操作。首頁的統(tǒng)一彈窗本質(zhì)上是一個(gè)H5頁面,以往的方案都是首頁加載完成后再去請(qǐng)求url,然后loadurl,預(yù)加載的思路也比較容易理解,即在首頁創(chuàng)建之前先去請(qǐng)求url并load。

經(jīng)過上訴的優(yōu)化后首頁視覺以及體驗(yàn)上連貫絲滑,用戶體驗(yàn)大幅提升。然而首頁下半部分的H5部分因?yàn)榧軜?gòu)設(shè)計(jì)以及業(yè)務(wù)牽扯問題,目前還沒有去做預(yù)加載操作,但整體效果已經(jīng)好了不少。

啟動(dòng)速度優(yōu)化

從用戶手指點(diǎn)擊桌面上的應(yīng)用圖標(biāo)到屏幕上顯示出應(yīng)用主 Activity 界面而完成應(yīng)用啟動(dòng),快的話往往不到一秒,但是這整個(gè)過程卻是十分復(fù)雜的,其中涉及了 Android 系統(tǒng)的幾乎所有核心知識(shí)點(diǎn)。同時(shí)應(yīng)用的啟動(dòng)速度也絕對(duì)是系統(tǒng)的核心用戶體驗(yàn)指標(biāo)之一,多年來,無論是谷歌或是手機(jī)系統(tǒng)廠商們還是各Android應(yīng)用開發(fā)者,都在為實(shí)現(xiàn)應(yīng)用打開速度更快一點(diǎn)的目標(biāo)而不斷努力。但是要想真正做好應(yīng)用啟動(dòng)速度優(yōu)化這件事情,須要對(duì)應(yīng)用啟動(dòng)的整個(gè)流程有充分的認(rèn)識(shí)和理解。首先應(yīng)用的啟動(dòng)類型分為三種:

1:冷啟動(dòng)

2:熱啟動(dòng)

3:溫啟動(dòng)

其中冷啟動(dòng)是指從點(diǎn)擊應(yīng)用圖標(biāo)到UI界面完全顯示且用戶可操作的全部過程,可以簡單的理解為一個(gè)應(yīng)用從未被啟動(dòng)到完全啟動(dòng)的整個(gè)流程。

熱啟動(dòng)是當(dāng)我們按了Home鍵或其它情況app被切換到后臺(tái),再次啟動(dòng)app的過程。
熱啟動(dòng)時(shí),系統(tǒng)將activity帶回前臺(tái)。如果應(yīng)用程序的所有activity存在內(nèi)存中,則應(yīng)用程序可以避免重復(fù)對(duì)象初始化、渲染、繪制操作。
如果由于內(nèi)存不足導(dǎo)致對(duì)象被回收,則需要在熱啟動(dòng)時(shí)重建對(duì)象,此時(shí)與冷啟動(dòng)時(shí)將界面顯示到手機(jī)屏幕上一樣。

溫啟動(dòng)包含了冷啟動(dòng)的一些操作,由于app進(jìn)程依然在,溫啟動(dòng)只會(huì)重走Activity的生命周期,而不會(huì)重走進(jìn)程的創(chuàng)建,Application的創(chuàng)建與生命周期等,這代表著它比熱啟動(dòng)有更多的開銷。
溫啟動(dòng)有很多場景,例如:
用戶按連續(xù)按返回退出了app,然后重新啟動(dòng)app;
由于系統(tǒng)收回了app的內(nèi)存,然后重新啟動(dòng)app。

限于內(nèi)容與篇幅的問題,本文則主要聚焦在冷啟動(dòng)流程的優(yōu)化上,本文所述啟動(dòng)也皆指冷啟動(dòng)。

冷啟動(dòng)流程

1643354818710.jpg

啟動(dòng)流程:

1:點(diǎn)擊桌面App圖標(biāo),Launcher進(jìn)程采用Binder IPC向system_server進(jìn)程發(fā)起startActivity請(qǐng)求;

2:system_server進(jìn)程接收到請(qǐng)求后,向zygote進(jìn)程發(fā)送創(chuàng)建進(jìn)程的請(qǐng)求;

3:Zygote進(jìn)程fork出新的子進(jìn)程,即App進(jìn)程;

4:App進(jìn)程,通過Binder IPC向sytem_server進(jìn)程發(fā)起attachApplication請(qǐng)求;

5:system_server進(jìn)程在收到請(qǐng)求后,進(jìn)行一系列準(zhǔn)備工作后,再通過binder IPC向App進(jìn)程發(fā)送scheduleLaunchActivity請(qǐng)求;

6:App進(jìn)程的binder線程(ApplicationThread)在收到請(qǐng)求后,通過handler向主線程發(fā)送LAUNCH_ACTIVITY消息;

7:主線程在收到Message后,通過發(fā)射機(jī)制創(chuàng)建目標(biāo)Activity,并回調(diào)Activity.onCreate()等方法。

8:到此,App便正式啟動(dòng),開始進(jìn)入Activity生命周期,執(zhí)行完onCreate/onStart/onResume方法,UI渲染結(jié)束后便可以看到App的主界面。

通常到了界面首幀繪制完成后,我們就可以認(rèn)為啟動(dòng)已經(jīng)結(jié)束了。然而這些都是系統(tǒng)行為,一般情況下我們是無法直接干預(yù)的。我們對(duì)啟動(dòng)速度的優(yōu)化方向就是 Application和Activity的生命周期,因?yàn)檫@個(gè)階段的時(shí)機(jī)對(duì)于我們來說是可控的。如下圖是早期借款A(yù)pp對(duì)于啟動(dòng)項(xiàng)的管理,基本上是完全堆砌在主線程上的,隨著業(yè)務(wù)的快速迭代,啟動(dòng)項(xiàng)也逐漸增多,啟動(dòng)速度瀕于失控。我們對(duì)啟動(dòng)速度的優(yōu)化,對(duì)啟動(dòng)流程的治理,很大一部分工作就是對(duì)啟動(dòng)任務(wù)做高效的管理。

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

啟動(dòng)速度優(yōu)化

說到啟動(dòng)任務(wù)管理,第一時(shí)間可能會(huì)想到異步加載。將耗時(shí)任務(wù)放到子線程加載,等到所有加載任務(wù)加載完成之后,再進(jìn)入首頁。多線程異步加載方案確實(shí)是ok 的。但如果遇到前后依賴的關(guān)系呢。比如任務(wù)2 依賴于任務(wù) 1,這時(shí)候要怎么解決呢。最簡單的方案是將任務(wù)1 丟到主線程加載,然后再啟動(dòng)多線程異步加載。
如果遇到更復(fù)雜的依賴呢?任務(wù)3 依賴于任務(wù) 2, 任務(wù) 2 依賴于任務(wù) 1 呢,這時(shí)候你要怎么解決。更復(fù)雜的依賴關(guān)系呢?總不能將任務(wù) 2,任務(wù) 3 都放到主線程加載吧,這樣多線程加載的意義就不大了。有沒有更好的方案呢?答案肯定是有的,使用有向無環(huán)圖。它可以完美解決先后依賴關(guān)系。

有向無環(huán)圖(Directed Acyclic Graph, DAG)是有向圖的一種,字面意思的理解就是圖中沒有環(huán)。常常被用來表示事件之間的驅(qū)動(dòng)依賴關(guān)系,管理任務(wù)之間的調(diào)度。有向無環(huán)圖的原來以及具體實(shí)現(xiàn)過程,本文不再贅述。

自定義預(yù)設(shè) 2.jpg

如上圖,想要執(zhí)行任務(wù)5,必須是3和4都執(zhí)行過,同理1執(zhí)行完才能執(zhí)行2和4,4執(zhí)行完才能執(zhí)行3,那么上述這個(gè)依賴關(guān)系的正確執(zhí)行順序即為12435。

2.jpg

前置任務(wù):任務(wù)3依賴于任務(wù) 0,1,那么任務(wù)3的前置任務(wù)是任務(wù) 0,1。
子任務(wù):任務(wù)0執(zhí)行完之后,任務(wù)3才能執(zhí)行,那么稱呼任務(wù)3為任務(wù)0的子任務(wù)。

多線程中,任務(wù)的執(zhí)行是隨機(jī)的,那如何保證任務(wù)被依賴的任務(wù)先于任務(wù)執(zhí)行呢?
首先我們要解決一個(gè)問題,它有哪些前置任務(wù),這個(gè)可以用隊(duì)列存儲(chǔ),代表它依賴的任務(wù)隊(duì)列。當(dāng)它所依賴的任務(wù)隊(duì)列沒有執(zhí)行完畢,當(dāng)前任務(wù)需要等待。當(dāng)前依賴的任務(wù)隊(duì)列為空,即代表改任務(wù)無前置任務(wù)或前置任務(wù)全都執(zhí)行完畢,可以立刻執(zhí)行。

具體實(shí)現(xiàn)

首先我們定義一個(gè)ITask的接口,再實(shí)現(xiàn)一個(gè)Task的任務(wù)包裝類,將啟動(dòng)的任務(wù)項(xiàng)全都包裝成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流轉(zhuǎn)與調(diào)度上,

 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);
                    }
                }
            }
        }
    }

做了上面的實(shí)現(xiàn),已經(jīng)可以保證任務(wù)之間的依賴關(guān)系以及執(zhí)行順序不會(huì)出錯(cuò),然而
Java線程調(diào)度是搶占式的,線程優(yōu)先級(jí)比較重要,需要區(qū)分。沒有區(qū)分IO和CPU密集型任務(wù),有可能導(dǎo)致主線程搶不到CPU于是在線程池設(shè)計(jì)上,我們提供了3個(gè)線程池:主線程,iO密集型線程池,CPU密集型線程池。我們在把啟動(dòng)相關(guān)做task包裝的時(shí)候就要知道并定義好當(dāng)前任務(wù)需要進(jìn)入的線程池類型。

IO密集型任務(wù):IO密集型任務(wù)不消耗CPU,核心池可以很大。常見的IO密集型任務(wù)如文件讀取、寫入,網(wǎng)絡(luò)請(qǐng)求等等。
CPU密集型任務(wù):核心池大小和CPU核心數(shù)相關(guān)。常見的CPU密集型任務(wù)如比較復(fù)雜的計(jì)算操作,此時(shí)需要使用大量的CPU計(jì)算單元。

任務(wù)集 (1).jpg

為了更好的監(jiān)視啟動(dòng)任務(wù)的執(zhí)行狀況,我們還做了個(gè)小工具可以直觀的展示啟動(dòng)任務(wù)的執(zhí)行情況。通過對(duì)任務(wù)執(zhí)行情況的分析靈活調(diào)整任務(wù)的調(diào)度流程,對(duì)于比較耗時(shí)的任務(wù)也做了專項(xiàng)的優(yōu)化。通過一些列優(yōu)化后,debug情況下,啟動(dòng)主線程耗時(shí)從1.5s降低到了300ms,啟動(dòng)速度得到了大幅提升。
啟動(dòng)任務(wù)執(zhí)行情況見下圖:


e0d224a9f31dcead2df7109bcf22485b.png

優(yōu)化前借款A(yù)pp線上的平均啟動(dòng)速度近4s。


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

優(yōu)化后線上的平均啟動(dòng)速度降低到1.3s。


1643354802224.jpg

未來規(guī)劃

以上就是借款A(yù)pp在啟動(dòng)流程上的主要優(yōu)化工作。雖然現(xiàn)在的啟動(dòng)速度比較快了,啟動(dòng)流程的體驗(yàn)也好了很多,但是我們追求極致性能與體驗(yàn)的腳步并沒有停止,我們的啟動(dòng)速度目標(biāo)是1s以內(nèi),也即所謂的秒開。當(dāng)然我們也還有一些規(guī)劃與手段還沒來得及落地,比如前文提到的首頁的webview還沒有做預(yù)加載,空閃屏頁也因?yàn)闅v史負(fù)債問題沒有去掉,我們還可以對(duì)首頁接口預(yù)請(qǐng)求,對(duì)布局做預(yù)渲染。當(dāng)然我們還可以從系統(tǒng)層面考慮做MultiDex優(yōu)化等等。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容