美文网首页
2020 面试题汇总(OC)

2020 面试题汇总(OC)

作者: Abner_XuanYuan | 来源:发表于2021-05-07 17:54 被阅读0次

1 内存结构及相关作用

https://www.jianshu.com/p/c41023ee5ba0

2 NSCache 优于 NSDictionary 的几点

NSCache 是一个容器,通过 key-value 形式存储和查询值,用于临时存储对象。
NSCache 胜过 NSDictionary 之处在于,当系统资源将要耗尽时,它可以自动删减缓存。
NSCache 是线程安全的,NSDictionary 不是。

3 知不知道 Designated Initializer?使用它的时候有什么需要注意的问题?

指定初始化函数,使用原则如下

  1. 类的初始化过程是从子类到父类依次调用 Designated Initializer。
  2. 如果子类指定了新的初始化器,那么在这个初始化器内部必须调用父类的 Designated Initializer。并且需要重写父类的 Designated Initializer,将其指向子类新的初始化器。
  3. 若调用父类的类的 Designated Initializer 方法时,要调用直接父类的Designated Initializer。
  4. 多个Secondary initializers(次要初始化器),它们之间可以任意调用,但最后必须指向 Designated Initializer。在Secondary initializers内不能直接调用父类的初始化器。
  5. 如果有多个不同数据源的 Designated Initializer,那么不同数据源下的 Designated Initializer 应该调用相应的 [super (designated initializer)]。如果父类没有实现相应的方法,则需要根据实际情况来决定是给父类补充一个新的方法还是调用父类其他数据源的 Designated Initializer。

4 数据结构的存储一般常用的有几种?各有什么特点?

1,顺序存储方式
顺序存储方式就是在一块连续的存储区域一个接着一个的存放数据,把逻辑上相连的结点存储在物理位置上相邻的存储单元里.线性存储方式主要用于线性逻辑结构的数据存放.而对于图和树等非线性逻辑结构则不适用.
2,链接存储方式
链接存储方式比较灵活,逻辑上相邻的结点在物理位置上未必相邻.结点间的逻辑关系由附加的引用字段表示,一个结点的引用字段会指向下一个结点的存放位置.一般在原数据项中增加应用类型来表示结点之间的位置关系.
3,索引存储方式
索引存储方式是采用附加索引表的方式来存储结点信息的一种存储方式.索引表由若干个索引项组成.索引存储方式中索引项的一般形式为:(关键字、地址).其中,关键字是能够唯一标识一个结点的数据项.
索引存储方式还可以细分为如下两类:
稠密索引(Dense Index):这种方式中每个结点在索引表中都有一个索引项.其中,索引项的地址指示结点所在的的存储位置.
稀疏索引(Spare Index):这种方式中一组结点在索引表中只对应一个索引项.其中,索引项的地址指示一组结点的起始存储位置.
4, 散列存储方式
散列存储方式是根据结点的关键计算出该结点的存储地址的一种存储的方式.

5 CoreData 的使用,如何处理多线程问题

CoreData 中的 NSManagedObjectContext 在多线程中不安全.多线程访问 CoreData 最好的方法是一个线程一个 NSManagedObjectContext,每个 NSManagedObjectContext 对象实例使用同一个 NSPersistentStoreCoordinator 实例,这个实例可以很安全的顺序访�问永久存储,这是因为 NSManagedObjectContext 会在使用 NSPersistentStoreCoordinator 前上锁.

6 哈希表如何处理冲突

1 链地址法
指把所有的冲突关键字存储在一个线性链表中,这个链表由其散列地址唯一标识.
2 开放定址法
开放地址法通常需要有三种方法:线性探测,二次探测,再哈希法.
2.1 线性探测
线性探测方法就是线性探测空白单元.哈希表越来越满时会产生非常长的探测长度,后续的数据插入将会非常费时.线性探测就是使用算术取余的方法计算余数,当产生冲突时就通过线性递增的方法进行探测,一直到数组的位置为空,插入数据项即可.
2.2 二次探测
二次探测的过程是 x+1,x+4,x+9 等以此类推,二次探测的步数是原始位置相隔的步数的平方.二次探测可以消除在线性探测中产生的聚集问题,但是二次探测也会产生一种更明确更细的聚集.
2.3 再哈希法
再哈希是把关键字用不同的哈希函数再做一遍哈希化,用这个结果作为步长.对指定的关键字,探测的步长是不变的,可以说不同的关键字可以使用不同的步长,并且步长可以控制.
3、再散列法
当发生冲突时,利用另一个哈希函数再次计算一个地址,直到冲突不再发生.
4、建立公共溢出区
一旦由哈希函数得到的地址冲突,就都填入溢出表.

7 方法交换(Swizzling)有什么风险?

Swizzling 要用 dispatch_once 函数避免代码被重复执行.
Swizzling 要在 +load 中执行,要在被交换方法调用之前调用.且不要调用 [super load],可能会出现“Swizzle无效”的假象.

8 方法签名有什么作用?

方法签名的作用是在支持方法重载的语言中标示一个方法的唯一性.(方法名,参数和返回值类型)

9 静态库的原理是什么?你有没有自己写过静态编译库,遇到了哪些问题?

库本质上讲是一种可执行的二进制格式,可以载入内存中执行。是程序代码的集合,共享代码的一种方式。静态库是闭源库,不公开源代码,都是编译后的二进制文件,不暴露具体实现。静态库一般都是以 .a 或者 .framework 形式存在。静态库编译的文件比较大,因为整个函数库的数据都会被整合到代码中,这样的好处就是编译后的程序不需要外部的函数库支持,不好的一点就是如果改变静态函数库,就需要程序重新编译。多次使用就有多份冗余拷贝。
使用静态库的好处:模块化分工合作、可重用、避免少量改动导致大量的重复编译链接。
有静态库自然就有动态库了。这里所谓的静态和动态是相对编译期和运行期的。
静态库在程序编译时会被链接到代码中,程序运行时将不再需要改静态库,而动态库在编译时不会被链接到代码中,只有程序运行时才会被载入,所以做插件都是运用了 runtime 机制,然后动态库注入修改的。
制作 .a 文件时候,要注意 CPU 架构的支持,i386、X86_64、 armv7、armv7s。 查看可以通过命令 “ lipo -info 静态库名称” 。模拟器 .a 文件和真机 .a 文件合并可以通过 "lipo -create 模拟器静态库1名 真机静态库2名 -output 新静态库名称"
使用静态库注意事项
命名要规范。
framework中用到了NSClassFromString,但是转换出来的class 一直为nil。
解决方法:在主工程的【Other Linker Flags】需要添加参数[-ObjC]即可。
如果Xcode找不到框架的头文件,你可能是忘记将它们声明为public了。
解决方法:进入target的Build Phases页,展开Copy Headers项,把需要public的头文件从Project或Private部分拖拽到Public部分。
尽量不要用 xib 。由于静态框架采用静态链接,linker会剔除所有它认为无用的代码。不幸的是,linker不会检查xib文件,因此如果类是在xib中引用,而没有在OC代码中引用,linker将从最终的可执行文件中删除类。这是linker的问题,不是框架的问题(当你编译一个静态库时也会发生这个问题)。苹果内置框架不会发生这个问题,因为他们是运行时动态加载的,存在于iOS设备固件中的动态库是不可能被删除的。
有两个解决的办法:
1、 让框架的最终用户关闭linker的优化选项,通过在他们的项目的Other Linker Flags中添加-ObjC和-all_load。
2、 在框架的另一个类中加一个该类的代码引用。例如,假设你有个MyTextField类,被linker剔除了。假设你还有一个MyViewController,它在xib中使用了MyTextField,MyViewController并没有被剔除。你应该这样做:
在MyTextField中:
+(void)forceLinkerLoad_ {}
在MyViewController中:
+(void)initialize {[MyTextField forceLinkerLoad_];}
他们仍然需要添加-ObjC到linker设置,但不需要强制all_load了。
第2种方法需要你多做一点工作,但却让最终用户避免在使用你的框架时关闭linker优化(关闭linker优化会导致object文件膨胀)。

10 以+ scheduledTimerWithTimeInterval...的方式触发的timer,在滑动页面上的列表时,timer会暂定回调,为什么?如何解决?

主线程的 RunLoop 里有两个预置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode.这两个 Mode 都已经被标记为”Common”属性.DefaultMode 是 App 平时所处的状态,TrackingRunLoopMode 是追踪 ScrollView 滑动时的状态.当你创建一个 Timer 并加到 DefaultMode 时,Timer 会得到重复回调,但此时滑动一个TableView时,RunLoop 会将 mode 切换为 TrackingRunLoopMode,这时 Timer 就不会被回调,并且也会影响到滑动操作.
设置Timer 的 [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

11 判断单链表有没有环及两个无限长度链表(也就是可能有环) 有没有交点

单链表:
用快指针(fast)和慢指针(slow),快指针每次走两个节点,慢指针每次走一个节点,若有环,则快慢指针必然会重合.
两个链表
先判断两个链表是否带环;
1> 若两个都不带环
将一个链表的头节点加在另一个链表的尾节点上,若有环,必相交.
2> 一个带环一个不带环,则肯定不相交.
3> 两个都带环:找到两个入环点r1,r2,环1的入环点为r1,从r1开始遍历一圈,每个结点和r2比较.

12 runtime 中,SEL 和 IMP 的区别

SEL:类成员方法的指针,方法编号.
IMP:函数指针,指向方法的地址.
SEL(方法编号)最终会通过 Dispatch table(存放SEL和IMP的对应) 表寻找到对应的IMP(函数指针)并执行函数.

13 category属性是存储在那里?

我们可以通过 Runtime 的objc_setAssociatedObject、objc_getAssociatedObject 两个方法给category 的属性重写get、set方法,此属性的值保存在 AssociationsManager 里面。

14 AFN2.0为什么添加一条常驻线程?AFN3.0为什么又不需要了?

究其原因是2.x和3.x版本分别使用的是NSURLConnection和NSURLSession.
在2.x版本的AFN中,使用的是 NSURLCollection 进行封装.NSURLConnection 的网络请求是异步发起的,结果的回调在原来线程的 RunLoop 中进行.
在3.x版本的AFN中,由于 NSURLSession 不需要再当前线程等待网络请求得回调结果,所以说在AFN中就不需要常驻线程等待回调,而是交给 NSOperationQueue 来处理,让系统自己来决定是否需要开启新的线程.并且设置 maxConcurrentOperationCount 为1,让回调串行执行.

15 离屏渲染

GPU屏幕渲染有两种方式:

On-Screen Rendering (当前屏幕渲染) :指的是GPU的渲染操作是在当前用于显示的屏幕缓冲区进行.
Off-Screen Rendering (离屏渲染):指的是GPU在当前屏幕缓冲区以外开辟一个缓冲区进行渲染操作.

为什么用到离屏渲染?

有些效果不能直接呈现于屏幕,而需要在别的地方做额外的处理预合成.图层属性的混合体没有预合成之前不能直接在屏幕中绘制,所以就需要离屏渲染.

离屏渲染的代价

创建新的缓冲区:要想进行离屏渲染则首先要先创建新的缓冲区.
上下文切换:离屏渲染的整个过程,需要多次切换上下文环境.先是从当前屏幕切换到离屏,等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上时又需要将上下文环境从离屏切换到当前屏幕.而上下文环境的切换是要付出很大代价的.

会造成离屏渲染的操作

shadows(阴影)
解决:设置阴影后,设置CALayer的 shadowPath

view.layer.shadowPath = [UIBezierPath pathWithCGRect:view.bounds].CGPath;

将图层masksToBounds / masksToBounds
解决:不使用layer.masksToBounds,只是用cornerRadius。或使用Core Graphic。
mask(遮罩)
解决:不实用mask(遮罩),使用混合图层。
allowsGroupOpacity(组不透明)
解决:关闭 allowsGroupOpacity 属性,按产品需求自己控制layer透明度。
edge antialiasing(抗锯齿)
解决:不设置 allowsEdgeAntialiasing 属性为YES(默认为NO)

16 设计一个检测主线程卡顿的方案。

方案一:基于Runloop
主线程绝大部分计算或者绘制任务都是以 Runloop 为单位发生.单次 Runloop 如果时长超过16ms,就会导致UI体验的卡顿.Runloop 的生命周期及运行机制虽然不透明,但苹果提供了一些API去检测部分行为.我们可以通过如下代码监听Runloop每次进入的事件:

- (void)setupRunloopObserver
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        CFRunLoopRef runloop = CFRunLoopGetCurrent();
        
        CFRunLoopObserverRef enterObserver;
        enterObserver = CFRunLoopObserverCreate(CFAllocatorGetDefault(),
                                               kCFRunLoopEntry | kCFRunLoopExit,
                                               true,
                                               -0x7FFFFFFF,
                                               BBRunloopObserverCallBack, NULL);
        CFRunLoopAddObserver(runloop, enterObserver, kCFRunLoopCommonModes);
        CFRelease(enterObserver);
    });
}

static void BBRunloopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
    switch (activity) {
        case kCFRunLoopEntry: {
            NSLog(@"enter runloop...");
        }
            break;
        case kCFRunLoopExit: {
            NSLog(@"leave runloop...");
        }
            break;
        default: break;
    }
}

用 kCFRunLoopExit 减去 kCFRunLoopEntry 的时间,即为一次 Runloop 所耗费的时间.
方案二
用一个 worker 线程每隔一小段时间(delta)ping 一下主线程(发送一个NSNotification),如果主线程此时有空.必然能接收到这个通知,并回复pong(发送另一个NSNotification),如果worker线程超过 delta 时间没有收到 pong 的回复,那么可以推测UI线程必然在处理其他任务了.
具体参考:https://www.jianshu.com/p/c8ee2103ca92

17 说几个你在工作中使用到的线程安全的例子

常见锁

自旋锁:会不断的进行尝试请求.如:OSSpinLock.
互斥锁:对于某一资源同时只允许有一个访问,无论读写,会进入休眠.如:NSLock.
读写锁:对于某一资源同时只允许有一个写访问或多个读访问或读写只能一个.如:pthread_rwlock,dispatch_barrier_async.
条件锁:在满足某个条件的时候进行加锁或者解锁.如:NSConditionLock.
递归锁:可以被一个线程多次获得,而不会引起死锁.它记录了成功获得锁的次数,每一次成功的获得锁,必须有一个配套的释放锁和其对应,这样才不会引起死锁.只有当所有的锁被释放之后,其他线程才可以获得锁.如:NSRecursiveLock

性能对比
性能对比
https://www.jianshu.com/p/9319d0560cf9

18 集合结构 线性结构 树形结构 图形结构

按照元素和元素间的关系
若元素之间没有任何关系则为集合结构,
若元素之间只和另外一个元素有关系则为线性结构,
若一个元素同时和其他几个元素有关系则为树形结构,
若多个元素之间相互有关系则为图形结构。

19 常用的排序算法

https://blog.csdn.net/MLcongcongAI/article/details/88081244

常见的排序算法
常见排序算法
性能比较
性能
直接插入排序
直接插入排序
插入排序

思路:将数组中的所有元素依次跟前面已经排好的元素相比较,如果选择的元素比已排序的元素小,则交换,直到全部元素都比较过。

希尔排序
希尔排序
希尔排序

思路:将待排序数组按照步长gap进行分组,然后将每组的元素利用直接插入排序的方法进行排序;每次将gap折半减小,循环上述操作;当gap=1时,利用直接插入,完成排序。

简单选择排序
简单选择排序
选择排序

思路:比较+交换。
从待排序序列中,找到关键字最小的元素;
如果最小元素不是待排序序列的第一个元素,将其和第一个元素互换;
从余下的 N - 1 个元素中,找出关键字最小的元素,重复(1)、(2)步,直到排序结束。

堆排序
堆排序

思路:堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
过程:
1 将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区;
2 将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];
3 由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。
4 不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。

冒泡排序
冒泡排序
思路:冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
过程:
比较相邻的元素:如果第一个比第二个大,就交换它们两个;
对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
针对所有的元素重复以上的步骤,除了最后一个;
快速排序
快速排序

思路:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
快速排序使用分治法来把一个串(list)分为两个子串(sub-lists),过程:
从数列中挑出一个元素,称为 “基准”(pivot),即枢纽元;
重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边),在这个分区退出之后,该基准就处于数列的中间位置,称为分区(partition)操作;
递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

归并排序
归并排序
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(n·log n)的时间复杂度(稳定)。
过程:
把长度为n的输入序列分成两个长度为n/2的子序列;
对这两个子序列分别采用归并排序;
将两个排序好的子序列合并成一个最终的排序序列。
基数排序
基数排序

基数排序也是非比较的排序算法,又称卡片排序;
对每一位进行排序,从最低位开始排序,复杂度为O(kn), n为数组长度,k为数组中的数的最大的位数;
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。
有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。
基数排序基于分别排序,分别收集,所以是稳定的。
过程:
取得数组中的最大数,并取得位数;
arr为原始数组,从最低位开始取每个位组成radix数组;
对radix进行计数排序(利用计数排序适用于小范围数的特点);
MSD :从高位开始进行排序
LSD :从低位开始进行排序

20 网络七层/五层协议,重点TCP/UDP

https://www.jianshu.com/writer#/notebooks/3859988/notes/15536643/preview

特点

为了通过IP数据报实现可靠传输,需要考虑很多事情,例如数据的破坏、丢包、重复以及分片顺序混乱等问题.如果不解决这些问题,也就无从谈起可靠传输.所以TCP作为面向连接的可靠传输协议有以下特点:面向连接、可靠传输、面向字节流、流量控制、拥塞控制

1 面向连接

TCP是面向连接的协议,它有三次握手建立连接和四次挥手断开连接.
为什么是三次握手?
为了解决请求超时而导致重复建立连接的问题.客户端发送SYN请求建立连接时,如果网络发生延迟超时后客户端会重复发送SYN消息,服务器接收到第二次发送的建立连接请求返回确认信息,连接建立完成.第一次发送SYN建立连接请求经过一段时间后到达服务器,服务器会认为客户端要建立新的连接,会重新发送应答响应.这就导致了重复建立连接的情况.
为什么是四次挥手?
关闭连接时需要双向关闭才算真正的关闭.客户端发送FIN请求切断连接,服务器端发送ACK应答切断,当前连接处于半关闭状态.服务器端向客户端发送FIN请求切断连接,客户端返回ACK应答请求,此时连接才真正的关闭.

2 可靠传输
  1. 通过序列号和确认应答提高可靠性
    当客户端发送数据之后需等待对端的确认信息.如果收到确认应答,表示数据成功发送到对端.如果在一定时间内没有收到确认应答,则认为数据已丢失需要重新发送.未收到应答消息,有两种情况数据丢失,另一种是应答消息丢失.应答信号丢失,重新发送对端就会重复接收相同的数据,为了解决目标端重复接收相同数据,可以通过为发送数据的每个字节标上序号,接收端收到数据根据首部序号和长度,将下一次应该的接收的序号作为应答返回出去.这样就可以通过序列号和确认应答,实现可靠传输.
  2. 重发超时如何确定
    上面提到客户端在超过一定时间没有接收到应答消息会进行数据重发,那么这个超时时长是多少呢?理想状态下,找到一个小的时间,确保应答消息在该时间段中可以返回,但是不同的网络环境下这个时间会发生变化.TCP为了在不同网络环境下都能提供高性能通信,为此每次发送数据都会计算往返时间及其偏差,超时时间比这个往返时间与偏差的和稍大一点的时间.
3 面向字节流

TCP在建立连接时,会确认一次数据包传输的单位,称之为“最大消息长度(MSS)”.建立连接时客户端SYN消息中携带一个MSS值,服务器的返回的SYN中也会携带一个MSS值,最终会选择两者中较小的按个作为数据包的传输单位.

4 滑动窗口流量控制

TCP以一个MSS大小传输数据,往返时间越长,网络的吞吐量也差.为了解决这个问题,引入了窗口的概念.发送端在发送一个段后不需要等待应答信息进行发送数据.窗口大小是指无需等待应答消息继续发送数据的最大值.窗口提供了缓存机制,实现对多个段同时应答的功能.

5 拥塞控制

为了解决大量数据对网络的影响,通过慢启动算法得出的值,对发送数据量进行控制.

UDP

UDP:用户数据报协议,是一个非连接的协议.传输数据之前源端和终端不建立连接,当它想传送时就简单地去抓取应用程序的数据并尽可能的把它扔到网络上.
1、在发送端,UDP传送数据的速度仅仅是受应用程序生成数据的速度、计算机的能力和传输带宽的限制.在接收端,UDP把每个消息段放在队列中,应用程序每次从队列中读一个消息段.
2、 由于传输数据不建立连接,因此也就不需要维护连接状态,包括收发状态等,因此一台服务机可同时向多个客户机传输相同的消息.
3、UDP信息包的标题很短,只有8个字节,相对于TCP的20个字节信息包的额外开销很小.
4、吞吐量不受拥挤控制算法的调节,只受应用软件生成数据的速率、传输带宽、源端和终端主机性能的限制.
5、UDP使用尽最大努力交付,即不保证可靠交付,因此主机不需要维持复杂的链接状态表.
6、UDP是面向报文的.发送方的UDP对应用程序交下来的报文,在添加首部后就向下交付给IP层.既不拆分也不合并,而是保留这些报文的边界,因此,应用程序需要选择合适的报文大小.

总结

1、TCP 是面向连接的,UDP 是面向无连接的.故 TCP 需要建立连接和断开连接,UDP 不需要.
2、TCP 是流协议,UDP 是数据包协议.故 TCP 数据没有大小限制,UDP 数据报有大小限制.
3、TCP 是可靠协议,UDP 是不可靠协议.故 TCP 会处理数据丢包重发以及乱序等情况,UDP 则不会处理.

21 面向对象编程的六大原则

-> 单一职责:即一个类只负责一项职责.
-> 里氏替换原则:子类可以扩展父类的功能,但不能改变父类原有的功能.
-> 依赖倒置:尽量面向接口编程.
-> 接口隔离:接口功能单一化.
-> 迪米特法则:高内聚,低耦合.
-> 开闭原则:尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化.

22 OC为什么不能实现多继承?

因为OC的消息机制,名字查找发生在运行时,而不是编译时,不能解决多个基类的二义性.

23 分类

//分类的定义

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;

分类可以添加实例方法,类方法,协议,属性,而不能添加实例变量,因为没有存储实例变量列表的指针.
注:虽然结构中有属性,但是源码中并没有生成属性的set/get方法,需要使用关联对象手动实现.

24 Category 中是否有 +load 方法?+load 什么时候调用?+load 方法可以继承么?

Category 中也有 load 方法,和类中 load 方法不同的是它不是简单的继承或者覆盖,而是独立的 load 方法,和类中的 load 方法没有关系.
在 runtime 加载时调用 load 方法,调用顺序:父类-->子类-->分类
load 方法是不可以继承的,因为load方法不是通过消息传递(_objc_msgSend)方式调用的,是直接通过函数指针调用的.因此 load 方法不存在类的层级遍历.

25 OC中 load 方法和 initialize 的方法有什么区别?

-> load 方法不能继承,是通过函数指针调用,runtime运行时调用,较早
-> initialize 方法可以继承,是通过消息传递调用的,第一次收到消息时调用,较晚

26 autoreleasepool

-> autoreleasepool 底层是使用了AutoreleasePoolPage 来管理.AutoreleasePoolPage 是一个双向的链表,每个 AutoreleasePoolPage 都有4096个字节,用来存放 autorelease 对象的地址.
-> 在 autoreleasepool 开始的时候,会调用 AutorelasePoolPage 的 push 方法,会将一个标识 POOL_BOUNDARY 添加到 AutoreleasePoolPage 对象里面并且返回 POOL_BOUNDARY 的地址r.
-> 当对像进行 relase 的时候,会将对象的地址依次添加到当前 AutorelasePoolPage 里.
-> 当 autoreleasepool 作用域结束的时候,会调用 AutorelasePoolPage 的 pop(r),AutorelasePoolPage 则会将里面保存的对象从最后一个开始进行 release 操作,当碰到 r 的时候,标识当前 autoreleasepool 里面所有的对象都进行了一次 release 操作.

相关文章

网友评论

      本文标题:2020 面试题汇总(OC)

      本文链接:https://www.haomeiwen.com/subject/lumvuhtx.html