2020-iOS最新面试题解析(原理篇) - 图1

runtime怎么添加属性、方法等

  • ivar表示成员变量
  • class_addIvar
  • class_addMethod
  • class_addProperty
  • class_addProtocol
  • class_replaceProperty

是否可以把比较耗时的操作放在NSNotificationCenter中

  • 首先必须明确通知在哪个线程中发出,那么处理接受到通知的方法也在这个线程中调用
  • 如果在异步线程发的通知,那么可以执行比较耗时的操作;
  • 如果在主线程发的通知,那么就不可以执行比较耗时的操作

runtime 如何实现 weak 属性

首先要搞清楚weak属性的特点

  1. weak策略表明该属性定义了一种“非拥有关系” (nonowning relationship)。
  2. 为这种属性设置新值时,设置方法既不保留新值,也不释放旧值。此特质同assign类似;
  3. 然而在属性所指的对象遭到摧毁时,属性值也会清空(nil out)

那么runtime如何实现weak变量的自动置nil?

  1. runtime对注册的类,会进行布局,会将 weak 对象放入一个 hash 表中。
  2. weak 指向的对象内存地址作为 key,当此对象的引用计数为0的时候会调用对象的 dealloc 方法,
  3. 假设 weak 指向的对象内存地址是a,那么就会以akey,在这个 weak hash表中搜索,找到所有以akey weak 对象,从而设置为 nil

weak属性需要在dealloc中置nil么

  • 在ARC环境无论是强指针还是弱指针都无需在 dealloc 设置为 nil , ARC 会自动帮我们处理
  • 即便是编译器不帮我们做这些,weak也不需要在dealloc中置nil
  • 在属性所指的对象遭到摧毁时,属性值也会清空
  1. // 模拟下weak的setter方法,大致如下
  2. - (void)setObject:(NSObject *)object
  3. {
  4. objc_setAssociatedObject(self, "object", object, OBJC_ASSOCIATION_ASSIGN);
  5. [object cyl_runAtDealloc:^{
  6. _object = nil;
  7. }];
  8. }

一个Objective-C对象如何进行内存布局?(考虑有父类的情况)

  • 所有父类的成员变量和自己的成员变量都会存放在该对象所对应的存储空间中
  • 父类的方法和自己的方法都会缓存在类对象的方法缓存中,类方法是缓存在元类对象中
  • 每一个对象内部都有一个isa指针,指向他的类对象,类对象中存放着本对象的如下信息
    • 对象方法列表
    • 成员变量的列表
    • 属性列表
  • 每个 Objective-C 对象都有相同的结构,如下图所示 | Objective-C 对象的结构图 | | —- | | ISA指针 | | 根类(NSObject)的实例变量 | | 倒数第二层父类的实例变量 | | … | | 父类的实例变量 | | 类的实例变量 |
  • 根类对象就是NSObject,它的super class指针指向nil
  • 类对象既然称为对象,那它也是一个实例。类对象中也有一个isa指针指向它的元类(meta class),即类对象是元类的实例。元类内部存放的是类方法列表,根元类的isa指针指向自己,superclass指针指向NSObject类

一个objc对象的isa的指针指向什么?有什么作用?

  • 每一个对象内部都有一个isa指针,这个指针是指向它的真实类型
  • 根据这个指针就能知道将来调用哪个类的方法

下面的代码输出什么?

  1. @implementation Son : Father
  2. - (id)init
  3. {
  4. self = [super init];
  5. if (self) {
  6. NSLog(@"%@", NSStringFromClass([self class]));
  7. NSLog(@"%@", NSStringFromClass([super class]));
  8. }
  9. return self;
  10. }
  11. @end
  • 答案:都输出 Son
  • 这个题目主要是考察关于objc中对 self 和 super 的理解:

    • self 是类的隐藏参数,指向当前调用方法的这个类的实例。而 super 本质是一个编译器标示符,和 self 是指向的同一个消息接受者
    • 当使用 self 调用方法时,会从当前类的方法列表中开始找,如果没有,就从父类中再找;
    • 而当使用 super时,则从父类的方法列表中开始找。然后调用父类的这个方法
    • 调用[self class] 时,会转化成 objc_msgSend函数

      1. id objc_msgSend(id self, SEL op, ...)
    • 调用 [super class]时,会转化成 objc_msgSendSuper函数

      1. id objc_msgSendSuper(struct objc_super *super, SEL op, ...)
    • 第一个参数是 objc_super 这样一个结构体,其定义如下

      1. struct objc_super {
      2. __unsafe_unretained id receiver;
      3. __unsafe_unretained Class super_class;
      4. };
    • 第一个成员是 receiver, 类似于上面的 objc_msgSend函数第一个参数self

    • 第二个成员是记录当前类的父类是什么,告诉程序从父类中开始找方法,找到方法后,最后内部是使用 objc_msgSend(objc_super->receiver, @selector(class))去调用, 此时已经和[self class]调用相同了,故上述输出结果仍然返回 Son
    • objc Runtime开源代码对- (Class)class方法的实现
  1. -(Class)class {
  2. return object_getClass(self);
  3. }

面试资料:

面试题持续整理更新中,如果你想一起进阶去大厂,不妨添加一下交流群1012951431

面试题资料或者相关学习资料都在群文件中 进群即可下载!
2020-iOS最新面试题解析(原理篇) - 图2

runtime如何通过selector找到对应的IMP地址?(分别考虑类方法和实例方法)

  • 每一个类对象中都一个对象方法列表(对象方法缓存)
  • 类方法列表是存放在类对象中isa指针指向的元类对象中(类方法缓存)
  • 方法列表中每个方法结构体中记录着方法的名称,方法实现,以及参数类型,其实selector本质就是方法名称,通过这个方法名称就可以在方法列表中找到对应的方法实现.
  • 当我们发送一个消息给一个NSObject对象时,这条消息会在对象的类对象方法列表里查找
  • 当我们发送一个消息给一个类时,这条消息会在类的Meta Class对象的方法列表里查找

objc中的类方法和实例方法有什么本质区别和联系

类方法

  1. 1 类方法是属于类对象的
  2. 2 类方法只能通过类对象调用
  3. 3 类方法中的self是类对象
  4. 4 类方法可以调用其他的类方法
  5. 5 类方法中不能访问成员变量
  6. 6 类方法中不能直接调用对象方法
  7. 7 类方法是存储在元类对象的方法缓存中

实例方法

  1. 1 实例方法是属于实例对象的
  2. 2 实例方法只能通过实例对象调用
  3. 3 实例方法中的self是实例对象
  4. 4 实例方法中可以访问成员变量
  5. 5 实例方法中直接调用实例方法
  6. 6 实例方法中可以调用类方法(通过类名)
  7. 7 实例方法是存放在类对象的方法缓存中

使用runtime Associate方法关联的对象,需要在主对象dealloc的时候释放么?

  • 无论在MRC下还是ARC下均不需要
  • 被关联的对象在生命周期内要比对象本身释放的晚很多,它们会在被 NSObject -dealloc 调用的 object_dispose()方法中释放
  • 补充:对象的内存销毁时间表,分四个步骤
  1. 1.调用 -release :引用计数变为零
  2. * 对象正在被销毁,生命周期即将结束.
  3. * 不能再有新的 __weak 弱引用,否则将指向 nil.
  4. * 调用 [self dealloc]
  5. 2\. 父类调用 -dealloc
  6. * 继承关系中最直接继承的父类再调用 -dealloc
  7. * 如果是 MRC 代码 则会手动释放实例变量们(iVars
  8. * 继承关系中每一层的父类 都再调用 -dealloc
  9. 3\. NSObject -dealloc
  10. * 只做一件事:调用 Objective-C runtime 中的 object_dispose() 方法
  11. 4\. 调用 object_dispose()
  12. * C++ 的实例变量们(iVars)调用 destructors
  13. * ARC 状态下的 实例变量们(iVars 调用 -release
  14. * 解除所有使用 runtime Associate方法关联的对象
  15. * 解除所有 __weak 引用
  16. * 调用 free()

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

  • _objc_msgForward是IMP类型,用于消息转发的:当向一个对象发送一条消息,但它并没有实现的时候,_objc_msgForward会尝试做消息转发
  • 直接调用_objc_msgForward是非常危险的事,这是把双刃刀,如果用不好会直接导致程序Crash,但是如果用得好,能做很多非常酷的事
  • JSPatch就是直接调用_objc_msgForward来实现其核心功能的

能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?

  • 不能向编译后得到的类中增加实例变量;
  • 能向运行时创建的类中添加实例变量;
  • 分析如下:
    • 因为编译后的类已经注册在runtime中,类结构体中的objc_ivar_list 实例变量的链表和instance_size实例变量的内存大小已经确定,同时runtime 会调用class_setIvarLayout 或 class_setWeakIvarLayout来处理strong weak引用,所以不能向存在的类中添加实例变量
    • 运行时创建的类是可以添加实例变量,调用 class_addIvar函数,但是得在调用objc_allocateClassPair之后,objc_registerClassPair之前,原因同上。

runloop和线程有什么关系?

  • 每条线程都有唯一的一个RunLoop对象与之对应的
  • 主线程的RunLoop是自动创建并启动
  • 子线程的RunLoop需要手动创建
  • 子线程的RunLoop创建步骤如下:

    • 在子线程中调用[NSRunLoop currentRunLoop]创建RunLoop对象(懒加载,只创建一次)
    • 获得RunLoop对象后要调用run方法来启动一个运行循环

      1. // 启动RunLoop
      2. [[NSRunLoop currentRunLoop] run];
    • RunLoop的其他启动方法

      1. // 第一个参数:指定运行模式
      2. // 第二个参数:指定RunLoop的过期时间,即:到了这个时间后RunLoop就失效了
      3. [[NSRunLoop currentRunLoop] runMode:kCFRunLoopDefaultMode beforeDate:[NSDate distantFuture]];

runloop的mode作用是什么?

  • 用来控制一些特殊操作只能在指定模式下运行,一般可以通过指定操作的运行mode来控制执行时机,以提高用户体验
  • 系统默认注册了5个Mode
    • kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行,对应OC中的:NSDefaultRunLoopMode
    • UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他Mode影响
    • kCFRunLoopCommonModes:这是一个标记Mode,不是一种真正的Mode,事件可以运行在所有标有common modes标记的模式中,对应OC中的NSRunLoopCommonModes,带有common modes标记的模式有:UITrackingRunLoopMode和kCFRunLoopDefaultMode
    • UIInitializationRunLoopMode:在启动 App时进入的第一个 Mode,启动完成后就不再使用
    • GSEventReceiveRunLoopMode:接受系统事件的内部Mode,通常用不到

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

  • 这里强调一点:在主线程中以+scheduledTimerWithTimeInterval…的方式触发的timer默认是运行在NSDefaultRunLoopMode模式下的,当滑动页面上的列表时,进入了UITrackingRunLoopMode模式,这时候timer就会停止
  • 可以修改timer的运行模式为NSRunLoopCommonModes,这样定时器就可以一直运行了
  • 以下是我的笔记补充:
    • 在子线程中通过scheduledTimerWithTimeInterval:…方法来构建NSTimer
      • 方法内部已经创建NSTimer对象,并加入到RunLoop中,运行模式为NSDefaultRunLoopMode
      • 由于Mode有timer对象,所以RunLoop就开始监听定时器事件了,从而开始进入运行循环
      • 这个方法仅仅是创建RunLoop对象,并不会主动启动RunLoop,需要再调用run方法来启动
    • 如果在主线程中通过scheduledTimerWithTimeInterval:…方法来构建NSTimer,就不需要主动启动RunLoop对象,因为主线程的RunLoop对象在程序运行起来就已经被启动了 ``` // userInfo参数:用来给NSTimer的userInfo属性赋值,userInfo是只读的,只能在构建NSTimer对象时赋值 [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(run:) userInfo:@”ya了个hoo” repeats:YES];

// scheduledTimer…方法创建出来NSTimer虽然已经指定了默认模式,但是【允许你修改模式】 [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

// 【仅在子线程】需要手动启动RunLoop对象,进入运行循环 [[NSRunLoop currentRunLoop] run];

  1. 面试资料:
  2. 面试题持续整理更新中,如果你想一起进阶去大厂,不妨添加一下交流群[**1012951431**](https://links.jianshu.com/go?to=https%3A%2F%2Fjq.qq.com%2F%3F_wv%3D1027%26k%3D5JFjujE)
  3. 面试题资料或者相关学习资料都在群文件中 进群即可下载!<br />![](https://cdn.nlark.com/yuque/0/2020/png/1728826/1594368979324-ba3b60d9-cd19-475d-98ce-f62201951dd1.png#align=left&display=inline&height=312&margin=%5Bobject%20Object%5D&originHeight=312&originWidth=489&size=0&status=done&style=none&width=489)
  4. <a name="ec98b967"></a>
  5. ### 猜想runloop内部是如何实现的?
  6. - 从字面意思看:运行循环、跑圈;
  7. - 本质:内部就是do-while循环,在这个循环内部不断地处理各种事件(任务),比如:SourceTimerObserver
  8. - 每条线程都有唯一一个RunLoop对象与之对应,主线程的RunLoop默认已经启动,子线程的RunLoop需要手动启动;
  9. - 每次RunLoop启动时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode,如果需要切换Mode,只能退出Loop,再重新指定一个Mode进入,这样做主要是为了隔离不同Mode中的SourceTimerObserver,让其互不影响;
  10. <a name="87414405"></a>
  11. ### 不手动指定autoreleasepool的前提下,一个autorealese对象在什么时刻释放?(比如在一个vc的viewDidLoad中创建)
  12. - 分两种情况:手动干预释放时机、系统自动去释放
  13. - 手动干预释放时机:指定autoreleasepool就是所谓的:当前作用域大括号结束时就立即释放
  14. - 系统自动去释放:不手动指定autoreleasepoolAutorelease对象会在当前的 runloop 迭代结束时释放,下面详细说明释放时机
  15. - RunLoop中的三个状态会处理自动释放池,通过打印代码发现有两个Observer监听到状态值为:116032+128
  16. - kCFRunLoopEntry(1) // 第一次进入会创建一个自动释放池
  17. - kCFRunLoopBeforeWaiting(32) // 进入休眠状态前先销毁自动释放池,再创建一个新的自动释放池
  18. - kCFRunLoopExit(128) // 退出RunLoop时销毁最后一次创建的自动释放池
  19. - 如果在一个vcviewDidLoad中创建一个Autorelease对象,那么该对象会在 viewDidAppear 方法执行前就被销毁了(是这样的吗???)
  20. <a name="902a40fd"></a>
  21. ### 苹果是如何实现autoreleasepool的?
  22. - autoreleasepool以一个队列数组的形式实现,主要通过下列三个函数完成.

objc_autoreleasepoolPush objc_autoreleasepoolPop objc_aurorelease

  1. - 看函数名就可以知道,对autorelease分别执行push,和pop操作。销毁对象时执行release操作
  2. <a name="a69a8fd6"></a>
  3. ### GCD的队列(dispatch_queue_t)分哪两种类型?背后的线程模型是什么样的?
  4. - 串行队列
  5. - 并行队列
  6. - dispatch_global_queue();是全局并发队列
  7. - dispatch_main_queue();是一种特殊串行队列
  8. - 背后的线程模型:自定义队列 dispatch_queue_t queue; 可以自定义是并行:DISPATCH_QUEUE_CONCURRENT 或者 串行DISPATCH_QUEUE_SERIAL
  9. <a name="b826038e"></a>
  10. ### 苹果为什么要废弃dispatch_get_current_queue?
  11. - 容易误用造成死锁
  12. <a name="b739905f"></a>
  13. ### 如何用GCD同步若干个异步调用?(如根据若干个url异步加载多张图片,然后在都下载完成后合成一张整图)
  14. - 必须是并发队列才起作用
  15. - 需求分析
  16. - 首先,分别异步执行2个耗时的操作
  17. - 其次,等2个异步操作都执行完毕后,再回到主线程执行一些操作
  18. - 使用队列组实现上面的需求

// 创建队列组 dispatch_group_t group = dispatch_group_create();

// 获取全局并发队列 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

// 往队列组中添加耗时操作 dispatch_group_async(group, queue, ^{ // 执行耗时的异步操作1 });

// 往队列组中添加耗时操作 dispatch_group_async(group, queue, ^{ // 执行耗时的异步操作2 });

// 当并发队列组中的任务执行完毕后才会执行这里的代码 dispatch_group_notify(group, queue, ^{ // 如果这里还有基于上面两个任务的结果继续执行一些代码,建议还是放到子线程中,等代码执行完毕后在回到主线程

  1. // 回到主线程
  2. dispatch_async(group, dispatch_get_main_queue(), ^{
  3. // 执行相关代码...
  4. });

});

  1. <a name="6191d073"></a>
  2. ### dispatch_barrier_async的作用是什么?
  3. - 函数定义

dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);

  1. - 必须是并发队列,要是串行队列,这个函数就没啥意义了
  2. - 注意:这个函数的第一个参数queue不能是全局的并发队列
  3. - 作用:在它前面的任务执行结束后它才执行,在它后面的任务等它执行完成后才会执
  4. - 示例代码

-(void)barrier { dispatch_queue_t queue = dispatch_queue_create(“12342234”, DISPATCH_QUEUE_CONCURRENT);

  1. dispatch_async(queue, ^{
  2. NSLog(@"----1-----%@", [NSThread currentThread]);
  3. });
  4. dispatch_async(queue, ^{
  5. NSLog(@"----2-----%@", [NSThread currentThread]);
  6. });
  7. // 在它前面的任务执行结束后它才执行,在它后面的任务等它执行完成后才会执行
  8. dispatch_barrier_async(queue, ^{
  9. NSLog(@"----barrier-----%@", [NSThread currentThread]);
  10. });
  11. dispatch_async(queue, ^{
  12. NSLog(@"----3-----%@", [NSThread currentThread]);
  13. });
  14. dispatch_async(queue, ^{
  15. NSLog(@"----4-----%@", [NSThread currentThread]);
  16. });

}

  1. <a name="0195d868"></a>
  2. ### 以下代码运行结果如何?
  • (void)viewDidLoad { [super viewDidLoad]; NSLog(@”1”); dispatch_sync(dispatch_get_main_queue(), ^{

    1. NSLog(@"2");

    }); NSLog(@”3”); } ```

  • 答案:主线程死锁

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

  • po:打印对象,会调用对象description方法。是print-object的简写
  • expr:可以在调试时动态执行指定表达式,并将结果打印出来,很有用的命令
  • print:也是打印命令,需要指定类型
  • bt:打印调用堆栈,是thread backtrace的简写,加all可打印所有thread的堆栈
  • br l:是breakpoint list的简写

BAD_ACCESS在什么情况下出现?

  • 访问一个僵尸对象,访问僵尸对象的成员变量或者向其发消息
  • 死循环

如何调试BAD_ACCESS错误

  • 设置全局断点快速定位问题代码所在行

2020-iOS最新面试题解析(原理篇) - 图3

  • 开启僵尸对象调试功能

2020-iOS最新面试题解析(原理篇) - 图4

简述下Objective-C中调用方法的过程(runtime)

  • Objective-C是动态语言,每个方法在运行时会被动态转为消息发送,即:objc_msgSend(receiver, selector),整个过程介绍如下:
    • objc在向一个对象发送消息时,runtime库会根据对象的isa指针找到该对象实际所属的类
    • 然后在该类中的方法列表以及其父类方法列表中寻找方法运行
    • 如果,在最顶层的父类(一般也就NSObject)中依然找不到相应的方法时,程序在运行时会挂掉并抛出异常unrecognized selector sent to XXX
    • 但是在这之前,objc的运行时会给出三次拯救程序崩溃的机会,这三次拯救程序奔溃的说明见问题《什么时候会报unrecognized selector的异常》中的说明
  • 补充说明:Runtime 铸就了Objective-C 是动态语言的特性,使得C语言具备了面向对象的特性,在程序运行期创建,检查,修改类、对象及其对应的方法,这些操作都可以使用runtime中的对应方法实现。

什么是method swizzling(俗称黑魔法)

  • 简单说就是进行方法交换
  • 在Objective-C中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是selector的名字。利用Objective-C的动态特性,可以实现在运行时偷换selector对应的方法实现,达到给方法挂钩的目的
  • 每个类都有一个方法列表,存放着方法的名字和方法实现的映射关系,selector的本质其实就是方法名,IMP有点类似函数指针,指向具体的Method实现,通过selector就可以找到对应的IMP

2020-iOS最新面试题解析(原理篇) - 图5

  • 交换方法的几种实现方式
    • 利用 method_exchangeImplementations 交换两个方法的实现
    • 利用 class_replaceMethod 替换方法的实现
    • 利用 method_setImplementation 来直接设置某个方法的IMP

2020-iOS最新面试题解析(原理篇) - 图6

objc中向一个nil对象发送消息将会发生什么?

  • 在Objective-C中向nil发送消息是完全有效的——只是在运行时不会有任何作用
    • 如果一个方法返回值是一个对象,那么发送给nil的消息将返回0(nil)
    • 如果方法返回值为指针类型,其指针大小为小于或者等于sizeof(void*)
    • float,double,long double 或者long long的整型标量,发送给nil的消息将返回0
    • 如果方法返回值为结构体,发送给nil的消息将返回0。结构体中各个字段的值将都是0
    • 如果方法的返回值不是上述提到的几种情况,那么发送给nil的消息的返回值将是未定义的
  • 具体原因分析
    • objc是动态语言,每个方法在运行时会被动态转为消息发送,即:objc_msgSend(receiver, selector)
    • 为了方便理解这个内容,还是贴一个objc的源代码
  1. struct objc_class
  2. {
  3. // isa指针指向Meta Class,因为Objc的类的本身也是一个Object,
  4. // 为了处理这个关系,runtime就创造了Meta Class,
  5. // 当给类发送[NSObject alloc]这样消息时,实际上是把这个消息发给了Class Object
  6. Class isa OBJC_ISA_AVAILABILITY;
  7. #if !__OBJC2__
  8. Class super_class OBJC2_UNAVAILABLE; // 父类
  9. const char *name OBJC2_UNAVAILABLE; // 类名
  10. long version OBJC2_UNAVAILABLE; // 类的版本信息,默认为0
  11. long info OBJC2_UNAVAILABLE; // 类信息,供运行期使用的一些位标识
  12. long instance_size OBJC2_UNAVAILABLE; // 该类的实例变量大小
  13. struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; // 该类的成员变量链表
  14. struct objc_method_list **methodLists OBJC2_UNAVAILABLE; // 方法定义的链表
  15. // 方法缓存,对象接到一个消息会根据isa指针查找消息对象,
  16. // 这时会在method Lists中遍历,
  17. // 如果cache了,常用的方法调用时就能够提高调用的效率。
  18. // 这个方法缓存只存在一份,不是每个类的实例对象都有一个方法缓存
  19. // 子类会在自己的方法缓存中缓存父类的方法,父类在自己的方法缓存中也会缓存自己的方法,而不是说子类就不缓存父类方法了
  20. struct objc_cache *cache OBJC2_UNAVAILABLE;
  21. struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 协议链表
  22. #endif
  23. } OBJC2_UNAVAILABLE;
  • objc在向一个对象发送消息时,runtime库会根据对象的isa指针找到该对象实际所属的类,然后在该类中的方法列表以及其父类方法列表中寻找方法运行,然后再发送消息的时候,objc_msgSend方法不会返回值,所谓的返回内容都是具体调用时执行的。
  • 如果向一个nil对象发送消息,首先在寻找对象的isa指针时就是0地址返回了,所以不会出现任何错误

objc中向一个对象发送消息[obj foo]和objc_msgSend()函数之间有什么关系?

  • [obj foo];在objc动态编译时,会被转意为:objc_msgSend(obj, @selector(foo));

什么时候会报unrecognized selector的异常?

  • 当调用该对象上某个方法,而该对象上没有实现这个方法的时候, 可以通过“消息转发”进行解决,如果还是不行就会报unrecognized selector异常
  • objc是动态语言,每个方法在运行时会被动态转为消息发送,即:objc_msgSend(receiver, selector),整个过程介绍如下:
    • objc在向一个对象发送消息时,runtime库会根据对象的isa指针找到该对象实际所属的类
    • 然后在该类中的方法列表以及其父类方法列表中寻找方法运行
    • 如果,在最顶层的父类中依然找不到相应的方法时,程序在运行时会挂掉并抛出异常unrecognized selector sent to XXX 。但是在这之前,objc的运行时会给出三次拯救程序崩溃的机会
  • 三次拯救程序崩溃的机会
    • Method resolution
      • objc运行时会调用+resolveInstanceMethod:或者 +resolveClassMethod:,让你有机会提供一个函数实现。
      • 如果你添加了函数并返回 YES,那运行时系统就会重新启动一次消息发送的过程
      • 如果 resolve 方法返回 NO ,运行时就会移到下一步,消息转发
    • Fast forwarding
      • 如果目标对象实现了-forwardingTargetForSelector:,Runtime 这时就会调用这个方法,给你把这个消息转发给其他对象的机会
      • 只要这个方法返回的不是nil和self,整个消息发送的过程就会被重启,当然发送的对象会变成你返回的那个对象。
      • 否则,就会继续Normal Fowarding。
      • 这里叫Fast,只是为了区别下一步的转发机制。因为这一步不会创建任何新的对象,但Normal forwarding转发会创建一个NSInvocation对象,相对Normal forwarding转发更快点,所以这里叫Fast forwarding
    • Normal forwarding
      • 这一步是Runtime最后一次给你挽救的机会。
      • 首先它会发送-methodSignatureForSelector:消息获得函数的参数和返回值类型。
      • 如果-methodSignatureForSelector:返回nil,Runtime则会发出-doesNotRecognizeSelector:消息,程序这时也就挂掉了。
      • 如果返回了一个函数签名,Runtime就会创建一个NSInvocation对象并发送-forwardInvocation:消息给目标对象

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

  • 系统的某些block api中,UIView的block版本写动画时不需要考虑,但也有一些api 需要考虑
  • 以下这些使用方式不会引起循环引用的问题
  1. [UIView animateWithDuration:duration animations:^
  2. { [self.superview layoutIfNeeded]; }];
  3. [[NSOperationQueue mainQueue] addOperationWithBlock:^
  4. { self.someProperty = xyz; }];
  5. [[NSNotificationCenter defaultCenter] addObserverForName:@"someNotification"
  6. object:nil
  7. queue:[NSOperationQueue mainQueue]
  8. usingBlock:^(NSNotification * notification)
  9. { self.someProperty = xyz; }];
  • 但如果方法中的一些参数是 成员变量,那么可以造成循环引用,如 GCD 、NSNotificationCenter调用就要小心一点,比如 GCD 内部如果引用了 self,而且 GCD 的参数是 成员变量,则要考虑到循环引用,举例如下:

    • GCD

      • 分析:self—>_operationsQueue—>block—>self形成闭环,就造成了循环引用
        1. __weak __typeof__(self) weakSelf = self;
        2. dispatch_group_async(_operationsGroup, _operationsQueue, ^
        3. {
        4. [weakSelf doSomething];
        5. [weakSelf doSomethingElse];
        6. } );
    • NSNotificationCenter

      • 分析:self—>_observer—>block—>self形成闭环,就造成了循环引用
        1. __weak __typeof__(self) weakSelf = self;
        2. _observer = [[NSNotificationCenter defaultCenter]
        3. addObserverForName:@"testKey"
        4. object:nil
        5. queue:nil
        6. usingBlock:^(NSNotification *note){
        7. [weakSelf dismissModalViewControllerAnimated:YES];
        8. }];

面试资料:

面试题持续整理更新中,如果你想一起进阶去大厂,不妨添加一下交流群1012951431

面试题资料或者相关学习资料都在群文件中 进群即可下载!
2020-iOS最新面试题解析(原理篇) - 图7