美文网首页
iOS OC对象的本质

iOS OC对象的本质

作者: 笔头还没烂 | 来源:发表于2023-04-07 17:20 被阅读0次
    1. 查看 oc 的底层实现

      • 将 .m 文件转成 .cpp 文件,命令如下:
      clang -rewrite-objc main.m -o main.cpp
      
      • 将 .m 文件转成 iphoneos 平台 arm64 架构 的 .cpp 文件,命令如下:
      xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main_arm64.cpp
      //如果需要链接其他框架,使用 -framework参数,比如 -framework UIKit
      
    2. iOS CPU 架构分类: 模拟器(i386)、32位(armv7)、64位(arm64)

    3. Objective-C 的 NSObject 类底层实现:

          NSObject *obj = [[NSObject alloc] init];
      
          //我们通过鼠标右键跳转到 NSObject 类定义,能看到下面的代码:
          @interface NSObject<NSObject> {
            Class isa;
          }
      
          //通过 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main_arm64.cpp 命令转化之后,得到以下代码:
          struct NSObject_IMPL {
              Class isa;
          };
      
          //(1)可以看出OC类的底层实现实际上就是结构体;
          //(2)isa 成员的地址即该结构体的地址;
          //(3)Class:typedef struct objc_class *Class; 指向结构体的指针;
      
    4. 指针占8个字节;

    5. 获取某个类实例对象的大小,示例代码如下:

       #import <Foundation/Foundation.h>
       #import <objc/runtime.h>
      
       int main(int argc, char * argv[]) {
      
           @autoreleasepool {
               // Setup code that might create autoreleased objects goes here.
               NSObject *obj = [[NSObject alloc] init];
       
               size_t size = class_getInstanceSize([NSObject class]);
               NSLog(@"%zd",size);
           }
           return 0;
       }
      

      输出结果:

      8

    6. 获取某个指针指向的内存大小,示例代码如下:

      #import <Foundation/Foundation.h>
      #import <objc/runtime.h>
      #import <malloc/malloc.h>
      
      int main(int argc, char * argv[]) {
      
          @autoreleasepool {
              // Setup code that might create autoreleased objects goes here.
              NSObject *obj = [[NSObject alloc] init];
        
              size_t size1 = class_getInstanceSize([NSObject class]);
              size_t size2 =  malloc_size((__bridge const void *)(obj));
              NSLog(@"%zd",size1);
              NSLog(@"%zd",size2);
          }
          return 0;
      }
      

      输出结果如下:

      8
      16

      小结:

      • class_getInstanceSize 是将类作为参数传入,获取实例对象的大小;
      • 而 malloc_size 是将指针作为参数传入,获取指针指向的那块内存的大小
    7. 探究OC对象的本质,可查看苹果 Objective-C 开源的源码,下载链接: https://github.com/apple-oss-distributions/objc4/tags,下载时选最新的,即找数字最大的。如下图所示:

      苹果 Objective-C 开源的源码.png
      如想查看上面 class_getInstanceSize 的源码,在工程全局搜 class_getInstanceSize,会得到下面的代码:
         size_t class_getInstanceSize(Class cls)
         {
           if (!cls) return 0;
           return cls->alignedInstanceSize();
         }
      

      鼠标右键点进去 alignedInstanceSize(),能看到下面的代码:

      // Class's ivar size rounded up to a pointer-size boundary.
      uint32_t alignedInstanceSize() const {
          return word_align(unalignedInstanceSize());
      }
      

      注释中 Class's ivar size 即类成员变量的大小。所以得出结论:
      class_getInstanceSize 返回的是类所有成员变量所占用的内存大小的总和,并且是经过“内存对齐”处理过后返回的大小。

      我们能看到 NSObject_IMPL 只有一个 Class 指针,该指针只占8个字节的大小。因此可得出:一个 NSObject 对象占用多少内存?解:对象创建时系统会分配16个字节(通过 malloc_size 实现),但是真正利用起来的只有8个字节(通过 class_getInstanceSize 得以验证)。

    8. 同样的方式,我们查看 alloc 方法的源码。搜索 allocWithZone,可得以下代码:

          // Replaced by ObjectAlloc
          + (id)allocWithZone:(struct _NSZone *)zone {
              return _objc_rootAllocWithZone(self, (objc_zone_t)zone);
          }
      

      继续查看 _objc_rootAllocWithZone 的源码:

      id
      _objc_rootAllocWithZone(Class cls, objc_zone_t zone __unused)
      {
          // allocWithZone under __OBJC2__ ignores the zone parameter
          return _class_createInstanceFromZone(cls, 0, nil,
                                               OBJECT_CONSTRUCT_CALL_BADALLOC);
      }
      

      继续查看 _class_createInstanceFromZone 的源码:

          _class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
                                    int construct_flags = OBJECT_CONSTRUCT_NONE,
                                    bool cxxConstruct = true,
                                    size_t *outAllocatedSize = nil)
          {
          ASSERT(cls->isRealized());
      
          // Read class's info bits all at once for performance
          bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();
          bool hasCxxDtor = cls->hasCxxDtor();
          bool fast = cls->canAllocNonpointer();
          size_t size;
      
          size = cls->instanceSize(extraBytes);
          if (outAllocatedSize) *outAllocatedSize = size;
      
          id obj;
          #if SUPPORT_ZONES
              if (zone) {
                  obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
              } else {
          #endif
                  obj = (id)calloc(1, size);
          #if SUPPORT_ZONES
              }
          #endif
              if (slowpath(!obj)) {
                 if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
                      return _objc_callBadAllocHandler(cls);
                  }
                  return nil;
              }
      
          if (!zone && fast) {
              obj->initInstanceIsa(cls, hasCxxDtor);
          } else {
              // Use raw pointer isa on the assumption that they might be
              // doing something weird with the zone or RR.
              obj->initIsa(cls);
          }
      
          if (fastpath(!hasCxxCtor)) {
              return obj;
          }
      
          construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
          return object_cxxConstructFromClass(obj, cls, construct_flags);
      }
      

      能看到有一行代码如下:

       obj = (id)calloc(1, size);
      

      这里传入了一个 size参数,这个 size 是前面定义的,即:

      size = cls->instanceSize(extraBytes);
      

      继续跳转 instanceSize 方法实现处,可得:

      inline size_t instanceSize(size_t extraBytes) const {
          if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
              return cache.fastInstanceSize(extraBytes);
          }
      
          size_t size = alignedInstanceSize() + extraBytes;
          // CF requires all objects be at least 16 bytes.
          if (size < 16) size = 16;
          return size;
      }
      

      可看到注释说明,CF requires all objects be at least 16 bytes. 当 size 小于16,则苹果框架强制把它等于16. CF 即 Cocoa Foundation 框架。

      而 alignedInstanceSize() 我们前面已经看过,返回的是类所有成员的大小,extraBytes 一般为0 ,则可得:

      size_t size = alignedInstanceSize() + extraBytes;//这里的 size 最终计算出来结果为 8
      
    9. 查看对象在内存中如何布局,可使用 Xcode ---> Debug ---> Debug Workflow ---> ViewMemory,main.m 示例代码如下:

          #import <Foundation/Foundation.h>
          #import <objc/runtime.h>
          #import <malloc/malloc.h>
      
          int main(int argc, char * argv[]) {
      
              @autoreleasepool {
                  // Setup code that might create autoreleased objects goes here.
                  NSObject *obj = [[NSObject alloc] init];
                  NSObject *obj2 = [[NSObject alloc] init];
          
                  size_t size1 = class_getInstanceSize([NSObject class]);
                  size_t size2 =  malloc_size((__bridge const void *)(obj));
                  NSLog(@"%zd",size1);
                  NSLog(@"%zd",size2);
              }  
              return 0;
          }
      
      我们在创建 obj2 对象下面一行处打下断点,通过xcode自带的 lldb 调试器,输入 p obj ,即可拿到该对象在内存中的起始地址,如下图所示: 获取对象地址值.png

      有了对象了内存地址之后,我们再通过Xcode ---> Debug ---> Debug Workflow ---> ViewMemory,打开内存查看器,在内存查看器中输入刚刚获取到的对象的内存起始地址,如下图所示:

      ViewMemory.png
      这里,内存查看器以十六进制的形式显示,每一位十六进制数代表4个二进制位,即每两位十六进制数代表8个二进制位,而8位即为一个字节。 即每两位十六进制数代表一个字节。通过内存查看器可以看到 obj 对象在内存中的布局即为上图蓝色框框住的部分,而该部分占据的内存大小刚好也是16个字节。而前面8个字节是有数据的,后面8个字节没有数据则置为0 。同时也论证了,当我们创建一个对象时,系统会先将这块内存置为0
    10. LLDB 动态调试器的使用

    • 将某个对象的地址值直接输出:po 对象名
    • 将某个对象的地址值以十六进制的形式输出:p 对象名
    • 读取以某个地址值开始的一段连续的内存空间:memory read + 地址值(读内存)
      • 与命令 x + 地址值的效果一致。
      • x/数量/格式字节数 + 地址值; eg: x/3xg + 地址值(读内存)
        • 其中“数量”,eg: x/3xg,这里的3表示输出的结果为3串地址值
        • 其中“格式”,eg: x/3xg,这里的x 表示每串地址值均以16进制的形式输出
          • x 为16进制,f 是浮点数,d 是10进制
        • 其中“字节大小”,eg: x/3xg,这里的g 表示8个字节
          • b: byte 1个字节; h: half word 2个字节;
          • w: word 4个字节;g: giant word 8个字节;
      • memory write + 要修改的精确到某一位的地址值+修改后的数(写内存)
    1. OC 转 c++ 代码,示例代码如下:

      #import <Foundation/Foundation.h>
      #import <objc/runtime.h>
      #import <malloc/malloc.h>
      
      @interface Student: NSObject {
          @public
          //学号
          int _no;
          //年龄
          int _age;
      }
      @end
      @implementation Student
      @end
      
      struct Student_IMPL {
          Class isa;
          int _no;
          int _age;
      };
      
      int main(int argc, char * argv[]) {
          @autoreleasepool {
              // Setup code that might create autoreleased objects goes here.
              NSObject *obj = [[NSObject alloc] init];
              NSObject *obj2 = [[NSObject alloc] init];
      
              size_t size1 = class_getInstanceSize([NSObject class]);
              size_t size2 =  malloc_size((__bridge const void *)(obj));
              NSLog(@"%zd",size1);
              NSLog(@"%zd",size2);
      
              Student *stu = [[Student alloc] init];
              stu->_no = 4;
              stu->_age = 5;
      
            //OC 代表转 C++ 代表需要桥接
            struct Student_IMPL *stu_IMPL = (__bridge struct Student_IMPL *)(stu);
            NSLog(@"学生的学号是%d,学生的年龄是%d",stu_IMPL->_no,stu_IMPL->_age);
          }
          return 0;
      }  
      

      运行结果如下:

      8
      16
      学生的学号是4,学生的年龄是5

      结论:通过上面的代码,能看出 类的本质实际上就是结构体,因为通过上面代码实现了从OC 到 C++ 结构体的转换,转换后再读取结构体中的成员没有任何问题。

    2. iOS 都是小端模式:从高地址开始读取。

    3. 内存对齐原则:结构体的最终大小必须是该结构体最大成员大小的倍数。

    4. 关于对象的属性问题,代码如下:

          @interface Student: NSObject {
          @public
          //学号
          int _no;
          //年龄
          int _age;
      }
          @property (nonatomic,assign) int height;
          @end
          @implementation Student
          @end
      
    • 对象的属性通过编绎后,会生成 _height 成员变量,同时生成 getter 和 setter 方法;
    • 创建出来的对象所占据的那块内存中,只会存放对象的成员变量,不存储对象的成员方法。(原因:不管创建多少个对象,成员方法只需一份)

    相关文章

      网友评论

          本文标题:iOS OC对象的本质

          本文链接:https://www.haomeiwen.com/subject/oinpddtx.html