接着iOS面试题收录(一)继续刷面试题!

21._objc_msgForward函数是做什么的,直接调用它将会发生什么?

_objc_msgForward是void函数指针,也是IMP函数指针(typedef void (*IMP)(void /* id, SEL, ... */ );),在消息传递过程中,如果最终没找到方法的IMP,并且在resolve阶段也没有提供一个方法实现,则会指定_objc_msgForward为方法的IMP去执行以实现完整的消息转发机制。

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
// objc-runtime-new.mm
...
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
...
imp = cache_getImp(cls, sel);
...
// No implementation found. Try method resolver once.
if (resolver && !triedResolver) {
runtimeLock.unlockRead();
_class_resolveMethod(cls, sel, inst);
// Don't cache the result; we don't hold the lock so it may have
// changed already. Re-do the search from scratch instead.
triedResolver = YES;
goto retry;
}
// No implementation found, and method resolver didn't help.
// Use forwarding.
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
...
}
...

通过断点暂停程序执行并在lldb中输入call (void)instrumentObjcMessageSends(YES)命令,之后可在/tmp/msgSend-xxxx文件(可在终端输入open /private/tmp前往相应路径)中查看运行时发送的所有消息,包括了消息转发中的相关方法。以下为实测结果:

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
36
37
38
39
40
41
42
43
44
45
// main.m
...
#import "Test.h"
int main(int argc, char * argv[]) {
@autoreleasepool {
Test *test = [[Test alloc] init];
[test performSelector:@selector(monkiyang)];// 此处打断点
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
// lldb
# -[Test monkiyang]: unrecognized selector sent to instance 0x600000000ee0
# *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Test monkiyang]: unrecognized selector sent to instance 0x600000000ee0'
# *** First throw call stack:
# (
# 0 CoreFoundation 0x0000000109a07b0b __exceptionPreprocess + 171
# 1 libobjc.A.dylib 0x00000001090d3141 objc_exception_throw + 48
# 2 CoreFoundation 0x0000000109a77134 -[NSObject(NSObject) doesNotRecognizeSelector:] + 132
# 3 CoreFoundation 0x000000010998e840 ___forwarding___ + 1024
# 4 CoreFoundation 0x000000010998e3b8 _CF_forwarding_prep_0 + 120
# 5 test 0x00000001080fc7b5 main + 101
# 6 libdyld.dylib 0x000000010cfc665d start + 1
# 7 ??? 0x0000000000000001 0x0 + 1
# )
# libc++abi.dylib: terminating with uncaught exception of type NSException
# (lldb) call (void)instrumentObjcMessageSends(YES)
// msgSends-9816
+ Test NSObject initialize
+ Test NSObject alloc
- Test NSObject init
- Test NSObject performSelector:
+ Test NSObject resolveInstanceMethod:
+ Test NSObject resolveInstanceMethod:
- Test NSObject forwardingTargetForSelector:
- Test NSObject forwardingTargetForSelector:
- Test NSObject methodSignatureForSelector:
- Test NSObject methodSignatureForSelector:
- Test NSObject class
- Test NSObject doesNotRecognizeSelector:
- Test NSObject doesNotRecognizeSelector:
- Test NSObject class
...

直接调用_objc_msgForward是非常危险的事情,用不好的话程序就会崩溃。因为直接调用_objc_msgForward,会跳过查找IMP的过程,触发消息转发,而消息转发没实现的话最终就会导致doesNotRecognizeSelector异常。_objc_msgForward函数的参数包括id receciver、SEL sel和可选参数,使用案例有JSPatchReactiveCocoa

22.runloop和线程有什么关系?

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// CFRunLoop.c
...
static CFMutableDictionaryRef __CFRunLoops = NULL;
static CFLock_t loopsLock = CFLockInit;
// should only be called by Foundation
// t==0 is a synonym for "main thread" that always works
//根据线程取RunLoop
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
if (pthread_equal(t, kNilPthreadT)) {
t = pthread_main_thread_np();
}
__CFLock(&loopsLock);
//如果存储RunLoop的字典不存在
if (!__CFRunLoops) {
__CFUnlock(&loopsLock);
//创建一个临时字典dict
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
//创建主线程的RunLoop
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
//把主线程的RunLoop保存到dict中,key是线程,value是RunLoop
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
//此处NULL和__CFRunLoops指针都指向NULL,匹配,所以将dict写到__CFRunLoops
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
//释放dict
CFRelease(dict);
}
//释放mainrunloop
CFRelease(mainLoop);
__CFLock(&loopsLock);
}
//以上说明,第一次进来的时候,不管是getMainRunloop还是get子线程的runloop,主线程的runloop总是会被创建
//从字典__CFRunLoops中获取传入线程t的runloop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFUnlock(&loopsLock);
//如果没有获取到
if (!loop) {
//根据线程t创建一个runloop
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFLock(&loopsLock);
//把newLoop存入字典__CFRunLoops,key是线程t
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
if (!loop) {
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
loop = newLoop;
}
// don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
__CFUnlock(&loopsLock);
CFRelease(newLoop);
}
//如果传入线程就是当前线程
if (pthread_equal(t, pthread_self())) {
_CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
//注册一个回调,当线程销毁时,销毁对应的RunLoop
_CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
}
}
return loop;
}
...

根据从CFRunloop.c中摘录的代码看出,Runloop和线程是一一对应的,对应方式为键值对(线程:Runnloop)方式保存在一个全局字典中;主线程的Runnloop会在初始化全局字典时创建;子线程的Runloop会在首次获取不到时创建;Runloop会在线程销毁时销毁。

23.runloop的mode作用是什么?

RunLoop中的mode包含了一个name,若干source0、source1、timer、observer和port,在RunLoop运行过程中只有某一个mode会被处理,因此mode会指定这些事件在RunLoop中的优先级。Cocoa和Core Foundation框架定义了一些标准mode,如下:
NSDefaultRunLoopMode(Cocoa)/kCFRunLoopDefaultMode(Core Foundation),默认mode,主线程的RunLoop默认在该mode下运行;
GSEventReceiveRunLoopMode(Cocoa),接收系统内部事件;
UIInitializationRunLoopMode(Cocoa),程序初始化时运行在该mode下;
UITrackingRunLoopMode(Cocoa),追踪触摸手势,确保界面刷新不会卡顿,滑动tableview、scrollview等都运行在该mode下;
NSRunLoopCommonModes(Cocoa)/kCFRunLoopCommonModes(Core Foundation),标记为common的mode集合。(可指定name和添加事件来自定义一个mode添加至_commonModes)

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

因为RunLoop只能运行在一种mode下,如果要换mode,当前的loop也需要停下并重启一个新的。在这个机制下,当scrollview滚动时NSDefaultRunLoopMode(kCFRunLoopDefaultMode)会切换到UITrackingRunLoopMode来保证scrollview的流畅滑动。以+scheduledTimerWithTimeInterval…方式触发的timer是以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)添加到主RunLoop中的,所以在滑动页面上的列表时,timer会暂停回调。解决方案是将timer添加到NSRunLoopCommonModes(kCFRunLoopCommonModes)中,代码如下:

1
2
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(monkiyang:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

25.猜想runloop内部是如何实现的?

一般来说,一个线程一次只能执行一个任务,执行完成后线程就会退出。如果我们需要一个机制,让线程能随时处理事情但并不退出,通常的代码逻辑是这样的:

1
2
3
4
5
6
7
function loop() {
initialize();
do {
var message = get_next_message();
process_message(message);
} while (message != quit);
}

26.objc使用什么机制管理对象内存?

通过retainCount(引用计数)机制来决定对象是否需要释放。每次RunLoop时,都会检查对象的retainCount,如果retainCount为0,说明该对象没有地方需要继续使用了,可以释放掉。

27.ARC通过什么方式帮助开发者管理内存?

ARC是基于MRC的,会在编译期间通过插入对应的内存管理代码(如retain、release等)来管理对象的内存分配和释放。

28.不手动指定autoreleasepool的前提下,一个autorealese对象在什么时刻释放?(比如在一个vc的viewDidLoad中创建)

一个对象发送autorelease消息,就是将这个对象加入到当前的自动释放池(实质是由若干个AutoreleasePoolPage对象以双向链表的形式组合而成)中,在不指定@autoreleasepool{}的前提下,这个对象会在当前RunLoop迭代结束时释放,因为系统在每个RunLoop迭代中都加入了自动释放池Push和Pop。

1
2
3
void *context = objc_autoreleasePoolPush();// 哨兵对象(0,即nil)地址
// {}中的代码
objc_autoreleasePoolPop(context);

29.BAD_ACCESS在什么情况下出现?

访问野指针(对一个已经释放的对象发消息),死循环。

30.苹果是如何实现autoreleasepool的?

AutoreleasePool实质是由若干个AutoreleasePoolPage以双向链表的形式组织而成,对应着每个AutoreleasePoolPage中都包含一个parent指针和child指针,其内部的thread指针指向当前线程,每个AutoreleasePoolPage对象会开辟4096字节内存(也就是虚拟内存一页的大小),除了实例变量所占空间,剩余空间全部用来存储autorelease对象的地址,其内部的next指针作为游标指向栈顶最新add进来的autorelease对象的下一个位置,如果一个AutoreleasePoolPage的空间被占满,会新建一个AutoreleasePoolPage对象,连接链表,后来的autorelease对象会加入新的page中。如果一个对象发送autorelease消息,就是将这个对象加入到当前AutoreleasePoolPage的栈顶next指针指向的位置。每当调用一次objc_autoreleasePoolPush时,会向当前的AutoreleasePoolPage中加入一个哨兵对象(0,即nil),此函数返回值就是这个哨兵对象的地址,被objc_autoreleasePoolPop(哨兵对象)作为入参,于是根据传入的哨兵对象地址找到所处的page,将晚于哨兵对象插入的所有autorelease对象发送release消息,并向栈底移动next指针,可跨越多个page,直到哨兵所处的page。

31.使用block时什么情况会发生引用循环,如何解决?

当一个对象强引用了block,而在block中又强引用了该对象,就会发生循环引用。通过使用weak修饰这个对象(weak typeof(self) weakSelf = self)或者将其中一方强制置空(比如在viewDidAppear中将self.block = nil)。

32.在block内如何修改block外部变量?

使用_block修饰可实现修改block外部变量,_block所起的作用是只要观察到该变量被block所持有,就将外部变量(纯量/指针类型)在栈中的内存地址放到了堆中,进而在block内部也可以修改外部变量的值。验证如下:

1
2
3
4
5
6
7
8
9
10
11
12
__block int a = 0;
NSLog(@"定义前:%p", &a); //栈区
void (^foo)(void) = ^{
a = 1;
NSLog(@"block内部:%p", &a); //堆区
};
NSLog(@"定义后:%p", &a); //堆区
foo();
# 定义前:0x7fff51b7e078
# 定义后:0x60800002f3b8
# block内部:0x60800002f3b8

34.使用系统的某些block api(如UIView的block版本写动画时),是否也考虑引用循环问题?

所谓引用循环是指双向的强引用,所以那些单向的强引用(block强引用self)是没有问题的,比如UIView的block版本写动画[UIView animateWithDuration:duration animations:^{[self.superview layoutIfNeeded]}]等等,但如果你使用一些参数中可能含有实例变量的系统api或者返回值赋给实例变量时,需要考虑引用循环:

1
2
3
4
5
6
7
8
9
10
11
12
13
//场景1
__weak typeof(self) weakSelf = self;
dispatch_group_async(_operationGroup, _operationsQueue, ^ {
__strong typeof(weakSelf) strongSelf = weakSelf;
[strongSelf dosSomething];
});
//场景2
__weak typeof(self) weakSelf = self;
_observer = [[NSNotificationCenter defaultCenter] addObserverForName:@"monkiyang" object:nil queue:nil usingBlock:^(NSNotification *notification) {
__strong typeof(weakSelf) strongSelf = self;
[strongSelf doSomething];
}];

35.GCD的队列(dispatch_queue_t)分哪两种类型?

串行队列(Serial Dispatch Queue)和并行队列(Concurrent Dispatch Queue)

36.如何用GCD同步若干个异步调用?(如根据若干个url异步加载多张图片,然后在都下载完成后合成一张整图)

使用dispatch_group_async追加block到并行队列中,全部执行完毕通过dispatch_group_notify在其block中结束处理。

1
2
3
4
5
6
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, ^{// TODO: 加载图片1});
dispatch_group_async(group, queue, ^{// TODO: 加载图片2});
dispatch_group_async(group, queue, ^{// TODO: 加载图片3});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{// TODO: 合并图片});

37.dispatch_barrier_async的作用是什么?

dispatch_barrier_async会等待之前追加到并行队列中的操作全部执行完毕之后,再执行dispatch_barrier_async追加的处理,等dispatch_barrier_async追加的处理执行结束之后,才恢复队列中之后操作的并行处理。(注意:使用dispatch_barrier_async,只能搭配自定义并行队列dipatch_queue_t,不能通过dispatch_get_global_queue获取,否则dispatch_barrier_async不起作用,追加的操作一样并行处理)

38.苹果为什么要废弃dispatch_get_current_queue?

dispatch_get_current_queue并不能解决死锁问题,场景如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
dispatch_queue_t queueA = dispatch_queue_create("com.monkiyang.queueA", NULL);
dispatch_queue_t queueB = dispatch_queue_create("com.monkiyang.queueB", NULL);
dispatch_sync(queueA, ^{
dispatch_sync(queueB, ^{
dispatch_block_t block = ^{ /* ... */ };
// queueB != queueA
if (dispatch_get_current_queue() == queueA) {
block();
} else {
// 死锁
dispatch_sync(queueA, block);
}
})
})

39.以下代码运行结果如何?

1
2
3
4
5
6
7
8
9
- (void)viewDidLoad
{
[super viewDidLoad];
NSLog(@"1");
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"2");
});
NSLog(@"3");
}

主线程死锁。

40.addObserver:forKeyPath:options:context:各个参数的作用分别是什么,observer中需要实现哪个方法才能获得KVO回调?

各个参数分别是观察者、观察的属性、观察的选项、上下文,回调方法是- (void)observerValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context

41.如何手动触发一个value的KVO

键值观察通知(KVO)依赖于NSObject的两个方法:willChangeValueForKey:didChangeValueForKey:,在一个被观察属性发生改变之前,willChangeValueForKey:会被调用并记录了旧值,而当改变发生后,observeValueForKey:ofObject:change:context:会被调用,继而调用didChangeValueForKey:。如果实现了这些调用,便可手动触发一个value的KVO,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
...
@property (nonatomic, copy) NSString *test;
...
- (void)viewDidLoad {
[super viewDidLoad];
_test = @"test";
[self addObserver:self forKeyPath:@"test" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
//will..., did...必须成对存在
[self willChangeValueForKey:@"test"];
_test = @"hello monkiyang";
[self didChangeValueForKey:@"test"];
_test = @"test";
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"keyPath=%@, object=%@, change.old=%@, change.new=%@", keyPath, object, change[NSKeyValueChangeOldKey], change[NSKeyValueChangeNewKey]);
}

42.若一个类有实例变量 NSString *_foo ,调用setValue:forKey:时,可以以foo还是 _foo 作为key?

都可以。

1
2
3
4
5
6
7
8
9
10
11
12
13
@interface ViewController() {
NSString *_foo;
}
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self setValue:@"monkiyang" forKey:@"foo"];
[self setValue:@"test" forKey:@"_foo"];
}
@end

43.KVC的keyPath中的集合运算符如何使用?

必须用在集合对象上或普通对象的集合属性上,简单集合运算符包括@avg、@count、@max、@min、@sum,格式为@”@sum.age”或@“集合属性.@max.age”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 自定义Person类,包括一个NSInteger类型的age
- (void)viewDidLoad {
Person *person = [[Person alloc] init];
person.age = 18;
Person *person1 = [[Person alloc] init];
person1.age = 20;
Person *person2 = [[Person alloc] init];
person2.age = 22;
self.persons = @[person, person1, person2];
NSInteger sum = [[_persons valueForKeyPath:@"@sum.age"] integerValue];
NSInteger avg = [[_persons valueForKeyPath:@"@avg.age"] integerValue];
NSLog(@"sum=%ld, avg=%ld", sum, avg);
}

44.KVC和KVO的keyPath一定是属性么?

KVC支持实例变量,KVO只能手动支持实例变量的监听。

45.如何关闭默认的KVO的默认实现,并进入自定义的KVO实现?

通过重写+ (BOOL)automaticallyNotifiesObserversForKey:方法对相应属性的返回为NO即可关闭自动触发的键值观察通知,为了实现手动观察通知,你需要在值改变前调用willChangeValueForKey:和值改变后调用didChangeValueForKey:(当你改变多个属性值时,可以嵌套这些调用)。如果是实现集合类型属性中的值发生改变的通知,则要调用willChange:valueAtIndexes:ForKey:didChange:valueAtIndexes:ForKey:,不仅要指定key还要指定change的类型和改变的那些值的索引集合。

46.apple用什么方式实现对一个对象的KVO?

当你观察一个对象时,运行时会创建一个新的类,这个类继承自该对象的类,并重写了被观察属性的setter方法,在重写的setter方法中会在调用原setter方法之前和之后分别调用willChangeValueForKey:didChangeValueForKey:,以实现通知所有观察对象值的改变,最后通过isa交换(isa swizzling)把这个对象的isa指针指向这个新创建的子类,这样对象就变成了新创建的子类的实例。苹果还重写了class方法并返回原来的类,借此欺骗我们。

47.IBOutlet连出来的视图属性为什么可以被设置成weak?

因为IBOutlet连出来的视图是属于某个视图层级中的,最终会被控制器的view所包含,当xib/storyboard文件加载进内存中时,初始化的控制器便强引用了view,而后随着视图层级各自强引用了其子视图,包括IBOutlet连出来的视图,所以它的属性可以被设置为weak。

48.IB中User Defined Runtime Attributes如何使用?

它是通过KVC的方式配置一些IB未提供的属性,当IB被加载了时,会发送相应的消息[xxx setValue:xxx forKeyPath:xxx],这个对象的KeyPath必须要存在,否则会报错。

49.如何调试BAD_ACCESS错误

通过Xcode设置Diagnostics->Enable Zombie Objects,或者设置全局断点捕捉异常。

50.lldb(gdb)常用的调试命令?

po 打印对象;
n 跳至下一步;
更多调试命令可查看the LLDB Debugger

参考资料

《招聘一个靠谱的iOS》面试题参考答案(下)
RunLoop系列之源码分析 by YEVEN
Core Foundation源码下载地址
黑幕背后的Autorelease