Android 应用性能优化--资源图片的内存管理

本文探讨了Android应用中资源图片的内存占用问题,分析了不同图片类型的特点及内存占用计算方法,并介绍了inPurgeable参数的作用及其对性能的影响。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

综述

图片从来源上可以分成三大类:网络图片、手机存储中(EMMC 和Sdcard)的图片、APK资源图片,目前有很多成熟的图片加载库,主流的有Picasso 、Glide 、Fresco。但是没有覆盖APK资源图片的管理。

资源图片特征:
1、一般在xml中引用 ,在Java中也是通过资源ID查找 。
2、一般不使用异步记载,不会出现loading图这些中间状态。
3、如果加载失败了那么APP Crash。

由于这三个原因的存在,不能使用第三方加载库,很容易出现的一个问题就是图片过大会导致OOM.为了追求显示的效果和用户体验,有时候我们会使用资源图片。使用资源图片的方式如下:

<com.owen.provider.CustomView
    android:layout_width="300dp"
    android:layout_height="300dp"
    android:background="@mipmap/ic_launcher_round"/>

如果图片较大,在App内存紧张的情况下很容易出现00M, 特别是在Android 系统5.0版本一下的手机。出现的原因跟资源图片的内存占用有关系。

Bitmap 内存占用

Bitmap 内存的占用跟资源图片的尺寸有关系,跟图片的质量没有关系,也就是说一张纯黑的图片和一张彩色的图片在大小相同的情况下占用的内存是一样的。

我们以一张宽高为720 x 1280图片为例,对应的资源文件夹为mipmap-xhdpi,设备以标准720p手机为例,density=320。Android设备上资源图片被处理成Bitmap对象,生成Bitmap的一个非常重要的参数是Config,属性值有ALPHA_8、RGB_565、ARGB_4444、ARGB_8888四种。不同的属性值对应的图片每个像素点占用内存大小不同,ALPHA_8每个像素占用1byte,RGB_565和ARGB_4444占用2byte,ARGB_8888占用4byte,其中ARGB_4444在高版本中已经废弃。

我们分析下相关的代码(API 20 Android 4.4):

Resources.java

/*package*/ Drawable loadDrawable(TypedValue value, int id) throws NotFoundException {


    Drawable dr = getCachedDrawable(isColorDrawable ? mColorDrawableCache : mDrawableCache, key);

         ...

            } else {
                Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, file);
                try {
                    InputStream is = mAssets.openNonAsset(
                            value.assetCookie, file, AssetManager.ACCESS_STREAMING);
           ...
                    dr = Drawable.createFromResourceStream(this, value, is, file, null);
                    is.close();
    //                System.out.println("Created stream: " + dr);
                } catch (Exception e) {
            ...        
                }
                Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
            }
        }
    }
    return dr;
}

Drawable.java

/**
 * Create a drawable from an inputstream, using the given resources and
 * value to determine density information.
 */
public static Drawable createFromResourceStream(Resources res, TypedValue value,
        InputStream is, String srcName, BitmapFactory.Options opts) {

    if (is == null) {
        return null;
    }
    ...
    Rect pad = new Rect();
    if (opts == null) opts = new BitmapFactory.Options();
    opts.inScreenDensity = res != null
            ? res.getDisplayMetrics().noncompatDensityDpi : DisplayMetrics.DENSITY_DEVICE;
    Bitmap  bm = BitmapFactory.decodeResourceStream(res, value, is, pad, opts);
    ...
    return null;
}

BitmapFactory.java

public static class Options {
    /**
     * Create a default Options object, which if left unchanged will give
     * the same result from the decoder as if null were passed.
     */
    public Options() {
        inDither = false;
        inScaled = true;
        inPremultiplied = true;
    }
}

从生成的代码中可以看出 opts = new BitmapFactory.Options(); Bitmap.Config使用的是默认值ARGB_8888,也就是每个像素点占用内存4byte, 720 * 1280的图片的像素个数是 720 * 1280 = 921600, 所有像素点占用的内存是 720 * 1280 * 4 = 3686400 byte 大约为 3.5 M 这是图片不做任何处理的情况下decode 后的大小,但是系统将图片处理成Drawable对象的时候还会做处理。我们看BitmapFactory.java 中的decodeResourceStream 方法。

BitmapFactory.java

/**
 * Decode a new Bitmap from an InputStream. This InputStream was obtained from
 * resources, which we pass to be able to scale the bitmap accordingly.
 */
public static Bitmap decodeResourceStream(Resources res, TypedValue value,
        InputStream is, Rect pad, Options opts) {

    if (opts == null) {
        opts = new Options();
    }

    if (opts.inDensity == 0 && value != null) {
        final int density = value.density;
        if (density == TypedValue.DENSITY_DEFAULT) {
            opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
        } else if (density != TypedValue.DENSITY_NONE) {
            opts.inDensity = density;
        }
    }

    if (opts.inTargetDensity == 0 && res != null) {
        opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
    }

    return decodeStream(is, pad, opts);
}

Options有两个重要的参数,inDensity和inTargetDensity。

inDensity表示被设定的图像密度,决定这个值的是图片所放置的文件目录,比如drawable-hdpi、drawable-xhdpi等等,hdpi 密度值为240 , xhdpi 密度值为 320 ,xxhdip 密度值为480

代码中opts.inDensity 被赋值为 value.density,也就是资源维度对应的密度值。如果图片放在drawable-hdpi下,inDensity=240,如果放在drawable-xhdpi下,inDensity=320。

inTargetDensity表示最终需要适配到的图片密度,这个值由手机设备来决定,上面代码中其值为DisplayMetrics的densityDpi,手机屏幕越高清这个值越大,而我们例子中720p对应的densityDpi=320。

如果inDensity的值和inTargetDensity的值不相等,那么图片尺寸就被会缩放,缩放的比例为 inTargetDensity / inDensity。当然,宽高是需要同时等比缩放的,不然图片就变形了。

图片占用内存与图片的尺寸有关,如果被尺寸缩放了,内存大小就变了。前面未作任何缩放处理的720×1280图占用内存是3.5M,假设放在drawable-ldpi目录下inDensity=120,设备inTargetDensity=320,那么最终的占用内存大小将是3.5Mx(320/120)x(320/120)大于是25M。

结论

在开发的过程中资源图片放的目录一定要慎重。
资源图片内存 = 宽 x 高 x 4 x (设备密度 / 资源维度密度)x(设备密度 / 资源维度密度)以为长宽需要等比例缩放。

inPurgeable 介绍

开发的过程中如果资源文件目录防止的不对会导致内存占用翻倍,但也不是放的密度维度越高越好,毕竟还是要做适配,图片的缩放会影响图片的显示效果。然而即使图片放对位置,Bitmap 消耗的内存还是巨大的。解决方案是 inPurgeable, 代码如下:

public static Bitmap decodeBitmap(Context context, int resId) {
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inPurgeable = true;
    options.inInputShareable = true;
    InputStream inputStream = context.getResources().openRawResource(resId);
    return BitmapFactory.decodeStream(inputStream, null, options);
}

inPurgeable 的注释

/**
 * If this is set to true, then the resulting bitmap will allocate its
 * pixels such that they can be purged if the system needs to reclaim
 * memory. In that instance, when the pixels need to be accessed again
 * (e.g. the bitmap is drawn, getPixels() is called), they will be
 * automatically re-decoded.
 *
 * <p>For the re-decode to happen, the bitmap must have access to the
 * encoded data, either by sharing a reference to the input
 * or by making a copy of it. This distinction is controlled by
 * inInputShareable. If this is true, then the bitmap may keep a shallow
 * reference to the input. If this is false, then the bitmap will
 * explicitly make a copy of the input data, and keep that. Even if
 * sharing is allowed, the implementation may still decide to make a
 * deep copy of the input data.</p>
 *
 * <p>While inPurgeable can help avoid big Dalvik heap allocations (from
 * API level 11 onward), it sacrifices performance predictability since any
 * image that the view system tries to draw may incur a decode delay which
 * can lead to dropped frames. Therefore, most apps should avoid using
 * inPurgeable to allow for a fast and fluid UI. To minimize Dalvik heap
 * allocations use the {@link #inBitmap} flag instead.</p>
 *
 * <p class="note"><strong>Note:</strong> This flag is ignored when used
 * with {@link #decodeResource(Resources, int,
 * android.graphics.BitmapFactory.Options)} or {@link #decodeFile(String,
 * android.graphics.BitmapFactory.Options)}.</p>
 */
public boolean inPurgeable;

解释:虽然inPurgeable能避免在Heap中分配一大段内存,但这个是以牺牲性能为代价的,如果图片要绘制到View上可能出现延时导致掉帧。

原理实现:

public static Bitmap decodeStream(InputStream is, Rect outPadding, Options opts) {
    // we don't throw in this case, thus allowing the caller to only check
    // the cache, and not force the image to be decoded.
    if (is == null) {
        return null;
    }

    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;
}

private static Bitmap decodeStreamInternal(InputStream is, Rect outPadding, Options opts) {
    // ASSERT(is != null);
    byte [] tempStorage = null;
    if (opts != null) tempStorage = opts.inTempStorage;
    if (tempStorage == null) tempStorage = new byte[DECODE_BUFFER_SIZE];
    return nativeDecodeStream(is, tempStorage, outPadding, opts);
}

private static native Bitmap nativeDecodeStream(InputStream is, byte[] storage,
        Rect padding, Options opts);

decodeStream这段代码最终调用的是native层的类库,C代码如下。

BitmapFactory.cpp

static jobject doDecode(JNIEnv* env, SkStream* stream, jobject padding,
        jobject options, bool allowPurgeable, bool forcePurgeable = false,
        bool applyScale = false, float scale = 1.0f) {

    ...
    if (!isPurgeable) {
        decoder->setAllocator(&javaAllocator);
    }
    ...
    if (isPurgeable) {
        decodeMode = SkImageDecoder::kDecodeBounds_Mode;
    }
    ...
    if (isPurgeable) {
        pr = installPixelRef(bitmap, stream, sampleSize, doDither);
    } else {
        pr = bitmap->pixelRef();
    }
    ... 

}
static SkPixelRef* installPixelRef(SkBitmap* bitmap, SkStream* stream,
        int sampleSize, bool ditherImage) {

    SkImageRef* pr;
    // only use ashmem for large images, since mmaps come at a price
    if (bitmap->getSize() >= 32 * 1024) {
        pr = new SkImageRef_ashmem(stream, bitmap->config(), sampleSize);
    } else {
        pr = new SkImageRef_GlobalPool(stream, bitmap->config(), sampleSize);
    }
    ...
    return pr;
}

图片的decode逻辑都在installPixelRef中,如果图片大小(占用内存)大于32×1024=32K,那么就使用Ashmem,否则就就放入一个引用池中。如果图片不大,直接放到native层内存中,读取方便且迅速。如果图片过大,放到native层内存也就不合理了,不然图片一多,native层内存很难管理。但是如果使用Ashmem匿名共享内存方式,写入到设备文件中,需要时再读取就能避免很大的内存消耗了,另外,这块内存是由Linux系统的内存管理来管理的,系统内存不足可以直接回收。而且,由于Ashmem跨进程的特性,同一张图片内存是可以跨进程共享的,这也是inInputShareable属性的由来。由此可见,如果inPurgeable=true,图片所占用的内存就完全与Java Heap无关了,自然就不会有OOM这种烦恼了。

inPurgeable过时

Android系统从5.0开始对Java Heap内存管理做了大幅的优化。和以往不同的是,对象不再统一管理和回收,而是在Java Heap中单独开辟了一块区域用来存放大型对象,比如Bitmap这种,同时这块内存区域的垃圾回收机制也是和其它区域完全分开的,这样就使得OOM的概率大幅降低,而且读取效率更高。所以,用Ashmem来存储图片就完全没有必要了,何况后者还会导致性能问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值