自定义安卓图片懒加载

最近整理了公司有关图片加载代码,这部分代码也不知道当时怎么想的,自己写了一套图片懒加载控件,我是觉得这应该用一些稳定的图片加载开源库,比如 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 继承过来的,圆角什么的就自己搞了吧!

结语

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

end

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值