iOS 里使用WebP(含 WebP 介绍)
前言
webp格式
,谷歌开发的一种旨在加快图片加载速度的图片格式。图片压缩体积大约只有JPEG的2/3,并能节省大量的服务器宽带资源和数据空间。是一种同时提供了有损压缩与无损压缩(可逆压缩)的图片档案格式,衍生自影像编码格式VP8,被认为是WebM多媒体格式的姊妹项目,是由Google以BSD授权条款释出。VP8编解码器的其中一个强大特性是帧内预测压缩,或者说,视频的每一帧都被压缩,后续帧与帧之间的差异也会被压缩。这就是WebP的由来:WebM文件里单个被压缩的帧。更精确的说WebP的核心来则WebM。
webp
最初在2010年释出,目标是减少档案大小,达到和JPEG格式相同的图片品质,希望能够减少图片档在网路上的传送时间。
原理
一、有损压缩
1.宏块(MacroBlocking)
图片编码第一个阶段的任务是把图片分割成不同的宏块
。常见的就是包括一个‘16×16’的亮度像素块和两个‘8×8’的色度像素块,如下图:
亮度像素块(luma)工作原理:指的是像素抠像,把一幅含有RGB通道的图像转换为单通道的黑白图像,这幅黑白图像就代表着这幅图像的亮部和暗部区域。
色度像素块(chroma)工作原理:指的是RGB空间,就是把 R、G、B 放到数学里面的立体坐标系上,然后可以把对应的RGB 表示的图像放到这个坐标系上,就可以通过这个坐标系来观察和分析整幅图像的红绿蓝颜色分布了,因为引入了立体坐标系,所以可以使用研究立体几何的方法来研究和处理图像了。
2.预测
宏块里每个4x4的子块都有一个预测模型。(又名过滤)。在PNG里过滤用得非常多,它对每一行都做同样的事,而WebP过滤的是每一块。它是这样处理的,在一个块周围定义两组像素:有一行在它上面为A,在它左边那一列为L。如图: 预测模型利用 A 和 L,编码器会将他们放在一个4x4的测试像素块填满,并确定哪一个生成了最接近原始块的值。这些用不同方法填满的块叫做"预测块"。
预测方式
WebP
编码器四种帧内预测模式:
(1)Horiz prediction
(水平预测):将块的每列使用左列L数据的副本进行填充。
(2)Vertical Prediction
(垂直预测):将块的每行使用上列A数据的副本进行填充。
(3)DC Prediction
(DC 预测):将块使用 A 上列的像素与 L 左列的像素的平均值作为宏块唯一的值进行填充。
(4)True Motion
(TrueMotion 预测):除了行 A 和列 L 之外,用宏块上方和左侧的像素P、A(从P开始)中像素块之间的水平差异以列 L 为基准拓展每一行。
3.处理 DCT
当图片处理到此处时,还剩下小的残差,通过 FDCT (正向离散余弦变换),让变换后的数据低频部分分布在数据块左上方,而高频部分集中于右下方实现更高效的压缩。在DCT阶段输入的数据不是原始的数据块本身,而是预测后的数据。WebP的预测阶段相比JPG是最大的优势,它减少了特殊颜色,使得在以后的处理阶段能更有效的压缩图片数据。WebP只是比JPG所有处理过程多了一个预测模式,在数据压缩方面就比JPG优秀很多。
4.有损压缩图片与 jpg 体积比较
webp有损压缩与jpg比较总结:当WebP
将 JPG
压缩到相当于原图 90% 质量时,图片体积减少了 50% 左右。当WebP
将 JPG
压缩到相当于原图 80% 质量时,图片体积则减少了 60%~80%。
有损 WebP
压缩性能优于JPG
的原因主要是其预测编码技术先进,并且宏块自适应量化也带来了压缩效率的提升。
二、无损压缩
WebP
无损压缩采用了预测变换、颜色变换、减去绿色变换、彩色缓存编码、LZ77 反向参考等不同技术来处理图像,之后对变换图像数据和参数进行熵编码。
1.预测变换
预测空间变换
通过利用相邻像素的数据相关性减少熵[shāng]
。在预测变换中,对已解码的像素预测当前像素值,并且仅对差值(实际预测)进行编码。预测变换有 13 种不同的模式,使用较多的是左、上、左上以及右上的像素预测模式,其余为左、上、左上和右上组合的平均值预测模式。
2.颜色变换
借助颜色变换去除每个像素的 R,G 和 B 值。彩色变换时保持绿色(G)值原样,根据绿色(G)值变换红色(R)值,再根据绿色值转换蓝色(B)值,最后根据红色(R)值进行转换。
如果与预测变换的情况一样,就需要将图像划分为宏块,并且对于宏块中的所有像素使用相同的变换模式。变换模式分为 3 种:green_to_red,green_to_blue和red_to_blue。
3.减去绿色变换
减去绿色变换
从每个像素的红色、蓝色值中减去绿色值。当此变换存在时,解码器需要将绿色值添加到红色和蓝色。
4.彩色缓存编码
无损 WebP 压缩使用已经看到的图像片段来重构新的像素。如果没有找到对应的匹配值,可以使用本地调色板,同时本地调色板也会不断更新最近使用的颜色。
5.无损压缩图片与PNG体积比较
无损webp与PNG对比总结:WebP 无损对 PNG 图片的优化到达了 20%~40% 。
三、WebP 与主流的图片格式功能对比
WebP 与主流的图片格式功能对比四、iOS 里面原生控件对 webp 的支持
1.SDWebImage 可以直接支持
$ pod 'SDWebImage/WebP'
2.遇到的问题及解决办法
问题:
查看SDWebImage
的 .podspec
pod 配置文件,可知道 SDWebImage
支持 webp
格式图片是依赖于其 Webp
子库,而子库进一步依赖于 libwebp
库。libwebp 是来自谷歌的被墙的,源地址为 https://chromium.googlesource.com/webm/libwebp
解决办法:
添加终端翻墙:
~ git config --global http.proxy 'socks5://127.0.0.1:1086' `socks5://127.0.0.1:1086要查看你翻墙软件里面的配置`
~ git config --global https.proxy 'socks5://127.0.0.1:1086'
然后 pod install
另外提供删除终端翻墙
~ git config --global --unset http.proxy
~ git config --global --unset https.proxy
3.原生控件使用
(1)使用方式
直接使用 SDWebImage
的sd_setImageWithURL
系列方法即可加载 webp 图片。
(2)原理
pod之后看SDWebImageCodersManager
和SDWebImageWebPCoder
SDWebImageCodersManager
类里面
初始化:
- (instancetype)init {
if (self = [super init]) {
// initialize with default coders
NSMutableArray<id<SDWebImageCoder>> *mutableCoders = [@[[SDWebImageImageIOCoder sharedCoder]] mutableCopy];
#ifdef SD_WEBP
[mutableCoders addObject:[SDWebImageWebPCoder sharedCoder]];
#endif
_coders = [mutableCoders copy];
_codersLock = dispatch_semaphore_create(1);
}
return self;
}
注:在`SDWebImageCodersManager`初始化的地方,判断如果当前环境支持 webp就会加上`[SDWebImageWebPCoder sharedCoder]`
这个`SDWebImageWebPCoder `是内置的加载 webp 图片或者webp动画的编码器
把 data 解码成 image
- (UIImage *)decodedImageWithData:(NSData *)data {
LOCK(self.codersLock);
NSArray<id<SDWebImageCoder>> *coders = self.coders;
UNLOCK(self.codersLock);
for (id<SDWebImageCoder> coder in coders.reverseObjectEnumerator) {
if ([coder canDecodeFromData:data]) {
return [coder decodedImageWithData:data];
}
}
return nil;
}
注:判断如果 webp 的 coder 可以解码此 data 的话就进入 code 里面进行解码
SDWebImageWebPCoder
类里面
解码成 image
- (UIImage *)decodedImageWithData:(NSData *)data {
if (!data) {
return nil;
}
WebPData webpData;
WebPDataInit(&webpData);//初始化一个有缺省值的 webp_data
webpData.bytes = data.bytes;
webpData.size = data.length;
WebPDemuxer *demuxer = WebPDemux(&webpData);//解析“data”给出的完整WebP文件,成功解析后返回WebPDemuxer对象,否则返回NULL。WebPDemuxer包含图的宽高,colorSpace(解码之后图片的像素格式)等属性
if (!demuxer) {
return nil;
}
uint32_t flags = WebPDemuxGetI(demuxer, WEBP_FF_FORMAT_FLAGS);//获取 webpFeatureFlags 的位操作组合
CGColorSpaceRef colorSpace = [self sd_colorSpaceWithDemuxer:demuxer];//获取解码之后图片的像素格式
if (!(flags & ANIMATION_FLAG)) {
// for static single webp image
UIImage *staticImage = [self sd_rawWebpImageWithData:webpData colorSpace:colorSpace];
WebPDemuxDelete(demuxer);
CGColorSpaceRelease(colorSpace);
staticImage.sd_imageFormat = SDImageFormatWebP;
return staticImage;
}
int loopCount = WebPDemuxGetI(demuxer, WEBP_FF_LOOP_COUNT);//WEBP_FF_LOOP_COUNT ->用于动画文件
int canvasWidth = WebPDemuxGetI(demuxer, WEBP_FF_CANVAS_WIDTH);
int canvasHeight = WebPDemuxGetI(demuxer, WEBP_FF_CANVAS_HEIGHT);
CGBitmapInfo bitmapInfo;//指定bitmap是否包含alpha通道,像素中alpha通道的相对位置,像素组件是整形还是浮点型等信息的字符串。
// `CGBitmapContextCreate` does not support RGB888 on iOS. Where `CGImageCreate` supports.
if (!(flags & ALPHA_FLAG)) {
// RGBX8888
bitmapInfo = kCGBitmapByteOrder32Big | kCGImageAlphaNoneSkipLast;
} else {
// RGBA8888
bitmapInfo = kCGBitmapByteOrder32Big | kCGImageAlphaPremultipliedLast;
}
CGContextRef canvas = CGBitmapContextCreate(NULL, canvasWidth, canvasHeight, 8, 0, SDCGColorSpaceGetDeviceRGB(), bitmapInfo);//绘制图片上下文,第一个参数创建BitmapContext的内存空间,第二个图片的宽度,第三个图片的高度,第四个内存中像素的每个组件的位数.例如,对于32位像素格式和RGB 颜色空间,应该将这个值设为8.,第五个是每一行在内存所占的比特数,第六个是colorSpace
if (!canvas) {
WebPDemuxDelete(demuxer);
CGColorSpaceRelease(colorSpace);
return nil;
}
// for animated webp image
WebPIterator iter;
if (!WebPDemuxGetFrame(demuxer, 1, &iter)) {
WebPDemuxReleaseIterator(&iter);
WebPDemuxDelete(demuxer);
CGContextRelease(canvas);
CGColorSpaceRelease(colorSpace);
return nil;
}
NSMutableArray<SDWebImageFrame *> *frames = [NSMutableArray array];
do {
@autoreleasepool {
UIImage *image = [self sd_drawnWebpImageWithCanvas:canvas iterator:iter colorSpace:colorSpace];
if (!image) {
continue;
}
int duration = iter.duration;
if (duration <= 10) {
// WebP standard says 0 duration is used for canvas updating but not showing image, but actually Chrome and other implementations set it to 100ms if duration is lower or equal than 10ms
// Some animated WebP images also created without duration, we should keep compatibility
duration = 100;
}
SDWebImageFrame *frame = [SDWebImageFrame frameWithImage:image duration:duration / 1000.f];
[frames addObject:frame];
}
} while (WebPDemuxNextFrame(&iter));
WebPDemuxReleaseIterator(&iter);
WebPDemuxDelete(demuxer);
CGContextRelease(canvas);
CGColorSpaceRelease(colorSpace);
UIImage *animatedImage = [SDWebImageCoderHelper animatedImageWithFrames:frames];
animatedImage.sd_imageLoopCount = loopCount;
animatedImage.sd_imageFormat = SDImageFormatWebP;
return animatedImage;
}
注:整个解码功能的实现是依赖于 libwebp 库。
4.WebView 使用
在以 Native 方式开发的 App 中也会大量使用的 UIWebView 来展示一些简单页面,然而 Safari 及 UIWebView 当前并不支持 WebP 格式。若是想在 UIWebView 中也把图片显示出来,一个解决思路就是:拦截替换。拦截 WebP 图片然后转换为 jpg 或者 png 再交给 UIWebView 进行渲染和展示。
(1)使用方式:
使用MagicWebViewWebP.framework导入到项目中。
引用头文件:#import <MagicWebViewWebP/MagicWebViewWebPManager.h>
在 webview 加载之前,注册MagicURLProtocol
_web = [[UIWebView alloc]initWithFrame:CGRectMake(0, 200, ScreenWidth, 300)];
[self.view addSubview:_web];
[[MagicWebViewWebPManager shareManager] registerMagicURLProtocolWebView:_web];
NSURLRequest *req = [NSURLRequest requestWithURL:[NSURLURLWithString:@"http://isparta.github.io/compare-webp/index.html#12345"]];
[_web loadRequest:req];
dealloc中销毁MagicURLProtocol
// 销毁
-(void)dealloc{
[[MagicWebViewWebPManager shareManager] unregisterMagicURLProtocolWebView: _web];
}
注:若有特殊需求,需要在web页面退出时销毁MagicURLProtocol,否则会拦截整个app的网络请求。
(2)原理:拦截替换
方法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 *imgBase = [UIImagePNGRepresentation(image) base64EncodedStringWithOptions:0];
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 {
if (url) {
id operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:0 progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
dispatch_main_sync_safe(^{
if (image && completedBlock){
completedBlock(image, error, cacheType, url);
return;
}else if (image) { //没有回调,但是图片下载完成了
} else { //image 下载失败
}
if (completedBlock && finished) {
completedBlock(image, error, cacheType, url);
}
});
}];
//这一步,将这个 View 之前的下载操作全部取消,然后将这次的操作放进去
} 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
(推荐):
NSURLProtocol
可以拦截的网络请求包括NSURLSession,NSURLConnection以及UIWebVIew。
步骤:
注册—>拦截—>转发—>回调—>结束
参考MagicWebViewWebP.framework的实现方式
注:NSURLProtocol 的全局性质,影响范围大,这种方式存在潜在的风险,需要严格的过滤和限制 WebP 请求的拦截。NSURLProtocol 作用的叠加性质,也无法保证与其它第三方代码的兼容。每次只能只有一个protocol进行处理,如果有多个自定义protocol,系统将采取你registerClass的倒序进行调用,一旦你需要对这个请求进行处理,那么接下来的所有相关操作都需要这个protocol进行管理。
参考文章
app图片优化-webp格式图片原理及在Android、IOS中的应用
MagicWebViewWebP
WebP 极限压缩及ios实现
都说 WebP 厉害,究竟厉害在哪里?