app图片优化-webp格式图片原理及在Android、IOS中
参考: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目录
image3.在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格式。