app图片优化-webp格式图片原理及在Android、IOS中的应用

96
天空的守望者
0.7 2017.11.20 17:27 字数 2753

参考:http://www.jianshu.com/p/ff8444fbc773

http://www.jianshu.com/p/555859783f63

http://blog.csdn.net/lmj623565791/article/details/53240600

http://www.jianshu.com/p/ed7562a34af1

http://www.jianshu.com/p/0244e431fb3c

一、app体积优化的几种方式

1.使用TinyPng来优化png格式图片大小,就是使用方法压缩png图片。

2.不包含透明像素的图片,改为JPEG格式,这样可以减少图片的大小。

3.大图拆分为小图,大小也会减少。

4.使用九宫格图片,也就是.9.png的图片,android和ios都适用,对于比较大的图片和使用范围,可是形成无损拉伸效果。

5.使用IconFont,通过字体文件来构建纯色图,不占图片体积,通过加载字库文件,来显示纯色图片。阿里的字库IOS接入第三发库:github上的开源库IconFont和阿里的FontAwesomeKit(这个是个很重要的趋势,以后将特意写篇文章来说IconFont)

6.本章将讲解的webp格式图片。谷歌2011年发布WebP,一直对它做了很多升级和改变,2014年WebP开始支持动图。webp格式是图片压缩更加极致,比常用的jpg png格式小24%-35%,webp格式已经支持大部分浏览器,Android 原生。

二、webp格式的原理

2.1 来源

WebP文件格式来源于VP8视频编解码(你可能更知道WebM)。VP8编解码器的其中一个强大特性是帧内预测压缩,或者说,视频的每一帧都被压缩,后续帧与帧之间的差异也会被压缩。这就是WebP的由来:WebM文件里单个被压缩的帧。更精确的说WebP的核心来则WebM。

2.1 原理分析 —— 有损模式

WebP的有损模式是建立在与一张静止的JPG竞争的基础上,因此,你会注意到在对文件处理上有一些惊人相识。

*宏块(MacroBlocking)

编码器的第一个阶段是将图片分割成不同"宏块"。典型的宏块包括一个16x16的亮度像素块,和两个8x8的色度像素块。这个阶段非常像JPEG格式里转换颜色空间,对色度通道降低采样,以及细分图片。


image

*预测

宏块里每个4x4的子块都有一个预测模型。(又名过滤)。在PNG里过滤用得非常多,它对每一行都做同样的事,而WebP过滤的是每一块。它是这样处理的,在一个块周围定义两组像素:有一行在它上面为A,在它左边那一列为L。


image

利用A和L,编码器会将它们放在一个4x4的测试像素块填满,并确定哪一个生成了最接近原始块的值。这些用不同方法填满的块叫做"预测块"。

Horiz prediction(水平预测)——将块的每列使用左列(L)数据的副本进行填充。

Vertical Prediction(垂直预测)——将块的每行使用上列(A)数据的副本进行填充。

DC Prediction(DC 预测)——将块使用 A 上列的像素与 L 左列的像素的平均值进行填充。

True Motion (TrueMotion 预测)——一种超级先进的模式,我暂时不讲。

值得注意的是,4x4的亮度块还有另外6种模式,但你现在只需知道这些就好;)

image

基本流程是我们找到这个快最佳的预测块,并导出过滤结果(剩余误差),然后送到下个阶段。

*JPGify it

WebP编码的最后阶段看起来非常像我们的老朋友JPG:

对块里剩余的值执行DCT过滤

DCT矩阵后量化

转成量化矩阵后重新排序,然后送到一个静态压缩器里。


image

这有主要有两点不同:

在DCT阶段输入的数据不是原始的数据块本身,而是预测后的数据

WebP用得静态压缩器是算术压缩器,它和JPG用的霍夫曼编码器类似。

从最后的结果看WebP感觉有点像加强版的JPG。WebP的预测阶段相比JPG是最大的优势,它减少了特殊颜色,使得在以后的处理阶段能更有效的压缩图片数据。

WebP只是比JPG所有处理过程多了一个预测模式,在数据压缩方面就把JPG干倒。

三、webp在Android中支持的情况

Webp在app中的使用一般包括两个方面:

1.对与服务端交互过程中使用webp图片

从Android 4.0 开始就对webp图片进行支持,直接使用就可以了。但是要注意:4.2.1+对webp格式的decode、encode是完全支持的(包含半透明的webp图);对于4.0+ 到 4.2.1 ,只支持完全不透明的decode、encode的webp图;4.0 以下,应该是默认不支持webp了。

Android 4.0之前的兼容可以通过官方的源码库(需要手动转为so文件),也可以使用前人封装好的库:webp-android-backport和webp-android。

示例代码:


InputStreamis= getAssets().open("weixin.webp");

Bitmapbitmap=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);privatestaticbyte[]streamToBytes(InputStreamis) {

ByteArrayOutputStream os =newByteArrayOutputStream(1024);byte[] buffer =newbyte[1024];intlen;try{while((len =is.read(buffer)) >=0) {

os.write(buffer,0, len);

}

}catch(java.io.IOException e) {

}returnos.toByteArray();

}

2.应用中的资源文件

4.0以上简单的使用:

直接将png转化为webp,放到res/drawable目录

image

3.在4.0以下支持


public classMainActivityextendsAppCompatActivity {

private static final int[]LL=new int[]

{//

android.R.attr.src,//

};

@Override

protected voidonCreate(Bundle savedInstanceState) {

if(Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN){

LayoutInflaterCompat.setFactory(LayoutInflater.from(this),newLayoutInflaterFactory() {

@Override

publicView onCreateView(View parent, String name, Context context, AttributeSet attrs) {

AppCompatDelegate delegate = getDelegate();

View view = delegate.createView(parent, name, context, attrs);

if(viewinstanceofImageView) {

ImageView imageView = (ImageView) view;

TypedArray a = context.obtainStyledAttributes(attrs,LL);

intwebpSourceResourceID = a.getResourceId(0,0);

if(webpSourceResourceID ==0) {

returnview;

}

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

finalBitmap webpBitmap = WebPDecoder.getInstance().decodeWebP(data);

imageView.setImageBitmap(webpBitmap);

}

a.recycle();

}

returnview;

}

});

}

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

}

}

写一个基类,所有的Activity都继承此基类。大致逻辑为:对于4.2以下的版本,我们设置一个LayoutInflaterFactory,当创建ImageView的时候,因为AppCompatActivity,ImageView的创建是由上述代码中的delegate指向的对象完成的,我们通过传入attrs,取出用户声明的src属性,经过一系列操作转化为bitmap,最好设置到创建好的ImageView上。

四、webp在IOS中支持的情况

这里先说的一点xcode目前无法像Android Studio一样支持webp作为资源,所以IOS目前无法将webp作为资源直接在IDE中支持,只能作为网络图片和H5图片显示在IOS程序中。

1.IOS本地中使用webp图片

本地使用webp图片,可以使用SDWebImage里面有个webP 框架,可以将webp-->NSData-->UIImage最后变为可识别的图片格式直接给控件调用。

但是需要额外需要‘webp’包

$pod 'SDWebImage/WebP'


image

#ifdef SD_WEBP

#import

// Fix for issue #416 Undefined symbols for architecture armv7 since WebP introduction when deploying to device

void WebPInitPremultiplyNEON(void);

void WebPInitUpsamplersNEON(void);

void VP8DspInitNEON(void);

@interface UIImage (WebP)

+ (UIImage *)sd_imageWithWebPData:(NSData *)data;

@end

#endif

2.IOS 网络或webview使用webp格式图片

webView 等网页上如果用的是 webP 图片,iOS 直接解析的话,会显示?图片。

思路 1:

A. 在网页加载出后截取到HTML及内部的JS后,调用JS预先准备好的方法获取需要转码的webP格式图片下载地址(其实一个一个的遍历也行).

B. 在App 本地开启线程下载图片,下载完成后,将图片转码由 webP—> png—>Base64(因为实验出直接用 png/jpg 的话 没用)

C. 将 Base64及原图片下载地址一一对应调用JS准备好的方法进行替换

D. 将下载后的图片进行缓存,并进行管理

注意点:

A. 图片在最终显示成功前会显示成?,此处为了用户体验应该采用占位图片

B. 图片显示成功前应该保持网页布局不调整,需要由 JS 预先设置好布局

C. 图片在本地的缓存需要管理


//在 `webView` 加载完 `HTML` 后,解析源码,执行相应的 `JS` 方法

-(void)webViewDidFinishLoad:(UIWebView *)webView

{

//获取`HTML`代码

NSString *lJs = @"document.documentElement.innerHTML";

NSString *str = [webView stringByEvaluatingJavaScriptFromString:lJs];

//执行约定好的方法,获取需要下载的 webp 图片

NSString *imgs = [self.webView stringByEvaluatingJavaScriptFromString:@"YongChe.getAllWebPImg();"];

NSArray *array = [NSJSONSerialization JSONObjectWithData:[imgs dataUsingEncoding:NSUTF8StringEncoding] options:0 error:nil];

//此处,做示范,只转换第一个,将图片下载下来,并且转为 PNG 后,再转成 Base64,传给 JS 脚本执行

NSString *imgUrl = array.firstObject;

__weak typeof (self) weakSelf = self;

[SDWebImageCustomeDownLoad downloadWithURL:[NSURL URLWithString:imgUrl] progress:nil completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {

NSString *base = [NSString stringWithFormat:@"data:image/png;base64,%@",imgBase];

NSString *js = [NSString stringWithFormat:@"YongChe.replaceWebPImg('%@','%@')",imageURL,base];

[weakSelf.webView stringByEvaluatingJavaScriptFromString:js];

}];

}


+ (void)downloadWithURL:(NSURL *)url progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock

{

//    [self sd_cancelCurrentImageLoad];

//    objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

if (url) {

//        __weak __typeof(self)wself = self;

id operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:0 progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {

//            if (!wself) return;

dispatch_main_sync_safe(^{

//                if (!wself) return;

if (image && completedBlock)

{

completedBlock(image, error, cacheType, url);

return;

}

else if (image) {  //没有回调,但是图片下载完成了

} else {    //image 下载失败

}

if (completedBlock && finished) {

completedBlock(image, error, cacheType, url);

}

});

}];

//这一步,将这个 View 之前的下载操作全部取消,然后将这次的操作放进去

//        [self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];

} else {

dispatch_main_async_safe(^{

NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];

if (completedBlock) {

completedBlock(nil, error, SDImageCacheTypeNone, url);

}

});

}

}

思路2:

采用webP的目的就是减少网络传输数据量,加快请求整体速度,现在的问题就在于webView不识别webP这种格式而已,再想想 iOS内的所有网络请求/响应我们都可以控制,那我们获取到数据时,转化成它识别的给它就可以了,而可以帮我们在拿到数据时进行某些操作,操作完以后又正常进行网络流程的。这就需要用到NSURLProtocol。

NSURLProtocol是 iOS里面的URL Loading System的一部分,但是从它的名字来看,你绝对不会想到它会是一个对象,可是它偏偏是个对象。。。而且还是抽象对象(可是OC里面没有抽象这一说)。平常我们做网络相关的东西基本很少碰它,但是它的功能却强大得要死。

*可以拦截UIWebView,基于系统的NSUIConnection或者NSUISession进行封装的网络请求。

*忽略网络请求,直接返回自定义的Response

*修改request(请求地址,认证信息等等)

*返回数据拦截

*干你想干的。。。

对URL Loading System不清楚的,可以看看下面这张图,看看里面有哪些类:


image

思路很简单,就是拦截请求URL带.png.jpg.gif的请求,首先去缓存里面取,有的话直接返回,没有的去请求,并保存本地。


static NSString * const hasInitKey = @"JYCustomWebViewProtocolKey";

@interface JYCustomWebViewProtocol ()

@property (nonatomic, strong) NSMutableData *responseData;

@property (nonatomic, strong) NSURLConnection *connection;

@end

@implementation JYCustomWebViewProtocol

+ (BOOL)canInitWithRequest:(NSURLRequest *)request

{

if ([request.URL.scheme isEqualToString:@"http"]) {

NSString *str = request.URL.path;

//只处理http请求的图片

if (([str hasSuffix:@".png"] || [str hasSuffix:@".jpg"] || [str hasSuffix:@".gif"])

&& ![NSURLProtocol propertyForKey:hasInitKey inRequest:request]) {

return YES;

}

}

return NO;

}

+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {

NSMutableURLRequest *mutableReqeust = [request mutableCopy];

//这边可用干你想干的事情。。更改地址,提取里面的请求内容,或者设置里面的请求头。。

return mutableReqeust;

}

- (void)startLoading

{

NSMutableURLRequest *mutableReqeust = [[self request] mutableCopy];

//做下标记,防止递归调用

[NSURLProtocol setProperty:@YES forKey:hasInitKey inRequest:mutableReqeust];

//查看本地是否已经缓存了图片

NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];

NSData *data = [[SDImageCache sharedImageCache] diskImageDataBySearchingAllPathsForKey:key];

if (data) {

NSURLResponse *response = [[NSURLResponse alloc] initWithURL:mutableReqeust.URL

MIMEType:[NSData sd_contentTypeForImageData:data]

expectedContentLength:data.length

textEncodingName:nil];

[self.client URLProtocol:self

didReceiveResponse:response

cacheStoragePolicy:NSURLCacheStorageNotAllowed];

[self.client URLProtocol:self didLoadData:data];

[self.client URLProtocolDidFinishLoading:self];

}

else {

self.connection = [NSURLConnection connectionWithRequest:mutableReqeust delegate:self];

}

}

- (void)stopLoading

{

[self.connection cancel];

}

#pragma mark- NSURLConnectionDelegate

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {

[self.client URLProtocol:self didFailWithError:error];

}

#pragma mark - NSURLConnectionDataDelegate

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response

{

self.responseData = [[NSMutableData alloc] init];

[self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];

}

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {

[self.responseData appendData:data];

[self.client URLProtocol:self didLoadData:data];

}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection

{

UIImage *cacheImage = [UIImage sd_imageWithData:self.responseData];

//利用SDWebImage提供的缓存进行保存图片

[[SDImageCache sharedImageCache] storeImage:cacheImage

recalculateFromImage:NO

imageData:self.responseData

forKey:[[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL]

toDisk:YES];

[self.client URLProtocolDidFinishLoading:self];

}

注意点:

每次只能只有一个protocol进行处理,如果有多个自定义protocol,系统将采取你registerClass的倒序进行调用,一旦你需要对这个请求进行处理,那么接下来的所有相关操作都需要这个protocol进行管理。

一定要注意标记请求,不然你会无限的循环下去。。。因为一旦你需要处理这个请求,那么系统会创建你这个protocol的实例,然后你自己又开启了connection进行请求的话,又会触发URL Loading system的回调。系统给我们提供了+ (void)setProperty:(id)value forKey:(NSString *)key inRequest:(NSMutableURLRequest *)request;和+ (id)propertyForKey:(NSString *)key inRequest:(NSURLRequest *)request;这两个方法进行标记和区分。

五、总结

工具:jpeg/png可以转换成为webp格式,官方转换工具:webp转换工具

在网页端已经大规模使用webp格式进行传输、显示了,但是在app端还有很多人在用png和jpg,webp是一个很好的可以减小app大小的图片格式,在Android 端4.0之后才支持作为资源存在,在ios端无法作为资源存在,但是无论Android 和IOS端在webview 和网络途径上有方式来实现webp格式解析,对于网络大图需求多的app,webp格式是很好的,建议所有Android app换webp格式,IOS app看需求使用网络显示webp格式。

移动端合集