结合Bitmap三级缓存自己做个ImageLoader 解决UI卡顿问题

本文深入探讨图片缓存机制,包括三级缓存策略、内存与磁盘缓存的实现,以及图片异步加载器的设计。通过优化UI卡顿,确保流畅的用户体验。

前言

Bitmap三连

结合Bitmap三级缓存自己做个ImageLoader 解决UI卡顿问题
Android之带你从源码解析Bitmap占用内存正确的计算公式
自己动手写Bitmap高效加载 跟OOM说再见

在Android开发中图片下载和内存的使用是永远绕不开的话题,页面的加载离不开图片的使用,图片的使用必会占用一定的内存,但是手机内存总是有限的,只要你一点使用不当,就会给APP造成非常差的使用体验;所以怎么合适的对图片和内存进行一个合理的搭配就是一个重点了,图片缓存的实现能很好的解决这一个矛盾,现在图片相关的开源框架很多,比如Glide,Fresco,Picasso等,它们都实现了很好的图片缓存策略,但是如果你自己实现怎么做呢,今天就来实践一下

本文所含代码随时更新,可从这里下载最新代码
传送门

演示(因为GIF录制帧数的原因,演示效果没有真实情况下的那么流畅)

在这里插入图片描述

三级缓存

现在流行的一般是三级缓存机制,即

  • 第一级:内存缓存(从内存中加载图片,速度最快,不浪费流量,但是会消耗手机运行内存)
  • 第二级:磁盘缓存,或者说文件缓存(从本地加载图片,速度快,不浪费流量,但是会消耗磁盘容量,不过可以忽略这点,毕竟现在磁盘容量都很大了)
  • 第三级:网络缓存(从网络加载图片,速度慢,浪费流量)

使用逻辑是:每次使用图片时,先从内存缓存中取出,如果有即使用,没有就从磁盘缓存中取出,如果有即使用并添加到内存缓存中,没有就从网络下载,下载完成后添加到磁盘缓存和内存缓存中

内存缓存

LRU算法

这里使用Android自带的LruCache,从这个名字可以看出使用的是LRU(Least Recently Used)算法,即最近最少使用算法;
它的核心是存储最近添加最后使用的图片,当你往里继续添加要缓存的元素时,如果预先设置的缓存容量满了,那就剔除掉那些最久添加最少使用的缓存元素

LruCache类支持泛型,内部维护了一个LinkedHashMap,可以接受多种key和value

public class LruCache<K, V> {
    private final LinkedHashMap<K, V> map;
}

这里你会不会有疑虑,为什么使用LinkedHashMap而不是使用HashMap

虽然LinkedHashMap和HashMap都是实现Map接口,同时LinkedHashMap还继承自HashMap,但是它俩有个最大的区别:HashMap中添加的元素是无序存放的,但是LinkedHashMap内部维护了一个双向链表,可以控制元素的迭代顺序,该迭代顺序可以是插入顺序,也可以是访问顺序,相当于是将所有Entry节点链入一个双向链表的HashMap

也正是因为这个特性,所以LinkedHashMap能很好的支持LRU算法

辅助类

/**
 * @Description TODO(内存缓存)
 * @author cxy
 * @Date 2018/11/13 9:48
 */
public class MemoryLruCache {

    private String TAG = MemoryLruCache.class.getSimpleName();

    private LruCache<String,Bitmap> mMemoryCache;

    private MemoryLruCache instance;
    private MemoryLruCache(){
        //虚拟机能获得的最大内存
        long maxMemory = Runtime.getRuntime().maxMemory();
        //内存缓存所使用的内存
        int cache = (int) (maxMemory / 8);
        mMemoryCache = new LruCache<String,Bitmap>(cache){

            //重写两个方法

            //计算一张图片所占内存
            @Override
            protected int sizeOf(String key, Bitmap value) {

                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {    //API 19
                    int size = value.getAllocationByteCount();
                    Log.e(TAG,"sizeOf size="+size);
                    return size;
                }
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) {//API 12
                    return value.getByteCount();
                }
                // 在低版本中使用 Bitmap所占用的内存空间数等于Bitmap的每一行所占用的空间数乘以Bitmap的行数
                return value.getRowBytes() * value.getHeight();
            }

            //回收内存
            @Override
            protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
                super.entryRemoved(evicted, key, oldValue, newValue);
                Log.e(TAG,"entryRemoved");
                if (oldValue != null && !oldValue.isRecycled()) {
                    oldValue.recycle();
                }
            }
        };

    }

    public MemoryLruCache getInstance(){
        if (instance == null) {
            instance = new MemoryLruCache();
        }
        return instance;
    }

    /**
     * 从内存中取出图片
     * @param key 通常是图片下载地址
     * @return
     */
    public Bitmap getBitmap(String key){
        if (TextUtils.isEmpty(key)) return null;
        return mMemoryCache.get(key);
    }

    /**
     * 将bitmap保存到内存
     * @param key
     * @param bitmap
     */
    public void putBitmap(String key,Bitmap bitmap){
        mMemoryCache.put(key,bitmap);
    }

}

这里有几个注意点:

  • 这里使用单例模式,这样全局都使用同一份缓存
  • 在构建LruCache实例的时候需要指定缓存大小,通常是JVM虚拟机能获得的最大内存的八分之一
  • 重写sizeOf方法,返回一张图片所占内存值,为了适配不同版本,使用不同API
  • 重写entryRemoved方法,因为这些图片都是存在内存中,当保存的图片超过缓存容量,就会从LruCache中的LinkedHashMap中去除掉,但是还占用内存的,我们需要将它回收掉,释放内存

这里其实有一个值的计算比较重要,那就是内存缓存空间到底应该多少,毕竟设置大了就可能导致内存浪费,设置小了又会导致图片频繁回收;我这里选用的是一种通用的算法,如果你觉得不好,但是自己又想不到该怎么计算,没关系,去看看一些开源图片框架怎么计算这个值的,比如Glide

// Package private to avoid PMD warning.
  MemorySizeCalculator(MemorySizeCalculator.Builder builder) {
    this.context = builder.context;

    arrayPoolSize =
        isLowMemoryDevice(builder.activityManager)
            ? builder.arrayPoolSizeBytes / LOW_MEMORY_BYTE_ARRAY_POOL_DIVISOR
            : builder.arrayPoolSizeBytes;
    int maxSize =
        getMaxSize(
            builder.activityManager, builder.maxSizeMultiplier, builder.lowMemoryMaxSizeMultiplier);

    int widthPixels = builder.screenDimensions.getWidthPixels();
    int heightPixels = builder.screenDimensions.getHeightPixels();
    int screenSize = widthPixels * heightPixels * BYTES_PER_ARGB_8888_PIXEL;

    int targetBitmapPoolSize = Math.round(screenSize * builder.bitmapPoolScreens);

    int targetMemoryCacheSize = Math.round(screenSize * builder.memoryCacheScreens);
    int availableSize = maxSize - arrayPoolSize;

    if (targetMemoryCacheSize + targetBitmapPoolSize <= availableSize) {
      memoryCacheSize = targetMemoryCacheSize;
      bitmapPoolSize = targetBitmapPoolSize;
    } else {
      float part = availableSize / (builder.bitmapPoolScreens + builder.memoryCacheScreens);
      memoryCacheSize = Math.round(part * builder.memoryCacheScreens);
      bitmapPoolSize = Math.round(part * builder.bitmapPoolScreens);
    }

    if (Log.isLoggable(TAG, Log.DEBUG)) {
      Log.d(
          TAG,
          "Calculation complete"
              + ", Calculated memory cache size: "
              + toMb(memoryCacheSize)
              + ", pool size: "
              + toMb(bitmapPoolSize)
              + ", byte array size: "
              + toMb(arrayPoolSize)
              + ", memory class limited? "
              + (targetMemoryCacheSize + targetBitmapPoolSize > maxSize)
              + ", max size: "
              + toMb(maxSize)
              + ", memoryClass: "
              + builder.activityManager.getMemoryClass()
              + ", isLowMemoryDevice: "
              + isLowMemoryDevice(builder.activityManager));
    }
  }

可以看到Glide是根据每个APP具体内存情况,屏幕分辨率和系统版本计算出一个合理的值

这里还有一个问题,需要图片缓存的通常是有列表这种页面,每个item都含有图片,没有缓存需要大量重复的网络请求;但是缓存过后不再停留在这个页面,用户到别的页面了,或者所在的Activity销毁了,那这个缓存就需要清除,但是LruCache没有提供清空LinkedHashMap和回收其中的bitmap方法,且LinkedHashMap的定义又是private的,那这里只能通过反射去清空缓存了

	/**
     * 通过反射剔除缓存中的bitmap
     * 回收bitmap内存
     * @param urls 需要清除的value对应的key 可以为null
     */
    public void cleanCache(String[] urls){

        try {
            Class classType = Class.forName("android.util.LruCache");

            Field field = classType.getDeclaredField("map");
            field.setAccessible(true);
            LinkedHashMap<String,Bitmap> map = (LinkedHashMap<String, Bitmap>) field.get(mMemoryCache);
            if (map == null) return;

            Iterator<Map.Entry<String,Bitmap>>  iterator = map.entrySet().iterator();
            while (iterator.hasNext()) {

                Map.Entry<String,Bitmap> entry = iterator.next();
                Bitmap bit = entry.getValue();

                if (urls != null && urls.length > 0) {
                    for (int i=0; i<urls.length; i++) {
                        if (TextUtils.equals(entry.getKey(),urls[i])) {
                            if (bit != null && !bit.isRecycled()) {
                                bit.recycle();
                            }
                            iterator.remove();
                            break;
                        }
                    }
                } else {
                    if (bit != null && !bit.isRecycled()) {
                        bit.recycle();
                    }
                    iterator.remove();
                }
            }

        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

磁盘缓存

这种缓存通常情况下是将文件保存在SD卡上,不要保存到内置存储中,因为内置存储内存空间太宝贵了,不要浪费;但是保存到外置存储(一般是SD卡)也不要随便放到一个目录,如果你允许第三方应用操作你的数据,那可以随便找个目录存储;除此之外,通常将数据放在外置存储的私有缓存目录,如:

/storage/sdcard0/Android/data/包名/cache
/storage/sdcard0/Android/data/包名/cache/imagecache

辅助类

/**
 * @Description TODO(磁盘缓存)
 * @author cxy
 * @Date 2018/11/14 11:18
 */
public class DiskLruCache{

    private String TAG = DiskCache.class.getSimpleName();

    private Context mContext;
    private File cachePath;

    private static DiskCache instance;
    private DiskCache(Context context) {
        this.mContext = context;
        cachePath = FileStorageTools.getInstance(mContext).getExternalStoragePrivateCache();
    }
    public static DiskCache getInstance(Context context){
        if (instance == null) {
            instance = new DiskCache(context);
        }
        return instance;
    }

    /**
     * 设置私有缓存目录
     * @param pathName 次级目录名称 比如
     *                 /imagecache
     *                 /httpcache   
     */
    public void setCachePath(String pathName){
        if (TextUtils.isEmpty(pathName)) return ;
        cachePath = null;
        cachePath = new File(cachePath.getAbsolutePath()+pathName);
        cachePath.mkdirs();
    }

    public void putFileStream(String url, InputStream is){
        FileStorageTools.getInstance(mContext).putStreamToExternalStorage(cachePath,encryptUrl(url),is);
    }

    public void putBitmap(String url, Bitmap bitmap){
        FileStorageTools.getInstance(mContext).putBitmapToExternalStorage(cachePath,encryptUrl(url),bitmap);
    }

    public Bitmap getBitmap(String url,int inSamplesize){
        byte[] data = FileStorageTools.getInstance(mContext).getDataFromExternalStorage(cachePath.getAbsolutePath()+File.separator+encryptUrl(url));
        if (data == null) return null;
        return BitmapTools.byte2Bitmap(data,inSamplesize);
    }


    /**
     * 将url使用md5加密作为文件名
     * md5加密是不可逆加密,防止资源盗用
     * @param url
     * @return
     */
    private String encryptUrl(String url){

        if (TextUtils.isEmpty(url)) return null;

        String result = "";
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] msg = md.digest(url.getBytes());
            for (byte b:msg) {
                String temp = Integer.toHexString(b & 0xff);
                if (temp.length() == 1) {
                    temp = "0" + temp;
                }
                result += temp;
            }
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return result;
    }
}

这里面的FileStorageTools工具类可以在博主前面讲解Android数据存储文章中找到

这里有的同学可能考虑到这里只是存数据和取数据,那要不要删数据呢;其实存放在外置存储的私有目录的数据在App卸载的时候会被删除掉,而且现在手机SD卡内存容量都是64G,128G甚至更多,可以满足缓存容量需要;不过本着为用户考虑,还是需要提供删除的方法的

	/**
     * 清除指定目录缓存文件
     * @param path 缓存目录
     *             如果是null,默认为cachePath.getAbsolutePath()
     */
    public void cleanAllCache(final String path){
        LocalThreadPools.THREAD_POOL_EXECUTOR.execute(new Runnable() {
            @Override
            public void run() {
                if (TextUtils.isEmpty(path)) {
                    FileStorageTools.getInstance(mContext).delFile(cachePath.getAbsolutePath());
                } else {
                    FileStorageTools.getInstance(mContext).delFile(path);
                }
                
            }
        });
    }

这是手动清除缓存,或者说提供一个按钮给用户清除

文件更新逻辑

有人可能会开始提需求了,上面做的只是存文件,而且是一直存下去,那能不能对这些文件进行一些动态管理操作,让缓存空间或者缓存时间控制在一个合理的范围呢?不用担心,这是可以做到的

  • 按访问顺序删除文件:我这里以时间进行排序,保存文件和修改文件都会自动更新修改时间,但是我们需要做的是在获取文件的时候也去更新文件的更新时间

    file.setLastModified(System.currentTimeMillis());
    

    这样当缓存空间达到一定值的时候,就可以删除那些更早时间的文件

  • 按时间删除文件:有时候你缓存文件是没有达到指定缓存大小,因为需要缓存的文件少呀;但是这时候可能出现的情况是:你的文件是上个月的,甚至去年的,那这些可能就是垃圾文件了,有必要删除的

这时候我们就需要派LinkedHashMap上场了

	/**
     * 存放文件路径和时间信息
     * 有两种清除睡眠文件方法:
     *                      可根据时间删除文件
     *                      也可根据访问顺序删除文件
     */
    private LinkedHashMap<String, Long> map;

	map = new LinkedHashMap<>(0, 0.75f, true);

  1. map的key是文件路径,因为需要唯一确定一个文件
  2. value是文件更新时间,后续可以通过时间删除文件
  3. 构造方法第三个参数传入true,这样map中的元素就会以访问顺序保存,方便后续将那些访问最少的文件删除掉

然后需要提前设置缓存空间,缓存时间

	//默认缓存空间大小 100M
    private long FILE_CACHE_SIZE = 1024 * 1024 * 100;
    //文件保留时间 默认保存一个月内的缓存文件
    private int FARTHEST_TIME_FROM_NOW = 30 * 1;

接下来就是往map里填充值了,这里一定要注意先将文件数组按修改时间排序好,再往map里存

private void getFileMsg(){

        List<File> files = FileStorageTools.getInstance(mContext).listFile(cachePath.getAbsolutePath());
        if (files == null) return;

        File[] fi = FileStorageTools.getInstance(mContext).sortFile(files,true);
        for (int i=0; i<fi.length; i++){
            File f = fi[i];
            map.put(f.getAbsolutePath(),f.lastModified());
        }
    }

接下来一定要注意在保存文件和获取文件的时候更新map里面对应的值,然后就进入主题了

	/**
     * 修正缓存目录文件
     * 超出预设缓存大小 就删除那些早期文件
     * 早于文件保留时间跨度 删除
     */
    public void reviseCacheFile(){

        Calendar calendar = Calendar.getInstance();
        calendar.set(Calendar.DAY_OF_YEAR,FARTHEST_TIME_FROM_NOW);
        farthestDate = calendar.getTime().getTime();

        Iterator<Map.Entry<String, Long>> entrys = map.entrySet().iterator();
        while (entrys.hasNext()) {
            Map.Entry<String, Long> entry = entrys.next();
            String key = entry.getKey();
            long value = entry.getValue();

            /**
             * 先判断缓存空间是否超出预设值,这是最重要的
             * 因为map里面的顺序是按时间先后存放的,最先迭代出来的总是更新时间最久远的
             */
            if (cachePath.length() > FILE_CACHE_SIZE) {
                File file = new File(key);
                file.delete();
                entrys.remove();
                continue;
            }

            /**
             * 再判断文件更新时间是否早于预设时间
             * 如果早于预设时间 删除
             */
            if (value < farthestDate) {
                File file = new File(key);
                file.delete();
                entrys.remove();
                continue;
            }
        }

    }

这里主要分为两步

  • 先判断缓存目录的大小有没有超出预设值,这是最重要的,如果超出,哪个文件时间最早就把它删了
  • 接着判断文件时间是否早于预设的时间范围,要是早于那就删除

图片异步加载器

有了内存缓存和文件缓存,那我们可以自己做一个简单的图片加载器
一般来说一个好的ImageLoader要做到

  • 图片的同步加载
  • 图片的异步加载
  • 图片压缩处理
  • 图片三级缓存机制

那我们就按照这几点来做

/**
 * @Description TODO(结合缓存机制异步加载图片)
 * @author cxy
 * @Date 2018/11/14 11:18
 */
public class EasyImageLoader{

    private String TAG = AsyncImageLoader.class.getSimpleName();

    private final int LOAD_IMAGE_BITMAP = 1000;
    private final int LOAD_IMAGE_ERROR = 2000;

    private WeakReference<Context> mContext;

    private MemoryLruCache mMemoryCache;
    private DiskLruCache mDiskCache;

    private int errorLoadId = -1;
    private int loadingId = -1;

    private static AsyncImageLoader imageLoader;
    private AsyncImageLoader(Context context) {
        this.mContext = new WeakReference<>(context);
        mMemoryCache = new MemoryLruCache();
        mDiskCache = new DiskLruCache(context);
    }

    public static AsyncImageLoader getInstance(Context context){
        if (imageLoader == null) {
            imageLoader = new AsyncImageLoader(context);
        }
        return imageLoader;
    }

    public AsyncImageLoader setErrorLoadView(int resourceID){
        errorLoadId = resourceID;
        return this;
    }

    public AsyncImageLoader setLoadingView(int loadingId){
        this.loadingId = loadingId;
        return this;
    }

    public AsyncImageLoader setMemoryCache(int cacheSize){
        mMemoryCache.setMemoryCache(cacheSize);
        return this;
    }

    public AsyncImageLoader setFarthestTime(int days){
        mDiskCache.setFarthestTime(days);
        return this;
    }

    public AsyncImageLoader setCacheSize(long size){
        mDiskCache.setCacheSize(size);
        return this;
    }

    public AsyncImageLoader setCachePath(String pathName){
        mDiskCache.setCachePath(pathName);
        return this;
    }

    private boolean loadMemoryBitmap(ImageView view, String imgUrl){

        if (loadingId != -1) {
            view.setBackgroundResource(loadingId);
        }

        Bitmap bitmap = mMemoryCache.getBitmap(imgUrl);
        if (bitmap != null) {
            view.setImageBitmap(bitmap);
            return true;
        }
        return false;
    }

    private boolean loadDiskBitmap(ImageView view, String imgUrl, int targetWidth, int targetHeight){
        Bitmap bitmap = mDiskCache.getBitmap(imgUrl, targetWidth,targetHeight);
        if (bitmap != null) {
            sendMessage(view,bitmap,imgUrl);
            mMemoryCache.putBitmap(imgUrl,bitmap);
            return true;
        }
        return false;
    }

    /**
     * 加载图片
     * @param view
     * @param imageUrl
     * @param targetWidth
     * @param targetHeight
     */
    public void loadImage(final ImageView view, final String imageUrl, final int targetWidth, final int targetHeight) {

        //从内存缓存获取
        if (loadMemoryBitmap(view,imageUrl)) {
            return;
        }

        LocalThreadPools.THREAD_POOL_EXECUTOR.execute(new Runnable() {
            @Override
            public void run() {
                //从磁盘读取
                if (loadDiskBitmap(view,imageUrl,targetWidth,targetHeight)) {
                    return ;
                }
                //从网络下载
                try {
                    URL url = new URL(imageUrl);
                    HttpURLConnection connection = (HttpURLConnection) url.openConnection();
                    connection.connect();
                    mDiskCache.putFileStream(imageUrl,connection.getInputStream());

                    loadDiskBitmap(view,imageUrl,targetWidth,targetHeight);
                } catch (MalformedURLException e) {
                    if (errorLoadId != -1) {
                        Message message = mHandler.obtainMessage();
                        message.what = LOAD_IMAGE_ERROR;
                        message.obj = view;
                        mHandler.sendMessage(message);
                    }
                    e.printStackTrace();
                } catch (IOException e) {
                    if (errorLoadId != -1) {
                        Message message = mHandler.obtainMessage();
                        message.what = LOAD_IMAGE_ERROR;
                        message.obj = view;
                        mHandler.sendMessage(message);
                    }
                    e.printStackTrace();
                }

            }
        });
    }

    private void sendMessage(ImageView view, Bitmap bitmap, String imageUrl){
        Message message = mHandler.obtainMessage();
        message.what = LOAD_IMAGE_BITMAP;
        message.obj = view;
        Bundle data = new Bundle();
        data.putParcelable("bitmap",bitmap);
        data.putString("url",imageUrl);
        message.setData(data);
        mHandler.sendMessage(message);
    }

    private Handler mHandler = new Handler(){

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);

            int what = msg.what;
            switch (what) {
                case LOAD_IMAGE_BITMAP:
                    ImageView view = (ImageView) msg.obj;
                    Bundle bundle = msg.getData();
                    Bitmap bitmap = bundle.getParcelable("bitmap");
                    String imageUrl = bundle.getString("url");
                    if (bitmap == null || view.getTag() == null) return;
                    if (TextUtils.equals((String)view.getTag(),imageUrl)) {
                        view.setImageBitmap(bitmap);
                    }
                    break;
                case LOAD_IMAGE_ERROR:
                    ImageView v = (ImageView) msg.obj;
                    v.setBackgroundResource(errorLoadId);
                    break;
            }
        }
    };
    
    public void cleanCache(String[] urls){
        mMemoryCache.cleanCache(urls);
    }
}

加载图片逻辑就是先从内存缓存中取,如果没有从磁盘缓存中取,都没有就从网络下载,然后再从磁盘加载,最后添加到内存缓存中;这样形成了一个简单的图片加载工具,当然了还有很多Bitmap的加载方法没有写,准备放到下一篇关于Bitmap的详细解析文章中

可以看到我这里使用的异步加载是用线程池实现的(同时结合Handler达到更新UI的效果),为什么呢?因为如果直接new Thread,数据量小还好,如果数据量大的话,那这样大量new内存,手机是吃不消的;还有一个原因是如果使用AsyncTask,就没办法实现并发需求(关于AsyncTask的解析可以参考博主之前的文章带你从源码掌握AsyncTask工作原理(为什么串行执行 为什么内存泄漏)),显然是不行的

全局线程池

/**
 * @Description TODO(全局使用的线程池)
 * @author cxy
 * @Date 2018/11/14 17:22
 */
public class LocalThreadPools {

    public static final Executor THREAD_POOL_EXECUTOR;

    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
    private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT-1,4));
    private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2;
    private static final int KEEP_ALIVE_SECONDS = 16;

    private static final ThreadFactory sThreadFactory = new ThreadFactory() {
        private final AtomicInteger mCount = new AtomicInteger(1);

        public Thread newThread(Runnable r) {
            return new Thread(r, "MangoTask #" + mCount.getAndIncrement());
        }
    };

    private static final BlockingQueue<Runnable> sPoolWorkQueue = new LinkedBlockingQueue<>(16);

    static {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,
                sPoolWorkQueue, sThreadFactory){
            @Override
            public void execute(Runnable command) {
                //如果激活的线程过多就return,可以在这里提醒用户
                if(getActiveCount()+1 >= MAXIMUM_POOL_SIZE){
                    return;
                }
                super.execute(command);
            }
        };
        threadPoolExecutor.allowCoreThreadTimeOut(true);
        THREAD_POOL_EXECUTOR = threadPoolExecutor;
    }


}

接下来在Adapter中就很方便使用了

final String imgUrl = list[position];
// 给 ImageView 设置一个 tag
holder.img.setTag(imgUrl);
// 预设一个图片
holder.img.setImageResource(R.mipmap.ic_launcher);

if (!TextUtils.isEmpty(imgUrl)) {
	AsyncImageLoader.getInstance(context).loadImage(holder.img, imgUrl, 
											DisplayTools.dp2px(context,40),DisplayTools.dp2px(context,40));
}

优化UI卡顿

做到这里其实基本上没有卡顿情况了,不过还是有待优化的地方;一般情况下UI列表卡顿造成的原因包含两个

  • 在主线程做了太多耗时操作,比如在adapter中直接在主线程加载图片,就很容易导致卡顿,这个上面异步加载解决了
  • 优化内存使用,其实这点上面还没做的更好,因为上面没有控制异步任务的执行频率;假如用户恶意的快速滑动屏幕,在短时间内会造成大量的异步任务在执行,且伴随着大量的UI操作,但是UI操作是在主线程执行,这样势必会在一定情况下造成UI卡顿,解决办法其实很简单:在用户滑动的时候停止异步任务,在用户停止滑动的时候再执行,这样只会产生当前屏幕所包含的定量的异步任务

不管是ListView还是RecyclerView等,都只用设置OnScrollListener监听,然后起个标志位判断下即可


/**
 * Author:Mangoer
 * Time:2018/11/17 17:40
 * Version:
 * Desc:TODO()
 */
public class RecycleAdapter extends RecyclerView.Adapter<RecycleAdapter.ViewHolder> {

    private Context mContext;
    private String[] list;

    private boolean isShouldBeLoaded = true;

    public RecycleAdapter(Context mContext) {
        this.mContext = mContext;
    }

    public void setList(String[] list) {
        this.list = list;
    }

    public void setmRecyvlerView(RecyclerView mRecyvlerView) {
        mRecyvlerView.addOnScrollListener(scrollListener);
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View content = LayoutInflater.from(mContext).inflate(R.layout.list_item,parent,false);
        ViewHolder viewHolder = new ViewHolder(content);
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {

        final String imgUrl = list[position];
        // 给 ImageView 设置一个 tag
        holder.view.setTag(imgUrl);
        // 预设一个图片
        holder.view.setImageResource(R.mipmap.ic_launcher);
        if (!TextUtils.isEmpty(imgUrl) && isShouldBeLoaded) {
            AsyncImageLoader.getInstance(mContext).loadImage(holder.view, imgUrl, DisplayTools.dp2px(mContext,80),DisplayTools.dp2px(mContext,80));
        }
    }

    @Override
    public int getItemCount() {
        if (list == null) return 0;
        return list.length;
    }

    class ViewHolder extends RecyclerView.ViewHolder{

        ImageView view;

        public ViewHolder(View itemView) {
            super(itemView);
            view = (ImageView) itemView.findViewById(R.id.userimage);
        }
    }

    RecyclerView.OnScrollListener scrollListener = new RecyclerView.OnScrollListener() {
        @Override
        public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
            super.onScrollStateChanged(recyclerView, newState);
            if (SCROLL_STATE_IDLE == newState) {
                isShouldBeLoaded = true;
                notifyDataSetChanged();
            } else {
                isShouldBeLoaded = false;
            }
        }

        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
            super.onScrolled(recyclerView, dx, dy);
        }
    };
}

这样我们就能省掉大量的非必要的异步操作,节省内存开销

到此关于图片缓存及加载告一段落,然后关于Bitmap的内存占用放在下一篇博客Android之带你从源码解析Bitmap占用内存正确的计算公式

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值