ViewController
VC生命周期
单个VC的生命周期
- initWithCoder:(NSCoder *)aDecoder:(如果使用storyboard或者xib)
- loadView:加载view
- viewDidLoad:view加载完毕
- viewWillAppear:控制器的view将要显示
- viewWillLayoutSubviews:控制器的view将要布局子控件
- viewDidLayoutSubviews:控制器的view布局子控件完成
- viewDidAppear:控制器的view完全显示
- viewWillDisappear:控制器的view即将消失的时候
- viewDidDisappear:控制器的view完全消失的时候
- dealloc 控制器销毁
多个VC的生命周期
rootViewController为TabbarController,里面有三个VC,分别为A,B,C。A VC上有两个button,一个push出D,一个presentE
冷启动加载Tab
-[A_VC initWithCoder:]
-[B_VC initWithCoder:]
-[C_VC initWithCoder:]
-[TabViewController initWithCoder:]
-[TabViewController loadView]
-[TabViewController viewDidLoad]
-[TabViewController viewWillAppear:]
-[TabViewController viewWillLayoutSubviews]
-[TabViewController viewDidLayoutSubviews]
-[A_VC loadView]
-[A_VC viewDidLoad]
-[A_VC viewWillAppear:]
-[A_VC viewWillLayoutSubviews]
-[A_VC viewDidLayoutSubviews]
-[A_VC viewDidAppear:]
-[TabViewController viewDidAppear:]
点击B Tab跳转到B
-[A_VC viewWillDisappear:]
-[B_VC loadView]
-[B_VC viewDidLoad]
-[B_VC viewWillAppear:]
-[B_VC viewWillLayoutSubviews]
-[B_VC viewDidLayoutSubviews]
-[A_VC viewDidDisappear:]
-[B_VC viewDidAppear:]
点击A Tab返回A时
-[A_VC viewWillAppear:]
-[B_VC viewWillDisappear:]
-[B_VC viewDidDisappear:]
-[A_VC viewDidAppear:]
再次点击B Tab跳转到B
-[B_VC viewWillAppear:]
-[A_VC viewWillDisappear:]
-[A_VC viewDidDisappear:]
-[B_VC viewDidAppear:]
再次点击A Tab返回A
-[A_VC viewWillAppear:]
-[B_VC viewWillDisappear:]
-[B_VC viewDidDisappear:]
-[A_VC viewDidAppear:]
点击A上的按钮 push D
-[TabViewController viewWillLayoutSubviews]
-[TabViewController viewDidLayoutSubviews]
-[D_VC loadView]
-[D_VC viewDidLoad]
-[A_VC viewWillDisappear:]
-[D_VC viewWillAppear:]
-[D_VC viewWillLayoutSubviews]
-[D_VC viewDidLayoutSubviews]
-[A_VC viewDidDisappear:]
-[D_VC viewDidAppear:]
-[TabViewController viewWillLayoutSubviews]
-[TabViewController viewDidLayoutSubviews]
点击D上的按钮返回A
-[TabViewController viewWillLayoutSubviews]
-[TabViewController viewDidLayoutSubviews]
-[D_VC viewWillDisappear:]
-[A_VC viewWillAppear:]
-[D_VC viewDidDisappear:]
-[A_VC viewDidAppear:]
-[D_VC dealloc]
-[TabViewController viewWillLayoutSubviews]
-[TabViewController viewDidLayoutSubviews]
-[A_VC viewWillLayoutSubviews]
-[A_VC viewDidLayoutSubviews]
-[A_VC viewWillLayoutSubviews]
-[A_VC viewDidLayoutSubviews]
点击A上的按钮 present 出E
-[E_VC loadView]
-[E_VC viewDidLoad]
-[A_VC viewWillDisappear:]
-[TabViewController viewWillDisappear:]
-[E_VC viewWillAppear:]
-[E_VC viewWillLayoutSubviews]
-[E_VC viewDidLayoutSubviews]
-[E_VC viewDidAppear:]
-[A_VC viewDidDisappear:]
-[TabViewController viewDidDisappear:]
点击E上的按钮 dismiss
-[E_VC viewWillDisappear:]
-[A_VC viewWillAppear:]
-[TabViewController viewWillAppear:]
-[A_VC viewDidAppear:]
-[TabViewController viewDidAppear:]
-[E_VC viewDidDisappear:]
-[E_VC dealloc]
loadView 的作用
loadView 用来自定义 view,只要实现了这个方法,其他通过 xib 或 storyboard 创建的 view 都不会被加载
为什么必须在主线程中操作UI
-
UIKit 不是线程安全的
- 如果在多线程下操作UI,会存在多个线程访问、修改以及资源抢夺问题等,会造成死锁和崩溃。
- 而为其加锁则会耗费大量资源并拖慢运行速度
-
整个程序的起点
UIApplication
是在主线程进行初始化的,所有的用户事件也都是在主线程上进行传递,所以view只能在主线程上才能对事件进行响应 -
在渲染方面,由于图像的渲染需要以60帧的刷新率在屏幕上 同时 更新,在非主线程异步化的情况下无法确定这个处理过程能够实现同步更新。
UIView
UIView 的Frame、Bounds、Center
- Frame:描述当前界面元素在其父界面元素中的位置和大小(参照点是父view的坐标系统)
- Bounds:描述当前界面元素在其自身坐标系统中的位置和大小(参照点是本身坐标系统)
- Center:描述当前界面元素的中心点在其父界面元素中的位置(参照点是父view的坐标系统)
setNeedsLayout、layoutIfNeeded、layoutSubViews区别
- layoutIfNeeded:方法调用后,在主线程对当前视图及其所有子视图立即强制更新布局
- layoutSubviews:方法只能重写,不能主动调用。在屏幕旋转、滑动或触摸界面、子视图修改时被系统自动调用,用来调整自定义视图的布局
- setNeedsLayout:方法与layoutIfNeeded相似,不同的是方法被调用后不会立即强制更新布局,而是在下一个布局周期进行更新
layoutSubview和drawRect
layoutSubviews调用场景
- init初始化UIView,不会触发调用
- addSubview会触发调用
- 设置view的Frame会触发layoutSubviews,当然前提是frame的值设置前后发生了变化
- UIScrollView滚动会触发调用
- 旋转Screen会触发父UIView的方法调用
- 改变一个UIView大小的时候会触发父UIView的方法调用
触发drawRect的场景
- 如果UIView没有设置frame大小,直接导致drawRect不能被自动调用
- drawRect在loadView和viewDidLoad这两个方法之后调用
- 调用sizeToFit后自动调用drawRect
- 设置contentMode值为UIViewContentModeRedraw。那么每次设置或者更改frame自动调用drawRect。
- 直接调用setNeedsDisplay或者setNeedsDisplayInRect会触发调用
重写drawRect 有什么影响
- drawRect 方法依赖 Core Graphics 框架进行自定义绘制
缺点
- 当你调用 setNeedsDisplay 方法时,UIKit 将会把当前图层标记为 dirty,但还是会显示原来的内容
- 直到下一次的视图渲染周期,才会将标记为 dirty 的图层重新建立 Core Graphics 上下文
- 然后将内存中的数据恢复出来,再使用 CGContextRef 进行绘制,带来额外的CPU和内存开销
view的touch事件有哪些?
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;
View之间传值方式有哪些
-
通过视图类对外提供的属性直接传值
-
通过代理传值
-
通过通知传值
-
通过 Block 传值
-
通过NSUserDefault, 不建议
UIWindow是什么,有什么特点
- UIWindow提供一个区域用于显示UIView并且将事件分发给 UIView
- UIWindow 继承自 UIView,作为根视图来承载 View元素
- 一般一个应用只有一个 UIWindow
- UIWindow不需要添加到另一个window上,创建出来之后即会显示出来
UIButton 和UITableView的层级结构
继承结构
UIButton -> UIControl -> UIView -> UIResponder -> NSObject
UITableView -> UIScrollView -> UIView -> UIResponder -> NSObject
内部子控件结构
UIButton
- 默认有两个,一个UIImageView,一个UILabel,分别可以设置图片和文字
- button设置属性基本都是set方法
UITableView
- UITableView中每一行数据都是UITableViewCell
- UITableViewCell内部有一个UIView控件 (contentView,作为其它元素的父控件),两个UILable 控件 (textLabel, detailTextLabel) , 一个UIImageView控件 (imageView),分别用于容器,显示内容,详情和图片
CALayer
CALayer的Frame、Bounds、AnchorPoint、Position
- Frame:与view中的frame概念相同
- x,y: subLayer左上角相对于supLayer坐标系的位置关系;
- width, height表示subLayer的宽度和高度
- bounds:与view中的bounds概念相同,
- x,y:subLayer左上角相对于自身坐标系的关系;
- width, height 表示subLayer的宽度和高度
- anchorPoint(锚点):锚点在自身坐标系中的相对位置,默认值为(0.5,0.5),左上角为(0,0),右下角为(1,1),其他位置以此类推;
- 锚点都是对于自身来讲的。
- 确定自身的锚点,通常用于做相对的tranform变换。当然也可以用来确定位置
- position:锚点在supLayer坐标系中的位置
CALayer如何添加点击事件
- 通过
touchesBegan: withEvent
方法监听屏幕点击事件 - 在这个方法中通过
convertPoint
找到点击位置 进行判断,如果点击了 layer 视图内坐标就触发点击事件 - 通过 hitTest方法找到包含坐标系的 layer 视图进行判断
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
// 方法一,通过 convertPoint 转为为 layer 坐标系进行判断
CGPoint point = [[touches anyObject] locationInView:self.view];
CGPoint redPoint = [self.redLayer convertPoint:point fromLayer:self.view.layer];
if ([self.redLayer containsPoint:redPoint]) {
NSLog(@"点击了calayer");
}
// 方法二 通过 hitTest 返回包含坐标系的 layer 视图
CGPoint point1 = [[touches anyObject] locationInView:self.view];
CALayer *layer = [self.view.layer hitTest:point1];
if (layer == self.redLayer) {
NSLog(@"点击了calayer");
}
}
CALayer 和 UIView
相同点
- UIView 和 CALayer 都是 UI 操作的对象。
- 两者都是 NSObject 的子类,发生在 UIView 上的操作本质上也发生在对应的 CALayer 上
不同点
-
UIView 是 CALayer 用于交互的抽象
- UIView是CALayer的delegate,为CALayer提供渲染所需内容
- UIView是UIResponder的子类,负责处理触摸等事件,参与响应链
-
CALayer 在图像和动画渲染上性能更好
- 这是因为 UIView 有冗余的交互接口,而且相比 CALayer 还有层级之分
- CALayer负责显示内容contents,在无需处理交互时进行渲染可以节省大量时间
-
CALayer的动画要通过
逻辑树、动画树和显示树
来实现
masksToBounds 和clipsToBounds
- masksToBounds 是指子 layer 在超出父 layer时是否被裁剪,YES表示裁剪,NO 表示不裁剪,默认是NO
- clipsToBounds 是指子 View 在超出父 View时是否被裁剪
为什么iOS提供 UIView 和CAlayer 两个平行的层级结构
- 主要是用于职责分离,实现视图的绘制、显示、布局解耦,避免重复代码
- 在iOS 和 Mac OS两个平台上,事件和用户交互有很多不同的地方,创建两个层级结构,可以在两个平台上共享代码,从而使得开发快捷
绘制
绘制和渲染过程
-
每个UIView都有一个layer,每个layer都有个contents,指向一块缓存,这块缓存叫做
Backing Store
,用来获取图形上下文 -
当UIView被绘制时,
view.layer
判断这个layer
是否有 delegate- 如果有则调用
[layer.delegate drawLayer:inContext:]
,进而调用[UIView DrawRect:]
- 如果没有
delegate
,那么会调用[CALayer drawInContext:]
- 如果有则调用
-
最终
CALayer
都会将位图提交到Backing Store
,通过render server将bitmap数据提交给GPU
渲染出来 -
CPU执行绘制的操作,把内容放到缓存里,GPU负责从缓存里读取数据然后渲染到屏幕上
调用setNeedDisplay时,页面的渲染过程?
- 当我们调用
[UIView setNeedsDisplay]
时,会调用当前View.layer
的[view.layer setNeedsDisplay]
- 这等于给当前的
layer
打上了一个脏标记,但此时并不直接进行绘制工作 - 而是会等到当前
Runloop
即将休眠,也就是beforeWaiting
时才进行绘制工作 - 紧接着会调用
[CALayer display]
,进入到真正绘制的工作 CALayer
层会判断自己的delegate
有没有实现异步绘制的代理方法displayLayer:
,这个代理方法是异步绘制的入口- 如果没有实现这个方法,那么会继续进行系统绘制的流程,然后绘制结束
UI卡顿掉帧原因
- iOS设备的硬件时钟会定时发出Vsync(垂直同步信号)
- CPU会去计算屏幕要显示的内容,之后将计算好的内容提交到GPU去渲染
- GPU将渲染结果提交到帧缓冲区,等到下一个VSync到来时将缓冲区的帧显示到屏幕上。
- 也就是说,一帧的显示是由CPU和GPU共同决定的
- 一般来说,页面滑动流畅是60fps,也就是1s有60帧更新,即每隔16.7ms就要产生一帧画面,而如果CPU和GPU加起来的处理时间超过了16.7ms,就会造成掉帧甚至卡顿
GPU屏幕渲染的两种方式
On-Screen Rendering (当前屏幕渲染)
即当前屏幕渲染,在用于显示的屏幕缓冲区中进行,不需要额外创建新的缓存,也不需要开启新的上下文,所以性能较好,但是受到缓存大小限制等因素,一些复杂的操作无法完成。
Off-Screen Rendering (离屏渲染)
- GPU在当前屏幕缓冲区以外开辟一个新缓冲区进行渲染操作
什么是离屏渲染?
- 如果要在显示屏上显示内容,我们至少需要一块与屏幕像素数据量一样大的frame buffer,作为像素数据存储区域,而这也是GPU存储渲染结果的地方。
- 如果有时因为面临一些限制,无法把渲染结果直接写入frame buffer,而是先暂存在另外的内存区域,之后再写入frame buffer,那么这个过程被称之为离屏渲染。
为什么离屏这么耗时?
- 创建缓冲区
- 离屏渲染的整个过程,需要多次切换上下文环境:先从当前屏幕切换到离屏,等待离屏渲染结束后,将离屏缓冲区的渲染结果显示到到屏幕上,这又需要将上下文环境从离屏切换到当前屏幕。
- 创建新的缓冲区代价都不算大,付出最大代价的是上下文切换
哪些情况会造成离屏渲染
- 为图层设置遮罩(layer.mask)
- 将图层的layer.masksToBounds / view.clipsToBounds属性设置为true
- 将图层layer.allowsGroupOpacity属性设置为YES和layer.opacity小于1.0
- 为图层设置阴影(layer.shadow *)
- 为图层设置layer.shouldRasterize=true
- 具有layer.cornerRadius,layer.edgeAntialiasingMask,layer.allowsEdgeAntialiasing的图层
- 文本(任何种类,包括UILabel,CATextLayer,Core Text等)
什么是光栅化
- 光栅化是将几何数据经过一系列变换后最终转换为像素,从而呈现在显示设备上的过程
- 光栅化的本质是坐标变换、几何离散化
CPU 渲染和离屏渲染的区别
- 由于GPU的浮点运算能力比CPU强,CPU渲染的效率可能不如离屏渲染。
- 但如果仅仅是实现一个简单的效果,直接使用 CPU 渲染的效率又可能比离屏渲染好,毕竟普通的离屏渲染要涉及到缓冲区创建和上下文切换等耗时操作。
- 对一些简单的绘制过程来说,这个过程有可能用CoreGraphics,全部用CPU来完成反而会比GPU做得更好。一个常见的 CPU 渲染的例子是:重写 drawRect 方法,并且使用任何 Core Graphics 的技术进行了绘制操作,就涉及到了 CPU 渲染。整个渲染过程由 CPU 在 App 内同步地完成,渲染得到的bitmap最后再交由GPU用于显示。
- 总之,具体使用 CPU 渲染还是使用 GPU 离屏渲染更多的时候需要进行性能上的具体比较才可以。
什么是异步绘制
异步绘制就是在子线程把需要绘制的图形处理好,然后将准备好的图像数据直接返给主线程使用
实现原理
- 当我们改变UIView的frame时,会调用layer的
setNeedsDisplay
,然后调用layer的display方法 - 不能在非主线程将内容绘制到layer的context上
- 但我们可以开一个子线程调用
CGBitmapContextCreateImage()
绘制内容Bitmap,绘制完成之后切回主线程,将内容赋值到contents上
动画
隐式动画和显示动画
隐式动画
- Core Animation在每个runloop周期中会自动开始一次新的事务,即使你不显式的用[CATransaction begin]开始一次事务
- 在一次runloop循环中任何属性的改变,都会被集中起来,然后做一次0.25秒的动画
+animateWithDuration:animations:
内部会自动调用CATransaction的+begin和+commit方法
,从而将block中所有属性的改变都包含在事务内。这样就完成了隐私动画
显式动画
- Core Animation提供的显式动画类型,既可以直接对layer层属性做动画,也可以覆盖默认的图层行为。
- 我们经常使用的
CABasicAnimation,CAKeyframeAnimation,CATransitionAnimation,CAAnimationGroup
等都是显式动画类型,这些CAAnimation类型可以直接提交到CALayer上。 - 显式动画可用于实现更为复杂的动画效果
CABasicAnimation *positionAnima = [CABasicAnimation animationWithKeyPath:@"position.y"];
positionAnima.duration = 0.8;
positionAnima.fromValue = @(self.imageView.center.y);
positionAnima.toValue = @(self.imageView.center.y-30);
positionAnima.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
positionAnima.repeatCount = HUGE_VALF;
positionAnima.repeatDuration = 2;
positionAnima.removedOnCompletion = NO;
positionAnima.fillMode = kCAFillModeForwards;
[self.imageView.layer addAnimation:positionAnima forKey:@"AnimationMoveY"];
动画有哪几种方式
UIView animation
-
是对CALayer Animation的封装,可以实现移动、旋转、变色、缩放等基本操作
-
它实现的动画无法回撤、暂停、与手势交互
[UIView animateWithDuration: 10 animations:^{ // 动画操作 }];
UIViewPropertyAnimator
- iOS10 中引入的 处理交互式动画的接口,
- 它是基于 UIView 实现的,用法同 UIView animation类似,增加了一些新的属性以及方法
CALayer Animation
- 可以实现各种复杂的动画效果,实现的动画可以回撤、暂停与手势交互等。
1. CABasicAnimation——基本动画
2. CAKeyframeAnimation——关键帧动画,与CABasicAnimation的区别是:
CABasicAnimation只能从一个数值(fromValue)变到另一个数值(toValue)
CAKeyframeAnimation:会使用一个NSArray保存这些数值
3. CAAnimationGroup——动画组,是CAAnimation的子类,可以保存一组动画对象,将CAAnimationGroup对象加入层后,组中所有动画对象可以同时并发运行
4. 转场动画——CATransition,是CAAnimation的子类,用于做转场动画,能够为层提供移出屏幕和移入屏幕的动画效果
Cocoa Touch默认提供了几种CoreAnimation过渡类
Cocoa Touch提供了4种CoreAnimation过渡类型,分别为:交叉淡化、推挤、显示和覆盖
fade
, moveIn
, push
and reveal
. Defaults to fade
.
布局
AutoLayout 中的优先级是什么
-
AutoLayout 中添加的约束也有优先级,优先级的数值范围是
1~1000
,默认是1000 -
Content Hugging Priority
抗拉伸优先级,值越小,越容易被拉伸 -
Content Compression Resistance
抗压缩优先级,优先级越小,越先被压缩
Auto Layout 和 Frame 在 UI 布局和渲染上有什么区别?
-
Auto Layout 是针对多尺寸屏幕的设计。
其本质是通过线性不等式对 UI 控件的相对位置进行设定,从而适配多种 iPhone/iPad 屏幕的尺寸。
-
Frame 是基于 xy 坐标轴系统的布局机制。
它从数学上限定了 UI 控件的具体位置,是 iOS 开发中最底层、最基本的界面布局机制。
-
**Auto Layout 的性能比 Frame 差很多。**Auto Layout 的布局过程首先求解线性不等式,然后再转化为 Frame 去进行布局。其中求解的计算量非常大,通常 Auto Layout 的性能损耗是 Frame 布局的 10 倍左右。
-
怎么弥补差距
- 尽量压缩视图层级减少计算量;
- Layout 的计算也可以通过后台线程来处理,这样就可以不阻塞主线程操作。
- 计算结果亦可以缓存起来,加速之后界面布局渲染。
- 成熟的解决方案有 Facebook 的 ComponentKit,Pinterest 的 Texture(前身是 ASDK ),以及 LinkdedIn 的 LayoutKit。
IBOutlet 连出来的视图属性为什么可以被设置成 weak
- 如果是拖到一个view上,因为父控件的 subViews 数组已经对它有一个强引用
- 如果拖出来的view是一个单独的view,没有添加到其他的view上,就需要使用strong修饰
SafeArea, SafeAreaLayoutGuide, SafeAreaInsets关键词的比较说明
SafeArea
- 指App 显示内容的区域,不包括
StatusBar、Navigationbar、tabbar和 toolbar
- 在 X系列中一般是指扣除了statusBar(44pt)和底部home indicator(高度为34pt)的区域
SafeAreaLayoutGuide
- 指 Safe Area 的区域范围和限制,在布局设置中,可以取得它的上左下右4个边界位置进行相应的布局
SafeAreaInsets
- 限定了Safe Area区域与整个屏幕之间的布局关系,一般用上左下右4个值来获取 SafeArea 与屏幕边缘之间的距离
UIScrollView
滚动原理
- UIScrollView继承自UIView,内部有一个UIPanGestureRecongnizer手势
- frame 是相对父视图坐标系来决定自己的位置和大小,而bounds是相对于自身坐标系的位置和尺寸。
- 更改视图 bounds 的 origin,视图本身没有发生变化,但是它的子视图的位置却发生了变化
- UIScrollView检测到pan手势之后就是通过不断的更改自身bounds的origin来实现滚动的
contentView, contentInset, contentSize, contentOffset比较
- contentView 是指 UIScrollView上显示内容的区域,用户 addSubView 都是在 contentView上进行的
- contentInset 是指 contentView与 UIScrollView的边界
- contentSize 是指 contentView 的大小,表示可以滑动的范围
- contentOffset 是当前 contentView 浏览位置左上角的坐标
TableView
简述UITableView的复用机制
- 每次创建cell的时候通过dequeueReusableCellWithIdentifier:方法创建cell,它先到缓存池中找指定标识的cell,如果没有就直接返回nil
- 如果没有找到指定标识的cell,那么会通过initWithStyle:reuseIdentifier:创建一个cell
- 当 cell 离开界面就会被放到缓存池中,以供将要出现在界面中的cell复用
Cell动态计算高度,都有哪些方案
-
使用iOS系统自身的机制
- 使用AutoLayout来布局约束
- 设置tableview的
estimatedRowHeight
为一个非零的预估高度值 - 设置tableview的rowHeight属性为
UITableViewAutomaticDimension
-
手动计算每个子控件的高度并相加,最后缓存高度
-
使用第三方框架,如
UITableView+FDTemplateLayoutCell
如何让TableView 刷新完成后再执行操作
-
[tableView reloadData]
并不会同步等待更新结束后才执行后续代码,而是立即执行后续代码,然后异步地去计算scrollView的高度,获取cell等等 -
可以通过以下方法在reloaddata执行完毕再执行操作
-
通过layoutIfNeeded方法,强制重绘并等待完成
[self.tableView reloadData]; [self.tableView layoutIfNeeded]; //刷新完成,执行后续需要执行的代码
-
reloadData方法会在主线程中执行,通过GCD使后续操作排队在reloadData后面执行
[self.tableView reloadData]; dispatch_async(dispatch_get_main_queue(), ^{ //刷新完成,执行后续代码 });
-
图片
图片png与jpg的区别是什么
PNG
-
优点:无损格式,不论保存多少次,理论上图片质量都不会受任何影响;支持透明度
-
缺点:尺寸过大;打开速度与保存速度和jpg没法比
JPG
-
优点:尺寸较小,节省空间;打开速度快
-
缺点:有损格式,在修图时不断保存会导致图片质量不断降低;不支持透明
-
在开发中,尺寸比较大的图片(例如背景图片)一般适用jpg格式,减小对内存的占用
imageNamed 和 imageWithContentsOfFile区别
- imageNamed 会自动缓存新加载的图片并且重复利用缓存图片,一般用于App 内经常使用的尺寸不大的图片
- imageWithContentsOfFile 根据路径加载,没有缓存取缓存的过程,用于一些大图,使用完毕会释放内存