• 由LruCache和DiskLruCache提供三级缓存支持的ImageLoader


    从三天前一直报错到今天中午,总算出了个能用的版本了。

    一如既往先发链接:

    https://github.com/mlxy/ImageLoader

    缓存处理

    ·LruCacheHelper:

    封装第一级缓存,也就是内存缓存的处理。

    LruCache是Android自带的缓存处理类,如名字所说,和使用软引用的映射相比,优势在于可以忽略缓存上限处理的细节问题,初始化时在构造函数中给一个缓存上限即可。一般做法是使用最大内存的八分之一:

    Runtime.getRuntime().maxMemory() / 8

    但是我觉得八分之一实在太少,所以干脆给了三分之一。

    另外在初始化时需要重写LruCache类的sizeOf方法来自行计算图片的大小并返回,默认情况返回的是图片数量。

    封装类给出四个接口,分别是打开和关闭,保存和读取。

    没什么好说的,直接放代码。

     1 public class LruCacheHelper {
     2     private LruCacheHelper() {}
     3 
     4     private static LruCache<String, Bitmap> mCache;
     5 
     6     /** 初始化LruCache。 */
     7     public static void openCache(int maxSize) {
     8         mCache = new LruCache<String, Bitmap>((int) maxSize) {
     9             @Override
    10             protected int sizeOf(String key, Bitmap value) {
    11                 return value.getRowBytes() * value.getHeight();
    12             }
    13         };
    14     }
    15 
    16     /** 把图片写入缓存。 */
    17     public static void dump(String key, Bitmap value) {
    18         mCache.put(key, value);
    19     }
    20 
    21     /** 从缓存中读取图片数据。 */
    22     public static Bitmap load(String key) {
    23         return mCache.get(key);
    24     }
    25 
    26     public static void closeCache() {
    27         // 暂时没事干。
    28     }
    29 }
    LruCacheHelper

    ·DiskLruCacheHelper:

    DiskLruCache工具的使用以及这个类的基本介绍可以参考我前两天写的基于Demo解析缓存工具DiskLruCache

    为了适应这个工程的需要对这个封装类做了一点变动,直接保存和读取Bitmap。

    依然没什么好说的,直接看代码。

     1 public class DiskLruCacheHelper {
     2     private DiskLruCacheHelper() {}
     3 
     4     private static DiskLruCache mCache;
     5 
     6     /** 打开DiskLruCache。 */
     7     public static void openCache(Context context, int appVersion, int maxSize) {
     8         try {
     9             if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
    10                     || !Environment.isExternalStorageRemovable()) {
    11                 mCache = DiskLruCache.open(context.getExternalCacheDir(), appVersion, 1, maxSize);
    12             } else {
    13                 mCache = DiskLruCache.open(context.getCacheDir(), appVersion, 1, maxSize);
    14             }
    15         } catch (IOException e) { e.printStackTrace(); }
    16     }
    17 
    18     /** 写出缓存。 */
    19     public static void dump(Bitmap bitmap, String keyCache) throws IOException {
    20         if (mCache == null) throw new IllegalStateException("Must call openCache() first!");
    21 
    22         DiskLruCache.Editor editor = mCache.edit(Digester.hashUp(keyCache));
    23 
    24         if (editor != null) {
    25             OutputStream outputStream = editor.newOutputStream(0);
    26             boolean success = bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream);
    27 
    28             if (success) {
    29                 editor.commit();
    30             } else {
    31                 editor.abort();
    32             }
    33         }
    34     }
    35 
    36     /** 读取缓存。 */
    37     public static Bitmap load(String keyCache) throws IOException {
    38         if (mCache == null) throw new IllegalStateException("Must call openCache() first!");
    39 
    40         DiskLruCache.Snapshot snapshot = mCache.get(Digester.hashUp(keyCache));
    41 
    42         if (snapshot != null) {
    43             InputStream inputStream = snapshot.getInputStream(0);
    44             Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
    45             return bitmap;
    46         }
    47 
    48         return null;
    49     }
    50 
    51     /** 检查缓存是否存在。 */
    52     public static boolean hasCache(String keyCache) {
    53         try {
    54             return mCache.get(Digester.hashUp(keyCache)) != null;
    55         } catch (IOException e) {
    56             e.printStackTrace();
    57         }
    58 
    59         return false;
    60     }
    61 
    62     /** 同步日志。 */
    63     public static void syncLog() {
    64         try {
    65             mCache.flush();
    66         } catch (IOException e) { e.printStackTrace(); }
    67     }
    68 
    69     /** 关闭DiskLruCache。 */
    70     public static void closeCache() {
    71         syncLog();
    72     }
    73 }
    DiskLruCacheHelper

    ImageLoader主类

    从接口说起,类依然是四个接口,初始化,关闭,载入图片,取消载入。

    载入分三步,逐级访问三级缓存。

    ·一:

    首先使用内存缓存的封装类调取内存缓存,如果内存中有,就直接显示。

    /** 从内存缓存中加载图片。 */
    private boolean loadImageFromMemory(View parent, String url) {
        Bitmap bitmap = LruCacheHelper.load(url);
        if (bitmap != null) {
            setImage(parent, bitmap, url);
            return true;
        }
    
        return false;
    }

    返回一个标志位用以判断是否已经加载成功。

    如果没成功就要访问第二级缓存也即磁盘缓存了,使用封装类检查缓存存在与否,之后分成两个分支。

    ·二:

    如果磁盘缓存已经存在了,就启动读取磁盘缓存的任务。

    启动时记得把任务加入一个HashMap中,用于在被外部中断或程序执行结束时取消任务。

    任务使用Android自带的AsyncTask异步任务类。

    编写一个类,继承AsyncTask并指定泛型。

    class LoadImageDiskCacheTask extends AsyncTask<String, Void, Bitmap>

    泛型第一位是启动任务时传入的参数类型,我们这里要传入的是图片的URL,所以用String。

    这个参数在用AsyncTask.execute(Params...)启动任务时传入,在继承AsyncTask类必须重写的抽象方法doInBackground(Params...)中接收。

    第二位是进度的类型。在任务执行的过程中,可以调用publishProgress(Progress)方法不断更新任务进度,比如已下载的文件大小或者已经删除的文件数量之类。

    之后重写onProgressUpdate(Progress)方法,在进度更新时做出相应处理,比如修改进度条的值。

    在这里我们不需要进度的处理,所以直接给Void,注意V大写。

    泛型第三位就是任务结束后返回的结果的类型了。

    重写onPostExecute(Result)方法,参数就是doInBackground方法返回的结果。在这里接收图片并显示就可以了。

    注意,doInBackground方法是在新线程中执行,而onPostExecute是在主线程中执行的,这也是这个类高明的地方之一,使用AsyncTask类从头至尾都不需要手动处理线程问题,只需要关注业务逻辑。

    之后可能研究一下这个类再单独写一篇博文。

    在磁盘缓存读取成功之后我们也在内存缓存中保存一份。

    ·三:

    如果没有磁盘缓存,比如第一次打开应用程序的时候,就需要从网络上重新下载图片了。

    依旧是继承AsyncTask类,在doInBackground方法中联网下载图片,下载成功后分别保存到磁盘缓存和内存缓存,之后再onPostExecute方法中显示图片。

    逻辑和第二步是一样的。

    ·显示图片:

    但是。

    如果就这么不管不顾地开始用,比如用在一个纯图片的ListView中,就会发现在滑动ListView的时候有时图片会显示不出来,有时还会不停闪烁。

    问题就出在多线程上。

    如果使用Google官方推荐的ListView优化方式,也就是在列表适配器中的getView方法里复用convertView

    if (convertView == null) {
        imageView = (ImageView) View.inflate(MainActivity.this, R.layout.image_view, null);
    
        convertView = imageView;
    } else {
        imageView = (ImageView) convertView;
    }

    的话,由于读取图片需要一定的时间,当图片读取完毕时,传给ImageLoader的那个ImageView可能已经不是当初的那个ImageView了。

    我在解决这个问题时,发现网上多数的建议是给ImageView绑定URL作为Tag,然后在显示图片时检查Tag和URL是否一致,不一致就不显示。

    但是不显示明显不行啊。

    我的解决办法是改变思路。

    在调用ImageLoader.load时传入的不是符合直觉的ImageView和URL,而是getView的第三个参数,ImageView的父视图parent和URL,到了显示图片的时候再在主线程中用View.findViewWithTag方法来现场获取ImageView并设置图片。

    这样就成功地避免了图片的显示错位。

    ·OutOfMemory异常:

    其实这个异常在正常情况下不是很容易出现了,这里只提供一个思路。

    给ListView绑定RecyclerListener,实现onMovedToScrapHeap(View)方法,这个方法在列表项移出屏幕外时会被调用,我们在这个方法中取消图片的加载任务,始终保持只加载屏幕内的图片,基本就不会出现内存不够用的情况了。

    当然,如果图片实在太大,那就要在解析Bitmap的时候配合Options来自行缩放图片大小,那就是另一回事了。

    最后还是代码说话:

      1 public class ImageLoader {
      2     private static final int MEMORY_CACHE_SIZE_LIMIT =
      3             (int) (Runtime.getRuntime().maxMemory() / 3);
      4     private static final int LOCAL_CACHE_SIZE_LIMIT =
      5             100 * 1024 * 1024;
      6 
      7     private static final int NETWORK_TIMEOUT = 5000;
      8 
      9     private HashMap<String, AsyncTask> taskMap = new HashMap<>();
     10 
     11     public ImageLoader(Context context) {
     12         initMemoryCache();
     13         initDiskCache(context);
     14     }
     15 
     16     /** 初始化内存缓存器。 */
     17     private void initMemoryCache() {
     18         LruCacheHelper.openCache(MEMORY_CACHE_SIZE_LIMIT);
     19     }
     20 
     21     /** 初始化磁盘缓存器。 */
     22     private void initDiskCache(Context context) {
     23         int appVersion = 1;
     24         try {
     25             appVersion = context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionCode;
     26         } catch (PackageManager.NameNotFoundException e) {
     27             e.printStackTrace();
     28         }
     29 
     30         DiskLruCacheHelper.openCache(context, appVersion, LOCAL_CACHE_SIZE_LIMIT);
     31     }
     32 
     33     /** 载入图片。
     34      *  @param parent 要显示图片的视图的父视图。
     35      *  @param url 要显示的图片的URL。
     36      * */
     37     public void load(View parent, String url) {
     38         // 尝试从内存缓存载入图片。
     39         boolean succeeded = loadImageFromMemory(parent, url);
     40         if (succeeded) return;
     41 
     42         boolean hasCache = DiskLruCacheHelper.hasCache(url);
     43         if (hasCache) {
     44             // 有磁盘缓存。
     45             loadImageFromDisk(parent, url);
     46         } else {
     47             // 联网下载。
     48             loadFromInternet(parent, url);
     49         }
     50     }
     51 
     52     /** 取消任务。 */
     53     public void cancel(String tag) {
     54         AsyncTask removedTask = taskMap.remove(tag);
     55         if (removedTask != null) {
     56             removedTask.cancel(false);
     57         }
     58     }
     59 
     60     /** 从内存缓存中加载图片。 */
     61     private boolean loadImageFromMemory(View parent, String url) {
     62         Bitmap bitmap = LruCacheHelper.load(url);
     63         if (bitmap != null) {
     64             setImage(parent, bitmap, url);
     65             return true;
     66         }
     67 
     68         return false;
     69     }
     70 
     71     /** 从磁盘缓存中加载图片。 */
     72     private void loadImageFromDisk(View parent, String url) {
     73         LoadImageDiskCacheTask task = new LoadImageDiskCacheTask(parent);
     74         taskMap.put(url, task);
     75         task.execute(url);
     76     }
     77 
     78     /** 从网络上下载图片。 */
     79     private void loadFromInternet(View parent, String url) {
     80         DownloadImageTask task = new DownloadImageTask(parent);
     81         taskMap.put(url, task);
     82         task.execute(url);
     83     }
     84 
     85     /** 把图片保存到内存缓存。 */
     86     private void putImageIntoMemoryCache(String url, Bitmap bitmap) {
     87         LruCacheHelper.dump(url, bitmap);
     88     }
     89 
     90     /** 把图片保存到磁盘缓存。 */
     91     private void putImageIntoDiskCache(String url, Bitmap bitmap) throws IOException {
     92         DiskLruCacheHelper.dump(bitmap, url);
     93     }
     94 
     95     /** 重新设置图片。 */
     96     private void setImage(final View parent, final Bitmap bitmap, final String url) {
     97         parent.post(new Runnable() {
     98             @Override
     99             public void run() {
    100                 ImageView imageView = findImageViewWithTag(parent, url);
    101                 if (imageView != null) {
    102                     imageView.setImageBitmap(bitmap);
    103                 }
    104             }
    105         });
    106     }
    107 
    108     /** 根据Tag找到指定的ImageView。 */
    109     private ImageView findImageViewWithTag(View parent, String tag) {
    110         View view = parent.findViewWithTag(tag);
    111         if (view != null) {
    112             return (ImageView) view;
    113         }
    114 
    115         return null;
    116     }
    117 
    118     /** 读取图片磁盘缓存的任务。 */
    119     class LoadImageDiskCacheTask extends AsyncTask<String, Void, Bitmap> {
    120         private final View parent;
    121         private String url;
    122 
    123         public LoadImageDiskCacheTask(View parent) {
    124             this.parent = parent;
    125         }
    126 
    127         @Override
    128         protected Bitmap doInBackground(String... params) {
    129             Bitmap bitmap = null;
    130 
    131             url = params[0];
    132             try {
    133                 bitmap = DiskLruCacheHelper.load(url);
    134 
    135                 if (bitmap != null && !isCancelled()) {
    136                     // 读取完成后保存到内存缓存。
    137                     putImageIntoMemoryCache(url, bitmap);
    138                 }
    139             } catch (IOException e) {
    140                 e.printStackTrace();
    141             }
    142 
    143             return bitmap;
    144         }
    145 
    146         @Override
    147         protected void onPostExecute(Bitmap bitmap) {
    148             // 显示图片。
    149             if (bitmap != null) setImage(parent, bitmap, url);
    150             // 移除任务。
    151             if (taskMap.containsKey(url)) taskMap.remove(url);
    152         }
    153     }
    154 
    155     /** 下载图片的任务。 */
    156     class DownloadImageTask extends AsyncTask<String, Void, Bitmap> {
    157         private final View parent;
    158         private String url;
    159 
    160         public DownloadImageTask(View parent) {
    161             this.parent = parent;
    162         }
    163 
    164         @Override
    165         protected Bitmap doInBackground(String... params) {
    166             Bitmap bitmap = null;
    167 
    168             url = params[0];
    169             try {
    170                 // 下载并解析图片。
    171                 InputStream inputStream = NetworkAdministrator.openUrlInputStream(url, NETWORK_TIMEOUT);
    172                 bitmap = BitmapFactory.decodeStream(inputStream);
    173 
    174                 if (bitmap != null && !isCancelled()) {
    175                     // 保存到缓存。
    176                     putImageIntoMemoryCache(url, bitmap);
    177                     putImageIntoDiskCache(url, bitmap);
    178                 }
    179             } catch (IOException e) {
    180                 e.printStackTrace();
    181             }
    182 
    183             return bitmap;
    184         }
    185 
    186         @Override
    187         protected void onPostExecute(Bitmap bitmap) {
    188             // 显示图片。
    189             if (bitmap != null) setImage(parent, bitmap, url);
    190             // 移除任务。
    191             if (taskMap.containsKey(url)) taskMap.remove(url);
    192         }
    193     }
    194 
    195     /** 使用完毕必须调用。 */
    196     public void close() {
    197         for (Map.Entry<String, AsyncTask> entry : taskMap.entrySet()) {
    198             entry.getValue().cancel(true);
    199         }
    200 
    201         DiskLruCacheHelper.closeCache();
    202         LruCacheHelper.closeCache();
    203     }
    204 }
    ImageLoader

    碎碎念

    我怎么觉得我今天行文风格有点异常……

  • 相关阅读:
    微分方程概述
    Vite 使用TSX/JSX
    java去掉html标签,只留文本内容
    设置gradle默认缓存文件路径(笔记)
    mysql 求年龄
    sql 工作记录1
    windows脚本创建桌面快捷图标方式
    vue命名规范
    span做成按钮时,文字不被选中样式
    Vue3 + Vite + TS项目引入iconfont图标(Svg方式)
  • 原文地址:https://www.cnblogs.com/chihane/p/4538845.html
Copyright © 2020-2023  润新知