iOS 手把手探索类的结构

1、前言

在项目中,咱们常常建立类,而后经过类去 alloc 咱们所须要的对象,那咱们类的结构在底层是什么样的呢?咱们在类中所写的属性和方法又被 Xcode 编译到哪里去了呢?接下来针对这两个问题来一探究竟。ios

2、类的结构

  • 准备工做 首先咱们建立一个 Person,并增长一些属性、成员变量和方法,而后在 main 方法里面 alloc 对象。
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [Person alloc];
        NSLog(@"%@",person);
    }
    return 0;
}
复制代码

Person对象

1.clang 编译成 cpp 文件的几种方式

// 经常使用方式
clang -rewrite-objc main.m -o main.cpp

// 存在UIKit等其余动态引用库时
clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot/Application/Xcode.app/Comtents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.0.sdk main.m

// 模拟器
xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp

// 真机
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp

复制代码

2.经过 cpp 文件查看类的结构

因为 cpp 文件代码太多,不方便查看,咱们能够经过 command + F 搜索 Person,如下代码是我精简过的代码:数组

typedef struct objc_object Person;
typedef struct objc_class *Class;
struct objc_object {
    Class _Nonnull isa __attribute__((deprecated));
};

__OBJC_RW_DLLIMPORT struct objc_class *objc_getClass(const char *);//获取当前类
__OBJC_RW_DLLIMPORT struct objc_class *class_getSuperclass(struct objc_class *);//获取superClass
__OBJC_RW_DLLIMPORT struct objc_class *objc_getMetaClass(const char *);//获取元类
复制代码

由此能够得出咱们的 Person 在底层是一个 objc_object 结构体,结构体里面有一个传说中的 isa ,这个 isa 又是一个 objc_class 结构体指针。那这个 objc_objectobjc_class 又是一个什么鬼?它们两个又有什么关系?带着这样的疑问,咱们来到 Objc 源码中一探究竟。缓存

3、objc_class 的结构分析

1.struct objc_class 的源码分析

咱们在源码中全局搜索 struct objc_class,能获得一些重要的信息bash

  • #if !__OBJC2__ 宏定义看出这个是老版本的 objc_class 结构,因此稍做了解一下,不作重点研究对象。
struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY; // isa 指针

#if !__OBJC2__
    Class _Nullable super_class   // 父类                           OBJC2_UNAVAILABLE;
    const char * _Nonnull name   // 类的名称                         OBJC2_UNAVAILABLE;
    long version                // 版本                             OBJC2_UNAVAILABLE;
    long info                   // 信息                             OBJC2_UNAVAILABLE;
    long instance_size          // 对象大小                             OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars   // 成员变量列表               OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists    // 方法列表                   OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache     // 缓存                     OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols      // 协议列表     OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */
复制代码

接下来咱们来到重点 objc-runtime-new.h 文件中找到 objc_class 结构体,发现 objc_class 是继承自 objc_object 的结构体,而 objc_object 里面自带一个 isa 指针,因此上面的一切的一切又是那么的天然。app

struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

struct objc_class : objc_object {
    // Class ISA;     // 隐藏的isa指针,占 8 字节
    Class superclass; // 父类, 占 8 字节
    cache_t cache;    // 缓存, 占 16 字节 
    class_data_bits_t bits; // 类的字节数据,占 8 字节
    
    class_rw_t *data() { 
        return bits.data();
    }
    
    /*** 此处省略若干行代码  ***/
}
复制代码

从上面的源码中发现,咱们能够 bits 这个属性入手,而下面恰好又有经过 data() 方法获取 bits 里面的数据,返回值是 class_rw_t 类型,因此查看一下 class_rw_t 里面都有什么东西。iphone

2.class_rw_t 源码分析

struct class_rw_t {
    uint32_t flags;
    uint32_t version;
    const class_ro_t *ro;
    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;

    /*** 此处省略若干行代码  ***/
}
复制代码

class_rw_t 里面咱们对 methodspropertiesprotocols 都很熟悉,这不就是咱们要找的方法列表、属性列表和协议列表吗,可是会不会以为有一丝丝的怪,这要怎么去验证就是咱们要找的呢?并且成员变量 ivars 哪里去了呢?别急,接下来咱们经过 LLDB 一步一步去验证。源码分析

3.LLDB 验证

为了节省你们的时间,下面验证的代码我会贴出来,你们能够直接复制过去验证,不过就是排版不是很好看。ui

(lldb) x/4gx pClass    //** 咱们经过 x/4gx pClass 指令将类的地址经过16进制打印出来
0x100002648: 0x001d800100002621 0x0000000100aff140
0x100002658: 0x00000001003a2290 0x0000000000000000

(lldb) po 0x0000000100aff140
NSObject
// po 0x0000000100aff140 获得 NSObject,由此能够验证咱们从源码中发现 objc_class 结构体
// 第一个是隐藏的 isa 指针,第二个是 superclass

(lldb) p/x 0x100002648 + 0x20     //** 经过地址偏移获得 bits
(long) $1 = 0x0000000100002668
(lldb) p (class_data_bits_t *)0x0000000100002668 //** 将 bits 强转 class_data_bits_t 类型
(class_data_bits_t *) $2 = 0x0000000100002668
(lldb) p $2->data()      //** 经过 bits 的 data() 方法获得 class_rw_t
(class_rw_t *) $3 = 0x0000000102020850
(lldb) p *$3            //** 打印 class_rw_t 的内容
(class_rw_t) $4 = {
  flags = 2148139008
  version = 0
  ro = 0x0000000100002530
  methods = {
    list_array_tt<method_t, method_list_t> = {
       = {
        list = 0x0000000100002438
        arrayAndFlag = 4294976568
      }
    }
  }
  properties = {
    list_array_tt<property_t, property_list_t> = {
       = {
        list = 0x0000000100002518
        arrayAndFlag = 4294976792
      }
    }
  }
  protocols = {
    list_array_tt<unsigned long, protocol_list_t> = {
       = {
        list = 0x0000000000000000
        arrayAndFlag = 0
      }
    }
  }
  firstSubclass = nil
  nextSiblingClass = Person
  demangledName = 0x0000000000000000
}

(lldb) p $4.properties      //** 打印出 properties
(property_array_t) $5 = {
  list_array_tt<property_t, property_list_t> = {
     = {
      list = 0x0000000100002518
      arrayAndFlag = 4294976792
    }
  }
}
(lldb) p $5.list           //** 经过 list 打印出属性
(property_list_t *) $6 = 0x0000000100002518
(lldb) p $6->first
(property_t) $9 = (name = "nickName", attributes = "T@\"NSString\",C,N,V_nickName")

(lldb) p $4.methods      //** 打印出 methods
(method_array_t) $10 = {
  list_array_tt<method_t, method_list_t> = {
     = {
      list = 0x0000000100002438
      arrayAndFlag = 4294976568
    }
  }
}
(lldb) p $10.list       //** 经过 methods 打印出方法
(method_list_t *) $11 = 0x0000000100002438
(lldb) p $11->get(0)
(method_t) $12 = {
  name = "sayHello"
  types = 0x0000000100001f8d "v16@0:8"
  imp = 0x0000000100001ad0 (LGTest`-[LGPerson sayHello] at LGPerson.m:13)
}
复制代码

经过上面的 LLDB 调试,发现 class_rw_t 里面只找到了 propertiesmethods,那咱们的 ivars 哪去了呢?还有咱们的类方法又存到哪里去了呢? 咱们回到 class_rw_t 结构体发现 const class_ro_t *ro 属性,点进去一看,柳暗花明又一村啊!spa

4.class_ro_t 源码分析

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif
    const uint8_t * ivarLayout;
    const char * name;
    method_list_t * baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;
    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;
    method_list_t *baseMethods() const {
        return baseMethodList;
    }
};
复制代码

class_ro_t 源码中,貌似类的结构一目了然,话很少说,直接经过 LLDB 验证。指针

(lldb) x/4gx pClass
0x1000025f0: 0x001d8001000025c9 0x0000000100aff140
0x100002600: 0x00000001003a2290 0x0000000000000000
(lldb) p/x 0x1000025f0 + 0x20 
(long) $1 = 0x0000000100002610
(lldb) p (class_data_bits_t *)0x0000000100002610 
(class_data_bits_t *) $2 = 0x0000000100002610
(lldb) p $2->data()
(class_rw_t *) $3 = 0x0000000101ebccf0
(lldb) p $3->ro             // 打印出 ro
(const class_ro_t *) $4 = 0x0000000100002388
(lldb) p *$4
(const class_ro_t) $5 = {
  flags = 388
  instanceStart = 8
  instanceSize = 24
  reserved = 0
  ivarLayout = 0x0000000100001f11 "\x02"
  name = 0x0000000100001f0a "Person"
  baseMethodList = 0x00000001000022c0
  baseProtocols = 0x0000000000000000
  ivars = 0x0000000100002328
  weakIvarLayout = 0x0000000000000000
  baseProperties = 0x0000000100002370
}

(lldb) p $5.baseProperties      // 经过 ro 打印出 baseProperties 
(property_list_t *const) $6 = 0x0000000100002370
(lldb) p $6[0]                  // 经过 baseProperties 数组取出属性
(property_list_t) $7 = {
  entsize_list_tt<property_t, property_list_t, 0> = {
    entsizeAndFlags = 16
    count = 1
    first = (name = "nickName", attributes = "T@\"NSString\",C,N,V_nickName")
  }
}

(lldb) p $5.ivars               // 经过 ivars 取出成员变量
(const ivar_list_t *const) $11 = 0x0000000100002328
(lldb) p *$11
(const ivar_list_t) $12 = {
  entsize_list_tt<ivar_t, ivar_list_t, 0> = {
    entsizeAndFlags = 32
    count = 2
    first = {
      offset = 0x00000001000025a8
      name = 0x0000000100001f52 "hobby"
      type = 0x0000000100001fa8 "@\"NSString\""
      alignment_raw = 3
      size = 8
    }
  }
}

(lldb) p $5.baseMethodList     // 经过 baseMethodList 取出方法
(method_list_t *const) $13 = 0x00000001000022c0
(lldb) p *$13
(method_list_t) $14 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 4
    first = {
      name = "sayHello"
      types = 0x0000000100001f8d "v16@0:8"
      imp = 0x00000001000014b0 (LGTest`-[Person sayHello] at Person.m:12)
    }
  }
}
复制代码

经过上述 LLDB 验证得出,类的属性、成员变量、方法、协议等都是存在了 objc_class 结构体里面 bits 中的 rw 中的 ro 属性里。

5.如何找到类方法

首先说两个概念,知道这两个概念以后,就好办了。

  • 对象方法是存在类里面的
  • 类方法是存在元类里面的
(lldb) x/4gx pClass         // 这里打印的是类,因此第一个是类的 isa
0x1000025f0: 0x001d8001000025c9 0x0000000100aff140
0x100002600: 0x00000001003a2290 0x0000000000000000
(lldb) p/x 0x001d8001000025c9 & 0x00007ffffffffff8      // 经过类的 isa & 0x00007ffffffffff8 获得元类地址
(long) $1 = 0x00000001000025c8        // po 元类地址获得元类
(lldb) po 0x00000001000025c8
Person
复制代码

拿到元类地址以后,经过地址 +0x20 的地址偏移获得元类的 bits ,以后的步骤就和上面同样,此处就再也不演示了。

6.补充

LLDB 调试打印属性和方法的时候,会有 $6->first$11->get(0) 的操做,那是由于 method_list_tproperty_list_t 是继承自 entsize_list_tt,在这个结构体里有 first 属性和 Element& get(uint32_t i) const 方法。

4、总结

  • 经过 clang 找到类的本质是一个 objc_class 结构体,而后再去 objc 源码中找到 objc_class 结构体的底层源码。
  • 经过分析 objc_class 结构体找出属性、方法等原来是保存在 bits->class_rw_tro 属性里面,而后再经过 LLDB 一步一步打印验证出来。
相关文章
相关标签/搜索