[OC学习笔记]类对象的结构

Posted Billy Miracle

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[OC学习笔记]类对象的结构相关的知识,希望对你有一定的参考价值。

对象的分类

Objective-C中所有对象可以分为3类:实例对象,类对象,元类对象。其中我们开发者常用的继承自NSObject都属于实例对象,实例对象通过isa指针指向的是类对象。类对象通过isa指向的是元类对象。类对象和元类对象拥有同样的结构,都是来自objc_class。关于这部分可以先学习一下我前面的博客:[OC学习笔记]浅学元类

objc_class结构

因为所有的类都是继承于objct_class,在源码中查找objc_class可以找到其结构:

struct objc_class : objc_object 
  objc_class(const objc_class&) = delete;
  objc_class(objc_class&&) = delete;
  void operator=(const objc_class&) = delete;
  void operator=(objc_class&&) = delete;
    // 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
    ...

其中isa指针是继承自objc_object的。isa在之前的内容里面已经讲过,主要是指向类对象或者元类对象的。类结构里面默认一个Class ISA同时包含Class superclasscache_t cacheclass_data_bits_t bitssuperclass是体现出继承关系的。cache是方法缓存。bits是类的其他信息,例如成员变量,方法列表,协议,属性。
注意:元类对象结构也是如此,但元类对象里面没有成员变量,协议,属性这些内容。

isa指向

放出这张经典图片:

isa指向做一个简单的总结:

实例对象的isa指针指向对应的类对象,类对象的isa指针指向对应的元类对象,元类对象的isa指向基类的元类对象(根元类)。

可以用lldb调试来验看下。

- (void)test 
    NSObject *objc = [[NSObject alloc] init];
    // isa指向的就是类对象
    Class objcClass1 = [NSObject class];
    Class objcMetaClass = object_getClass([NSObject class]);


根据上一篇内容,我们先获取objc的isa指针的值,然后用这个值与运算ISA_MASK,得到的值,正好是类对象的地址。然后用同样的方法获取类对象的值,和ISA_MASK与运算得到元类对象的地址。
可以继续这样再操作一轮,得到的还是元类对象的地址。这是因为该类是NSObjectNSObject元类对象的isa是指向自己的。

define ISA_MASK        0x007ffffffffffff8ULL

0x00007ffffffffff8 转二进制为

0001 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1000

其实就是,000…000…中间44个1…000。其他位抹零,只保留中间44位,即取到shiftcls类信息。


可看到NSObjectisa走势为: NSObject →根元类 →根元类自身

superClass指向

再看上面那张图:superClass的指向也在上面那个经典的图中。这里要注意superclassobjc_class才有的,所以实例对象是没有的。类对象的superclass指向父类对象,元类对象的superclass指向父类的元类对象,根元类对象的superclass指针指向根类对象。根类对象的superclassnil

bits

类的结构中还有一个bits,里面也存了很多信息。我们之前知道,已知首地址,可以通过平移方法,得到我们64位下结构体指针类型占8字节,即isa占8字节,superclass同理也是结构体指针类型占8字节,即superclass占8字节。
接下来我们看下cache的大小:
因为cachecache_t类型, 最简单的方法lldb命令 po读一下cache_t
可以看到,大小为16字节。
这一小节,我们主要来看bits的数据结构 class_data_bits_t

struct class_data_bits_t 
    friend objc_class;

    // 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)
    
        ASSERT((set & clear) == 0);
        uintptr_t newBits, oldBits = LoadExclusive(&bits);
        do 
            newBits = (oldBits | set) & ~clear;
         while (slowpath(!StoreReleaseExclusive(&bits, &oldBits, newBits)));
    

    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);
    

public:

    class_rw_t* data() const 
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    
    void setData(class_rw_t *newData)
    
        ASSERT(!data()  ||  (newData->flags & (RW_REALIZING | RW_FUTURE)));
        // Set during realization or construction only. No locking needed.
        // Use a store-release fence because there may be concurrent
        // readers of data and data's contents.
        uintptr_t newBits = (bits & ~FAST_DATA_MASK) | (uintptr_t)newData;
        atomic_thread_fence(memory_order_release);
        bits = newBits;
    

    // 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() const 
        class_rw_t *maybe_rw = data();
        if (maybe_rw->flags & RW_REALIZED) 
            // maybe_rw is rw
            return maybe_rw->ro();
         else 
            // maybe_rw is actually ro
            return (class_ro_t *)maybe_rw;
        
    

#if SUPPORT_INDEXED_ISA
    void setClassArrayIndex(unsigned Idx) 
        // 0 is unused as then we can rely on zero-initialisation from calloc.
        ASSERT(Idx > 0);
        data()->index = Idx;
    
#else
    void setClassArrayIndex(__unused unsigned Idx) 
    
#endif

    unsigned classArrayIndex() 
#if SUPPORT_INDEXED_ISA
        return data()->index;
#else
        return 0;
#endif
    

    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();
    
;

其实最最重要的是其中的2个方法。datasafe_ro,两个方法分别返回class_rw_tclass_ro_t

class_rw_t* data() const 
	return (class_rw_t *)(bits & FAST_DATA_MASK);

const class_ro_t *safe_ro() const 
	class_rw_t *maybe_rw = data();
	if (maybe_rw->flags & RW_REALIZED) 
		// maybe_rw is rw
		return maybe_rw->ro();
	 else 
		// maybe_rw is actually ro
		return (class_ro_t *)maybe_rw;
	

仔细观察获取class_ro_t的方法,里面也用到了data()方法,也可以理解为class_ro_t也在class_rw_t之中。

class_rw_t

继续看class_rw_t的数据结构:

struct class_rw_t 
    // Be warned that Symbolication knows the layout of this structure.
    uint32_t flags;
    uint16_t witness;
#if SUPPORT_INDEXED_ISA
    uint16_t index;
#endif

    explicit_atomic<uintptr_t> ro_or_rw_ext;

    Class firstSubclass;
    Class nextSiblingClass;

private:
    using ro_or_rw_ext_t = objc::PointerUnion<const class_ro_t, class_rw_ext_t, PTRAUTH_STR("class_ro_t"), PTRAUTH_STR("class_rw_ext_t")>;

    const ro_or_rw_ext_t get_ro_or_rwe() const ...

    void set_ro_or_rwe(const class_ro_t *ro) ...

    void set_ro_or_rwe(class_rw_ext_t *rwe, const class_ro_t *ro) ...

    class_rw_ext_t *extAlloc(const class_ro_t *ro, bool deep = false);

public:
    void setFlags(uint32_t set)
    ...

    void clearFlags(uint32_t clear) 
    ...

    // set and clear must not overlap
    void changeFlags(uint32_t set, uint32_t clear) 
    ...

    class_rw_ext_t *ext() const ...

    class_rw_ext_t *extAllocIfNeeded() ...

    class_rw_ext_t *deepCopy(const class_ro_t *ro) ...

    const class_ro_t *ro() const ...

    void set_ro(const class_ro_t *ro) ...

    const method_array_t methods() const 
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) 
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->methods;
         else 
            return method_array_tv.get<const class_ro_t *>(&ro_or_rw_ext)->baseMethods;
        
    

    const property_array_t properties() const 
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) 
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->properties;
         else 
            return property_array_tv.get<const class_ro_t *>(&ro_or_rw_ext)->baseProperties;
        
    

    const protocol_array_t protocols() const 
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) 
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->protocols;
         else 
            return protocol_array_tv.get<const class_ro_t *>(&ro_or_rw_ext)->baseProtocols;
        
    
;

可以看到有三个主要的方法:通过这三个方法分别能获取到类的方法、属性、协议。

属性列表打印

已知首地址以及 ISA占 8字节,superclass占8字节,cache占16字节,所以bits前面总共8+8+16 = 32字节, 可通过首地址平移32字节获取bits信息。

@interface MyPerson : NSObject 
    NSString *myHobby;
    NSString *myCars;


@property (nonatomic, strong) NSString *myName;
@property (nonatomic, assign) int age;

- (void)sayHello;
- (void)sayBye;
+ (void)sayFuckOff;

@end

lldb调试区调试:

bits 数据信息在前面已经讲过了,我们从读bits数据信息$2之后开始

  • p $3.properties()获得的属性列表的list结构, 其中list中的ptr就是属性数组的参数指针地址。(p $3.properties()命令中的properties方法是由class_rw_t提供的, 方法中返回的实际类型为property_array_t
  • p *$4.list.ptr读一下指针地址指向内容, 可看到获得属性list信息, count = 2, 也符合我们建的2个属性
  • p $5.get(0)可获取到myName对应属性(property_t) $6 = (name = "myName", attributes = "T@\\"NSString\\",&,N,V_myName")
  • p $5.get(1)可获取到age属性(property_t) $7 = (name = "age", attributes = "Ti,N,V_age")
  • p $5.get(2)数组越界, 因为我们只建立了2个属性

方法列表打印

先仔细看一下上面的那三个方法,可以看到一个叫class_rw_ext_t的东西,先了解一些东西:

struct class_rw_ext_t 
    DECLARE_AUTHED_PTR_TEMPLATE(class_ro_t)
    class_ro_t_authed_ptr<const class_ro_t> ro;
    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;
    char *demangledName;
    uint32_t version;
;

struct property_t 
    const char *name;
    const char *attributes;
;

struct method_t 
    static const uint32_t smallMethodListFlag = 0x80000000;

    method_t(const method_t &other) = delete;

    // The representation of a "big" method. This is the traditional
    // representation of three pointers storing the selector, types
    // and implementation.
    struct big 
        SEL name;
        const char *types;
        MethodListIMP imp;
    ;

private:
    ...
;

想必与属性列表不同,方法列表的相关内容nametypesimp储存在struct big里面(818新改动),所以获取方法列表里面的信息也要稍微变一下。看别人是这样取的:

人家使用p $5.get(0).big()可以取到,但我就不行,反而能用small()取到。通过观察,我只好换用getDescription()获取:

只获取了实例方法,没法获取类方法。或者我们直接上代码获取,哎,这里真的没太理解啊。代码获取:

Class metaClass = objc_getClass("MyPerson");
unsigned int count = 0;
Method *methods = class_copyMethodList(metaClass, &count);
NSLog(@"%u", count);
for (unsigned int i = 0; i < count; i++) 
	Method const method = methods[i];
	//获取方法名
	NSString *key = NSStringFromSelector(method_getName(method));
	NSLog(@"Method, name: %@", key);

metaClass = objc_getMetaClass("MyPerson");
count = 0;
methods = class_copyMethodList(metaClass, &count);
NSLog(@"%u", count);
for (unsigned int i = 0; i < count; i++) 
	Method const method = methods[i];
	//获取方法名
	NSString *key = NSStringFromSelector(method_getName(method));
	NSLog(@"Method, name: %@", key);

free(methods);

输出:

可以看出来,本类有6个方法(实例方法),元类有一个方法(类方法)。

class_ro_t

其实,我们刚刚也可以发现,成员变量以及类方法并没有在属性列表、方法列表里面。回过头我们再看struct class_rw_t,看到里面有这么一个方法:

const class_ro_t *ro() const 
	auto v = get_ro_or_rwe();
	if (slowpath(v.is<class_rw_ext_t *>())) 
		return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->ro;
	
	return v.get<const class_ro_t *>(&ro_or_rw_ext);

看下ro底层:

struct class_ro_t 
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif

    union 
        const uint8_t * ivarLayout;
        Class nonMetaclass;
    ;

    explicit_atomic<const char *> name;
    WrappedPtr<method_list_t, method_list_t::Ptrauth> baseMethods;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;

    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;

    // This field exists only when RO_HAS_SWIFT_INITIALIZER is set.
    _objc_swiftMetadataInitializer __ptrauth_objc_method_list_imp _swiftMetadataInitializer_NEVER_USE[0];

    _objc_swiftMetadataInitializer swiftMetadataInitializer() const ...

    const char *getName() const ...
    class_ro_t *duplicate() const ...

    Class getNonMetaclass() const ...

    const uint8_t *getIvarLayout() const ...
;

可以看到有方法、属性、协议和成员变量。但方法、属性、协议的命名都是base开头的。

探索成员变量

可看到const ivar_list_t * ivars;,有一个ivars属性(ivars: 实例变量),我们仿照下上面也读一下ro



  • p $3.ro()获得的ro的里面的信息
  • p $5.ivars获得的ivars_list_t即成员变量的列表

接下来我们仿照属性列表去读取,发现实例变量储存在ivars_list_t里面,同时也会发现还有属性的成员变量。

小结:

  • 通过XXXX 定义的成员变量,会存储在类的bits属性中,通过bits --> data() -->ro() --> ivars获取成员变量列表,除了包括成员变量,还包括属性的成员变量。
  • 通过@property定义的属性,存储在bits属性中,通过bits --> data() --> properties() --> list获取属性列表,其中只包含property属性。

探索类方法

所谓的对象/实例方法类方法其实是OC上层或者说苹果官方人为加入的概念, 其底层是都是函数,不区分+-。但实例方法与类方法还是有必要区分的,则苹果将实例方法存在类里面,而类方法存在元类里面。一方面避免对象存储太大会发生混乱,一方面也是为了有个调用区分。
所以说类方法要在元类中查找。
ojective-C学习笔记关于面向对象编程

[OC学习笔记]对象消息运行期

[OC学习笔记]对象的本质探索

Objective-C学习笔记(十九)——对象方法和类方法的相互调用

[OC学习笔记]ARC与引用计数

[OC学习笔记]ARC与引用计数