美文网首页
android图片加载、缓存。及分步式加载Demo。

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

作者: Yapple | 来源:发表于2019-12-21 16:08 被阅读0次

    图片,一直是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,展示还没有时间完成,后续会补上。

    相关文章

      网友评论

          本文标题:android图片加载、缓存。及分步式加载Demo。

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