runtime 小测试

下面代码输出什么?

self super

@implementation Son : Father

- (id)init

{

      self = [super init];

       if (self){

         NSLog(@"%@", NSStringFromClass([self class]));

          NSLog(@"%@", NSStringFromClass([super class]));

       }

return self;

}

@end

这道面试题,主要是考察self与super的

OC中调用方法,会被转成消息发送机制的函数 objc_msgSend(id self, SEL cmd, ...),因此,方法内self与objc_msgSend()函数中的self是等价的,就是调用时传入的消息的接受者(Objective-C高级编程 p94)。

super是一个编译器指示符

当使用 self 调用方法时,会从当前类的方法列表中开始找,如果没有,就从父类中再找;而当使用 super 时,则从父类的方法列表中开始找。这种机制到底底层是如何实现的?

例如,当调用[super class]时,会转为 objc_msgSendSuper(),而非objc_msgSend(),看下 objc_msgSendSuper 的函数定义:

id objc_msgSendSuper(struct objc_super *super, SEL op, ...)

struct objc_super {

    id receiver;

    Class superClass;

};

当编译器遇到 Son 里 init 方法里的 [super class] 时,开始做这几个事:

构建 objc_super 的结构体变量,此时该变量的第一个成员变量 receiver 就是当前方法内的self(Son *)而第二个成员变量 superClass 就是指当前方法所属类的父类 Father

调用 objc_msgSendSuper ,将结构体变量和 @selector(class) 传过去

objc_msgSendSuper函数里面在做的事情类似这样:从 objc_super 结构体指向的 superClass 的方法列表开始找 @selector(class) ,找到后再以 objc_super->receiver 去调用这个 selector,可能也会使用 objc_msgSend 这个函数

所以,当调用[self class]时,此时的self,就是init方法的接受者Son *。因此,[self class]转为objc_msgSend(),第一个参数是Son *,第二个参数是@selector(class)。根据isa先从Son类开始找,没有,然后到 Son的父类 Father中去找,也没有,再去 Father 的父类 NSObject 去找,一层一层向上找之后,在 NSObject 的类中发现这个 class 方法

- (Class)class {

     return object_getClass(self);

}

Class object_getClass(id obj)

{

      if (obj) return obj->getIsa();

       else return Nil;

}

self就是消息的接受者Son*,因此输出 Son

当使用 [super class] 时,这时要转换成 objc_msgSendSuper 的方法。先构造 objc_super 的结构体变量,第一个成员变量就是 self(Son *)第二个成员变量是 Father,然后要找@selector(class) 。先去 superClass 也就是 Father 中去找,没有,然后去 Father 的父类中去找,结果还是在 NSObject 中找到了。然后内部使用函数 objc_msgSend(objc_super->receiver, @selector(class))  去调用,此时已经和我们用 [self class] 调用时相同了,此时的 receiver 还是Son *,所以这里输出的还是Son

其实很好理解。例如,在Son init方法中,调用[super init];,消息的接受者还是self,不然给谁初始化?只是,查找方法从父类开始了。


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;

}

先来分析一下源码这两个函数的对象实现

类对象调用class方法,直接返回类本身

+ (Class)class {

     return self;

}

实例对象调用class方法,返回对象的isa

- (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);

}

无论是实例对象还是类对象,object_getClass(obj),均返回对象isa

类对象调用这个与元类比较,由于元类同样的继承关系,只要是类对象的元类是所比较元类或其子类都返回真

+ (BOOL)isKindOfClass:(Class)cls {

     //tcls 等价 于tcls != nil 因为根元类的父类是NSObject,NSObject的父类是nil

       for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {

   //能这么比较 tcls == cls,比较地址,也说明了 元类对象的唯一

              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;

//能这么比较 tcls == cls,比较地址,也说明了 类对象的唯一

}

类对象与元类对象比较,只有类对象的元类与所比较的元类相同才为真

+ (BOOL)isMemberOfClass:(Class)cls {

      return object_getClass((id)self) == cls;

}

实例对象与类对象比较,只有实例对象的类与所比较的类相同才为真

- (BOOL)isMemberOfClass:(Class)cls {

       return [self class] == cls;

}

这道题,除了考察这几个方法还是考察了类与元类的继承体系,尤其是NSObject

BOOL res1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];

[NSObject class]还是NSObject,NSObject的元类是根元类,与[NSObject class] == NSObject不等。循环,根元类的父类是NSObject,相等。res1 = YES

BOOL res2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];

[NSObject class]还是NSObject,NSObject的元类是根元类,与[NSObject class] == NSObject不等

BOOL res3 = [(id)[Sark class] isKindOfClass:[Sark class]];

[Sark class]还是Sark,Sark的元类与Sark不等。循环,Sark元类的父类是根元类,与Sark不等。循环,根元类的父类是NSObject,与Sark不等。NSObject的父类是nil,循环结束。

BOOL res4 = [(id)[Sark class] isMemberOfClass:[Sark class]];

Sark的元类与Sark不等

总结,isMember比较傲娇,调用者只取一次isa不等就是不等。isKindOf调用者取一次isa 不等取其父类


Class与内存地址

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

@interface Person : NSObject

@property (nonatomic, copy) NSString *name;

- (void)speak;

@end

@implementation Person

- (void)speak {

      NSLog(@"my name's %@", self.name);

}

@end

@implementation ViewController

- (void)viewDidLoad {

     [super viewDidLoad];

     id cls = [Person class];

      void *obj = &cls;

      [(__bridge id)obj speak];

}

@end

首先编译能不能通过?其次,调用输出什么?

答案是可以编译通过,输出 my name is

这道题,涉及到实例变量的内存结构和类与对象的关系

cls指向Sark类的指针,而obj是一个指针,存储着cls的地址,也就指向obj的指针,类似一个二级指针。然后调用实例方法。这是怎么回事呢?

首先,将C语言的指针类型转为OC的id类型,但方法也不能随便调用,这个方法存在当前类或者导入头文件的声明中,而我们导入了#import "Person.h",编译通过。

然后,cls指向了Person类,而我们定义一个对象Person *p = [Person new]; 对象的isa也指向Person类,而实例对象的第一个成员是isa,因此实例对象的地址与isa的地址是相同的。obj指向cls,如同指向了实例对象的isa,如同指向了对象。那么,也就是cls如同一个实例对象,obj类似一个指向实例对象的指针,即p。

Person *person = [[Person alloc] init];

NSLog(@"%@",person);  //

NSLog(@"%p %p",person,&person);  //0x6000000098c0 0x7fff5a353a88

一个是指针所指对象地址,一个是指针地址

我们说过,方法最终转为函数,而函数默认两个参数,一个是消息的接受者,一个是选择子,对应的是参数名是self与_cmd。我们在viewDidLoad打印几个地址:

NSLog(@"%p",self); //0x7fe5e2e0b570

NSLog(@"%@",self); //

NSLog(@"%p  %p",&self,&_cmd); //0x7fff58e08aa8  0x7fff58e08aa0

Class cls = [Person class];

void *p = &cls;

NSLog(@"%p %p",cls,&cls);  //0x106df8140 0x7fff58e08a88

NSLog(@"%p %p",p,&p);  //0x7fff58e08a88 0x7fff58e08a80

首先是ViewController实例对象的地址,然后是指向实例对象,指向选择子的指针地址,

NSLog(@"%p %p",cls,&cls);  打印的是cls指向的Person类的地址和cls的地址

NSLog(@"%p %p",p,&p);  打印的是p指向的cls的地址和p的地址

cls如同Person的实例对象,而p如同指向实例对象的指针,调用Person的方法

- (void)speak {

     NSLog(@"%p",&_cmd);//0x7fff58e08a40

     NSLog(@"%p  %p",self,&self);//0x7fff58e08a88  0x7fff58e08a48

     NSLog(@"my name's %@", self.name);

}

我们看到speak方法内,self指向的地址是0x7fff58e08a88,正是实例变量cls的地址。但是很明显,这个地址7fff与Person类地址106明显不像,一个是栈上的地址,一个是堆上的地址。

我们知道内存,对内存进行编制后,由下到上,地址越来越大,栈空间在上,分配时由上到下,越后分配的地址越小,而堆空间在下,分配时由下而上,越后分配的地址越大

cls的栈地址也说明了,它不是一个真正的分配在堆上的对象,或许这是披了一件外衣。但是,只有我们知道。

打印self.name时。前面说过实例对象在类中的内存结构。

Person对象

isa

*name

当一个类被编译时,实例变量的布局也就形成了,访问类的实例变量。从对象头部开始,实例变量依次根据自己所占空间而产生偏移量。

查找self.name也就是在实例变量的起始地址,偏移8个字节(64位下isa 8个字节),就得到name的首地址,而实例变量的首地址是 0x7fff58e08a88,偏移8个字节。(按字节编制,每个地址八位)

90   name

8f

8e

8d

8c

8b

8a

89

88 Person *

要去0x7fff58e08a90 查找name指针指向的字符串对象。可那么为什么会输出viewController相关的呢?

我们回到viewDidLoad方法,

NSLog(@"%p  %p",&self,&_cmd); //0x7fff58e08aa8  0x7fff58e08aa0

我们调用函数,参数入栈,self,_cmd的地址是a8,a0,方法内,[super viewDidLoad]; 我们前面说过super是个指示符,需要构造结构体作为函数参数

struct objc_super {

    id receiver;

    Class superClass;

};

superClass指针是self所在类的父类 98,receiver指针指向self所指的实例对象 90,,至于为什么_cmd没分配,我也不知道。接下来的代码就是 Class cls = [Person class]; cls是88,void *p = &cls; p是80

这也就符合我们的打印结果,那么,我们上面寻找的90就是self了。因此,打印 my name's %@ 就是self所指的对象ViewController了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容