android图片加载、缓存。及分步式加载Demo。

2019-12-21  本文已影响0人  Yapple

图片,一直是android应用最重要的一部分,它是信息的载体,也是app给人视觉体验最直接的区域。
但是它又不像文字那样轻量级,越是美观,越是内容丰富的图片往往占用的内存资源、网络资源越多。当开发者处理各种图片加载的需求时,很容易碰到图片加载卡顿、OOM的问题。本篇将介绍图片加载缓存相关内容,以提供android开发有关图片处理的各种思路。当然,图片相关开源的成熟框架已经有不少了,比如最著名的glide,那我这篇文章还有什么意义呢?我觉得总是生吞别人设计好的框架,容易吸收不好,本文可以当作一盘开胃菜。让你了解一些图片缓存加载的思想,对于glide的使用就会有更深刻的体会。而且一般的应用环境,本篇所介绍的图片加载缓存方式就够用了。

一、图片加载
Bitmap:

android中谈到图片的加载基本上离不开Bitmap,我在本文中有关于图片的处理将通过Bitmap来进行操作。那么BItmap是什么呢?其实从字面意思就可以了解这个概念了:位图。上一次接触相关概念的时候,是处理8583报文,它的位图有8个字节(也就是8 × 8 = 64byte)用来描述64个区域是否存在:1表示存在,0表示不存在。对于图片性质的Bitmap也是同样的道理,8583中Bitmap的每一“位”是由一个byte来组成,而图片Bitmap的每一“位”则是由像素点够成。根据不同的图片格式,一个像素点的信息可能是RGB构成(红绿蓝三原色)亦或者ARGB(多了透明度)或者别的更为复杂的信息。而在android中通过Bitmap对象不仅可以获得图片的各种信息,还可以对图片进行修改等。
下面的表格是不同格式的图片消耗的内存:

Bitmap.Config 描述 内存消耗(字节/像素)
ARGB_8888 32位的ARGB位图 4
ARGB_4444 16位的ARGB位图 2
RGB_565 16位的RGB位图 2
ALPHA_8 8位的Alpha位图 1

那么如何创建Bitmap呢?可能是Bitmap占用的内存过多。所以android中Bitmap并没有公开的构造方法,而是提供了BitmapFactory工厂类进行加载Bitmap。
通过BitmapFactory进行加载Bitmap有以下四种常用的方法:
BitmapFactory.decodeFile() //将图片文件解码得到Bitmap对象
BitmapFactory.decodeByteArray() //通过字节数组来获得Bitmap对象
BitmapFactory.decodeResource() //通过Resource资源来获得Bitmap对象
BitmapFactory.decodeStream() //将Stream流解码得到Bitmap对象

优化加载Bitmap:

现在顶配手机像素好像已经达到了1亿像素。我们不说这么高清的图片了,就拿1千万像素为例:如果是ARGB_8888格式的照片,那么内存大小将达到 4千万字节,也就是40 * 1000 * 1000 ≈ 40M。瓦的天!一般的图片没有这么高清也要消耗几M的内存,图片加载多了,即使没有OOM也会占用过多的内存,导致app运行不流畅。

所以优化加载Bitmap刻不容缓。
如何优化加载呢?可以从两条思路出发:

  1. 降低单位像素所占的内存:就如上图表格所示,如果当前图片格式是ARGB_8888格式,则在不需要如此高清的情况下,我们可以转换成RGB_565格式的图片来展示,这样内存就相当于缩小到了一半。(开源库Glided的默认解码格式是RGB565,Picasso是ARGB8888 ,所以同一个图片,Glide消耗内存更少,但清晰度会有所牺牲)
  2. 降低图片采样率:图片采样率,顾名思义就是单位图片区域选择像素样本的数量,有时候由于界面的限制我们不需要展示原图大小的图片(假设,我们原图为1000 × 1000像素,而我们的ImageView大小只有500 × 500,如果我们将原图全部加载,岂不是很浪费内存?这时候我们就需要采样并获取合适的大小)。修改图片采样率是项耗时复杂的处理,一般来说不会在我们应用层去执行而是通过底层编码进行高效处理。当然素点的操作并不需要我们自己来实现,有关采样率的修改BitmapFactory早已有了封装好的处理流程。

第一种思路是降低了单位像素的内存消耗,而第二种种思路则是降低采样率,减少像总素点的数量。通过这两种手段有效的结合,足够适应大多数图片的加载。下面我们就来讲讲代码中是如何来实现这些功能的。
上面提到的BitmapFactory4个加载Bitmap的方法都是重载的,他们的参数除了表明来源的参数(第一个参数),还有第另一个参数Options。Options是BitmapFactory的内部类,用来控制采样的选项,以及图像是否应该完全解码,或者只是返回图片大小。Options可以说是高效加载Bitmap的核心控制器。

BitmapFactory.Options:

Options的构造方法是公开的,我们可以直接new一个Options对象。使用Options关键的操作是设置它里面一些重要属性。Options中的属性真的是非常的多,加上几个@Deprecated的属性差不多20来种吧,这里将选择几个最常见也是最有用的属性来介绍。如果有需求可以通过阅读源码的注释来了解每一种属性的作用。

  1. inPreferredConfig (Bitmap.Config类型)这个属性便是上边讲的图片格式,它的默认值为ARGB_8888。当如果将其设置为null,则图片解码器将会通过适配系统的屏幕根据原始图片的分辨率,设置最为接近的图片格式。
  2. outHeight&outWidth (int类型)这两个属性可以用来获取图片的宽和高。注释中还说明了如果inJustDecodeBounds被设置为false则返回压缩后的宽和高,如果inJustDecodeBounds被设置为true则不考虑压缩比例,返回来源图片的实际宽高。
  3. inJustDecodeBounds (boolean类型)显而易见,设置了这个属性,BitmapFactory将不会加载真正的Bitmap对象,而只是获取了图片的Bounds(宽和高)。
  4. inSampleSize(int类型)采样率,这就是实现降低图片采样率的关键属性。如果我们将它设置为>1的情况将对图片进行压缩,反之则不进行压缩。举个例子,inSampleSize = 2,则加载的图片的长和宽均为原始图片的 1 / 2,这样,整张图的大小则为原图的 (1 / 2) × (1 / 2) = 1 / 4。同理如果是inSampleSize = 4的话则片大小将为原图的1 / 16。注意:inSampleSize只能是基于2的幂,如果不是,最后的将向下取与之最接近的2的幂。

这三个便是和本文最相关的三个属性。下面将通过代码来完整的实现图片的压缩:

    /**
     * 压缩加载Bitmap
     *
     * @param resources 以Resources为图片来源加载Bitmap
     * @param pixWidth  需要显示的宽
     * @param pixHeight 需要显示的高
     * @return 压缩后的Bitmap
     */
    public static Bitmap ratioBitmap(Resources resources, int ResId, int pixWidth, int pixHeight) {
        BitmapFactory.Options options = new BitmapFactory.Options();
        /*
          inJustDecodeBounds设置为true,只加载原始图片的宽和高,
          我们先获取原始图片的高和宽,从而计算缩放比例
         */
        options.inJustDecodeBounds = true;
        options.inPreferredConfig = Bitmap.Config.RGB_565;
        BitmapFactory.decodeResource(resources, ResId, options);
        int originalWidth = options.outWidth;
        int originalHeight = options.outHeight;

        options.inSampleSize = getSimpleSize(originalWidth, originalHeight, pixWidth, pixHeight);
        /*
          inJustDecodeBounds设置为false, 真正的去加载Bitmap
         */
        options.inJustDecodeBounds = false;

        return BitmapFactory.decodeResource(resources, ResId, options);
    }

    /**
     * 获取压缩比例,如果原图宽比高长,则按照宽来压缩,反之则按照高来压缩
     * 
     * @return 压缩比例,原图和压缩后图的比例
     */
    private static int getSimpleSize(int originalWidth, int originalHeight, int pixWidth, int pixHeight) {
        int simpleSize = 1;
        if (originalWidth > originalHeight && originalWidth > pixWidth) {
            simpleSize = originalWidth / pixWidth;
        } else if (originalHeight > originalWidth && originalHeight > pixHeight) {
            simpleSize = originalHeight / pixHeight;
        }
        if (simpleSize <= 0) {
            simpleSize = 1;
        }
        return simpleSize;
    }
二、图片缓存

加载是为了让我们节约使用内存空间,而缓存则可以节约我们的网络资源,增加我们应用的流畅性。比如说打开某个app每次翻到首页,上面的图片如果每次都需要从服务器获取,那么我们的app用户体验将会变得非常糟糕。但是我们如果将同样的图片缓存起来,使用时直接从缓存中取出来,那么页面加载将会变的更加流畅。

android图片缓存可以分为两种方式:

  1. 将图片保存到内存中。
  2. 将图片保存到本地磁盘中。

第一种,无论是保存还是读取的速度都更快,但是占用了更加珍贵的内存资源,所以一般会限制内存缓存大小,而且在应用退出或者内存清空后,缓存的图片也就不见了,需要重新从服务器获取。第二种,相较第一种,由于是保存在磁盘中所以更加持久,能使用的空间也就更大。但是相应的速度没有内存缓存快,而且如果不做定期清理,可能会生成过多的垃圾资源占用我们的储存空间。所以一般情况,我们会根据需求,两者配合使用。

图片缓存的策略:LRU策略
图片缓存的方式我们清楚了,那如何制定图片缓存的策略呢?总不能将所有的图片都缓存下来,满了之后再清理,那么我们需要多大的内存和磁盘空间才能满足需求啊!那么什么样的图片值得缓存呢?当然是之后越可能再次用到的图片越值得缓存。
LRU策略便是为了估计出可能被重复使用的资源。它的全称是“Least recently used”,也就是最近最少使用:根据资源的历史访问记录来进行淘汰数据,其核心思想是“如果资源最近被访问过,那么将来被访问的几率也更高”。
下面将通过LRU策略去实现两种方式(内存和磁盘)的缓存,如果对LRU算法感兴趣,可以移步到LRU算法(如果这几个字不是链接,那说明我还没有写相关博客,当然网上相关介绍也不少,大家可以自行搜素了解学习),如果没时间看也不影响下面内容的阅读。

  1. 内存缓存:LruCache
    LruCache是android提供了内存缓存的类,它实现了LRU操作。我们只需要设置其大小,存放数据,读取数据。
    构造方法的参数便是设置缓存空间的最大值: public LruCache(int maxSize),在之后还可以通过LruCache.resize(int maxSize)方法进行修改最大缓存空间。
    往cache存放Bitmap的方法是:LruCache.put(@NonNull String key, @NonNull Bitmap value)(key和value的类型是泛型,我们在声明LruCache对象时规定为String和Bitmap类型)。
    从cache读取Bitmap的方法是:LruCache.get(@NonNull String key)
    还有手动从LruCache中移除Bitmap的方法是remove(@NonNull String key),当你判断某一图片确定不会再加载时可以主动移除。

  2. 磁盘缓存:DiskLruCache
    磁盘缓存android SDK并没有提供相关的类,但是square团队(Retrofit,OkHttp等开源库的制作团队)提供了DiskLruCache开源库,可以方便的帮我实现该功能。
    使用它需要先在gradle中导入:
    implementation 'com.jakewharton:disklrucache:2.0.2'
    DiskLruCache私有化了构造方法,它通过open方法进行创建:
    public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
    第一个参数是保存的文件地址,一般保存在context.getCacheDir()这个目录下,改目录的文件会随着应用的卸载被一同删除。
    第二个是app版本号(在版本号更新后缓存的内容将被清空,如果不考虑版本问题,这里可以写个1不再修改)。
    第三个是每个可以所保存的value的个数,必须是正数,根据项目功能需求决定填入多少,一般填入1即可。
    最后一个参数是保存文件的最大大小,以byte为单位。
    保存数据是通过Editor进行的DiskLruCache.edit(key)通过传递参数的方法使得key与资源进行绑定。最后通过Editor对象获取输出流,来将数据保存到文件中:Editor.newOutputStream(0)(参数是value的下标,由于我们valueCount设置为1,所以这里直接填入0)它会返回一个OutputStream对象,我们通过该对象将Bitmap的byte数组写入文件。最后调用editor.commit()保存成功。
    获取的方式也不复杂diskLruCache.get(key)会返回一个DiskLruCache.Snapshot对象。我们通过Snapshot的getInputStream(0)来获取输入流,最后调用文章开头讲过的BitmapFactory.decodeByteArray()获得Bitmap对象。

上面的加载和缓存方式也是主流图片框架的核心思想。Glide默认缓存的为只压缩后的图片,比如比如,当你网络请求的图片为1000 × 1000的大小,而你ImageView的大小为500 × 500,Glide在压缩处理后,会将压缩后的500 × 500大小的图片进行缓存。如果你需要使用其他大小的该图片,则需要重新从网络获取并进行压缩等处理。当然你可以通过代码来设置保存的对象,一共有三种方案以满足不同情况的使用:1.只缓存压缩后的图片。2.只缓存原图。3.缓存原图和压缩后的图片。

代码实现缓存逻辑,相关代码结合网络请求模块效果更佳。

package com.example.demojava;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;

import androidx.collection.LruCache;

import com.jakewharton.disklrucache.DiskLruCache;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;

/**
 * @author yapple
 * @date 2019/12/21
 * # Description bitmap 缓存、加载类
 */
public class BitmapLoader {

    private static BitmapLoader mBitmapLoader;

    private LruCache<String, Bitmap> mCache;
    private DiskLruCache mDiskLruCache;

    /**
     * 将DISK_FILE_PATH字符串中的<application package>替换成自己的包名
     * DISK_FILE_PATH  使用 context.getExternalFilesDir(Environment.DIRECTORY_PICTURES).getAbsolutePath() + "/cache";  来获取。下面这个路径在交新的android版本好像无法使用了。
     */
    private static final String DISK_FILE_PATH = "/data/data/Android/<application package>/cache/bitmapCache";
    private static final long DISK_MAX_SIZE = 100 * 1024 * 1024;

    /**
      * 内存缓存的大小
     * 上面说了内存资源很珍贵,这里我们规定好内存资源的大小以kb为单位
     */
    private int mCacheSize;

    private BitmapLoader() {
        long maxSize = Runtime.getRuntime().maxMemory();
        mCacheSize = (int) (maxSize / 8);
        mCache = new LruCache<String, Bitmap>(mCacheSize) {
            @Override
            protected int sizeOf(@NonNull String key, @NonNull Bitmap value) {
                //计算一个元素的缓存大小
                return value.getByteCount();
            }
        };
        try {
            File file = new File(DISK_FILE_PATH);
            if (!file.exists()) {
                boolean mkdirs = file.mkdirs();
                if (!mkdirs) {
                    throw new IOException("yapple.e " + DISK_FILE_PATH + " cant be create");
                }
            }
            mDiskLruCache = DiskLruCache.open(file, 1, 1, DISK_MAX_SIZE);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static BitmapLoader getInstance() {
        if (mBitmapLoader == null) {
            synchronized (BitmapLoader.class) {
                if (mBitmapLoader == null) {
                    mBitmapLoader = new BitmapLoader();
                }
            }
        }
        return mBitmapLoader;
    }

    public int getmCacheSize() {
        return mCacheSize;
    }

    /**
     * 修改内存缓存的大小
     */
    public void setmCacheSize(int mCacheSize) {
        this.mCacheSize = mCacheSize;
        mCache.resize(mCacheSize);
    }

    /**
     * 将bitmap保存到缓存中, 由于我这里并没有写网络相关的环节,所以直接将bitmap作为参数进行保存,
     * 实际通过上流的方式来保存会更加方便,也比较接近项目需求。
     * @param key 通过key value形式保存bitmap,key可以是URL等
     */
    public void putBitmapToCache(String key, Bitmap bitmap) {
        if (key != null && bitmap != null) {
            mCache.put(key, bitmap);
            try {
                /*int bytes = bitmap.getByteCount();
                ByteBuffer buffer = ByteBuffer.allocate(bytes);
                bitmap.copyPixelsToBuffer(buffer);
                DiskLruCache.Editor editor = mDiskLruCache.edit(key);
                OutputStream outputStream = editor.newOutputStream(0);
                outputStream.write(buffer.array());
                outputStream.flush();
                outputStream.close();*/
                DiskLruCache.Editor editor = mDiskLruCache.edit(key);
                OutputStream outputStream = editor.newOutputStream(0);
                bitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream);
                outputStream.flush();
                outputStream.close();
                editor.commit();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 从本地获取图片
     * 当内存中存在时,直接取内存中的bitmap,当内存中不存在时,则会从磁盘中获取。
     * 如果都不存在,则返回null;请从网络中加载
     */
    public Bitmap getBitmapFromLocal(String key) {
        Bitmap bitmap = mCache.get(key);
        if (bitmap == null) {
            bitmap = getBitmapFromDisk(key);
        }
        return bitmap;
    }

    private Bitmap getBitmapFromDisk(String key) {
        try {
            DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
            InputStream inputStream = snapshot.getInputStream(0);
            return BitmapFactory.decodeStream(inputStream);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}

有错误欢迎指出!!!
之后会根据相关知识点写一个图片加载的demo,展示还没有时间完成,后续会补上。

上一篇下一篇

猜你喜欢

热点阅读