2019 iOS 面试题

1. KVO 实现机制,缺陷

KVO是通过一种称作isa-swizzling的机制实现的,这个机制会在被观察对象的属性被监听时修改对象的isa指针,让指针指向一个中间类而非对象自身的类。
在监控过程中,KVO生成的新子类需要重写setter的实现,在属性发生修改的上下文插入执行回调的代码:

- (void)setVal: (id)val {
    [self willChangeValueForKey: @"val"];
    [super setVal: val];
    [self didChangeValueForKey: @"val"];
}

参考:分析实现-实现KVO

[参考 KVO 官方文档:]
(https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/KeyValueObserving/Articles/KVOBasics.html#//apple_ref/doc/uid/20002252-BAJEAIEE)

如何自己动手实现 KVO(含缺陷以及替换的方案demo)

2. KVC 替换方案

KVC 实现原理:

  • ①优先调用 set<key> 方法 如果有该方法 在setter 方法中完成设置
  • ②当没有 set 方法时,KVC 机制会检查 +(bool)accessInstanceVariablesDirectly; 是否返回YES。当你重写了该方法并且返回的时 NO 时 KVC 机制会直接执行 setValue:forUndefinedKey: ,这样做可以让你的类不被别人使用 KVC ;
  • ③一般情况开发者不会重写 +(bool)accessInstanceVariablesDirectly 方法 所以 KVC 会搜索有没有名称为 _<key> 的成员变量。无论是在 .h 还是在 .m 部分定义也无论是使用了什么访问修饰符只要存在 _成员变量都可以对其成员变量进行赋值。
  • ④如果该类中没有 set ,也没有 _成员变量,KVC 机制会搜索 _is<key> 成员变量。
  • ⑤如果该类还是没有 _, 也没有 _is, KVC 会继续搜索 <key>, is<key> 再给它们赋值。
  • ⑥如果都不存在,系统将会执行 setValue:forUndefinedKey 抛出异常。

KVC 缺陷:

    1. 硬编码,key 可能不存在,或者尚未创建,引发 crash ;但我们可以通过重写 -(void)setValue:(id)value forUndefinedKey:(NSString *)key 方法来阻止 crash 并自定义报错方式。
    1. KVC需要解析字符串来获取所需要的内容,因此速度较慢。
    1. 其实某些角度而言KVC是一个破坏封装又暴力的做法,而我已经两次因为KVC而导致应用程序出现闪退的情况。
场景:1、使用KVC修改某一个UIView的属性,比如width。
   2、在viewWillAppear使用上面的第1步。
结果:应用程序闪退。
原因:视图的UI还没创建和加载,KVC就对其进行强制赋值操作。空对象无法操作,所以闪退或者崩溃。

3. block 为什么用copy ,几种分类

参考:iOS block几种类型,为什么使用copy修饰

彩蛋:
苹果系统将内存分配分类并使用不同的zone完成内存申请,因此在应用启动之后,每一种类型能够使用的地址范围已经被限定。这导致了即便只有单一类型的内存地址被分配完毕,设备依然有可用内存的情况下,同样会引发OOM问题。

参考:
腾讯开源的 iOS 内存监控组件: iOS OOM(out of memory)检测
iOS 界的毒瘤 method swizzling

4. 分类与扩展的区别,分类如何实现的

  • 分类只能扩充方法,不能扩展属性和成员变量(如果包含成员变量会直接报错)。
  • 如果分类中声明了一个属性,那么分类只会生成这个属性的set、get方法声明,也就是不会有实现。

1.分类是用于给原有类添加方法的,因为分类的结构体指针中,没有属性列表,只有方法列表。所以< 原则上讲它只能添加方法, 不能添加属性(成员变量),实际上可以通过其它方式添加属性> ;
2.分类中的可以写@property, 但不会生成setter/getter方法, 也不会生成实现以及私有的成员变量(编译时会报警告);
3.可以在分类中访问原有类中.h中的属性;
4.如果分类中有和原有类同名的方法, 会优先调用分类中的方法, 就是说会忽略原有类的方法。所以同名方法调用的优先级为 分类 > 本类 > 父类。因此在开发中尽量不要覆盖原有类;
5.如果多个分类中都有和原有类中同名的方法, 那么调用该方法的时候执行谁由编译器决定;编译器会执行最后一个参与编译的分类中的方法。

①类别中原则上只能增加方法(能添加属性的的原因只是通过runtime解决无setter/getter的问题而已);
②类扩展不仅可以增加方法,还可以增加实例变量(或者属性),只是该实例变量默认是@private类型的(
用范围只能在自身类,而不是子类或其他地方);
③类扩展中声明的方法没被实现,编译器会报警,但是类别中的方法没被实现编译器是不会有任何警告的。这是因为类扩展是在编译阶段被添加到类中,而类别是在运行时添加到类中。
④类扩展不能像类别那样拥有独立的实现部分(@implementation部分),也就是说,类扩展所声明的方法必须依托对应类的实现部分来实现。
⑤定义在 .m 文件中的类扩展方法为私有的,定义在 .h 文件(头文件)中的类扩展方法为公有的。类扩展是在 .m 文件中声明私有方法的非常好的方式。

  1. category的方法没有“完全替换掉”原来类已经有的方法,也就是说如果category和原来类都有methodA,那么category附加完成之后,类的方法列表里会有两个methodA
  2. category的方法被放到了新方法列表的前面,而原来类的方法被放到了新方法列表的后面,这也就是我们平常所说的category的方法会“覆盖”掉原来类的同名方法,这是因为运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,就会罢休_,殊不知后面可能还有一样名字的方法。

extension看起来很像一个匿名的category,但是extension和有名字的category几乎完全是两个东西。 extension在编译期决议,它就是类的一部分,在编译期和头文件里的@interface以及实现文件里的@implement一起形成一个完整的类,它伴随类的产生而产生,亦随之一起消亡。extension一般用来隐藏类的私有信息,你必须有一个类的源码才能为一个类添加extension,所以你无法为系统的类比如NSString添加extension。(详见2)

但是category则完全不一样,它是在运行期决议的。 就category和extension的区别来看,我们可以推导出一个明显的事实,extension可以添加实例变量,而category是无法添加实例变量的(因为在运行期,对象的内存布局已经确定,如果添加实例变量就会破坏类的内部布局,这对编译型语言来说是灾难性的)。

1)、类的名字(name)
2)、类(cls)
3)、category中所有给类添加的实例方法的列表(instanceMethods)
4)、category中所有添加的类方法的列表(classMethods)
5)、category实现的所有协议的列表(protocols)
6)、category中添加的所有属性(instanceProperties)

typedef struct category_t {
    const char *name;
    classref_t cls;
    struct method_list_t *instanceMethods;
    struct method_list_t *classMethods;
    struct protocol_list_t *protocols;
    struct property_list_t *instanceProperties;
} category_t;

从category的定义也可以看出category的可为(可以添加实例方法,类方法,甚至可以实现协议,添加属性)和不可为(无法添加实例变量)。

参考链接:深入理解Objective-C:Category

5. assign 关键字用到指针类型会有什么发生

会造成野指针,造成崩溃。

首先我们需要明确,对象的内存一般被分配到堆上,基本数据类型和oc数据类型的内存一般被分配在栈上。
如果用assign修饰对象,当对象被释放后,指针的地址还是存在的,也就是说指针并没有被置为nil,从而造成了野指针。因为对象是分配在堆上的,堆上的内存由程序员分配释放。而因为指针没有被置为nil,如果后续的内存分配中,刚好分配到了这块内存,就会造成崩溃。
而assign修饰基本数据类型或oc数据类型,因为基本数据类型是分配在栈上的,由系统分配和释放,所以不会造成野指针。

6. 通知,代理 ,block 区别

1.NotificationCenter 通知中心:“一对多”,在APP中,很多控制器都需要知道一个事件,应该用通知;
2.delegate 代理委托:“一对一”,对同一个协议,一个对象只能设置一个代理delegate,所以单例对象就不能用代理;代理更注重过程信息的传输:比如发起一个网络请求,可能想要知道此时请求是否已经开始、是否收到了数据、数据是否已经接受完成、数据接收失败
3.block(闭包),block和delegate一样,一般都是“一对一”之间通信交互,相比代理block有以下特点:写法更简练,不需要写protocol、函数等;block注重结果的传输:比如对于一个事件,只想知道成功或者失败,并不需要知道进行了多少或者额外的一些信息;block需要注意防止循环引用.

7. websocket 讲一下

Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。

而 WebSocket 则不同,它是一个完整的 应用层协议,包含一套标准的 API 。所以,从使用上来说,WebSocket 更易用,而 Socket 更灵活。WebSocket API 是 HTML5 标准的一部分, 但这并不代表 WebSocket 一定要用在 HTML 中,或者只能在基于浏览器的应用程序中使用。实际上,许多语言、框架和服务器都提供了 WebSocket 支持。

socket在第五层会话层,它并不是一个协议,而是一组接口(api),更是一个规范,为了方便使用底层协议而存在的一种抽象层。
websocket,http 。。等协议都是应用层协议(更面向于用户),依赖于传输层tcp协议。

websocket 在进行通信时,使用了http进行一次握手,数据传输使用tcp通道传输

socket更像是一种网络编程的概念,是抽象出来的。

根据定义,WebSocket是通过单个TCP连接提供全双工(双向通信)通信信道的计算机通信协议。此WebSocket API可在用户的浏览器和服务器之间进行双向通信。用户可以向服务器发送消息并接收事件驱动的响应,而无需轮询服务器。 它可以让多个用户连接到同一个实时服务器,并通过API进行通信并立即获得响应。

参考:
WebSocket、Socket、TCP、HTTP区别
OSI七层模型详解
为什么引入WebSocket协议

推荐阅读更多精彩内容