iOS常见面试题(block,runtime,runloop,类结构)附参考答案

3,212 阅读14分钟

趁着开年的空闲时间,找了一些面试题写写,算是回顾总结一下iOS开发方面的知识, 水平渣,答案仅作参考! 欢迎指导讨论,写的不对或不妥的地方望及时提出! 原题在这里

iOS常见面试题基础篇(附参考答案)看这里

  • Block

  • block的实质是什么?一共有几种block?都是什么情况下生成的?

    • 简单的来讲,block在OC中的表现可以看作为带有自动变量(局部变量)的匿名函数

      C语言函数定义的时候,可以将函数的地址赋值给函数指针类型的变量,如下:

      int func(int count){
          return count + 1;
      }
      // 赋值
      int (*funcprt)(int) = &func;
      int result = (*funcprt)(10);

      同理,我们也可以将block语法赋值给声明为block类型的变量中。如下:

      // 声明一个block类型的变量,其与函数指针类型的变量不同之处只有'*'改为'^'
      int (^blk)(int);
      // 赋值
      int (^blk)(int) = ^(int count){return count + 1;};
      int result = blk(10);

      还可以通过typedef来声明blk_t类型变量,如下:

      typedef int (^blk_t)(int);
      blk_t blk = ^(int count){return count + 1;};
      int result = blk(10)

      以上解释了匿名函数,现在来解释一下带有自动变量

      int value = 10;
      void(^blk)() = ^{
          NSLog(@"value === %d", value);
      };
      blk();
      value = 2;
      NSLog(@"%@", value);

      value结果为 10 。在block中,block表达式截获所使用的自动变量的值,即保存该变量的瞬间值,所以在执行了block后,改变block外自动变量的值,并不会影响block执行时自动变量的值。

    • block的本质

      关于block的本质, ibireme大神objc 中的 block这篇博客里,有详细的分析。

      struct Block_descriptor_1 {
          uintptr_t reserved;
          uintptr_t size;
      };
      
      struct Block_layout {
          void *isa;
          volatile int32_t flags; // contains ref count
          int32_t reserved; 
          void (*invoke)(void *, ...);
          struct Block_descriptor_1 *descriptor;
          // imported variables
      };

      根据block的数据结构可以发现,它是含有*isa指针的,在OC中根据对象的定义,凡是首地址是*isa的结构体指针,都可以认为是对象(id)。这样在objc中,block实际上就算是对象。

    • 既然OC处理Block是按照对象来处理的。在iOS中,常见的就是_NSConcreteStackBlock_NSConcreteMallocBlock_NSConcreteGlobalBlock这3种,还有另外几种,暂不做讨论。

    • ARC下:

      void(^blockA)() = ^{
          NSLog(@"just a block");
      };
      NSLog(@"%@", blockA);
      
      int value = 10;
      void(^blockB)() = ^{
          NSLog(@"just a block === %d", value);
      };
      NSLog(@"%@", blockB);

      ARC下打印结果如下:

      <__NSGlobalBlock__: 0x10c81c308> <__NSMallocBlock__: 0x60400025d160>

      MRC下打印结果如下: <__NSGlobalBlock__: 0x1056bf308> <__NSStackBlock__: 0x7ffeea540a48>

      我们对栈上的block做copy操作:

      NSLog(@"%@", [blockB copy])

      结果为: <__NSMallocBlock__: 0x600000444b60>

      由此我们可以得出以下结果:

      • 当block没有引用外部局部变量的时候,block为全局block

      • 当block引用外部局部变量的时候,ARC下为堆block,MRC下为栈block,此时对MRC下的栈block进行copy,栈block就变为堆block。在ARC下,编译器会把block从栈拷贝到堆。

      • 经验证,当block只引用静态变量,全局变量的时候,block均为全局block

  • 为什么在默认情况下无法修改被block捕获的变量? __block都做了什么?

    • 默认情况下,在block中是无法修改被block捕获的自动变量,因为block捕获自动变量时,仅仅捕获到该自动变量的值,并非是内存地址,因此早block内部无法改变自动变量的值。
    • __block的实现原理详见深入研究 Block 捕获外部变量和 __block 实现原理。大致意思是说: 带有__block的自动变量,经过编译后会变成一个结构体,通过结构体中的__forwarding指针可以访问到变量,自然就可以修改变量了。
  • 模拟一下循环引用的一个情况?block实现界面反向传值如何实现?

    • 循环引用场景

      // 当block作为属性时:
      @property(nonatomic, copy) void(^block)();
      
      self.block = ^{
          NSLog(@"%@",self);
      }
      

      此时会出现警告:

      这时,可以用__weak修饰self,避免循环引用:

    • 界面反向传值

      //SecondViewController.h
      #import < UIKit/UIKit.h>
      
      typedef void(^CallBackBlock) (NSString *string);
      
      @interface SecondViewController : UIViewController
      
      @property (nonatomic,strong)UItextField *textField;
      @property (nonatomic,copy)CallBackBlcok callBackBlock;
      
      @end
      
      // 在implementation中添加一个点击事件:
      - (IBAction)click:(id)sender {
         self.callBackBlock(_textField.text);
          [self.navigationController popToRootViewControllerAnimated:YES];
      }
      

      在FirstViewController中:

      // FirstViewController.m
      - (IBAction)push:(id)sender {
          SecondViewController *secondVC = [[SecondViewController alloc]init]
          secondVC.callBackBlock = ^(NSString *string){
              NSLog(@"string is %@",string);
              self.label.text = string;
          };
          [self.navigationController pushViewController:secondVC animated:YES];
      }
  • 思考一下这个问题: ARC下会发生什么? MRC呢?若blockA并没有引用自动变量val的话情况又是什么样?

     @property(nonatomic, weak) void(^block)();
      
      - (void)viewDidLoad {
          [superviewDidLoad];
          int val = 10;
          void(^blockA)() = ^{
              NSLog(@"val:%d", val);
          };
          NSLog(@"%@", blockA);
          _block = blockA;
      }
          
      -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
          NSLog(@"%@", _block);
      }
  • Runtime

  • objc在向一个对象发送消息时,发生了什么?

    • 众所周知,在objc中方法调用的本质是发消息,例如:

      [obj message];
      // 运行时会转化为:
      objc_msgSend(obj, selector)
      
      // 当有参数时:
      [obj message:(id)arg...];
      // 运行时会转化为:
      objc_msgSend(obj, selector, arg1, arg2, ...)

      消息在运行时才会与方法绑定

      当向一个对象发送消息时:

      1.首先检测这个 selector 是不是要忽略。比如 Mac OS X 开发,有了垃圾回收就不理会 retain,release 这些函数。

      2.检测这个 selector 的 target 是不是 nil,Objc 允许我们对一个 nil 对象执行任何方法不会 Crash,因为运行时会被忽略掉。

      3.如果上面两步都通过了,那么就开始查找这个类的实现 IMP,先从 cache 里查找,如果找到了就运行对应的函数去执行相应的代码。

      4.如果 cache 找不到就找类的方法列表中是否有对应的方法。 如果类的方法列表中找不到就到父类的方法列表中查找,一直找到 NSObject 类为止。

      5.如果还找不到,就要开始进入消息转发流程

      如下图所示:

  • 什么时候会报unrecognized selector错误?iOS有哪些机制来避免走到这一步?

    • 当调用某对象上某个方法,而该对象并没有实现这个方法的时候, 可以通过消息转发进行解决。

    • 消息转发步骤如下:

      1. objc运行时会调用 +resolveInstanceMethod:或者 +resolveClassMethod:,让我们有机会提供一个函数实现。如果你添加了函数,那运行时系统就会重新启动一次消息发送的过程,否则,运行时就会移到下一步,消息转发(Message Forwarding)。

      2. 调用forwardingTargetForSelector:方法,尝试找到一个能响应该消息的对象。如果获取到,则直接转发给它。如果返回了nil,继续下面的动作。

      3. 调用methodSignatureForSelector:方法,尝试获得一个方法签名。如果获取不到,则直接调用doesNotRecognizeSelector抛出异常。如果返回了一个方法签名,Runtime就会创建一个NSInvocation对象,然后继续进行第四步

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

      如图所示:

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

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

    • weak修饰符表示该属性是非拥有关系,运行期系统会将每一个类的weak变量放入相应的一个hash表中,在这个表中以weak变量所指向的对象的内存地址为key,当weak指向的对象引用计数为0执行dealloc方法,对象被销毁,运行期系统通过key去hash表中找到相应的weak对象将他们设置成nil。
  • 给类添加一个属性后,在类结构体里哪些元素会发生变化?

    • 类的结构体如下:
      struct objc_class {
          Class isa  OBJC_ISA_AVAILABILITY;
      
      #if !__OBJC2__
          Class super_class                                        OBJC2_UNAVAILABLE;
          const char *name                                         OBJC2_UNAVAILABLE;
          long version                                             OBJC2_UNAVAILABLE;
          long info                                                OBJC2_UNAVAILABLE;
          long instance_size                                       OBJC2_UNAVAILABLE;
          struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;
          struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;
          struct objc_cache *cache                                 OBJC2_UNAVAILABLE;
          struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
      #endif
      } OBJC2_UNAVAILABLE;
      /* Use `Class` instead of `struct objc_class *` */
    • 当我们给类添加属性后,实例对象的内存大小:instance_size和属性列表:objc_ivar_list *ivars会发生改变。

RunLoop

  • runloop是来做什么的?runloop和线程有什么关系?主线程默认开启了runloop么?子线程呢?

    • runloop字面的意思就是跑圈,实际上App能一直不停的运行下去,runloop功不可没!我们来分析一下main函数:

      int main(int argc, char * argv[]) {
            @autoreleasepool {
                return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
            }
        }

      如果没有runloop,线程执行完之后就会退出,就不能再执行任务了。这时我们就需要采用一种方式来让线程能够处理任务,并不退出。所以,我们就有了RunLoop,其中UIApplicationMain函数内部帮我们开启了主线程的RunLoop,UIApplicationMain内部拥有一个无线循环的代码。

    • runloop与线程对应关系如下:

      1. 一条线程对应一个RunLoop对象,每条线程都有唯一一个与之对应的RunLoop对象。
      2. 我们只能在当前线程中操作当前线程的RunLoop,而不能去操作其他线程的RunLoop。
      3. RunLoop对象在第一次获取RunLoop时创建,销毁则是在线程结束的时候。
      4. 主线程的RunLoop对象系统自动帮助我们创建好了(原理如下),而子线程的RunLoop对象需要我们主动创建。
  • runloop的mode是用来做什么的?有几种mode?

    • 在ibireme大神的深入理解 Runloop一文中,详细的介绍了一个Runloop包含了哪些东西,如下图所示:

      Mode代表RunLoop的运行模式,一个RunLoop可以包含若干个Mode,每个Mode又包含若干个Source/Timer/Observer。每次调用RunLoop的主函数时,只能指定其中一个Mode,这个Mode被称作CurrentMode。如果需要切换Mode,只能退出Loop,再重新指定一个Mode进入。这样做主要是为了分隔开不同组的Source/Timer/Observer,让其互不影响。

    • 常见的Mode如下:

      1.kCFRunLoopDefaultMode:App的默认运行模式,通常主线程是在这个运行模式下运行 2.UITrackingRunLoopMode:跟踪用户交互事件(用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他Mode影响) 3.UIInitializationRunLoopMode:在刚启动App时第进入的第一个 Mode,启动完成后就不再使用 4.GSEventReceiveRunLoopMode:接受系统内部事件,通常用不到 5.kCFRunLoopCommonModes:伪模式,不是一种真正的运行模式

  • 为什么把NSTimer对象以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)添加到主运行循环以后,滑动scrollview的时候NSTimer却不动了?

    • 1.当我们不做任何操作的时候,RunLoop处于NSDefaultRunLoopMode下。
    • 2.而当我们拖动scrollview的时候,RunLoop就结束NSDefaultRunLoopMode,切换到了UITrackingRunLoopMode模式下,这个模式下没有添加NSTimer,所以我们的NSTimer就不工作了。
    • 3.但当我们松开鼠标的时候,RunLoop就结束UITrackingRunLoopMode模式,又切换回NSDefaultRunLoopMode模式,所以NSTimer就又开始正常工作了
    • 4.我们可以把NSTimer也添加到UITrackingRunLoopMode模式下,或者开启新的线程,把NSTimer添加到子线程的Runloop中,这样就可以解决滑动时NSTimer失效的问题
  • 苹果是如何实现Autorelease Pool的?

    • autorelease

      一个被autorelease修饰的对象会被加到最近的autoreleasePool中,当这个autoreleasePool自身drain的时候,其中的autoreleased对象会被release

    • autoreleasePool是怎么实现的?

      1.AutoreleasePool并没有单独的结构,而是由若干个AutoreleasePoolPage以双向链表的形式组合而成

      2.AutoreleasePoolPage对象会记录autorelease对象地址

      3.AutoreleasePool的操作时通过以下这几个函数实现的:objc_autoreleasepoolPush,objc_autoreleasepoolPop,objc_autorelease

      4.Autorelease对象是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了autoreleasePush和Pop

      推荐大家阅读这篇黑幕背后的Autorelease

类结构

  • isa指针?(对象的isa,类对象的isa,元类的isa都要说)

    • 上面题目中写了类经过编译后的结构体,其中包含有isa指针,在OC中类也是一种对象,它属于元类metaClasss,对象的isa指针指向类,类的isa指针指向元类,元类的isa指针指向父类的元类,一直到根元类,最后根元类的isa指针指向了自身,如图:
  • 类方法和实例方法有什么区别?

    • 调用方式不同,类方法由类名直接调用,实例方法由该类生成的对象调用。原因是类方法是在元类结构体的methodLists里面,而实例方法位于类结构体的methodLists中。
    • 类方法不能使用该类的属性,实例方法可以使用属性
    • 类方法中不能调用实例方法,而实例方法中可以调用类方法
    • 类方法中self代表类本身,实例方法中self代表实例对象本身
  • 介绍一下分类,能用分类做什么?内部是如何实现的?它为什么会覆盖掉原来的方法?

      • 扩展已有的类
      • 分散原类的实现
      • 声明私有方法
      • 模拟多继承
      • 公开framework的部分私有方法
    • 分类经过编译后也会成为一个结构体:

      struct category_t {
          const char *name; // 类名
          classref_t cls;   // 分类所属的类
          struct method_list_t *instanceMethods;  // 实例方法列表
          struct method_list_t *classMethods;     // 类方法列表
          struct protocol_list_t *protocols;      // 遵循的协议列表
          struct property_list_t *instanceProperties; // 属性列表
      };

      在运行时,category会被附加到类上面,包括把category的实例方法、协议以及属性添加到类上和category的类方法和协议添加到类的metaclass上。

    • category的方法没有完全替换掉原来类已经有的方法,也就是说如果category和原来类都有methodA,那么category附加完成之后,类的方法列表里会有两个methodA,category的方法被放到了新方法列表的前面,而原来类的方法被放到了新方法列表的后面,这也就是我们平常所说的category的方法会“覆盖”掉原来类的同名方法,这是因为运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,就会罢休^_^,殊不知后面可能还有一样名字的方法。

      推荐大家阅读美团出品的深入理解Objective-C:Category,我是知识的搬运工~ 也欢迎大家给我推荐高质量的文章!独乐乐不如众乐乐~

  • 运行时能增加成员变量么?能增加属性么?如果能,如何增加?如果不能,为什么?

    • class_addIvar给指定的类添加成员变量,但是不能为已经生成的类添加,运行时规定,只能在objc_allocateClassPairobjc_registerClassPair两个函数之间为类添加变量。原因在上面的题目中有过解释。
    • class_addProperty给指定的类添加属性,可以成功添加了属性但是不能用点调用法调用,可以利用KVC/关联方式来该表这个属性的值
    • runtimeobjc_setAssociatedObjectobjc_getAssociatedObject方法来实现关联,给分类添加属性就是利用这个方法实现的。
  • objc中向一个nil对象发送消息将会发生什么?(返回值是对象,是标量,结构体)

    • 向 nil 发送消息并不会引起程序crash,只是在运行时不会有任何作用。但是对[NSNull null]对象发送消息时,是会crash的。
    • 当方法返回值为对象的时候, 给nil发消息返回nil
    • 当方法返回值为结构体的时候,给nil发消息返回0,结构体中的各个参数也是0
    • 当方法返回值为指针类型的时候, 给nil发消息返回0

参考博客:

招聘一个靠谱的iOS

深入研究 Block 捕获外部变量和 __block 实现原理

深入理解 Runloop

深入理解Objective-C:Category

黑幕背后的Autorelease