內存優化可以說是性能優化中最重要的優化點之一,可以說,如果你沒有掌握系統的內存優化方案,就不能說你對Android的性能優化有過多的研究與探索。本篇,筆者將帶領大家一起來系統地學習Android中的內存優化。

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

一、Android內存管理機制

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

1.1、Java對象生命週期

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

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

1、Created(創建)

Java對象的創建分爲如下幾步:

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

2、InUse(應用)

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

3、Invisible(不可見)

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

4、Unreachable(不可達)

該對象不再被任何強引用持有。

5、Collected(收集)

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

6、Finalized(終結)

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

7、Deallocated(對象空間重新分配)

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

注意:

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

1.2 內存分配

在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的對象需要在系統每次啓動時,根據運行情況都重新創建一遍。

1.3、內存回收機制

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

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

模型示意圖如下所示:

1、Young Generation

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

2、Old Generation

一般情況下,年老代中的對象生命週期都比較長。

3、Permanent Generation

用於存放靜態的類和方法,持久代對垃圾回收沒有顯著影響。

總結:內存對象的處理過程如下:

  • 1、對象創建後在Eden區。
  • 2、執行GC後,如果對象仍然存活,則複製到S0區。
  • 3、當S0區滿時,該區域存活對象將複製到S1區,然後S0清空,接下來S0和S1角色互換。
  • 4、當第3步達到一定次數(系統版本不同會有差異)後,存活對象將被複制到Old Generation。
  • 5、當這個對象在Old Generation區域停留的時間達到一定程度時,它會被移動到Old Generation,最後累積一定時間再移動到Permanent Generation區域。

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

執行GC佔用的時間與Generation和Generation中的對象數量有關:

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

4、Young Generation GC

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

5、Old Generation GC

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

1.4、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內存佔用上升時,避免Heap內存滿了而觸發的GC。
  • 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/6261K 表明回收後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導致的卡頓問題。

二、優化內存的意義

  • 減少OOM,提高應用穩定性。
  • 減少卡頓,提高應用流暢度。
  • 減少內存佔用,提高應用後臺運行時的存活率。
  • 減少異常發生和代碼邏輯隱患。

注意:出現OOM是因爲內存溢出導致,這種情況不一定會發生在相同的代碼,也不一定是出現OOM的代碼使用內存有問題,而是剛好執行到這段代碼。

三、避免內存泄漏

3.1 內存泄漏定義

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

3.2 使用MAT來查找內存泄漏

MAT工具可以幫助開發者定位導致內存泄漏的對象,以及發現大的內存對象,然後解決內存泄漏並通過優化內存對象,達到減少內存消耗的目的。

3.2.1 使用步驟

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

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

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

./hprof-conv file.hprof converted.hprof

4、通過MAT打開轉換後的HPROF文件。

3.2.2 MAT視圖

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

1、Histogram

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

2、Dominator Tree

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

3、Top Consumers

通過圖像列出最大的Object。

4、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上總共能釋放掉的內存大小。

3.2.3 查找內存泄漏具體位置

常規方式:

  • 按照包名類型分類進行實例篩選或直接使用頂部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集合的碰撞率較高的罪魁禍首。

在本人平時的項目開發中,一般會使用如下兩種方式來快速對指定頁面進行內存泄漏的檢測:

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

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

3.3 常見內存泄漏場景

1、資源性對象未關閉

對於資源性對象不再使用時,應該立即調用它的close()函數,將其關閉,然後在置爲null。

2、註冊對象未註銷

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

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

該實例的生命週期和應用一樣長,這就導致該靜態實例一直持有該Activity的引用,Activity的內存資源不能正常回收。

解決方案:

將內部類設爲靜態內部類或將內部類抽取來作爲一個單例,如果需要使用Context,儘量使用Application Context,如果需要使用Activity Context,就記得用完後置空讓GC可以回收,否則還是會內存泄漏。

5、Handler臨時性內存泄漏

Message發出之後存儲在MessageQueue中,在Message中存在一個target,它是Handler的一個引用,Message在Queue中存在的時間過長,就會導致Handler無法被回收。如果Handler是非靜態的,則會導致Activity或者Service不會被回收。並且消息隊列是在一個Looper線程中不斷地輪詢處理消息,當這個Activity退出時,消息隊列中還有未處理的消息或者正在處理的消息,並且消息隊列中的Message持有Handler實例的引用,Handler又持有Activity的引用,所以導致該Activity的內存資源無法及時回收,引發內存泄漏。

解決方案:

  • 1、使用一個靜態Handler內部類,然後對Handler持有的對象(一般是Activity)使用弱引用,這樣在回收時,也可以回收Handler持有的對象。
  • 2、在Activity的Destroy或者Stop時,應該移除消息隊列中的消息,避免Looper線程的消息隊列中有待處理的消息需要處理。

注意:AsyncTask內部也是Handler機制,同樣存在內存泄漏風險,當其一般是臨時性的。

6、容器中的對象沒清理造成的內存泄漏

7、WebView

WebView都存在內存泄漏的問題,在應用中只要使用一次WebView,內存就不會被釋放掉。

解決方案:

爲WebView開啓一個獨立的進程,使用AIDL與應用的主進程進行通信,WebView所在的進程可以根據業務的需要選擇合適的時機進行銷燬,達到正常釋放內存的目的。

3.4 內存泄漏監控

一般使用LeakCanary進行內存泄漏的監控即可,具體使用和原理分析請參見我之前的文章 Android主流三方庫源碼分析(六、深入理解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.1 對象引用

從Java 1.2版本開始引入了三種對象引用方式:SoftReference、WeakReference和PhantomReference三個引用類,引用類的主要功能就是能夠引用但仍可以被垃圾回收器回收的對象。在引入引用類之前,只能使用Strong Reference,如果沒有指定對象引用類型,默認是強引用。

1、強引用

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

2、軟引用

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

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

3、弱引用

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

注意:可能需要運行多次GC,才能找到並釋放弱引用對象。

4、虛引用

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

4.2 減少不必要的內存開銷

1、AutoBoxing

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

檢測方式:使用TraceView查看耗時,如果發現調用了大量的integer.value,就說明發生了AutoBoxing。

2、內存複用

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

4.3 使用最優的數據類型的

1、HashMap與ArrayMap

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

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

總體來說,在ArrayMap中執行插入或者刪除操作時,從性能角度上看,比HashMap還要更差一些,但如果只涉及很小的對象數,比如1000以下,就不需要擔心這個問題了。因爲此時ArrayMap不會分配過大的數組。

2、枚舉類型

使用枚舉類型的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'

3、LruCache

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

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

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

4、圖片內存優化

在Android默認情況下,當圖片文件解碼成位圖時,會被處理成32bit/像素。紅色、綠色、藍色和透明通道各8bit,即使是沒有透明通道的圖片,如JEPG隔世是沒有透明通道的,但然後會處理成32bit位圖,這樣分配的32bit中的8bit透明通道數據是沒有任何用處的,這完全沒有必要,並且在這些圖片被屏幕渲染之前,它們首先要被作爲紋理傳送到GPU,這意味着每一張圖片會同時佔用CPU內存和GPU內存。

減少內存開銷常用方式如下:

  • 1、設置位圖的規格:當顯示小圖片或對圖片質量要求不高時可以考慮使用RGB_565,用戶頭像或圓角圖片一般可以嘗試ARGB_4444。通過設置inPreferredConfig參數來實現不同的位圖規格,代碼如下所示:
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.RGB_565;
BitmapFactory.decodeStream(is, null, options);
  • 2、inSampleSize:位圖功能對象中的inSampleSize屬性實現了位圖的縮放功能,代碼如下所示:
BitampFactory.Options options = new BitmapFactory.Options();
// 設置爲4就是1/4大小的圖片。因此,圖片大小總會比原始圖片小一倍以上。
options.inSampleSize = 4;
BitmapFactory.decodeSream(is, null, options);
  • 3、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);

4、inBitmap

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

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

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

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

  • 1、單一職責
  • 2、避免不同功能之間的耦合
  • 3、接口隔離

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

5.1 實現異步加載功能

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.整體內存開銷不可控,雖然減少了單個圖片開銷,但是在片非常多的情況下,沒有合理管理機制仍然對性能有嚴重影的。

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

5.2 實現三級緩存

內存–本地–網絡

1、內存緩存

使用軟引用和弱引用(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中。

2、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();
}

3、磁盤緩存

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

  • 1、首先調用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機制來避免同步問題。

  • 2、寫入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;
}

5.3 圖片加載三方庫

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

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

缺點:

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

六、內存優化

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

6.1、常見的內存泄漏案例(完善3.3小節):

  • 1、單例造成的內存泄漏(使用Application的Context)
  • 2、非靜態內部類創建靜態實例造成的內存泄漏(將該內部類設爲靜態內部類或將該內部類抽取出來封裝成一個單例,如果需要使用Context,就使用Application的Context)
  • 3、Handler造成的內存泄漏(將Handler類獨立出來或者使用靜態內部類)
  • 4、線程造成的內存泄漏(將AsyncTask和Runnable類獨立出來或者使用靜態內部類)
  • 5、BraodcastReceiver、Bitmap等資源未關閉造成的內存泄漏(應該在Activity銷燬時及時關閉或者註銷)
  • 6、使用ListView時造成的內存泄漏(在構造Adapter時,使用緩存的convertView)
  • 7、集合容器中的內存泄露(在退出程序之前,將集合裏的東西clear,然後置爲null,再退出程序)
  • 8、WebView造成的泄露(爲WebView另外開啓一個進程,通過AIDL與主線程進行通信,WebView所在的進程可以根據業務的需要選擇合適的時機進行銷燬,從而達到內存的完整釋放)

6.2、常見的內存優化點:

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

2、圖片優化:

  • 顏色模式:RGB_8888->RGB_565
  • 降低圖片大小
  • 降低採樣率

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

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

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

5、集合優化:Android提供了一系列優化過後的數據集合工具類,如SparseArray、SparseBooleanArray、LongSparseArray,使用這些API可以讓我們的程序更加高效。HashMap工具類會相對比較低效,因爲它需要爲每一個鍵值對都提供一個對象入口,而SparseArray就避免掉了基本數據類型轉換成對象數據類型的時間。

6、避免創作不必要的對象:字符串拼接使用StringBuffer,StringBuilder。

7、onDraw方法裏面不要執行對象的創建。

8、使用static final 優化成員變量。

9、使用增強型for循環語法。

10、在沒有特殊原因的情況下,儘量使用基本數據類型來代替封裝數據類型,int比Integer要更加有效,其它數據類型也是一樣。

11、適當採用軟引用和弱引用。

12、採用內存緩存和磁盤緩存。

13、儘量採用靜態內部類,可避免潛在由於內部類導致的內存泄漏。

七、總結

對於內存優化,一般都是通過使用MAT等工具來進行檢查和使用LeakCanary等內存泄漏監控工具來進行監控,以此來發現問題,再分析問題原因,解決發現的問題或者對當前的實現邏輯進行優化,優化完後在進行檢查,直到達到預定的性能指標。下一篇,將會深入分析一下Android系統的存儲優化相關技術,盡請期待~

相關文章