您好,欢迎访问一九零五行业门户网

[iOS Animation]-CALayer 图像IO三_html/css_WEB-ITnose

结果catiledlayer工作的很好,性能问题解决了,而且和用gcd实现的代码量差不多。仅有一个问题在于图片加载到屏幕上后有一个明显的淡入(图14.4)。
图14.4 加载图片之后的淡入
我们可以调整catiledlayer的fadeduration属性来调整淡入的速度,或者直接将整个渐变移除,但是这并没有根本性地去除问题:在图片加载到准备绘制的时候总会有一个延迟,这将会导致滑动时候新图片的跳入。这并不是catiledlayer的问题,使用gcd的版本也有这个问题。
即使使用上述我们讨论的所有加载图片和缓存的技术,有时候仍然会发现实时加载大图还是有问题。就和13章中提到的那样,ipad上一整个视网膜屏图片分辨率达到了2048x1536,而且会消耗12mb的ram(未压缩)。第三代ipad的硬件并不能支持1/60秒的帧率加载,解压和显示这种图片。即使用后台线程加载来避免动画卡顿,仍然解决不了问题。
我们可以在加载的同时显示一个占位图片,但这并没有根本解决问题,我们可以做到更好。
分辨率交换 视网膜分辨率(根据苹果市场定义)代表了人的肉眼在正常视角距离能够分辨的最小像素尺寸。但是这只能应用于静态像素。当观察一个移动图片时,你的眼睛就会对细节不敏感,于是一个低分辨率的图片和视网膜质量的图片没什么区别了。
如果需要快速加载和显示移动大图,简单的办法就是欺骗人眼,在移动传送器的时候显示一个小图(或者低分辨率),然后当停止的时候再换成大图。这意味着我们需要对每张图片存储两份不同分辨率的副本,但是幸运的是,由于需要同时支持retina和非retina设备,本来这就是普遍要做到的。
如果从远程源或者用户的相册加载没有可用的低分辨率版本图片,那就可以动态将大图绘制到较小的cgcontext,然后存储到某处以备复用。
为了做到图片交换,我们需要利用uiscrollview的一些实现uiscrollviewdelegate协议的委托方法(和其他类似于uitableview和uicollectionview基于滚动视图的控件一样):
- (void)scrollviewdidenddragging:(uiscrollview *)scrollview willdecelerate:(bool)decelerate;- (void)scrollviewdidenddecelerating:(uiscrollview *)scrollview;
你可以使用这几个方法来检测传送器是否停止滚动,然后加载高分辨率的图片。只要高分辨率图片和低分辨率图片尺寸颜色保持一致,你会很难察觉到替换的过程(确保在同一台机器使用相同的图像程序或者脚本生成这些图片)。
缓存 如果有很多张图片要显示,最好不要提前把所有都加载进来,而是应该当移出屏幕之后立刻销毁。通过选择性的缓存,你就可以避免来回滚动时图片重复性的加载了。
缓存其实很简单:就是存储昂贵计算后的结果(或者是从闪存或者网络加载的文件)在内存中,以便后续使用,这样访问起来很快。问题在于缓存本质上是一个权衡过程 - 为了提升性能而消耗了内存,但是由于内存是一个非常宝贵的资源,所以不能把所有东西都做缓存。
何时将何物做缓存(做多久)并不总是很明显。幸运的是,大多情况下,ios都为我们做好了图片的缓存。
+imagenamed:方法 之前我们提到使用[uiimage imagenamed:]加载图片有个好处在于可以立刻解压图片而不用等到绘制的时候。但是[uiimage imagenamed:]方法有另一个非常显著的好处:它在内存中自动缓存了解压后的图片,即使你自己没有保留对它的任何引用。
对于ios应用那些主要的图片(例如图标,按钮和背景图片),使用[uiimage imagenamed:]加载图片是最简单最有效的方式。在nib文件中引用的图片同样也是这个机制,所以你很多时候都在隐式的使用它。
但是[uiimage imagenamed:]并不适用任何情况。它为用户界面做了优化,但是并不是对应用程序需要显示的所有类型的图片都适用。有些时候你还是要实现自己的缓存机制,原因如下:
[uiimage imagenamed:]方法仅仅适用于在应用程序资源束目录下的图片,但是大多数应用的许多图片都要从网络或者是用户的相机中获取,所以[uiimage imagenamed:]就没法用了。
[uiimage imagenamed:]缓存用来存储应用界面的图片(按钮,背景等等)。如果对照片这种大图也用这种缓存,那么ios系统就很可能会移除这些图片来节省内存。那么在切换页面时性能就会下降,因为这些图片都需要重新加载。对传送器的图片使用一个单独的缓存机制就可以把它和应用图片的生命周期解耦。
[uiimage imagenamed:]缓存机制并不是公开的,所以你不能很好地控制它。例如,你没法做到检测图片是否在加载之前就做了缓存,不能够设置缓存大小,当图片没用的时候也不能把它从缓存中移除。
自定义缓存 构建一个所谓的缓存系统非常困难。菲尔 卡尔顿曾经说过:“在计算机科学中只有两件难事:缓存和命名”。
如果要写自己的图片缓存的话,那该如何实现呢?让我们来看看要涉及哪些方面:
选择一个合适的缓存键 - 缓存键用来做图片的唯一标识。如果实时创建图片,通常不太好生成一个字符串来区分别的图片。在我们的图片传送带例子中就很简单,我们可以用图片的文件名或者表格索引。
提前缓存 - 如果生成和加载数据的代价很大,你可能想当第一次需要用到的时候再去加载和缓存。提前加载的逻辑是应用内在就有的,但是在我们的例子中,这也非常好实现,因为对于一个给定的位置和滚动方向,我们就可以精确地判断出哪一张图片将会出现。
缓存失效 - 如果图片文件发生了变化,怎样才能通知到缓存更新呢?这是个非常困难的问题(就像菲尔 卡尔顿提到的),但是幸运的是当从程序资源加载静态图片的时候并不需要考虑这些。对用户提供的图片来说(可能会被修改或者覆盖),一个比较好的方式就是当图片缓存的时候打上一个时间戳以便当文件更新的时候作比较。
缓存回收 - 当内存不够的时候,如何判断哪些缓存需要清空呢?这就需要到你写一个合适的算法了。幸运的是,对缓存回收的问题,苹果提供了一个叫做nscache通用的解决方案
nscache nscache和nsdictionary类似。你可以通过-setobject:forkey:和-object:forkey:方法分别来插入,检索。和字典不同的是,nscache在系统低内存的时候自动丢弃存储的对象。
nscache用来判断何时丢弃对象的算法并没有在文档中给出,但是你可以使用-setcountlimit:方法设置缓存大小,以及-setobject:forkey:cost:来对每个存储的对象指定消耗的值来提供一些暗示。
指定消耗数值可以用来指定相对的重建成本。如果对大图指定一个大的消耗值,那么缓存就知道这些物体的存储更加昂贵,于是当有大的性能问题的时候才会丢弃这些物体。你也可以用-settotalcostlimit:方法来指定全体缓存的尺寸。
nscache是一个普遍的缓存解决方案,我们创建一个比传送器案例更好的自定义的缓存类。(例如,我们可以基于不同的缓存图片索引和当前中间索引来判断哪些图片需要首先被释放)。但是nscache对我们当前的缓存需求来说已经足够了;没必要过早做优化。
使用图片缓存和提前加载的实现来扩展之前的传送器案例,然后来看看是否效果更好(见清单14.5)。
清单14.5 添加缓存
#import viewcontroller.h@interface viewcontroller() @property (nonatomic, copy) nsarray *imagepaths;@property (nonatomic, weak) iboutlet uicollectionview *collectionview;@end@implementation viewcontroller- (void)viewdidload{ //set up data self.imagepaths = [[nsbundle mainbundle] pathsforresourcesoftype:@png ?indirectory:@vacation photos]; //register cell class [self.collectionview registerclass:[uicollectionviewcell class] forcellwithreuseidentifier:@cell];}- (nsinteger)collectionview:(uicollectionview *)collectionview numberofitemsinsection:(nsinteger)section{ return [self.imagepaths count];}- (uiimage *)loadimageatindex:(nsuinteger)index{ //set up cache static nscache *cache = nil; if (!cache) { cache = [[nscache alloc] init]; } //if already cached, return immediately uiimage *image = [cache objectforkey:@(index)]; if (image) { return [image iskindofclass:[nsnull class]]? nil: image; } //set placeholder to avoid reloading image multiple times [cache setobject:[nsnull null] forkey:@(index)]; //switch to background thread dispatch_async( dispatch_get_global_queue(dispatch_queue_priority_low, 0), ^{ //load image nsstring *imagepath = self.imagepaths[index]; uiimage *image = [uiimage imagewithcontentsoffile:imagepath]; //redraw image using device context uigraphicsbeginimagecontextwithoptions(image.size, yes, 0); [image drawatpoint:cgpointzero]; image = uigraphicsgetimagefromcurrentimagecontext(); uigraphicsendimagecontext(); //set image for correct image view dispatch_async(dispatch_get_main_queue(), ^{ //cache the image [cache setobject:image forkey:@(index)]; //display the image nsindexpath *indexpath = [nsindexpath indexpathforitem: index insection:0]; uicollectionviewcell *cell = [self.collectionview cellforitematindexpath:indexpath]; uiimageview *imageview = [cell.contentview.subviews lastobject]; imageview.image = image; }); }); //not loaded yet return nil;}- (uicollectionviewcell *)collectionview:(uicollectionview *)collectionview cellforitematindexpath:(nsindexpath *)indexpath{ //dequeue cell uicollectionviewcell *cell = [collectionview dequeuereusablecellwithreuseidentifier:@cell forindexpath:indexpath]; //add image view uiimageview *imageview = [cell.contentview.subviews lastobject]; if (!imageview) { imageview = [[uiimageview alloc] initwithframe:cell.contentview.bounds]; imageview.contentmode = uiviewcontentmodescaleaspectfit; [cell.contentview addsubview:imageview]; } //set or load image for this index imageview.image = [self loadimageatindex:indexpath.item]; //preload image for previous and next index if (indexpath.item 0) { [self loadimageatindex:indexpath.item - 1]; } return cell;}@end
果然效果更好了!当滚动的时候虽然还有一些图片进入的延迟,但是已经非常罕见了。缓存意味着我们做了更少的加载。这里提前加载逻辑非常粗暴,其实可以把滑动速度和方向也考虑进来,但这已经比之前没做缓存的版本好很多了。
文件格式 图片加载性能取决于加载大图的时间和解压小图时间的权衡。很多苹果的文档都说png是ios所有图片加载的最好格式。但这是极度误导的过时信息了。
png图片使用的无损压缩算法可以比使用jpeg的图片做到更快地解压,但是由于闪存访问的原因,这些加载的时间并没有什么区别。
清单14.6展示了标准的应用程序加载不同尺寸图片所需要时间的一些代码。为了保证实验的准确性,我们会测量每张图片的加载和绘制时间来确保考虑到解压性能的因素。另外每隔一秒重复加载和绘制图片,这样就可以取到平均时间,使得结果更加准确。
清单14.6
#import viewcontroller.hstatic nsstring *const imagefolder = @coast photos;@interface viewcontroller () @property (nonatomic, copy) nsarray *items;@property (nonatomic, weak) iboutlet uitableview *tableview;@end@implementation viewcontroller- (void)viewdidload{ [super viewdidload]; //set up image names self.items = @[@2048x1536, @1024x768, @512x384, @256x192, @128x96, @64x48, @32x24];}- (cftimeinterval)loadimageforonesec:(nsstring *)path{ //create drawing context to use for decompression uigraphicsbeginimagecontext(cgsizemake(1, 1)); //start timing nsinteger imagesloaded = 0; cftimeinterval endtime = 0; cftimeinterval starttime = cfabsolutetimegetcurrent(); while (endtime - starttime < 1) { //load image uiimage *image = [uiimage imagewithcontentsoffile:path]; //decompress image by drawing it [image drawatpoint:cgpointzero]; //update totals imagesloaded ++; endtime = cfabsolutetimegetcurrent(); } //close context uigraphicsendimagecontext(); //calculate time per image return (endtime - starttime) / imagesloaded;}- (void)loadimageatindex:(nsuinteger)index{ //load on background thread so as not to //prevent the ui from updating between runs dispatch_async( dispatch_get_global_queue(dispatch_queue_priority_high, 0), ^{ //setup nsstring *filename = self.items[index]; nsstring *pngpath = [[nsbundle mainbundle] pathforresource:filename oftype:@png indirectory:imagefolder]; nsstring *jpgpath = [[nsbundle mainbundle] pathforresource:filename oftype:@jpg indirectory:imagefolder]; //load nsinteger pngtime = [self loadimageforonesec:pngpath] * 1000; nsinteger jpgtime = [self loadimageforonesec:jpgpath] * 1000; //updated ui on main thread dispatch_async(dispatch_get_main_queue(), ^{ //find table cell and update nsindexpath *indexpath = [nsindexpath indexpathforrow:index insection:0]; uitableviewcell *cell = [self.tableview cellforrowatindexpath:indexpath]; cell.detailtextlabel.text = [nsstring stringwithformat:@png: %03ims jpg: %03ims, pngtime, jpgtime]; }); });}- (nsinteger)tableview:(uitableview *)tableview numberofrowsinsection:(nsinteger)section{ return [self.items count];}- (uitableviewcell *)tableview:(uitableview *)tableview cellforrowatindexpath:(nsindexpath *)indexpath{ //dequeue cell uitableviewcell *cell = [self.tableview dequeuereusablecellwithidentifier:@cell]; if (!cell) { cell = [[uitableviewcell alloc] initwithstyle: uitableviewcellstylevalue1 reuseidentifier:@cell]; } //set up cell nsstring *imagename = self.items[indexpath.row]; cell.textlabel.text = imagename; cell.detailtextlabel.text = @loading...; //load image [self loadimageatindex:indexpath.row]; return cell;}@end
png和jpeg压缩算法作用于两种不同的图片类型:jpeg对于噪点大的图片效果很好;但是png更适合于扁平颜色,锋利的线条或者一些渐变色的图片。为了让测评的基准更加公平,我们用一些不同的图片来做实验:一张照片和一张彩虹色的渐变。jpeg版本的图片都用默认的photoshop60%“高质量”设置编码。结果见图片14.5。
图14.5 不同类型图片的相对加载性能
如结果所示,相对于不友好的png图片,相同像素的jpeg图片总是比png加载更快,除非一些非常小的图片、但对于友好的png图片,一些中大尺寸的图效果还是很好的。
所以对于之前的图片传送器程序来说,jpeg会是个不错的选择。如果用jpeg的话,一些多线程和缓存策略都没必要了。
但jpeg图片并不是所有情况都适用。如果图片需要一些透明效果,或者压缩之后细节损耗很多,那就该考虑用别的格式了。苹果在ios系统中对png和jpeg都做了一些优化,所以普通情况下都应该用这种格式。也就是说在一些特殊的情况下才应该使用别的格式。
混合图片 对于包含透明的图片来说,最好是使用压缩透明通道的png图片和压缩rgb部分的jpeg图片混合起来加载。这就对任何格式都适用了,而且无论从质量还是文件尺寸还是加载性能来说都和png和jpeg的图片相近。相关分别加载颜色和遮罩图片并在运行时合成的代码见14.7。
清单14.7 从png遮罩和jpeg创建的混合图片
#import viewcontroller.h@interface viewcontroller ()@property (nonatomic, weak) iboutlet uiimageview *imageview;@end@implementation viewcontroller- (void)viewdidload{ [super viewdidload]; //load color image uiimage *image = [uiimage imagenamed:@snowman.jpg]; //load mask image uiimage *mask = [uiimage imagenamed:@snowmanmask.png]; //convert mask to correct format cgcolorspaceref grayspace = cgcolorspacecreatedevicegray(); cgimageref maskref = cgimagecreatecopywithcolorspace(mask.cgimage, grayspace); cgcolorspacerelease(grayspace); //combine images cgimageref resultref = cgimagecreatewithmask(image.cgimage, maskref); uiimage *result = [uiimage imagewithcgimage:resultref]; cgimagerelease(resultref); cgimagerelease(maskref); //display result self.imageview.image = result;}@end
对每张图片都使用两个独立的文件确实有些累赘。jpng的库(https://github.com/nicklockwood/jpng)对这个技术提供了一个开源的可以复用的实现,并且添加了直接使用+imagenamed:和+imagewithcontentsoffile:方法的支持。
jpeg 2000 除了jpeg和png之外ios还支持别的一些格式,例如tiff和gif,但是由于他们质量压缩得更厉害,性能比jpeg和png糟糕的多,所以大多数情况并不用考虑。
但是ios之后,苹果低调添加了对jpeg 2000图片格式的支持,所以大多数人并不知道。它甚至并不被xcode很好的支持 - jpeg 2000图片都没在interface builder中显示。
但是jpeg 2000图片在(设备和模拟器)运行时会有效,而且比jpeg质量更好,同样也对透明通道有很好的支持。但是jpeg 2000图片在加载和显示图片方面明显要比png和jpeg慢得多,所以对图片大小比运行效率更敏感的时候,使用它是一个不错的选择。
但仍然要对jpeg 2000保持关注,因为在后续ios版本说不定就对它的性能做提升,但是在现阶段,混合图片对更小尺寸和质量的文件性能会更好。
pvrtc 当前市场的每个ios设备都使用了imagination technologies powervr图像芯片作为gpu。powervr芯片支持一种叫做pvrtc(powervr texture compression)的标准图片压缩。
和ios上可用的大多数图片格式不同,pvrtc不用提前解压就可以被直接绘制到屏幕上。这意味着在加载图片之后不需要有解压操作,所以内存中的图片比其他图片格式大大减少了(这取决于压缩设置,大概只有1/60那么大)。
但是pvrtc仍然有一些弊端:
尽管加载的时候消耗了更少的ram,pvrtc文件比jpeg要大,有时候甚至比png还要大(这取决于具体内容),因为压缩算法是针对于性能,而不是文件尺寸。
pvrtc必须要是二维正方形,如果源图片不满足这些要求,那必须要在转换成pvrtc的时候强制拉伸或者填充空白空间。
质量并不是很好,尤其是透明图片。通常看起来更像严重压缩的jpeg文件。
pvrtc不能用core graphics绘制,也不能在普通的uiimageview显示,也不能直接用作图层的内容。你必须要用作opengl纹理加载pvrtc图片,然后映射到一对三角板来在caeagllayer或者glkview中显示。
创建一个opengl纹理来绘制pvrtc图片的开销相当昂贵。除非你想把所有图片绘制到一个相同的上下文,不然这完全不能发挥pvrtc的优势。
pvrtc使用了一个不对称的压缩算法。尽管它几乎立即解压,但是压缩过程相当漫长。在一个现代快速的桌面mac电脑上,它甚至要消耗一分钟甚至更多来生成一个pvrtc大图。因此在ios设备上最好不要实时生成。
如果你愿意使用opehgl,而且即使提前生成图片也能忍受得了,那么pvrtc将会提供相对于别的可用格式来说非常高效的加载性能。比如,可以在主线程1/60秒之内加载并显示一张2048×2048的pvrtc图片(这已经足够大来填充一个视网膜屏幕的ipad了),这就避免了很多使用线程或者缓存等等复杂的技术难度。
xcode包含了一些命令行工具例如texturetool来生成pvrtc图片,但是用起来很不方便(它存在于xcode应用程序束中),而且很受限制。一个更好的方案就是使用imagination technologies pvrtextool,可以从http://www.imgtec.com/powervr/insider/sdkdownloads免费获得。
安装了pvrtextool之后,就可以使用如下命令在终端中把一个合适大小的png图片转换成pvrtc文件:
/applications/imagination/powervr/graphicssdk/pvrtextool/cl/osx_x86/pvrtextoolcl -i {input_file_name}.png -o {output_file_name}.pvr -legacypvr -p -f pvrtc1_4 -q pvrtcbest
清单14.8的代码展示了加载和显示pvrtc图片的步骤(第6章caeagllayer例子代码改动而来)。
清单14.8 加载和显示pvrtc图片
#import viewcontroller.h #import #import @interface viewcontroller ()@property (nonatomic, weak) iboutlet uiview *glview;@property (nonatomic, strong) eaglcontext *glcontext;@property (nonatomic, strong) caeagllayer *gllayer;@property (nonatomic, assign) gluint framebuffer;@property (nonatomic, assign) gluint colorrenderbuffer;@property (nonatomic, assign) glint framebufferwidth;@property (nonatomic, assign) glint framebufferheight;@property (nonatomic, strong) glkbaseeffect *effect;@property (nonatomic, strong) glktextureinfo *textureinfo;@end@implementation viewcontroller- (void)setupbuffers{ //set up frame buffer glgenframebuffers(1, &_framebuffer); glbindframebuffer(gl_framebuffer, _framebuffer); //set up color render buffer glgenrenderbuffers(1, &_colorrenderbuffer); glbindrenderbuffer(gl_renderbuffer, _colorrenderbuffer); glframebufferrenderbuffer(gl_framebuffer, gl_color_attachment0, gl_renderbuffer, _colorrenderbuffer); [self.glcontext renderbufferstorage:gl_renderbuffer fromdrawable:self.gllayer]; glgetrenderbufferparameteriv(gl_renderbuffer, gl_renderbuffer_width, &_framebufferwidth); glgetrenderbufferparameteriv(gl_renderbuffer, gl_renderbuffer_height, &_framebufferheight); //check success if (glcheckframebufferstatus(gl_framebuffer) != gl_framebuffer_complete) { nslog(@failed to make complete framebuffer object: %i, glcheckframebufferstatus(gl_framebuffer)); }}- (void)teardownbuffers{ if (_framebuffer) { //delete framebuffer gldeleteframebuffers(1, &_framebuffer); _framebuffer = 0; } if (_colorrenderbuffer) { //delete color render buffer gldeleterenderbuffers(1, &_colorrenderbuffer); _colorrenderbuffer = 0; }}- (void)drawframe{ //bind framebuffer & set viewport glbindframebuffer(gl_framebuffer, _framebuffer); glviewport(0, 0, _framebufferwidth, _framebufferheight); //bind shader program [self.effect preparetodraw]; //clear the screen glclear(gl_color_buffer_bit); glclearcolor(0.0, 0.0, 0.0, 0.0); //set up vertices glfloat vertices[] = { -1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f }; //set up colors glfloat texcoords[] = { 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f }; //draw triangle glenablevertexattribarray(glkvertexattribposition); glenablevertexattribarray(glkvertexattribtexcoord0); glvertexattribpointer(glkvertexattribposition, 2, gl_float, gl_false, 0, vertices); glvertexattribpointer(glkvertexattribtexcoord0, 2, gl_float, gl_false, 0, texcoords); gldrawarrays(gl_triangle_fan, 0, 4); //present render buffer glbindrenderbuffer(gl_renderbuffer, _colorrenderbuffer); [self.glcontext presentrenderbuffer:gl_renderbuffer];}- (void)viewdidload{ [super viewdidload]; //set up context self.glcontext = [[eaglcontext alloc] initwithapi:keaglrenderingapiopengles2]; [eaglcontext setcurrentcontext:self.glcontext]; //set up layer self.gllayer = [caeagllayer layer]; self.gllayer.frame = self.glview.bounds; self.gllayer.opaque = no; [self.glview.layer addsublayer:self.gllayer]; self.gllayer.drawableproperties = @{keagldrawablepropertyretainedbacking: @no, keagldrawablepropertycolorformat: keaglcolorformatrgba8}; //load texture glactivetexture(gl_texture0); nsstring *imagefile = [[nsbundle mainbundle] pathforresource:@snowman oftype:@pvr]; self.textureinfo = [glktextureloader texturewithcontentsoffile:imagefile options:nil error:null]; //create texture glkeffectpropertytexture *texture = [[glkeffectpropertytexture alloc] init]; texture.enabled = yes; texture.envmode = glktextureenvmodedecal; texture.name = self.textureinfo.name; //set up base effect self.effect = [[glkbaseeffect alloc] init]; self.effect.texture2d0.name = texture.name; //set up buffers [self setupbuffers]; //draw frame [self drawframe];}- (void)viewdidunload{ [self teardownbuffers]; [super viewdidunload];}- (void)dealloc{ [self teardownbuffers]; [eaglcontext setcurrentcontext:nil];}@end
如你所见,非常不容易,如果你对在常规应用中使用pvrtc图片很感兴趣的话(例如基于opengl的游戏),可以参考一下glview的库(https://github.com/nicklockwood/glview),它提供了一个简单的glimageview类,重新实现了uiimageview的各种功能,但同时提供了pvrtc图片,而不需要你写任何opengl代码。
其它类似信息

推荐信息