Android开发之手写Glide图片加载/缓存 框架

挥舞着指尖,谱写指尖的艺术

声明

这次的手写Glide图片缓存框架,并不是引入Glide三方,对其进行自定义配置;而是自己实现一个类似Glide图片加载框架。

附带源码下载地址,文末有地址。

Glide三方框架的特点

  • Glide生命周期跟随Context生命周期-生而生-灭而灭;
  • 内部图片编码采用RGB_565,图片唯一key由多元素组成;
  • 默认支持gif动画图片,还是很Nice;
  • 可以手动禁用/开启,内存缓存或者是磁盘缓存;
  • 支持网络地址加载、本地文件路径加载、URL加载等;
  • 内部采用LruCache内存缓存和弱引用,对内存还是很友好的;
  • 可以根据控件大小,Glide的缓存的大小跟ImageView的尺寸相同;
  • 采用队列模式进行请求处理;

大概就这么多,而我们这次手写Glide主要实现的有:

1 .这里实现的是有缺点的,调用顺序不能改变,必须按照三步走方式;
2 .有兴趣爱好的可以自己完善修复;

  • 根据MD5,生成图片的唯一key;
  • 使用LruCache作为内存缓存,图片实现了弱引用;
  • 舍弃磁盘方式缓存,改为数据库缓存作为二级缓存;
  • 使用三方框架OkHttp下载图片;
  • 对图片的像素进行实质的压缩,节约内存;
  • 多线程去处理图片下载机制,这里没有使用到线程池;
  • 图片断网下载失败支持重连下载(后续更改);
  • 使用图片的tag标志,防止图片错位;

效果图展示

Android开发之手写Glide图片加载/缓存 框架

正文开始

实现的几个关键类:

  1. Glide.java //链式调用者
  2. bitmapRequest.java //链式封装
  3. RequestManager.java //对每个请求进行封装管理,队列模式管理请求
  4. PicDispatcherThread.java //图片下载/内存缓存/数据库缓存 的线程类
  5. DatabaseSQLite.java //二级缓存-数据库缓存的创建者
  6. DatabaseManager.java //二级缓存-数据库缓存的put,get,remove方法实现类
  7. CompressPic.java //图片像素压缩处理
  8. LruCacheUtils.java //内存缓存机制

1. 三步走

//上下文、下载地址、显示的控件
Glide.with(this).load(URL).into(view);

2. 实现链式调用

  1. 首先调用Glide.with,那么with就是静态方法,返回的就是with里的类的this;
  2. 只有返回了this,才能链式调用;
  3. 接着再实现with类中的load、loaderror(占位图)、remove(移出二级缓存),每个方法都返回this,则实现链式调用
  4. into之后不再有this返回,因为into之后不需要再调用;

先上部分代码#Glide

public class Glide {
    public static bitmapRequest with(Context context){
        return new bitmapRequest(context);
    }
}
2.1 bitmapRequest类,实现了get、set
   public class bitmapRequest {
    //上下文
    private Context context;
    //图片URL
    private String URL;
    //显示图片的控件
    private SoftReference<ImageView> imageView;
    //回调接口
    private RequestListener listener;
    //请求访问标志
    private String urlMD5;
    //占位图
    private int ResId;
    public static ArrayList<String> md5 = new ArrayList<>();
    public bitmapRequest(Context context) {
        this.context = context;
        //创建缓存图片的数据库
        DatabaseManager.getInstance(context);
    }
    public bitmapRequest load(String URL){
        this.URL = URL;
        this.urlMD5 = MD5Tool.getMD5(URL);
        md5.add(urlMD5);
        return this;
    }
    public void removeToSqlCache(){
        DatabaseManager.remove();
    }
    public bitmapRequest loadError(int redId){
        this.ResId = redId;
        return this;
    }
    public bitmapRequest listener(RequestListener listener){
        this.listener = listener;
        return this;
    }

    /**
     * 当执行到最后一步时,拿到RequestManager
     * 把请求加入请求队列
     * @param imageView
     */
    public void into(ImageView imageView){
        imageView.setTag(this.urlMD5);
        this.imageView = new SoftReference<>(imageView);
        RequestManager.getInstance().addRequest(this);
    }
   

}

回调接口没用上,可用于监听图片下载进度等… 有兴趣的可以根据回调接口拦截图片下载失败/下载成功后的下一步 步骤。

在调用构造函数时,会创建一个单例模式的数据库二级缓存。

这里把urlMD5作为图片控件的唯一tag标志,根据这个标志去判断图片的唯一key,也防止图片错位。再把每个key都保存下来,方便一键清除缓存。

into方法中调用了单例模式的请求管理,把每个请求都封装好,给每个请求安排多线程模式进行处理。

RequestManager.getInstance().addRequest(this);

3.RequestManager请求管理类

先上代码


/*
 * 管理消息隊列
 * 單例模式
 * */
public class RequestManager {
    private static RequestManager manager = new RequestManager();
    //创建请求队列,放入各个请求
    LinkedBlockingQueue<bitmapRequest> queue;
    //每个请求对应的线程
    private PicDispatcherThread[] dispatcher;
    //创建线程的最大数
    private int threadCount;

    public RequestManager() {
        queue = new LinkedBlockingQueue<>();
        //执行线程时,先关闭线程池里所有运行时的线程
        stopThread();
        //初始化线程
        initThread();
    }

    public static RequestManager getInstance() {
        return manager;
    }

    /**
     * 1.拿到APP最大能创建的线程数
     */
    private void initThread() {
        threadCount = Runtime.getRuntime().availableProcessors();
        dispatcher = new PicDispatcherThread[threadCount];
        //创建线程,把线程存入线程数组中
        for (int i = 0; i < threadCount; i++) {
            PicDispatcherThread thread = new PicDispatcherThread(queue);
            thread.start();
            dispatcher[i] = thread;
        }
    }

    /**
     * 将请求的对象加入到请求队列之中
     *
     * @param request
     */
    public void addRequest(bitmapRequest request) {
        if (!queue.contains(request)) {
            queue.add(request);
        }
    }

    public void stopThread() {
        if (dispatcher != null && dispatcher.length > 0) {
            for (PicDispatcherThread thread : dispatcher) {
                if (!thread.isInterrupted()) {//当线程没被回收,停止时
                    thread.interrupt();//kill thread
                }
            }
        }
    }
}

可自行看注释,注释详细标明了。

  1. LinkedBlockingQueue 管理着请求队列
  2. PicDispatcherThread[ ] 线程数组,类似线程池,多线程
  3. initThread 该方法实现了多线程的开启,逻辑部分看线程内容

4. 重头戏:PicDispatcherThread多线程逻辑实现者

接着上部分代码,一次性上太多了有点眼花。
先看主体内容,线程运行中的几个方法。

  @Override
    public void run() {
        super.run();
        //无限循环请求对象
        while (true) {
            try {
                bitmapRequest request = queue.take();
                //占位图片
                showDefaultPic(request);
                //下载图片
                Bitmap bitmap = loadBitmap(request);
                //加载图片
                showPic(request, bitmap);
            } catch (Exception e) {
                Log.e("thread:", "下载异常");
                e.printStackTrace();
            }

        }
    }

这里无限循环去读取队列中的消息,暂未实现线程同步,有需求的可以去加上synchronize进行加锁机制,因为实现的请求队列内部已经是有锁状态,我这里没太多需求,不需要去另行加锁,首先queue.take()取出其中一个请求队列,先给个图片占位图(图片未下载成功时,显示的默认图片),再去下载图片,下载返回一个bitmap类型再执行showPic回到主线程加载图片。

主要看loadBitmap方法。先上代码


    /**
     * 请求图片,使用三方框架
     * OKhttp
     *
     * @param request
     * @return
     */
    private Bitmap loadBitmap(bitmapRequest request) throws Exception {
        Bitmap bitmap = null;
        lruCacheUtils = LruCacheUtils.getInstance();
        //从缓存拿
        bitmap = (Bitmap) lruCacheUtils.get(request.getUrlMD5());
        if (bitmap != null) {
            Log.e("缓存", "从缓存中拿到");
            return bitmap;
        }
        //从数据库拿
        bitmap = DatabaseManager.get(request.getUrlMD5());
        if (bitmap != null) {
            Log.e("数据库", "从数据库中拿到");
            lruCacheUtils.put(request.getUrlMD5(), bitmap);
            return bitmap;
        }
        //下载图片
        bitmap = downLoadUrlPic(request.getURL(),request);
        if (bitmap != null) {
            //加入缓存中
            Cache_put(request, bitmap);
        }
        return bitmap;
    }
  • 首先读取内存缓存(LruCache),是否有图片,有则返回;
  • 如果内存缓存中无图片,则去二级缓存拿,数据库中拿;
  • 拿到后加入内存缓存之中,避免过于的数据库读取操作,耗时耗力;
  • 如果数据库中也没有图片,则去下载图片,下载成功后,加入到内存缓存和数据库缓存中;

该类的全部代码奉上


/**
 * 创建请求队列中的每个线程
 */
public class PicDispatcherThread extends Thread {
    //内存缓存
    LruCacheUtils lruCacheUtils;
    //请求队列
    private LinkedBlockingQueue<bitmapRequest> queue;
    //拿到主线程
    private Handler handler = new Handler(Looper.getMainLooper());

    public PicDispatcherThread(LinkedBlockingQueue<bitmapRequest> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        super.run();
        //无限循环请求对象
        while (true) {
            try {
                bitmapRequest request = queue.take();
                //占位图片
                showDefaultPic(request);
                //下载图片
                Bitmap bitmap = loadBitmap(request);
                //加载图片
                showPic(request, bitmap);
            } catch (Exception e) {
                Log.e("thread:", "下载异常");
                e.printStackTrace();
            }

        }
    }

    /**
     * 加载图片到控件中
     *
     * @param request
     * @param bitmap
     */
    private void showPic(bitmapRequest request, final Bitmap bitmap) {
        final ImageView imageView = request.getImageView();
        //md5解决图片错位问题,给每个图片设置tag
        if (bitmap != null && request.getImageView() != null && request.getImageView().getTag().equals(request.getUrlMD5())) {
            handler.post(new Runnable() {
                @Override
                public void run() {
                    imageView.setImageBitmap(bitmap);
                }
            });
        }
    }

    /**
     * 请求图片,使用三方框架
     * OKhttp
     *
     * @param request
     * @return
     */
    private Bitmap loadBitmap(bitmapRequest request) throws Exception {
        Bitmap bitmap = null;
        lruCacheUtils = LruCacheUtils.getInstance();
        //从缓存拿
        bitmap = (Bitmap) lruCacheUtils.get(request.getUrlMD5());
        if (bitmap != null) {
            Log.e("缓存", "从缓存中拿到");
            return bitmap;
        }
        //从数据库拿
        bitmap = DatabaseManager.get(request.getUrlMD5());
        if (bitmap != null) {
            Log.e("数据库", "从数据库中拿到");
            lruCacheUtils.put(request.getUrlMD5(), bitmap);
            return bitmap;
        }
        //下载图片
        bitmap = downLoadUrlPic(request.getURL(),request);
        if (bitmap != null) {
            //加入缓存中
            Cache_put(request, bitmap);
        }
        return bitmap;
    }

    private void Cache_put(bitmapRequest request, Bitmap bitmap) {
        //内存缓存
        lruCacheUtils.put(request.getUrlMD5(), bitmap);
        //数据库缓存-将bitmap转换为byte
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream);
        byte[] bit = outputStream.toByteArray();
        DatabaseManager.put(request.getUrlMD5(), bit);
    }

    private Bitmap downLoadUrlPic(String URL,bitmapRequest requestBitmap) {
        Bitmap bitmap = null;
        OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder().url(URL).build();
        try {
            Response response = client.newCall(request).execute();
            Log.e("下载", "图片 "+requestBitmap.getURL()+"下载成功");
            //bitmap = CompressTwo.simaplePic(BitmapFactory.decodeStream(response.body().byteStream()));
            bitmap = CompressPic.decodeBitmap(response.body().bytes(), 100, 100);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return bitmap;
    }

    private void showDefaultPic(bitmapRequest request) {
        //切回主线程
        if (request.getResId() > 0 && request.getImageView() != null) {
            final int res = request.getResId();
            final ImageView imageView = request.getImageView();
            handler.post(new Runnable() {
                @Override
                public void run() {
                    imageView.setImageResource(res);
                }
            });
        }
    }
}

5. LruCacheUtils.java

该类就是实现了一个单例的LruCache缓存

public class LruCacheUtils extends LruCache {
    public static LruCacheUtils lruCacheUtils;
    public LruCacheUtils(int maxSize) {
        super(maxSize);
    }
    public static LruCacheUtils getInstance(){
        if (lruCacheUtils == null){
            lruCacheUtils = new LruCacheUtils(1000);
        }
        return lruCacheUtils;
    }

    @Override
    protected int sizeOf(Object key, Object value) {
        return super.sizeOf(key, value);
    }

}

6. 数据库创建者—DatabaseSQLite

/**
 * 数据库创建
 * 充当二级缓存
 */
public class DatabaseSQLite extends SQLiteOpenHelper {
    public static final String CREATE_TABLE = "create table cache_table (" +
            "id integer primary key autoincrement," +
            "ke text unique," +
            "content blob" +
            ")";
    private Context context;
    public DatabaseSQLite(@Nullable Context context, @Nullable String name, @Nullable SQLiteDatabase.CursorFactory factory, int version) {
        super(context, name, factory, version);
        this.context = context;
    }

    @Override
    public void onCreate(SQLiteDatabase sqLiteDatabase) {
        sqLiteDatabase.execSQL(CREATE_TABLE);
        Toast.makeText(context,"表创建成功",Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) {

    }
}

这里就简单的创建了数据库和表

7.二级缓存管理者—DatabaseManager

上代码


/**
 * 数据库CRUD
 */
public class DatabaseManager {
    private static DatabaseManager manager;
    private static SQLiteDatabase db;
    private static String DatabaseName = "cache.db";

    /**
     * 单例模式
     * @param context
     * @return
     */
    public static DatabaseManager getInstance(Context context) {
        if (manager == null) {
            manager = new DatabaseManager(context);
        }
        return manager;
    }

    /**
     * getWritableDatabase():它以读写的方式去打开数据库,当数据库的磁盘空间满了时,就只能读不能写。
     * getReadableDatabase():它内部也调用了getWritableDatabase(),以读写的方式打开,如果数据库磁盘空间满了,则打开失败。
     * 打开失败后,如果继续尝试对数据库的读,则会读取数据,不能写
     *
     * @param context
     */
    private DatabaseManager(Context context) {
        DatabaseSQLite sqlIte = new DatabaseSQLite(context, DatabaseName, null, 1);
        db = sqlIte.getWritableDatabase();
    }

    /**
     * 插入数据进数据库
     * @param md5Key 图片的key
     * @param value 图片value
     */
    public static void put(String md5Key, byte[] value) {
        ContentValues values = new ContentValues();
        values.put("ke", md5Key);//md5
        values.put("content", value);//图片资源
        db.insert("cache_table", null, values);
    }

    /**
     * 读取数据库数据
     * @param key 图片key
     * @return 返回Bitmap
     */
    public static Bitmap get(String key) {
        String md5Key;//存放图片key
        byte[] PicContent = null;//存放图片二进制
        Bitmap resultPic = null;//返回图片
        Cursor query = db.query(true, "cache_table", null,
                null, null, null, null, null, null);
        if (query.moveToFirst()) {
            do {
                //查询到当前图片的key时
                md5Key = query.getString(query.getColumnIndex("ke"));
                //一个key,对应一个value,当查询到了就break退出
                if (key.equals(md5Key)){
                    //拿到图片的value
                    PicContent = query.getBlob(query.getColumnIndex("content"));
                    break;
                }
            } while (query.moveToNext());
        }
        //如果数据为null时则不进行转换
        if (PicContent != null) {
            resultPic = BitmapFactory.decodeByteArray(PicContent, 0, PicContent.length);
        }
        query.close();
        return resultPic;
    }

    /**
     * 删除数据库缓存
     */
    public static void remove() {
        try {
            db.delete("cache_table", null,null);
            Log.e("delete", "删除成功");
        } catch (Exception e) {
            Log.e("delete", "数据库暂无数据");
        }

    }
}

这里主要就是一些 put,get,remove操作。自行看注释,很详细了。

get读取图片时,传入的key,就是urlMD5,和数据库中的urlMD5进行对比,拿到则返回图片资源

8.图片压缩工具类—CompressPic


public class CompressPic {
    /**
     * 图片缩小工具类
     * @param stream 图片的字节流
     * @param reqWidth 要缩小的宽
     * @param reqHeight 要缩小的高
     * @return
     */
    public static Bitmap decodeBitmap(byte[] stream, int reqWidth, int reqHeight){
        int inPictureHeight;
        int inPictureWidth;
        int inSample = 1;
        BitmapFactory.Options options = new BitmapFactory.Options();
        // 设置为true时,bitmap = null,不加载进内存,但可以得到图片的宽高
        //只获取图片的大小信息,而不是将整张图片载入在内存中,避免内存溢出
        options.inJustDecodeBounds = true;
        //byte, byte.length, options
        BitmapFactory.decodeByteArray(stream, 0,stream.length, options);
        //获取图片资源的宽高
        inPictureHeight = options.outHeight;
        inPictureWidth = options.outWidth;
        //图片缩小算法
        BitmapFactory.Options resultOption = calculationWidthAndHeight(options,inPictureHeight,inPictureWidth,reqWidth,reqHeight,inSample);
        return BitmapFactory.decodeByteArray(stream,0,stream.length,resultOption);

    }

    private static BitmapFactory.Options calculationWidthAndHeight(BitmapFactory.Options options, int inPictureHeight, int inPictureWidth,
                                                                   int reqWidth, int reqHeight, int inSample) {
        //更改原始像素为reqWidth,reqHeight比例,如果原始像素比自定义像素要大,则跳过此步骤
        if (inPictureWidth > reqWidth || inPictureHeight >reqHeight){
            while (inPictureWidth / inSample > reqWidth || inPictureHeight / inSample >reqHeight){
                //要求取值为n的2次冥,不是二次幂则四舍五入到最近的二次幂
                inSample = inSample *2;
            }
        }
        //得到的最终值
        options.inSampleSize = inSample;
        //设置编码为RGB_565,默认为ARGB_8888
        options.inPreferredConfig = Bitmap.Config.RGB_565;
        //设置为false,得到实际的bitmap,不然只会得到宽高等信息
        options.inJustDecodeBounds = false;
        //设置图片可以复用
        options.inMutable = true;
        return options;
    }
}

注释很详细了喔 !!!

完结

我这里用的是RecyclerView进行图片的加载。
具体调用如下:
Android开发之手写Glide图片加载/缓存 框架_第1张图片
with是上下文唷。loadError是占位图。
图片的重试会在后序补充。

最后 感谢 腾讯课堂给予的知识补充 !!!

你可能感兴趣的:(Android开发旅途,android,java,多线程,队列,移动开发)