13

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

 3 years ago
source link: https://blog.csdn.net/lmj623565791/article/details/53240600
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

本文在我的微信公众号:鸿洋(hongyangAndroid)首发。

转载请标明出处:
http://blog.csdn.net/lmj623565791/article/details/53240600
本文出自:【张鸿洋的博客】

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

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

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

  • 如何将现有的jpeg/png等图转化为webp?
  • webp格式的图片如何使用?
  • 有没有兼容性的问题?

下面就跟着上面3个问题开始进行。

二、jpeg/png到webp的互转

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

20161120135316651

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

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

首先下载工具:

我这里下载的是对应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
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到项目中即可,注意保持原有包名。

20161120135434887

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目录,我们看看效果

20161120135714001

这样就可以了~~

从目前来看有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" />

这样就可以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,欢迎踩坑与交流。


欢迎关注我的微博:
http://weibo.com/u/3165018720


群号: 497438697 ,欢迎入群

微信公众号:hongyangAndroid
(欢迎关注,不要错过每一篇干货,支持投稿)
1422600516_2905.jpg


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK