iOS中OC对象的观察

标签: 编程学习 iOS学习
查看OC对象的底层

创建一个OC对象

@interface  DCPerson : NSObject {
    int _age;
    int _height;
}
@end

我们可以用以下命令去把该类转换为C\C++代码

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc (OC源⽂件) -o (输出的CPP⽂件)
// 如果需要链接其他框架,使⽤-framework参数。⽐如-framework UIKit

经查看得到以下结构

typedef struct objc_class * Class;

// 只是一个OC对象的话就是这个样子,Class其实就是一个指针,对象底层实现就是这个样子
struct NSObject_IMPL {
    Class isa;
}

// 添加上两个变量之后
struct Person_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    int _age;
    int _height;
}

由此可见OC的对象、类主要是基于C\C++的结构体实现的。NSObject对象的底层就是一个isa指针,指针在64位架构中占8个字节。也就是说一个NSObjec对象所占用的内存是8个字节。类是包含很多方法的,但是这些方法所占用的存储空间并不在NSObject对象中,所以一个对象占8个字节,而添加了两个变量的Person的对象就占 16 个字节。

想要获取对象占用内存的大小,可以通过更便捷的运行时方法来获取

class_getInstanceSize([NSObject class]); //创建⼀个实例对象⾄少需要的内存
malloc_size((__bridge const void *)obj); //创建⼀个实例对象实际上分配的内存
NSLog(@"%zd,%zd", class_getInstanceSize([NSObject class]) ,class_getInstanceSize([Person class])); // 打印信息 8和16

更复杂的继承关系

/* Person */
@interface Person : NSObject
{
    int _age;
}
@end

@implementation Person
@end

/* Student */
@interface Student : Person
{
    int _no;
}
@end

NSLog(@"%zd  %zd",
              class_getInstanceSize([Person class]),
              class_getInstanceSize([Student class])
              );

根据分析发现,类对象实质上是以结构体的形式存储在内存中如下

我们发现只要是继承自NSObject的对象,那么底层结构体内一定有一个isa指针。上述代码实际打印的内容是16 16,也就是说,person对象和student对象所占用的内存空间都为16个字节。实际上 person 对象只使用了12个字节,但是因为内存对齐的原因,使 person 对象也占用16个字节。

内存对齐:前面的地址必须是后面的地址整数倍,不是就补齐; 整个Struct的地址必须是最大字节的整数倍。

而看过底层代码得知,其实就算没有变量,单单只有一个isa的情况下,系统依然会强制分配16个字节的空间。

OC对象的分类

instance 实例对象 实例对象就是通过类alloc出来的对象,实例对象在内存中存储的信息包括 isa指针和其他成员变量

class 类对象 每个类在内存中有且只有一个类对象。类对象中存储的信息主要包括 isa指针、superClass指针、类的属性信息(property)、类的对象方法信息(instance method)、类的协议信息(protocol)、类的成员变量信息(ivar)

获取类对象

Class objectClass = [NSObject class];
Class objectClass = object_getClass([NSObject class]);
NSObject *objc = [[NSObject alloc] init];
Class objectClass = [objc class];
Class objectClass = object_getClass(objc);

meta-class 元类对象 每个类在内存中有且只有一个元类对象。与类对象的结构是一样的,但是用途不一样,元类对象中存储的信息主要包括 isa指针、superClass 指针、类方法信息(class method)

获取元类对象

Class metaClass = object_getClass([NSObject class]);
Class objectClass = [NSObject class];
Class metaClass = object_getClass(objectClass);

class_isMetaClass(metaClass); // 判断是否是元类

注意:[[NSObject class] class]; 无论调用几次class返回的都是类对象。

三种对象的关系

网上到处都能看到这张图,确实能够清晰的表现出三种对象的关系

isa指针指向

实例对象的isa指向类对象,当调用对象方法的时候,通过实例的isa找到类对象,最后找到对象方法的实现进行调用。

类对象的isa指向元类对象,当调用类方法的时候,通过类对象的isa找到元类对象,最后找到类方法的实现进行调用。

元类对象的isa指向基类的元类对象。

superClass指针

类对象的superClass指针指向的是父类的类对象,如果没有父类,superClass指针为nil。

元类对象的superClass指针指向的是父类的元类对象。

基类的元类对象的superClass指针指向基类的类对象。(比如调用一个未实现的类方法,如果都没实现,只有NSObject实现了一个同名的实例方法,那就会调用这个方法)

ISA指针的结构

在arm64架构之前,isa就是⼀个普通的指针,存储着Class、Meta-Class对象的内存地址,从arm64架构开始,对isa进⾏了优化,变成了⼀个共⽤体(union)结构,使⽤位域来存储更多的信息,isa_t 的结构如下

nonpointer 值为0代表普通的指针,存储着Class、Meta-Class对象的内存地址,值为1代表优化过,使⽤位域存储更多的信息

has_assoc 是否有设置过关联对象,如果没有,释放时会更快

has_cxx_dtor 是否有C++的析构函数(.cxx_destruct),如果没有,释放时会更快

shiftcls 存储着Class、Meta-Class对象的内存地址信息

magic ⽤于在调试时分辨对象是否未完成初始化

weakly_referenced 是否有被弱引⽤指向过,如果没有,释放时会更快

deallocating 对象是否正在释放

extra_rc ⾥⾯存储的值是引⽤计数器减1

has_sidetable_rc 引⽤计数器是否过⼤⽆法存储在isa中,如果值为1,那说明引用计数器过大,那么引⽤计数会存储在⼀个叫SideTable的类的属性中

类对象和元类对象的结构

isa是一个Class类型的指针,64bit之前直接指向对象地址,64bit之后查找的话就需要与ISA_MASK进行一次位运算才能计算出各个对象的真实地址,不论class、meta-class对象的本质结构都是struct objc_class,它的结构如下

cache_t cache 用于快速查找方法执行函数,结构是可增量扩展的哈希表结构,是局部性原理的最佳应用(调用次数高的方法放进缓存提高效率),里面存储的是 bucket_t,结构就是{key:IMP}

class_data_bits_t bits 是用来存储具体的类信息的,是对 class_rw_t 的封装,里面包含了方法列表、属性列表、协议列表等可读可写的结构,但是成员变量是只读的,里面包含了实例的大小,所以一个类不能添加成员变量。

方法列表、属性列表、协议列表都是二维数组的结构,是可读可写的,所以分类添加的方法、属性、协议最后都添加到了这里

class_ro_t ⾥⾯的 baseMethodList、baseProtocols、ivars、baseProperties 是⼀维数组,是只读的,包含了类的初始内容

method_t结构

struct method_t {
    SEL name; //函数名称
    const char types; //函数编码(返回值类型,参数类型)
    IMP imp; //指向函数的指针(函数地址)
}

SEL代表⽅法\函数名,⼀般叫做选择器,底层结构跟char *类似 可以通过@selector()sel_registerName()获得,可以通过sel_getName()NSStringFromSelector()转成字符串,不同类中相同名字的⽅法,所对应的⽅法选择器是相同的。

typedef struct objc_selector *SEL;

IMP代表函数的具体实现

typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...);

types包含了函数返回值、参数编码的字符串,iOS中提供了⼀个叫做 @encode的指令,可以将具体的类型表示成字符串编码