【iOS】OC底层系列二 - 对象的分类

面试题:对象的isa指针指向哪里?OC的类信息存放在哪里?

一、OC对象的分类

Objective-C中的对象,简称OC对象,主要可以分为3种:

  • instance对象(实例对象)
  • class对象(类对象)
  • meta-class对象(元类对象)

1.1. instance对象

instance对象就是通过类alloc出来的对象,每次调用alloc都会产生新的instance对象。

1
2
3
4
5
NSObject *object1 = [[NSObject alloc] init];
NSObject *object2 = [[NSObject alloc] init];

// 输出:0x100527a20 - 0x100527730
NSLog(@"%p - %p", object1, object2);

object1object2是NSObject的instance对象(实例对象)。

它们是不同的两个对象,分别占据着两块不同的内存。

instance对象在内存中存储的信息包括:

  • isa指针
  • 其他成员变量

1.2. class对象

1
2
3
4
5
6
7
8
Class objectClass1 = [object1 class];
Class objectClass2 = [object2 class];
Class objectClass3 = [NSObject class];
Class objectClass4 = object_getClass(object1);
Class objectClass5 = object_getClass(object2);

// 输出:0x7fff88a8ae08 - 0x7fff88a8ae08 - 0x7fff88a8ae08 - 0x7fff88a8ae08 - 0x7fff88a8ae08
NSLog(@"%p - %p - %p - %p - %p", objectClass1, objectClass2, objectClass3, objectClass4, objectClass5);

objectClass1 ~ objectClass5都是NSObject的class对象(类对象)。

它们是同一个对象,每个类在内存中有且只有一个class对象

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

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

1.3. meta-class对象

1
2
3
4
5
// 将类对象当做参数传入,获得元类对象
Class objectMetaClass = object_getClass([NSObject class]);

// 输出:0x7fff88a8ade0
NSLog(@"%p", objectMetaClass);

objectMetaClass是NSObject的meta-class对象(元类对象)。

每个类在内存中有且只有一个meta-class对象。

meta-class对象和class对象的内存结构是一样的(都是Class类型),但是用途不一样,在内存中存储的信息主要包括:

  • isa指针
  • superclass指针
  • 类的类方法信息(class method)
  • ……

注意:以下代码获取的objectClass是class对象(class方法返回的一直是class对象),并不是meta-class对象。

1
Class objectClass = [[NSObject class] class];

查看Class是否为meta-class:

1
2
3
4
5
6
7
8
BOOL result = class_isMetaClass([NSObject class]);
// 输出:0
NSLog(@"%d", result);

Class objectMetaClass = object_getClass([NSObject class]);
BOOL result2 = class_isMetaClass(objectMetaClass);
// 输出:1
NSLog(@"%d", result2);

1.4. object_getClass和objc_getClass

object_getClassobjc_getClass有什么区别呢?我们可以看一下源码(objc-class.mm)。

由于object_getClass内部调用了getIsa方法,因此这个方法会返回指向父类的信息。

  • 如果是instance对象,返回class对象。
  • 如果是class对象,返回meta-class对象。
  • 如果是meta-class对象,返回NSObject(基类)的meta-class对象。
1
2
3
4
5
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}

通过objc_getClass方法传入一个类名字符串,最终返回对应的类对象。如果类不存在,就返回nil

由于看不到- (Class)class+ (Class)class方法的具体实现,但是从两个方法的返回结果可以看出(也可以把代码转为C++),内部调用的就是objc_getClass方法。

1
2
3
4
5
6
7
Class objc_getClass(const char *aClassName)
{
if (!aClassName) return Nil;

// NO unconnected, YES class handler
return look_up_class(aClassName, NO, YES);
}

二、isa和superclass

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@interface Person : NSObject<NSCopying>
{
@public // 作用:在非Person类中,Person对象可以访问成员变量
int _age;
}
@property (nonatomic, assign) int height;
+ (void)personClassMethod;
- (void)personInstanceMethod;
@end

@implementation Person
+ (void)personClassMethod {}
- (void)personInstanceMethod {}
- (id)copyWithZone:(NSZone *)zone {
return nil;
}
@end

@interface Student : Person<NSCoding>
{
int _no;
}
@property (nonatomic, assign) int grade;
+ (void)studentClassMethod;
- (void)studentInstanceMethod;
@end

@implementation Student
+ (void)studentClassMethod {}
- (void)studentInstanceMethod {}
- (instancetype)initWithCoder:(NSCoder *)coder {
return nil;
}
- (void)encodeWithCoder:(NSCoder *)coder {}
@end

2.1. isa指针

1
2
3
4
5
6
int main(int argc, const char * argv[]) {
Student *stu = [[Student alloc] init];
[stu studentInstanceMethod];
[Student studentClassMethod];
return 0;
}

把上面的代码转为C++后可以看到,OC调用方法(实例方法/类方法)的本质就是消息转发

1
2
3
4
5
6
int main(int argc, const char * argv[]) {
Student *stu = ((Student *(*)(id, SEL))(void *)objc_msgSend)((id)((Student *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Student"), sel_registerName("alloc")), sel_registerName("init"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)stu, sel_registerName("studentInstanceMethod"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Student"), sel_registerName("studentClassMethod"));
return 0;
}

当一个实例对象调用对象方法或类方法时,是如何让实例对象与类对象和元类对象之间产生关系呢?通过isa。

Student的实例对象调用实例方法时,会向实例对象发送消息,然后通过实例对象的isa找到class,最终调用class中的实例方法。

Student类调用类方法时,会向类对象发送消息,然后通过类对象的isa找到meta-class,最终调用meta-class中的类方法。

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

2.2. class对象的superclass指针

1
2
3
4
5
6
int main(int argc, const char * argv[]) {
Student *stu = [[Student alloc] init];
// 调用父类的实例方法
[stu personInstanceMethod];
return 0;
}

把上面的代码转为C++:

1
2
3
4
5
int main(int argc, const char * argv[]) {
Student *stu = ((Student *(*)(id, SEL))(void *)objc_msgSend)((id)((Student *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Student"), sel_registerName("alloc")), sel_registerName("init"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)stu, sel_registerName("personInstanceMethod"));
return 0;
}

很神奇的发现,调用父类的实例方法和调用自己的实例方法,两者代码几乎是一样的,都是向Student的实例对象发送消息。Student的实例对象是怎么找到父类中的方法呢?通过superclass。

当Student的实例对象要调用Person的对象方法时,会先通过isa找到Student的class,然后通过superclass找到Person的class,最后找到对象方法的实现进行调用。

2.3. meta-class对象的superclass指针

1
2
3
4
5
int main(int argc, const char * argv[]) {
// 调用父类的类方法
[Student personClassMethod];
return 0;
}

把上面的代码转为C++:

1
2
3
4
int main(int argc, const char * argv[]) {
((void (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Student"), sel_registerName("personClassMethod"));
return 0;
}

原理和上面的2.2差不多,这里就不在赘述,但需要再次强调一点:类方法存储在元类对象的内存中

当Student的class要调用Person的类方法时,会先通过isa找到Student的meta-class,然后通过superclass找到Person的meta-class,最后找到类方法的实现进行调用。

2.4. isa和superclass之间的关系

下图中的Subclass(子类)、Superclass(父类)、Rootclass(基类)很好的诠释了Instance、class、meta这三者之间的关系。

  • instance的isa指向class。
  • class的isa指向meta-class。
  • meta-class的isa指向基类的meta-class(基类的meta-class的isa指向自己的meta-class)。
  • class的superclass指向父类的class(如果没有父类,superclass指针为nil)。
  • meta-class的superclass指向父类的meta-class(基类的meta-class的superclass指向基类的class)。
  • instance调用对象方法的轨迹:instance的isa找到自己的class,在class中找方法,如果方法不存在,就通过superclass找父类的class中的方法,直到基类的class,如果最终没找到并且没有做处理,程序就会崩溃 unrecognized selector sent to instance 0x1005453a0)。
  • class调用类方法的轨迹:instance的isa找自己的class,通过class的isa找到meta-class,在meta-class中找方法,如果方法不存在,就通过superclass找父类的meta-class中的方法,直到基类的meta-class,如果基类的meta-class中也没有找到对应的方法,就会到基类的class中找,如果最终没找到并且没有做处理,程序就会崩溃unrecognized selector sent to class 0x100008478

2.5. 流程分析

接下来通过一个小案例,加强对2.4的理解。

新建一个Person类,并添加一个类方法+ (void)test。创建NSObject的分类,并新增类方法+ (void)test

Person:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@interface Person : NSObject

+ (void)test;

@end

@implementation Person

+ (void)test {
NSLog(@"%s-%p", __func__, self);
}

@end

NSObject+Test:

1
2
3
4
5
6
7
8
9
10
11
12
13
@interface NSObject (Test)

+ (void)test;

@end

@implementation NSObject (Test)

+ (void)test {
NSLog(@"%s-%p", __func__, self);
}

@end

main.m:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 
int main(int argc, const char * argv[]) {
NSLog(@"[Person class] - %p", [Person class]);
NSLog(@"[NSObject class] - %p", [NSObject class]);

[Person test];
[NSObject test];
return 0;
}
/*
输出:
[Person class] - 0x100008200
[NSObject class] - 0x7fff88a8ae08

+[Person test]-0x100008200
+[NSObject(Test) test]-0x7fff88a8ae08
*/

如果不实现Person类的+ (void)test方法,最终Person调用test方法时会找父类元类对象,看其是否存在该方法。

[Person test]本质是向Person类对象发消息,所以在NSObject分类中打印的self是Person类对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@implementation Person

/*
+ (void)test {
NSLog(@"%s-%p", __func__, self);
}
*/

@end

/*
输出:
[Person class] - 0x1000081e0
[NSObject class] - 0x7fff88a8ae08

+[NSObject(Test) test]-0x1000081e0
+[NSObject(Test) test]-0x7fff88a8ae08
*/

在上面的基础上,如果把NSObject分类中的实现方法修改为对象方法,会出现什么情况呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@implementation NSObject (Test)

- (void)test {
NSLog(@"%s-%p", __func__, self);
}

@end

/*
输出:
[Person class] - 0x1000081e0
[NSObject class] - 0x7fff88a8ae08
-[NSObject(Test) test]-0x1000081e0
-[NSObject(Test) test]-0x7fff88a8ae08
*/

发现最终调用的是对象方法。代码中调用的是类方法,为什么会调用对象方法呢?

其实上面讲解的2.3和2.4已经给出答案了。调用类方法会从元类对象中查找方法,如果不存在会沿着元类的superclass依次往上查找,最终基类的元类对象也没有找到方法时,会找基类中的方法。

疑问:为什么最终基类的meta-class中类方法找不到,会到基类的class中找(class中只有对象方法,类方法也只会在meta-class中存储)?

这是因为OC调用方法的本质是对象发送消息,[Person test]的本质就是objc_msgSend([Person class], @selector(test));。重点来了,objc_msgSend的第一个参数传入的是对象,第二个参数是让传入一个方法名,但是并没有说明是类方法(+)还是对象方法(-)。因此,当基类中的元类对象中没有找到类方法时,会通过基类的元类对象superclass找到基类对象,然后在基类中查找对象方法,如果还是没有找到就会报错。

通过上面的示例代码可以看出,无论是实例对象还是类对象,调用方法的流程基本都是一样的。先通过isa找自己类/元类中的方法,然后通过superclass找,最终到达基类的superclass(指向nil)。

三、Class的结构

通过上面的了解,我们已经知道每一个对象都有一个Class类型的isa指针,也就是说实例对象的isa保存的是类对象的地址,类对象的isa保存的是元类对象的地址。我们通过打印相关参数验证一下。

1
2
3
4
5
Person *person = [[Person alloc] init];

Class personClass = [Person class];

Class personMetaClass = object_getClass(personClass);

由于无法直接获取到isa信息,我们可以通过lldb指令在控制台打印相关信息。

为什么类的实例对象isa指向的地址和类对象地址不一样?这是因为从64bit开始,isa需要进行一次位运算,才能计算出真实地址。

ISA_MASK的值在objc源码可以看到,分为arm64(真机)和x86_64(模拟器),下面的代码是精简后的:

1
2
3
4
5
# if __arm64__
# define ISA_MASK 0x007ffffffffffff8ULL
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# endif

通过位运算就可以得到真正的isa地址,最终结果和personClass地址一致(注意:由于我运行的是Mac程序,所以使用的是x86对应的值)。

默认情况下,类对象的isa和元类对象的isa是无法直接获取到的(即使通过控制台访问也不行),我们可以模仿objc_class的实现。

Class在API中的相关定义(class、meta-class对象的本质结构都是struct objc_class):

1
2
3
4
5
6
typedef struct objc_class *Class;

struct objc_class {
Class isa;
// ...
};

模仿objc_class:

1
2
3
4
5
6
7
8
struct db_objc_class {
Class isa;
};

Class personClass = [Person class];
struct db_objc_class *personClass2 = (__bridge struct db_objc_class *)personClass;

Class personMetaClass = object_getClass(personClass);

只有isa会进行位运算,superclass保存的还是原值。


面试题1:对象的isa指针指向哪里?

解答:instance对象的isa指向class对象,class对象的isa指向meta-class对象,meta-class对象的isa指向基类的meta-class对象。

面试题2:OC的类信息存放在哪里?

解答:对象方法、属性、成员变量、协议信息,存放在class对象中。类方法,存放在meta-class对象中。成员变量的具体值,存放在instance对象。