美文网首页
自定义安卓图片懒加载

自定义安卓图片懒加载

作者: 小城哇哇 | 来源:发表于2023-06-06 16:44 被阅读0次

    最近整理了公司有关图片加载代码,这部分代码也不知道当时怎么想的,自己写了一套图片懒加载控件,我是觉得这应该用一些稳定的图片加载开源库,比如 Glide 之类的,毕竟这些开源库有那么多人的多年维护,用起来不会有很多暗病,最近整理这些图片加载的代码真是弄的心力交瘁。

    一直改不是办法,想着应该也不难,就自己动手写了一个,下面看看吧!

    实现思路

    这里整理了一下图片懒加载的一个过程,实际就是下载到显示,当然我这写的思路仅供参考:

    1. 初始化
    2. 设置 (默认图、取内存缓存)
    3. 加载 (取本地缓存、下载、存本地缓存)
    4. 处理 (创建Bitmap、压缩)
    5. 缓存 (内存缓存)
    6. 更新 (主线程更新)

    简单说下,初始化就是设置一些数据,设置就是设置图片链接,我把默认图和内存缓存图写一起了,放在里面,加载就是取文件,处理是从文件到 bitmap 并加上其他处理,缓存是到内存缓存,更新是主线程更新,这里还有一层本地缓存,但是我不想全部写在这里,耦合性太高,后面使用一个专门的文件处理类来实现。

    具体实现

    通过上面思路,实际只要将各个部分解耦开来,一步步实现就好,这里代码也不多,我就直接全部贴出来了,看下面:

    import android.content.Context;
    import android.graphics.Bitmap;
    import android.support.v4.util.Consumer;
    import android.support.v7.widget.AppCompatImageView;
    import android.util.AttributeSet;
    import android.util.Log;
    
    import java.io.File;
    import java.util.LinkedHashMap;
    import java.util.Map;
    
    /**
     * @author: silence
     * @date: 2021-05-27
     * @description: 简单图片懒加载
     */
    
    public class LazyImageView extends AppCompatImageView {
    
        //图片链接
        private String mUrl = "";
    
        //默认图片
        private static Bitmap sDefaultBitmap;
    
        //失败图片
        private static Bitmap sErrorBitmap;
    
        //文件处理工具
        private static IFileHelper sFileHelper;
    
        //图片处理工具
        private static IBitmapHelper sBitmapHelper;
    
        //内存缓存 - 10条,自动删除最老数据,Bitmap会自动回收
        private final static Map<String, Bitmap> sBitmapCache = new LinkedHashMap<String, Bitmap>() {
            protected boolean removeEldestEntry(Map.Entry<String, Bitmap> eldest) {
                return size() >= 10;
            }
        };
    
        public LazyImageView(Context context) {
            super(context);
        }
    
        public LazyImageView(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        public LazyImageView(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
        }
    
        //初始化
        public static void init(Bitmap defaultBitmap, Bitmap errorBitmap, IFileHelper fileHelper, IBitmapHelper bitmapHelper) {
            //BitmapFactory.decodeResource(getResources(), R.mipmap.img_def);
            LazyImageView.sDefaultBitmap = defaultBitmap;
            LazyImageView.sErrorBitmap = errorBitmap;
            LazyImageView.sFileHelper = fileHelper;
            LazyImageView.sBitmapHelper = bitmapHelper;
        }
    
    
        //设置
        public void show(String url) {
            Log.d("TAG", "show: " + url);
            if (!mUrl.equals(url)) {
                mUrl = url;
                //取内存缓存,无内存缓存设为默认图
                display(null != sBitmapCache.get(url) ? sBitmapCache.get(url) : sDefaultBitmap);
                //加载链接
                load(file -> {
                    if (null != file) {
                        Bitmap bitmap = handle(file);
                        cache(bitmap);
                        display(bitmap);
                    } else {
                        display(sErrorBitmap);
                    }
                });
            }
        }
    
    
        //加载
        private void load(Consumer<File> resultHandler) {
            Log.d("TAG", "load: ");
            sFileHelper.download(mUrl, resultHandler);
        }
    
    
        //处理
        private Bitmap handle(File file) {
            Log.d("TAG", "handle: ");
            return sBitmapHelper.handle(file);
        }
    
    
        //缓存
        private void cache(Bitmap bitmap) {
            Log.d("TAG", "cache: ");
            sBitmapCache.put(mUrl, bitmap);
        }
    
    
        //显示
        private void display(Bitmap bitmap) {
            Log.d("TAG", "display: ");
            this.post(()-> setImageBitmap(bitmap));
        }
    
        //文件处理,解耦,如有需要重写 download 即可
        public interface IFileHelper {
            void download(String url, Consumer<File> resultHandle);
        }
    
        //图片处理,解耦,如有需要重写handle函数
        public interface IBitmapHelper {
            Bitmap handle(File file);
        }
    
    }
    
    
    

    简单说明

    简单说明一下,实际这里的代码和上面思路完全一致,处理构造函数,剩下的就是六个步骤对应的函数,这里写了两个接口,用来处理获取文件和处理 bitmap,具体实现可以根据需要另做处理。

        //默认图片
        private static Bitmap sDefaultBitmap;
    
        //失败图片
        private static Bitmap sErrorBitmap;
    
    

    上面是默认显示图片和加载失败图片的 bitmap,写成了类变量,节省内存占用。

        //内存缓存 - 10条,自动删除最老数据,Bitmap会自动回收
        private final static Map<String, Bitmap> sBitmapCache = new LinkedHashMap<String, Bitmap>() {
            protected boolean removeEldestEntry(Map.Entry<String, Bitmap> eldest) {
                return size() >= 10;
            }
        };
    
    

    内存缓存使用了 LinkedHashMap,主要使用它的移除老元素功能,内存缓存我不希望过多,但是页面经常刷新的时候,快速复用已有 bitmap 还是很有必要的。

        //初始化
        public static void init(Bitmap defaultBitmap, Bitmap errorBitmap, IFileHelper fileHelper, IBitmapHelper bitmapHelper) {
            //BitmapFactory.decodeResource(getResources(), R.mipmap.img_def);
            LazyImageView.sDefaultBitmap = defaultBitmap;
            LazyImageView.sErrorBitmap = errorBitmap;
            LazyImageView.sFileHelper = fileHelper;
            LazyImageView.sBitmapHelper = bitmapHelper;
        }
    
    

    在 init 函数总对类变量就行赋值,写成了静态函数,全局只需要设置一次即可。

    BitmapFactory.decodeResource(getResources(), R.mipmap.img_def);
    
    

    对于从图片资源 id 到 bitmap 可以通过上面方法实现,需要在 context 环境中执行,我不想耦合进来,所以在 init 函数前自行转换吧。

        //设置
        public void show(String url) {
            Log.d("TAG", "show: " + url);
            if (!mUrl.equals(url)) {
                mUrl = url;
                //取内存缓存,无内存缓存设为默认图
                display(null != sBitmapCache.get(url) ? sBitmapCache.get(url) : sDefaultBitmap);
                //加载链接
                load(file -> {
                    if (null != file) {
                        Bitmap bitmap = handle(file);
                        cache(bitmap);
                        display(bitmap);
                    } else {
                        display(sErrorBitmap);
                    }
                });
            }
        }
    
    

    可以看到,实际上主要逻辑都是在 show 函数中实现的,这里将其他函数组合起来,因为 load 函数中需要在异步线程中处理,所以传递了一个 Consumer 进去,这里 Consumer 要用旧版本的 Consumer 不然需要安卓 v24 以上才能用。通过 Lambda 表达式,我们对拿到 load 函数完成后的结果,成功则缓存、显示,失败则显示失败的 bitmap。至于缓存和显示里面的代码,应该不用解释了,很简单。

    实现获取文件

    上面代码中我们只设置了一个接口来获取文件,下面我们来实现该接口:

    /**
     * @author: silence
     * @date: 2021-05-27
     * @description: 简单文件处理工具
     */
    
    public class FileHelper implements LazyImageView.IFileHelper {
    
        //缓存路径,应用默认储存路径
        private static final String CACHE_DIR =
                "/data" + Environment.getDataDirectory().getAbsolutePath() + "/" +
                        getApplication().getPackageName() + "/cache/";
    
        //缓存大小
        private static final int BUFFER_SIZE = 1024;
    
        //线程池
        final ExecutorService threadPool = Executors.newFixedThreadPool(8);
    
        //相同链接的锁, 这里用LinkedHashMap限制一下储存的数量
        private final Map<String, Semaphore> mUrlLockMap =
                new LinkedHashMap<String, Semaphore>() {
                    protected boolean removeEldestEntry(Map.Entry<String, Semaphore> eldest) {
                        return size() >= 64 * 0.75;
                    }
                };
    
        //下载文件
        public void download(String url, Consumer<File> resultHandler) {
    
            threadPool.execute(()-> {
                File downloadFile = null;
    
                //空路径
                if (TextUtils.isEmpty(url)) {
                    //注意使用旧版本Consumer
                    resultHandler.accept(null);
                    return;
                }
    
                //检查本地缓存
                downloadFile = new File(getLocalCacheFileName(url));
                if (downloadFile.exists()) {
                    resultHandler.accept(downloadFile);
                    return;
                }
    
    
                //同时下载文件会对同一个文件做修改,需要使用锁机制,使用信号量简单点
                Semaphore semaphore;
                synchronized (mUrlLockMap) {
                    semaphore = mUrlLockMap.get(url);
                    if (null == semaphore) {
                        semaphore = new Semaphore(1);
                        mUrlLockMap.put(url, semaphore);
                    }
                }
    
                //保证锁一定解锁
                try {
                    semaphore.acquire();
    
                    //再次检查是否有本地缓存,解开锁之后可能下载完成
                    downloadFile = new File(getLocalCacheFileName(url));
                    if (downloadFile.exists()) {
                        resultHandler.accept(downloadFile);
                        return;
                    }
    
                    //网络下载部分
                    HttpURLConnection conn = null;
                    BufferedInputStream inputStream = null;
                    FileOutputStream outputStream = null;
                    RandomAccessFile randomAccessFile;
    
                    File cacheFile = new File(getLocalCacheFileName(url));
    
                    //要下载文件大小
                    long remoteFileSize = 0, sum = 0;
                    byte[] buffer = new byte[BUFFER_SIZE];
                    try {
    
                        URL conUrl = new URL(url);
                        conn = (HttpURLConnection) conUrl.openConnection();
    
                        remoteFileSize = Long.parseLong(conn.getHeaderField("Content-Length"));
    
                        existsCase:
                        if (cacheFile.exists()) {
                            long cacheFileSize = cacheFile.length();
    
                            //异常情况
                            if (cacheFileSize == remoteFileSize) {
                                break existsCase;
                            } else if (cacheFileSize > remoteFileSize) {
                                //如果出现文件错误,要删除
                                //noinspection ResultOfMethodCallIgnored
                                cacheFile.delete();
                                cacheFile = new File(getLocalCacheFileName(url));
                                cacheFileSize = 0;
                            }
    
                            conn.disconnect(); // must reconnect
                            conn = (HttpURLConnection) conUrl.openConnection();
                            conn.setConnectTimeout(30000);
                            conn.setReadTimeout(30000);
                            conn.setInstanceFollowRedirects(true);
                            conn.setRequestProperty("User-Agent", "VcareCity");
                            conn.setRequestProperty("RANGE", "buffer=" + cacheFileSize + "-");
                            conn.setRequestProperty("Accept",
                                    "image/gif,image/x-xbitmap,application/msword,*/*");
    
                            //随机访问
                            randomAccessFile = new RandomAccessFile(cacheFile, "rw");
                            randomAccessFile.seek(cacheFileSize);
                            inputStream = new BufferedInputStream(conn.getInputStream());
    
                            //继续写入文件
                            int size;
                            sum = cacheFileSize;
                            while ((size = inputStream.read(buffer)) > 0) {
                                randomAccessFile.write(buffer, 0, size);
                                sum += size;
                            }
                            randomAccessFile.close();
                        } else {
    
                            conn.setConnectTimeout(30000);
                            conn.setReadTimeout(30000);
                            conn.setInstanceFollowRedirects(true);
                            if (!cacheFile.exists()) {
                                //noinspection ResultOfMethodCallIgnored
                                cacheFile.createNewFile();
                            }
                            inputStream = new BufferedInputStream(conn.getInputStream());
                            outputStream = new FileOutputStream(cacheFile);
    
                            int size;
                            while ((size = inputStream.read(buffer)) > 0) {
                                outputStream.write(buffer, 0, size);
                                sum += size;
                            }
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    } finally {
                        try {
                            if (null != conn) conn.disconnect();
                            if (null != inputStream) inputStream.close();
                            if (null != outputStream) outputStream.close();
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
    
                    //下载结束
                    long dwonloadFileSize = cacheFile.length();
                    if (dwonloadFileSize == remoteFileSize && dwonloadFileSize > 0) {
                        //成功
                        resultHandler.accept(new File(getLocalCacheFileName(url)));
                    }else {
                        resultHandler.accept(null);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                    //异常的话传递空值
                    resultHandler.accept(null);
                } finally {
                    //释放信号量
                    semaphore.release();
                }
            });
        }
    
        //获取缓存文件名
        private String getLocalCacheFileName(String url) {
            return CACHE_DIR + url.substring(url.lastIndexOf("/"));
        }
    }
    
    

    这里使用了线程池来下载文件,缓存路径中需要使用到 context,解耦不太充分,读者可以直接设置路径。

        //相同链接的锁, 这里用LinkedHashMap限制一下储存的数量
        private final Map<String, Semaphore> mUrlLockMap =
                new LinkedHashMap<String, Semaphore>() {
                    protected boolean removeEldestEntry(Map.Entry<String, Semaphore> eldest) {
                        return size() >= 64 * 0.75;
                    }
                };
    
    

    为了避免相同的链接触发对同一个文件的修改,这里还是用到了锁机制,具体做法是对每个链接设置一个允许数量为1的信号量,相同的链接被允许下载的时候才能下载,不过要记得释放信号量。

    实现图片处理

    下面实现图片处理逻辑,实际上这里可以对 bitmap 进行压缩,读者可以自行设计。

    /**
     * @author: silence
     * @date: 2021-05-27
     * @description: 简单图片处理工具
     */
    
    public class BitmapHelper implements LazyImageView.IBitmapHelper {
    
        @Override
        public Bitmap handle(File file) {
            return decodePhotoFile(file);
        }
    
        //根据文件生成bitmap
        private Bitmap decodePhotoFile(File file) {
            Bitmap bitmap = null;
            BitmapFactory.Options options = new BitmapFactory.Options();
            try(FileInputStream instream = new FileInputStream(file);) {
                bitmap = BitmapFactory.decodeStream(instream, null, options);
            }catch (Exception e) {
                e.printStackTrace();
            }
            return bitmap;
        }
    
    }
    
    

    我这里只是最简单的将文件转换成了 bitmap。

    实际使用

    这里演示一下代码使用,而在 XML 中使用类似且更简单。

    LazyImageView.init(
        BitmapFactory.decodeResource(getResources(), R.mipmap.img_def),
        BitmapFactory.decodeResource(getResources(), R.mipmap.img_load_fail),
        new FileHelper(), new BitmapHelper());
    LinearLayout linearLayout = findViewById(R.id.title);
    LazyImageView lazyImageView = new LazyImageView(this);
    linearLayout.addView(lazyImageView);
    lazyImageView.show("https://api.dujin.org/bing/1920.php");
    
    

    这里我用必应每日一图做了实验,可以下载并显示,第一次下载完成后取缓存速度会快很多。

    其他设置宽高大小什么的和 ImageView 一样,毕竟是从 ImageView 继承过来的,圆角什么的就自己搞了吧!

    结语

    这里虽然自己写代码实现图片的懒加载,可是我还是觉得应该用一些稳定的开源库去加载图片,一是文档,而是交接给别人也好理解,当然这样一个功能写在来,还是挺有意思的,特别是解耦、下载和锁机制,希望能帮到有需要的读者!

    相关文章

      网友评论

          本文标题:自定义安卓图片懒加载

          本文链接:https://www.haomeiwen.com/subject/jypgedtx.html