深刻探究Android應用啓動起點

背景

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

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

三個時機簡述

本文中涉及的3個App層進程建立時間的起點:Application <init>,Process.getStartElapsedRealTime,/proc/self/stats starttime。微信

簡單介紹下3個進程建立時間起點:數據結構

  • Application <init>:Application構造方法;
  • Process.getStartElapsedRealTime:Framework中記錄的進程建立的起點,此接口有版本限制,Android N如下版本沒法使用;
  • /proc/self/stats starttime:內核中記錄的進程建立的起點。

3個進程建立時間起點時序以下:/proc/self/stats starttime 早於 Process.getStartElapsedRealTime 早於 Application <init>。
三個時機.jpgapp

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

深刻分析

詳細看下三個時機:async

1.Application <init>時機

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

2.Process.getStartElapsedRealTime時機

時序總覽圖:
時序總覽圖.jpg函數

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

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>時機早,在Application構造方法中打斷點狀況以下:
Application構造方法中打斷點狀況.jpg

3./proc/self/stats starttime時機

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

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

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

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

 .jpg

 .jpg

 .jpg

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 <init>時機和/proc/self/stats starttime時機做爲應用程序啓動的起點。

  • Application <init>時機是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從睡夢中醒來,開始建立進程,細節不贅述了,感興趣的童鞋能夠搜下相關資料,在網上有不少教程。

本文做者:
liuwenlong


在微信-搜索頁面中輸入「百度App技術」,便可關注微信官方帳號;或使用微信識別如下二維碼,亦可關注。
微信連接.jpg

相關文章
相關標籤/搜索