1. 程式人生 > >App效能優化淺談

App效能優化淺談

前言

前段時間給公司的小夥伴們進行了關於app效能優化的技術分享,這裡我稍微整理一下也給大家分享一下,關於效能優化這個話題很大,涉及面可以很廣,也可以很深入,本人能力有限,不會給大家講特別難懂,特別底層的東西,都是我們開發能著手去做的點,大家都在講效能優化,但對於專案經驗不夠豐富的朋友很難有一個概念,做優化的時候也會比較茫然,這裡我就給大家指明方向。

從何講起?

筆者在做產品開發的時候,也遇到效能瓶頸,測試工程師反饋了一些比較明顯的問題,比如UI介面的過度繪製,列表滑動有明顯示卡頓,比較耗記憶體等等,但以往的都沒有針對性的去做相應的優化,所以藉著保證產品質量的出發點,自己定了相關的效能優化方案,可能不太成熟,不過可以逐步完善,並找到最適合自己產品的優化方案。

這裡我定了四個方向:
- 響應時間(Response Time)
- 介面卡頓(ANR)
- 耗記憶體(Memory)
- 記憶體洩露(Out of memory)

響應時間

這裡指的是客戶端與服務端互動,拿到資料、解析、再到顯示到介面整個過程耗費的時間。

這個部分涉及客戶端的優化,也涉及服務端的優化,這裡只討論客戶端。

HTTP請求方式

我們的app一般離不開網路,請求介面是最平常的操作了,如何請求,請求什麼我們在開發初期就要定好,服務端給我的提供的介面,大致可以通過GET、POST、HEAD、PUT、DELETE這幾種請求方式,不同的請求方式有不同應用場景,比如GET請求,應當用來請求返回結果,引數是作為url的一部分;POST請求,用於請求會更改服務端資料或狀態;HEAD請求跟GET一樣,只是伺服器不能在響應裡返回訊息主體;PUT請求,用於將網頁放置正確的地方;DELETE請求用於刪除伺服器指定文件。

使用優秀的開源Http框架是我們比較好的選擇,它的優點是經過市場的驗證,很多坑都被填過,缺點也是我們需要去深究它才能對其進行擴充套件,遇到坑也不一定能填。

如果自己造輪子的話,還需要我們花時間去驗證去適應我們的業務需求,但好處是我們可以自己去擴充套件可把控,不過這很考量開發者的素質。

資料解析

實際開發當中服務端的返回資料格式無非就兩種:
- JSON
- XML

這兩種格式資料格式各有優劣,從可讀性來看,xml略微好一點,不過JSON也有規範的標籤,從解析難度和速度來看,大家都比較傾向使用JSON,目前JSON也是主流的資料格式。

在Android中均可以使用優秀的解析庫來加快我們的解析速度,XML中有dom4j,JSON有Jackson、Gson,我們通過這些庫實現我們更快的完成資料解析,提高我們的開發效率。

資料儲存

上一節講的是資料解析,我們解析完後的資料,可能就需要將資料儲存在某個地方,Android的五種儲存方式:
- Content Provider(主要用來向其他應用程式共享資料)
- SQLite(儲存資料到資料庫中)
- File(本地檔案儲存)
- SharedPreference(主要用來儲存簡單的配置資訊)
- 網路儲存(WebService返回的資料或是解析HTTP協議實現網路資料互動)

為了提高應用程式的響應時間,資料快取是一個比較好的方式,我們可以預處理伺服器返回的資料,對資料進行快取重新整理。

優化點:
- 非同步請求網路資料
- 預處理伺服器返回資料
- 非同步進行資料儲存操作
- 資料快取重新整理
- Timeout超時重試
- 在主執行緒中操作UI

介面卡頓

ANR表示”應用程式無響應”,這個是需要我們避免發生的事情,出現這個異常的原因:
- 主執行緒 (“事件處理執行緒” / “UI執行緒”) 在5秒內沒有響應輸入事件
- BroadcastReceiver在10秒內沒有執行完畢

導致ANR的原因有很多,一般情況就是在UI執行緒做了耗時的操作,例如”網路請求”、資料庫操作。

那麼如何避免?
- UI執行緒只做介面重新整理,不做任何耗時操作,耗時操作放在子執行緒來做
- 可以使用Thread+handle或者AsyncTask來進行邏輯處理

耗記憶體

每部手機的記憶體有限,我們這裡所說的記憶體指的是手機的RAM,它是Ramdom Access Memory的縮寫,我們應用程式的需要隨機讀寫的資料就存在RAM中,Android手機之所以會比較耗記憶體,這跟Android後臺的處理有關,我們知道Android應用是使用Java開發的,執行Java需要有虛擬機器,說明每開啟一個應用都會建立一個虛擬機器,而這是需要記憶體的,所以我們開的應用越多,後臺程序越多,記憶體都分配出去了,才導致記憶體消耗的嚴重。

其實這個問題我們是沒得破的,只要記憶體不夠,我們的應用還是會卡。我們開發的應用依賴與系統給我們分配的堆記憶體,一般上限在16M~48M,但我們可以通過在AndroidManifest設定Application屬性largeHeap=“true”來申請更多的堆記憶體。

通過以下程式碼獲取可用堆記憶體限制:

mActivityManager = (ActivityManager) this.getSystemService(Context.ACTIVITY_SERVICE);
 mMaxMemory = mActivityManager.getMemoryClass();

記憶體洩露

記憶體洩露這個問題已經被說爛了,大家都知道有記憶體洩露這個問題存在,但為什麼會發生記憶體洩露?

這裡的記憶體洩露並不是真正意思上的洩露,而是因為記憶體不足不能進行GC操作,從而導致佔用記憶體過大,丟擲out of memory異常,而被系統Kill掉。

JVM回收機制

是時候講講JVM的回收機制了,看下圖:

JVM分代

JVM對Java物件分了三個代進行管理,分別為年輕代、年老代、永久代。
年輕代(Young Generation):絕大多數的Java物件會在年輕代被分配,也會在年輕代被回收。
年老代(Old Generation):在年輕代長期存在沒有被回收的Java物件會轉移到年老代,這個堆空間通常會被比年輕代的堆空間要大。
永久代:存放VM和Java類的元資料,以及interned字串和類的靜態變數。

這裡涉及到JVM的相關知識,這裡不繼續深入探討。

但我們應該可以知道垃圾回收器的作用:
- 分配記憶體
- 保證所有正在被引用的物件還存在於記憶體中
- 回收執行程式碼已經不再引用的物件所佔的記憶體

物件引用

Java的引用型別可以分為以下幾種:
- 強引用(Strong Ref):強可達,去掉強可達,才會被回收。
- 軟引用(Soft Ref):記憶體夠用,就保持,記憶體吃緊,則回收,主要用來做快取。
- 弱引用(Weak Ref):比Soft Ref弱,即使記憶體不吃緊也會被回收。
- 虛引用(Phantom Ref):不會在記憶體保持任何物件。

一圖勝千言:

物件引用

利用Strong Ref,儲存大量資料,直到heap撐破,利用inter strings(或者class loader載入大量的類)把perm gen撐破,然後就是記憶體洩露了。

如何優化?

前面講了一些背景知識,對我們理解記憶體優化有一定的幫助,下面就簡單說一下我們優化的方向:
- 佈局優化
- 記憶體優化

佈局優化

大家可以拿出你們的Android機
開發者工具-Profile GPU Rendering-選擇在螢幕上顯示條形圖

-藍色代表測量繪製Display List的時間
-紅色代表OpenGL渲染Display List所需要的時間
-黃色代表CPU等待GPU處理的時間
-中間綠色橫線代表VSYNC時間16ms,儘量將所有條形圖控制在這條綠線下

為什麼是16ms?

Android 通知介面渲染和重繪的時間要在16ms內完成,如果超過16ms,就會導致丟幀,也就是我們常說的卡頓。

優化點:
- 避免OverDraw
- 優化佈局層級
- 避免過多無用巢狀
- 使用<include>標籤重用layout
- 使用<ViewStub>延遲載入
- Hierarchy View進行層級分析

具體的使用方法,這裡不介紹了,不懂就百度。

記憶體優化

記憶體優化的點有很多,這裡我主要分為兩大塊:
- Bitmap優化
- 程式碼優化

Bitmap優化

  1. 使用適當解析度和大小的圖片
  2. 及時回收記憶體(bitmap.recycle())
  3. 使用圖片快取(LruCache和DiskLruCache)

第一點,就是按需顯示,比如列表中的圖片,你可以顯示縮圖,詳情頁,你就可以載入相應的解析度的圖片,這樣可以減少記憶體消耗,一般可以要求服務端提供多種解析度的圖片。

第二點,Bitmap是很耗記憶體,尤其是載入比較大的bitmap,可以想到的優化方案就是使用記得回收,對Bitmap進行壓縮,使用BitmapFactory.Options設定inSampleSize就可以縮小圖片。

第三點,影象快取,這個可以利用成熟的圖片載入框架,比如Universal-ImageLoader、Fresco、Picasso,這些框架都對圖片進行了很好的優化,大家可以對比一下,選擇使用即可。

程式碼優化

關於程式碼這個就有的說了,任何能改進我們程式的優化點都能寫在這裡,這裡沒辦法把所有優化的點列在這裡,只提供相關的參考,剩下的就好各位經驗總結和積累了。

優化點:
- 對常量使用static修飾符
- 使用靜態方法
- 減少不必要的成員變數
- 儘量不要使用列舉,少用迭代器
- 對Cursor、Receiver、Sensor、File等物件,要注意它們的建立、回收與註冊、反註冊
- 避免大量使用註解、反射
- 使用RenderScript、OpenGL來進行復雜的繪圖操作
- 使用SurfaceView來替代View進行大量、頻繁的繪圖操作
- 儘量使用檢視快取,而不是每次都執行inflate()方法解析檢視

注:這裡引用了Android群英傳的相關優化點

  • 建立新的物件都需要額外的記憶體空間,要儘量減少建立新的物件。
  • 將類、變數、方法等等的可見性修改為最小。
  • 針對字串的拼接,使用StringBuffer替代String。
  • 不要在迴圈當中宣告臨時變數,不要在迴圈中捕獲異常。
  • 如果對於執行緒安全沒有要求,儘量使用執行緒不安全的集合物件。
  • 使用集合物件,如果事先知道其大小,則可以在構造方法中設定初始大小。
  • 檔案讀取操作需要使用快取類,及時關閉檔案。
  • 慎用異常,使用異常會導致效能降低。
  • 如果程式會頻繁建立執行緒,則可以考慮使用執行緒池。

以上都是些經驗總結,大致都相差無幾,朋友們在做程式碼優化的時候,可以根據這些優化點,有針對性去重構程式碼,其實最重要還是程式碼的可讀性,結構清晰。

效能優化工具

  • Memory Monitor - 記憶體監視工具
  • TraceView
  • MAT

Android開發者對與以上幾個效能調優的工具一定不陌生,這裡我也不再寫那麼多廢話了,關於它們的使用方法,官網還有一些大牛的部落格都有介紹。

最後

寫這篇文章的出發點也是對Android效能優化有個比較清楚的認識,任何事情都不可能一蹴而就,需要循循漸進,對一個初學者你談優化很不現實,我們先把基本的做好,再去考慮相應的優化,筆者也在不斷學習當中,借鑑別人好的優化方案,提高產品的質量,感謝大家對筆者的關注。