美文网首页
Objective-C 对象探究

Objective-C 对象探究

作者: pengquanhua | 来源:发表于2021-02-06 15:57 被阅读0次

    本文将分析 OC 对象的本质,对象的内存布局,已经如何为对象分配内存。分析的源码来自 objc-812

    对象的本质

    打开 objc-812 runtime 的源码可以找到对象的定义:

    typedef struct objc_object *id;
    
    struct objc_object {
    private:
        isa_t isa;
    }
    

    id 被类型定义为 objc_object *,也就是说对象本质上一个 objc_object 结构体。其唯一的变量 isa 的类型为 isa_t:

    #define ISA_MAGIC_MASK  0x001f800000000001ULL
    #define ISA_MAGIC_VALUE 0x001d800000000001ULL
    #define RC_ONE   (1ULL<<56)
    #define RC_HALF  (1ULL<<7)
    
    union isa_t {
        isa_t() { }
        isa_t(uintptr_t value) : bits(value) { }
    
        Class cls;
        uintptr_t bits;
    
        struct {
            uintptr_t nonpointer           : 1;
            uintptr_t has_assoc         : 1;
            uintptr_t has_cxx_dtor      : 1;
            uintptr_t shiftcls          : 44;
            uintptr_t magic             : 6;
            uintptr_t weakly_referenced : 1;
            uintptr_t deallocating      : 1;
            uintptr_t has_sidetable_rc  : 1;
            uintptr_t extra_rc          : 8;
        };
    };
    

    isa_t 是一个联合体,可简单理解为 64 位二进制,每一位都代表特定的信息:

    • nonpointer: 表示是否对 isa 指针开启指针优化。0:纯isa指针,1:不止是类对象地址,isa 中包含了类信息、对象的引用计数等。

    • has_assoc:关联对象标志位,0没有,1存在

    • has_cxx_dtor:该对象是否有 C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑, 如果没有,则可以更快的释放对象。

    • shiftcls:存储类指针的值。开启指针优化的情况下,在 arm64 架构中有 33 位用来存储类指针。

    • magic:用于调试器判断当前对象是真的对象还是没有初始化的空间。

    • weakly_referenced:志对象是否被指向或者曾经指向一个 ARC 的弱变量,
      没有弱引用的对象可以更快释放。

    • deallocating:标志对象是否正在释放内存

    • has_sidetable_rc:当对象引用技术大于 10 时,则需要借用该变量存储进位

    • extra_rc:当表示该对象的引用计数值,实际上是引用计数值减 1, 例如,如果对象的引用计数为 10,那么 extra_rc 为 9。如果引用计数大于 10, 则需要使用到下面的 has_sidetable_rc。

    其中需重点理解 nonpointershiftcls,举个例子:假如 isa 的值为
    0x011d800100008b1d,转为二进制:

    nonpinter = 1 时,第 3-47 位为 shiftcls,即类的指针,这是什么呢?后面会分析。
    为了方便取出 shiftcls,可以使用 isa & ISA_MASK

    define ISA_MASK        0x00007ffffffffff8ULL // 3-47 位为 1
    

    对象的内存布局

    在 OC 中,一切对象都是以 objc_object 为基础,那如果一个类声明了多个属性,它的对象在内存中布局是怎样的呢?

    @interface MYObject : NSObject
    
    @property(nonatomic, strong) NSString *property1;
    @property(nonatomic, strong) NSString *property2;
    @property(nonatomic, assign) BOOL bool1;
    @property(nonatomic, assign) NSInteger int10;
    @property(nonatomic, strong) NSString *property3;
    
    - (void)instanceMethod1;
    
    @end
    
    @implementation MYObject
    
    - (void)instanceMethod1 {
        
    }
    
    @end
    
    int main(int argc, const char * argv[]) {
        
        MYObject *myObject = [MYObject alloc];
        myObject.property1 = @"property1";
        myObject.property2 = @"property2";
        myObject.bool1 = YES;
        myObject.int10 = 10;
        myObject.property3 = @"property3";
        return 0;
    }
    

    return 0; 打个断点,运行程序,然后在 lldb 中输入 x/8gx myObject 将 myObject 对象内存打印出来。 我们已经知道对象的第一个变量为 isa,并且 isa 中的 3-47 位对应类的指针:


    接着打印其他数据:

    可以看到对象的内存布局不一定和变量声明的顺序是一样的。由于字节对齐和节省内存,在编译时编译器会进行重排。

    对象的内存分配

    上面我们已经知道了,内存的布局情况。那么在创建一个对象时,是如何为它分配内存的呢?
    OC 的所有对象都是通过 alloc 方法来分配内存,研究 alloc 的内部实现,需要下载可以编译的 runtime 源码。在 [MYObject alloc]; 打个断点,此时就可以跳进 alloc 源码里研究它的流程了。大致如下:

    [MYObject alloc]; 
    -> _objc_rootAlloc(self); 
    -> callAlloc(cls, false, true);
    -> _objc_rootAllocWithZone(cls, nil); 
    -> _class_createInstanceFromZone(cls, 0, nil,OBJECT_CONSTRUCT_CALL_BADALLOC);
    

    最后的函数 _class_createInstanceFromZone 进行分配,看一下源码:

    static ALWAYS_INLINE id
    _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 (zone) {
            obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
        } else {
            obj = (id)calloc(1, size);
        }
        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);
    }
    

    通过设置断点,可以忽略无效的条件判断,可以得到核心的过程为:

    size = cls->instanceSize(extraBytes); // 计算对象内存大小
    obj = (id)calloc(1, size); // 分配内存
    obj->initInstanceIsa(cls, hasCxxDtor);    // 初始化 isa,即把类指针关联到对象
    

    所以分配对象内存过程经过了三个步骤:

    1. 计算内存大小
    2. 分配内存
    3. 对象关联类指针

    计算内存大小

    通过设置断点,可以忽略无效的条件判断,size_t instanceSize(size_t extraBytes) 过程为:

    alignedInstanceSize()
    -> cache.fastInstanceSize(extraBytes); 
    -> align16(size + extra - FAST_CACHE_ALLOC_DELTA16);
    
    1. fastInstanceSize 获取对象的内存大小:
    size_t fastInstanceSize(size_t extra) const
        {
            ASSERT(hasFastInstanceSize(extra));
    
            if (__builtin_constant_p(extra) && extra == 0) {
                return _flags & FAST_CACHE_ALLOC_MASK16;
            } else {
                size_t size = _flags & FAST_CACHE_ALLOC_MASK;
                // remove the FAST_CACHE_ALLOC_DELTA16 that was added
                // by setFastInstanceSize
                return align16(size + extra - FAST_CACHE_ALLOC_DELTA16);
            }
        }
    

    这里暂时不展开,后续写到类的缓存 cache 时,会补充。现在只需知道 这个函数获取对象大小。

    1. align16 字节对齐
    static inline size_t align16(size_t x) {
        return (x + size_t(15)) & ~size_t(15);
    }
    

    OC 的对象是以 16 位进行对齐。

    打个断点,输出 MYObject 的对象大小为 48 个字节。

    来验证一下,isa 占 8 个字节,property1/property2/property3 各占 8 个,bool1 占 1 个字节,int10 占 8 个字节,根据 C++ 结构体内存对齐原则,加起来占 48 个字节。

    对象关联类指针

    obj->initInstanceIsa(cls, hasCxxDtor);; 对象关联类指针
    

    calloc 已经为对象分配好了内存,但此时这块内存还是空的,所以需要类信息关联到这个对象上,也就是为对象的 isa 赋值。

    inline void 
    objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
    {
        isa_t newisa(0);
    
        if (!nonpointer) {
            newisa.setClass(cls, this);
        } else {
            newisa.bits = ISA_MAGIC_VALUE;
            newisa.has_cxx_dtor = hasCxxDtor;
            newisa.setClass(cls, this);
            newisa.extra_rc = 1;
        }
        isa = newisa;
    }
    

    上面代码是删了无效信息后的核心代码,先判断 nonpointer 是否有效:

    1. nonpointerfalse,只为 isa 设置类指针。
    2. nonpointertrue,为 isa 设置类指针,并且设置 isa 的其他位。
      再来看看是如何设置类指针的:
    inline void
    isa_t::setClass(Class newCls, UNUSED_WITHOUT_PTRAUTH objc_object *obj)
    {
        shiftcls = (uintptr_t)newCls >> 3;
    }
    

    将类指针向右移 3 位后赋值给 shiftcls,这和在分析 isa_t 时讲的是一致的,但为什么要向右移 3 位呢,前面说到 OC 对象是以 16 进行内存对齐,而 OC 的类指针是以 8 字节进行对齐的,也就是地址后面 3 位都是 0,也就没必要进行存储了。

    验证
    obj->initInstanceIsa(cls, hasCxxDtor); 打个断点
    输入 x/4gx 打印对象的内存上的内容:

    此时,isa 为空的,往下运行一步,输入 x/4gx

    此时,就找到对象的类信息了。

    至此,已经为对象分配好了内存,并且关联了 isa。

    小结

    文中分析了 OC 对象本质都是 objc_object,每个对象都有一个 isa_t 类型的变量 isa,其存储了类的信息。并分析了对象的内存布局情况,以及对象内存分配和关联 isa 的过程。
    那么在 OC 中,类以及属性、方法的本质又是什么呢?类是怎么存储属性和方法的呢?类的缓存又是什么呢?在下一篇文章,将为大家揭晓。

    相关文章

      网友评论

          本文标题:Objective-C 对象探究

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