美文网首页
类的结构分析

类的结构分析

作者: 深圳_你要的昵称 | 来源:发表于2020-09-14 19:53 被阅读0次

    前言

    书接上回isa结构分析,我们得知,对象的isa指针指向类,确切的说,是isa指针的shiftcls位域中指向类对象Class的地址。那OC层的Class在底层C、C++层面里,所对应的结构体里,有哪些成员变量和函数呢?下面将大致分析下类的底层结构。

    isa指向

    首先我们来看看isa指针的指向问题,我们都知道,对象的isa指针指向类对象Class,那类对象Class的isa指针指向哪里呢?我们先通过在lldb中打印地址来查看详情。

    我们先定义一个类LGPerson

    #import <Foundation/Foundation.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface LGPerson : NSObject
    
    @end
    
    NS_ASSUME_NONNULL_END
    
    //--------------------分割线----------------------
    
    #import "LGPerson.h"
    
    @implementation LGPerson
    
    @end
    
    

    在程序入口main.m中生成LGperson对象

    #import <Foundation/Foundation.h>
    #import "LGPerson.h"
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            // insert code here...
            LGPerson *person = [[LGPerson alloc] init];
            
    
        }
        return 0;
    }
    

    打断点查看person对象的地址信息

    (lldb) po person
    <LGPerson: 0x10124dcb0>
    
    (lldb) p person
    (LGPerson *) $2 = 0x000000010124dcb0
    

    我们常用的LLDB指令po,p,他俩的区别如下:

    指令名称 释义
    p 是 expression 的别名,打印某个东西,可以是变量和表达式
    po 一般用于打印对象,是 expression -O 的别名,会输出对应的值

    再来看看另外两个LLDB指令

    指令名称 释义
    p/x 以16进制形式读取对象的地址或者值
    x/4gx 以16进制形式读取4段8位的内存空间里面存储的地址或者值

    用上面指令看看person对象的地址

    (lldb) p/x person
    (LGPerson *) $4 = 0x000000010124dcb0
    
    (lldb) x/4gx person
    0x10124dcb0: 0x001d8001000020e9 0x0000000000000000
    0x10124dcc0: 0x0000000080080000 0x00007fff8d07d208
    

    首先第一段0x10124dcb0: 0x001d8001000020e9这个就是person对象的isa指针地址,再查看isa指针中位域shiftcls的值,有个小技巧,通过和ISA_MASK进行与运算,过滤出shiftcls的地址值

    在查看isa_t结构体中,可以找到ISA_MASK的宏定义(以x86_64为例)
    define ISA_MASK 0x00007ffffffffff8ULL

    (lldb) p/x 0x001d8001000020e9 & 0x00007ffffffffff8ULL
    (unsigned long long) $6 = 0x00000001000020e8
    (lldb) po $6
    LGPerson
    

    验证得出:person对象isa指针的位域shiftcls中指向的是类LGPerson的地址。
    同理,我们在继续看看类LGPerson对象的isa指针位域shiftcls中存放的什么值?

    // 以4段方式查看LGPeson类对象地址
    (lldb) x/4gx $6
    0x1000020e8: 0x00000001000020c0 0x00000001003ef190
    0x1000020f8: 0x0000000101063830 0x0004801000000007
    
    // 首地址isa & ISA_MASK,得到位域shiftcls中存放的值
    (lldb) p/x 0x00000001000020c0 & 0x00007ffffffffff8ULL
    (unsigned long long) $7 = 0x00000001000020c0
    
    // 打印shiftcls值
    (lldb) po $7
    LGPerson
    

    发现,类对象LGPerson的isa指针中shiftcls存储的值也是LGPerson,但是$6 = 0x00000001000020e8$7 = 0x00000001000020c0的地址值显然不同,说明$7明显不是指向类对象LGPerson它自己,但是打印出的值是LGPerson,这就是苹果提出的一个概念元类(meta class)

    验证得出,类对象的isa指向与对象的isa指向不同,对象的isa指向类,而类的isa指向元类(苹果爸爸定义为meta class)。

    接着我们继续查看$7的isa指针的位域shiftcls中存放的地址值是什么?

    (lldb) x/4gx $7
    0x1000020c0: 0x00000001003ef140 0x00000001003ef140
    0x1000020d0: 0x0000000101063b40 0x0003e03100000007
    (lldb) p/x 0x00000001003ef140 & 0x00007ffffffffff8ULL
    (unsigned long long) $8 = 0x00000001003ef140
    (lldb) po $8
    NSObject
    

    $7指针的位域shiftcls中存放的地址$8值是NSObject。接着看$8shiftcls的值,如下:

    (lldb) x/4gx $8
    0x1003ef140: 0x00000001003ef140 0x00000001003ef190
    0x1003ef150: 0x0000000100640ff0 0x0004e03100000007
    
    (lldb) p/x 0x00000001003ef140 & 0x00007ffffffffff8ULL
    (unsigned long long) $9 = 0x00000001003ef140
    
    (lldb) po $9
    NSObject
    

    $8 = 0x00000001003ef1400x1003ef140: 0x00000001003ef140(isa指针地址)一样,shiftcls打印出的地址也是0x00000001003ef140,值也是NSObject,说明$8isa指针指向自己。
    那这个$9的值NSObject,是不是我们通常的基类NSObject类呢?接下来验证一下

    (lldb) p/x NSObject.class
    (Class) $10 = 0x00000001003ef190 NSObject
    

    $10的地址与$9不同,说明不是基类。

    验证得出,元类的isa指针指向NSObject根元类(苹果爸爸定义为root meta class)。

    上一张isa指向经典图:

    isa指向.png

    以上通过LLDB中的指令查看地址,得出isa指针的指向是:

    对象 --> 类 --> 元类 --> 根元类-->根元类 指向自身

    特殊问题:类信息在内存中存在几份

    以上我们得知,类对象的isa指针指向元类,那类对象Class的isa指针指向的地址是同一个还是不同的?我们还是用代码验证一下:

    Class class1 = [LGPerson class];
    Class class2 = [LGPerson alloc].class;
    Class class3 = object_getClass([LGPerson alloc]);
    NSLog(@"\n%p\n%p\n%p", class1, class2, class3);
    

    编译运行后得出:

    2020-09-14 17:23:07.926664+0800 KCObjc[9752:305959] 
    0x100002100
    0x100002100
    0x100002100
    

    地址一模一样,说明类信息在内存中只存在一份。

    类Class结构分析

    言归正传,分析了isa指针指向问题后,现在再来回头看看Class结构的具体情况:

    /// An opaque type that represents an Objective-C class.
    typedef struct objc_class *Class;
    

    Class对应底层结构体是objc_class,再来看看objc_class

    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() const {
            return bits.data();
        }
        
        // 省略部分代码.......
    }
    
    • objc_class继承objc_object
    • superclass是父类Class
    • cache_t cache缓存
    • class_data_bits_t bits成员

    cache_t结构体

    // 只列举了成员变量
    struct cache_t {
    #if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
        explicit_atomic<struct bucket_t *> _buckets;
        explicit_atomic<mask_t> _mask;
    #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
        explicit_atomic<uintptr_t> _maskAndBuckets;
        mask_t _mask_unused;
        
    #if __LP64__
        uint16_t _flags;
    #endif
        uint16_t _occupied;
    }
    
    // 省略部分代码...
    
    • CACHE_MASK_STORAGE_OUTLINED这种缓存模式,则占用内存大小是:bucket_t +mask_t+uint16_t+uint16_t

    • CACHE_MASK_STORAGE这种缓存模式,则占用内存大小是:uintptr_t+mask_t+uint16_t+uint16_t

    uintptr_t内存大小
    #ifndef _UINTPTR_T
    #define _UINTPTR_T
    typedef unsigned long           uintptr_t;
    #endif /* _UINTPTR_T */
    

    uintptr_t是long类型,8个字节

    bucket_t内存大小
    struct bucket_t {
    private:
        // IMP-first is better for arm64e ptrauth and no worse for arm64.
        // SEL-first is better for armv7* and i386 and x86_64.
    #if __arm64__
        explicit_atomic<uintptr_t> _imp;
        explicit_atomic<SEL> _sel;
    #else
        explicit_atomic<SEL> _sel;
        explicit_atomic<uintptr_t> _imp;
    #endif
    // 省略部分代码...
    }
    

    不管是__arm64__还是其它,都是uintptr_t+_sel指针 --> 4 + 8 = 12

    mask_t内存大小
    #if __LP64__
    typedef uint32_t mask_t;  // x86_64 & arm64 asm are less efficient with 16-bits
    #else
    typedef uint16_t mask_t;
    #endif
    

    一目了然,即整型,大小为4。

    综上所述,

    • CACHE_MASK_STORAGE_OUTLINED模式下,explicit_atomic<struct bucket_t *> _buckets是结构体bucket_t的指针 8,+ mask_t 4 + 2 * uint16_t 2 = 16
    • CACHE_MASK_STORAGE模式下是8+4+2+2,也是16;

    结论cache_t 所占内存大小:16

    重点 class_data_bits_t

    结构体class_data_bits_t大致分为几个部分:

    1. bits操作相关,为私有函数,包含获取、设置、清空等操作
     // Values are the FAST_ flags above.
        uintptr_t bits;
    private:
        bool getBit(uintptr_t bit) const
        {
            return bits & bit;
        }
    
        // Atomically set the bits in `set` and clear the bits in `clear`.
        // set and clear must not overlap.
        void setAndClearBits(uintptr_t set, uintptr_t clear)
        {
           // ...省略部分代码
        }
    
        void setBits(uintptr_t set) {
            __c11_atomic_fetch_or((_Atomic(uintptr_t) *)&bits, set, __ATOMIC_RELAXED);
        }
    
        void clearBits(uintptr_t clear) {
            __c11_atomic_fetch_and((_Atomic(uintptr_t) *)&bits, ~clear, __ATOMIC_RELAXED);
        }
    

    2.重要部分data(),返回结构体class_rw_t *指针类型,里面存放了属性列表,实例方法列表,协议列表等信息

    class_rw_t* data() const {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
    void setData(class_rw_t *newData) {
        // ...省略部分代码
    }
    

    3.ro data相关

    // Get the class's ro data, even in the presence of concurrent realization.
    // fixme this isn't really safe without a compiler barrier at least
    // and probably a memory barrier when realizeClass changes the data field
    const class_ro_t *safe_ro() {
        // ...省略部分代码
    }
    
    void setClassArrayIndex(unsigned Idx) {
    #if SUPPORT_INDEXED_ISA
        // 0 is unused as then we can rely on zero-initialisation from calloc.
        ASSERT(Idx > 0);
        data()->index = Idx;
    #endif
    }
    
    unsigned classArrayIndex() {
    #if SUPPORT_INDEXED_ISA
        return data()->index;
    #else
        return 0;
    #endif
    }
    

    4.swift相关

    bool isAnySwift() {
        return isSwiftStable() || isSwiftLegacy();
    }
    
    bool isSwiftStable() {
        return getBit(FAST_IS_SWIFT_STABLE);
    }
    void setIsSwiftStable() {
        setAndClearBits(FAST_IS_SWIFT_STABLE, FAST_IS_SWIFT_LEGACY);
    }
    
    bool isSwiftLegacy() {
        return getBit(FAST_IS_SWIFT_LEGACY);
    }
    void setIsSwiftLegacy() {
        setAndClearBits(FAST_IS_SWIFT_LEGACY, FAST_IS_SWIFT_STABLE);
    }
    
    // fixme remove this once the Swift runtime uses the stable bits
    bool isSwiftStable_ButAllowLegacyForNow() {
        return isAnySwift();
    }
    
    _objc_swiftMetadataInitializer swiftMetadataInitializer() {
        // This function is called on un-realized classes without
        // holding any locks.
        // Beware of races with other realizers.
        return safe_ro()->swiftMetadataInitializer();
    }        
    
    class_rw_t内部信息

    class_rw_t里存放了类的实例方法列表,属性列表,协议列表等信息,如何查看呢?下面举例说明。

    首先在LGPerson类中添加属性和方法

    @interface LGPerson : NSObject
    
    @property (nonatomic, strong) NSString *name;
    @property (nonatomic, copy) NSString *nikeName;
    
    - (void)sayHello;
    + (void)sayByebye;
    
    @end
    

    查看LGPerson类的地址

    (lldb) p/x LGPerson.class
    (Class) $0 = 0x00000001000021f8
    
    // 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
    

    根据上面objc_class的结构分析,要获取class_data_bits_t bits的地址,需要位移isa8位+Class8位 + cache_t16位 = 32位

    0x00000001000021f8 + 0x20 = 0x0000000100002218
    

    获取class_data_bits_t

    (lldb) p (class_data_bits_t *)0x0000000100002218
    (class_data_bits_t *) $1 = 0x0000000100002218
    

    获取data()

    (lldb) p $1->data()
    (class_rw_t *) $2 = 0x0000000101142b80
    (lldb) p *$2
    (class_rw_t) $3 = {
      flags = 2148007936
      witness = 1
      ro_or_rw_ext = {
        std::__1::atomic<unsigned long> = 4294975608
      }
      firstSubclass = nil
      nextSiblingClass = NSUUID
    }
    
    1. 查看属性列表信息
    (lldb) p $3.properties()
    (const property_array_t) $4 = {
      list_array_tt<property_t, property_list_t> = {
         = {
          list = 0x0000000100002188
          arrayAndFlag = 4294975880
        }
      }
    }
    
    (lldb) p $4.list
    (property_list_t *const) $5 = 0x0000000100002188
    
    (lldb) p *$5
    (property_list_t) $6 = {
      entsize_list_tt<property_t, property_list_t, 0> = {
        entsizeAndFlags = 16
        count = 2
        first = (name = "name", attributes = "T@\"NSString\",&,N,V_name")
      }
    }
    

    count = 2说明有2个属性,再仔细查看这两个属性:

    
    (lldb) p $6.get(0)
    (property_t) $7 = (name = "name", attributes = "T@\"NSString\",&,N,V_name")
    (lldb) p $6.get(1)
    (property_t) $8 = (name = "nikeName", attributes = "T@\"NSString\",C,N,V_nikeName")
    (lldb) p $6.get(2)
    Assertion failed: (i < count), function get, file /Users/Aron/objc4_debug/objc4-781/runtime/objc-runtime-new.h, line 438.
    error: Execution was interrupted, reason: signal SIGABRT.
    The process has been returned to the state before expression evaluation.
    

    name nikeName都有了,当获取第三个时越界,Assertion failed: (i < count)表示下标必须小于count:2

    2. 查看方法列表信息

    同理,查看方法列表methods()

    (lldb) p $3.methods()
    (const method_array_t) $9 = {
      list_array_tt<method_t, method_list_t> = {
         = {
          list = 0x00000001000020c0
          arrayAndFlag = 4294975680
        }
      }
    }
    (lldb) p $9.list
    (method_list_t *const) $10 = 0x00000001000020c0
    
    (lldb) p *$10
    (method_list_t) $11 = {
      entsize_list_tt<method_t, method_list_t, 3> = {
        entsizeAndFlags = 26
        count = 5
        first = {
          name = "nikeName"
          types = 0x0000000100000f98 "@16@0:8"
          imp = 0x0000000100000e20 (KCObjc`-[LGPerson nikeName])
        }
      }
    }
    

    count = 5说明有5个方法,明明我只定义了2个方法,- (void)sayHello;+ (void)sayByebye;,再一一打印看看是哪5个方法:

    (lldb) p $11.get(0)
    (method_t) $12 = {
      name = "nikeName"
      types = 0x0000000100000f98 "@16@0:8"
      imp = 0x0000000100000e20 (KCObjc`-[LGPerson nikeName])
    }
    (lldb) p $11.get(1)
    (method_t) $13 = {
      name = "setNikeName:"
      types = 0x0000000100000fa0 "v24@0:8@16"
      imp = 0x0000000100000e50 (KCObjc`-[LGPerson setNikeName:])
    }
    (lldb) p $11.get(2)
    (method_t) $14 = {
      name = ".cxx_destruct"
      types = 0x0000000100000f90 "v16@0:8"
      imp = 0x0000000100000d90 (KCObjc`-[LGPerson .cxx_destruct])
    }
    (lldb) p $11.get(3)
    (method_t) $15 = {
      name = "name"
      types = 0x0000000100000f98 "@16@0:8"
      imp = 0x0000000100000dd0 (KCObjc`-[LGPerson name])
    }
    (lldb) p $11.get(4)
    (method_t) $16 = {
      name = "setName:"
      types = 0x0000000100000fa0 "v24@0:8@16"
      imp = 0x0000000100000df0 (KCObjc`-[LGPerson setName:])
    }
    

    原来是2个属性的get set方法和一个析构函数cxx_destruct,那定义的2个方法去哪了?

    补坑:
    原来之前只定义了方法,并未实现sayHello sayByebye这2个方法。。。

    先补上方法实现,重新编译

    @implementation LGPerson
    
    - (void)sayHello {
        
    }
    
    + (void)sayByebye {
        
    }
    
    @end
    

    再打印方法列表看看:

    (lldb) p *$5
    (method_list_t) $6 = {
      entsize_list_tt<method_t, method_list_t, 3> = {
        entsizeAndFlags = 26
        count = 6
        first = {
          name = "sayHello"
          types = 0x0000000100000f35 "v16@0:8"
          imp = 0x0000000100000d60 (KCObjc`-[LGPerson sayHello])
        }
      }
    }
    

    发现count = 6,不是7个,看看少了哪个?

    (lldb) p $6.get(0)
    (method_t) $7 = {
      name = "sayHello"
      types = 0x0000000100000f35 "v16@0:8"
      imp = 0x0000000100000d60 (KCObjc`-[LGPerson sayHello])
    }
    (lldb) p $6.get(1)
    (method_t) $8 = {
      name = "nikeName"
      types = 0x0000000100000f49 "@16@0:8"
      imp = 0x0000000100000e00 (KCObjc`-[LGPerson nikeName])
    }
    (lldb) p $6.get(2)
    (method_t) $9 = {
      name = "setNikeName:"
      types = 0x0000000100000f51 "v24@0:8@16"
      imp = 0x0000000100000e30 (KCObjc`-[LGPerson setNikeName:])
    }
    (lldb) p $6.get(3)
    (method_t) $10 = {
      name = ".cxx_destruct"
      types = 0x0000000100000f35 "v16@0:8"
      imp = 0x0000000100000d70 (KCObjc`-[LGPerson .cxx_destruct])
    }
    (lldb) p $6.get(4)
    (method_t) $11 = {
      name = "name"
      types = 0x0000000100000f49 "@16@0:8"
      imp = 0x0000000100000db0 (KCObjc`-[LGPerson name])
    }
    (lldb) p $6.get(5)
    (method_t) $12 = {
      name = "setName:"
      types = 0x0000000100000f51 "v24@0:8@16"
      imp = 0x0000000100000dd0 (KCObjc`-[LGPerson setName:])
    }
    

    发现少了sayByebye,这是类方法,根据isa指针走位分析,实例方法存储在类中,那类方法应该存储在元类中。去元类里看看:

    (lldb) p/x LGPerson.class
    (Class) $0 = 0x0000000100002230
    
    (lldb) x/4gx 0x0000000100002230
    0x100002230: 0x0000000100002208 0x00000001003f0190
    0x100002240: 0x000000010142d880 0x0001802400000003
    

    LGPersonisa指针地址是0x0000000100002208,偏移32位获取class_data_bits_t,即0x0000000100002228,再打印出这个地址对应的方法列表信息:

    (lldb) p (class_data_bits_t *)0x0000000100002228
    (class_data_bits_t *) $1 = 0x0000000100002228
    (lldb) p $1->data()
    (class_rw_t *) $2 = 0x000000010142d7e0
    (lldb) p *$2
    (class_rw_t) $3 = {
      flags = 2684878849
      witness = 1
      ro_or_rw_ext = {
        std::__1::atomic<unsigned long> = 4294975536
      }
      firstSubclass = nil
      nextSiblingClass = 0x00007fff8dcd5c60
    }
    (lldb) p $3.methods()
    (const method_array_t) $4 = {
      list_array_tt<method_t, method_list_t> = {
         = {
          list = 0x0000000100002078
          arrayAndFlag = 4294975608
        }
      }
    }
    (lldb) p $4.list
    (method_list_t *const) $5 = 0x0000000100002078
    (lldb) p *$5
    (method_list_t) $6 = {
      entsize_list_tt<method_t, method_list_t, 3> = {
        entsizeAndFlags = 26
        count = 1
        first = {
          name = "sayByebye"
          types = 0x0000000100000f35 "v16@0:8"
          imp = 0x0000000100000d50 (KCObjc`+[LGPerson sayByebye])
        }
      }
    }
    

    count = 1只有1个方法sayByebye给自己一个666,哈哈!印证了isa的走位!

    补充:objc_class继承objc_object
    struct objc_class : objc_object {
        // 省略部分代码.......
    }
    
    /// Represents an instance of a class.
    struct objc_object {
        Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
    };
    

    分析:

    • 结构体类型objc_class 继承自objc_object类型,其中objc_object也是一个结构体,且有一个isa属性,所以objc_class也拥有了isa属性;
    @interface NSObject <NSObject> {
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Wobjc-interface-ivars"
        Class isa  OBJC_ISA_AVAILABILITY;
    #pragma clang diagnostic pop
    }
    
    • NSObject中的isa在底层是Class类型,而Class的底层编码来自 objc_class类型,所以NSObject也拥有了isa指针

    • NSObject类初始化一个实例对象objc,objc 满足 objc_object 的特性(即有isa指针),主要是因为isaNSObjectobjc_class继承过来的,而objc_class继承自objc_objectobjc_objectisa指针。所以对象都有一个 isaisa表示指向,来自于当前的objc_object

    • objc_object就好比一个模板,不论是Class对象还是继承NSObject的对象,都最终指向继承objc_object(实质是继承了成员 isa),所以说万物皆对象。

    总结

    1.首先从底层源码分析了isa指针的走位,并且通过示例得以证明。

    2.通过isa的走位,我们分析了类Class的底层结构objc_class ,通过示例打印属性列表信息和方法列表信息,证明了实例方法存储在类中,类方法存储在元类中

    3.最后通过objc_classobjc_object的继承关系,得出万物皆对象,但始终离不开isa

    相关文章

      网友评论

          本文标题:类的结构分析

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