深入探究Android應用啓動起點 背景 三個時機簡述 深入分析 總結

好文推薦
原文鏈接:https://www.androidos.net.cn/doc/2020/10/7/537.html

背景

開發者文檔中提到,Android應用有三種啓動狀態,每種狀態都會影響應用向用戶顯示所需的時間:冷啓動、溫啓動或熱啓動。三種啓動狀態中,冷啓動耗時最久,系統和App有較多初始化的工作。如果啓動時間過長,可能會導致用戶在應用商店打低分,甚至完全棄用app,所以冷啓動速度是各個app非常重要的性能指標之一。

在冷啓動速度優化的工作中,打點是非常重要的一環,統計點位該如何選,以及爲什麼要這麼選,有很多細節值得探究,本文主要深入探究Android端app層如何選擇進程創建的起點。

三個時機簡述

本文中涉及的3個App層進程創建時間的起點:Application <init style="box-sizing: border-box;">,Process.getStartElapsedRealTime,/proc/self/stats starttime。</init>

簡單介紹下3個進程創建時間起點:

  • Application <init style="box-sizing: border-box;">:Application構造方法;</init>
  • Process.getStartElapsedRealTime:Framework中記錄的進程創建的起點,此接口有版本限制,Android N以下版本無法使用;
  • /proc/self/stats starttime:內核中記錄的進程創建的起點。

3個進程創建時間起點時序如下:/proc/self/stats starttime 早於 Process.getStartElapsedRealTime 早於

這三個時機哪個更好?哪個能指導優化工作?哪個更接近用戶點擊桌面創建進程的起始點?帶着幾個問題,繼續往下看。

深入分析

詳細看下三個時機:

  • Application <init style="box-sizing: border-box;"></init>時機

Applciation的構造方法,Android Java代碼可以最先埋點的時機,Android開發童鞋對此時機都會比較熟悉,不過多贅述。

  • Process.getStartElapsedRealTime****時機

    時序總覽圖:

Process.getStartElapsedRealTime的賦值接口爲handleBindApplication接口,賦值時機爲App進程進入Java世界後,進程attach到ActivityManagerService,再通過binder call返回到App進程時。原理細節可繼續閱讀源碼解析。

源碼解析:

Android 8.1.0的源碼中一段說明(Process.java):
487    /**
488     * Return the {@link SystemClock#elapsedRealtime()} at which this process was started.
489     */
490    public static final long getStartElapsedRealtime() {
491        return sStartElapsedRealtime;
492    }

從源碼的說明中可知,Process.getStartElapsedRealTime代表程序創建開始的時間,
SystemClock#elapsedRealtime表示距離boot的真實時間,看下其賦值時機(ActivityThread.java):
5429    private void handleBindApplication(AppBindData data) {...
5436        // Note when this process has started.
5437        Process.setStartTimes(SystemClock.elapsedRealtime(), SystemClock.uptimeMillis());

handleBindApplication是在ActivityThread主線程H的消息處理中被調用的,
H作爲ActivityThread的內部類,是主線程處理消息的Handler。
234    final H mH = new H();

 這個消息是誰發的,什麼時候發的呢?瞭解Android App的入口函數及創建過程的同學,可能不難解答這個問題。
App的創建,Java層調用的入口爲ActivityThread main方法,看下:
6459    public static void main(String[] args) {...
6478        Looper.prepareMainLooper();
6479
6480        ActivityThread thread = new ActivityThread();
6481        thread.attach(false);...
6494        Looper.loop();

 從代碼中看,main方法中主要是準備主線程消息Looper,執行ActivityThread attach方法,然後主線程開始消息循環。
看下ActivityThread attach:
6315    private void attach(boolean system) {
6318        if (!system) {
6328            final IActivityManager mgr = ActivityManager.getService();
6329            try {
6330                mgr.attachApplication(mAppThread);
6331            } catch (RemoteException ex) {
6332                throw ex.rethrowFromSystemServer();
6333            }

 從代碼可知,此處有binder調用,調用AMS的attachApplication,此調用是在system_server進程,執行如下操作。
看下ActivityManagerService處理過程:
7215    public final void attachApplication(IApplicationThread thread) {
7216        synchronized (this) {
7219            attachApplicationLocked(thread, callingPid);
7221        }
7222    }
6911    private final boolean attachApplicationLocked(IApplicationThread thread,
6912            int pid) {…
7102                thread.bindApplication(processName, appInfo, providers,
7103                        app.instr.mClass,
7104                        profilerInfo, app.instr.mArguments,
7105                        app.instr.mWatcher,
7106                        app.instr.mUiAutomationConnection, testMode,
7107                        mBinderTransactionTrackingEnabled, enableTrackAllocation,
7108                        isRestrictedBackupMode || !normalMode, app.persistent,
7109                        new Configuration(getGlobalConfiguration()), app.compat,
7110                        getCommonServicesLocked(app.isolated),
7111                        mCoreSettingsObserver.getCoreSettingsLocked(),
7112                        buildSerial);

 比較關鍵的調用:thread.bindApplication, thread是Binder對象,這個地方又有binder調用,看看執行者:
690    private class ApplicationThread extends IApplicationThread.Stub {
899        public final void bindApplication(String processName, ApplicationInfo appInfo,
900                List<ProviderInfo> providers, ComponentName instrumentationName,
901                ProfilerInfo profilerInfo, Bundle instrumentationArgs,
902                IInstrumentationWatcher instrumentationWatcher,
903                IUiAutomationConnection instrumentationUiConnection, int debugMode,
904                boolean enableBinderTracking, boolean trackAllocation,
905                boolean isRestrictedBackupMode, boolean persistent, Configuration config,
906                CompatibilityInfo compatInfo, Map services, Bundle coreSettings,
907                String buildSerial)
A 
pplicationThread執行sendMessage(H.BIND_APPLICATION, data);

將消息發送出去,此部分的執行爲App進程的binder線程池裏,是如何切換至主線程執行的呢?
2605    private void sendMessage(int what, Object obj, int arg1, int arg2, boolean async) {
2609        Message msg = Message.obtain();...
2617        mH.sendMessage(msg);
2618    }  

 通過mH,將消息發送到主線程的Looper,主線程執行,
1462    private class H extends Handler {
1473        public static final int BIND_APPLICATION = 110;
1580        public void handleMessage(Message msg) {
1653                case BIND_APPLICATION:
1656                    handleBindApplication(data);
1658                    break;

handleBindApplication就是Process.getStartElapsedRealTime獲取對進程創建的起點,後續邏輯就是Application的初始化的工作,由此可見Process.getStartElapsedRealTime時機是比Application<init style="box-sizing: border-box;">時機早,在Application構造方法中打斷點情況如下:</init>

/proc/self/stats starttime時機

/proc/self/stats starttime時機是kernel層記錄的進程創建起點,爲3個時機中最早的。詳細看下:

proc/pid/stat用於獲取某一個進程的統計信息,內容形式如下:

在proc/pid/stat統計信息中,starttime爲第22個元素。starttime的值什麼含義,以及是如何計算出來的呢?看下fs/proc/array.c的do_task_stat()

從內核代碼中可知:start_time取值爲task的real_start_time,先看下nesc_to_clock_t方法:

div_u64_rem方法爲無符號除法操作:除數是無符號64bit,被除數是無符號32,remainder爲餘數。

從計算過程來看,是把real_start_time除以1000000000/100=10000000,real_start_time單位是什麼呢?看下數據結構task_struct定義:

    struct timespec start_time;      
    struct timespec real_start_time;

task_struct中有兩個時間:start_time 和 real_start_time,其中後者包含睡眠時間,兩個時間單位均爲ns,/proc/self/stats starttime取的值爲real_start_time:

struct timespec
{
__time_t tv_sec;        /* Seconds. */
long   tv_nsec;       /* Nanoseconds. */
};

由此可見,real_start_time單位爲ns,如果將real_start_time除以1000000000/100=10000000,換算完單位爲10ms,比如/proc/self/stats starttime讀取到的值爲100,則需換算爲100*10ms=1000ms。而我們啓動速度日常大概率會以ms爲計算精度,/proc/self/stats starttime會損失一定的精度,內核爲何會做此種處理呢?

在內核的時間統計方式中,有個單位爲jiffies,jiffies是內核中的一個全局變量,用來記錄自系統啓動以來產生的節拍數。簡單描述就是1s內,內核發起的時鐘中斷次數,kernel中就使用這個來對程序的運行時間進行統計。而/proc/self/stats starttime統計單位正是jiffies,代表應用程序冷啓動後經過了多少個內核時鐘。

那我們該如何科學的統計以及換算/proc/self/stats starttime的值呢?Linux 系統上Man proc有下面一段解釋:

(22) starttime %llu

The time the process started after system boot. In kernels before Linux 2.6, this value was expressed in jiffies. Since Linux 2.6, the value is expressed in clock ticks (divide by sysconf(_SC_CLK_TCK)).

The format for this field was %lu before Linux 2.6.

在內核態的常量USER_HZ我們無法獲取,但可以通過在用戶態通過sysconf(_SC_CLK_TCK)獲取到其值。

計算公式如下:

/proc/self/stats starttime * 1000 / sysconf(_SC_CLK_TCK),單位ms

可能有些同學會說,sysconf(_SC_CLK_TCK)的值是100,直接用/proc/self/stats starttime * 10即可,但需考慮內核的升級或內核定製場景,使用sysconf(_SC_CLK_TCK)獲取並參與計算爲最穩妥的方式。

再一個問題,/proc/self/stats starttime 是來自task_struct real_start_time,這個時間初始化是在什麼時候呢?答案就是task_struct數據結構被創建的時候,也就是進程被創建的時候,即 zygote fork時機,fork系統調用會把子進程的數據結構task_struct、線程棧等數據結構初始化,感興趣的同學可以去看內核的fork源碼。

總結

通過上述的詳細分析,已經對三個時機有較爲詳細的瞭解。在實際App工程中,建議結合使用Application時機和/proc/self/stats starttime時機作爲應用程序啓動的起點。

  • Application時機是Android Java代碼可以最先埋點的地方,通過此起點,再結合冷啓動的結束點位,可明確知曉工程代碼的詳細耗時,對於指導日常優化工作有較大意義;

  • /proc/self/stats starttime時機爲三個時機中最早的,其中有工程代碼不可控的耗時,涉及到進程數據結構、線程棧等初始化工作,但是此時機會更接近用戶的實際感受,可以最大程度用來衡量用戶啓動體驗;

  • Process.getStartElapsedRealTime由於有版本的限制,在Android N以下版本無法獲取,無法兼顧大盤所有的用戶機器,此值的指導價值就沒那麼大,優化工作中,重中之重是優化中低端機器的性能體驗,如果Android N以下機型無法獲取,則會有大量的低端機器的啓動性能不在統計範圍內。

  • 可能有的童鞋還會有一個疑問,爲什麼說/proc/self/stats starttime更接近用戶的實際啓動體驗,而不是用戶的全部啓動體驗呢?熟悉應用程序啓動過程的同學就會比較瞭解這個問題,Android應用程序啓動是從用戶點擊桌面圖標開始,點擊圖標的第一響應是在Launcher進程,通過ActivityManagerService將創建進程信息傳給zygote,zygote再執行fork,中間經歷了兩次跨進程通信,一次是Launcher進程通過Binder調用進入system_server進程,一次是system_server進程通過socket將創建進程信息傳給zygote,zygote從睡夢中醒來,開始創建進程,細節不贅述了

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