目錄介紹
- 01.圖片基礎(chǔ)概念介紹
- 1.1 圖片占用內(nèi)存介紹
- 1.2 加載網(wǎng)絡(luò)圖片流程
- 1.3 三方庫加載圖片邏輯
- 1.4 從網(wǎng)絡(luò)直接拉取圖片
- 1.5 加載圖片的流程
- 1.6 Bitmap能直接存儲嗎
- 1.7 Bitmap創(chuàng)建流程
- 1.8 圖片框架如何設(shè)計
- 02.圖片內(nèi)存計算方式
- 2.1 如何計算占用內(nèi)存
- 2.2 上面計算內(nèi)存對嗎
- 2.3 一個像素占用內(nèi)存
- 2.4 使用API獲取內(nèi)存
- 2.5 影響B(tài)itmap內(nèi)存因素
- 2.6 加載xhdpi和xxhdpi圖片
- 2.7 圖片一些注意事項
- 03.大圖的內(nèi)存優(yōu)化
- 3.1 常見圖片壓縮
- 3.2 圖片尺寸壓縮
- 3.3 圖片質(zhì)量壓縮
- 3.4 雙線性采樣壓縮
- 3.5 高清圖分片加載
- 3.6 圖片綜合壓縮
- 04.色彩格式及內(nèi)存優(yōu)化
- 4.1 RGB顏色種類
- 4.2 ARGB色彩模式
- 4.3 改變色彩格式優(yōu)化
- 05.緩存的使用實踐優(yōu)化
- 5.1 Lru內(nèi)存緩存
- 5.2 Lru內(nèi)存注意事項
- 5.3 使用Lru磁盤緩存
- 06.不同版本對Bitmap管理
- 6.1 演變進(jìn)程
- 6.2 管理Bitmap內(nèi)存
- 6.3 提高Bitmap復(fù)用
- 07.圖片其他方面優(yōu)化
- 7.1 減少PNG圖片的使用
- 7.2 控件切割圓角優(yōu)化
- 7.3 如何給圖片置灰色
- 7.4 如何處理圖片旋轉(zhuǎn)呢
- 7.5 保存圖片且刷相冊
- 7.6 統(tǒng)一圖片域名優(yōu)化
- 7.7 優(yōu)化H5圖片加載
- 7.8 優(yōu)化圖片陰影效果
- 7.9 圖片資源的壓縮
01.圖片基礎(chǔ)概念介紹
1.1 圖片占用內(nèi)存介紹
- 移動設(shè)備的系統(tǒng)資源有限,所以應(yīng)用應(yīng)該盡可能的降低內(nèi)存的使用。
- 在應(yīng)用運行過程中,Bitmap (圖片)往往是內(nèi)存占用最大的一個部分,Bitmap 圖片的加載和處理,通常會占用大量的內(nèi)存空間,所以在操作 Bitmap 時,應(yīng)該盡可能的小心。
- Bitmap 會消耗很多的內(nèi)存,特別是諸如照片等內(nèi)容豐富的大圖。
- 例如,一個手機拍攝的 2700 * 1900 像素的照片,需要 5.1M 的存儲空間,但是在圖像解碼配置 ARGB_8888 時,它加載到內(nèi)存需要 19.6M 內(nèi)存空間(2592 * 1936 * 4 bytes),從而迅速消耗掉該應(yīng)用的剩余內(nèi)存空間。
- OOM 的問題也是我們常見的嚴(yán)重問題,OOM 的產(chǎn)生的一個主要場景就是在大圖片分配內(nèi)存的時候產(chǎn)生的,如果 APP 可用內(nèi)存緊張,這時加載了一張大圖,內(nèi)存空間不足以分配該圖片所需要的內(nèi)存,就會產(chǎn)生 OOM,所以控制圖片的高效使用是必備技能。
1.2 加載網(wǎng)絡(luò)圖片流程
- 這一部分壓縮和緩存圖片,在glide源碼分析的文章里已經(jīng)做出了比較詳細(xì)的說明。在這里簡單說一下圖片請求加載過程……
- 在使用App的時候,會經(jīng)常需要加載一些網(wǎng)絡(luò)圖片,一般的操作步驟大概是這樣的:
- 第一步從網(wǎng)絡(luò)加載圖片:一般都是通過網(wǎng)絡(luò)拉取的方式去服務(wù)器端獲取到圖片的文件流后,再通過BitmapFactory.decodeStream(InputStream)來加載圖片Bitmap。
- 第二步這種壓縮圖片:網(wǎng)絡(luò)加載圖片方式加載一兩張圖片倒不會出現(xiàn)問題,但是如果短時間內(nèi)加載十幾張或者幾十張圖片的時候,就很有可能會造成OOM(內(nèi)存溢出),因為現(xiàn)在的圖片資源大小都是非常大的,所以我們在加載圖片之前還需要進(jìn)行相應(yīng)的圖片壓縮處理。
- 第三步變換圖片:比如需要裁剪,切割圓角,旋轉(zhuǎn),添加高斯模糊等屬性。
- 第四步緩存圖片:但又有個問題來了,在使用移動數(shù)據(jù)的情況下,如果用戶每次進(jìn)入App的時候都會去進(jìn)行網(wǎng)絡(luò)拉取圖片,這樣就會非常的浪費數(shù)據(jù)流量,這時又需要對圖片資源進(jìn)行一些相應(yīng)的內(nèi)存緩存以及磁盤緩存處理,這樣不僅節(jié)省用戶的數(shù)據(jù)流量,還能加快圖片的加載速度。
- 第五步異步加載:雖然利用緩存的方式可以加快圖片的加載速度,但當(dāng)需要加載很多張圖片的時候(例如圖片墻瀑布流效果),就還需用到多線程來加載圖片,使用多線程就會涉及到線程同步加載與異步加載問題。
1.3 三方庫加載圖片邏輯
- 先說出結(jié)論,目前市面較為常用的大概是Glide,Picasso,F(xiàn)resco等。大概的處理圖片涉及主要邏輯有:
- 從網(wǎng)絡(luò)或者本地等路徑拉取圖片;然后解碼圖片;然后進(jìn)行壓縮;接著會有圖片常用圓角,模糊或者裁剪等處理;然后三級緩存加載的圖片;當(dāng)然加載圖片過程涉及同步加載和異步加載;最后設(shè)置到具體view控件上。
1.4 從網(wǎng)絡(luò)直接拉取圖片
-
直接通過網(wǎng)絡(luò)請求將網(wǎng)絡(luò)圖片轉(zhuǎn)化成bitmap
- 在這將采用最原生的網(wǎng)絡(luò)請求方式HttpURLConnection方式進(jìn)行圖片獲取。
- 經(jīng)過測試,請求8張圖片,耗時毫秒值174。一般是通過get請求拉取圖片的。這種方法應(yīng)該是最基礎(chǔ)的網(wǎng)絡(luò)請求,大家也可以回顧一下,一般開發(fā)中很少用這種方式加載圖片。具體可以看:ImageToolLib
- 如何加載一個圖片呢?
- 可以看看BitmapFactory類為我們提供了四類方法來加載Bitmap:decodeFile、decodeResource、decodeStream、decodeByteArray;也就是說Bitmap,Drawable,InputStream,Byte[] 之間是可以進(jìn)行轉(zhuǎn)換。
1.5 加載圖片的流程
- 搞清楚一個圖片概念
- 在電腦上看到的 png 格式或者 jpg 格式的圖片,png(jpg) 只是這張圖片的容器。是經(jīng)過相對應(yīng)的壓縮算法將原圖每個像素點信息轉(zhuǎn)換用另一種數(shù)據(jù)格式表示。
- 加載圖片顯示到手機
- 通過代碼,將這張圖片加載進(jìn)內(nèi)存時,會先解析(也就是解碼操作)圖片文件本身的數(shù)據(jù)格式,然后還原為位圖,也就是 Bitmap 對象。
- 圖片大小vs圖片內(nèi)存大小
- 一張 png 或者 jpg 格式的圖片大小,跟這張圖片加載進(jìn)內(nèi)存所占用的大小完全是兩回事。
1.6 Bitmap能直接存儲嗎
- Bitmap基礎(chǔ)概念
- Bitmap對象本質(zhì)是一張圖片的內(nèi)容在手機內(nèi)存中的表達(dá)形式。它將圖片的內(nèi)容看做是由存儲數(shù)據(jù)的有限個像素點組成;每個像素點存儲該像素點位置的ARGB值。每個像素點的ARGB值確定下來,這張圖片的內(nèi)容就相應(yīng)地確定下來了。
- Bitmap本質(zhì)上不能直接存儲
- 為什么?bitmap是一個對象,如果要存儲成本地可以查看的圖片文件,則必須對bitmap進(jìn)行編碼,然后通過io流寫到本地file文件上。
1.7 Bitmap創(chuàng)建流程
- 對于圖片OOM,可以發(fā)現(xiàn)一個現(xiàn)象。
- heapsize(虛擬機的內(nèi)存配置)越大越不容易 OOM,Android8.0 及之后的版本更不容易 OOM,這個該如何理解呢?
- Bitmap對象內(nèi)存的變化:
- 在 Android 8.0 之前,Bitmap 像素占用的內(nèi)存是在 Java heap 中分配的;8.0 及之后,Bitmap 像素占用的內(nèi)存分配到了 Native Heap。
- 由于 Native heap 的內(nèi)存分配上限很大,32 位應(yīng)用的可用內(nèi)存在 3~4G,64 位上更大,虛擬內(nèi)存幾乎很難耗盡,所以推測 OOM 時 Java heap 中占用內(nèi)存較多的對象是 Bitmap” 成立的情況下,應(yīng)用更不容易 OOM。
- 搞清楚Bitmap對象內(nèi)存分配
- Bitmap 的構(gòu)造方法是不公開的,在使用 Bitmap 的時候,一般都是通過 Bitmap、BitmapFactory 提供的靜態(tài)方法來創(chuàng)建 Bitmap 實例。
- 以 Bitmap.createBitmap 說明了 Bitmap 對象的主要創(chuàng)建過程分析,可以看到 Java Bitmap 對象是在 Native 層通過 NewObject 創(chuàng)建的。
- allocateJavaPixelRef,是 8.0 之前版本為 Bitmap 像素從 Java heap 申請內(nèi)存。其核心原理是Bitmap 的像素是保存在 Java 堆上。
- allocateHeapBitmap,是 8.0 版本為 Bitmap 像素從 Native heap 申請內(nèi)存。其核心原理主要是通過 calloc 為 Bitmap 的像素分配內(nèi)存,這個分配就在 Native 堆上。
- 更多詳細(xì)內(nèi)容可以看:Bitmap對象內(nèi)存分配
1.8 圖片框架如何設(shè)計
- 大多數(shù)圖片框架加載流程
- 概括來說,圖片加載包含封裝,解析,下載,解碼,變換,緩存,顯示等操作。
- 圖片框架是如何設(shè)計的
- 封裝參數(shù):從指定來源,到輸出結(jié)果,中間可能經(jīng)歷很多流程,所以第一件事就是封裝參數(shù),這些參數(shù)會貫穿整個過程;
- 解析路徑:圖片的來源有多種,格式也不盡相同,需要規(guī)范化;比如glide可以加載file,io,id,網(wǎng)絡(luò)等各種圖片資源
- 讀取緩存:為了減少計算,通常都會做緩存;同樣的請求,從緩存中取圖片(Bitmap)即可;
- 查找文件/下載文件:如果是本地的文件,直接解碼即可;如果是網(wǎng)絡(luò)圖片,需要先下載;比如glide這塊是發(fā)起一個請求
- 解碼:這一步是整個過程中最復(fù)雜的步驟之一,有不少細(xì)節(jié);比如glide中解析圖片數(shù)據(jù)源,旋轉(zhuǎn)方向,圖片頭等信息
- 變換和壓縮:解碼出Bitmap之后,可能還需要做一些變換處理(圓角,濾鏡等),還要做圖片壓縮;
- 緩存:得到最終bitmap之后,可以緩存起來,以便下次請求時直接取結(jié)果;比如glide用到三級緩存
- 顯示:顯示結(jié)果,可能需要做些動畫(淡入動畫,crossFade等);比如glide設(shè)置顯示的時候可以添加動畫效果
02.圖片內(nèi)存計算方式
2.1 如何計算占用內(nèi)存
- 如果圖片要顯示下Android設(shè)備上,ImageView最終是要加載Bitmap對象的,就要考慮單個Bitmap對象的內(nèi)存占用了,如何計算一張圖片的加載到內(nèi)存的占用呢?其實就是所有像素的內(nèi)存占用總和:
- bitmap內(nèi)存大小 = 圖片長度 x 圖片寬度 x 單位像素占用的字節(jié)數(shù)
- 起決定因素就是最后那個參數(shù)了,Bitmap常見有2種編碼方式:ARGB_8888和RGB_565,ARGB_8888每個像素點4個byte,RGB_565是2個byte,一般都采用ARGB_8888這種。那么常見的1080*1920的圖片內(nèi)存占用就是:1920 x 1080 x 4 = 7.9M
2.2 上面計算內(nèi)存對嗎
- 我看到好多博客都是這樣計算的,但是這樣算對嗎?有沒有哥們試驗過這種方法正確性?我覺得看博客要對博主表示懷疑,論證別人寫的是否正確。
- 說出我的結(jié)論:上面2.1這種說法也對,但是不全對,沒有說明場景,同時也忽略了一個影響項:Density。接下來看看源代碼。
- inDensity默認(rèn)為圖片所在文件夾對應(yīng)的密度;inTargetDensity為當(dāng)前系統(tǒng)密度。
- 加載一張本地資源圖片,那么它占用的內(nèi)存 = width * height * nTargetDensity/inDensity 一個像素所占的內(nèi)存。
@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 * 一個像素所占的內(nèi)存;
- 別的:width * height * 一個像素所占的內(nèi)存;
2.3 一個像素占用內(nèi)存
- 一個像素占用多大內(nèi)存?Bitmap.Config用來描述圖片的像素是怎么被存儲的?
- ARGB_8888: 每個像素4字節(jié). 共32位,默認(rèn)設(shè)置。
- Alpha_8: 只保存透明度,共8位,1字節(jié)。
- ARGB_4444: 共16位,2字節(jié)。
- RGB_565:共16位,2字節(jié),只存儲RGB值。
2.4 使用API獲取內(nèi)存
- Bitmap使用API獲取內(nèi)存
- getByteCount()
- getByteCount()方法是在API12加入的,代表存儲Bitmap的色素需要的最少內(nèi)存。API19開始getAllocationByteCount()方法代替了getByteCount()。
- getAllocationByteCount()
- API19之后,Bitmap加了一個Api:getAllocationByteCount();代表在內(nèi)存中為Bitmap分配的內(nèi)存大小。
- getByteCount()
- 思考: getByteCount()與getAllocationByteCount()的區(qū)別?
- 一般情況下兩者是相等的;通過復(fù)用Bitmap來解碼圖片,如果被復(fù)用的Bitmap的內(nèi)存比待分配內(nèi)存的Bitmap大,那么getByteCount()表示新解碼圖片占用內(nèi)存的大?。ú⒎菍嶋H內(nèi)存大小,實際大小是復(fù)用的那個Bitmap的大小),getAllocationByteCount()表示被復(fù)用Bitmap真實占用的內(nèi)存大?。磎Buffer的長度)。
- 在復(fù)用Bitmap的情況下,getAllocationByteCount()可能會比getByteCount()大。
2.5 影響B(tài)itmap內(nèi)存因素
- 影響B(tài)itmap占用內(nèi)存的因素:
- 圖片最終加載的分辨率;
- 圖片的格式(PNG/JPEG/BMP/WebP);
- 圖片所存放的drawable目錄;
- 圖片屬性設(shè)置的色彩模式;
- 設(shè)備的屏幕密度;
2.6 加載xhdpi和xxhdpi圖片
- 提個問題,加載xhdpi和xxhdpi中相同的圖片,顯示在控件上會一樣嗎?內(nèi)存大小一樣嗎?為什么?
- 肯定是不一樣的。xhdpi:240dpi--320dpi,xxhdpi:320dpi--480dpi,
- app中設(shè)置的圖片是如何從hdpi中查找的?
- 首先計算dpi,比如手機分辨率是1920x1080,5.1寸的手機。那么得到的dpi公式是(√ ̄19202 + 10802)/5.1 =2202/5.1= 431dpi。這樣優(yōu)先查找xxhdpi
- 如果xxhdpi里沒有查找圖片,如果沒有會往上找,遵循“先高再低”原則。如果xhdpi里有這個圖片會使用xhdpi里的圖片,這時候發(fā)現(xiàn)會比在xhdpi里的圖片放大了。
- 為何要引入不同hdpi的文件管理?
- 比如:xxhdpi放94x94,xhdpi放74x74,hdpi放45x45,這樣不管是什么樣的手機圖片都能在指定的比例顯示。引入多種hdpi是為了讓這個圖片在任何手機上都是手機的這個比例。
2.7 圖片一些注意事項
- 同樣圖片顯示在大小不相同的ImageView上,內(nèi)存是一樣嗎?
- 圖片占據(jù)內(nèi)存空間大小與圖片在界面上顯示的大小沒有關(guān)系。
- 圖片放在res不同目錄,加載的內(nèi)存是一樣的嗎?
- 最終圖片加載進(jìn)內(nèi)存所占據(jù)的大小會不一樣,因為系統(tǒng)在加載 res 目錄下的資源圖片時,會根據(jù)圖片存放的不同目錄做一次分辨率的轉(zhuǎn)換,而轉(zhuǎn)換的規(guī)則是:新圖的高度 = 原圖高度 * (設(shè)備的 dpi / 目錄對應(yīng)的 dpi )
03.大圖的內(nèi)存優(yōu)化
3.1 常見圖片壓縮
- 常見壓縮方法Api
- Bitmap.compress(),質(zhì)量壓縮,不會對內(nèi)存產(chǎn)生影響;
- BitmapFactory.Options.inSampleSize,內(nèi)存壓縮;
- Bitmap.compress()質(zhì)量壓縮
- 質(zhì)量壓縮,不會對內(nèi)存產(chǎn)生影響。它是在保持像素的前提下改變圖片的位深及透明度等,來達(dá)到壓縮圖片的目的,不會減少圖片的像素。進(jìn)過它壓縮的圖片文件大小會變小,但是解碼成bitmap后占得內(nèi)存是不變的。
- BitmapFactory.Options.inSampleSize內(nèi)存壓縮
- 解碼圖片時,設(shè)置BitmapFactory.Options類的inJustDecodeBounds屬性為true,可以在Bitmap不被加載到內(nèi)存的前提下,獲取Bitmap的原始寬高。而設(shè)置BitmapFactory.Options的inSampleSize屬性可以真實的壓縮Bitmap占用的內(nèi)存,加載更小內(nèi)存的Bitmap。
- 設(shè)置inSampleSize之后,Bitmap的寬、高都會縮小inSampleSize倍。例如:一張寬高為2048x1536的圖片,設(shè)置inSampleSize為4之后,實際加載到內(nèi)存中的圖片寬高是512x384。占有的內(nèi)存就是0.75M而不是12M,足足節(jié)省了15倍。
- 備注:inSampleSize值的大小不是隨便設(shè)、或者越大越好,需要根據(jù)實際情況來設(shè)置。inSampleSize比1小的話會被當(dāng)做1,任何inSampleSize的值會被取接近2的冪值。
3.2 圖片尺寸壓縮
3.2.1 如何理解尺寸壓縮
- 通常在大多數(shù)情況下,圖片的實際大小都比需要呈現(xiàn)的尺寸大很多。
- 例如,我們的原圖是一張 2700 * 1900 像素的照片,加載到內(nèi)存就需要 19.6M 內(nèi)存空間,但是,我們需要把它展示在一個列表頁中,組件可展示尺寸為 270 * 190,這時,我們實際上只需要一張原圖的低分辨率的縮略圖即可(與圖片顯示所對應(yīng)的 UI 控件匹配),那么實際上 270 * 190 像素的圖片,只需要 0.2M 的內(nèi)存即可。
- 可以看到,優(yōu)化前后相差了 98 倍,原來顯示 1 張,現(xiàn)在可以顯示 98 張圖片,效果非常顯著。
- 既然在對原圖縮放可以顯著減少內(nèi)存大小,那么我們應(yīng)該如何操作呢?
- 先加載到內(nèi)存,再進(jìn)行操作嗎,可以如果先加載到內(nèi)存,好像也不太對,這樣只接占用了 19.6M + 0.2M 2份內(nèi)存了,而我們想要的是,在原圖不加載到內(nèi)存中,只接將縮放后的圖片加載到內(nèi)存中,可以實現(xiàn)嗎?
- BitmapFactory 提供了從不同資源創(chuàng)建 Bitmap 的解碼方法:
- decodeByteArray()、decodeFile()、decodeResource() 等。但是,這些方法在構(gòu)造位圖的時候會嘗試分配內(nèi)存,也就是它們會導(dǎo)致原圖直接加載到內(nèi)存了,不滿足我們的需求。我們可以通過 BitmapFactory.Options 設(shè)置一些附加的標(biāo)記,指定解碼選項,以此來解決該問題。
- 如何操作呢?答案來了:將 inJustDecodeBounds 屬性設(shè)置為 true,可以在解碼時避免內(nèi)存的分配,它會返回一個 null 的 Bitmap ,但是可以獲取 outWidth、outHeight 和 outMimeType 值。利用該屬性,我們就可以在圖片不占用內(nèi)存的情況下,在圖片壓縮之前獲取圖片的尺寸。
- 怎樣才能對圖片進(jìn)行壓縮呢?
- 通過設(shè)置BitmapFactory.Options中inSampleSize的值就可以實現(xiàn)。其計算方式大概就是:計算出實際寬高和目標(biāo)寬高的比率,然后選擇寬和高中最小的比率作為inSampleSize的值,這樣可以保證最終圖片的寬和高。
3.2.2 設(shè)置BitmapFactory.Options屬性
- 大概步驟如下所示
- 要將BitmapFactory.Options的inJustDecodeBounds屬性設(shè)置為true,解析一次圖片。注意這個地方是核心,這個解析圖片并沒有生成bitmap對象(也就是說沒有為它分配內(nèi)存控件),而僅僅是拿到它的寬高等屬性。
- 然后將BitmapFactory.Options連同期望的寬度和高度一起傳遞到到calculateInSampleSize方法中,就可以得到合適的inSampleSize值了。這一步會壓縮圖片。
- 之后再解析一次圖片,使用新獲取到的inSampleSize值,并把inJustDecodeBounds設(shè)置為false,就可以得到壓縮后的圖片了。此時才正式創(chuàng)建了bitmap對象,由于前面已經(jīng)對它壓縮了,所以你會發(fā)現(xiàn)此時所占內(nèi)存大小已經(jīng)很少了。
- 具體的實現(xiàn)代碼
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) { // 第一次解析將inJustDecodeBounds設(shè)置為true,來獲取圖片大小 final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeResource(res, resId, options); // 調(diào)用上面定義的方法計算inSampleSize值 options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); // 使用獲取到的inSampleSize值再次解析圖片 options.inJustDecodeBounds = false; return BitmapFactory.decodeResource(res, resId, options); } - 思考:inJustDecodeBounds這個參數(shù)是干什么的?
- 如果設(shè)置為true則表示decode函數(shù)不會生成bitmap對象,僅是將圖像相關(guān)的參數(shù)填充到option對象里,這樣我們就可以在不生成bitmap而獲取到圖像的相關(guān)參數(shù)了。
- 為何設(shè)置兩次inJustDecodeBounds屬性?
- 第一次:設(shè)置為true則表示decode函數(shù)不會生成bitmap對象,僅是將圖像相關(guān)的參數(shù)填充到option對象里,這樣我們就可以在不生成bitmap而獲取到圖像的相關(guān)參數(shù)。
- 第二次:將inJustDecodeBounds設(shè)置為false再次調(diào)用decode函數(shù)時就能生成bitmap了。而此時的bitmap已經(jīng)壓縮減小很多了,所以加載到內(nèi)存中并不會導(dǎo)致OOM。
3.3 圖片質(zhì)量壓縮
- 在Android中,對圖片進(jìn)行質(zhì)量壓縮,通常我們的實現(xiàn)方式如下所示:
//quality 為0~100,0表示最小體積,100表示最高質(zhì)量,對應(yīng)體積也是最大 bitmap.compress(Bitmap.CompressFormat.JPEG, quality , outputStream); - 在上述代碼中,我們選擇的壓縮格式是CompressFormat.JPEG,除此之外還有兩個選擇:
- 其一,CompressFormat.PNG,PNG格式是無損的,它無法再進(jìn)行質(zhì)量壓縮,quality這個參數(shù)就沒有作用了,會被忽略,所以最后圖片保存成的文件大小不會有變化;
- 其二,CompressFormat.WEBP,這個格式是google推出的圖片格式,它會比JPEG更加省空間,經(jīng)過實測大概可以優(yōu)化30%左右。
- Android質(zhì)量壓縮邏輯,函數(shù)compress經(jīng)過一連串的java層調(diào)用之后,最后來到了一個native函數(shù):
- 具體看:Bitmap.cpp,最后調(diào)用了函數(shù)encoder->encodeStream(…)編碼保存本地。該函數(shù)是調(diào)用skia引擎來對圖片進(jìn)行編碼壓縮。
3.4 雙線性采樣壓縮
- 雙線性采樣(Bilinear Resampling)在 Android 中的使用方式一般有兩種:
bm = Bitmap.createScaledBitmap(bitmap, bitmap.getWidth()/2, bitmap.getHeight()/2, true); //或者直接使用 matrix 進(jìn)行縮放 Matrix matrix = new Matrix(); matrix.setScale(0.5f, 0.5f); bm = Bitmap.createBitmap(bitmap, 0, 0, bit.getWidth(), bit.getHeight(), matrix, true); - 看源碼可以知道createScaledBitmap函數(shù)最終也是使用第二種方式的matrix進(jìn)行縮放
- 雙線性采樣使用的是雙線性內(nèi)插值算法,這個算法不像鄰近點插值算法一樣,直接粗暴的選擇一個像素,而是參考了源像素相應(yīng)位置周圍2x2個點的值,根據(jù)相對位置取對應(yīng)的權(quán)重,經(jīng)過計算之后得到目標(biāo)圖像。
3.5 高清圖分片加載
- 適用場景 : 當(dāng)一張圖片非常大 , 在手機中只需要顯示其中一部分內(nèi)容 , BitmapRegionDecoder 非常有用 。
- 主要作用 : BitmapRegionDecoder 可以從圖像中 解碼一個矩形區(qū)域 。相當(dāng)于手在滑動的過程中,計算當(dāng)前顯示區(qū)域的圖片繪制出來。
- 基本使用流程 : 先創(chuàng)建,后解碼 。調(diào)用 newInstance 方法 , 創(chuàng)建 BitmapRegionDecoder 對象 ;然后調(diào)用 decodeRegion 方法 , 獲取指定 Rect 矩形區(qū)域的解碼后的 Bitmap 對象
3.6 圖片綜合壓縮
- 一般情況下圖片綜合壓縮的整體思路如下:
- 第一步進(jìn)行采樣率壓縮;
- 第二步進(jìn)行寬高的等比例壓縮(微信對原圖和縮略圖限制了最大長寬或者最小長寬);
- 第三步就是對圖片的質(zhì)量進(jìn)行壓縮(一般75或者70);
- 第四步就是采用webP的格式。
- 關(guān)于圖片壓縮的綜合案例如下
- 具體可以參考:CompressServer
04.色彩格式及內(nèi)存優(yōu)化
4.1 RGB顏色種類
- RGB 色彩模式是工業(yè)界的一種顏色標(biāo)準(zhǔn)
- 通過對紅(R)、綠(G)、藍(lán)(B)三個顏色通道的變化以及它們相互之間的疊加來得到各式各樣的顏色的,RGB即是代表紅、綠、藍(lán)三個通道的顏色,這個標(biāo)準(zhǔn)幾乎包括了人類視力所能感知的所有顏色,是運用最廣的顏色系統(tǒng)之一。Android 中,像素的存儲方式使用的色彩模式正是 RGB 色彩模式。
4.2 ARGB色彩模式
- 在 Android 中,我們常見的一些顏色設(shè)置,都是 RGB 色彩模式來描述像素顏色的,并且他們都帶有透明度通道,也就是所謂的 ARGB。例如,我們常見的顏色定義如下:
//在代碼中定義顏色值:藍(lán)色 public final int blue=0xff0000ff; //或者在xml中定義: <drawable name="blue">#ff0000ff</drawable> - 以上設(shè)置中,顏色值都是使用 16 進(jìn)制的數(shù)字來表示的。以上顏色值都是帶有透明度(透明通道)的顏色值,格式是 AARRGGBB,透明度、紅色、綠色、藍(lán)色四個顏色通道,各占有 2 位,也就是一個顏色通道,使用了 1 個字節(jié)來存儲。
4.3 改變色彩格式優(yōu)化
- Android 中有多種 RGB 模式,我們可以設(shè)置不同的格式,來控制圖片像素顏色的顯示質(zhì)量和存儲空間。
- Android.graphics.Bitmap 類里有一個內(nèi)部類 Bitmap.Config 類,它定義了可以在 Android 中使用的幾種色彩格式:
public enum Config { ALPHA_8 (1), RGB_565 (3), @Deprecated ARGB_4444 (4), ARGB_8888 (5), RGBA_F16 (6), HARDWARE (7); } - 解釋一下這幾個值分別代表了什么含義?我們已經(jīng)知道了:A 代表透明度、R 代表紅色、G 代表綠色、B 代表藍(lán)色。
- ALPHA_8:表示,只存在 Alpha 通道,沒有存儲色彩值,只含有透明度,每個像素占用 1 個字節(jié)的空間。
- RGB_565:表示,R 占用 5 位二進(jìn)制的位置,G 占用了6位,B 占用了 5 位。每個像素占用 2 個字節(jié)空間,并且不包含透明度。
- ARGB_4444:表示,A(透明度)、R(紅色)、G(綠色)、B(藍(lán)色)4個通道各占用 4 個 bit 位。每個像素占用 2 個字節(jié)空間。
- ARGB_8888:表示,A(透明度)、R(紅色)、G(綠色)、B(藍(lán)色)4個通道各占用 8 個 bit 位。每個像素占用 4 個字節(jié)空間。
- RGBA_F16:表示,每個像素存儲在8個字節(jié)上。此配置特別適合廣色域和HDR內(nèi)容。
- HARDWARE:特殊配置,當(dāng)位圖僅存儲在圖形內(nèi)存中時。 此配置中的位圖始終是不可變的。
- 那么開發(fā)中一般選擇哪一種比較合適呢
- Android 中的圖片在加載時,默認(rèn)的色彩格式是 ARGB_8888,也就是每個像素占用 4 個字節(jié)空間,一張 2700 * 1900 像素的照片,加載到內(nèi)存就需要 19.6M 內(nèi)存空間(2592 * 1936 * 4 bytes)。
- 如果圖片在 UI 組件中顯示時,不需要太高的圖片質(zhì)量,例如顯示一張縮略圖(不透明圖片)等場景,這時,我們就沒必要使用 ARGB_8888 的色彩格式了,只需要使用 RGB_565 模式即可滿足顯示的需要。
- 那么,我們的優(yōu)化操作就可以是:
- 將 2700 * 1900 像素的原圖,壓縮到原圖的低分辨率的縮略圖 270 * 190 像素的圖片,這時需要 0.2M 的內(nèi)存。也就是從 19.6M內(nèi)存,壓縮為 0.2 M內(nèi)存。
- 我們還可以進(jìn)一步優(yōu)化色彩格式,由 ARGB_8888 改為 RGB_565 模式,這時,目標(biāo)圖片需要的內(nèi)存就變?yōu)?270 * 190 * 2 = 0.1M 了。圖片內(nèi)存空間又減小了一倍。
05.緩存的使用實踐優(yōu)化
5.1 Lru內(nèi)存緩存
- LruCache 類特別適合用來緩存 Bitmap,它使用一個強引用的 LinkedHashMap 保存最近引用的對象,并且在緩存超出設(shè)定大小時,刪除最近最少使用的對象。
- 給 LruCache 確定一個合適的緩存大小非常重要,我們需要考慮幾個因素:
- 應(yīng)用剩余多少可用內(nèi)存?
- 需要有多少張圖片同時顯示到屏幕上?有多少圖片需要準(zhǔn)備好以便馬上顯示到屏幕?
- 設(shè)備的屏幕大小和密度是多少?高密度的設(shè)備需要更大的緩存空間來緩存同樣數(shù)量的圖片。
- Bitmap 的尺寸配置是多少,花費多少內(nèi)存?
- 圖片被訪問的頻率如何?如果其中一些比另外一些訪問更頻繁,那么我們可能希望在內(nèi)存中保存那些最常訪問的圖片,或者根據(jù)訪問頻率給 Bitmap 分組,為不同的 Bitmap 組設(shè)置多個 LruCache 對象。
- 是否可以在緩存圖片的質(zhì)量和數(shù)量之間尋找平衡點?有時,保存大量低質(zhì)量的 Bitmap 會非常有用,加載更高質(zhì)量的圖片的任務(wù)可以交給另外一個后臺線程處理。
- 緩存太小會導(dǎo)致額外的花銷卻沒有明顯的好處,緩存太大同樣會導(dǎo)致 java.lang.OutOfMemory 的異常,并且使得你的程序只留下小部分的內(nèi)存用來工作(緩存占用太多內(nèi)存,導(dǎo)致其他操作會因為內(nèi)存不夠而拋出異常)。所以,我們需要分析實際情況之后,提出一個合適的解決方案。
- LruCache是Android提供的一個緩存類,通常運用于內(nèi)存緩存
- LruCache是一個泛型類,它的底層是用一個LinkedHashMap以強引用的方式存儲外界的緩存對象來實現(xiàn)的。
- 為什么使用LinkedHashMap來作為LruCache的存儲,是因為LinkedHashMap有兩種排序方式,一種是插入排序方式,一種是訪問排序方式,默認(rèn)情況下是以訪問方式來存儲緩存對象的;LruCache提供了get和put方法來完成緩存的獲取和添加,當(dāng)緩存滿時,會將最近最少使用的對象移除掉,然后再添加新的緩存對象。如下源碼所示,底層是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的時候,首先需要獲取當(dāng)前設(shè)備的內(nèi)存容量,通常情況下會將總?cè)萘康陌朔种蛔鳛長ruCache的容量,然后重寫LruCache的sizeof方法,sizeof方法用于計算緩存對象的大小,單位需要與分配的容量的單位一致;
// 獲取系統(tǒng)最大緩存 int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); // set LruCache size; // 使用最大可用內(nèi)存值的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) { // 重寫此方法來衡量每張圖片的大小,默認(rèn)返回圖片數(shù)量 return bitmap.getRowBytes() * bitmap.getHeight() / 1024; } }; //插入對象 memoryCache.put(key, bitmap); //取出對象 memoryCache.get(key); - 如何淘汰緩存
- 這個就要看LinkedHashMap集合的特點呢!LinkedHashMap 構(gòu)造函數(shù)的第三個參數(shù):accessOrder,傳入true時, 元素會按訪問順序排列,最后訪問的在遍歷器最后端。在進(jìn)行淘汰時,移除遍歷器前端的元素,直至緩存總大小降低到指定大小以下。
5.2 Lru內(nèi)存注意事項
- 看一個真實的場景
- 假設(shè)我們的LruCache可以緩存80張,每次刷新從網(wǎng)絡(luò)獲取20張圖片且不重復(fù),那么在刷新第五次的時候,根據(jù)LruCache緩存的規(guī)則,第一次刷新的20張圖片就會從LruCache中移出,處于等待被系統(tǒng)GC的狀態(tài)。如果我們繼續(xù)刷新n次,等待被回收的張數(shù)就會累積到 20 * n 張。
- 會出現(xiàn)什么問題
- 會出現(xiàn)大量的Bitmap內(nèi)存碎片,我們不知道系統(tǒng)什么時候會觸發(fā)GC回收掉這些無用的Bitmap,對于內(nèi)存是否會溢出,是否會頻繁GC導(dǎo)致卡頓等未知問題。
- 解決方案該怎么做?
- 第一種:在3.0以后引入了 BitmapFactory.Options.inBitmap,如果設(shè)置此項,需要解碼的圖片就會嘗試使用該Bitmap的內(nèi)存,這樣取消了內(nèi)存的動態(tài)分配,提高了性能,節(jié)省了內(nèi)存。
- 第二種:把處于無用的狀態(tài)的Bitmap放入SoftReference。SoftReference引用的對象會在內(nèi)存溢出之前被回收。
- 關(guān)于Lru緩存案例和代碼可以參考:AppLruCache
5.3 使用Lru磁盤緩存
- 內(nèi)存緩存能夠提高訪問最近用過的 Bitmap 的速度,但是我們無法保證最近訪問過的 Bitmap 都能夠保存在緩存中。像類似 GridView 等需要大量數(shù)據(jù)填充的控件很容易就會用盡整個內(nèi)存緩存。另外,我們的應(yīng)用可能會被類似打電話等行為而暫停并退到后臺,因為后臺應(yīng)用可能會被殺死,那么內(nèi)存緩存就會被銷毀,里面的 Bitmap 也就不存在了。一旦用戶恢復(fù)應(yīng)用的狀態(tài),那么應(yīng)用就需要重新處理那些圖片。
- 磁盤緩存可以用來保存那些已經(jīng)處理過的 Bitmap,它還可以減少那些不再內(nèi)存緩存中的 Bitmap 的加載次數(shù)。當(dāng)然從磁盤讀取圖片會比從內(nèi)存要慢,而且由于磁盤讀取操作時間是不可預(yù)期的,讀取操作需要在后臺線程中處理。
- 注意:如果圖片會被更頻繁的訪問,使用 ContentProvider 或許會更加合適,比如在圖庫應(yīng)用中。
- 注意:因為初始化磁盤緩存涉及到 I/O 操作,所以它不應(yīng)該在主線程中進(jìn)行。但是這也意味著在初始化完成之前緩存可以被訪問。為了解決這個問題,在上面的實現(xiàn)中,有一個鎖對象(lock object)來確保在磁盤緩存完成初始化之前,應(yīng)用無法對它進(jìn)行讀取。
- 內(nèi)存緩存的檢查是可以在 UI 線程中進(jìn)行的,磁盤緩存的檢查需要在后臺線程中處理。磁盤操作永遠(yuǎn)都不應(yīng)該在 UI 線程中發(fā)生。當(dāng)圖片處理完成后,Bitmap 需要添加到內(nèi)存緩存與磁盤緩存中,方便之后的使用。
06.不同版本對Bitmap管理
6.1 演變進(jìn)程
- Android 2.3.3 (API level 10)以及之前,
- 一個 Bitmap 的像素數(shù)據(jù)是存放在 Native 內(nèi)存空間中的。這些數(shù)據(jù)與 Bitmap 對象本身是隔離的,Bitmap 本身被存放在 Dalvik 堆中。并且無法預(yù)測在 Native 內(nèi)存中的像素級數(shù)據(jù)何時會被釋放,這意味著程序容易超過它的內(nèi)存限制并且崩潰。
- Android 3.0 (API Level 11)開始
- 像素數(shù)據(jù)則是與 Bitmap 本身一起存放在 Dalvik 堆中。
- Android 8.0(Android O)及之后的版本中
- Bitmap 的像素數(shù)據(jù)的內(nèi)存分配又回到了 Native 層,它是在 Native 堆空間進(jìn)行分配的。
6.2 管理Bitmap內(nèi)存
- 管理 Android 2.3.3 及以下版本的內(nèi)存使用
- 在 Android 2.3.3 (API level 10) 以及更低版本上,推薦使用 recycle() 方法。 如果在應(yīng)用中顯示了大量的 Bitmap 數(shù)據(jù),我們很可能會遇到 OutOfMemoryError 的錯誤。 recycle() 方法可以使得程序更快的釋放內(nèi)存。
- 管理 Android 3.0 及其以上版本的內(nèi)存
- 從 Android 3.0 (API Level 11)開始,引進(jìn)了 BitmapFactory.Options.inBitmap 字段。 如果使用了這個設(shè)置字段,decode 方法會在加載 Bitmap 數(shù)據(jù)的時候去重用已經(jīng)存在的 Bitmap。這意味著 Bitmap 的內(nèi)存是被重新利用的,這樣可以提升性能,并且減少了內(nèi)存的分配與回收。然而,使用 inBitmap 有一些限制,特別是在Android 4.4 (API level 19)之前,只有同等大小的位圖才可以被重用。
- 管理 Android 8.0 及其以上版本的內(nèi)存
- 在 Android 8.0 及其以上版本,處理內(nèi)存,也遵循 Android 3.0 以上版本同樣的方式。同時,圖片像素數(shù)據(jù)存儲在 native 層,并且不占用 Java 堆的空間,這也代表著我們擁有更大的圖片存儲空間,可以加載質(zhì)量更高、數(shù)據(jù)更多的圖片到內(nèi)存中。但是,內(nèi)存依然不是無限的,應(yīng)用還是要受到手機內(nèi)存的限制,所以一定要注意這一點。
6.3 提高Bitmap復(fù)用
- Android3.0之后,并沒有強調(diào)Bitmap.recycle();而是強調(diào)Bitmap的復(fù)用。
- 使用LruCache對Bitmap進(jìn)行緩存,當(dāng)再次使用到這個Bitmap的時候直接獲取,而不用重走編碼流程。
- Android3.0(API 11之后)引入了BitmapFactory.Options.inBitmap字段,設(shè)置此字段之后解碼方法會嘗試復(fù)用一張存在的Bitmap。這意味著Bitmap的內(nèi)存被復(fù)用,避免了內(nèi)存的回收及申請過程,顯然性能表現(xiàn)更佳。
- 使用這個字段有幾點限制:
- 聲明可被復(fù)用的Bitmap必須設(shè)置inMutable為true;
- Android4.4(API 19)之前只有格式為jpg、png,同等寬高(要求苛刻),inSampleSize為1的Bitmap才可以復(fù)用;
- Android4.4(API 19)之前被復(fù)用的Bitmap的inPreferredConfig會覆蓋待分配內(nèi)存的Bitmap設(shè)置的inPreferredConfig;
- Android4.4(API 19)之后被復(fù)用的Bitmap的內(nèi)存必須大于需要申請內(nèi)存的Bitmap的內(nèi)存;
- Android4.4(API 19)之前待加載Bitmap的Options.inSampleSize必須明確指定為1。
- Bitmap復(fù)用的實驗,代碼如下所示,然后看打印的日志信息
- 從內(nèi)存地址的打印可以看出,兩個對象其實是一個對象,Bitmap復(fù)用成功;
- bitmapReuse占用的內(nèi)存(4346880)正好是bitmap占用內(nèi)存(1228800)的四分之一;
- getByteCount()獲取到的是當(dāng)前圖片應(yīng)當(dāng)所占內(nèi)存大小,getAllocationByteCount()獲取到的是被復(fù)用Bitmap真實占用內(nèi)存大小。雖然bitmapReuse的內(nèi)存只有4346880,但是因為是復(fù)用的bitmap的內(nèi)存,因而其真實占用的內(nèi)存大小是被復(fù)用的bitmap的內(nèi)存大?。?228800)。這也是getAllocationByteCount()可能比getByteCount()大的原因。
@RequiresApi(api = Build.VERSION_CODES.KITKAT) private void initBitmap() { BitmapFactory.Options options = new BitmapFactory.Options(); // 圖片復(fù)用,這個屬性必須設(shè)置; options.inMutable = true; // 手動設(shè)置縮放比例,使其取整數(shù),方便計算、觀察數(shù)據(jù); options.inDensity = 320; options.inTargetDensity = 320; Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.bg_autumn_tree_min, options); // 對象內(nèi)存地址; Log.i("ycBitmap", "bitmap = " + bitmap); Log.i("ycBitmap", "ByteCount = " + bitmap.getByteCount() + ":::bitmap:AllocationByteCount = " + bitmap.getAllocationByteCount()); // 使用inBitmap屬性,這個屬性必須設(shè)置; options.inBitmap = bitmap; options.inDensity = 320; // 設(shè)置縮放寬高為原始寬高一半; options.inTargetDensity = 160; options.inMutable = true; Bitmap bitmapReuse = BitmapFactory.decodeResource(getResources(), R.drawable.bg_kites_min, options); // 復(fù)用對象的內(nèi)存地址; 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.圖片其他方面優(yōu)化
7.1 減少PNG圖片的使用
- 這里要介紹一種新的圖片格式:Webp,它是由 Google 推出的一種既保留 png 格式的優(yōu)點,又能夠減少圖片大小的一種新型圖片格式。
- 在 Android 4.0(API level 14) 中支持有損的 WebP 圖像,在 Android 4.3(API level 18) 和更高版本中支持無損和透明的 WebP 圖像。
- 注意一下,Webp格式圖片僅僅只是減少圖片的質(zhì)量大小,并不會減少加載圖片后的內(nèi)存占用。
7.2 切割圓角優(yōu)化
- 方案1:直接采用Canvas.clipPath 相關(guān)api,裁剪出一個圓角區(qū)域。
- 該方案簡單暴力,通用性強。如果只是一個靜態(tài)的單圖視圖,該方法問題不大,但如果是復(fù)雜頁面,滾動的時候,測試就會跟你說,頁面卡頓了,要優(yōu)化。原因就是 Canvas.clip的相關(guān)api損耗相對較大。
- 方案2:系統(tǒng)提供的CardView設(shè)置圓角
- 把原來全工程各個視頻控件和圖片控件的外層,都加上一層CardView。改造成本大,布局層級更深一層,layout時間加長。
- 方案3:使用setXfermode法
- 此種方式就是再new一個相同尺寸的bitmap,然后使用paint.setXfermode(new PorterDuffXfermode(Mode.SRC_IN));先畫圓角矩形,再畫原始bitmap,然后就得到了一個圓角的bitmap。早期用得較多,占用bitmap雙倍內(nèi)存。
- 方案4:圖片加載庫比如Glide,F(xiàn)resco等
- 在底層,無非也是使用上面的這兩種種方式。早期的使用setXfermode來實現(xiàn),后來使用BitmapShader實現(xiàn)。使用簡單,穩(wěn)定。
- 方案5:遮罩
- 還是使用setXfermode,不過與方法一不同的是:不對圖片作任何更改,只在圓角之外再畫一層與背景顏色相同的四個角來遮擋,在視覺上造成圓角圖片的效果。
- 那個切割圓角該怎么優(yōu)化呢?
- 使用方案3,可以采用自定義view,支持LinearLayout、RelativeLayout、FrameLayout、ConstraintLayout、ImageView、TextView、View、Button等設(shè)置圓角。
- 具體案例可見:RoundCorners
7.3 如何給圖片置灰色
- 大概的操作步驟。具體可以參考:PicCalculateUtils
- 第一步:獲取原始圖片的寬高,然后創(chuàng)建一個bitmap可變位圖對象。
- 第二步:創(chuàng)建畫板canvas對象,然后創(chuàng)建畫筆paint。然后調(diào)用canvas.drawBitmap方法繪制圖片
- 第三步:對畫筆進(jìn)行修飾,設(shè)置畫筆顏色屬性,這里使用到了ColorMatrix,核心就是設(shè)置飽和度為0,即可繪制灰色內(nèi)容
7.4 如何處理圖片旋轉(zhuǎn)呢
- 在Android中使用ImageView顯示圖片的時候發(fā)現(xiàn)圖片顯示不正,方向偏了或者倒過來了。
- 解決這個問題很自然想到的兩步走,首先是要自動識別圖像方向,計算旋轉(zhuǎn)角度,然后對圖像進(jìn)行旋轉(zhuǎn)并顯示。
- 識別圖像方向
- 首先在這里提一個概念EXIF(Exchangeable Image File Format,可交換圖像文件)。簡而言之,Exif是一個標(biāo)準(zhǔn),用于電子照相機(也包括手機、掃描器等)上,用來規(guī)范圖片、聲音、視屏以及它們的一些輔助標(biāo)記格式。
- Exif支持的格式如下:圖像;壓縮圖像文件:JPEG、DCT;非壓縮圖像文件:TIFF;音頻;RIFF、WAV
- Android提供了對JPEG格式圖像Exif接口支持,可以讀取JPEG文件metadata信息,參見ExifInterface。這些Metadata信息總的來說大致分為三類:日期時間、空間信息(經(jīng)緯度、高度)、Camera信息(孔徑、焦距、旋轉(zhuǎn)角、曝光量等等)。
- 關(guān)于圖像旋轉(zhuǎn)
- 獲取了圖片的旋轉(zhuǎn)方向后,然后再設(shè)置圖像旋轉(zhuǎn)。最后Bitmap提供的靜態(tài)createBitmap方法,可以對圖片設(shè)置旋轉(zhuǎn)角度。具體看:PicCalculateUtils
7.5 保存圖片且刷相冊
- 大概的操作步驟如下所示。具體可看:ImageSaveUtils
- 第一步:創(chuàng)建圖片文件,然后將bitmap對象寫到圖片文件中
- 第二步:通過MediaStore將圖片插入到共享目錄相冊中
- 第三步:發(fā)送通知,通知相冊中刷新插入圖片的數(shù)據(jù)。注意,獲取圖片資源uri刷新即可,避免刷新所有數(shù)據(jù)造成等待時間過長。
7.6 統(tǒng)一圖片域名優(yōu)化
- 域名統(tǒng)一
- 減少了10%+的重復(fù)圖片下載和內(nèi)存消耗。同時減少之前多域名圖片加載時重復(fù)創(chuàng)建HTTPS請求的過程,減少圖片加載時間。
7.7 優(yōu)化H5圖片加載
- 通過攔截WebView圖片加載的方式,讓原生圖片庫來下載圖片之后傳遞圖片二進(jìn)制數(shù)據(jù)給WebView顯示。
- 采用OkHttp攔截資源緩存,下面是大概的思路。緩存的入口從shouldInterceptRequest出發(fā)
- 第一步,拿到WebResourceRequest對象中請求資源的url還有header,如果開發(fā)者設(shè)置不緩存則返回null
- 第二步,如果緩存,通過url判斷攔截資源的條件,過濾非http,音視頻等資源,這個是可自由配置緩存內(nèi)容比如css,png,jpg,xml,txt等
- 第三步,判斷本地是否有OkHttp緩存數(shù)據(jù),如果有則直接讀取本地資源,通過url找到對應(yīng)的path路徑,然后讀取文件流,組裝數(shù)據(jù)返回。
- 第四步,如果沒有緩存數(shù)據(jù),創(chuàng)建OkHttp的Request請求,將資源網(wǎng)絡(luò)請求交給okHttp來處理,并且用它自帶的緩存功能,當(dāng)然如果是請求失敗或者異常則返回null,否則返回正常數(shù)據(jù)
- 關(guān)于webView圖片緩存的方案,可以直接參考:YCWebView
7.8 優(yōu)化圖片陰影效果
- 陰影效果有哪些實現(xiàn)方式
- 第一種:使用CardView,但是不能設(shè)置陰影顏色
- 第二種:采用shape疊加,存在后期UI效果不便優(yōu)化
- 第三種:UI切圖
- 第四種:自定義View
- 第五種:自定義Drawable
- 否定上面前兩種方案原因分析?
- 第一個方案的CardView漸變色和陰影效果很難控制,只能支持線性或者環(huán)裝形式漸變,這種不滿足需要,因為陰影本身是一個四周一層很淡的顏色包圍,在一個矩形框的層面上顏色大概一致,而且這個CardView有很多局限性,比如不能修改陰影的顏色,不能修改陰影的深淺。所以這個思路無法實現(xiàn)這個需求。
- 第二個采用shape疊加,可以實現(xiàn)陰影效果,但是影響UI,且陰影部分是占像素的,而且不靈活。
- 第三個方案詢問了一下ui。他們給出的結(jié)果是如果使用切圖的話那標(biāo)注的話很難標(biāo),身為一個優(yōu)秀的設(shè)計師大多對像素點都和敏感,界面上的像素點有一點不協(xié)調(diào)那都是無法容忍的。
- 網(wǎng)上一些介紹陰影效果方案
- 所有在深奧的技術(shù),也都是為需求做準(zhǔn)備的。也就是需要實踐并且可以用到實際開發(fā)中,這篇文章不再抽象介紹陰影效果原理,理解三維空間中如何處理偏移光線達(dá)到陰影視差等,網(wǎng)上看了一些文章也沒看明白或者理解。這篇博客直接通過調(diào)用api實現(xiàn)預(yù)期的效果。
- 多個drawable疊加,使用layer-list可以將多個drawable按照順序?qū)盈B在一起顯示,默認(rèn)情況下,所有的item中的drawable都會自動根據(jù)它附上view的大小而進(jìn)行縮放,layer-list中的item是按照順序從下往上疊加的,即先定義的item在下面,后面的依次往上面疊放
- 陰影是否占位
- 使用CardView陰影不占位,不能設(shè)置陰影顏色和效果
- 使用shape陰影是可以設(shè)置陰影顏色,但是是占位的
- 幾種方案優(yōu)缺點對比分析
- CardView 優(yōu)點:自帶功能實現(xiàn)簡單 缺點:自帶圓角不一定可適配所有需求
- layer(shape疊加) 優(yōu)點:實現(xiàn)形式簡單 缺點:效果一般
- 自定義實現(xiàn) 優(yōu)點:實現(xiàn)效果好可配置能力高 缺點:需要開發(fā)者自行開發(fā)
- 關(guān)于解決陰影效果
- 具體各種方案的對比可以參考這個demo:AppShadowLib
7.9 圖片資源的壓縮
- 我們應(yīng)用中使用的圖片,設(shè)計師出的原圖通常都非常大,他們通常會使用工具,經(jīng)過一定的壓縮,縮減到比較小一些的大小。
- 但是,這些圖片通常都有一定的可壓縮空間,我在之前的項目中,對圖片進(jìn)行了二次壓縮,整體壓縮率達(dá)到了 40%~50% ,效果還是非常不錯的。
- 這里介紹下常用的,圖片壓縮的方法:
- 使用壓縮工具對圖片進(jìn)行二次壓縮。
- 根據(jù)最終圖片是否需要透明度展示,優(yōu)先選擇不透明的圖片格式,例如,我們應(yīng)該避免使用 png 格式的圖片。
- 對于色彩簡單,例如,一些背景之類的圖片,可以選擇使用布局文件來定義(矢量圖),這樣就會非常節(jié)省內(nèi)存了。
- 如果包含透明度,優(yōu)先使用 WebP 等格式圖像。
- 圖片在上線前進(jìn)行壓縮處理,不但可以減少內(nèi)存的使用,如果圖片是網(wǎng)絡(luò)獲取的,也可以減少網(wǎng)絡(luò)加載的流量和時間。
- 推薦一個圖片壓縮網(wǎng)站:tinypng網(wǎng)站