您好,登錄后才能下訂單哦!
這篇文章將為大家詳細講解有關如何深入探究Android應用啟動起點,文章內容質量較高,因此小編分享給大家做個參考,希望大家閱讀完這篇文章后對相關知識有一定的了解。
開發者文檔中提到,Android應用有三種啟動狀態,每種狀態都會影響應用向用戶顯示所需的時間:冷啟動、溫啟動或熱啟動。三種啟動狀態中,冷啟動耗時最久,系統和App有較多初始化的工作。如果啟動時間過長,可能會導致用戶在應用商店打低分,甚至完全棄用app,所以冷啟動速度是各個app非常重要的性能指標之一。
在冷啟動速度優化的工作中,打點是非常重要的一環,統計點位該如何選,以及為什么要這么選,有很多細節值得探究,本文主要深入探究Android端app層如何選擇進程創建的起點。
文中涉及的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>。
這三個時機哪個更好?哪個能指導優化工作?哪個更接近用戶點擊桌面創建進程的起始點?帶著幾個問題,繼續往下看。
詳細看下三個時機:
Application <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>時機早,在Application構造方法中打斷點情況如下:
/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 <init>時機和/proc/self/stats starttime時機作為應用程序啟動的起點。
Application <init>時機是Android Java代碼可以最先埋點的地方,通過此起點,再結合冷啟動的結束點位,可明確知曉工程代碼的詳細耗時,對于指導日常優化工作有較大意義;
/proc/self/stats starttime時機為三個時機中最早的,其中有工程代碼不可控的耗時,涉及到進程數據結構、線程棧等初始化工作,但是此時機會更接近用戶的實際感受,可以最大程度用來衡量用戶啟動體驗;
Process.getStartElapsedRealTime由于有版本的限制,在Android N以下版本無法獲取,無法兼顧大盤所有的用戶機器,此值的指導價值就沒那么大,優化工作中,重中之重是優化中低端機器的性能體驗,如果Android N以下機型無法獲取,則會有大量的低端機器的啟動性能不在統計范圍內。
關于如何深入探究Android應用啟動起點就分享到這里了,希望以上內容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。