iOS源码解析:runtime super,isKindOfClass,isMemberOfClass

栏目: IOS · 发布时间: 6年前

内容简介:一 super的本质先从一个面试题开始探讨super相关的问题。有一个Person类和一个Student类,Student类继承自Person类。现在在Student类的init方法中有如下打印:问打印结果是什么。

一 super的本质

先从一个面试题开始探讨super相关的问题。有一个Person类和一个Student类,Student类继承自Person类。现在在Student类的init方法中有如下打印:

NSLog(@"[self class] = %@", [self class]);
NSLog(@"[super class] = %@", [super class]);
NSLog(@"[self superclass] = %@", [self superclass]);
NSLog(@"[super superclass] = %@", [super superclass]);

问打印结果是什么。

按照以前的理解,第一个应该是Student类的类对象,第二个是student类的父类的类对象,也就是Person类的类对象,第三个也是Student类的父类的类对象,第四个是Student类的父类的父类的类对象也就是NSObject类的类对象。

那么真实的打印情况是不是这样?我们看一下:

2018-09-17 15:54:02.224686+0800 TEST[8409:174143] [self class] = Student
2018-09-17 15:54:02.224922+0800 TEST[8409:174143] [super class] = Student
2018-09-17 15:54:02.225040+0800 TEST[8409:174143] [self superclass] = Person
2018-09-17 15:54:02.225922+0800 TEST[8409:174143] [super superclass] = Person

通过打印结果可以看到,第二个和第四个与我之前理解的不一致,这是为什么呢? [super class] 的打印结果为什么是Student呢?在搞清楚这些问题之前,我们先搞清楚 class superclass 方法的实现。我在runtime的NSObject.mm文件中找到了实现的源码:

/*******************************************************

 + (Class)class {
 return self;
 }

 - (Class)class {
 return object_getClass(self);
 }

 + (Class)superclass {
 return self->superclass;
 }

 - (Class)superclass {
 return [self class]->superclass;
 }
 //通过对象的isa指针获取类的类对象
Class object_getClass(id obj)
 {
 if (obj) return obj->getIsa();
 else return Nil;
 }

Class class_getSuperclass(Class cls)
{
    if (!cls) return nil;
    return cls->superclass;
}
******************************************************/

为了搞清楚super的实现,我在Person类中实现run方法,然后在Student类的init方法中使用 [super run] 来调用,然后将其转化为C++的源码,找到 [super run] 的实现:

((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Student"))}, sel_registerName("run"));

简化一下:

objc_msgSendSuper(__rw_objc_super{
self, 
class_getSuperclass(objc_getClass("Student")}, 
@selector(run));

我们可以看到,这个 objc_msgSendSuper() 函数中传入了两个参数,一个是一个结构体:

__rw_objc_super{
self, 
class_getSuperclass(objc_getClass("Student")}

还有一个就是消息 @selector(run)

第一个参数这个结构体中有两个成员变量,一个是self也就是Student实例对象,还有一个是Student类的父类的类对象,也即是Person类的类对象。

struct objc_super {
    __unsafe_unretained _Nonnull id receiver;
    __unsafe_unretained _Nonnull Class super_class;
};

第一个成员是消息接收者,第二个参数是父类对象。

我们再看一下 objc_msgSendSuper

的实现:

iOS源码解析:runtime super,isKindOfClass,isMemberOfClass

这个解释已经非常清楚了,也就是objc_msgSendSuper()中相当于传入了三个参数: objc_msgSendSuper(object ,superclass, @selector(run)) ,第一个参数是消息的接收者,第二个参数决定了从这个父类对象开始寻找方法的实现,第三个参数就是消息。

回到 [super run] ,这个方法也就是给student对象发送@selector(run)消息,但是查找run方法的实现要从Student类的父类对象也即是Person类的类对象中开始查找。

[self class]

就是通过实例对象的isa指针找到找到其类对象,所以打印是Student。

[super class]
是给self对象发送@selector(class)消息,但是class方法的实现要从Person类对象开始查找。class方法是在基类NSObject类中实现的,所以不管是从Student类对象中开始查找还是从Person类对象中开始查找方法的实现,做种都是找打NSObject的实现中,所以 [self class][super class] 并无差异,打印都是Student。
[self superclass]

这个获取的就是自己的类对象的superclass指针的指向,就是父类的类对象,所以打印是Person。

[super superclass]

这个其实和第二个的情况是一样的,给student对象发送 @selector(superclass) 消息,但是superclass的实现要从父类Person类的类对象开始找起,但是superclass的实现是基类NSObject类实现的,所以从Student类的类对象和Person类的类对象开始查是没有区别的。最终输出都是student对象的父类对象,打印结果是Person。

二 isKindOfClass ,isMemberOfClass

再来看一个面试题,看下面的打印结果:

BOOL res1 = [[NSObject class] isKindOfClass:[NSObject class]];
        BOOL res2 = [[NSObject class] isMemberOfClass:[NSObject class]];
        BOOL res3 = [[Person class] isKindOfClass:[Person class]];
        BOOL res4 = [[Person class] isMemberOfClass:[Person class]];

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

我们首先看一下打印结果:

1, 0, 0, 0

在分析这个问题之前,我们先查看 isMemberOfClass: isKindOfClass: 的源码来搞明白其具体实现:

/*******************************************
//object_getClass()取得的是对象的isa指针指向的对象,也就是判断传入的类对象的元类对象是否与传入的这个对象相等,所以这个cls应该是元类对象才有可能相等
 + (BOOL)isMemberOfClass:(Class)cls {
 return object_getClass((id)self) == cls;
 }

 判断传入的实例对象的类对象是否与传入的对象相等,所以cls只有可能是类对象才有可能相等
 - (BOOL)isMemberOfClass:(Class)cls {
 return [self class] == cls;
 }

//循环判断传入的类对象的元类对象及其父类的元类对象是否等于传入的cls
 + (BOOL)isKindOfClass:(Class)cls {
 for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
 if (tcls == cls) return YES;
 }
 return NO;
 }

//循环判断实例对象的父类的类对象是否等于传入的对象cls,也就是判断实例对象是否是cls及其子类的一种
 - (BOOL)isKindOfClass:(Class)cls {
 for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
 if (tcls == cls) return YES;
 }
 return NO;
 }
****************************************/

通过这个两个方法的源码我们可以知道, isMemberOfClass: 是检测方法调用者对象的类是否等于传入的这个类。 isKindOfClass: 是判断方法调用者对象的类是否等于传入的这个类或者其子类。还有一个适用于这四个方法的一点是,如果方法调用者是实例对象,那么传入的就应该是类对象;如果方法调用者是类对象,那么传入的就应该是元类对象。

下面先从第二个开始分析:

[[NSObject class] isMemberOfClass:[NSObject class]];

方法调用者是 [NSObject class] 也就是类对象,但是传入的参数也是类对象,所以很显然打印结果是0。
带三个:

[[Person class] isKindOfClass:[Person class]];

方法的调用者是类对象,传入的参数也是类对象,所以打印结果是0。

第四个:

[[Person class] isMemberOfClass:[Person class]];

方法调用者是类对象,传入的参数也是类对象,所以打印的是0。

最后来看第一个:

[[NSObject class] isKindOfClass:[NSObject class]];

按照和上面一样的分析,方法调用者和传入的对象都是类方法,那么应该打印0才对呀,为何会打印1呢?我们回过头去看一下 + (BOOL)isKindOfClass:(Class)cls; 的实现:

//循环判断传入的类对象的元类对象及其父类的元类对象是否等于传入的cls
 + (BOOL)isKindOfClass:(Class)cls {
 for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
 if (tcls == cls) return YES;
 }
 return NO;
 }

我们看到这个实现方法, object_getClass((id)self) 是先获得元类方法,判断其是否等于cls,然后沿着继承链取元类对象的superclass,我们想一下,当沿着继承链,一直不满足 tcls == cls 时,最终会找到NSObject类,取NSObject类的元类的superclass,而NSOBject的元类的superclass指向的是NSOBject类的类对象,所以打印输出是1。

三 一个很绕的面试题

Person类中有一个属性叫name,Person类中还有一个print方法用来打印name属性:

//Person.m
- (void)print{

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

}

问题是下面这段代码的打印结果:

//ViewController.m
- (void)viewDidLoad {
    [super viewDidLoad];

    id cls = [Person class];
    void *obj = &cls;

    [(__bridge id)obj print];

}

如果在正常工作中写出这种代码会被炒鱿鱼的,绕来绕去非常古怪,但是如果是学习的话还是可以好好探究一番。

首先我们还是来看一下打印结果吧:

 my name is 

  
 
   0x7fd18a50a550>
 
   

通过打印结果,这里面有两个疑惑:

  • 1.print是一个实例方法但是代码中自始至终没有创建实例对象,只有一个类对象,那么为什么能够调用对象方法呢?

  • 2.即使能够调用对象方法,为什么打印出来的是ViewController的地址呢?

下面我们先来分析一下

id cls = [Person class];
    void *obj = &cls;

这两句代码。

首先创建了一个id类型的cls指针指向Person类对象。然后又创建了一个指针变量obj,obj中存放的是cls的地址,用图表示其结构如下:

iOS源码解析:runtime super,isKindOfClass,isMemberOfClass

然后我们再看下面一句代码:

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

这里创建了一个Person类型的指针变量指向alloc出来的Person实例对象。Person实例对象的结构其实就是一个结构体,这个结构体里面存放着isa指针和成员变量,具体到Person实例对象,这个实例对象中存放着指向Person类对象的isa指针和_name成员变量。由于person对象的第一个成员变量是isa指针,所以person指针指向的其实就是isa指针所在的内存,所以其结构图如下:

iOS源码解析:runtime super,isKindOfClass,isMemberOfClass

对比一下上下两个图,是否非常相似呢?

调用 [person print]

那么对于obj指针也是一样的,对于 [obj print] ,通过obj指针获取cls指针的起始位置,同样可以取其前八字节(虽然整个cls只占8字节,但是没关系),作为'isa'指针,然后读出其地址值,就可以获取类对象,从而调用其中的方法。

这样就回答了第一个问题,即为什么obj能调用对象方法。

对于第二个问题,我们在cls指针前面创建一个字符串试试:

NSString *str = @"test";

    id cls = [Person class];
    void *obj = &cls;

    [(__bridge id)obj print];

看一下打印结果:

my name is test

这就很神器了,居然打印出来的死str字符串的值,那么我们再试着在cls指针前面创建一个NSObject对象试试:

NSObject *object = [[NSObject alloc] init];

    id cls = [Person class];
    void *obj = &cls;

    [(__bridge id)obj print];

打印结果:

my name is 

  
 
   0x6040000074c0>
 
   

打断点后证实object对象的地址正是打印的地址,也就是打印的是object对象,这就很玄乎了。

我们先来搞清楚一个问题,栈空间的分配是从高地址开始分配还是低地址开始分配?我们试验一下便知道:

NSObject *obj1 = [[NSObject alloc] init];
    NSObject *obj2 = [[NSObject alloc] init];
    NSObject *obj3 = [[NSObject alloc] init];
    NSObject *obj4 = [[NSObject alloc] init];

    NSLog(@"%p, %p, %p, %p", &obj1, &obj2, &obj3, &obj4);

打印结果:

0x7ffeeec26ac8, 0x7ffeeec26ac0, 0x7ffeeec26ab8, 0x7ffeeec26ab0

obj1,obj2,obj3,obj4都是局部变量,分配在栈区,可以看到,obj1的地址位最高,是 0x7ffeeec26ac8 ,由于指针变量占8字节,所以obj2的地址是在此基础上减8,也就是 0x7ffeeec26ac0 ,obj3的地址是在obj2的基础上减8。

这就说明栈区的内存是从高地址到低地址分配的。

我们再来分析下下面代码的内存关系:

NSString *str = @"test";

    id cls = [Person class];
    void *obj = &cls;

    [(__bridge id)obj print];

首先在栈空间分配了一个NSString类型的指针str,这个指针指向常量区的test字符串,然后在低8字节的栈空间又分配了一个id类型的指针cls,这个指针指向Person类对象,然后又在低8字节的占空间分配了一个指针obj,这个指针是指向cls指针的。test,cls,obj它们的地址四连续分配的。

iOS源码解析:runtime super,isKindOfClass,isMemberOfClass

我们知道print方法是打印self.name,也就是读取person对象的成员变量_name的值,由于Person类只有一个属性name,所以其实例对象的结构也是非常简单的,就是一个isa指针和一个_name成员变量。 [person print] 是怎样去获取_name的值的呢?

person指针指向的是person实例对象的地址,person实例对象的前8字节是isa指针,那么只需要读取person指针的地址,从这个地址开始的前8字节就是isa指针,同理,再往下取8字节就是_name成员变量的值。

那么 [obj print] 也是一样的,obj指针把cls当做了person实例对象,所以它会怎么去取_name的值呢?还是一样的,通过obj存放的地址获取cls的起始地址,然后读从这个起始地址开始的前八字节当做isa指针,再往后取8字节当做_name成员变量的值。cls往后取第9到16字节的值正是@"test"字符串,所以打印出来的也就是字符串。

现在我们再回到原来的问题,为什么打印的是 也就是视图控制器对象。

我们来分析一下下面的一句代码:

[super viewDidLoad];

我们在第一部分就讲过super调用的问题,这段代码的本质就是:

objc_msgSendSuper({self,
    class_getSuperclass(objc_getClass("ViewController"))},
    @selector(viewDidLoad))

第一个参数传入的是一个结构体,结构体如下:

struct objc_super{
        self,
        class_getSuperclass(objc_getClass("ViewController"))
    };

那么 [super viewDidLoad]; 也就相当于声明了一个结构体类型的局部变量,这个局部变量有两个成员,所有新的内存结构如下:

iOS源码解析:runtime super,isKindOfClass,isMemberOfClass 这个时候去调用print,读取的_name也即是self,就是控制器了。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

PERL學習手札.

PERL學習手札.

簡信昌 / 上奇科技 / 20040816 / NT$ 390

1. 關於Perl 當你翻開這本書的時候,你也就進入了一個奇幻的世界。Perl確實是一種非常吸引人的程式語言,而之所以這麼引人入勝的原因不單單在於他的功能,也在於他寫作的方式,或說成為一種程式寫作的藝術。即使你只是每天埋首於程式寫作的程式設計師,也不再讓生活過份單調,至少你可以嘗試在程式碼中多一些變化。而且許多Perl的程式設計師已經這麼作了,這也是Perl的理念-「There is mor......一起来看看 《PERL學習手札.》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

SHA 加密
SHA 加密

SHA 加密工具