Android 應用性能優化--資源圖片的內存管理

綜述

圖片從來源上可以分成三大類:網絡圖片、手機存儲中(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來存儲圖片就完全沒有必要了,何況後者還會導致性能問題。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章