Android處理非UI線程Bitmap

2018-08-02 17:35 更新

編寫:kesenhoo - 原文:http://developer.android.com/training/displaying-bitmaps/process-bitmap.html

在上一課中介紹了一系列的BitmapFactory.decode*方法,當(dāng)圖片來源是網(wǎng)絡(luò)或者是存儲(chǔ)卡時(shí)(或者是任何不在內(nèi)存中的形式),這些方法都不應(yīng)該在UI 線程中執(zhí)行。因?yàn)樵谏鲜銮闆r下加載數(shù)據(jù)時(shí),其執(zhí)行時(shí)間是不可估計(jì)的,它依賴于許多因素(從網(wǎng)絡(luò)或者存儲(chǔ)卡讀取數(shù)據(jù)的速度,圖片的大小,CPU的速度等)。如果其中任何一個(gè)子操作阻塞了UI線程,系統(tǒng)都會(huì)容易出現(xiàn)應(yīng)用無響應(yīng)的錯(cuò)誤。

這一節(jié)課會(huì)介紹如何使用AsyncTask在后臺(tái)線程中處理Bitmap并且演示如何處理并發(fā)(concurrency)的問題。

使用AsyncTask(Use a AsyncTask)

AsyncTask 類提供了一個(gè)在后臺(tái)線程執(zhí)行一些操作的簡單方法,它還可以把后臺(tái)的執(zhí)行結(jié)果呈現(xiàn)到UI線程中。下面是一個(gè)加載大圖的示例:

class BitmapWorkerTask extends AsyncTask {
    private final WeakReference imageViewReference;
    private int data = 0;

    public BitmapWorkerTask(ImageView imageView) {
        // Use a WeakReference to ensure the ImageView can be garbage collected
        imageViewReference = new WeakReference(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);
            }
        }
    }
}

ImageView使用WeakReference確保了AsyncTask所引用的資源可以被垃圾回收器回收。由于當(dāng)任務(wù)結(jié)束時(shí)不能確保ImageView仍然存在,因此我們必須在onPostExecute()里面對(duì)引用進(jìn)行檢查。該ImageView在有些情況下可能已經(jīng)不存在了,例如,在任務(wù)結(jié)束之前用戶使用了回退操作,或者是配置發(fā)生了改變(如旋轉(zhuǎn)屏幕等)。

開始異步加載位圖,只需要?jiǎng)?chuàng)建一個(gè)新的任務(wù)并執(zhí)行它即可:

public void loadBitmap(int resId, ImageView imageView) {
    BitmapWorkerTask task = new BitmapWorkerTask(imageView);
    task.execute(resId);
}

處理并發(fā)問題(Handle Concurrency)

通常類似ListView與GridView等視圖控件在使用上面演示的AsyncTask 方法時(shí),會(huì)同時(shí)帶來并發(fā)的問題。首先為了更高的效率,ListView與GridView的子Item視圖會(huì)在用戶滑動(dòng)屏幕時(shí)被循環(huán)使用。如果每一個(gè)子視圖都觸發(fā)一個(gè)AsyncTask,那么就無法確保關(guān)聯(lián)的視圖在結(jié)束任務(wù)時(shí),分配的視圖已經(jīng)進(jìn)入循環(huán)隊(duì)列中,給另外一個(gè)子視圖進(jìn)行重用。而且, 無法確保所有的異步任務(wù)的完成順序和他們本身的啟動(dòng)順序保持一致。

Multithreading for Performance 這篇博文更進(jìn)一步的討論了如何處理并發(fā)問題,并且提供了一種解決方法:ImageView保存最近使用的AsyncTask的引用,這個(gè)引用可以在任務(wù)完成的時(shí)候再次讀取檢查。使用這種方式, 就可以對(duì)前面提到的AsyncTask進(jìn)行擴(kuò)展。

創(chuàng)建一個(gè)專用的Drawable的子類來儲(chǔ)存任務(wù)的引用。在這種情況下,我們使用了一個(gè)BitmapDrawable,在任務(wù)執(zhí)行的過程中,一個(gè)占位圖片會(huì)顯示在ImageView中:

static class AsyncDrawable extends BitmapDrawable {
    private final WeakReference bitmapWorkerTaskReference;

    public AsyncDrawable(Resources res, Bitmap bitmap,
            BitmapWorkerTask bitmapWorkerTask) {
        super(res, bitmap);
        bitmapWorkerTaskReference =
            new WeakReference(bitmapWorkerTask);
    }

    public BitmapWorkerTask getBitmapWorkerTask() {
        return bitmapWorkerTaskReference.get();
    }
}

在執(zhí)行BitmapWorkerTask 之前,你需要?jiǎng)?chuàng)建一個(gè)AsyncDrawable并且將它綁定到目標(biāo)控件ImageView中:

public void loadBitmap(int resId, ImageView imageView) {
    if (cancelPotentialWork(resId, imageView)) {
        final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
        final AsyncDrawable asyncDrawable =
                new AsyncDrawable(getResources(), mPlaceHolderBitmap, task);
        imageView.setImageDrawable(asyncDrawable);
        task.execute(resId);
    }
}

在上面的代碼示例中,cancelPotentialWork 方法檢查是否有另一個(gè)正在執(zhí)行的任務(wù)與該ImageView關(guān)聯(lián)了起來,如果的確是這樣,它通過執(zhí)行cancel()方法來取消另一個(gè)任務(wù)。在少數(shù)情況下, 新創(chuàng)建的任務(wù)數(shù)據(jù)可能會(huì)與已經(jīng)存在的任務(wù)相吻合,這樣的話就不需要進(jìn)行下一步動(dòng)作了。下面是 cancelPotentialWork方法的實(shí)現(xiàn) 。

public static boolean cancelPotentialWork(int data, ImageView imageView) {
    final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);

    if (bitmapWorkerTask != null) {
        final int bitmapData = bitmapWorkerTask.data;
        if (bitmapData == 0 || bitmapData != data) {
            // Cancel previous task
            bitmapWorkerTask.cancel(true);
        } else {
            // The same work is already in progress
            return false;
        }
    }
    // No task associated with the ImageView, or an existing task was cancelled
    return true;
}

在上面的代碼中有一個(gè)輔助方法:getBitmapWorkerTask(),它被用作檢索AsyncTask是否已經(jīng)被分配到指定的ImageView:

private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
   if (imageView != null) {
       final Drawable drawable = imageView.getDrawable();
       if (drawable instanceof AsyncDrawable) {
           final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
           return asyncDrawable.getBitmapWorkerTask();
       }
    }
    return null;
}

最后一步是在BitmapWorkerTask的onPostExecute() 方法里面做更新操作:

class BitmapWorkerTask extends AsyncTask {
    ...

    @Override
    protected void onPostExecute(Bitmap bitmap) {
        if (isCancelled()) {
            bitmap = null;
        }

        if (imageViewReference != null && bitmap != null) {
            final ImageView imageView = imageViewReference.get();
            final BitmapWorkerTask bitmapWorkerTask =
                    getBitmapWorkerTask(imageView);
            if (this == bitmapWorkerTask && imageView != null) {
                imageView.setImageBitmap(bitmap);
            }
        }
    }
}

這個(gè)方法不僅僅適用于ListView與GridView控件,在那些需要循環(huán)利用子視圖的控件中同樣適用:只需要在設(shè)置圖片到ImageView的地方調(diào)用 loadBitmap方法。例如,在GridView 中實(shí)現(xiàn)這個(gè)方法可以在 getView()中調(diào)用。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)