(1)drawable目录详解(mdpi,hdpi,xhdpi,xxhdpi,xxxhdpi)
1.1、图片在各个目录中要如何存放?(必须理解)
android的drawable目录有:
- drawable-ldpi(低密度)
- drawable-mdpi(中等密度)
- drawable-hdpi(高密度)
- drawable-xhdpi(超高密度)
- drawable-xxhdpi(超超高密度)
- drawable-xxxhdpi(超超超高密度)
- drawable-nohdpi(无缩放)
- 默认的drawable
而安卓加载图片的原理是根据手机的密度(dpi)来选择不同的文件夹下的图片,如果没有,就会从别的密度文件夹来获取图片并按照一定比例来缩放展示图片。获取设备密度的方法为:
float xdpi = getResources().getDisplayMetrics().xdpi;
float ydpi = getResources().getDisplayMetrics().ydpi;
知道了dpi,就知道会去哪个文件夹获取图片了:
对应文件夹 | 密度范围(dpi) | 缩放比例 |
---|---|---|
ldpi | 0~120 | 0.75 |
mdpi | 120~160 | 1(基准) |
hdpi | 160~240 | 1.5 |
xhdpi | 240~320 | 2.0 |
xxhdpi | 320~480 | 3.0 |
xxxhdpi | 480~640 | 4.0 |
而当前的主流机型密度基本都是在320~480之间,一般放两套图就够了,一套放到xhdpi,一套放到xxhdpi,那么如果是一个hdpi的手机来运行会发生什么呢?安卓是有一套图片匹配规则的:
一个hdpi密度的手机,肯定是先去匹配hdpi
目录下的图片,如果没有,那么就会向上级去查找,分别是xhdpi
->xxhdpi
->xxxhdpi
->nodpi
,如果都没有,就会往下级目录去查,分别是mdpi
->ldpi
,如果还没有,就会去drawable
目录去查找,如果还没有!就会开启自毁模式,嘣!!(resouse not found)
1.2、相同图片在不同目录所占用资源分析(了解即可)
不知道有没有朋友做过这样一个实验,把一个xhdpi密度的图片放到xxhdpi密度的文件夹中去加载会发生什么,它所占的内存会有区别吗?
例如一个在xhdpi下为200 * 200的图片,xxhdpi密度下为300 * 300,但此时图片只有200 * 200,所以它会放大到300 * 300,这时候图片就会出现模糊的情况了,所以如果你在项目中发现图片模糊的情况可以检查下图片的尺寸是否正确。
接下来我们来做一个实验,使用一个名叫icon_database_xls
64 * 64 PNG
的图片,将它放到xhdpi
目录中,执行下面的代码获取内存占用情况:
Bitmap bitmap = BitmapFactory.decodeResource(getResources(),R.mipmap.icon_database_xls);
Log.i("HJ",bitmap.getByteCount()+"");
结果如下:
2019-01-08 10:56:01.872 18507-18507/jie.com.imageoptimize I/HJ: 30976
接下来将图片放到xxhdpi
目录下,再次执行代码:
2019-01-08 10:56:32.123 18677-18677/jie.com.imageoptimize I/HJ: 13924
可以看到结果差距巨大,我们知道,一个图片的内存占用公式为:图片内存宽度 * 图片内存高度* 每像素占用的内存位数,那我们的内存宽高是如何计算的呢?我们可以追踪framework的源码,路径为/frameworks/base/core/jni/android/graphics/BitmapFactory.cpp
,找到doDecode
方法:
static jobject doDecode(JNIEnv* env, std::unique_ptr<SkStreamRewindable> stream,
jobject padding, jobject options) {
int sampleSize = 1;
bool onlyDecodeSize = false;
... ...
float scale = 1.0f;
... ...
if (options != NULL) {
sampleSize = env->GetIntField(options, gOptions_sampleSizeFieldID);
if (sampleSize <= 0) {
sampleSize = 1;
}
if (env->GetBooleanField(options, gOptions_justBoundsFieldID)) {
onlyDecodeSize = true;
}
... ...
if (env->GetBooleanField(options, gOptions_scaledFieldID)) {
const int density = env->GetIntField(options, gOptions_densityFieldID);
const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID);
const int screenDensity = env->GetIntField(options, gOptions_screenDensityFieldID);
if (density != 0 && targetDensity != 0 && density != screenDensity) {
scale = (float) targetDensity / density; //缩放比在这里计算的
}
}
}
... ...
if (scale != 1.0f) {
willScale = true;
scaledWidth = static_cast<int>(scaledWidth * scale + 0.5f); //内存宽度计算
scaledHeight = static_cast<int>(scaledHeight * scale + 0.5f); //内存高度计算
}
以上源码总结为:
内存宽高 = 图片本身的宽高 * (设备密度/使用的密度)+ 0.5
所以,内存的占用与图片所在的密度目录是有密切关系的
1.3、 drawable-xxhdpi与mipmap-xxhdpi的区别(了解即可)
从新版Android studio开始,系统会默认给我们创建mipmap
文件夹而不是drawable
,先前我一直以为这个目录只是放icon的(流下了菜逼的心酸泪水),后来发现这个目录与drawable都可以放图片,而且貌似也没有任何差别????,为了弄清楚,上网查了一下,大意是会对图片缩放做性能优化,不过我在平常的使用中并没有发现有太大的区别,可能是高版本drawable也做了优化把,所以这个简单了解一下即可。
(2)Bitmap优化详解
2.1、基础知识
安卓的图片加载都会对应到bitmap对象,当bitmap占用的内存过高,超过了android为app分配的最大内存,那么它就会不开心,它就会OOM,所以我们的图片优化,本质就是内存优化。
我们常用的图片格式一般有三种:
- JPEG 一种有损压缩格式,不支持透明通道,所以有透明背景需求的图片不要用jpeg格式,它会变成黑色,会变黑!!!,会变黑!!!,会变黑!!!
- PNG 无损压缩格式,支持透明通道
- WEBP 同时支持无损和有损压缩格式,然而兼容性较差,需要适配库
可参考:webp-android
在代码中可以通过Bitmap.CompressFormat
来指定生成的图片格式
bitmap还可以配置像素占用字节数(一个字节对应8位),这与内存占用息息相关,对应Bitmap.Config
类:
- ALPHA_8 8位ALPHA通道,即A=8,一个像素占用一个字节,它是没有颜色的,只有透明度
- ARGB_4444 16位 A=4,R=4,G=4,B=4,一个像素占用字节 = 4+4+4+4 = 16位 = 2个字节
- ARGB_8888 32位 A=8,R=8,G=8,B=8,一个像素占用字节 = 8+8+8+8 = 32位 = 4个字节
- RGB_565 16位 R=5,G=6,B=5,因为没有A,所以它没有透明度,计算同上也是占用2个字节
像平常如果没有透明度要求使用RGB_565
即可
2.2、优化的本质
前面已经提到我们的图片优化实际是内存的优化,而我们的内存计算公式是:
内存占用宽高相乘 * 每像素占用的内存位数,所以要么就减少图片内存占用宽高,要么就减少像素占用内存数。减少像素内存占用数可以通过配置Bitmap.Config解决,而减少图片内存占用宽高,可以有以下几个方式:
1.缩小图片实际宽高,缩小到正好正常展示
2.匹配合适的像素密度
此外还能主动回收占用的内存从而缓解内存紧张的问题。
2.3、优化方式
分析了一下优化的本质问题,我们可以针对性的使用以下几种方式来进行优化:
1.配置BitmapConfig(前面已经讲到)
2.拿到一个图片,通过inJustDecodeBounds
不消耗内存拿到图片的宽高,然后使用inSampleSize
来设置图片的缩放比。
简单示例代码:
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(),R.mipmap.icon_database_xls,options);
Log.i("HJ","初始图片宽:"+options.outWidth);
Log.i("HJ","初始图片高:"+options.outHeight);
options.inSampleSize = 2;
options.inJustDecodeBounds = false;
BitmapFactory.decodeResource(getResources(),R.mipmap.icon_database_xls,options);
Log.i("HJ","缩放后图片宽:"+options.outWidth);
Log.i("HJ","缩放后图片高:"+options.outHeight);
运行后:
2019-01-08 17:07:50.935 25900-25900/jie.com.imageoptimize I/HJ: 初始图片宽:64
2019-01-08 17:07:50.935 25900-25900/jie.com.imageoptimize I/HJ: 初始图片高:64
2019-01-08 17:07:50.936 25900-25900/jie.com.imageoptimize I/HJ: 缩放后图片宽:32
2019-01-08 17:07:50.937 25900-25900/jie.com.imageoptimize I/HJ: 缩放后图片高:32
可以看到inSampleSize设为2,图片的宽和高都变成了原来的一半,所以可得缩放公式为:
原始图片宽度/inSampleSize = 缩放后的图片宽度。高度同理,所以图片的整体大小变为了原来的1/4。
3.压缩图片,除了外部压缩,内部也可以使用compress()
来压缩图片。或者自定义压缩算法,使用比较广泛的有Luban算法
compress()示例代码:
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.icon_database_xls);
ByteArrayOutputStream stream = new ByteArrayOutputStream();
boolean compress = bitmap.compress(Bitmap.CompressFormat.PNG, 90, stream);
第一个参数是需要生成的图片格式,第二个是压缩百分比,第三个是生成的输出流容器。请注意,此方法只会压缩图片大小,但是并不会减少内存占用。
4.主动释放内存,绑定控件生命周期,在销毁的时候调用recycler()
方法
if(bitmap != null && !bitmap.isRecycled()){
bitmap.recycle();
bitmap = null;
}