Android开发Android技术知识Android开发实战总结

图片加载和Bitmap的内存优化

2019-02-23  本文已影响0人  zackyG

图片加载

在客户端开发中,图片加载和显示,是非常常见的功能了。常见的图片获取途径有网络传输,本地文件获取和资源加载。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方法中完成的。为此,我们需要追踪到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这两个参数都是从options中获取到的。而这个options就对应于BitmapFactory的Options配置。

用过BitmapFactory类的,肯定都对这个Options配置不会陌生。它包含几个常用的属性:

bitmap内存 ≈ 像素数据总大小 = 图片的像素宽 * 图片的像素高 * (设备屏幕的像素密度/bitmap的像素密度)^2 * 每个像素的内存

以举例的图片来说就是 44236800 = 1280 * 960 *(480/160) ^2 * 4

Bitmap的内存优化
从上面的公式,不难看出,Bitmap的内存优化,主要有三种方式:

第一种方式,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属性。下面介绍下其具体步骤:

  1. 将BitmapFactory.Options的inJustDecodeBounds属性设为true,加载图片。
  2. 从BitmapFactory.Options中取出图片的尺寸信息,对应于outWidth和outHeight属性。
  3. 根据采样率的取值规则(2的N次方),结合目标控件的尺寸大小,算出采样率inSampleSize的值。
  4. 将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

https://my.oschina.net/rengwuxian/blog/182885

https://www.jianshu.com/p/3f6f6e4f1c88

上一篇 下一篇

猜你喜欢

热点阅读