高頻面試點:Android性能優化之內存優化(下篇)

碼個蛋(codeegg) 第 939 次推文

鏈接:https://juejin.im/post/5e72b2d151882549236f9cb8

上篇:高頻面試點:Android性能優化之內存優化(上篇)

下篇來了~

一、優化內存空間

1、對象引用

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

a. 強引用

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

b. 軟引用

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

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

c. 弱引用

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

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

d. 虛引用

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

2、減少不必要的內存開銷

1、AutoBoxing

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

檢測方式

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

2、內存複用

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

  • 資源複用:通用的字符串、顏色定義、簡單頁面佈局的複用。

  • 視圖複用:可以使用ViewHolder實現ConvertView複用。

  • 對象池:顯示創建對象池,實現複用邏輯,對相同的類型數據使用同一塊內存空間。

  • Bitmap對象的複用:使用inBitmap屬性可以告知Bitmap解碼器嘗試使用已經存在的內存區域,新解碼的bitmap會嘗試使用之前那張bitmap在heap中佔據的pixel data內存區域。

3、使用最優的數據類型

1、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避免掉了基本數據類型轉換成對象數據類型的時間

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

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

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

注意

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

<code>

compile

'com.android.support:support-annotations:22.0.0'

/<code>

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參數來實現不同的位圖規格,代碼如下所示:


<code>BitmapFactory.Options options = 

new

BitmapFactory.Options;/<code><code>options.inPreferredConfig = Bitmap.Config.RGB_565;/<code><code>BitmapFactory.decodeStream(

is

, , options);/<code>

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

<code>BitampFactory.Options options = 

new

BitmapFactory.Options;/<code><code>

//

設置為

4

就是寬和高都變為原來

1

/

4

大小的圖片/<code><code>options.inSampleSize =

4

;/<code><code>BitmapFactory.decodeSream(

is

, , options);/<code>

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

<code>BitampFactory.Options options = 

new

BitampFactory.Options;/<code><code>options.inScaled =

true

;/<code><code>options.inDensity = srcWidth;/<code><code>options.inTargetDensity = dstWidth;/<code><code>BitmapFactory.decodeStream(

is

, , options);/<code>

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


<code>

BitmapFactory.Options

options

=

new

BitampFactory.Options;

/<code><code>

options.inJustDecodeBounds

=

true

;

/<code><code>

BitmapFactory.decodeStream(is,

,

options);

/<code><code>

options.inScaled

=

true

;

/<code><code>

options.inDensity

=

options.outWidth;

/<code><code>

options.inSampleSize

=

4

;

/<code><code>

Options.inTargetDensity

=

desWith

*

options.inSampleSize;

/<code><code>

options.inJustDecodeBounds

=

false

;

/<code><code>

BitmapFactory.decodeStream(is,

,

options);

/<code>

5、inBitmap

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

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

6、圖片放置優化

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

7、在App可用內存過低時主動釋放內存

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

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

  • ListView:因此每次item被回收後再次利用都會重新綁定數據,只需在ImageView onDetachFromWindow的時候釋放掉圖片引用即可。

  • RecyclerView:因為被回收不可見時第一選擇是放進mCacheView中,這裡item被複用並不會只需bindViewHolder來重新綁定數據,只有被回收進mRecyclePool中後拿出來複用才會重新綁定數據,因此重寫Recycler.Adapter中的onViewRecycled方法來使item被回收進RecyclePool的時候去釋放圖片引用。

9、避免創作不必要的對象

例如,我們可以在字符串拼接的時候使用StringBuffer,StringBuilder。

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

例如,在onDraw方法裡面不要執行對象的創建,一般來說,都應該在自定義View的構造器中創建對象。

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

除了上面的一些內存優化點之外,這裡還有一些內存優化的點我們需要注意,如下所示:

  • 盡使用static final 優化成員變量。

  • 使用增強型for循環語法。

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

  • 在合適的時候適當採用軟引用和弱引用。

  • 採用內存緩存和磁盤緩存。

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


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

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

  • 1、單一職責

  • 2、避免不同功能之間的耦合

  • 3、接口隔離

在編寫代碼前先畫好UML圖確定每一個對象、方法、接口的功能

,首先儘量做到功能單一原則,在這個基礎上,再明確模塊與模塊的直接關係,最後使用代碼實現。

1、實現異步加載功能

1. 實現網絡圖片顯示

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

<code>

public

abstract

class

ImageLoader

{/<code><code>

private

boolean

mExitTasksEarly =

false

; /<code><code><code>
<code>
<code>

<code><code><code><code>
<code><code><code><code><code><code><code><code>
<code>
<code><code>
<code><code><code><code>
<code><code><code><code>
<code><code><code><code><code><code><code><code><code>
<code><code><code><code><code><code><code><code>
<code><code><code><code><code>
<code><code><code><code><code>
<code><code><code><code><code><code><code><code>
<code><code><code><code><code><code><code><code>
<code><code><code><code>
<code><code><code>
<code><code>

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

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

<code>

public

class

MiniImageLoader

extends

ImageLoader

{/<code><code>

private

volatile

static

MiniImageLoader sMiniImageLoader = ;/<code><code>

private

ImageCache mImageCache = ;/<code><code>

public

static

MiniImageLoader getInstance {/<code><code>

if

( == sMiniImageLoader) {/<code><code>

synchronized

(MiniImageLoader

.

class

) {/<code><code> MiniImageLoader tmp = sMiniImageLoader;/<code><code>

if

(tmp == ) {/<code><code> tmp =

new

MiniImageLoader;/<code><code> }/<code><code> sMiniImageLoader = tmp;/<code><code> }/<code><code> }/<code><code>

return

sMiniImageLoader;/<code><code> }/<code><code>

public

MiniImageLoader {/<code><code> mImageCache =

new

ImageCache;/<code><code> }/<code><code> /<code><code>

protected

Bitmap

downLoadBitmap

(String mUrl)

{/<code><code> HttpURLConnection urlConnection = ;/<code><code> InputStream in = ;/<code><code>

try

{/<code><code>

final

URL url =

new

URL(mUrl);/<code><code> urlConnection = (HttpURLConnection) url.openConnection;/<code><code> in = urlConnection.getInputStream;/<code><code> Bitmap bitmap = decodeSampledBitmapFromStream(in, );/<code><code>

return

bitmap;/<code><code> }

catch

(MalformedURLException e) {/<code><code> e.printStackTrace;/<code><code> }

catch

(IOException e) {/<code><code> e.printStackTrace;/<code><code> }

finally

{/<code><code>

if

(urlConnection != ) {/<code><code> urlConnection.disconnect;/<code><code> urlConnection = ;/<code><code> }/<code><code>

if

(in != ) {/<code><code>

try

{/<code><code> in.close;/<code><code> }

catch

(IOException e) {/<code><code> e.printStackTrace;/<code><code> }/<code><code> }/<code><code> }/<code>
<code>

return

;/<code><code> }/<code><code>

public

Bitmap

decodeSampledBitmapFromStream

(InputStream is, BitmapFactory.Options options)

{/<code><code>

return

BitmapFactory.decodeStream(is, , options);/<code><code> }/<code><code>}/<code>

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

<code>

public

class

CloseUtils

{/<code>
<code>

public

static

void

closeQuietly

(Closeable closeable)

{/<code><code>

if

( != closeable) {/<code><code>

try

{/<code><code> closeable.close;/<code><code> }

catch

(IOException e) {/<code><code> e.printStackTrace;/<code><code> }/<code><code> }/<code><code> }/<code><code>}/<code>

改造後如下所示:

<code>

finally

{/<code><code>

if

(urlConnection != ) {/<code><code>

urlConnection

.disconnect

;/<code><code> }/<code><code>

CloseUtil

.closeQuietly

(in);/<code><code>}/<code>

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

<code>listView.setOnScrollListener(

new

AbsListView.OnScrollListener {/<code><code> /<code><code>

public

void

onScrollStateChanged

(AbsListView absListView,

int

scrollState) {/<code><code>

if

(scorllState == AbsListView.OnScrollListener.SCROLL_STAE_FLING) {/<code><code> MiniImageLoader.getInstance.setPauseWork(

true

);/<code><code> }

else

{/<code><code> MiniImageLoader.getInstance.setPauseWork(

false

);/<code><code> }/<code><code>}/<code>

2. 單個圖片內存優化

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

<code>

public

class

BitmapConfig

{/<code>
<code>

private

int

mWidth, mHeight;/<code><code>

private

Bitmap.Config mPreferred;/<code>
<code>

public

BitmapConfig

(

int

width,

int

height) {/<code><code>

this

.mWidth = width;/<code><code>

this

.mHeight = height;/<code><code>

this

.mPreferred = Bitmap.Config.RGB_565;/<code><code> }/<code>
<code>

public

BitmapConfig

(

int

width,

int

height, Bitmap.Config preferred) {/<code><code>

this

.mWidth = width;/<code><code>

this

.mHeight = height;/<code><code>

this

.mPreferred = preferred;/<code><code> }/<code>
<code>

public

BitmapFactory.Options getBitmapOptions {/<code><code>

return

getBitmapOptions;/<code><code> }/<code>
<code> /<code><code><code><code><code><code><code><code><code><code><code><code>
<code><code><code><code><code><code><code><code><code><code><code><code><code><code><code>

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

<code>

final

URL url =

new

URL(urlString);/<code><code>urlConnection = (HttpURLConnection) url.openConnection;/<code><code>in = urlConnection.getInputStream;/<code><code>

final

BitmapFactory.Options options = mConfig.getBitmapOptions(in);/<code><code>in.close;/<code><code>urlConnection.disconnect;/<code><code>urlConnection = (HttpURLConnection) url.openConnection;/<code><code>in = urlConnection.getInputStream;/<code><code>Bitmap bitmap = decodeSampledBitmapFromStream(in, options);/<code>

優化後仍存在一些問題:

  • 1.相同的圖片,每次都要重新加載;

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

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

2、實現三級緩存

內存--本地--網絡

a、內存緩存

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


<code>

public

class

MemoryCache

{/<code>
<code>

private

final

int

DEFAULT_MEM_CACHE_SIZE =

1024

*

12

;/<code><code>

private

LruCache mMemoryCache;/<code><code>

private

final

String TAG =

"MemoryCache"

;/<code><code>

public

MemoryCache

(

float

sizePer) {/<code><code> init(sizePer);/<code><code> }/<code>
<code>

private

void

init

(

float

sizePer) {/<code><code>

int

cacheSize = DEFAULT_MEM_CACHE_SIZE;/<code><code>

if

(sizePer >

0

) {/<code><code> cacheSize = Math.round(sizePer * Runtime.getRuntime.maxMemory /

1024

);/<code><code> }/<code>
<code> mMemoryCache =

new

LruCache(cacheSize) {/<code><code> /<code><code>

protected

int

sizeOf

(String key, Bitmap value)

{/<code><code>

final

int

bitmapSize = getBitmapSize(value) /

1024

;/<code><code>

return

bitmapSize ==

0

?

1

: bitmapSize;/<code><code> }/<code>
<code> /<code><code>

protected

void

entryRemoved

(

boolean

evicted, String key, Bitmap oldValue, Bitmap newValue) {/<code><code>

super

.entryRemoved(evicted, key, oldValue, newValue);/<code><code> }/<code><code> };/<code><code> }/<code>
<code> (Build.VERSION_CODES.KITKAT)/<code><code>

public

int

getBitmapSize

(Bitmap bitmap)

{/<code><code>

if

(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {/<code><code>

return

bitmap.getAllocationByteCount;/<code><code> }/<code><code>

if

(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) {/<code><code>

return

bitmap.getByteCount;/<code><code> }/<code>
<code>

return

bitmap.getRowBytes * bitmap.getHeight;/<code><code> }/<code>
<code>

public

Bitmap

getBitmap

(String url)

{/<code><code> Bitmap bitmap = ;/<code><code>

if

(mMemoryCache != ) {/<code><code> bitmap = mMemoryCache.get(url);/<code><code> }/<code><code>

if

(bitmap != ) {/<code><code> Log.d(TAG,

"Memory cache exiet"

);/<code><code> }/<code>
<code>

return

bitmap;/<code><code> }/<code>
<code>

public

void

addBitmapToCache

(String url, Bitmap bitmap)

{/<code><code>

if

(url == || bitmap == ) {/<code><code>

return

;/<code><code> }/<code>
<code> mMemoryCache.put(url, bitmap);/<code><code> }/<code>
<code>

public

void

clearCache {/<code><code>

if

(mMemoryCache != ) {/<code><code> mMemoryCache.evictAll;/<code><code> }/<code><code> }/<code><code>}/<code>

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

  • 1.應用中內存的佔用情況,除了圖片以外,是否還有大內存的數據需要緩存到內存。

  • 2.在應用中大部分情況要同時顯示多少張圖片,優先保證最大圖片的顯示數量的緩存支持。

  • 3.Bitmap的規格,計算出一張圖片佔用的內存大小。

  • 4.圖片訪問的頻率。

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

b、bitmap內存複用

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

<code>

if

(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {/<code><code>

mReusableBitmaps

= Collections.synchronizedSet(newHashSet>);/<code><code>}/<code>

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

<code>

if

(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {/<code><code> mReusableBitmaps.

add

(

new

SoftReference(oldValue));/<code><code>}/<code>

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

<code>

public

static

Bitmap

decodeSampledBitmapFromStream

(

InputStream

is

, BitmapFactory.Options options, ImageCache cache) {/<code><code>

if

(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {/<code><code> addInBitmapOptions(options, cache);/<code><code> }/<code><code>

return

BitmapFactory.decodeStream(

is

, , options);/<code><code> }/<code>
<code>@TargetApi(Build.VERSION_CODES.HONEYCOMB)/<code><code>

private

static

void

addInBitmapOptions

(

BitmapFactory.Options options, ImageCache cache

) {/<code><code> options.inMutable =

true

;/<code><code>

if

(cache != ) {/<code><code> Bitmap inBitmap = cache.getBitmapFromReusableSet(options);/<code><code>

if

(inBitmap != ) {/<code><code> options.inBitmap = inBitmap;/<code><code> }/<code><code> }/<code><code> }/<code>

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

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

if

(mReusableBitmaps != && !mReusableBitmaps.isEmpty) {/<code><code> final Iterator> iterator = mReusableBitmaps.iterator;/<code><code> Bitmap item;/<code>
<code>

while

(iterator.hasNext) {/<code><code> item = iterator.next.get;/<code>
<code>

if

( != item && item.isMutable) {/<code><code>

if

(canUseForInBitmap(item, options)) {/<code>
<code> Log.v(

"TEST"

,

"canUseForInBitmap!!!!"

);/<code>
<code> bitmap = item;/<code>
<code> // Remove from reusable

set

so it can

't be used again

/<code><code>

iterator.remove;

/<code><code>

break;

/<code><code>

}

/<code><code>

} else {

/<code><code>

// Remove from the set if the reference has been cleared.

/<code><code>

iterator.remove;

/<code><code>

}

/<code><code>

}

/<code><code>

}

/<code>
<code>

return bitmap;

/<code><code>

}

/<code>

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

<code> (Build.VERSION_CODES.KITKAT)/<code><code>

private

static

boolean

canUseForInBitmap

(

/<code><code>

Bitmap candidate, BitmapFactory.Options targetOptions)

{/<code>
<code>

if

(Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {/<code><code>

return

candidate.getWidth == targetOptions.outWidth/<code><code> && candidate.getHeight == targetOptions.outHeight/<code><code> && targetOptions.inSampleSize ==

1

;/<code><code> }/<code><code>

int

width = targetOptions.outWidth / targetOptions.inSampleSize;/<code><code>

int

height = targetOptions.outHeight / targetOptions.inSampleSize;/<code>
<code>

int

byteCount = width * height * getBytesPerPixel(candidate.getConfig);/<code>

<code>

return

byteCount <= candidate.getAllocationByteCount;/<code><code>}/<code>

c、磁盤緩存

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

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

<code>

public

static

DiskLruCache

open

(File directory,

int

appVersion,

int

valueCou9nt,

long

maxSize)/<code>

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

<code>

private

void

init

(

final

long

cacheSize,

final

File cacheFile) {/<code><code>

new

Thread(

new

Runnable {/<code><code> /<code><code>

public

void

run {/<code><code>

synchronized

(mDiskCacheLock) {/<code><code>

if

(!cacheFile.exists){/<code><code> cacheFile.mkdir;/<code><code> }/<code><code> MLog.d(TAG,

"Init DiskLruCache cache path:"

+ cacheFile.getPath +

"\r\n"

+

"Disk Size:"

+ cacheSize);/<code><code>

try

{/<code><code> mDiskLruCache = DiskLruCache.open(cacheFile, MiniImageLoaderConfig.VESION_IMAGELOADER,

1

, cacheSize);/<code><code> mDiskCacheStarting =

false

;/<code><code> /<code><code><code><code><code><code><code><code><code><code>

如果在初始化前就要操作寫或者讀會導致失敗,所以在整個DiskCache中使用的Object的wait/notifyAll機制來避免同步問題。

2、寫入DiskLruCache

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

<code>

private

String

hashKeyForDisk(

String

key) {/<code><code>

String

cacheKey;/<code><code>

try

{/<code><code> final MessageDigest mDigest = MessageDigest.getInstance(

"MD5"

);/<code><code> mDigest.update(key.getBytes);/<code><code> cacheKey = bytesToHexString(mDigest.digest);/<code><code> }

catch

(NoSuchAlgorithmException e) {/<code><code> cacheKey =

String

.valueOf(key.hashCode);/<code><code> }/<code><code>

return

cacheKey;/<code><code>}/<code><code>

private

String

bytesToHexString(byte[] bytes) {/<code><code> StringBuilder sb =

new

StringBuilder;/<code><code>

for

(int i =

0

; i < bytes.length; i++) {/<code><code>

String

hex = Integer.toHexString(

0xFF

& bytes[i]);/<code><code>

if

(hex.length ==

1

) {/<code><code> sb.append(

'0'

);/<code><code> }/<code><code> sb.append(hex);/<code><code> }/<code><code>

return

sb.toString;/<code><code>}/<code>

然後,寫入需要保存的圖片數據,圖片數據寫入本地緩存的整體代碼如下所示:

<code>public void saveToDisk(String imageUrl, InputStream  

in

) {/<code><code>

//

add to disk cache/<code><code> synchronized (mDiskCacheLock) {/<code><code>

try

{/<code><code>

while

(mDiskCacheStarting) {/<code><code>

try

{/<code><code> mDiskCacheLock.wait;/<code><code> }

catch

(InterruptedException e) {}/<code><code> }/<code><code> String key = hashKeyForDisk(imageUrl);/<code><code> MLog.d(TAG,

"saveToDisk get key:"

+ key);/<code><code> DiskLruCache.Editor editor = mDiskLruCache.edit(key);/<code><code>

if

(

in

!= && editor != ) {/<code><code>

//

當 valueCount指定為

1

時,index傳

0

即可/<code><code> OutputStream outputStream = editor.newOutputStream(

0

);/<code><code> MLog.d(TAG,

"saveToDisk"

);/<code><code>

if

(FileUtil.copyStream(

in

,outputStream)) {/<code><code> MLog.d(TAG,

"saveToDisk commit start"

);/<code><code> editor.commit;/<code><code> MLog.d(TAG,

"saveToDisk commit over"

);/<code><code> }

else

{/<code><code> editor.abort;/<code><code> MLog.e(TAG,

"saveToDisk commit abort"

);/<code><code> }/<code><code> }/<code><code> mDiskLruCache.flush;/<code><code> }

catch

(IOException e) {/<code><code> e.printStackTrace;/<code><code> }/<code><code> }/<code><code>}/<code>

接著,讀取圖片緩存,通過DiskLruCache的get方法實現,代碼如下所示:

<code>public Bitmap getBitmapFromDiskCache(String imageUrl,BitmapConfig bitmapconfig) {/<code><code> synchronized (mDiskCacheLock) {/<code><code> 

//

Wait

while

disk cache

is

started

from

background thread/<code><code>

while

(mDiskCacheStarting) {/<code><code>

try

{/<code><code> mDiskCacheLock.wait;/<code><code> }

catch

(InterruptedException e) {}/<code><code> }/<code><code>

if

(mDiskLruCache != ) {/<code><code>

try

{/<code>
<code> String key = hashKeyForDisk(imageUrl);/<code><code> MLog.d(TAG,

"getBitmapFromDiskCache get key:"

+ key);/<code><code> DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);/<code><code>

if

( == snapShot){/<code><code>

return

;/<code><code> }/<code><code> InputStream

is

= snapShot.getInputStream(

0

);/<code><code>

if

(

is

!= ){/<code><code> final BitmapFactory.Options options = bitmapconfig.getBitmapOptions;/<code><code>

return

BitmapUtil.decodeSampledBitmapFromStream(

is

, options);/<code><code> }

else

{/<code><code> MLog.e(TAG,

"is not exist"

);/<code><code> }/<code><code> }

catch

(IOException e){/<code><code> MLog.e(TAG,

"getBitmapFromDiskCache ERROR"

);/<code><code> }/<code><code> }/<code><code> }/<code><code>

return

;/<code><code>}/<code>

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

<code><code>  Bitmap doInBackground(

Void

... params) {/<code><code> Bitmap bitmap = ;/<code><code> synchronized (mPauseWorkLock) {/<code><code>

while

(mPauseWork && !isCancelled) {/<code><code>

try

{/<code><code> mPauseWorkLock.wait;/<code><code> }

catch

(InterruptedException e) {/<code><code> e.printStackTrace;/<code><code> }/<code><code> }/<code><code> }/<code><code>

if

(bitmap == && !isCancelled/<code><code> && imageViewReference.

get

!= && !mExitTasksEarly) {/<code><code> bitmap = getmImageCache.getBitmapFromDisk(mUrl, mBitmapConfig);/<code><code> }/<code>
<code>

if

(bitmap == && !isCancelled/<code><code> && imageViewReference.

get

!= && !mExitTasksEarly) {/<code><code> bitmap = downLoadBitmap(mUrl, mBitmapConfig);/<code><code> }/<code><code>

if

(bitmap != ) {/<code><code> getmImageCache.addToCache(mUrl, bitmap);/<code><code> }/<code>
<code>

return

bitmap;/<code><code>}/<code>

3、圖片加載三方庫

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

總結下Fresco的優點

,如下所示:

  • 1、內存管理

  • 2、漸進式呈現:先呈現大致的圖片輪廓,然後隨著圖片下載的繼續,呈現逐漸清晰的圖片。

  • 3、支持更多的圖片格式:如Gif和Webp。

  • 4、圖像加載策略豐富:其中的Image Pipeline可以為同一個圖片指定不同的遠程路徑,比如先顯示已經存在本地緩存中的圖片,等高清圖下載完成之後在顯示高清圖集。

缺點

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

總結

對於內存優化,一般都是通過使用MAT等工具來進行檢查和使用LeakCanary等內存洩漏監控工具來進行監控,以此來發現問題,再分析問題原因,解決發現的問題或者對當前的實現邏輯進行優化優化完後再進行檢查,直到達到預定的性能指標

內存優化方面有什麼經驗嘛?


分享到:


相關文章: