Android 圖片加載框架的簡(jiǎn)單設(shè)計(jì)

目前Android 發(fā)展至今優(yōu)秀的圖片加載框架太多,例如: Volley ,Picasso,Imageloader,Glide等等。但是作為程序猿,懂得其中的實(shí)現(xiàn)原理還是相當(dāng)重要的,只有懂得才能更好地使用。于是乎,今天我就簡(jiǎn)單設(shè)計(jì)一個(gè)網(wǎng)絡(luò)加載圖片框架。主要就是熟悉圖片的網(wǎng)絡(luò)加載機(jī)制。


一般來說,一個(gè)優(yōu)秀的 圖片加載框架(ImageLoader) 應(yīng)該具備如下功能:

  • 圖片壓縮
  • 內(nèi)存緩存
  • 磁盤緩存
  • 圖片的同步加載
  • 圖片的異步加載
  • 網(wǎng)絡(luò)拉取

那我們就從以上幾個(gè)方面進(jìn)行介紹:

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

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

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.內(nèi)存緩存和磁盤緩存

緩存直接選擇 LruCache 和 DiskLruCache 來完成內(nèi)存緩存和磁盤緩存工作。
首先對(duì)其初始化:

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

public ImageLoader(Context context) {
        mContext = context.getApplicationContext();
      //分配內(nèi)存緩存為當(dāng)前進(jìn)程的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();
            }
        }
    }

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

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

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

相對(duì)來說內(nèi)存緩存比較簡(jiǎn)單,而磁盤緩存則復(fù)雜的多。磁盤緩存(LruDiskCache)并沒有直接提供方法來實(shí)現(xiàn),而是要通過Editor以及Snapshot 來實(shí)現(xiàn)對(duì)于文件系統(tǒng)的添加以及讀取的操作。
首先看一下,Editor,它提供了commit 和 abort 方法來提交和撤銷對(duì)文件系統(tǒng)的寫操作。

//將下載的圖片寫入文件系統(tǒng),實(shí)現(xiàn)磁盤緩存
    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, 通過它可以獲取磁盤緩存對(duì)象對(duì)應(yīng)的 FileInputStream,但是FileInputStream 無法便捷的進(jìn)行壓縮,所以通過FileDescriptor 來加載壓縮后的圖片,最后將加載后的bitmap添加到內(nèi)存緩存中。

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.同步加載

同步加載的方法需要外部在子線程中調(diào)用。

//同步加載
    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;
    }

從方法中可以看出工作過程遵循如下幾步:
首先嘗試從內(nèi)存緩存中讀取圖片,接著嘗試從磁盤緩存中讀取圖片,最后才會(huì)從網(wǎng)絡(luò)中拉取。此方法不能再主線程中執(zhí)行,執(zhí)行環(huán)境的檢測(cè)是在loadBitmapFromHttp中實(shí)現(xiàn)的。

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的實(shí)現(xiàn)來看,bindBitmap 方法會(huì)嘗試從內(nèi)存緩存中讀取圖片,如果讀取成功就直接返回結(jié)果,否則會(huì)在線程池中去調(diào)用loadBitmap方法,當(dāng)圖片加載成功后再將圖片、圖片的地址以及需要綁定的imageView封裝成一個(gè)LoaderResult對(duì)象,然后再通過mMainHandler向主線程發(fā)送一個(gè)消息,這樣就可以在主線程中給imageView設(shè)置圖片了。


下面來看一下,bindBitmap這個(gè)方法中用到的線程池和Handler,首先看一下線程池 THREAD_POOL_EXECUTOR 的實(shí)現(xiàn)。

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的原因。

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

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

    };

總結(jié):
圖片加載的問題 ,尤其是大量圖片的加載,對(duì)于android 開發(fā)者來說一直是比較困擾的問題。本文只是提到了最基礎(chǔ)的一種解決方法,用于學(xué)習(xí)還是不錯(cuò)的。

最后結(jié)尾不多說,直接上demo:
自定義圖片加載框架--運(yùn)用MVP+Retrofit+Rxjava的應(yīng)用架構(gòu)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,606評(píng)論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,582評(píng)論 3 418
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,540評(píng)論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,028評(píng)論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,801評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,223評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,294評(píng)論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,442評(píng)論 0 289
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,976評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,800評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,996評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,543評(píng)論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,233評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,926評(píng)論 1 286
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,702評(píng)論 3 392
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,991評(píng)論 2 374

推薦閱讀更多精彩內(nèi)容