[原]Android Webp 完全解析 快来缩小apk的大小吧

RobertoChri 4年前
   <h2>一、概述</h2>    <p>最近项目准备尝试使用webp来缩小包的体积,于是抽空对相关知识进行了调研和学习。</p>    <p>至于什么是webp,使用webp有什么好处我就不赘述了,具体可以参考腾讯isux上的这篇文章 <a href="/misc/goto?guid=4959646000448600321" rel="nofollow,noindex">WebP 探寻之路</a> ,大致了解下就ok了。</p>    <p>入手大致需要考虑以下几个问题:</p>    <ul>     <li>如何将现有的jpeg/png等图转化为webp?</li>     <li>webp格式的图片如何使用?</li>     <li>有没有兼容性的问题?</li>    </ul>    <p>下面就跟着上面3个问题开始进行。</p>    <h2>二、jpeg/png到webp的互转</h2>    <p>这个官方提供了相互转化的工具,以及具体的使用方式,可以参考:</p>    <ul>     <li><a href="/misc/goto?guid=4958977584956900839" rel="nofollow,noindex">https://developers.google.com/speed/webp/docs/cwebp</a></li>    </ul>    <p><img src="https://simg.open-open.com/show/a87a53f1a3edbb082d73b42f8ee17638.jpg"></p>    <p>截个图,可以看到左侧的功能列表,包含一系列的功能,encode、decode、view等…</p>    <p>因为有比较详细的文档,这里简单介绍下:</p>    <p>首先下载工具:</p>    <ul>     <li><a href="/misc/goto?guid=4959725767430833884" rel="nofollow,noindex">webp download page</a></li>    </ul>    <p>我这里下载的是对应mac os的 <a href="/misc/goto?guid=4959725767517660646" rel="nofollow,noindex"> </a></p>    <p><a href="/misc/goto?guid=4959725767517660646" rel="nofollow,noindex">libwebp-0.4.1-mac-10.8-2.tar.gz</a></p>    <p> </p>    <p>下载完成后解压,然后进入bin目录:</p>    <pre>  <code class="language-java">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</code></pre>    <p>大致有4个命令工具,分别用于png等转换为webp;webp转化为png;git转化为webp;查看webp图片;最后一个是用于创建webp动画文件的。</p>    <h3>(1) jpeg、png 转为webp [cwebp]</h3>    <pre>  <code class="language-java">cwebp weixin.png -o weixin.webp</code></pre>    <h3>(2) webp转为jpeg、png [dwebp]</h3>    <pre>  <code class="language-java">dwebp weixin.webp -o weixin.png</code></pre>    <h3>(3) gif 转化为webp</h3>    <pre>  <code class="language-java">./gif2webp xingye.gif -o xingye.webp</code></pre>    <p>每个命令都有一堆options,可以自己研究下</p>    <h2>三、使用</h2>    <p>Webp在app中一般可以用于两个方面</p>    <ul>     <li>一个是对与服务端交互过程中使用webp图片</li>     <li>另一个是应用中的资源文件</li>    </ul>    <h3>(1)与服务端交互使用webp图片</h3>    <p>这种方式非常简单,因为从Android4.0开始已经对webp图片进行的支持。</p>    <p>下面我们写个例子,这里我准备了一个webp的图片,我直接放到assets目录,然后编写如下代码:</p>    <pre>  <code class="language-java"># 这是一个完全不透明图的测试  Bitmap bitmap = BitmapFactory.decodeStream(getAssets().open("icon.webp"));  imageView.setImageBitmap(bitmap);</code></pre>    <p>找了台4.0.4(API15)的三星手机(ps:实在是找不到4.0的手机了),运行感觉还不错哟~</p>    <p>正在窃喜的时候,我又换了张图片,因为有些时候我们的图部分区域是透明了,于是我找了张图片,转化为webp,按照上述的代码,同样的操作,运行完成后,发现, <strong>整个图都显示不出来了</strong> 。</p>    <p>赶紧找了个4.2.2(API17)的手机,显示正常。</p>    <p>于是看一眼文档:</p>    <p>文档上对webp decode和encode的支持,是这样写的:</p>    <pre>  <code class="language-java">decode / encode  (Android 4.0+)  (Lossless, Transparency, Android 4.2.1+)</code></pre>    <p><a href="/misc/goto?guid=4959725767612065436" rel="nofollow,noindex">https://developer.android.com/guide/appendix/media-formats.html</a></p>    <p>那么结合文档和实验,大致可以有如下的结论:</p>    <ul>     <li>4.2.1+ 对于webp的decode、encode是完全支持的(包含半透明的webp图)</li>     <li>对于4.0+ 到 4.2.1 ,只支持完全不透明的decode、encode的webp图</li>     <li>4.0 以下,应该是默认不支持webp了</li>    </ul>    <p>看到这个结论,那么就是大家的产品最低的支持版本了。</p>    <p>4.2.1起步的话,目前来看,我是不能接受的,所以只有引入so来做低版本兼容了。</p>    <h3>(2)兼容so的获取</h3>    <p>好在官方已经提供了相关webp支持的源码了,点击下载:</p>    <ul>     <li><a href="/misc/goto?guid=4959725767687538006" rel="nofollow,noindex">libwebp-0.5.1.tar.gz</a></li>    </ul>    <p>如果你的ndk的知识足够的话,可以自己利用源码,去生成so文件使用。</p>    <p>当然了,你也可以使用前人已经封装好的库:</p>    <ul>     <li><a href="/misc/goto?guid=4958971087868336402" rel="nofollow,noindex">webp-android-backport</a></li>     <li><a href="/misc/goto?guid=4959725767807044361" rel="nofollow,noindex">webp-android</a></li>    </ul>    <p>我们这里选择使用第二个库,这里选择copy它生成的so文件以及辅助类到项目中,你也可以根据其readme打包一个aar出来使用。</p>    <p>首先下载下来 <a href="/misc/goto?guid=4959725767807044361" rel="nofollow,noindex">webp-android</a> ,然后切换到 webp-android/src/main/jni ,执行 ndk-build</p>    <p>然后等待执行结束,可以在其 /webp-android/src/main/libs 目录下copy出你需要的so,如果需要其他cpu架构的so,可以自己修改Application.mk文件。</p>    <pre>  <code class="language-java">/webp-android/src/main/libs  .  ├── armeabi  │   └── libwebp_evme.so  ├── armeabi-v7a  │   └── libwebp_evme.so  └── x86      └── libwebp_evme.so</code></pre>    <p>然后将其WebDecoder的辅助类copy到项目中即可,注意保持原有包名。</p>    <p><img src="https://simg.open-open.com/show/f62dce768a8c6172e9411180eeb2cff0.jpg"></p>    <p>ok,然后就可以用它提供的decode的方法了:</p>    <pre>  <code class="language-java">WebPDecoder.getInstance().decodeWebP(byte[] encoded)</code></pre>    <p>于是,上述以InputStream为webp图片源的代码可以改写为:</p>    <pre>  <code class="language-java"># 大致的示例代码  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>    <p>ok,这样就可以对4.2.1以下的webp图片进行decode了。</p>    <p>服务端下发的图片为webp格式,然后app去decode显示即可。</p>    <p>注: <a href="/misc/goto?guid=4959725767807044361" rel="nofollow,noindex">webp-android</a> 这个库只提供了decode方法,如果需要encode需要自己去添加;建议有时间,看下源码中提供的方法,自己利用源码结合ndk相关知识自己做so文件的生成.</p>    <h3>(3)应用中的资源文件</h3>    <p>除了上述去加载外部图片的方式以外,还有个使用场景就是将项目中的资源文件直接替换为webp。</p>    <p>简单的使用:</p>    <p>直接将png转化为webp,放到res/drawable目录,我们看看效果</p>    <p><img src="https://simg.open-open.com/show/93687b5909f24a21422dcbd6d1c57149.jpg"></p>    <p>这样就可以了~~</p>    <p>从目前来看有2个选择:</p>    <ol>     <li>仅替换不存在局部透明的图片,如果项目最小版本是4.0,可以不引入so直接使用。</li>     <li>全部替换(需要引入so的支持)</li>    </ol>    <p>第一种,目前来看没什么好介绍的,换图即可。</p>    <p>主要看第二种的处理了, <a href="/misc/goto?guid=4959725767807044361" rel="nofollow,noindex">webp-android</a> 提供了一种做法是这样的:</p>    <pre>  <code class="language-java"><me.everything.webp.WebPImageView    android:layout_width="wrap_content"    android:layout_height="wrap_content"    webp:webp_src="@drawable/your_webp_image" /></code></pre>    <p>这样就可以happy的使用webp了。</p>    <p>但是我一点都不happy,使用webp很多都是已经存在的项目,让我去使用自定义类还要加属性,多麻烦,万一发现坑,我还得一个一个换回去,坚决不干。</p>    <p>所以我们需要一种,可以无缝切换的方式,基本不费力也能还原。</p>    <p>最无缝的方式,就是不动原本的布局文件了,那么如何去动态修改ImageView使其支持Webp呢(4.-)?</p>    <p>其实我们的SDK也有类似的做法,比如对很多View支持了tint属性,原本是不支持的,忽然就支持了,怎么做到的呢?</p>    <p>就是在根据布局文件中ImageView标签名称,创建的时候去做了一些手脚,如果你一脸懵逼,可以先看 <a href="/misc/goto?guid=4959725767905472023" rel="nofollow,noindex">Android 探究 LayoutInflater setFactory</a> 。</p>    <p>实际上就是利用 LayoutInflaterFactory 了,有了方案,那么代码就好写了:</p>    <pre>  <code class="language-java">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>    <p>一般我们的项目中的Activity都存在一个基类,那么直接在其中添加上述代码即可。</p>    <p>大致逻辑为:对于4.2以下的版本,我们设置一个 LayoutInflaterFactory ,当创建ImageView的时候,因为AppCompatActivity,ImageView的创建是由上述代码中的delegate指向的对象完成的,我们通过传入attrs,取出用户声明的src属性,经过一系列操作转化为bitmap,最好设置到创建好的ImageView上。</p>    <p>这样,剩下的我们直接将图换成webp就好了,如果发现不适合,只需要去掉这个factory设置的代码即可。</p>    <p>正在我窃喜的时候,忽然发现了一个问题。</p>    <p>就是假设我的资源文件更换并不彻底,还存在部分png的图,但是png的图在4.2以下的版本是不需要上述操作的。</p>    <ul>     <li>那么问题来了,如何区分webp和非webp的图片资源呢?</li>    </ul>    <p>当然是根据后缀,那么我们现在能获取的仅仅是图片的resId,还能拿到文件完整的名称吗?</p>    <p>让人开心的是,可以拿到的。</p>    <pre>  <code class="language-java">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>    <p>当然应该也可以通过图片的header信息来判断,header判断这种方式应该会更加精确,具体可以查找下相关代码。</p>    <p>对了,如果你的基类是FragmentActivity,那就不需要去设置什么LayoutFactory了,直接复写其onCreateView方法:</p>    <pre>  <code class="language-java">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>    <p>ok,到此应该对于webp都有了一定的认识,也应该大致了解了在Android使用webp的兼容性的问题,以及如何处理。</p>    <p>文章中还有很多细节的地方没有去处理,后面要踩得坑还有很多,后续还会有一篇博客来写踩到的坑。</p>    <p> </p>    <h2>参考</h2>    <ul>     <li><a href="/misc/goto?guid=4959725767430833884" rel="nofollow,noindex">https://storage.googleapis.com/downloads.webmproject.org/releases/webp/index.html</a></li>     <li><a href="/misc/goto?guid=4959725767999108930" rel="nofollow,noindex">https://developers.google.com/speed/webp/docs/api</a></li>     <li><a href="/misc/goto?guid=4959725768078546821" rel="nofollow,noindex">https://groups.google.com/a/webmproject.org/forum/#!forum/webp-discuss</a></li>     <li><a href="/misc/goto?guid=4959646000448600321" rel="nofollow,noindex">http://isux.tencent.com/introduction-of-webp.html</a></li>     <li><a href="/misc/goto?guid=4959725768170771198" rel="nofollow,noindex">http://stackoverflow.com/questions/9403321/android-how-to-retrieve-file-name-and-extension-of-a-resource-by-resource-id/22063704#22063704</a></li>     <li><a href="/misc/goto?guid=4959725767612065436" rel="nofollow,noindex">https://developer.android.com/guide/appendix/media-formats.html</a></li>     <li><a href="/misc/goto?guid=4958971087868336402" rel="nofollow,noindex">https://github.com/alexey-pelykh/webp-android-backport</a></li>     <li><a href="/misc/goto?guid=4959725768273540631" rel="nofollow,noindex">http://hahack.com/wiki/sundries-webp.html#android-</a> 开发中使用-webp</li>    </ul>    <p> </p>    <p>来自:http://blog.csdn.net/lmj623565791/article/details/53240600</p>    <p> </p>