OC底层探究(4)-- 类的结构分析

类的结构

老规矩,还是从源码搞起。我们先在main.m中定义一个类ZPerson,继承自NSObject。

@interface ZPerson : NSObject{
   NSString *hobby;
}
@property (nonatomic, copy) NSString *name;

- (void)sayHello;
+ (void)sayHappy;

@end

@implementation ZPerson
- (void)sayHello{
   NSLog(@"ZPerson say : Hello!!!");
}

+ (void)sayHappy{
   NSLog(@"ZPerson say : Happy!!!");
}
@end

然后通过通过命令行进行编译。

clang -rewrite-objc main.m
typedef struct objc_class *Class;

编译后得到一个main.cpp文件。之后我们可以在文件中查找关于类Class的定义,即Class是一个objc_class类型的结构体。在objc的源码中,继续查找关于objc_class结构体的一些信息。

struct objc_class : objc_object {
    // Class ISA;  继承自objc_object  //8
    Class superclass;  //8
    cache_t cache;  //16           // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
   
    //bits 从bits中读取数据
    class_rw_t *data() { 
        return bits.data();
    }
    void setData(class_rw_t *newData) {
        bits.setData(newData);
    }
    ...
}

根据源码我们可以知道,objc_class集成了objc_object。哈哈,原来类也是一个对象啊,类对象。万物皆对象!objc_class中包含了isa(从objc_object集成而来)、superclass(它的父类)、cache_t(缓存, 以后再分析)、bits(存储的数据,属性、方法、协议等)等一系列信息。

补充:NSObject是一个类,而类的底层是objc_class,所以我们可以认为NSObject就是对底层的objc_class的上层封装。

类中的数据存储

从上面类的结构中,我们知道类的数据存贮才bits中,可以通过data()方法得到。data()返回一个class_rw_t结构,我们可以从中拿到我们想要的哟。rw代表可读可写,类中的属性、方法还有遵循的协议等信息都保存在 class_rw_t 中。下面看一下class_rw_t的源码结构:

struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    uint32_t flags;
    uint32_t version;

    const class_ro_t *ro;

    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;
    ...
}

再看一下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;
    }
};

ro是一个class_ro_t 结构体类型的指针,占8个bytes大小。class_ro_t与class_rw_t名字类似,rw是可读可写,那ro就是只读(readonly)了吧,哈哈。class_ro_t与class_rw_t结构也有些类似,class_ro_t中存储了当前类在编译期就已经确定的属性、方法以及遵循的协议,不包括分类和后来动态添加的东西。baseProperties存贮了属性,属性在编译时会将一个“_”+属性名成员变量添加到ivars中。ivar中存贮了声明的所有成员变量。属性在编译时还会自动生成set、get方法存储在baseMethodList方法列表中,成员变量不会生成set、get方法。baseMethodList存储了类中的所有原始方法。
下面我们就会汇编探究一下是不是这样。
创建一个person对象


image.png

然后在控制台做如下打印:
打印出person.class的类

(lldb) x/6gx person.class
0x1000024e0: 0x001d8001000024b9 0x0000000100b37140
0x1000024f0: 0x0000000101e633f0 0x0000000200000003
0x100002500: 0x0000000101e62d24 0x0000000100b370f0

0x1000024e0 为类在内存中起始指针,指向isa

(lldb) po 0x1000024e0
ZPerson

根据上面类的结构和内存偏移,再偏移8位,就是superclass

(lldb) po 0x1000024e8
<NSObject: 0x1000024e8>

同理0x1000024e0指针偏移32位, 就是bits, 强转为class_data_bits_t类型

(lldb) p (class_data_bits_t *)0x100002500
(class_data_bits_t *) $3 = 0x0000000100002500

调用data()方法,获取bits里面的数据,返回一个class_rw_t结构体

(lldb) p $3->data()
(class_rw_t *) $4 = 0x0000000101872d30

打印出$4指针所指向的值 看一下class_rw_t结构

(lldb) p *$4
(class_rw_t) $5 = {
  flags = 2148139008
  version = 0
  ro = 0x00000001000022e8
  methods = {
    list_array_tt<method_t, method_list_t> = {
       = {
        list = 0x0000000100002220
        arrayAndFlag = 4294976032
      }
    }
  }
  properties = {
    list_array_tt<property_t, property_list_t> = {
       = {
        list = 0x00000001000022d0
        arrayAndFlag = 4294976208
      }
    }
  }
  protocols = {
    list_array_tt<unsigned long, protocol_list_t> = {
       = {
        list = 0x0000000000000000
        arrayAndFlag = 0
      }
    }
  }
  firstSubclass = nil
  nextSiblingClass = NSUUID
  demangledName = 0x0000000100001f75 "ZPerson"
}

打印一下ro

(lldb) p $5.ro
(const class_ro_t *) $6 = 0x00000001000022e8
(lldb) p *$6
(const class_ro_t) $7 = {
  flags = 388
  instanceStart = 8
  instanceSize = 24
  reserved = 0
  ivarLayout = 0x0000000100001f7d "\x02"
  name = 0x0000000100001f75 "ZPerson"
  baseMethodList = 0x0000000100002220
  baseProtocols = 0x0000000000000000
  ivars = 0x0000000100002288
  weakIvarLayout = 0x0000000000000000
  baseProperties = 0x00000001000022d0
}

打印ro中的baseProperties属性列表, 我们找到了我们声明的属性nickName

(lldb) p *$7.baseProperties
(property_list_t) $8 = {
  entsize_list_tt<property_t, property_list_t, 0> = {
    entsizeAndFlags = 16
    count = 1
    first = (name = "nickName", attributes = "T@\"NSString\",C,N,V_nickName")
  }
}

在打印一下ivars成员变量列表

(lldb) p *$7.ivars
(const ivar_list_t) $9 = {
  entsize_list_tt<ivar_t, ivar_list_t, 0> = {
    entsizeAndFlags = 32
    count = 2 //总有2个成员变量
    first = {
      offset = 0x0000000100002498
      name = 0x0000000100001e62 "hobby"
      type = 0x0000000100001fa2 "@\"NSString\""
      alignment_raw = 3
      size = 8
    }
  }
}

在打印一下第二个成员变量

(lldb) p $9.get(1)
(ivar_t) $10 = {
  offset = 0x00000001000024a0
  name = 0x0000000100001e68 "_nickName"
  type = 0x0000000100001fa2 "@\"NSString\""
  alignment_raw = 3
  size = 8
}

找到了nickName, 这是自动生成的,验证了上面所说的属性在编译时会将一个“”+属性名成员变量添加到ivars中

再找一下方法

(lldb) p *$7.baseMethodList
(method_list_t) $11 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 4
    first = {
      name = "sayHello"
      types = 0x0000000100001f87 "v16@0:8"
      imp = 0x0000000100001a80 (LGTest`-[ZPerson sayHello] at main.m:109)
    }
  }
}
(lldb) p $11.get(0)
(method_t) $13 = {
  name = "sayHello"
  types = 0x0000000100001f87 "v16@0:8"
  imp = 0x0000000100001a80 (LGTest`-[ZPerson sayHello] at main.m:109)
}
(lldb) p $11.get(1)
(method_t) $12 = {
  name = ".cxx_destruct"
  types = 0x0000000100001f87 "v16@0:8"
  imp = 0x0000000100001b50 (LGTest`-[ZPerson .cxx_destruct] at main.m:108)
}
(lldb) p $11.get(2)
(method_t) $14 = {
  name = "setNickName:"
  types = 0x0000000100001f97 "v24@0:8@16"
  imp = 0x0000000100001b10 (LGTest`-[ZPerson setNickName:] at main.m:101)
}
(lldb) p $11.get(3)
(method_t) $15 = {
  name = "nickName"
  types = 0x0000000100001f8f "@16@0:8"
  imp = 0x0000000100001ae0 (LGTest`-[ZPerson nickName] at main.m:101)
}

总共四个方法我们创建的sayHello()、系统自动为属性nickName自动添加的get/set方法和一个系统默认添加的方法。并没有找到我们创建的sayHappy(),说明类方法并没有存在类里。那么类方法存在哪里呢?类方法存在元类里!那么我们怎么去验证呢?
根据isa的走位图我们知道,类的isa指向元类,所以我们要先拿到当前类isa,再按照上面的流程,去元类中查看class_ro_t结构,就能找到了。
先拿到类的isa的指向地址,也就是元类

(lldb) p/x 0x001d8001000024b9 & 0x00007ffffffffff8ULL
(unsigned long long) $3 = 0x00000001000024b8

打印元类内存地址信息

(lldb) x/6gx 0x00000001000024b8
0x1000024b8: 0x001d800100b370f1 0x0000000100b370f0
0x1000024c8: 0x0000000102d1a9a0 0x0000000100000003
0x1000024d8: 0x0000000102d1a900 0x001d8001000024b9

参考上面拿到类的class_ro_t信息流程,我们可以拿到了元类的class_ro_t,打印一下baseMethodList,我们看到sayHappy方法。验证了类方法存贮在元类中。还有谁!

(lldb) p *$11
(method_list_t) $12 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 1
    first = {
      name = "sayHappy"
      types = 0x0000000100001f87 "v16@0:8"
      imp = 0x0000000100001ab0 (LGTest`+[ZPerson sayHappy] at main.m:113)
    }
  }
}

我们再看一下除了ro以外的其他信息:
method_list_t methods:方法列表(如果是类对象存储的是对象方法,元类对象存储的是类方法)。methods是一个二维数组,外层是method_list_t,每个method_list_t又包含了多个method_t。这个是可写的,因为后期可能会有多个分类需要合并到类的方法列表中,还有可能动态添加方法。关于动态方法我们放在后面再探究,目前先简单看一下method_array_t和method_t的数据结构:

class method_array_t : 
    public list_array_tt<method_t, method_list_t> 
{
    typedef list_array_tt<method_t, method_list_t> Super;

 public:
    method_list_t **beginCategoryMethodLists() {
        return beginLists();
    }
    
    method_list_t **endCategoryMethodLists(Class cls);

    method_array_t duplicate() {
        return Super::duplicate<method_array_t>();
    }
};

struct method_t {
    //方法\函数名,一般叫做选择器
    SEL name;
   //包含了函数返回值,参数编码的字符串。通过字符串拼接的方式将返回值和参数拼接成一个字符串,来代表函数返回值及参数。
    const char *types;
    //代表函数的具体实现,存储的内容是函数地址。也就是说当找到imp的时候就可以找到函数实现,进而对函数进行调用。
    MethodListIMP imp;

    struct SortBySELAddress :
        public std::binary_function<const method_t&,
                                    const method_t&, bool>
    {
        bool operator() (const method_t& lhs,
                         const method_t& rhs)
        { return lhs.name < rhs.name; }
    };
};

我们发现method_array_t中有一个beginCategoryMethodLists()方法。我们猜想,能否通过它来读取存储在method_array_t中的方法呢?
先打印一下methods

(lldb) p $8.methods
(method_array_t) $20 = {
  list_array_tt<method_t, method_list_t> = {
     = {
      list = 0x00000001000021f8
      arrayAndFlag = 4294975992
    }
  }
}

//再打印一下beginCategoryMethodLists()方法的返回,返回一个指向method_list_t的二级指针

(lldb) p $20.beginCategoryMethodLists()
(method_list_t **) $21 = 0x0000000101b53bb0

//取一下二级指针的值,变成指向method_list_t的指针

(lldb) p *$21
(method_list_t *) $22 = 0x00000001000021f8

//取一下method_list_t指针指向的内存空间,找到了跟ro中存储的一样的方法,method_array_t取值成功。

(lldb) p *$22
(method_list_t) $23 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 5
    first = {
      name = "sayHello"
      types = 0x0000000100001f82 "v16@0:8"
      imp = 0x0000000100001a30 (LGTest`-[ZPerson sayHello] at main.m:124)
    }
  }
}
(lldb) p $23.get(0)
(method_t) $24 = {
  name = "sayHello"
  types = 0x0000000100001f82 "v16@0:8"
  imp = 0x0000000100001a30 (LGTest`-[ZPerson sayHello] at main.m:124)
}

property_array_t、protocol_array_t与method_list_t类似,就不重复列举了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,539评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,911评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,337评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,723评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,795评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,762评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,742评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,508评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,954评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,247评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,404评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,104评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,736评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,352评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,557评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,371评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,292评论 2 352

推荐阅读更多精彩内容