前言
建網(wǎng)站原本是網(wǎng)站策劃師、網(wǎng)絡(luò)程序員、網(wǎng)頁設(shè)計師等,應(yīng)用各種網(wǎng)絡(luò)程序開發(fā)技術(shù)和網(wǎng)頁設(shè)計技術(shù)配合操作的協(xié)同工作。創(chuàng)新互聯(lián)公司專業(yè)提供網(wǎng)站設(shè)計制作、網(wǎng)站制作,網(wǎng)頁設(shè)計,網(wǎng)站制作(企業(yè)站、響應(yīng)式網(wǎng)站開發(fā)、電商門戶網(wǎng)站)等服務(wù),從網(wǎng)站深度策劃、搜索引擎友好度優(yōu)化到用戶體驗的提升,我們力求做到極致!圖片作為內(nèi)存消耗大戶,一直是開發(fā)人員嘗試優(yōu)化的重點對象。Bitmap的內(nèi)存從3.0以前的位于native,到后來改成jvm,再到8.0又改回到native。fresco花費(fèi)很多精力在5.0系統(tǒng)之前把Bitmap內(nèi)存改回到native,高版本上面則遵循系統(tǒng)實現(xiàn),卻又被官方打臉。
jvm每個進(jìn)程都有內(nèi)存上限,而native則沒有限制(不是沒有影響,至少不會oom),所以把內(nèi)存大戶Bitmap挪到native可能是很多人的夢想,但native的管理和實現(xiàn)明顯比jvm更為復(fù)雜,除非有現(xiàn)成實現(xiàn),很少有人去動這一塊。行業(yè)里面的大部分圖片庫都沒有涉及這塊,大部分的程序員也秉著夠用就好的態(tài)度用了很多年,這說明程序員也是會偷懶的。官方的策略修改到底原因幾何,其實我也沒搜到相關(guān)說明,有知道的同學(xué)歡迎留言。
概念
圖片占用的內(nèi)存:圖片高度 * 圖片寬度 * 一個像素占用的內(nèi)存大小這個公式代表一個圖片最終占用的內(nèi)存大小,項目中的優(yōu)化圖片占用內(nèi)存都是通過這個三個參數(shù)來優(yōu)化的。
第一條規(guī)則:把Bitmap保存到native
一個app里面的圖片都會有尺寸,一般情況下面圖片的尺寸就是view的大小,而view的大小在我們使用dp單位后在不同的機(jī)器上面表現(xiàn)出來的實際像素都有差別,為了節(jié)約流量開銷,加快返回速度,同時符合按需加載的原則,我們應(yīng)該只加載實際view尺寸大小的圖片。一般圖片存儲提供商都會提供在線壓縮服務(wù),我們只需要在請求鏈接里面加上參數(shù)即可。這里還有個問題我們一般請求加載圖片的代碼都是寫在Activity的onCreate,或者Adapter的getView函數(shù)里面,這個時候其實是獲取不到view尺寸的(還未measure),這里有幾種做法:
使用目測:比如一個列表是左右圖片布局的,那就可以請求屏幕一半寬度的尺寸圖片
view使用了固定尺寸:這個沒有問題,我們直接拿getLayoutParams()的width和height就可以了
view的maxWidth/maxHeight:view無法固定尺寸,我們可以在xml里面給view配置maxWidth/maxHeight來指導(dǎo)圖片庫加載什么尺寸的圖片
加載圖片前先measure:不怎么推薦,因為圖片加載出來后view還得measure一次
一般做法是給圖片加載庫包裝一層,根據(jù)傳進(jìn)來的url判斷是否已經(jīng)指定大?。ㄩ_發(fā)者當(dāng)然可以決定想加載多大圖片),如果還未指定則使用上面的策略進(jìn)行動態(tài)調(diào)整,如果最后還是沒能加上縮放參數(shù),則有個兜底策略,不加載超過屏幕尺寸大小。
第二條規(guī)則:按需請求
做了上面按需加載后還有個問題,會發(fā)現(xiàn)有時候不同的頁面需要加載同一個圖片url,但在尺寸上面有細(xì)微差別,結(jié)果導(dǎo)致請求重復(fù)(一般圖片加載庫都是url作為緩存key),有點弄巧成拙,反倒浪費(fèi)了流量和時間。這種情況我們需要做些微調(diào)。對于A頁面圖片尺寸是200x200,對于B頁面圖片尺寸是180x180,我們認(rèn)為可以使用200x200的圖片縮放到180x180,這有兩種做法:第一種是讓開發(fā)者始終都去加載稍微大一點的圖,這個要求有點高,一個頁面開發(fā)的時候很難前后聯(lián)系。第二種是修改圖片加載庫,自動完成這個事情。后者自然合理,修改圖片加載庫在決定使用緩存的那一步判斷是否有比自己大的緩存已經(jīng)存在即可,當(dāng)然這個策略可以每個產(chǎn)品自己調(diào)整,比如也可以認(rèn)為已經(jīng)存在的緩存尺寸小于一定值也是可以接受也是可以的。還有復(fù)雜的情況比如緩存圖片高寬比和要加強(qiáng)的不一樣如何處理等等,策略都可以自己定,但一定有必要做這個事情。
這里還要補(bǔ)充一點,大型產(chǎn)品一般圖片域名會有好幾個,用來做鏈路擇優(yōu)用的,一定要記得緩存的時候用來做key的url要去掉域名影響。
再補(bǔ)充一點,有些特殊的使用場景可以考慮采用上面說的第一種方式來做,舉個例子比如一個操作一定會加載100x100的圖,然后也一定會等會加載500x500的同一張圖,這種場景下面按第二種方式來處理顯然會加載兩次,但如果開發(fā)者這2個位置寫死都加載500x500則明顯更好一些。所以方法是死的,人是活的,要看實際使用場景。
還有一些特殊場景,比如程序里面有兩個進(jìn)程,A進(jìn)程會加載500x500的圖,B進(jìn)程會加載不管什么尺寸的同一張圖,默認(rèn)情況下面這2個請求會同時發(fā)出,這就很可能會造成重復(fù)請求,這種情況下面需要做一點跨進(jìn)程同步,或者簡單一點其中一個進(jìn)程請求做一點延遲處理。
第三條規(guī)則:合并相似請求
實在不得已要從服務(wù)端加載大圖或者原始尺寸下來,或者因為上面說的策略故意加載大圖下來,在decode的時候要進(jìn)行采樣,這個是老生常談了,使用options.inJustDecodeBounds來獲取原始尺寸,然后按需使用options.inSampleSize來采樣圖片到接近view尺寸。
第四條規(guī)則:按需加載
Bitmap在decode的時候可以使用inPreferredConfig指定配置格式,常見的有:
參數(shù)取值含義ALPHA_8圖片中每個像素用一個字節(jié)(8位)存儲,該字節(jié)存儲的是圖片8位的透明度值RGB_565圖片中每個像素用兩個字節(jié)(16位)存儲,兩個字節(jié)中高5位表示紅色通道,中間6位表示綠色通道,低5位表示藍(lán)色通道ARGB_4444圖片中每個像素用兩個字節(jié)(16位)存儲,Alpha,R,G,B四個通道每個通道用4位表示ARGB_8888圖片中每個像素用四個字節(jié)(32位)存儲,Alpha,R,G,B四個通道每個通道用8位表示
對于質(zhì)量細(xì)節(jié)要求比較高的圖片可以使用ARGB_888,這也是fresco的默認(rèn)配置。而對于JPG圖片可以使RGB_565,從上面可以看出內(nèi)存占用之間減少一半,非常有吸引力,而app里面事實上大部分應(yīng)該都是JPG。但往往在和視覺的PK當(dāng)中開發(fā)往往敗下陣來,降低了圖片質(zhì)量不行!!開發(fā)總是鍥而不舍,我們可以建議采用這樣的策略:對于尺寸小于一定尺寸的JPG(比如300),我們使用565,而對于大圖為了保留細(xì)節(jié)我們?nèi)匀皇褂?888。還是那句話策略是活的。
第五條規(guī)則:進(jìn)一步按需加載
使用三級緩存機(jī)制,內(nèi)存磁盤網(wǎng)絡(luò),這也是官方推薦的方式。內(nèi)存緩存旨在加快訪問速度,磁盤緩存避免反復(fù)請求。關(guān)于這一點就不在贅述了,基本開源圖片庫都會這么做
第六條規(guī)則:使用三級緩存機(jī)制
很多場景下面我們需要顯示圖片的一部分,或者進(jìn)行圖片效果疊加,比如做個倒影之類的。很多同學(xué)上來就準(zhǔn)備createBitmap,然后把疊加效果繪制到這個臨時Bitmap,或者從原始Bitmap里面先剪一部分出來生成一個新的Bitmap,再設(shè)給ImageView?;蛘呤褂胏reateScaledBitmap進(jìn)行縮放。更不小心的同學(xué)可能直接把這些操作代碼寫在UI線程,然后寫在子線程又比較麻煩,這邊推薦的是使用自定義繪制,canvas有個drawBitmap方法可以把某個區(qū)域繪制到指定位置。疊加效果也可以完全使用自定義view來自己draw,這樣不會有臨時Bitmap生成,效率會更高。
如果自定義view有困難,我們可以使用Drawable,只要能拿到canvas,這兩種做法是一樣的。
這里列舉一些實例,好讓大家可以進(jìn)一步理解:
一個按鈕有普通和按下狀態(tài),按下是普通狀態(tài)上面疊加一個遮罩,不需要切兩張圖,按下狀態(tài)的Drawable可以使用自定義Drawable的canvas先繪制普通狀態(tài)的圖,再在上面繪制一層顏色?;蛘甙聪聽顟B(tài)使用LayerDrawable,這個Drawable自動幫你做了這個事情
需要把Bitmap的[0,0,200,200]的區(qū)域顯示到ImageView上面,使用canvas.drawBitmap(bitmap, [0,0,200,200], [0,0,圖片寬,圖片高],paint)
繪制倒影,這個邏輯性比較強(qiáng)了,這里就不具體展開,canvas的操作學(xué)習(xí)下,結(jié)合局部繪制其實很簡單
有個圖片,需要在左上角顯示一個角標(biāo),正常情況下面需要在左上角擺一個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; } } }