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

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

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

  • Blockgit

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

    • 简单的来说,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类型变量,以下:oop

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

      以上解释了匿名函数,如今来解释一下带有自动变量post

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

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

    • 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

相关文章
相关标签/搜索