如何在 Android App 上高效顯示點陣圖

2017-05-08    分類:Android開發、程式設計開發、首頁精華0人評論發表於2017-05-08

本文由碼農網 – 小峰原創翻譯,轉載請看清文末的轉載要求,歡迎參與我們的付費投稿計劃

為了建立具有視覺魅力的app,顯示影像是必須的。學會在你的Android app上高效地顯示點陣圖,而不是放棄效能。

在Android上顯示影像的痛苦

當工作於開發視覺魅力的app時,顯示影像是必須的。問題是,Android作業系統不能很好地處理影像解碼,從而迫使開發者要小心某些任務以避免搞亂效能。

Google寫了一個有關於高效顯示點陣圖的完整指南,我們可以按照這個指南來理解和解決在顯示點陣圖時Android作業系統的主要缺陷。

Android app效能殺手

按照Google的指南,我們可以列出一些我們在Android apps上顯示影像時遇到的主要問題。

降低影像取樣率

無論檢視大小,Android總是解碼並全尺寸/大小顯示影像。因為這個原因,所以如果你試圖載入一個大影像,那就很容易使你的裝置出現outOfMemoryError。

為了避免這種情況,正如Google所說的那樣,我們應該使用BitmapFactory 解碼影像,為inSampleSize 引數設定一個值。圖象尺寸由inSampleSize劃分,減少儲存器的使用量。

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);
}

你可以手動設定inSampleSize,或使用顯示器的尺寸計算。

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時,影像解碼在UI執行緒上完成。這可以凍結app,並導致ANR(“Application Not Responding應用程式沒有響應”)警報。

這個容易解決,你只需要將解碼過程放到工作執行緒上。一種方法是使用非同步任務,正如Google指導中解釋的那樣:

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);
            }
        }
    }
}

影像快取

每當對影像進行解碼並放置在一個檢視中的時候,Android作業系統預設重複整個渲染過程,浪費了寶貴的裝置儲存器。如果你打算在不同的地方展示相同的影像,或因為app生命週期或行為要多次重新載入,那麼這可能會特別煩人。

為了避免佔用過多的記憶體,推薦使用記憶體和磁碟快取。接下來,我們將看到這些快取之間的主要區別,以及為什麼同時使用兩者有用的原因。程式碼在這裡顯示的話太複雜了,所以請自行參閱Google指南的點陣圖快取部分以瞭解如何實現記憶體和磁碟的快取。

  • 記憶體快取:影像儲存在裝置記憶體中。記憶體訪問快速。事實上,比影像解碼過程要快得多,所以將影像儲存在這裡是讓app更快更穩定的一個好主意。記憶體快取的唯一缺點是,它只存活於app的生命週期,這意味著一旦app被Android作業系統記憶體管理器關閉或殺死(全部或部分),那麼儲存在那裡的所有影像都將丟失。請記住,記憶體快取必須設定一個最大可用的記憶體量。否則可能會導致臭名昭著的outOfMemoryError。
  • 磁碟快取:影像儲存在裝置的物理儲存器上(磁碟)。磁碟快取可以一直存活於app啟動期間,安全地儲存圖片,只要有足夠的空間。缺點是,磁碟讀取和寫入操作可能會很慢,而且總是比訪問記憶體快取慢。由於這個原因,因此所有的磁碟操作必須在工作執行緒執行,UI執行緒之外。否則,app會凍結,並導致ANR警報。

每個快取都有其優點和缺點,因此最好的做法是兩者皆用,並從首先可用的地方讀取,通過記憶體快取開始。

最後的思考以及EpicBitmapRenderer

不知道你有沒有注意到,正如我在本文開頭所述,在Android app上顯示圖片真的很讓人頭疼。絕非看上去那麼簡單。

為了避免在每個專案中重複這些任務,我開發了一個100%免費又開源的Android庫,EpicBitmapRenderer 。你可以在EpicBitmapRenderer GitHub repo選擇它,或在EpicBitmapRenderer網站了解更多。

EpicBitmapRenderer 易於使用,並在每個影像解碼操作中自動化了所有這些惱人的任務,這樣你就可以專注於app開發。

你只需要新增增加EpicBitmapRenderer 依賴在你的Gradle上(檢視其他構建工具的替代品,看看EpicBitmapRenderer文件的匯入庫部分)。

compile 'com.isaacrf.epicbitmaprenderer:epicbitmaprenderer:1.0'

EpicBitmapRenderer 中解碼影像是很容易的:只需要呼叫所需的解碼方法並管理結果。看看下面這個例子,我們從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();
            }
        });

許可證

這篇文章以及任何相關的原始碼和檔案,遵循 The Creative Commons Attribution-Share Alike 3.0 Unported License的授權許可。

譯文連結:http://www.codeceo.com/article/android-app-display-bitmaps.html
英文原文:Displaying Bitmaps Efficiently on Android Apps
翻譯作者:碼農網 – 小峰
轉載必須在正文中標註並保留原文連結、譯文連結和譯者等資訊。]

相關文章