android基礎知識:Google提供的高效載入大圖方案

鹹魚正翻身發表於2020-03-15

前言

最近線上有使用者反饋在App使用過程中遇到大圖的時,App異常的卡頓,甚至會出現崩潰的情況。後來排查了一番,發現一個同事在處理圖片時,直接原圖載入沒有做任何“壓縮”。這個case的出現,也就引出了這篇文章的必要性。

我們們日常開發過程中,都會使用各種各樣的圖片庫比如Glide。由於所有圖片操作都是一股腦的交給圖片庫去處理,所以即使在遇到大圖載入的時候,也無法“復現”這類問題。

因為主流的圖片庫都幫我們們對大圖進行了處理(正印證了那句話:當你能輕鬆進去的時候,你就該明白,不是你厲害,只是有人在前面替你開路——“魯訊”)。

既然話都說開了,我們們作為新時代下的福報程式設計師,那就必須要在這條路上探探深淺。其實圖片壓縮的方式有很多種,今天我們們只要一種,那就是Google原生的高效載入大圖的方案

正文

進行壓縮之前,我們們先來感受一下不壓縮會怎樣...

一、不壓縮,直接載入大圖

我隨便new了一下專案,搞了一個這樣的圖:

android基礎知識:Google提供的高效載入大圖方案

其實也不是特別大,就是一張1080P的圖。

然後隨便的用一個ImageView去載入一下:

iv.setImageResource(R.drawable.test)
複製程式碼

當我嘗試run的時候,我高估了我的測試機....沒有載入出來,就直接崩了。Logcat也是夠直接,無情吐槽:

android基礎知識:Google提供的高效載入大圖方案

android基礎知識:Google提供的高效載入大圖方案

這麼一張圖,一共需要132710400Bytes的記憶體,也就是132m....等等,不對?!解析度1080 * 1920的圖片怎麼可能會使用100+m的記憶體?

我們都知道,正常一個圖片被載入到記憶體裡的檔案大小 = 圖片解析度的寬 * 圖片解析度的高 * 色彩格式。帶入這個公式記憶體大小 = 1080 * 1920 * 4 = 7.9m,絕不可能是100+m這麼多!

這裡可能有朋友會有疑問,為啥JPEG的格式會4,JPEG格式沒有alpha通道,不應該佔這麼大的空間。其實具體幾,還是需要看這張圖最終Bitmap.Config解出來的值,我這張圖解出來是ARGB_8888,所以還是要*4。

如果你也有這個疑問,那麼接下來的內容你要好好看咯。這個知識點恐怕是盲區...

二、番外:drawble、drawble-xxhdpi有什麼區別

作為一個番外的內容部分。這一章節其實和圖片壓縮沒有什麼關係,只是額外聊一聊drawble這個資料夾

上述問題的根本原因就是在於檔案放置的位置,我只在drawble資料夾下放置了圖片資源。

所以...這種case下,如果載入這個資源的手機是一個高密度螢幕,那麼這張圖片被展示時,並非1080 * 1920...

接下來我們們來看一看,為什麼資原始檔隨便放會帶來這麼大的問題!(以下內容,部分來自於官方文件

android基礎知識:Google提供的高效載入大圖方案

文件中提到,如果資源提供不當,會導致縮放失真...。這裡為什麼系統要進行縮放其實也很好理解:

  • 對於系統來說,如果它向下(低密度)才找到需要引用的資原始檔,那麼最佳的策略便是將找到的圖片資源整體放大。因為那裡的圖,預期是給低解析度手機準備的。

  • 那麼同理,如果系統向上(高密度)找到了需要引用的資原始檔,那麼縮小無疑是最佳的選擇。因為那裡的圖,預期是給高解析度手機準備的。

所以基於此,上述中OOM的記憶體值132710400bytes是這麼算出來的:1080 * 4(這個4是手機dpi640 / 資源dpi160 所得) * 1920 * 4 * 4

小貼士:dpi = 手機解析度長寬各自平方之和開方,除以對角線長度(單位英寸)。 當然我們也可以通過api:resources.displayMetrics.xdpi。這裡得到的值就基本等於當前手機的dpi


所以,強制載入這麼大的一張圖,是不是不負責任!這麼大,硬往裡塞,擱誰誰受得了?

三、Google提供的解決方案

既然我們們已經明確硬來是不行了,所以還是要採取一些技巧的。文章中開篇就道出了問題的所在:

Images come in all shapes and sizes. In many cases they are larger than required for a typical application user interface (UI). For example, the system Gallery application displays photos taken using your Android devices's camera which are typically much higher resolution than the screen density of your device.

Given that you are working with limited memory, ideally you only want to load a lower resolution version in memory. The lower resolution version should match the size of the UI component that displays it. An image with a higher resolution does not provide any visible benefit, but still takes up precious memory and incurs additional performance overhead due to additional on the fly scaling.

簡單翻譯一下就是:太大就不要硬塞,縮到合適的尺寸再塞

文件裡還有比較有意思的一句話:There are several libraries that follow best practices for loading images. You can use these libraries in your app to load images in the most optimized manner. We recommend the Glide

官方推薦,最為致命

其實文件中直接貼出了可以Ctrl +C/V就能使用的程式碼:

imageView.setImageBitmap(
    decodeSampledBitmapFromResource(resources, R.id.myimage, 100, 100)
)

fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {
    // Raw height and width of image
    val (height: Int, width: Int) = options.run { outHeight to outWidth }
    var inSampleSize = 1

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

        val halfHeight: Int = height / 2
        val halfWidth: Int = width / 2

        // Calculate the largest inSampleSize value that is a power of 2 and keeps both
        // height and width larger than the requested height and width.
        while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) {
            inSampleSize *= 2
        }
    }

    return inSampleSize
}

fun decodeSampledBitmapFromResource(
        res: Resources,
        resId: Int,
        reqWidth: Int,
        reqHeight: Int
): Bitmap {
    // First decode with inJustDecodeBounds=true to check dimensions
    return BitmapFactory.Options().run {
        inJustDecodeBounds = true
        BitmapFactory.decodeResource(res, resId, this)

        // Calculate inSampleSize
        inSampleSize = calculateInSampleSize(this, reqWidth, reqHeight)

        // Decode bitmap with inSampleSize set
        inJustDecodeBounds = false

        BitmapFactory.decodeResource(res, resId, this)
    }
}
複製程式碼

程式碼很好理解,就是將需要載入的圖片,按目標所需的載入尺寸進行一次取樣,通過取樣的值進行等比縮放。

不過這裡有一個有趣的細節:官方的程式碼裡是將取樣結果進行了 * 2 (inSampleSize *= 2)。當時通過實戰我們會發現,inSampleSize並不一定要傳2的冪,傳3傳5傳其他也是有效果的。

文件中提到這麼一句話:

Note: A power of two value is calculated because the decoder uses a final value by rounding down to the nearest power of two, as per the inSampleSize documentation.(以2的冪作為計算結果,是根據inSampleSize文件,解碼器通過四捨五入到最接近的2的冪來使用最終值。)

按照文件的解釋inSampleSize為2/3時,效果一樣,畢竟3最接近2的冪的值還是2。當時事實跑起來會發現,2和3的結果並不一樣:

android基礎知識:Google提供的高效載入大圖方案

當inSampleSize = 3時,圖片長和寬就是比減少了3倍...所以真是不知道官網的葫蘆裡賣的什麼藥。

尾聲

到這,該嘮的基本也就嘮完了...內容並不深奧,但也算是必備的知識點~

我是一個應屆生,最近和朋友們維護了一個公眾號,內容是我們在從應屆生過渡到開發這一路所踩過的坑,以及我們一步步學習的記錄,如果感興趣的朋友可以關注一下,一同加油~

個人公眾號:鹹魚正翻身

相關文章