張博涵
(武漢郵電科學研究院,湖北武漢430074)
作為開發者,在日常開發工作中,性能優化不是最難的問題,卻是最讓人頭疼的問題,因為導致性能變差的原因很多,界面布局、功能邏輯、數據處理甚至是不同版本型號的安卓機都會導致不同的問題。性能優化,是為了讓程序能夠運行得更快、更省、更穩,先前的開發者們就性能問題雖提出了一些中肯的建議,但并不通用。文中將總結先前開發者們的經驗,結合在日常開發中的應用,從應用啟動速度、頁面加載速度以及操作響應速度入手,提出能提升應用程序流暢性的普適優化建議和一些能使應用程序跑得更快的通用方案。
啟動速度慢的原因,主要是源于在初次打開應用時,需要加載大量的功能邏輯和頁面顯示的資源。解決該問題可以采取以下幾點措施。
一個Activity 的生命周期如圖1所示。由圖可知,onCreate()方法是打開一個Activity 時啟動的第一個方法,其功能為完成啟動一個Activity 的必要的初始化工作,如設置頁面的布局資源,初始化組件信息等。如果在onCreate()中的代碼過多,該環節耗時過長,則會導致啟動速度變慢。

圖1 Activity生命周期
舉例說明,假如一個頁面有20 個控件,開發者將這些控件的所有屬性信息,全部放在MainActivity中的onCreate()方法內去初始化。接下來進入到命令提示符cmd 內,通過adb 指令的am start-W-n 包名/.LauncherActivity 指令來啟動應用程序,其中的TotalTime 即為該次啟動App 所花的時間,大約為600 ms。
接下來可以改變代碼,寫一個私有方法initView(),將初始化各個控件信息的語句全部放在這個方法里,而在onCreate()中只執行initView()方法,解決了其中代碼太過冗雜的問題。重復上一段的指令,得到的應用程序啟動時間大約為550 ms。
由以上對比可知,onCreate()內代碼的數量直接決定應用程序的啟動時長,因為本例中的應用程序非常簡單,時長差看起來并不大,但是在代碼量龐大的應用程序中體現得尤其明顯,直接影響到用戶的使用體驗。
Adapter 可以理解成是連接前端顯示和后端數據庫的適配器接口,是數據(Data)和UI(View)之間的重要橋梁,在經常使用的ListView、GridView 等處都要用到創建對應的Adapter 來配合使用[1-2]。Adapter、Data、View 三者的關系可以用圖2來表示。

圖2 Adapter、View和Data之間的關系
而Adapter 本身包含很多的接口,其具體的結構如圖3所示。

圖3 Adapter的結構圖
在日常開發中,開發者們常繪繼承BaseAdapter類,因為其包含很多需要實現和重寫的方法,故具有較高的靈活性。其通常被用于ListView、GridView 等列表或表單型的布局當中。通過重寫getView()方法來執行和視圖相關的一些操作,每當代碼運行到這里的時候,都會調用getView()方法來重繪某個List,尋找該List 的item 布局文件,將其中的控件一個個再重新初始化,但很多情況下,使用者并不需要如此頻繁地重繪List,甚至在此次App 的使用中,該List只需要加載一次就夠了。在這種情況下,頻繁地重繪List 毫無疑問會影響頁面的啟動速度,而且會造成大量的資源浪費,在布局復雜的時候會尤其明顯。為了解決該問題,開發者可以通過convertView+ViewHolder來進行優化。首先要寫出一個ViewHolder靜態類,接下來去判斷getView()方法內的第二個參數convertView 是否為空,如果為空,則表示該List 還沒有被加載過,這樣便可以通過View.inflate()方法來尋找對應控件的ID 并賦值給convertView,接下來獲取一個新的ViewHolder 對象,將convertView 中的各個控件綁定到Viewholder 上,再通過convertView 里的setTag()方法將ViewHolder 儲存。當convertView判斷不為空的時候,便表示之前已經加載過該List了,不用再重新繪制,此時便可以通過convertView 的getTag()方法直接調用儲存的ViewHolder,省掉了繪制的時間,大大縮短了頁面啟動的速度[3-5]。
減少主線程阻塞的時間是為了防止ANR(Application Not Responding),當用戶失去響應超過5 s以上便會造成ANR。造成ANR 的原因有很多,但是總的來說,都是在主線程里進行了太多耗時操作,比如加載圖片、申請網絡下載、操作數據庫等。從界面優化的角度來說,一方面,開發者要保證進行UI 操作的線程只處理一些跟UI 有關的操作,另一方面,如果實在無法避免一些耗時操作,則需要單獨開啟一個新線程來處理這些操作,并且通過Handler 來處理UI 線程和其他線程之間的交互。
以上方法都是在代碼方面作出的改變,除了這些,開發者們也可以使用一些界面優化的工具,比如Android SDK 提供的一個名叫Layoutopt 優化工具,它可以告訴開發者哪些布局或者控件是多余的,可以刪除,也可以告訴開發者當前界面布局太多或者嵌套太多,建議刪除或重新設計[7-9]。
渲染的意思是將各種布局控件繪制到屏幕上,渲染速度便代表了一個完整頁面呈現在用戶眼前的速度。如果想給用戶以流暢的使用體驗,便需要加快渲染速度。
安卓的渲染機制如圖4所示,從Google 官方推出的性能優化典范可以得知,60 fps 是當前最佳的圖像顯示速度,所以目前安卓機的刷新頻率都被設置為60 fps,為了達到這個要求,開發者們需要在16 ms內完成一次頁面刷新的操作。由圖4可知,系統每隔16 ms 便會發出一個垂直同步信號VSYNC 來觸發事件,并且在16 ms 之內完成界面的更新與渲染。如此一來體現在用戶眼中的便是一次流的展示。

圖4 流暢狀態下的安卓渲染機制
不管因為任何原因導致這一過程沒有在16 ms內完成,便會出現掉幀現象,刷新的幀率也會下降。如此一來體現在用戶眼中的便是明顯的卡頓。掉幀狀態下的安卓渲染機制如圖5所示。

圖5 掉幀狀態下的安卓渲染機制
渲染的過程是由GPU 和CPU 協作完成,兩者有各自的分工、不同的潛在問題以及不同的解決方案,具體分工如圖6所示。總的來說,便是通過HierarchyViewer 來檢測渲染的效率,進而找出不必要的布局嵌套以及控件;通過智能手機上的Show GPU OverDraw 來檢測多余的背景[10]。

圖6 渲染過程分工圖
過度繪制是屏幕上的某一個像素在同一幀時間里被繪制的次數過多,一般是布局套嵌過多或是背景顏色重疊導致的。開發者可以通過手機上的設置-開發者選項-調試GPU 過度繪制-顯示GPU 過度繪制來查看當前頁面是否有過度繪制發生。屏幕上深紅色覆蓋區域表示過度繪制4 次;淺紅色覆蓋的區域表示被過度繪制了3 次;被藍色覆蓋區域表示被過度繪制了兩次;綠色覆蓋區域部分被過度繪制一次。如果一個界面被大量的深紅色和淺紅色覆蓋,則表示該頁面必須進行優化。
布局優化流程如下:先把容器之間的多層嵌套取消,改為單層結構或兩層嵌套,如此一來雖然在表達能力上稍有欠缺,但在性能優化上卻頗有成效。然后只給最外層容器設置白色背景,內層容器和控件的背景設置全部取消,如此一來便可以避免多次繪制背景色。最后刪除一些沒有輸出內容的控件,比如充當分割區域的空文本框等,起到簡化布局代碼的作用。優化過的頁面應當是藍色區域占大多數,紅色區域較少,這并不是完美的布局,但是在實際開發中是完全可以接受的。
通過上述步驟可以發現,在實際開發中,開發者首先要考慮布局容器的選擇和嵌套,安卓系統提供的Layout容器包括線性布局、相對布局、幀布局等,各有用處也各有優劣。通常來說,描繪能力差的容器更加簡潔,但或許需要多層嵌套。而描繪能力強的容器可以實現絕大多數頁面,或許也無需多層套嵌,但計算量也非常大。布局設置不一樣,即便功能一致,界面大體相同,但在細節上是有差異的。開發者需要判斷什么樣的差異可以忽略的,什么樣的差異不可接受。所以在實際開發中,要做到既能保證性能又能達到要求還能避免過度繪制是非常重要的。
其次開發者可以考慮去掉多余的背景,主要有兩種情況:1)在使用某些安卓系統提供的主題時窗口會自帶背景,如果再在Layout 容器中設置背景便會造成重疊浪費,此時開發者可以選擇在Activity 的onCreat() 方法中,通過將語句 getWindow().setBackgroundDrawable()的賦值設為null 來取消背景。2)在設計布局界面時,被覆蓋的部分的背景也要考慮去掉,這種情況非常多見,比如容器覆蓋容器,列表覆蓋容器,表格覆蓋容器等,被覆蓋的容器都應設置背景為空。
最后,開發者要著重注意App 的設計思路,要盡量避免過度設計。App 需要有個漂亮的外表,但是有些App 過于注重外表華麗,反而忽略了一個App簡潔直觀實用才是最重要的,用戶并不會喜歡過于華麗復雜的東西,App 也會因此變得不流暢。
除此之外,開發者們還可以通過ViewStub 來設置屬性并賦予指向的布局,只需要通過操作ViewStub來決定是否顯示指定的布局;也可以使用draw9patch制作圖片,給ImageView 制作背景來充當邊框,將重疊部分設置為透明,來滿足減少過度繪制的要求等。總之,處理不同的情況,需要不同的思路,使用不同的工具,才能保持性能和流暢度之間的平衡。針對過度繪制的優化方案沒有誰對誰錯,只有合適與否。
在實際開發中,安卓系統提供的View 通常是無法滿足要求的,此時便需要自定義View。自定義View 的繪制大致分以下步驟:onMeasure()方法用于測量布局寬高尺寸以及位置;onLayout()方法用于排列控件;onDraw()方法用于繪制。自定義View 繪制流程如圖7所示。

圖7 自定義View繪制流程圖
由于onDraw()方法的作用是將已經測好寬高尺寸的View 畫出來,所以經常面臨重復多次調用的情況。開發者很難去限制其調用次數,所以更多時候會選擇盡可能地簡化onDraw()方法,正如前文中簡化onCreate()方法一樣。一方面,開發者要避免在onDraw()方法里創造過多的局部變量,這些局部變量會伴隨方法的調用而被創建,在一瞬間會占用較多的資源,而如此反復下來必然會降低效率。另一方面,開發者要把耗時的操作移出onDraw(),比如在繪制界面時需要從網絡上請求一張圖片顯示出來,這種情況下需要另開新線程來作處理,然后通過Handler通知onDraw(),圖片請求好了,可以加載即可。
響應速度變慢,出現的最常見的情況就是ANR。當出現ANR 時,程序會失去響應,屏幕會直接在某個界面卡死。如前文提到的一樣,造成ANR的原因還是由于把一些耗時的操作放在了主線程內進行,如果廣播或者服務在一段時間內沒有響應就必會觸發ANR。解決方法無外乎以下幾種,異步實現、新開線程、消息機制等[11-12]。
除了上述內容,在實際開發中遇到的問題數不勝數,只能結合實際情況去考慮。比如在處理即時更新的界面時,如何既能保證刷新頻率,又能限制資源消耗?使用動畫時如何合理選擇框架,是否需要使用硬件加速?某些使用不頻繁的界面是退出再加載,還是隱藏再顯示?諸如此類的問題都將是開發過程中的難題[13-14]。
隨著技術的發展和手機的普及,應用程序的使用必然越來越廣泛,開發者們也需要去滿足用戶更多的要求。文中提供的思路都是在代碼端就可以實現的,不用考慮手機型號和安卓版本,也具有較為廣泛的適用性。而測試結果表明,合理的代碼設計不僅能夠縮短各種操作耗費的時間,也節省了系統資源,使用戶的體驗更出色,在代碼量大的程序上顯現更加明顯,而且通過精簡、合理規劃代碼,降低代碼的耦合度,也會增加代碼的可讀性,可以使開發者們更輕松地維護和更新代碼。而除了流暢性,還有穩定性、節省性等諸多評價一個程序好壞的指標,文中并未討論。在實際開發中,如何在兼顧這些指標的同時,做到功能不打折扣,外觀盡量美觀,操作簡單清晰是開發者們無法回避的難題。只有不斷鉆研,廣納建議,收集用戶反饋,才能做出跟得上技術進步和審美發展的出色的應用程序[15-16]。