iOS底层-OC对象

OC对象的本质

  1. 简介
    1. 我们平时编写的Objective-C代码,底层实现其实都是C\C++代码
      1. OC->C/C++->汇编语言->机器语言
    2. 所以Objective-C的面向对象都是基于C\C++的数据结构实现的
    3. 思考:Objective-C的对象、类主要是基于C\C++的什么数据结构实现的?
      1. 结构体
  2. 如何将OC代码转化为C++代码?
    1. 打开终端cd 到想要转化的文件目录,输入一下命令
    2. xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc OC源文件 -o 输出的CPP文件
      1. xcrun:xcode run
      2. -sdk  iphoneos: sdk 运行在iOS平台
      3. clang:Xcode内置的编译器
      4. -arch  arm64 : 编译后支持的架构
      5. -rewrite-objc :重写OC代码
      6. -o: 写入到哪个文件
    3. 如果需要链接其他框架,使用-framework参数。比如-framework UIKit

NSObject对象在内存中的分布

  1. main.m实现如下代码

     int main(int argc, const char * argv[]) {
         @autoreleasepool {
             NSObject *obj = [[NSObject alloc] init];
         }
         return 0;
     }
    
    1. command 右击进入NSObject类,定义如下:

        //command右击进入NSObject类
        @interface NSObject {
        Class isa ;
        }
        @end
      
  2. 将上面的main.m通过命令转化成cpp,然后搜索NSObject可以搜索到NSObject的类定义如下:

     struct NSObject_IMPL {
         Class isa;
     };
    
  3. 说明以下问题:
    1. 说明在C/C++中NSObject类就是一个结构体,含有一个成员变量isa,那么Class呢?
    2. command右击进入Class类:

       typedef struct objc_class *Class;
      
      1. 发现Class就是一个结构体指针,在Arm64位中(32位中,即armv7,占4个字节),一个指针占8个字节
      2. 因此,推断NSObject对象(根据C++对象内存分析得出)占据8个字节
        1. C++对象内存分析: C++中空对象(没有成员变量)占据一个字节,非空对象占据的字节数等于成员变量占据字节数之和
         NSObject *obj = [[NSObject alloc] init];
         这句话那么就是分配一个8个字节的内存,然后把地址赋值给obj变量
         而且该地址也是isa的地址
        
  4. 用一些计算内存的方法验证一下上面结论是否正确:

     #import <Foundation/Foundation.h>
     #import <objc/runtime.h>
     #import <malloc/malloc.h>
     int main(int argc, const char * argv[]) {
         @autoreleasepool {
             NSObject *obj = [[NSObject alloc] init];
                
             // 证明依据:根据C++的结论,只要拿到成员变量的大小,就等于对象占据的内存大小
             //1. 获得NSObject实例对象的成员变量所占用的大小结果为: 8
             NSLog(@"%zd", class_getInstanceSize([NSObject class]));
            
             //2. 获得obj指针所指向内存的大小结果为:16
             NSLog(@"%zd", malloc_size((__bridge const void *)obj));
         }
         return 0;
     }
    
    1. 发现C++的结论不适应与OC对象,一个OC对象的占据的内存大小并不等于成员变量占据的大小
    2. 查找苹果官方源码,地址:https://opensource.apple.com/tarballs
      1. 在里面搜索objc,找到obc4进入下载最新的即可
      2. 打开源码项目,搜索class_getInstanceSize,找到实现,如下:

         size_t class_getInstanceSize(Class cls)
         {
             if (!cls) return 0;
             return cls->alignedInstanceSize();
         }
        
        1. 由此可见class_getInstanceSize方法仅仅是返回了一个类成员变量实例大小,并不是整个类的内存分配大小
      3. 打开源码项目,搜索allocWithZone(alloc 实际调用的方法),找到实现如下:

         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;
              }
        
        1. 进入:class_createInstance

           id class_createInstance(Class cls, size_t extraBytes){
               return _class_createInstanceFromZone(cls, extraBytes, nil);
           }
          
        2. 进入_class_createInstanceFromZone

          1. 该方法内部调用了:obj = (id)calloc(1, size);
          2. size获取: size_t size = cls->instanceSize(extraBytes);
          3. 进入instanceSize内部实现:

             size_t instanceSize(size_t extraBytes) {
                 size_t size = alignedInstanceSize() + extraBytes;
                 //CF框架规定所有的对象至少占据是16个字节
                 // CF requires all objects be at least 16 bytes.
                 if (size < 16) size = 16;
                 return size;
             }
            
            1. CF框架规定所有的对象至少占据是16个字节
  5. 结论:一个NSOject对象的内存大小是16个字节,但是实际上只用了8个字节
    1. 通过Xcode查看内存证明,NSObject对象只使用了8个字节
      1. 在main函数的NSLog打一个断点
      2. 在控制台p obj 获取obj对象的地址为0x100517610
      3. Debug -> Debug Workfllow -> View Memory (Shift + Command + M)
      4. 在Address框中输入0x100517610,搜索,得到结果如下

         41 F1 FC A6 FF FF 1D 00 00 00 00 00 00 00 00 00
        
        1. 可以看出,后8个字节全部为0
      5. 也可以直接通过LLDB命令获取某一段内存的值

         memory read 0x100517610
        
  6. 常见的LLDB指令
    1. print、p:打印
    2. po:打印对象
    3. 读取内存
      1. memory read/数量格式字节数  内存地址
      2. memory read也可以简写成x :x/数量格式字节数  内存地址
      3. 例如: x/3xw  0x10010
        1. 打印3串,每一串是16进制、每一串占据4个字节
      4. 格式
        1. x是16进制,f是浮点,d是10进制
      5. 字节大小
        1. b:byte 1字节,h:half word 2字节
        2. w:word 4字节,g:giant word 8字节
    4. 修改内存中的值
      1. memory  write  内存地址  数值
      2. 例如:memory  write  0x0000010  10

OC对象在内存中的分布

  1. main函数代码:

     @interface Person : NSObject
     {
     @public
         int _age;
     }
     @end
        
     @implementation Person
     @end
        
     int main(int argc, const char * argv[]) {
         @autoreleasepool {
             Person *per = [[Person alloc] init];
             per->_age = 5;
                
             // 获得Person实例对象的成员变量所占用的大小 --- 16
             NSLog(@"===%zd", class_getInstanceSize([Person class]));
                
             // 获得per指针所指向内存的大小 ---  16
             NSLog(@"%zd", malloc_size((__bridge const void *)per));
         }
         return 0;
     }
    
  2. 编译成CPP,然后搜索Person_IMPL,如下:

     //Person内部实现
     struct Person_IMPL {
         //继承:NSObject的类
         struct NSObject_IMPL NSObject_IVARS;
         int _age;
     };
    
    1. NSObject_IMPL从上面我们知道,他是一个拥有一个成员变量isa的结构体,因此等价于下面

       struct Person_IMPL {
           //继承:NSObject的类
           Class isa;
           int _age;
       };
      
  3. 分析;
    1. 从上面的打印可以看出,Person对象占据16个字节
    2. 通过内存查看如下:

       99 12 00 00 01 80 1D 00 05 00 00 00 00 00 00 00
      
      1. 因此可以看出,内存分配16个字节,前8个字节放isa
      2. 接下来4个字节放_age
  4. 那如果在创建一个Student继承自Person呢? (Student新增一个_no成员变量)
    1. 经过同样的分析,Student内存也是占据16个字节
    2. 前8个字节存放isa,接下来4个字节存放_age,最后4个存放_no

    图1

内存分配对齐

  1. main.m中代码

     @interface Student : NSObject
     {
     @public
         int _no;
         int _age;
         int _height;
     }
     @end
     @implementation Student
     @end
        
     int main(int argc, const char * argv[]) {
         @autoreleasepool {
            
             Student *stu = [[Student alloc] init];
             stu->_no = 4;
             stu->_age = 5;
                            
             // 获得Student实例对象的成员变量所占用的大小 --- 24
             NSLog(@"===%zd", class_getInstanceSize([Student class]));
                
             // 获得stu指针所指向内存的大小 ---  32
             NSLog(@"%zd", malloc_size((__bridge const void *)stu));
             //分配4个字节的内存
             void *p = malloc(4);
             //获得分配内存的大小--- 16  iOS平台内存分配对齐
             NSLog(@"%zd", malloc_size(p))
         }
         return 0;
     }
    
  2. 分析:

    1. 从前面的理论分析,Student有3个成员变量,加上一个父类的isa变量
    2. 按照结构体的内存对齐(结构体分配内存:是占据内存最大的那个成员变量的整数倍)那么,这个Student对象的内存应该是24,但是为何malloc_size打印的事32呢?
    3. 内存分配对齐
      1. iOS平台的对象分配内存时是16的整数倍。
      2. 按照16个字节对齐。

相关面试题

  1. 一个NSObject对象占用多少内存?
    1. 系统分配了16个字节给NSObject对象(通过malloc_size函数获得)
    2. 但NSObject对象内部只使用了8个字节的空间(64bit环境下,可以通过class_getInstanceSize函数获得)

OC对象的种类

  1. Objective-C中的对象,简称OC对象,主要可以分为3种
    • instance对象(实例对象)
    • class对象(类对象)
    • meta-class对象(元类对象)

instance对象

  1. instance对象就是通过类alloc出来的对象,每次调用alloc都会产生新的instance对象
  2. 举例:

     NSObject *object1 = [[NSObject alloc] init];
     NSObject *object2 = [[NSObject alloc] init];
    
    1. object1、object2是NSObject的instance对象(实例对象)
    2. 它们是不同的两个对象,分别占据着两块不同的内存
  3. instance对象在内存中存储的信息包括
    1. isa指针
    2. 其他成员变量

    图1

class 对象

  1. 举例:

     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);
     Class objectClass5 = object_getClass(object2);
     //一个类的类对象在内存中只有一份,打印地址都一样
     NSLog(@"%p %p %p %p %p ",objectClass1,objectClass2,objectClass3,objectClass4,objectClass5);
    
    1. objectClass1 ~ objectClass5都是NSObject的class对象(类对象)
    2. 它们是同一个对象。每个类在内存中有且只有一个class对象
  2. 实例对象在内存中存储的是对应实例的成员变量,那么类对象呢?

    1. 我们可以分析,类对象是存储实例对象都共有的东西
    2. class对象在内存中存储的信息主要包括
      1. isa指针
      2. superclass指针
      3. 类的属性信息(@property)、类的对象方法信息(instance method)
      4. 类的协议信息(protocol)、类的成员变量信息(ivar)

mata-class 对象

  1. 举例

     Class objectMetaClass = object_getClass([NSObject class]);
    
    1. objectMetaClass是NSObject的meta-class对象(元类对象)
  2. 每个类在内存中有且只有一个meta-class对象
  3. meta-class对象和class对象的内存结构是一样的,但是用途不一样,在内存中存储的信息主要包括
    1. isa指针
    2. superclass指针
    3. 类的类方法信息(class method)
  4. 注意:
    1. 以下代码获取的objectClass是class对象,并不是meta-class对象

        Class objectClass = [[NSObject class]class];
        //查看Class是否为meta-class
       BOOL result = class_isMetaClass([NSObject class]);
      
    2. object_getClass与objc_getClass函数的区别:

       Class object_getClass(id obj);
           1. 如果参数为实例对象,返回的是类对象
           2. 如果参数为类对象,返回的是元类对象
           3. 如果参数为元类对象,返回的是根类对象
           4. 本质就是返回参数对象的isa指针
               
       Class objc_getClass(const char * _Nonnull name);
           1. 根据一个字符串返回一个类对象
      
Table of Contents