最近產品經理一直抱怨圖片加載慢,為此客戶端開發這邊也做了許多努力,比如重定向到CDN,使用webp減小圖片大小,使用降低圖片壓縮質量,更換圖片加載框架等等動作。現在講一下webp格式圖片這個方案。
WebP格式,谷歌(google)開發的一種旨在加快圖片加載速度的圖片格式。圖片壓縮體積大約只有JPEG的2/3,並能節省大量的服務器帶寬資源和數據空間。Facebook Ebay等知名網站已經開始測試並使用WebP格式。
但WebP是一種有損壓縮。相較編碼JPEG文件,編碼同樣質量的WebP文件需要占用更多的計算資源。
桌面版Chrome,Android4.0以上可打開WebP格式。 ——摘自百度百科
Picasso原生支持webp格式圖片的加載
理論上說,不需要修改任何代碼,只要服務器提供支持,Picasso就能像加載jpg一樣加載webp格式的圖片。那麼現在問題來了
1、如何識別是個下載的文件是不是webp格式。
2、如何將webp解析成bitmap的,會不會出現內存上的問題
帶著這兩個問題,我仔細閱讀了一下Picasso的源碼,摸清Picasso加載webp文件的流程。首先看一下下載代碼,以okhttp下載器為例
@Override public Response load(Uri uri, int networkPolicy) throws IOException {
CacheControl cacheControl = null;
//...省略一部分緩存控制代碼
Request.Builder builder = new Request.Builder().url(uri.toString());
if (cacheControl != null) {
builder.cacheControl(cacheControl);
}
com.squareup.okhttp.Response response = client.newCall(builder.build()).execute();//發起一次新的網絡請求
int responseCode = response.code();
if (responseCode >= 300) {//如果遇到網絡錯誤
response.body().close();
throw new ResponseException(responseCode + " " + response.message(), networkPolicy,
responseCode);
}
boolean fromCache = response.cacheResponse() != null;
ResponseBody responseBody = response.body();//取出http響應的body,這個body就是圖片文件
return new Response(responseBody.byteStream(), fromCache, responseBody.contentLength());
}
從下載器中可以看出,okhttp交給Picasso的是一個InputStream,那麼Picasso是怎樣處理這個輸入流的呢,於是繼續讀代碼
Bitmap hunt() throws IOException {
Bitmap bitmap = null;
if (shouldReadFromMemoryCache(memoryPolicy)) {
//省略從本地LRU緩存獲得bitmap的代碼
}
data.networkPolicy = retryCount == 0 ? NetworkPolicy.OFFLINE.index : networkPolicy;
RequestHandler.Result result = requestHandler.load(data, networkPolicy);//通過downloader發起網絡請求
if (result != null) {
loadedFrom = result.getLoadedFrom();//看看是不是來自磁盤緩存
exifRotation = result.getExifOrientation();//查看exif信息,主要是看旋轉角度
bitmap = result.getBitmap();//嘗試能否直接拿到bitmap
// If there was no Bitmap then we need to decode it from the stream.
if (bitmap == null) {//如果不能直接拿到bitmap,那麼就需要從輸入流中轉換
InputStream is = result.getStream();//獲取文件輸入流,可能是網絡流,也可能是磁盤緩存文件的流
try {
bitmap = decodeStream(is, data);//重要的方法,從輸入流中解析出bitmap
} finally {
Utils.closeQuietly(is);
}
}
}
//...省略根據exif信息調整圖片的代碼
return bitmap;
}
以上代碼來自BitmapHunter這個類
其中我們需要的就是decodeStream()這個方法,是這個方法將網絡獲得的輸入流轉換成bitmap的,那麼這個方法怎麼寫的呢?
/**
* Decode a byte stream into a Bitmap. This method will take into account additional information
* about the supplied request in order to do the decoding efficiently (such as through leveraging
* {@code inSampleSize}).
*/
static Bitmap decodeStream(InputStream stream, Request request) throws IOException {
MarkableInputStream markStream = new MarkableInputStream(stream);
stream = markStream;
long mark = markStream.savePosition(65536); // TODO fix this crap.
final BitmapFactory.Options options = RequestHandler.createBitmapOptions(request);
final boolean calculateSize = RequestHandler.requiresInSampleSize(options);
boolean isWebPFile = Utils.isWebPFile(stream);//判斷是不是webp格式的圖片文件,webp格式使用BitmapFactory與Jpg,png不同
markStream.reset(mark);
// When decode WebP network stream, BitmapFactory throw JNI Exception and make app crash.
// Decode byte array instead
if (isWebPFile) {
byte[] bytes = Utils.toByteArray(stream);//將輸入流讀取成字節數組,此處會不會OOM呢?
if (calculateSize) {
BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options);
RequestHandler.calculateInSampleSize(request.targetWidth, request.targetHeight, options,//根據imageView的大小計算應該讀取的bitmap大小
request);
}
return BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options);//webp格式的圖片要使用decodeByteArray方法,否則會crash
} else {
//...省略jpg和png的轉換代碼
return bitmap;
}
}這裡有個很重要的方法是Utils.isWebPFile(),那麼這個方法是怎麼根據一個輸入流判斷是jpg還是webp格式的文件呢,肯定不能用擴展名吧,於是點進去看下
private static final int WEBP_FILE_HEADER_SIZE = 12;
private static final String WEBP_FILE_HEADER_RIFF = "RIFF";
private static final String WEBP_FILE_HEADER_WEBP = "WEBP";
static boolean isWebPFile(InputStream stream) throws IOException {
byte[] fileHeaderBytes = new byte[WEBP_FILE_HEADER_SIZE];
boolean isWebPFile = false;
if (stream.read(fileHeaderBytes, 0, WEBP_FILE_HEADER_SIZE) == WEBP_FILE_HEADER_SIZE) {
// If a file's header starts with RIFF and end with WEBP, the file is a WebP file
isWebPFile = WEBP_FILE_HEADER_RIFF.equals(new String(fileHeaderBytes, 0, 4, "US-ASCII"))
&& WEBP_FILE_HEADER_WEBP.equals(new String(fileHeaderBytes, 8, 4, "US-ASCII"));
}
return isWebPFile;
}
實現原理也不麻煩,用的linux常用的判斷文件類型的方法,就是讀取一個文件的頭幾個字節,然後轉換成ASCII字符看看是啥文件,這樣正好也利用了inputStream讀取的順序,很簡單的完成了jpg,png和webp文件的識別。
注意:不要在這個方法執行之前對inputStream執行讀取操作,否則很有可能無法判斷webp文件
如果你的webp文件加載不出來,不要忘了測試這個方法的返回值。
更多Android相關信息見Android 專題頁面 http://www.linuxidc.com/topicnews.aspx?tid=11