OC super关键字,isMemberOfClass,isKindOfClass区别

今天研究一下super关键字,在讲之前我们先看一下下面四条语句输出打印什么:备注说明:Student 继承 Person,两个类中都有 - (void)test 方法

@implementation Student

- (void)test{
    [super test];
    [self class];
    // class 方法的本质
    NSLog(@"[self class] : %@",[self class]); //取出 self 的类对象,所以应该输出 Student
    NSLog(@"[self superclass] : %@",[self superclass]);//取出 self 的 superclass ,应该输出  Peson
    NSLog(@"[super class] : %@",[super class]); //取出 self 的父类的类对象,应该输出 Person
    NSLog(@"[super superclass] : %@",[super superclass]);//取出 self 的父类的 superclass ,应该输出 NSObject
}

运行一下代码,看看我们分析的对不对:

19-12-03 11:36:13.991206+0800 superTest[1494:254018] [self class] : Student
2019-12-03 11:36:13.991651+0800 superTest[1494:254018] [self superclass] : Person
2019-12-03 11:36:13.991691+0800 superTest[1494:254018] [super class] : Student
2019-12-03 11:36:13.991734+0800 superTest[1494:254018] [super superclass] : Person

[self class],[self superclass]我们都分析对了,为什么[super class],[super superclass]的结果和我们分析的不一样呢?
要搞清楚这个问题我们就需要搞懂super关键字,class(),superClass()的底层,我们把Student.m转为c++代码看看底层是怎样的:

super 底层

发现super底层被转换为objc_msgSendSuper(arg1,arg2)函数,里面传入两个参数__rw_objc_super 结构体和 SEL,所以上面的代码也可以把__rw_objc_super抽离出去,换一种写法:
__rw_objc_super 分离

那么__rw_objc_super是什么呢,我们在runtime源码中搜搜objc_super:
objc_super

objc_super的底层就是消息的接受者和他的父类,结合这些底层知识我们把[super test]底层的c++代码修改一下:
super etst

super 方法的接受者仍然是子类,传入的父类是干嘛用的呢?
我们在runtime源码中搜索objc_msgSendSuper:
objc_msdSendSuper 注释

原来 superclass 是为了告诉 runtime ,查找方法的时候直接从 superclass 的类中查找.❎不要在像以前那样通过 实例对象 isa 找到类对象,从类对象的方法列表中查找,如果找不到再通过类对象的 superclass 找到父类从父类的方法列表中查找.❎

  • class方法的底层:
//class 底层实现
- (Class)class{
    return object_getClass(self);//获取方法接受者的类对象或者元类对象
   // object_getClass底层调用的 getIsa(),如果是实例对象获取的就是类对象,如果是类对象获取的就是元类对象.
}
  • superClass方法的底层:
// superclass 底层实现
- (Class)superclass{
    //先获取方法接受者的类对象
    //在获取它的父类对象
    return class_getSuperclass(object_getClass(self));
}

搞明白这几个关键字后,我们再回头看看[super class],[super superclass]:

  • [super class]:方法的接受者仍然是self,class()方法内部获取到self的类对象,所以还是Student.
  • [super superclass]:方法的接受者仍然是self,superclass()方法内部会现获取self的类对象Student,在获取Student的父类Person.
    其实我们再调用class()的时候,最终调用的都是NSObject类中的class()方法.

isMemberOfClass 和 isKindOfClass 区别:

我们看看下面四句输出语句,仔细想想会打印输出什么:

- (void)test2{
    NSLog(@"%d", [NSObject isKindOfClass:[NSObject class]]); 
    NSLog(@"%d", [NSObject isMemberOfClass:[NSObject class]]); 
    NSLog(@"%d", [Student isKindOfClass:[Student class]]); 
    NSLog(@"%d", [Student isMemberOfClass:[Student class]]); 
}

分析一下,感觉好像都是YES呀,实际运行一下看看结果:

2019-12-03 15:31:24.838726+0800 superTest[1794:338777] 1
2019-12-03 15:31:24.839133+0800 superTest[1794:338777] 0
2019-12-03 15:31:24.839170+0800 superTest[1794:338777] 0
2019-12-03 15:31:24.839224+0800 superTest[1794:338777] 0

怎么样,跟你们分析的答案一样吗?我们还是看看这两个方法的本质:


+ (BOOL)isMemberOfClass:(Class)cls {

// 获取接收者的元类对象 , 直接和 传进来的 cls 比较,也就是说传进来的 cls 也应该是 元类对象,否则就为 false
    return object_getClass((id)self) == cls;
}

- (BOOL)isMemberOfClass:(Class)cls {
// 获取消息接收者的类对象 和 传入的 cls 判断是否相等
    return [self class] == cls;
}

+ (BOOL)isKindOfClass:(Class)cls {

// 第一步: 获取传入的消息接收者的元类对象
// 第二步: 判断和传入的 cls 是否相等,也就是说传入的也必须是 元类对象, 否则为 false
// 第三步: 如果不相等,继续找这个 元类对象的 superclass 继续比较,直到 superclass 为 nil.
    for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}

- (BOOL)isKindOfClass:(Class)cls {
// 第一步: 取出消息接收者的类对象,和传入的 cls 判断是否相等
// 第二步: 如果不相等,继续找这个类对象的 superclass 继续比较,直到 superclass 为nil 为止.
    for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}

根据源码分析,+开头的方法,传入的必须是元类对象,所以我们后面三条输出语句都是false.如果想要输出true,这样改动即可:

- (void)test2{
    NSLog(@"%d", [NSObject isKindOfClass:[NSObject class]]); // 1
    NSLog(@"%d", [NSObject isMemberOfClass:object_getClass([NSObject class])]); //1
    NSLog(@"%d", [Student isKindOfClass:object_getClass([Student class])]); //1
    NSLog(@"%d", [Student isMemberOfClass:object_getClass([Student class])]); //1
}
// 输出结果
2019-12-03 16:05:07.352199+0800 superTest[1820:351570] 1
2019-12-03 16:05:07.352531+0800 superTest[1820:351570] 1
2019-12-03 16:05:07.352564+0800 superTest[1820:351570] 1
2019-12-03 16:05:07.352590+0800 superTest[1820:351570] 1

那为什么第一条语句[NSObject isKindOfClass:[NSObject class]]同样也是输出true呢?右边应该是一个元类对象呀.我们在OC对象的底层结构及isa、superClass详解已经详细讲过isasuperclass.当时还重点把基类元类对象的 superclass 指向类对象这条线用⭕️标记了出来,因为它太特殊.

基类元类对象的 superclass 指向类对象

所以[NSObject isKindOfClass:[NSObject class]]这条语句会从元类对象一直找到NSObject类对象.事实上,所有继承自NSObject类的子类调用[** isKindOfClass:[NSObject class]]都是成立的.

进阶训练:

创建一个Person类,类中有一个有一个test方法:

@interface Person : NSObject

@property(nonatomic,copy)NSString *name;

- (void)test;

@end



@implementation Person

- (void)test{
    NSLog(@"my name is %@",self.name);
}

@end

现在我们像下面这样调用:

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    NSString *str = @"123";
    id personClass = [Person class];
    void * pointer = &personClass;
    [(__bridge Person *)pointer test];
}

@end

大家分析一下pointer test能不能调用成功,为什么?如果调用成功会打印什么?为什么?
我们运行一下代码,看看能不能成功:

2019-12-04 09:56:44.802983+0800 指针调用方法[942:50150] my name is 123

调用成功了,但是为什么打印的是123?这个123是不是局部变量 str = @"123"?我们修改str = @"666再运行一下:

2019-12-04 09:59:01.033985+0800 指针调用方法[953:51910] my name is 666

发现打印的就是局部变量str的值.很奇怪,怎么会这样呢?下面我们就好好分析一下:
分析:
如果我们要调用test方法,正常的做法应该是这样:

    //调用test方法正常步骤
    Person *person = [[Person alloc]init];
    [person test];

我们在OC对象的底层结构及isa、superClass详解这一篇中知道了方法调用的本质就是通过isa指针找到对应的类,然后查找方法.所以上面的代码本质上就是这样:

[person test] 本质

我们再画图分析一下[(__bridge Person *)pointer test]:
[(__bridge Person *)pointer test] 图解

他们的内存结构何其相似.personClass在这里不就等价于isa么?他们都指向Person类对象.[person test]是通过实例对象 personisa指针找到Person 类对象,然后从类对象的方法列表中查找方法.所以,方法的调用本质就是只要能找到类对象.而[(__bridge Person *)pointer test],指针变量pointer中存储的personClass恰巧就指向类对象,所以最后能调用成功.
那为什么打印my name is 666是局部变量的值呢?这是因为栈内存分配空间的机制导致的,我们写一个方法:

- (void)stackMemoryTest{
    int a = 1;
    int b = 2;
    int c = 3;
    int d = 4;
    NSLog(@"a: %p",&a);
    NSLog(@"b: %p",&b);
    NSLog(@"c: %p",&c);
    NSLog(@"d: %p",&d);
}
// 打印结果
2019-12-04 10:34:37.514299+0800 指针调用方法[1125:79517] a: 0x7ffeefbfea3c
2019-12-04 10:34:37.514362+0800 指针调用方法[1125:79517] b: 0x7ffeefbfea38
2019-12-04 10:34:37.514390+0800 指针调用方法[1125:79517] c: 0x7ffeefbfea34
2019-12-04 10:34:37.514415+0800 指针调用方法[1125:79517] d: 0x7ffeefbfea30

从打印结果中可以看到,栈内存分配空间是从高地址往低地址分配的,先创建的局部变量分配在高地址,后创建的分配在低地址.所以一下这段代码在内存中的布局如图:

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    NSString *str = @"123";
    id personClass = [Person class];
    void * pointer = &personClass;
    [(__bridge Person *)pointer test];
}

@end

viewdidLoad 内存布局

那最后如何会输出 my name is 666呢?我们参考一下[ person test ]:
[ person test ] 内存布局

我们在获取self.name的时候,如果转换成汇编代码会发现本质上就是找到isa指针,然后越过8个字节,从而找到_name.这就是内存访问的本质:找到某块内存的地址,读取地址中的值.
回到[(__bridge Person *)pointer test],pointer也会同样的越过personClass这8个字节找到str.所以最后就打印的是my name is 666;
如果把NSString *str = @"666";这段代码注释掉会打印什么?

2019-12-04 11:38:31.350106+0800 指针调用方法[1201:119557] my name is <ViewController: 0x600002913720>

会发现打印的是ViewController,这又是为什么呢?
这就是super关键字引起的.因为[super viewDidLoad];这句代码:

[super viewDidLoad] 的底层

super底层被转换为objc_msgSendSuper(arg1,arg2)这个函数,而这个函数需要两个参数.第一个参数就是__rw_objc_super结构体,这个结构体中有两个成员:self (ViewController) 和它的父类 UIViewController';第二个参数就是方法名了.
所以,这里就相当于存在一个结构体类型的局部变量,我们画图说明:
隐藏的局部结构体变量

图中已经用红线标记出来了,在取self->name的时候,越过personClass的8个指针,整好找到了self也就是ViewController.从图中可以看到,只要比personClass先声明的局部变量,并且是先后声明的关系就会打印出来.
比如,我们在添加一个str2:
新增str2

str2 内存布局

关于 super 关键字的一点补充

在前面讲super关键字的时候,我们看到super转换为c++代码的时候,被转换成了objc_msgSendSuper(arg1,arg2)函数.其实实际上底层执行并不是objc_msgSendSuper(arg1,arg2)函数,而是objc_msgSendSuper2(arg1,arg2)函数.我们在[super viewDidLoad];处打个断点,然后显示汇编语言看一下:

super 底层调用

并且上面讲的objc_msgSendSuper(arg1,arg2)中的第一个参数arg1__rw_objc_super结构体,这个结构体如下:

struct __rw_objc_super { 
    struct objc_object *object; 
    struct objc_object *superClass; 
}

objc_super2的结构体如下:

struct objc_super2 {
    id receiver;
    Class current_class;
};

可以看到objc_super2这个结构体中传入的是Class current_class;也就是当前类.而在_objc_msgSendSuper2内部获取当前类的superClass:

_objc_msgSendSuper2 内部获取 superclass

我们可以通过访问内存验证一下:

如上图所示,我们取出结构体中的第二个成员看看是什么就清楚了.
取出 结构体第二个参数内容

所以,super底层其实是调用objc_msgSendSuper2()函数,然后传入的是当前类对象,只不过在内部又会取出当前类对象的superclass.
这只是一个小细节,和我们最开头说的也不矛盾,知悉就好.

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