前言
今天聊聊Bitmap相關的面試題/知識點,看看你是否都弄明白了呢?
- Bitmap是什麼,怎麼存儲圖片?
- Bitmap內存如何計算?
- Bitmap內存 和drawable目錄的關係。
- Bitmap加載優化?不改變圖片質量的情況下怎麼優化?
- inJustDecodeBounds是什麼?
- Bitmap內存複用怎麼實現?
- 高清大圖加載該怎麼處理?
- 如何跨進程傳遞大圖?
Bitmap是什麼,怎麼存儲圖片。
Bitmap
,位圖,本質上是一張圖片的內容在內存中的表達形式。它將圖片的內容看做是由存儲數據的有限個像素點組成;每個像素點存儲該像素點位置的ARGB
值,每個像素點的ARGB
值確定下來,這張圖片的內容就相應地確定下來。其中,A代表透明度,RGB代表紅綠藍三種顏色通道值。
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,依此類推。
面試前做好準備戰!
接下來將分享面試的一個複習路線,如果你也在準備面試但是不知道怎麼高效複習,可以參考一下我的複習路線,有任何問題也歡迎一起互相交流,加油吧!
這裏給大家提供一個方向,進行體系化的學習:
1、看視頻進行系統學習
前幾年的Crud經歷,讓我明白自己真的算是菜雞中的戰鬥機,也正因爲Crud,導致自己技術比較零散,也不夠深入不夠系統,所以重新進行學習是很有必要的。我差的是系統知識,差的結構框架和思路,所以通過視頻來學習,效果更好,也更全面。關於視頻學習,個人可以推薦去B站進行學習,B站上有很多學習視頻,唯一的缺點就是免費的容易過時。
另外,我自己也珍藏了好幾套視頻,有需要的我也可以分享給你。
2、進行系統梳理知識,提升儲備
客戶端開發的知識點就那麼多,面試問來問去還是那麼點東西。所以面試沒有其他的訣竅,只看你對這些知識點準備的充分程度。so,出去面試時先看看自己複習到了哪個階段就好。
系統學習方向:
架構師築基必備技能:深入Java泛型+註解深入淺出+併發編程+數據傳輸與序列化+Java虛擬機原理+反射與類加載+動態代理+高效IO
Android高級UI與FrameWork源碼:高級UI晉升+Framework內核解析+Android組件內核+數據持久化
360°全方面性能調優:設計思想與代碼質量優化+程序性能優化+開發效率優化
解讀開源框架設計思想:熱修復設計+插件化框架解讀+組件化框架設計+圖片加載框架+網絡訪問框架設計+RXJava響應式編程框架設計+IOC架構設計+Android架構組件Jetpack
NDK模塊開發:NDK基礎知識體系+底層圖片處理+音視頻開發
微信小程序:小程序介紹+UI開發+API操作+微信對接
Hybrid 開發與Flutter:Html5項目實戰+Flutter進階
知識梳理完之後,就需要進行查漏補缺,所以針對這些知識點,我手頭上也準備了不少的電子書和筆記,這些筆記將各個知識點進行了完美的總結。
3、讀源碼,看實戰筆記,學習大神思路
“編程語言是程序員的表達的方式,而架構是程序員對世界的認知”。所以,程序員要想快速認知並學習架構,讀源碼是必不可少的。閱讀源碼,是解決問題 + 理解事物,更重要的:看到源碼背後的想法;程序員說:讀萬行源碼,行萬種實踐。
主要內含微信 MMKV 源碼、AsyncTask 源碼、Volley 源碼、Retrofit源碼、OkHttp 源碼等等。
4、面試前夕,刷題衝刺
面試的前一週時間內,就可以開始刷題衝刺了。請記住,刷題的時候,技術的優先,算法的看些基本的,比如排序等即可,而智力題,除非是校招,否則一般不怎麼會問。
關於面試刷題,我個人也準備了一套系統的面試題,幫助你舉一反三:
還有耗時一年多整理的一系列Android學習資源:Android源碼解析、Android第三方庫源碼筆記、Android進階架構師七大專題學習、歷年BAT面試題解析包、Android大佬學習筆記
等等。