[原]Android Webp 完全解析 快來縮小apk的大小吧
一、概述
最近項目準備嘗試使用webp來縮小包的體積,于是抽空對相關知識進行了調研和學習。
至于什么是webp,使用webp有什么好處我就不贅述了,具體可以參考騰訊isux上的這篇文章 WebP 探尋之路 ,大致了解下就ok了。
入手大致需要考慮以下幾個問題:
- 如何將現有的jpeg/png等圖轉化為webp?
- webp格式的圖片如何使用?
- 有沒有兼容性的問題?
下面就跟著上面3個問題開始進行。
二、jpeg/png到webp的互轉
這個官方提供了相互轉化的工具,以及具體的使用方式,可以參考:
截個圖,可以看到左側的功能列表,包含一系列的功能,encode、decode、view等…
因為有比較詳細的文檔,這里簡單介紹下:
首先下載工具:
libwebp-0.4.1-mac-10.8-2.tar.gz
下載完成后解壓,然后進入bin目錄:
MacBook-Pro:bin zhanghongyang01$ pwd
/Users/zhanghongyang01/hongyang/works/libwebp-0.4.1-mac-10.8 2/bin
MacBook-Pro:bin zhanghongyang01$ ls -l
total 5152
-rwxr-xr-x@ 1 zhanghongyang01 staff 1302772 9 20 2014 cwebp
-rwxr-xr-x@ 1 zhanghongyang01 staff 421508 9 20 2014 dwebp
-rwxr-xr-x@ 1 zhanghongyang01 staff 402128 9 20 2014 gif2webp
-rwxr-xr-x@ 1 zhanghongyang01 staff 264588 9 20 2014 vwebp
-rwxr-xr-x@ 1 zhanghongyang01 staff 237376 9 20 2014 webpmux
大致有4個命令工具,分別用于png等轉換為webp;webp轉化為png;git轉化為webp;查看webp圖片;最后一個是用于創建webp動畫文件的。
(1) jpeg、png 轉為webp [cwebp]
cwebp weixin.png -o weixin.webp
(2) webp轉為jpeg、png [dwebp]
dwebp weixin.webp -o weixin.png
(3) gif 轉化為webp
./gif2webp xingye.gif -o xingye.webp
每個命令都有一堆options,可以自己研究下
三、使用
Webp在app中一般可以用于兩個方面
- 一個是對與服務端交互過程中使用webp圖片
- 另一個是應用中的資源文件
(1)與服務端交互使用webp圖片
這種方式非常簡單,因為從Android4.0開始已經對webp圖片進行的支持。
下面我們寫個例子,這里我準備了一個webp的圖片,我直接放到assets目錄,然后編寫如下代碼:
# 這是一個完全不透明圖的測試
Bitmap bitmap = BitmapFactory.decodeStream(getAssets().open("icon.webp"));
imageView.setImageBitmap(bitmap);
找了臺4.0.4(API15)的三星手機(ps:實在是找不到4.0的手機了),運行感覺還不錯喲~
正在竊喜的時候,我又換了張圖片,因為有些時候我們的圖部分區域是透明了,于是我找了張圖片,轉化為webp,按照上述的代碼,同樣的操作,運行完成后,發現, 整個圖都顯示不出來了 。
趕緊找了個4.2.2(API17)的手機,顯示正常。
于是看一眼文檔:
文檔上對webp decode和encode的支持,是這樣寫的:
decode / encode
(Android 4.0+)
(Lossless, Transparency, Android 4.2.1+)
https://developer.android.com/guide/appendix/media-formats.html
那么結合文檔和實驗,大致可以有如下的結論:
- 4.2.1+ 對于webp的decode、encode是完全支持的(包含半透明的webp圖)
- 對于4.0+ 到 4.2.1 ,只支持完全不透明的decode、encode的webp圖
- 4.0 以下,應該是默認不支持webp了
看到這個結論,那么就是大家的產品最低的支持版本了。
4.2.1起步的話,目前來看,我是不能接受的,所以只有引入so來做低版本兼容了。
(2)兼容so的獲取
好在官方已經提供了相關webp支持的源碼了,點擊下載:
如果你的ndk的知識足夠的話,可以自己利用源碼,去生成so文件使用。
當然了,你也可以使用前人已經封裝好的庫:
我們這里選擇使用第二個庫,這里選擇copy它生成的so文件以及輔助類到項目中,你也可以根據其readme打包一個aar出來使用。
首先下載下來 webp-android ,然后切換到 webp-android/src/main/jni ,執行 ndk-build
然后等待執行結束,可以在其 /webp-android/src/main/libs 目錄下copy出你需要的so,如果需要其他cpu架構的so,可以自己修改Application.mk文件。
/webp-android/src/main/libs
.
├── armeabi
│ └── libwebp_evme.so
├── armeabi-v7a
│ └── libwebp_evme.so
└── x86
└── libwebp_evme.so
然后將其WebDecoder的輔助類copy到項目中即可,注意保持原有包名。
ok,然后就可以用它提供的decode的方法了:
WebPDecoder.getInstance().decodeWebP(byte[] encoded)
于是,上述以InputStream為webp圖片源的代碼可以改寫為:
# 大致的示例代碼
InputStream is = getAssets().open("weixin.webp");
Bitmap bitmap = null;
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN) {
bitmap = WebPDecoder.getInstance().decodeWebP(streamToBytes(is));
} else {
bitmap = BitmapFactory.decodeStream(is);
}
imageView.setImageBitmap(bitmap);
private static byte[] streamToBytes(InputStream is) {
ByteArrayOutputStream os = new ByteArrayOutputStream(1024);
byte[] buffer = new byte[1024];
int len;
try {
while ((len = is.read(buffer)) >= 0) {
os.write(buffer, 0, len);
}
} catch (java.io.IOException e) {
}
return os.toByteArray();
}</code></pre>
ok,這樣就可以對4.2.1以下的webp圖片進行decode了。
服務端下發的圖片為webp格式,然后app去decode顯示即可。
注: webp-android 這個庫只提供了decode方法,如果需要encode需要自己去添加;建議有時間,看下源碼中提供的方法,自己利用源碼結合ndk相關知識自己做so文件的生成.
(3)應用中的資源文件
除了上述去加載外部圖片的方式以外,還有個使用場景就是將項目中的資源文件直接替換為webp。
簡單的使用:
直接將png轉化為webp,放到res/drawable目錄,我們看看效果

這樣就可以了~~
從目前來看有2個選擇:
- 僅替換不存在局部透明的圖片,如果項目最小版本是4.0,可以不引入so直接使用。
- 全部替換(需要引入so的支持)
第一種,目前來看沒什么好介紹的,換圖即可。
主要看第二種的處理了, webp-android 提供了一種做法是這樣的:
<me.everything.webp.WebPImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
webp:webp_src="@drawable/your_webp_image" />
這樣就可以happy的使用webp了。
但是我一點都不happy,使用webp很多都是已經存在的項目,讓我去使用自定義類還要加屬性,多麻煩,萬一發現坑,我還得一個一個換回去,堅決不干。
所以我們需要一種,可以無縫切換的方式,基本不費力也能還原。
最無縫的方式,就是不動原本的布局文件了,那么如何去動態修改ImageView使其支持Webp呢(4.-)?
其實我們的SDK也有類似的做法,比如對很多View支持了tint屬性,原本是不支持的,忽然就支持了,怎么做到的呢?
就是在根據布局文件中ImageView標簽名稱,創建的時候去做了一些手腳,如果你一臉懵逼,可以先看 Android 探究 LayoutInflater setFactory 。
實際上就是利用 LayoutInflaterFactory 了,有了方案,那么代碼就好寫了:
public class MainActivity extends AppCompatActivity {
private static final int[] LL = new int[]
{ //
android.R.attr.src,//
};
@Override
protected void onCreate(Bundle savedInstanceState) {
if(Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN){
LayoutInflaterCompat.setFactory(LayoutInflater.from(this), new LayoutInflaterFactory() {
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
AppCompatDelegate delegate = getDelegate();
View view = delegate.createView(parent, name, context, attrs);
if (view instanceof ImageView) {
ImageView imageView = (ImageView) view;
TypedArray a = context.obtainStyledAttributes(attrs, LL);
int webpSourceResourceID = a.getResourceId(0, 0);
if (webpSourceResourceID == 0) {
return view;
}
InputStream rawImageStream = getResources().openRawResource(webpSourceResourceID);
byte[] data = streamToBytes(rawImageStream);
final Bitmap webpBitmap = WebPDecoder.getInstance().decodeWebP(data);
imageView.setImageBitmap(webpBitmap);
a.recycle();
}
return view;
}
});
}
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}</code></pre>
一般我們的項目中的Activity都存在一個基類,那么直接在其中添加上述代碼即可。
大致邏輯為:對于4.2以下的版本,我們設置一個 LayoutInflaterFactory ,當創建ImageView的時候,因為AppCompatActivity,ImageView的創建是由上述代碼中的delegate指向的對象完成的,我們通過傳入attrs,取出用戶聲明的src屬性,經過一系列操作轉化為bitmap,最好設置到創建好的ImageView上。
這樣,剩下的我們直接將圖換成webp就好了,如果發現不適合,只需要去掉這個factory設置的代碼即可。
正在我竊喜的時候,忽然發現了一個問題。
就是假設我的資源文件更換并不徹底,還存在部分png的圖,但是png的圖在4.2以下的版本是不需要上述操作的。
- 那么問題來了,如何區分webp和非webp的圖片資源呢?
當然是根據后綴,那么我們現在能獲取的僅僅是圖片的resId,還能拿到文件完整的名稱嗎?
讓人開心的是,可以拿到的。
TypedValue value = new TypedValue();
getResources().getValue(webpSourceResourceID, value, true);
String resname = value.string.toString().substring(13,
value.string.toString().length());
if (resname.endsWith(".webp")) {
// do
}</code></pre>
當然應該也可以通過圖片的header信息來判斷,header判斷這種方式應該會更加精確,具體可以查找下相關代碼。
對了,如果你的基類是FragmentActivity,那就不需要去設置什么LayoutFactory了,直接復寫其onCreateView方法:
onCreateView(View parent, String name, Context context, AttributeSet attrs) {
final View view = super.onCreateView(parent, name, context, attrs);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
if (view instanceof ImageView) {
ImageView imageView = (ImageView) view;
TypedArray a = context.obtainStyledAttributes(attrs, LL);
int webpSourceResourceID = a.getResourceId(0, 0);
if (webpSourceResourceID == 0) {
return view;
}
TypedValue value = new TypedValue();
getResources().getValue(webpSourceResourceID, value, true);
String resname = value.string.toString().substring(13,
value.string.toString().length());
if (resname.endsWith(".webp")) {
InputStream rawImageStream = getResources().openRawResource(webpSourceResourceID);
byte[] data = streamToBytes(rawImageStream);
Bitmap webpBitmap = WebPDecoder.getInstance().decodeWebP(data);
imageView.setImageBitmap(webpBitmap);
}
a.recycle();
}
}
return view;
}</code></pre>
ok,到此應該對于webp都有了一定的認識,也應該大致了解了在Android使用webp的兼容性的問題,以及如何處理。
文章中還有很多細節的地方沒有去處理,后面要踩得坑還有很多,后續還會有一篇博客來寫踩到的坑。
參考
- https://storage.googleapis.com/downloads.webmproject.org/releases/webp/index.html
- https://developers.google.com/speed/webp/docs/api
- https://groups.google.com/a/webmproject.org/forum/#!forum/webp-discuss
- http://isux.tencent.com/introduction-of-webp.html
- http://stackoverflow.com/questions/9403321/android-how-to-retrieve-file-name-and-extension-of-a-resource-by-resource-id/22063704#22063704
- https://developer.android.com/guide/appendix/media-formats.html
- https://github.com/alexey-pelykh/webp-android-backport
- http://hahack.com/wiki/sundries-webp.html#android- 開發中使用-webp
來自:http://blog.csdn.net/lmj623565791/article/details/53240600