OC底层学习-对象的本质
1. OC对象的本质
- OC代码经过编译的转换过程- OC-> C/C++ -> 汇编语言-> 机器语言;
- OC的面向对象都是基于C/C++的结构体来实现的,OC对象、类的实质就是一个结构体
1.1结构的内存对齐
- 结构体的总大小,必须要是其内部最大成员的整数倍,不足的要补齐。
- 结构体或联合的数据成员,第一个数据成员是要放在offset == 0的地方,如果遇上子成员,要根据子成员的类型存放在对应的整数倍的地址上。
- 如果结构体作为成员,则要找到这个结构体中的最大元素,然后从这个最大成员的整数倍地址开始存储。
示例代码:
typedef struct one {
char a;=====> 1 -> 4
int b;======> 4
double c;===> 8. //max
char d;=====> 1 -> 8 //补齐到8的整数倍
} ONE:
//第一点:结构体one总大小: 4+4+8 = 16
typedef struct two {
char array[2];==> 2 -> 4
int b;==========> 4
double c;=======> 8 //max
float d;========> 4 -> 8 //原则1
} TWO;
//第二点:结构体two总大小: 4+4+8+8 = 24
struct three {
char a; ====> 1 -> 4
int b;======> 4
double c;===> 8
short d;====> 2 -> 8 //原则3 ,下面是个结构体,其中最大成员为8,则需要从8的整数倍地址存放,所以变量d补齐到8
TWO e; ==> 24 (max 8)
};
// 第三点: 结构体two总大小: 4+4+8+8+24 = 48
1.2 怎么把OC类通过终端转换成C++文件
- clang -rewrite-objc OC原文件 -o 输出的cpp文件(c plus plus C++的意思) -> 没有指定平台,转出来的c++文件可能会不同
- 转成iPhone的c++文件:xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc oc源文件 -o 输出的cpp文件
- xcrun中的xc 是xcode的简称 通过工具可以指定sdk
1.3 查看源码
- 查看OC源码的地址: 源码地址
//首先我们创建一个对象
NSObject *obj = [[NSObject alloc] init];
// C++ 中的原文件 类的实现
struct NSObject_IMPL {
Class isa;//(指针在64位下占 8个字节 32 位占 4个字节)
//isa的地址就是结构体在内存中的地址(结构体的内存地址就是里面第一个成员的地址)
//结构体占8个字节
//指针类型 占8个字节
};
//NSObject.h中的实现:
@interface NSObject <NSObject> {
Class isa ;
}
// 指针:
typedef struct objc_class *Class;
- 可以看出NSObject的实质转换过程:
1.4面试题:一个NSObject的对象占多少内存?
NSObject *obj = [[NSObject alloc] init];
//返回的是NSObject类的实例对象的成员变量所占用的大小 >> 8
//需要导入#import <objc/runtime.h>
class_getInstanceSize([NSObject class]);
NSLog(@"%zd",class_getInstanceSize([NSObject class]));
//获得obj指针所指向的大小 ->16
//需要导入#import <malloc/malloc.h>
NSLog(@"zd%",malloc_size((__bridge const void *)obj));
查看alloc源码的实现过程:
+ (id)allocWithZone:(struct _NSZone *)zone {
return _objc_rootAllocWithZone(self, (malloc_zone_t *)zone);
}
id _objc_rootAllocWithZone(Class cls, malloc_zone_t *zone)
{
id obj;
obj = class_createInstance(cls, 0);
return obj;
}
id class_createInstance(Class cls, size_t extraBytes)
{
return _class_createInstanceFromZone(cls, extraBytes, nil);
}
id _class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
size_t size = cls->instanceSize(extraBytes);
obj = (id)calloc(1, size);
return obj;
}
size_t instanceSize(size_t extraBytes) {
size_t size = alignedInstanceSize() + extraBytes;
if (size < 16) size = 16; //限定最少都是16字节
return size;
}
系统分配了16个字节给NSObject对象(可以通过malloc_size函数获得),但是NSObject对象内部只使用了8个字节的空间(64bit的环境下们可以通过class_getInstanceSize函数获得),这8个字正是 存放结构体(NSObject_IMPL)中的isa指针(core fountion 中限制了一个NSObject对象的内存最小是16个字节)
- 我们多增加几个成员变量,来看看内存的变化:
@interface Person : NSObject
{
@public
int _age;
int _age1;
int _age2;
}
NSLog(@"person - %zd", class_getInstanceSize([Person class]));
NSLog(@"person - %zd", malloc_size((__bridge const void *)person));
2020-08-01 11:27:37.169431+0800 Interview01-OC对象的本质[3967:115063] person - 24
2020-08-01 11:27:37.169903+0800 Interview01-OC对象的本质[3967:115063] person - 32
分配内存的时候,如果前面的内存空间还有空余,就不会分配新的,只有不够的时候才会多分配:
例如Person对象通过malloc_size()方法打印出来是32个字节,如下: Person继承自NSObject对象,隐式含有struct NSObject_IMPL NSObject_IVARS;继承父类的属性, 这个属性占据16个字节,但是实际用到也就是一个结构体 isa指针也就是8个字节,还空余8个字节,而Person对象中特有的3个int类型的变量,占12个字节,不够放,根据内存对齐的原则,内存空间是最大成员大小的倍数也就是16 的倍数,一个16放不下,也就扩大2倍,也就是32个字节。–可以猜想,如果32还不够放,拿就继续扩大
class_getInstanceSize 是计算所有成员大小的实际内存大小, struct NSObject_IMPL NSObject_IVARS实际大小是8 所以是按照8的倍数来扩大
1.5实时查看内存数据
1.6 常用的LLDB指令
- print、p:打印 对于基础类型可以指定打印格式 比如打印16进制-> p/x
- po: 打印对象
- 读取内存:
- memory read/数量格式字节数 内存地址
- x/数量格式字节数 内存地址
- 格式:
- x:代表16机制,f是浮点数,d是十进制
- 字节大小:b >byte1字节;h >half word2字节;w >word4字节;g >giant word 8字节
- 如: x/3xw 0x10010
- 修改内存中的值
- memory write 内存地址 数值(第几位地址) -> memory write 0x0000000010 10
1.7 示例-验证-结论
//创建一个student类
@interface Student : NSObject
{
@public
int _no;
int _age;
}
@end
//student的实质是 存在继承关系的时候父类的成员变量放在前面
struct Student_IMPL {
Class isa; //继承父类的
int _no;
int _age;
};
//测试代码
Student *stu = [[Student alloc] init];
stu->_no = 4; //通过指针直接访问成员变量 ->
stu->_age = 5;
-
本质:
-
一个结构体内的内存是连续的,结构体的地址值是里面第一个成员变量的地址值,也就是isa指针的地址值,也就是stu的地址是 isa指针指向的地址 占8个字节
NSLog(@"%zd", class_getInstanceSize([Student class])); //16
NSLog(@"%zd", malloc_size((__bridge const void *)stu));//16
//这两个方法返回的是内存对齐后的值
struct Student_IMPL *stuImpl = (__bridge struct Student_IMPL *)stu;
NSLog(@"no is %d, age is %d", stuImpl->_no, stuImpl->_age);
-
所以stu在内存中占用的空间是 16个字节
-
OC对象在分配内存的时候都是16的倍数
-
IOS都是小端模式-> 是从高地址开始读取
-
声明一个属性做了什么事情?
- 生成一个_xxx的成员变量
- 生成了xxx变量的set和get方法
@interface Person : NSObject
{
@public
int _age;
}
@property (nonatomic, assign) int height;
//底层实现代码
struct Person_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _age;
int _height;
};
按理来说内存中应该也存有方法,但是实例对象的内存中没有方法,实际上我们创建出来的实例对象内存中是不放方法的,只存有它的成员变量;为什么? 因为后面我们可能会创建出很多个实例对象,每个实例对象的成员变量的值可能不一样,所以每个实例变量都需要有一份自己的内存在存放自己的实例变量(存放自己特有的),但是方法方法是一样的,只放一份就可以了。
sizeof(type):
传递参数是一个类型,如 int 、struct 等,然后返回这个类型占多少内存,不是函数,是一个运算符,在编译器就确定了值。如 你传递是int ,在编译器的时候就直接使用4代替了class_getInstanceSize(Class _Nullable cls):
传递参数是一个类对象,返回这个类对象占多少内存空间
struct GYPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _age;
int _height;
};
GYPerson *person = [[GYPerson alloc] init];
NSLog(@"sizeof=====%zd ====%zd",sizeof(struct GYPerson_IMPL),sizeof(person));
//sizeof(person) 不是告诉你person占多少内存 返回的是p这个指针占多少内存,而不是实际的person的对象占多少内存
2 .insatnce、class、meta-class对象
上面讲的OC对象的本质,是讲的实例对象的本质。
- Objection-C中的对象,简称OC对象,主要分为3种,instance对像(实例对象)、class对象(类对象)、meta-class对象(元类对象)。
2.1 instance对象
- instance对象就是通过alloc出来的对象,每次调用alloc都会产生新的instance对象
NSObject *object1 = [[NSObject alloc] init];
NSObject *objecte2 = [[NSObject alloc] init];
- object1,object2是NSObject的
instance
对象(实例对象) - 他们是两个不同的对象,分别占据着不同的内存
- instance对象在内存中储存的信息包括:
isa
: 所有的实例对象都会有这个成员变量- 其他成员变量
- 几乎所有的类都继承自NSObject对象,所以所有的实例对象中都包含NSObject的结构东西(转成c++之后是结构体)
- 注意: 实例对象内存中是不储存方法的
- 使用copy方法得到对象,和原来的对象是不是同一个对象,取决于你的
-(id)copyWithZone(NSZone *)zone
是怎么实现的,如果你返回的是self,那么产生的就是同一个对象,如果返回的是重新生成的对象,就是不同的对象
2.2 Class对象
NSObject *object = [[NSObject alloc] init];//实例对象
NSObject *object2 = [[NSObject alloc] init];
//获取类对象
Class objectClass = [object class];
//第二种获取类对象的方法 直接调用class方法
Class objectClass1 = [NSObject class];
//第三种获取类对象方法 Runtime API获取
Class objectClass2 = object_getClass(object);
Class objeceClass3 = object_getClass(object2);
NSLog(@"%p %p %p %p", objectClass,objectClass1,objectClass2,objeceClass3);
//打印结果: 0x7fff9087e118 0x7fff9087e118 0x7fff9087e118 0x7fff9087e118
- 上述objectClass- objectClass3都是NSObject的class对象(类对象)
- 他们都是同一个对象(NSObject),每个类在内存中有且只有一个class对象
- class对象在内存中储存的信息主要包括:
isa
指针supperclass
指针- 类的属性信息(@propertory)、类的对象方法信息(insatnce method)
- 类的协议信息(protocol)、类的成员变量信息(ivar)(成员变量信息指 成员变量的描述信息,类型、名称等)
- …
- class方法返回的一直都是class对象,类对象
2.2.1 窥探struct objc_class(Class)的结构
查看源码:
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
//去掉结构体里面的方法之后 剩下的源码
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();
}
//其他结构体方法。。。
}
class_rw_t* data() {
return (class_rw_t *)(bits & FAST_DATA_MASK);//得到class_rw_t的地址
}
//去除方法的部分源码
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint32_t version;
const class_ro_t *ro; //->源码如下
method_array_t methods;//方法列表
property_array_t properties; //属性列表
protocol_array_t protocols;// 协议列表
Class firstSubclass;
Class nextSiblingClass;
char *demangledName;
}
//class_ro_t源码
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;//instance对象的占用内存空间
#ifdef __LP64__
uint32_t reserved;
#endif
const uint8_t * ivarLayout;
const char * name;//类名
method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;//成员变量列表
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
method_list_t *baseMethods() const {
return baseMethodList;
}
};
2.3 meta_class元类对象
- 获取元类对象的方法
//获取元类对象 meta-class
//将类对象作为参数传入,获得元类对象
Class objecMetaClass = object_getClass(objectClass1);
//判断一个对象是否是元类对象
BOOL rs = class_isMetaClass(objecMetaClass);
- objecMetaClass是NSObject的meta-class对象(元类对象)
- 每个类在内存中有且只有一个
meta-class
对象 - meta-class对象和class对象的内存结构是一样的(两个对象的类型都是Class类型),但是用途不一样,在内存中存储的信息主要包括
isa
指针superclass
指针- 类的类方法信息(class method-> + 号方法)
- …
2.4 isa指针
2.4.1 instance的isa指针
insatnce
的isa
指针指想class
对象(类对象)- 当调用对象方法时,通过
instance
的isa找到class
,最后找到对象方法的实现进行调用
- 当调用对象方法时,通过
class
的isa
指向meta
-class(元类对象)- 当调用类方法时,通过class的isa找到meta-class,最好找到类方法的实现进行调用。
2.4.1.1 验证insatance的isa指正指向class对象
- 从64位开始,isa需要进行一次为运算,才能计算出真实的地址
- 在64位以前 isa的地址值和class对象的地址值是一样的
ISA_MASK简化后的源码
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
示例代码:
首先我们创建一个GYPerson类,然后分别获取GYPerson的instance对象、class对象、meta-class对象
@interface GYPerson : NSObject <NSCopying>
@end
@implementation GYPerson
@end
//获取实例对象
GYPerson *person = [[GYPerson alloc] init];
//获取类对象
Class personClass = [GYPerson class];
//获取元类对象object_getClass
Class personMetaClass = object_getClass(personClass);
NSLog(@"insatnce===%p class=====%p meta-class=====%p",person,personClass,personMetaClass);
下个断点,然后通过LLDB的指令来算出person的isa地址,和ISA_MASK值位运算之后和personMetaClass的地址值对比,看是不是相等,这里我编写的是mac编译程序,选择的是mac上的位运算值,也就是 # elif __x86_64__# define ISA_MASK 0x00007ffffffffff8ULL
结论:经过运算之后,发现位运算之后的isa
的地址值确实和class
对象的地址值相等,也就是说instance
内的isa
确实指向class
对象
2.4.1.2 验证class的isa指向meta-class对象
- 示例代码:
struct gy_objc_class {
Class isa;
};
//首先你直接p/x personClass->isa 会报错,首先把class对象转成结构体
//首先我们定义一个结构体和objc_class类似的结构体,把isa指针暴露出来
struct gy_objc_class *personClass2 = (__bridge struct gy_objc_class *)(personClass);
-
结论:
class
对象的isa
的地址值和meta-calss
的地址值一样,也就是说class
的isa
指针指向meta-class
对象 -
但是superclass指针是直接指向父类对象的,不需要进行&ISA_MASK位运算
2.4.2 class对象的superclass指针
// 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
- 当Student的
instance
对象要调用Person的对象方法时,会先通过isa
找到Student的class,然后通过superclass
找到Person的class
,最后找到对象方法的实现进行调用 class
对象的superclass
指针指向的是父类的类对象
2.4.3 meta-class对象的superclass指针
meta-class
(元类对象)的superclass
指针指向的是父类的元类对象
2.4.4 isa、superclass总结
- 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对象
- insatnce调用对象方法的轨迹
- isa找到class对象,方法不存在,就通过superclass找父类的class对象,一直找到到基类,如果最后没有找到就会报错
- class调用类方法的轨迹
- 通过isa找到meta-class对象,方法不存在,就通过superclass找父类的meta-class对象,一直找到基类,如果基类的meta-class没有这个方法,基类的meta-class是superclass指针是指向基类的class对象的,所以回去基类的class中找,如果还找不到就报错,找到就调用
- 示例代码:
@interface MJPerson : NSObject
+ (void)test;
@end
@implementation MJPerson
@end
@interface NSObject (Test)
+ (void)test;
@end
@implementation NSObject (Test)
- (void)test
{
NSLog(@"-[NSObject test] - %p", self);
}
@end
//测试代码
NSLog(@"[MJPerson class] - %p", [MJPerson class]);
NSLog(@"[NSObject class] - %p", [NSObject class]);
[MJPerson test];
[NSObject test];
执行的结果:
结果表明:我们调用的是类方法,但是最后却是执行了NSOjbect的一个对象实例方法,证明了上述class调用方法的轨迹
- 总结:
- 方法调用流程: isa -> superclass -> suerpclass -> superclass -> … superclass == nil
- OC中发消息的本质是像某个对象发送消息,但是并没有说是调用类方法还是调用实例对象方法
objc_msgSend()
2.5 .面试题
2.5.1 对象的isa指针指向哪里
- instance对象的isa指针指向class对象
- class对象的isa指向meta-calss对象
- meta-class对象isa指针指向基类的meta-class对象
2.5.1 OC的类信息存放在哪里?
- 对象方法、成员变量(类型、名称…)、属性、协议信息,存放在class对象中
- 类方法、存放在meta-class对象中
- 成员变量的具体指,存放在instance对象
2.6 答疑
objc_getClass(<#const char * _Nonnull name#>)
、object_getClass(id _Nullable obj)
、-(Class)class、+(Class)class
的区别objc_getClass(const char * _Nonnull name)
方法将一个类名传进去,返回一个对应class对象(类对象),如果不存在就返回nilobject_getClass(id obj)
传入的obj可能是instance对象、class对象、meta-class对象- 如果传递进来是instance对象,返回是class对象
- 如果是class对象,返回meta-class对象
- 如果是meta-class对象,返回NSObject(基类)的meta-class对象
-(Class)class、+(Class)class
: 返回的就是类对象
- 源码:
//objc_getClass的源码
Class objc_getClass(const char *aClassName)
{
if (!aClassName) return Nil;
// NO unconnected, YES class handler
return look_up_class(aClassName, NO, YES);
}
//object_getClass的源码
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
- class对象和meta-class对象在什么时候分配内存? 什么时候释放?
- 程序启动,执行main函数后,就开始加载类的信息放到内存中,不管你用不用,都会加载到内存中
- class对象和meta-class对象一直都在内存中 ,程序退出的时候一起释放(因为class和meta-class在内存中只有一份)。