面試大廠,Android中的內存大戶Bitmap這些知識點集合還沒整明白就上場,果然被懟了...

前言

今天聊聊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,那麼decodebitmap爲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大佬學習筆記等等。

以上這些內容均免費分享給大家,需要完整版的朋友,點這裏可以看到全部內容
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章