core_animation笔记

个人博客地址:Lixuzong's Blog

我们所在屏幕上看到的都是Core Animation框架提供的,所以这并不是一个只关于动画的框架,他包含了我们在屏幕上所能看到的一切东西。这里首先说一下CALayer和UIView的关系,UIView是CALayer的管理者,CALayer是我们在屏幕上所能看到的UIView说呈现的。用过Photoshop的都知道,图片可以是很多图层的叠加计算的结果,与之类似,我们在屏幕上所看到的也是CALayer层叠加的结果,UIView就管理着这个相互叠加的过程。再者,UIView还管理着手势的响应。如果只从视图的角度来看的话,UIView只是对CALayer的一层封装。

core_animation_book

首先先放上本书的链接,看的过程中有些翻译不是非常的准确,所以是结合着原版的一起看的。

关于Layer的知识点

图层树

关于图层树,我们看到的图层与UIView的层级是对应的,而UIView的层级与CALayer是对应的,其中CALayer tree又分为呈现树(presentation layer tree)和模型树(model layer tree),因为layer默认是带有隐式动画的,但是我们直接改变layer属性的时候是立即生效的,也就是说layer的属性改变是立即执行的,但是界面上还是依然反应有一个动画的过程,所以这个时候就分成了两个tree,一个是界面上所呈现的层级(presentation layer tree),一个是我们修改的层级(model layer tree)。

Layer的contents

contents是一个id类型的对象,但是接收的类型应该是Core Foundation框架的类型,这里是因为在MAC OS系统上CGImage和NSImage类型值都是可以起作用的。在iOS上的话就是用bridge将CGImage转成Core Foundation的id类型就可以了。下面具体看一下contens的属性。

相关属性

  • contentsGravity
    我们操作UIView属性的时候,会有一个contentMode属性定义怎么与图层的边界对其,与之对应的CALayer属性就是contentsGravity属性,并且其根本上操作的还是对应的Layer的属性。

  • contentsScale
    iOS的屏幕的单位并不是直接使用像素,而是直接使用点来作为距离单位,这样的话方便兼容不同分辨率的屏幕。scale就是一个点包含几个像素,视网膜屏就是2.

  • maskToBounds
    默认情况下,UIView会绘制超过边界的内容或者子视图,CALayer下也是这样的。UIView有一个叫做clipsToBounds的属性可以用来决定是否显示超出边界的内容,CALayer对应的属性叫做maskToBounds。

  • contentsRect
    这个属性是用来给图片截图的,配合maskToBoundds可以确定显现image的局部信息。

  • contentsCenter
    是用来确定contents变大情况下的拉伸情况。

custom drawing

是在不设置contents属性为image的情况下直接画一个,在UIView里面有drawRect:,虽然drawRect是UIView的方法,但是在底层还是通过CALayer安排重绘工作和保存产生的图片的。绘制用到的是CADelegate提供的两个方法,分别是drawLayer:(CALayer)layer inContext:(CGContext)ctx调用这个方法之前CALayer会自动生成一个空的图像(由bounds和contentsSacle决定)和一个Core Graphics的绘制上下文环境,为绘制图做准备。调用的时候与UIView方法setNeedDisplay类似,CALayer的方法是调用displayLayer来触发重绘操作。

图层几何

图层几何是看图层内部是如何根据父图层和兄弟图层来控制位置和尺寸的。另外我们也会涉及如何管理图层的几何结构,以及它是如何被自动调整和自动布局影响的。

  • 布局
    UIView有三个比较重要的布局属性:frame\bounds\center,CALayer对应的叫做frame\bounds\position,其中center和position是相对于父图层anchorPoint(锚点)所在的位置。frame的值和bounds并不是严格的宽高对应的,当一个视图发生旋转之后,其bounds是不便的,而frame的值是包含bounds的矩形。这里就可以看出frame是一个虚拟属性,是根据bounds、position和transform计算出来的,所以改变其中一个值都会使frame发生改变。

  • 锚点
    contentsRect和contentsCenter属性类似,anchorPoint用单位坐标来描述,也就是图层的相对坐标,图层左上角是{0, 0},右下角是{1, 1},因此默认坐标是{0.5, 0.5}。anchorPoint可以通过指定x和y值小于0或者大于1,使它放置在图层范围之外。改变锚点也会影响到frame的值。

  • 坐标系
    和视图一样,图层在图层树当中也是相对于父图层按层级关系放置,一个图层的position依赖于它父图层的bounds,如果父图层发生了移动,它的所有子图层也会跟着移动。与UIView相似,也有改变坐标系的方法。与UIView不同的是CALayer是三维空间的构图,可以改变zPosition和anchorPointZ来改变z轴上的顺序。比如layer重叠顺序。

  • Hit Testing
    CALayer并不关心任何响应链事件,所以不能直接处理触摸事件或者手势。但是它有一系列的方法帮你处理事件:-containsPoint:-hitTest:。第一个方法可以判定点击的点是否在这个layer的区域内,但是要注意的是坐标系的转换,第二个方式是直接判断响应这个点的图层,所以没必要转换坐标系,结果是返回这个响应的图层。

  • layout
    与UIView的另一个不同点,layer是没有autoLayout的属性的,所以这也是要使用UIView而不是使用CALayer的原因之一。如果想控制layer的布局,这里提供了一个CALayerDelegate的方法.

-(void)layoutSublayersOfLayer:(CALayer*)layer

当图层的bounds改变或者图层 -setNeedsLayout 方法被调用的时候,这个函数就会执行。在这个方法里面可以重新摆放或者调整子图层的大小。

Layer视觉效果

  • 圆角
    圆角主要是使用cornerRadius属性,直接设置半径就会出现圆角的效果。同时设置圆角会触发离屏渲染。

  • 图层边框
    绘制边框使用的是borderColor和borderWidth属性,其中borderColor是CGColorRef类型的,所以不是Cocoa框架的对象,即便CGColorRef是强引用也只能声明称assign。

  • 阴影
    给shadowOpacity属性一个大于默认值(也就是0)的值,阴影就可以显示在任意图层之下。shadowOpacity是一个必须在0.0(不可见)和1.0(完全不透明)之间的浮点数。如果设置为1.0,将会显示一个有轻微模糊的黑色阴影稍微在图层之上。若要改动阴影的表现,你可以使用CALayer的另外三个属性:shadowColor,shadowOffset和shadowRadius。另外阴影也会有性能隐患。

  • 蒙版
    蒙版的作用就是覆盖一层Layer,将这个layer的轮廓显示出来,填充内容是contents的内容。

  • 拉伸过滤
    最后就是minificationFilter(缩小)和magnificationFilter(放大)属性。这两个属性主要是使用在要展示非图像原始大小情况下使用,遵循怎样的算法进行拉伸和缩放。

  • 组透明
    当我们给一个layer tree添加透明度属性的时候,往往是里面的layer的透明度会小一些,因为里面的layer是父layer与其透明度的一个混合,所以会出现两个layer混合之后的效果不是我们想要的效果,这个时候就需要组透明来解决。这里有两种解决方案,一种是将layer光栅化成一个图层,这样的话透明度就一致了,另一个方法就是通过设置Info.plist文件中的UIViewGroupOpacity为YES来达到这个效果,但是会对整个App造成影响。

CGAffineTransform

首先,所有的变换都是基于CGPoint的,仿射变换之后的矩形两对边是相互平行的。

CGTransform3D

3D变换主要是配置一个矩阵来实现每个点的变化,从而达到整个layer实现3D变换的效果。要想做到灭点相同的话,必须要将layer都放在view的中心,然后在平移到指定的位置,改变layer的frame的话灭点也会发生改变。还有一个简单的办法就是使用sublayerTransform属性。我们可以任意的放置子layer的位置,这样的话也会共享一个灭点。

专用图层

CAShapeLayer

CAShapeLayer是一个通过矢量图而不是通过bitmap来绘制的图层子类。指定线宽和颜色等属性,用CGPath来定义想要绘制的图形,最后CAShapeLayer就会自动的渲染出来。与直接向CALayer绘制路径相比,CAShapeLayer的优点如下:

1、渲染快速。CAShapeLayer使用了硬件加速,绘制同一图形会比用Core Graphics快很多。
2、高效使用内存。一个CAShapeLayer不需要像普通layer一样创建一个contents图形,所以无论多大都不会占用过多的内存。
3、不会被图层边界裁掉。一个CAShapeLayer可以在边界之外绘制,不会像普通的layer被裁减掉。
4、不会出现像素化。当你给CAShapeLayer做3D变换的时候,他不像一个contents普通图层一样变得像素化。

使用CAShapeLayer主要就是设置path属性,path是CGPathRef类型,但是为了方便管理内存,使用UIKit框架的UIBezierPath来创建,然后转成CGPathRef类型赋值给path属性,这样的话就会渲染出一个path形状的图层。利用之前的蒙版的功能,我们可以直接将contents裁剪成任意形状。例如可以让两个角圆角,另外两个角是正常的,这些都是很容易实现的。

CATextLayer

它以图层的形式包含了UILabel几乎所有的绘制特性,另外还提供了一些新的额外的特性。在ios6之前UILabel是用WebKit来实现的,而CATextLayer是用Core Text支持的,所以渲染速度要快很多,但是iOS7之后文字渲染交给了Text Kit来实现,其底层还是使用Core Text,所以性能上的差距应该没有这么明显了(自己认为的)。CATextLayer可以使用普通文本和富文本。

CATransformLayer

这个属性没有自己的contents属性,而是管理子layer的Transform变化,主要是为了解决CALayer会将其子layer平面化,而CATransformLayer不会平面化其子layer。这样的话就可以在一个layer里面构造两种不同视角的3D物体。

CAGradientLayer

CAGradientLayer是用来生成两种或更多颜色平滑渐变的。用Core Graphics复制一个CAGradientLayer并将内容绘制到一个普通图层的寄宿图也是有可能的,但是CAGradientLayer的真正好处在于绘制使用了硬件加速。

CAReplicatorLayer

其目的是高效生成许多相似的图层。它会绘制一个或多个图层的子图层,并且在每个复制体上应用不同的变换。使用的时候就是将layer加入到CARelicatorLayer类型的layer中,然后可以在CARelicatorLayer中设置复制的数量和变换。可以高效的来做反射的效果。

CAScrollLayer

Core Animation并不处理用户输入,所以CAScrollLayer并不负责将触摸事件转换为滑动事件,既不渲染滚动条,也不实现任何iOS指定行为例如滑动反弹。用法就是直接重写+ (Class)layerClass方法,将UIView的根layer返回成CAScrollLayer类型,这样的话其中的子layer都是可以滑动的。

CATiledLayer

有些时候你可能需要绘制一个很大的图片,常见的例子就是一个高像素的照片或者是地球表面的详细地图。iOS应用通畅运行在内存受限的设备上,所以读取整个图片到内存中是不明智的。载入大图可能会相当地慢,那些对你看上去比较方便的做法(在主线程调用UIImage的-imageNamed:方法或者-imageWithContentsOfFile:方法)将会阻塞你的用户界面,至少会引起动画卡顿现象。
能高效绘制在iOS上的图片也有一个大小限制。所有显示在屏幕上的图片最终都会被转化为OpenGL纹理,同时OpenGL有一个最大的纹理尺寸(通常是2048/2048,或4096/4096,这个取决于设备型号)。如果你想在单个纹理中显示一个比这大的图,即便图片已经存在于内存中了,你仍然会遇到很大的性能问题,因为Core Animation强制用CPU处理图片而不是更快的GPU(见第12章『速度的曲调』,和第13章『高效绘图』,它更加详细地解释了软件绘制和硬件绘制)。
CATiledLayer为载入大图造成的性能问题提供了一个解决方案:将大图分解成小片然后将他们单独按需载入.

CAEmitterLayer

是一个高性能的粒子引擎,被用来创建实时例子动画如:烟雾,火,雨等等这些效果。

CAEAGLLayer

它是CALayer的一个子类,用来显示任意的OpenGL图形。

AVPlayerLayer

AVPlayerLayer是有别的框架(AVFoundation)提供的,它和Core Animation紧密地结合在一起,提供了一个CALayer子类来显示自定义的内容类型。AVPlayerLayer是用来在iOS上播放视频的。他是高级接口例如MPMoivePlayer的底层实现,提供了显示视频的底层控制。AVPlayerLayer的使用相当简单:你可以用+playerLayerWithPlayer:方法创建一个已经绑定了视频播放器的图层,或者你可以先创建一个图层,然后用player属性绑定一个AVPlayer实例。

动画

implicit animation

隐式动画就是系统会自动的给layer做动画,layer位置、颜色等的改变会引起一个0.25秒的动画过程。core animation是根据什么来判断动画类型和时间的那,看起来是自动的设置,实际上是有事务来进行管理的。

事务实际上是Core Animation用来包含一系列属性动画集合的机制,任何用指定事务去改变可以做动画的图层属性都不会立刻发生变化,而是当事务一旦提交的时候开始用一个动画过渡到新值。事务是通过CATransaction类来做管理,这个类的设计有些奇怪,不像你从它的命名预期的那样去管理一个简单的事务,而是管理了一叠你不能访问的事务。CATransaction没有属性或者实例方法,并且也不能用+alloc和-init方法创建它。但是可以用+begin和+commit分别来入栈或者出栈。即使不显示的调用begin方法,在一次runloop循环中

我们也可以手动的关闭动画,layer tree中,作为UIView的根layer的动画是被关上的,并不是通过layer的开关关闭的,首先看一下CALayer的代理方法。

  • 图层首先检测它是否有委托,并且是否实现CALayerDelegate协议指定的-actionForLayer:forKey方法。如果有,直接调用并返回结果。
  • 如果没有委托,或者委托没有实现-actionForLayer:forKey方法,图层接着检查包含属性名称对应行为映射的actions字典。
  • 如果actions字典没有包含对应的属性,那么图层接着在它的style字典接着搜索属性名。
  • 最后,如果在style里面也找不到对应的行为,那么图层将会直接调用定义了每个属性的标准行为的-defaultActionForKey:方法。

于是这就解释了UIKit是如何禁用隐式动画的:每个UIView对它关联的图层都扮演了一个委托,并且提供了-actionForLayer:forKey的实现方法。对于改变可动画的属性,当其不在一个动画块的实现中,UIView对所有图层行为返回nil,但是在动画block范围之内,它就返回了一个非空值。

explicit aniamtion

显示动画是使用CAAnimation做的动画,我们来看一下CAAniamtion的子类。

CAPropertyAniamtion(属性动画)

  • CABasicAniamtion
    CABasicAnimation是设置Layer的属性的fromValue和toValue,然后有一个渐变的过程,这个其实是和隐式动画相似,只是多了一个设置fromValue。

  • CAKeyframeAnimation
    CAKeyframeAnimation是另一种UIKit没有暴露出来但是功能强大的类,与CABasicAniamtion相似,也是ACPropertyAniamtion的子类,也是只能作用于一个属性,但是和basic不同的是,它不限制于设置一个起始和最终值,而是可以根据一系列任意的值来做动画。一种方式是根据values数组来给出关键帧,core animation会在这些关键帧之间自动的插入动画,但是这种方式并不直观,还有一种方式就是使用CGPath,这是一种直观的方式,使用CoreGraphics函数定义运动序列来绘制动画。

  • 虚拟属性做动画
    虚拟属性指的是layer没用暴露的属性,比如rotation,transfrom等,我们可以指定 keyPath = @“transform.rotation” 来对角度做动画。

  • 动画组(CAAnimationGroup)
    可以将动画放到CAAnimationGroup里面,将不同的动画组合进行

过渡动画(CATransition)

对于没有办法做动画的属性,或者交换一段文本和图片,或者用一段网格来替换,这个时候属性动画是没用作用的。属性动画只对图层可动画属性起作用,所以如果要改变一个不能动画的属性(比如图片,因为CoreAnimation不知道什么时候插入图片),又或者成层级关系中添加或者移除图层,属性动画将不起作用,于是就有了过度的概念。

过度并不像属性动画那样平滑的在两个值之间做动画,而是影响到整个图层的变化。过度动画首先展示之前的图层外观,然后通过一个过度变换到新的外观。

我们使用CATransition来管理过度效果,和别的子类不同,CATransition有一个type和subType来标识变换效果。type是一个NSString类型,提供了四种过度类型,分别是:

kCATransitionFade

kCATransitionMoveIn
kCATransitionPush
kCATransitionReveal

过渡动画和之前的属性动画或者动画组添加到图层上的方式一致,都是通过-addAnimation:forKey:方法。但是和属性动画不同的是,对指定的图层一次只能使用一次CATransition,因此,无论你对动画的键设置什么值,过渡动画都会对它的键设置成“transition”,也就是常量kCATransition。

CATransision可以对图层任何变化平滑过渡的事实使得它成为那些不好做动画的属性图层行为的理想候选。苹果当然意识到了这点,并且当设置了CALayer的content属性的时候,CATransition的确是默认的行为。但是对于视图关联的图层,或者是其他隐式动画的行为,这个特性依然是被禁用的,但是对于你自己创建的图层,这意味着对图层contents图片做的改动都会自动附上淡入淡出的动画。

图层树的动画

CATransition并不作用于指定的图层属性,这就是说你可以在即使不能准确得知改变了什么的情况下对图层做动画,例如,在不知道UITableView哪一行被添加或者删除的情况下,直接就可以平滑地刷新它,或者在不知道UIViewController内部的视图层级的情况下对两个不同的实例做过渡动画。因为它们不仅涉及到图层的属性,而且是整个图层树的改变--我们在这种动画的过程中手动在层级关系中添加或者移除图层。要确保CATransition添加到的图层在过渡动画发生时不会在树状结构中被移除,否则CATransition将会和图层一起被移除。一般来说,你只需要将动画添加到被影响图层的superlayer。

自定义动画

苹果通过UIView +transitionFromView:toView:duration:options:completion:和+transitionWithView:duration:options:animations:方法提供了Core Animation的过渡特性。但是这里的可用的过渡选项和CATransition的type属性提供的常量完全不同

动画的时间、缓冲

model layer & presentation layer

我们直接改变layer的position,可以发现position属性会直接发生变化,但是屏幕上我们看到的layer却是有一个渐变的动画,而不是根据我们设置的属性实时的发生变化,所以,详细的划分一下,这里的layer就是model layer,其属性表示的是动画结束之后layer的属性,而屏幕上我们直接看到的就是presentation layer,layer实时在屏幕上的位置。

性能调优

性能陷阱

软件绘图不仅效率低,还会消耗可观的内存。CALayer只需要一些与自己相关的内存:只有它的寄宿图会消耗一定的内存空间。即使直接赋给contents属性一张图片,也不需要增加额外的照片存储大小。如果相同的一张图片被多个图层作为contents属性,那么他们将会共用同一块内存,而不是复制内存块。

但是一旦你实现了CALayerDelegate协议中的-drawLayer:inContext:方法或者UIView中的-drawRect:方法(其实就是前者的包装方法),图层就创建了一个绘制上下文,这个上下文需要的大小的内存可从这个算式得出:图层宽图层高4字节,宽高的单位均为像素。对于一个在Retina iPad上的全屏图层来说,这个内存量就是 204815264字节,相当于12MB内存,图层每次重绘的时候都需要重新抹掉内存然后重新分配。

加载图片消耗内存和占用CPU的原因

一旦图片文件被加载就必须要进行解码,解码过程是一个相当复杂的任务,需要消耗非常长的时间。解码后的图片将同样使用相当大的内存。

用于加载的CPU时间相对于解码来说根据图片格式而不同。对于PNG图片来说,加载会比JPEG更长,因为文件可能更大,但是解码会相对较快,而且Xcode会把PNG图片进行解码优化之后引入工程。JPEG图片更小,加载更快,但是解压的步骤要消耗更长的时间,因为JPEG解压算法比基于zip的PNG算法更加复杂。

图片加载的性能优化(TableView优化)

  • 在后台线程中加载图片,会有效果,但是并不是性能的瓶颈。
  • iOS为了节省内存延迟解压,解压会消耗很长的时间,这里带来的问题就是在使用的时候进行解压,就会造成界面的卡顿[UIImage imageWithContentsOfFile:]会延迟图片的解压,[UIimage imageName:]会立即解压图片,另外两种方式也会直接解压图片,一种是作为layercontents属性的时候,还有一种就是作为UIImageViewimage属性的时候,但是这两种方法必须在主线程中才有效。第四种方式是不用UIKit框架,直接使用ImageIO框架来实现:
NSInteger index = indexPath.row;
NSURL *imageURL = [NSURL fileURLWithPath:self.imagePaths[index]]; 
NSDictionary *options = @{(__bridge id)kCGImageSourceShouldCache: @YES};
CGImageSourceRef source = CGImageSourceCreateWithURL(
(__bridge CFURLRef)imageURL, NULL);
CGImageRef imageRef = CGImageSourceCreateImageAtIndex(source, 0,
(__bridge CFDictionaryRef)options);
UIImage *image = [UIImage imageWithCGImage:imageRef]; CGImageRelease(imageRef);
CFRelease(source);

这里还有第五种方式依然使用UIKit框架来实现立即解压图片,以为绘制图片之前会解压图片,那么就直接将图片画到CGContext里面,从而实现立即解压,这有个好处就是不是必须在主线程中实现

  • 中间插一个离屏渲染导致的性能问题,前面有说过设置Layer的conerRadius属性会触发离屏渲染,导致内存增加。另外maskToBounds属性是结合conerRadius使用的,所以这个属性也是需要注意的。

离屏渲染影响性能的原因:「直接将图层合成到帧的缓冲区中(在屏幕上)比先创建屏幕外缓冲区,然后渲染到纹理中,最后将结果渲染到帧的缓冲区中要廉价很多。因为这其中涉及两次昂贵的环境转换(转换环境到屏幕外缓冲区,然后转换环境到帧缓冲区)。」触发离屏渲染后这种转换发生在每一帧,在界面的滚动过程中如果有大量的离屏渲染发生时会严重影响帧率。

  • 使用图片的大小最好是直接适配最终的大小,这样的话imageView不用再调整大小之后再使用。

  • 缓存图片也是可以实现优化,[UIImage imageNamed:]方法会直接缓存之后的图片,但是我们并不能手动控制,而且也不知道什么时候图片是被缓存的,那么就要使用NSCache来实现缓存,并且缓存的是解压之后的图片,使用UIKit的话就是要在CGContext里面实现。这里是处理好的例子:

#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 < [self.imagePaths count] - 1) {
        [self loadImageAtIndex:indexPath.item + 1]; }
    if (indexPath.item > 0) {
        [self loadImageAtIndex:indexPath.item - 1]; }
    return cell;
}

@end
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容