六 OC 底层原理 类的结构

前言

当一个相同的类创建多个对象时,那么这么对象的类是不是创建了多个呢

Class d1 = [Desk class];
Class d2 = [Desk alloc].class;
Class d3 =  object_getClass([Desk alloc]);
Class d4 = [Desk alloc].class;
NSLog(@"%p - %p - %p - %p", d1, d2, d3, d4);

--------------
Room[42703:2055484] 0x1000080f0 - 0x1000080f0 - 0x1000080f0 - 0x1000080f0

类对象地址显然相同, 所以 类在内存空间中只存了一份

回顾

上篇我们知道类在底层一个继承于 objc_object 的对象,也就是object_class 结构体

//截取其中一部分
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

    class_rw_t *data() const {
        return bits.data();
    }
    void setData(class_rw_t *newData) {
        bits.setData(newData);
    }

    void setInfo(uint32_t set) {
        ASSERT(isFuture()  ||  isRealized());
        data()->setFlags(set);
    }
}

我们知道 objc_class 第一个内存是 isasuperclass 存储的父类信息,占据8个字节,cache 缓存占用 16 字节, bits 存储了类的方法,属性,协议等信息的地方,具体的注释表示 class_data_bits_t 相当于 class_rw_t 指针加上 rr/alloc 的标识

class_data_bits_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:
    // ro_or_rw_ext_t会有两种情况:
    // 1): 值是 class_ro_t *
    // 2): 值是 class_rw_ext_t *,
    //          而 class_ro_t * 作为 class_rw_ext_t 的 const class_ro_t *ro 成员变量保存
    using ro_or_rw_ext_t = objc::PointerUnion<const class_ro_t *, class_rw_ext_t *>;
    const ro_or_rw_ext_t get_ro_or_rwe() const {
          return ro_or_rw_ext_t{ro_or_rw_ext};
    }
    ... 此处省略
}
// class_rw_ext_t 其中 rw 可以理解为 read write
struct class_rw_ext_t {
    // 存放只读类型
    const class_ro_t *ro;
    // 方法列表
    method_array_t methods;
    // 属性列表
    property_array_t properties;
    // 协议列表
    protocol_array_t protocols;
    // 所属类名
    char *demangledName;
    // 版本号
    uint32_t version;
};

// class_ro_t 其中 ro 可以理解为 read only
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;
    ... 此处省略
};

可以看出 class_rw_t 存放者类的属性 协议 方法等信息

现在我们验证一下类的成员变量 属性存储的位置

准备

@interface TObject : NSObject
{
    NSString *tname;
}
@property (nonatomic, copy) NSString* toName;

+ (void) classMethodTest;
- (void) instanceMethodTest;
@end

@implementation TObject
+(void)classMethodTest{
    NSLog(@"%s",__func__);
}
-(void)instanceMethodTest{
    NSLog(@"%s",__func__);
}
@end

成员变量 / 属性 存储的位置

接下我 我们就要利用 lldb 调试 一步一步验证

  1. 我们先要获取类对象的内存地址,通过类结构分析 class_data_bits_t bits 字段之前 还有32 字节
    所以我们需要通过内存便宜获取 bits的内存地址
    在这我们介绍一个命令 x/5gx 其中第一个 x 表示读取内存,5表示分成5段,g 表示一个单元8个字节,x 表示16进制, 所以其中第5个地址 就是 bits 的地址
(lldb) p obj1.class
(Class) $0 = TObject
(lldb) x/5gx $0
0x1000021f0: 0x00000001000021c8 0x0000000100333140
0x100002200: 0x00000001018335c0 0x0001802400000007
0x100002210: 0x00000001006656a4
(lldb) 
  1. 我们现在需要将 地址强转为 class_data_bits_t 类型,并通过 data() 函数 获取 class_rw_t
(lldb) p (class_data_bits_t*)0x100002210
(class_data_bits_t *) $1 = 0x0000000100002210
(lldb) p $1->data()
(class_rw_t *) $2 = 0x000020a000000000
(lldb) p *$2
(class_rw_t) $3 = {
  flags = 2148007936
  witness = 1
  ro_or_rw_ext = {
    std::__1::atomic<unsigned long> = 4294975648
  }
  firstSubclass = nil
  nextSiblingClass = NSUUID
}
  1. 此时我们调用class_rw_t 结构体中的 properties() 获取属性列表,可以看到属性名称 toName
(lldb) p $3.properties()
(const property_array_t) $4 = {
  list_array_tt<property_t, property_list_t> = {
     = {
      list = 0x0000000100002198
      arrayAndFlag = 4294975896
    }
  }
}
(lldb) p $4.list
(property_list_t *const) $5 = 0x0000000100002198
p *$5
(property_list_t) $6 = {
  entsize_list_tt<property_t, property_list_t, 0> = {
    entsizeAndFlags = 16
    count = 1
    first = (name = "toName", attributes = "T@\"NSString\",C,N,V_toName") // 其中 C 代Copy,N 代表 nonatomic
  }
}
  1. 此时 我们的成员变量又去哪了呢,接下来 我们接着调用 class_rw_t 的 ro属性,并获取 ro 的 ivar
(lldb) p $3.ro()
(const class_ro_t *) $9 = 0x00000001000020a0
(lldb) p $9->ivars
(const ivar_list_t *const) $10 = 0x0000000100002150
(lldb) p *$10
(const ivar_list_t) $12 = {
  entsize_list_tt<ivar_t, ivar_list_t, 0> = {
    entsizeAndFlags = 32
    count = 2
    first = {
      offset = 0x00000001000021c0  // 偏移量
      name = 0x0000000100000eb9 "tname" // 名称
      type = 0x0000000100000f8a "@\"NSString\"" // 类型
      alignment_raw = 3 // 8字节对齐
      size = 8 // 内存占用大小
    }
  }
}

由此可见 我们的 属性是存在 class_rw_t 中的 属性列表中的,而成员变量是存储在 class_ro_t 中的 ivars ,成员变量列表中的

实例方法 / 类方法 的存储位置

  1. 接着上面的调用,我们先获取class_rw_t的方法列表 methods(),在读取 列表信息
(lldb) p $3.methods()
(const method_array_t) $7 = {
  list_array_tt<method_t, method_list_t> = {
     = {
      list = 0x00000001000020e0
      arrayAndFlag = 4294975712
    }
  }
}
(lldb) p $7.list
(method_list_t *const) $8 = 0x00000001000020e0
(lldb) p *$8
(method_list_t) $9 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 3
    first = {
      name = "instanceMethodTest"
      types = 0x0000000100000f34 "v16@0:8"
      imp = 0x0000000100000db0 (Objc_alloc`-[TObject instanceMethodTest])
    }
  }
}
(lldb) 

由此可见我们的实例方法确实是存储在 class_rw_t 中的方法列表中的

  1. 现在找找我们的类方法存到哪了, 先看看 class_ro_t 中的baseMethodList
lldb) p $3.ro()
(const class_ro_t *) $10 = 0x0000000100002098
(lldb) p $10->baseMethodList
(method_list_t *const) $12 = 0x00000001000020e0
(lldb) p *$12
(method_list_t) $13 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 3
    first = {
      name = "instanceMethodTest"
      types = 0x0000000100000f34 "v16@0:8"
      imp = 0x0000000100000db0 (Objc_alloc`-[TObject instanceMethodTest])
    }
  }
}
(lldb) 

我们看到 在 class_ro_t 中的方法列表中,还是存储着我们的实例方法信息
那我们的类方法到底去哪了呢,通过 isa 的继承关系 推导,会不会在我们的元类中呢

  1. 现在我们开始获取元类地址,然后获取元类的 class_rw_t 中的方法列表中,流程和上面的是一样的
(lldb) p objc_getMetaClass(object_getClassName(obj1))
(Class) $14 = 0x00000001000021a8
(lldb) x/5gx $14
0x1000021a8: 0x00000001003330f0 0x00000001003330f0
0x1000021b8: 0x0000000101925020 0x0001e03500000007
0x1000021c8: 0x00000001006ce2e4
(lldb) p (class_data_bits_t*)0x1000021c8
(class_data_bits_t *) $15 = 0x00000001000021c8
(lldb) p $15->data()
(class_rw_t *) $17 = 0x00000001006ce2e0
(lldb) p *$17
(class_rw_t) $18 = {
  flags = 2684878849
  witness = 1
  ro_or_rw_ext = {
    std::__1::atomic<unsigned long> = 4294975536
  }
  firstSubclass = nil
  nextSiblingClass = 0x00007fff89e08cf8
}
(lldb) p $18.methods
(const method_array_t) $19 = {
  list_array_tt<method_t, method_list_t> = {
     = {
      list = 0x0000000100002078
      arrayAndFlag = 4294975608
    }
  }
}
  Fix-it applied, fixed expression was: 
    $18.methods()
(lldb) p $19.list
(method_list_t *const) $20 = 0x0000000100002078
(lldb) p *$20
(method_list_t) $21 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 1
    first = {
      name = "classMethodTest"
      types = 0x0000000100000f34 "v16@0:8"
      imp = 0x0000000100000d80 (Objc_alloc`+[TObject classMethodTest])
    }
  }
}
(lldb) 

由此可见 我们的类方法是存储在对象所属类的元类当中的方法列表

总结

这是一张类的结构图


image.png
  • 方法
    • 实例方法确实是存储在 class_rw_t 中的methods中的
    • 类方法存储在 当前对象所属类的元类中的 class_rw_t 中的methods
  • 属性
    • 对象属性 -> class_rw_t中的properties
    • 成员变量 -> class_rw_t -> class_ro_t *ro -> ivars

补充 - 验证以上

  1. 例1 通过class_getInstanceMethod 获取类的实例方法
Class class = TObject.class;
const char *className = class_getName(class);
Class metaClass = objc_getMetaClass(className);
 // ---  class_getInstanceMethod
Method method1 = class_getInstanceMethod(class, @selector(instanceMethodTest));
Method method2 = class_getInstanceMethod(metaClass, @selector(instanceMethodTest));

Method method3 = class_getInstanceMethod(class, @selector(classMethodTest));
Method method4 = class_getInstanceMethod(metaClass, @selector(classMethodTest));
        
NSLog(@"class_getInstanceMethod - %p-%p-%p-%p",method1,method2,method3,method4);
===========
 TObjc[4080:382660] class_getInstanceMethod - 0x100003d51-0x0-0x0-0x100003d39

class_getInstanceMethod返回的是类的实例方法,如果类或者父类中没有找到,就返回nil

/***********************************************************************
* class_getInstanceMethod.  Return the instance method for the
* specified class and selector.
**********************************************************************/
Method class_getInstanceMethod(Class cls, SEL sel)
{
    if (!cls  ||  !sel) return nil;

    // This deliberately avoids +initialize because it historically did so.

    // This implementation is a bit weird because it's the only place that 
    // wants a Method instead of an IMP.

#warning fixme build and search caches
        
    // Search method lists, try method resolver, etc.
    lookUpImpOrForward(nil, sel, cls, LOOKUP_RESOLVER);

#warning fixme build and search caches

    return _class_getMethod(cls, sel);
}

以上 method1 和 method4 是有值的,进一步验证了 实例方法存在类中,类方法存在元类中,
也给了我们另一个定义 也就是说 类方法,对于元类来说 其实就是一个实例方法

  1. 例2 获取 类方法 class_getClassMethod
// ---  class_getClassMethod
Method method5 = class_getClassMethod(class, @selector(instanceMethodTest));
Method method6 = class_getClassMethod(metaClass, @selector(instanceMethodTest));

Method method7 = class_getClassMethod(class, @selector(classMethodTest));
Method method8 = class_getClassMethod(metaClass, @selector(classMethodTest));
        
NSLog(@"class_getClassMethod - %p-%p-%p-%p",method5,method6,method7,method8);
========
2020-12-15 21:54:56.966095+0800 TObjc[4080:382660] class_getClassMethod - 0x0-0x0-0x100003d39-0x100003d39

class_getClassMethod 这个方法 其实就是使用元类 获取实例方法

Method class_getClassMethod(Class cls, SEL sel)
{
    if (!cls  ||  !sel) return nil;

    return class_getInstanceMethod(cls->getMeta(), sel);
}
Class getMeta() {
  if (isMetaClass()) return (Class)this;
  else return this->ISA();
}

其实都是走的class_getInstanceMethod 函数,method7 之所以会有值,也是因为 getMeta()这个函数返回元类的原因, 然而当 传入的是元类的话 就会返回本身,这也就意味着 method7method8其实是一样的

3.例3 获取方法实现class_getMethodImplementation

// 寻找方法实现 class_getMethodImplementation
IMP imp1 = class_getMethodImplementation(class, @selector(instanceMethodTest));
IMP imp2 = class_getMethodImplementation(metaClass, @selector(instanceMethodTest));

IMP imp3 = class_getMethodImplementation(class, @selector(classMethodTest));
IMP imp4 = class_getMethodImplementation(metaClass, @selector(classMethodTest));
NSLog(@" class_getMethodImplementation %p-%p-%p-%p",imp1,imp2,imp3,imp4);

=======
2020-12-15 21:54:56.966114+0800 KCObjc[4080:382660]  class_getMethodImplementation 0x100003ab4-0x199ce6600-0x199ce6600-0x100003a7c

其中有一个很重要的东西 _objc_msgForward 消息转发

IMP class_getMethodImplementation(Class cls, SEL sel)
{
    IMP imp;

    if (!cls  ||  !sel) return nil;

    imp = lookUpImpOrNil(nil, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER);

    // Translate forwarding function to C-callable external version
    if (!imp) {
        return _objc_msgForward;
    }

    return imp;
}

所以 imp2imp3 是没有找到的,只是进行了消息转发,所以打印都是有值的

class_getInstanceMethod:返回的是类的实例方法,如果在传入的类或者类的父类中没有找到指定的实例方法,则返回空。
class_getClassMethod:获取类方法,本质上还是调用 class_getInstanceMethod,不过在这其中的参数 变成了元类,如果传入的类或者父类中 对应的元类中没有找到,那就返回空
class_getMethodImplementation: 寻找方法实现,如果在对应的类,或者元类中没有找到,那就进行消息转发

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

推荐阅读更多精彩内容