薛海龍,陳 渝,雷 蕾,王 丹
1.北京工業大學 計算機學院,北京 100124
2.清華大學 計算機學院,北京 100084
Android系統以其突出的市場占有率和開放的特性吸引了大量的開發人員,上百萬類型豐富而且創意獨特的應用程序應運而生。然而,用戶在享受豐富多彩應用的同時,較差的響應性能一直影響著使用體驗。例如,應用的啟動時間過長,不能在短時間內迅速對用戶輸入進行反饋等。在功能相似的Android應用中,響應性能差的往往最先被淘汰。面對競爭如此激烈的Android應用市場,開發人員迫切需要研制有效的手段來解決這個問題。
Android系統的程序開發框架使用單線程模型來處理用戶輸入事件,應用啟動時,系統創建一個UI線程來運行應用程序,這個線程主要負責處理生命周期事件、用戶輸入事件和顯示更新事件。所有的事件默認情況下在UI線程中順序執行。因此,響應性能低下的原因往往是由于在UI線程執行耗時操作導致的,例如磁盤I/O、數據庫的讀取或者網絡訪問等,這些操作會導致UI線程阻塞,使得顯示不能及時更新,或者用戶陷入長時間的等待中。為避免出現這個問題,移動開發采用了異步編程技術,將這些耗時操作放到UI線程以外的工作線程中去執行。由于Android提供了多種異步編程模型供開發人員使用,例如HandlerThread、IntentService、AsyncTask、ThreadPool以及新建Thread類來執行異步操作。因此,使用異步編程技術可在一定程度上提升系統性能。
然而,從異步編程模型的特點可知,采用該模型的程序往往會在多條線程中異步執行,使得它的執行過程變得十分復雜。如果使用了這些編程模型,性能卻沒有達到預期的效果,開發人員就很難找到性能異常的根源所在。因此需要研究幫助開發人員分析程序在多條線程中的執行情況的有效方法。
2012年,微軟的Ravindranath等人為了幫助Windows手機應用開發人員了解相應應用在真實使用過程中的性能瓶頸和運行失敗原因,提出了一種在真實使用過程中跟蹤應用性能的檢測分析方法[1]。該方法關注以用戶對UI的操作為開始節點,以由操作觸發應用程序中的所有同步和異步任務處理完成為結束節點,為識別異步處理任務的性能瓶頸還需要正確跟蹤跨異步邊界處理的因果關系。在實現上,該方法首先確定構建用戶事務的執行跟蹤所需的全部信息,包括UI操作事件信息、線程執行信息、異步調用因果關系、線程同步信息、UI更新信息以及未處理的異常[2]。然后通過引入兩個自定義的庫,對Applications層App的二進制代碼進行動態插樁,向開發人員展示異步程序執行的全部流程,最終構建出程序執行的關鍵路徑,給開發人員指向可能導致響應性能異常的根源。
利用這種方法,Ravindranath等人開發了App-Insight[3],它準確地向開發人員展示了異步程序執行的整體流程,并構建出程序執行關鍵路徑幫助開發人員識別性能瓶頸和異常根源。AppInsight是基于Windows編程框架中的指定接口實現的,因此只能對Windows平臺上的應用進行性能監測與分析,并不能直接應用到Android應用性能的監測分析上。
Facebook公司在這個方法的基礎上,針對于Android編程框架的特點,開發出了一套遠程性能監控工具,用于對自己所開發的應用在真實用戶的使用情況進行監測[4]。Facebook官方稱利用此方法確實解決了一些性能問題。
然而,以上對App的二進制源碼進行動態插樁的辦法只能作用于App自身,即只有被測的App啟動之后,才可以監測用戶的行為操作得到插入的Log,App啟動過程中的異步性能瓶頸是沒辦法檢測到的。針對這個問題,本文提出對Android系統的Framework層進行靜態插樁來捕捉異步任務處理的各項信息,以監測到App啟動之后,并覆蓋到App啟動過程中的所有異步執行流程。本文的主要貢獻:
(1)提出在Framework層進行靜態插樁對應用程序層的所有異步任務的執行進行的跟蹤方法。該方法可以有針對性地在Framework層的API接口中找到其對應的關鍵代碼,通過一次插樁就可以解決所有應用程序的監測,相比較對應用程序的插樁是更輕量級的,減輕了性能分析的復雜性。
(2)在實現策略上,本文通過對異步任務上的所有事務信息進行插樁后,找到處理流程的一條關鍵路徑。該路徑上包括觸發用戶事務的關鍵事件。如果存在異步任務的處理會有新線程開始的時間戳,之后就是異步任務開始處理的過程,處理完成后會通過Handler發送消息給主線程進行界面的刷新。通過對關鍵路徑的分析,可幫助開發人員找到應用程序的異步任務性能瓶頸。
Android的系統架構和其操作系統一樣,采用了分層的架構,分為4層,從高層到低層分別是應用程序層、應用程序框架層(Framework)、系統運行庫層和Linux核心層[5],如圖1所示。
Framework層提供給Android開發人員一系列的服務和API的接口,然后將一些基本功能實現,通過接口提供給上層調用,可以重復調用。應用程序層的所有App都是通過調用Framework層的各種API來實現業務需求的。
Android程序的大多數代碼操作都必須在主線程執行,例如系統事件、輸入事件、程序回調服務、UI繪制等。那么在上述事件或者方法中插入的代碼也將在主線程中執行。一旦在主線程里面添加了操作復雜的代碼,這些代碼會影響應用程序的響應性能。因此,為了提高用戶體驗或者避免ANR(application is not responding)通常都會把一些耗時的任務放在子線程中去處理。
對于異步執行的任務,不需要等待返回結果,而是等任務處理完成后,再通知界面刷新。在Android中開啟異步任務的方式主要有Thread、HandlerThread、IntentService、AsyncTask和ThreadPool這5種[6],開發人員可以針對不同的應用場景選擇不同的異步處理方式。
子線程和主線程的消息傳遞使用Android中的異步消息處理機制:Handler。Handler是谷歌封裝好的一個消息處理接口,它可以綁定在主線程或者子線程中,當異步任務處理完成后可以利用Handler發送一個消息出去,主線程接收到這個消息之后就說明異步任務已經執行完成了進而可以刷新界面,這就實現了異步消息的傳遞[7]。
本文通過分析用戶事務期間所對應的關鍵路徑來判斷性能瓶頸。為方便敘述,下面先給出這些術語的定義。
定義1(用戶事務)用戶事務指用戶對UI的操作開始,并完成操作觸發的所有異步任務結束。例如在圖2中,用戶事務從onClick事件開始,并在UI update事件結束。
定義2(關鍵路徑)關鍵路徑是指用戶事務中的瓶頸路徑,從而更改關鍵路徑的任何部分的長度將改變用戶感知的持久性。關鍵路徑以用戶操縱事件開始,并以UI更新事件結束。在圖2中,從①到⑦的整個路徑構成事務的關鍵路徑。

Fig.1 Android system structure圖1 Android系統結構

Fig.2 Critical path圖2 關鍵路徑
本文通過對這條關鍵路徑上的節點所對應Framework層中的源碼進行插樁來確定每個節點運行的時間點和整個用戶事務的運行時間,并通過Logcat輸出插樁信息。
鑒于Android自帶的Logcat功能非常強大,利用它可以得到插樁Log,并通過進程號和一些篩選條件可以過濾掉大部分的無關Log,然后通過打印Log所在的線程ID,可以一目了然地得到用戶事務的關鍵路徑。
本文對應用程序的迭代過程中的所有用戶事務構建關鍵路徑,并對關鍵路徑的運行時間進行對比分析。如果某個版本的一個用戶事務的運行時間明顯過長,可以確定此處是有問題,即是性能瓶頸,然后把這個問題反饋給開發人員,讓開發人員對代碼進行優化。
由于關鍵路徑以用戶操縱事件開始,并以UI更新事件結束,下面介紹事件的插樁。
Android中點擊事件分為4類:(1)匿名內部類;(2)自定義事件監聽類;(3)Activity繼承View.On-ClickListener,由 Activity 實現OnClick(View view)方法;(4)在XML文件中顯示指定按鈕的onClick屬性,這樣點擊按鈕時會利用反射的方式調用對應Activity中的 Click()方法[8]。
無論哪種寫法,經過分析源碼,其最終都會調用Framework中View.java文件中的performClick函數,因此在這個位置插樁就可以得到點擊事件觸發和結束的臨界點,如圖2中的①和③所示。

Fig.3 Handler dispatch message process圖3 Handler處理消息流程
Android中異步消息處理使用最廣泛的就是Handler機制,Handler處理消息的流程如圖3所示,包括以下4個要素。(1)Message:消息,理解為線程間通訊的數據單元;(2)Message Queue:消息隊列,用來存放通過Handler發布的消息,按照先進先出執行;(3)Handler:Handler是Message的主要處理者,負責將Message添加到消息隊列以及對消息隊列中的Message進行處理;(4)Looper:循環器,扮演Message Queue和Handler之間橋梁的角色,循環取出Message Queue里面的Message,并交付給相應的Handler進行處理[9]。
經過對Handler源碼的分析,確定對于異步消息處理的插樁需要分為兩部分:子線程的sendMessage和主線程的handlerMessage,找到這個關系就可以確定主線程和子線程的因果關系。
4.2.1 sendMessage
在應用層子線程發送消息主要有send和post兩種方式[10],其中send包括sendEmptyMessage(int what)、sendEmptyMessageAtTime(int what,long uptimeMillis)、sendEmptyMessageDelayed(int what,long delayMillis)、sendMessage(Message msg)4 種;而 post包括 post(Runnable)、postAtTime(Runnable long)和 postDelayed(Runable long)3種。跟蹤這幾種方法的調用棧發現最終調用的會是sendMessageAtTime和send-MessageAtFrontOfQueue其中一個函數,而這兩個函數最終返回的都是enqueueMessage,因此選擇在enqueueMessage的關鍵位置進行插樁,得到如圖2中的⑥。
4.2.2 handlerMessage
應用層處理消息是在綁定Handler的線程中進行的,利用handler的handlerMessage處理MessageQueue中的消息,handlermessage最終都會調用Framework層的dispatchMessage[11],因此選擇在dispatchMessage的關鍵位置進行插樁,得到如圖2所示的⑦。
Android中開啟子線程主要有Thread、Handler-Thread、IntentService、AsyncTask和ThreadPool 5種方式,針對不同的應用場景開發人員可以選擇不同的開啟方式。其中AsyncTask和ThreadPool都是開啟一個線程池來處理異步任務,而這種方式開啟子線程其實就是封裝了一個Thread,因此這3種方式可以歸為一種來討論。以上5種方式可以分為3類:Thread、HandlerThread和IntentService。
4.3.1 Thread
Android用Thread開啟子線程的方式和Java中的相似,有繼承Thread和實現Runnable接口兩種方式,Runnable接口里只有一個無參無返回值的run()方法,而Thread類也是實現了Runnable接口并重寫了run()方法,因此主要分析Thread類來找關鍵位置插樁。
開發人員在應用層start開啟子線程是調用的libcore庫中Thread類的start方法,接著轉調VMThread的native方法create,然后轉到native層去創建子線程并設置屬性,創建成功后調用Thread類的run方法執行開發人員自定義的異步任務[12]。在這個創建的過程中,需要關注的有兩個地方:線程start和線程run。線程start是主線程調用的,這就是子線程開啟的始點,如圖2所示的②。線程run的過程是子線程處理異步任務的過程,找到子線程run的起始點,就得到了如圖2所示的④和⑤。
4.3.2 HandlerThead
HandlerThread本質上是一個Thread對象,只不過其內部創建了該線程的Looper和MessageQueue[13]。開發人員使用HandlerThread很簡單,只需要新建一個對象,讓它start,并在handlerMessage中處理異步任務。
分析源碼知道這里調用的start仍然會調用是Thread的start方法來創建新的線程,但是創建完成之后不會調用Thread的run方法,而直接調用Handler-Thread的run方法,因此需要在HandlerThread的run方法里插樁來分辨開啟的是一個HandlerThread。它處理異步任務是在handlerMessage中進行的,這就可以利用之前消息處理的插樁結合線程ID得到它處理異步任務的臨界點。
4.3.3 IntentService
IntentService 是Looper、Handler、Service的集合體[14],IntentService是繼承于Service并處理異步請求的一個類,在IntentService內有一個工作線程來處理耗時操作,啟動IntentService的方式和啟動傳統Service一樣。IntentService可以自動開啟一個Handle-Thread,并自動調用IntentService中的onHandleIntent方法來處理異步任務。
既然IntentService是開啟了一個HandlerThread,那之前插的樁都可以直接使用,只是IntentService處理異步任務和之前的方式都不同,它是自定義了一個onHandleIntent方法,因此捕捉它處理異步任務的臨界點就需要在這個方法里插樁。
Android中的任何一個布局、任何一個控件其實都是直接或間接繼承自View實現的,當然自定義控件也不例外,因此這些View應該都具有相同的繪制流程與機制才能顯示到屏幕上。經過總結發現每一個View的繪制過程都必須經歷3個最主要的過程,也就是measure、layout和draw[15]。而整個View樹的繪圖流程是在ViewRootImpl類的performTraversals方法開始的,監測界面刷新的流程需要在perform-Traversals處插樁。
為了測試此方法的可行性和有效性,本文設計了相關的測試用例,并在實際Android項目中使用。
5.1.1 軟件環境
本文的所有實驗都在OPENTHOS系統上完成。OPENTHOS是將Android 5.1原生系統移植到PC端并實現多窗口、多任務等一系列功能的開源操作系統,是由清華大學、清華同方和一銘公司共同聯合開發的。
5.1.2 硬件環境
把OPENTHOS系統安裝在清華同方T45筆記本上進行實驗與分析。T45的配置如下:
CPU,Intel酷睿i56200U;內存,4 GB;磁盤,500 GB;顯卡,2 GB獨立顯卡。
在上文已經介紹過,在Android異步編程模型中有5種開啟子線程的方式,為了驗證本文插樁方案的有效性,需要對每一種進行測試,基于此編寫了一個測試用例。本文設計的這個測試用例的主要功能是根據一個圖片的網絡地址,開啟一個子線程下載這個圖片并通知主線程刷新界面,將圖片顯示到界面上,分別用 Thread、HandlerThread、IntentService、AsyncTask、ThreadPool實現這個功能,通過插樁得到異步任務處理的關鍵路徑。圖4所示就是一條關鍵路徑,其中前兩列是日期和時間,第3列是進程ID,第4列是線程ID、第5列和第6列是插樁的Log標簽,第7列是主要的Log信息。

Fig.4 Critical path of Thread圖4 Thread的關鍵路徑
如圖4是Thread的關鍵路徑,第1行和第4行是用戶點擊事件的臨界點,對應圖2的①和③;第3行是主線程開啟子線程時調用的Thread的start方法,對應的是圖2的②;第5行和第9行是子線程處理耗時任務的臨界點,對應圖2的④和⑤;第6行是子線程處理完異步消息后向主線程發送消息通知主線程刷新界面,對應圖2的⑥;第7、8、10、11行是主線程處理子線程發來的消息并進行界面刷新的過程,對應圖2的⑦。
圖5是HandlerThread的關鍵路徑,它與Thread的最大區別就是子線程在dispatchMessage中處理異步任務,所以第4行的Log會顯示這是一個Handler-Thread,它的異步任務是在第7行和第8行的地方處理的。
圖6是IntentService的關鍵路徑,上文提到它其實是封裝了一個HandlerThread,因此對Handler-Thread的插樁也存在,而且IntentService是在onHandleIntent中處理異步任務,通過對onHandleIntent的插樁得到圖6的第8條和第12條Log。

Fig.5 Critical path of HandlerThread圖5 HandlerThread的關鍵路徑

Fig.6 Critical path of IntentService圖6 IntentService的關鍵路徑
圖7和圖8分別是AsyncTask和ThreadPool的關鍵路徑,這兩種異步處理的方式和Thread是一致的,因此得到的關鍵路徑和Thread無很大的區別。

Fig.7 Critical path ofAsyncTask圖7 AsyncTask的關鍵路徑

Fig.8 Critical path of ThreadPool圖8 ThreadPool的關鍵路徑
得到以上關鍵路徑之后,為了對比當異步處理出現問題影響響應性能的實例,讓子線程都sleep 2 s,這樣得到的關鍵路徑如圖9所示,可以發現第5行開始子線程處理異步任務,第7條處理完成的時間比圖4延長了1.993 s,這就說明這次的異步處理任務是有性能問題的(其余的幾種異步方式結果都是類似的,就不再贅述)。

Fig.9 Thrad performance exception圖9 Thread性能異常
OPENTHOS在開發的過程中一直碰到Start-Menu卡頓的問題,尤其是在安裝的幾十個應用的時候,點擊StartMenu之后需要2 s左右才能顯示出來,為了分析這個問題利用本文的方法在OPENTHOS中進行插樁并分析原因。
本文對初期版本和近期的分別進行分析,在對初期版本進行插樁分析的時候發現StartMenu啟動的時候并沒有開啟子線程,也就是說啟動的過程中所有的任務都是在主線程完成的。帶著這個問題對StartMenu的源碼進行了分析,發現在啟動的過程中是需要查詢數據庫把所有已經安裝的應用顯示出來的,而查詢數據庫這樣耗時的任務在主線程執行,勢必會影響性能。
接下來,對近期的版本進行分析,插樁得到的Log信息如圖10所示,第3行是子線程處理耗時任務,即查詢數據庫的開始點,第5行是結束點,但是可以發現這本應該是一個時間段,結果卻顯示是一個時間點,而且子線程只發送了一條消息就結束了,這絕對是不應該的。帶著這樣的問題,分析源碼并梳理StartMenu的邏輯關系,發現啟動的時候開啟的子線程仍然沒有處理耗時任務,而只是進行了一個消息的傳遞,這和初期的版本區別并不大。因此和開發人員一起重新整理StartMenu的邏輯流程,對代碼進行調整,得到最新的版本。如圖11所示,這樣耗時的任務就在子線程中執行,性能也提升了不少。

Fig.10 StartMenu analysis 1圖10 StartMenu分析1

Fig.11 StartMenu analysis 2圖11 StartMenu分析2
(1)對Android的Framework層進行插樁,把插樁得到的Log信息保存下來進行離線分析,以發現異步處理任務的關鍵路徑,可以幫助移動應用程序的開發人員監控和診斷他們的應用程序的性能。
(2)對Framework層進行插樁是輕量級的,只需要一次插樁就可以獲得關鍵路徑,有更好的通用性,對于迭代發展的版本之間的性能對比尤其明顯。
(3)對Framework層進行插樁分析可以找到程序中性能瓶頸的位置,并向開發人員提供反饋。