美文网首页
图片缓存技术浅析

图片缓存技术浅析

作者: 坤_RTFSC | 来源:发表于2020-12-05 11:17 被阅读0次

    Bitmap简介

    bitmap位于graphics包下。实现了parceable接口,实现了parceable就可以是个序列化对象,意味着可以进行进程间传递该对象了(序列化是将对象转化为二进制流的过程)。parceable是android封装的接口,效率比java封装的Serializable快,但是呢,如果是需要存储到本地磁盘的数据不建议使用这个,据说是因为实现parceable的对象不能保证数据的连续性如果外界有变的情况下,这一块是为什么,我也没去深究...ok,日常跑题...

    android是如何加载一个图片到内存中继而给imageview展示的?

    本地图片或者网络图是如何加载到设备端中的?txt是通过将里面的内容转化为字符串,再转为字节或字符流的。那么图片不可能转为字符串的再去转为流的。android就是通过bitmap位图来实现的。

    bitmap的基础构成

    bitmap位图是由像素信息构成的,也就是ARGB四个通道,A代表透明度。RGB也就是三个颜色通道。每个通道的值是0~255,也就是一个字节。一个字节大小是8比特。例如10111011就是一个大小为8比特又称为8位大小的二进制数。java中byte-1,short-2,int-4,long-8,char-2,float-4,..ok,日常跑题..

    也就是说bitmap的颜色加透明度可以用255255255*255来表示,那么问题来了,一个像素点的东西真的需要1 个字节的内容去表达吗?是否都需要透明度?是否都需要这么多颜色?当然不需要。这就引申出了bitmap的配置类bitmap.Config

    Bitmap.Config

    这是个内部枚举类,分为几种

     public enum Config {
            // these native values must match up with the enum in SkBitmap.h
    
            /**
             * 每个像素信息只存储alpha一个信息
             * This is very useful to efficiently store masks for instance.
             * No color information is stored.
             * With this configuration, each pixel requires 1 byte of memory.
             */
            ALPHA_8     (1),
    
            /**
             *每个像素占用两个字节信息,只记录rgb三种颜色信息
             *
             * This configuration can produce slight visual artifacts depending
             * on the configuration of the source. For instance, without
             * dithering, the result might show a greenish tint. To get better
             * results dithering should be applied.
             *
             * This configuration may be useful when using opaque bitmaps
             * that do not require high color fidelity.
             */
            RGB_565     (3),
    
            /**
             * Each pixel is stored on 2 bytes. The three RGB color channels
             * and the alpha channel (translucency) are stored with a 4 bits
             * precision (16 possible values.)
             *
             * This configuration is mostly useful if the application needs
             * to store translucency information but also needs to save
             * memory.
             *
             * It is recommended to use {@link #ARGB_8888} instead of this
             * configuration.
             *
             * Note: as of {@link android.os.Build.VERSION_CODES#KITKAT},
             * any bitmap created with this configuration will be created
             * using {@link #ARGB_8888} instead.
             *
             * @deprecated 在level13的时候已经被遗弃了,因为丢失色彩太严重了,建议使用ARGB_8888
             */
            @Deprecated
            ARGB_4444   (4),
    
            /**
             * Each pixel is stored on 4 bytes. Each channel (RGB and alpha
             * for translucency) is stored with 8 bits of precision (256
             * possible values.)
             *
             * This configuration is very flexible and offers the best
             * quality. It should be used whenever possible.
             */
            ARGB_8888   (5),
    
            /**
             * Each pixels is stored on 8 bytes. Each channel (RGB and alpha
             * for translucency) is stored as a
             * {@link android.util.Half half-precision floating point value}.
             *
             * This configuration is particularly suited for wide-gamut and
             * HDR content.
             */
            RGBA_F16    (6),
    
            /**
             * Special configuration, when bitmap is stored only in graphic memory.
             * Bitmaps in this configuration are always immutable.
             *
             * It is optimal for cases, when the only operation with the bitmap is to draw it on a
             * screen.
             */
            HARDWARE    (7);
    
            final int nativeInt;
    
            private static Config sConfigs[] = {
                null, ALPHA_8, null, RGB_565, ARGB_4444, ARGB_8888, RGBA_F16, HARDWARE
            };
    
            Config(int ni) {
                this.nativeInt = ni;
            }
    
            static Config nativeToConfig(int ni) {
                return sConfigs[ni];
            }
        }
    

    rgb应该不用解释了吧(red,green,blue...三原色),格式后面的颜色就代表位数,它的和就是色深,色深马上就会讲到。例如ARGB_8888,代表都是占8位。也就是4个字节,也就是32色深位。

    • RGB_565 :只记录rgb,r.g.b分别占5.6.5位
    • ARGB_8888 :最清楚的,同时也是占内存较大的。
    • ALPHA_8 :只记录透明信息,一般用于特殊例如作背景
    • ARGB_444 :已被遗弃,因为失真严重

    bitmap是内存中的表示,最终在磁盘上是例如jpg,jpeg,gif等格式保存的。

    bitmap基本概念

    位深

    位深是指对bitmap进行压缩时存储每个像素所用的bit。也就是说最终存储变为文件时的大小。

    色深

    每一个像素点用多少bit位表示。

    简单来讲,色深-内存,位深-磁盘,考个简单的小问题,在你的D盘里有一个10kb大小的图片,色深是16,位深是32,那么请问它在内存中所占大小为多少?

    各种常用磁盘使用格式
    • png :无损压缩,32位存储格式
    • jpg/jpeg :有损压缩,24位,无透明通道,比较常用,但是会丢失色彩
    • bmp :直接持久化没有进行压缩。
    • webp :google推出的比jpg压缩率更高,同时支持动画
    • gif : 8位存储格式,50%压缩率,可插入多帧图片

    bitmap的简单使用

    android中提供了bitmapFactory来方便地操作bitmap,主要是解析三种来源files, streams, byte-arrays.
    不管你是哪种来源,最终都调用到了decodeStream.直接看下这个源码:

    public static Bitmap decodeStream(InputStream is, Rect outPadding, Options opts) {
            ···
            validate(opts);
            Bitmap bm = null;
            Trace.traceBegin(Trace.TRACE_TAG_GRAPHICS, "decodeBitmap");
            try {
                if (is instanceof AssetManager.AssetInputStream) {
                    final long asset = ((AssetManager.AssetInputStream) is).getNativeAsset();
                    bm = nativeDecodeAsset(asset, outPadding, opts);
                } else {
                    bm = decodeStreamInternal(is, outPadding, opts);
                }
    
                if (bm == null && opts != null && opts.inBitmap != null) {
                    throw new IllegalArgumentException("Problem decoding into existing bitmap");
                }
    
                setDensityFromOptions(bm, opts);
            } finally {
                Trace.traceEnd(Trace.TRACE_TAG_GRAPHICS);
            }
    
            return bm;
        }
    

    validate是子类options中的方法,就是对options做一个校验,不符合则抛出异常。还会区分这个stream来源,去调用不同的jni方法(所以真正的实现是在c++文件里做的,这里又来了个新概念jni..新手还在疑惑,老手已经起立..后面花几篇文章讲讲jni吧,看看jvm是如何进行跨语言编译的,这也是java强大的地方),然后拿到生成的bitmap去设置density.

    上面提到了options,这里还是有必要介绍一下这个类的。

    属性 说明
    inJustDecodeBounds 设置只解码轮廓,为true的话,return为null,只获取轮廓,因此这个可以用来仅仅获取图片的宽高,而不占用内存!
    inSampleSize 采样率,必须为偶数,如果不是会取偶数最近的数;<1的时候默认为1,>1的话,就是宽高的除倍数,例如值为4,宽高为原图的1/4.大小是原图的1/16倍。
    inDensity 像素密度
    inScaled 是否可以缩放
    outWidth bitmap宽
    outHeight bitmap高
    inPreferredConfig 色深值

    使用采样率设置图片

    我就用我司现在的一个产品来讲解下如何避免oom,愉快地展示图片!(oom:out of memory,内存溢出,这里不做展开,后面花一篇文章专门讲)
    例:有1个500w像素的相机拍了张照,照片的分辨率是2592*1944,色深32位,那么一张正常的照片大小约为19M!这能忍?

    1.用inJustDecodeBounds,获取图片的宽高

    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeFile(filePath, options);
    

    2.计算图片采样率

    public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
            // 原图片的宽高
            final int height = options.outHeight;
            final int width = options.outWidth;
            int inSampleSize = 1;
            if (height > reqHeight || width > reqWidth) {
                final int halfHeight = height / 2;
                final int halfWidth = width / 2;
                // 计算inSampleSize值
                while ((halfHeight / inSampleSize) >= reqHeight
                        && (halfWidth / inSampleSize) >= reqWidth) {
                    inSampleSize *= 2;
                }
            }
            return inSampleSize;
        }
    

    reqWidth和height是屏幕或者说imageview需要的尺寸。假设一个设备的宽高是1280*720,那么通过这种方法算出来的采用率应该是:2.那么大小最后应该是9.5M,小了一倍,不过还是不够狠,如果设置为4的话,大小为1.18M.

    3.采样率计算完毕,再来decode。

            options.inJustDecodeBounds = false;
            // 根据具体情况选择具体的解码方法
            return BitmapFactory.decodeFile(filePath, options);
    

    bitmap常用优化技术

    原始压缩技术:compress();

    public boolean compress(CompressFormat format, int quality, OutputStream stream) {
            checkRecycled("Can't compress a recycled bitmap");
            // do explicit check before calling the native method
            if (stream == null) {
                throw new NullPointerException();
            }
            if (quality < 0 || quality > 100) {
                throw new IllegalArgumentException("quality must be 0..100");
            }
            StrictMode.noteSlowCall("Compression of a bitmap is slow");
            boolean result = nativeCompress(mNativePtr, format.nativeInt,
                    quality, stream, new byte[WORKING_COMPRESS_STORAGE]);
            Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
            return result;
        }
    

    返回true表示压缩成功,但是不是所有格式都一定能压缩成功。format代表压缩的格式,quality代表质量0-100,例如png无损图片就是100,stream就是写入压缩数据的流。

    android中常用的缓存技术分为内存缓存和硬盘缓存,下面我们详细地介绍一下这两种缓存技术。内存缓存一般可以使用android自带的LruCache,硬盘缓存就介绍下Glide的吧,是非常流行受欢迎的一个缓存框架。

    LruCache用法

    Lru(Least Recently Used),顾名思义,最近使用较少的先回收。后面我们通过分析源码看看是不是这么回事。
    基础用法:

            //Lru示例
            int maxRom = (int) Runtime.getRuntime().maxMemory();
            int cacheSize = maxRom / 8;
            LruCache<String, Bitmap> lru = new LruCache<String,Bitmap>(cacheSize){
                @Override
                protected int sizeOf(String key, Bitmap value) {
                    //为每个item设置大小
                    return super.sizeOf(key, value);
                }
            };
            lru.put("a",图像);
            lru.get("a");
    

    LruCache原理分析

    LruCache是android sdk中自带的一个类。首先咱看下它的构造方法:

    public LruCache(int maxSize) {
            //不能小于0
            if (maxSize <= 0) {
                throw new IllegalArgumentException("maxSize <= 0");
            }
            this.maxSize = maxSize;
            //new 了1个linkedHashMap
            this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
        }
    

    可以看到,构造传进来的是一个最大值,就是这个entry个数不会超过这个范围,然后new 了一个linkedHashMap。大家对hashMap肯定很熟悉了,那么这个linkedHashMap和hashMap的区别是什么?首先LinkedHashMap继承自HashMap,但是它是可以有序的,原理是通过数组+双向链表实现的(这里再埋一个坑,后面专门花篇幅介绍hashMap和linkedHashMap),它的顺序同时分为访问顺序和插入顺序。默认为false是插入顺序。
    访问顺序是什么?举个简单的例子:

    LinkedHashMap<Integer, String> iKun = new LinkedHashMap(0, 0.75f, true);
            iKun.put(0, "老李");
            iKun.put(1, "老王");
            iKun.put(2, "赞神");
            iKun.put(3, "喵总");
            iKun.put(4, "6D");
            iKun.get(1);
            iKun.get(0);
            for (Map.Entry entry : iKun.entrySet()) {
                Log.d("frank", entry.getKey() + " : " + entry.getValue());
            }
    

    看看打印的结果:


    微信图片_20201204163951.png

    ok,到这里应该懂了吧,使用频率较高的放在最后面,使用频率最少的放在前面,所以频率最少会第一个被回收!

    再来看看Lru是如何put和get的

    添加元素

    //以下代码基于android sdk 28
    public final V put(K key, V value) {
            //key 和 value不能为null。
            if (key == null || value == null) {
                throw new NullPointerException("key == null || value == null");
            }
    
            V previous;
            //因为hashMap是线程不安全的,所以这里加了同步锁
            synchronized (this) {
                putCount++;
                //sizeOf 也就是分配每个item的大小,这里会去做个安全检查,默认是1,同时把整个size+=一下
                size += safeSizeOf(key, value);
                //map去添加
                previous = map.put(key, value);
                if (previous != null) {
                //如果已经有了对象,则size-=操作
                    size -= safeSizeOf(key, previous);
                }
            }
    
            if (previous != null) {
            //结果如果不为空,子类可以去实现这个方法,空方法
                entryRemoved(false, key, previous, value);
            }
            //调整缓存大小
            trimToSize(maxSize);
            return previous;
        }
        
        private void trimToSize(int maxSize) {
            while (true) {
                K key;
                V value;
                //同步锁,保证线程安全
                synchronized (this) {
                    //非空判断
                    if (size < 0 || (map.isEmpty() && size != 0)) {
                        throw new IllegalStateException(getClass().getName()
                                + ".sizeOf() is reporting inconsistent results!");
                    }
                    //size比最大值小就跳出循环
                    if (size <= maxSize) {
                        break;
                    }
                    // BEGIN LAYOUTLIB CHANGE
                    // 获取map里最近的一个item
                    // 这里的方法不是高效的,目的是为了最小化改变
                    // 和之前的比较
                    Map.Entry<K, V> toEvict = null;
                    for (Map.Entry<K, V> entry : map.entrySet()) {
                        toEvict = entry;
                    }
                    // END LAYOUTLIB CHANGE
    
                    if (toEvict == null) {
                        break;
                    }
    
                    key = toEvict.getKey();
                    value = toEvict.getValue();
                    map.remove(key);
                    size -= safeSizeOf(key, value);
                    evictionCount++;
                }
    
                entryRemoved(true, key, value, null);
            }
        }
    

    通过注释应该能看的明白了,大概就是添加元素的时候先判断key和value是否为空,然后把size+1(不一定为1),然后再通过map去put,之后再调整一次缓存的大小,如果size已经满了,那么把头部的item remove掉。

    获取元素

    public final V get(K key) {
            //非空判断
            if (key == null) {
                throw new NullPointerException("key == null");
            }
        
            V mapValue;
            synchronized (this) {
                //直接从map里获取
                mapValue = map.get(key);
                if (mapValue != null) {
                    //hitCount可以理解为频次
                    hitCount++;
                    return mapValue;
                }
                //否则miss的频次相加
                missCount++;
            }
    
            //创建这个key对应的value
            V createdValue = create(key);
            if (createdValue == null) {
                return null;
            }
    
            synchronized (this) {
                //如果创建成功了,就把这个value put到map里。
                createCount++;
                mapValue = map.put(key, createdValue);
    
                if (mapValue != null) {
                    //如果value已经存在了,那么不用去计数了
                    map.put(key, mapValue);
                } else {
                    size += safeSizeOf(key, createdValue);
                }
            }
    
            if (mapValue != null) {
                entryRemoved(false, key, createdValue, mapValue);
                return mapValue;
            } else {
                trimToSize(maxSize);
                return createdValue;
            }
        }
    
    • 首先从map里直接去get值,get到了直接返回,同时把hitcount++;
    • 如果没有value了,去create,这个方法同样需要子类去重写;
    • 判断create出来的value是否已经存在,如果不存在直接map里put,如果存在不用map里添加了,把整个size++;

    LruCache小结

    LruCache的原理其实就是内部封装了一个LinkedHashMap,通过访问顺序的方法,当put元素的时候,调用trimToSize,来判断缓存是否已满了,如果满了则删除队列中的第一个元素。当用get元素时,也是调用的map来get,还是挺简单的吧。

    Glide基础用法

    Glide是一款非常优秀的图片加载库,它支持拉取,解码,展示图片或者视频的缩略图。提供了简单易用的API,且有着非常不错的性能,它的链式调用对开发人员更是舒服,下面简单的说下这个Glide的使用方法:

    首先在gradle文件中引入

    implementation 'com.github.bumptech.glide:glide:4.11.0'
    annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
    

    如何使用?以下代码是官方指导文档:

    // For a simple view:
    @Override public void onCreate(Bundle savedInstanceState) {
      ...
      ImageView imageView = (ImageView) findViewById(R.id.my_image_view);
      Glide.with(this).load("http://goo.gl/gEgYUd").into(imageView);
    }
    
    // For a simple image list:
    @Override public View getView(int position, View recycled, ViewGroup container) {
      final ImageView myImageView;
      if (recycled == null) {
        myImageView = (ImageView) inflater.inflate(R.layout.my_image_view, container, false);
      } else {
        myImageView = (ImageView) recycled;
      }
    
      String url = myUrls.get(position);
    
      Glide
        .with(myFragment)
        .load(url)
        .centerCrop()
        .placeholder(R.drawable.loading_spinner)
        .into(myImageView);
    
      return myImageView;
    }
    

    喜闻乐见的链式调用方法,使用起来还是很简单的。
    ok今天就简单介绍下两种缓存技术的使用方法,Glide的设计结构就比Lru复杂多了,篇幅有限,近期将尽快推出Glide的原理分析_

    相关文章

      网友评论

          本文标题:图片缓存技术浅析

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