图片加载
在客户端开发中,图片加载和显示,是非常常见的功能了。常见的图片获取途径有网络传输,本地文件获取和资源加载。Android中用来显示图片的控件,除了一般的可设置背景的组件外,主要就是ImageView。
通过查看ImageView的源代码,可以大致了解图片加载的过程
public void setImageBitmap(Bitmap bm) {
// Hacky fix to force setImageDrawable to do a full setImageDrawable
// instead of doing an object reference comparison
mDrawable = null;
if (mRecycleableBitmapDrawable == null) {
mRecycleableBitmapDrawable = new BitmapDrawable(mContext.getResources(), bm);
} else {
mRecycleableBitmapDrawable.setBitmap(bm);
}
setImageDrawable(mRecycleableBitmapDrawable);
}
public void setImageDrawable(@Nullable Drawable drawable) {
......
updateDrawable(drawable);
if (oldWidth != mDrawableWidth || oldHeight != mDrawableHeight) {
requestLayout();
}
invalidate();
}
}
public void setImageURI(@Nullable Uri uri) {
......
resolveUri();
if (oldWidth != mDrawableWidth || oldHeight != mDrawableHeight) {
requestLayout();
}
invalidate();
}
}
public void setImageResource(@DrawableRes int resId) {
.....
resolveUri();
if (oldWidth != mDrawableWidth || oldHeight != mDrawableHeight) {
requestLayout();
}
invalidate();
}
private void resolveUri() {
......
Drawable d = null;
if (mResource != 0) {
try {
d = mContext.getDrawable(mResource);
} catch (Exception e) {
Log.w(LOG_TAG, "Unable to find resource: " + mResource, e);
// Don't try again.
mResource = 0;
}
} else if (mUri != null) {
d = getDrawableFromUri(mUri);
if (d == null) {
Log.w(LOG_TAG, "resolveUri failed on bad bitmap uri: " + mUri);
// Don't try again.
mUri = null;
}
} else {
return;
}
updateDrawable(d);
}
private void updateDrawable(Drawable d) {
.......
mDrawable = d;
if (d != null) {
d.setCallback(this);
d.setLayoutDirection(getLayoutDirection());
if (d.isStateful()) {
d.setState(getDrawableState());
}
if (!sameDrawable || sCompatDrawableVisibilityDispatch) {
final boolean visible = sCompatDrawableVisibilityDispatch
? getVisibility() == VISIBLE
: isAttachedToWindow() && getWindowVisibility() == VISIBLE && isShown();
d.setVisible(visible, true);
}
d.setLevel(mLevel);
mDrawableWidth = d.getIntrinsicWidth();
mDrawableHeight = d.getIntrinsicHeight();
applyImageTint();
applyColorMod();
configureBounds();
} else {
mDrawableWidth = mDrawableHeight = -1;
}
}
可以看到IamgeView的setImage相关的方法加载图片的过程大致是这样
1.根据图片路径(资源目录或者文件路径)或者Bitmap对象,生成一个Drawable对象
2.然后调用updateDrawable()方法,设置Drawable对象的宽高
3.执行requestLayout()方法重新布局View
4.执行invalidate()重新绘制ImageView
这里值一提的是,setImageUri()方法加载网络图片,只能用来加载本地图片文件。加载网络图片,应该先下载图片,将其转换成bitmap,再用setImageBitmap显示。
类似的,其他控件设置背景图片的加载过程也大致是这样。
BItmap的内存占用分析
上面提到了加载网络图片,需要先下载图片,转换成Bitmap对象。在实际开发中,因为本地文件和资源目录的图片都不能灵活的应对各种变化,加载显示网络图片的场景,越来越多。而Bitmap的缓存和内存优化就是图片加载优化过程中的一个关键点。先看来来Bitmap内存占用的计算方式。
Bitmap作为位图,需要读入图片在每个像素点上的数据,其主要占据内存的地方,也就是这些像素数据。一张图片像素数据的总大小为,图片的像素大小 * 每个像素点的字节大小,通常你就可以把这个值理解为Bitmap对象所占内存的大小。而图片的像素大小为横向像素值 * 纵向像素值。所以就有了下面这个公式:
Bitmap内存 ≈ 像素数据总大小 = 横向像素值 * 纵向像素值 * 每个像素的内存
单个像素的字节大小
它取决于Bitmap类表示图片质量的参数Config值。Bitmap.Config是一个枚举类,它定义了Bitmap支持的图片色彩质量的类型:
Config | 占用内存(byte) | 说明 |
---|---|---|
ALPHA_8 | 1 | 单透明通道 |
RGB_565 | 2 | 简易RGB色调 |
ARGB_4444 | 4 | 已废弃 |
ARGB_8888 | 4 | 24位真彩色 |
RGBA_F16 | 8 | Android8.0新增(更丰富的色彩表现HDR) |
HARDWARE | Special | Android 8.0 新增 (Bitmap直接存储在graphic memory) |
通常,BitmapFactory解析图片生成的Bitmap对象,默认的配置是ARGB_8888。
以分辨率为1280 * 960,大小约4.9M的图片为例,分析下Bitmap对象的内存占用情况。
图片在res/drawable目录下,将它加载到320dp * 240dp的ImageView。
Bitmap bitmapDecode = BitmapFactory.decodeResource(getResources(), resId);
imageView.setImageBitmap(bitmapDecode);
执行程序后,打印出了Bitmap对象的宽高、内存大小以及色彩类型:
首先,从数据上可以验证:44236800 = 3840 * 2880 * 4。
然后,来解释为什么width=3840,height=2880。
带着这个问题,我们需要来看看BitmapFactory的decode过程
private static native Bitmap nativeDecodeStream(InputStream is, byte[] storage,
Rect padding, Options opts);
private static native Bitmap nativeDecodeFileDescriptor(FileDescriptor fd,
Rect padding, Options opts);
private static native Bitmap nativeDecodeAsset(long nativeAsset, Rect padding, Options opts);
private static native Bitmap nativeDecodeByteArray(byte[] data, int offset,
int length, Options opts);
private static native boolean nativeIsSeekable(FileDescriptor fd);
查看相关源代码,不难发现,真正解析生成Bitmap对象,是在native方法中完成的。为此,我们需要追踪到BitmapFactory.cpp#nativeDecodeXXX方法,我们只看相关的部分:
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;
}
}
...
int scaledWidth = decoded->width();
int scaledHeight = decoded->height();
if (willScale && mode != SkImageDecoder::kDecodeBounds_Mode) {
scaledWidth = int(scaledWidth * scale + 0.5f);
scaledHeight = int(scaledHeight * scale + 0.5f);
}
...
if (willScale) {
const float sx = scaledWidth / float(decoded->width());
const float sy = scaledHeight / float(decoded->height());
bitmap->setConfig(decoded->getConfig(), scaledWidth, scaledHeight);
bitmap->allocPixels(&javaAllocator, NULL);
bitmap->eraseColor(0);
SkPaint paint;
paint.setFilterBitmap(true);
SkCanvas canvas(*bitmap);
canvas.scale(sx, sy);
canvas.drawBitmap(*decoded, 0.0f, 0.0f, &paint);
}
从代码中,我们可以看到,Bitmap最终是通过canvas绘制出来。但是绘制之前会有一个缩放(scale)过程。
scale = (float) targetDensity / density;
这一行代码说明,缩放的倍率由targetDensity和density决定。
- targetDensity,一般对应于设备屏幕的像素密度
-
density,一般对应于bitmap对象的像素密度。如果图片放在资源目录下,density就是该资源目录对应的像素密度。比如文件在drawable-mdpi目录下,对应的density为1。文件在drawable-hdpi目录下,对应的density为1.5。 同一张图片放置在不同目录下density会有不同的值:
具体来讲,Bitmap对象的内存大小是和图片所在资源目录的density成反比,和设备屏幕的targetDensity成正比。回到上面那个例子,图片放在res/drawable目录下,它是默认的drawable目录,对应的像素密度(density)为1,也就是density是1。设备屏幕的像素密度(targetDensity)是3。所以scale等于3。这就是1280 * 960的图片,经过decode以后,width为3840,height为2880的原因。
targetDensity和density这两个参数都是从options中获取到的。而这个options就对应于BitmapFactory的Options配置。
用过BitmapFactory类的,肯定都对这个Options配置不会陌生。它包含几个常用的属性:
- inDensity,The pixel density to use for the bitmap. bitmap对象自身的像素密度
- inTargetDensity,The pixel density of the destination this bitmap will be drawn to.图片绘制的目标区域的像素密度,一般可以理解为设备屏幕的像素密度。
- inScreenDensity,The pixel density of the actual screen that is being used。设备屏幕的像素密度。
- inSampleSize,可以理解为采样率。它的值表示decode操作时,width和height缩小的倍数。默认是1,它的值只能是2的N次方,并且大于1。
-
inJustDecodeBounds,这个属性如果为true,表示当前的这次decode操作,不会生成Bitmap对象,而是仅仅读取图片的尺寸和类型信息。
inDensity和图片存放的资源目录有关。inTargetDensity和inScreenDensity一般来说,很少手动去赋值。默认情况下,这俩都是和设备屏幕的像素密度保持一致。
以下是在同一台设备上,图片放在不同资源文件目录(mdpi、hdpi、xhdpi、xxhdpi)下加载的Bitmap对象参数:
通过以上的执行结果,可以得出这样几个结论:
- 在同一台设备上,图片所在资源目录的dpi越大,生成的bitmap尺寸越小
- 设备屏幕的像素密度越大,生成的bitmap尺寸越大
- res/drawable目录对应的density值和res/drawable-mdpi目录一样,等于1,dpi值为160。
- 资源目录的像素密度与设备相同的图片,生成的bitmap不会缩放,尺寸是原始大小。
因此,之前的bitmap内存的计算公式可以演化成:
bitmap内存 ≈ 像素数据总大小 = 图片的像素宽 * 图片的像素高 * (设备屏幕的像素密度/bitmap的像素密度)^2 * 每个像素的内存
以举例的图片来说就是 44236800 = 1280 * 960 *(480/160) ^2 * 4
Bitmap的内存优化
从上面的公式,不难看出,Bitmap的内存优化,主要有三种方式:
- 加载Bitmap时,选择低色彩的质量参数(Bitmap.Config),如RGB_5665,这样相比默认的ARGB_8888,占用内存缩小一半。
- 将图片放在合理的资源目录下,尽可能保持和屏幕密度一致。但也不要全都放在最高密度的资源目录下,资源目录的像素密度高于屏幕密度,加载的Bitmap尺寸会小于原始尺寸,甚至小于显示区域的尺寸,就会导致图片被拉伸,这也不能满足有些需求。
- 根据目标控件的尺寸,在加载图片时,对bitmap的尺寸进行缩放。比如在像素密度为480dpi的屏幕上,width为300dp,height为200dp的ImageView,能显示的无缩放的图片分辨率为900*600,如果图片分辨率大于这个尺寸,解析时就要考虑按比例缩小。
第一种方式,BitmapFactory.Options配置默认的色彩质量参数是ARGB_8888,每个像素占4个字节。而RGB_565每个像素占2个字节。适用于对色彩多样性要求比较低的场景。
第二种方式,在实际开发当中,将图片放置在合理的资源目录下。不能简单的放在res/drawable目录下,也最好不要以为地放在最高密度的drawable-xxxhdpi目录下。需要结合app的实际使用场景,比如通过统计得出,装机量占比中,以480dpi的屏幕密度为主的话,可考虑将原始图片放在drawable-xxhdpi的资源目录下,其他资源目录下放置的图片,根据density比例缩放。如drawable-xhdpi目录放置原始宽高2/3的图片。这样,图片在各个分辨率的屏幕上显示的尺寸和内存占用的情况,基本一致。
第三种方式,主要涉及到BitmapFactory解析Bitmap的优化处理。简单来说就是灵活使用inJustDecodeBounds和inSampleSize属性。下面介绍下其具体步骤:
- 将BitmapFactory.Options的inJustDecodeBounds属性设为true,加载图片。
- 从BitmapFactory.Options中取出图片的尺寸信息,对应于outWidth和outHeight属性。
- 根据采样率的取值规则(2的N次方),结合目标控件的尺寸大小,算出采样率inSampleSize的值。
- 将BitmapFactory.Options的inJustDecodeBounds属性设为false,重新加载图片,获取到bitmap对象。
值得注意的是,这种方式在解析FIleInputStream的缩放时存在问题,原因是FileInputStream是一种有序的文件流,两次decodeStream调用会影响文件流的位置属性,导致第二次调用decodeStream得到的是null。解决这个问题的方法就是,可以通过FIleInputStream得到对应FileDescriptor,然后调用BitmapFactory.decodeFileDescriptor方法来加载缩放后的图片。
本文参考:
https://blog.csdn.net/qq1263292336/article/details/78867461
https://blog.csdn.net/hoyouly/article/details/52839015