OC底层探究之类的底层原理结构

一、通过isa分析到元类

《OC底层探究之对象的本质以及isa》文章中我们知道了如何通过isa找到,即isa&掩码

image-20210617153942649

那么是否还有isa呢?

我们把转为16进制打印出来:

(lldb) p/x 0x011d8001000080f9 & 0x00007ffffffffff8
(long) $2 = 0x00000001000080f8
复制代码

再打印其内存

(lldb) x/4gx 0x00000001000080f8
0x1000080f8: 0x00000001000080d0 0x00007fff8061d008
0x100008108: 0x00007fff2020eaf0 0x0000801000000000
复制代码

与我们打印对象的情况非常相似,那么继续按照isa的方式进行:

image-20210617154314056

发现isa中也是

那么这2个类是否一样呢?把新得到的转为16进制打印:

(lldb) p/x 0x00000001000080d0 & 0x00007ffffffffff8
(long) $4 = 0x00000001000080d0
复制代码

发现最开始的类(0x00000001000080f8)与后面的这个类(0x00000001000080d0)并不一样!

那么类是否和对象一样在内存中会不断的开辟内存空间,会存在多个类呢?

那么我们来验证一下:

image-20210617154501845

发现都是一样的!且都是第一个类(0x00000001000080f8)

那么后面的这个类(0x00000001000080d0)是什么呢?

把编译好的Mach-O文件导入到烂苹果里面去看一看:

image-20210617155752663

在符号表里面就会看到一个METACLASS,这就是后面的那个,也就是元类元类是由系统生成和编译的!

整理一下流程,如下图所示:

发现元类

二、isa走位图以及继承链

1、isa走位图

那么元类isa中还会有其他的类吗?

我们继续探索:

(lldb) x/4gx 0x00000001000080d0
0x1000080d0: 0x00007fff8061cfe0 0x00007fff8061cfe0
0x1000080e0: 0x00000001089bab10 0x0002e03100000003
(lldb) p/x 0x00007fff8061cfe0 & 0x00007ffffffffff8
(long) $5 = 0x00007fff8061cfe0
复制代码

发现元类的isaisa中的类是完全一样的!

打印其内存地址

(lldb) x/4gx 0x00007fff8061cfe0
0x7fff8061cfe0: 0x00007fff8061cfe0 0x00007fff8061d008
0x7fff8061cff0: 0x0000000108b09aa0 0x0002e03100000007
复制代码

发现这个类的isa是完全一样的!

打印一下这个特殊的,看看是什么:

image-20210617162144934

这个就NSObject——根元类

那么我们再来探索一下NSObject

(lldb) p/x NSObject.class
(Class) $8 = 0x00007fff8061d008 NSObject
复制代码

发现NSObject和我们打印的不一样,继续探索:

(lldb) x/4gx 0x00007fff8061d008
0x7fff8061d008: 0x00007fff8061cfe0 0x0000000000000000
0x7fff8061d018: 0x0000000108b09c70 0x0002801000000003
(lldb) p/x 0x00007fff8061cfe0 & 0x00007ffffffffff8
(long) $10 = 0x00007fff8061cfe0
复制代码

发现NSObjectisa中的就是isa自己!

通过代码验证一下:

image-20210617164634183

和我们探索的一致,于是我们可以得出一个isa的走位图

根元类

2、继承链

OC对象是有继承链的,那么元类是否也有继承链呢?

我们打印一下HPerson元类的父类

image-20210617174233163

发现HPerson的元类的父类是NSObject,即根元类

那么是不是元类的父类就是根元类呢?

如果HPerson父类呢?HPerson元类的父类又会是什么呢?

新建一个KPerson类,让KPerson继承于HPerson,打印其父类

image-20210617173208973

发现KPerson元类的父类HPerson

这意味着元类也有继承链

那么根元类父类又是什么呢?

我们打印一下:

image-20210617174538886

发现根元类父类就是NSObject,即根类

那么根类父类呢?

再来打印一下:

image-20210617175851020

发现根类没有父类

所以我们得出一个继承链的图:

继承图

3、总结

根据isa走位图继承链就可以还原到isa的经典流程图

isa流程图

根据图片可以看出一共有3条路线:

  1. isa路线:对象->类->元类->根元类->根类->nil
  2. 类的继承:对象->类->父类->根类->nil
  3. 元类的继承:对象->类->元类->元类的父类->根元类->根类->nil

三、源码分析类的结构

我们已经知道了一个对象内存空间存储着isa成员变量,那么一个内存空间呢?存储些什么呢?

我们再objc源码里面搜索objc_class,就可以看到class这个结构体:

/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

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
};
复制代码

可以发现继承于objc_object,拥有4成员变量,其中ISA是继承于objc_object

那么剩下的3个,superclass明显可以看出是父类,另外2个目前还需要我们继续探索!

先看bits,注释写着是class_rw_t!在objc_class结构体中往下找就可以找到:

class_rw_t *data() const {
        return bits.data();
    }
复制代码

然后进入到class_rw_t结构体:

image-20210617210541791

就会发现里面有方法列表属性列表协议列表等等!

那我们应该如何去查看内存结构呢?

四、指针和内存平移

1、指针

  • 普通指针:

    image-20210618112103234

    可以看出,这2内存地址不一样,但是却是指向了同一个!也就是我们经常听到的值copy

  • 对象指针:

    image-20210618133828194

    可以看出,指针地址不同,内存地址也不同!

    2种情况可以一个来表述:

    指针

  • 数组指针:

image-20210618140232152

可以看出数组第一个元素地址就是首地址,以及数组指针加减是依照数组的类型大小移动的!

那么我们是不是可以直接移动指针位置来获取数据呢?

2、内存平移

通过移动指针地址来取值:

image-20210618140647166

我们发现,内存是可以进行偏移的,偏移之后取地址的值就可以得到内存中存储的了,这就是取值操作

我们可以获取内存地址,是不是可以通过内存偏移来获取中存储的数据呢?

五、类的结构内存计算

我们打开objc源码工程(《OC底层探究之alloc探索》文章中有说明),打印HPerson类的内存:

(lldb) x/6gx HPerson.class
0x100008238: 0x0000000100008210 0x0000000108679140
0x100008248: 0x0000000108671380 0x0000802c00000000
0x100008258: 0x0000000108f2a234 0x00000002000b9980
复制代码

根据前面探索的类的结构可知,第一个8字节是isa(class类型,即结构体指针),第二个8字节为superclass(class类型,即结构体指针),打印一下superclass

(lldb) po 0x0000000108679140
NSObject
复制代码

正是HPerson类的父类NSObject,我们还可以验证一下:

(lldb) p/x NSObject.class
(Class) $2 = 0x0000000108679140 NSObject
复制代码

HPerson类的superclass内存地址一样!

类结构剩下的2个元素cachebits大小我们并不知道,所以不知道该怎么读!

继续探索一下cache_t源码:

struct cache_t {
private:
    explicit_atomic<uintptr_t> _bucketsAndMaybeMask;
    union {
        struct {
            explicit_atomic<mask_t>    _maybeMask;
#if __LP64__
            uint16_t                   _flags;
#endif
            uint16_t                   _occupied;
        };
        explicit_atomic<preopt_cache_t *> _originalPreoptCache;
    };
    //省略方法、全局变量等等,方法在方法区、全局变量在全局区不占用结构体内存
}
复制代码

发现cache_t结构体里面有一个explicit_atomic<uintptr_t>和一个联合体

进入explicit_atomic结构体:

// Version of std::atomic that does not allow implicit conversions
// to/from the wrapped type, and requires an explicit memory order
// be passed to load() and store().
template <typename T>
struct explicit_atomic : public std::atomic<T> {
    explicit explicit_atomic(T initial) noexcept : std::atomic<T>(std::move(initial)) {}
    operator T() const = delete;
    
    T load(std::memory_order order) const noexcept {
        return std::atomic<T>::load(order);
    }
    void store(T desired, std::memory_order order) noexcept {
        std::atomic<T>::store(desired, order);
    }
    
    // Convert a normal pointer to an atomic pointer. This is a
    // somewhat dodgy thing to do, but if the atomic type is lock
    // free and the same size as the non-atomic type, we know the
    // representations are the same, and the compiler generates good
    // code.
    static explicit_atomic<T> *from_pointer(T *ptr) {
        static_assert(sizeof(explicit_atomic<T> *) == sizeof(T *),
                      "Size of atomic must match size of original");
        explicit_atomic<T> *atomic = (explicit_atomic<T> *)ptr;
        ASSERT(atomic->is_lock_free());
        return atomic;
    }
};
复制代码

发现explicit_atomic结构体是一个泛型类型,它的真正大小是来自于泛型

explicit_atomic<uintptr_t>的大小取决于uintptr_t,即为8字节:

(lldb) po sizeof(uintptr_t)
8
复制代码

再来看联合体,联合体互斥,联合体里面一个结构体和一个explicit_atomic<preopt_cache_t *>类型,直接看explicit_atomic<preopt_cache_t *>类型,explicit_atomic<preopt_cache_t *>的大小取决于preopt_cache_t *preopt_cache_t *指针类型,即大小为8

所以cache_t类型的大小为16

根据类的结构可以知道bits的地址是在类的首地址加上32字节,转为16进制即为20,然后强转为class_data_bits_t*类型:

image-20210618164954540

六、lldb分析类的结构

我们之前已经分析了bitsclass_rw_t*类型:

class_rw_t *data() const {
        return bits.data();
    }
复制代码

所以可以通过我们强转为class_data_bits_t*的变量$4去执行data方法,因为$4指针,所以使用->:

(lldb) p $4->data()
(class_rw_t *) $5 = 0x0000000108f2a230
复制代码

得到了class_rw_t *类型的变量$5,继续打印$5

(lldb) p *$5
(class_rw_t) $6 = {
  flags = 2148007936
  witness = 0
  ro_or_rw_ext = {
    std::__1::atomic<unsigned long> = {
      Value = 4295000184
    }
  }
  firstSubclass = nil
  nextSiblingClass = NSUUID
}
复制代码

得到了class_rw_t

但是没有我们想要的方法列表属性列表等等,怎么办呢?

和刚刚一样,我们调用获取属性列表的方法:

(lldb) p $6.properties()
(const property_array_t) $7 = {
  list_array_tt<property_t, property_list_t, RawPtr> = {
     = {
      list = {
        ptr = 0x00000001000081c0
      }
      arrayAndFlag = 4295000512
    }
  }
}
复制代码

但是还是没有属性,怎么办呢?

我们先看一下property_array_t,从properties方法进入:

class property_array_t : 
    public list_array_tt<property_t, property_list_t, RawPtr>
{
    typedef list_array_tt<property_t, property_list_t, RawPtr> Super;

 public:
    property_array_t() : Super() { }
    property_array_t(property_list_t *l) : Super(l) { }
};
复制代码

再进入到list_array_tt,就看到注释:

/***********************************************************************
* list_array_tt<Element, List, Ptr>
* Generic implementation for metadata that can be augmented by categories.
*
* Element is the underlying metadata type (e.g. method_t)
* List is the metadata's list type (e.g. method_list_t)
* List is a template applied to Element to make Element*. Useful for
* applying qualifiers to the pointer type.
*
* A list_array_tt has one of three values:
* - empty
* - a pointer to a single list
* - an array of pointers to lists
*
* countLists/beginLists/endLists iterate the metadata lists
* count/begin/end iterate the underlying metadata elements
**********************************************************************/
复制代码

可以得出list是元数据的列表!

那么我们继续在lldb中查看list

(lldb) p $7.list
(const RawPtr<property_list_t>) $8 = {
  ptr = 0x00000001000081c0
}
复制代码

发现又进了一层,继续打印ptr

(lldb) p $8.ptr
(property_list_t *const) $9 = 0x00000001000081c0
复制代码

然后还原property_list_t

(lldb) p *$9
(property_list_t) $10 = {
  entsize_list_tt<property_t, property_list_t, 0, PointerModifierNop> = (entsizeAndFlags = 16, count = 2)
}
复制代码

得到了property_list_t就是属性列表,count = 2

然后打印类的属性

(lldb) p $10.get(0)
(property_t) $11 = (name = "name", attributes = "T@\"NSString\",&,N,V_name")
(lldb) p $10.get(1)
(property_t) $12 = (name = "nickName", attributes = "T@\"NSString\",&,N,V_nickName")
复制代码

成功打印出类的属性,以下是HPerson类:

image-20210618172109118

展示一下全过程:

image-20210618171543536

七、类的bit数据分析

给类里面添加上类方法属性方法

image-20210618173555843

按照获取属性的方式,我们获取一下方法

(lldb) x/6gx HPerson.class
0x100008220: 0x00000001000081f8 0x0000000108679140
0x100008230: 0x0000000108671380 0x0000802400000000
0x100008240: 0x0000000108c7d4b4 0x00000002000b9980
(lldb) p (class_data_bits_t *)0x100008240
(class_data_bits_t *) $1 = 0x0000000100008240
(lldb) p $1->data()
(class_rw_t *) $2 = 0x0000000108c7d4b0
(lldb) p $2.methods()
(const method_array_t) $3 = {
  list_array_tt<method_t, method_list_t, method_list_t_authed_ptr> = {
     = {
      list = {
        ptr = 0x00000001000080d8
      }
      arrayAndFlag = 4295000280
    }
  }
}
  Fix-it applied, fixed expression was: 
    $2->methods()
(lldb) p $3.list
(const method_list_t_authed_ptr<method_list_t>) $4 = {
  ptr = 0x00000001000080d8
}
(lldb) p $4.ptr
(method_list_t *const) $5 = 0x00000001000080d8
(lldb) p *$5
(method_list_t) $6 = {
  entsize_list_tt<method_t, method_list_t, 4294901763, method_t::pointer_modifier> = (entsizeAndFlags = 27, count = 6)
}
复制代码

可以看到有6个方法,我们来打印一下方法:

(lldb) p $6.get(0)
(method_t) $7 = {}
(lldb) p $6.get(1)
(method_t) $8 = {}
复制代码

发现为

为什么呢?

我们看一看源码,找到属性列表property_list_t结构体:

struct property_list_t : entsize_list_tt<property_t, property_list_t, 0> {
};
复制代码

里面是空的,我们获取的是property_t,继续找property_t

struct property_t {
    const char *name;
    const char *attributes;
};
复制代码

发现里面只有nameattributes这2个成员变量,和我们打印的信息相符!

在看方法列表,找到method_array_t

// Two bits of entsize are used for fixup markers.
// Reserve the top half of entsize for more flags. We never
// need entry sizes anywhere close to 64kB.
//
// Currently there is one flag defined: the small method list flag,
// method_t::smallMethodListFlag. Other flags are currently ignored.
// (NOTE: these bits are only ignored on runtimes that support small
// method lists. Older runtimes will treat them as part of the entry
// size!)
struct method_list_t : entsize_list_tt<method_t, method_list_t, 0xffff0003, method_t::pointer_modifier> {
    bool isUniqued() const;
    bool isFixedUp() const;
    void setFixedUp();

    uint32_t indexOfMethod(const method_t *meth) const {
        uint32_t i = 
            (uint32_t)(((uintptr_t)meth - (uintptr_t)this) / entsize());
        ASSERT(i < count);
        return i;
    }

    bool isSmallList() const {
        return flags() & method_t::smallMethodListFlag;
    }

    bool isExpectedSize() const {
        if (isSmallList())
            return entsize() == method_t::smallSize;
        else
            return entsize() == method_t::bigSize;
    }

    method_list_t *duplicate() const {
        method_list_t *dup;
        if (isSmallList()) {
            dup = (method_list_t *)calloc(byteSize(method_t::bigSize, count), 1);
            dup->entsizeAndFlags = method_t::bigSize;
        } else {
            dup = (method_list_t *)calloc(this->byteSize(), 1);
            dup->entsizeAndFlags = this->entsizeAndFlags;
        }
        dup->count = this->count;
        std::copy(begin(), end(), dup->begin());
        return dup;
    }
};
复制代码

里面有很多内容,但是我们需要获取的是method_t,继续看method_t

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;
    };
public:
    big &big() const {
        ASSERT(!isSmall());
        return *(struct big *)this;
    }
    //其余内容省略
}
复制代码

发现里面没有成员变量,但是有一个big结构体big方法,所以我们应该打印big结构体

(lldb) p $6.get(0).big()
(method_t::big) $10 = {
  name = "printName"
  types = 0x0000000100003f77 "v16@0:8"
  imp = 0x0000000100003d80 (HObjectBuild`-[HPerson printName])
}
(lldb) p $6.get(1).big()
(method_t::big) $11 = {
  name = "name"
  types = 0x0000000100003f8b "@16@0:8"
  imp = 0x0000000100003d90 (HObjectBuild`-[HPerson name])
}
(lldb) p $6.get(2).big()
(method_t::big) $12 = {
  name = ".cxx_destruct"
  types = 0x0000000100003f77 "v16@0:8"
  imp = 0x0000000100003e30 (HObjectBuild`-[HPerson .cxx_destruct])
}
(lldb) p $6.get(3).big()
(method_t::big) $13 = {
  name = "setName:"
  types = 0x0000000100003f93 "v24@0:8@16"
  imp = 0x0000000100003db0 (HObjectBuild`-[HPerson setName:])
}
(lldb) p $6.get(4).big()
(method_t::big) $14 = {
  name = "nickName"
  types = 0x0000000100003f8b "@16@0:8"
  imp = 0x0000000100003de0 (HObjectBuild`-[HPerson nickName])
}
(lldb) p $6.get(5).big()
(method_t::big) $15 = {
  name = "setNickName:"
  types = 0x0000000100003f93 "v24@0:8@16"
  imp = 0x0000000100003e00 (HObjectBuild`-[HPerson setNickName:])
}
复制代码

方法就被正确的打印出来了!

但是,类方法却没有,为什么呢?因为类方法元类里面!

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享