iOS runtime面试题

作者:一缕殇流化隐半边冰霜
神经病院Objective-C Runtime住院第二天——消息发送与转发
神经病院Objective-C Runtime入院第一天——isa和Class
神经病院Objective-C Runtime出院第三天——如何正确使用Runtime

一. [self class] 与 [super class]

下面的代码分别输出什么?

@implementation Son : Father
- (id)init {
    self = [super init];
    if (self) {
        NSLog(@"%@", NSStringFromClass([self class]));
        NSLog(@"%@", NSStringFromClass([super class]));
    }
    return self;
}
@end

答案: 都是输Son

解释:

  • selfsuper 的区别
    self是类的一个隐藏参数,每个方法的实现的第一个参数即为self
    super并不是隐藏参数,它实际上只是一个“编译器标识符”,它负责告诉编译器,当调用方法时去调用父类的方法,而不是本类的方法。
    在调用[super class]的时候,runtime 会去调用objc_msgSendSuper方法,而不是objc_msgSend.
OBJC_EXPORT void objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )


/// Specifies the superclass of an instance. 
struct objc_super {
    /// Specifies an instance of a class.
    __unsafe_unretained id receiver;

    /// Specifies the particular superclass of the instance to message. 
#if !defined(__cplusplus)  &&  !__OBJC2__
    /* For compatibility with old objc-runtime.h header */
    __unsafe_unretained Class class;
#else
    __unsafe_unretained Class super_class;
#endif
    /* super_class is the first class to search */
};

objc_msgSendSuper方法中,第一个参数是一个objc_super的结构体,这个结构体里面有两个变量,一个是接收消息的receiver,一个是当前类的父类super_class.

我们很容易错误的认为[super class]是调用的[super_class class],其实objc_msgSendSuper的工作原理应该是这样:

// 注意这里是从父类开始msgSend,而不是从本类开始,
objc_msgSend(objc_super->receiver, @selector(class))

/// Specifies an instance of a class.  这是类的一个实例
    __unsafe_unretained id receiver;   


// 由于是实例调用,所以是减号方法
- (Class)class {
    return object_getClass(self);
}

由于找到了父类NSObject里面的class方法的IMP,又因为传入的参数objc_super->receiver = self;self就是Son,所以父类的方法class执行IMP之后,输出还是Son,最后输出的两个值都是一样,都是Son.

二. isKindOfClass 与 isMemberOfClass

下面代码输出什么?

 @interface Sark : NSObject
 @end

 @implementation Sark
 @end

 int main(int argc, const char * argv[]) {
@autoreleasepool {
    BOOL res1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
    BOOL res2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
    BOOL res3 = [(id)[Sark class] isKindOfClass:[Sark class]];
    BOOL res4 = [(id)[Sark class] isMemberOfClass:[Sark class]];

   NSLog(@"%d %d %d %d", res1, res2, res3, res4);
}
return 0;
}

答案: 1 0 0 0
解答:

  • 先来分析这两个函数的实现:

      + (Class)class {
          return self;
      }
    
      - (Class)class {
          return object_getClass(self);
      }
    
      Class object_getClass(id obj)
      {
          if (obj) return obj->getIsa();
          else return Nil;
      }
    
      inline Class 
      objc_object::getIsa() 
      {
          if (isTaggedPointer()) {
              uintptr_t slot = ((uintptr_t)this >> TAG_SLOT_SHIFT) & TAG_SLOT_MASK;
              return objc_tag_classes[slot];
          }
      return ISA();
      }
    
      inline Class 
      objc_object::ISA() 
      {
          assert(!isTaggedPointer()); 
          return (Class)(isa.bits & ISA_MASK);
      }
    
      + (BOOL)isKindOfClass:(Class)cls {
          for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
              if (tcls == cls) return YES;
          }
          return NO;
      }
    
      - (BOOL)isKindOfClass:(Class)cls {
          for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
              if (tcls == cls) return YES;
          }
          return NO;
      }
    
      + (BOOL)isMemberOfClass:(Class)cls {
          return object_getClass((id)self) == cls;
      }
    
      - (BOOL)isMemberOfClass:(Class)cls {
          return [self class] == cls;
      }
    
  • 首先第一题BOOL res1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];

    a. [NSObject class]执行完之后返回NSObject自身。

    b. +(BOOL)isKindOfClass:(Class)cls方法内部,会先去获得object_getClass的类,而object_getClass的源码实现会去调用当前类的obj->getIsa(),最后ISA()方法中获得meta class指针。

    c. 接着在isKindOfClass中有一个循环,先判断class是否等于meta class,不等就继续循环判断是否等于super class,不能再继续取super class,如此循环下去。

    d. [NSObject class]执行完之后调用isKindOfClass,第一次先判断NSObjectNSObjectmeta class是否相等,根据下图我们可以看出,NSObjectmeta class与本身不等。接着第二次循环判断: NSObjectmeta classsuperclass是否相等,根据下图可以看出Root class(meta)superclass就是Root class(class),也就是NSObject本身。所以第二次循环相等,于是第一行res1应该为YES.

iOS runtime面试题_第1张图片
image.png
  • 同理,第三行BOOL res3 = [(id)[Sark class] isKindOfClass:[Sark class]];

    a. 在[Sark class]执行完之后调用isKindOfClass,第一次for循环,SarkMeta Class[Sark class]不等,第二次for循环,Sark Meta classsuper class指向的是 NSObject Meta ClassSark Class不相等。 第三次 for 循环, NSObject Meta Classsuper class 指向的是 NSObject classSark Class不相等。 第四次循环,NSObject Classsuper class指向nilSark Class不相等。 第四次循环之后,退出循环。所以 第三行 res输出为NO.

    b. 如果这里把Sark改成它的实例对象,[sark isKindOfClass:[Sark class]], 那么此时就应该输出为YES,因为在isKindOfClass函数中,判断sarkisa指向的是自己的类Sark,所以第一次for循环就会输出YES.

  • 第二行BOOL res2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];,这里的isMemberOfClass的源码实现是拿到自己的isa指针和自己比较,判断是否相等。这里NSObjectisa指针指向NSObjectMeta Class,所以和NSObject Class不相等,所以res2输出NO

  • 同理第四行BOOL res4 = [(id)[Sark class] isMemberOfClass:[Sark class]];,Sarkisa指针指向SarkMeta Class,和 Sark Class也不等,所以ret4输出为NO.

三. Class与内存地址

下面的代码会输出什么?

@interface Sark : NSObject
@property (nonatomic, copy) NSString *name;
- (void)speak;
@end
@implementation Sark
- (void)speak {
    NSLog(@"my name's %@", self.name);
}
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    id cls = [Sark class];
    void *obj = &cls;
    [(__bridge id)obj speak];
}
@end

这道题目有两个难点:

  1. obj调用speak方法,到底会不会崩溃。
  2. 如果speak方法不崩溃,应该输出什么?

首先谈谈隐藏参数self和_cmd的问题。
[receiver message]调用方法时,系统会在运行时偷偷的动态传入两个隐藏参数self_cmd,之所以称之为隐藏参数,是因为在源代码中没有声明和定义这两个参数。self就是方法接收者,_cmd表示当前调用方法,其实它就是一个方法的选择器SEL.

难点一: 能不能调用speak方法?

id cls = [Sark class];
void *obj = &cls;
[(__bridge id)obj speak];

答案是可以的。obj被转换成了一个指向Sark Class的指针,然后使用id转换成了objc_object类型。obj现在已经是一个Sark类型的实例对象了,当然可以调用speak的方法。

objc_classobjc_object的定义:

typedef struct objc_class *Class;

typedef struct objc_object *id;

@interface Object { 
    Class isa; 
}

@interface NSObject  {
    Class isa  OBJC_ISA_AVAILABILITY;
}

struct objc_object {
private:
    isa_t isa;
}

struct objc_class : objc_object {
    // Class ISA;     Class superclass;
    cache_t cache;             // formerly cache pointer and vtable     

    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags }union isa_t 
{
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }
    Class cls;
    uintptr_t bits;
}

难点二:如果能调用speak,会输出什么?

很多人认为会输出sark相关信息,这样答案就错误了。

正确的答案会输出:

my name's 

内存地址每次运行都不同,但前面一定是ViewController
我们改变下代码,打印更多信息看一下。

- (void)viewDidLoad {
    [super viewDidLoad];

    NSLog(@"ViewController = %@ , 地址 = %p", self, &self);

    id cls = [Sark class];
    NSLog(@"Sark class = %@ 地址 = %p", cls, &cls);

    void *obj = &cls;
    NSLog(@"Void *obj = %@ 地址 = %p", obj,&obj);

    [(__bridge id)obj speak];

    Sark *sark = [[Sark alloc]init];
    NSLog(@"Sark instance = %@ 地址 = %p",sark,&sark);

    [sark speak];
}

我们把指针地址打印出来,结果如下:

 ViewController =  , 地址 = 0x7fff5d52b998

 Sark class = Sark 地址 = 0x7fff5d52b978

 Void *obj =  地址 = 0x7fff5d52b970

 my name's 

 Sark instance =  地址 = 0x7fff5d52b968

 my name's (null)

从地址我们可以看出,随着参数的压栈,地址越来越小。

// 以下数字越高表示地址越小
// 压栈参数1: id self (0)
// 压栈参数2: SEL _cmd (1)
- (void)viewDidLoad {
    // objc_msgSendSuper2[struct objc_super, SEL)
    [super viewDidLoad];

    // struct objc_super2 {
    // id receiver 等价于 self (2)
    // Class super_class 指向父类类型的指针,

    id cls = [Sark class];

    // (4)
    void *obj = &cls;

    [(__bridge id)obj speak];

}

// objc_msgSendSuper2() takes the current search class, not its superclass.
OBJC_EXPORT id objc_msgSendSuper2(struct objc_super *super, SEL op, ...)
__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_2_0);

objc_msgSendSuper2方法参数是一个objc_super *super.

/// Specifies the superclass of an instance. 
struct objc_super {
    /// Specifies an instance of a class.
    __unsafe_unretained id receiver;

    /// Specifies the particular superclass of the instance to message. 
#if !defined(__cplusplus)  &&  !__OBJC2__
    /* For compatibility with old objc-runtime.h header */
    __unsafe_unretained Class class;
#else
    __unsafe_unretained Class super_class;
#endif
    /* super_class is the first class to search */
};
#endif

所以按viewDidLoad执行时各个变量入栈顺序从高到低为self,_cmd,super_class(等同于self.class),receiver(等同于self),obj

iOS runtime面试题_第2张图片
入栈顺序.png

第一个self和第二个_cmd是隐藏参数。第三个super_class和第四个self[super viewDidLoad]方法执行时候的参数。

在调用self.name的时候,本质上是self指针在内存想高位地址偏移一个指针。
因为在Objc中对象实质上是一个指向ClassObject地址的变量,即id obj = &ClassObject,而对象的实例变量 void *ivar = &obj + offset(N)

- Obj :        0x7fff5d52b978
- Self:        0x7fff5d52b980
- super_class: 0x7fff5d52b988
- _cmd:        0x7fff5d52b990
- self:        0x7fff5d52b998

所以如上,obj就是cls的地址,向上偏移一个指针(这里指针占8个字节)就是self的地址,所以输出:

my name's 

四. Category

下面的代码会?Compile Error / Runtime Crash / NSLog…?

@interface NSObject (Sark)
 + (void)foo;
 - (void)foo;
 @end
@implementation NSObject (Sark)
 - (void)foo
 {
      NSLog(@"IMP: -[NSObject(Sark) foo]");
 }
 @end
 int main(int argc, const char * argv[]) {
  @autoreleasepool {
      [NSObject foo];
      [[NSObject new] foo];
  }
  return 0;
}

**答案: **

IMP: -[NSObject(Sark) foo]
IMP: -[NSObject(Sark) foo]

分析:

  • 在加载NSObjectCategory中,在编译期会提示我们没有实现+(void)foo的方法,因为在.m文件中并没有找到+方法的实现,只有一个-号方法,所以会警告提示。

  • 在实际加载Category的时候,会把-(void)foo加载进去,由于是实例方法,所以会放在NSObject的实例方法链表里面。(这里涉及到Category的知识,推荐:美团的这篇经典的深入理解Objective-C:Category)

  • 根据objc_msgSend原理,我们知道在调用[NSObject foo]的时候,会先在NSObjectmeta-class中去查找foo方法的IMP,未找到,继续在superClass中去查找,NSObjectmeta-classsuperClass就是本身NSObject,于是又回到NSObject的类方法中查找foo方法,于是找到了,就执行foo方法,输出:

    IMP: -[NSObject(Sark) foo]
    
  • 同样在调用[[NSObject new] foo]的时候,会先生成一个NSObject的对象,用这个NSObject实例对象再去调用foo方法的时候,会先去NSObject的类方法里面去查找,找到,输出:

    IMP: -[NSObject(Sark) foo]
    
  • 所以会输出两个相同的结果。

你可能感兴趣的:(iOS runtime面试题)