Android Webp 完全解析 缩小apk的大小

转载鸿洋大神: http://blog.csdn.net/lmj623565791/article/details/53240600

一、概述

最近项目准备尝试使用webp来缩小包的体积,于是抽空对相关知识进行了调研和学习。

至于什么是webp,使用webp有什么好处我就不赘述了,具体可以参考腾讯isux上的这篇文章WebP 探寻之路,大致了解下就ok了。

入手大致需要考虑以下几个问题:

如何将现有的jpeg/png等图转化为webp?
webp格式的图片如何使用?
有没有兼容性的问题?
下面就跟着上面3个问题开始进行。

二、jpeg/png到webp的互转

这个官方提供了相互转化的工具,以及具体的使用方式,可以参考:

https://developers.google.com/speed/webp/docs/cwebp
Paste_Image.png

截个图,可以看到左侧的功能列表,包含一系列的功能,encode、decode、view等…

因为有比较详细的文档,这里简单介绍下:

首先下载工具:

webp download page

我这里下载的是对应mac os的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
 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支持的源码了,点击下载:
libwebp-0.5.1.tar.gz

如果你的ndk的知识足够的话,可以自己利用源码,去生成so文件使用。
当然了,你也可以使用前人已经封装好的库:
webp-android-backport
webp-android

我们这里选择使用第二个库,这里选择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到项目中即可,注意保持原有包名。

Paste_Image.png

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();
 }

ok,这样就可以对4.2.1以下的webp图片进行decode了。
服务端下发的图片为webp格式,然后app去decode显示即可。
注:webp-android这个库只提供了decode方法,如果需要encode需要自己去添加;建议有时间,看下源码中提供的方法,自己利用源码结合ndk相关知识自己做so文件的生成.
(3)应用中的资源文件
除了上述去加载外部图片的方式以外,还有个使用场景就是将项目中的资源文件直接替换为webp。
简单的使用:
直接将png转化为webp,放到res/drawable目录,我们看看效果

![Upload Paste_Image.png failed. Please try again.]

这样就可以了~~
从目前来看有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); 
           }      
}

一般我们的项目中的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}

当然应该也可以通过图片的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;
}

ok,到此应该对于webp都有了一定的认识,也应该大致了解了在Android使用webp的兼容性的问题,以及如何处理。

文章中还有很多细节的地方没有去处理,后面要踩得坑还有很多,后续还会有一篇博客来写踩到的坑。

如果你也想用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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容