閱讀838 返回首頁    go 阿裏雲 go 技術社區[雲棲]


Android性能與優化

本文比較全麵的描述了一般項目在Android開發中如何提高性能,如何著手調優項目,如果你目前正在優化你的Android項目,恭喜你,這正是你要的,也十分歡迎拍磚代。

I. 編碼習慣

盡量避免分配內存(創建對象)

  • 如果一個方法返回一個String,並且這個方法的返回值始終都是被用來append到一個StringBuffer上,就改為傳入StringBuffer直接append上去,避免創建一個短生命周期的臨時對象;
  • 如果使用的字符串是截取自某一個字符串,就直接從那個字符串上麵substring,不要拷貝一份,因為通過substring雖然創建了新的String對象,但是共享了裏麵的char數組中的char對象,減少了這塊對象的創建;
  • 盡量使用多個一維數組,其性能高於多維數組;int數組性能遠大於Integer數組性能;

盡可能static方法

如果你確定不需要訪問類成員,讓方法static,這樣調用時可以提升15%~20%的速度,因為不需要切換對象狀態。

盡可能使用常量而非變量*

如果某個參數是常量,別忘了使用static final,這樣可以讓Class首次初始化時,不需要調用<clinit>來創建static方法,而是在編譯時就直接將常量替換代碼中使用的位置。

從性能層麵出發,盡可能直接訪問變量而非方法*

Android開發中,類內盡量避免通過get/set訪問成員變量,雖然這在語言的開發中是一個好的習慣,但是Android虛擬機中,對方法的調用開銷遠大於對變量的直接訪問。在沒有JIT的情況下,直接的變量訪問比調用方法快3倍,在JIT下,直接的變量訪問更是比調用方法快7倍!

對被內部類調用的方法/變量改為包可見

當內部類需要訪問外部類的私有方法/變量時,考慮將這些外部類的私有方法/變量改用包可見的方式。首先在編寫代碼的時候,通過內部類訪問外部類的私有方法/變量是合法的,但是在編譯的時候為了滿足這個會將需要被內部類訪問的私有方法/變量封裝一層包可見的方法,實現讓內部類訪問這些私有的方法/變量,根據前麵我們有提到說方法的調用開銷大於變量的調用,因此這樣使得性能變差,所以我們在編碼的時候可以考慮直接將需要被內部類調用的外部類私有方法/變量,改為包可見。

少用float*

  • 盡量少使用float。在很多現代設備中,double的性能與float的性能幾乎沒有差別,但是從大小上麵doublefloat的兩倍的大小。
  • 盡量考慮使用整型而非浮點數,在較好的Android設備中,浮點數比整型慢一倍。

使用乘法代替除法

盡量不要使用除法操作,有很多處理器有乘法器,但是沒有除法器,也就是說在這些設備中需要將除法分解為其他的計算方式速度會比較慢。

使用內部實現,而非上層實現

盡量使用係統sdk中提供的方法,而非自己去實現。如String.indexOf()相關的API,Dalvik將會替換為內部方法;System.arraycopy()方法在Nexus One手機上,會比我們上層寫的類似方法的執行速度快9倍。

謹慎編寫Native

Android JVM相關知識,可參看: ART、Dalvik
Android JNI、NDK相關知識,可參看: NDK

寫native性能不一定更好,Native並不是用於使得性能更好,而是用於有些已經存在的庫是使用native語言實現的,我們需要引入Android,這時才使用。

  • 需要多出開銷在維持Java-native的通信,Java調用JNI的耗時較Java調用Java肯定更慢,雖然隨著JDK版本的升級,差距已經越來越小(JDK1.6版本是5倍Java調用Java方法的耗時
  • 在native中創建的資源由於在native heap上麵,因此需要主動的釋放,但也因此對應用而言沒有OOM的問題,並且不需要考慮GC時鎖線程帶來的掉幀,如Facebook的Fresco就是將圖片緩存到Native Heap中
  • 需要對不同的處理器架構進行支持,存在明顯的兼容性問題需要解決
  • 如果是Dalvik,將省去了由JIT編譯期轉為本地代碼的這個步驟

一些重要的參數之類,也可以考慮放在Native層,保證安全性。參考: Android應用程序通用自動脫殼方法研究

權衡麵向接口編程

在沒有JIT的設備中,麵向接口編程的模式(如Map map),相比直接訪問對象類(如HashMap map),會慢6%,但是在存在JIT的設備中,兩者的速度差不多。但是內存占用方麵麵向接口變成會消耗更多內存,因此如果你的麵向接口編程不是十分的必要的情況下可以考慮不用。

重複訪問的變量,賦值為本地變量

在沒有JIT的設備中,訪問本地化變量相對與成員變量會快20%,但是在存在JIT的設備中,兩者速度差不多。

遍曆優化

盡量使用Iterable而不是通過長度判斷來進行遍曆。

// 這種性能是最差的,JIT也無法對其優化。
public void zero() {
    int sum = 0;
    for (int i = 0; i < mArray.length; ++i) {
        sum += mArray[i].mSplat;
    }
}

// 相對zero()來說,這種寫法會更快些,在存在JIT的情況下速度幾乎和two()速度一樣快。
public void one() {
    int sum = 0;
    // 1) 通過本地化變量,減少查詢,在不存在JIT的手機下,優化較明顯。
    Foo[] localArray = mArray;
    // 2) 獲取隊列長度,減少每次遍曆訪問變量的長度,有效優化。
    int len = localArray.length;

    for (int i = 0; i < len; ++i) {
        sum += localArray[i].mSplat;
    }
}

// 在無JIT的設備中,是最快的遍曆方式,在存在JIT的設備中,與one()差不多快。
public void two() {
    int sum = 0;
    for (Foo a : mArray) {
        sum += a.mSplat;
    }
}

II. 數據庫相關

建多索引的原則: 哪個字段可以最快的**減少查詢**結果,就把該字段放在最前麵

無法使用索引的情況

  • 操作符BETWEENLIKEOR
  • 表達式
  • CASE WHEN

不推薦

  • 不要設計出索引是其他索引的前綴(沒有意義)
  • 更新時拒絕直接全量更新,要更新哪列就put哪列的數據
  • 如果最頻繁的是更新與插入,別建很多索引 (原本表就很小就也沒必要建)
  • 拒絕用大字符串創建索引
  • 避免建太多索引,查詢時可能就不會選擇最好的來執行

推薦

  • 多使用整型索引,效率遠高於字符串索引
  • 搜索時使用SQL參數("?", parameter)代替字符串拚接(底層有特殊優化與緩存)
  • 查詢需要多少就limit多少(如判斷是否含有啥,就limit 1就行了嘛)
  • 如果出現很寬的列(如blob類型),考慮放在單獨表中(在查詢或者更新其他列數據時防止不必要的大數據i/o影響性能)

III. 網絡調優

更多網絡優化,可移步微信Mars與其策略

當然無論是網速評估、心跳間隔、超時間隔,我認為這些在往常是基於特定環境下指定算法,然後結合自己的經驗值給出的結果(如微信中的網速評估、超時間隔等),都能夠借助AI整合原本的經驗數據,給出一個更優數據的可能性(如某環境下超時間隔為5s為最優值的可能性為80%),來替代人的經驗值。但是目前可預見的難點是在於如何去區分以及定義訓練的數據,如:網速評估,其實是根據不同的環境(2G、3G、LTE、4G、千兆4G、5G、Wifi、之類的),之前微信其實有自己的一個評估策略,但是如果要接入AI,是因為網速這個評估的結果一直不是一個準確值,之前隻是根據我們自己的經驗給一個粗略的算法;可能這塊要結合各類網絡因素,參考RTT(這塊的計算算法),輸入的因素越多,對應我們能夠確定的結果越少,應該訓練出來的模型能夠越有效,這樣可以結合AI給出的”經驗”,讓網速評估更準確些。這也是目前我在探究的,所以才有了前幾天寫的敲開TensorFlow的大門

策略層麵優化

1. 通過If-Modified-SinceLast-Modified

  • 第一次請求時,服務端在頭部通過Last-Modified帶下來最後一次修改的時間。
  • 之後的請求,在請求頭中通過If-Modified-Since帶上之前服務端返回的Last-Modified的值
  • 如果服務端判斷最後一次修改的時間距離目前數據沒有修改過,就直接返回304 NOT MODIFIED的狀態碼,此時客戶端直接呈現之前的數據,由於不需要帶下來重複的數據,減少用戶流量的同時也提高了響應速度。

2. 通過EtagIf-None-Match

  • 第一次請求時,服務端在頭部通過Etag帶下來請求數據的hash值
  • 之後的請求,在請求頭中通過If-None-Match帶上之前服務端返回的Etag的值
  • 如果服務端判斷文件沒有修改過,就直接返回304 NOT MODIFIED,此時客戶端直接呈現之前的數據,由於不需要帶下來重複的數據,減少用戶流量的同時也提高了響應速度。

如果你使用Okhttp3與Retrofit2,對於304 NOT MODIFIED的緩存便可以直接通過下麵的代碼直接創建一個2M緩存文件來戶緩存這類數據。一旦是304 NOT MODIFIED, Retrofit2與Okhttp3將會偽裝一個與之前一樣的響應給上層,因此對上層是透明的。

private final static int CACHE_SIZE_BYTES = 1024 * 1024 * 2;
public static Retrofit getAdapter(Context context, String baseUrl) {
    OkHttpClient.Builder builder = new OkHttpClient().newBuilder();
    builder.cache(
        new Cache(context.getCacheDir(), CACHE_SIZE_BYTES));
    Retrofit.Builder retrofitBuilder = new Retrofit.Builder();
    retrofitBuilder.baseUrl(baseUrl).client(client);
    return retrofitBuilder.build();
}

數據結構層麵

  • 如果是需要全量數據的,考慮使用Protobuffers (序列化反序列化性能高於json),並且考慮使用nano protocol buffer
  • 如果傳輸回來的數據不需要全量讀取,考慮使用Flatbuffers (序列化反序列化幾乎不耗時,耗時是在讀取對象時(就這一部分如果需要優化,可以參看Flatbuffer Use Optimize

其他層麵優化

  • 通過自實現DNS(如實現自己的HTTPDNS(用Okhttp3實現尤為簡單,因為Okhttp3支持定製DNS)),來降低沒必要的DNS更新(由於DNS常見策略是與文件大小以及TTL相關,如果我們分文件以及分域名協商TTL有效期,可能case by case有效這塊的刷新率),甚至防止DNS劫持
  • 圖片、JS、CSS等靜態資源,采用CDN(當然如果是使用7牛之類的服務就已經給你搭建布置好了)
  • 全局圖片處理采用漏鬥模型全局管控,所請求的圖片大小最好依照業務大小提供/最大不超過屏幕分辨率需要,如果請求原圖,也不要超過GL10.GL_MAX_TEXTURE_SIZE
  • 全局縮略圖直接采用webp,在盡可能不損失圖片質量的前提下,圖片大小與png比縮小30% ~ 70%
  • 如果列表裏的縮略圖服務器處理好的小圖,考慮到減少下載時的RTT,可以考慮直接在列表數據請求中,直接以base64在列表數據中直接帶上圖片(但是需要注意的是通常base64後的圖片大小會大於原圖片大小,適當權衡)(國內還比較少,海外有些這種做法,好像web端比較常見)
  • 輪詢或者socket心跳采用係統AlarmManager提供的鬧鍾服務來做,保證在係統休眠的時候cpu可以得到休眠,在需要喚醒時可以喚醒(持有cpu喚醒鎖),這塊考慮到省點等問題可以參考這篇文章
  • 在一些異步的任務時,可以考慮合並請求

IV. 多進程抉擇

360 17個進程: 360手機衛士 Android開發 InfoQ視頻 總結
,但是考慮到多進程的消耗,我們更需要關注多個組件複用同一進程。
在沒有做任何操作的空進程而言,其大約需要額外暫用1.4MB的內存。

  • 充分獨立,解耦部分
  • 大內存(如臨時展示大量圖片的Activity)、無法解決的crash、內存泄漏等問題,考慮通過獨立進程解決
  • 獨立於UI進程,需要在後台長期存活的服務(參看Android中線程、進程與組件的關係)
  • 非己方第三方庫(無法保證穩定、性能等問題,並且獨立組件),可考慮獨立進程

最後,多進程存在的兩個問題: 1. 由於進程間通訊或者首次調起進程的消耗等,帶來的cpu、i/o等的資源競爭。2. 也許對於部分同事來說,會還有可讀性問題吧,畢竟多了層IPC繞了點。

V. UI層麵

相關深入優化,可參看Android繪製布局相關
對於卡頓相關排查推薦參看: Android性能優化案例研究(上)Android性能優化案例研究(下)

  • 可以考慮使用ConstraintLayout,有效減少了布局的層級,提高了性能
  • 減少不必要的不透明背景相互覆蓋,減少重繪,因為GPU不得不一遍又一遍的畫這些圖層
  • 保證UI線程一次完整的繪製(measure、layout、draw)不超過16ms(60Hz),否則就會出現掉幀,卡頓的現象
  • 在UI線程中頻繁的調度中,盡量少的對象創建,減少gc等。
  • 分步加載(減少任務顆粒)、預加載、異步加載(區別出耗時任務,采用異步加載)

VI. 內存

根據設備可用內存的不同,每個設備給應用限定的Heap大小是有限的,當達到對應限定值還申請空間時,就會收到OutOfMemoryError的異常。

1. 內存管理

Android根據不同的進程優先級,對不同進程進行回收來滿足內存的供求,可以參照這篇文章: Android中線程、進程與組件的關係
在後台進程的LRU隊列中,除了LRU為主要的規則以外,係統也會根據殺死一個後台進程所獲得的內存是否更多作為一定的參考依據,因此後台進程為了保活,盡量少的內存,盡可能的釋放內存也是十分必要的。

  • 盡可能的縮短Service的存活周期(可以考慮直接使用執行完任務直接關閉自己的IntentService),也就是說在Service沒有任何任務的時候,盡可能的將其關閉,以減少係統資源的浪費。
  • 可以通過係統服務ActivityManager中的getMemoryClass()獲知當前設備允許每個應用大概可以有多少兆的內存使用(如果在AndroidManifest設置了largeHeap=true,使用getLargeMemoryClass()獲知),並且讓應用中的內存始終低於這個值,避免OOM。
  • 相對於靜態常量而言,通常Enum枚舉需要大於兩倍的內存空間來存儲相同的數據。
  • Java中的每個class(或者匿名類)大約占用500字節。
  • 每個對象實例大約開銷12~16字節的內存。

onTrimMemory()回調處理

監聽onTrimMemory()的回調,根據不同的內存等級,做相應的釋放以此讓係統資源更好的利用,以及自己的進程可以更好的保活。

當應用還在前台

  • TRIM_MEMORY_RUNNING_MODERATE: 當前應用還在運行不會被殺,但是設備可運行的內存較低,係統正在從後台進程的LRU列表中殺死進程其他進程。
  • TRIM_MEMORY_RUNNING_LOW: 當前應用還在運行不會被殺,但是設備可運行內存很低了,會直接影響當前應用的性能,當前應用也需要考慮釋放一些無用資源。
  • TRIM_MEMORY_RUNNING_CRITICAL: 當前應用還在運行中,但是係統已經殺死了後台進程LRU隊列中絕大多數的進程了,當前應用需要考慮釋放所有不重要的資源,否則很可能係統就會開始清理服務進程,可見進程等。也就說,如果內存依然不足以支撐,當前應用的服務也很有可能會被清理掉。

TRIM_MEMORY_UI_HIDDEN

當回調回來的時候,說明應用的UI對用戶不可見的,此時釋放UI使用的一些資源。這個不同於onStop()onStop()的回調,有可能僅僅是當前應用中進入了另外一個Activity

當應用處於後台

  • TRIM_MEMORY_BACKGROUND: 係統已經處於低可用內存的情況,並且當前進程處於後台進程LRU隊列隊頭附近,因此還是比較安全的,但是係統可能已經開始從LRU隊列中清理進程了,此時當前應用需要釋放部分資源,以保證盡量的保活。
  • TRIM_MEMORY_MODERATE: 係統處於低可用內存的情況,並且當前進程處於後台進程LRU隊列中間的位置,如果內存進一步緊缺,當前進程就有可能被清理掉,需要進一步釋放資源。
  • TRIM_MEMORY_COMPLETE: 係統處於低可用內存的情況,並且當前進程處於後天進程LRU隊列隊首的位置,如果內存進一步緊缺,下一個清理的就是當前進程,需要釋放盡可能的資源來保活當前進程。在API14之前,onLowMemory()就相當於這個級別的回調。

2. 避免內存泄漏相關

  • 無法解決的泄漏(如係統底層引起的)移至獨立進程(如2.x機器存在webview的內存泄漏)
  • 大圖片資源/全屏圖片資源,要不放在assets下,要不放在nodpi下,要不都帶,否則縮放會帶來額外耗時與內存問題
  • 4.x在AndroidManifest中配置largeHeap=true,一般dvm heep最大值可增大50%以上。但是沒有特殊明確的需要,盡可能的避免這樣設置,因為這樣一來很可能隱藏了消耗了完全沒有必要的內存的問題。
  • Activity#onDestory以後,遍曆所有View,幹掉所有View可能的引用(通常泄漏一個Activity,連帶泄漏其上的View,然後就泄漏了大於全屏圖片的內存)。
  • 萬金油: 靜態化內部類,使用WeakReference引用外部類,防止內部類長期存在,泄漏了外部類的問題。

3. 圖片

Android 2.3.x或更低版本的設備,是將所有的Bitmap對象存儲在native heap,因此我們很難通過工具去檢測其內存大小,在Android 3.0或更高版本的設備,已經調整為存儲到了每個應用自身的Dalvik heap中了。

  • 全局統一BitmapFactory#decode出口,捕獲此處decode oom,控製長寬(小於屏幕分辨率大小 )
  • 如果采用RGB_8888 oom了,嚐試RGB_565(相比內存小一半以上(w*h*2(bytes)))
  • 如果還考慮2.x機器的話,設置BitmapFactory#optionsInNativeAlloc參數為true,此時decode的內存不會上報到dvm中,便不會oom。
  • 建議采用lingochamp/QiniuImageLoader的方式,所有圖片的操作都放到雲端處理,本地默認使用Webp,並且獲取的每個位置的圖片,盡量通過精確的大小按需獲取,避免內存沒必要的消耗。

VII. 線程

VIII. 編譯與發布

關於開發流程優化,可以參考這裏

  • 考慮采用DexGuard,或ProGuard結合相關資源混淆來提高安全與包大小,參考: DexGuard、Proguard、Multi-dex
  • 結合Gradle、Gitlab-CI 與Slack(Incoming WebHooks),快速實現,打相關git上打相關Tag,自動編相關包通知Slack。
  • 結合Gitlab-CI與Slack(Incoming WebHooks),快速實現,所有的push,Slack快速獲知。
  • 結合Gradle中Android提供的productFlavors參數,定義不同的variations,快速批量打渠道包
  • 迭代過程中,包定期做多緯度掃描,如包大小、字節碼大小變化、紅線掃描、資源變化掃描、相同測試用例耗電量內存等等,更多的可以參考 360手機衛士 Android開發 InfoQ視頻 總結
  • 迭代過程中,對關鍵Activity以及Application對打開的耗時進行統計,觀察其變化,避免因為迭代導致某些頁麵非預期的打開變慢。

IX. 工具

這塊的拓展閱讀,可以直接參考Android開發周邊

  • TraceView可以有效的更重一段時間內哪個方法最耗時,但是需要注意的是目前TraceView在錄製過中,會關閉JIT,因此也許有些JIT的優化在TraceView過程被忽略了。
  • Systrace可以有效的分析掉幀的原因。
  • HierarchyViewer可以有效的分析View層級以及布局每個節點measurelayoutdraw的耗時。

X. 其他

  • 懶預加載,如簡單的ListViewRecyclerView等滑動列表控件,停留在當前頁麵的時候,可以考慮直接預加載下個頁麵所需圖片
  • 智能預加載,通過權重等方式結合業務層麵,分析出哪些更有可能被用戶瀏覽使用,然後再在某個可能的時刻進行預加載。如,進入朋友圈之前通過用戶行為,智能預加載部分原圖。
  • 做好有損體驗的準備,在一些無法避免的問題麵前做好有損體驗(如,非UI進程crash,可以自己解決就不要讓用戶感知,或者UI進程crash了,做好場景恢複)
  • 做好各項有效監控:crash(注意還有JNI的)、anr(定期掃描文件)、掉幀(繪製監控、activity生命周期監控等)、異常狀態監控(本地Log根據需要不同級別打Log並選擇性上報監控)等
  • 文件存儲推薦放在/sdcard/Android/data/[package name]/裏(在應用卸載時,會隨即刪除)(Context#getExternalFilesDir()),而非/sdcard/根目錄建文件夾(節操問題)
  • 通過gradle的shrinkResourcesminifyEnabled參數可以簡單快速的在編包的時候自動刪除無用資源
  • 由於resources.arsc在api8以後,aapt中默認采用UTF-8編碼,導致資源中大都是中文的resources.arsc相比采用UTF-16編碼更大,此時,可以考慮aapt中指定使用UTF-16
  • 穀歌建議,大於10M的大型應用考慮安裝到SD卡上: App Install Location
  • 當然運維也是一方麵: Optimize Your App
  • 在已知並且不需要棧數據的情況下,就沒有必要需要使用異常,或創建Throwable生成棧快照是一項耗時的工作。
  • 需要十分明確發布環境以及測試環境,明確僅僅為了方便測試的代碼以及工具在發布環境不會被帶上。
  • 國內環境的長連接抉擇: 根據各廠商設備在日活的排行,優先適配,而後再結合後台的工作量,評估是否自己做,客戶端做主要就考慮電量以及可靠性權衡。如果要接第三方的,一定要了解清楚,國內現在第三方的,依然不太有節操(甚至有些會把你加入某套餐,就是會各種喚起其他應用),如果要自己實現可以看看本文有提到的這篇文章
  • 控製合理加載資源的時間區間: 如由於圖片的加載通常都與頁麵的生命周期有關係,在Android中可以考慮當從頁麵A進入頁麵B時,暫停所有頁麵A的圖片加載,退出頁麵B時,終止所有頁麵B相關的圖片加載,回到頁麵A時恢複頁麵A的所有圖片加載(這些操作使用Picasso十分快速的實現,因此Picasso支持不同TAG的圖片加載暫停、恢複、取消)
  • 代碼質量: phabricator 的arc diff (盡量小顆粒度的arc diff 與update review),其實也可以看看Google是如何做的: 筆記-穀歌是如何做代碼審查的,還有一點的TODO要寫好deadline與master

- 編包管理: Gitlab CI (結合Gitlab,功能夠用,方便)

最後更新:2017-04-10 15:00:06

  上一篇:go Java新手如何學習三大框架
  下一篇:go 袋鼠雲助力光伏產業 | 基於阿裏雲數加平台做算法預測