您好,登錄后才能下訂單哦!
前言
圖片作為內存消耗大戶,一直是開發人員嘗試優化的重點對象。Bitmap的內存從3.0以前的位于native,到后來改成jvm,再到8.0又改回到native。fresco花費很多精力在5.0系統之前把Bitmap內存改回到native,高版本上面則遵循系統實現,卻又被官方打臉。
jvm每個進程都有內存上限,而native則沒有限制(不是沒有影響,至少不會oom),所以把內存大戶Bitmap挪到native可能是很多人的夢想,但native的管理和實現明顯比jvm更為復雜,除非有現成實現,很少有人去動這一塊。行業里面的大部分圖片庫都沒有涉及這塊,大部分的程序員也秉著夠用就好的態度用了很多年,這說明程序員也是會偷懶的。官方的策略修改到底原因幾何,其實我也沒搜到相關說明,有知道的同學歡迎留言。
概念
圖片占用的內存:圖片高度 * 圖片寬度 * 一個像素占用的內存大小這個公式代表一個圖片最終占用的內存大小,項目中的優化圖片占用內存都是通過這個三個參數來優化的。
第一條規則:把Bitmap保存到native
一個app里面的圖片都會有尺寸,一般情況下面圖片的尺寸就是view的大小,而view的大小在我們使用dp單位后在不同的機器上面表現出來的實際像素都有差別,為了節約流量開銷,加快返回速度,同時符合按需加載的原則,我們應該只加載實際view尺寸大小的圖片。一般圖片存儲提供商都會提供在線壓縮服務,我們只需要在請求鏈接里面加上參數即可。這里還有個問題我們一般請求加載圖片的代碼都是寫在Activity的onCreate,或者Adapter的getView函數里面,這個時候其實是獲取不到view尺寸的(還未measure),這里有幾種做法:
使用目測:比如一個列表是左右圖片布局的,那就可以請求屏幕一半寬度的尺寸圖片
view使用了固定尺寸:這個沒有問題,我們直接拿getLayoutParams()的width和height就可以了
view的maxWidth/maxHeight:view無法固定尺寸,我們可以在xml里面給view配置maxWidth/maxHeight來指導圖片庫加載什么尺寸的圖片
加載圖片前先measure:不怎么推薦,因為圖片加載出來后view還得measure一次
一般做法是給圖片加載庫包裝一層,根據傳進來的url判斷是否已經指定大小(開發者當然可以決定想加載多大圖片),如果還未指定則使用上面的策略進行動態調整,如果最后還是沒能加上縮放參數,則有個兜底策略,不加載超過屏幕尺寸大小。
第二條規則:按需請求
做了上面按需加載后還有個問題,會發現有時候不同的頁面需要加載同一個圖片url,但在尺寸上面有細微差別,結果導致請求重復(一般圖片加載庫都是url作為緩存key),有點弄巧成拙,反倒浪費了流量和時間。這種情況我們需要做些微調。對于A頁面圖片尺寸是200x200,對于B頁面圖片尺寸是180x180,我們認為可以使用200x200的圖片縮放到180x180,這有兩種做法:第一種是讓開發者始終都去加載稍微大一點的圖,這個要求有點高,一個頁面開發的時候很難前后聯系。第二種是修改圖片加載庫,自動完成這個事情。后者自然合理,修改圖片加載庫在決定使用緩存的那一步判斷是否有比自己大的緩存已經存在即可,當然這個策略可以每個產品自己調整,比如也可以認為已經存在的緩存尺寸小于一定值也是可以接受也是可以的。還有復雜的情況比如緩存圖片高寬比和要加強的不一樣如何處理等等,策略都可以自己定,但一定有必要做這個事情。
這里還要補充一點,大型產品一般圖片域名會有好幾個,用來做鏈路擇優用的,一定要記得緩存的時候用來做key的url要去掉域名影響。
再補充一點,有些特殊的使用場景可以考慮采用上面說的第一種方式來做,舉個例子比如一個操作一定會加載100x100的圖,然后也一定會等會加載500x500的同一張圖,這種場景下面按第二種方式來處理顯然會加載兩次,但如果開發者這2個位置寫死都加載500x500則明顯更好一些。所以方法是死的,人是活的,要看實際使用場景。
還有一些特殊場景,比如程序里面有兩個進程,A進程會加載500x500的圖,B進程會加載不管什么尺寸的同一張圖,默認情況下面這2個請求會同時發出,這就很可能會造成重復請求,這種情況下面需要做一點跨進程同步,或者簡單一點其中一個進程請求做一點延遲處理。
第三條規則:合并相似請求
實在不得已要從服務端加載大圖或者原始尺寸下來,或者因為上面說的策略故意加載大圖下來,在decode的時候要進行采樣,這個是老生常談了,使用options.inJustDecodeBounds來獲取原始尺寸,然后按需使用options.inSampleSize來采樣圖片到接近view尺寸。
第四條規則:按需加載
Bitmap在decode的時候可以使用inPreferredConfig指定配置格式,常見的有:
參數取值含義ALPHA_8圖片中每個像素用一個字節(8位)存儲,該字節存儲的是圖片8位的透明度值RGB_565圖片中每個像素用兩個字節(16位)存儲,兩個字節中高5位表示紅色通道,中間6位表示綠色通道,低5位表示藍色通道ARGB_4444圖片中每個像素用兩個字節(16位)存儲,Alpha,R,G,B四個通道每個通道用4位表示ARGB_8888圖片中每個像素用四個字節(32位)存儲,Alpha,R,G,B四個通道每個通道用8位表示
對于質量細節要求比較高的圖片可以使用ARGB_888,這也是fresco的默認配置。而對于JPG圖片可以使RGB_565,從上面可以看出內存占用之間減少一半,非常有吸引力,而app里面事實上大部分應該都是JPG。但往往在和視覺的PK當中開發往往敗下陣來,降低了圖片質量不行!!開發總是鍥而不舍,我們可以建議采用這樣的策略:對于尺寸小于一定尺寸的JPG(比如300),我們使用565,而對于大圖為了保留細節我們仍然使用8888。還是那句話策略是活的。
第五條規則:進一步按需加載
使用三級緩存機制,內存磁盤網絡,這也是官方推薦的方式。內存緩存旨在加快訪問速度,磁盤緩存避免反復請求。關于這一點就不在贅述了,基本開源圖片庫都會這么做
第六條規則:使用三級緩存機制
很多場景下面我們需要顯示圖片的一部分,或者進行圖片效果疊加,比如做個倒影之類的。很多同學上來就準備createBitmap,然后把疊加效果繪制到這個臨時Bitmap,或者從原始Bitmap里面先剪一部分出來生成一個新的Bitmap,再設給ImageView。或者使用createScaledBitmap進行縮放。更不小心的同學可能直接把這些操作代碼寫在UI線程,然后寫在子線程又比較麻煩,這邊推薦的是使用自定義繪制,canvas有個drawBitmap方法可以把某個區域繪制到指定位置。疊加效果也可以完全使用自定義view來自己draw,這樣不會有臨時Bitmap生成,效率會更高。
如果自定義view有困難,我們可以使用Drawable,只要能拿到canvas,這兩種做法是一樣的。
這里列舉一些實例,好讓大家可以進一步理解:
一個按鈕有普通和按下狀態,按下是普通狀態上面疊加一個遮罩,不需要切兩張圖,按下狀態的Drawable可以使用自定義Drawable的canvas先繪制普通狀態的圖,再在上面繪制一層顏色。或者按下狀態使用LayerDrawable,這個Drawable自動幫你做了這個事情
需要把Bitmap的[0,0,200,200]的區域顯示到ImageView上面,使用canvas.drawBitmap(bitmap, [0,0,200,200], [0,0,圖片寬,圖片高],paint)
繪制倒影,這個邏輯性比較強了,這里就不具體展開,canvas的操作學習下,結合局部繪制其實很簡單
有個圖片,需要在左上角顯示一個角標,正常情況下面需要在左上角擺一個view,如果使用Drawable自定義繪制,canvas畫一下就好,類似下面的示例代碼。
給大家一個自定義繪制的例子,隨心組合:
class WithLineDrawable extends DrawableWrapper { private MyConstantState mMyConstantState; private boolean mForTop; private Paint mLinePaint = new Paint(); public WithLineDrawable(Drawable drawable, boolean forTop) { super(drawable); mLinePaint.setColor(getLineColor()); mForTop = forTop; } @Override public void draw(Canvas canvas) { super.draw(canvas); if (mForTop) { canvas.drawLine(0, 0, getBounds().width(), 0, mLinePaint); } else { canvas.drawLine(0, getBounds().height(), getBounds().width(), getBounds().height(), mLinePaint); } } @Nullable @Override public ConstantState getConstantState() { if (mMyConstantState == null) { mMyConstantState = new MyConstantState(); } return mMyConstantState; } class MyConstantState extends ConstantState { @NonNull @Override public Drawable newDrawable() { return new WithLineDrawable(getWrappedDrawable().getConstantState().newDrawable(), mForTop); } @Override public int getChangingConfigurations() { return 0; } } }
一定要把觀念從Bitmap轉變到Drawable,當還在費勁心思Bitmap該如何處理的時候,想想Drawable里面如何使用canvas進行各種自定義繪制。
第七條規則:多使用自定義View或者Drawable自定義繪制
圖片格式發展到今天已經非常多樣了,目前很多開源庫都支持了webp來代替jpg和gif,webp在壓縮率上面有很多優勢,雖然解碼上面略遜一籌,經過我們測試還是很不錯的。也是推薦大家使用,不論是網絡圖片下載還是apk內置,用來代替jpg很合適,而代替png則還需要一些時間,主要是低版本系統對于透明webp還有些兼容問題。Android P上面支持了heif格式也是想代替jpg,不過這個格式目前還沒仔細研究過。
對于內置apk的圖標類,則推薦使用svg,不再需要切幾套圖,而且非常小,官方使用的compat包里面解碼svg會做緩存,也進一步提升性能。不過也正因為此盡量不要一個圖片使用過多不同尺寸。大部分的圖標都使用代碼代替圖片后,apk大小可以明顯減少,這也符合我們的原則:能程序畫的就絕不切圖。
第八條規則:使用更好的圖片格式
很多時候我們需要給圖標換色,關于顏色混合有一套理論,官方很早就支持,使用ColorFilter,后來compat包里面出了個tint,所以如果有顏色混合處理的相關邏輯,千萬不要去生成臨時Bitmap,使用類似如下代碼:
//1:通過圖片資源文件生成Drawable實例 Drawable drawable = getResources().getDrawable(R.mipmap.ic_launcher).mutate(); //2:先調用DrawableCompat的wrap方法 drawable = DrawableCompat.wrap(drawable); //3:再調用DrawableCompat的setTint方法,為Drawable實例進行著色 DrawableCompat.setTint(drawable, Color.RED);
第九條規則:使用著色API
內置apk的圖片資源非常多,總有一些常規圖片仍然需要使用jpg或者png,我們要想辦法進一步壓縮他們,這樣可以有效控制apk大小,這里推薦使用ImageOptim,這個工具集合了很多種壓縮方式,效果顯著。
第十條規則:使用壓縮工具
后記:
很多面試的時候問如何做圖片加載優化,他們會回答recycle
bitmap,事實上這個操作要很謹慎,一不留神就會導致出問題。大部分的應用不太會干這個事情,吃力不討好,交給jvm垃圾回收多好。圖片解碼還有一些參數可以優化,比如inBitmap,這里就不具體展開了。
總結下
好了,以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家對億速云的支持。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。