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 查看源码

//首先我们创建一个对象
 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: 打印对象
  • 读取内存:
    1. memory read/数量格式字节数 内存地址
    2. x/数量格式字节数 内存地址
    3. 格式:
      • x:代表16机制,f是浮点数,d是十进制
      • 字节大小:b >byte1字节;h >half word2字节;w >word4字节;g >giant word 8字节
    4. 如: x/3xw 0x10010
    5. 修改内存中的值
      • 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都是小端模式-> 是从高地址开始读取

  • 声明一个属性做了什么事情?

    1. 生成一个_xxx的成员变量
    2. 生成了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对象在内存中储存的信息包括:
    1. isa: 所有的实例对象都会有这个成员变量
    2. 其他成员变量
    3. 几乎所有的类都继承自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对象在内存中储存的信息主要包括:
    1. isa 指针
    2. supperclass指针
    3. 类的属性信息(@propertory)、类的对象方法信息(insatnce method)
    4. 类的协议信息(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类型),但是用途不一样,在内存中存储的信息主要包括
    1. isa 指针
    2. superclass指针
    3. 类的类方法信息(class method-> + 号方法)

2.4 isa指针

在这里插入图片描述

2.4.1 instance的isa指针

  • insatnceisa指针指想class对象(类对象)
    • 当调用对象方法时,通过instance的isa找到class,最后找到对象方法的实现进行调用
  • classisa指向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的地址值一样,也就是说classisa指针指向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调用对象方法的轨迹
    1. isa找到class对象,方法不存在,就通过superclass找父类的class对象,一直找到到基类,如果最后没有找到就会报错
  • class调用类方法的轨迹
    1. 通过isa找到meta-class对象,方法不存在,就通过superclass找父类的meta-class对象,一直找到基类,如果基类的meta-class没有这个方法,基类的meta-class是superclass指针是指向基类的class对象的,所以回去基类的class中找,如果还找不到就报错,找到就调用
    2. 示例代码:
@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调用方法的轨迹

  • 总结
    1. 方法调用流程: isa -> superclass -> suerpclass -> superclass -> … superclass == nil
    2. 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 答疑

  1. objc_getClass(<#const char * _Nonnull name#>)object_getClass(id _Nullable obj)-(Class)class、+(Class)class的区别
    1. objc_getClass(const char * _Nonnull name)方法将一个类名传进去,返回一个对应class对象(类对象),如果不存在就返回nil
    2. object_getClass(id obj)传入的obj可能是instance对象、class对象、meta-class对象
      • 如果传递进来是instance对象,返回是class对象
      • 如果是class对象,返回meta-class对象
      • 如果是meta-class对象,返回NSObject(基类)的meta-class对象
    3. -(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;
}
  1. class对象和meta-class对象在什么时候分配内存? 什么时候释放?
    • 程序启动,执行main函数后,就开始加载类的信息放到内存中,不管你用不用,都会加载到内存中
    • class对象和meta-class对象一直都在内存中 ,程序退出的时候一起释放(因为class和meta-class在内存中只有一份)。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值