最近收拾了公司有关图片加载代码,这部分代码也不知道当时怎么想的,自己写了一套图片懒加载控件,我是觉得这应该用一些稳定的图片加载开源库,比方 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