Android 圖片加載框架的簡單設計

目前Android 發展至今優秀的圖片加載框架太多,例如: Volley ,Picasso,Imageloader,Glide等等。但是作為程序猿,懂得其中的實現原理還是相當重要的,只有懂得才能更好地使用。于是乎,今天我就簡單設計一個網絡加載圖片框架。主要就是熟悉圖片的網絡加載機制。

一般來說,一個優秀的 圖片加載框架(ImageLoader) 應該具備如下功能:

  • 圖片壓縮

  • 內存緩存

  • 磁盤緩存

  • 圖片的同步加載

  • 圖片的異步加載

  • 網絡拉取

那我們就從以上幾個方面進行介紹:

1.圖片壓縮(有效的降低OOM的發生概率)

圖片壓縮功能我在Bitmap 的高效加載中已經做了介紹這里不多說直接上代碼。這里直接抽象一個類用于完成圖片壓縮功能。

public class ImageResizer {
    private static final String TAG = "ImageResizer";

    public ImageResizer() {
        super();
        // TODO Auto-generated constructor stub
    }

    public Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
            int reqWidth, int reqHeight) {
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(res, resId, options);

        options.inSampleSize = calculateInSampleSize(options, reqWidth,
                reqHeight);

        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeResource(res, resId, options);
    }

    public Bitmap decodeSampledBitmapFromBitmapFileDescriptor(FileDescriptor fd,
            int reqWidth,int reqHeight){
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;

        BitmapFactory.decodeFileDescriptor(fd, null, options);

        options.inSampleSize = calculateInSampleSize(options, reqWidth,
                reqHeight);

        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeFileDescriptor(fd, null, options);
    }





    public int calculateInSampleSize(BitmapFactory.Options options,
            int reqWidth, int reqHeight) {

        final int width = options.outWidth;
        final int height = options.outHeight;

        int inSampleSize = 1;
        if (height > reqHeight || width > reqWidth) {
            final int halfHeight = height / 2;
            final int halfWidth = width / 2;
            while ((halfHeight / inSampleSize) > reqHeight
                    && (halfWidth / inSampleSize) > halfWidth) {
                inSampleSize *= 2;
            }
        }
        return inSampleSize;

    }

}

2.內存緩存和磁盤緩存

緩存直接選擇 LruCache 和 DiskLruCache 來完成內存緩存和磁盤緩存工作。

首先對其初始化:

private LruCache<String, Bitmap> mMemoryCache;
private DiskLruCache mDiskLruCache;

public ImageLoader(Context context) {
        mContext = context.getApplicationContext();
      //分配內存緩存為當前進程的1/8,磁盤緩存容量為50M
        int maxMemory = (int) (Runtime.getRuntime().maxMemory() * 1024);
        int cacheSize = maxMemory / 8;
        mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {

            @Override
            protected int sizeOf(String key, Bitmap value) {
                return value.getRowBytes() * value.getHeight() / 1024;
            }

        };

        File diskCacheDir = getDiskChaheDir(mContext, "bitmap");
        if (!diskCacheDir.exists()) {
            diskCacheDir.mkdirs();
        }
        if (getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) {
            try {
                mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1,
                        DISK_CACHE_SIZE);
                mIsDiskLruCacheCreated = true;
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

創建完畢后,接下來則需要提供方法來視線添加以及獲取的功能。首先來看內存緩存。

private void addBitmapToMemoryCache(String key, Bitmap bitmap) {
        if (getBitmapFromMemCache(key) == null) {
            mMemoryCache.put(key, bitmap);
        }
    }

    private Bitmap getBitmapFromMemCache(String key) {
        return mMemoryCache.get(key);
    }

相對來說內存緩存比較簡單,而磁盤緩存則復雜的多。磁盤緩存(LruDiskCache)并沒有直接提供方法來實現,而是要通過Editor以及Snapshot 來實現對于文件系統的添加以及讀取的操作。

首先看一下,Editor,它提供了commit 和 abort 方法來提交和撤銷對文件系統的寫操作。

//將下載的圖片寫入文件系統,實現磁盤緩存
    private Bitmap loadBitmapFromHttp(String url, int reqWidth, int reqHeight)
            throws IOException {
        if (Looper.myLooper() == Looper.getMainLooper()) {
            throw new RuntimeException("can not visit network from UI Thread.");
        }
        if (mDiskLruCache == null)
            return null;
        String key = hashKeyFormUrl(url);
        DiskLruCache.Editor editor = mDiskLruCache.edit(key);
        if (editor != null) {
            OutputStream outputStream = editor
                    .newOutputStream(DISK_CACHE_INDEX);
            if (downloadUrlToStream(url, outputStream)) {
                editor.commit();
            } else {
                editor.abort();
            }

        }
        mDiskLruCache.flush();
        return loadBitmapForDiskCache(url, reqWidth, reqHeight);
    }

Snapshot, 通過它可以獲取磁盤緩存對象對應的 FileInputStream,但是FileInputStream 無法便捷的進行壓縮,所以通過FileDescriptor 來加載壓縮后的圖片,最后將加載后的bitmap添加到內存緩存中。

public Bitmap loadBitmapForDiskCache(String url, int reqWidth, int reqHeight)
            throws IOException {
        if (Looper.myLooper() == Looper.getMainLooper()) {
            Log.w(TAG, "load bitmap from UI Thread , it's not recommended");
        }
        if (mDiskLruCache == null)
            return null;
        Bitmap bitmap = null;
        String key = hashKeyFormUrl(url);
        DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
        if (snapshot != null) {
            FileInputStream fileInputStream = (FileInputStream) snapshot
                    .getInputStream(DISK_CACHE_INDEX);
            FileDescriptor fileDescriptor = fileInputStream.getFD();
            bitmap = mImageResizer.decodeSampledBitmapFromBitmapFileDescriptor(
                    fileDescriptor, reqWidth, reqHeight);
            if (bitmap != null) {
                addBitmapToMemoryCache(key, bitmap);
            }
        }
        return bitmap;
    }

3.同步加載

同步加載的方法需要外部在子線程中調用。

//同步加載
    public Bitmap loadBitmap(String uri, int reqWidth, int reqHeight) {
        Bitmap bitmap = loadBitmpaFromMemCache(uri);
        if (bitmap != null) {
            return bitmap;
        }
        try {
            bitmap = loadBitmapForDiskCache(uri, reqWidth, reqHeight);
            if (bitmap != null) {
                return bitmap;
            }
            bitmap = loadBitmapFromHttp(uri, reqWidth, reqHeight);

        } catch (IOException e) {
            e.printStackTrace();
        }
        if (bitmap == null && !mIsDiskLruCacheCreated) {
            bitmap = downloadBitmapFromUrl(uri);
        }
        return bitmap;
    }

從方法中可以看出工作過程遵循如下幾步:

首先嘗試從內存緩存中讀取圖片,接著嘗試從磁盤緩存中讀取圖片,最后才會從網絡中拉取。此方法不能再主線程中執行,執行環境的檢測是在loadBitmapFromHttp中實現的。

if (Looper.myLooper() == Looper.getMainLooper()) {
            throw new RuntimeException("can not visit network from UI Thread.");
        }

4.異步加載

//異步加載
    public void bindBitmap(final String uri, final ImageView imageView,
            final int reqWidth, final int reqHeight) {

        imageView.setTag(TAG_KEY_URI, uri);
        Bitmap bitmap = loadBitmpaFromMemCache(uri);
        if (bitmap != null) {
            imageView.setImageBitmap(bitmap);
            return;
        }
        Runnable loadBitmapTask = new Runnable() {

            @Override
            public void run() {
                Bitmap bitmap = loadBitmap(uri, reqWidth, reqHeight);
                if (bitmap != null) {
                    LoaderResult result = new LoaderResult(imageView, uri,
                            bitmap);
                    mMainHandler.obtainMessage(MESSAGE_POST_RESULT, result)
                            .sendToTarget();

                }
            }
        };
        THREAD_POOL_EXECUTOR.execute(loadBitmapTask);
    }

從bindBitmap的實現來看,bindBitmap 方法會嘗試從內存緩存中讀取圖片,如果讀取成功就直接返回結果,否則會在線程池中去調用loadBitmap方法,當圖片加載成功后再將圖片、圖片的地址以及需要綁定的imageView封裝成一個LoaderResult對象,然后再通過mMainHandler向主線程發送一個消息,這樣就可以在主線程中給imageView設置圖片了。

下面來看一下,bindBitmap這個方法中用到的線程池和Handler,首先看一下線程池 THREAD_POOL_EXECUTOR 的實現。

private static final int CPU_COUNT = Runtime.getRuntime()
            .availableProcessors();
private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
private static final long KEEP_ALIVE = 10L;


private static final ThreadFactory sThreadFactory = new ThreadFactory() {
        private final AtomicInteger mCount = new AtomicInteger();

        @Override
        public Thread newThread(Runnable r) {
            // TODO Auto-generated method stub
            return new Thread(r, "ImageLoader#" + mCount.getAndIncrement());
        }
    };


public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(
            CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE, TimeUnit.SECONDS,
            new LinkedBlockingDeque<Runnable>(), sThreadFactory);

1.使用線程池和handler的原因。

首先不能用普通線程去實現,如果采用普通線程去加載圖片,隨著列表的滑動可能會產生大量的線程,這樣不利于效率的提升。 Handler 的實現 ,直接采用了 主線程的Looper來構造Handler 對象,這就使得 ImageLoader 可以在非主線程構造。另外為了解決由于View復用所導致的列表錯位這一問題再給ImageView 設置圖片之前會檢查他的url有沒有發生改變,如果發生改變就不再給它設置圖片,這樣就解決了列表錯位問題。

private Handler mMainHandler = new Handler(Looper.getMainLooper()) {

        @Override
        public void handleMessage(Message msg) {
            LoaderResult result = (LoaderResult) msg.obj;
            ImageView imageView = result.imageView;
            imageView.setImageBitmap(result.bitmap);
            String uri = (String) imageView.getTag(TAG_KEY_URI);
            if (uri.equals(result.uri)) {
                imageView.setImageBitmap(result.bitmap);
            } else {
                Log.w(TAG, "set image bitmap,but url has changed , ignored!");
            }
        }

    };

總結:

圖片加載的問題 ,尤其是大量圖片的加載,對于android 開發者來說一直是比較困擾的問題。本文只是提到了最基礎的一種解決方法,用于學習還是不錯的。

 

 

來自:http://www.jianshu.com/p/2319b16d269f

 

 本文由用戶 ljqlovelxy 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!