Bitmap的加載和Cache

Bitmap的高效加載

核心思想:用 BitmapFactory.Options 來(lái)加載合適尺寸的圖片浓镜。通過(guò)BitmapFactory.Options 設(shè)置采樣率來(lái)壓縮圖片盒蟆。
拿一張1024 * 1024 像素的圖片來(lái)說(shuō)叽奥,假定采用ARGB8888格式存儲(chǔ),那么它占有的內(nèi)存為 1024 * 1024 * 4 Bytes,即4MB蝴乔,如果ImageView的尺寸為512 * 512四啰,那么就沒(méi)必要把全尺寸的圖片加載到ImageView中宁玫。BitmapFactory.Options 中有一個(gè)field叫 inSampleSize,即采樣率柑晒,默認(rèn)值是1撬统,如果設(shè)置該值小于1則無(wú)任何效果,如果設(shè)置為大于1的值敦迄,則縮放后圖片占用內(nèi)存為 1 / (inSampleSize ^ 2)恋追。還用剛才的例子,如果把 1024 * 1024的圖片設(shè)置到 512 * 512 的ImageView中罚屋,只需設(shè)置 inSampleSize = 2:

BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 2;
return BitmapFactory.decodeFromResource(res, resId, options);

怎樣計(jì)算加載圖片占用內(nèi)存的大锌啻选?
圖片占用內(nèi)存的大小取決于3個(gè)變量:

  1. 存儲(chǔ)格式

寬和高很好理解脾猛,就是圖片在橫向和縱向上的像素?cái)?shù)撕彤。存儲(chǔ)格式?jīng)Q定了每個(gè)像素占用多少內(nèi)存。常見(jiàn)的存儲(chǔ)格式為 ARGB8888猛拴,我們就以它為例羹铅。
ARGB8888的意思是 A(Alpha),R(Red)愉昆,G(Green)职员,B(Blue)四個(gè)通道,每個(gè)通道占用8bit跛溉,一共32bit焊切,也就是4byte
此外還有 RGB565,RGB888等等格式芳室,后面的數(shù)字就代表對(duì)應(yīng)的通道所占的bit數(shù)专肪,把所有數(shù)字加起來(lái)就是1個(gè)像素占有的bit數(shù),也就可以算出byte值堪侯。

怎樣計(jì)算采樣率嚎尤?
采樣率的計(jì)算也需要依賴 BitmapFactory.Options。當(dāng) BitmapFactory.Options的 inJustDecodeBounds 參數(shù)設(shè)置為true時(shí)芽死,BitmapFactory只會(huì)解析圖片的原始寬/高信息坪哄,并不會(huì)去真正加載圖片。有了圖片的寬高和ImageView的寬高,我們就可以計(jì)算出合適的采樣率:

    public Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) {
        final BitmapFactory.Options options = new BitmapFactory.Options();
        // 把 inJustDecodeBounds設(shè)為true,BitmapFactory就不會(huì)把圖片加載到內(nèi)存,只會(huì)去計(jì)算圖片的尺寸
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(res, resId, options);
        options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeResource(res, resId, options);
    }

    private int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
        if (reqWidth == 0 || reqHeight == 0) {
            return 1;
        }
        final int height = options.outHeight;
        final int width = options.outWidth;
        int inSampleSize = 1;
        if (height > reqHeight || width > reqHeight) {
            final int halfHeight = height / 2;
            final int halfWidth = width / 2;
            while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
                inSampleSize *= 2;
            }
        }
        return inSampleSize;
    }

Android 中的緩存策略

LruCache
LruCache 是Android提供的緩存類锉矢,實(shí)現(xiàn)LRU算法缠俺,底層用LinkedHashMap來(lái)存儲(chǔ)需要緩存的對(duì)象偿警。這個(gè)類已經(jīng)納入到Android源碼當(dāng)中七嫌,一般被用來(lái)作為內(nèi)存中的緩存使用绍赛。

DiskLruCache
顧名思義腿倚,這個(gè)類實(shí)現(xiàn)的是磁盤(pán)緩存。雖然它得到了Android官方文檔的推薦蚯妇,但目前并不屬于Android SDK的一部分敷燎,所以我們發(fā)現(xiàn)像Glide和OkHttp這些開(kāi)源框架里都用到了這個(gè)類,但都針對(duì)自己的情況作了修改箩言。

ImageLoader的實(shí)現(xiàn)

如果要自己實(shí)現(xiàn)一個(gè)ImageLoader硬贯,那么一般要具備如下功能:

  • 圖片的同步加載
  • 圖片的異步加載
  • 圖片壓縮 (計(jì)算采樣率)
  • 內(nèi)存緩存
  • 磁盤(pán)緩存
  • 網(wǎng)絡(luò)拉取

完整代碼:

package com.anjiawei.httpdemo.bitmap.imageloader;

import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Build;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.StatFs;
import android.support.v4.util.LruCache;
import android.util.Log;
import android.widget.ImageView;


import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import okhttp3.internal.cache.DiskLruCache;



public class ImageLoader {
    private static final String TAG = "ImageLoader";
    private static final int DISK_CACHE_SIZE = 1024 * 1024 * 50;
    private static final int IO_BUFFER_SIZE = 1024;
    private static final int MSG_POST_RESULT = 0;
    private static final int TAG_KEY_URL = 0;

    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
    private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
    private static final int MAX_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(1);

        @Override
        public Thread newThread(Runnable r) {
            return new Thread(r, "ImageLoader#" + mCount.getAndIncrement());
        }
    };
    public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(CORE_POOL_SIZE,
            MAX_POOL_SIZE, KEEP_ALIVE, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(), sThreadFactory);

    private LruCache<String, Bitmap> mMemoryCache;
    private DiskLruCache mDiskLruCache;
    private Context mContext;
    private boolean mIsDiskLruCacheCreated;
    private ImageResizer mImageResizer;
    private Handler mMainHandler = new Handler(Looper.getMainLooper()) {
        @Override
        public void handleMessage(Message msg) {
            LoaderResult result = (LoaderResult) msg.obj;
            ImageView imageView = result.mImageView;
            String url = (String) imageView.getTag(TAG_KEY_URL);
            if (url.equals(result.mUrl)) {
                imageView.setImageBitmap(result.mBitmap);
            } else {
                Log.w(TAG, "set image bitmap, but url has changed, ignored");
            }
        }
    };

    private ImageLoader(Context context) {
        mContext = context.getApplicationContext();
        int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
        int cacheSize = maxMemory / 8;
        mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
            }
        };
        File diskCacheDir = getDiskCacheDir(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();
            }
        }
        mImageResizer = new ImageResizer();
    }

    public static ImageLoader getInstance(Context context) {
        return new ImageLoader(context);
    }

    /**
     * load bitmap from memory cache or disk or network
     * This is a synchronized method
     *
     * @param url
     * @param reqWidth
     * @param reqHeight
     * @return
     */
    public Bitmap loadBitmap(String url, int reqWidth, int reqHeight) {
        Bitmap bitmap = loadBitmapFromMemCache(url);
        if (bitmap != null) {
            Log.d(TAG, "loadBitmapFromMemCache, url = " + url);
            return bitmap;
        }
        try {
            bitmap = loadBitmapFromDiskCache(url, reqWidth, reqHeight);
            if (bitmap != null) {
                Log.d(TAG, "loadBitmapFromDisk, url = " + url);
                return bitmap;
            }
            bitmap = loadBitmapFromHttp(url, reqWidth, reqHeight);
            Log.d(TAG, "loadBitmapFromHttp, url = " + url);
        } catch (IOException e) {
            e.printStackTrace();
        }
        if (bitmap == null && !mIsDiskLruCacheCreated) {
            Log.w(TAG, "encounter error, DiskLruCache is not created");
            bitmap = downloadBitmapFromUrl(url);
        }
        return bitmap;
    }

    /**
     *
     * @param url
     * @param imageView
     * @param reqWidth
     * @param reqHeight
     */
    public void bindBitmap(final String url, final ImageView imageView, final int reqWidth, final int reqHeight) {
        imageView.setTag(TAG_KEY_URL, url);
        Bitmap bitmap = loadBitmapFromMemCache(url);
        if (bitmap != null) {
            imageView.setImageBitmap(bitmap);
            return;
        }
        Runnable loadBitmapTask = new Runnable() {
            @Override
            public void run() {
                Bitmap bitmap = loadBitmap(url, reqWidth, reqHeight);
                if (bitmap != null) {
                    LoaderResult result = new LoaderResult(imageView, url, bitmap);
                    mMainHandler.obtainMessage(MSG_POST_RESULT, result).sendToTarget();
                }
            }
        };
        THREAD_POOL_EXECUTOR.execute(loadBitmapTask);
    }

    private Bitmap downloadBitmapFromUrl(String urlString) {
        Bitmap bitmap = null;
        HttpURLConnection urlConnection = null;
        BufferedInputStream in = null;
        try {
            final URL url = new URL(urlString);
            urlConnection = (HttpURLConnection) url.openConnection();
            in = new BufferedInputStream(urlConnection.getInputStream(), IO_BUFFER_SIZE);
            bitmap = BitmapFactory.decodeStream(in);
        } catch (IOException e) {
            Log.e(TAG, "error in downloadBitmap: " + e);
        } finally {
            if (urlConnection != null) {
                urlConnection.disconnect();
            }
            MyUtils.close(in);
        }
        return bitmap;
    }

    public File getDiskCacheDir(Context context, String uniqueName) {
        boolean externalStorageAvailable = Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
        String cachePath = null;
        if (externalStorageAvailable) {
            cachePath = context.getExternalCacheDir().getPath();
        } else {
            context.getCacheDir().getPath();
        }
        return new File(cachePath + File.separator + uniqueName);
    }

    @TargetApi(Build.VERSION_CODES.GINGERBREAD)
    private long getUsableSpace(File path) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
            return path.getUsableSpace();
        }
        final StatFs stats = new StatFs(path.getPath());
        return stats.getBlockSizeLong() * stats.getAvailableBlocksLong();
    }

    private Bitmap loadBitmapFromMemCache(String url) {
        String key = hashKeyFromUrl(url);
        return mMemoryCache.get(key);
    }

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

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

    private Bitmap loadBitmapFromHttp(String url, int reqWidth, int reqHeight) throws IOException {
        if (Looper.myLooper() == Looper.getMainLooper()) {
            throw new RuntimeException("cannot visit network from UI thread");
        }
        if (mDiskLruCache == null) {
            return null;
        }
        String key = hashKeyFromUrl(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 loadBitmapFromDiskCache(url, reqWidth, reqHeight);
    }

    private Bitmap loadBitmapFromDiskCache(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 = hashKeyFromUrl(url);
        DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
        if (snapshot != null) {
            FileInputStream fileInputStream = (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);
            FileDescriptor fileDescriptor = fileInputStream.getFD();
            bitmap = mImageResizer.decodeSampledBitmapFromFileDescriptor(fileDescriptor, reqWidth, reqHeight);
            if (bitmap != null) {
                addBitmapToMemoryCache(key, bitmap);
            }
        }
        return bitmap;
    }

    public boolean downloadUrlToStream(String urlString, OutputStream outputStream) {
        HttpURLConnection urlConnection = null;
        BufferedOutputStream out = null;
        BufferedInputStream in = null;
        try {
            final URL url = new URL(urlString);
            urlConnection = (HttpURLConnection) url.openConnection();
            in = new BufferedInputStream(urlConnection.getInputStream(), IO_BUFFER_SIZE);
            out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE);
            int b;
            while ((b = in.read()) != -1) {
                out.write(b);
            }
            return true;
        } catch (IOException e) {
            Log.e(TAG, "downloadBitmap failed" + e);
        } finally {
            if (urlConnection != null) {
                urlConnection.disconnect();
            }
            MyUtils.close(in);
            MyUtils.close(out);
        }
        return false;
    }

    private String hashKeyFromUrl(String url) {
        String cacheKey;
        try {
            final MessageDigest mDigest = MessageDigest.getInstance("MD5");
            mDigest.update(url.getBytes());
            cacheKey = bytesToHexString(mDigest.digest());
        } catch (NoSuchAlgorithmException e) {
            cacheKey = String.valueOf(url.hashCode());
        }
        return cacheKey;
    }

    private String bytesToHexString(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < bytes.length; i++) {
            String hex = Integer.toHexString(0xFF & bytes[i]);
            if (hex.length() == 1) {
                sb.append('0');
            }
            sb.append(hex);
        }
        return sb.toString();
    }
}

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市分扎,隨后出現(xiàn)的幾起案子澄成,更是在濱河造成了極大的恐慌,老刑警劉巖畏吓,帶你破解...
    沈念sama閱讀 216,692評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件墨状,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡菲饼,警方通過(guò)查閱死者的電腦和手機(jī)肾砂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,482評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)宏悦,“玉大人镐确,你說(shuō)我怎么就攤上這事”罚” “怎么了源葫?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,995評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)砖瞧。 經(jīng)常有香客問(wèn)我息堂,道長(zhǎng),這世上最難降的妖魔是什么块促? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,223評(píng)論 1 292
  • 正文 為了忘掉前任荣堰,我火速辦了婚禮,結(jié)果婚禮上竭翠,老公的妹妹穿的比我還像新娘振坚。我一直安慰自己,他們只是感情好斋扰,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,245評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布渡八。 她就那樣靜靜地躺著啃洋,像睡著了一般。 火紅的嫁衣襯著肌膚如雪呀狼。 梳的紋絲不亂的頭發(fā)上裂允,一...
    開(kāi)封第一講書(shū)人閱讀 51,208評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音哥艇,去河邊找鬼绝编。 笑死,一個(gè)胖子當(dāng)著我的面吹牛貌踏,可吹牛的內(nèi)容都是我干的十饥。 我是一名探鬼主播,決...
    沈念sama閱讀 40,091評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼祖乳,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼逗堵!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起眷昆,我...
    開(kāi)封第一講書(shū)人閱讀 38,929評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤蜒秤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后亚斋,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體作媚,經(jīng)...
    沈念sama閱讀 45,346評(píng)論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,570評(píng)論 2 333
  • 正文 我和宋清朗相戀三年帅刊,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了纸泡。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,739評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡赖瞒,死狀恐怖女揭,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情栏饮,我是刑警寧澤吧兔,帶...
    沈念sama閱讀 35,437評(píng)論 5 344
  • 正文 年R本政府宣布,位于F島的核電站袍嬉,受9級(jí)特大地震影響掩驱,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜冬竟,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,037評(píng)論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望民逼。 院中可真熱鬧泵殴,春花似錦、人聲如沸拼苍。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,677評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至吆你,卻和暖如春弦叶,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背妇多。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,833評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工伤哺, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人者祖。 一個(gè)月前我還...
    沈念sama閱讀 47,760評(píng)論 2 369
  • 正文 我出身青樓立莉,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親七问。 傳聞我的和親對(duì)象是個(gè)殘疾皇子蜓耻,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,647評(píng)論 2 354

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