前言
事情是这样的,之前研究在iOS上进行webp编码并把研究的细节写了一篇文章。上篇文章有提及iOS上进行webp编码时会占用大量的cpu资源(若图片像素过大,可以飙到90%以上),由于实际项目当中有部分逻辑需要依赖大量的计算,在多线程同时进行webp编码时可能会严重损耗手机性能。针对cpu占用这件事专门作了一些研究,因为最终没有很好地解决方案,所以本文只是对于该问题的简单记录,并没有太多实际性的干货。由于webp是Google推出,且在Android底层已经很好地支持。本文探讨的终点会落在对比Android的实现上。
测试两端webp编码的cpu占用情况
我选用一张2400 × 1350的壁纸,两端各写一个webp编码的简单demo,界面上只有一个按钮,点击触发webp编码。
测试的图片如下(鬼灭之刃😜 ):
Android部分
- demo代码:
- 编码一次的cpu占用情况:
用上述profiler信息可知,在编码过程中app对于手机cpu的占用率只有17%左右,而该过程主要消耗在WebpEncode(),这也是前文提到的webp编码时最终调用的方法。这里的SkEncodeImage也验证了Android的webp编码底层是通过skia封装的。具体源码地址(Android api 28):SkWebpEncoder
iOS部分
- demo代码:
- 编码一次的cpu占用情况:
代码中使用的是前文提到的,通过参考SDWebImage的webp编码代码封装的,同样是quality为90的编码,基本于Android底层的webp编码配置相同。Instruments监控显示,在执行编码的时候cpu能飙升到90以上,而主要消耗也是在WebpEncode()上。
提出假设
针对以上的测试结果,我提出了几个可能的假设。
- libwebp编码中的WebPConfig配置是否因为有部分不同导致差异?
- Android的webp编码是使用skia封装的,是否因为skia的原因导致差异?
- Android的webp编码是否有用到gpu? 带着以上的疑惑又进行了相关的验证。
关于WebPConfig的配置对齐
再一次通过上述提到的SkWebpEncoder.cpp中可以得知,在编码前确实对WebPConfig有修改。
关于WebPConfig的初始化是位于libwebp中的config_enc.c的int WebPConfigInitInternal(WebPConfig* config,
WebPPreset preset, float quality, int version)函数,以下是部分代码,项目源码。
结合配置的初始化可知,lossless即是否为无损编码,默认是0即有损。而Android的api在默认情况下都是有损的。故只需要看if里面的代码块即可。 对于method这个参数,默认是4,我的理解是,编码效果和效率的权衡值,它代表的是编码过程中是否需要经过某些方法处理,来保证编码效果。以下是libwebp对于method的解释,标明了参数是多少时会经历什么处理,项目源码。由于本人对于相关的图像编码处理只停留在应用层级别,所以无法很好地分析具体其中的差异性。如果有这方面知识的朋友可以留言一下。
在上述的Skia源码中还有一个宏定义SK_WEBP_ENCODER_USE_DEFAULT_METHOD控制method是否为3。由于无法得知其是否为true,故我在自己的iOS代码中直接将method设置为3进行测试。然而测试的效果与默认的4差别不大。
Skia
关于Skia及其集成的图像编码逻辑
关于Skia,网络上普遍地介绍是“Skia 是一个 Google 开源的 C++ 二维图形库,提供各种常用的API,并可在多种软硬件平台上运行。”。我对于Skia的理解是它更像是一个适配平台,将不同的第三库适配一套统一的接口api以供外部使用。 以图像编码为例,以下是Android Bitmap的compress方法对应的native方法调用的encode方法SkImageEncoder.cpp(Android api 28)
bool SkEncodeImage(SkWStream* dst, const SkPixmap& src,
SkEncodedImageFormat format, int quality) {
#ifdef SK_USE_CG_ENCODER
(void)quality;
return SkEncodeImageWithCG(dst, src, format);
#elif SK_USE_WIC_ENCODER
return SkEncodeImageWithWIC(dst, src, format, quality);
#else
switch(format) {
case SkEncodedImageFormat::kJPEG: {
SkJpegEncoder::Options opts;
opts.fQuality = quality;
opts.fBlendBehavior = SkTransferFunctionBehavior::kIgnore;
return SkJpegEncoder::Encode(dst, src, opts);
}
case SkEncodedImageFormat::kPNG: {
SkPngEncoder::Options opts;
opts.fUnpremulBehavior = SkTransferFunctionBehavior::kIgnore;
return SkPngEncoder::Encode(dst, src, opts);
}
case SkEncodedImageFormat::kWEBP: {
SkWebpEncoder::Options opts;
opts.fCompression = SkWebpEncoder::Compression::kLossy;
opts.fQuality = quality;
opts.fUnpremulBehavior = SkTransferFunctionBehavior::kIgnore;
return SkWebpEncoder::Encode(dst, src, opts);
}
default:
return false;
}
#endif
}
可以看出SkEncodedImageFormat其实是和应用层的Bitmap.CompressFormat枚举一一对应的。而不同类型(jpg、png、webp)则对应不同的Encoder,也就是对应不同的lib(ps: 即jpg的编码对应libjpg、webp的编码对应libwebp...)。这里插个题外话,Skia这种设计就有点像是一个适配器的样子了,比较工具化。
验证在iOS上使用Skia进行webp编码
Skia官网文档因为Skia默认没有现成可用的iOS库,所以可以考虑通过其源码和文档进行静态库编译。但在实际操作时发现,编译时会遇到各种问题一时之间难以解决。所以这里引用了别人已经编好的库,原文里有对应的源码地址及Skia静态库。 在 iOS 中使用 Skia
这里就直接上代码了,使用Skia进行Webp编码。
+ (NSData *)encodeWithSkia:(UIImage *)image quality:(float)q {
SkBitmap orig;
orig.setInfo(SkImageInfo::Make((int)image.size.width, (int)image.size.height, kRGBA_8888_SkColorType, kOpaque_SkAlphaType, nullptr));
orig.allocPixels();
CGColorSpaceRef colorSpace = CGImageGetColorSpace(image.CGImage);
CGFloat cols = image.size.width;
CGFloat rows = image.size.height;
size_t bytesPerRow = CGImageGetBytesPerRow(image.CGImage);
CGContextRef contextRef = CGBitmapContextCreate(orig.getPixels(), // Pointer to backing data
cols, // Width of bitmap
rows, // Height of bitmap
8, // Bits per component
bytesPerRow, // Bytes per row
colorSpace, // Colorspace
kCGImageAlphaNoneSkipLast |
kCGBitmapByteOrderDefault); // Bitmap info flags
CGContextDrawImage(contextRef, CGRectMake(0, 0, cols, rows), image.CGImage);
CGContextRelease(contextRef);
auto webpData = SkEncodeBitmap(orig, SkEncodedImageFormat::kWEBP, 90);
SkData* skData = webpData.get();
NSData* webFinalData = [NSData dataWithBytes:skData->bytes() length:skData->size()];
return webFinalData;
}
遗憾的是,实际测试中对比直接使用libwebp和使用skia在iOS端进行编码,对于cpu的占用并没有明显的区别。数据上感觉不足以支撑Skia是对webp编码进行过优化。
webp编码gpu加速?
之前一直听闻Skia是一个图像引擎,而网络上对其介绍得更多是画面渲染这一块。这不尽令人猜测,是否使用Skia的图像编码在Android会有gpu加速?但是在上述的研究可以发现,Skia更多地是像一个平台去集成不同的第三方库进行统一化,而Skia本身似乎并没有和gpu加速沾边,至少在图像编码这一块是这样的。
而gpu加速的思想是令重复多次计算且无关联性的逻辑(譬如遍历)拆分成多个小块并行计算,以此提高性能。可能这个理解比较浅,但是可以以此思路否定上述webp编码可以使用gpu的可能。尽管编码的计算是一个重复遍历的过程,但是在外层调用角度来看并没有拆分计算的余地,除非libwebp本身支持gpu加速的能力。
最后
本文只是记录我对于iOS的webp编码占用cpu高的研究,并没有什么实质性的干货。可以证明的是两端对于webp编码都是在cpu执行的。而Android占用cpu低这件事,在研究后只能猜测是Android系统底层对于webp编码有所限制。有此猜测也是由于Android虽然cpu占用率低,相对的编码耗时也会加长(ps: 此现象可能也和设备配置、性能有关)。最后的最后,如果有朋友了解相关知识,可以留言一起探讨~