文章目录
  1. 1. 25 _objc_msgForward函数是做什么的,直接调用它将会发生什么?
    1. 1.0.1. 25.1 下面回答下第二个问题“直接_objc_msgForward调用它将会发生什么?”
  • 2. 26 runtime如何实现weak变量的自动置nil?
  • 3. 27 能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?
  • 4. 28 runloop和线程有什么关系?
  • 5. 29 runloop的mode作用是什么?
  • 6. 30 以+ scheduledTimerWithTimeInterval:...的方式触发的timer,在滑动页面上的列表时,timer会暂定回调,为什么?如何解决?
  • 7. 31 猜想runloop内部是如何实现的?
  • 8. 32 objc使用什么机制管理对象内存?
  • 9. 33 ARC通过什么方式帮助开发者管理内存?
  • 10. 34 不手动指定autoreleasepool的前提下,一个autorealese对象在什么时刻释放?(比如在一个vc的viewDidLoad中创建)
  • 来自:01《招聘一个靠谱的iOS》面试题参考答案/《招聘一个靠谱的iOS》面试题参考答案(下)

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

    _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
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    // 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)
    {
    IMP imp;
    if (!cls || !sel) return nil;
    imp = lookUpImpOrNil(cls, sel, nil,
    YES/*initialize*/, YES/*cache*/, YES/*resolver*/);
    // Translate forwarding function to C-callable external version
    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
    #import <UIKit/UIKit.h>
    #import "AppDelegate.h"
    #import "SGHTest.h"
    int main(int argc, char * argv[]) {
    NSLog(@"%s",__func__);
    @autoreleasepool {
    SGHTest *test = [[SGHTest alloc] init];
    [test performSelector:@selector(p_test)];
    return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
    }

    你可以在/tmp/msgSend-xxxx(我这一次是/tmp/msgSends-47869)文件里,看到打印出来:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    - SGHTest NSObject performSelector:
    + SGHTest NSObject resolveInstanceMethod:
    + SGHTest NSObject resolveInstanceMethod:
    - SGHTest NSObject forwardingTargetForSelector:
    - SGHTest NSObject forwardingTargetForSelector:
    - SGHTest NSObject methodSignatureForSelector:
    - SGHTest NSObject methodSignatureForSelector:
    - SGHTest NSObject class
    - SGHTest NSObject doesNotRecognizeSelector:
    - SGHTest NSObject doesNotRecognizeSelector:
    - SGHTest NSObject class

    结合《NSObject官方文档》,排除掉 NSObject 做的事,剩下的就是_objc_msgForward消息转发做的几件事:

    1. 调用resolveInstanceMethod:方法 (或 resolveClassMethod:)。允许用户在此时为该 Class 动态添加实现。如果有实现了,则调用并返回YES,那么重新开始objc_msgSend流程。这一次对象会响应这个选择器,一般是因为它已经调用过class_addMethod。如果仍没实现,继续下面的动作。

    2. 调用forwardingTargetForSelector:方法,尝试找到一个能响应该消息的对象。如果获取到,则直接把消息转发给它,返回非 nil 对象。否则返回 nil ,继续下面的动作。注意,这里不要返回 self ,否则会形成死循环。

    3. 调用methodSignatureForSelector:方法,尝试获得一个方法签名。如果获取不到,则直接调用doesNotRecognizeSelector抛出异常。如果能获取,则返回非nil:创建一个 NSlnvocation 并传给forwardInvocation:

    4. 调用forwardInvocation:方法,将第3步获取到的方法签名包装成 Invocation 传入,如何处理就在这里面了,并返回非nil。

    5. 调用doesNotRecognizeSelector: ,默认的实现是抛出异常。如果第3步没能获得一个方法签名,执行该步骤。

    上面前4个方法均是模板方法,开发者可以override,由 runtime 来调用。最常见的实现消息转发:就是重写方法3和4,吞掉一个消息或者代理给其他对象都是没问题的。

    也就是说_objc_msgForward在进行消息转发的过程中会涉及以下这几个方法:

    1. resolveInstanceMethod:方法 (或 resolveClassMethod:)。
    2. forwardingTargetForSelector:方法
    3. methodSignatureForSelector:方法
    4. forwardInvocation:方法
    5. 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类型 :

    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实现原理详解》详细记录了实现原理,有兴趣可以看下。

    同时 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。

    1. 在b非nil时,a和b指向同一个内存地址,在b变nil时,a变nil。此时向a发送消息不会崩溃:在Objective-C中向nil发送消息是安全的。

    2. 而如果a是由assign修饰的,则: 在b非nil时,a和b指向同一个内存地址,在b变nil时,a还是指向该内存地址,变野指针。此时向a发送消息极易崩溃。

    下面我们将基于objc_storeWeak(&a, b)函数,使用伪代码模拟“runtime如何实现weak属性”:

    1
    2
    3
    4
    5
    // 使用伪代码模拟:runtime如何实现weak属性
    id obj1;
    objc_initWeak(&obj1, obj);
    /*obj引用计数变为0,变量作用域结束*/
    objc_destroyWeak(&obj1);

    下面对用到的两个方法objc_initWeakobjc_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
    // 使用伪代码模拟: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 能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?

    1. 不能向编译后得到的类中增加实例变量;
    2. 能向运行时创建的类中添加实例变量;

    解释下:

    1. 因为编译后的类已经注册在 runtime 中,类结构体中的 objc_ivar_list 实例变量的链表 和 instance_size 实例变量的内存大小已经确定,同时runtime 会调用 class_setIvarLayoutclass_setWeakIvarLayout 来处理 strong、 weak 引用。所以不能向存在的类中添加实例变量;

    2. 运行时创建的类是可以添加实例变量,调用 class_addIvar 函数。但是得在调用 objc_allocateClassPair 之后,objc_registerClassPair 之前,原因同上。
      即在运行时创建的类添加实例变量的代码格式如下:

      1
      2
      3
      objc_allocateClassPair
      //class_addIvar
      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
      5
      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作用是什么?

    mode 主要是用来指定事件在运行循环中的优先级的,分为:

    1. NSDefaultRunLoopModekCFRunLoopDefaultMode):默认,空闲状态
    2. UITrackingRunLoopMode:ScrollView滑动时
    3. UIInitializationRunLoopMode:启动时
    4. NSRunLoopCommonModeskCFRunLoopCommonModes):Mode集合

    苹果公开提供的 Mode 有两个:

    1. NSDefaultRunLoopModekCFRunLoopDefaultMode
    2. NSRunLoopCommonModeskCFRunLoopCommonModes

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

    RunLoop只能运行在一种mode下,如果要换mode,当前的loop也需要停下重启成新的。利用这个机制,ScrollView滚动过程中NSDefaultRunLoopModekCFRunLoopDefaultMode)的mode会切换到UITrackingRunLoopMode来保证ScrollView的流畅滑动:只能在NSDefaultRunLoopMode模式下处理的事件会影响ScrollView的滑动。

    如果我们把一个NSTimer对象以NSDefaultRunLoopModekCFRunLoopDefaultMode)添加到主运行循环中的时候, ScrollView滚动过程中会因为mode的切换,而导致NSTimer将不再被调度。

    同时因为mode还是可定制的,所以:

    Timer计时会被scrollView的滑动影响的问题可以通过将timer添加到NSRunLoopCommonModeskCFRunLoopCommonModes)来解决。 代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    //将timer添加到NSDefaultRunLoopMode中
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0
    target:self
    selector:@selector(timerTick:)
    userInfo:nil
    repeats:YES];
    //然后再添加到NSRunLoopCommonModes里
    [[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
    int main(int argc, char * argv[]) {
    //程序一直运行状态
    while (AppIsRunning) {
    //睡眠状态,等待唤醒事件
    id whoWakesMe = SleepForWakingUp();
    //得到唤醒事件
    id event = GetEvent(whoWakesMe);
    //开始处理事件
    HandleEvent(event);
    }
    return 0;
    }

    参考链接:

    1. 《深入理解RunLoop》
    2. 摘自博文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中创建)

    分两种情况:手动干预释放时机、系统自动去释放。

    1. 手动干预释放时机–指定autoreleasepool 就是所谓的:当前作用域大括号结束时释放。
    2. 系统自动去释放–不手动指定autoreleasepool
      Autorelease对象出了作用域之后,会被添加到最近一次创建的自动释放池中,并会在当前的 runloop 迭代结束时释放

    释放的时机总结起来,可以用下图来表示:

    下面对这张图进行详细的解释:

    从程序启动到加载完成是一个完整的运行循环,然后会停下来,等待用户交互,用户的每一次交互都会启动一次运行循环,来处理用户所有的点击事件、触摸事件。

    我们都知道: 所有 autorelease 的对象,在出了作用域之后,会被自动添加到最近创建的自动释放池中。

    但是如果每次都放进应用程序的 main.m 中的 autoreleasepool 中,迟早有被撑满的一刻。这个过程中必定有一个释放的动作。何时?在一次完整的运行循环结束之前,会被销毁。

    那什么时间会创建自动释放池?运行循环检测到事件并启动后,就会创建自动释放池。

    子线程的 runloop 默认是不工作,无法主动创建,必须手动创建。

    自定义的 NSOperationNSThread 需要手动创建自动释放池。比如: 自定义的 NSOperation 类中的 main 方法里就必须添加自动释放池。否则出了作用域后,自动释放对象会因为没有自动释放池去处理它,而造成内存泄露。

    但对于 blockOperation 和 invocationOperation 这种默认的Operation ,系统已经帮我们封装好了,不需要手动创建自动释放池。

    @autoreleasepool 当自动释放池被销毁或者耗尽时,会向自动释放池中的所有对象发送 release 消息,释放自动释放池中的所有对象。

    如果在一个vc的viewDidLoad中创建一个 Autorelease对象,那么该对象会在 viewDidAppear 方法执行前就被销毁了。

    参考链接:《黑幕背后的Autorelease》

    文章目录
    1. 1. 25 _objc_msgForward函数是做什么的,直接调用它将会发生什么?
      1. 1.0.1. 25.1 下面回答下第二个问题“直接_objc_msgForward调用它将会发生什么?”
  • 2. 26 runtime如何实现weak变量的自动置nil?
  • 3. 27 能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?
  • 4. 28 runloop和线程有什么关系?
  • 5. 29 runloop的mode作用是什么?
  • 6. 30 以+ scheduledTimerWithTimeInterval:...的方式触发的timer,在滑动页面上的列表时,timer会暂定回调,为什么?如何解决?
  • 7. 31 猜想runloop内部是如何实现的?
  • 8. 32 objc使用什么机制管理对象内存?
  • 9. 33 ARC通过什么方式帮助开发者管理内存?
  • 10. 34 不手动指定autoreleasepool的前提下,一个autorealese对象在什么时刻释放?(比如在一个vc的viewDidLoad中创建)