类,类结构分析

忙不是不学习的借口

isa和类的关联中我们知道isa中存储着类信息,今天我们就来探索一下类与类的结构。

准备工作

  • 自定义一个继承NSObject的类Person
//.h文件
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

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

+ (void)sayHello;
- (void)sayGood;

@end

//.m文件
#import "Person.h"

@implementation Person
+ (void)sayHello {
    
}
- (void)sayGood {
    
}
@end
  • 继承Person的类Student
#import "Person.h"

NS_ASSUME_NONNULL_BEGIN

@interface Student : Person

@end

NS_ASSUME_NONNULL_END

#import "Student.h"

@implementation Student

@end

元类

通过上篇文章,我们知道isa & ISA_MASK可以得到类信息。
我们实例化一个 Person对象kevin,来摸一下isa研究一下。

(lldb) p/x kevin
(Person *) $0 = 0x00000001004b2180
(lldb) po 0x00000001004b2180
<Person: 0x1004b2180>

(lldb) x/4gx 0x00000001004b2180
0x1004b2180: 0x001d80010000848d 0x0000000000000000
0x1004b2190: 0x0000000000000000 0x0000000000000000
(lldb) po 0x001d80010000848d
-dßd�

(lldb) p/x 0x001d80010000848d & 0x00007ffffffffff8ULL
(unsigned long long) $3 = 0x0000000100008488
(lldb) po 0x0000000100008488
Person

(lldb) x/4gx 0x0000000100008488
0x100008488: 0x0000000100008460 0x00007fff9497d118
0x100008498: 0x0000000100407610 0x0004802400000007
(lldb) po 0x0000000100008460
Person

(lldb) p/x 0x0000000100008460 & 0x00007ffffffffff8ULL
(unsigned long long) $6 = 0x0000000100008460
(lldb) po 0x0000000100008460
Person

(lldb) x/4gx 0x0000000100008460
0x100008460: 0x00007fff9497d0f0 0x00007fff9497d0f0
0x100008470: 0x0000000100705bb0 0x0003e03500000007
(lldb) po 0x00007fff9497d0f0
NSObject

(lldb) p/x 0x00007fff9497d0f0 & 0x00007ffffffffff8ULL
(unsigned long long) $9 = 0x00007fff9497d0f0
(lldb) po 0x00007fff9497d0f0
NSObject

(lldb) x/4gx 0x00007fff9497d0f0
0x7fff9497d0f0: 0x00007fff9497d0f0 0x00007fff9497d118
0x7fff9497d100: 0x000000010050f270 0x0004e03100000007
(lldb) po 0x00007fff9497d0f0
NSObject

lldb调试:

  1. p/x kevin获取对象在内存中的首地址
    • 打印首地址po 0x00000001004b2180,我们得到了一个指向Person的指针地址0x1004b2180
  2. x/4gx 0x00000001004b2180获取kevin的内存情况
    • 拿到kevin对象的isa 0x001d80010000848d
  3. 我们将kevin对象的类信息isa(0x001d80010000848d) & 0x00007ffffffffff8ULL
    • 打印类信息的首地址(0x0000000100008488)
    • 对首地址进行po,我们得到Person,说明首地址指向的是内存中的Person,也印证了isa中的shiftcls中存放着类信息。
  4. 我们再读取Person类的内存情况(x/4gx 0x0000000100008488)
    • Person的isa(0x0000000100008460)进行po,发现直接打印出了Person,这里大家不免就有疑惑了,0x00000001000084880x0000000100008460明明指向的不是同一片内存为啥会都打印出Person。继续往下分析...
  5. 我们既然拿到的Person类的isa(0x0000000100008460),我们再对类进行类信息获取(p/x 0x0000000100008460 & 0x00007ffffffffff8ULL)并打印出首地址(0x0000000100008460)
    • 我们发现在第4步得到的Person的类的isa(0x0000000100008460)与我们现在获取到的类信息地址是完全一致的。0x00000001000084880x0000000100008460指向的不是同一内存区域,却打印出了"同一结果"?
    • 这里引出元类0x0000000100008488指向的是Person类,0x0000000100008460指向的是Person的元类元类底层源码自动生成,每个类都会对应一个元类既然有了元类,我们继续摸元类的isa...
  6. 获取Person元类的内存情况x/4gx 0x0000000100008460
    • 打印其首地址po 0x00007fff9497d0f0,我们居然得到了NSObject
    • 继续对NSObjectisa,发现无论是首地址还是其isa都是同一个地址0x00007fff9497d0f0。那这个NSObject是不是就是我们所熟悉的那个NSObject呢?留在后面验证...
      isa走位图.png

      通过上面的分析,是不是就印证了经典的isa走位图。按图上的isa走位我们知道了,最后isa的走位一直指向0x00007fff9497d0f0其实就是根元类。那为什么会打印出NSObject呢?印证与系统的isa是不是同一个
(lldb) p/x NSObject.class
(Class) $12 = 0x00007fff9497d118 NSObject
(lldb) x/4gx 0x00007fff9497d118
0x7fff9497d118: 0x00007fff9497d0f0 0x0000000000000000
0x7fff9497d128: 0x00000001004b23c0 0x0001801000000003
(lldb) po 0x00007fff9497d118
NSObject

(lldb) po 0x00007fff9497d0f0
NSObject

(lldb) p/x 0x00007fff9497d0f0 & 0x00007ffffffffff8ULL
(unsigned long long) $15 = 0x00007fff9497d0f0

分析

  1. 获取NSObject在内存中的首地址p/x NSObject.class得到0x00007fff9497d118
  2. 获取NSObject的内存分布情况x/4gx 0x00007fff9497d118
    • 得到isa(0x00007fff9497d0f0),此时的isa和首地址po出来都是NSObject,结合上面的分析,0x00007fff9497d0f0其实是NSObject的元类,并且内存地址和上面的内存地址是一致的,继续摸isa也是同一个地址。由于NSObject是根类,所以此时的0x00007fff9497d0f0根元类的地址,印证了类在内存中只有一份且再次印证上图。

继承关系走位图

继承关系走位图.png

结合两幅图,就得到了
继承关系及isa走位图.png

现在我们明白了对象,类,元类,根元类的关系,那么为什么无论是对象,还是类都有isa呢?

objc_class & objc_object

  • 通过clang
    • 我们发现在底层提供的获取父类,及元类runtime方法,返回值都是由struct objc_class定义的结构体
    • Class也是由struct objc_class定义的。
__OBJC_RW_DLLIMPORT struct objc_class *objc_getClass(const char *);
__OBJC_RW_DLLIMPORT struct objc_class *class_getSuperclass(struct objc_class *);
__OBJC_RW_DLLIMPORT struct objc_class *objc_getMetaClass(const char *);

typedef struct objc_class *Class;

struct objc_class {
    Class _Nonnull isa __attribute__((deprecated));
} __attribute__((unavailable));

我们由此,猜想是不是所有的Class都是以objc_class为模板来创建的。
objc4-781源码中搜索objc_class

  • runtime.h文件中发现
struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#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 *` */

这里我们可以看到,在OBJC2已经废弃了,从注释我们也可以看出现在用Class代替了struct objc_class *,是不是这里也可以说明Class都是以struct objc_class为模板创建的。继续往下找。

  • objc-runtime-new.h,发现了新版的objc_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();
    }
    ....//省略部分代码
}
  • objc-runtime-old.h,发现了老版的objc_class定义
struct objc_class : objc_object {
    Class superclass;
    const char *name;
    uint32_t version;
    uint32_t info;
    uint32_t instance_size;
    struct old_ivar_list *ivars;
    struct old_method_list **methodLists;
    Cache cache;
    struct old_protocol_list *protocols;
    // CLS_EXT only
    const uint8_t *ivar_layout;
    struct old_class_ext *ext;
    ...//省略部分代码
}

这里是不是大家就非常熟悉的看到了objc_class的继承关系,原来无论在老版还是新版objc_class都继承自objc_object

  • 我们再全局搜索一下objc_object
    • 在objc.h文件中
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

objc_object是一个结构体,其中包含了一个isa

  • objc-private.h
struct objc_object {
private:
    isa_t isa;

public:

    // ISA() assumes this is NOT a tagged pointer object
    Class ISA();

    // rawISA() assumes this is NOT a tagged pointer object or a non pointer ISA
    Class rawISA();

    // getIsa() allows this to be a tagged pointer object
    Class getIsa();
...
}

我们看到objc_object其中包含一个isaobjc_class又是继承于objc_object
结论

  • 结构体类型objc_class继承自objc_object类型,其中objc_object也是一个结构体,且有一个isa属性,所以objc_class也拥有了isa属性
  • 所有以objc_class为模板创建的类都会有isa
  • 所有以objc_object为模板创建的对象都会有isa
    objc_class,objc_object关系图.png

类结构分析

我们在上面的探索中得出了类都是以objc_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();
    }
    ....//省略部分代码
}
  • ISA:isa,8字节
  • superclass:指向父类的指针,8字节
  • cache:暂时无法确定大小,接下来一起探索
  • bits:探究的目标,通过内存偏移可以获取到,但需要确定cache的大小
cache的内存大小

进入cache_t类型的内部

struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
    explicit_atomic<struct bucket_t *> _buckets;
    explicit_atomic<mask_t> _mask;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    explicit_atomic<uintptr_t> _maskAndBuckets;
    mask_t _mask_unused;
#if __LP64__
    uint16_t _flags;
#endif
    uint16_t _occupied;

这里只贴出影响大小的属性,一些staic修饰的属性,不占大小,故不做大小分析

  • 计算第一部分,由if,elif修饰,所以只会存在一份

    • CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
      • _buckets 类型是struct bucket_t *,是个结构体指针类型,8字节
      • _mask 类型是mask_t,是uint32_t类型,4个字节
    • CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
      • _maskAndBuckets 类型是uintptr_t,是个指针类型,8个字节
      • _mask_unused 类型是mask_t,是uint32_t类型,4个字节
  • _flags 类型是unsigned short,2个字节

  • _occupied 类型是unsigned short,2个字节
    结论
    cache 总共占16个字节

类bits探索

  • 通过类的首地址偏移32个字节,打印Personbits的内存地址
(lldb) p/x Person.class
(Class) $0 = 0x00000001000080e8 Person
(lldb) p/x 0x00000001000080e8 + 32
(long) $1 = 0x0000000100008108
(lldb) p (class_data_bits_t *)0x0000000100008108
(class_data_bits_t *) $2 = 0x0000000100008108

这里我们知道首地址偏移32位是class_data_bits_t类型的,所有直接p此时的内存地址并强转为class_data_bits_t类型,没有报错,进一步论证此时的内存地址就是存储着bits

  • 获取bitsdata()
(lldb) p $2->data()
(class_rw_t *) $3 = 0x00000001011890e0

通过$2->data()去获取class_rw_t,这里可以从两个地方论证

  1. objc_class的结构中,我们可以清晰的看到bits.data()
    objc_class结构.png
  2. 进入class_data_bits_t的内部我们也能发现,class_data_bits内部有data()的get方法,还有set方法
    class_data_bits内部有data()的get方法,还有set方法.png
  • data()是一个class_rw_t类型,再探索一下data(),进入class_rw_t内部
    const method_array_t methods() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>()->methods;
        } else {
            return method_array_t{v.get<const class_ro_t *>()->baseMethods()};
        }
    }

    const property_array_t properties() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>()->properties;
        } else {
            return property_array_t{v.get<const class_ro_t *>()->baseProperties};
        }
    }

    const protocol_array_t protocols() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>()->protocols;
        } else {
            return protocol_array_t{v.get<const class_ro_t *>()->baseProtocols};
        }
    }

methods():方法列表

  • 获取方法列表
    在获取到bits的data()的基础上
  1. 获取data()数据信息
(lldb) p *$2
(class_rw_t) $3 = {
  flags = 2148007936
  witness = 0
  ro_or_rw_ext = {
    std::__1::atomic<unsigned long> = {
      Value = 4295000208
    }
  }
  firstSubclass = nil
  nextSiblingClass = NSUUID
}

  1. 获取方法列表
(lldb) p $3.methods()
(const method_array_t) $4 = {
  list_array_tt<method_t, method_list_t> = {
     = {
      list = 0x00000001000080d8
      arrayAndFlag = 4295000280
    }
  }
}
(lldb) p $4.list
(method_list_t *const) $5 = 0x00000001000080d8
(lldb) p *$5
(method_list_t) $6 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 4
    first = {
      name = "sayHello"
      types = 0x0000000100003f81 "v16@0:8"
      imp = 0x0000000100003e10 (KCObjc`-[Person sayHello])
    }
  }
}
  1. 对方法列表进行单个输出
(lldb) p $6.get(0)
(method_t) $7 = {
  name = "sayHello"
  types = 0x0000000100003f81 "v16@0:8"
  imp = 0x0000000100003e10 (KCObjc`-[Person sayHello])
}
(lldb) p $6.get(1)
(method_t) $8 = {
  name = "bobby"
  types = 0x0000000100003f95 "@16@0:8"
  imp = 0x0000000100003e20 (KCObjc`-[Person bobby])
}
(lldb) p $6.get(2)
(method_t) $9 = {
  name = "setBobby:"
  types = 0x0000000100003f9d "v24@0:8@16"
  imp = 0x0000000100003e40 (KCObjc`-[Person setBobby:])
}
(lldb) p $6.get(3)
(method_t) $10 = {
  name = ".cxx_destruct"
  types = 0x0000000100003f81 "v16@0:8"
  imp = 0x0000000100003e70 (KCObjc`-[Person .cxx_destruct])
}
(lldb) p $6.get(4)
Assertion failed: (i < count), function get, 
error: Execution was interrupted, reason: signal SIGABRT.
The process has been returned to the state before expression evaluation.

这里我们看到有4个方法,.cxx_destructsayHello,还有bobbygetset方法,为啥没有我们的类方法sayNB呢?

properties():属性列表

探索方法,同上,这里我们也会发现,没有成员边量,只有我们的属性。

成员变量的存储

刚刚我们在properties()中未发现成员变量bobby,那个成员变量存在哪里呢?
通过探索,我们在class_rw_t的结构中发现了一个ro()ro()的类型是``,进入其内部

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;

取到ro()中的ivars,然后就可以看到我们的成员变量,和属性都在里面

(lldb) p $16.ivars
(const ivar_list_t *const) $19 = 0x0000000100008140
(lldb) p *$19
(const ivar_list_t) $20 = {
  entsize_list_tt<ivar_t, ivar_list_t, 0> = {
    entsizeAndFlags = 32
    count = 2
    first = {
      offset = 0x00000001000081a8
      name = 0x0000000100003f4e "name"
      type = 0x0000000100003f89 "@\"NSString\""
      alignment_raw = 3
      size = 8
    }
  }
}

类方法的存储

我们在Person类的方法列表中,没有找到类方法。我们在元类的方法列表中发现了Person的类方法,由此我们可知,类的实例方法是存在类的bits里面。类的类方法是存放在元类的bits里面

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