接着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
...
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
...
imp = cache_getImp(cls, sel);
...
if (resolver && !triedResolver) {
runtimeLock.unlockRead();
_class_resolveMethod(cls, sel, inst);
triedResolver = YES ;
goto retry;
}
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
...
#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 ]));
}
}
# -[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)
+ 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和可选参数,使用案例有JSPatch 和ReactiveCocoa 。
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
...
static CFMutableDictionaryRef __CFRunLoops = NULL ;
static CFLock_t loopsLock = CFLockInit ;
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
if (pthread_equal(t, kNilPthreadT)) {
t = pthread_main_thread_np();
}
__CFLock(&loopsLock);
if (!__CFRunLoops) {
__CFUnlock(&loopsLock);
CFMutableDictionaryRef dict = CFDictionaryCreateMutable (kCFAllocatorSystemDefault, 0 , NULL , &kCFTypeDictionaryValueCallBacks);
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
CFDictionarySetValue (dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
if (!OSAtomicCompareAndSwapPtrBarrier(NULL , dict, (void * volatile *)&__CFRunLoops)) {
CFRelease (dict);
}
CFRelease (mainLoop);
__CFLock(&loopsLock);
}
CFRunLoopRef loop = (CFRunLoopRef )CFDictionaryGetValue (__CFRunLoops, pthreadPointer(t));
__CFUnlock(&loopsLock);
if (!loop) {
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFLock(&loopsLock);
loop = (CFRunLoopRef )CFDictionaryGetValue (__CFRunLoops, pthreadPointer(t));
if (!loop) {
CFDictionarySetValue (__CFRunLoops, pthreadPointer(t), newLoop);
loop = newLoop;
}
__CFUnlock(&loopsLock);
CFRelease (newLoop);
}
if (pthread_equal(t, pthread_self())) {
_CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL );
if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
_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();
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
__weak typeof (self ) weakSelf = self ;
dispatch_group_async(_operationGroup, _operationsQueue, ^ {
__strong typeof (weakSelf) strongSelf = weakSelf;
[strongSelf dosSomething];
});
__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, ^{
dispatch_group_async(group, queue, ^{
dispatch_group_async(group, queue, ^{
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
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 = ^{ };
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 ];
[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
- (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