《TBImageView》
—–一个异步实现图片添加圆角阴影的框架
1. 从UIImageView的缺陷,来看TBImageView框架的定位
[注:原始图片:无圆角无阴影,不支持透明的jpg图片]
开始做《淘宝读书》书架和未下载列表的时候,我们通常会直接使用<QuartzCore>来给图片添加阴影和圆角。如:
self.content = [[[UIView alloc] initWithFrame:startPostion] autorelease]; self.content.layer.cornerRadius = 10; self.content.layer.shadowRadius = 10; self.content.layer.shadowOpacity = 0.8f; self.content.layer.shadowOffset = CGSizeMake(-3, 3); self.content.layer.shadowColor = [UIColor blackColor].CGColor; |
可惜的是,iOS不支持UIView的圆角和阴影同时作用到一个UIView中,通过代码看到的结果是,没有圆角,只有阴影;只好再添加一行代码:
self.content.layer.masksToBounds = YES |
天啊,圆角有了,阴影没有了。不过,这个难不倒程序员,我们可以有N种方法解决:
1. UIImageView的后面添加View,让背景View.layer属性设置来显示阴影
2. UIImageView的后面添加一个大的UIImageView,图片设置为阴影图片
3. 重写drawRect函数,使用CGContext手动绘图
大功告成,使用了额外的一些行代码,完成了我们想要的效果。
[注:加工后的图片:圆角+阴影+透明的png图片]
如果使用TBImageView,只需要:
imageView.imageInfo.cornerRadius = 10; imageView.imageInfo.shadowOffset = CGSizeMake(-3, 3); imageView.imageInfo.shadowColor = 0xffb66107; [imageView setImageURL:@"http://img06.taobaocdn.com/bao/uploaded/i6/i1/T1f4emXadaXXcwv_MU_015850.jpg"]; |
2. 从UIImageView的性能,来看TBImageView框架的改进
iphone4以后Retina屏幕给我们带来视觉的极致粒度,为了保证app的效果,我们不得不使用640×960甚至1536×2048像素来展示图片。在ipad3上阅读类App,一个分页的UIScrollView至少显示一个UIImageView,为了性能和内存,重用UIImageView并把它放到当前显示的区域,即使它只有一个像素进入屏幕区域,你会发现每次进入下一个页面都会有一个明显的延迟,这个延迟来自于将图片从文件解压缩并渲染到屏幕上这一系列工作。而UIImage仅在UIImageView将要显示的时候才会做解压工作。
视图添加和显示必须在主线程中完成,所以UIImage的解压缩和渲染也自然落到了主线程,这就是造成这个延迟的原因。这种情况在UITableView新的row出现的瞬间、在瀑布流新图片的显示、在相册留言中的卡顿等经常遇到。
不幸的是,大多业务型的app的图片,都是从互联网上下载的,看一下一张1024×768的图片从网络传输到显示到手机屏幕上的消耗:
1. internet传输的时间 2. 保存/从磁盘空间读取 3. 解压缩jpg图片并保存到内存(640x960x4) 4. 将解压缩后的bytes转换成CGContext的时间(改变大小、图层混合等) |
如果是本地图片:
A. 1024×768分辨率,90%压缩质量的jpeg图片从加载,解压缩到渲染的时间
设备类型 | CPU主频 | RAM内存 | 耗时 |
iPhone3G | 400MHz | 128M | 527ms |
iPhone3GS | 600MHz | 256M | 134ms |
iPhone4 | 800MHz | 512M | 70ms |
iPad1 | 1.0GHz | 256M | 79ms |
iPad2 | 1.0GHz x 2 | 512M | 51ms |
如下图所示
[注:图片解码在iOS设备中占用了70%-80%左右时间。绿色是渲染时间,红色是解码时间,蓝色是对象初始化时间]
绿色部分表示主线程将解码后的图片渲染到屏幕上的时间,红色部分表示解码并转换到CGContext的时间,蓝色部分表示从磁盘空间加载的时间。
为了缩短图片解码时间,可以使用pngcrush工具优化png图片,解码时间可以提升40%以上,不幸的是,也无法撼动图片解码消耗总时间50%以上的事实。流畅的用户体验,要求动画帧数保持在30帧以上,也就是要求重绘的视图占用主线的时间缩短在40ms以内。
3. 从SDWebImage的优化和layer的重绘机制的碰撞看,TBImageView的改进
添加阴影后的UIImageView的显示区域会增大,因为图片周围有了一个渐变的阴影,如图:
[注:加工后的图片四边对称的增加了相等的阴影偏移量,总尺寸响应增加了]
真实大小就是UIImageView的frame,是否有一张带圆角阴影的图片,真实大小和frame一样,让UIImageView的图片禁止缩放同时居中,那么多出来的实际大小作为阴影显示在frame之外?
实际宽 = (abs(阴影x偏移量) + 模糊半径)x 2 + 图片的宽 实际高 = (abs(阴影y偏移量) + 模糊半径)x 2 + 图片的高 图片在视图中居中显示UIViewContentModeCenter |
从网络上下载图片后,在CGContext中添加阴影和圆角,并保存到本地
添加圆角: CGContextAddArcToPoint(context, fw, fh, fw/2, fh, cornerRadius); 添加阴影: CGContextSetShadowWithColor(ctx, CGSizeMake(0, 0), blur, CGColor); 生成PNG图片 UIImagePNGRepresentation(image); 保存到磁盘空间 [fileManager createFileAtPath:pathname contents:data attributes:nil]; |
4. TBImageView的缓存和加载策略
[注:最少的占用CPU时间,不阻塞主线程,GCD串行处理任务]
a) 图片缓存策略
采用系统现有的缓存机制NSCache,将强制解码后的图片(位图)缓存,监听内存警告消息,及时清空缓存
[NSNotificationCenter.defaultCenter addObserver:self selector:@selector(clearMemory) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; |
b) 图片存储策略
我们会在多个场景使用同一张图片,每个场景会有不同的阴影、圆角,手机流量和网络总是开发者的痛。可以将原图保存下来,同时将处理后的图片保存在另个一文件夹:
如果找到渲染后的图片,就直接显示;如果没有,那就从原始文件夹找到原始图片,重新加工并保存;如果还是没有,就从网络下载图片吧。
c) 图片加载策略
视图对象总是会在任意时间释放掉,就要求我们在dealloc函数中,及时的关闭请求。尽管iOS给我们提供了杀死线程的方法,为了安全,我们给每一个后台任务配置了一个开关,当一个后台图片加载任务,设置为关闭时,它将一个合适的时间停止运行,然后释放掉自己。
加载任务会有多个阶段:从本地获取图片、从网络获取图片、强制解码、渲染图片等等,我们在每个小阶段完成后,检查任务开关,并进行软退出操作和内存清理。
d) 图片显示的优先级调度
通常在低端机上,我们会遇到性能问题,在操作的流畅度和后台加载之间,有没有这种的办法?
iOS给我们提供多种优先级策略,NSURLConnectiond的RunLooper模式为NSEventTrackingRunLoopMode,这种模式下当UIScrollView滚动时,为了保证流畅性,主线程的RunLooper将暂时不处理网络下载请求,我们需要手动的修改NSURLConnection默认的运行模式为:
[self.connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; |
保证UIScollView滑动的同时下载并加载图片
5. iOS的图片和阴影的性能优化(概要)
使用QuartzCore库在layer层添加阴影,是一种非常方便常用的做法,在后台,系统将阴影和圆角,在内存中计算好,并显示在屏幕上。
屏幕的每一个像素都是多个视图叠加合并渲染出来的。阴影和圆角是有透明效果的。一个像素中,如果某个视图出现了透明效果,那么会引发这个视图的重绘。不幸的是,iOS给我们提供了便捷的阴影方案,在内存中,阴影效果并没有保存,所以,当视图重绘的时候,它会重新计算阴影完成后,在于当前其他视图做效果叠加。当然,都是主线程中完成的,槽糕的事情就这样发生了。
可以有很多方案减轻主线程压力,可以准备一张带阴影的图片,或者准备一张阴影图片的背景,当然,《TBImageView》框架也许是一种更加好的解决方式。