图片加载
在客户端开发中, 图片加载和显示, 是非常常见的功能了. 常见的图片获取途径有网络传输, 本地文件获取和资源加载. 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 对象的宽高, 内存大小以及色彩类型:
image.PNG
首先, 从数据上可以验证: 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 方法中完成的. 为此, 我们需要追踪到 #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 会有不同的值:
image.PNG
具体来讲, 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 对象参数:
image.PNG
通过以上的执行结果, 可以得出这样几个结论:
在同一台设备上, 图片所在资源目录的 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://my.oschina.net/rengwuxian/blog/182885
- https://www.jianshu.com/p/3f6f6e4f1c88
来源: http://www.jianshu.com/p/756817f12fd5