目录:
1.应用程序五种状态
2.OC的反射机制
3.UITableViewCell的卡顿你是怎么优化的?
4.说一下HTTP和HTTPS
5.UIViewController 的生命周期
6.说一下@property属性
7.UIView与CALayer的关系
8.事件的传递和响应链
9.如何高性能的给UIImageView加个圆角?
10.nil、Nil、NULL、NSNull的区别
11.如何实现一个线程安全的 NSMutableArray?
12.实现 isEqual 和 hash 方法时要注意什么?
13.id 和 instanceType 有什么区别?
14.iOS的沙盒目录结构是怎样的?
15.SDWebImage 底层实现原理?
16.Struct和Class的区别?
17.Swift对比OC的优点?
18.#include与#import的区别、#import 与@class 的区别?
1.应用程序五种状态:
Not Running (非运行状态)
Inactive (前台非活动状态)
Active (前台活动状态)
Background (后台状态)
Suspended (挂起状态)
- Not running -> Inactive 阶段, 发出 UIApplicationDidFinishLaunchingNotification 通知。
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
NSLog(@"%@", @"application:didFinishLaunchingWithOptions:");
return YES;
}
- Inactive -> Active 阶段, 发出 UIApplicationDidBecomeActiveNotification 通知
- (void)applicationDidBecomeActive:(UIApplication *)application{
NSLog(@"%@", @"applicationDidBecomeActive:");
}
- Active -> Inactive 阶段, 发出 UIApplicationWillResignActiveNotification 通知。
- (void)applicationWillResignActive:(UIApplication *)application{
NSLog(@"%@", @"applicationWillResignActive:");
}
- Background -> Suspended 阶段, 发出 UIApplicationDidEnterBackgroundNotification 通知。
- (void)applicationDidEnterBackground:(UIApplication *)application{
NSLog(@"%@", @"applicationDidEnterBackground:");
}
- Background -> Inactive 阶段, 发出 UIApplicationWillEnterForegroundNotification 通知。
- (void)applicationWillEnterForeground:(UIApplication *)application{
NSLog(@"%@", @"applicationWillEnterForeground:");
}
- Suspended -> Not running阶段, 发出 UIApplicationWillTerminateNotification通知
- (void)applicationWillTerminate:(UIApplication *)application{
NSLog(@"%@", @"applicationWillTerminate:");
}
2.OC的反射机制
OC
的反射机制类似于JAVA
,在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为反射机制,主要体现在三个方面:
-
获取Class对象
Class
对象其实本质上就是一个结构体,这个结构体中的成员变量还是自己,这种设计方式非常像链表的数据结构。
typedef struct objc_class *Class;
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
}
可以直接用一个实例对象或类对象,直接调用Class
方法,都可以获取Class
对象。我们调用下面三个方法,都可以获得Class
对象。
// 在实例方法中通过self调用class实例方法获取类对象
[self class]
// 通过ViewController类直接调用class类方法获取类对象
[ViewController class]
// 在类方法中使用类对象调用class方法获取类对象
+ (Class)classMethod {
return [self class];
}
-
反射方法
系统Foundation
框架为我们提供了一些方法反射的API
,我们可以通过这些API
执行将字符串转为SEL等操作。由于OC
语言的动态性,这些操作都是发生在运行时的。
// SEL和字符串转换
FOUNDATION_EXPORT NSString *NSStringFromSelector(SEL aSelector);
FOUNDATION_EXPORT SEL NSSelectorFromString(NSString *aSelectorName);
// Class和字符串转换
FOUNDATION_EXPORT NSString *NSStringFromClass(Class aClass);
FOUNDATION_EXPORT Class __nullable NSClassFromString(NSString *aClassName);
// Protocol和字符串转换
FOUNDATION_EXPORT NSString *NSStringFromProtocol(Protocol *proto) NS_AVAILABLE(10_5, 2_0);
FOUNDATION_EXPORT Protocol * __nullable NSProtocolFromString(NSString *namestr) NS_AVAILABLE(10_5, 2_0);
- 常用判断方法
// 当前对象是否这个类或其子类的实例
- (BOOL)isKindOfClass:(Class)aClass;
// 当前对象是否是这个类的实例
- (BOOL)isMemberOfClass:(Class)aClass;
// 当前对象是否遵守这个协议
- (BOOL)conformsToProtocol:(Protocol *)aProtocol;
// 当前对象是否实现这个方法
- (BOOL)respondsToSelector:(SEL)aSelector;
3.UITableViewCell的卡顿你是怎么优化的?
1. cell复用
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *Identifier = @"cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ID];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:ID];
}
return cell;
}
2. cell的高度计算
- 定高的cell,应该采用如下方式:
self.tableView.rowHeight = 88;
对于定高cell,直接采用上面方式给定高度,不需要实现tableView:heightForRowAtIndexPath:
以节省不必要的计算和开销。
-
动态高度的cell
关于这一点可以参考YY大神的博客在YYKit里面的微博demo
是把每条Cell
需要的数据都在后台线程计算并封装为一个布局对象CellLayout
。CellLayout
包含所有文本的CoreText
排版结果、Cell
内部每个控件的高度、Cell
的整体高度。每个CellLayout
的内存占用并不多,所以当生成后,可以全部缓存到内存,以供稍后使用。这样,TableView
在请求各个高度函数时,不会消耗任何多余计算量;当把CellLayout
设置到Cell
内部时,Cell
内部也不用再计算布局了。如果对性能的要求并不那么高,可以尝试用
TableView
的预估高度的功能,并把每个Cell
高度缓存下来。可以参考百度知道团队的开源项目:FDTemplateLayoutCell。
3. 渲染
CALayer
的 border
、圆角、阴影、遮罩(mask
),CASharpLayer
的矢量图形显示,通常会触发离屏渲染(offscreen rendering
),而离屏渲染通常发生在 GPU
中。当一个列表视图中出现大量圆角的 CALayer
,并且快速滑动时,可以观察到 GPU
资源已经占满,而 CPU
资源消耗很少。这时界面仍然能正常滑动,但平均帧数会降到很低。为了避免这种情况,可以尝试开启 CALayer.shouldRasterize
属性,但这会把原本离屏渲染的操作转嫁到 CPU
上去。对于只需要圆角的某些场合,也可以用一张已经绘制好的圆角图片覆盖到原本视图上面来模拟相同的视觉效果。最彻底的解决办法,就是把需要显示的图形在后台线程绘制为图片,避免使用圆角、阴影、遮罩等属性。
4. 减少视图的数目
当多个视图(或者说 CALayer
)重叠在一起显示时,GPU
会首先把他们混合到一起。如果视图结构过于复杂,混合的过程也会消耗很多GPU
资源。为了减轻这种情况的 GPU
消耗,应用应当尽量减少视图数量和层次,并在不透明的视图里标明 opaque
属性以避免无用的 Alpha
通道合成
5. 不要给cell动态添加subView
当视图层次调整时,UIView
、CALayer
之间会出现很多方法调用与通知,所以在优化性能时,应该尽量避免调整视图层次、添加和移除视图。
应该在初始化cell
的时候就添加好,然后根据需要来设置hide
属性显示和隐藏
6. 异步加载
对于网路数据的请求或者图片的加载,我们可以开启多线程,异步话操作
7. 按需加载
可以参考VVeboTableViewDemo
当滑动时,松开手指后,立刻计算出滑动停止时 Cell
的位置,并预先绘制那个位置附近的几个Cell
,而忽略当前滑动中的 Cell
。这个方法比较有技巧性,并且对于滑动性能来说提升也很大,唯一的缺点就是快速滑动中会出现大量空白内容。
8. 异步绘制
Facebook
开源的一个用于保持 iOS
界面流畅的库AsyncDisplayKit
,文本和布局的计算、渲染、解码、绘制都可以通过各种方式异步执行,但UIKit
和 Core Animation
相关操作必需在主线程进行。ASDK
的目标,就是尽量把这些任务从主线程挪走,而挪不走的,就尽量优化性能。YYKit
也参考了ASDK
的原理,实现了一个简单的异步绘制控件YYAsyncLayer,具体可以看 YYLable
和YYText
的调用
评测界面的流畅度
YYKit
里面有个FPSLabel 只有几十行代码,用到了CADisplayLink
来监视 CPU
的卡顿问题。
用 Instuments
的 GPU Driver
预设,能够实时查看到CPU
和 GPU
的资源消耗。在这个预设内,你能查看到几乎所有与显示有关的数据,比如 Texture
数量、CA
提交的频率、GPU
消耗等,在定位界面卡顿的问题时,这是最好的工具。
4.说一下HTTP和HTTPS
一、网络层结构
网络结构有两种主流的分层方式:OSI
七层模型和TCP/IP
四层模型。
OSI七层模型和TCP/IP四层模型
OSI
是指Open System Interconnect
,意为开放式系统互联。
TCP/IP
是指传输控制协议/网间协议,是目前世界上应用最广的协议。
两种模型区别
OSI
采用七层模型,TCP/IP
是四层模型
TCP/IP
网络接口层没有真正的定义,只是概念性的描述。OSI
把它分为2
层,每一层功能详尽。
在协议开发之前,就有了OSI
模型,所以OSI
模型具有共通性,而TCP/IP
是基于协议建立的模型,不适用于非TCP/IP
的网络。
实际应用中,OSI
模型是理论上的模型,没有成熟的产品;而TCP/IP
已经成为国际标准。
二、HTTP协议
Http
是基于TCP/IP
协议的应用程序协议,不包括数据包的传输,主要规定了客户端和服务器的通信格式,默认使用80
端口。
Http协议的发展史
参考链接 阮一峰老师博客
1991
年发布Http/0.9
版本,只有Get
命令,且服务端直返HTML
格式字符串,服务器响应完毕就关闭TCP连接。
1996
年发布Http/1.0
版本,优点:可以发送任何格式内容,包括文字、图像、视频、二进制。也丰富了命令Get
,Post
,Head
。请求和响应的格式加入头信息。缺点:每个TCP
连接只能发送一个请求,而新建TCP
连接的成本很高,导致Http/1.0
性能很差。
1997
发布Http/1.1
版本,完善了Http
协议,直至20
年后的今天仍是最流行的版本。
优点:
- 引入持久连接,
TCP
默认不关闭,可被多个请求复用,对于一个域名,多数浏览器允许同时建立6
个持久连接。 - 引入管道机制,即在同一个
TCP
连接中,可以同时发送多个请求,不过服务器还是按顺序响应。 - 在头部加入
Content-Length
字段,一个TCP
可以同时传送多个响应,所以就需要该字段来区分哪些内容属于哪个响应。 - 分块传输编码,对于耗时的动态操作,用流模式取代缓存模式,即产生一块数据,就发送一块数据。
- 增加了许多命令,头信息增加
Host
来指定服务器域名,可以访问一台服务器上的不同网站。
缺点:TCP
连接中的响应有顺序,服务器处理完一个回应才能处理下一个回应,如果某个回应特别慢,后面的请求就会排队等着( 队头堵塞)。
2015
年发布Http/2
版本,它有几个特性:二进制协议、多工、数据流、头信息压缩、服务器推送。
三、TCP三次握手和四次挥手
TCP三次握手解读:
- 客户端主动打开,发送连接请求报文段,将
SYN
标识位置为1,Sequence Number
置为x
(TCP
规定SYN=1
时不能携带数据,x
为随机产生的一个值),然后进入SYN_SEND
状态 - 服务器收到
SYN
报文段进行确认,将SYN
标识位置为1
,ACK
置为1
,Sequence Number
置为y
,Acknowledgment Number
置为x+1
,然后进入SYN_RECV
状态,这个状态被称为半连接状态 - 客户端再进行一次确认,将
ACK
置为1
(此时不用SYN
),Sequence Number
置为x+1
,Acknowledgment Number
置为y+1
发向服务器,最后客户端与服务器都进入ESTABLISHED
状态
解读:
当客户端没有数据再需要发送给服务端时,就需要释放客户端的连接,这整个过程为:
- 客户端发送一个报文给服务端(没有数据),其中
FIN
设置为1
,Sequence Number
置为u
,客户端进入FIN_WAIT_1
状态 - 服务端收到来自客户端的请求,发送一个
ACK
给客户端,Acknowledge
置为u+1
,同时发送Sequence Number
为v
,服务端年进入CLOSE_WAIT
状态 - 服务端发送一个
FIN
给客户端,ACK
置为1
,Sequence
置为w
,Acknowledge
置为u+1
,用来关闭服务端到客户端的数据传送,服务端进入LAST_ACK
状态 - 客户端收到
FIN
后,进入TIME_WAIT
状态,接着发送一个ACK
给服务端,Acknowledge
置为w+1
,Sequence Number
置为u+1
,最后客户端和服务端都进入CLOSED
状态
ACK:当ACK=1时,确认字段才有效。当ACK=0时,确认号无效。TCP规定,在连接建立后所有传送的报文段都必须把ACK置1。
SYN:连接标识,1表示建立连接,连接请求和连接接受报文段SYN=1,其他情况都是0
FIN:关闭连接标识,1标识关闭连接,关闭请求和关闭接受报文段FIN=1,其他情况都是0,跟SYN类似
seq number:序号,一个随机数X,请求报文段中会有该字段,响应报文段没有
ack number:应答号,值为请求seq+1,即X+1,除了连接请求和连接接受响应报文段没有该字段,其他的报文段都有该字段
为什么TCP建立连接协议是三次握手,而关闭连接却是四次握手呢?
- 服务端接收到客户端的
FIN
,为了表示接收到了,就会向客户端发送ACK - 但此时,服务端可能还在发送数据,并没有关闭
TCP
窗口的意思,所以服务端的FIN
和ACK
并不是同步发的,只有当数据发送完了,才会发送FIN
四、Https协议/SSL协议
Https
协议是以安全为目标的Http
通道,简单来说就是Http
的安全版。主要是在Http
下加入SSL
层(现在主流的是SLL/TLS
),SSL
是Https
协议的安全基础。Https
默认端口号为443
。
Http存在的风险
- 窃听风险:
Http
采用明文传输数据,第三方可以获知通信内容 - 篡改风险:第三方可以修改通信内容
- 冒充风险:第三方可以冒充他人身份进行通信
SSL/TLS协议就是为了解决这些风险而设计,希望达到:
- 所有信息加密传输,避免三方窃听通信内容
- 具有校验机制,内容一旦被篡改,通信双发立刻会发现
- 配备身份证书,防止身份被冒充
SSL原理及运行过程
image.png解读:
- 客户端给出协议版本号、一个客户端随机数
A
(Client random)以及客户端支持的加密方式 - 服务端确认双方使用的加密方式,并给出数字证书、一个服务器生成的随机数
B
(Server random
) - 客户端确认数字证书有效,生成一个新的随机数
C
(Pre-master-secret
),使用证书中的公钥对C
加密,发送给服务端 - 服务端使用自己的私钥解密出
C
- 客户端和服务器根据约定的加密方法,使用三个随机数
ABC
,生成对话秘钥,之后的通信都用这个对话秘钥进行加密。
5.UIViewController 的生命周期
//是在类或者它的子类接受第一条消息前被调用,也就是objc_msgSend()
+ (void)initialize{}
//执行关键数据初始化操作,非StoryBoard
创建UIViewController
都会调用这个方法
//注意: 不要在这里做View
相关操作,View
在loadView
方法中才初始化
- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {}
//使用文件加载的对象调用(如从xib
或stroyboard
中创建)
- (instancetype)initWithCoder:(NSCoder *)aDecoder{}
//从xib
或者storyboard
加载完毕就会调用,调用self.view
会导致viewDidLoad
多加载一次
- (void)awakeFromNib{}
//初始化方法,主要是一些初始化操作
如果不调用- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {}
方法,在此调用,否则在- (void)viewDidLoad{}
方法后调用
- (instancetype)init{}
//创建或加载一个view
并把它赋值给UIViewController
的view
属性。
- (void)loadView{}
//当loadView
将view
载入内存中,会进一步调用viewDidLoad
方法来进行进一步设置。此时,视图层次已经放到内存中,通常,我们对于各种初始化数据的载入,初始设定、修改约束、移除视图等很多操作都可以这个方法中实现。
- (void)viewDidLoad{}
//视图加载完成,并即将显示在屏幕上。还没设置动画,可以改变当前屏幕方向或状态栏的风格等。
- (void)viewWillAppear:(BOOL)animated{}
- (void)viewWillLayoutSubviews{}//即将开始子视图位置布局
- (void)viewDidLayoutSubviews{}//用于通知视图的位置布局已经完成
- (void)viewDidAppear:(BOOL)animated{}//视图已经展示在屏幕上,可以对视图做一些关于展示效果方面的修改
- (void)viewWillDisappear:(BOOL)animated{}//视图即将消失
- (void)viewDidDisappear:(BOOL)animated{}//视图已经消失
- (void)didReceiveMemoryWarning{}//内存不足的警告
- (void)dealloc{}//视图销毁的时候调用
6.说一下@property属性
@property 的本质是什么?
@property = ivar + getter + setter
ivar
就是实例变量,编译器会帮我们自动生成名字为'_属性名'
这样的实例变量,同时也会自动生成getter
和setter
方法。
@property 的修辞关键字
-
atomic(原子性)--默认属性
atomic
的作用只是给getter
和setter
加了个锁,atomic
只能保证代码进入getter
或者setter
函数内部时是安全的,一旦出了getter
和setter
,例如Release 的操作是不会受影响的,多线程安全问题只能自己去额外加锁做同步。另外,atomic
由于加锁也会带来一些性能损耗,所以我们在编写iOS
代码的时候,一般声明property
为nonatomic
。
-
nonatomic(非原子性)
不仅是线程不安全的,也无法保证setter/getter
的完整,多个线程同时访问这个属性,结果无法预计
nonatomic
速度比atomic
速度快,所以通常用nonatomic
。
-
copy ( ARC/MRC )
1、浅拷贝
浅拷贝就是对内存地址的复制,让目标对象指针和源对象指向同一片内存空间,当内存销毁的时候,指向这片内存的几个指针需要重新定义才可以使用,要不然会成为野指针。
浅拷贝就是拷贝指向原来对象的指针,使原对象的引用计数+1,可以理解为创建了一个指向原对象的新指针而已,并没有创建一个全新的对象
2、深拷贝
深拷贝是指拷贝对象的具体内容,而内存地址是自主分配的,拷贝结束之后,两个对象虽然存的值是相同的,但是内存地址不一样,两个对象也互不影响,互不干涉。
深拷贝就是拷贝出和原来仅仅是值一样,但是内存地址完全不一样的新的对象,创建后和原对象没有任何关系
深拷贝就是内容拷贝,浅拷贝就是指针拷贝
在iOS
中深拷贝与浅拷贝要更加的复杂,涉及到容器与非容器、可变与不可变对象的copy
与mutableCopy
,具体可以通过打印对象的内存地址验证,下面是总结
No1:可变对象的
copy
和mutableCopy
方法都是深拷贝(区别完全深拷贝与单层深拷贝)
No2:不可变对象的copy
方法是浅拷贝,mutableCopy
方法是深拷贝。
No3:copy
方法返回的对象都是不可变对象。
-
strong ( ARC )
1.直接赋值并且计数器 +1
.
2.在 ARC 里替代了 retain 的作用 .
-
assign ( ARC/MRC )
1.这个修饰词是直接赋值的意思 , 整型/浮点型等数据类型都用这个词修饰 .
2.如果没有使用 weak strong retain copy
修饰 , 那么默认就是使用 assign
了. ( 它们之间是有你没我的关系 )
3.当然其实对象也可以用 assign
修饰 , 只是对象的计数器不会+1
( 与 strong
的区别 )
4.如果用来修饰对象属性 , 那么当对象被销毁后指针是不会指向 nil
的 . 所以会出现野指针错误 . ( 与weak
的区别 )
-
weak ( ARC )
1.弱指针是针对对象的修饰词 , 就是说它不能修饰基本数据类型,否则会报错 .
Property with 'weak' attribute must be of object type
2.weak
修饰的对象计数器不会+1
, 也就是直接赋值 .
3.弱引用是为打破循环引用而生的 .
4.它最被人所喜欢的原因是 它所指向的对象如果被销毁 , 它会指向nil
. 而 nil
访问什么鬼都不会报野指针错误 .
-
retain ( MRC )
1.release
旧对象( 旧对象计数器 -1
) , retain
新对象( 新对象计数器 +1
) , 然后指向新对象 .
2.在set
方法里面是这样的 :
if (_delegate) {
[_delegate release];
}
_delegate = [delegate retain];
-
readonly
1.让Xcode
只生成get
方法 .
2.不想把暴露的属性被人随便替换时 , 可以使用 .
-
readwrite
1.让 Xcode
生成get/set
方法 .
2.不用readonly
修饰时 , 默认就是 readwrite
.
-
getter/setter 的自定义方法名
1.一般对于 有/无 是/否 等这样的属性 , getter
方法名前面加个 is
会显得通俗易懂 .
7.UIView与CALayer的关系
1.UIView是可以响应事件的,但是CALayer不能响应事件
先看一下类的继承关系
@interface UIView : UIResponder <NSCoding, UIAppearance, UIAppearanceContainer, UIDynamicItem, UITraitEnvironment, UICoordinateSpace, UIFocusItem, UIFocusItemContainer, CALayerDelegate>
@interface CALayer : NSObject <NSSecureCoding, CAMediaTiming>
UIKit
使用UIResponder
作为响应对象,来响应系统传递过来的事件并进行处理。从上面代码的继承关系可以看出,UIView
能响应事件,而CALayer
不能。
2.UIView本身更像是一个CALayer的管理器,访问它的跟绘图和跟坐标有关的属性,例如frame、bounds等,实际上内部都是在访问它所包含的CALayer的相关属性
自定义两个类TestView
和TestLayer
分别集成UIView
和CALyer
。
在TestView中重写以下方法:
- (instancetype)init{
self = [super init];
if (self) {
NSLog(@"%@ --- %s", [self class], __FUNCTION__);
}
return self;
}
+ (Class)layerClass{
NSLog(@"%@ --- %s", [self class], __FUNCTION__);
return [TestLayer class];
}
- (void)setFrame:(CGRect)frame{
NSLog(@"%@ --- %s", [self class], __FUNCTION__);
[super setFrame:frame];
}
- (void)setCenter:(CGPoint)center{
NSLog(@"%@ --- %s", [self class], __FUNCTION__);
[super setCenter:center];
}
- (void)setBounds:(CGRect)bounds{
NSLog(@"%@ --- %s", [self class], __FUNCTION__);
[super setBounds:bounds];
}
在TestLayer中重写以下方法:
- (instancetype)init{
self = [super init];
if (self) {
NSLog(@"%@ --- %s", [self class], __FUNCTION__);
}
return self;
}
+ (Class)layerClass{
NSLog(@"%@ --- %s", [self class], __FUNCTION__);
return [TestLayer class];
}
- (void)setFrame:(CGRect)frame{
NSLog(@"%@ --- %s", [self class], __FUNCTION__);
[super setFrame:frame];
}
- (void)setPosition:(CGPoint)position{
NSLog(@"%@ --- %s", [self class], __FUNCTION__);
[super setPosition:position];
}
- (void)setBounds:(CGRect)bounds{
NSLog(@"%@ --- %s", [self class], __FUNCTION__);
[super setBounds:bounds];
}
然后对TestView做一个简单的初始化调用
TestView *v = [[TestView alloc] init];
通过断点调试,可以看到调用的堆栈信息
可以看到在创建view
的时候会先调用- [TestLayer init]
,然后调用私有方法- [UIView _createLayerWithFrame]
来创建layer
。
去掉断点,运行可以看到如下代码执行顺序
+[TestView layerClass]
-[TestLayer init]
-[TestLayer setBounds:]
-[TestView setFrame:]
-[TestLayer setFrame:]
-[TestLayer setPosition:]
-[TestLayer setBounds:]
-[TestView init]
从结果来看TestView
的init
之前,是调用了TestLayer
的setBounds
、setFrame
、setPosition
方法,改变TestView
的布局,也会调上述TestLayer
的相关方法。
3.在 View显示的时候,UIView 做为 Layer 的 CALayerDelegate,View 的显示内容取决于内部的 CALayer 的display
在TestView中重写方法
- (void)drawRect:(CGRect)rect{
NSLog(@"%@ --- %s", [self class], __FUNCTION__);
[super drawRect:rect];
}
在TestLayer中重写方法
- (void)display{
NSLog(@"%@ --- %s", [self class], __FUNCTION__);
[super display];
}
简单调用
TestView *v = [[TestView alloc] initWithFrame:CGRectMake(10, 100, 50, 50)];
v.backgroundColor = [UIColor redColor];
[self.view addSubview:v];
断点运行
很明显在
view
将要显示的时候,先掉了[TestLayer display]
方法,然后走了[CALayer display]
方法,走[UIView(CALayerDelegate) drawLayer:inContext]
方法时,很明显可以看出UIView
是作为CALayer
的CALayerDelegate
,最后调用[TestView drawRect]
方法实现显示
4.CALayer维护着三个layer tree,分别是presentLayer Tree(动画树)、modeLayer (模型树)Tree、Render Tree(渲染树),在做动画的时候,我们修改动画的属性,其实是修改presentLayer的属性值,而最终展示在界面上的其实是提供UIView的modelLayer。
5.每个UIView默认就包含一个layer属性,两者都有树状层级结构,layer 内部有 SubLayers,View 内部有 SubViews.但是 Layer 比 View 多了个AnchorPoint
8.事件的传递和响应链
事件的种类
在iOS 开发中,常见的事件有三种类型,分别是:
- 触摸事件:平常手指在屏幕上滑动,产生的事件都是触摸事件
- 加速计事件:微信的摇一摇就是典型的加速计事件
- 远程控制事件:耳机控制歌曲上一首、下一首、暂停就是远程控制事件的应用。
事件的传递
- 发生事件后,系统会将该事件加入到一个由UIApplication管理的事件队列中。
- UIApplication会从事件队列中取出最前面的事件,并将该事件分发下去处理。通常,先发送事件给应用程序的主窗口(keywindow)。
- keywindow会在视图层次结构中找到一个最合适的视图来处理事件。
如果父view不能接受触摸事件,那么子view也不能接收到触摸事件。
demo:
image.png寻找最合适的view需要调用两个方法
// 此方法返回的View是本次点击事件需要的最佳View
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
// 判断一个点是否落在范围内
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
在redView
中加上如下代码验证
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
//首先判断是否可以接收事件
if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
//然后判断点是否在当前视图上
if ([self pointInside:point withEvent:event] == NO) return nil;
//循环遍历所有子视图,查找是否有最合适的视图
for (NSInteger i = self.subviews.count - 1; i >= 0; i--) {
UIView *childView = self.subviews[I];
//转换点到子视图坐标系上
CGPoint childPoint = [self convertPoint:point toView:childView];
//递归查找是否存在最合适的view
UIView *fitView = [childView hitTest:childPoint withEvent:event];
//如果返回非空,说明子视图中找到了最合适的view,那么返回它
NSLog(@"%@ --- %s childView:%@ --- fitView:%@", [self class], __FUNCTION__,[childView class],[fitView class]);
if (fitView) {
return fitView;
}
}
//循环结束,仍旧没有合适的子视图可以处理事件,那么就认为自己是最合适的view
return self;
}
当我们点击greenView
的时候可以看到结果如下:
RedView --- -[RedView hitTest:withEvent:] childView:BlueView --- fitView:(null)
RedView --- -[RedView hitTest:withEvent:] childView:GreenView --- fitView:GreenView
也可以通过runtime
黑魔法,hook
系统的方法验证
具体代码实现如下:
#import "UIView+Responder.h"
#import <objc/runtime.h>
@implementation UIView (Responder)
+ (void)load {
Method origin = class_getInstanceMethod([UIView class], @selector(hitTest:withEvent:));
Method custom = class_getInstanceMethod([UIView class], @selector(Responder_hitTest:withEvent:));
method_exchangeImplementations(origin, custom);
origin = class_getInstanceMethod([UIView class], @selector(pointInside:withEvent:));
custom = class_getInstanceMethod([UIView class], @selector(Responder_pointInside:withEvent:));
method_exchangeImplementations(origin, custom);
}
- (UIView *)Responder_hitTest:(CGPoint)point withEvent:(UIEvent *)event {
NSLog(@"%@ hitTest", NSStringFromClass([self class]));
UIView *result = [self Responder_hitTest:point withEvent:event];
NSLog(@"%@ hitTest return: %@", NSStringFromClass([self class]), NSStringFromClass([result class]));
return result;
}
- (BOOL)Responder_pointInside:(CGPoint)point withEvent:(UIEvent *)event {
NSLog(@"%@ pointInside", NSStringFromClass([self class]));
BOOL result = [self Responder_pointInside:point withEvent:event];
NSLog(@"%@ pointInside return: %@", NSStringFromClass([self class]), result ? @"YES":@"NO");
return result;
}
当我们点击greeView
时,可以看到如下结果:
UIWindow hitTest
UIWindow pointInside
UIWindow pointInside return: YES
UIView hitTest
UIView pointInside
UIView pointInside return: YES
RedView hitTest
RedView pointInside
RedView pointInside return: YES
BlueView hitTest
BlueView pointInside
BlueView pointInside return: NO
BlueView hitTest return: (null)
GreenView hitTest
GreenView pointInside
GreenView pointInside return: YES
GreenView hitTest return: GreenView
RedView hitTest return: GreenView
UIView hitTest return: GreenView
UIWindow hitTest return: GreenView
响应链
先看一下如下两张图:
事件响应会先从底层最合适的
view
开始,默认touch
事件会传递给上一层。如果到了viewcontroller
的view
,就会传递给viewcontroller
。如果viewcontroller
不能处理,就会传递给UIWindow
。如果UIWindow
无法处理,就会传递给UIApplication
。如果UIApplication
无法处理,就会传递给UIApplicationDelegate
。如果UIApplicationDelegate
不能处理,则会丢弃该事件。我们可以通过如下代码验证在greenView中加入如下代码
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
NSLog(@"%@ --- %s", [self class], __FUNCTION__);
[super touchesBegan:touches withEvent:event];
UIResponder *nextResponder = self.nextResponder;
NSMutableString *pre = [NSMutableString stringWithString:@"--"];
while (nextResponder) {
NSLog(@"%@%@", pre, NSStringFromClass([nextResponder class]));
[pre appendString:@"--"];
nextResponder = nextResponder.nextResponder;
}
}
点击greenView结果如下:
GreenView --- -[GreenView touchesBegan:withEvent:]
2019-06-22 14:44:15.297923+0800 ResponderTest[9051:2131899] --RedView
2019-06-22 14:44:15.298033+0800 ResponderTest[9051:2131899] ----UIView
2019-06-22 14:44:15.298141+0800 ResponderTest[9051:2131899] ------ViewController
2019-06-22 14:44:15.298247+0800 ResponderTest[9051:2131899] --------UIWindow
2019-06-22 14:44:15.298312+0800 ResponderTest[9051:2131899] ----------UIApplication
2019-06-22 14:44:15.298412+0800 ResponderTest[9051:2131899] ------------AppDelegate
9.如何高性能的给UIImageView加个圆角?
离屏渲染的概念
OpenGL
中,GPU
屏幕渲染有以下两种方式:
-
On-Screen Rendering
意为当前屏幕渲染,指的是GPU的渲染操作是在当前用于显示的屏幕缓冲区中进行。 -
Off-Screen Rendering
意为离屏渲染,指的是GPU在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。
离屏渲染的体现
相比于当前屏幕渲染,离屏渲染的代价是很高的,主要体现在两个方面:
-
创建新缓冲区
要想进行离屏渲染,首先要创建一个新的缓冲区。 -
上下文切换
离屏渲染的整个过程,需要多次切换上下文环境:先是从当前屏幕(On-Screen
)切换到离屏(Off-Screen
);等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上有需要将上下文环境从离屏切换到当前屏幕。而上下文环境的切换是要付出很大代价的。
离屏渲染触发方式
设置了以下属性时,都会触发离屏绘制:
- shouldRasterize(光栅化)
- masks(遮罩)
- shadows(阴影)
- edge antialiasing(抗锯齿)
- group opacity(不透明)
需要注意的是,如果shouldRasterize
被设置成YES
,在触发离屏绘制的同时,会将光栅化后的内容缓存起来,如果对应的layer
及其sublayers
没有发生改变,在下一帧的时候可以直接复用。这将在很大程度上提升渲染性能。
而其它属性如果是开启的,就不会有缓存,离屏绘制会在每一帧都发生。
另一种特殊的“离屏渲染”
按照之前的说法,如果将不在GPU
的当前屏幕缓冲区中进行的渲染都称为离屏渲染,那么就还有另一种特殊的“离屏渲染”方式:CPU
渲染。
如果我们重写了drawRect
方法,并且使用任何Core Graphics
的技术进行了绘制操作,就涉及到了CPU
渲染。整个渲染过程由CPU
在App
内同步地完成,渲染得到的bitmap
最后再交由GPU
用于显示。
关于imageView
设置圆角lz
试了三种方法,并用demo
(tableview设置1000行下图所示,然后滑动)分别测试了三方方法的效果
cornerRadius圆角设置
im.layer.cornerRadius = 15;
im.layer.masksToBounds = YES;
效果如下:
屏幕快照 2019-07-09 下午3.22.05.png
CAShapeLayer圆角设置
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:im.bounds byRoundingCorners:UIRectCornerAllCorners cornerRadii:im.bounds.size];
CAShapeLayer *maskLayer = [[CAShapeLayer alloc]init];
//设置大小
maskLayer.frame = im.bounds;
//设置图形样子
maskLayer.path = maskPath.CGPath;
im.layer.mask = maskLayer;
效果如下:
UIBezierPath 贝塞尔曲线
UIGraphicsBeginImageContextWithOptions(im.bounds.size, NO, 1.0);
//使用贝塞尔曲线画出一个圆形图
[[UIBezierPath bezierPathWithRoundedRect:im.bounds cornerRadius:im.frame.size.width] addClip];
[im drawRect:im.bounds];
im.image = UIGraphicsGetImageFromCurrentImageContext();
//结束画图
UIGraphicsEndImageContext();
效果如下:
10.nil、Nil、NULL、NSNull的区别
1、nil
nil一般是指把一个对象置空,既完全是一个空对象,完全从内存中释放。
id object = nil;
2、Nil
Nil和nil基本没有任何区别,也可以说只要是可以使用nil的地方都可以使用Nil,反之亦然。但是作为程序猿,我们应该更加严谨一些。nil和Nil的区别在于,nil表示置空一个对象,二Nil表示置空一个类。
Class class = Nil;
3、NULL
大家都知道oc 是基于c的,并且oc是完全兼容c的,NULL源于c,表示一个空指针.
int *p = NULL
4、NSNull
NSNull很有意思,大家一般都会觉得,NSNull也是空,但是看着这货又是“NS”开头的很像一个对象,实质上NSNull的确是一个对象,他继承于NSObject。那它和nil的区别在哪里呢?nil是把一对象完全释放,就是完全从内存中释放。但是当我想把一个对象置空但是又想要一个容器的时候,我们就可以使用NSNull。比如一瓶矿泉水,我们不想要里面的水,但是我们想保留瓶子一样。看一下代码的区别
NSMutableArray *mutableArray = [NSMutableArray array];
NSNull *null = [NSNull null];
// 使用NSNull,不会报错会正常运行
[mutableArray addObject:null];
//如果我们使用nil,没运行时会报警告,运行时程序会直接崩溃
[mutableArray addObject:nil];
因为数组存放的都是对象,对象都是有地址的。但是nil在内存中没有地址,所以直接报错
11.如何实现一个线程安全的 NSMutableArray?
先看一个在系统全局并发队列下添加的异步任务中添加数组元素的操作
dispatch_queue_t quene = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSMutableArray *originArray = [NSMutableArray new];
for (int i = 0 ; i < 10000; i++) {
dispatch_async(quene, ^{
[originArray addObject:[NSString stringWithFormat:@"item%d", I]];
});
}
看一下执行结果,对象被重复relaese了
malloc: *** error for object 0x115d05930: pointer being freed was not allocated
malloc: *** set a breakpoint in malloc_error_break to debug
下面是runtime的源码
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
if (offset == 0) {
object_setClass(self, newValue);
return;
}
id oldValue;
id *slot = (id*) ((char*)self + offset);
if (copy) {
newValue = [newValue copyWithZone:nil];
} else if (mutableCopy) {
newValue = [newValue mutableCopyWithZone:nil];
} else {
if (*slot == newValue) return;
newValue = objc_retain(newValue);
}
if (!atomic) {
oldValue = *slot;
*slot = newValue;
} else {
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;
slotlock.unlock();
}
objc_release(oldValue);
}
两个线程同时执行到objc_release(oldValue)的时候,如果一个线程先释放了oldValue,另一个线程在释放就会崩溃。
多个线程同时数组进行大量的读写操作也会导致数组越界的崩溃。
解决方案:
1.对数组的读写都加锁(也可以用其他的同步锁,效率较低)
dispatch_queue_t quene = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSMutableArray *originArray = [NSMutableArray new];
for (int i = 0 ; i < 10000; i++) {
dispatch_async(quene, ^{
@synchronized (originArray) {
[originArray addObject:[NSString stringWithFormat:@"item%d", I]];
}
});
}
2.读的时候我们使用GCD同步机制,写的时候使用GCD的Barrier
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface SafeMutableArray : NSObject
- (void)addObject:(id)anObject;
- (void)insertObject:(id)anObject atIndex:(NSUInteger)index;
- (void)removeLastObject;
- (void)removeObjectAtIndex:(NSUInteger)index;
- (void)replaceObjectAtIndex:(NSUInteger)index withObject:(id)anObject;
- (id)objectAtIndex:(NSUInteger)index;
- (nullable id)getFirstObject;
- (nullable id)getLastObject;
@end
NS_ASSUME_NONNULL_END
#import "SafeMutableArray.h"
@interface SafeMutableArray()
@property (nonatomic, strong) NSMutableArray *array;
@property (nonatomic, strong) dispatch_queue_t readWriteQuene;
@end
@implementation SafeMutableArray
- (instancetype)init {
self = [super init];
if (self) {
_array = [NSMutableArray array];
_readWriteQuene = dispatch_queue_create("com.safeMutableArray.quene", DISPATCH_QUEUE_CONCURRENT);
}
return self;
}
- (void)addObject:(id)anObject {
dispatch_barrier_async(self.readWriteQuene, ^{
[self.array addObject:anObject];
});
}
- (void)insertObject:(id)anObject atIndex:(NSUInteger)index {
dispatch_barrier_async(self.readWriteQuene, ^{
[self.array insertObject:anObject atIndex:index];
});
}
- (void)removeLastObject {
dispatch_barrier_async(self.readWriteQuene, ^{
[self.array removeLastObject];
});
}
- (void)removeObjectAtIndex:(NSUInteger)index {
dispatch_barrier_async(self.readWriteQuene, ^{
[self.array removeObjectAtIndex:index];
});
}
- (void)replaceObjectAtIndex:(NSUInteger)index withObject:(id)anObject {
dispatch_barrier_async(self.readWriteQuene, ^{
[self.array replaceObjectAtIndex:index withObject:anObject];
});
}
- (id)objectAtIndex:(NSUInteger)index {
__block id item = nil;
dispatch_sync(self.readWriteQuene, ^{
if (index <= self.array.count - 1) {
item = [self.array objectAtIndex:index];
}
});
return item;
}
- (nullable id)getFirstObject {
__block id item = nil;
dispatch_sync(self.readWriteQuene, ^{
if (self.array.count > 0) {
item = [self.array objectAtIndex:0];
}
});
return item;
}
- (nullable id)getLastObject {
__block id item = nil;
dispatch_sync(self.readWriteQuene, ^{
NSUInteger size = self.array.count;
if (size > 0) {
item = self.array[size - 1];
}
});
return item;
}
12.实现 isEqual 和 hash 方法时要注意什么?
为什么要有isEqual方法?
示例代码:
UIColor *color1 = [UIColor colorWithRed:0.5 green:0.5 blue:0.5 alpha:1.0];
UIColor *color2 = [UIColor colorWithRed:0.5 green:0.5 blue:0.5 alpha:1.0];
NSLog(@"[color1 内存地址&%p] == [color2 内存地址&%p] = %@",&color1,&color2, color1 == color2 ? @"YES" : @"NO");
NSLog(@"[color1的hash值=%lu isEqual:color2的hash值=%lu] = %@",(unsigned long)[color1 hash],(unsigned long)[color2 hash],[color1 isEqual:color2] ? @"YES" : @"NO");
执行结果如下:
[color1 内存地址&0x16d39dc68] == [color2 内存地址&0x16d39dc60] = NO
[color1的hash值=78020608 isEqual:color2的hash值=78020608] = YES
结论:
1.对于基本类型, ==
运算符比较的是值; 对于对象类型, ==
运算符比较的是对象的地址(即是否为同一对象)
2.isEqual
方法可以判断对象是否相同,如果相同,则两个对象的hash
值相同。
如何重写自己的isEqual方法?
对于Cocoa Framework
中定义的类型, 例如上面例子中的UIColor
, isEqual
方法已经实现好了
常见类型的isEqual
方法还有NSString isEqualToString
/ NSDate isEqualToDate
/ NSArray isEqualToArray
/ NSDictionary isEqualToDictionary
/ NSSet isEqualToSet
但对于自定义类型来说, 通常需要重写isEqual
方法
@interface Person : NSObject
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSDate *lastName;
@end
- (BOOL)isEqual:(id)object {
if (self == object) {
return YES;
}
if (![object isKindOfClass:[Person class]]) {
return NO;
}
return [self isEqualToPerson:(Person *)object];
}
- (BOOL)isEqualToPerson:(Person *)person {
if (!person) {
return NO;
}
BOOL haveEqualFirstName = (!self.firstName && !person.firstName) || [self.firstName isEqualToString:person.firstName];
BOOL haveEqualLastName = (!self.lastName && !person.lastName) || [self.lastName isEqualToDate:person.lastName];
return haveEqualFirstName && haveEqualLastName;
}
上述代码主要步骤如下
Step 1: ==
运算符判断是否是同一对象, 因为同一对象必然完全相同
Step 2: 判断是否是同一类型, 这样不仅可以提高判等的效率, 还可以避免隐式类型转换带来的潜在风险
Step 3: 通过封装的isEqualToPerson
方法, 提高代码复用性
Step 4: 判断对象是否是nil
, 做参数有效性检查
Step 5: 对各个属性分别使用默认判等方法进行判断
Step 6: 返回所有属性判等的与结果
为什么要有hash方法?
在数组未排序的情况下, 查找的时间复杂度是O(n)
。
为了提高查找的速度,Hash Table
出现了。
当成员被加入到Hash Table
中时, 会给它分配一个hash
值, 以标识该成员在集合中的位置,通过这个位置标识可以将查找的时间复杂度优化到O(1)
, 当然如果多个成员都是同一个位置标识, 那么查找就不能达到O(1)了。
分配的这个
hash
值(即用于查找集合中成员的位置标识), 就是通过hash方法计算得来的, 且hash方法返回的hash
值最好唯一
和数组相比, 基于hash
值索引的Hash Table
查找某个成员的过程就是
Step 1: 通过hash
值直接找到查找目标的位置
Step 2: 如果目标位置上有多个相同hash
值得成员, 此时再按照数组方式进行查找
hash什么时候调用?
HashTable是一种基本数据结构,NSSet和NSDictionary都是使用HashTable存储数据的,因此可以可以确保他们查询成员的速度为O(1)。而NSArray使用了顺序表存储数据,查询数据的时间复杂度为O(n)。
NSSet添加新成员时, 需要根据hash值来快速查找成员, 以保证集合中是否已经存在该成员
NSDictionary在查找key时, 也利用了key的hash值来提高查找的效率
hash和isEqual的关系
1、如果两个对象相等,那么他们hash值一定相等
2、如果两个对象hash值相等(hash算法不完美导致),他们不一定相等,还要继续通过isEqual进行判断是否真的相等
如何重写自己的hash方法?
- (NSUInteger)hash {
return [self.firstName hash] ^ [self.lastName hash];
}
对关键属性的hash
值进行位或运算作为hash值
13.id 和 instanceType 有什么区别?
- 相同点
instancetype
和id
都是万能指针,指向对象。
-
不同点:
1.
id
在编译的时候不能判断对象的真实类型,instancetype
在编译的时候可以判断对象的真实类型。2.
id
可以用来定义变量,可以作为返回值类型,可以作为形参类型;instancetype
只能作为返回值类型。
14.iOS的沙盒目录结构是怎样的
前言:
为了安全起见,iOS
应用程序与文件系统的交互仅限于应用程序沙盒目录中的目录(也有例外,比如在用户授权情况下访问通讯录,相册等)
沙盒目录结构
每个APP的沙盒下面都有相似目录结构,如下图所示:
NSString *path = NSHomeDirectory();
上面的代码得到的是应用程序目录的路径,在该目录下有三个文件夹:Documents
、Library
、tmp
以及一个.app
包!该目录下就是应用程序的沙盒,应用程序只能访问该目录下的文件夹!!!
1、Documents 目录:
该目录用于存储用户数据。可被iTunes
和iCloud
备份。(这里不能存缓存文件,否则上架不被通过)
NSString *docPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
2、.app 目录:
这是应用程序的程序包目录,包含应用程序的本身。由于应用程序必须经过签名,所以在运行时不能对这个目录中的内容进行修改,否则可能会使应用程序无法启动。
3、Library 目录:这个目录下有两个子目录:
Preferences 目录:包含应用程序的偏好设置文件。不能直接创建偏好设置文件,而是应该使用NSUserDefaults
类来取得和设置应用程序的偏好.
NSString *libPath = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) lastObject];
Caches 目录:用来存放缓存文件,此文件夹下数据在应用退出时不会删除。备份时不会包括此文件夹。
NSString *cachesPath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject];
4、tmp 目录:
这个目录用于存放临时文件,保存应用程序再次启动过程中不需要的信息。该路径下的文件不会被iTunes
备份。
NSString *tmpDir = NSTemporaryDirectory();
15.SDWebImage 底层实现原理
1.入口 sd_setImageWithURL: placeholderImage:options:
会先把 placeholderImage 显示,然后 SDWebImageManager 根据 URL 开始处理图片。
2.进入 SDWebImageManager->downloadWithURL:options:progress:completed:
交给 SDImageCache 从缓存查找图片是否已经下载。
3.先从内存图片缓存查找是否有图片,如果内存中已经有图片缓存,取缓存,没有从- (UIImage )diskImageForKey:(NSString )key
去磁盘缓存中去查找,根据 URLKey 在硬盘缓存目录下尝试读取图片文件。在磁盘缓存中找到后,同时更新置内存缓存中(如果空闲内存过小,会先清空内存缓存),有回调则调用doneBlock回调。
4.找到了就从SDWebImageQueryCompletedBlock到 UIImageView+WebCache 等前端展示图片。
5.如果从硬盘缓存目录读取不到图片,说明不存在该图片,需要下载图片,共享或重新生成一个下载器 SDWebImageDownloader 开始下载图片。 图片下载由 NSURLSession 来做,实现相关 delegate 来判断图片下载中、下载完成和下载失败。
6.URLSession:didReceiveData: 中利用 ImageIO 做了按图片下载进度加载效果。数据下载完成后交给 SDWebImageDecoder 做图片解码处理。
7.图片解码处理在一个 NSOperationQueue 完成,不会拖慢主线程 UI。如果有需要对下载的图片进行二次处理,最好也在这里完成,效率会好很多。
8.在主线程 SDWebImageDownloaderCompletedBlock里处理解码完成后的操作。回调给需要的地方展示图片。
9.从SDWebImageDownloaderProgressBlock 回调给 SDWebImageManager 告知图片下载信息。
10.将图片保存到 SDImageCache 中,内存缓存和硬盘缓存同时保存。写文件到硬盘也在以单独 NSInvocationOperation 完成,避免拖慢主线程。
11.SDImageCache 在初始化的时候会注册一些消息通知,在内存警告或退到后台的时候清理内存图片缓存,应用结束的时候清理过期图片。
12.SDWebImage 也提供了 UIButton+WebCache 和 MKAnnotationView+WebCache方便使用。 SDWebImagePrefetcher 可以预先下载图片,方便后续使用。
16.Struct和Class的区别?
struct Resolution {
var width = 0
var height = 0
}
class VideoMode {
var resolution = Resolution()
var interlaced = false
var frameRate = 0.0
var name: String?
}
类是引用类型,结构体和枚举是值类型。值类型被赋值给一个变量、常量或者被传递给一个函数的时候,其值会
被拷⻉,而引用类型是对已存在实例的引⽤,不是其拷⻉。
结构体变量分配在栈,而类的实例对象分配在堆,栈的空间相对于堆来说是比较小的,但是存储在栈中的数据访问效率相对于堆而言是比较高。
Swift 中结构体和类有很多共同点。两者都可以:
- 定义属性用于存储值
- 定义方法⽤用于提供功能
- 定义下标操作⽤于通过下标语法访问它们的值
- 定义构造器⽤用于设置初始值
- 通过扩展以增加默认实现之外的功能
- 遵循协议以提供某种标准功能
与结构体相⽐比,类还有如下的附加功能:
- 继承允许一个类继承另一个类的特征
- 类型转换允许在运行时检查和解释一个类实例的类型
- 析构器允许一个类实例释放任何其所被分配的资源
- 引⽤计数允许对一个类的多次引⽤
struct也有这样几个优势:
- 结构较小,适用于复制操作,相比于一个class的实例被多次引用更加安全。
- 无须担心内存memory leak或者多线程冲突问题
17.Swift对比OC的优点?
-
Swift
是类型安全的语言,类型检查在编译期,oc
是动态类型语言,类型检查在运行时,更注重灵活性。 -
Swift
注重面向协议编程、函数式编程、面向对象编程,OC
注重面向对象编程,OC
需要引入ReactiveCocoa
这个库才可支持函数式编程 -
Swift
容易阅读,文件结构和大部分语法简易化,只有.swift
文件,结尾不需要分号 -
Swift
中的可选类型,是用于所有数据类型,而不仅仅局限于类。相比于OC
中的nil
更加安全和简明 -
Swift
新增了两种权限,细化权限。open > public > internal(默认)> fileprivate > private
-
Swift
中独有的元组类型(tuples
),把多个值组合成复合值。元组内的值可以是任何类型,并不要求是相同类型的。 -
Swift
中的泛型类型更加方便和通用,而非OC
中只能为集合类型添加泛型。 -
Swift
中各种方便快捷的高阶函数(函数式编程)(Swift
的标准数组支持三个高阶函数:map
,filter
和reduce
,以及map
的扩展flatMap
) -
Swift
注重值类型,OC
注重指针和引用
18.include与#import的区别、#import 与@class 的区别?
1.#import是OC
导入头文件的关键字,#include是C/C++
导入头文件的关键字
2.#include与#import的效果相同,只是后者不会引起交叉编译,确保头文件只会被导入一次。
3.#include如果想防止头文件重复引用需要进一步处理
例如:test.h
文件
#ifndef test_h
#define test_h
...
#endif /* test_h */
4.#import会包含这个类的所有信息,包括实体变量和方法,而@class只是告诉编译器,其后面声明的名称是类的名称,至于这些类是如何定义的,暂时不用考虑。@class 比#import 编译效率更高。
网友评论