開發者文檔中提到,Android應用有三種啓動狀態,每種狀態都會影響應用向用戶顯示所需的時間:冷啓動、溫啓動或熱啓動。三種啓動狀態中,冷啓動耗時最久,系統和App有較多初始化的工做。若是啓動時間過長,可能會致使用戶在應用商店打低分,甚至徹底棄用app,因此冷啓動速度是各個app很是重要的性能指標之一。java
在冷啓動速度優化的工做中,打點是很是重要的一環,統計點位該如何選,以及爲何要這麼選,有不少細節值得探究,本文主要深刻探究Android端app層如何選擇進程建立的起點。express
本文中涉及的3個App層進程建立時間的起點:Application <init>,Process.getStartElapsedRealTime,/proc/self/stats starttime。微信
簡單介紹下3個進程建立時間起點:數據結構
3個進程建立時間起點時序以下:/proc/self/stats starttime 早於 Process.getStartElapsedRealTime 早於 Application <init>。
app
這三個時機哪一個更好?哪一個能指導優化工做?哪一個更接近用戶點擊桌面建立進程的起始點?帶着幾個問題,繼續往下看。socket
詳細看下三個時機:async
Applciation的構造方法,Android Java代碼能夠最早埋點的時機,Android開發童鞋對此時機都會比較熟悉,不過多贅述。ide
時序總覽圖:
函數
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構造方法中打斷點狀況以下:
/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 <init>時機和/proc/self/stats starttime時機做爲應用程序啓動的起點。
本文做者:
liuwenlong
在微信-搜索頁面中輸入「百度App技術」,便可關注微信官方帳號;或使用微信識別如下二維碼,亦可關注。