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的原理分析_
网友评论