1. 程式人生 > >Android Webp 完全解析 快來縮小apk的大小吧

Android Webp 完全解析 快來縮小apk的大小吧

                     

一、概述

最近專案準備嘗試使用webp來縮小包的體積,於是抽空對相關知識進行了調研和學習。

至於什麼是webp,使用webp有什麼好處我就不贅述了,具體可以參考騰訊isux上的這篇文章WebP 探尋之路,大致瞭解下就ok了。

入手大致需要考慮以下幾個問題:

  • 如何將現有的jpeg/png等圖轉化為webp?
  • webp格式的圖片如何使用?
  • 有沒有相容性的問題?

下面就跟著上面3個問題開始進行。

二、jpeg/png到webp的互轉

這個官方提供了相互轉化的工具,以及具體的使用方式,可以參考:

截個圖,可以看到左側的功能列表,包含一系列的功能,encode、decode、view等…

因為有比較詳細的文件,這裡簡單介紹下:

首先下載工具:

下載完成後解壓,然後進入bin目錄:

MacBook-Pro:bin zhanghongyang01$ pwd/Users/zhanghongyang01/hongyang/works/libwebp-0.4.1-mac-10.8 2/binMacBook-Pro:bin zhanghongyang01$ ls -ltotal 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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

大致有4個命令工具,分別用於png等轉換為webp;webp轉化為png;git轉化為webp;檢視webp圖片;最後一個是用於建立webp動畫檔案的。

(1) jpeg、png 轉為webp [cwebp]

cwebp weixin.png -o weixin.webp
  • 1

(2) webp轉為jpeg、png [dwebp]

dwebp weixin.webp -o weixin.png
  • 1

(3) gif 轉化為webp

./gif2webp xingye.gif -o xingye.webp
  • 1

每個命令都有一堆options,可以自己研究下

三、使用

Webp在app中一般可以用於兩個方面

  • 一個是對與服務端互動過程中使用webp圖片
  • 另一個是應用中的資原始檔

(1)與服務端互動使用webp圖片

這種方式非常簡單,因為從Android4.0開始已經對webp圖片進行的支援。

下面我們寫個例子,這裡我準備了一個webp的圖片,我直接放到assets目錄,然後編寫如下程式碼:

# 這是一個完全不透明圖的測試Bitmap bitmap = BitmapFactory.decodeStream(getAssets().open("icon.webp"));imageView.setImageBitmap(bitmap);
  • 1
  • 2
  • 3
  • 4

找了臺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+)
  • 1
  • 2
  • 3

那麼結合文件和實驗,大致可以有如下的結論:

  • 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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

然後將其WebDecoder的輔助類copy到專案中即可,注意保持原有包名。

ok,然後就可以用它提供的decode的方法了:

WebPDecoder.getInstance().decodeWebP(byte[] encoded)
  • 1

於是,上述以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();}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

ok,這樣就可以對4.2.1以下的webp圖片進行decode了。

服務端下發的圖片為webp格式,然後app去decode顯示即可。

注:webp-android這個庫只提供了decode方法,如果需要encode需要自己去新增;建議有時間,看下原始碼中提供的方法,自己利用原始碼結合ndk相關知識自己做so檔案的生成.

(3)應用中的資原始檔

除了上述去載入外部圖片的方式以外,還有個使用場景就是將專案中的資原始檔直接替換為webp。

簡單的使用:

直接將png轉化為webp,放到res/drawable目錄,我們看看效果

這樣就可以了~~

從目前來看有2個選擇:

  1. 僅替換不存在區域性透明的圖片,如果專案最小版本是4.0,可以不引入so直接使用。
  2. 全部替換(需要引入so的支援)

第一種,目前來看沒什麼好介紹的,換圖即可。

主要看第二種的處理了,webp-android提供了一種做法是這樣的:

<me.everything.webp.WebPImageView  android:layout_width="wrap_content"  android:layout_height="wrap_content"  webp:webp_src="@drawable/your_webp_image" />
  • 1
  • 2
  • 3
  • 4

這樣就可以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);    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40

一般我們的專案中的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}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

當然應該也可以通過圖片的header資訊來判斷,header判斷這種方式應該會更加精確,具體可以查詢下相關程式碼。

對了,如果你的基類是FragmentActivity,那就不需要去設定什麼LayoutFactory了,直接複寫其onCreateView方法:

onCreateView(View parent, String name, Context context, AttributeSet attrs) {    final View view = super.onCreateView(parent, name, context, attrs);    if(view == null){        if (name.equals("ImageView")) {            view = new ImageView(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;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

ok,到此應該對於webp都有了一定的認識,也應該大致瞭解了在Android使用webp的相容性的問題,以及如何處理。

文章中還有很多細節的地方沒有去處理,後面要踩得坑還有很多,後續還會有一篇部落格來寫踩到的坑。

如果你也想用webp,歡迎踩坑與交流。

 

群號: 497438697 ,歡迎入群

   

微信公眾號:hongyangAndroid
  (歡迎關注,不要錯過每一篇乾貨,支援投稿)
 

參考