isa走位及继承关系图
神图镇楼,相信做过iOS开发的同学一定非常熟悉这张经典图,每次看这张图都有不一样的体会,今天我们就借这张图,引出我们对类的探索,主要探索的是 类的结构分析以及isa的走向和 继承.
准备工作
objc781源码
创建两个类
ZGPerson继承NSObject,定义一些方法和属性
@interface ZGPerson : NSObject{
NSInteger age;
}
@property (nonatomic, copy) NSString *name;
- (void) say666;
+ (void) say888;
@end
- (void)say666
{}
+ (void)say888
{}
@end
ZGTeacher继承ZGPerson
@interface ZGTeacher : ZGPerson
@end
输出两个对象
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
ZGPerson *person = [ZGPerson alloc];
ZGTeacher *teacher = [ZGTeacher alloc];
NSLog(@"person %@\n teacher %@",person,teacher);
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
LLDB调试
利用LLDB我们打印内存,先介绍几个常用指令
x/4gx: 以16进制形式打印地址内容,读取4个16字节内容
p/x: 打印变量的16进制格式
po: 打印变量的description方法
调试结果如下图所示
LLDB调试内存信息
在我们调试的过程中发现p/x 0x001d8001000012d5 & 0x00007ffffffffff8ULL和p/x 0x00000001000012a8 & 0x00007ffffffffff8ULL中打印的信息都是ZGPerson,但是两者的含义却不相同。
0x001d8001000012d5是person对象isa指针所存储的类的信息0x00000001000012a8 是isa中获取的类信息所指的类的isa的指针地址,即ZGPerson类的类isa指针地址,我们称ZGPerson这样的类的类为元类
元类
什么是元类?,主要有以下几点说明:
- 对象的
isa是指向类,类的其实也是一个对象,可以称为类对象,其isa的位域指向苹果定义的元类元类是系统给的,其定义和创建都是由编译器完成,在这个过程中,类的归属来自于元类元类是类对象 的类,每个类都有一个独一无二的元类用来存储 类方法的相关信息。元类本身是没有名称的,由于与类相关联,所以使用了同类名一样的名称
isa指针指向
isa指针指向
由上图中LLDB调试信息我们得出isa的指向:
-
person实例对象的isa指向了ZGPerson类 -
ZGPerson类对象的isa指向了ZGPerson元类 -
ZGPerson元类对象的 isa 指向了NSObject类 -
NSObject类对象的isa指向了自己
类的继承关系如下图:
类的继承关系
类的继承关系链:ZGTeacher(子类) --> ZGPerson(父类) --> NSObject(根类)-->nil
元类的继承关系链:ZGTeacher(子元类) --> ZGPerson(父元类) --> NSObject(根元类)-->NSObject(根类)--> nil
为什么对象都有isa?
理清了isa走位和继承问题,又来了一个新的疑问:为什么 对象 和 类都有isa属性呢?这里就不得不提到两个结构体类型:objc_class& objc_object
我们在终端使用 clang命令:
clang -rewrite-objc main.m -o main.cpp
在生成的.cpp文件中我们注意到NSObject的底层编译是NSObject_IMPL结构体,其中Class是isa指针的类型,是由objc_class定义的类型,
而objc_class是一个结构体。在iOS中,所有的Class都是以objc_class为模板创建的
struct NSObject_IMPL {
Class isa;
};
typedef struct objc_class *Class;
我们在objc781的源码中搜索objc_class :发现在文件objc-runtime-new.h中
新版objc_class定义
可以发现结构体objc_class是继承objc_object结构体的(在c++中结构体是可以继承的),而我们搜索objc_object {发现位于 objc-privat.h的源码发现了isa的定义!由此就可以解释为什么OC中所有的对象都有一个isa的指针!
objc_object定义
总结:
- 结构体类型
objc_class继承自objc_object类型,其中objc_object也是一个结构体,且有一个isa属性,所以objc_class也拥有了isa属性- mian.cpp底层编译文件中,
NSObject中的isa在底层是由Class定义的,其中class的底层编码来自objc_class类型,所以NSObject也拥有了isa属性NSObject是一个类,用它初始化一个实例对象objc,objc满足objc_object的特性(即有isa属性),主要是因为isa是由NSObject从objc_class继承过来的,而objc_class继承自objc_object,objc_object有isa属性。所以对象都有一个isa,isa表示指向,来自于当前的objc_objectobjc_object(结构体) 是 当前的 根对象,所有的对象都有这样一个特性objc_object,即拥有isa属性
类的结构体分析
类的结构体分析我们主要分析类的结构体里面到底存储了哪些信息?
因为我们在分析类的结构体的时候需要用到内存偏移的知识,所以我们先补充一下内存偏移.
我们从普通指针,对象指针,数组指针逐个分析内存的偏移.
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
//普通指针
int a = 10; //变量
int b = 10;
NSLog(@"%d -- %p", a, &a);
NSLog(@"%d -- %p", b, &b);
//对象指针
ZGPerson *p1 = [ZGPerson alloc];
ZGPerson *p2 = [ZGPerson alloc];
NSLog(@"%@ -- %p", p1, &p1);
NSLog(@"%@ -- %p", p2, &p2);
//数组指针
int c[4] = {1, 2, 3, 4};
int *d = c;
NSLog(@"%p -- %p - %p", &c, &c[0], &c[1]);
NSLog(@"%p -- %p - %p", d, d+1, d+2);
// Setup code that might create autoreleased objects goes here.
}
return 0;
}
打印结果
打印结果
普通指针
普通指针打印结果
-
a、b都指向10,但是a、b的地址不一样,这是一种拷贝,属于值拷贝,也称为浅拷贝 -
a,b的地址之间相差4个字节,这取决于a、b的类型
普通指针内存情况如图:
普通指针
对象指针
对象指针打印结果
-
p1、p2是指针,p1是 指向[ZGPerson alloc]创建的空间地址,即内存地址,p2同理 -
&p1、&p2是 指向p1、p2对象指针的地址,这个指针就是二级指针
对象指针内存情况如图:
对象指针
数组指针
数组指针打印结果
-
&c和&c[0]都是取 首地址,即数组名等于首地址 -
&c与&c[1]相差4个字节,地址之间相差的字节数,主要取决于存储的数据类型 - 可以通过
首地址+偏移量取出数组中的其他元素,其中偏移量是数组的下标,内存中首地址实际移动的字节数 等于 偏移量 * 数据类型字节数
数组指针内存情况如图:
数组指针
类结构分析
接下来我们对objc781中objc-runtime-new.h文件中objc_class结构体的源码进行分析,
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
...省略部分代码
通过首地址+内存偏移我们可以打印objc_class结构体中存储的信息.
-
isa属性:继承自objc_object,占8个字节 -
superclass属性:Class类型指针,指针占8个字节 -
cache:cache_t是结构体类型,结构体的内存大小有结构体本身的属性决定,详情可参考本人的另一篇文章结构体与内存对齐 -
bits:我们得到上面3个属性的大小之后,通过首地址偏移就可以得到bits的内容
现在不能确定大小的就是结构体cache的内存大小,我们进入cache的源码中看一下
struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
explicit_atomic<struct bucket_t *> _buckets;
explicit_atomic<mask_t> _mask;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
explicit_atomic<uintptr_t> _maskAndBuckets;
mask_t _mask_unused;
...一些static属性
#if __LP64__
uint16_t _flags;
#endif
uint16_t _occupied;
...一些static属性
由于结构体中static的属性不占内存大小,所以我们只要关注上面的几个属性的内存大小
-
_flags属性:uint16_t类型,uint16_t是unsigned short的别称,占2个字节 -
_occupied属性:uint16_t类型,uint16_t是unsigned short的别称,占2个字节 - 下面我们看
if-else条件中各属性的大小
①if流程:
_buckets属性:struct bucket_t *类型的结构体指针类型,占8个字节
_mask属性:mask_t类型,mask_t是unsigned int的别名,占4字节
②elif流程:
_maskAndBuckets属性:uintptr_t类型,是一个指针,指针占8个字节
_mask_unused属性:mask_t类型,mask_t是unsigned int的别名,占4字节
总结:cache属性的内存大小:8+4+2+2 = 16字节,所以我们想获取bits属性中的内容我们需要偏移8+8+16字节。
bits属性内容
通过LLDB调试bits内容如下图所示
LLDB调试bits
其中data()获取数据是objc_class提供的方法
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() const {
return bits.data();
}
...省略部分代码
通过LLDB获取方法列表
LLDB获取方法列表
-
class_rw_t中的methods()函数,可以获取到方法列表.
*list函数获取列表的首地址,打印地址的值后,用get查看到类中的所有方法。通过下图能看到类中定义的say666方法、属性name的setter和getter方法,还有一个系统生成的.cxx_destruct方法: -
类中还定义了一个类方法
+ (void)say888,在这里是看不到的。因为类方法存在元类中。
通过LLDB获取属性列表
LLDB获取属性列表
-
class_rw_t中的properties()函数,可以获取到属性列表.
*list函数获取列表的首地址,打印地址的值后,用get查看到类中的所有属性。不过却看不到成员属性,调用p $6.get(2)会报错数组越界
成员属性
那么对象的成员属性到底存放在哪里呢?经过探索我们终于发现~
LLDB获取成员属性
- 原来
成员变量存在ro中,通过函数ro()获取,最终我们在ro的ivars属性中发现了我们的成员变量age
至此,大功告成!
总结一下我们这篇文章的内容
isa指针指向及继承关系- 探索为什么对象都有
isa?- 类的结构体分析
- 补充知识-
内存偏移














网友评论