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

图片缓存技术浅析

作者: 坤_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的原理分析_

相关文章

  • 图片缓存技术浅析

    Bitmap简介 bitmap位于graphics包下。实现了parceable接口,实现了parceable就可...

  • Android GLide图片加载的几种常用方法

    前言 Glide库是用来实现图片加载的框架,功能强大且易使用,深受大家喜爱。 缓存浅析 为啥要做缓存? andro...

  • Android 基础之图片加载(二)

    使用图片缓存技术 如何使用内存缓存技术来对图片进行缓存,从而让你的应用程序在加载很多图片的时候可以提高响应速度和流...

  • Android 图片三级缓存 _内存缓存LruCache

    本文目标 理解Android图片三级缓存机制,并重点理解内存缓存 (LruCache算法) 图片的缓存技术分为三级...

  • redis缓存介绍以及常见问题

    redis缓存介绍以及常见问题浅析 没缓存的日子 对于web来说,是用户量和访问量支持项目技术的更迭和前进。随着服...

  • 浅谈Fresco编码图片缓存

    (第一篇)Fresco架构设计赏析 (第二篇)Fresco缓存架构分析 (第三篇)Fresco图片显示原理浅析 通...

  • 图片懒加载总结

    浅析图片懒加载技术 出现的背景图片懒加载是一种网页优化技术。图片作为一种网络资源,在被请求时也与普通静态资源一样,...

  • Android缓存浅析

    Android缓存浅析 By吴思博 1、引言 2、常见的几种缓存算法 3、Android缓存的机制 4、LruCa...

  • Glide原理解析,

    推荐一篇关于三级缓存的文章三级缓存(MemoryCache,DiskCache,NetCache)浅析LRUCac...

  • Android ListView 与 RecyclerView

    Android ListView 与 RecyclerView 对比浅析:缓存机制 【备注】只用于个人收藏

网友评论

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

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