android性能優化(四)之啓動優化

 

 

一. 前言

 

當用戶在手機桌面上點擊一個從未打開過的App時(也就是冷啓動),到進入第一個頁面顯示這段時間,默認情況下您的App會首先出現一個白色/黑色屏,過一段時間纔是進入第一個Activity顯示其具體佈局內容。

 

對於一個專業的App來說,這種用戶體驗肯定是不能接受的,不僅會收到用戶吐槽,還會造成公司品牌受損。因此提升App的啓動速度是增強用戶體驗的重要指標。

 

而這個問題,相信很多朋友很早之前就遇到了,而且在網上也能搜到一大堆關於此問題的文章。但是,我看了前幾頁幾乎所有的文章之後,讓人失望的是這些文章要麼是隻提供解決方案,也就是說怎麼弄之後問題就解決了,至於爲什麼,不知道,反正大家都這麼幹;極少數有分析原因的文章,又讓人無法信服,讓人懷疑甚至分析是錯誤的。

 

爲了得到一個讓我自己信服的答案,哥不惜拖着疲憊的身軀深夜擼碼,一探究竟,接下來就給大家揭曉一下真實情況到底是什麼樣的。

 

二. 冷啓動白屏的真正原因

 

首先,要明確一點的是,要想弄清楚冷啓動白屏的真正原因,就必須知道冷啓動Android經歷了哪些代碼流程。

 

我們都知道Launcher本身也是一個特殊的App,其界面就是列出所有的App Icon,當用戶在手機桌面上點擊一個從未打開過的App時,其實是和正常的startActivity是一樣的。

 

通過閱讀源碼(具體代碼細節太長,這裏就省略了),我們知道主分支會經歷如下過程:

 

 1Launcher startActivity 
 2-> AMS查找應用進程是否創建 
 3-> 未創建,AMS通知從Zygote進程中fork創建出一個新的進程分配給該應用 
 4-> 進程創建完畢,AMS通過Binder通知應用進程
 5-> ActivityThread 調用 performLaunchActivity 
 6-> Application構造函數及attachBaseContext(),onCreate() 
 7-> new Activity(), 併爲此Activity創建一個new PhoneWindow(this) 
 8-> Activity onCreate() 
 9-> Activity setContentView() 
10-> new DecorView() & addView(contentRoot, contentParent) 
11-> onFinishInflate(): 此步驟只是inflate 所有的DecorView上的佈局views,並不可見 
12-> Activity onStart() 
13-> Activity onResume() 
14-> window.addView(mDecorView) 
15-> View onAttachedToWindow() onMeasure() onSizeChanged() onLayout() onDraw() 
16-> 至此步爲止才把DecorView加給Window,應用首頁纔可見,onWindowFocusChanged(true) 
17-> Activity onPause() 
18-> View onWindowFocusChanged(false )  
19-> Activity onStop() 
20-> Activity onDestroy() 
21-> View onDetackedFromWindow()

 

從以上流程,我們至少可以知道如下幾個重要的信息:

1)Application onCreate()與Activity,Window的生成時間,以及Activity和View生命週期的嚴格執行順序。

2)爲什麼只有當Activity在執行onResume()生命週期之後用戶才能真正看到佈局內容。

3)Activity, Window,DecorView及其之上的contentRoot, parentRoot, ToolBar之間的關係。

 

圖片來源於網絡

 

那麼問題來了:按道理來講,在點擊了App Icon之後一直到onResume才能看到App的第一屏頁面纔對,而且根據上面的流程,我們知道PhoneWindow()是在Application onCreate()之後才生成,那麼這個白屏究竟從何而來呢?

 

理論和實踐矛盾,必然是理論出了問題,肯定是哪裏有漏洞。就在我百思不得其解的情況下,搜索各方資料以及源碼,終於發現了線索:

 

     // android.view.WindowManager.LayoutParams
     /**
         * Window type: special application window that is displayed while the
         * application is starting.  Not for use by applications themselves;
         * this is used by the system to display something until the
         * application can show its own windows.
         * In multiuser systems shows on all users' windows.
         */
        public static final int TYPE_APPLICATION_STARTING = 3;

 

原來Android系統早已爲我們考慮到了一個問題:

就是當冷啓動一個APP時,創建進程需要一定時間,再創建完成前,界面不會作出反應。此時會給用戶造成一種沒有點擊到APP的錯覺,影響體驗。

 

爲了改善用戶體驗,Starting Window出現了,它會在創建進程這個期間顯示,讓用戶感覺到APP啓動了,而Starting Window就是白屏/黑屏的原因,它是黑屏還是白屏,默認取決於第一個啓動的Activity的Theme,如果該Activity沒設置Theme,默認使用Application的Theme ,這就有了Starting Window的概念,也可以稱之爲Preview Window。

 

那Starting Window具體是什麼時候出現的呢?

 

其實在AMS請求創建應用進程之前,就已經通過WMS請求創建了,代碼是在Activity狀態管理者ActivityStack開始執行顯示啓動窗口的流程:

 

 //ActivityStack
 final void startActivityLocked(ActivityRecord r, boolean newTask, boolean keepCurTransition,
            ActivityOptions options) {
        if (!isHomeStack() || numActivities() > 0) {//HOME_STACK表示Launcher桌面所在的Stack
            // 1.首先當前啓動棧不在Launcher的桌面棧裏,並且當前系統已經有激活過Activity
            // We want to show the starting preview window if we are
            // switching to a new task, or the next activity's process is
            // not currently running.

            boolean doShow = true;
            if (newTask) {
                // 2.要將該Activity組件放在一個新的任務棧中啓動              
                // Even though this activity is starting fresh, we still need
                // to reset it to make sure we apply affinities to move any
                // existing activities from other tasks in to it.
                if ((r.intent.getFlags() & Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED) != 0) {
                    resetTaskIfNeededLocked(r, r);
                    doShow = topRunningNonDelayedActivityLocked(null) == r;
                }
            } else if (options != null && options.getAnimationType()
                    == ActivityOptions.ANIM_SCENE_TRANSITION) {
                doShow = false;
            }
            if (r.mLaunchTaskBehind) {
                //3. 熱啓動,不需要啓動窗口
                // Don't do a starting window for mLaunchTaskBehind. More importantly make sure we
                // tell WindowManager that r is visible even though it is at the back of the stack.
                mWindowManager.setAppVisibility(r.appToken, true);
                ensureActivitiesVisibleLocked(null, 0, !PRESERVE_WINDOWS);
            } else if (SHOW_APP_STARTING_PREVIEW && doShow) {
                //4. 顯示啓動窗口
                r.showStartingWindow(prev, showStartingIcon);
            }
        } else {
            // 當前啓動的是桌面Launcher (開機啓動)
            // If this is the first activity, don't do any fancy animations,
            // because there is nothing for it to animate on top of.    
        }
    }

圖片來源於網絡

 

Starting Window就是一個用於在應用程序進程創建並初始化成功前顯示的臨時窗口,擁有的Window Type是TYPE_APPLICATION_STARTING。

 

在程序初始化完成前顯示這個窗口,以告知用戶系統已經知道了他要打開這個應用並做出了響應,當程序初始化完成後顯示用戶UI並移除這個窗口。

 

至此爲止,冷啓動白屏的原因真相大白!

 

一切都解釋通了,從啓動開始,系統創建了Starting Window,其主題和LAUNCHER Activity/Application theme一致,如果沒有設置就使用系統默認的主題,即系統會將屏幕填充主題默認的背景色,亮系主題填充白色,暗系主題填充黑色,就出現了Activity啓動之前的黑/白屏現象。

 

 

三. 白屏視覺優化

 

知道了原因,那麼解決辦法也很簡單。既然Starting Window的主題依賴於LAUNCHER Activity或者Application theme,我們完全可以設置一個SplashActivity閃屏頁,併爲其設置主題如下:

 

<style name=SplashTheme" parent="@android:style/Theme.Light.NoTitleBar.Fullscreen">
        <item name="android:windowBackground">@drawable/splash_bg</item>
        <item name="android:windowFullscreen">true</item>
</style>

 

配置入口Activity的theme屬性爲上面定義的style:

 

<activity
    android:name=".SplashActivity"
    android:configChanges="orientation|screenSize|keyboardHidden"
    android:screenOrientation="portrait"
    android:theme="@style/SplashTheme">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

 

自定義的splash_bg可以放任意設置的圖片,如此一來,Preview Window在冷啓動時就能立馬看到splash_bg,直到跳轉到啓動頁時同樣也顯示的是splash_bg,這樣就能做到視覺上的無縫過渡,不僅讓App看起來有及時的點擊反饋,而且更加漂亮。

 

至此,白屏問題得到完美解決。

 

最後,這裏再提一下網上說的幾種錯誤的說法:

1)因爲Application.onCreate()執行比較耗時,纔出現了白屏現象。

很明顯Application.onCreate()執行耗時只會延長白屏時間,並非白屏出現的根本原因。

 

2)將主題背景變成透明的,就不會有黑/白屏的現象。

這種方法將使得在LAUNCHER Activity加載出來之前,用戶會透過Window看到桌面,就又讓用戶回到了點擊App無及時反應的境地。

 

3)禁用Starting Window

和2)一樣,系統好不容易提供了一個預覽窗口,你居然給禁了?肯定不行。

 

好了,到現在爲止,我們知道,閃屏效果會從點擊App Icon開始,一直持續到應用的LAUNCHER Activity纔會移除,即使我們對白屏做了視覺優化,那也僅僅是在響應效果上做了反饋優化,讓用戶感覺App有了及時的反應和漂亮的UI,客觀上並沒有改變啓動的等待時長。

 

因此,我們還必須從本質上要減少執行的時間。從之前的流程分析,我們可以控制的有三大步:Application onCreate()執行的時間,SplashActivity頁面的停留時間,用戶進入HomeActivity頁面後進入onResume()的時間。

 

一. Application onCreate()速度優化

 

而隨着App開發的功能越來越多,需要加入的第三方庫也越來越多,而這些第三方庫大多要求在Application的onCreate()裏初始化,而我們知道onCreate默認是在UI線程裏運行的,如果初始化的東西非常多的話,勢必會造成進入第一個Activity的時間推遲。

 

一邊是要用到的功能需要初始化,一邊又會延長onCreate()的執行時間,那我們該如何平衡二者之間的關係呢?

 

我們就需要對SDK的初始化做一下具體的分類了,分爲如下四類:

 

1)一定要在主線程中初始化,且入口Activity可能立即會用到,或者第三方SDK強制要求。

這種沒辦法,必須放在Application的onCreate()中執行。

 

2)一定要在主線程中初始化,但是入口Activity不會用到,即可以延遲初始化。

這種就可以在Application的onCreate()中去掉了。取而代之的是,可以用懶加載的方式進行初始化,即只有在第一次使用的時候纔去初始化。

 

3)可以在子線程中初始化,但是入口Activity不會用到,即可以延遲初始化。

可以放在Application的onCreate()中,但是需要放在子線程中執行。由於這種情況可能比較多,所以最好是放入線程池中。

 

4)可以在子線程中初始化,且入口Activity可能立即會用到。

這種情況下可以依然放在Application的onCreate()的子線程中執行,但是需要注意的是做好線程間的通信,即子線程初始化完畢後,必須能夠通知到HomeActivity的使用處。

 

總結成表格,如下:

 

二. SplashActivity速度優化

 

以上的分類和初始化策略,同樣適用於SplashActivity,目的都是儘量縮短進入HomeActivity的時間。

 

另外,對於SplashActivity經常會有廣告圖片的加載,這種情況下可以採用靜默下載,每次加載上次下載下來的圖片和數據的緩存的策略,從而避免了首次實時下載耗時的情況。

 

還有一些特殊的業務場景,比如SplashActivity有大量網絡請求(有可能還插入本地DB),此時可以和後臺溝通,把多個網絡接口整合爲一個,如此多次網絡I/O和磁盤I/O就被縮減爲了一次。

 

三. HomeActivity加速顯示

 

在提速了之前2個步驟之後,就來到了HomeActivity,此時仍然還不能看到佈局,所以我們需要儘量減少onCreate()的時間,儘快進入onResume()使用戶能看到頁面。

 

在這一步上,我們可以做的:

1)儘量減少佈局的複雜度,詳情請參考《android性能優化(一)之UI渲染優化》

2)當有多個Fragment在不同底Tab或者ViewPager時,可以採用懶加載的方式使用戶在使用到某個Fragment時再去加載,避免同時加載這些Fragment。


四. GC抑制提速

 

以上三個步驟都是基於流程的常規提速手段。最後再提一種三界之外的黑科技,就是支付寶團隊採用的GC抑制思想。

 

所謂GC抑制就是,在App啓動的過程中,通過修改內存中的 Dalvik 庫文件 libdvm.so 影響 Dalvik 的行爲,從而阻止Davlik在此過程中進行垃圾回收的思想。因爲在運行過程中,由於Java的GC機制會阻塞 Java 程序的執行,佔用 CPU 資源,佔用額外內存。

 

其實,平時你只要稍微留意,就能發現LogCat中有關於GC的log打印出來。主要有以下3種:

 

GC_EXPLICIT:Dalivk 給開發人員提供的主動觸發GC的API,讀者可以參看Google Maps的設計來體會這個API的用法。

 

GC_FOR _ALLOCK:是分配對象失敗時觸發的GC,這個GC會將應用所有的Java線程暫停運行,直到GC結束。

 

GC_CONCURRENT:是Java虛擬機根據堆的當前狀態觸發的GC,這個GC在Dalvik單獨GC線程裏運行,在部分時間裏不影響應用Java線程的運行。


通過簡單統計這些GC消耗的時間,我們能夠得出GC嚴重影響應用啓動時間的結論:

 

 

通過GC抑制的思想,阻止App在啓動過程中Dalvik做任何的GC操作,任其內存增長,在觸發OOM前停止GC抑制行爲,採用空間換時間的方式進一步使App啓動速度達到極致!

一切從android的handler說起(一)之message

一切從android的handler說起(二)之threadLocal

一切從android的handler說起(三)之UI線程不卡頓

一切從android的handler說起(四)之postDelay原理

一切從android的handler說起(五)之觸摸事件模型

一切從android的handler說起(六)之生命週期來源

一切從android的handler說起(七)之Handler內存泄露


 

進入公衆號,回覆“程序員“可以領取一份計算機技術電子書福利合集

歡迎轉發,關注公衆號 肖暉

每天幾分鐘,掌握一個硬核面試知識點

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章