iOS底层原理总结 - 探寻OC对象的本质
iOS底层原理总结 - 探寻OC对象的本质
对小码哥底层班视频学习的总结与记录。
面试题:一个NSObject对象占用多少内存?
-
探寻OC对象的本质,我们平时编写的Objective-C代码,底层实现其实都是C\C++代码。
OC代码的转化过程.png - 所以OC的面向对象都是基于C\C++的数据结构实现的。
- 思考:OC的对象、类主要是基于C\C++的什么数据结构实现的?
- 结构体
- 我们通过创建OC文件及对象,并将OC文件转化为C++文件来探寻OC对象的本质,将OC代码转换成C\C++代码
- OC如下代码
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *objc = [[NSObject alloc] init];
NSLog(@"Hello, World!");
}
return 0;
}
- 我们通过命令行将OC的mian.m文件转化为c++文件。
clang -rewrite-objc main.m -o main.cpp // 这种方式没有指定架构例如arm64架构 其中cpp代表(c plus plus)
生成 main.cpp
- 我们可以指定架构模式的命令行,使用xcode工具 xcrun
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
//生成 main-arm64.cpp
//xcrun是xcode的一个工具,xc是xcode的简称
//iphoneos是指定平台,上面一句是在iphone上面的
//-arch 后面是架构,例如:模拟器(i386),32bit(armv7),64bit(arm63)
// OC源文件 -o 输出的CPP文件
- main-arm64.cpp 文件中搜索NSObjcet,可以找到NSObjcet_IMPL(IMPL代表 implementation 实现)
- 我们看一下NSObject_IMPL内部
struct NSObject_IMPL {
Class isa;
};
// 查看Class本质,command点击进入class
typedef struct objc_class *Class;
我们发现Class其实就是一个指针,对象底层实现其实就是这个样子。
- 补充:查看 objc_class,结构如下:
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
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;
- 有时候我们在看底层实现的时候会直接commen点击进入查看;
- NSObjcet的底层实现,点击NSObjcet进入发现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
- NSObjcet的定义,简化一下就是:
@interface NSObject {
Class isa ;
}
@end
- 最终转化为c语言其实就是一个结构体
struct NSObject_IMPL {
Class isa;
};
-
思考: 一个OC对象在内存中是如何布局的。
NSObject的底层实现 .png -
那么这个结构体占多大的内存空间呢,我们发现这个结构体只有一个成员,isa指针,而指针在64位架构中占用8个字节。也就是说一个NSObjec对象所占用的内存是8个字节。(这个地方说法是不准确的,准确的说是,一个NSObjec实例对象的成员变量所占用的内存是8个字节);
-
但是我们发现NSObject对象中还有很多方法,那这些方法不占用内存空间吗?其实类的方法等也占用内存空间,但是这些方法所占用的存储空间并不在NSObject对象中。
-
为了探寻OC对象在内存中如何体现,我们来看下面一段代码
NSObject *objc = [[NSObject alloc] init];
- 上面一段代码在内存中如何体现的呢?上述一段代码中系统为NSObject对象分配8个字节的内存空间,用来存放一个成员isa指针。那么isa指针这个变量的地址就是结构体的地址,也就是NSObjcet对象的地址。
- 假设isa的地址为0x100400110,那么上述代码分配存储空间给NSObject对象,然后将存储空间的地址赋值给objc指针。objc存储的就是isa的地址。objc指向内存中NSObject对象地址,即指向内存中的结构体,也就是isa的位置。
- 到这里我们回想面试题,一个NSObject对象占用多少内存?,按照我们的分析,应该是占用了8个字节,但是,这道题的答案不是8个字节,是16个字节;分析如下:
- 导入runtime的头文件,(#import <objc/runtime.h>)里面有一个函数,class_getInstanceSize();
NSLog(@"%zd", class_getInstanceSize([NSObject class]));
//这个函数的解释是返回一个类的实例对象的大小
//获得NSObject实例对象的成员变量所占用的大小 >> 8
- 导入malloc头文件,(#import <malloc/malloc.h>)里面有一个函数,malloc_size();
NSLog(@"%zd", malloc_size((__bridge const void *)obj));
//获得obj指针所指向内存的大小 >> 16
- 此时可以看到,第一个函数打印的是8,第二个函数打印的是16;那为什么第一个函数打印的是8,第二个函数打印的是16呢?
- 输入opensource.apple.com/tarballs查看苹果的源码;
- 搜索objc4,下载,打开,
- 搜索class_getInstanceSize,查看具体的实现方法
size_t class_getInstanceSize(Class cls)
{
if (!cls) return 0;
return cls->alignedInstanceSize();
}
- 我们发现内部调用了 alignedInstanceSize()这个函数,command点击进入发现,我们发现这个函数的描述是返回这个类的成员变量所占用的大小
// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() {
return word_align(unalignedInstanceSize());
}
- alloc的本质调用的是allocWithZone,搜索allocWithZone,发现最终调用的是rootAllocWithZone,找到rootAllocWithZone
id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone)
{
id obj;
#if __OBJC2__
// allocWithZone under __OBJC2__ ignores the zone parameter
(void)zone;
obj = class_createInstance(cls, 0);
#else
if (!zone) {
obj = class_createInstance(cls, 0);
}
else {
obj = class_createInstanceFromZone(cls, 0, zone);
}
#endif
if (slowpath(!obj)) obj = callBadAllocHandler(cls);
return obj;
}
- command点击进入class_createInstance()函数
id
class_createInstance(Class cls, size_t extraBytes)
{
return _class_createInstanceFromZone(cls, extraBytes, nil);
}
- command进入_class_createInstanceFromZone()函数
id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
if (!cls) return nil;
assert(cls->isRealized());
// Read class's info bits all at once for performance
bool hasCxxCtor = cls->hasCxxCtor();
bool hasCxxDtor = cls->hasCxxDtor();
bool fast = cls->canAllocNonpointer();
size_t size = cls->instanceSize(extraBytes);
if (outAllocatedSize) *outAllocatedSize = size;
id obj;
if (!zone && fast) {
obj = (id)calloc(1, size);
if (!obj) return nil;
obj->initInstanceIsa(cls, hasCxxDtor);
}
else {
if (zone) {
obj = (id)malloc_zone_calloc ((malloc_zone_t *)zone, 1, size);
} else {
obj = (id)calloc(1, size);
}
if (!obj) return nil;
// Use raw pointer isa on the assumption that they might be
// doing something weird with the zone or RR.
obj->initIsa(cls);
}
if (cxxConstruct && hasCxxCtor) {
obj = _objc_constructOrFree(obj, cls);
}
return obj;
}
- 在这段源码里面我们会发现calloc()函数来分配内存,分配的size,来自instanceSize()函数,command进入instanceSize()函数
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;
}
- 此时会发现当size小于16的时候,就把size设置为16,所以,到现在为止,这道面试题的答案是16;
回答面试题:
- 一个NSObject对象占用多少内存?
- 系统分配了16个字节给NSObject对象(通过malloc_size函数获得)
- 但NSObject对象内部只使用了8个字节的空间(64bit环境下,可以通过class_getInstanceSize函数获得)
自定义类的实例对象内存分配情况
面试题:在64bit环境下,自定类的实例对象占用多少内存呢?
- 首先创建一个Student类
@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
- 我们按照上面的OC代码转C++文件的方式进行转换。我们从 main-arm64.cpp 文件中搜索 Student,并查找Student,我们发现Student_IMPL:
struct Student_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _no;
int _age;
};
- 我们发现Student类转化为C++的结构体后第一项是struct NSObject_IMPL ,而通过上面的实验我们知道NSObject_IMPL内部其实就是Class isa
struct NSObject_IMPL {
Class isa;
};
- 那么我们假设 struct NSObject_IMPL NSObject_IVARS; 等价于 Class isa;可以将上述代码转化为
struct Student_IMPL {
Class *isa;
int _no;
int _age;
};
- 因此此结构体占用多少存储空间,对象就占用多少存储空间。遵循上面计算NSObject对象内存的方式,结构体内的各个成员变量占用内存总和就是结构体占用总的内存大小,isa指针8个字节空间+int类型_no4个字节空间+int类型_age4个字节空间共16个字节空间;
Student *stu = [[Student alloc] init];
stu -> _no = 4;
stu -> _age = 5;
-
那么上述代码实际上在内存中的体现为,创建Student对象首先会分配16个字节,存储3个东西,isa指针8个字节,4个字节的_no ,4个字节的_age
Student对象的存储空间.png - sutdent对象的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这个对象指向的内存确实是一个结构体。
- 上面的方法是根据类型推算出来的内存大小,我们还可以根据代码计算出来,即运行时方法来获取。
NSLog(@"NSObject = %zd",class_getInstanceSize([NSObject class]));
//类对象实际需要内存大小
NSLog(@"Student = %zd", class_getInstanceSize([Student class]));
//系统分配
NSLog(@"Student = %zd", malloc_size((__bridge const void *)stu));
OC对象本身占用内存大小.png
窥探内存结构
实时查看内存数据
- 方式一:通过打断点。
-
Debug Workflow -> viewMemory address中输入stu的地址,类似查看NSObject对象的地址
查看内存地址方式.png
查看结果.png
-
- 从上图中,我们可以发现读取数据从高位数据开始读,查看前16位字节,每四个字节读出的数据为16进制 0x0000004(4字节) 0x0000005(4字节) isa的地址为 00D1081000001119(8字节)
方式二:通过lldb指令xcode自带的调试器
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/4xw // /后面表示如何读取数据 w表示4个字节4个字节读取,x表示以16进制的方式读取数据,4则表示读取4次
- 同时也可以通过lldb修改内存中的值
memory write 0x100400c68 6
将_no的值改为了6
lldb读取内存.png
更复杂的继承关系(继承关系的类的类的对象内存分配情况)
面试题:在64bit环境下,继承关系的子父类占用内存情况如何呢?
/* Person */
@interface Person : NSObject
{
int _age;
}
@end
@implementation Person
@end
/* Student */
@interface Student : Person
{
int _no;
}
@end
@implementation Student
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"%zd %zd",
class_getInstanceSize([Person class]),
class_getInstanceSize([Student class])
);
}
return 0;
}
//打印结果如下:
Interview01-OC对象的本质[2872:67593] stu - 16
Interview01-OC对象的本质[2872:67593] person - 16
-
这道面试题的实质是想问一个Person对象,一个Student对象分别占用多少内存空间?
preson对象和student对象内存结构.png - 我们依次将上面的Student子类跟Person父类转化成C++结构体写出来
struct NSObject_IMPL {
Class isa;//8
};
struct Person_IMPL {
struct NSObject_IMPL NSObject_IVARS; // 8
int _age; // 4
}; // 16 内存对齐:结构体的大小必须是最大成员大小的倍数
struct Student_IMPL {
struct Person_IMPL Person_IVARS; // 16
int _no; // 4
}; // 16
- 我们发现只要是继承自NSObject的对象,那么底层结构体内一定有一个isa指针。
- 那么他们所占的内存空间是多少呢?单纯的将指针和成员变量所占的内存相加即可吗?上述代码实际打印的内容是16 16,也就是说,person对象和student对象所占用的内存空间都为16个字节。
- 其实实际上person对象确实只使用了12个字节。但是因为内存对齐的原因。使person对象也占用16个字节。
系统给对象分配内存时会遵循内存对齐:结构体的大小必须是最大成员大小的倍原则,也就说Person_IMPL结构体中的成员变量(isa跟_age)实际需要12字节空间,但是系统根据原则确分配了16字节,所以结果是16字节。
而** Student_IMPL怎么又成了16字节呢,上面说了系统给Person_IMPL分配了16字节,实际占用12字节,还留有4字节空余,恰好放_no**4字节的变量,这样出来的结果就是系统分配16字节恰好够Student_IMPL对象使用。
所以,综上:
- 我们总结一下系统给对象分配存储空间的原则:编译器在给结构体开辟空间时,首先找到结构体中最宽的基本数据类型,然后寻找内存地址能是该基本数据类型的整倍的位置,作为结构体的首地址。将这个最宽的基本数据类型的大小作为对齐模数。
- 为结构体的一个成员开辟空间之前,编译器首先检查预开辟空间的首地址相对于结构体首地址的偏移是否是本成员的整数倍,若是,则存放本成员,反之,则在本成员和上一个成员之间填充一定的字节,以达到整数倍的要求,也就是将预开辟空间的首地址后移几个字节。
- 我们可以总结内存对齐为两个原则:
- 原则 1. 前面的地址必须是后面的地址正数倍,不是就补齐。
- 原则 2. 整个Struct的地址必须是最大字节的整数倍。
- 通过上述内存对齐的原则我们来看,person对象的第一个地址要存放isa指针需要8个字节,第二个地址要存放_age成员变量需要4个字节,根据原则一,8是4的整数倍,符合原则一,不需要补齐。然后检查原则2,目前person对象共占据12个字节的内存,不是最大字节数8个字节的整数倍,所以需要补齐4个字节,因此person对象就占用16个字节空间。
- 而对于student对象,我们知道sutdent对象中,包含person对象的结构体实现,和一个int类型的_no成员变量,同样isa指针8个字节,_age成员变量4个字节,_no成员变量4个字节,刚好满足原则1和原则2,所以student对象占据的内存空间也是16个字节。
补充:
- 如果此时在Preson类再加一个height的变量,那么内存是多大呢?
// Person
@interface Person : NSObject
{
@public
int _age;
}
@property (nonatomic, assign) int height;
@end
@implementation Person
@end
//Student
@interface Student : Person
{
int _no;
}
@end
@implementation Student
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"stu - %zd", class_getInstanceSize([Student class]));
NSLog(@"person - %zd", class_getInstanceSize([Person class]));
}
return 0;
}
//打印结果如下:
Interview01-OC对象的本质[2872:67593] stu - 24
Interview01-OC对象的本质[2872:67593] person - 16
- 此时,Student类实际占用的内存空间是24,为什么不是16+4呢,因为在Student类本质的内存里面,实质就是isa+age+height+no,这4个,根据原则,是最大内存的倍数,所以是24;
补充:
@interface MJPerson : NSObject
{
int _age;
int _height;
int _no;
}
@end
@implementation MJPerson
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
MJPerson *p = [[MJPerson alloc] init];
NSLog(@"%zd", sizeof(struct MJPerson_IMPL)); // 24
NSLog(@"%zd %zd",
class_getInstanceSize([MJPerson class]), // 24
malloc_size((__bridge const void *)(p))); // 32
}
return 0;
}
- 我们依次将上面的MJPerso父类转化成C++结构体写出来
struct MJPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS; //8
int _age; //4
int _height; //4
int _no; //4
}; // 计算结构体大小,本质应该是20,根据上面讲到的原则,内存对齐和是最大内存的倍数,所以是24
struct NSObject_IMPL {
Class isa;
};
- 但此时malloc_size()函数打印的结果是32,为什么呢?
- 我们通过打断点。
-
Debug Workflow -> viewMemory查看
内存查看.png
-
- 发现真的是32个字节
- 查看源码(objc4),搜索allocWithZone,查看其.m文件的实现方法
id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone)
{
id obj;
#if __OBJC2__
// allocWithZone under __OBJC2__ ignores the zone parameter
(void)zone;
obj = class_createInstance(cls, 0);
#else
if (!zone) {
obj = class_createInstance(cls, 0);
}
else {
obj = class_createInstanceFromZone(cls, 0, zone);
}
#endif
if (slowpath(!obj)) obj = callBadAllocHandler(cls);
return obj;
}
- 里面有一个创建实例的方法
obj = class_createInstance(cls, 0);
- command点击进入
id
class_createInstance(Class cls, size_t extraBytes)
{
return _class_createInstanceFromZone(cls, extraBytes, nil);
}
- command点击进入
id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
if (!cls) return nil;
assert(cls->isRealized());
// Read class's info bits all at once for performance
bool hasCxxCtor = cls->hasCxxCtor();
bool hasCxxDtor = cls->hasCxxDtor();
bool fast = cls->canAllocNonpointer();
size_t size = cls->instanceSize(extraBytes);
if (outAllocatedSize) *outAllocatedSize = size;
id obj;
if (!zone && fast) {
obj = (id)calloc(1, size);
if (!obj) return nil;
obj->initInstanceIsa(cls, hasCxxDtor);
}
else {
if (zone) {
obj = (id)malloc_zone_calloc ((malloc_zone_t *)zone, 1, size);
} else {
obj = (id)calloc(1, size);
}
if (!obj) return nil;
// Use raw pointer isa on the assumption that they might be
// doing something weird with the zone or RR.
obj->initIsa(cls);
}
if (cxxConstruct && hasCxxCtor) {
obj = _objc_constructOrFree(obj, cls);
}
return obj;
}
- 里面有创建的语句calloc()函数
obj = (id)calloc(1, size);
//知道这个函数传入了一个size,这个size是从
// size_t size = cls->instanceSize(extraBytes);得到的;
- command进入instanceSize
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;
}
//extraBytes是额外的字节数
//通过_objc_rootAllocWithZone()函数里面的obj = class_createInstance(cls, 0);可以知道,这个额外的字节数,传入的是0;
- 我们发现这个size是通过alignedInstanceSize() + extraBytes,得到的;
- command进入alignedInstanceSize()
// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() {
return word_align(unalignedInstanceSize());
}
- 在这个地方我们知道,command进入alignedInstanceSize()函数是一个叫class_getInstanceSize()的函数在调用
size_t class_getInstanceSize(Class cls)
{
if (!cls) return 0;
return cls->alignedInstanceSize();
}
- 这样在calloc()函数里面
obj = (id)calloc(1, size);
//相当于
obj = (id)calloc(1, class_getInstanceSize(Class cls));
//所以这个地方的size传入的的确是24
- 此时我们需要查看calloc的底层实现源码(libmalloc源码),搜索malloc.c文件,查看他的.m文件,在里面找到calloc方法
void *
calloc(size_t num_items, size_t size)
{
void *retval;
retval = malloc_zone_calloc(default_zone, num_items, size);
if (retval == NULL) {
errno = ENOMEM;
}
return retval;
}
- command进入malloc_zone_calloc()函数
void *
malloc_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size)
{
void *ptr;
size_t alloc_size;
if (malloc_check_start && (malloc_check_counter++ >= malloc_check_start)) {
internal_check();
}
if (os_mul_overflow(num_items, size, &alloc_size) || alloc_size > MALLOC_ABSOLUTE_MAX_SIZE){
errno = ENOMEM;
return NULL;
}
ptr = zone->calloc(zone, num_items, size);
if (malloc_logger) {
malloc_logger(MALLOC_LOG_TYPE_ALLOCATE | MALLOC_LOG_TYPE_HAS_ZONE | MALLOC_LOG_TYPE_CLEARED, (uintptr_t)zone,
(uintptr_t)(num_items * size), 0, (uintptr_t)ptr, 0);
}
return ptr;
}
-
综述:苹果在分配内存的时候也是有内存对齐的,(相似于结构体内存对齐,但是又不一样)这个时候有一个Buckets sized这个东西,看到它是16的倍数,所以,到这就明白为什么不是24是32了,是16的倍数;就相当于,分配内存的时候是按快分配的,16是一块,32是一块,48是一块。。。。
内存桶.png
总结
- 创建一个实例对象,至少需要多少内存?
- 导入#import <objc/runtime.h>
- class_getInstanceSize([NSObject class]);
- 创建一个实例对象,实际上分配了多少内存?
- 导入#import <malloc/malloc.h>
- malloc_size((__bridge const void *)obj);
- 需要注意的是sizeof是在编译的时候就确定的内存的大小,例如,如果是int就是4,如果是指针就是8。。。
- 不同类型所占内存大小:
2019-03-15 10:51:36.718391+0800 iOSProject[28310:10739021] ********64位环境********
2019-03-15 10:51:36.718741+0800 iOSProject[28310:10739021] bool size:1
2019-03-15 10:51:36.718817+0800 iOSProject[28310:10739021] BOOL size:1
2019-03-15 10:51:36.718856+0800 iOSProject[28310:10739021] char size:1
2019-03-15 10:51:36.718895+0800 iOSProject[28310:10739021] int8_t size:1
2019-03-15 10:51:36.718928+0800 iOSProject[28310:10739021] unsigned char size:1
2019-03-15 10:51:36.718960+0800 iOSProject[28310:10739021] Boolean size:1
2019-03-15 10:51:36.718993+0800 iOSProject[28310:10739021] short size:2
2019-03-15 10:51:36.719025+0800 iOSProject[28310:10739021] int16_t size:2
2019-03-15 10:51:36.719059+0800 iOSProject[28310:10739021] unsigned short size:2
2019-03-15 10:51:36.719092+0800 iOSProject[28310:10739021] unichar size:2
2019-03-15 10:51:36.719125+0800 iOSProject[28310:10739021] int size:4
2019-03-15 10:51:36.719158+0800 iOSProject[28310:10739021] int32_t size:4
2019-03-15 10:51:36.719190+0800 iOSProject[28310:10739021] unsigned int size:4
2019-03-15 10:51:36.719222+0800 iOSProject[28310:10739021] boolean_t size:4
2019-03-15 10:51:36.719297+0800 iOSProject[28310:10739021] long size:8
2019-03-15 10:51:36.719334+0800 iOSProject[28310:10739021] NSInteger size:8
2019-03-15 10:51:36.719367+0800 iOSProject[28310:10739021] long size:8
2019-03-15 10:51:36.719401+0800 iOSProject[28310:10739021] unsigned long size:8
2019-03-15 10:51:36.719435+0800 iOSProject[28310:10739021] NSUInteger size:8
2019-03-15 10:51:36.719471+0800 iOSProject[28310:10739021] long long size:8
2019-03-15 10:51:36.719503+0800 iOSProject[28310:10739021] double size:8
OC对象的分类
面试题:OC对象都有哪些呢?
- 示例代码
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
/* Person */
@interface Person : NSObject <NSCopying>
{
@public
int _age;
}
@property (nonatomic, assign) int height;
- (void)personMethod;
+ (void)personClassMethod;
@end
@implementation Person
- (void)personMethod {}
+ (void)personClassMethod {}
@end
/* Student */
@interface Student : Person <NSCoding>
{
@public
int _no;
}
@property (nonatomic, assign) int score;
- (void)studentMethod;
+ (void)studentClassMethod;
@end
@implementation Student
- (void)studentMethod {}
+ (void)studentClassMethod {}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *object1 = [[NSObject alloc] init];
NSObject *object2 = [[NSObject alloc] init];
Student *stu = [[Student alloc] init];
[Student load];
Person *p1 = [[Person alloc] init];
p1->_age = 10;
[p1 personMethod];
[Person personClassMethod];
Person *p2 = [[Person alloc] init];
p2->_age = 20;
}
return 0;
}
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对象(实例对象)
- object1和object2都是NSObject的instace对象(实例对象),但他们是不同的两个对象,并且分别占据着两块不同的内存。
- instance对象在内存中存储的信息包括
- isa指针
-
其他成员变量
Preson类.png
-
如图,我们创建了一个Preson类,并且创建了两个实例变量,那么实例对象的内存如图所示:
instance对象内存.png
衍生问题:在上图实例对象中根本没有看到方法,那么实例对象的方法的代码放在什么地方呢?那么类的方法的信息,协议的信息,属性的信息都存放在什么地方呢?
class对象(类对象)
- 我们通过class方法或runtime方法得到一个class对象。class对象也就是类对象
Class objectClass1 = [object1 class];
Class objectClass2 = [object2 class];
Class objectClass3 = [NSObject class];
// runtime
Class objectClass4 = object_getClass(object1);
Class objectClass5 = object_getClass(object2);
NSLog(@"%p %p %p %p %p", objectClass1, objectClass2, objectClass3, objectClass4, objectClass5);
//内存打印地址如下:
objectClass1 = 0x7fff97528118 objectClass2 = 0x7fff97528118 objectClass3 = 0x7fff97528118 objectClass4 = 0x7fff97528118 objectClass5 = 0x7fff97528118
// 而调用类对象的class方法时得到还是类对象,无论调用多少次都是类对象
Class cls = [[NSObject class] class];
Class objectClass6 = [NSObject class];
NSLog(@"objectClass = %p cls = %p", objectClass6, cls); // 后面两个地址相同,说明多次调用class得到的还是类对象
//打印结果如下:
objectClass = 0x7fff97528118 cls = 0x7fff97528118
- 每一个类在内存中有且只有一个class对象。可以通过打印内存地址证明
class对象在内存中存储的信息主要包括
- isa指针
- superclass指针
- 类的属性信息(@property),类的成员变量信息(ivar)
-
类的对象方法信息(instance method),类的协议信息(protocol)
class对象在内存中存储的信息图例.png - 成员变量的值时存储在实例对象中的,因为只有当我们创建实例对象的时候才为成员变赋值。但是成员变量叫什么名字,是什么类型,只需要有一份就可以了。所以存储在class对象中。
- 类方法放在那里?
meta-class对象(元类对象)
- 只能是通过class对象获取到meta-class对象,通过下面的方法获取到。
//runtime中传入类对象此时得到的就是元类对象
Class objectMetaClass = object_getClass([NSObject class]);
NSLog(@"objectMetaClass = %p",objectMetaClass);
//内存打印地址如下:
objectMetaClass = 0x7fff975280f0
// 而调用类对象的class方法时得到还是类对象,无论调用多少次都是类对象
Class cls = [[NSObject class] class];
Class objectClass3 = [NSObject class];
class_isMetaClass(objectMetaClass) // 判断该对象是否为元类对象
NSLog(@"%p %p %p", objectMetaClass, objectClass3, cls); // 后面两个地址相同,说明多次调用class得到的还是类对象
//检查是否为元类对象
BOOL ismetaclass = class_isMetaClass(objectMetaClass);// 判断该对象是否为元类对象
NSLog(@"objectMetaClass 是否是元类对象 - %ld",ismetaclass);
//打印结果如下:
objectMetaClass 是否是元类对象 - 1
-
每一个类的meta-class对象在内存中有且只有一个,class对象跟meta-class对象结构一样,都是*struct objc_class Class,但是用途不一样。
-
meta-class对象在内存中存储的信息包括:
- isa指针
- superclass指针
-
类的类方法信息(class-method)
元类meta-class对象在内存存储的信息图例.png
-
meta-class对象和class对象的内存结构是一样的,所以meta-class中也有类的属性信息,类的对象方法信息等成员变量,但是其中的值可能是空的。
补充
objc_getClass()和object_getClass()的区别;那么[NSObject 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);
}
//分析:接收的是类名;其实质就是字符串;
- 里面有一个look_up_class()函数,返回的是这个函数;
Class
look_up_class(const char *name,
bool includeUnconnected __attribute__((unused)),
bool includeClassHandler __attribute__((unused)))
{
if (!name) return nil;
Class result;
bool unrealized;
{
rwlock_reader_t lock(runtimeLock);
result = getClass(name);
unrealized = result && !result->isRealized();
}
if (unrealized) {
rwlock_writer_t lock(runtimeLock);
realizeClass(result);
}
return result;
}
- 我们分析发现return result;看result是什么,发现是result = getClass(name);那么getClass()函数内部实现是
static Class getClass(const char *name)
{
runtimeLock.assertLocked();
// Try name as-is
Class result = getClass_impl(name);
if (result) return result;
// Try Swift-mangled equivalent of the given name.
if (char *swName = copySwiftV1MangledName(name)) {
result = getClass_impl(swName);
free(swName);
return result;
}
return nil;
}
- 里面有语句Class result = getClass_impl(name);,command进入getClass_impl()函数;
static Class getClass_impl(const char *name)
{
runtimeLock.assertLocked();
// allocated in _read_images
assert(gdb_objc_realized_classes);
// Try runtime-allocated table
Class result = (Class)NXMapGet(gdb_objc_realized_classes, name);
if (result) return result;
// Try table from dyld shared cache
return getPreoptimizedClass(name);
}
-
在这段代码我们可以发现有一个Class result = (Class)NXMapGet(gdb_objc_realized_classes, name);语句,可以发现是根据一个字符串找到一个class的结构,所以,返回的其实就是类对象,所以,到这就可以分析出,objc_getClass()这个函数就是将一个类名传进入,返回一个类对象;
-
object_getClass()函数
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
//这句的意思是,如果传进来的是,instance对象,就返回class对象;如果传进来的是,class对象,就返回meta-class对象;如果传进来是meta-class对象,就返回NSObject(基类)的meta-class对象;
//getIsa()其实就是返回的是isa;
else return Nil;
}
//分析:接收的是类对象;
- [NSObject class]中的class
- -(Class) class
- +(Class) class
- 返回的都是类对象
- 总结:
- objc_getClass()这个函数就是将一个类名传进入,返回一个类对象。
- object_getClass()函数这个函数就是将一个类对象传进入,返回的是元类对象,如果传进来的是,instance对象,就返回class对象;如果传进来的是,class对象,就返回meta-class对象;如果传进来是meta-class对象,就返回NSObject(基类)的meta-class对象。
- [NSObject class]中的class有两种情况,-(Class)class和+ (Class) class,都返回的就是类对象;
面试题:对象的isa指针指向哪里。
- 首先说明
[Preson personClassMethod];
//实质就是给这个Preson类发送一个消息
//objc_msgSend([Preson class] @selector(personClassMethod))
- 当对象调用实例方法的时候,我们上面讲到,实例方法信息是存储在class类对象中的,那么要想找到实例方法,就必须找到class类对象,那么此时isa的作用就来了。
- 代码如下
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
// 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
int main(int argc, const char * argv[]) {
@autoreleasepool {
MJStudent *student = [[MJStudent alloc] init];
[student test];
[student personInstanceMethod];
[student init];
[MJStudent studentClassMethod];
[MJStudent personClassMethod];
[MJStudent load];
}
return 0;
}
- 根据上例代码,知道MJStudent继承于MJPerson,MJPerson继承于NSObject
MJStudent *student = [[MJStudent alloc]init];
//方法1:调用实例方法
[student studentInstanceMethod];
-
方法1:student实例对象调用了实例方法,我们在前面讲到过,实例方法信息存储在class对象中,这时候instace对象中存储的isa指针起到作用了,instace对象中的isa指针指向class对象,我们通过isa指针找到class对象,进而找到实例方法列表,调用对应方法。
对象方法调用轨迹.png
//方法2:调用类方法
[MJStudent studentClassMethod];
- 方法2:MJStudent类对象调用了类方法,我们在前面讲到过,类方法信息存储在meta-class对象中,这时候class对象中存储的isa指针起到作用了,class对象中的isa指针指向meta-class对象,我们通过isa指针找到meta-class对象,进而找到类方法列表,调用对应方法。
- 如上图所示;
总结
- 当对象调用实例方法的时候,实例方法信息是存储在class类对象中的,instance的isa指向class,当调用对象方法时,通过instance的isa找到class,最后找到对象方法的实现进行调用。
- 当类对象调用类方法的时候,同上,类方法是存储在meta-class元类对象中的。那么要找到类方法,就需要找到meta-class元类对象,而class类对象的isa指针就指向元类对象,class的isa指向meta-class,当调用类方法时,通过class的isa找到meta-class,最后找到类方法的实现进行调用
-
具体如下图:
isa指针指向图例.png
对象调用父类对象方法图例.png
OC对象的superclass指针指向位置
当对象调用其父类对象方法的时候,又是怎么找到父类对象方法的呢?
对象调用父类对象方法图例.png
//方法1:调用父类的实例方法
[student personInstanceMethod];
//方法2:调用父类的类方法
[MJStudent personClassMethod];
- 方法1:当给student实例对象发送personInstanceMethod消息时,student实例对象会通过isa指针找到对应MJStudent类对象,因为类对象中存储中对象方法信息,先从MJStudent类对象的实例方法信息中查找对应的方法,如果找到进行相应,没找到则继续向父类查找,那么子类怎么才能找到父类呢,这时候需要用到superclass指针了,通过superclass指针找到MJPerson的类对象,继续从类对象那个的实例方法中查找,如果找到进行相应,没找到则继续通过superclass查找基类NSObject类对象方法列表,如果还没找到,返回nil,就是常见的报错信息,找不到此方法。
-
方法2:跟方法1类似,当给MJStudent类对象发送personClassMethod消息时,MJStudent类对象会通过isa指针找到对应MJStudent元类对象,因为元类对象中存储中类方法信息,先从MJStudent元类对象的类信息中查找对应的方法,如果找到进行相应,没找到则继续向父类查找,那么子类怎么才能找到父类呢,这时候需要用到superclass指针了,通过superclass指针找到MJPerson的元类对象,继续从元类对象那个的类方法中查找,如果找到进行相应,没找到则继续通过superclass查找基类NSObject元类对象方法列表,如果还没找到,这个时候跟方法1的查找不太一样了,如果NSObject的元类对象的类方法中找到,就从NSObject的类方法的实例方法中去查找,还没有找到,则返回nil,就是常见的报错信息,找不到此方法。
isa-superclass.png
总结
- instance的isa指向class
- class的isa指向meta-class
- meta-class的isa指向基类的meta-class,基类的isa指向自己
- class的superclass指向父类的class,如果没有父类,superclass指针为nil
- meta-class的superclass指向父类的meta-class,基类的meta-class的superclass指向基类的class
- instance调用对象方法的轨迹,isa找到class,方法不存在,就通过superclass找父类
- class调用类方法的轨迹,isa找meta-class,方法不存在,就通过superclass找父类
如何证明isa指针的指向真的如上面所说?代码求证isa指针指向是否正确;
NSObject *object = [[NSObject alloc] init];//instance对象
Class objectClass = [NSObject class];//类对象
Class objectMetaClass = object_getClass([NSObject class]);//元类对象
NSLog(@"object - %p objectClass - %p objectMetaClass - %p", object, objectClass, objectMetaClass);
//打印结果如下:
object - 0x10051e0b0 //instance对象内存地址
objectClass - 0x7fff9abb6118 //类对象内存地址
objectMetaClass - 0x7fff9abb60f0 //元类对象内存地址
- 代码如下
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
// 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
struct mj_objc_class {
Class isa;
Class superclass;
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
MJPerson *person = [[MJPerson alloc] init];
Class personClass = [MJPerson class];
NSLog(@"%p %p %p", person, personClass, personMetaClass);
}
return 0;
}
-
在NSLog()函数打断点,如图,我们可以看出来
Preson类的结构.png -
如下,通过控制台打印相应对象的isa指针
地址打印.png -
此时我们发现这两个地址是不一样的,为什么呢?
-
这是因为从64bit开始,isa需要进行一次位运算,才能计算出真实地址。而位运算的值我们可以通过下载objc源代码找到。
ISA_MASK.png -
我们通过位运算进行验证。
计算之后的打印结果.png -
我们发现,person-isa指针地址0x001d8001000014c9经过同0x00007ffffffffff8位运算,得出personClass的地址0x00000001000014c8
-
果然跟程序打印出来的结果一样,这足以证明上面总结的isa指针指向的正确性。
-
但我们再次尝试验证类方法的isa指针指向的元类对象的内存地址跟程序自然打印的是否一样的时候,发现了如下问题
类对象isa指针内存地址.png -
同时也发现左边objectClass对象中并没有isa指针。
presonClass.png -
我们来到Class内部看一下
typedef struct objc_class *Class;
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
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;
/* Use `Class` instead of `struct objc_class *` */
- 我们只看第一个对象是一个isa指针,为了拿到isa指针的地址,我们自己创建一个同样的结构体并通过强制转化拿到isa指针。
struct xx_cc_objc_class{
Class isa;
};
Class objectClass = [NSObject class];
struct xx_cc_objc_class *objectClass2 = (__bridge struct xx_cc_objc_class *)(objectClass);
-
此时我们重新验证一下
类对象位运算结果.png
补充
- 对superclass进行分析,看是不是和isa是一样的
- 代码如下
Class personClass = [MJPerson class];
Class studentClass = [MJStudent class];
NSLog(@"1111");
- 在命令号输入p personClass->superclass
-
我们发现和isa是一样的,找不到;
打印superclass地址.png - 我们在mj_objc_class结构体里面加一个变量superclass
struct mj_objc_class {
Class isa;
Class superclass;
};
- 和isa处理一样
struct mj_objc_class *personClass = (__bridge struct mj_objc_class *)([MJPerson class]);
struct mj_objc_class *studentClass = (__bridge struct mj_objc_class *)([MJStudent class]);
NSLog(@"1111");
验证superclass.png
本文面试题总结
- 一个NSObject对象占用多少内存?
- 答:一个指针变量所占用的大小(64bit占8个字节,32bit占4个字节)
- 对象的isa指针指向哪里?
- instance对象(实例对象)
- class对象(类对象)
- meta-class对象(元类对象)
- OC对象的isa指针指向哪里呢?
- instance对象的isa指针指向class对象,class对象的isa指针指向meta-class对象,meta-class对象的isa指针指向基类的meta-class对象,基类自己的isa指针也指向自己。
- OC的类信息存放在哪里?
- nstance对象(实例方法):存放isa指针,成员变量的具体数据
- class对象(类对象):存放isa指针,superclass指针,类的成员变量(ivar),类的属性信息(property),类的协议信息(protocol),类的方法列表(instance method list)
- meta-class对象(元类对象:存放isa指针,superclass指针,类的方法列表(class method list)