iOS - Objective-C 对象的分类以及 isa、superclass 指针

image

在 Objective-C 中对象分为三类:

  • Instance 对象(实例对象)
  • Class 对象(类对象)
  • Meta-class(元类对象)

实例对象

在 Objective-C 中,就是通过类 alloc 出来的对象,每次调用 alloc 都会产生新的实例对象。
基本创建形式:

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

其中 objc1 和 objc2 为两个不同的两个对象,占据着两块不同的内存空间。对象在内存中存储的信息包括:

  • isa 指针
  • 其他成员变量

关于 NSObject 的「本质」 中讨论过,对象最终都会转成 C\C++ 的结构体形式。并且,所有的实例对象中都存储着 isa 这个成员变量,因为 Objective-C 中几乎所有的类都继承自 NSObject,那么在这些类的对象的内存中肯定包含着 isa。

image

并且 isa 的内存地址即是该对象的地址。

类对象

类对象在前面其实已经见过面,就是 Class 对象,我们可以通过调用 Objective-C 对象的 class 方法获取,如:

Class objClass = [obj1 class];

或者:

Class objClass2 = [NSObject class];

或者借助 runtime 方法:

Class objClass3 = object_getClass(obj1);

并且,一个类的类对象在内存中只有一份,可以推测上述 objClass1、objClass2、objClass3 在内存中的地址应该是一样的。打印三者内存地址发现都是:

0x7fffb1439140

Class 对象在内存中存储的信息主要包括:

  • isa 指针
  • superclass 指针
  • 类的属性信息(@property)、类对象的方法信息(instance method)
  • 类的协议信息(protocol)、类的成员变量信息(ivar)

其中成员变量信息不指该类的成员变量的值,值是由实例变量决定的,成员变量信息是指成员变量的名字、类型... 这些描述信息。这些描述信息只需要存储一份,就可以放到类对象里面去。

元类对象

元类对象(Meta-Class),为描述类对象的对象。元类对象也是能够获取的,方法如下:

Class metaClass = object_getClass([NSObject class]);

或者

Class metaClass2 = object_getClass(objClass);

元类对象也是通过 Class 接收。

获取类对象也是通过 object_getClass() 方法,只不过参数一个是实例对象,一个是类对象。

每个类在内存中有且只有一个元类对象,元类对象和类对象内存结构其实是一样的,在内存中存储的信息主要包括:

  • isa 指针
  • superclass 指针
  • 类的类方法信息
  • ...

另,可通过 class_isMetaClass() 来检验是否为元类对象。

isa 指针

instance、class、meta-class 三者关系为:


image

连接三者的纽带,就是这个 isa 指针:

  • instance 的 isa 指针指向 class,当调用对象方法时,通过 instance 的 isa 找到 class,最后找到对象方法的实现进行调用
  • class 的 isa 指向 meta-class,当调用类方法时,通过 class 的 isa 找到 meta-class,最后找到类方法的实现进行调用

当前有 Person 类,定义、实现如下:

@interface Person : NSObject
{
    @public
    int _age;
    
}

- (void)instanceMethod;
+ (void)classMethod;

@end

@implementation Person

- (void)instanceMethod {
    NSLog(@"Instance Method");
}
+ (void)classMethod {
    NSLog(@"Class Method");
}

@end

实例方法 instanceMethod() 调用形为:

Person* p = [[Person alloc] init];
[p instanceMethod];

[p instanceMethod] 本质为:

objc_msgSend(p, @selector(instanceMethod));

即所谓的消息转发机制。类方法调用形为:

[Person classMethod]

其本质和实例方法相同,形如

objc_msgSend([Person class], @selector(classMethod))

这里也可以间接的看到 isa 的作用,由于对象方法并不在对象里面,而是在类对象中存储,所以需要有个媒介联系对象和类对象,同理,类方法也并不在类对象里,而是在元类对象中存储,所以同样的也需要一个媒介来联系类对象和元类对象。

superclass 指针

superclass 指针亦是重要的一个角色,从名字便可得知,这个指针和继承和关系。那么它和 isa 又有什么不同?

新定义 Valenti 类继承自 Person:

@interface Valenti : Person
{
    @public
    int _height;
    int _weight;
}

@end

@implementation Valenti

@end

此时的继承关系为:Valenti -> Person -> NSObject,所以 superclass 指针连接的三者关系为:

image.png

由于是继承关系,Valenti 的实例可以调用 Person 的实例方法 instanceMethod()

[v instanceMethod];

Q. 那么,在这层调用关系中,isa 指针和 superclass 指针起了什么作用?
A. 实例对象 v 的 isa 指针先找到 Valenti 的类对象,再通过 Valenti 的 superclass 指针找到 Person 的类对象,然后找到方法调用。同理 init() 方法,最终是找到 NSObject 的 init() 方法。

元类对象的 superclass 指针

和类对象的 superclass 指针道理相同,上节三者元类关系如下:

image.png

同理,当 Valenti 的类对象调用 Person 的 classMethod() 时,会先通过 isa 找到 Valenti 的元类,通过 superclass 指针找到 Person 的元类,最后找到对应方法调用。

最后,实例对象、类对象、元类对象的 isa 与 superclass 指针的最终指向关系为:

image.png

若没有父类,则 superclass 指针为 nil,基类的 superclass 指向基类的 class。

类别

那么类别(Category)中的类方法或者实例方法的调用 isa 和 suerclass 指针又起着什么作用?

假如此时有 NSObject 类别 NSObject+Category

定义:
@interface NSObject (Category)

+ (void)method;
@end

实现:
@implementation NSObject (Category)
+ (void)method {
    
    NSLog(@"[NSObject]%p", self);
}
@end

然后声明 Person 类继承 NSObject,声明和 NSObject 类别同名的 method()

@interface Person : NSObject

+ (void)method;

@end

@implementation Person

+ (void)method {
    
    NSLog(@"[Person]%p", self);
}

运行如下代码:

[Person method];
[NSObject method];

结果为:

[Person]0x1000011d8
[NSObject]0x7fff8a78e140

两个方法调用者分别是 Person 的类对象和 NSObject 的类对象,通过 isa 指针可以找到 Person 的元类对象以及 NSObject 的元类对象,最后进行方法调用。

假如把 Person 的 method() 方法实现去掉,保留声明:

@interface Person : NSObject

+ (void)method;

@end

@implementation Person

@end

运行代码,结果如下:

[NSObject]0x100001198
[NSObject]0x7fff8a78e140

此时 Person 的元类方法已经没有 method() 类方法的实现,通过 superclass 指针找到 NSObject 的元类方法,找到 method() 类方法进行调用。

我们再做些改动,将 NSObject 分类的 method() 类方法实现改成实例方法:

定义:
@interface NSObject (Category)
+ (void)method;
@end

实现:
@implementation NSObject (Category)
- (void)method {    
    NSLog(@"[NSObject]%p", self);   
}
@end

运行代码,结果如下:

[NSObject]0x100001198
[NSObject]0x7fff8a78e140

居然调用成功,那为什么用类调用 method() 方法会成功调用实例方法 method() ?

两次打印结果,发现第一行方法调用者都是 Person 的类对象?答案很简单,给 Person 类对象发送消息的时候,首先会通过 isa 指针找到 Person 的元类对象,发现并无对应方法,然后通过 superclass 找到 NSObject 的元类对象,但还是无 method() 的类方法,然后通过 NSObject 的 superclass 指针找到 NSObject 的类对象,发现有 method() 的实例方法,进行调用。

事实证明,类别中的方法和在普通类中声明实现是一样的,isa 和 superclass 指针的作用也未曾变化。

借助 Xcode 调试 isa 和 superclass

isa

上述 isa 的关系也可以通过 Xcode 调试来证明,打印:

Person* person = [[Person alloc] init];
Class personClass = [Person class];
Class personMetaClass = object_getClass(personClass);
NSLog(@"%p, %p, %p", person, personClass, personMetaClass);

结果为:

0x100555f90, 0x100001148, 0x100001120

分别代表着实例对象、类对象、元类对象。

实例对象 isa 存储的地址值是类对象的地址,类对象的 isa 存储的地址是 元类对象的地址,此时通过 Xcode 调试:

image.png

在 [0] (Person) 指针一栏,右键 -> Print Desciption of "[0]",借用命令打印地址:
print/x (long)person->isa

long 表示将地址转成 long 形式。
/x 表示格式化成十六进制。

结果:


image.png

相当于 person 的地址为 0x001d800100001149,地址形式略有偏差,因为在旧的 iOS 系统中实例对象的 isa 存储的值直接类对象的地址,但是从 64bit 开始,isa 需要和 ISA_MASK 进行一次位运算,才能计算出地址,同理类对象和元类对象。

image.png

在 objc 源码中有对应宏定义:

# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
# elif __x86_64__
#   define ISA_MASK        0x00007ffffffffff8ULL
# endif

但当我们打印 print/x (long)personClass->isa 的时候却发现报错:

error: member reference base type 'Class' is not a structure or union

这是因为底层的结构体的 isa 指针是不对外暴露的,我们干脆自己定义结构体,这个结构体为底层结构体的高仿版:

struct v_objc_class {
    Class isa;
};

对 personClass 进行强转:

struct v_objc_class* personClass2 = (__bridge struct v_objc_class *)(personClass);

再进行调试,打印 print/x (long)personClass2->isa,结果如下:

image.png

再打印元类的地址:


image.png

类对象的 isa 指针存储的地址和元类对象的地址并不相同,我们猜想,假如我们通过类对象的 isa 地址和 ISA_MASK 进行位运算得到的结果是否能元类对象的地址呢?此时我们执行命令:
print/x 0x001d800100001121 & 0x00007ffffffffff8 结果为:

image.png

刚好是元类对象的地址!

superclass

按照同样的方式调试 superclass,首选我们定义两个类 Person 和 Valenti,Valenti 继承自 Person,保留上节自定义的结构体并增加 superclass 属性:


struct v_objc_class {
    Class isa;
    Class superclass;
};
@interface Person : NSObject
@end

@implementation Person

@end

@interface Valenti : Person

@end

@implementation Valenti

@end

加断点运行:


image.png

借助命令 print/x valentiClass->superclass 得到结果:

image.png

0x00000001000011c8 也刚好是 Person 的类对象地址。

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

推荐阅读更多精彩内容