42《招聘一个靠谱的iOS》面试题参考答案(下)
来自:01《招聘一个靠谱的iOS》面试题参考答案/《招聘一个靠谱的iOS》面试题参考答案(下)
25 _objc_msgForward
函数是做什么的,直接调用它将会发生什么?
_objc_msgForward
是 IMP 类型,用于消息转发的:当向一个对象发送一条消息,但它并没有实现的时候,_objc_msgForward
会尝试做消息转发。
我们可以这样创建一个_objc_msgForward
对象:
在上篇中的《objc中向一个对象发送消息[obj foo]
和objc_msgSend()
函数之间有什么关系?》曾提到objc_msgSend
在“消息传递”中的作用。在“消息传递”过程中,objc_msgSend
的动作比较清晰:首先在 Class 中的缓存查找 IMP (没缓存则初始化缓存),如果没找到,则向父类的 Class 查找。如果一直查找到根类仍旧没有实现,则用_objc_msgForward
函数指针代替 IMP 。最后,执行这个 IMP 。
Objective-C运行时是开源的,所以我们可以看到它的实现。打开 Apple Open Source 里Mac代码里的obj包 下载一个最新版本,找到 objc-runtime-new.mm
,进入之后搜索_objc_msgForward
。
里面有对_objc_msgForward
的功能解释:
对 objc-runtime-new.mm
文件里与_objc_msgForward
有关的三个函数使用伪代码展示下:
虽然Apple没有公开_objc_msgForward
的实现源码,但是我们还是能得出结论:
_objc_msgForward
是一个函数指针(和 IMP 的类型一样),是用于消息转发的:当向一个对象发送一条消息,但它并没有实现的时候,_objc_msgForward
会尝试做消息转发。在上篇中的《objc中向一个对象发送消息
[obj foo]
和objc_msgSend()
函数之间有什么关系?》曾提到objc_msgSend
在“消息传递”中的作用。在“消息传递”过程中,objc_msgSend
的动作比较清晰:首先在 Class 中的缓存查找 IMP (没缓存则初始化缓存),如果没找到,则向父类的 Class 查找。如果一直查找到根类仍旧没有实现,则用_objc_msgForward
函数指针代替 IMP 。最后,执行这个 IMP 。
为了展示消息转发的具体动作,这里尝试向一个对象发送一条错误的消息,并查看一下_objc_msgForward
是如何进行转发的。
首先开启调试模式、打印出所有运行时发送的消息: 可以在代码里执行下面的方法:
或者断点暂停程序运行,并在 gdb 中输入下面的命令:
以第二种为例,操作如下所示:
之后,运行时发送的所有消息都会打印到 /tmp/msgSend-xxxx
文件里了。
终端中输入命令前往:
可能看到有多条,找到最新生成的,双击打开
在模拟器上执行执行以下语句(这一套调试方案仅适用于模拟器,真机不可用,关于该调试方案的拓展链接: Can the messages sent to an object in Objective-C be monitored or printed out? ),向一个对象发送一条错误的消息:
你可以在/tmp/msgSend-xxxx
(我这一次是/tmp/msgSends-47869
)文件里,看到打印出来:
结合《NSObject官方文档》,排除掉 NSObject 做的事,剩下的就是_objc_msgForward
消息转发做的几件事:
调用
resolveInstanceMethod:
方法 (或resolveClassMethod:
)。允许用户在此时为该 Class 动态添加实现。如果有实现了,则调用并返回YES,那么重新开始objc_msgSend
流程。这一次对象会响应这个选择器,一般是因为它已经调用过class_addMethod
。如果仍没实现,继续下面的动作。调用
forwardingTargetForSelector:
方法,尝试找到一个能响应该消息的对象。如果获取到,则直接把消息转发给它,返回非 nil 对象。否则返回 nil ,继续下面的动作。注意,这里不要返回 self ,否则会形成死循环。调用
methodSignatureForSelector:
方法,尝试获得一个方法签名。如果获取不到,则直接调用doesNotRecognizeSelector
抛出异常。如果能获取,则返回非nil:创建一个NSlnvocation
并传给forwardInvocation:
。调用
forwardInvocation:
方法,将第3步获取到的方法签名包装成 Invocation 传入,如何处理就在这里面了,并返回非nil。调用
doesNotRecognizeSelector:
,默认的实现是抛出异常。如果第3步没能获得一个方法签名,执行该步骤。
上面前4个方法均是模板方法,开发者可以override,由 runtime 来调用。最常见的实现消息转发:就是重写方法3和4,吞掉一个消息或者代理给其他对象都是没问题的。
也就是说_objc_msgForward
在进行消息转发的过程中会涉及以下这几个方法:
resolveInstanceMethod:
方法 (或resolveClassMethod:
)。forwardingTargetForSelector:
方法methodSignatureForSelector:
方法forwardInvocation:
方法doesNotRecognizeSelector:
方法
为了能更清晰地理解这些方法的作用,git仓库里也给出了一个Demo,名称叫 _objc_msgForward_demo,可运行起来看看。
25.1 下面回答下第二个问题“直接_objc_msgForward
调用它将会发生什么?”
直接调用_objc_msgForward
是非常危险的事,如果用不好会直接导致程序Crash,但是如果用得好,能做很多非常酷的事。
就好像跑酷,干得好,叫“耍酷”,干不好就叫“作死”。
正如前文所说:
_objc_msgForward
是 IMP 类型,用于消息转发的:当向一个对象发送一条消息,但它并没有实现的时候,_objc_msgForward
会尝试做消息转发。
如何调用_objc_msgForward
? _objc_msgForward
隶属 C 语言,有三个参数 :
_objc_msgForward 参数 |
类型 | |
---|---|---|
1 | 所属对象 | id类型 |
2 | 方法名 | SEL类型 |
3 | 可变参数 | 可变参数类型 |
首先了解下如何调用 IMP 类型的方法,IMP类型是如下格式:
为了直观,我们可以通过如下方式定义一个 IMP类型 :
一旦调用_objc_msgForward
,将跳过查找 IMP 的过程,直接触发“消息转发”, 如果调用了_objc_msgForward
,即使这个对象确实已经实现了这个方法,你也会告诉objc_msgSend
:
“我没有在这个对象里找到这个方法的实现”
想象下objc_msgSend
会怎么做?通常情况下,下面这张图就是你正常走objc_msgSend
过程,和直接调用_objc_msgForward
的前后差别:
有哪些场景需要直接调用_objc_msgForward
?最常见的场景是:你想获取某方法所对应的 NSInvocation
对象。举例说明:
JSPatch (Github 链接)就是直接调用_objc_msgForward
来实现其核心功能的:
JSPatch 以小巧的体积做到了让JS调用/替换任意OC方法,让iOS APP具备热更新的能力。
作者的博文《JSPatch实现原理详解》详细记录了实现原理,有兴趣可以看下。
同时 RAC(ReactiveCocoa) 源码中也用到了该方法。
26 runtime如何实现weak变量的自动置nil?
runtime 对注册的类,会进行布局,对于 weak 对象会放入一个 hash 表中。 用 weak 指向的对象内存地址作为 key,当此对象的引用计数为0的时候会
dealloc
。假如 weak 指向的对象内存地址是a,那么就会以a为键, 在这个 weak 表中搜索,找到所有以a为键的 weak 对象,从而设置为 nil。
在上篇中的《runtime 如何实现 weak 属性》有论述。(注:在上篇的《使用runtime Associate方法关联的对象,需要在主对象dealloc的时候释放么?》里给出的“对象的内存销毁时间表”也提到__weak
引用的解除时间。)
我们可以设计一个函数(伪代码)来表示上述机制:
objc_storeWeak(&a, b)
函数:
objc_storeWeak
函数把第二个参数–赋值对象(b)的内存地址作为键值key,将第一个参数–weak修饰的属性变量(a)的内存地址(&a)作为value,注册到 weak 表中。如果第二个参数(b)为0(nil),那么把变量(a)的内存地址(&a)从weak表中删除, 你可以把objc_storeWeak(&a, b)
理解为:objc_storeWeak(value, key)
,并且当key变nil,将value置nil。
在b非nil时,a和b指向同一个内存地址,在b变nil时,a变nil。此时向a发送消息不会崩溃:在Objective-C中向nil发送消息是安全的。
而如果a是由assign修饰的,则: 在b非nil时,a和b指向同一个内存地址,在b变nil时,a还是指向该内存地址,变野指针。此时向a发送消息极易崩溃。
下面我们将基于objc_storeWeak(&a, b)
函数,使用伪代码模拟“runtime如何实现weak属性”:
下面对用到的两个方法objc_initWeak
和objc_destroyWeak
做下解释:
总体说来,作用是: 通过objc_initWeak
函数初始化“附有weak修饰符的变量(obj1)”,在变量作用域结束时通过objc_destoryWeak
函数释放该变量(obj1)。
下面分别介绍下方法的内部实现:
objc_initWeak
函数的实现是这样的:在将“附有weak修饰符的变量(obj1)”初始化为0(nil)后,会将“赋值对象”(obj)作为参数,调用objc_storeWeak
函数。
也就是说:
weak 修饰的指针默认值是 nil (在Objective-C中向nil发送消息是安全的)
然后obj_destroyWeak
函数将0(nil)作为参数,调用objc_storeWeak
函数。
前面的源代码与下列源代码相同。
objc_storeWeak
函数把第二个参数–赋值对象(obj)的内存地址作为键值,将第一个参数–weak修饰的属性变量(obj1)的内存地址注册到 weak 表中。如果第二个参数(obj)为0(nil),那么把变量(obj1)的地址从weak表中删除。
27 能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?
- 不能向编译后得到的类中增加实例变量;
- 能向运行时创建的类中添加实例变量;
解释下:
因为编译后的类已经注册在 runtime 中,类结构体中的
objc_ivar_list
实例变量的链表 和instance_size
实例变量的内存大小已经确定,同时runtime 会调用class_setIvarLayout
或class_setWeakIvarLayout
来处理 strong、 weak 引用。所以不能向存在的类中添加实例变量;运行时创建的类是可以添加实例变量,调用
class_addIvar
函数。但是得在调用objc_allocateClassPair
之后,objc_registerClassPair
之前,原因同上。
即在运行时创建的类添加实例变量的代码格式如下:123objc_allocateClassPair//class_addIvarobjc_registerClassPair
28 runloop和线程有什么关系?
总的说来,Run loop,正如其名,loop表示某种循环,和run放在一起就表示一直在运行着的循环。实际上,run loop和线程是紧密相连的,可以这样说run loop是为了线程而生,没有线程,它就没有存在的必要。Run loops是线程的基础架构部分, Cocoa 和 CoreFundation 都提供了 run loop 对象方便配置和管理线程的 run loop(以下都以 Cocoa 为例)。每个线程,包括程序的主线程( main thread )都有与之相应的 run loop 对象。
runloop 和线程的关系:
- 1、 主线程的run loop默认是启动的。
iOS的应用程序里面,程序启动后会有一个如下的main()函数12345int main(int argc, char * argv[]) {@autoreleasepool {return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));}}
重点是UIApplicationMain()
函数,这个方法会为main thread设置一个NSRunLoop
对象,这就解释了:为什么我们的应用可以在无人操作的时候休息,需要让它干活的时候又能立马响应。
2、对其它线程来说,run loop默认是没有启动的,如果你需要更多的线程交互则可以手动配置和启动,如果线程只是去执行一个长时间的已确定的任务则不需要。
3、在任何一个 Cocoa 程序的线程中,都可以通过以下代码来获取到当前线程的 run loop 。
1NSRunLoop *runloop = [NSRunLoop currentRunLoop];
参考链接:《Objective-C之run loop详解》。
29 runloop的mode作用是什么?
mode 主要是用来指定事件在运行循环中的优先级的,分为:
NSDefaultRunLoopMode
(kCFRunLoopDefaultMode
):默认,空闲状态UITrackingRunLoopMode
:ScrollView滑动时UIInitializationRunLoopMode
:启动时NSRunLoopCommonModes
(kCFRunLoopCommonModes
):Mode集合
苹果公开提供的 Mode 有两个:
NSDefaultRunLoopMode
(kCFRunLoopDefaultMode
)NSRunLoopCommonModes
(kCFRunLoopCommonModes
)
30 以+ scheduledTimerWithTimeInterval:...
的方式触发的timer,在滑动页面上的列表时,timer会暂定回调,为什么?如何解决?
RunLoop只能运行在一种mode下,如果要换mode,当前的loop也需要停下重启成新的。利用这个机制,ScrollView滚动过程中NSDefaultRunLoopMode
(kCFRunLoopDefaultMode
)的mode会切换到UITrackingRunLoopMode
来保证ScrollView的流畅滑动:只能在NSDefaultRunLoopMode
模式下处理的事件会影响ScrollView的滑动。
如果我们把一个NSTimer
对象以NSDefaultRunLoopMode
(kCFRunLoopDefaultMode
)添加到主运行循环中的时候, ScrollView滚动过程中会因为mode的切换,而导致NSTimer
将不再被调度。
同时因为mode还是可定制的,所以:
Timer计时会被scrollView的滑动影响的问题可以通过将timer添加到NSRunLoopCommonModes
(kCFRunLoopCommonModes
)来解决。 代码如下:
31 猜想runloop内部是如何实现的?
一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出。如果我们需要一个机制,让线程能随时处理事件但并不退出,通常的代码逻辑 是这样的:
|
|
或使用伪代码来展示下:
参考链接:
- 《深入理解RunLoop》
- 摘自博文CFRunLoop,原作者是微博@我就叫Sunny怎么了
32 objc使用什么机制管理对象内存?
通过 retainCount 的机制来决定对象是否需要释放。 每次 runloop 的时候,都会检查对象的 retainCount,如果retainCount 为 0,说明该对象没有地方需要继续使用了,可以释放掉了。
33 ARC通过什么方式帮助开发者管理内存?
ARC相对于MRC,不是在编译时添加retain/release/autorelease这么简单。应该是编译期和运行期两部分共同帮助开发者管理内存。
在编译期,ARC用的是更底层的C接口实现的retain/release/autorelease,这样做性能更好,也是为什么不能在ARC环境下手动retain/release/autorelease,同时对同一上下文的同一对象的成对retain/release操作进行优化(即忽略掉不必要的操作);ARC也包含运行期组件,这个地方做的优化比较复杂,但也不能被忽略。【TODO:后续更新会详细描述下】
34 不手动指定autoreleasepool
的前提下,一个autorealese
对象在什么时刻释放?(比如在一个vc的viewDidLoad中创建)
分两种情况:手动干预释放时机、系统自动去释放。
- 手动干预释放时机–指定autoreleasepool 就是所谓的:当前作用域大括号结束时释放。
- 系统自动去释放–不手动指定autoreleasepool
Autorelease对象出了作用域之后,会被添加到最近一次创建的自动释放池中,并会在当前的 runloop 迭代结束时释放。
释放的时机总结起来,可以用下图来表示:
下面对这张图进行详细的解释:
从程序启动到加载完成是一个完整的运行循环,然后会停下来,等待用户交互,用户的每一次交互都会启动一次运行循环,来处理用户所有的点击事件、触摸事件。
我们都知道: 所有 autorelease 的对象,在出了作用域之后,会被自动添加到最近创建的自动释放池中。
但是如果每次都放进应用程序的 main.m
中的 autoreleasepool 中,迟早有被撑满的一刻。这个过程中必定有一个释放的动作。何时?在一次完整的运行循环结束之前,会被销毁。
那什么时间会创建自动释放池?运行循环检测到事件并启动后,就会创建自动释放池。
子线程的 runloop 默认是不工作,无法主动创建,必须手动创建。
自定义的 NSOperation
和 NSThread
需要手动创建自动释放池。比如: 自定义的 NSOperation 类中的 main 方法里就必须添加自动释放池。否则出了作用域后,自动释放对象会因为没有自动释放池去处理它,而造成内存泄露。
但对于 blockOperation 和 invocationOperation 这种默认的Operation ,系统已经帮我们封装好了,不需要手动创建自动释放池。
@autoreleasepool
当自动释放池被销毁或者耗尽时,会向自动释放池中的所有对象发送 release
消息,释放自动释放池中的所有对象。
如果在一个vc的viewDidLoad中创建一个 Autorelease对象,那么该对象会在 viewDidAppear 方法执行前就被销毁了。
参考链接:《黑幕背后的Autorelease》