类的原理分析(上)——认识类的结构

694 阅读8分钟

类是我们iOS开发中的最小单元,作为一个合格的开发者,不仅要会简单的使用,更要对它的内部结构做到心中有数,这样我们才能用的更好!

一、isa和元类

在前面对于对象的探究中,我们了解到isa是类中很重要的东西,那么接下来,我们将用一个例子去探索说明isa和元类之间的关系。

  • 1、新建一个Person类,然后在main.m中,打印如下:

image.png

注:object_getClass 是runtime用于获取类方法的一个函数

通过添加断点的方式,我们可以在控制台,分别打印:p1,p2,p3,p4,p5的地址值。

其中:p1为实例对象,p2为LGPerson对象,p3为一个未知类,p4和p5地址一样,打印结果为NSObject。

  • 2、那么,p3是一个什么类呢?

我们把编译的二进制machO文件,使用MachOView分析查看。

image.png

通过我们对符号表的查看,发现,除了常规的__OBJC_CLASS_RO__LGPerson类之外,系统又创建了一个__OBJC_METACLASS_RO__LGPerson。

由此,我们猜想,未知类就是就是上面的p3的类型。

  • 3、如果 p3可以通过runtime的函数找到,那么,我们就顺藤摸瓜,通过runtime源码分析,类的内部机制。

image.png

  • 4、原来,实例对象去查找类,是通过isa找到的,那么接着往下找内部实现。

image.png

看到这里,查找的核心逻辑就找到了,接下来我们进行分析: if (fastpath(!isTaggedPointer())) return ISA();

① 先判断快速查找,如果存在isa,就返回。如果没有isa,就进行慢速查找.

② 然后slot = ptr & 0xf,然后根据得到的地址,取出objc_tag_classes中的类。

③ 最后判断取出的类,是不是Class类型,并且是objc_class类型,如果符合就对slot操作,先向右位移4个字节,然后对_OBJC_TAG_EXT_SLOT_MASK进行与预算。

④ 根据位移后的地址,取出对应的类。

  • 5、我们对isa进行MASK的与运算,如下:

(lldb) p/x p1
(LGPerson *) $0 = 0x000000010044c700
(lldb) p/x p2
(Class) $1 = 0x00000001000081f8 LGPerson
(lldb) p/x p3
(Class) $2 = 0x00000001000081d0
(lldb) p/x p4
(Class) $3 = 0x00007fff908200f0
(lldb) p/x p5
(Class) $4 = 0x00007fff908200f0

(lldb) x/4gx p1
0x10044c700: 0x001d8001000081f9 0x0000000000000000
0x10044c710: 0x50626154534e5b2d 0x65695672656b6369
(lldb) p/x 0x001d8001000081f9 & 0x00007ffffffffff8
(long) $6 = 0x00000001000081f8

(lldb) x/4gx $6
0x1000081f8: 0x00000001000081d0 0x00007fff90820118
0x100008208: 0x00007fff690cb140 0x0000801000000000
(lldb) p/x 0x00000001000081d0 & 0x00007ffffffffff8
(long) $7 = 0x00000001000081d0

(lldb) x/4gx $7
0x1000081d0: 0x00007fff908200f0 0x00007fff908200f0
0x1000081e0: 0x0000000100455dc0 0x0001e03100000003
(lldb) p/x 0x00007fff908200f0 & 0x00007ffffffffff8
(long) $8 = 0x00007fff908200f0

(lldb) x/4gx $8
0x7fff908200f0: 0x00007fff908200f0 0x00007fff90820118
0x7fff90820100: 0x00000001004077c0 0x0001e03100000003
(lldb) p/x 0x00007fff908200f0 & 0x00007ffffffffff8
(long) $9 = 0x00007fff908200f0

通过分析上述,可以得出以下结论:

p1 = 1=1 = 6 = 0x00000001000081f8 = p1 (InstanceOfClass)

p2 = 2=2 = 7 = 0x00000001000081d0 = p2 (LGPerson)

p3 = 3=3 = 8 = 0x00007fff908200f0 = p3 (LGPerson_MetaClass)

p4 = 4=4 = 9 = 0x00007fff908200f0 = p5 (NSObject)

上述的运算,都通过isa和mask进行运算,找到p1,p2,p3,p4之间的关系。
由此我们可以推理,p1(实例对象)通过isa可以获取p2(类对象),
p2(类对象)通过isa可以获取到p3(MetaClass元类对象),
p3(元类对象)通过isa又获取到p4(根元类对象),
p4通过isa获取到本身。
  • 6、于是,我们可以得出,isa与实例对象、类、元类、根类之间有一个链式走位的关系!接下来我们就继续探索!


二、isa的走位图和继承链

  • 1、我们通过分析上述结论,可以得出以下isa的关系图。

isa走位图.png

  • 2、了解了isa的走位图之后,我们发现还有一种类继承的关系。

image.png

通过API层面的调试,我们可以看出superclass的继承链关系,对照苹果官方给出的继承走位链,我们可以进一步认识实例化对象、类、元类、根元类、根根元类之间的联系。

isa流程图.png

分析:

① 图中的关系是由superclass与isa一起关联起来的

② superclass表示了继承的关系,isa表示了各个对象间的关联关系


  • 3、这里我们就简单分析到这里,关于更多isa和superclass的内容,将在后面专题分析。认识了isa之后,接下来我们就来了解下类的结构。


三、类的结构分析

1、源码分析

  • 1.1、不同代码环境的命名

    OC 环境objc环境
    NSObjectobjc_object
    Classobjc_class
    ......
  • 1.2、查看objc_class的结构

我们提取出关键部分的代码,便于我们接下来分析类的结构( objc_class的结构 )


#pragma mark - objc_class的结构

struct objc_class : objc_object {
    // Class ISA;
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;  //8bit
    Class superclass;                           //8bit
    cache_t cache;                              //8bit
    class_data_bits_t bits;                     //8bit 关键数据
    ...
    class_rw_t *data() const {
            return bits.data();                 //关键数据
     }
    ...
  }
    

经分析,isa为结构体指针,占用8字节;superclass也是结构体指针8字节,cache_t需要分析下结构,如下:其中


#pragma mark - cache_t

struct cache_t {
private:
    explicit_atomic<uintptr_t> _bucketsAndMaybeMask; // 8
    union {
        struct {
            explicit_atomic<mask_t>    _maybeMask; // 4
#if __LP64__
            uint16_t                   _flags;  // 2
#endif
            uint16_t                   _occupied; // 2
        };
        explicit_atomic<preopt_cache_t *> _originalPreoptCache; // 8
    };

  • 1.3、查看class_data_bits_t中的数据

struct class_data_bits_t {
    friend objc_class;

    // Values are the FAST_ flags above.
    uintptr_t bits;
    class_rw_t* data() const {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
}

class_data_bits_t中的一些关键数据格式为class_rw_t,接下来分析class_rw_t。


#pragma mark - class_rw_t

struct class_rw_t {
    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;
    
     const class_ro_t *ro() const              {...} //ro信息
     const method_array_t methods() const      {...} //方法数组
     const property_array_t properties() const {...} //属性数组
     const protocol_array_t protocols() const  {...} //协议数组
}

接下来分别查看class_ro_t、method_array_t、property_array_t、protocol_array_t的结构:

#pragma mark -  class_ro_t
struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
    union {
        const uint8_t * ivarLayout;
        Class nonMetaclass;
    };
    explicit_atomic<const char *> name;
    void *baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;
    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;
 }

#pragma mark -  method_array_t
class method_array_t : 
    public list_array_tt<method_t, method_list_t, method_list_t_authed_ptr>
{}

#pragma mark - 方法体method_t的结构
struct method_t {
    static const uint32_t smallMethodListFlag = 0x80000000;
    method_t(const method_t &other) = delete;
    struct big {
        SEL name;
        const char *types;
        MethodListIMP imp;
    };
}

#pragma mark - property_array_t
class property_array_t : 
    public list_array_tt<property_t, property_list_t, RawPtr>{}
    
#pragma mark - property_t
struct property_t {
    const char *name;
    const char *attributes;
};

#pragma mark - protocol_array_t
class protocol_array_t : 
    public list_array_tt<protocol_ref_t, protocol_list_t, RawPtr>{}
#pragma mark - protocol_list_t
struct protocol_list_t {
    // count is pointer-sized by accident.
    uintptr_t count;
    protocol_ref_t list[0]; // variable-size

    size_t byteSize() const {
        return sizeof(*this) + count*sizeof(list[0]);
    }
    ...
}

通过class_rw_t的结构,我们可以知道,内部存储的信息,主要有方法、属性、协议。然后我们再看另外一个成员变量class_ro_t *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;
    // With ptrauth, this is signed if it points to a small list, but
    // may be unsigned if it points to a big list.
    void *baseMethodList;             //方法列表
    protocol_list_t * baseProtocols;  //协议列表
    const ivar_list_t * ivars;        //成员变量列表

    const uint8_t * weakIvarLayout;   //成员变量布局
    property_list_t *baseProperties;  //属性列表

  • 1.4、既然类的基本布局信息,已经明白,那么我们通过实际项目案例去验证下。下面将通过LLDB调试,分析方法、属性、成员变量的分布情况。

四、指针和内存平移

  • 1、新建Person类,提供方法、属性、成员变量,然后再main.m中调用,如下:

image.png

image.png

  • 2、下面就开始LLDB,分步分析过程:

① 通过 x/4gx 获取Person类的内存结构信息

(lldb) x/4gx LGPerson.class
0x100008408: 0x0000000100008430 0x0000000100357140
0x100008418: 0x0000000101236020 0x0002802800000003

② 获取到首地址,根据上面objc_class结构,通过32位地址偏移,可以得到bits数据,class_data_bits_t类型。


(lldb) p/x 0x100008428 // = 0x100008408 + 0x20
(long) $1 = 0x0000000100008428

(lldb) p (class_data_bits_t *)$1
(class_data_bits_t *) $2 = 0x0000000100008428

③ 拿到bits之后,通过内部成员变量data,获取内部结构为class_rw_t的值


(lldb) p $2->data()
(class_rw_t *) $3 = 0x0000000101235fe0

(lldb) p *$3
(class_rw_t) $4 = {
  flags = 2148007936
  witness = 1
  ro_or_rw_ext = {
    std::__1::atomic<unsigned long> = {
      Value = 4295000456
    }
  }
  firstSubclass = nil
  nextSiblingClass = NSUUID
}

然后,通过 class_rw_t 的结构,我们可以获取其中的方法、属性、协议信息。


//获取类内部存储的方法信息

(lldb) p $4.methods()
(const method_array_t) $5 = {
  list_array_tt<method_t, method_list_t, method_list_t_authed_ptr> = {
     = {
      list = {
        ptr = 0x00000001000081d0
      }
      arrayAndFlag = 4295000528
    }
  }
}

(lldb) p $5.list
(const method_list_t_authed_ptr<method_list_t>) $6 = {
  ptr = 0x00000001000081d0
}

(lldb) p $6.ptr
(method_list_t *const) $7 = 0x00000001000081d0

(lldb) p *$7
(method_list_t) $8 = {
  entsize_list_tt<method_t, method_list_t, 4294901763, method_t::pointer_modifier> = (entsizeAndFlags = 27, count = 6)
}

(lldb) p $8.get(0).big()
(method_t::big) $9 = {
  name = "saySomething"
  types = 0x0000000100003f69 "v16@0:8"
  imp = 0x0000000100003c60 (KCObjcBuild`-[LGPerson saySomething])
}
...

上面是获取的类的方法信息,以下是获取属性信息

(lldb) p $4.properties()
(const property_array_t) $12 = {
  list_array_tt<property_t, property_list_t, RawPtr> = {
     = {
      list = {
        ptr = 0x00000001000082d0
      }
      arrayAndFlag = 4295000784
    }
  }
}

(lldb) p $12.list
(const RawPtr<property_list_t>) $13 = {
  ptr = 0x00000001000082d0
}

(lldb) p $13.ptr
(property_list_t *const) $14 = 0x00000001000082d0

(lldb) p *$14
(property_list_t) $15 = {
  entsize_list_tt<property_t, property_list_t, 0, PointerModifierNop> = (entsizeAndFlags = 16, count = 2)
}

(lldb) p $15.get(0)
(property_t) $16 = (name = "name", attributes = "T@\"NSString\",C,N,V_name")

(lldb) p $15.get(1)
(property_t) $17 = (name = "age", attributes = "Ti,N,V_age")

...

通过本案例的LLDB,我们可以查找到类中包含的属性和实例方法。

类中存储的成员变量和对象方法,又存储在哪里呢?接下来让我们继续探索。

  • 3、LLDB分析类中的成员变量和对象方法

为了节省篇幅长度,这里就不分段介绍,直接展示打印结果:

(lldb) x/4gx LGPerson.class
0x100008408: 0x0000000100008430 0x0000000100357140
0x100008418: 0x000000010067d9b0 0x0002802800000003

(lldb) p/x (class_data_bits_t *)0x100008428
(class_data_bits_t *) $1 = 0x0000000100008428

(lldb) p/x $1->data()
(class_rw_t *) $2 = 0x000000010067d970

(lldb) p/x $2->ro()
(const class_ro_t *) $3 = 0x0000000100008188

(lldb) p/x *$3
(const class_ro_t) $4 = {
  ...
  baseMethodList = 0x00000001000081d0
  baseProtocols = 0x0000000000000000
  ivars = 0x0000000100008268
  weakIvarLayout = 0x0000000000000000
  baseProperties = 0x00000001000082d0
  _swiftMetadataInitializer_NEVER_USE = {}
}

(lldb) p/x $4.ivars
(const ivar_list_t *const) $5 = 0x0000000100008268

(lldb) p/x *$5
(const ivar_list_t) $6 = {
  entsize_list_tt<ivar_t, ivar_list_t, 0, PointerModifierNop> = (entsizeAndFlags = 0x00000020, count = 0x00000003)
}

(lldb) p/x $6.get(0) // 拿到成员变量 hobby 调试结束
(ivar_t) $7 = {
  offset = 0x00000001000083a0
  name = 0x0000000100003e18 "hobby"
  type = 0x0000000100003f55 "@\"NSString\""
  alignment_raw = 0x00000003
  size = 0x00000008
}

经过同样的方式,我们查看baseMethodList中的值,发现依然找不到对象方法的信息,猜想对象方法可能不存储在类中,我们知道有对象、类、元类,那么对象方法会不会存在元类中?

我们可以通过isa去找到元类,然后用相同的方式进行调试,获取到,由于步骤相同和方法相同,这里就不做演示。

五、总结

本篇文章,主要分析了类的结构,并通过LLDB调试的方式,分析了类的成员变量、属性、方法、协议等存储的位置,从底层的角度认识了类的存储过程。

写在最后,类的分析是一个系统的过程,先从基本的结构去分析,把握类的存储位置,为后面学习和认识类的相关操作做好扎实的基础!