美文网首页
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