前言
今天聊聊Bitmap相關的面試題/知識點,看看你是否都弄明白了呢?html
-
Bitmap是什麼,怎麼存儲圖片?面試
-
Bitmap內存如何計算?緩存
-
Bitmap內存 和drawable目錄的關係。socket
-
Bitmap加載優化?不改變圖片質量的狀況下怎麼優化?ide
-
inJustDecodeBounds是什麼?學習
-
Bitmap內存複用怎麼實現?測試
-
高清大圖加載該怎麼處理?優化
-
如何跨進程傳遞大圖?this
Bitmap是什麼,怎麼存儲圖片。
Bitmap
,位圖,本質上是一張圖片的內容在內存中的表達形式。它將圖片的內容看作是由存儲數據的有限個像素點組成;每一個像素點存儲該像素點位置的ARGB
值,每一個像素點的ARGB
值肯定下來,這張圖片的內容就相應地肯定下來。其中,A表明透明度,RGB表明紅綠藍三種顏色通道值。spa
Bitmap內存如何計算
Bitmap一直都是Android
中的內存大戶,計算大小的方式有三種:
-
getRowBytes()
這個在API Level 1
添加的,返回的是bitmap一行所佔的大小,須要乘以bitmap的高,才能得出btimap的大小 -
getByteCount()
這個是在API Level 12
添加的,實際上是對getRowBytes()乘以高的封裝 -
getAllocationByteCount()
這個是在API Level 19
添加的
這裏我將一張圖片放到項目的drawable-xxhdpi文件夾中,而後經過方法獲取圖片所佔的內存大小:
var bitmap = BitmapFactory.decodeResource(resources, R.drawable.test) img.setImageBitmap(bitmap) Log.e(TAG,"dpi = ${resources.displayMetrics.densityDpi}") Log.e(TAG,"size = ${bitmap.allocationByteCount}")
打印出來的結果是
size=1960000
具體是怎麼計算的呢?
圖片內存=寬 * 高 * 每一個像素所佔字節。
這個像素所佔字節又和Bitmap.Config
有關,Bitmap.Config
是個枚舉類,用於描述每一個像素點的信息,好比:
-
ARGB_8888
。經常使用類型,總共32位,4個字節,分別表示透明度和RGB通道。 -
RGB_565
。16位,2個字節,只能描述RGB通道。
因此咱們這裏的圖片內存計算就得出:
寬700 * 高700 * 每一個像素4字節=1960000
Bitmap內存 和drawable目錄的關係
首先放一張drawable
目錄對應的屏幕密度對照表,來自郭霖的博客:
對照表
剛纔的案例,咱們是把圖片放到drawable-xxhdpi
文件夾,而drawable-xxhdpi
文件夾對應的dpi就是咱們測試手機的dpi—480。因此圖片的內存就是咱們所計算的寬 * 高 * 每一個像素所佔字節
。
若是咱們把圖片放到其餘的文件夾,好比drawable-hdpi
文件夾(對應的dpi是240),會發生什麼呢?
再次打印結果:
size = 7840000
這是由於一張圖片的實際佔用內存大小計算公式是:
佔用內存 = 寬 * 縮放比例 * 高 * 縮放比例 * 每一個像素所佔字節
這個縮放比例就跟屏幕密度DPI有關了:
縮放比例 = 設備dpi/圖片所在目錄的dpi
因此咱們這張圖片的實際佔用內存位:
寬700 * (480/240) * 高700 * (480/240) * 每一個像素4字節 = 7840000
Bitmap加載優化?不改變圖片質量的狀況下怎麼優化?
經常使用的優化方式是兩種:
-
修改Bitmap.Config
這一點剛纔也說過,不一樣的Conifg
表明每一個像素不一樣的佔用空間,因此若是咱們把默認的ARGB_8888
改爲RGB_565
,那麼每一個像素佔用空間就會由4字節變成2字節了,那麼圖片所佔內存就會減半了。
可能必定程度上會下降圖片質量,可是我實際測試看不出什麼變化。
-
修改inSampleSize
inSampleSize
,採樣率,這個參數是用於圖片尺寸壓縮的,他會在寬高的維度上每隔inSampleSize
個像素進行一次採集,從而達到縮放圖片的效果。這種方法只會改變圖片大小,不會影響圖片質量。
val options=BitmapFactory.Options() options.inSampleSize=2 val bitmap = BitmapFactory.decodeResource(resources, R.drawable.test2,options) img.setImageBitmap(bitmap)
實際項目中,咱們能夠設置一個與目標圖像大小相近的inSampleSize
,來減小實際使用的內存:
fun getImage(): Bitmap { var options = BitmapFactory.Options() options.inJustDecodeBounds = true BitmapFactory.decodeResource(resources, R.drawable.test2, options) // 計算最佳採樣率 options.inSampleSize = getImageSampleSize(options.outWidth, options.outHeight) options.inJustDecodeBounds = false return BitmapFactory.decodeResource(resources, R.drawable.test2, options) }
inJustDecodeBounds是什麼?
上面的例子你們應該發現了,其中有個inJustDecodeBounds
,又設置爲true,又設置成false的,總感受畫蛇添足,那麼他究竟是幹嗎呢?
由於咱們要獲取圖片自己的大小,若是直接decodeResource
加載一遍的話,那麼就會增長內存了,因此官方提供了這樣一個參數inJustDecodeBounds
。若是inJustDecodeBounds
爲ture,那麼decode
的bitmap
爲null,也就是不返回實際的bitmap
,只把圖片的大小信息放到了options的值中。
因此這個參數就是用來獲取圖片的大小信息的同時不佔用內存。
Bitmap內存複用怎麼實現?
若是有個需求,是在同一個imageview
中能夠加載不一樣的圖片,那咱們須要每次都去新建一個Bitmap
對象,佔用新的內存空間嗎?若是咱們這樣寫的話:
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.actvitiy_bitmap) btn1.setOnClickListener { img.setImageBitmap(getBitmap(R.drawable.test)) } btn2.setOnClickListener { img.setImageBitmap(getBitmap(R.drawable.test2)) } } fun getBitmap(resId: Int): Bitmap { var options = BitmapFactory.Options() return BitmapFactory.decodeResource(resources, resId, options) }
這樣就會Bitmap
就會頻繁去申請內存,釋放內存,從而致使大量GC
,內存抖動。
爲了防止這種狀況呢,咱們就能夠用到inBitmap
參數,用於Bitmap
的內存複用。這樣同一塊內存空間就能夠被多個Bitmap
對象複用,從而減小了頻繁的GC。
val options by lazy { BitmapFactory.Options() } val reuseBitmap by lazy { options.inMutable = true BitmapFactory.decodeResource(resources, R.drawable.test, options) } fun getBitmap(resId: Int): Bitmap { options.inMutable = true options.inBitmap = reuseBitmap return BitmapFactory.decodeResource(resources, resId, options) }
這裏有幾個要注意的點
-
inBitmap
要和inMutable
屬性配套使用,不然將沒法複用。 -
在
Android 4.4
以前,只能重用相同大小的Bitmap
內存區域;4.4以後
只要複用內存空間的Bitmap對象大小比inBitmap
指向的內存空間要小便可。
因此通常在複用以前,還要判斷下,新的Bitmap
內存是否是小於能夠複用的Bitmap
內存,而後才能進行復用。
高清大圖加載該怎麼處理?
若是是高清大圖,那就說明不容許進行圖片壓縮,好比微博長圖,清明上河圖。
因此咱們就要對圖片進行局部顯示,這就用到BitmapRegionDecoder
屬性,主要用於顯示圖片的某一塊矩形區域。
好比我要顯示左上角的100 * 100區域:
fun setImagePart() { val inputStream: InputStream = assets.open("test.jpg") val bitmapRegionDecoder: BitmapRegionDecoder = BitmapRegionDecoder.newInstance(inputStream, false) val options = BitmapFactory.Options() val bitmap = bitmapRegionDecoder.decodeRegion( Rect(0, 0, 100, 100), options) image.setImageBitmap(bitmap) }
實際項目使用中,咱們能夠根據手勢滑動,而後不斷更新咱們的Rect參數來實現具體的功能便可。
具體實現源碼能夠參考鴻洋的博客:https://blog.csdn.net/lmj623565791/article/details/49300989
如何跨進程傳遞大圖?
-
Bundle直接傳遞
。bundle最經常使用於Activity間傳遞,也屬於跨進程的一種方式,可是傳遞的大小有限制,通常爲1M。
//intent.put的putExtra方法實質也是經過bundle intent.putExtra("image",bitmap); bundle.putParcelable("image",bitmap)
Bitmap
之因此能夠直接傳遞,是由於其實現了Parcelable
接口進行了序列化。而Parcelable的傳遞原理是利用了Binder
機制,將Parcel
序列化的數據寫入到一個共享內存(緩衝區)中,讀取的時候也會從這個緩衝區中去讀取字節流,而後再反序列化成對象使用。這個共享內存也就是緩存區有一個大小限制—1M,並且是公用的。因此傳圖片的話很容易就容易超過這個大小而後報錯TransactionTooLargeException
。
因此這個方案不可靠。
-
文件傳輸
。
將圖片保存到文件,而後只傳輸文件路徑,這樣確定是能夠的,可是不高效。
-
putBinder
這個就是考點了。經過傳遞binder
的方式傳遞bitmap。
//傳遞binder val bundle = Bundle() bundle.putBinder("bitmap", BitmapBinder(mBitmap)) //接收binder中的bitmap val imageBinder: BitmapBinder = bundle.getBinder("bitmap") as BitmapBinder val bitmap: Bitmap? = imageBinder.getBitmap() //Binder子類 class BitmapBinder :Binder(){ private var bitmap: Bitmap? = null fun ImageBinder(bitmap: Bitmap?) { this.bitmap = bitmap } fun getBitmap(): Bitmap? { return bitmap } }
爲何用putBinder
就沒有大小限制了呢?
-
由於
putBinder
中傳遞的實際上是一個文件描述符fd,文件自己被放到一個共享內存中,而後獲取到這個fd以後,只須要從共享內存中取出Bitmap數據便可,這樣傳輸就很高效了。 -
而用
Intent/bundle
直接傳輸的時候,會禁用文件描述符fd,只能在parcel的緩存區中分配空間來保存數據,因此沒法突破1M的大小限制。
文件描述符是一個簡單的整數,用以標明每個被進程所打開的文件和socket。第一個打開的文件是0,第二個是1,依此類推。
參考
https://kaiwu.lagou.com/course/courseInfo.htm?courseId=67#/detail/pc?id=1872
https://www.cnblogs.com/shakinghead/p/11025805.html
https://blog.csdn.net/lmj623565791/article/details/49300989
https://blog.csdn.net/ylyg050518/article/details/97671874
拜拜
有一塊兒學習的小夥伴能夠關注下❤️ 個人公衆號——碼上積木,天天剖析一個知識點,咱們一塊兒積累知識。公衆號回覆111可得到面試題《思考與解答》以往期刊。