帶你學習Android圖片緩存機制

本文已授權(quán)微信公眾號:鴻洋(hongyangAndroid)在微信公眾號平臺原創(chuàng)首發(fā)

一. 前言

我們?yōu)槭裁匆獙W習圖片緩存機制?簡單來說,就是幫助用戶省時省流量。當用戶使用RecyclerView或者ListView的時候,頻繁的發(fā)起網(wǎng)絡(luò)請求不僅會消耗大量的流量,還會消耗大量的時間,毫無疑問,這會讓用戶的體驗相當糟糕。雖然Glide等圖片加載框架已經(jīng)替我們處理好了圖片緩存的問題,但是我們?nèi)匀挥斜匾チ私夂蛯W習圖片緩存機制,知其然才能知其所以然。

二. 思路

帶你學習Android緩存機制.png

三. 簡單了解Android圖片緩存機制

在這里,我們只要了解Android圖片的三級緩存機制就行了,何為三級緩存機制?

  1. 內(nèi)存緩存,讀取速度最快。
  2. 硬盤緩存(文件緩存),讀取速度比內(nèi)存緩存稍慢。
  3. 網(wǎng)絡(luò)緩存,讀取速度最慢。

所以,我們正確的圖片的讀取順序應(yīng)該是 內(nèi)存緩存 > 硬盤緩存 > 網(wǎng)絡(luò)緩存,講到這里,我們是不是要手動開始擼碼了?看官別急,我們先講解一下基本的API。

四. 了解一下常用的API

緩存機制的通用調(diào)度算法是LRU(最近最久未使用),不熟悉的同學,可以自行谷歌,本篇不做介紹。與內(nèi)存緩存和硬盤緩存對應(yīng)的類分別是LruCacheDiskLruCache,Android在Android 3.1加入了LruCache緩存類,而DiskLruCache并非谷歌官方編寫,所以我們在寫程序的時候不能直接調(diào)用,好在Jake Wharton大神集成了庫,我們直接用就好了,只要在build.gradle添加如下語句:

implementation 'com.jakewharton:disklrucache:2.0.2'

1.LruCache常用API介紹

方法 簡介
LruCache(int maxSize) 構(gòu)造方法,maxSize是緩存大小
put(@NonNull K key, @NonNull V value) 以鍵值對的方式存入內(nèi)存緩存
get(@NonNull K key) 使用鍵取出存入的值
remove(@NonNull K key) 從內(nèi)存緩存中移除指定鍵的值

2.DiskLruCache常用API介紹

介紹之前,我們需要了解DiskLruCache使用比LruCache復雜,我們不能直接使用構(gòu)造方法直接創(chuàng)建一個DiskLruCache,而是使用open(File directory, int appVersion, int valueCount, long maxSize)這個靜態(tài)方法創(chuàng)建。如果想要將數(shù)據(jù)存入緩存,需要通過一個key獲取到DiskLruCache.Editor對象,然后使用Editor對象獲取輸出流將我們的數(shù)據(jù)存入硬盤緩存,最后使用flush更新journal文件。對于想要深入探究的同學,請移步郭神的Android DiskLruCache完全解析,硬盤緩存的最佳方案

方法 簡介
open(File directory, int appVersion, int valueCount, long maxSize) directory是緩存目錄,appVersion是版本號,valueCount是指定key可以對應(yīng)多個緩存數(shù)量,
get(String key) 返回Snapshot對象,通過調(diào)用該對象的getInputStream(int index)方法可以獲取輸入流
edit(String key) 返回DiskLruCache.Editor對象
DiskLruCache.Editor的newOutputStream(int index) 創(chuàng)建一個輸出流,可以用來存入數(shù)據(jù)
DiskLruCache.Editor的commit() 在使用輸出流緩存數(shù)據(jù)后,使用commit()才會生效
DiskLruCache.Editor的abort() commit()方法相反,使用abort()終止緩存生效
flush() 同步緩存日志到j(luò)ournal文件

這些是我們常用的方法,當然還有計算當前緩存數(shù)據(jù)字節(jié)的size()方法、關(guān)閉DiskLruCache的close()方法和清空緩存的delete()方法等。

五. 手擼代碼

網(wǎng)絡(luò)請求這里我們使用Okhttp,同樣需要在build.gradle中添加一行代碼,如下:

implementation 'com.squareup.okhttp3:okhttp:3.12.1'

1. 布局
布局這里挺簡單,就是一個線性布局里面放一個RecyclerViewRecyclerView子布局里面就是一個ImageView,具體的可以看代碼。
2. GridPhotoAdapter
這個適配器可以說是本文里面最重要的一個類了(需要繼承自RecyclerView.Adapter),我們慢慢往下看。

    // 照片的網(wǎng)絡(luò)路徑
    private String[] urls;
    // 內(nèi)存緩存
    private LruCache<String,Bitmap> mMemoryCache;
    // 硬盤緩存
    private DiskLruCache mDisLruCache;
    // OkhttpClient
    private OkHttpClient okHttpClient;
    // 線程池 用來請求下載圖片
    private ExecutorService service;
    // 主線程Handler 用來圖片下載完成后更新ImageView
    private Handler mHandler;
    private Context mContext;

上面是我們需要用到的實例,作用已經(jīng)在注釋中標注出來了。接下來我們來介紹我們的構(gòu)造函數(shù)和一些初始化工作:

    public GridPhotoAdapter(String[] urls, Handler mHandler, Context context) {
        this.urls = urls;
        this.mHandler = mHandler;
        this.mContext = context;
        init();
    }

    /*
        一些必要的初始化的工作
     */
    private void init() {
        okHttpClient = new OkHttpClient.Builder()
                .build();

        // 構(gòu)建一定數(shù)量的線程池
        service = Executors.newFixedThreadPool(6);

        // 構(gòu)建內(nèi)存緩存
        // 取最大的1/8內(nèi)存作為內(nèi)存緩存
        int maxMemory = (int) Runtime.getRuntime().maxMemory();
        int cacheSize = maxMemory/8;
        mMemoryCache = new LruCache<String,Bitmap>(cacheSize){
            @Override
            protected int sizeOf(@NonNull String key, @NonNull Bitmap value) {
                return value.getByteCount();
            }
        };

        // 構(gòu)建硬盤緩存實例
        File file = getDiskCacheDir(mContext,"photo");
        if(!file.exists())
            file.mkdirs();
        try {
            mDisLruCache = DiskLruCache.open(file,getAppInfoVersion(),1,10*1024*1024);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /*
        根據(jù)傳入的uniqueName獲取唯一的硬盤的緩存路徑
    */
    private File getDiskCacheDir(Context context, String uniqueName) {
        String cachePath;
        if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
                || !Environment.isExternalStorageRemovable()) {
            cachePath = context.getExternalCacheDir().getPath();
        } else {
            cachePath = context.getCacheDir().getPath();
        }
        return new File(cachePath + File.separator + uniqueName);
    }

    /*
        獲取當前程序的應(yīng)用版本號
    */
    private int getAppInfoVersion() {
        try {
            PackageInfo info = mContext.getPackageManager().getPackageInfo(mContext.getPackageName(), 0);
            return info.versionCode;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        return 1;
    }

init()中,我們初始化了okHttpClient、service(線程池)mMemoryCache(內(nèi)存緩存)mDisLruCache(硬盤緩存)。需要注意的是,我們在構(gòu)建硬盤緩存路徑的時候調(diào)用了getDiskCacheDir(Context context, String uniqueName)函數(shù),這個函數(shù)給我們的程序提供了一個緩存地址。介紹完了構(gòu)造函數(shù),我們再來看一下繼承自RecyclerView.Adapter必須要復寫的三個方法:

    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
        View root = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.recycle_item_net_work,viewGroup,false);
        ViewHolder viewHolder = new ViewHolder(root);
        viewHolder.imageView = root.findViewById(R.id.grid_photo);
        // root.setTag(urls[i]);
        return viewHolder;
    }

    public class ViewHolder extends RecyclerView.ViewHolder{
        public ImageView imageView;
        public ViewHolder(@NonNull View itemView) {
            super(itemView);
        }
    }

    @Override
    public int getItemCount() {
        // 返回路徑的長度
        return urls.length;
    }

onCreateViewHolder()getItemCount()很簡單,這里就不再介紹了。這邊我們著重介紹onBindViewHolder()方法:

    @Override
    public void onBindViewHolder(@NonNull ViewHolder viewHolder, int i) {
        ImageView imageView = viewHolder.imageView;
        String url = urls[i];
        // imageView.setTag(url);
        imageView.setImageResource(R.drawable.shape_item_empty);
        loadBitmaps(imageView, url);
    }

在上面的函數(shù)中,我們先找到控件ImageView和路徑url,在真正的搜索圖片緩存之前先設(shè)置一個占位圖,然后到了我們真正進行圖片請求的函數(shù)loadBitmaps(ImageView imageView, String url):

    /**
     * 加載Bitmap對象,如果Bitmap不在LruCache中,就開啟線程去查詢
     *
     * @param imageView 圖片
     * @param url       地址
     */
    private void loadBitmaps(ImageView imageView, String url) {
        Bitmap bitmap = getBitmapFromMemoryCache(url);
        if (bitmap != null) {
            if (imageView != null) {
                imageView.setImageBitmap(bitmap);
            }
        } else {
            service.execute(new ImageRunnable(url,imageView));
        }
    }

    // 添加Bitmap到內(nèi)存緩存中
    private void addBitmapToMemoryCache(Bitmap bitmap, String url) {
        if (getBitmapFromMemoryCache(url) == null)
            mMemoryCache.put(url, bitmap);
    }

loadBitmaps()函數(shù)中,我們先從內(nèi)存緩存中查找是否有該路徑的緩存,有的話就直接放到我們的ImageView中,沒有就利用我們的線程池執(zhí)行一個ImageRunnable,我們再來看看ImageRunnable的代碼:

public class ImageRunnable implements Runnable {
        private String url;
        private Bitmap bitmap;
        private ImageView mView;

        public ImageRunnable(String url,ImageView imageView) {
            this.url = url;
            this.mView = imageView;
        }

        @Override
        public void run() {
            FileDescriptor fileDescriptor = null;
            FileInputStream fileInputStream = null;
            DiskLruCache.Snapshot snapshot = null;
            // 對url進行加密得到key
            final String key = hashKeyForDisk(url);
            // 查找key對應(yīng)的硬盤緩存
            try {
                snapshot = mDisLruCache.get(key);
                if (snapshot == null) {
                    // 如果對應(yīng)的硬盤緩存沒找到,就開始網(wǎng)絡(luò)請求,并且寫入緩存
                    DiskLruCache.Editor editor = mDisLruCache.edit(key);
                    if (editor != null) {
                        OutputStream outputStream = editor.newOutputStream(0);
                        if (downloadImage(url, outputStream)) {
                            editor.commit();
                        } else {
                            editor.abort();
                        }
                    }
                    snapshot = mDisLruCache.get(key);
                }

                if (snapshot != null) {
                    fileInputStream = (FileInputStream) snapshot.getInputStream(0);
                    fileDescriptor = fileInputStream.getFD();
                }
                // 將緩存數(shù)據(jù)解析成Bitmap對象
                if (fileDescriptor != null)
                    bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor);
                if (bitmap != null) {
                    // 將圖片添加到內(nèi)存緩存中
                    addBitmapToMemoryCache(bitmap, url);
                }
                if (bitmap != null)
                    // 在主線程中更新
                    mHandler.post(new Runnable() {
                        @Override
                        public void run() {
                           /* ImageView image = mRecyclerView.findViewWithTag(url);
                            if (image != null)
                                image.setImageBitmap(bitmap);*/
                           mView.setImageBitmap(bitmap);
                        }
                    });
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

這里的邏輯其實也比較簡單,首先通過url得到我們的key,然后利用key去取我們的硬盤緩存,取不到的情況下進行網(wǎng)絡(luò)請求,利用輸出流存到我們的硬盤路徑下面,最后取到我們的硬盤緩存,在主線程中更新我們的ImageView。不要以為我們到此就結(jié)束了,我們的下載圖片downloadImage(final String url, OutputStream outputStream)還沒有看,哈哈~

    /*
        下載圖片
     */
    private boolean downloadImage(final String url, OutputStream outputStream) {
        Request request = new Request.Builder()
                .url(url)
                .build();

        // 執(zhí)行操作
        Call call = okHttpClient.newCall(request);
        Response response = null;
        BufferedInputStream in = null;
        BufferedOutputStream out = null;
        try {
            response = call.execute();
            in = new BufferedInputStream(response.body().byteStream(), 8 * 1024);
            out = new BufferedOutputStream(outputStream, 8 * 1024);
            int b;
            while ((b = in.read()) != -1) {
                out.write(b);
            }
            return true;
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                in.close();
                out.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return false;
    }

利用Okhttp寫的同步下載圖片請求,看代碼就ok。我們再來回顧一下邏輯,用一張流程圖概括吧:

循環(huán)結(jié)構(gòu)流程圖.png

3. NetWorkActivity

 public class NetWorkActivity extends AppCompatActivity {

    public final static String[] imageThumbUrls = new String[]{
            // 路徑省略了 具體的可以看代碼
    };

    private GridPhotoAdapter mAdapter;

    public static void show(Context context) {
        Intent intent = new Intent(context, NetWorkActivity.class);
        context.startActivity(intent);
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_net_work);

        initWidget();
    }

    private void initWidget() {
        RecyclerView mRecyclerView = findViewById(R.id.recycle);
        Handler mHandler = new Handler(Looper.getMainLooper());
        mRecyclerView.setLayoutManager(new GridLayoutManager(this, 3));
        mRecyclerView.setAdapter(mAdapter = new GridPhotoAdapter(imageThumbUrls, mHandler,this));
    }

    @Override
    protected void onPause() {
        super.onPause();
        // 將日志同步到j(luò)ournal文件中
        mAdapter.flushCache();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        // 退出程序時結(jié)束所有的下載任務(wù)
        mAdapter.cancelDownloadImage();
    }
}

這里省略了相關(guān)urls,代碼就是寫RecyclerView的通用代碼,同學們可以自行查看。寫完效果就出來了,如圖:

手機效果圖.png

六. 總結(jié)

通過以上的學習,相信同學們可以對Android圖片緩存機制有了更深入的了解,本人水平有限,如有錯誤,歡迎指出,Over~
地址:
Demo地址
引用:
Android照片墻完整版,完美結(jié)合LruCache和DiskLruCache

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 【Android 庫 Glide】 引用 Android圖片加載框架最全解析(一),Glide的基本用法Andro...
    Rtia閱讀 5,909評論 0 22
  • 7.1 壓縮圖片 一、基礎(chǔ)知識 1、圖片的格式 jpg:最常見的圖片格式。色彩還原度比較好,可以支持適當壓縮后保持...
    AndroidMaster閱讀 2,704評論 0 13
  • 本文主要內(nèi)容出自《Android 開發(fā)藝術(shù)探索》,作為記錄的同時加入個人的理解和思考,同時搜索其它資料和自己動手翻...
    Marker_Sky閱讀 4,115評論 0 14
  • 2016年6月6日,第一篇!
    DOGWiT閱讀 519評論 0 51
  • 今天的課程是認識函數(shù),比我想象的要早了些,努力吧! 首先讓我們來認識函數(shù) 函數(shù)一共有346個,Excel2016又...
    海潔百圖閱讀 580評論 0 0

友情鏈接更多精彩內(nèi)容