Android之優(yōu)雅地加載大圖片

轉(zhuǎn)載注明出處:http://www.itdecent.cn/p/0f56f35068e2

1. 引子

前幾天跟服務(wù)端的一個妹子聯(lián)調(diào)接口,服務(wù)器配置一張圖片,幾十KB就行,她問我圖片從哪里找,我告訴她先隨便在網(wǎng)上找個圖片鏈接就行了。結(jié)果一運(yùn)行程序,就崩潰了,出現(xiàn)了下面的異常。

java.lang.OutofMemoryError

內(nèi)存溢出OOM,我當(dāng)時一臉懵逼。

一臉懵逼

于是拿著后臺返回的鏈接去查看了一下圖片,是一張6M的壁紙。

我內(nèi)心幾乎是崩潰的

這只是一個簡單的聯(lián)調(diào),而在聯(lián)調(diào)過程中操作不當(dāng)導(dǎo)致出現(xiàn)OOM問題,大家就當(dāng)是個玩笑。其實在Android中很容易出現(xiàn)OOM的異常,特別是對圖片操作的時候,所以當(dāng)面對大圖片,需要我們對圖片進(jìn)行適當(dāng)?shù)膲嚎s,在不影響圖片顯示的情況下,盡量保證不出現(xiàn)OOM的異常。

2. 概述

在開發(fā)中,對于圖片的操作,稍有不慎,可能就會消耗大量的內(nèi)存,導(dǎo)致程序崩潰,所以了解一種通用的技術(shù)去處理和加載圖片,同時保證UI流暢避免OOM現(xiàn)象,是非常有必要的。那么為什么在Android中對于圖片的處理會如此棘手呢?主要有以下一些原因:

  • 通常情況下,移動設(shè)備的內(nèi)存資源是有限的,Android系統(tǒng)會根據(jù)手機(jī)的屏幕大小和密度,為每個程序設(shè)置一個最大內(nèi)存限制,應(yīng)用程序消耗的內(nèi)存不能超過這個最大內(nèi)存限制,否則就會出現(xiàn)OOM現(xiàn)象。當(dāng)然,這個內(nèi)存限制是跟手機(jī)配置相關(guān)聯(lián)的。
  • 圖片的操作會消耗大量的內(nèi)存,特別是細(xì)節(jié)豐富的圖片,例如照片。以Galaxy Nexus相機(jī)為例子,它拍攝一張2592x1936像素的照片,如果使用的位圖配置是ARGB_8888(默認(rèn)從Android 2.3開始),那么這張照片加載到內(nèi)存,大約會消耗19MB的內(nèi)存(2592 x 1936 x 4字節(jié)),僅僅是圖片消耗內(nèi)存的數(shù)值可能已經(jīng)超過了某些設(shè)備的內(nèi)存限制
  • Android的UI經(jīng)常會一次加載多張圖片,例如,ListView、GridView、ViewPager等等

圖片有各種形狀和大小。通常情況下,它們普遍比設(shè)備所需要的圖片要大一些,例如手機(jī)相冊顯示手機(jī)拍攝的照片,而手機(jī)的相機(jī)分辨率大多時候是要高于手機(jī)屏幕的分辨率。鑒于手機(jī)的內(nèi)存有限,我們只需要在內(nèi)存中加載一個低分辨率的照片版本就可以了,而這個低分辨率的照片應(yīng)該與顯示它的控件相匹配,這就需要對圖片進(jìn)行壓縮處理了。

Android中有兩種壓縮圖片的方法。

  • 第一種是針對圖片的長寬進(jìn)行壓縮,在將圖片加載到內(nèi)存過程中將圖片的長寬進(jìn)行壓縮,獲取長寬壓縮版的的圖片
  • 第二種是針對圖片的像素進(jìn)行壓縮,圖片加載到內(nèi)存后,針對圖片質(zhì)量進(jìn)行壓縮,會導(dǎo)致圖片質(zhì)量下降。

3. 圖片長寬壓縮

3.1 獲取加載圖片的屬性

Android中的BitmapFactory類提供了一些解碼方法,decodeByteArray()、decodeFile()、decodeResource()等等,根據(jù)不通的圖片源選擇不同的解碼方法加載圖片創(chuàng)建出Bitmap。這些方法中都會傳入一個BitmapFactory.Options實例化對象,通過這個對象,可以更改一些加載圖片的設(shè)置。由于這些解碼方法用于解碼加載圖片,會占用內(nèi)存構(gòu)建Bitmap,因此很容易導(dǎo)致OOM的異常。
如果將options.inJustDecodeBounds設(shè)置為true,在解碼過程中就不會申請內(nèi)存去創(chuàng)建Bitmap,返回的是一個空的Bitmap,但是可以獲取圖片的一些屬性,例如圖片寬高,圖片類型等等。

BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;      // 設(shè)置為true,不將圖片解碼到內(nèi)存中
BitmapFactory.decodeResource(getResources(), R.id.myimage, options);
int imageHeight = options.outHeight;    // 圖片高度
int imageWidth = options.outWidth;      // 圖片寬度
String imageType = options.outMimeType; // 圖片類型

一般來說,為了避免OOM的異常,在加載圖片到內(nèi)存之前,會先檢查圖片的尺寸,除非你能確保圖片源不會導(dǎo)致OOM。

3.2 縮小圖片的長寬來壓縮圖片

我們知道圖片的大小之后,就可以決定是否將完整的圖片加載到內(nèi)存或者加載壓縮版的圖片到內(nèi)存??梢曰谝韵聨c(diǎn)做出決定:

  • 估計完整圖片加載到內(nèi)存中所使用內(nèi)存
  • 可分配給加載圖片的內(nèi)存
  • 用于顯示圖片的控件的大小
  • 當(dāng)前設(shè)備的屏幕大小和密度

例如,如果顯示圖片的控件大小為128x96像素,就沒有必要將一個1024x768像素的圖片加載到內(nèi)存中。

設(shè)置options.inSampleSize的數(shù)值,來控制壓縮圖片程度。例如,將options.inSampleSize設(shè)置為4,將一個2048x1536像素的圖片解碼加載到內(nèi)存后產(chǎn)生的Bitmap大約為512x384像素,如果使用的位圖配置是ARGB_8888,那么僅僅需要0.75M就加載了縮小版的圖片到內(nèi)存,而加載完整的圖片需要12M。

也就是說,如果我們設(shè)置inSampleSize == 2,解碼出來的位圖的寬高是原圖的1/2,圖片所占用內(nèi)存縮小了1/4(1/2 x 1/2)。如果inSampleSize設(shè)置的值小于等1,都會當(dāng)做inSampleSize == 1來解碼加載圖片。

于是我們可以在加載圖片的時候,根據(jù)控件的大小(顯示到屏幕上的大?。﹣碛嬎愠黾訅嚎s版圖片的inSampleSize值。

    /**
     * 計算inSampleSize值
     *
     * @param options
     *          用于獲取原圖的長寬
     * @param reqWidth
     *          要求壓縮后的圖片寬度
     * @param reqHeight
     *          要求壓縮后的圖片長度
     * @return
     *          返回計算后的inSampleSize值
     */
    public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
        // 原圖片的寬高
        final int height = options.outHeight;
        final int width = options.outWidth;
        int inSampleSize = 1;

        if (height > reqHeight || width > reqWidth) {

            final int halfHeight = height / 2;
            final int halfWidth = width / 2;

            
            // 計算inSampleSize值
            while ((halfHeight / inSampleSize) >= reqHeight
                    && (halfWidth / inSampleSize) >= reqWidth) {
                inSampleSize *= 2;
            }
        }

        return inSampleSize;
    }

有人可能會疑問為什么每次inSampleSize都是乘以2,指數(shù)增長。這是因為在加載圖片過程中,解析器使用的inSampleSize都是2的指數(shù)倍,如果inSampleSize是其他值,則找一個離這個值最近的2的指數(shù)值。

上面已經(jīng)獲取了inSampleSize,然后就可以根據(jù)這個值來加載壓縮版的圖片了。

public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
        int reqWidth, int reqHeight) {
    // 先將inJustDecodeBounds設(shè)置為true來獲取圖片的長寬屬性
    final BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeResource(res, resId, options);

    // 計算inSampleSize
    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

    // 加載壓縮版圖片
    options.inJustDecodeBounds = false;
    // 根據(jù)具體情況選擇具體的解碼方法
    return BitmapFactory.decodeResource(res, resId, options);
}

獲取到了壓縮版的Bitmap之后就可以直接設(shè)置到屏幕的控件上了。

mImageView.setImageBitmap(
    decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));

4. 圖片質(zhì)量壓縮

4.1 方法介紹

上面一種方法是通過縮放圖片的大小來達(dá)到壓縮效果,基本不會對圖片的顯示效果有影響。但是現(xiàn)在介紹的這一種方法,可能會導(dǎo)致圖片質(zhì)量下降。

使用的是下面這個方法來進(jìn)行壓縮。

Bitmap.compress(CompressFormat format, int quality, OutputStream stream)

這個方法有三個參數(shù),是布爾類型的返回值

  • CompressFormat 指定的Bitmap被壓縮成的圖片格式,只支持JPEG,PNG,WEBP三種
  • quality 圖片壓縮質(zhì)量的控制,范圍為0~100,0表示壓縮后體積最小,但是質(zhì)量也是最差,100表示壓縮后體積最大,但是質(zhì)量也是最好的(個人認(rèn)為相當(dāng)于未壓縮),有些格式,例如png,它是無損的,所以會忽略這個值。
  • OutputStream 壓縮后的數(shù)據(jù)會寫入這個字節(jié)流中
  • 返回值表示返回的字節(jié)流是否可以使用BitmapFactory.decodeStream()解碼成Bitmap,至于返回值是怎么得到的,因為是Native的代碼,沒法找到邏輯。

4.2 色位深度介紹

接下來說說為什么用這個方法可能會導(dǎo)致圖片質(zhì)量下降。在Bitmap中有一個Config的屬性,這個屬性是用來描述每個像素被儲存的大小。目前Config有四個值:ALPHA_8、RGB_565、ARGB_4444、ARGB_8888。這個說明一下(我個人的理解,真心不好解釋),每一個像素會可能由四個屬性組成,R(Red紅色通道)、G(Green綠色通道)、B(Blue藍(lán)色通道)、A(Alpha透明度通道)。

Config 每個像素占用的字節(jié) 說明
ALPHA_8 1 bytes 每個像素僅僅儲存透明度通道
RGB_565 2 bytes 每個像素的RGB通道會保存,透明度不會保存,紅色通道5位,有2 ^ 5=32種表現(xiàn)形式;綠色通道6位,有2 ^ 6=64種表現(xiàn)形式;藍(lán)色通道5位,有2 ^ 5=32種表現(xiàn)形式
ARGB_4444 2 bytes 每個像素的ARGB通道都會保存,透明度/紅色/綠色/藍(lán)色通道4位,有2 ^ 4=16種表現(xiàn)形式
ARGB_8888 4 bytes 每個像素的ARGB通道都會保存,透明度/紅色/綠色/藍(lán)色通道8位,有2 ^ 8=256種表現(xiàn)形式

有什么區(qū)別呢?最簡單的,當(dāng)一個顏色表現(xiàn)形式越多,那么畫面整體的色彩就會更豐富,圖片質(zhì)量就會越高,當(dāng)然,圖片占用的儲存空間也越大。

4.3 圖片質(zhì)量下降原因介紹

前面提到過調(diào)用Bitmap.compress()方法時候,會傳入一個壓縮后的圖片格式,但是由于并不是所有的圖片格式都支持上面說的Config的所有通道,比如說,JPEG格式的圖片,是不支持Alpha(透明度)屬性的,這樣將壓縮后返回的字節(jié)流通過BitmapFactory.decodeStream()轉(zhuǎn)換成Bitmap的過程中,會將透明度屬性給丟棄,導(dǎo)致圖片質(zhì)量下降。

4.4 壓縮過程介紹

壓縮過程如下,通過依次減少圖片質(zhì)量,將圖片大小控制在限制值范圍內(nèi)。

/**
 * 壓縮圖片
 * 
 * @param bitmap
 *          被壓縮的圖片
 * @param sizeLimit
 *          大小限制
 * @return
 *          壓縮后的圖片
 */
private Bitmap compressBitmap(Bitmap bitmap, long sizeLimit) {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    int quality = 100;
    bitmap.compress(Bitmap.CompressFormat.JPEG, quality, baos);

    // 循環(huán)判斷壓縮后圖片是否超過限制大小
    while(baos.toByteArray().length / 1024 > sizeLimit) {
        // 清空baos
        baos.reset();
        bitmap.compress(Bitmap.CompressFormat.JPEG, quality, baos);
        quality -= 10;
    }

    Bitmap newBitmap = BitmapFactory.decodeStream(new ByteArrayInputStream(baos.toByteArray()), null, null);
    
    return newBitmap;
}

5. 更近一步的優(yōu)化

上面提到的很多壓縮方法,如果是在UI線程執(zhí)行的話,很有可能阻塞到主線程,這是在開發(fā)過程中非常不愿意見到的事情,所以我們需要在后臺線程去執(zhí)行這些壓縮圖片比較耗時的操作,然后獲取到壓縮后的圖片,設(shè)置到屏幕中。使用AsyncTask可以幫助我們很好的實現(xiàn)。

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
    private final WeakReference<ImageView> imageViewReference;
    private int data = 0;

    public BitmapWorkerTask(ImageView imageView) {
        // 使用弱引用
        imageViewReference = new WeakReference<ImageView>(imageView);
    }

    // 在后臺線程壓縮圖片
    @Override
    protected Bitmap doInBackground(Integer... params) {
        data = params[0];
        return decodeSampledBitmapFromResource(getResources(), data, 100, 100));
    }

    // 壓縮完成后,將圖片設(shè)置到控件中
    @Override
    protected void onPostExecute(Bitmap bitmap) {
        if (imageViewReference != null && bitmap != null) {
            final ImageView imageView = imageViewReference.get();
            if (imageView != null) {
                imageView.setImageBitmap(bitmap);
            }
        }
    }
}

最終的執(zhí)行代碼。

    BitmapWorkerTask task = new BitmapWorkerTask(imageView);
    task.execute(resId);

6. 總結(jié)

圖片的處理,時刻都需要注意,因為機(jī)型配置的不同,以及現(xiàn)場設(shè)備內(nèi)存使用的情況,都有可能導(dǎo)致OOM的現(xiàn)象,上述提到了壓縮方法,基本適用與大部分圖片壓縮情況。當(dāng)然如果對圖片畫質(zhì)顯示有要求,可能就需要特殊的處理了,這個就不在大部分場景的考慮內(nèi)。

7. 高斯模糊的建議

我在項目中遇見的關(guān)于圖片操作的OOM異常,有80%源自于高斯模糊。是的,有些產(chǎn)品經(jīng)理為了和iOS保持一致,需要將某些頁面背景設(shè)置成高斯模糊效果。

一般的做法是將上一個頁面截圖,然后做高斯模糊處理,設(shè)置成背景。正好我接受過這種需求,說一下自己對于高斯模糊的建議。

  • 確定產(chǎn)品經(jīng)理的需求,高斯模糊的效果是不是一定要上。之前遇見一個需求,需要高斯模糊,結(jié)果Android做出來效果很不理想,后來,我把背景直接設(shè)置成60%的透明度白色。產(chǎn)品經(jīng)理看了之后,覺得Android的高斯模糊效果(其實是透明度)比iOS的要好一些,就讓iOS改。所以,一定要首先確認(rèn)產(chǎn)品經(jīng)理的需求,產(chǎn)品經(jīng)理想要的效果可能并不是他口中說出的效果,就像我遇見的這位,可能誤將透明度和高斯模糊混合了。
  • 如果高斯模糊效果一定要上。先將圖片長寬縮小,然后壓縮圖片質(zhì)量,再進(jìn)行高斯模糊的渲染,最后將高斯模糊之后的效果圖放大至控件大小,顯示到屏幕。
    • 縮放圖片長寬
    • 壓縮圖片質(zhì)量
    • 高斯模糊渲染
    • 放大高斯模糊效果圖

最后,希望Android工程師不要遇見高斯模糊的需求,因為,真的,很坑。但是如果遇見了,也不要怕,因為你已經(jīng)知道該如何處理了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容