面试题
一个 NSObject 对象占用多少内存?
- 系统分配了16个字节给NSObject对象(通过malloc_size函数获得)
- 但NSObject对象内部只使用了8个字节的空间(64bit环境下,可以通过class_getInstanceSize函数获得)
对象的 isa 指针指向哪里?
- instance对象的isa指向class对象
- class对象的isa指向meta-class对象
- meta-class对象的isa指向基类的meta-class对象
OC 的类信息存放在哪里?
- 对象方法、属性、成员变量、协议信息,存放在class对象中
- 类方法,存放在meta-class对象中
- 成员变量的具体值,存放在instance对象
探寻OC对象的本质
我们平时编写的Objective-C代码,底层实现其实都是C\C++代码
所以Objective-C的面向对象都是基于C\C++的数据结构实现的
思考:Objective-C的对象、类主要是基于C\C++的什么数据结构实现的?
答:结构体
将Objective-C代码转换为C\C++代码
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc OC源文件 -o 输出的CPP文件
如果需要链接其他框架,使用-framework参数。比如-framework UIKit
NSObject对象的内部实现
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
Class isa OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
@end
转化为 C 语言其实就是一个结构体
struct NSObject_IMPL {
Class isa;
};
// 查看Class本质
typedef struct objc_class *Class;
这个结构体占多大的内存空间呢,我们发现这个结构体只有一个成员变量,isa 指针,而指针在 64 位架构中占 8 个字节。但由于系统内存对齐是按16个字节对齐,对于小于16个字节的会补齐到16个字节,也就是说一个 NSObject 对象所占用的内存是 16 个字节,前面8个字节存储 isa 指针,后面没用到的内存填充 0。
【注】编译器在给结构体开辟空间时,首先找到结构体中最宽的基本数据类型,然后寻找内存地址能是该基本数据类型的整倍的位置,作为结构体的首地址。将这个最宽的基本数据类型的大小作为对齐模数,即结构体的大小必须是最大成员大小的倍数。
为结构体的一个成员开辟空间之前,编译器首先检查预开辟空间的首地址相对于结构体首地址的偏移是否是本成员的整数倍,若是,则存放本成员,反之,则在本成员和上一个成员之间填充一定的字节,以达到整数倍的要求,也就是将预开辟空间的首地址后移几个字节。
为了探寻 OC 对象的内存中如何体现,我们来看下面一段代码
NSObject *objc = [[NSObject alloc] init];
上述一段代码中系统为 NSObject 对象分配 16 个字节的内存空间,其中8个字节用来存放一个成员 isa 指针。那么 isa 指针这个变量的地址就是结构体的地址,也就是 NSObject 对象的地址。
假设 isa 的地址为 0x100400110,那么上述代码分配存储空间给 NSObject 对象,然后将存储空间的地址赋值给 objc 指针。objc 指向内存中 NSObject 对象地址,即指向内存中的结构体,也就是 isa 的位置。
自定义类的内部实现
@interface Student : NSObject{
@public
int _no;
int _age;
}
@end
@implementation Student
int main(int argc, const char * argv[]) {
@autoreleasepool {
Student *stu = [[Student alloc] init];
stu -> _no = 4;
stu -> _age = 5;
NSLog(@"%@",stu);
}
return 0;
}
@end
将上述代码转化生成 C++ 文件。并查找 Student,发现 Student_IMPL
struct Student_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _no;
int _age;
};
发现第一个是 NSObject_IMPL 的实现。而通过上面的探索,我们知道 NSObject_IMPL 内部其实就是 Class isa
那么我们假设 struct NSObject_IMPL NSObject_IVARS; 等价于 Class isa;
可以将上述代码转化为
struct Student_IMPL {
Class *isa;
int _no;
int _age;
};
因此此结构占用多少存储空间,对象就占用多少存储空间。因此结构体占用的存储空间为,isa 指针 8 个字节 + int 类型 _no 4个字节 + int 类型 _age 4个字节空间共 16 个字节空间
那么上述代码实际上在内存中体现为,创建 Student 对象首先会分配 16 个字节,存储 3 个东西:isa 指针 8个字节,4个字节的_no,4个字节的 _age
student 对象的3个变量分别有自己的地址。而 stu 指向 isa 指针的地址。因此 stu 的地址为 0x100400110,stu 对象的内存中占用 16 个字节的空间。并且经过赋值,_no 里面存储 4, _age 里面存储5
验证 Student 的内存中模样
struct Student_IMPL {
Class isa;
int _no;
int _age;
};
@interface Student : NSObject
{
@public
int _no;
int _age;
}
@end
@implementation Student
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 强制转化
struct Student_IMPL *stuImpl = (__bridge struct Student_IMPL *)stu;
NSLog(@"_no = %d, _age = %d", stuImpl->_no, stuImpl->_age); // 打印出 _no = 4, _age = 5
}
return 0;
}
上述代码将 oc 对象强转成 Student_IMPL 的结构体。也就是说把一个指向 oc 对象的指针,指向这种结构体。由于我们之前猜想,对象在内存中的布局与结构体在内存中的布局相同,那么如果转化成功,说明我们猜想正确。由此说明 stu 这个对象指向的内存确实是一个结构体。
实际上想要获取对象占用内存的大小,可以通过更便捷的运行时方法来获取
class_getInstanceSize([Student class])
NSLog(@"%zd,%zd", class_getInstanceSize([NSObject class]) ,class_getInstanceSize([Student class]));
// 打印信息 8和16
实时查看内存数据
方式一:Debug Workflow -> viewMemory address (Shift + Command + M) 中输入stu的地址
方式二:也可以使用LLDB指令
常用LLDB指令
print、p:打印
po:打印对象
读取内存
memory read 内存地址
简写:x 内存地址memory read 0x10074c450
x 0x10074c450增加读取条件
memory read/数量格式字节数 内存地址
简写:x/数量格式字节数 内存地址
格式:x是16进制,f是浮点,d是10进制
字节大小:b:byte 1字节,h:half word 2字节,w:word 4字节,g:giant word 8字节x/3xw 0x10010
修改内存中的值
memory write 内存地址 数值memory write 0x0000010 10
复杂继承关系的类的内部实现
// Person
@interface Person : NSObject
{
@public
int _age;
}
@end
@implementation Person
@end
//Student
@interface Student : Person
{
int _no;
}
@end
@implementation Student
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Student *stu = [[Student alloc] init];
NSLog(@"stu - %zd", class_getInstanceSize([Student class])); //打印 stu - 16
NSLog(@"stu - %zd", malloc_size((__bridge const void *)stu)); //打印 stu - 16
}
return 0;
}
【注意】2个容易混淆的函数
//创建一个实例对象,至少需要多少内存? #import <objc/runtime.h> class_getInstanceSize([NSObject class]);
//创建一个实例对象,实际上分配了多少内存? #import <malloc/malloc.h> malloc_size((__bridge const void *)obj);
思考:一个Person对象、一个Student对象占用多少内存空间?
由上图的 Person 对象和 Student 对象转化为 C++ 结构体可看出
- Person_IMPL 结构体中可将
struct NSObject_IMPL NSObject_IVARS;
替换成Class isa;
,Person_IMPL 结构体中 isa 指针 8 个字节 + int 成员 _no 4个字节,但由于结构体内存对齐的规则,结构体的大小必须是最大成员大小的倍数,故Person_IMPL 结构体的大小为 16 个字节,由于系统为对象实际为对象分配的内存大小是16的倍数,所以一个Person对象实际占用了16个字节的内存空间; - Student_IMPL 结构体中可将
struct Person_IMPL Person_IVARS
替换为Class isa; int _age;
,Student_IMPL 结构体中 isa 指针 8 个字节 + int 成员 _no 4个字节 + int 成员 _age 4个字节 ,结构体内存对齐的规则可知Student_IMPL 结构体的大小为 16 个字节,由于系统为对象实际为对象分配的内存大小是16的倍数,所以一个Student对象实际占用了16个字节的内存空间;
OC对象的分类
Objective-C中的对象,简称OC对象,主要可以分为3种
- instance对象(实例对象)
- class对象(类对象)
- meta-class对象(元类对象)
instance 对象
instance对象就是通过类alloc出来的对象,每次调用alloc都会产生新的instance对象
NSObjcet *object1 = [[NSObjcet alloc] init];
NSObjcet *object2 = [[NSObjcet alloc] init];
object1、object2是NSObject的instance对象(实例对象),它们是不同的两个对象,分别占据着两块不同的内存
instance对象在内存中存储的信息包括
- isa指针
- 其他成员变量
@interface Person : NSObject <NSCopying>
{
@public
int _age;
}
@implementation Person
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p1 = [[Person alloc] init];
p1->_age = 3;
Person *p2 = [[Person alloc] init];
p2->_age = 4;
}
return 0;
}
class
NSObject *object1 = [[NSObject alloc] init];
NSObject *object2 = [[NSObject alloc] init];
Class objectClass1 = [object1 class];
Class objectClass2 = [object2 class];
Class objectClass3 = [NSObject class];
Class objectClass4 = object_getClass(object1); //Runtime API
Class objectClass5 = object_getClass(object2); //Runtime API
- objectClass1 ~ objectClass5都是NSObject的class对象(类对象)
- 它们是同一个对象。每个类在内存中有且只有一个class对象
class对象在内存中存储的信息主要包括
- isa指针
- superclass指针
- 类的属性信息(@property)、类的对象方法信息(instance method)
- 类的协议信息(protocol)、类的成员变量信息(ivar)
- ......
meta-class
// meta-class对象,元类对象
// 将类对象当做参数传入,获得元类对象
Class objectMetaClass = object_getClass([NSObject class]);
- objectMetaClass是NSObject的meta-class对象(元类对象)
- 每个类在内存中有且只有一个meta-class对象
meta-class对象和class对象的内存结构是一样的,但是用途不一样,在内存中存储的信息主要包括
- isa指针
- superclass指针
- 类的类方法信息(class method)
- ......
以下代码获取的objectClass是class对象,并不是meta-class对象
Class objectClass = [[NSObject class] class];
查看Class是否为meta-class
#import <objc/runtime.h>
Bool result = class_isMetaClass(objectMetaClass);
isa 指针
- instance 的 isa 指向 class
当调用对象方法时,通过instance的isa找到class,最后找到对象方法的实现进行调用 - class的isa指向meta-class
当调用类方法时,通过class的isa找到meta-class,最后找到类方法的实现进行调用
从64bit开始,isa需要进行一次位运算,才能计算出真实地址
class对象的superclass指针
当Student的instance对象要调用Person的对象方法时,会先通过isa找到Student的class,然后通过superclass找到Person的class,最后找到对象方法的实现进行调用
meta-class对象的superclass指针
当Student的class要调用Person的类方法时,会先通过isa找到Student的meta-class,然后通过superclass找到Person的meta-class,最后找到类方法的实现进行调用
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
- instance调用对象方法的轨迹:isa找到class,方法不存在,就通过superclass找父类
- class调用类方法的轨迹:isa找meta-class,方法不存在,就通过superclass找父类
窥探struct objc_class的结构
class、meta-class对象的本质结构都是struct objc_class
验证探索结论
自定义一个结构体,其结构和objc_class真实结构一样,当我们强制转化的时候,就会一一对应的赋值。就可以拿到结构体内部的信息。
仿照代码如下,提取其中需要使用到的信息,自定义的一个结构体。
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# endif
#if __LP64__
typedef uint32_t mask_t;
#else
typedef uint16_t mask_t;
#endif
typedef uintptr_t cache_key_t;
struct bucket_t {
cache_key_t _key;
IMP _imp;
};
struct cache_t {
bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
};
struct entsize_list_tt {
uint32_t entsizeAndFlags;
uint32_t count;
};
struct method_t {
SEL name;
const char *types;
IMP imp;
};
struct method_list_t : entsize_list_tt {
method_t first;
};
struct ivar_t {
int32_t *offset;
const char *name;
const char *type;
uint32_t alignment_raw;
uint32_t size;
};
struct ivar_list_t : entsize_list_tt {
ivar_t first;
};
struct property_t {
const char *name;
const char *attributes;
};
struct property_list_t : entsize_list_tt {
property_t first;
};
struct chained_property_list {
chained_property_list *next;
uint32_t count;
property_t list[0];
};
typedef uintptr_t protocol_ref_t;
struct protocol_list_t {
uintptr_t count;
protocol_ref_t list[0];
};
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;
};
struct class_rw_t {
uint32_t flags;
uint32_t version;
const class_ro_t *ro;
method_list_t * methods; // 方法列表
property_list_t *properties; // 属性列表
const protocol_list_t * protocols; // 协议列表
Class firstSubclass;
Class nextSiblingClass;
char *demangledName;
};
#define FAST_DATA_MASK 0x00007ffffffffff8UL
struct class_data_bits_t {
uintptr_t bits;
public:
class_rw_t* data() {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
};
/* OC对象 */
struct wkb_objc_object {
void *isa;
};
/* 类对象 */
struct wkb_objc_class : wkb_objc_object {
Class superclass;
cache_t cache;
class_data_bits_t bits;
public:
class_rw_t* data() {
return bits.data();
}
wkb_objc_class* metaClass() {
return (wkb_objc_class *)((long long)isa & ISA_MASK);
}
};
#endif /* WKBClassInfo_h */
将自定义的类强制转化为自定义的精简的class结构体类型。
// objective-c++
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import "WKBClassInfo.h"
// WKBPerson
@interface WKBPerson : NSObject <NSCopying>
{
@public
int _age;
}
@property (nonatomic, assign) int no;
- (void)personInstanceMethod;
+ (void)personClassMethod;
@end
@implementation WKBPerson
- (void)test
{
}
- (void)personInstanceMethod
{
}
+ (void)personClassMethod
{
}
- (id)copyWithZone:(NSZone *)zone
{
return nil;
}
@end
// WKBStudent
@interface WKBStudent : WKBPerson <NSCoding>
{
@public
int _weight;
}
@property (nonatomic, assign) int height;
- (void)studentInstanceMethod;
+ (void)studentClassMethod;
@end
@implementation WKBStudent
- (void)test
{
}
- (void)studentInstanceMethod
{
}
+ (void)studentClassMethod
{
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
return nil;
}
- (void)encodeWithCoder:(NSCoder *)aCoder
{
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *object = [[NSObject alloc] init];
WKBPerson *person = [[WKBPerson alloc] init];
WKBStudent *student = [[WKBStudent alloc] init];
wkb_objc_class *objectClass = (__bridge wkb_objc_class *)([NSObject class]);
wkb_objc_class *studentClass = (__bridge wkb_objc_class *)([WKBStudent class]);
wkb_objc_class *personClass = (__bridge wkb_objc_class *)([WKBPerson class]);
wkb_objc_class *objectMetaClass = objectClass->metaClass();
wkb_objc_class *personMetaClass = personClass->metaClass();
wkb_objc_class *studentMetaClass = studentClass->metaClass();
class_rw_t *objectClassData = objectClass->data();
class_rw_t *personClassData = personClass->data();
class_rw_t *studentClassData = studentClass->data();
class_rw_t *objectMetaClassData = objectMetaClass->data();
class_rw_t *personMetaClassData = personMetaClass->data();
class_rw_t *studentMetaClassData = studentMetaClass->data();
// 0x00007ffffffffff8
NSLog(@"%p %p %p %p %p %p", objectClassData, personClassData, studentClassData,
objectMetaClassData, personMetaClassData, studentMetaClassData);
NSLog(@"1111");
}
return 0;
}
instance对象
首先看instance对象,我们知道,instance对象中存储着isa指针和其他成员变量,并且instance对象的isa指针是指向其类对象地址的。
由图可知instance对象中确实存储了isa指针和其成员变量,同时将instance对象的isa指针经过&运算之后计算出的地址确实是其相应类对象的内存地址。
class对象
接着看class对象,我们知道class对象中存储着isa指针,superclass指针,以及类的属性信息,类的成员变量信息,类的对象方法,和类的协议信息,而这些信息存储在class对象的class_rw_t中,我们通过强制转化来窥探其中的内容
通过模拟对person类对象调用.data函数,即对bits进行
&FAST_DATA_MASK(0x00007ffffffffff8UL)
运算,并转化为class_rw_t,即上图中的personClassData。发现成员变量信息,对象方法,属性等信息只显示first第一个,如果想要拿到更多的需要通过代码将指针后移获取。而上图中的instaceSize = 16也同person对象中isa指针8个字节+_age4个字节+_no4个字节相对应起来。那么类对象中的isa指针和superclass指针的指向如何,我们来验证一下
meta-class对象
最后看meta-class元类对象,meta-class中存储着isa指针,superclass指针,以及类的类方法信息。同时我们知道meta-class元类对象与class类对象,具有相同的结构,只不过存储的信息不同,并且元类对象的isa指针指向基类的元类对象,基类的元类对象的isa指针指向自己。元类对象的superclass指针指向其父类的元类对象,基类的元类对象的superclass指针指向其类对象。
可以看到结构同personClassData相同,并且成员变量及属性列表等信息为空,而methods中存储着类方法personClassMethod。
接着来验证isa及superclass指针的指向
上图中通过地址证明meta-class的isa指向基类的meta-class,基类的isa指针也指向自己。
上图中通过地址证明meta-class的superclass指向父类的meta-class,基类的meta-class的superclass指向基类的class类。