话不多说,直接开干
一、从源码的角度了解isa及isa底层代码实现
我们都知道Objective-C是一门面向对象的语言,所有的类都继承自NSObject
, 既然这样,我们就从NSObject
下手。
进入runtime
源码, 搜索OC的NSObject {
, 找到NSObject
的实现部分,
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0)
OBJC_ROOT_CLASS
OBJC_EXPORT
@interface NSObject {
Class isa OBJC_ISA_AVAILABILITY;
}
我们在NSObject
的定义部分找到了isa
,并且这个isa
是一个Class
类型的,
继续点击Class
typedef struct objc_class *Class;
发现Class
其实是一个结构体objc_class
的指针,打开这个结构体我们可以发现
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY; //isa指针
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;//父类
const char * _Nonnull name OBJC2_UNAVAILABLE;//类名
long version OBJC2_UNAVAILABLE;//类的版本信息,默认0
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;
点击这个结构体,发现objc_class
继承自objc_object
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
class_rw_t *data() {
return bits.data();
}
...
}
继续深入objc_object
,找到了isa_t
类型的isa
struct objc_object {
private:
isa_t isa;
public:
// ISA() assumes this is NOT a tagged pointer object
Class ISA();
// getIsa() allows this to be a tagged pointer object
Class getIsa();
...
};
继续点击ISA()
进入,我们来看一下isa
的初始化
inline Class
objc_object::ISA()
{
assert(!isTaggedPointer());
#if SUPPORT_INDEXED_ISA
if (isa.nonpointer) {
uintptr_t slot = isa.indexcls;
return classForIndex((unsigned)slot);
}
return (Class)isa.bits;
#else
return (Class)(isa.bits & ISA_MASK);
#endif
}
重点来了, 摘掉优化部分代码,isa
的初始化代码就变成下面这样子
inline Class
objc_object::ISA()
{
return (Class)(isa.bits & ISA_MASK);
}
注意return这段代码,isa
在初始化的时候会进行一个&
运算,为什么要进行这样的一个运算呢?
其实从64bit开始,isa需要进行一次位运算,才能计算出真实地址,这个位运算的对象就是ISA_MASK
ISA_MASK的值不是唯一的,在arm64和X86架构下是不同的,我把源码中ISA_MASK的部分代码单独剪了出来
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL # else
#
# endif
取到一个类的isa,然后和ISA_MASK进行一次位运算,就能够得到类对象的真实地址值。这个下面会做验证。
那么源码分析到此为止,从源码分析,我们知道了isa
其实就是一个指向类的结构体指针,也知道了isa
是一个经过优化的指针。既然isa是指向类的指针,因为Objective—C的类有很多种,有的类之间也存在继承关系,那么isa在类中间的指向到底如何呢?那么下面我们开始验证isa的指向。
二、控制变量法验证isa的指向
还是新建工程,创建一个继承自NSObject
的BMPerson
类,并给BMPerson
类添加三个属性,然后再创建一个BMStudent
类继承自BMPerson,然后去ViewDidLoad
中创建对象并给属性赋值,最后打印一下
BMPerson * person = [[BMPerson alloc] init];
person.name = @"张三";
person.age = 18;
person.height = 180;
Class class1 = person.class;
Class class2 = [BMPerson class];
Class class3 = object_getClass(person);
Class class4 = object_getClass(class1);
NSLog(@"\n实例对象地址:%p\n类地址:class1:%p\n类地址:class2:%p\n类地址:class3:%p\n元类地址:class4:%p", person,class1, class2, class3, class4);
BMStudent * student = [[BMStudent alloc] init];
student.name = @"张三的学生";
student.age = 18;
student.height = 180;
Class class5 = student.class;
Class class6 = object_getClass(class5);
NSLog(@"\nBMStudent\n实例对象地址:%p\n类地址:class1:%p\n元类地址:class4:%p", student,class5,class6);
得到结果
2020-06-19 16:55:42.996185+0800 TestProject[21084:1722006]
BMPerson
实例对象地址:0x600002bdd1a0
类地址:class1:0x1059b8798
类地址:class2:0x1059b8798
类地址:class3:0x1059b8798
元类地址:class4:0x1059b8770
2020-06-19 16:55:42.996416+0800 TestProject[21084:1722006]
BMStudent
实例对象地址:0x600002b943c0
类地址:class1:0x1059b86f8
元类地址:class4:0x1059b86d0
先来看上面这块log,分析BMPerson这块:
此时BMPerson实例对象的地址是0x600002bdd1a0
,class1、class2、class3指向的是同一个地址0x1059b8798
,这也说明了类的地址只有一个,也就是说:类对象只有一个,而class4则不同,指向了0x1059b8770
元类地址,为什么这么说呢,下面就是验证结果:
1、验证实例对象的isa指向的是类的地址
先拿到BMPerson实例的地址0x600002bdd1a0
,然后查看内存
(lldb) x 0x600002bdd1a0
0x600002bdd1a0: 98 87 9b 05 01 00 00 00 12 00 00 00 b4 00 00 00 ................
0x600002bdd1b0: 00 00 00 00 00 00 00 00 18 60 9b 05 01 00 00 00 .........`......
我们知道前八位是isa,小端模式从后往前读八位得到 0x1059B8798
,发现这个地址和上面打印的类的地址完全一致!如果感觉地址对比你不够清晰,可以直接po一下,发现这个isa就是指向了 BMPerson
类,所以实例对象的isa指向的就是类结论成立。
(lldb) x 0x600002bdd1a0
0x600002bdd1a0: 98 87 9b 05 01 00 00 00 12 00 00 00 b4 00 00 00 ................
0x600002bdd1b0: 00 00 00 00 00 00 00 00 18 60 9b 05 01 00 00 00 .........`......
(lldb) po 0x01059B8798
BMPerson
上面有说,isa是一个经过优化的指针,我们把打印的类的地址和isa_mask进行 &
操作,对比一下实际内存空间,发现是完全一致的
BMPerson的实例对象person的内存空间
(lldb) x 0x600002bdd1a0
0x600002bdd1a0: 98 87 9b 05 01 00 00 00 12 00 00 00 b4 00 00 00 ................
0x600002bdd1b0: 00 00 00 00 00 00 00 00 18 60 9b 05 01 00 00 00 .........`......
(lldb) p/x (long)person->isa & 0x00007ffffffffff8ULL
(unsigned long long) $16 = 0x00000001059b8798
这就验证了:
一个类对象的isa,然后和ISA_MASK进行一次位运算,就能够得这个类对象的真实地址值。
2、验证类对象的isa指向的是元类的地址
接下来我们读一下类地址的内存地址0x01059B8798
(lldb) x 0x01059B8798
0x1059b8798: 70 87 9b 05 01 00 00 00 00 ed c1 89 ff 7f 00 00 p...............
0x1059b87a8: 80 f9 5f 00 00 60 00 00 07 00 00 00 2c 80 02 00 .._..`......,...
把前八位也同样读取出来,得到0x01059B8770
这个和我们最开始打印得到的元类地址也是完全一致,所以类对象的isa指向的就是元类的结论也成立。
3、验证元类对象的isa指向的是根元类地址
接下来我们对BMPerson
的子类BMStudent
也做同样的调试,得到的也是同样的结果,student
实例的isa指向的就是BMStudent
类的地址,BMStudent
类的isa指向的就是BMStudent
元类的地址。
(lldb) po student
(lldb) x 0x600002b943c0
0x600002b943c0: f8 86 9b 05 01 00 00 00 12 00 00 00 b4 00 00 00 ................
0x600002b943d0: 00 00 00 00 00 00 00 00 58 60 9b 05 01 00 00 00 ........X`......
(lldb) po 0x01059B86F8
BMStudent
(lldb) x 0x01059B86F8
0x1059b86f8: d0 86 9b 05 01 00 00 00 98 87 9b 05 01 00 00 00 ................
0x1059b8708: 00 0c 5e 00 00 60 00 00 07 00 00 00 2c 80 04 00 ..^..`......,...
(lldb)
此时重点来了,我们分别得到了BMPerson
元类的地址和BMStudent
元类的地址,
我们看一下这两个类的元类的isa指向的是什么!
(lldb) x 0x1059b8770
0x1059b8770: d8 ec c1 89 ff 7f 00 00 d8 ec c1 89 ff 7f 00 00 ................
0x1059b8780: 80 6a 5f 00 00 60 00 00 07 00 00 00 35 c0 02 00 .j_..`......5...
(lldb) x 0x1059b86d0
0x1059b86d0: d8 ec c1 89 ff 7f 00 00 70 87 9b 05 01 00 00 00 ........p.......
0x1059b86e0: 80 6b 5f 00 00 60 00 00 07 00 00 00 35 c0 01 00 .k_..`......5...
我们发现了共同点!这两个元类的isa指向的是同一块内存空间,都是0x7FFF89C1ECD8
, 我们暂且称这个类为未知类,我们再看这个未知类的内存地址!
(lldb) x 0x7FFF89C1ECD8
0x7fff89c1ecd8: d8 ec c1 89 ff 7f 00 00 00 ed c1 89 ff 7f 00 00 ................
0x7fff89c1ece8: 00 60 7f 01 00 60 00 00 0f 00 00 00 31 c0 09 00 .`...`......1...
打印之后发现,这个未知类的isa竟然指向的是自己!其实这个未知类我们现在有另一种称呼,叫做根元类
。
再po一下这个地址
(lldb) po 0x7FFF89C1ECD8
NSObject
妈耶,这个根元类的类型竟然是NSObject
,重点来了!!!类型打印出来虽然是NSObject,但是这个地址代表的并不是NSObject类本身,而是NSObject元类的地址,我通过NSObject创建对象,然后获取这个对象的类,打印一下地址你就明白了
(lldb) po 0x7FFF89C1ECD8
NSObject
(lldb) po NSLog(@"%p", [[[NSObject alloc] init] class]);
2020-06-19 18:56:17.978190+0800 TextProject[21084:1722006] 0x7fff89c1ed00
上面是获取到的根元类的地址,下面才是NSObject类的地址,这俩地址不相同就能证明,根元类其实就是NSObject这个类的元类!
至此,isa的指向图已经很清楚了,如果我上面分析的这么多,其实思路就是按照这张图来分析的
三、知识点总结
1、**什么是isa
?
isa是一个一个经过特殊处理优化过的指针,指向对象的类。
实例对象的isa,指向它的类;
类对象的isa,指向这个类对象对应的元类;
元类对象的isa,指向根元类;
根元类的isa指向的是自己;
根元类是NSObject的元类
2、什么是元类
?
在Objective-C中,每当我们创建一个类,编译时就会自动创建一个元类(元类由系统进行维护),而我们创建的这个类就是这个元类的对象, 只不过这个元类只有一个实例,也就是说我们的 类对象只有一个!
3、为什么元类这么设计?
因为如果没有元类,则类对象的信息和实例对象的信息都会存储在类中,这样对于查找对
应的信息,会更加复杂且混乱(如类方法和实例方法一样的情况下,需要区分查找对
象的类型)
元类和类的类型也必须一致!如果类型不一样,那类对象的信息和实例对象的信息又怎么去找呢?
4、isa走向图
溪浣双鲤的技术摸爬滚打之路