iOS面试题总结
UI视图相关的知识
- UITableview相关问题
-
重用机制
重用机制主要用到了一个可变数组visiableCells和一个可变的字典类型reusableTableCells,其中visiableCells用来存储当前UITableView显示的cell,reusableTableCells用来存储已经用'identify'缓存的cell。当UITableView滚动的时候,会先在reusableTableCells中根据identify找是否有有已经缓存的cell,如果有直接用,没有再去初始化。 -
多线程下的数据源同步
描述:加载更多时,用户删除了一条数据。如何同步数据?
方案1:并发访问、数据拷贝
在主线程中记录删除、更新操作,然后到子线程去同步删除、更新操作,最后回到主线程刷新UI。
缺点:需要记录同步操作,大量数据源的拷贝造成内存的浪费。
方案2:串行访问
把加载数据任务和删除、更新任务放到一个串行队列当中,删除任务要等到子线程的任务完成后才执行,这就保证数据的同步了。
缺点:子线程任务处理时间较长,主线程的删除、更新操作会有延时,造成卡顿现象。
- 事件传递&视图响应
UIView和CALayer区别
UIView为CALayer提供内容,并负责处理触摸事件参与响应链
CALayer负责显示内容contents
事件传递
方法1:-(UIView *)hitTest:(CGPoint)point withEvent:(UIevent *)event;
方法2:- (BOOL)pointInside:(CGPoint)point withEvent:(UIevent *)event;
视图响应
-
图像显示原理
CPU计算好屏幕要显示的内容,将它提交给GPU,GPU开始渲染后将结果放入到帧缓冲区,随后视频控制器读取帧缓冲区的数据经过转换后传递给显示器显示。 -
卡顿&掉帧
原因:在16.7ms时间内,在下一帧VSync信号到来前,CPU和GPU没有完成下一帧画面的合成,就会导致界面的卡顿、掉帧。
优化方案:
CPU:对象的创建,调整和销毁放到子线程。在子线程进行预排版(文字计算,布局计算)。预渲染(文本等的异步绘制,图片编解码等)。
GPU:避免离屏渲染,减少视图层级的复杂性。通过CPU的异步绘制也可以减轻GPU的压力。 -
绘制原理&异步绘制
UIView的绘制原理:
当我们调用[UIView setNeedsDisplay]方法时,实际上并没有立即进行当前视图的绘制工作,而是调用当前视图的layer同名方法[view.layer setNeedsDisplay],然后再当前Runloop结束之后调用[CALayer display]方法,最后才进入真正视图绘制当中。在[CALayer display]内部实现中会先判断layer的delegate是否响应displayLayer方法,不响应则进入系统绘制流程,否则进入异步绘制入口。
系统的绘制流程:
在CALayer内部会创建一个backing store(CGContextRef),然后判断layer是否有代理。没有的话调用[CALayer drawInContext]方法,有就调用[layer.delegate drawLayer:inContext],在一个合适的时机系统会调用[UIView drawRect:]回调方法允许我们做一些其他的相关绘制工作。最后由CALayer 来上传backing store(bitmap)给GPU。
如何实现异步绘制:
实现当前视图layer的delegate的displayLayer方法就可以进入异步绘制流程。在异步绘制过程中需要代理负责生成的bitmap,然后将bitmap设置为layer的contents属性的值。具体步骤首先在子线程中通过CGBitmapContextCreate()创建一个bitmap,通过CoreGraphic对应API来进行当前UI控件的绘制工作,然后通过CGBitmapContextCreateImage()将当前上下文绘制成一张CGImage图片。然后回到主队列当中提交绘制的bitmap,设置给CALayer的contents属性。 -
离屏渲染
含义:当我们设置了UI视图的某些属性(圆角,阴影,遮罩等)在未显示到屏幕之前就会触发离屏渲染。指的是GPU在屏幕缓冲区以外新开辟了一个缓冲区进行渲染操作。
何时回触发:
设置圆角(必须和maskToBounds一起使用)
设置视图的图层蒙版
设置视图的阴影
光栅化的设置
为何避免以及如何避免:
触发离屏渲染的时候,会增加GPU的工作量。有可能导致CPU和GPU的工作总耗时大于16.7ms引起UI的卡顿、掉帧。
避免:
设置圆角
方案1:对控件的主图层进行裁剪,核心代码如下:- (void)setRaidus:(CGFloat)radius view:(UIView *)view { UIImage *image = nil; UIGraphicsBeginImageContextWithOptions(view.bounds.size, NO, [UIScreen mainScreen].scale); CGContextRef currnetContext = UIGraphicsGetCurrentContext(); [[UIBezierPath bezierPathWithRoundedRect:view.bounds cornerRadius:radius] addClip]; [view.layer renderInContext:currnetContext]; image = UIGraphicsGetImageFromCurrentImageContext(); view.layer.contents = (__bridge id _Nullable)(image.CGImage); UIGraphicsEndImageContext(); }
使用UIBezierPath 的 addClip方法,该方法的作用是在当前上下文环境中让闭合路径区域可视化,外部区域不可视。然后再把layer在上下文环境中渲染成一张图片,最后设置到layer.contents中。
这种思路不会额外添加控件,性能是相对比较高,而且当控件父容器的背景颜色为非纯色的时候很好用。
但是有个问题就是每次修改控件的外观(颜色、文字、图片,尺寸)都需要重新对图层进行裁剪,赋值。所以如果想要使用这种方法,那么需要把方法抽成一个分类方法,然后利用KVO监听所有外观属性的变化,当发生改变的时候重新修改图层。
方案2:为控件添加一个遮罩(非layer.mask),核心代码:
- (void)bp_setRaidus:(CGFloat)radius view:(UIView *)view backgroundColor:(UIColor *)color {
CALayer *layer = [CALayer layer];
layer.frame = view.bounds;
[view.layer addSublayer:layer];
UIGraphicsBeginImageContextWithOptions(view.bounds.size, NO, [UIScreen mainScreen].scale);
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:layer.bounds cornerRadius:radius];
UIBezierPath *path1 = [UIBezierPath bezierPathWithRect: layer.bounds];
[color setFill];
[path fill];
[path1 fillWithBlendMode:kCGBlendModeXOR alpha:1];
layer.contents = (__bridge id)(UIGraphicsGetImageFromCurrentImageContext().CGImage);
UIGraphicsEndImageContext();
}
OC语言特性
-
分类
用途:
声明一些私有方法
减少类文件的体积
把Framework的私有方法公开
特点:
运行时决议:在运行时通过runtime为宿主类添加方法属性等
可以为系统类添加分类
添加的内容:实例方法,类方法,协议,属性(不是实例变量) -
关联对象
- (void)setAssociatedObject:(id)object {
objc_setAssociatedObject(self, @selector(associatedObject), object, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (id)associatedObject {
return objc_getAssociatedObject(self, @selector(associatedObject));
}
-
拓展
用途:
声明私有属性
声明私有方法
声明私有成员变量
特点:
编译时决议
只能以声明形式存在
不能为系统类添加拓展 -
代理
是一种软件设计模式,传递方式是一对一的。一般声明为weak来避免循环引用。 -
通知
使用观察者模式来实现的用于跨层传递消息的机制。传递方式是一对多的。
如何实现一个通知:
-
KVC
可以允许开发者通过Key名直接访问对象的属性,或者给对象的属性赋值。而不需要调用明确的存取方法。这样就可以在运行时动态地访问和修改对象的属性。而不是在编译时确定。
如果没有找到Set<Key>方法的话,会按照_key,_iskey,key,iskey的顺序搜索成员并进行赋值操作。 -
KVO
kvo是观察者设计模式的实现,oc采用了isa混写(isa-swizzling)来实现kvo。
原理:
当观察一个对象时,一个新的类会被动态创建。这个类继承自该对象的原本的类,并重写了被观察属性的 setter 方法。重写的 setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象:值的更改。最后通过 isa 混写(isa-swizzling) 把这个对象的 isa 指针 ( isa 指针告诉 Runtime 系统这个对象的类是什么 ) 指向这个新创建的子类,对象就神奇的变成了新创建的子类的实例。
触发方式:
使用setter方法改变值(a.value=1)
使用setValue:forKey改变值([obj setValue:1 forKey:@"key"])
成员变量直接修改不能触发KVO,需要手动调用- (void)test { [self willChangeValueForKey:@"now"]; _now = 1; [self didChangeValueForKey:@"now"]; }
-
属性关键字
Runtime
-
数据结构
objc_objcet:
objc_class:
isa指针:
cache_t:
class_data_bits_t:
class_rw_t:
class_ro_t:
method_t: -
对象、类对象和元类对象
类对象:存储实例方法等信息的数据结构。
元类对象:存储类方法等信息的数据结构。
对象的isa指针指向所属的类
类的isa指针指向了所属的元类
元类的isa指向了根元类,根元类指向了自己。 -
消息传递
objc_msgSend(void id self,SEL op)
objc_msgSendSuper(void struct objc_super *super,SEL op)
接收者会根据isa指针找到接收者自己所属的类,然后在所属类的”方法列表“(method list)中从上向下遍历。如果能找到与选择子名称相符的方法,就根据IMP指针跳转到方法的实现代码,调用这个方法的实现。
如果找不到与选择子名称相符的方法,接收者会根据所属类的superClass指针,沿着类的继承体系继续向上查找(向父类查找),如果 能找到与名称相符的方法,就根据IMP指针跳转到方法的实现代码,调用这个方法的实现。
如果在继承体系中还是找不到与选择子相符的方法,此时就会执行”消息转发(message forwarding)“操作。 -
消息转发
此处输入图片的描述
-
Method-Swizzling
+ (void){ Method origMethod = class_getInstanceMethod(self, origSel_); Method altMethod = class_getInstanceMethod(self, altSel_); class_addMethod(self, origSel_, class_getMethodImplementation(self,origSel_), method_getTypeEncoding(origMethod)); class_addMethod(self, altSel_, class_getMethodImplementation(self,altSel_), method_getTypeEncoding(altMethod)); method_exchangeImplementations(class_getInstanceMethod(self,origSel_), class_getInstanceMethod(self, altSel_)); }
注:1,一般在分类的load方法里执行
2,有个问题 被 hook 的方法必须是当前类自身的方法,如果把继承来的 IMP copy 到自身上面会存在问题。父类的方法应该在调用的时候使用,而不是 swizzling 的时候 copy 到子类。
参考:iOS界的毒瘤-MethodSwizzling
- 动态添加方法
class_addMethod([self class], sel, (IMP)speak, "V@:"); - 动态方法解析
动态方法解析的意思就是,征询消息接受者所属的类,看其是否能动态添加方法,以处理当前“这个未知的选择子(unknown selector)“。实例对象在接受到无法解读的消息后,首先会调用其所属类的下列类方法:
+(BOOL)resolveInstanceMethod:(SEL)selector
类对象在接受到无法解读的消息后,那么运行期系统就会调用另外的一个方法,如下:
+(BOOL)resolveClassMethod:(SEL)selector
如果运行期系统已经执行完了动态方法解析,那么消息接受者自己就无法再以动态新增方法的形式来响应包含该未知选择子的消息了,此时就进入了第二阶段——完整的消息转发。运行期系统会请求消息接受者以其他手段来处理与消息相关的方法调用。
内存管理
- 什么是ARC
arc是LLVM编辑器和Runtime团结协作实现自动引用计数的管理。 - 为甚weak指针指向的对象再被废弃后会被自动置为nil
当对象被废弃之后,dealloc方法内部会调用清除弱引用的方法,该方法会通过哈希算法查找被废弃对象在弱引用表里面的位置获取对应弱引用指针的列表数组,再通过for循环遍历把每一个weak指针置为nil。 - 苹果如何实现AutoreleasePool的
以栈为节点,由双向链表合成的数据结构。 - 遇到的循环引用
NSTimer Block等
Block
- 什么是Block
Block是将函数及其执行上下文封装起来的对象 - 截获变量
对于基本数据类型的局部变量截获值
对于对象类型的局部变量连同所有权修饰符一起截获
以指针形式截获静态局部变量
不截获全局变量、静态全局变量 - __block修饰符
一般情况下,对截获的变量进行赋值操作时需要添加__block修饰符
RunLoop
-
什么是runloop
通过内部维护的事件循环来对事件/消息管理的一个对象。
事件循环:没有消息处理休眠,避免资源占用。有消息要处理立即唤醒 -
什么是mode,为什么有多个
model 主要是用来指定事件在运行循环中的优先级的 -
和线程关系,如何实现一个常驻线程
为当前线程开启一个runloop,向runloop添加Port/Source等维护时间循环,启动runloop -
定时器失效
RunLoop只能运行在一种mode下,如果要换mode,当前的loop也需要停下重启成新的。利用这个机制,ScrollView滚动过程中NSDefaultRunLoopMode(kCFRunLoopDefaultMode)的mode会切换到UITrackingRunLoopMode来保证ScrollView的流畅滑动:只能在NSDefaultRunLoopMode模式下处理的事件会影响ScrollView的滑动。
如果我们把一个NSTimer对象以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)添加到主运行循环中的时候, ScrollView滚动过程中会因为mode的切换,而导致NSTimer将不再被调度。
可以通过将timer添加到NSRunLoopCommonModes(kCFRunLoopCommonModes)来解决。代码如下://将timer添加到NSDefaultRunLoopMode中 [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerTick:) userInfo:nil repeats:YES]; //然后再添加到NSRunLoopCommonModes里 NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerTick:) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
网络相关
- HTTP
请求方式:get post head put delete poitions
特点:无连接,无状态
get和post的区别:
get是用来获取资源的是安全的、幂等的、可缓存的
get是用来处理资源的是不安全的、不幂等的、不可缓存的
持久链接:添加头部字段 connection :keep-live time :20 max : 10
怎么判断一个请求结束:content-length等于服务器返回的最大值 最后一个chunked为空
carles抓包原理:中间人攻击
指攻击者与通讯的两端分别建立独立的联系,并交换其所收到的数据,使通讯的两端认为他们正在通过一个私密的连接与对方直接对话,但事实上整个会话都被攻击者完全控制。在中间人攻击中,攻击者可以拦截通讯双方的通话并插入新的内容。 - tcp的三次握手、四次挥手:
三次握手的目的是连接服务器指定端口,建立 TCP 连接,并同步连接双方的序列号和确认号,交换 TCP 窗口大小信息。在 socket 编程中,客户端执行 connect() 时。将触发三次握手。
- 第一次握手(SYN=1, seq=x):
客户端发送一个 TCP 的 SYN 标志位置1的包,指明客户端打算连接的服务器的端口,以及初始序号 X,保存在包头的序列号(Sequence Number)字段里。
发送完毕后,客户端进入 SYN_SEND 状态。
- 第二次握手(SYN=1, ACK=1, seq=y, ACKnum=x+1):
服务器发回确认包(ACK)应答。即 SYN 标志位和 ACK 标志位均为1。服务器端选择自己 ISN 序列号,放到 Seq 域里,同时将确认序号(Acknowledgement Number)设置为客户的 ISN 加1,即X+1。 发送完毕后,服务器端进入 SYN_RCVD 状态。
- 第三次握手(ACK=1,ACKnum=y+1)
客户端再次发送确认包(ACK),SYN 标志位为0,ACK 标志位为1,并且把服务器发来 ACK 的序号字段+1,放在确定字段中发送给对方,并且在数据段放写ISN的+1
发送完毕后,客户端进入 ESTABLISHED 状态,当服务器端接收到这个包时,也进入 ESTABLISHED 状态,TCP 握手结束。
TCP 的连接的拆除需要发送四个包,因此称为四次挥手(Four-way handshake),也叫做改进的三次握手。客户端或服务器均可主动发起挥手动作,在 socket 编程中,任何一方执行 close() 操作即可产生挥手操作。
- 第一次挥手(FIN=1,seq=x)
假设客户端想要关闭连接,客户端发送一个 FIN 标志位置为1的包,表示自己已经没有数据可以发送了,但是仍然可以接受数据。
发送完毕后,客户端进入 FIN_WAIT_1 状态。
- 第二次挥手(ACK=1,ACKnum=x+1)
服务器端确认客户端的 FIN 包,发送一个确认包,表明自己接受到了客户端关闭连接的请求,但还没有准备好关闭连接。
发送完毕后,服务器端进入 CLOSE_WAIT 状态,客户端接收到这个确认包之后,进入 FIN_WAIT_2 状态,等待服务器端关闭连接。
- 第三次挥手(FIN=1,seq=y)
服务器端准备好关闭连接时,向客户端发送结束连接请求,FIN 置为1。
发送完毕后,服务器端进入 LAST_ACK 状态,等待来自客户端的最后一个ACK。
- 第四次挥手(ACK=1,ACKnum=y+1)
客户端接收到来自服务器端的关闭请求,发送一个确认包,并进入 TIME_WAIT状态,等待可能出现的要求重传的 ACK 包。
服务器端接收到这个确认包之后,关闭连接,进入 CLOSED 状态。
客户端等待了某个固定时间(两个最大段生命周期,2MSL,2 Maximum Segment Lifetime)之后,没有收到服务器端的 ACK ,认为服务器端已经正常关闭连接,于是自己也关闭连接,进入 CLOSED 状态。
- TCP 的特性
TCP 提供一种面向连接的、可靠的字节流服务
在一个 TCP 连接中,仅有两方进行彼此通信。广播和多播不能用于 TCP
TCP 使用校验和,确认和重传机制来保证可靠传输
TCP 给数据分节进行排序,并使用累积确认保证数据的顺序不变和非重复
TCP 使用滑动窗口机制来实现流量控制,通过动态改变窗口的大小进行拥塞控制 - UDP 是一个简单的传输层协议。和 TCP 相比,UDP 有下面几个显著特性:
UDP 缺乏可靠性。UDP 本身不提供确认,序列号,超时重传等机制。UDP 数据报可能在网络中被复制,被重新排序。即 UDP 不保证数据报会到达其最终目的地,也不保证各个数据报的先后顺序,也不保证每个数据报只到达一次
UDP 数据报是有长度的。每个 UDP 数据报都有长度,如果一个数据报正确地到达目的地,那么该数据报的长度将随数据一起传递给接收方。而 TCP 是一个字节流协议,没有任何(协议上的)记录边界。
UDP 是无连接的。UDP 客户和服务器之前不必存在长期的关系。UDP 发送数据报之前也不需要经过握手创建连接的过程。
UDP 支持多播和广播。 - HTTPS 基本过程
- 客户端发送一个 ClientHello 消息到服务器端,消息中同时包含了它的 Transport Layer Security (TLS) 版本,可用的加密算法和压缩算法。
- 服务器端向客户端返回一个 ServerHello 消息,消息中包含了服务器端的 TLS 版本,服务器所选择的加密和压缩算法,以及数字证书认证机构(Certificate Authority,缩写 CA)签发的服务器公开证书,证书中包含了公钥。客户端会使用这个公钥加密接下来的握手过程,直到协商生成一个新的对称密钥。证书中还包含了该证书所应用的域名范围(Common Name,简称 CN),用于客户端验证身份。
- 客户端根据自己的信任 CA 列表,验证服务器端的证书是否可信。如果认为可信(具体的验证过程在下一节讲解),客户端会生成一串伪随机数,使用服务器的公钥加密它。这串随机数会被用于生成新的对称密钥
- 服务器端使用自己的私钥解密上面提到的随机数,然后使用这串随机数生成自己的对称主密钥
- 客户端发送一个 Finished 消息给服务器端,使用对称密钥加密这次通讯的一个散列值
服务器端生成自己的 hash 值,然后解密客户端发送来的信息,检查这两个值是否对应。如果对应,就向客户端发送一个 Finished 消息,也使用协商好的对称密钥加密 - 从现在开始,接下来整个 TLS 会话都使用对称秘钥进行加密,传输应用层(HTTP)内容