一羡洛、內(nèi)存管理的獨到之處
在Fresco中煌恢,提供了PlatformDecoder這個接口用于處理bitmap的decode過程,下面包括了該接口的具體實現(xiàn)類:
在 ImagePipelineFactory 類中是根據(jù)不同的平臺去 build 不同的 decoder:
/**
* Provide the implementation of the PlatformDecoder for the current platform using the
* provided PoolFactory
*
* @param poolFactory The PoolFactory
* @return The PlatformDecoder implementation
*/
public static PlatformDecoder buildPlatformDecoder(
PoolFactory poolFactory,
boolean directWebpDirectDecodingEnabled) {
// 5.0及以上铛只,返回 ArtDecoder
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
int maxNumThreads = poolFactory.getFlexByteArrayPoolMaxNumThreads();
return new ArtDecoder(
poolFactory.getBitmapPool(),
maxNumThreads,
new Pools.SynchronizedPool<>(maxNumThreads));
} else {
// directWebpDirectDecodingEnabled 為 true 且 小于4.4
if (directWebpDirectDecodingEnabled
&& Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
return new GingerbreadPurgeableDecoder();
} else {// 其余情況跟磨,都使用 KitKatPurgeableDecoder
return new KitKatPurgeableDecoder(poolFactory.getFlexByteArrayPool());
}
}
}
/**
* Bitmap decoder for ART VM (Lollipop and up).
*/
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@ThreadSafe
public class ArtDecoder implements PlatformDecoder
/**
* Bitmap decoder (Gingerbread to Jelly Bean). API 9(2.3) —> API 16(4.1)
* <p/>
* <p>This copies incoming encoded bytes into a MemoryFile, and then decodes them using a file
* descriptor, thus avoiding using any Java memory at all. This technique only works in JellyBean
* and below.
* decode 前會先把原始數(shù)據(jù)(encoded data)copy 到 MemoryFile 中去杆怕,借助 MemoryFile 把 encoded data
* 拷貝到 ashmem 中去族购,盡量避免在 Java Heap 上分配內(nèi)存而造成頻繁 GC 的問題
*/
public class GingerbreadPurgeableDecoder extends DalvikPurgeableDecoder
/**
* Bitmap Decoder implementation for KitKat
*
* <p>The MemoryFile trick used in GingerbreadPurgeableDecoder does not work in KitKat. Here, we
* instead use Java memory to store the encoded images, but make use of a pool to minimize
* allocations. We cannot decode from a stream, as that does not support purgeable decodes.
*/
@TargetApi(Build.VERSION_CODES.KITKAT)
@ThreadSafe
public class KitKatPurgeableDecoder extends DalvikPurgeableDecoder
- ArtDecoder 中通過 BitmapOptions 的 inBitmap 和 inTempStorage 去優(yōu)化內(nèi)存使用(inBitmap 是由 BitmapPool 去分配內(nèi)存,inTempStorage 是由SynchronizedPool 分配內(nèi)存陵珍,都是用緩存池的方式分配和回收內(nèi)存寝杖,做到對這些區(qū)域的內(nèi)存可管理,減少各個不同地方自行分配內(nèi)存)互纯,最終去調(diào)用 BitmapFactory.decodeStream() 方法瑟幕。
- DalvikPurgeableDecoder 中設(shè)置 inPurgeable 和 inInputShareable 為 true,從注釋看伟姐,inPurgeable 能使 bitmap 的內(nèi)存分配到 ashmem 上收苏;對于通過 filedescriptor 去decode的方式,還要設(shè)置 inInputShareable 為true愤兵,只能夠使內(nèi)存分配到 ashmem 上鹿霸。
private static BitmapFactory.Options getBitmapFactoryOptions(
int sampleSize,
Bitmap.Config bitmapConfig) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inDither = true; // known to improve picture quality at low cost
options.inPreferredConfig = bitmapConfig;
// Decode the image into a 'purgeable' bitmap that lives on the ashmem heap
options.inPurgeable = true;
// Enable copy of of bitmap to enable purgeable decoding by filedescriptor
options.inInputShareable = true;
// Sample size should ONLY be different than 1 when downsampling is enabled in the pipeline
options.inSampleSize = sampleSize;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
options.inMutable = true; // no known perf difference; allows postprocessing to work
}
return options;
}
為了理解Facebook到底做了什么工作,在此之前我們需要了解在Android可以使用的堆內(nèi)存之間的區(qū)別秆乳。
- Java Heap(Dalvik Heap)懦鼠,這部分的內(nèi)存區(qū)域是由Dalvik虛擬機管理,通過Java中 new 關(guān)鍵字來申請一塊新內(nèi)存屹堰。這塊區(qū)域的內(nèi)存是由GC直接管理肛冶,能夠自動回收內(nèi)存。這塊內(nèi)存的大小會受到系統(tǒng)限制扯键,當內(nèi)存超過APP最大可用內(nèi)存時會OOM睦袖。
- Native Heap,這部分內(nèi)存區(qū)域是在C++中申請的荣刑,它不受限于APP的最大可用內(nèi)存限制馅笙,而只是受限于設(shè)備的物理可用內(nèi)存限制伦乔。它的缺點在于沒有自動回收機制,只能通過C++語法來釋放申請的內(nèi)存董习。
- Ashmem(Android匿名共享內(nèi)存)烈和,這部分內(nèi)存類似于Native內(nèi)存區(qū),但是它是受Android系統(tǒng)底層管理的皿淋,當Android系統(tǒng)內(nèi)存不足時招刹,會回收Ashmem區(qū)域中狀態(tài)是 unpin 的對象內(nèi)存塊,如果不希望對象被回收窝趣,可以通過 pin 來保護一個對象疯暑。
Ashmem不能被Java應(yīng)用直接處理,但是也有一些例外高帖,圖片就是其中之一缰儿。當你創(chuàng)建一張沒有經(jīng)過壓縮的Bitmap的時候畦粮,Android的API允許你指定是否是可清除的散址。
BitmapFactory.Options = new BitmapFactory.Options();
options.inPurgeable = true;
Bitmap bitmap = BitmapFactory.decodeByteArray(jpeg, 0, jpeg.length, options);
上面的代碼便是通過設(shè)置 inPurgeable 為 true 來創(chuàng)建一個 Purgeable Bitmap ,這樣decode出來的bitmap是在 Ashmem 內(nèi)存中宣赔,GC無法自動回收它预麸。當該Bitmap在被使用時會被 pin 住,使用完之后就 unpin 儒将,這樣系統(tǒng)就可以在將來某一時間釋放這部分內(nèi)存吏祸。
如果一個 unpinned 的 bitmap 在之后又要被使用,系統(tǒng)會運行時又將它重新 decode钩蚊,但是這個 decode 操作是發(fā)生在UI線程中的有可能會造成掉幀現(xiàn)象贡翘,因此該做法已經(jīng)被 Google 廢棄掉,轉(zhuǎn)為鼓勵使用 inBitmap 來告知
bitmap 解碼器去嘗試使用已經(jīng)存在的內(nèi)存區(qū)域砰逻,新解碼的 bitmap 會嘗試去使用之前那張 bitmap 在 heap 中所占據(jù)的 pixel data 內(nèi)存區(qū)域鸣驱,而不是去問內(nèi)存重新申請一塊區(qū)域來存放 bitmap。利用這種特性蝠咆,即使是上千張的圖片踊东,也只會僅僅只需要占用屏幕所能夠顯示的圖片數(shù)量的內(nèi)存大小。
這聽起來很完美刚操,但是我們來看 inPurgeable:
/**
* @deprecated As of {@link android.os.Build.VERSION_CODES#LOLLIPOP}, this is
* ignored.
*
* In {@link android.os.Build.VERSION_CODES#KITKAT} and below, if this
* is set to true, then the resulting bitmap will allocate its
* pixels such that they can be purged if the system needs to reclaim
* memory. In that instance, when the pixels need to be accessed again
* (e.g. the bitmap is drawn, getPixels() is called), they will be
* automatically re-decoded.
*
* <p>For the re-decode to happen, the bitmap must have access to the
* encoded data, either by sharing a reference to the input
* or by making a copy of it. This distinction is controlled by
* inInputShareable. If this is true, then the bitmap may keep a shallow
* reference to the input. If this is false, then the bitmap will
* explicitly make a copy of the input data, and keep that. Even if
* sharing is allowed, the implementation may still decide to make a
* deep copy of the input data.</p>
*
* <p>While inPurgeable can help avoid big Dalvik heap allocations (from
* API level 11 onward), it sacrifices performance predictability since any
* image that the view system tries to draw may incur a decode delay which
* can lead to dropped frames. Therefore, most apps should avoid using
* inPurgeable to allow for a fast and fluid UI. To minimize Dalvik heap
* allocations use the {@link #inBitmap} flag instead.</p>
*
* <p class="note"><strong>Note:</strong> This flag is ignored when used
* with {@link #decodeResource(Resources, int,
* android.graphics.BitmapFactory.Options)} or {@link #decodeFile(String,
* android.graphics.BitmapFactory.Options)}.</p>
*/
@Deprecated
public boolean inPurgeable;
- 在 LOLLIPOP(API 21—Android 5.0)時被棄用闸翅,fresco 5.0使用 inBitmap。
- 在 KITKAT 及之前使用設(shè)置為 true菊霜,當系統(tǒng)需要回收內(nèi)存時坚冀,bitmap 的 pixels 可以被清除,當 pixels 需要被重新訪問的時候(例如 bitmap draw或者調(diào)用 getPixels() 的時候)鉴逞,它們又可以重新被 decode 出來记某。
- 需要重新 decode 的話联喘,自然需要 encoded data,encoded data 可能來源于對原始那份 encoded data 的引用辙纬,或者是對原始數(shù)據(jù)的拷貝豁遭。具體是引用或者拷貝,就是根據(jù) inInputShareable 來決定的贺拣,如果是 true 那就是引用蓖谢,不然就是 deep copy磅崭,但是 inInputShareable 即使設(shè)置為 true集漾,不同的實現(xiàn)也可能是直接進行 deep copy。
- inPurgeable 能夠避免大的 Dalvik heap 內(nèi)存分配(從 API 11—Android
3.0 開始)谓松,然而會犧牲 UI 的流暢性涡匀,因為重新 decode 的過程在 UI 線程中進行盯腌,會導致掉幀問題,因此不建議使用 inPurgeable陨瘩,推薦使用 inBitmap 特性腕够。 - 然而 inBitmap 這個特性直到 Android 3.0 之后才被支持,在 Android 4.4 之前重用的 bitmap 大小必須是一致的且必須是 jpeg 或 png 格式舌劳,從SDK 19(Android 4.4)開始帚湘,新申請的bitmap大小必須小于或者等于已經(jīng)賦值過的bitmap大小,新申請的bitmap與舊的bitmap必須有相同的解碼格式甚淡。
那么如何解決 drop frames 導致的卡頓問題大诸?
在 DalvikPurgeableDecoder 中可以看到,每次 decode 之后調(diào)用了 pinBitmap 方法贯卦。
/**
* Creates a bitmap from encoded bytes.
*
* @param encodedImage the encoded image with reference to the encoded bytes
* @param bitmapConfig the {@link android.graphics.Bitmap.Config}
* used to create the decoded Bitmap
* @return the bitmap
* @throws TooManyBitmapsException if the pool is full
* @throws java.lang.OutOfMemoryError if the Bitmap cannot be allocated
*/
@Override
public CloseableReference<Bitmap> decodeFromEncodedImage(
final EncodedImage encodedImage,
Bitmap.Config bitmapConfig) {
BitmapFactory.Options options = getBitmapFactoryOptions(
encodedImage.getSampleSize(),
bitmapConfig);
CloseableReference<PooledByteBuffer> bytesRef = encodedImage.getByteBufferRef();
Preconditions.checkNotNull(bytesRef);
try {
Bitmap bitmap = decodeByteArrayAsPurgeable(bytesRef, options);
return pinBitmap(bitmap);
} finally {
CloseableReference.closeSafely(bytesRef);
}
}
為了讓 inPurgeable 的 bitmap 不被自動 unpinned 资柔,可以通過使用 jni 函數(shù) AndroidBitmap_lockPixels() 函數(shù)來強制 pin bitmap ,這樣我們就可以在 bitmap 被使用時不會被系統(tǒng)自動 unpinned 撵割,從而也就避免了 unpinned 的 bitmap 在重新被使用時又會被重新 decode 而引起的掉幀問題贿堰。
這做后,F(xiàn)resco 需要自己去管理這塊內(nèi)存區(qū)域睁枕,保證當這個 Bitmap 不再使用時官边,Ashmem 的內(nèi)存空間能被 unpin,F(xiàn)resco 選擇在 Bitmap 離開屏幕可視范圍時候(onDetachWindow等時候)外遇,通過調(diào)用 bitmap.recycle() 方法去做 unpin注簿。
參考:
Fresco介紹 - 一個新的android圖片加載庫
談?wù)刦resco的bitmap內(nèi)存分配
Fresco內(nèi)存機制(Ashmem匿名共享內(nèi)存)