如何在Android App上高效顯示位圖
為了創(chuàng)建具有視覺魅力的app,顯示圖像是必須的。學(xué)會(huì)在你的Android app上高效地顯示位圖,而不是放棄性能。
在Android上顯示圖像的痛苦
當(dāng)工作于開發(fā)視覺魅力的app時(shí),顯示圖像是必須的。問題是,Android操作系統(tǒng)不能很好地處理圖像解碼,從而迫使開發(fā)者要小心某些任務(wù)以避免搞亂性能。
Google寫了一個(gè)有關(guān)于高效顯示位圖的完整指南,我們可以按照這個(gè)指南來理解和解決在顯示位圖時(shí)Android操作系統(tǒng)的主要缺陷。
Android app性能殺手
按照Google的指南,我們可以列出一些我們在Android apps上顯示圖像時(shí)遇到的主要問題。
降低圖像采樣率
無論視圖大小,Android總是解碼并全尺寸/大小顯示圖像。因?yàn)檫@個(gè)原因,所以如果你試圖加載一個(gè)大圖像,那就很容易使你的設(shè)備出現(xiàn)outOfMemoryError。
為了避免這種情況,正如Google所說的那樣,我們應(yīng)該使用BitmapFactory
解碼圖像,為inSampleSize
參數(shù)設(shè)置一個(gè)值。圖象尺寸由inSampleSize劃分,減少存儲(chǔ)器的使用量。
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) {
// First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
// Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
你可以手動(dòng)設(shè)置inSampleSize,或使用顯示器的尺寸計(jì)算。
public static int calculateInSampleSize( BitmapFactory.Options options, int reqWidth, int reqHeight) {
// Raw height and width of image
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
// Calculate the largest inSampleSize value that is a power of 2 and keeps both
// height and width larger than the requested height and width.
while ((halfHeight / inSampleSize) >= reqHeight
&& (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
異步解碼
即使在使用BitmapFactory時(shí),圖像解碼在UI線程上完成。這可以凍結(jié)app,并導(dǎo)致ANR(“Application Not Responding應(yīng)用程序沒有響應(yīng)”)警報(bào)。
這個(gè)容易解決,你只需要將解碼過程放到工作線程上。一種方法是使用異步任務(wù),正如Google指導(dǎo)中解釋的那樣:
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
private final WeakReference<ImageView> imageViewReference;
private int data = 0;
public BitmapWorkerTask(ImageView imageView) {
// Use a WeakReference to ensure the ImageView can be garbage collected
imageViewReference = new WeakReference<ImageView>(imageView);
}
// Decode image in background.
@Override
protected Bitmap doInBackground(Integer... params) {
data = params[0];
return decodeSampledBitmapFromResource(getResources(), data, 100, 100));
}
// Once complete, see if ImageView is still around and set bitmap.
@Override
protected void onPostExecute(Bitmap bitmap) {
if (imageViewReference != null && bitmap != null) {
final ImageView imageView = imageViewReference.get();
if (imageView != null) {
imageView.setImageBitmap(bitmap);
}
}
}
}
圖像緩存
每當(dāng)對(duì)圖像進(jìn)行解碼并放置在一個(gè)視圖中的時(shí)候,Android操作系統(tǒng)默認(rèn)重復(fù)整個(gè)渲染過程,浪費(fèi)了寶貴的設(shè)備存儲(chǔ)器。如果你打算在不同的地方展示相同的圖像,或因?yàn)閍pp生命周期或行為要多次重新加載,那么這可能會(huì)特別煩人。
為了避免占用過多的內(nèi)存,推薦使用內(nèi)存和磁盤緩存。接下來,我們將看到這些緩存之間的主要區(qū)別,以及為什么同時(shí)使用兩者有用的原因。代碼在這里顯示的話太復(fù)雜了,所以請自行參閱Google指南的位圖緩存部分以了解如何實(shí)現(xiàn)內(nèi)存和磁盤的緩存。
- 內(nèi)存緩存:圖像存儲(chǔ)在設(shè)備內(nèi)存中。內(nèi)存訪問快速。事實(shí)上,比圖像解碼過程要快得多,所以將圖像存儲(chǔ)在這里是讓app更快更穩(wěn)定的一個(gè)好主意。內(nèi)存緩存的唯一缺點(diǎn)是,它只存活于app的生命周期,這意味著一旦app被Android操作系統(tǒng)內(nèi)存管理器關(guān)閉或殺死(全部或部分),那么儲(chǔ)存在那里的所有圖像都將丟失。請記住,內(nèi)存緩存必須設(shè)置一個(gè)***可用的內(nèi)存量。否則可能會(huì)導(dǎo)致臭名昭著的outOfMemoryError。
- 磁盤緩存:圖像存儲(chǔ)在設(shè)備的物理存儲(chǔ)器上(磁盤)。磁盤緩存可以一直存活于app啟動(dòng)期間,安全地存儲(chǔ)圖片,只要有足夠的空間。缺點(diǎn)是,磁盤讀取和寫入操作可能會(huì)很慢,而且總是比訪問內(nèi)存緩存慢。由于這個(gè)原因,因此所有的磁盤操作必須在工作線程執(zhí)行,UI線程之外。否則,app會(huì)凍結(jié),并導(dǎo)致ANR警報(bào)。
每個(gè)緩存都有其優(yōu)點(diǎn)和缺點(diǎn),因此***的做法是兩者皆用,并從首先可用的地方讀取,通過內(nèi)存緩存開始。
***的思考以及EpicBitmapRenderer
不知道你有沒有注意到,正如我在本文開頭所述,在Android app上顯示圖片真的很讓人頭疼。絕非看上去那么簡單。
為了避免在每個(gè)項(xiàng)目中重復(fù)這些任務(wù),我開發(fā)了一個(gè)100%免費(fèi)又開源的Android庫,EpicBitmapRenderer
。你可以在EpicBitmapRenderer GitHub repo選擇它,或在EpicBitmapRenderer網(wǎng)站了解更多。
EpicBitmapRenderer
易于使用,并在每個(gè)圖像解碼操作中自動(dòng)化了所有這些惱人的任務(wù),這樣你就可以專注于app開發(fā)。
你只需要添加增加EpicBitmapRenderer
依賴在你的Gradle上(查看其他構(gòu)建工具的替代品,看看EpicBitmapRenderer文檔的導(dǎo)入庫部分)。
compile 'com.isaacrf.epicbitmaprenderer:epicbitmaprenderer:1.0'
在EpicBitmapRenderer
中解碼圖像是很容易的:只需要調(diào)用所需的解碼方法并管理結(jié)果??纯聪旅孢@個(gè)例子,我們從URL獲取圖片并顯示于ImageVIew上。
//Sample 3: Decode Bitmap from URL (Async)
EpicBitmapRenderer.decodeBitmapFromUrl(
"http://isaacrf.com/wp-content/themes/Workality-Lite-child/images/IsaacRF.png",
200, 200,
new OnBitmapRendered() {
@Override
public void onBitmapRendered(Bitmap bitmap) {
//Display rendered Bitmap when ready
ImageView imgView = findViewById(R.id.imgSampleDecodeUrl);
imgView.setImageBitmap(bitmap);
}
},
new OnBitmapRenderFailed() {
@Override
public void onBitmapRenderFailed(Exception e) {
//Take actions if Bitmap fails to render
Toast.makeText(MainActivity.this,
"Failed to load Bitmap from URL",
Toast.LENGTH_SHORT).show();
}
});
許可證
這篇文章以及任何相關(guān)的源代碼和文件,遵循 The Creative Commons Attribution-Share Alike 3.0 Unported License的授權(quán)許可。
譯文鏈接:http://www.codeceo.com/article/android-app-display-bitmaps.html
英文原文:Displaying Bitmaps Efficiently on Android Apps