IOS面试题
Posted 四叔
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了IOS面试题相关的知识,希望对你有一定的参考价值。
_objc_msgForward是 IMP 类型,用于消息转发的:当向一个对象发送一条消息,但它并没有实现的时候,_objc_msgForward会尝试做消息转发。
我们可以这样创建一个_objc_msgForward对象:
1
|
IMP msgForwardIMP = _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的功能解释:
1
2
3
4
5
6
7
8
9
10
11
12
|
/*********************************************************************** * lookUpImpOrForward. * The standard IMP lookup. * initialize==NO tries to avoid +initialize (but sometimes fails) * cache==NO skips optimistic unlocked lookup (but uses cache elsewhere) * Most callers should use initialize==YES and cache==YES. * inst is an instance of cls or a subclass thereof, or nil if none is known. * If cls is an un-initialized metaclass then a non-nil inst is faster. * May return _objc_msgForward_impcache. IMPs destined for external use * must be converted to _objc_msgForward or _objc_msgForward_stret. * If you don‘t want forwarding at all, use lookUpImpOrNil() instead. **********************************************************************/ |
对 objc-runtime-new.mm文件里与_objc_msgForward有关的三个函数使用伪代码展示下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
// objc-runtime-new.mm 文件里与 _objc_msgForward 有关的三个函数使用伪代码展示 // Created by https://github.com/ChenYilong // Copyright (c) 微博@ios程序犭袁(http://weibo.com/luohanchenyilong/). All rights reserved. // 同时,这也是 obj_msgSend 的实现过程 id objc_msgSend(id self, SEL op, ...) { if (!self) return nil; IMP imp = class_getMethodImplementation(self->isa, SEL op); imp(self, op, ...); //调用这个函数,伪代码... } //查找IMP IMP class_getMethodImplementation(Class cls, SEL sel) { if (!cls || !sel) return nil; IMP imp = lookUpImpOrNil(cls, sel); if (!imp) return _objc_msgForward; //_objc_msgForward 用于消息转发 return imp; } IMP lookUpImpOrNil(Class cls, SEL sel) { if (!cls->initialize()) { _class_initialize(cls); } Class curClass = cls; IMP imp = nil; do { //先查缓存,缓存没有时重建,仍旧没有则向父类查询 if (!curClass) break ; if (!curClass->cache) fill_cache(cls, curClass); imp = cache_getImp(curClass, sel); if (imp) break ; } while (curClass = curClass->superclass); return imp; } |
虽然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是如何进行转发的。
首先开启调试模式、打印出所有运行时发送的消息: 可以在代码里执行下面的方法:
1
|
(void)instrumentObjcMessageSends(YES); |
或者断点暂停程序运行,并在 gdb 中输入下面的命令:
1
|
call (void)instrumentObjcMessageSends(YES) |
以第二种为例,操作如下所示:
之后,运行时发送的所有消息都会打印到/tmp/msgSend-xxxx文件里了。
终端中输入命令前往:
1
|
open /private/tmp |
可能看到有多条,找到最新生成的,双击打开
在模拟器上执行执行以下语句(这一套调试方案仅适用于模拟器,真机不可用,关于该调试方案的拓展链接: Can the messages sent to an object in Objective-C be monitored or printed out? ),向一个对象发送一条错误的消息:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
// // main.m // CYLObjcMsgForwardTest // // Created by http://weibo.com/luohanchenyilong/. // Copyright (c) 2015年 微博@iOS程序犭袁. All rights reserved. // #import #import "AppDelegate.h" #import "CYLTest.h" int main(int argc, char * argv[]) { @autoreleasepool { CYLTest *test = [[CYLTest alloc] init]; [test performSelector:(@selector(iOS程序犭袁))]; return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } } |
你可以在/tmp/msgSend-xxxx(我这一次是/tmp/msgSend-9805)文件里,看到打印出来:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
+ CYLTest NSObject initialize + CYLTest NSObject alloc - CYLTest NSObject init - CYLTest NSObject performSelector: + CYLTest NSObject resolveInstanceMethod: + CYLTest NSObject resolveInstanceMethod: - CYLTest NSObject forwardingTargetForSelector: - CYLTest NSObject forwardingTargetForSelector: - CYLTest NSObject methodSignatureForSelector: - CYLTest NSObject methodSignatureForSelector: - CYLTest NSObject class - CYLTest NSObject doesNotRecognizeSelector: - CYLTest NSObject doesNotRecognizeSelector: - CYLTest NSObject class |
结合《NSObject官方文档》,排除掉 NSObject 做的事,剩下的就是_objc_msgForward消息转发做的几件事:
-
调用resolveInstanceMethod:方法 (或 resolveClassMethod:)。允许用户在此时为该 Class 动态添加实现。如果有实现了,则调用并返回YES,那么重新开始objc_msgSend流程。这一次对象会响应这个选择器,一般是因为它已经调用过class_addMethod。如果仍没实现,继续下面的动作。
-
调用forwardingTargetForSelector:方法,尝试找到一个能响应该消息的对象。如果获取到,则直接把消息转发给它,返回非 nil 对象。否则返回 nil ,继续下面的动作。注意,这里不要返回 self ,否则会形成死循环。
-
调用methodSignatureForSelector:方法,尝试获得一个方法签名。如果获取不到,则直接调用doesNotRecognizeSelector抛出异常。如果能获取,则返回非nil:创建一个 NSlnvocation 并传给forwardInvocation:。
-
调用forwardInvocation:方法,将第3步获取到的方法签名包装成 Invocation 传入,如何处理就在这里面了,并返回非ni。
-
调用doesNotRecognizeSelector: ,默认的实现是抛出异常。如果第3步没能获得一个方法签名,执行该步骤。
上面前4个方法均是模板方法,开发者可以override,由 runtime 来调用。最常见的实现消息转发:就是重写方法3和4,吞掉一个消息或者代理给其他对象都是没问题的
也就是说_objc_msgForward在进行消息转发的过程中会涉及以下这几个方法:
-
resolveInstanceMethod:方法 (或 resolveClassMethod:)。
-
forwardingTargetForSelector:方法
-
methodSignatureForSelector:方法
-
forwardInvocation:方法
-
doesNotRecognizeSelector: 方法
下面回答下第二个问题“直接_objc_msgForward调用它将会发生什么?”
直接调用_objc_msgForward是非常危险的事,如果用不好会直接导致程序Crash,但是如果用得好,能做很多非常酷的事。
就好像跑酷,干得好,叫“耍酷”,干不好就叫“作死”。
正如前文所说:
_objc_msgForward是 IMP 类型,用于消息转发的:当向一个对象发送一条消息,但它并没有实现的时候,_objc_msgForward会尝试做消息转发。
如何调用_objc_msgForward? _objc_msgForward隶属 C 语言,有三个参数 :
首先了解下如何调用 IMP 类型的方法,IMP类型是如下格式:
为了直观,我们可以通过如下方式定义一个 IMP类型 :
1
|
typedef void (*voidIMP)(id, SEL, ...) |
一旦调用_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实现原理详解》详细记录了实现原理,有兴趣可以看下。
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属性”:
1
2
3
4
5
6
7
8
|
// 使用伪代码模拟:runtime如何实现weak属性 id obj1; objc_initWeak(&obj1, obj); /*obj引用计数变为0,变量作用域结束*/ objc_destroyWeak(&obj1); |
下面对用到的两个方法objc_initWeak和objc_destroyWeak做下解释:
总体说来,作用是: 通过objc_initWeak函数初始化“附有weak修饰符的变量(obj1)”,在变量作用域结束时通过objc_destoryWeak函数释放该变量(obj1)。
下面分别介绍下方法的内部实现:
objc_initWeak函数的实现是这样的:在将“附有weak修饰符的变量(obj1)”初始化为0(nil)后,会将“赋值对象”(obj)作为参数,调用objc_storeWeak函数。
1
2
|
obj1 = 0; obj_storeWeak(&obj1, obj); |
也就是说:
weak 修饰的指针默认值是 nil (在Objective-C中向nil发送消息是安全的)
然后obj_destroyWeak函数将0(nil)作为参数,调用objc_storeWeak函数。
1
|
objc_storeWeak(&obj1, 0); |
前面的源代码与下列源代码相同。
1
2
3
4
5
6
7
8
9
|
// 使用伪代码模拟:runtime如何实现weak属性 id obj1; obj1 = 0; objc_storeWeak(&obj1, obj); /* ... obj的引用计数变为0,被置nil ... */ objc_storeWeak(&obj1, 0); |
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 之前,原因同上。
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()函数
1
2
3
4
|
int 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 。
1
|
NSRunLoop *runloop = [NSRunLoop currentRunLoop]; |
参考链接:《Objective-C之run loop详解》。
29. runloop的mode作用是什么?
model 主要是用来指定事件在运行循环中的优先级的,分为:
-
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模式下处理的事件会影响scrllView的滑动。
如果我们把一个NSTimer对象以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)添加到主运行循环中的时候, ScrollView滚动过程中会因为mode的切换,而导致NSTimer将不再被调度。
同时因为mode还是可定制的,所以:
Timer计时会被scrollView的滑动影响的问题可以通过将timer添加到NSRunLoopCommonModes(kCFRunLoopCommonModes)来解决。代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// // http://weibo.com/luohanchenyilong/ (微博@iOS程序犭袁) //将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]; |
31. 猜想runloop内部是如何实现的?
一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出。如果我们需要一个机制,让线程能随时处理事件但并不退出,通常的代码逻辑 是这样的:
1
2
3
4
5
6
7
|
function loop() { initialize(); do { var message = get_next_message(); process_message(message); } while (message != quit); } |
或使用伪代码来展示下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
// // http://weibo.com/luohanchenyilong/ (微博@iOS程序犭袁) int main(int argc, char * argv[]) { //程序一直运行状态 while (AppIsRunning) { //睡眠状态,等待唤醒事件 id whoWakesMe = SleepForWakingUp(); //得到唤醒事件 id event = GetEvent(whoWakesMe); //开始处理事件 HandleEvent(event); } return 0; } |
参考链接:
-
摘自博文CFRunLoop,原作者是微博@我就叫Sunny怎么了
32. objc使用什么机制管理对象内存?
通过 retainCount 的机制来决定对象是否需要释放。 每次 runloop 的时候,都会检查对象的 retainCount,如果retainCount 为 0,说明该对象没有地方需要继续使用了,可以释放掉了。
33. ARC通过什么方式帮助开发者管理内存?
编译时根据代码上下文,插入 retain/release
34. 不手动指定autoreleasepool的前提下,一个autorealese对象在什么时刻释放?(比如在一个vc的viewDidLoad中创建)
分两种情况:手动干预释放时机、系统自动去释放。
-
手动干预释放时机--指定autoreleasepool 就是所谓的:当前作用域大括号结束时释放。
-
系统自动去释放--不手动指定autoreleasepool
Autorelease对象会在当前的 runloop 迭代结束时释放。
如果在一个vc的viewDidLoad中创建一个 Autorelease对象,那么该对象会在 viewDidAppear 方法执行前就被销毁了。
参考链接:《黑幕背后的Autorelease》
35. BAD_ACCESS在什么情况下出现?
访问了野指针,比如对一个已经释放的对象执行了release、访问已经释放对象的成员变量或者发消息。 死循环
36. 苹果是如何实现autoreleasepool的?
autoreleasepool以一个队列数组的形式实现,主要通过下列三个函数完成.
-
objc_autoreleasepoolPush
-
objc_autoreleasepoolPop
-
objc_aurorelease
看函数名就可以知道,对autorelease分别执行push,和pop操作。销毁对象时执行release操作。
37. 使用block时什么情况会发生引用循环,如何解决?
一个对象中强引用了block,在block中又使用了该对象,就会发射循环引用。 解决方法是将该对象使用__weak或者__block修饰符修饰之后再在block中使用。
-
id weak weakSelf = self; 或者 weak __typeof(&*self)weakSelf = self该方法可以设置宏
-
id __block weakSelf = self;
38. 在block内如何修改block外部变量?
默认情况下,在block中访问的外部变量是复制过去的,即:写操作不对原变量生效。但是你可以加上__block来让其写操作生效,示例代码如下:
1
2
3
4
5
6
|
__block int a = 0; void (^foo)(void) = ^{ a = 1; } f00(); //这里,a的值被修改为1 |
参考链接:微博@唐巧_boy的著作《iOS开发进阶》中的第11.2.3章节
39. 使用系统的某些block api(如UIView的block版本写动画时),是否也考虑引用循环问题?
系统的某些block api中,UIView的block版本写动画时不需要考虑,但也有一些api 需要考虑:
所谓“引用循环”是指双向的强引用,所以那些“单向的强引用”(block 强引用 self )没有问题,比如这些:
1
2
3
4
5
6
|
[UIView animateWithDuration:duration animations:^{ [self.superview layoutIfNeeded]; }]; [[NSOperationQueue mainQueue] addOperationWithBlock:^{ self.someProperty = xyz; }]; [[NSNotificationCenter defaultCenter] addObserverForName:@ "someNotification" object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * notification) { self.someProperty = xyz; }]; |
这些情况不需要考虑“引用循环”。
但如果你使用一些参数中可能含有 ivar 的系统 api ,如 GCD 、NSNotificationCenter就要小心一点:比如GCD 内部如果引用了 self,而且 GCD 的其他参数是 ivar,则要考虑到循环引用:
1
2
3
4
5
6
7
|
__weak __typeof__(self) weakSelf = self; dispatch_group_async(_operationsGroup, _operationsQueue, ^ { __typeof__(self) strongSelf = weakSelf; [strongSelf doSomething]; [strongSelf doSomethingElse]; } ); |
类似的:
1
2
3
4
5
6
7
8
|
__weak __typeof__(self) weakSelf = self; _observer = [[NSNotificationCenter defaultCenter] addObserverForName:@ "testKey" object:nil queue:nil usingBlock:^(NSNotification *note) { __typeof__(self) strongSelf = weakSelf; [strongSelf dismissModalViewControllerAnimated:YES]; }]; |
self --> _observer --> block --> self 显然这也是一个循环引用。
40. GCD的队列(dispatch_queue_t)分哪两种类型?
-
串行队列Serial Dispatch Queue
-
并行队列Concurrent Dispatch Queue
41. 如何用GCD同步若干个异步调用?(如根据若干个url异步加载多张图片,然后在都下载完成后合成一张整图)
使用Dispatch Group追加block到Global Group Queue,这些block如果全部执行完毕,就会执行Main Dispatch Queue中的结束处理的block。
1
2
3
4
5
6
7
8
|
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_group_t group = dispatch_group_create(); dispatch_group_async(group, queue, ^{ /*加载图片1 */ }); dispatch_group_async(group, queue, ^{ /*加载图片2 */ }); dispatch_group_async(group, queue, ^{ /*加载图片3 */ }); dispatch_group_notify(group, dispatch_get_main_queue(), ^{ // 合并图片 }); |
42. dispatch_barrier_async的作用是什么?
在并行队列中,为了保持某些任务的顺序,需要等待一些任务完成后才能继续进行,使用 barrier 来等待之前任务完成,避免数据竞争等问题。 dispatch_barrier_async 函数会等待追加到Concurrent Dispatch Queue并行队列中的操作全部执行完之后,然后再执行 dispatch_barrier_async 函数追加的处理,等 dispatch_barrier_async 追加的处理执行结束之后,Concurrent Dispatch Queue才恢复之前的动作继续执行。
打个比方:比如你们公司周末跟团旅游,高速休息站上,司机说:大家都去上厕所,
以上是关于IOS面试题的主要内容,如果未能解决你的问题,请参考以下文章