特别备注
本系列文章总结自MJ老师在腾讯课堂开设的OC底层原理课程,相关图片素材均取自课程中的课件。
面试题 – 面向对象
对象的isa指针指向哪里?
- instance对象的isa指向class对象
- class对象的isa指向meta-class对象
- meta-class对象的isa指向基类的meta-class对象
OC的类信息存放在哪里?
- 对象方法、属性、成员变量、协议信息,存放在class对象中
- 类方法,存放在meta-class对象中
- 成员变量的具体值,存放在instance对象
isa指针
OC对象分为三类
instance对象
- 成员变量
- 特殊的成员变量 isa指针
class对象
,用来描述instance对象
- isa指针
- superclass指针
- 属性信息
- 对象方法信息(-方法)
- 协议信息
- instance对象的成员变量的描述信息
meta-class对象
,用来存放类方法
- isa指针
- superclass指针
- 类方法信息(+方法)
对一个类来说,它的instance
、class
、mete-class
对象之间,一定是有某种联系的。假设这种联系不存在,我们看看会碰到什么问题。比如我调用一个instance对象的方法
MJPerson *person = [[MJPerson alloc] init];
person->_age = 10;
[person personInstanceMethod];
它的底层是
通过xcrun编译转化成cpp文件
$ xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
objc_msgSend([MJPerson class], @selector(personClassMethod))
objc_msgSend(instanceObj, @sel_registerName("instanceObjMethod"));
也就是给instanceObj对象发消息。
在instance对象
不跟外界关联的情况下,它内部只有一些成员变量信息,是不可能完成方法调用的,因为对象方法是存放在class对象
里面的,对class对象
调用+方法
的时候也是一样,必须有办法跟meta-class对象
关联起来,才能完成对+方法
的调用。所以isa指针,就只它们之间的关联。可以通过下图来理解isa的作用。
大致可以归纳为
-
instance对象
的isa
指针指向class对象
。当调用对象方法(-方法)时,通过instance
的isa
找到class
,然后在class
的方法列表里面找到对应的实现进行调用。 -
class对象
的isa指针指向meta-class对象
。当调用类方法(+)方法时,通过class
的isa
找到meta-class
,最后在meta-class
的方法列表找到对应的实现进行调用。
那么通过isa的桥接作用,我们应该能更近一步地理解OC消息发送以及方法调用的过程了。
superclass指针
显而易见,从字面意思,我们就能知道,superclass就是父类的意思。
假定我们有以下几个类
// MJPerson
@interface MJPerson : NSObject <NSCopying>
{
@public
int _age;
}
@property (nonatomic, assign) int no;
- (void)personInstanceMethod;
+ (void)personClassMethod;
@end
// MJStudent
@interface MJStudent : MJPerson <NSCoding>
{
@public
int _weight;
}
@property (nonatomic, assign) int height;
- (void)studentInstanceMethod;
+ (void)studentClassMethod;
@end
我们知道superclass
指针存在于class对象
和meta-class对象
里面。我们根据接下来的图示来阐述一下:
一个类Student
的class对象
里面的superclass指针
指向该类的父类的class对象
当Student
的instance对象
要调用Person
的对象方法时,会先通过isa
找到Student
的class对象
,然后通过这个class对象
的superclass
找到Person(Student的父类)
的class对象
,最后找到相应的对象方法(-方法)的实现进行调用
一个类的meta-class
里面的superclass
指针指向该类的父类的meta-class
对象
当Student
的class对象
要调用Person
的类方法时,会先通过isa
找到Student
的meta-class对象
,然后通过这个meta-class对象
的superclass
找到Person(Student的父类)
的meta-class对象
,最后找到相应的类方法(+方法)的实现进行调用
isa、superclass总结
上图完整描述了isa、superclass指针的作用,为了更加便于理解,我们在后面的图例中用Student
代替subclass,Person
代替superclass,NSObject
代替Rootclass。
-
instance
的isa
指向class
-
class
的isa
指向meta-class
-
meta-class
的isa
指向基类的meta-class
-
class
的superclass
指向父类的class
,如果没有父类,superclass
指针为nil
-
meta-class
的superclass
指向父类的meta-class
,基类的meta-class
的superclass
指向基类的class
instance调用对象方法的轨迹
我们以[student abc];
为例,student
是Student
类的实例对象,调用轨迹如下图
对于
student
来说,并不知道abc
方法在哪里,唯一知道的就是可以去它的class对象
里面找,
- 于是先通过
isa
指针进入Student
类的class对象
,如果在其中找到了abc
就直接进行调用,调用过程结束,- 没找到的话,就通过
class对象
的superclass
指针进入Student
类的父类,也就是Person
类的class对象
,重复上一步的查找逻辑- 以此类推,一层一层往上寻找,如果最终到了基类,也就是
NSObject
类的class对象
里面,还没找到的话,由于它的superclass
为nil
,最终就会碰到一个经典的报错[ERROR: unrecognized selector sent to instance]
,调用轨迹结束
class调用类方法的轨迹
我们以[MJStudent abc];
为例调用轨迹图如下
对与
Student类
来说,abc
在哪也是不知道的,我们知道类方法被规定放在meta-class对象
里面,所以
- 首先,通过
Student
的class对象
的isa指针
找到其meta-class对象
,然后在方法列表里面寻找是否有abc
,有的话就调用,调用逻辑结束。- 没有的话,就通过
meta-class对象
的superclass指针
找到Student
的父类Person
的meta-class对象
,然后查找abc
方法,找到就调用,结束调用轨迹- 没有的话,就通过
Person
的meta-class对象
的superclass指针
,重复上一步的流程- 一次类推,通过
meta-class对象
的superclass指针
,一层层往上查找- 如果到了基类(
NSObject
)的meta-class
还没能够找到abc
,此时比较特殊,接下来的superclass指针
会找到NSObjec
t的class对象
,你可能会奇怪,我们调用一个类方法,怎么跑到class对象
里面来了,先保留你的疑问,只需记住,苹果确实是这么设计的,此时会继续在NSObjec
t的class对象
里面,寻找abc
,如果真的找到了abc
,就会调用
- 如果还没有找到,由于此时的superclass是nil,最终系统将给出报错
[ERROR: unrecognized selector sent to instance]
,调用轨迹结束
#import "NSObject+Test.h"
@implementation NSObject (Test)
+ (void)test
{
NSLog(@"+[NSObject test] ++++++ %p", self);
}
- (void)test
{
NSLog(@"-[NSObject test] -------- %p", self);
}
@end
@interface MJPerson : NSObject
+ (void)test;
@end
@implementation MJPerson
+ (void)test
{
NSLog(@"+[MJPerson test] ++++++++ %p", self);
}
@end
测试instance调用对象方法的轨迹及class调用类方法的轨迹
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"[MJPerson class] - %p", [MJPerson class]);
NSLog(@"[NSObject class] - %p", [NSObject class]);
[MJPerson test];
[NSObject test];
}
return 0;
}
输出
2021-04-08 11:29:59.749350+0800 Interview02-isa和superclass[2891:125121] [MJPerson class] - 0x100004260 2021-04-08 11:29:59.749897+0800 Interview02-isa和superclass[2891:125121] [NSObject class] - 0x7fff8faa3118 2021-04-08 11:29:59.749951+0800 Interview02-isa和superclass[2891:125121] +[MJPerson test] ++++++++ 0x100004260 2021-04-08 11:29:59.749974+0800 Interview02-isa和superclass[2891:125121] +[NSObject test] ++++++ 0x7fff8faa3118
当我们把NSobject的test方法去掉
@implementation NSObject (Test)
- (void)test
{
NSLog(@"-[NSObject test] -------- %p", self);
}
@end
此时调用轨迹右会发生神马变化?
isa指针指向哪里?
根据我们上面的梳理和总结,我们可以得出结论
isa(of instance) --> isa(of class) --> isa(of meta-class)
下面我们通过代码来验证一下
// MJPerson
@interface MJPerson : NSObject <NSCopying>
{
@public
int _age;
}
@property (nonatomic, assign) int no;
- (void)personInstanceMethod;
+ (void)personClassMethod;
@end
@implementation MJPerson
- (void)test
{
}
- (void)personInstanceMethod
{
}
+ (void)personClassMethod
{
}
- (id)copyWithZone:(NSZone *)zone
{
return nil;
}
@end
// MJStudent
@interface MJStudent : MJPerson <NSCoding>
{
@public
int _weight;
}
@property (nonatomic, assign) int height;
- (void)studentInstanceMethod;
+ (void)studentClassMethod;
@end
@implementation MJStudent
- (void)test
{
}
- (void)studentInstanceMethod
{
}
+ (void)studentClassMethod
{
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
return nil;
}
- (void)encodeWithCoder:(NSCoder *)aCoder
{
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
MJPerson *person = [[MJPerson alloc] init];
Class personClass = [MJPerson class];
Class personMetaClass = object_getClass(personClass);
NSLog(@"%p %p %p", person, personClass, personMetaClass);
}
}
输出
2021-04-08 11:52:08.970783+0800 Interview03-isa[3039:136373] 0x100415cf0 0x1000044c8 0x1000044a0
我们在代码中加入断点,通过控制台查看一下person
的isa
信息。
通过p/x
命令来打印指针,/
后面是打印参数,x
参数表示用16进制数输出。因为我们知道person
这个instance
的结构体的包含一个isa
成员变量,person
本身就是指针,所以可以通过person->isa
访问isa
的值。
代码里面,personClass
是Person类
的class对象
,输出结果显示,
person的isa = 0x001d8001000044c9
personClass = 0x00000001000044c8 它俩。。。并不相等!!! 这是什么情况?不是说好了
instance对象的
isa指向
class`对象嘛?
其实在64位机器出现之前,instance对象
的isa
确实是直接指向class对象
的,
也就是
person->isa == personClass
,
从64bit开始,isa
需要进行一次为运算,才能计算出真实的class对象
地址,系统给我们提供了一个ISA_MASK,这个可以在objc4源码里面找到。我先直接贴出来
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t shiftcls : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; \
uintptr_t deallocating : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 19
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x001f800000000001ULL
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t shiftcls : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; \
uintptr_t deallocating : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 8
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
# else
# error unknown architecture for packed isa
# endif
大家请看清这里是分了arm64和x86_64的,分别对应的是移动设备开发和mac开发。我的代码是一个mac命令行工程,所以我们用x86的这个值来试一下
可以看到,结果就显而易见了。通过和ISA_MASK进行一次&运算,我们得到了personClass的地址。同样,我们来试一下personClass的isa指针。
结果我试图通过personClass->isa
先打印出其isa
指针的时候,得到了错误提示,告诉我们说personClass
的类型Class
不是一个结构体,看不太明白,那就先查看一下Class
的定义,typedef struct objc_class *Class;
,然后在往下看一下objc_class
的细节
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
虽然这个结构体里面有isa指针,但是尾部的OBJC2_UNAVAILABLE;
提示我们,这已经是过时的API了。
不过我们知道class对象里面第一个成员变量确实是一个isa指针
struct mj_objc_class {
Class isa;
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
MJPerson *person = [[MJPerson alloc] init];
Class personClass = [MJPerson class];
struct mj_objc_class *personClass2 = (__bridge struct cl_objc_class *)(personClass);
Class personMetaClass = object_getClass(personClass);
NSLog(@"%p %p %p", person, personClass, personMetaClass);
}
return 0;
}
我们自定义一个struct,包含一个isa指针,然后再借助这个结构体类型来读取personClass里面的内容,如上代码,我们用personClass2在来尝试一次
ok,结果显示,class对象
的isa指针
直接指向了mete-class对象
的地址。
到此,上面的面试题相信大家已经可以完整回答了。在用一个图来总结一下就是
你会许还会问,那么superclass指针呢,是不是也需要一个什么mask转换?答案是不需要的,可以用上面相同的方法进行验证。总之isa指针稍微特殊一点点,特别记住一下关于ISA_MASK的细节就行。
struct mj_objc_class {
Class isa;
Class superclass;
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
// MJPerson类对象的地址:0x00000001000014c8
// isa & ISA_MASK:0x00000001000014c8
// MJPerson实例对象的isa:0x001d8001000014c9
struct mj_objc_class *personClass = (__bridge struct mj_objc_class *)([MJPerson class]);
struct mj_objc_class *studentClass = (__bridge struct mj_objc_class *)([MJStudent class]);
NSLog(@"1111");
}
}
特别备注
本系列文章总结自MJ老师在腾讯课堂开设的OC底层原理课程,相关图片素材均取自课程中的课件。