Object-C-runtime
Object-C ,runtime原理,oc对象的原理,主要记录oc对象在底层的实现原理;
项目代码地址GitHub
https://github.com/xuyushiguang/Object-C-runtime
//
// Person.m
// Interview001-OC对象的本质
//
// Created by xingye yang on 2021/9/18.
// Copyright © 2021 xingye. All rights reserved.
//
#import "Person.h"
#import <objc/runtime.h>
#import <malloc/malloc.h>
/*
用来计算结构体的属性在内存中的偏移量
*/
#define __OFFSETOFIVAR__(TYPE, MEMBER) ((long long) &((TYPE *)0)->MEMBER)
@interface Person()
{
@public
//定义一个私有属性,这个属性系统不会生成set/get方法
int _age;
}
//定义一个property类型的属性,这个属性系统会帮我们生成set/get方法
@property(nonatomic,assign)int height;
-(void)run;
+(void)home;
@end
@implementation Person
-(void)run{
}
+(void)home{
}
@end
#pragma mark ########### 以下是把.m文件转成.cpp后的样子,在这里我把关键的部分取出来,写在这里;#####
#pragma mark################################### Person start ##################################################
#pragma mark##################### 以下是把.m文件转成.cpp后的样子,在这里我把关键的部分取出来,写在这里;#####################
#pragma mark#################################### Person start ################################################
/*
Object-C中所有继承NSObject的类 底层就是一个结构体,所以我们把OC类转成.cpp文件后,看到的就是一个结构体
*/
/*
这个结构体是NSObject类底层的样子,说明NSObject类只有一个isa属性,isa是一个指针类型占8个字节,说明NSObject的一个对象实际内存是8字节;
但是系统分配内存的时候却分配了16个字节,在下面这个函数中我们可以看到原因,当内存字节小于16字节时返回16字节的长度;
size_t instanceSize(size_t extraBytes) {
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
return size;
}
综上所述:一个NSObject对象的占内存长度是16个字节
*/
struct NSObject_IMPL {
Class isa;
};
/*
这个就是Person类的底层结构体;我们可以看到结构体中有两个属性:
1)NSObject_IVARS属性:是NSObject_IMPL类型的属性,也可以理解为isa指针;因为NSObject_IMPL只有一个isa属性
2)_age 就是我们自定义的属性
*/
struct Person_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _age;
int _height;
};
/*
实例方法
*/
static void _I_Person_run(Person * self, SEL _cmd) {
}
/*
类方法
*/
static void _C_Person_home(Class self, SEL _cmd) {
}
/*
我们发现无论是对象方法还是类方法都没有在结构体内部,c++的结构体是可以定义方法的,但是为什么不在结构体内部定义;
这里是把方法实现单独定义成了函数,这些函数会单独存放在一个内存区域,这个函数的内存区域和结构体对象的内存区域是分开的;
也就是说结构只存储属性值,比如我们初始化一个结构体,系统就会给这个结构体分配内存,内存占用的多少是根据结构体的属性的多少来分配的;
如果结构体内部有方法的话,也会给方法分配内存,这样就会消耗大量内存;
如果把结构体的方法单独存放在一个内存区域,当我们取调用结构体对象的方法的时候,去这个内存区域找到对应的函数方法,并把结构体对象也传给这个方法,结构体对象相当于上下文功能,
这样就不用给每个结构体实例的方法都分配内存,结构体所有对象共用一个方法内存,结构体对象只需要保存好属性的值的内存就可以了;
下面会讲方法存储在什么地方
*/
/*
这个就是@property(nonatomic,assign)int height;由系统生成的set/get方法
*/
static int _I_Person_height(Person * self, SEL _cmd) { return (*(int *)((char *)self + OBJC_IVAR_$_Person$_height)); }
static void _I_Person_setHeight_(Person * self, SEL _cmd, int height) { (*(int *)((char *)self + OBJC_IVAR_$_Person$_height)) = height; }
#pragma mark#################################### Person end ########################################################
#pragma mark#############################到这就是Person类被转成底层时的样子##################################
#pragma mark#################################### Person end ########################################################
/*
下面就是对Person的属性的配置
*/
/*
这个定义是用来记录Person_IMPL结构体中的_age属性的内存偏移量;
因为当我们给Person对象的_age属性设置值或者获取_age的值,都需要知道_age属性在内存中的位置,
然后找到_age内存,把值写入到_age内存中,或者从_age内存中读取值
*/
extern "C" unsigned long OBJC_IVAR_$_Person$_age;
extern "C" unsigned long OBJC_IVAR_$_Person$_height;
/*
用来计算OBJC_IVAR_$_Person$_age的值
*/
extern "C" unsigned long int OBJC_IVAR_$_Person$_age __attribute__ ((used, section ("__DATA,__objc_ivar"))) = __OFFSETOFIVAR__(struct Person_IMPL, _age);
extern "C" unsigned long int OBJC_IVAR_$_Person$_height __attribute__ ((used, section ("__DATA,__objc_ivar"))) = __OFFSETOFIVAR__(struct Person_IMPL, _height);
int main(int argc, const char * argv[]) {
/*
这里是初始化一个Person对象;
Person *p = [[Person alloc] init];
*/
Person *p = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init"));
/*
p->_age = 20;
这里是给Person对象的_age属性赋值,我们来分解这个式子到底是什么意思;
0)其中p是Person类的一个对象,实际就是一个指针;是指针就好办了,我们可以转换成其他任意类型的指针;注意在ARC中编译器不让转化,可以在MRC中转换,把xcode修改成MRC;
1)(char *)p 的意思是把p转换成char *类型的指针;
2)((char *)p + OBJC_IVAR_$_Person$_age) 这个就是指针的移动,由于先前已经转换长char*类型的,所以指针移动的步长是一个字节,
总共移动OBJC_IVAR_$_Person$_age长度的字节,OBJC_IVAR_$_Person$_age就是_age属性在Person_IMPL结构体中的内存偏移量;
OBJC_IVAR_$_Person$_age的长度是这样计算的:__OFFSETOFIVAR__(struct Person_IMPL, _age)
3)(int *)((char *)p + OBJC_IVAR_$_Person$_age)这一步是指针移动完成后,把指针转成int *类型的;这是因为_age属性是int类型的;
转成int *类型是为了方便赋值或者取值;
4)*(int *)((char *)p + OBJC_IVAR_$_Person$_age)这一步我们看到在3)的基础上最左边添加了一个*号,由于式子是在等号=左边,
所以加*号是赋值的意思,
*/
(*(int *)((char *)p + OBJC_IVAR_$_Person$_age)) = 20;
/*
int a = p->_age;
这里是取值,取Person对象的_age属性值;这里就不做过多介绍了
*/
int a = (*(int *)((char *)p + OBJC_IVAR_$_Person$_age));
/*
下面是调用@proper描述的属性,我们可以发现和调用_age属性不同;
这里调用了set/get方法;而不是直接给属性赋值或者直接取属性的值;
其实是在set/get方法内部实现了取值和赋值操作;
不同点就是一个直接操作内存,另一个通过消息发送的方式调用set/get方法取操作内存;
效率的话,肯定是直接操作内存快;
*/
/*
p.height = 100;
这是height的set方法,其方法内部实现和_age属性一样
_I_Person_setHeight_(Person * self, SEL _cmd, int height) { (*(int *)((char *)self + OBJC_IVAR_$_Person$_height)) = height; }
*/
((void (*)(id, SEL, int))(void *)objc_msgSend)((id)p, sel_registerName("setHeight:"), 100);
/*
int h = p.height;
这是height属性的get方法,我们可以看到方法内部的实现和_age属性一样
_I_Person_height(Person * self, SEL _cmd) { return (*(int *)((char *)self + OBJC_IVAR_$_Person$_height)); }
*/
int h = ((int (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("height"));
/*
调用对象方法
[p run];
*/
((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("run"));
/*
调用类方法
[Person home];
*/
((void (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("home"));
/*
调用对象方法和类方法不同点在于:
调用对象方法是直接传p对象;
调用类方法是获取类对象objc_getClass("Person")
*/
return 0;
}
#pragma mark ########### 以下是 Person的属性和方法是怎么保存的 #####
#pragma mark #######################################################
/*
从转换后的.cpp文件中可以看到;Person类的变量,属性,方法,isa,父类的isa,以及缓存都可以从这里找到;
_class_ro_t结构体非常重要,里面保存的都是Person类的所有的数据结构;
也就是说我们这样Person *p = [[Person alloc] init];创建的p对象;p的isa指针指向的就是_class_t结构体对象;
也即是Person *p = [[Person alloc] init];
可以把p->isa转成:
struct _class_t *c = (struct _class_t *)(p->isa);
对于Person类来说,全局只有一个Person类对象,记住是类对象,不是Person对象,类对象全局只有一个,保存的是这个类的所有数据结构;
这个类对象就是_class_t对象,我们创建p对象的时候,会把类对象(也就是_class_t对象)的指针地址保存一份到p对象的isa上;
也就是我们每创建一个p对象,p对象都会在isa上保存一份类对象的指针地址
这样有什么用呢?非常重要,比如:
我们想要获取_age的值,平常我们是直接p._age就可以获取到,但是系统在底层帮我们做了好多事;
1)首先我们要知道Person类有没有这个属性?
2)我们还要知道_age属性在内存中的位置吧,不然怎么获取值呢?
3)找到这个属性后才能对这个属性操作;
所以Person的所有数据结构都保存在_class_t这个结构体中;
下面一步步讲解
*/
struct _class_t {
struct _class_t *isa;
struct _class_t *superclass;
void *cache;
void *vtable;
struct _class_ro_t *ro;
};
/*
这个结构体体保存的都是Person类的具体内容了;一个一个看:
flags:不清楚,不用关心
instanceStart:这个就是Person对象在内存地址中开始的位置,如果我们要获取某个对象,就需要先拿到这个属性,通过指针移动,获取到这个对象内存的起始位置
instanceSize :这个就是对象的内存长度,也就是这个对象在内存中占用的字节数
reserved:不清楚
ivarLayout:不清楚
name:类名
baseMethods:保存方法的地方,类的方法都保存在这里
baseProtocols:保存协议的地方
ivars:保存对象变量的地方,就是在{int _age;}定义的变量;
weakIvarLayout:不清楚
properties:保存属性的地方,就是@proper()int height;定义的属性;
*/
struct _class_ro_t {
unsigned int flags;
unsigned int instanceStart;
unsigned int instanceSize;
unsigned int reserved;
const unsigned char *ivarLayout;
const char *name;
const struct _method_list_t *baseMethods;
const struct _objc_protocol_list *baseProtocols;
const struct _ivar_list_t *ivars;
const unsigned char *weakIvarLayout;
const struct _prop_list_t *properties;
};
/*
先看变量列表;在.cpp文件中可以找到这个;这个就是保存变量的列表
entsize:是_ivar_t结构体的内存长度
count:保存变量的数量,
ivar_list:保存真的变量的具体内容,看下面的_ivar_t介绍
*/
static struct _ivar_list_t {
unsigned int entsize; // sizeof(struct _prop_t)
unsigned int count;
struct _ivar_t ivar_list[2];
}
/*
这个是描述变量的,也就是这个变量的名字,类型,占用的内存字节数,内存偏移量
offset:保存这个变量在结构体内存中的内存偏移量;方便找到这个变量的位置;类似_class_ro_t结构体中的instanceStart属性
name:变量名字
type:变量的类型,比如int,float,String等,是简写的类型;
alignment:字节对齐
size:这个变量占的内存长度;类似_class_ro_t结构体中的instanceSize属性
*/
struct _ivar_t {
unsigned long int *offset; // pointer to ivar offset location
const char *name;
const char *type;
unsigned int alignment;
unsigned int size;
};
/*
这个是方法列表;类的所有方法都保存在这个地方;
entsize:保存的是_objc_method结构体的占内存的大小;
method_count:保存的是方法数量
method_list:所有的方法都保存在这个数组中;
一个方法的具体内容保存在_objc_method结构体中;
继续往下看
*/
static struct _method_list_t {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[1];
}
/*
这个是描述一个方法;也就是这个方法具有哪些内容;
_cmd:方法的名字
method_type:方法的类型,像这样"v16@0:8",描述这个方法的返回值类型,参数类型等
_imp:函数指针,可以理解为方法的实现的地址,通过这个指针就能拿到这个方法;从而调用这个方法;
runtime里面的黑魔法,方法交换;其实就是修改_imp指针,让它指针另一个函数的地址;
*/
struct _objc_method {
struct objc_selector * _cmd;
const char *method_type;
void *_imp;
};
/*
_objc_protocol_list 协议列表
_prop_list_t 属性列表
有时间在讲解
*/
#pragma mark ######################### 类对象 到底是怎么产生的? ##############################
#pragma mark ######################### 类对象 属性和方法是怎么存储的? ##############################
/*
这里是类对象实现的地方
在.cpp文件我们看到,这里初始化了一个_class_t类型的变量 OBJC_CLASS_$_Person
OBJC_CLASS_$_Person就是 Person 类对象,全局只有这一个;
我们发现初始化的时候:
struct _class_t *isa; 这个是0,也就是说类对象初始化的时候,他的isa指针式空的,别担心,后面会对他从新赋值
struct _class_t *superclass; 这个也是0,也就是说类对象初始化的时候,他的指向父类的superclass指针式空的,别担心,后面会对他从新赋值
void *cache; 缓存也是空的
void *vtable; 也是空的
struct _class_ro_t *ro; 这个有值_OBJC_CLASS_RO_$_Person,这个值保存的就是Person类的数据结构,保存的属性,方法,协议等
我们全局找到_OBJC_CLASS_RO_$_Person这个在那实现的;请继续看下面
*/
extern "C" __declspec(dllexport) struct _class_t OBJC_CLASS_$_Person __attribute__ ((used, section ("__DATA,__objc_data"))) = {
0, // &OBJC_METACLASS_$_Person,
0, // &OBJC_CLASS_$_NSObject,
0, // (void *)&_objc_empty_cache,
0, // unused, was (void *)&_objc_empty_vtable,
&_OBJC_CLASS_RO_$_Person,
};
/*
我们发现_OBJC_CLASS_RO_$_Person是_class_ro_t类型的结构体对象;
可以看到
unsigned int flags; 值是0
unsigned int instanceStart;可以看到内存初始位置就是竟然是Person类的第一个自定义属性_age的指针位置;竟然不是isa的指针位置,那是因为isa保存的是类对象的位置,
如果从isa开始取值,就取不到_age,和其他属性的值了;
unsigned int instanceSize;可以看到内存长度是Person_IMPL结构体的长度
unsigned int reserved; 值是0
const unsigned char *ivarLayout;也是0
const char *name; 类名"Person"
const struct _method_list_t *baseMethods; 这个有值,_OBJC_$_INSTANCE_METHODS_Person,这个保存的是方法列表
const struct _objc_protocol_list *baseProtocols;也是0,因为我们没有定义协议,所以没有
const struct _ivar_list_t *ivars;保存的是变量列表
const unsigned char *weakIvarLayout;应该是存的弱引用
const struct _prop_list_t *properties;属性列表
下面主要看方法列表_method_list_t和_ivar_list_t变量列表
*/
static struct _class_ro_t _OBJC_CLASS_RO_$_Person __attribute__ ((used, section ("__DATA,__objc_const"))) = {
0,
__OFFSETOFIVAR__(struct Person, _age),
sizeof(struct Person_IMPL),
(unsigned int)0,
0,
"Person",
(const struct _method_list_t *)&_OBJC_$_INSTANCE_METHODS_Person,
0,
(const struct _ivar_list_t *)&_OBJC_$_INSTANCE_VARIABLES_Person,
0,
(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_Person,
};
/*
这里是方法列表。初始化方法列表
我们可以看到run,这个是我们自己定义的方法
还有height和setHeight,这个是@property()定义的属性,系统帮我们实现了set/get方法
下面我们看看变量列表是怎么实现的
_I_Person_run
_I_Person_height
_I_Person_setHeight_
这三个就是函数指针,保存的是函数实现的位置
当我的Person对象调用run方法的时候,其实就是根据“run”字符串去找到_I_Person_run这个函数指针;调用这个函数;
我们发现_I_Person_run函数的第一个参数就是Person类型的,
*/
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[5];
} _OBJC_$_INSTANCE_METHODS_Person __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
5,
{{(struct objc_selector *)"run", "v16@0:8", (void *)_I_Person_run},
{(struct objc_selector *)"height", "i16@0:8", (void *)_I_Person_height},
{(struct objc_selector *)"setHeight:", "v20@0:8i16", (void *)_I_Person_setHeight_},
}
};
/*
这个就是变量列表,初始化变量列表
在.cpp文件中我们可以找到这个,这个就是把Person类的所有属性保存在 _OBJC_$_INSTANCE_VARIABLES_Person 这个变量里;
_INSTANCE_VARIABLES_Person 是_ivar_list_t类型的,上面有介绍;
这个式子的意思是定义一个_ivar_list_t类型的结构体,并且创建一个结构体对象_OBJC_$_INSTANCE_VARIABLES_Person 并给这个对象初始化;初始化的时候把Persion类的属性都放进去了;
*/
static struct _ivar_list_t {
unsigned int entsize; // sizeof(struct _prop_t)
unsigned int count;
struct _ivar_t ivar_list[2];
} _OBJC_$_INSTANCE_VARIABLES_Person __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_ivar_t),
2,
{
/*在这里说一下:
OBJC_IVAR_$_Person$_age :这个在上面有定义过,就是_age属性在结构体内存中的偏移量;
"_age":是属性的名字
"i":是属性的类型;
2:未知,可能是属性的数量;
4:就是这个属性的内存长度因为是int类型,所以长度是4
*/
{(unsigned long int *)&OBJC_IVAR_$_Person$_age, "_age", "i", 2, 4},
{(unsigned long int *)&OBJC_IVAR_$_Person$_height, "_height", "i", 2, 4}
}
};
#pragma mark ############# 重要 isa指针什么时候赋值的? ############
/*
重要
其实在这里赋值的,
首先是NSObject的元类对象给Person的元类isa赋值;
最后Person的元类对象赋值给Person类对象的isa
*/
static void OBJC_CLASS_SETUP_$_Person(void ) {
OBJC_METACLASS_$_Person.isa = &OBJC_METACLASS_$_NSObject;
OBJC_METACLASS_$_Person.superclass = &OBJC_METACLASS_$_NSObject;
OBJC_METACLASS_$_Person.cache = &_objc_empty_cache;
OBJC_CLASS_$_Person.isa = &OBJC_METACLASS_$_Person;
OBJC_CLASS_$_Person.superclass = &OBJC_CLASS_$_NSObject;
OBJC_CLASS_$_Person.cache = &_objc_empty_cache;
}
#pragma section(".objc_inithooks$B", long, read, write)
__declspec(allocate(".objc_inithooks$B")) static void *OBJC_CLASS_SETUP[] = {
(void *)&OBJC_CLASS_SETUP_$_Person,
};
#pragma mark ######################### 元类对象 到底是怎么产生的? ##############################
/*
这里是元类对象实现的地方
在.cpp文件我们看到,这里初始化了一个_class_t类型的变量 OBJC_METACLASS_$_Person
OBJC_METACLASS_$_Person就是 Person 元类对象,全局只有这一个;
我们发现初始化的时候:
struct _class_t *isa; 这个是0,也就是说元类对象初始化的时候,他的isa指针式空的,别担心,后面会对他从新赋值
struct _class_t *superclass; 这个也是0,也就是说元类对象初始化的时候,他的指向父类的superclass指针式空的,别担心,后面会对他从新赋值
void *cache; 缓存也是空的
void *vtable; 也是空的
struct _class_ro_t *ro; 这个有值,这个值保存的就是Person元类的数据结构, 基本上保存的是类方法,+加号方法
我们全局找到_OBJC_METACLASS_RO_$_Person这个在那实现的;请继续看下面
*/
extern "C" __declspec(dllexport) struct _class_t OBJC_METACLASS_$_Person __attribute__ ((used, section ("__DATA,__objc_data"))) = {
0, // &OBJC_METACLASS_$_NSObject,
0, // &OBJC_METACLASS_$_NSObject,
0, // (void *)&_objc_empty_cache,
0, // unused, was (void *)&_objc_empty_vtable,
&_OBJC_METACLASS_RO_$_Person,
};
/*
我们发现上面的_OBJC_METACLASS_RO_$_Person是在这里实现的;
可以看到
unsigned int flags; 值是1
unsigned int instanceStart;可以看到内存初始位置就是_class_t结构体的长度
unsigned int instanceSize;可以看到内存长度
unsigned int reserved; 值是0
const unsigned char *ivarLayout;也是0
const char *name; 类名"Person"
const struct _method_list_t *baseMethods; 这个有值,_OBJC_$_CLASS_METHODS_Person,我们可以看到初始化的时候保存的是类方法,也就是+加号方法
const struct _objc_protocol_list *baseProtocols;也是0
const struct _ivar_list_t *ivars;也是0
const unsigned char *weakIvarLayout;也是0
const struct _prop_list_t *properties;也是0
我们取看看_method_list_t的值是在哪初始化的,这个一开始保存的是类方法,也就是+加号方法
去找找类方法在哪实现的
*/
static struct _class_ro_t _OBJC_METACLASS_RO_$_Person __attribute__ ((used, section ("__DATA,__objc_const"))) = {
1,
sizeof(struct _class_t),
sizeof(struct _class_t),
(unsigned int)0,
0,
"Person",
(const struct _method_list_t *)&_OBJC_$_CLASS_METHODS_Person,
0,
0,
0,
0,
};
/*
类方法在这里实现的;我们发现类方法实现的方式和对象方法实现的是一样的,
那就不多介绍了
*/
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[1];
} _OBJC_$_CLASS_METHODS_Person __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"home", "v16@0:8", (void *)_C_Person_home}}
};