天天看点

LruCache与DiskLruCache的使用

在前面的Bitmap文章中提到,Bitmap在使用中非常容易出现OOM,而本节主要介绍2个方法对加载多图/大图的情况进行优化,有效的避免OOM。

1.LruCache缓存

在使用RecyclerView、ListView等加载多图时,屏幕上显示的图片会通过滑动屏幕等事件不断地增加,最终导致OOM。为了保证内存始终维持在一个合理的范围,当item移除屏幕时要对图片进行回收,重新滚入屏幕时又要重新加载;Google官方推荐的是使用LruCache内存缓存技术,完美的解决了上面的问题。

内存缓存技术对大量占用程序应用内存的对象提供快速访问的方法。主要算法:把最近使用的对象用强引用存储在LinkedHashMap中,把最近最少使用的对象在缓存值达到限定时进行移除。

在使用LruCache缓存时应该考虑的因素:

  1. 设备最大为每个程序分配的内存
  2. 图片被访问的频率有多高,如存在个别访问频率高的图片可以考虑使用多个LruCache来区分对象组
  3. 图片的尺寸大小,每张图片占用的内存大小
  4. 设备的屏幕大小和分辨率
  5. 存储多个低像素的图片,而在后台去开线程加载高像素的图片会更加的有效

使用案例:

使用程序内存的1/8作为缓存,向ImageView加载一张图片时,先从LruCache缓存中获取,为空则开启线程去加载;否则直接填充。

private LruCache<String, Bitmap> mMemoryCache;
 
@Override
protected void onCreate(Bundle savedInstanceState) {
	// 获取到可用内存的最大值,使用内存超出这个值会引起OutOfMemory异常。
	// LruCache通过构造函数传入缓存值,以KB为单位。
	int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
	// 使用最大可用内存值的1/8作为缓存的大小。
	int cacheSize = maxMemory / 8;
	mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
		@Override
		protected int sizeOf(String key, Bitmap bitmap) {
			return bitmap.getByteCount() / 1024;
		}
	};
}
 
 /*
 添加Bitmap到内存缓存中去
*/
public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
	if (getBitmapFromMemCache(key) == null) {
		mMemoryCache.put(key, bitmap);
	}
}
 
  /*
 内存缓存中获取对应key的Bitmap
*/
public Bitmap getBitmapFromMemCache(String key) {
	return mMemoryCache.get(key);
}

// 从缓存中删除指定的Bitmap
    public void removeBitmapFromMemory(String key) {
        mMemoryCache.remove(key);
    }

public void loadBitmap(int resId, ImageView imageView) {
	final String imageKey = String.valueOf(resId);
	final Bitmap bitmap = getBitmapFromMemCache(imageKey);
	if (bitmap != null) {
		imageView.setImageBitmap(bitmap);
	} else {
		imageView.setImageResource(R.drawable.image_placeholder);
		BitmapWorkerTask task = new BitmapWorkerTask(imageView);
		task.execute(resId);
	}
}


           

加载的异步任务:

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
	// 在后台加载图片。
	@Override
	protected Bitmap doInBackground(Integer... params) {
		final Bitmap bitmap = decodeSampledBitmapFromResource(
				getResources(), params[0], 100, 100);
		addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
		return bitmap;
	}
}

           

2.DiskLruCache硬盘缓存

第一部分中提到LruCache缓存技术实现了管理内存中图片的存储与释放,如果图片从内存中被移除的话,那么又需要从网络上重新加载一次图片,这显然非常耗时。因此,Google又提出了一个新的方法,使用DiskLruCache对图片进行硬盘缓存。

现在我们大多数加载图片等用的都是第三方的框架如Glide等,会发现它们内部其实使用的也是DiskLruCache,它是如何使用的呢?

先从缓存的位置来说,它可以自定义缓存的路径,默认的路径为

/sdcard/Android/data/<application package>/cache路径

;默认路径的好处:

  1. 存储在SD卡上,不会对内置存储造成影响
  2. 应用程序卸载后,相应的文件也会被删除

以包名为com.wdl.card的APP为例,它的硬盘缓存的路径为:/sdcard/Android/data/com.wdl.card/cache

使用DiskLruCache的标志:一个名为journal的文件,它是DiskLruCache的一个日志文件

使用简介:工具类中包含获取APP版本号/获取缓存文件位置等功能

package com.example.cachedemo;

import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Environment;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.MessageDigest;

/**
 * 项目名:  PhotoWallDemo
 * 包名:    com.example.photowalldemo
 * 创建者:   wdl
 * 创建时间: 2019/2/18 14:39
 * 描述:    TODO
 */
public class Util {
    public static File getDiskCacheDir(Context context, String uniqueName) {
        String cachePath;
        if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
                || !Environment.isExternalStorageRemovable()) {
            //SD卡存在且不可被移除时,调用getExternalCacheDir()获取缓存地址
            //  cachePath = /sdcard/Android/data/<application package>/cache
            cachePath = context.getExternalCacheDir().getPath();
        } else {
            // cachePath = /data/data/<application package>/cache
            cachePath = context.getCacheDir().getPath();
        }
        return new File(cachePath + File.separator + uniqueName);
    }

    /**
     * 网络下载并写入文件
     *
     * @param urlStr ip地址
     * @param os     OutputStream输出流
     * @return 是否写入成功
     */
    public static boolean downUrlToStream(final String urlStr, OutputStream os) {
        HttpURLConnection urlConnection = null;
        BufferedInputStream bis = null;
        BufferedOutputStream bos = null;
        try {
            final URL url = new URL(urlStr);
            urlConnection = (HttpURLConnection) url.openConnection();
            bis = new BufferedInputStream(urlConnection.getInputStream(), 8 * 1024);
            bos = new BufferedOutputStream(os, 8 * 1024);
            int b;
            while ((b = bis.read()) != -1) {
                bos.write(b);
            }
            return true;
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (urlConnection != null) {
                urlConnection.disconnect();
            }
            try {
                if (bos != null) {
                    bos.close();
                }
                if (bis != null) {
                    bis.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return false;
    }


    /**
     * 获取加密key
     *
     * @param key 图片对应的url
     * @return 加密后的url, 即为缓存文件的名称
     */
    public static String hashKeyForDisk(String key) {
        String cacheKey;
        try {
            final MessageDigest messageDigest = MessageDigest.getInstance("MD5");
            messageDigest.update(key.getBytes());
            cacheKey = bytesToHexString(messageDigest.digest());
        } catch (Exception e) {
            return String.valueOf(key.hashCode());
        }
        return cacheKey;
    }

    private static 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();
    }


    /**
     * 获取版本号
     *
     * @param context
     * @return
     */
    public static int getVersionCode(Context context) {
        int versionCode = 0;
        try {
            versionCode = context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionCode;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        return versionCode;
    }

}

           

1.打开缓存 调用其open()方法

public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
           

各个参数的含义如下表:

参数 含义
directory 数据的缓存地址–获取缓存地址(考虑SD卡不存在或者SD卡刚刚被移除)
appVersion app版本号
valueCount 一个Key对应的缓存文件数
maxSize 最多可以缓存多少字节的数据

例子:省略抛出异常

DiskLruCache mDiskLruCache = null;
File cacheDir = getDiskCacheDir(context,'bitmap');
if(!cacheDir.exists()){
	cacheDir.mkdirs();
}
mDiskLruCache = DiskLruCache.open(cacheDir,getVersion(context),1,10*1024*1024);
           

获得DiskLruCache实例后,就可以对其进行缓存的读取/写入/删除了

2.写入

先获取editor的实例后进行操作。

public Editor edit(String key) throws IOException key:代表缓存文件的名称且必须和图片url一一对应(利用MD5进行加密实现)
           
new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    String url = "https://img-my.csdn.net/uploads/201309/01/1378037235_7476.jpg";
                    //获取缓存文件名
                    String key = Util.hashKeyForDisk(url);

                    DiskLruCache.Editor editor = mDiskLruCache.edit(key);
                    if (editor != null) {
                        OutputStream os = editor.newOutputStream(0);
                        if (Util.downUrlToStream(url, os)) {
                            editor.commit();
                        } else {
                            editor.abort();
                        }
                    }
                    mDiskLruCache.flush();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }).start();
           

获取实例后可调用它的newOutputStream(inte index)获取输出流,然后将其传入downloadUrlToStream中,即可实现下载并写入缓存目录

index:由于前面在设置valueCount的时候指定的是1,所以这里index传0就可以了。

写入后必须调用editor.commit()进行提交,调用abort()方法的话则表示放弃此次写入。

3.读取–借助DiskLruCache.get()方法

public synchronized Snapshot get(String key) throws IOException key:缓存文件名  Snapshot为返回值
           

利用 返回值的getInputStream(int index)获取输入流,最后进行显示

4.移除缓存–DiskLruCache.remove(String key)

public synchronized boolean remove(String key) throws IOException
           

5.常用API

size()

获取缓存文件的大小

flush()

将内存中的操作记录同步至日志文件(也就是journal文件)。。比较标准的做法就是在Activity的onPause()方法中去调用一次flush()方法就可以了。

close()

关闭,通常是在destroy中调用,关闭后不可进行操作

delete()

将缓存数据全部删除

journal解读

LruCache与DiskLruCache的使用

第一行 : libcore.io.DiskLruCache固定字符,标志我们使用DiskLruCache技术

第二行 : DiskLruCache的版本号,恒为1

第三行 : open时传入的app版本号

第四行 : open时传入的每个key对应的缓存文件数

第五行 : 空行

第六行 : DIRTTY前缀,后跟缓存图片的key; 每次调用DiskLruCache.edit(String key时)都会产生一条

后一行代表edit的结果,假如editor.commit()产生CLEAN key +该条缓存数据的大小,以字节为单位;缓存成功;假如editor.abort()产生REMOVE key,写入失败,删除;

前面我们所学的size()方法可以获取到当前缓存路径下所有缓存数据的总字节数,其实它的工作原理就是把journal文件中所有CLEAN记录的字节数相加,求出的总合再把它返回而已。

READ key 即调用DiskLruCache.get(String key)时产生的。

DiskLruCache中使用了一个redundantOpCount变量来记录用户操作的次数,每执行一次写入、读取或移除缓存的操作,这个变量值都会加1,当变量值达到2000的时候就会触发重构journal的事件,这时会自动把journal中一些多余的、不必要的记录全部清除掉,保证journal文件的大小始终保持在一个合理的范围内。

3.DiskLruCache与LruCache结合实现照片墙

案例:

RecyclerView的设配器类:内部包含LruCache与DiskLruCache的使用;详见注释

package com.example.cachedemo;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.support.annotation.NonNull;
import android.support.v7.widget.RecyclerView;
import android.util.LruCache;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;

import com.jakewharton.disklrucache.DiskLruCache;

import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;

/**
 * 创建时间: 2019/2/20 9:16
 * 描述:    TODO
 */
public class Adapter extends RecyclerView.Adapter<Adapter.ViewHolder> {

    private List<String> mList;
    private LruCache<String, Bitmap> lruCache;
    private DiskLruCache mDiskLruCache;
    private Set<Task> tasks;
    private RecyclerView rv;
    private Context context;
    private boolean isScrolling = false;

    /**
     * 记录每个子项的高度。
     */
    private List<Integer> hList;//定义一个List来存放图片的height

    public Adapter(List<String> mList, Context context, RecyclerView rv) {
        this.mList = mList;
        this.rv = rv;
        this.context = context;
        tasks = new HashSet<>();

        hList = new ArrayList<>();
        for (int i = 0; i < mList.size(); i++) {
            //每次随机一个高度并添加到hList中
            int height = new Random().nextInt(200) + 300;//[100,500)的随机数
            hList.add(height);
        }
        //初始化内存缓存
        int size = (int) ((Runtime.getRuntime().maxMemory() / 1024) / 8);
        lruCache = new LruCache<String, Bitmap>(size) {
            @Override
            protected int sizeOf(String key, Bitmap value) {
                return value.getByteCount() / 1024;
            }
        };

        //初始化硬盘缓存
        try {
            File cacheDir = Util.getDiskCacheDir(context, "bitmap");
            if (!cacheDir.exists())
                cacheDir.mkdirs();
            mDiskLruCache = DiskLruCache.open(cacheDir,
                    Util.getVersionCode(context), 1, 30 * 1024 * 1024);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    /**
     * 创建viewHolder,将xml传给viewholder
     *
     * @param parent
     * @param viewType
     * @return
     */
    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_view_item, parent, false);
        return new ViewHolder(view);
    }

    @Override
    public void onBindViewHolder(@NonNull ViewHolder viewHolder, int position) {
        final String urlStr = mList.get(position);
        //设置tag
        viewHolder.imageView.setTag(urlStr);
        //设置占位 防止图片错位
        viewHolder.imageView.setImageResource(R.drawable.ic_launcher_background);
        //设置宽高
        FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(context
                .getResources().getDisplayMetrics().widthPixels / 3, hList.get(position));
        viewHolder.imageView.setLayoutParams(params);

            loadBitmaps(viewHolder.imageView, urlStr);
    }

    public void setScrolling(boolean scrolling) {
        isScrolling = scrolling;
        notifyDataSetChanged();
    }

    /**
     * 加载图片
     *
     * @param imageView ImageView
     * @param urlStr    先从内存缓存中找,找不到,从硬盘缓存中找,找不到,网络下载并加载(存储硬盘缓存),存储到内存缓存
     */
    private void loadBitmaps(ImageView imageView, String urlStr) {
        Bitmap bitmap = getBitmapFromMemoryCache(urlStr);
        //根据滑动的状态判断是否加载图片
        if (bitmap == null&&!isScrolling) {
            Task task = new Task();
            tasks.add(task);
            task.execute(urlStr);
        } else {
            if (imageView != null) {
                imageView.setImageBitmap(bitmap);
            }
        }
    }


    @Override
    public int getItemCount() {
        return mList.size();
    }


    /**
     * 添加内存缓存中不存在的Bitmap
     *
     * @param key    键
     * @param bitmap 值
     */
    private void addBitmapToLruCache(String key, Bitmap bitmap) {
        if (getBitmapFromMemoryCache(key) == null)
            lruCache.put(key, bitmap);
    }

    /**
     * 从内存缓存中获取键为key的Bitmap
     *
     * @param key 键
     * @return Bitmap
     */
    private Bitmap getBitmapFromMemoryCache(String key) {
        return lruCache.get(key);
    }

    /**
     * 取消所有正在下载或等待下载的任务。
     */
    public void cancelAllTasks() {
        if (tasks != null) {
            for (Task task : tasks) {
                task.cancel(false);
            }
        }
    }


    /**
     * 将缓存记录同步到journal文件中。
     */
    public void flushCache() {
        if (mDiskLruCache != null) {
            try {
                mDiskLruCache.flush();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }


    class ViewHolder extends RecyclerView.ViewHolder {
        ImageView imageView;

        public ViewHolder(@NonNull View itemView) {
            super(itemView);
            imageView = itemView.findViewById(R.id.iv_image);
        }
    }


    class Task extends AsyncTask<String, Void, Bitmap> {

        private String imageUrl;

        @Override
        protected Bitmap doInBackground(String... strings) {
            imageUrl = strings[0];
            FileDescriptor fd = null;
            FileInputStream fis = null;
            DiskLruCache.Snapshot snapshot = null;
            try {
                //生成key
                final String key = Util.hashKeyForDisk(imageUrl);
                snapshot = mDiskLruCache.get(key);
                //如果未找到缓存,则从网络上下载并存储至缓存中
                if (snapshot == null) {
                    DiskLruCache.Editor editor = mDiskLruCache.edit(key);
                    if (editor != null) {
                        OutputStream os = editor.newOutputStream(0);
                        if (Util.downUrlToStream(imageUrl, os)) {
                            editor.commit();
                        } else {
                            editor.abort();
                        }
                    }
                    //缓存被写入后再次从缓存中查找
                    snapshot = mDiskLruCache.get(key);
                }
                if (snapshot != null) {
                    fis = (FileInputStream) snapshot.getInputStream(0);
                    fd = fis.getFD();
                }
                //将缓存数据加载成Bitmap
                Bitmap bitmap = null;
                if (fd != null) {
                    bitmap = BitmapFactory.decodeFileDescriptor(fd);
                }
                if (bitmap != null) {
                    //将bitmap写入内存缓存中去
                    addBitmapToLruCache(imageUrl, bitmap);
                }
                return bitmap;
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    if (fd == null && fis != null)
                        fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            return null;
        }

        @Override
        protected void onPostExecute(Bitmap bitmap) {
            super.onPostExecute(bitmap);
            ImageView imageView = rv.findViewWithTag(imageUrl);
            if (bitmap != null && imageView != null) {
                imageView.setImageBitmap(bitmap);
            }
            tasks.remove(this);

        }
    }
}

           

MainActivity:使用

package com.example.cachedemo;

import android.support.annotation.NonNull;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.StaggeredGridLayoutManager;
import android.util.Log;
import android.view.ViewTreeObserver;

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;

import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.List;

import static android.support.v7.widget.RecyclerView.SCROLL_STATE_DRAGGING;
import static android.support.v7.widget.RecyclerView.SCROLL_STATE_IDLE;

public class MainActivity extends AppCompatActivity {

    private RecyclerView rv;
    private Adapter adapter;
    @Override
    protected void onPause() {
        super.onPause();
        adapter.flushCache();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        // 退出程序时结束所有的下载任务
        adapter.cancelAllTasks();
    }
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        rv = findViewById(R.id.recycler_view);
        //设置瀑布流,2列竖直
        StaggeredGridLayoutManager layoutManager = new StaggeredGridLayoutManager(3,
                StaggeredGridLayoutManager.VERTICAL);
        //解决item跳动
       // layoutManager.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_NONE);
        rv.setLayoutManager(layoutManager);

        rv.setItemAnimator(null);
        adapter = new Adapter(Arrays.asList(Common.urls),this,rv);
        rv.setAdapter(adapter);

        //添加recycler view 滚动状态的监听,控制是否加载图片
        rv.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
                super.onScrollStateChanged(recyclerView, newState);
                switch (newState){
                    case SCROLL_STATE_IDLE:
                        //滑动停止
                        adapter.setScrolling(false);
                        break;
                    case SCROLL_STATE_DRAGGING:
                        //正在滚动
                        adapter.setScrolling(true);
                        break;

                }

            }

            @Override
            public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);
            }
        });
//        Gson gson = new Gson();
//        List<Entity> mList = gson.fromJson(Common.s, new TypeToken<List<Entity>>() {
//        }.getType());
//        StringBuilder str = new StringBuilder();
//        for (Entity entity : mList) {
//            str.append("\"").append(entity.getUrl()).append("\",").append("\n");
//        }
//        Log.e("wdl",str.toString());

//        Entity[] entities = gson.fromJson(Common.s,new TypeToken<Entity>() {
//        }.getType());
//        for (Entity entity : entities) {
//            str.append(entity.getUrl()).append("\n");
//        }
//        Log.e("wdl",str.toString());
    }
}

           

效果展示:

LruCache与DiskLruCache的使用

onScrollStateChanged()回调方法的主要功能。

优化之一,在RecyclerView子项滚动时禁止加载图片,停止滑动时开始加载图片。

主要参考了郭神的文章,感谢大佬

案例下载:https://download.csdn.net/download/qq_34341338/10975828