動態

詳情 返回 返回

高性能圖片優化方案 - 動態 詳情

10.高性能圖片優化方案

目錄介紹
  • 01.圖片基礎概念介紹

    • 1.1 圖片佔用內存介紹
    • 1.2 網絡圖片加載流程
    • 1.3 三方庫加載圖片邏輯
    • 1.4 BitmapFactory
    • 1.5 圖片大小VS內存
    • 1.6 Bitmap能直接存儲嗎
    • 1.7 Bitmap創建流程
    • 1.8 圖片框架如何設計
  • 02.圖片內存計算方式

    • 2.1 如何計算佔用內存
    • 2.2 上面計算內存對嗎
    • 2.3 一個像素佔用內存
    • 2.4 使用API獲取內存
    • 2.5 影響Bitmap內存因素
    • 2.6 加載xhdpi和xxhdpi圖片
    • 2.7 圖片一些注意事項
  • 03.大圖的內存優化

    • 3.0 圖片壓縮核心思想
    • 3.1 常見圖片壓縮
    • 3.2 圖片尺寸壓縮
    • 3.3 圖片質量壓縮
    • 3.4 雙線性採樣壓縮
    • 3.5 高清圖分片加載
    • 3.6 魯班圖片綜合壓縮
  • 04.色彩格式及內存優化

    • 4.1 RGB顏色種類
    • 4.2 ARGB色彩模式
    • 4.3 改變色彩格式優化
  • 05.圖片內存緩存設計

    • 5.1 圖片內存緩存思想
    • 5.2 Lru內存緩存
    • 5.3 Lru緩存注意事項
    • 5.4 使用Lru磁盤緩存
  • 06.不同版本對Bitmap管理

    • 6.1 演變進程
    • 6.2 管理Bitmap內存
    • 6.3 提高Bitmap複用
  • 07.圖片其他方面優化

    • 7.1 減少PNG圖片使用
    • 7.2 控件切割圓角優化
    • 7.3 如何給圖片置灰色
    • 7.4 如何處理圖片旋轉呢
    • 7.5 保存圖片且刷相冊
    • 7.6 統一圖片域名優化
    • 7.7 優化H5圖片加載
    • 7.8 優化圖片陰影效果
    • 7.9 圖片資源的壓縮

01.圖片基礎概念介紹

1.1 圖片佔用內存介紹

移動設備的系統資源有限,所以應用應該儘可能的降低內存的使用。

在應用運行過程中,Bitmap (圖片)往往是內存佔用最大的一個部分,Bitmap 圖片的加載和處理,通常會佔用大量的內存空間,所以在操作 Bitmap 時,應該儘可能的小心。

Bitmap 會消耗很多的內存,特別是諸如照片等內容豐富的大圖。例如,一個手機拍攝的 2700 1900 像素的照片,需要 5.1M 的存儲空間,但是在圖像解碼配置 ARGB_8888 時,它加載到內存需要 19.6M 內存空間(2592 1936 * 4 bytes),從而迅速消耗掉該應用的剩餘內存空間。

OOM 的問題也是我們常見的嚴重問題,OOM 的產生的一個主要場景就是在大圖片分配內存的時候產生的,如果 APP 可用內存緊張,這時加載了一張大圖,內存空間不足以分配該圖片所需要的內存,就會產生 OOM,所以控制圖片的高效使用是必備技能。

1.2 網絡圖片加載流程

這一部分壓縮和緩存圖片,在glide源碼分析的文章裏已經做出了比較詳細的説明。在這裏簡單説一下圖片請求加載過程……

在使用App的時候,會經常需要加載一些網絡圖片,一般的操作步驟大概是這樣的:

  • 第一步從網絡加載圖片:一般都是通過網絡拉取的方式去服務器端獲取到圖片的文件流後,再通過BitmapFactory.decodeStream(InputStream)來加載圖片Bitmap。
  • 第二步這種壓縮圖片:網絡加載圖片方式加載一兩張圖片倒不會出現問題,但是如果短時間內加載十幾張或者幾十張圖片的時候,就很有可能會造成OOM(內存溢出),因為現在的圖片資源大小都是非常大的,所以我們在加載圖片之前還需要進行相應的圖片壓縮處理。
  • 第三步變換圖片:比如需要裁剪,切割圓角,旋轉,添加高斯模糊等屬性。
  • 第四步緩存圖片:但又有個問題來了,在使用移動數據的情況下,如果用户每次進入App的時候都會去進行網絡拉取圖片,這樣就會非常的浪費數據流量,這時又需要對圖片資源進行一些相應的內存緩存以及磁盤緩存處理,這樣不僅節省用户的數據流量,還能加快圖片的加載速度。
  • 第五步異步加載:雖然利用緩存的方式可以加快圖片的加載速度,但當需要加載很多張圖片的時候(例如圖片牆瀑布流效果),就還需用到多線程來加載圖片,使用多線程就會涉及到線程同步加載與異步加載問題。

1.3 三方庫加載圖片邏輯

先説出結論,目前市面較為常用的大概是Glide,Picasso,Fresco等。大概的處理圖片涉及主要邏輯有:

從網絡或者本地等路徑拉取圖片;然後解碼圖片;然後進行壓縮;接着會有圖片常用圓角,模糊或者裁剪等處理;然後三級緩存加載的圖片;當然加載圖片過程涉及同步加載和異步加載;最後設置到具體view控件上。

1.4 BitmapFactory

直接通過網絡請求將網絡圖片轉化成bitmap,在這將採用最原生的網絡請求方式HttpURLConnection方式進行圖片獲取。

經過測試,請求8張圖片,耗時毫秒值174。一般是通過get請求拉取圖片的。這種方法應該是最基礎的網絡請求,大家也可以回顧一下,一般開發中很少用這種方式加載圖片。具體可以看:ImageToolLib

如何加載一個圖片呢?可以看看BitmapFactory類為我們提供了四類方法來加載Bitmap:decodeFile、decodeResource、decodeStream、decodeByteArray;也就是説Bitmap,Drawable,InputStream,Byte[] 之間是可以進行轉換。

1.5 圖片大小VS內存

搞清楚一個圖片概念,在電腦上看到的 png 格式或者 jpg 格式的圖片,png(jpg) 只是這張圖片的容器。是經過相對應的壓縮算法將原圖每個像素點信息轉換用另一種數據格式表示。

加載圖片顯示到手機,通過代碼,將這張圖片加載進內存時,會先解析(也就是解碼操作)圖片文件本身的數據格式,然後還原為位圖,也就是 Bitmap 對象。

圖片大小vs圖片內存大小,一張 png 或者 jpg 格式的圖片大小,跟這張圖片加載進內存所佔用的大小完全是兩回事。

1.6 Bitmap能直接存儲嗎

Bitmap基礎概念,Bitmap對象本質是一張圖片的內容在手機內存中的表達形式。它將圖片的內容看做是由存儲數據的有限個像素點組成;每個像素點存儲該像素點位置的ARGB值。每個像素點的ARGB值確定下來,這張圖片的內容就相應地確定下來了。

Bitmap本質上不能直接存儲 為什麼?bitmap是一個對象,如果要存儲成本地可以查看的圖片文件,則必須對bitmap進行編碼,然後通過io流寫到本地file文件上。

1.7 Bitmap創建流程

對於圖片OOM,可以發現一個現象。heapsize(虛擬機的內存配置)越大越不容易 OOM,Android8.0 及之後的版本更不容易 OOM,這個該如何理解呢?Bitmap對象內存的變化:

  • 在 Android 8.0 之前,Bitmap 像素佔用的內存是在 Java heap 中分配的;8.0 及之後,Bitmap 像素佔用的內存分配到了 Native Heap。
  • 由於 Native heap 的內存分配上限很大,32 位應用的可用內存在 3~4G,64 位上更大,虛擬內存幾乎很難耗盡,所以推測 OOM 時 Java heap 中佔用內存較多的對象是 Bitmap” 成立的情況下,應用更不容易 OOM。

搞清楚Bitmap對象內存分配,Bitmap 的構造方法是不公開的,在使用 Bitmap 的時候,一般都是通過 Bitmap、BitmapFactory 提供的靜態方法來創建 Bitmap 實例。以 Bitmap.createBitmap 説明了 Bitmap 對象的主要創建過程分析,可以看到 Java Bitmap 對象是在 Native 層通過 NewObject 創建的。

  • allocateJavaPixelRef,是 8.0 之前版本為 Bitmap 像素從 Java heap 申請內存。其核心原理是Bitmap 的像素是保存在 Java 堆上。
  • allocateHeapBitmap,是 8.0 版本為 Bitmap 像素從 Native heap 申請內存。其核心原理主要是通過 calloc 為 Bitmap 的像素分配內存,這個分配就在 Native 堆上。

1.8 圖片框架如何設計

大多數圖片框架加載流程,概括來説,圖片加載包含封裝,解析,下載,解碼,變換,緩存,顯示等操作。

圖片框架是如何設計的

  • 封裝參數:從指定來源,到輸出結果,中間可能經歷很多流程,所以第一件事就是封裝參數,這些參數會貫穿整個過程;
  • 解析路徑:圖片的來源有多種,格式也不盡相同,需要規範化;比如glide可以加載file,io,id,網絡等各種圖片資源
  • 讀取緩存:為了減少計算,通常都會做緩存;同樣的請求,從緩存中取圖片(Bitmap)即可;
  • 查找文件/下載文件:如果是本地的文件,直接解碼即可;如果是網絡圖片,需要先下載;比如glide這塊是發起一個請求
  • 解碼:這一步是整個過程中最複雜的步驟之一,有不少細節;比如glide中解析圖片數據源,旋轉方向,圖片頭等信息
  • 變換和壓縮:解碼出Bitmap之後,可能還需要做一些變換處理(圓角,濾鏡等),還要做圖片壓縮;
  • 緩存:得到最終bitmap之後,可以緩存起來,以便下次請求時直接取結果;比如glide用到三級緩存
  • 顯示:顯示結果,可能需要做些動畫(淡入動畫,crossFade等);比如glide設置顯示的時候可以添加動畫效果

02.圖片內存計算方式

2.1 如何計算佔用內存

如果圖片要顯示下Android設備上,ImageView最終是要加載Bitmap對象的,就要考慮單個Bitmap對象的內存佔用了,如何計算一張圖片的加載到內存的佔用呢?其實就是所有像素的內存佔用總和:

bitmap內存大小 = 圖片長度 x 圖片寬度 x 單位像素佔用的字節數

起決定因素就是最後那個參數了,Bitmap常見有2種編碼方式:ARGB_8888和RGB_565,ARGB_8888每個像素點4個byte,RGB_565是2個byte,一般都採用ARGB_8888這種。那麼常見的1080*1920的圖片內存佔用就是:1920 x 1080 x 4 = 7.9M

2.2 上面計算內存對嗎

我看到好多博客都是這樣計算的,但是這樣算對嗎?有沒有哥們試驗過這種方法正確性?我覺得看博客要對博主表示懷疑,論證別人寫的是否正確。

説出我的結論:上面2.1這種説法也對,但是不全對,沒有説明場景,同時也忽略了一個影響項:Density。接下來看看源代碼。

inDensity默認為圖片所在文件夾對應的密度;inTargetDensity為當前系統密度。

加載一張本地資源圖片,那麼它佔用的內存 = width height nTargetDensity/inDensity 一個像素所佔的內存。

@Nullable
public static Bitmap decodeResourceStream(@Nullable Resources res, @Nullable TypedValue value,
        @Nullable InputStream is, @Nullable Rect pad, @Nullable Options opts) {
    validate(opts);
    if (opts == null) {
        opts = new Options();
    }

    if (opts.inDensity == 0 && value != null) {
        final int density = value.density;
        if (density == TypedValue.DENSITY_DEFAULT) {
            opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
        } else if (density != TypedValue.DENSITY_NONE) {
            opts.inDensity = density;
        }
    }
    
    if (opts.inTargetDensity == 0 && res != null) {
        opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
    }
    
    return decodeStream(is, pad, opts);
}

正確説法,這個注意呢?計算公式如下所示

  • 對資源文件:width height nTargetDensity/inDensity nTargetDensity/inDensity 一個像素所佔的內存;
  • 別的:width height 一個像素所佔的內存;

2.3 一個像素佔用內存

一個像素佔用多大內存?Bitmap.Config用來描述圖片的像素是怎麼被存儲的?

  • ARGB_8888: 每個像素4字節. 共32位,默認設置。
  • Alpha_8: 只保存透明度,共8位,1字節。
  • ARGB_4444: 共16位,2字節。
  • RGB_565:共16位,2字節,只存儲RGB值。

2.4 使用API獲取內存

Bitmap使用API獲取內存

  • getByteCount(),方法是在API12加入的,代表存儲Bitmap的色素需要的最少內存。API19開始getAllocationByteCount()方法代替了getByteCount()。
  • getAllocationByteCount(),API19之後,Bitmap加了一個Api:getAllocationByteCount();代表在內存中為Bitmap分配的內存大小。

思考:getByteCount()與getAllocationByteCount()的區別?

一般情況下兩者是相等的;通過複用Bitmap來解碼圖片,如果被複用的Bitmap的內存比待分配內存的Bitmap大,那麼getByteCount()表示新解碼圖片佔用內存的大小(並非實際內存大小,實際大小是複用的那個Bitmap的大小),getAllocationByteCount()表示被複用Bitmap真實佔用的內存大小(即mBuffer的長度)。

在複用Bitmap的情況下,getAllocationByteCount()可能會比getByteCount()大。

2.5 影響Bitmap內存因素

影響Bitmap佔用內存的因素:

  • 圖片最終加載的分辨率;
  • 圖片的格式(PNG/JPEG/BMP/WebP);
  • 圖片所存放的drawable目錄;
  • 圖片屬性設置的色彩模式;
  • 設備的屏幕密度;

2.6 加載xhdpi和xxhdpi圖片

提個問題,加載xhdpi和xxhdpi中相同的圖片,顯示在控件上會一樣嗎?內存大小一樣嗎?為什麼?

肯定是不一樣的。xhdpi:240dpi--320dpi,xxhdpi:320dpi--480dpi,

app中設置的圖片是如何從hdpi中查找的?

  • 首先計算dpi,比如手機分辨率是1920x1080,5.1寸的手機。那麼得到的dpi公式是(√ ̄1920² + 1080²)/5.1 =2202/5.1= 431dpi。這樣優先查找xxhdpi
  • 如果xxhdpi裏沒有查找圖片,如果沒有會往上找,遵循“先高再低”原則。如果xhdpi裏有這個圖片會使用xhdpi裏的圖片,這時候發現會比在xhdpi裏的圖片放大了。

為何要引入不同hdpi的文件管理?比如:xxhdpi放94x94,xhdpi放74x74,hdpi放45x45,這樣不管是什麼樣的手機圖片都能在指定的比例顯示。引入多種hdpi是為了讓這個圖片在任何手機上都是手機的這個比例。

2.7 圖片一些注意事項

同樣圖片顯示在大小不相同的ImageView上,內存是一樣嗎?圖片佔據內存空間大小與圖片在界面上顯示的大小沒有關係。

圖片放在res不同目錄,加載的內存是一樣的嗎?最終圖片加載進內存所佔據的大小會不一樣,因為系統在加載 res 目錄下的資源圖片時,會根據圖片存放的不同目錄做一次分辨率的轉換,而轉換的規則是:新圖的高度 = 原圖高度 * (設備的 dpi / 目錄對應的 dpi )

03.大圖的內存優化

3.0 圖片壓縮核心思想

圖片尺寸壓縮的核心思想是通過減少圖片的像素數量(分辨率)或調整圖片的質量(壓縮率)來降低圖片文件的大小,從而節省存儲空間、減少內存佔用以及加快圖片加載和傳輸速度。

  1. 減少分辨率:原理是通過降低圖片的寬度和高度(即減少像素數量),從而減少圖片的存儲大小。
  2. 降低質量:原理是通過調整圖片的壓縮率(如 JPEG 的質量參數),減少圖片文件的大小。
  3. 轉化圖片格式:不同的圖片格式(如 JPEG、PNG、WebP)具有不同的壓縮算法和特性,選擇合適的格式可以在保證質量的同時減少文件大小。

3.1 常見圖片壓縮

常見壓縮方法Api

  • 降低質量:Bitmap.compress(),質量壓縮,不會對內存產生影響;
  • 減少分辨率:BitmapFactory.Options.inSampleSize,內存壓縮;
Bitmap.compress()質量壓縮

質量壓縮,不會對內存產生影響。它是在保持像素的前提下改變圖片的位深及透明度等,來達到壓縮圖片的目的,不會減少圖片的像素。進過它壓縮的圖片文件大小會變小,但是解碼成bitmap後佔得內存是不變的。

BitmapFactory.Options.inSampleSize內存壓縮

解碼圖片時,設置BitmapFactory.Options類的inJustDecodeBounds屬性為true,可以在Bitmap不被加載到內存的前提下,獲取Bitmap的原始寬高。而設置BitmapFactory.Options的inSampleSize屬性可以真實的壓縮Bitmap佔用的內存,加載更小內存的Bitmap。

設置inSampleSize之後,Bitmap的寬、高都會縮小inSampleSize倍。例如:一張寬高為2048x1536的圖片,設置inSampleSize為4之後,實際加載到內存中的圖片寬高是512x384。佔有的內存就是0.75M而不是12M,足足節省了15倍。

備註:inSampleSize值的大小不是隨便設、或者越大越好,需要根據實際情況來設置。inSampleSize比1小的話會被當做1,任何inSampleSize的值會被取接近2的冪值。

3.2 圖片尺寸壓縮

3.2.1 如何理解尺寸壓縮

通常在大多數情況下,圖片的實際大小都比需要呈現的尺寸大很多。例如,我們的原圖是一張 2700 1900 像素的照片,加載到內存就需要 19.6M 內存空間,但是,我們需要把它展示在一個列表頁中,組件可展示尺寸為 270 190,這時,我們實際上只需要一張原圖的低分辨率的縮略圖即可(與圖片顯示所對應的 UI 控件匹配),那麼實際上 270 * 190 像素的圖片,只需要 0.2M 的內存即可。可以看到,優化前後相差了 98 倍,原來顯示 1 張,現在可以顯示 98 張圖片,效果非常顯著。

既然在對原圖縮放可以顯著減少內存大小,那麼我們應該如何操作呢?先加載到內存,再進行操作嗎,可以如果先加載到內存,好像也不太對,這樣只接佔用了 19.6M + 0.2M 2份內存了,而我們想要的是,在原圖不加載到內存中,只接將縮放後的圖片加載到內存中,可以實現嗎?

BitmapFactory 提供了從不同資源創建 Bitmap 的解碼方法:decodeByteArray()、decodeFile()、decodeResource() 等。

但是,這些方法在構造位圖的時候會嘗試分配內存,也就是它們會導致原圖直接加載到內存了,不滿足我們的需求。我們可以通過 BitmapFactory.Options 設置一些附加的標記,指定解碼選項,以此來解決該問題。

如何操作呢?答案來了:將 inJustDecodeBounds 屬性設置為 true,可以在解碼時避免內存的分配,它會返回一個 null 的 Bitmap ,但是可以獲取 outWidth、outHeight 和 outMimeType 值。利用該屬性,我們就可以在圖片不佔用內存的情況下,在圖片壓縮之前獲取圖片的尺寸。

怎樣才能對圖片進行壓縮呢?通過設置BitmapFactory.Options中inSampleSize的值就可以實現。其計算方式大概就是:計算出實際寬高和目標寬高的比率,然後選擇寬和高中最小的比率作為inSampleSize的值,這樣可以保證最終圖片的寬和高。

3.2.2 設置BitmapFactory.Options屬性

BitmapFactory.Options.inSampleSize 的核心思想是通過降低圖片的分辨率來減少內存佔用。它是在解碼階段生效的,因此它可以在圖片加載到內存之前就完成壓縮,避免不必要的內存消耗。

大概步驟如下所示:

  1. 要將BitmapFactory.Options的inJustDecodeBounds屬性設置為true,解析一次圖片。注意這個地方是核心,這個解析圖片並沒有生成bitmap對象(也就是説沒有為它分配內存控件),而僅僅是拿到它的寬高等屬性。
  2. 然後將BitmapFactory.Options連同期望的寬度和高度一起傳遞到到calculateInSampleSize方法中,就可以得到合適的inSampleSize值了。這一步會壓縮圖片。
  3. 再解析一次圖片,使用新獲取到的inSampleSize值,並把inJustDecodeBounds設置為false,就可以得到壓縮後的圖片了。此時才正式創建了bitmap對象,由於前面已經對它壓縮了,所以你會發現此時所佔內存大小已經很少了。

具體的實現代碼:

public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, 
        int reqWidth, int reqHeight) { 
    // 第一次解析將inJustDecodeBounds設置為true,來獲取圖片大小 
    final BitmapFactory.Options options = new BitmapFactory.Options(); 
    options.inJustDecodeBounds = true; 
    BitmapFactory.decodeResource(res, resId, options); 
    // 調用上面定義的方法計算inSampleSize值 
    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); 
    // 使用獲取到的inSampleSize值再次解析圖片 
    options.inJustDecodeBounds = false; 
    return BitmapFactory.decodeResource(res, resId, options); 
}

思考:inJustDecodeBounds這個參數是幹什麼的?如果設置為true則表示decode函數不會生成bitmap對象,僅是將圖像相關的參數填充到option對象裏,這樣我們就可以在不生成bitmap而獲取到圖像的相關參數了。

為何設置兩次inJustDecodeBounds屬性?

  • 第一次:設置為true則表示decode函數不會生成bitmap對象,僅是將圖像相關的參數填充到option對象裏,這樣我們就可以在不生成bitmap而獲取到圖像的相關參數。
  • 第二次:將inJustDecodeBounds設置為false再次調用decode函數時就能生成bitmap了。而此時的bitmap已經壓縮減小很多了,所以加載到內存中並不會導致OOM。

3.3 圖片質量壓縮

圖片質量壓縮核心思想是:一種通過減少圖像文件中像素的細節和信息來降低圖像文件大小的技術。這種壓縮方法通常會犧牲一定程度的圖像質量,以換取更小的文件大小。

  1. 減少細節和信息:圖片質量壓縮通過減少圖像文件中的細節和信息來降低文件大小。這可能包括減少顏色深度、降低圖像分辨率、去除不可見的細節等。
  2. 壓縮算法:常用的圖片質量壓縮算法是基於 JPEG 格式的壓縮。JPEG 壓縮算法通過調整圖像的色彩和細節來實現壓縮,可以通過控制壓縮質量參數來調整壓縮比例。
  3. 質量參數:JPEG 壓縮算法中的質量參數通常是一個介於 0 到 100 之間的值,表示壓縮的質量等級。

在Android中,對圖片進行質量壓縮,通常我們的實現方式如下所示:

//quality 為0~100,0表示最小體積,100表示最高質量,對應體積也是最大
bitmap.compress(Bitmap.CompressFormat.JPEG, quality , outputStream);

在上述代碼中,我們選擇的壓縮格式是CompressFormat.JPEG,除此之外還有兩個選擇:

  • 其一,CompressFormat.PNG,PNG格式是無損的,它無法再進行質量壓縮,quality這個參數就沒有作用了,會被忽略,所以最後圖片保存成的文件大小不會有變化;
  • 其二,CompressFormat.WEBP,這個格式是google推出的圖片格式,它會比JPEG更加省空間,經過實測大概可以優化30%左右。

Android質量壓縮邏輯,函數compress經過一連串的java層調用之後,最後來到了一個native函數:

具體看:Bitmap.cpp,最後調用了函數encoder->encodeStream(…)編碼保存本地。該函數是調用skia引擎來對圖片進行編碼壓縮。

3.4 雙線性採樣壓縮

核心思路是什麼:這種壓縮方法通過對圖像進行插值計算,以平滑地減少圖像的像素數量,同時儘可能保留圖像的細節和質量。

雙線性採樣(Bilinear Resampling)在 Android 中的使用方式一般有兩種:

bm = Bitmap.createScaledBitmap(bitmap, bitmap.getWidth()/2, bitmap.getHeight()/2, true);

//或者直接使用 matrix 進行縮放
Matrix matrix = new Matrix();
matrix.setScale(0.5f, 0.5f);
bm = Bitmap.createBitmap(bitmap, 0, 0, bit.getWidth(), bit.getHeight(), matrix, true);

看源碼可以知道createScaledBitmap函數最終也是使用第二種方式的matrix進行縮放

雙線性採樣使用的是雙線性內插值算法,這個算法不像鄰近點插值算法一樣,直接粗暴的選擇一個像素,而是參考了源像素相應位置周圍2x2個點的值,根據相對位置取對應的權重,經過計算之後得到目標圖像。

3.5 高清圖分片加載

如何理解高清圖分片加載的核心思想:將大尺寸的高清圖像分割成多個小塊(片段),然後根據顯示需求動態加載和拼接這些片段,以實現高清圖像的顯示。這種技術可以幫助減少內存佔用和提高性能,同時保持高清圖像的清晰度。

這種技術常用於需要展示高清圖像的應用場景,如圖片查看器、地圖應用等,以實現高質量的圖像顯示效果。

適用場景 : 當一張圖片非常大 , 在手機中只需要顯示其中一部分內容 , BitmapRegionDecoder 非常有用 。

主要作用 : BitmapRegionDecoder 可以從圖像中 解碼一個矩形區域 。相當於手在滑動的過程中,計算當前顯示區域的圖片繪製出來。

基本使用流程 : 先創建,後解碼 。調用 newInstance 方法 , 創建 BitmapRegionDecoder 對象 ;然後調用 decodeRegion 方法 , 獲取指定 Rect 矩形區域的解碼後的 Bitmap 對象。

3.6 魯班圖片綜合壓縮

一般情況下圖片綜合壓縮的整體思路如下:

  • 第一步進行採樣率壓縮;
  • 第二步進行寬高的等比例壓縮(微信對原圖和縮略圖限制了最大長寬或者最小長寬);
  • 第三步就是對圖片的質量進行壓縮(一般75或者70);
  • 第四步就是採用webP的格式。

關於圖片壓縮的綜合案例如下,具體可以參考:CompressServer

04.色彩格式及內存優化

4.1 RGB顏色種類

RGB 色彩模式是工業界的一種顏色標準,通過對紅(R)、綠(G)、藍(B)三個顏色通道的變化以及它們相互之間的疊加來得到各式各樣的顏色的,RGB即是代表紅、綠、藍三個通道的顏色,這個標準幾乎包括了人類視力所能感知的所有顏色,是運用最廣的顏色系統之一。Android 中,像素的存儲方式使用的色彩模式正是 RGB 色彩模式。

  1. 基本原理:RGB 色彩模式基於三種原色(紅、綠、藍),通過不同強度的這三種顏色的組合來產生各種其他顏色。通過調整每種原色的亮度和飽和度,可以生成數百萬種不同的顏色。
  2. 顏色表示:在 RGB 色彩模式中,每種顏色由一個三元組(R,G,B)表示,其中 R、G、B 的取值範圍通常是 0 到 255,表示每種顏色的強度。例如,(255, 0, 0) 表示純紅色,(0, 255, 0) 表示純綠色,(0, 0, 255) 表示純藍色。
  3. 顏色混合:通過調整不同原色的強度,可以混合出各種中間色。例如,紅色和綠色的混合會產生黃色。
  4. 色彩範圍:RGB 色彩模式可以表示的顏色範圍非常廣泛,可以生成幾乎所有可見顏色,包括各種飽和度和亮度的顏色。

4.2 ARGB色彩模式

ARGB 色彩模式是一種在計算機圖形學和圖像處理中常用的色彩表示方式,它是在 RGB 色彩模式的基礎上增加了一個 Alpha 通道,用於表示像素的透明度。

在 Android 中,我們常見的一些顏色設置,都是 RGB 色彩模式來描述像素顏色的,並且他們都帶有透明度通道,也就是所謂的 ARGB。例如,我們常見的顏色定義如下:

//在代碼中定義顏色值:藍色
public final int blue=0xff0000ff;

//或者在xml中定義:
<drawable name="blue">#ff0000ff</drawable>  

以上設置中,顏色值都是使用 16 進制的數字來表示的。以上顏色值都是帶有透明度(透明通道)的顏色值,格式是 AARRGGBB,透明度、紅色、綠色、藍色四個顏色通道,各佔有 2 位,也就是一個顏色通道,使用了 1 個字節來存儲。

4.3 改變色彩格式優化

Android 中有多種 RGB 模式,我們可以設置不同的格式,來控制圖片像素顏色的顯示質量和存儲空間。

Android.graphics.Bitmap 類裏有一個內部類 Bitmap.Config 類,它定義了可以在 Android 中使用的幾種色彩格式:

public enum Config {
    ALPHA_8     (1),
    RGB_565     (3),
    @Deprecated
    ARGB_4444   (4),
    ARGB_8888   (5),
    RGBA_F16    (6),
    HARDWARE    (7);
}

解釋一下這幾個值分別代表了什麼含義?我們已經知道了:A 代表透明度、R 代表紅色、G 代表綠色、B 代表藍色。

  • ALPHA_8:表示,只存在 Alpha 通道,沒有存儲色彩值,只含有透明度,每個像素佔用 1 個字節的空間。
  • RGB_565:表示,R 佔用 5 位二進制的位置,G 佔用了6位,B 佔用了 5 位。每個像素佔用 2 個字節空間,並且不包含透明度。
  • ARGB_4444:表示,A(透明度)、R(紅色)、G(綠色)、B(藍色)4個通道各佔用 4 個 bit 位。每個像素佔用 2 個字節空間。
  • ARGB_8888:表示,A(透明度)、R(紅色)、G(綠色)、B(藍色)4個通道各佔用 8 個 bit 位。每個像素佔用 4 個字節空間。
  • RGBA_F16:表示,每個像素存儲在8個字節上。此配置特別適合廣色域和HDR內容。
  • HARDWARE:特殊配置,當位圖僅存儲在圖形內存中時。 此配置中的位圖始終是不可變的。

那麼開發中一般選擇哪一種比較合適呢

  • Android 中的圖片在加載時,默認的色彩格式是 ARGB_8888,也就是每個像素佔用 4 個字節空間,一張 2700 1900 像素的照片,加載到內存就需要 19.6M 內存空間(2592 1936 * 4 bytes)。
  • 如果圖片在 UI 組件中顯示時,不需要太高的圖片質量,例如顯示一張縮略圖(不透明圖片)等場景,這時,我們就沒必要使用 ARGB_8888 的色彩格式了,只需要使用 RGB_565 模式即可滿足顯示的需要。
  • 那麼,我們的優化操作就可以是:將 2700 1900 像素的原圖,壓縮到原圖的低分辨率的縮略圖 270 190 像素的圖片,這時需要 0.2M 的內存。也就是從 19.6M內存,壓縮為 0.2 M內存。
  • 我們還可以進一步優化色彩格式,由 ARGB_8888 改為 RGB_565 模式,這時,目標圖片需要的內存就變為 270 190 2 = 0.1M 了。圖片內存空間又減小了一倍。

05.圖片內存緩存設計

5.1 圖片內存緩存思想

圖片內存緩存的設計是優化應用性能的關鍵。內存緩存的核心思想是利用內存的高速讀寫特性,緩存最近使用的圖片,從而避免重複加載和減少磁盤 I/O 操作。

  1. 快速訪問:內存緩存的讀寫速度遠高於磁盤緩存和網絡加載,因此將最近使用的圖片保存在內存中,可以顯著提升圖片加載速度。
  2. 有限資源管理:內存資源有限,因此需要合理管理緩存大小,避免佔用過多內存導致應用崩潰或性能下降。
  3. 淘汰策略:當緩存達到上限時,需要淘汰部分緩存項以釋放空間。常用的淘汰策略是 LRU(Least Recently Used,最近最少使用)。

5.2 Lru內存緩存

LruCache 類特別適合用來緩存 Bitmap,它使用一個強引用的 LinkedHashMap 保存最近引用的對象,並且在緩存超出設定大小時,刪除最近最少使用的對象。

給 LruCache 確定一個合適的緩存大小非常重要,我們需要考慮幾個因素:

  • 應用剩餘多少可用內存?
  • 需要有多少張圖片同時顯示到屏幕上?有多少圖片需要準備好以便馬上顯示到屏幕?
  • 設備的屏幕大小和密度是多少?高密度的設備需要更大的緩存空間來緩存同樣數量的圖片。
  • Bitmap 的尺寸配置是多少,花費多少內存?
  • 圖片被訪問的頻率如何?如果其中一些比另外一些訪問更頻繁,那麼我們可能希望在內存中保存那些最常訪問的圖片,或者根據訪問頻率給 Bitmap 分組,為不同的 Bitmap 組設置多個 LruCache 對象。
  • 是否可以在緩存圖片的質量和數量之間尋找平衡點?有時,保存大量低質量的 Bitmap 會非常有用,加載更高質量的圖片的任務可以交給另外一個後台線程處理。
  • 緩存太小會導致額外的花銷卻沒有明顯的好處,緩存太大同樣會導致 java.lang.OutOfMemory 的異常,並且使得你的程序只留下小部分的內存用來工作(緩存佔用太多內存,導致其他操作會因為內存不夠而拋出異常)。所以,我們需要分析實際情況之後,提出一個合適的解決方案。

LruCache是Android提供的一個緩存類,通常運用於內存緩存,LruCache是一個泛型類,它的底層是用一個LinkedHashMap以強引用的方式存儲外界的緩存對象來實現的。

為什麼使用LinkedHashMap來作為LruCache的存儲,是因為LinkedHashMap有兩種排序方式,一種是插入排序方式,一種是訪問排序方式,默認情況下是以訪問方式來存儲緩存對象的;LruCache提供了get和put方法來完成緩存的獲取和添加,當緩存滿時,會將最近最少使用的對象移除掉,然後再添加新的緩存對象。如下源碼所示,底層是LinkedHashMap。

public LruCache(int maxSize) {
    if (maxSize <= 0) {
        throw new IllegalArgumentException("maxSize <= 0");
    }
    this.maxSize = maxSize;
    this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
}

在使用LruCache的時候,首先需要獲取當前設備的內存容量,通常情況下會將總容量的八分之一作為LruCache的容量,然後重寫LruCache的sizeof方法,sizeof方法用於計算緩存對象的大小,單位需要與分配的容量的單位一致;

// 獲取系統最大緩存
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
// set LruCache size;
// 使用最大可用內存值的1/8作為緩存的大小
int cacheSize = maxMemory / 8;
LruCache<String, Bitmap> memoryCache = new LruCache<String, Bitmap>(cacheSize) {
    @Override
    protected int sizeOf(@NonNull String uri, @NonNull Bitmap bitmap) {
        // 重寫此方法來衡量每張圖片的大小,默認返回圖片數量
        return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
    }
};
//插入對象
memoryCache.put(key, bitmap);
//取出對象
memoryCache.get(key);

如何淘汰緩存,這個就要看LinkedHashMap集合的特點呢!LinkedHashMap 構造函數的第三個參數:accessOrder,傳入true時, 元素會按訪問順序排列,最後訪問的在遍歷器最後端。在進行淘汰時,移除遍歷器前端的元素,直至緩存總大小降低到指定大小以下。

5.3 Lru緩存注意事項

看一個真實的場景:假設我們的LruCache可以緩存80張,每次刷新從網絡獲取20張圖片且不重複,那麼在刷新第五次的時候,根據LruCache緩存的規則,第一次刷新的20張圖片就會從LruCache中移出,處於等待被系統GC的狀態。如果我們繼續刷新n次,等待被回收的張數就會累積到 20 * n 張。

會出現什麼問題:會出現大量的Bitmap內存碎片,我們不知道系統什麼時候會觸發GC回收掉這些無用的Bitmap,對於內存是否會溢出,是否會頻繁GC導致卡頓等未知問題。

解決方案該怎麼做?

第一種:在3.0以後引入了 BitmapFactory.Options.inBitmap,如果設置此項,需要解碼的圖片就會嘗試使用該Bitmap的內存,這樣取消了內存的動態分配,提高了性能,節省了內存。

第二種:把處於無用的狀態的Bitmap放入SoftReference。SoftReference引用的對象會在內存溢出之前被回收。

關於Lru緩存案例和代碼可以參考:AppLruCache

5.4 使用Lru磁盤緩存

內存緩存能夠提高訪問最近用過的 Bitmap 的速度,但是我們無法保證最近訪問過的 Bitmap 都能夠保存在緩存中。像類似 GridView 等需要大量數據填充的控件很容易就會用盡整個內存緩存。另外,我們的應用可能會被類似打電話等行為而暫停並退到後台,因為後台應用可能會被殺死,那麼內存緩存就會被銷燬,裏面的 Bitmap 也就不存在了。一旦用户恢復應用的狀態,那麼應用就需要重新處理那些圖片。

磁盤緩存可以用來保存那些已經處理過的 Bitmap,它還可以減少那些不再內存緩存中的 Bitmap 的加載次數。當然從磁盤讀取圖片會比從內存要慢,而且由於磁盤讀取操作時間是不可預期的,讀取操作需要在後台線程中處理。

注意:如果圖片會被更頻繁的訪問,使用 ContentProvider 或許會更加合適,比如在圖庫應用中。

注意:因為初始化磁盤緩存涉及到 I/O 操作,所以它不應該在主線程中進行。但是這也意味着在初始化完成之前緩存可以被訪問。為了解決這個問題,在上面的實現中,有一個鎖對象(lock object)來確保在磁盤緩存完成初始化之前,應用無法對它進行讀取。

內存緩存的檢查是可以在 UI 線程中進行的,磁盤緩存的檢查需要在後台線程中處理。磁盤操作永遠都不應該在 UI 線程中發生。當圖片處理完成後,Bitmap 需要添加到內存緩存與磁盤緩存中,方便之後的使用。

06.不同版本對Bitmap管理

6.1 演變進程

Android 2.3.3 (API level 10)以及之前,一個 Bitmap 的像素數據是存放在 Native 內存空間中的。這些數據與 Bitmap 對象本身是隔離的,Bitmap 本身被存放在 Dalvik 堆中。並且無法預測在 Native 內存中的像素級數據何時會被釋放,這意味着程序容易超過它的內存限制並且崩潰。

Android 3.0 (API Level 11)開始,像素數據則是與 Bitmap 本身一起存放在 Dalvik 堆中。

Android 8.0(Android O)及之後的版本中,Bitmap 的像素數據的內存分配又回到了 Native 層,它是在 Native 堆空間進行分配的。

6.2 管理Bitmap內存

管理 Android 2.3.3 及以下版本的內存使用。在 Android 2.3.3 (API level 10) 以及更低版本上,推薦使用 recycle() 方法。 如果在應用中顯示了大量的 Bitmap 數據,我們很可能會遇到 OutOfMemoryError 的錯誤。 recycle() 方法可以使得程序更快的釋放內存。

管理 Android 3.0 及其以上版本的內存,從 Android 3.0 (API Level 11)開始,引進了 BitmapFactory.Options.inBitmap 字段。 如果使用了這個設置字段,decode 方法會在加載 Bitmap 數據的時候去重用已經存在的 Bitmap。這意味着 Bitmap 的內存是被重新利用的,這樣可以提升性能,並且減少了內存的分配與回收。然而,使用 inBitmap 有一些限制,特別是在Android 4.4 (API level 19)之前,只有同等大小的位圖才可以被重用。

管理 Android 8.0 及其以上版本的內存。在 Android 8.0 及其以上版本,處理內存,也遵循 Android 3.0 以上版本同樣的方式。同時,圖片像素數據存儲在 native 層,並且不佔用 Java 堆的空間,這也代表着我們擁有更大的圖片存儲空間,可以加載質量更高、數據更多的圖片到內存中。但是,內存依然不是無限的,應用還是要受到手機內存的限制,所以一定要注意這一點。

6.3 提高Bitmap複用

Android3.0之後,並沒有強調Bitmap.recycle();而是強調Bitmap的複用。

使用LruCache對Bitmap進行緩存,當再次使用到這個Bitmap的時候直接獲取,而不用重走編碼流程。

Android3.0(API 11之後)引入了BitmapFactory.Options.inBitmap字段,設置此字段之後解碼方法會嘗試複用一張存在的Bitmap。這意味着Bitmap的內存被複用,避免了內存的回收及申請過程,顯然性能表現更佳。

使用這個字段有幾點限制:

  • 聲明可被複用的Bitmap必須設置inMutable為true;
  • Android4.4(API 19)之前只有格式為jpg、png,同等寬高(要求苛刻),inSampleSize為1的Bitmap才可以複用;
  • Android4.4(API 19)之前被複用的Bitmap的inPreferredConfig會覆蓋待分配內存的Bitmap設置的inPreferredConfig;
  • Android4.4(API 19)之後被複用的Bitmap的內存必須大於需要申請內存的Bitmap的內存;
  • Android4.4(API 19)之前待加載Bitmap的Options.inSampleSize必須明確指定為1。

Bitmap複用的實驗,代碼如下所示,然後看打印的日誌信息

  • 從內存地址的打印可以看出,兩個對象其實是一個對象,Bitmap複用成功;
  • bitmapReuse佔用的內存(4346880)正好是bitmap佔用內存(1228800)的四分之一;
  • getByteCount()獲取到的是當前圖片應當所佔內存大小,getAllocationByteCount()獲取到的是被複用Bitmap真實佔用內存大小。雖然bitmapReuse的內存只有4346880,但是因為是複用的bitmap的內存,因而其真實佔用的內存大小是被複用的bitmap的內存大小(1228800)。這也是getAllocationByteCount()可能比getByteCount()大的原因。
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
private void initBitmap() {
    BitmapFactory.Options options = new BitmapFactory.Options();
    // 圖片複用,這個屬性必須設置;
    options.inMutable = true;
    // 手動設置縮放比例,使其取整數,方便計算、觀察數據;
    options.inDensity = 320;
    options.inTargetDensity = 320;
    Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.bg_autumn_tree_min, options);
    // 對象內存地址;
    Log.i("ycBitmap", "bitmap = " + bitmap);
    Log.i("ycBitmap", "ByteCount = " + bitmap.getByteCount() + ":::bitmap:AllocationByteCount = " + bitmap.getAllocationByteCount());
    // 使用inBitmap屬性,這個屬性必須設置;
    options.inBitmap = bitmap; options.inDensity = 320;
    // 設置縮放寬高為原始寬高一半;
    options.inTargetDensity = 160;
    options.inMutable = true;
    Bitmap bitmapReuse = BitmapFactory.decodeResource(getResources(), R.drawable.bg_kites_min, options);
    // 複用對象的內存地址;
    Log.i("ycBitmap", "bitmapReuse = " + bitmapReuse);
    Log.i("ycBitmap", "bitmap:ByteCount = " + bitmap.getByteCount() + ":::bitmap:AllocationByteCount = " + bitmap.getAllocationByteCount());
    Log.i("ycBitmap", "bitmapReuse:ByteCount = " + bitmapReuse.getByteCount() + ":::bitmapReuse:AllocationByteCount = " + bitmapReuse.getAllocationByteCount());
  
    //11-26 18:24:07.971 15470-15470/com.yc.ycbanner I/ycBitmap: bitmap = android.graphics.Bitmap@9739bff
    //11-26 18:24:07.972 15470-15470/com.yc.ycbanner I/ycBitmap: bitmap:ByteCount = 4346880:::bitmap:AllocationByteCount = 4346880
    //11-26 18:24:07.994 15470-15470/com.yc.ycbanner I/ycBitmap: bitmapReuse = android.graphics.Bitmap@9739bff
    //11-26 18:24:07.994 15470-15470/com.yc.ycbanner I/ycBitmap: bitmap:ByteCount = 1228800:::bitmap:AllocationByteCount = 4346880
    //11-26 18:24:07.994 15470-15470/com.yc.ycbanner I/ycBitmap: bitmapReuse:ByteCount = 1228800:::bitmapReuse:AllocationByteCount = 4346880
}

07.圖片其他方面優化

7.1 減少PNG圖片使用

這裏要介紹一種新的圖片格式:Webp,它是由 Google 推出的一種既保留 png 格式的優點,又能夠減少圖片大小的一種新型圖片格式。

在 Android 4.0(API level 14) 中支持有損的 WebP 圖像,在 Android 4.3(API level 18) 和更高版本中支持無損和透明的 WebP 圖像。

注意一下,Webp格式圖片僅僅只是減少圖片的質量大小,並不會減少加載圖片後的內存佔用。

Android 為何推薦用WebP格式圖片:WebP 格式的圖片壓縮率通常比 JPEG 和 PNG 更高,可以在保證圖像質量的前提下顯著減少文件大小。支持無損壓縮,支持透明度等。

7.2 切割圓角優化

業務背景介紹:在顯示圖片是有時候需要顯示圓角圖片,我們應該都知道圓角顯示肯定是更加耗費內存和性能,會導致圖片的過度繪製等問題。

給控件設置圓角,代碼層面一般怎麼做?

  1. 第一種:比如給TextView設置Shape圓角
  2. 第二種:自定義控件實現,大概原理:要實圓角或者圓形的顯示效果,就是對圖片顯示的內容區域進行“裁剪”,只顯示指定的區域即可。
  3. 第三種:使用Glide加載圖片設置圓角

clipPath切割圓角的核心原理

第一步:圖片是被繪製在畫布上的,所以用 canvas 的 clipPath()方法先將畫布裁剪成指定形狀。由於clipPath()方法不支持抗鋸齒,圖片邊緣會有明顯的毛糙感,體驗並不理想。

setXfermode切割圓角的核心原理

使用圖像的 Alpha 合成模式。整個過程就是先繪製目標圖像,也就是圖片;再繪製原圖像,即一個圓角矩形或者圓形,這樣最終目標圖像只顯示和原圖像重合的區域。如果是圖片,需要通過src屬性或者對應的方法來設置圖片,否則不能達到預期效果。

  1. 第一步:核心邏輯是在draw方法中進行繪製,首先調用canvas.saveLayer設置離屏緩存,新建一個控件區域大小的圖層
  2. 第二步:相當於使用super.onDraw繪製自己,這一步調用super方法即可
  3. 第三步:設置畫筆Paint屬性,先調用setXfermode設置混合模式,然後在調用canvas.drawPath(path, paint)繪製path,最後再清除Xfermode
  4. 第四步:添加邊框,只需要繪製一個指定樣式的圓角矩形或者圓形即可。比如繪製圓角,調用Path.addRoundRect添加path,然後調用canvas.drawPath繪製path

目前設置控件圓角有哪些方式

  1. 第一種:比如給TextView設置Shape圓角,非常常見的使用
  2. 第二種:使用背景圖片
  3. 第三種:自定義控件實現
  4. 第四種:使用ViewOutlineProvider裁剪View
  5. 第五種:使用CardView
  6. 第六種:使用Glide加載圖片設置圓角

各種設置圓角的優缺點對比

  1. 第一種:shape常見,簡單直觀。缺點是項目中xml,越寫越多
  2. 第二種:使用切圖沒什麼説的,使用起來不方便
  3. 第三種:自定義控件,彌補shape上不足,採用attr屬性設置圓角,那樣圓角樣式多,使用起來方便
  4. 第四種:用於實現view陰影和輪廓
  5. 第五種:使用CardView,官方支持陰影和圓角控件
  6. 第六種:使用Glide加載圓角,一般用於圖片設置,比較方便

具體案例可見:RoundCorners

7.3 如何給圖片置灰色

大概的操作步驟。具體可以參考:PicCalculateUtils

  • 第一步:獲取原始圖片的寬高,然後創建一個bitmap可變位圖對象。
  • 第二步:創建畫板canvas對象,然後創建畫筆paint。然後調用canvas.drawBitmap方法繪製圖片
  • 第三步:對畫筆進行修飾,設置畫筆顏色屬性,這裏使用到了ColorMatrix,核心就是設置飽和度為0,即可繪製灰色內容

7.4 如何處理圖片旋轉呢

在Android中使用ImageView顯示圖片的時候發現圖片顯示不正,方向偏了或者倒過來了。

解決這個問題很自然想到的兩步走,首先是要自動識別圖像方向,計算旋轉角度,然後對圖像進行旋轉並顯示。

  1. 第一步:識別圖像方向

首先在這裏提一個概念EXIF(Exchangeable Image File Format,可交換圖像文件)。簡而言之,Exif是一個標準,用於電子照相機(也包括手機、掃描器等)上,用來規範圖片、聲音、視屏以及它們的一些輔助標記格式。

Exif支持的格式如下:圖像;壓縮圖像文件:JPEG、DCT;非壓縮圖像文件:TIFF;音頻;RIFF、WAV

Android提供了對JPEG格式圖像Exif接口支持,可以讀取JPEG文件metadata信息,參見ExifInterface。這些Metadata信息總的來説大致分為三類:日期時間、空間信息(經緯度、高度)、Camera信息(孔徑、焦距、旋轉角、曝光量等等)。

  1. 第二步:關於圖像旋轉

獲取了圖片的旋轉方向後,然後再設置圖像旋轉。最後Bitmap提供的靜態createBitmap方法,可以對圖片設置旋轉角度。具體看:PicCalculateUtils

7.5 保存圖片且刷相冊

大概的操作步驟如下所示。具體可看:ImageSaveUtils

  • 第一步:創建圖片文件,然後將bitmap對象寫到圖片文件中
  • 第二步:通過MediaStore將圖片插入到共享目錄相冊中
  • 第三步:發送通知,通知相冊中刷新插入圖片的數據。注意,獲取圖片資源uri刷新即可,避免刷新所有數據造成等待時間過長。

MediaStore 是 Android 提供的一個內容提供者(Content Provider),用於管理設備上的媒體文件(如圖片、視頻、音頻等)。通過 MediaStore,可以將圖片文件插入到系統的媒體庫中,使其在相冊中可見。

7.6 統一圖片域名優化

域名統一,減少了10%+的重複圖片下載和內存消耗。同時減少之前多域名圖片加載時重複創建HTTPS請求的過程,減少圖片加載時間。

統一圖片域名優化 是一種通過將圖片資源統一託管在同一個域名下,並結合 CDN(內容分發網絡)和圖片處理服務,來提升圖片加載性能、降低服務器壓力、優化用户體驗的技術手段。

7.7 優化H5圖片加載

通過攔截WebView圖片加載的方式,讓原生圖片庫來下載圖片之後傳遞圖片二進制數據給WebView顯示。

採用OkHttp攔截資源緩存,下面是大概的思路。緩存的入口從shouldInterceptRequest出發

  • 第一步,拿到WebResourceRequest對象中請求資源的url還有header,如果開發者設置不緩存則返回null
  • 第二步,如果緩存,通過url判斷攔截資源的條件,過濾非http,音視頻等資源,這個是可自由配置緩存內容比如css,png,jpg,xml,txt等
  • 第三步,判斷本地是否有OkHttp緩存數據,如果有則直接讀取本地資源,通過url找到對應的path路徑,然後讀取文件流,組裝數據返回。
  • 第四步,如果沒有緩存數據,創建OkHttp的Request請求,將資源網絡請求交給okHttp來處理,並且用它自帶的緩存功能,當然如果是請求失敗或者異常則返回null,否則返回正常數據

關於webView圖片緩存的方案,可以直接參考:YCWebView

7.8 優化圖片陰影效果

陰影效果有哪些實現方式

  • 第一種:使用CardView,但是不能設置陰影顏色
  • 第二種:採用shape疊加,存在後期UI效果不便優化
  • 第三種:UI切圖
  • 第四種:自定義View
  • 第五種:自定義Drawable

否定上面前兩種方案原因分析?

第一個方案的CardView漸變色和陰影效果很難控制,只能支持線性或者環裝形式漸變,這種不滿足需要,因為陰影本身是一個四周一層很淡的顏色包圍,在一個矩形框的層面上顏色大概一致,而且這個CardView有很多侷限性,比如不能修改陰影的顏色,不能修改陰影的深淺。所以這個思路無法實現這個需求。

第二個採用shape疊加,可以實現陰影效果,但是影響UI,且陰影部分是佔像素的,而且不靈活。

第三個方案詢問了一下ui。他們給出的結果是如果使用切圖的話那標註的話很難標,身為一個優秀的設計師大多對像素點都和敏感,界面上的像素點有一點不協調那都是無法容忍的。

網上一些介紹陰影效果方案

所有在深奧的技術,也都是為需求做準備的。也就是需要實踐並且可以用到實際開發中,這篇文章不再抽象介紹陰影效果原理,理解三維空間中如何處理偏移光線達到陰影視差等,網上看了一些文章也沒看明白或者理解。這篇博客直接通過調用api實現預期的效果。

多個drawable疊加,使用layer-list可以將多個drawable按照順序層疊在一起顯示,默認情況下,所有的item中的drawable都會自動根據它附上view的大小而進行縮放,layer-list中的item是按照順序從下往上疊加的,即先定義的item在下面,後面的依次往上面疊放

陰影是否佔位?1.使用CardView陰影不佔位,不能設置陰影顏色和效果。2.使用shape陰影是可以設置陰影顏色,但是是佔位的

幾種方案優缺點對比分析

CardView 優點:自帶功能實現簡單 缺點:自帶圓角不一定可適配所有需求

layer(shape疊加) 優點:實現形式簡單 缺點:效果一般

自定義實現 優點:實現效果好可配置能力高 缺點:需要開發者自行開發

關於解決陰影效果,具體各種方案的對比可以參考這個demo:AppShadowLib

7.9 圖片資源的壓縮

我們應用中使用的圖片,設計師出的原圖通常都非常大,他們通常會使用工具,經過一定的壓縮,縮減到比較小一些的大小。

但是,這些圖片通常都有一定的可壓縮空間,我在之前的項目中,對圖片進行了二次壓縮,整體壓縮率達到了 40%~50% ,效果還是非常不錯的。

這裏介紹下常用的,圖片壓縮的方法:

  • 使用壓縮工具對圖片進行二次壓縮。
  • 根據最終圖片是否需要透明度展示,優先選擇不透明的圖片格式,例如,我們應該避免使用 png 格式的圖片。
  • 對於色彩簡單,例如,一些背景之類的圖片,可以選擇使用佈局文件來定義(矢量圖),這樣就會非常節省內存了。
  • 如果包含透明度,優先使用 WebP 等格式圖像。

圖片在上線前進行壓縮處理,不但可以減少內存的使用,如果圖片是網絡獲取的,也可以減少網絡加載的流量和時間。推薦一個圖片壓縮網站:tinypng網站

08.筆記彙總一下

模塊 描述 備註
GitHub 多個YC系列開源項目,包含Android組件庫,以及多個案例 GitHub
博客彙總 匯聚Java,Android,C/C++,網絡協議,算法,編程總結等 YCBlogs
設計模式 六大設計原則,23種設計模式,設計模式案例,面向對象思想 設計模式
Java進階 數據設計和原理,面向對象核心思想,IO,異常,線程和併發,JVM Java高級
網絡協議 網絡實際案例,網絡原理和分層,Https,網絡請求,故障排查 網絡協議
計算機原理 計算機組成結構,框架,存儲器,CPU設計,內存設計,指令編程原理,異常處理機制,IO操作和原理 計算機基礎
學習C編程 C語言入門級別系統全面的學習教程,學習三到四個綜合案例 C編程
C++編程 C++語言入門級別系統全面的教學教程,併發編程,核心原理 C++編程
算法實踐 專欄,數組,鏈表,棧,隊列,樹,哈希,遞歸,查找,排序等 Leetcode
Android 基礎入門,開源庫解讀,性能優化,Framework,方案設計 Android
  • 1.1 為什麼説圖片比較佔用內存:一個手機拍攝的 2700 * 1900 像素的照片,需要 5.1M 的存儲空間,但是在圖像解碼配置 ARGB_8888 時,它加載到內存需要 19.6M 內存空間
  • 1.2 加載網絡圖片大概流程:1.獲取網絡資源文件流;2.decodeStream加載到bitmap中;3.然後壓縮圖片;4.然後處理圖片變換;5.圖片緩存策略;
  • 1.3 三方庫加載圖片邏輯大概是怎樣:從網絡拉取圖片;然後解碼圖片;然後進行壓縮;接着會有圖片常用圓角裁剪等處理;然後三級緩存加載的圖片;當然加載圖片過程涉及同步加載和異步加載;最後設置到具體view控件上。
  • 1.4 BitmapFactory可以加載哪些內容:提供了四類方法來加載Bitmap:decodeFile加載文件、decodeResource加載資源、decodeStream加載流、decodeByteArray加載字節數組。
  • 1.5 如何理解圖片大小和佔用內存:圖片大小是經過相對應的壓縮算法將原圖每個像素點信息轉換用另一種數據格式表示;圖片內存是這張圖片加載進內存所佔用的大小。
  • 1.6 Bitmap能直接存儲嗎:bitmap是一個對象,如果要存儲成本地可以查看的圖片文件,則必須對bitmap進行編碼,然後通過io流寫到本地file文件上。
  • 1.7 Bitmap創建的對象內存分配在哪裏:8.0 之前版本為 Bitmap 像素從 Java heap 申請內存。 8.0 版本後為 Bitmap 像素從 Native heap 申請內存。
  • 1.8 如果你是谷歌開發,圖片框架如何設計:大多數圖片框架加載流程,概括來説,圖片加載包含封裝,解析,下載,解碼,變換,緩存,顯示等操作。
  • 2.1 如何計算圖片佔用內存:bitmap內存大小 = 圖片長度 x 圖片寬度 x 單位像素佔用的字節數(這個就跟編碼方式有關了)
  • 2.2 加載本地資源圖片佔用內存怎麼計算:它佔用的內存 = width height nTargetDensity/inDensity 一個像素所佔的內存。
  • 2.3 一個像素佔用多大內存:ARGB_8888: 每個像素4字節. 共32位,默認設置。RGB_565:共16位,2字節,只存儲RGB值。
  • 2.5 影響Bitmap內存因素有哪些:圖片加載的分辨率;編碼格式;設備的屏幕密度等因素。
  • 2.6 加載xhdpi和xxhdpi圖片有何區別:相同的圖片,顯示在控件上面所佔用內存是不一樣的。因為兩個文件夾對應的dpi是不一樣的。
  • 2.7 圖片放在res不同目錄,加載的內存是一樣的嗎:在加載 res 目錄下的資源圖片時,會根據圖片存放的不同目錄做一次分辨率的轉換,新圖的高度 = 原圖高度 * (設備的 dpi / 目錄對應的 dpi )
  • 3.1 常見圖片壓縮有哪些:1.質量壓縮不會對內存產生影響;2.內存壓縮,按照比例縮小圖片加載減少內存佔用
  • 3.2.1 如何理解圖片尺寸壓縮:原理是通過降低圖片的寬度和高度(即減少像素數量),從而減少圖片的存儲大小。
  • 3.2.2 BitmapFactory.Options.inSampleSize核心思想是什麼:是通過降低圖片的分辨率來減少內存佔用。它是在解碼階段生效的,因此它可以在圖片加載到內存之前就完成壓縮,避免不必要的內存消耗。
  • 3.2.3 inJustDecodeBounds這個參數是幹什麼的:如果設置為true則表示decode函數不會生成bitmap對象,僅是將圖像相關的參數填充到option對象裏,這樣我們就可以在不生成bitmap而獲取到圖像的相關參數。
  • 3.3 如何理解圖片質量壓縮:通過減少圖像文件中像素的細節和信息來降低圖像文件大小的技術。這種壓縮方法通常會犧牲一定程度的圖像質量,以換取更小的文件大小。
  • 3.4 如何理解雙線性採樣壓縮:這種壓縮方法通過對圖像進行插值計算,以平滑地減少圖像的像素數量,同時儘可能保留圖像的細節和質量。
  • 3.5 高清圖分片加載的核心思想:將大尺寸的高清圖像分割成多個小塊(片段),根據顯示需求動態加載和拼接這些片段,以實現高清圖像的顯示。幫助減少內存佔用和提高性能,保持高清圖像的清晰度。
  • 4.1 如何理解RGB色彩模式:一種基於紅、綠、藍三種原色的彩色表示方式,通過不同比例的這三種顏色的組合來表示各種顏色。
  • 4.2 如何理解ARGB色彩模式:在 RGB 色彩模式基礎上增加了透明度通道,用於控制像素的透明度,提供了更多的圖像處理和顯示選項。
  • 4.3 Android中色彩格式如何選擇:1.RGB 格式表示紅藍綠;2.ARGB 格式表示增加一個Alpha透明通道;3.十六進制格式顏色
  • 5.1 Android圖片內存緩存思想是什麼:內存緩存的核心思想是利用內存的高速讀寫特性,緩存最近使用的圖片,從而避免重複加載和減少磁盤 I/O 操作。
  • 5.2 Lru內存緩存核心思想是什麼:一種基於 最近最少使用(LRU) 策略的緩存算法,廣泛應用於內存緩存設計中。其核心思想是通過淘汰最近最少使用的緩存項,來高效管理有限的緩存空間。
  • 5.3 Lru緩存注意事項有哪些:
  • 6.1 Bitmap演變進程是什麼:不同版本對 Bitmap 管理有一些差異,主要涉及到內存管理、資源回收和性能優化等方面。
  • 7.1 為何推薦用WebP格式圖片:WebP 格式的圖片壓縮率通常比 JPEG 和 PNG 更高,可以在保證圖像質量的前提下顯著減少文件大小。支持無損壓縮,支持透明度等。
  • 7.4 如何處理圖片旋轉:解決這個問題很自然想到的兩步走,首先是要自動識別圖像方向,計算旋轉角度,然後對圖像進行旋轉並顯示。
  • 7.5 如何保存圖片到相冊:MediaStore 是 Android 提供的一個內容提供者(Content Provider),用於管理設備上的媒體文件(如圖片、視頻、音頻等)。
  • 7.6 如何理解統一圖片域名優化:將圖片資源統一託管在同一個域名下,並結合 CDN(內容分發網絡)和圖片處理服務,來提升圖片加載性能、降低服務器壓力、優化用户體驗的技術手段。
  • 7.7 如何理解優化H5圖片加載:通過攔截WebView圖片加載的方式,讓原生圖片庫來下載圖片之後傳遞圖片二進制數據給WebView顯示。

Add a new 評論

Some HTML is okay.