Android面試Android進階(十四)-Bitmap相關問題

問:drawable和mipmap的區別是什么?

答:根據官方說明:
應用圖標的圖片資源存放在mipmap系列文件夾中,而其余圖片存放在drawable系列文件夾中
1、mipmap紋理映射技術會將資源縮放到設備分辨率大小,drawable會將資源縮放到設備匹配的倍數大小
2、官方推薦開發者將位圖等資源放在對應dpi的drawable/下,而不是放在mipmap/下。這樣各種dpi可直接找到對應資源,減少了mipmap精確適配時需要縮放計算,也不會因為圖片縮放導致顯示問題
3、高密度系統的設備去使用低密度目錄下的圖片資源時,會將圖片長寬自動放大以去適應高密度的精度,當然圖片占用的內存會更大。
所以如果能提各種dpi的對應資源那是最好,可以達到較好內存使用效果。如果提供的圖片資源有限,那么圖片資源應該盡量放在高密度文件夾下,這樣可以節省圖片放大的內存開支

打包時,可以根據目標設備打不同的dpi圖片的包上架到應用市場中去,節省APK應用包體積。

問:Bitmap內存占用怎么算的?如加載一張1080*1920的圖片,內存占用多少?

答:Bitmap的內存占用的大小是通過:

寬 * 高 * 單位像素所占字節 //1080 * 1920 * 單位像素所占字節(ARGB值不同,占用字節不同)

Bitmap.Config中有四種不同的ARGB: ALPHA_8、RGB_565、ARGB_4444、ARGB_8888
ALPHA_8:每個像素占8位,沒有色彩,只有透明度A-8 即10801920(8/8/1024/1024)= 1.98M
RGB_565:每個像素每個像素占16位,沒有透明度 5+6+5 = 16 即10801920(16/8/1024/1024) = 3.96M
ARGB_4444:每個像素占16位,4+4+4+4 = 16 即 10801920(16/8/1024/1024) = 3.96M
ARGB_8888:每個像素占32位,8+8+8+8 = 32 即 10801920(32/8/1024/1024) = 7.92M

注意:加載圖片所在內存還和圖片放置的目錄有關系:放在mdpi、xhdpi之下是不一樣的。

在Android 160dpi是系統默認dpi

//獲取圖片bitmap寬高代碼:
Drawable drawable = imageView.getDrawable();
if (drawable != null) {
    BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
    Bitmap bitmap = bitmapDrawable.getBitmap();
    Log.d(TAG, " width = " + bitmap.getWidth() + " height = " + bitmap.getHeight());
} 

如果原圖大小:352 * 484
在320dpi(xhdpi)設備上運行,當圖片放置在xhdpi中時,獲取圖片寬高依然是352 * 484。當圖片放置在mdpi中時,獲取寬高是 704 * 968,設備是320dpi的設備,當放置在mdpi時,系統認為圖片需要放大,xhdpi是mdpi的兩倍,所以獲取bitmap的寬高放大了兩倍。

當圖片都放置在xhdpi時,使用320dpi(xhdpi)設備獲取圖片寬高是352 * 484,當使用480dpi(xxhdpi)設備獲取圖片寬高位 528 * 726,即在480dpi設備上時,xhdpi下的圖片都認為要被放大480/320(3/2)倍。

結論:
1、在同一個設備上,圖片放在依次放在由低到高的分辨率目錄中(mdpi~xxxhdpi),圖片的 Bitmap 內存的大小不斷減小。
2、在同一個分辨率目錄中,依次運行在由低到高的分辨率設備上,圖片的 Bitmap 的大小不斷增加。

所以:如果只使用一套圖片時,盡量把圖片放到最大分辨率目錄中

問:系統如何選擇drawable進行加載

答:Android系統中,在加載圖片時,會根據系統自身的dpi設備大小優先匹配最近的一個drawable目錄,如果當前目錄沒有找到,則向上查找,一直找到nodpi,如果都沒有找到則向下開始查找(肯定能找到,如果找不到編譯器就報錯了)。如:設備hdpi 則優先找drawable-hdpi目錄下的資源,如果沒有則向上 xhdpi、xxhdpi、xxxhdpi、nodpi,都沒有的話則開始查找mdpi...

問:Bitmap導致的OOM如何解決

答:Android加載大圖時極容易產生OOM,采用壓縮算法、緩存、軟引用、及時對不再使用的bitmap對象recycle釋放等方式解決。
通常會有四種壓縮方案:
1、質量壓縮:

   /**
    * 將圖片 bitmap 壓縮到指定大小 targetSize 以內 ,單位是 kb
    * 這里的大小指的是 “文件大小”,而不是 “內存大小”
    **/
   fun compressQuality(bitmap: Bitmap, targetSize: Int, declineQuality: Int = 10): ByteArray {
        val baos = ByteArrayOutputStream()
        bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos)
        var quality = 100
        while ((baos.toByteArray().size / 1024) > targetSize) {
            baos.reset()
            quality -= declineQuality
            bitmap.compress(Bitmap.CompressFormat.JPEG, quality, baos)
        }
        return baos.toByteArray()
    }

質量壓縮通過減少圖片色彩度,不會減少圖片像素及寬高,所以不會減少加載到內存中所占的內存大小,只會減少圖片所占磁盤的存儲大小,是一種有損壓縮。

2、采樣率壓縮:Options.inSampleSize

/**
   * 將圖片 [byteArray] 壓縮到 寬度小于 [targetWidth]、高度小于 [targetHeight]
   *
   **/
  fun compressInSampleSize(byteArray: ByteArray, targetWidth: Int, targetHeight: Int): Bitmap{

        val options = BitmapFactory.Options()  
        //設置inJustDecodeBounds = true 只加載圖片寬高等信息,不加載圖片到內存
        options.inJustDecodeBounds = true
        BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size, options)
        //默認采樣率為1,采樣率 > 1, 采樣率 inSampleSize 只能為 2 的整次冪,比如:2、4、8、16
        var inSampleSize = 1
        while (options.outWidth / inSampleSize > targetWidth || options.outHeight / inSampleSize > targetHeight) {
            inSampleSize *= 2
        }
        
        options.inJustDecodeBounds = false
        options.inSampleSize = inSampleSize
        val bitmap = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size, options)
  
        return bitmap 
    }

采樣率壓縮其原理是縮放 bitmap 的尺寸,采樣率inSampleSize為1時不變,2時寬高都變為原來的1/2,所占用內存大小就會變為原來的1/4,以此類推。
由于 inSampleSize 只能為 2 的整次冪,所以無法精確控制大小

3、縮放壓縮:Matrix矩陣

   /**
     * 將圖片 [bitmap] 壓縮到指定寬高范圍內
    **/
    fun compressScale(bitmap: Bitmap, targetWidth: Int, targetHeight: Int): Bitmap {
        return try {
            //計算縮放大小
            val scale = Math.min(targetWidth * 1.0f / bitmap.width, targetHeight * 1.0f / bitmap.height)
            //創建矩陣對象
            val matrix = Matrix()
            matrix.setScale(scale, scale)
            //利用矩陣對bitmap原始圖片進行壓縮生成新的bitmap
            val scaledBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true)

            scaledBitmap
        } catch (e: Exception) {
            e.printStackTrace()
            bitmap
        }
    }

縮放壓縮使用的是通過矩陣對圖片進行縮放,縮放后圖片的 寬度、高度以及占用的內存都會改變。

4、色彩模式壓縮:Options.inPreferredConfig = Bitmap.Config.XXXX

   /**
     * 將圖片格式更改為 Bitmap.Config.RGB_565,減少圖片占用的內存大小
    **/
    fun compressRGB565(byteArray: ByteArray): Bitmap {
        return try {
            val options = BitmapFactory.Options()
            options.inPreferredConfig = Bitmap.Config.RGB_565
            val compressedBitmap = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size, options)
            compressedBitmap
        } catch (e: Exception) {
            e.printStackTrace()
            BitmapFactory.decodeByteArray(ByteArray(0), 0, 0)
        }
    }

色彩模式壓縮后圖片的寬高不會產生變化,由于圖片的存儲格式改變,與 ARGB_8888 相比,每個像素的占用的字節由 8 變為 4 , 所以圖片占用的內存也為原來的一半。

緩存
簡單說一下緩存吧,后續看Glide源碼時再來了解。目前來講緩存一般有內存緩存和磁盤緩存(網絡緩存也不打算吧)
內存緩存:Android SDK中提供了一個LruCache,用于內存緩存。
磁盤緩存:Android SDK中不提供磁盤緩存的類,但google官方推薦的一個叫DiskLruCache算法。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容