Android性能優化以內存優化

前言

成爲一名優秀的Android開發,須要一份完備的知識體系,在這裏,讓咱們一塊兒成長爲本身所想的那樣~。

Tips:本篇是《深刻探索Android內存優化》的基礎篇,若是沒有掌握Android內存優化的同窗建議系統學習一遍。
複製代碼

衆所周知,內存優化能夠說是性能優化中最重要的優化點之一,能夠說,若是你沒有掌握系統的內存優化方案,就不能說你對Android的性能優化有過多的研究與探索。本篇,筆者將帶領你們一塊兒來系統地學習Android中的內存優化。php

可能有很多讀者都知道,在內存管理上,JVM擁有垃圾內存回收的機制,自身會在虛擬機層面自動分配和釋放內存,所以不須要像使用C/C++同樣在代碼中分配和釋放某一塊內存。Android系統的內存管理相似於JVM,經過new關鍵字來爲對象分配內存,內存的釋放由GC來回收。而且Android系統在內存管理上有一個 Generational Heap Memory模型,當內存達到某一個閾值時,系統會根據不一樣的規則自動釋放能夠釋放的內存。即使有了內存管理機制,可是,若是不合理地使用內存,也會形成一系列的性能問題,好比 內存泄漏、內存抖動、短期內分配大量的內存對象 等等。下面,我就先來談談Android的內存管理機制。android

1、Android內存管理機制

咱們都知道,應用程序的內存分配和垃圾回收都是由Android虛擬機完成的,在Android 5.0如下,使用的是Dalvik虛擬機,5.0及以上,則使用的是ART虛擬機git

一、Java對象生命週期

Java代碼編譯後生成的字節碼.class文件從從文件系統中加載到虛擬機以後,便有了JVM上的Java對象,Java對象在JVM上運行有7個階段,以下:github

  • Created
  • InUse
  • Invisible
  • Unreachable
  • Collected
  • Finalized
  • Deallocated

一、Created(建立)

Java對象的建立分爲以下幾步:正則表達式

  • 一、爲對象分配存儲空間。
  • 二、構造對象。
  • 三、從超類到子類對static成員進行初始化,類的static成員的初始化在ClassLoader加載該類時進行。
  • 四、超類成員變量按順序初始化,遞歸調用超類的構造方法。
  • 五、子類成員變量按順序初始化,一旦對象被建立,子類構造方法就調用該對象併爲某些變量賦值。

二、InUse(應用)

此時對象至少被一個強引用持有算法

三、Invisible(不可見)

當一個對象處於不可見階段時,說明程序自己再也不持有該對象的任何強引用,雖然該對象仍然是存在的。簡單的例子就是程序的執行已經超出了該對象的做用域了。可是,該對象仍可能被虛擬機下的某些已裝載的靜態變量線程或JNI等強引用持有,這些特殊的強引用稱爲「GC Root」被這些GC Root強引用的對象會致使該對象的內存泄漏,於是沒法被GC回收shell

四、Unreachable(不可達)

該對象再也不被任何強引用持有數據庫

五、Collected(收集)

GC已經對該對象的內存空間從新分配作好準備時,對象進入收集階段,若是該對象重寫了finalize()方法,則執行它。json

六、Finalized(終結)

等待垃圾回收器回收該對象空間數組

七、Deallocated(對象空間從新分配)

GC對該對象所佔用的內存空間進行回收或者再分配,則該對象完全消失。

注意

  • 一、不須要使用該對象時,及時置空。
  • 二、訪問本地變量優於訪問類中的變量。

二、內存分配

在Android系統中,實際上就是一塊匿名共享內存。Android虛擬機僅僅只是把它封裝成一個 mSpace由底層C庫來管理,而且仍然使用libc提供的函數malloc和free來分配和釋放內存

大多數靜態數據會被映射到一個共享的進程中。常見的靜態數據包括Dalvik Code、app resources、so文件等等。

在大多數狀況下,Android經過顯示分配共享內存區域(如Ashmem或者Gralloc)來實現動態RAM區域可以在不一樣進程之間共享的機制。例如,Window Surface在App和Screen Compositor之間使用共享的內存,Cursor Buffers在Content Provider和Clients之間共享內存

上面說過,對於Android Runtime有兩種虛擬機,Dalvik 和 ART,它們分配的內存區域塊是不一樣的,下面咱們就來簡單瞭解下。

Dalvik

  • Linear Alloc
  • Zygote Space
  • Alloc Space

ART

  • Non Moving Space
  • Zygote Space
  • Alloc Space
  • Image Space
  • Large Obj Space

無論是Dlavik仍是ART,運行時堆都分爲 LinearAlloc(相似於ART的Non Moving Space)、Zygote Space 和 Alloc Space。Dalvik中的Linear Alloc是一個線性內存空間,是一個只讀區域,主要用來存儲虛擬機中的類,由於類加載後只須要只讀的屬性,而且不會改變它。把這些只讀屬性以及在整個進程的生命週期都不能結束的永久數據放到線性分配器中管理,能很好地減小堆混亂和GC掃描,提高內存管理的性能Zygote Space在Zygote進程和應用程序進程之間共享,Allocation Space則是每一個進程獨佔。Android系統的第一個虛擬機由Zygote進程建立而且只有一個Zygote Space。可是當Zygote進程在fork第一個應用程序進程以前,會將已經使用的那部分堆內存劃分爲一部分,尚未使用的堆內存劃分爲另外一部分,也就是Allocation Space。但不管是應用程序進程,仍是Zygote進程,當他們須要分配對象時,都是在各自的Allocation Space堆上進行

當在ART運行時,還有另外兩個區塊,即 ImageSpace和Large Object Space

  • Image Space存放一些預加載類,相似於Dalvik中的Linear Alloc。與Zygote Space同樣,在Zygote進程和應用程序進程之間共享
  • Large Object Space離散地址的集合,分配一些大對象,用於提升GC的管理效率和總體性能

注意:Image Space的對象只建立一次,而Zygote Space的對象須要在系統每次啓動時,根據運行狀況都從新建立一遍。

三、內存回收機制

在Android的高級系統版本中,針對Heap空間有一個Generational Heap Memory的模型,其中將整個內存分爲三個區域:

  • Young Generation(年輕代)
  • Old Generation(年老代)
  • Permanent Generation(持久代)

模型示意圖以下所示:

一、Young Generation

一個Eden區和兩個Survivor區組成,程序中生成的大部分新的對象都在Eden區中,當Eden區滿時,還存活的對象將被複制到其中一個Survivor區,當此Survivor區滿時,此區存活的對象又被複制到另外一個Survivor區,當這個Survivor區也滿時,會將其中存活的對象複製到年老代

二、Old Generation

通常狀況下,年老代中的對象生命週期都比較長

三、Permanent Generation

用於存放靜態的類和方法,持久代對垃圾回收沒有顯著影響。(在 JDK 1.8 及以後的版本,在本地內存中實現的元空間(Meta-space)已經代替了永久代)

四、內存對象的處理過程小結

  • 一、對象建立後在Eden區
  • 二、執行GC後,若是對象仍然存活,則複製到S0區
  • 三、當S0區滿時,該區域存活對象將複製到S1區,而後S0清空,接下來S0和S1角色互換
  • 四、當第3步達到必定次數(系統版本不一樣會有差別)後,存活對象將被複制到Old Generation
  • 五、當這個對象在Old Generation區域停留的時間達到必定程度時,它會被移動到Old Generation,最後累積必定時間再移動到Permanent Generation區域

系統在Young Generation、Old Generation上採用不一樣的回收機制。每個Generation的內存區域都有固定的大小。隨着新的對象陸續被分配到此區域,當對象總的大小臨近這一級別內存區域的閾值時,會觸發GC操做,以便騰出空間來存放其餘新的對象

此外,執行GC佔用的時間與Generation和Generation中的對象數量有關,以下所示:

  • Young Generation < Old Generation < Permanent Generation
  • Generation中的對象數量與執行時間成反比

五、Young Generation GC

因爲其對象存活時間短,所以基於Copying算法(掃描出存活的對象,並複製到一塊新的徹底未使用的控件中)來回收。新生代採用空閒指針的方式來控制GC觸發,指針保持最後一個分配的對象在Young Generation區間的位置,當有新的對象要分配內存時,用於檢查空間是否足夠,不夠就觸發GC

六、Old Generation GC

因爲其對象存活時間較長,比較穩定,所以採用Mark(標記)算法(掃描出存活的對象,而後再回收未被標記的對象,回收後對空出的空間要麼合併,要麼標記出來便於下次分配,以減小內存碎片帶來的效率損耗)來回收。

四、GC類型

在Android系統中,GC有三種類型:

  • kGcCauseForAlloc:分配內存不夠引發的GC,會Stop World。因爲是併發GC,其它線程都會中止,直到GC完成。
  • kGcCauseBackground:內存達到必定閾值觸發的GC,因爲是一個後臺GC,因此不會引發Stop World。
  • kGcCauseExplicit:顯示調用時進行的GC,當ART打開這個選項時,使用System.gc時會進行GC。

接下來,咱們來學會如何分析Android虛擬機中的GC日誌,日誌以下:

D/dalvikvm(7030):GC_CONCURRENT freed 1049K, 60% free 2341K/9351K, external 3502K/6261K, paused 3ms 3ms
複製代碼

GC_CONCURRENT 是當前GC時的類型,GC日誌中有如下幾種類型:

  • GC_CONCURRENT:當應用程序中的Heap內存佔用上升時(分配對象大小超過384k),避免Heap內存滿了而觸發的GC。若是發現有大量的GC_CONCURRENT出現,說明應用中可能一直有大於384k的對象被分配,而這通常都是一些臨時對象被反覆建立,多是對象複用不夠所致使的
  • GC_FOR_MALLOC:這是因爲Concurrent GC沒有及時執行完,而應用又須要分配更多的內存,這時不得不停下來進行Malloc GC。
  • GC_EXTERNAL_ALLOC:這是爲external分配的內存執行的GC。
  • GC_HPROF_DUMP_HEAP:建立一個HPROF profile的時候執行。
  • GC_EXPLICIT:顯示調用了System.GC()。(儘可能避免)

咱們再回到上面打印的日誌:

  • freed 1049k:代表在此次GC中回收了多少內存。
  • 60% free 2341k/9351K:代表回收後60%的Heap可用,存活的對象大小爲2341kb,heap大小是9351kb。
  • external 3502/6261K:是Native Memory的數據。存放Bitmap Pixel Data(位圖數據)或者堆之外內存(NIO Direct Buffer)之類的數據。第一個值說明在Native Memory中已分配3502kb內存,第二個值是一個浮動的GC閾值,當分配內存達到這個值時,會觸發一次GC。
  • paused 3ms 3ms:代表GC的暫停時間,若是是Concurrent GC,會看到兩個時間,一個開始,一個結束,且時間很短,若是是其餘類型的GC,極可能只會看到一個時間,且這個時間是相對比較長的。而且,越大的Heap Size在GC時致使暫停的時間越長。

注意:在ART模式下,多了一個Large Object Space,這部份內存並非分配在堆上,但仍是屬於應用程序的內存空間

在Dalvik虛擬機下,GC的操做都是併發的,也就意味着每次觸發GC都會致使其它線程暫停工做(包括UI線程)。而在ART模式下,GC時不像Dalvik僅有一種回收算法,ART在不一樣的狀況下會選擇不一樣的回收算法,好比Alloc內存不夠時會採用非併發GC,但在Alloc後,發現內存達到必定閾值時又會觸發併發GC。因此在ART模式下,並非全部的GC都是非併發的。

整體來看,在GC方面,與Dalvik相比,ART更爲高效,不只僅是GC的效率,大大地縮短了Pause時間,並且在內存分配上對大內存分配單獨的區域,還能有算法在後臺作內存整理,減小內存碎片。所以,在ART虛擬機下,能夠避免較多的相似GC致使的卡頓問題。

2、優化內存的意義

優化內存的意義不言而喻,總的來講能夠歸結爲以下四點:

  • 一、減小OOM,提升應用穩定性
  • 二、減小卡頓,提升應用流暢度
  • 三、減小內存佔用,提升應用後臺運行時的存活率
  • 四、減小異常發生和代碼邏輯隱患

須要注意的是,出現OOM是由於內存溢出致使,這種狀況不必定會發生在相對應的代碼處,也不必定是出現OOM的代碼使用內存有問題,而是恰好執行到這段代碼。

3、避免內存泄漏

一、內存泄漏的定義

Android系統虛擬機的垃圾回收是經過虛擬機GC機制來實現的。GC會選擇一些還存活的對象做爲內存遍歷的根節點GC Roots,經過對GC Roots的可達性來判斷是否須要回收。內存泄漏就是在當前應用週期內再也不使用的對象被GC Roots引用,致使不能回收,使實際可以使用內存變小

二、使用MAT來查找內存泄漏

MAT工具能夠幫助開發者定位致使內存泄漏的對象,以及發現大的內存對象,而後解決內存泄漏並經過優化內存對象,以達到減小內存消耗的目的。

使用步驟

一、在https://eclipse.org/mat/downloads.php下載MAT客戶端。

二、從Android Studio進入Profile的Memory視圖,選擇須要分析的應用進程,對應用進行懷疑有內存問題的操做,結束操做後,主動GC幾回,最後export dump文件。

三、由於Android Studio保存的是Android Dalvik/ART格式的.hprof文件,因此須要轉換成J2SE HPROF格式才能被MAT識別和分析。Android SDK自帶了一個轉換工具在SDK的platform-tools下,其中轉換語句爲:

./hprof-conv file.hprof converted.hprof
複製代碼

四、經過MAT打開轉換後的HPROF文件。

MAT視圖

在MAT窗口上,OverView是一個整體概覽,顯示整體的內存消耗狀況和疑似問題。MAT提供了多種分析維度,其中Histogram、Dominator Tree、Top Consumers和Leak Suspects的分析維度是不一樣的。下面分別介紹下它們,以下所示:

一、Histogram

列出內存中的全部實例類型對象和其個數以及大小,並在頂部的regex區域支持正則表達式查找。

二、Dominator Tree

列出最大的對象及其依賴存活的Object。相比Histogram,能更方便地看出引用關係

三、Top Consumers

經過圖像列出最大的Object

四、Leak Suspects

經過MAT自動分析內存泄漏的緣由和泄漏的一份整體報告

分析內存最經常使用的是Histogram和Dominator Tree這兩個視圖,視圖中一共有四列:

  • Class Name:類名。
  • Objects:對象實例個數。
  • Shallow Heap:對象自身佔用的內存大小,不包括它引用的對象。非數組的常規對象的Shallow Heap Size由其成員變量的數量和類型決定,數組的Shallow Heap Size由數組元素的類型(對象類型、基本類型)和數組長度決定。真正的內存都在堆上,看起來是一堆原生的byte[]、char[]、int[],對象自己的內存都很小。所以Shallow Heap對分析內存泄漏意義不是很大
  • Retained Heap:是當前對象大小與當前對象可直接或間接引用到的對象的大小總和,包括被遞歸釋放的。即:Retained Size就是當前對象被GC後,從Heap上總共能釋放掉的內存大小。

查找內存泄漏具體位置

常規方式

  • 一、按照包名類型分類進行實例篩選或直接使用頂部Regex選取特定實例。
  • 二、右擊選中被懷疑的實例對象,選擇Merge Shortest Paths to GC Root->exclude all phantom/weak/soft etc references。(顯示GC Roots最短路徑的強引用)
  • 三、分析引用鏈或經過代碼邏輯找出緣由。

還有一種更快速的方法就是對比泄漏先後的HPROF數據:

  • 一、在兩個HPROF文件中,把Histogram或者Dominator Tree增長到Compare Basket。
  • 二、在Compare Basket中單擊 ! ,生成對比結果視圖。這樣就能夠對比相同的對象在不一樣階段的對象實例個數和內存佔用大小,如明顯只須要一個實例的對象,或者不該該增長的對象實例個數卻增長了,說明發生了內存泄漏,就須要去代碼中定位具體的緣由並解決。

須要注意的是,若是目標不太明確,能夠直接定位RetainedHeap最大的Object,經過Select incoming references查看引用鏈,定位到可疑的對象,而後經過Path to GC Roots分析引用鏈

此外,咱們知道,當Hash集合中過多的對象返回相同的Hash值時,會嚴重影響性能,這時能夠用 Map Collision Ratio 查找致使Hash集合的碰撞率較高的罪魁禍首

高效方式

在本人平時的項目開發中,通常會使用以下幾種方式來快速對指定頁面進行內存泄漏的檢測(也稱爲運行時內存分析優化):

  • 一、shell命令 + LeakCanary + MAT:運行程序,全部功能跑一遍,確保沒有改出問題,徹底退出程序,手動觸發GC,而後使用adb shell dumpsys meminfo packagename -d命令查看退出界面後Objects下的Views和Activities數目是否爲0,若是不是則經過LeakCanary檢查可能存在內存泄露的地方,最後經過MAT分析,如此反覆,改善滿意爲止。

  • 二、Profile MEMORY:運行程序,對每個頁面進行內存分析檢查。首先,反覆打開關閉頁面5次,而後收到GC(點擊Profile MEMORY左上角的垃圾桶圖標),若是此時total內存尚未恢復到以前的數值,則可能發生了內存泄露。此時,再點擊Profile MEMORY左上角的垃圾桶圖標旁的heap dump按鈕查看當前的內存堆棧狀況,選擇按包名查找,找到當前測試的Activity,若是引用了多個實例,則代表發生了內存泄露。

  • 三、從首頁開始用依次dump出每一個頁面的內存快照文件,而後利用MAT的對比功能,找出每一個頁面相對於上個頁面內存裏主要增長了哪些東西,作針對性優化。

  • 四、利用Android Memory Profiler實時觀察進入每一個頁面後的內存變化狀況,而後對產生的內存較大波峯作分析。

此外,除了運行時內存的分析優化,咱們還能夠對App的靜態內存進行分析與優化。靜態內存指的是在伴隨着App的整個生命週期一直存在的那部份內存,那咱們怎麼獲取這部份內存快照呢?

首先,確保打開每個主要頁面的主要功能,而後回到首頁,進開發者選項去打開"不保留後臺活動"。而後,將咱們的app退到後臺,GC,dump出內存快照。最後,咱們就能夠將對dump出的內存快照進行分析,看看有哪些地方是能夠優化的,好比加載的圖片、應用中全局的單例數據配置、靜態內存與緩存、埋點數據、內存泄漏等等。

三、常見內存泄漏場景

對於內存泄漏,其本質可理解爲沒法回收無用的對象。這裏我總結了我在項目中遇到的一些常見的內存泄漏案例(包含解決方案)。

一、資源性對象未關閉

對於資源性對象再也不使用時,應該當即調用它的close()函數,將其關閉,而後再置爲null。例如Bitmap等資源未關閉會形成內存泄漏,此時咱們應該在Activity銷燬時及時關閉。

二、註冊對象未註銷

例如BraodcastReceiver、EventBus未註銷形成的內存泄漏,咱們應該在Activity銷燬時及時註銷。

三、類的靜態變量持有大數據對象

儘可能避免使用靜態變量存儲數據,特別是大數據對象,建議使用數據庫存儲。

四、單例形成的內存泄漏

優先使用Application的Context,如需使用Activity的Context,能夠在傳入Context時使用弱引用進行封裝,而後,在使用到的地方從弱引用中獲取Context,若是獲取不到,則直接return便可。

五、非靜態內部類的靜態實例

該實例的生命週期和應用同樣長,這就致使該靜態實例一直持有該Activity的引用,Activity的內存資源不能正常回收。此時,咱們能夠將該內部類設爲靜態內部類或將該內部類抽取出來封裝成一個單例,若是須要使用Context,儘可能使用Application Context,若是須要使用Activity Context,就記得用完後置空讓GC能夠回收,不然仍是會內存泄漏。

六、Handler臨時性內存泄漏

Message發出以後存儲在MessageQueue中,在Message中存在一個target,它是Handler的一個引用,Message在Queue中存在的時間過長,就會致使Handler沒法被回收。若是Handler是非靜態的,則會致使Activity或者Service不會被回收。而且消息隊列是在一個Looper線程中不斷地輪詢處理消息,當這個Activity退出時,消息隊列中還有未處理的消息或者正在處理的消息,而且消息隊列中的Message持有Handler實例的引用,Handler又持有Activity的引用,因此致使該Activity的內存資源沒法及時回收,引起內存泄漏。解決方案以下所示:

  • 一、使用一個靜態Handler內部類,而後對Handler持有的對象(通常是Activity)使用弱引用,這樣在回收時,也能夠回收Handler持有的對象。
  • 二、在Activity的Destroy或者Stop時,應該移除消息隊列中的消息,避免Looper線程的消息隊列中有待處理的消息須要處理。

須要注意的是,AsyncTask內部也是Handler機制,一樣存在內存泄漏風險,但其通常是臨時性的。對於相似AsyncTask或是線程形成的內存泄漏,咱們也能夠將AsyncTask和Runnable類獨立出來或者使用靜態內部類。

七、容器中的對象沒清理形成的內存泄漏

在退出程序以前,將集合裏的東西clear,而後置爲null,再退出程序

八、WebView

WebView都存在內存泄漏的問題,在應用中只要使用一次WebView,內存就不會被釋放掉。咱們能夠爲WebView開啓一個獨立的進程,使用AIDL與應用的主進程進行通訊,WebView所在的進程能夠根據業務的須要選擇合適的時機進行銷燬,達到正常釋放內存的目的。

九、使用ListView時形成的內存泄漏

在構造Adapter時,使用緩存的convertView。

四、內存泄漏監控

通常使用LeakCanary進行內存泄漏的監控便可,具體使用和原理分析請參見我以前的文章Android主流三方庫源碼分析(6、深刻理解Leakcanary源碼)

除了基本使用外,咱們還能夠自定義處理結果,首先,繼承DisplayLeakService實現一個自定義的監控處理Service,代碼以下:

public class LeakCnaryService extends DisplayLeakServcie {
    
    private final String TAG = 「LeakCanaryService」;
    
    @Override
    protected void afterDefaultHandling(HeapDump heapDump, AnalysisResult result, String leakInfo) {
        ...
    }
}
複製代碼

重寫 afterDefaultHanding 方法,在其中處理須要的數據,三個參數的定義以下:

  • heapDump:堆內存文件,能夠拿到完成的hprof文件,以使用MAT分析。
  • result:監控到的內存狀態,如是否泄漏等。
  • leakInfo:leak trace詳細信息,除了內存泄漏對象,還有設備信息。

而後在install時,使用自定義的LeakCanaryService便可,代碼以下:

public class BaseApplication extends Application {
    
    @Override
    public void onCreate() {
        super.onCreate();
        mRefWatcher = LeakCanary.install(this, LeakCanaryService.calss, AndroidExcludedRefs.createAppDefaults().build());
    }
    
    ...
    
}
複製代碼

通過這樣的處理,就能夠在LeakCanaryService中實現本身的處理方式,如豐富的提示信息,把數據保存在本地、上傳到服務器進行分析。

注意

LeakCanaryService須要在AndroidManifest中註冊。

4、優化內存空間

一、對象引用

從Java 1.2版本開始引入了三種對象引用方式:SoftReference、WeakReference 和 PhantomReference 三個引用類,引用類的主要功能就是可以引用但仍能夠被垃圾回收器回收的對象。在引入引用類以前,只能使用Strong Reference,若是沒有指定對象引用類型,默認是強引用。下面,咱們就分別來介紹下這幾種引用。

一、強引用

若是一個對象具備強引用,GC就絕對不會回收它。當內存空間不足時,JVM會拋出OOM錯誤。

二、軟引用

若是一個對象只具備軟引用,則內存空間足夠,GC時就不會回收它;若是內存不足,就會回收這些對象的內存。可用來實現內存敏感的高速緩存。

軟引用能夠和一個ReferenceQueue(引用隊列)聯合使用,若是軟引用引用的對象被垃圾回收器回收,JVM會把這個軟引用加入與之關聯的引用隊列中。

三、弱引用

在垃圾回收器線程掃描它所管轄的內存區域的過程當中,一旦發現了只具備弱引用的對象,無論當前內存空間是否足夠,都會回收它的內存。不過,因爲垃圾回收器是一個優先級很低的線程,所以不必定會很快發現那些只具備弱引用的對象。

這裏要注意,可能須要運行屢次GC,才能找到並釋放弱引用對象。

四、虛引用

只能用於跟蹤即將對被引用對象進行的收集。虛擬機必須與ReferenceQueue類聯合使用。由於它可以充當通知機制。

二、減小沒必要要的內存開銷

一、AutoBoxing

自動裝箱的核心就是把基礎數據類型轉換成對應的複雜類型。在自動裝箱轉化時,都會產生一個新的對象,這樣就會產生更多的內存和性能開銷。如int只佔4字節,而Integer對象有16字節,特別是HashMap這類容器,進行增、刪、改、查操做時,都會產生大量的自動裝箱操做。

檢測方式

使用TraceView查看耗時,若是發現調用了大量的integer.value,就說明發生了AutoBoxing。

二、內存複用

對於內存複用,有以下四種可行的方式:

  • 資源複用:通用的字符串、顏色定義、簡單頁面佈局的複用。
  • 視圖複用:可使用ViewHolder實現ConvertView複用。
  • 對象池:顯示建立對象池,實現複用邏輯,對相同的類型數據使用同一塊內存空間。
  • Bitmap對象的複用:使用inBitmap屬性能夠告知Bitmap解碼器嘗試使用已經存在的內存區域,新解碼的bitmap會嘗試使用以前那張bitmap在heap中佔據的pixel data內存區域。

三、使用最優的數據類型

一、HashMap與ArrayMap

HashMap是一個散列鏈表,向HashMap中put元素時,先根據key的HashCode從新計算hash值,根據hash值獲得這個元素在數組中的位置,若是數組該位置上已經存放有其它元素了,那麼這個位置上的元素將以鏈表的形式存放,新加入的放在鏈頭,最後加入的放在鏈尾。若是數組該位置上沒有元素,就直接將該元素放到此數組中的該位置上。也就是說,向HashMap插入一個對象前,會給一個通向Hash陣列的索引,在索引的位置中,保存了這個Key對象的值。這意味着須要考慮的一個最大問題是衝突,當多個對象散列於陣列相同位置時,就會有散列衝突的問題。所以,HashMap會配置一個大的數組來減小潛在的衝突,而且會有其餘邏輯防止連接算法和一些衝突的發生。

ArrayMap提供了和HashMap同樣的功能,但避免了過多的內存開銷,方法是使用兩個小數組,而不是一個大數組。而且ArrayMap在內存上是連續不間斷的。

整體來講,在ArrayMap中執行插入或者刪除操做時,從性能角度上看,比HashMap還要更差一些,但若是隻涉及很小的對象數,好比1000如下,就不須要擔憂這個問題了。由於此時ArrayMap不會分配過大的數組

此外,Android自身還提供了一系列優化事後的數據集合工具類,如 SparseArray、SparseBooleanArray、LongSparseArray,使用這些API可讓咱們的程序更加高效。HashMap 工具類會相對比較 低效,由於它 須要爲每個鍵值對都提供一個對象入口,而 SparseArray避免 掉了 基本數據類型轉換成對象數據類型的時間

二、使用 IntDef和StringDef 替代枚舉類型

使用枚舉類型的dex size是普一般量定義的dex size的13倍以上,同時,運行時的內存分配,一個enum值的聲明會消耗至少20bytes。

枚舉最大的優勢是類型安全,但在Android平臺上,枚舉的內存開銷是直接定義常量的三倍以上。因此Android提供了註解的方式檢查類型安全。目前提供了int型和String型兩種註解方式:IntDef和StringDef,用來提供編譯期的類型檢查。

注意

使用IntDef和StringDef須要在Gradle配置中引入相應的依賴包:

compile 'com.android.support:support-annotations:22.0.0'
複製代碼

三、LruCache

最近最少使用緩存,使用強引用保存須要緩存的對象,它內部維護了一個由LinkedHashMap組成的雙向列表,不支持線程安全,LruCache對它進行了封裝,添加了線程安全操做。當其中的一個值被訪問時,它被放到隊列的尾部,當緩存將滿時,隊列頭部的值(最近最少被訪問的)被丟棄,以後能夠被GC回收。

除了普通的get/set方法以外,還有sizeOf方法,它用來返回每一個緩存對象的大小。此外,還有entryRemoved方法,當一個緩存對象被丟棄時調用的方法,當第一個參數爲true:代表環處對象是爲了騰出空間而被清理時。不然,代表緩存對象的entry被remove移除或者被put覆蓋時。

注意

分配LruCache大小時應考慮應用剩餘內存有多大。

四、圖片內存優化

在Android默認狀況下,當圖片文件解碼成位圖時,會被處理成32bit/像素。紅色、綠色、藍色和透明通道各8bit,即便是沒有透明通道的圖片,如JEPG隔世是沒有透明通道的,但而後會處理成32bit位圖,這樣分配的32bit中的8bit透明通道數據是沒有任何用處的,這徹底沒有必要,而且在這些圖片被屏幕渲染以前,它們首先要被做爲紋理傳送到GPU,這意味着每一張圖片會同時佔用CPU內存和GPU內存。下面,我總結了減小內存開銷的幾種經常使用方式,以下所示:

一、設置位圖的規格:當顯示小圖片或對圖片質量要求不高時能夠考慮使用RGB_565,用戶頭像或圓角圖片通常能夠嘗試ARGB_4444。經過設置inPreferredConfig參數來實現不一樣的位圖規格,代碼以下所示:

BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.RGB_565;
BitmapFactory.decodeStream(is, null, options);
複製代碼

二、inSampleSize:位圖功能對象中的inSampleSize屬性實現了位圖的縮放功能,代碼以下所示:

BitampFactory.Options options = new BitmapFactory.Options();
// 設置爲4就是寬和高都變爲原來1/4大小的圖片
options.inSampleSize = 4;
BitmapFactory.decodeSream(is, null, options);
複製代碼

三、inScaled,inDensity和inTargetDensity實現更細的縮放圖片:當inScaled設置爲true時,系統會按照現有的密度來劃分目標密度,代碼以下所示:

BitampFactory.Options options = new BitampFactory.Options();
options.inScaled = true;
options.inDensity = srcWidth;
options.inTargetDensity = dstWidth;
BitmapFactory.decodeStream(is, null, options);
複製代碼

上述三種方案的缺點:使用了過多的算法,致使圖片顯示過程須要更多的時間開銷,若是圖片不少的話,就影響到圖片的顯示效果。最好的方案是結合這兩個方法,達到最佳的性能結合,首先使用inSampleSize處理圖片,轉換爲接近目標的2次冪,而後用inDensity和inTargetDensity生成最終想要的準確大小,由於inSampleSize會減小像素的數量,而基於輸出密碼的須要對像素從新過濾。但獲取資源圖片的大小,須要設置位圖對象的inJustDecodeBounds值爲true,而後繼續解碼圖片文件,這樣才能生產圖片的寬高數據,並容許繼續優化圖片。整體的代碼以下所示:

BitmapFactory.Options options = new BitampFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(is, null, options);
options.inScaled = true;
options.inDensity = options.outWidth;
options.inSampleSize = 4;
Options.inTargetDensity = desWith * options.inSampleSize;
options.inJustDecodeBounds = false;
BitmapFactory.decodeStream(is, null, options);
複製代碼

五、inBitmap

能夠結合LruCache來實現,在LruCache移除超出cache size的圖片時,暫時緩存Bitamp到一個軟引用集合,須要建立新的Bitamp時,能夠從這個軟用用集合中找到最適合重用的Bitmap,來重用它的內存區域。

須要注意,新申請的Bitmap與舊的Bitmap必須有相同的解碼格式,而且在Android 4.4以前,只能重用相同大小的Bitamp的內存區域,而Android 4.4以後能夠重用任何bitmap的內存區域。

六、圖片放置優化

只須要UI提供一套高分辨率的圖,圖片建議放在drawable-xxhdpi文件夾下,這樣在低分辨率設備中圖片的大小隻是壓縮,不會存在內存增大的狀況。如若遇到不需縮放的文件,放在drawable-nodpi文件夾下。

七、在App可用內存太低時主動釋放內存

在App退到後臺內存緊張即將被Kill掉時選擇重寫 onTrimMemory/onLowMemory 方法去釋放掉圖片緩存、靜態緩存來自保。

八、item被回收不可見時釋放掉對圖片的引用

  • ListView:所以每次item被回收後再次利用都會從新綁定數據,只需在ImageView onDetachFromWindow的時候釋放掉圖片引用便可。
  • RecyclerView:由於被回收不可見時第一選擇是放進mCacheView中,這裏item被複用並不會只需bindViewHolder來從新綁定數據,只有被回收進mRecyclePool中後拿出來複用纔會從新綁定數據,所以重寫Recycler.Adapter中的onViewRecycled()方法來使item被回收進RecyclePool的時候去釋放圖片引用。

九、避免創做沒必要要的對象

例如,咱們能夠在字符串拼接的時候使用StringBuffer,StringBuilder。

十、自定義View中的內存優化

例如,在onDraw方法裏面不要執行對象的建立,通常來講,都應該在自定義View的構造器中建立對象。

十一、其它的內存優化注意事項

除了上面的一些內存優化點以外,這裏還有一些內存優化的點咱們須要注意,以下所示:

  • 盡使用static final 優化成員變量。
  • 使用加強型for循環語法。
  • 在沒有特殊緣由的狀況下,儘可能使用基本數據類型來代替封裝數據類型,int比Integer要更加有效,其它數據類型也是同樣。
  • 在合適的時候適當採用軟引用和弱引用。
  • 採用內存緩存和磁盤緩存。
  • 儘可能採用靜態內部類,可避免潛在因爲內部類致使的內存泄漏。

5、圖片管理模塊的設計與實現

在設計一個模塊時,須要考慮如下幾點:

  • 一、單一職責
  • 二、避免不一樣功能之間的耦合
  • 三、接口隔離

在編寫代碼前先畫好UML圖肯定每個對象、方法、接口的功能,首先儘可能作到功能單一原則,在這個基礎上,再明確模塊與模塊的直接關係,最後使用代碼實現。

一、實現異步加載功能

1.實現網絡圖片顯示

ImageLoader是實現圖片加載的基類,其中ImageLoader有一個內部類BitmapLoadTask是繼承AsyncTask的異步下載管理類,負責圖片的下載和刷新,MiniImageLoader是ImageLoader的子類,維護類一個ImageLoader的單例,而且實現了基類的網絡加載功能,由於具體的下載在應用中有不一樣的下載引擎,抽象成接口便於替換。代碼以下所示:

public abstract class ImageLoader {
    private boolean mExitTasksEarly = false;   //是否提早結束
    protected boolean mPauseWork = false;
    private final Object mPauseWorkLock = new   Object();

    protected ImageLoader() {

    }

    public void loadImage(String url, ImageView imageView) {
        if (url == null) {
            return;
        }

        BitmapDrawable bitmapDrawable = null;
        if (bitmapDrawable != null) {
            imageView.setImageDrawable(bitmapDrawable);
        } else {
            final BitmapLoadTask task = new BitmapLoadTask(url, imageView);
            task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
        }
    }

    private class BitmapLoadTask extends AsyncTask<Void, Void, Bitmap> {

        private String mUrl;
        private final WeakReference<ImageView> imageViewWeakReference;

        public BitmapLoadTask(String url, ImageView imageView) {
            mUrl = url;
            imageViewWeakReference = new WeakReference<ImageView>(imageView);
        }

        @Override
        protected Bitmap doInBackground(Void... params) {
            Bitmap bitmap = null;
            BitmapDrawable drawable = null;

            synchronized (mPauseWorkLock) {
                while (mPauseWork && !isCancelled()) {
                    try {
                        mPauseWorkLock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }

            if (bitmap == null
                    && !isCancelled()
                    && imageViewWeakReference.get() != null
                    && !mExitTasksEarly) {
                bitmap = downLoadBitmap(mUrl);
            }
            return bitmap;
        }

        @Override
        protected void onPostExecute(Bitmap bitmap) {
            if (isCancelled() || mExitTasksEarly) {
                bitmap = null;
            }

            ImageView imageView = imageViewWeakReference.get();
            if (bitmap != null && imageView != null) {
                setImageBitmap(imageView, bitmap);
            }
        }

        @Override
        protected void onCancelled(Bitmap bitmap) {
            super.onCancelled(bitmap);
            synchronized (mPauseWorkLock) {
                mPauseWorkLock.notifyAll();
            }
        }
    }

    public void setPauseWork(boolean pauseWork) {
        synchronized (mPauseWorkLock) {
            mPauseWork = pauseWork;
            if (!mPauseWork) {
                mPauseWorkLock.notifyAll();
            }
        }
    }

    public void setExitTasksEarly(boolean exitTasksEarly) {
        mExitTasksEarly = exitTasksEarly;
        setPauseWork(false);
    }

    private void setImageBitmap(ImageView imageView, Bitmap bitmap) {
        imageView.setImageBitmap(bitmap);
    }

    protected abstract Bitmap downLoadBitmap(String    mUrl);
}
複製代碼

setPauseWork方法是圖片加載線程控制接口,pauseWork控制圖片模塊的暫停和繼續工做,通常在listView等控件中,滑動時中止加載圖片,保證滑動流暢。另外,具體的圖片下載和解碼是和業務強相關的,所以在ImageLoader中不作具體的實現,只是定義類一個抽象方法。

MiniImageLoader是一個單例,保證一個應用只維護一個ImageLoader,減小對象開銷,並管理應用中全部的圖片加載。MiniImageLoader代碼以下所示:

public class MiniImageLoader extends ImageLoader {
    
    private volatile static MiniImageLoader sMiniImageLoader = null;
    
    private ImageCache mImageCache = null;
    
    public static MiniImageLoader getInstance() {
        if (null == sMiniImageLoader) {
            synchronized (MiniImageLoader.class) {
                MiniImageLoader tmp = sMiniImageLoader;
                if (tmp == null) {
                    tmp = new MiniImageLoader();
                }
                sMiniImageLoader = tmp;
            }
        }
        return sMiniImageLoader;
    }
    
    public MiniImageLoader() {
        mImageCache = new ImageCache();
    }
    
    @Override
    protected Bitmap downLoadBitmap(String mUrl) {
        HttpURLConnection urlConnection = null;
        InputStream in = null;
        try {
            final URL url = new URL(mUrl);
            urlConnection = (HttpURLConnection) url.openConnection();
            in = urlConnection.getInputStream();
            Bitmap bitmap = decodeSampledBitmapFromStream(in, null);
            return bitmap;
            
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (urlConnection != null) {
                urlConnection.disconnect();
                urlConnection = null;
            }
            
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        return null;
    }
    
    public Bitmap decodeSampledBitmapFromStream(InputStream is, BitmapFactory.Options options) {
        return BitmapFactory.decodeStream(is, null, options);
    }
}
複製代碼

其中,volatile保證了對象從主內存加載。而且,上面的try ...cache層級太多,Java中有一個Closeable接口,該接口標識類一個可關閉的對象,所以能夠寫以下的工具類:

public class CloseUtils {

    public static void closeQuietly(Closeable closeable) {
        if (null != closeable) {
            try {
                closeable.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
複製代碼

改造後以下所示:

finally {
    if  (urlConnection != null) {
        urlConnection.disconnect();    
    }
    CloseUtil.closeQuietly(in);
}
複製代碼

同時,爲了使ListView在滑動過程當中更流暢,在滑動時暫停圖片加載,減小系統開銷,代碼以下所示:

listView.setOnScrollListener(new AbsListView.OnScrollListener() {
    
    @Override
    public void onScrollStateChanged(AbsListView absListView, int scrollState) {
        if (scorllState == AbsListView.OnScrollListener.SCROLL_STAE_FLING) {
            MiniImageLoader.getInstance().setPauseWork(true);
        } else {
            MiniImageLoader.getInstance().setPauseWork(false);
        }
    
}
複製代碼

2 單個圖片內存優化

這裏使用一個BitmapConfig類來實現參數的配置,代碼以下所示:

public class BitmapConfig {

    private int mWidth, mHeight;
    private Bitmap.Config mPreferred;

    public BitmapConfig(int width, int height) {
        this.mWidth = width;
        this.mHeight = height;
        this.mPreferred = Bitmap.Config.RGB_565;
    }

    public BitmapConfig(int width, int height, Bitmap.Config preferred) {
        this.mWidth = width;
        this.mHeight = height;
        this.mPreferred = preferred;
    }

    public BitmapFactory.Options getBitmapOptions() {
		return getBitmapOptions(null);
    }

    // 精確計算,須要圖片is流現解碼,再計算寬高比
    public BitmapFactory.Options getBitmapOptions(InputStream is) {
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inPreferredConfig = Bitmap.Config.RGB_565;
        if (is != null) {
            options.inJustDecodeBounds = true;
            BitmapFactory.decodeStream(is, null, options);
            options.inSampleSize = calculateInSampleSize(options, mWidth, mHeight);
        }
        options.inJustDecodeBounds = false;
        return options;
    }

    private static int calculateInSampleSize(BitmapFactory.Options    options, int mWidth, int mHeight) {
        final int height = options.outHeight;
        final int width = options.outWidth;
        int inSampleSize = 1;
        if (height > mHeight || width > mWidth) {
            final int halfHeight = height / 2;
            final int halfWidth = width / 2;
            while ((halfHeight / inSampleSize) > mHeight
                    && (halfWidth / inSampleSize) > mWidth) {
                inSampleSize *= 2;
            }
        }
        
        return inSampleSize;
    }
}
複製代碼

而後,調用MiniImageLoader的downLoadBitmap方法,增長獲取BitmapFactory.Options的步驟:

final URL url = new URL(urlString);
urlConnection = (HttpURLConnection) url.openConnection();
in = urlConnection.getInputStream();
final BitmapFactory.Options options =    mConfig.getBitmapOptions(in);
in.close();
urlConnection.disconnect();
urlConnection = (HttpURLConnection)    url.openConnection();
in = urlConnection.getInputStream();
Bitmap bitmap = decodeSampledBitmapFromStream(in,    options);
複製代碼

優化後仍存在一些問題:

  • 1.相同的圖片,每次都要從新加載;
  • 2.總體內存開銷不可控,雖然減小了單個圖片開銷,可是在片很是多的狀況下,沒有合理管理機制仍然對性能有嚴重影的。

爲了解決這兩個問題,就須要有內存池的設計理念,經過內存池控制總體圖片內存,不從新加載和解碼已經顯示過的圖片。

二、實現三級緩存

內存--本地--網絡

一、內存緩存

使用軟引用和弱引用(SoftReference or WeakReference)來實現內存池是之前的經常使用作法,可是如今不建議。從API 9起(Android 2.3)開始,Android系統垃圾回收器更傾向於回收持有軟引用和弱引用的對象,因此不是很靠譜,從Android 3.0開始(API 11)開始,圖片的數據沒法用一種可碰見的方式將其釋放,這就存在潛在的內存溢出風險。 使用LruCache來實現內存管理是一種可靠的方式,它的主要算法原理是把最近使用的對象用強引用來存儲在LinkedHashMap中,而且把最近最少使用的對象在緩存值達到預設定值以前從內存中移除。使用LruCache實現一個圖片的內存緩存的代碼以下所示:

public class MemoryCache {

    private final int DEFAULT_MEM_CACHE_SIZE = 1024 * 12;
    private LruCache<String, Bitmap> mMemoryCache;
    private final String TAG = "MemoryCache";
    public MemoryCache(float sizePer) {
        init(sizePer);
    }

    private void init(float sizePer) {
        int cacheSize = DEFAULT_MEM_CACHE_SIZE;
        if (sizePer > 0) {
            cacheSize = Math.round(sizePer * Runtime.getRuntime().maxMemory() / 1024);
        }

        mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap value) {
                final int bitmapSize = getBitmapSize(value) / 1024;
                return bitmapSize == 0 ? 1 : bitmapSize;
            }

            @Override
            protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
               super.entryRemoved(evicted, key, oldValue, newValue);
            }
        };
    }

    @TargetApi(Build.VERSION_CODES.KITKAT)
    public int getBitmapSize(Bitmap bitmap) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            return bitmap.getAllocationByteCount();
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) {
            return bitmap.getByteCount();
        }

        return bitmap.getRowBytes() * bitmap.getHeight();
    }

    public Bitmap getBitmap(String url) {
        Bitmap bitmap = null;
        if (mMemoryCache != null) {
            bitmap = mMemoryCache.get(url);
        }
        if (bitmap != null) {
            Log.d(TAG, "Memory cache exiet");
        }

        return bitmap;
    }

    public void addBitmapToCache(String url, Bitmap bitmap) {
        if (url == null || bitmap == null) {
            return;
        }

        mMemoryCache.put(url, bitmap);
    }

    public void clearCache() {
        if (mMemoryCache != null) {
            mMemoryCache.evictAll();
        }
    }
}
複製代碼

上述代碼中cacheSize百分比佔比多少合適?能夠基於如下幾點來考慮:

  • 1.應用中內存的佔用狀況,除了圖片之外,是否還有大內存的數據須要緩存到內存。
  • 2.在應用中大部分狀況要同時顯示多少張圖片,優先保證最大圖片的顯示數量的緩存支持。
  • 3.Bitmap的規格,計算出一張圖片佔用的內存大小。
  • 4.圖片訪問的頻率。

在應用中,若是有一些圖片的訪問頻率要比其它的大一些,或者必須一直顯示出來,就須要一直保持在內存中,這種狀況可使用多個LruCache對象來管理多組Bitmap,對Bitmap進行分級,不一樣級別的Bitmap放到不一樣的LruCache中。

二、bitmap內存複用

從Android3.0開始Bitmap支持內存複用,也就是BitmapFactoy.Options.inBitmap屬性,若是這個屬性被設置有效的目標用對象,decode方法就在加載內容時重用已經存在的bitmap,這意味着Bitmap的內存被從新利用,這能夠減小內存的分配回收,提升圖片的性能。代碼以下所示:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
        mReusableBitmaps = Collections.synchronizedSet(newHashSet<SoftReference<Bitmap>>());
}
複製代碼

由於inBitmap屬性在Android3.0之後才支持,在entryRemoved方法中加入軟引用集合,做爲複用的源對象,以前是直接刪除,代碼以下所示:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
    mReusableBitmaps.add(new SoftReference<Bitmap>(oldValue));
}
複製代碼

一樣在3.0以上判斷,須要分配一個新的bitmap對象時,首先檢查是否有可複用的bitmap對象:

public static Bitmap decodeSampledBitmapFromStream(InputStream is, BitmapFactory.Options options, ImageCache cache) {
     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
         addInBitmapOptions(options, cache);
     }
     return BitmapFactory.decodeStream(is, null, options);
 }

@TargetApi(Build.VERSION_CODES.HONEYCOMB)
private static void addInBitmapOptions(BitmapFactory.Options options, ImageCache cache) {
     options.inMutable = true;
     if (cache != null) {
         Bitmap inBitmap = cache.getBitmapFromReusableSet(options);
         if (inBitmap != null) {
             options.inBitmap = inBitmap;
         }
     }

 }
複製代碼

接着,咱們使用cache.getBitmapForResubleSet方法查找一個合適的bitmap賦值給inBitmap。代碼以下所示:

// 獲取inBitmap,實現內存複用
public Bitmap getBitmapFromReusableSet(BitmapFactory.Options options) {
    Bitmap bitmap = null;

    if (mReusableBitmaps != null && !mReusableBitmaps.isEmpty()) {
        final Iterator<SoftReference<Bitmap>> iterator = mReusableBitmaps.iterator();
        Bitmap item;

        while (iterator.hasNext()) {
            item = iterator.next().get();

            if (null != item && item.isMutable()) {
                if (canUseForInBitmap(item, options)) {

                    Log.v("TEST", "canUseForInBitmap!!!!");

                    bitmap = item;

                    // Remove from reusable set so it can't be used again
                    iterator.remove();
                    break;
                }
            } else {
                // Remove from the set if the reference has been cleared.
                iterator.remove();
            }
        }
    }

    return bitmap;
}
複製代碼

上述方法從軟引用集合中查找規格可利用的Bitamp做爲內存複用對象,由於使用inBitmap有一些限制,在Android 4.4以前,只支持同等大小的位圖。所以使用了canUseForInBitmap方法來判斷該Bitmap是否能夠複用,代碼以下所示:

@TargetApi(Build.VERSION_CODES.KITKAT)
private static boolean canUseForInBitmap(
        Bitmap candidate, BitmapFactory.Options targetOptions) {

    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
        return candidate.getWidth() == targetOptions.outWidth
                && candidate.getHeight() == targetOptions.outHeight
                && targetOptions.inSampleSize == 1;
    }
    int width = targetOptions.outWidth / targetOptions.inSampleSize;
    int height = targetOptions.outHeight / targetOptions.inSampleSize;

    int byteCount = width * height * getBytesPerPixel(candidate.getConfig());

    return byteCount <= candidate.getAllocationByteCount();
}
複製代碼

三、磁盤緩存

因爲磁盤讀取時間是不可預知的,因此圖片的解碼和文件讀取都應該在後臺進程中完成。DisLruCache是Android提供的一個管理磁盤緩存的類。

一、首先調用DiskLruCache的open方法進行初始化,代碼以下:

public static DiskLruCache open(File directory, int appVersion, int valueCou9nt, long maxSize)
複製代碼

directory通常建議緩存到SD卡上。appVersion發生變化時,會自動刪除前一個版本的數據。valueCount是指Key與Value的對應關係,通常狀況下是1對1的關係。maxSize是緩存圖片的最大緩存數據大小。初始化DiskLruCache的代碼以下所示:

private void init(final long cacheSize,final File cacheFile) {
    new Thread(new Runnable() {
        @Override
        public void run() {
            synchronized (mDiskCacheLock) {
                if(!cacheFile.exists()){
                    cacheFile.mkdir();
                }
                MLog.d(TAG,"Init DiskLruCache cache path:" + cacheFile.getPath() + "\r\n" + "Disk Size:" + cacheSize);
                try {
                    mDiskLruCache = DiskLruCache.open(cacheFile, MiniImageLoaderConfig.VESION_IMAGELOADER, 1, cacheSize);
                    mDiskCacheStarting = false;
                    // Finished initialization
                    mDiskCacheLock.notifyAll(); 
                    // Wake any waiting threads
                }catch(IOException e){
                    MLog.e(TAG,"Init err:" + e.getMessage());
                }
            }
        }
    }).start();
}
複製代碼

若是在初始化前就要操做寫或者讀會致使失敗,因此在整個DiskCache中使用的Object的wait/notifyAll機制來避免同步問題。

二、寫入DiskLruCache

首先,獲取Editor實例,它須要傳入一個key來獲取參數,Key必須與圖片有惟一對應關係,但因爲URL中的字符可能會帶來文件名不支持的字符類型,因此取URL的MD4值做爲文件名,實現Key與圖片的對應關係,經過URL獲取MD5值的代碼以下所示:

private String hashKeyForDisk(String key) {
    String cacheKey;
    try {
        final MessageDigest mDigest = MessageDigest.getInstance("MD5");
        mDigest.update(key.getBytes());
        cacheKey = bytesToHexString(mDigest.digest());
    } catch (NoSuchAlgorithmException e) {
        cacheKey = String.valueOf(key.hashCode());
    }
    return cacheKey;
}
private String bytesToHexString(byte[] bytes) {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < bytes.length; i++) {
        String hex = Integer.toHexString(0xFF & bytes[i]);
        if (hex.length() == 1) {
            sb.append('0');
        }
        sb.append(hex);
    }
    return sb.toString();
}
複製代碼

而後,寫入須要保存的圖片數據,圖片數據寫入本地緩存的總體代碼以下所示:

public void saveToDisk(String imageUrl, InputStream in) {
    // add to disk cache
    synchronized (mDiskCacheLock) {
        try {
            while (mDiskCacheStarting) {
                try {
                    mDiskCacheLock.wait();
                } catch (InterruptedException e) {}
            }
            String key = hashKeyForDisk(imageUrl);
            MLog.d(TAG,"saveToDisk get key:" + key);
            DiskLruCache.Editor editor = mDiskLruCache.edit(key);
            if (in != null && editor != null) {
                // 當 valueCount指定爲1時,index傳0便可
                OutputStream outputStream = editor.newOutputStream(0);
                MLog.d(TAG, "saveToDisk");
                if (FileUtil.copyStream(in,outputStream)) {
                    MLog.d(TAG, "saveToDisk commit start");
                    editor.commit();
                    MLog.d(TAG, "saveToDisk commit over");
                } else {
                    editor.abort();
                    MLog.e(TAG, "saveToDisk commit abort");
                }
            }
            mDiskLruCache.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}
複製代碼

接着,讀取圖片緩存,經過DiskLruCache的get方法實現,代碼以下所示:

public Bitmap  getBitmapFromDiskCache(String imageUrl,BitmapConfig bitmapconfig) {
    synchronized (mDiskCacheLock) {
        // Wait while disk cache is started from background thread
        while (mDiskCacheStarting) {
            try {
                mDiskCacheLock.wait();
            } catch (InterruptedException e) {}
        }
        if (mDiskLruCache != null) {
            try {

                String key = hashKeyForDisk(imageUrl);
                MLog.d(TAG,"getBitmapFromDiskCache get key:" + key);
                DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
                if(null == snapShot){
                    return null;
                }
                InputStream is = snapShot.getInputStream(0);
                if(is != null){
                    final BitmapFactory.Options options = bitmapconfig.getBitmapOptions();
                    return BitmapUtil.decodeSampledBitmapFromStream(is, options);
                }else{
                    MLog.e(TAG,"is not exist");
                }
            }catch (IOException e){
                MLog.e(TAG,"getBitmapFromDiskCache ERROR");
            }
        }
    }
    return null;
}
複製代碼

最後,要注意讀取並解碼Bitmap數據和保存圖片數據都是有必定耗時的IO操做。因此這些方法都是在ImageLoader中的doInBackground方法中調用,代碼以下所示:

@Override
protected Bitmap doInBackground(Void... params) {
   
    Bitmap bitmap = null;
    synchronized (mPauseWorkLock) {
        while (mPauseWork && !isCancelled()) {
            try {
                mPauseWorkLock.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    if (bitmap == null && !isCancelled()
            && imageViewReference.get() != null && !mExitTasksEarly) {
        bitmap = getmImageCache().getBitmapFromDisk(mUrl, mBitmapConfig);
    }

    if (bitmap == null && !isCancelled()
            && imageViewReference.get() != null && !mExitTasksEarly) {
        bitmap = downLoadBitmap(mUrl, mBitmapConfig);
    }
    if (bitmap != null) {
        getmImageCache().addToCache(mUrl, bitmap);
    }

    return bitmap;
}
複製代碼

三、圖片加載三方庫

目前使用最普遍的有Picasso、Glide和Fresco。Glide和Picasso比較類似,可是Glide相對於Picasso來講,功能更豐富,內部實現更復雜,對Glide有興趣的同窗能夠閱讀這篇文章Android主流三方庫源碼分析(3、深刻理解Glide源碼)。Fresco最大的亮點在於它的內存管理,特別是在低端機和Android 5.0如下的機器上的優點更加明顯,而使用Fresco將很好地解決圖片佔用內存大的問題。由於,Fresco會將圖片放到一個特別的內存區域,當圖片再也不顯示時,佔用的內存會自動釋放。這類總結下Fresco的優勢,以下所示:

  • 一、內存管理
  • 二、漸進式呈現:先呈現大體的圖片輪廓,而後隨着圖片下載的繼續,呈現逐漸清晰的圖片。
  • 三、支持更多的圖片格式:如Gif和Webp。
  • 四、圖像加載策略豐富:其中的Image Pipeline能夠爲同一個圖片指定不一樣的遠程路徑,好比先顯示已經存在本地緩存中的圖片,等高清圖下載完成以後在顯示高清圖集。

缺點

安裝包過大,因此對圖片加載和顯示要求不是比較高的狀況下建議使用Glide。

6、總結

對於內存優化,通常都是經過使用MAT等工具來進行檢查和使用LeakCanary等內存泄漏監控工具來進行監控,以此來發現問題,再分析問題緣由,解決發現的問題或者對當前的實現邏輯進行優化優化完後再進行檢查,直到達到預約的性能指標。下一篇文章,將會和你們一塊兒來深刻探索Android的內存優化,盡請期待~

參考連接:

一、Android應用性能優化最佳實踐

二、必知必會 | Android 性能優化的方面方面都在這兒

三、實踐App內存優化:如何有序地作內存分析與優化

Contanct Me

● 微信:

歡迎關注個人微信:bcce5360

● 微信羣:

微信羣若是不能掃碼加入,麻煩你們想進微信羣的朋友們,加我微信拉你進羣。

● QQ羣:

2千人QQ羣,Awesome-Android學習交流羣,QQ羣號:959936182, 歡迎你們加入~

About me

很感謝您閱讀這篇文章,但願您能將它分享給您的朋友或技術羣,這對我意義重大。

但願咱們能成爲朋友,在 Github掘金上一塊兒分享知識。

相關文章
相關標籤/搜索