Tips:本篇是《深刻探索Android內存優化》的基礎篇,若是沒有掌握Android內存優化的同窗建議系統學習一遍。
複製代碼
衆所周知,內存優化能夠說是性能優化中最重要的優化點之一,能夠說,若是你沒有掌握系統的內存優化方案,就不能說你對Android的性能優化有過多的研究與探索。本篇,筆者將帶領你們一塊兒來系統地學習Android中的內存優化。php
可能有很多讀者都知道,在內存管理上,JVM擁有垃圾內存回收的機制,自身會在虛擬機層面自動分配和釋放內存,所以不須要像使用C/C++同樣在代碼中分配和釋放某一塊內存。Android系統的內存管理相似於JVM,經過new關鍵字來爲對象分配內存,內存的釋放由GC來回收。而且Android系統在內存管理上有一個 Generational Heap Memory模型,當內存達到某一個閾值時,系統會根據不一樣的規則自動釋放能夠釋放的內存。即使有了內存管理機制,可是,若是不合理地使用內存,也會形成一系列的性能問題,好比 內存泄漏、內存抖動、短期內分配大量的內存對象 等等。下面,我就先來談談Android的內存管理機制。android
咱們都知道,應用程序的內存分配和垃圾回收都是由Android虛擬機完成的,在Android 5.0如下,使用的是Dalvik虛擬機,5.0及以上,則使用的是ART虛擬機。git
Java代碼編譯後生成的字節碼.class文件從從文件系統中加載到虛擬機以後,便有了JVM上的Java對象,Java對象在JVM上運行有7個階段,以下:github
Java對象的建立分爲以下幾步:正則表達式
此時對象至少被一個強引用持有。算法
當一個對象處於不可見階段時,說明程序自己再也不持有該對象的任何強引用,雖然該對象仍然是存在的。簡單的例子就是程序的執行已經超出了該對象的做用域了。可是,該對象仍可能被虛擬機下的某些已裝載的靜態變量線程或JNI等強引用持有,這些特殊的強引用稱爲「GC Root」。被這些GC Root強引用的對象會致使該對象的內存泄漏,於是沒法被GC回收。shell
該對象再也不被任何強引用持有。數據庫
當GC已經對該對象的內存空間從新分配作好準備時,對象進入收集階段,若是該對象重寫了finalize()方法,則執行它。json
等待垃圾回收器回收該對象空間。數組
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,它們分配的內存區域塊是不一樣的,下面咱們就來簡單瞭解下。
無論是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的對象只建立一次,而Zygote Space的對象須要在系統每次啓動時,根據運行狀況都從新建立一遍。
在Android的高級系統版本中,針對Heap空間有一個Generational Heap Memory的模型,其中將整個內存分爲三個區域:
模型示意圖以下所示:
由一個Eden區和兩個Survivor區組成,程序中生成的大部分新的對象都在Eden區中,當Eden區滿時,還存活的對象將被複制到其中一個Survivor區,當此Survivor區滿時,此區存活的對象又被複制到另外一個Survivor區,當這個Survivor區也滿時,會將其中存活的對象複製到年老代。
通常狀況下,年老代中的對象生命週期都比較長。
用於存放靜態的類和方法,持久代對垃圾回收沒有顯著影響。(在 JDK 1.8 及以後的版本,在本地內存中實現的元空間(Meta-space)已經代替了永久代)
系統在Young Generation、Old Generation上採用不一樣的回收機制。每個Generation的內存區域都有固定的大小。隨着新的對象陸續被分配到此區域,當對象總的大小臨近這一級別內存區域的閾值時,會觸發GC操做,以便騰出空間來存放其餘新的對象。
此外,執行GC佔用的時間與Generation和Generation中的對象數量有關,以下所示:
因爲其對象存活時間短,所以基於Copying算法(掃描出存活的對象,並複製到一塊新的徹底未使用的控件中)來回收。新生代採用空閒指針的方式來控制GC觸發,指針保持最後一個分配的對象在Young Generation區間的位置,當有新的對象要分配內存時,用於檢查空間是否足夠,不夠就觸發GC。
因爲其對象存活時間較長,比較穩定,所以採用Mark(標記)算法(掃描出存活的對象,而後再回收未被標記的對象,回收後對空出的空間要麼合併,要麼標記出來便於下次分配,以減小內存碎片帶來的效率損耗)來回收。
在Android系統中,GC有三種類型:
接下來,咱們來學會如何分析Android虛擬機中的GC日誌,日誌以下:
D/dalvikvm(7030):GC_CONCURRENT freed 1049K, 60% free 2341K/9351K, external 3502K/6261K, paused 3ms 3ms
複製代碼
GC_CONCURRENT 是當前GC時的類型,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的代碼使用內存有問題,而是恰好執行到這段代碼。
Android系統虛擬機的垃圾回收是經過虛擬機GC機制來實現的。GC會選擇一些還存活的對象做爲內存遍歷的根節點GC Roots,經過對GC Roots的可達性來判斷是否須要回收。內存泄漏就是在當前應用週期內再也不使用的對象被GC Roots引用,致使不能回收,使實際可以使用內存變小。
MAT工具能夠幫助開發者定位致使內存泄漏的對象,以及發現大的內存對象,而後解決內存泄漏並經過優化內存對象,以達到減小內存消耗的目的。
./hprof-conv file.hprof converted.hprof
複製代碼
在MAT窗口上,OverView是一個整體概覽,顯示整體的內存消耗狀況和疑似問題。MAT提供了多種分析維度,其中Histogram、Dominator Tree、Top Consumers和Leak Suspects的分析維度是不一樣的。下面分別介紹下它們,以下所示:
列出內存中的全部實例類型對象和其個數以及大小,並在頂部的regex區域支持正則表達式查找。
列出最大的對象及其依賴存活的Object。相比Histogram,能更方便地看出引用關係。
經過圖像列出最大的Object。
經過MAT自動分析內存泄漏的緣由和泄漏的一份整體報告。
分析內存最經常使用的是Histogram和Dominator Tree這兩個視圖,視圖中一共有四列:
還有一種更快速的方法就是對比泄漏先後的HPROF數據:
須要注意的是,若是目標不太明確,能夠直接定位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能夠回收,不然仍是會內存泄漏。
Message發出以後存儲在MessageQueue中,在Message中存在一個target,它是Handler的一個引用,Message在Queue中存在的時間過長,就會致使Handler沒法被回收。若是Handler是非靜態的,則會致使Activity或者Service不會被回收。而且消息隊列是在一個Looper線程中不斷地輪詢處理消息,當這個Activity退出時,消息隊列中還有未處理的消息或者正在處理的消息,而且消息隊列中的Message持有Handler實例的引用,Handler又持有Activity的引用,因此致使該Activity的內存資源沒法及時回收,引起內存泄漏。解決方案以下所示:
須要注意的是,AsyncTask內部也是Handler機制,一樣存在內存泄漏風險,但其通常是臨時性的。對於相似AsyncTask或是線程形成的內存泄漏,咱們也能夠將AsyncTask和Runnable類獨立出來或者使用靜態內部類。
在退出程序以前,將集合裏的東西clear,而後置爲null,再退出程序
WebView都存在內存泄漏的問題,在應用中只要使用一次WebView,內存就不會被釋放掉。咱們能夠爲WebView開啓一個獨立的進程,使用AIDL與應用的主進程進行通訊,WebView所在的進程能夠根據業務的須要選擇合適的時機進行銷燬,達到正常釋放內存的目的。
在構造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 方法,在其中處理須要的數據,三個參數的定義以下:
而後在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中註冊。
從Java 1.2版本開始引入了三種對象引用方式:SoftReference、WeakReference 和 PhantomReference 三個引用類,引用類的主要功能就是可以引用但仍能夠被垃圾回收器回收的對象。在引入引用類以前,只能使用Strong Reference,若是沒有指定對象引用類型,默認是強引用。下面,咱們就分別來介紹下這幾種引用。
若是一個對象具備強引用,GC就絕對不會回收它。當內存空間不足時,JVM會拋出OOM錯誤。
若是一個對象只具備軟引用,則內存空間足夠,GC時就不會回收它;若是內存不足,就會回收這些對象的內存。可用來實現內存敏感的高速緩存。
軟引用能夠和一個ReferenceQueue(引用隊列)聯合使用,若是軟引用引用的對象被垃圾回收器回收,JVM會把這個軟引用加入與之關聯的引用隊列中。
在垃圾回收器線程掃描它所管轄的內存區域的過程當中,一旦發現了只具備弱引用的對象,無論當前內存空間是否足夠,都會回收它的內存。不過,因爲垃圾回收器是一個優先級很低的線程,所以不必定會很快發現那些只具備弱引用的對象。
這裏要注意,可能須要運行屢次GC,才能找到並釋放弱引用對象。
只能用於跟蹤即將對被引用對象進行的收集。虛擬機必須與ReferenceQueue類聯合使用。由於它可以充當通知機制。
自動裝箱的核心就是把基礎數據類型轉換成對應的複雜類型。在自動裝箱轉化時,都會產生一個新的對象,這樣就會產生更多的內存和性能開銷。如int只佔4字節,而Integer對象有16字節,特別是HashMap這類容器,進行增、刪、改、查操做時,都會產生大量的自動裝箱操做。
使用TraceView查看耗時,若是發現調用了大量的integer.value,就說明發生了AutoBoxing。
對於內存複用,有以下四種可行的方式:
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 就 避免 掉了 基本數據類型轉換成對象數據類型的時間。
使用枚舉類型的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'
複製代碼
最近最少使用緩存,使用強引用保存須要緩存的對象,它內部維護了一個由LinkedHashMap組成的雙向列表,不支持線程安全,LruCache對它進行了封裝,添加了線程安全操做。當其中的一個值被訪問時,它被放到隊列的尾部,當緩存將滿時,隊列頭部的值(最近最少被訪問的)被丟棄,以後能夠被GC回收。
除了普通的get/set方法以外,還有sizeOf方法,它用來返回每一個緩存對象的大小。此外,還有entryRemoved方法,當一個緩存對象被丟棄時調用的方法,當第一個參數爲true:代表環處對象是爲了騰出空間而被清理時。不然,代表緩存對象的entry被remove移除或者被put覆蓋時。
分配LruCache大小時應考慮應用剩餘內存有多大。
在Android默認狀況下,當圖片文件解碼成位圖時,會被處理成32bit/像素。紅色、綠色、藍色和透明通道各8bit,即便是沒有透明通道的圖片,如JEPG隔世是沒有透明通道的,但而後會處理成32bit位圖,這樣分配的32bit中的8bit透明通道數據是沒有任何用處的,這徹底沒有必要,而且在這些圖片被屏幕渲染以前,它們首先要被做爲紋理傳送到GPU,這意味着每一張圖片會同時佔用CPU內存和GPU內存。下面,我總結了減小內存開銷的幾種經常使用方式,以下所示:
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.RGB_565;
BitmapFactory.decodeStream(is, null, options);
複製代碼
BitampFactory.Options options = new BitmapFactory.Options();
// 設置爲4就是寬和高都變爲原來1/4大小的圖片
options.inSampleSize = 4;
BitmapFactory.decodeSream(is, null, options);
複製代碼
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);
複製代碼
能夠結合LruCache來實現,在LruCache移除超出cache size的圖片時,暫時緩存Bitamp到一個軟引用集合,須要建立新的Bitamp時,能夠從這個軟用用集合中找到最適合重用的Bitmap,來重用它的內存區域。
須要注意,新申請的Bitmap與舊的Bitmap必須有相同的解碼格式,而且在Android 4.4以前,只能重用相同大小的Bitamp的內存區域,而Android 4.4以後能夠重用任何bitmap的內存區域。
只須要UI提供一套高分辨率的圖,圖片建議放在drawable-xxhdpi文件夾下,這樣在低分辨率設備中圖片的大小隻是壓縮,不會存在內存增大的狀況。如若遇到不需縮放的文件,放在drawable-nodpi文件夾下。
在App退到後臺內存緊張即將被Kill掉時選擇重寫 onTrimMemory/onLowMemory 方法去釋放掉圖片緩存、靜態緩存來自保。
例如,咱們能夠在字符串拼接的時候使用StringBuffer,StringBuilder。
例如,在onDraw方法裏面不要執行對象的建立,通常來講,都應該在自定義View的構造器中建立對象。
除了上面的一些內存優化點以外,這裏還有一些內存優化的點咱們須要注意,以下所示:
在設計一個模塊時,須要考慮如下幾點:
在編寫代碼前先畫好UML圖,肯定每個對象、方法、接口的功能,首先儘可能作到功能單一原則,在這個基礎上,再明確模塊與模塊的直接關係,最後使用代碼實現。
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);
}
}
複製代碼
這裏使用一個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);
複製代碼
優化後仍存在一些問題:
爲了解決這兩個問題,就須要有內存池的設計理念,經過內存池控制總體圖片內存,不從新加載和解碼已經顯示過的圖片。
內存--本地--網絡
使用軟引用和弱引用(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百分比佔比多少合適?能夠基於如下幾點來考慮:
在應用中,若是有一些圖片的訪問頻率要比其它的大一些,或者必須一直顯示出來,就須要一直保持在內存中,這種狀況可使用多個LruCache對象來管理多組Bitmap,對Bitmap進行分級,不一樣級別的Bitmap放到不一樣的LruCache中。
從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提供的一個管理磁盤緩存的類。
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機制來避免同步問題。
首先,獲取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的優勢,以下所示:
安裝包過大,因此對圖片加載和顯示要求不是比較高的狀況下建議使用Glide。
對於內存優化,通常都是經過使用MAT等工具來進行檢查和使用LeakCanary等內存泄漏監控工具來進行監控,以此來發現問題,再分析問題緣由,解決發現的問題或者對當前的實現邏輯進行優化,優化完後再進行檢查,直到達到預約的性能指標。下一篇文章,將會和你們一塊兒來深刻探索Android的內存優化,盡請期待~
一、Android應用性能優化最佳實踐
二、必知必會 | Android 性能優化的方面方面都在這兒
歡迎關注個人微信:
bcce5360
微信羣若是不能掃碼加入,麻煩你們想進微信羣的朋友們,加我微信拉你進羣。
2千人QQ羣,Awesome-Android學習交流羣,QQ羣號:959936182, 歡迎你們加入~