iOS底层探索之类&类的结构分析

准备工作

创建一个LGPerson类并且创建一个LGTeach类继承自LGPerson类

@interface LGPerson : NSObject {
    NSString * hobby;
}
@property (nonatomic, strong) NSArray *array1;

- (void)sayHello;
+ (void)eat;

@end

@implementation LGPerson

- (void)sayHello {  }

+(void)eat {  }

@end


@interface LGTeracher : LGPerson

@end

@implementation LGTeracher

@end

然后在main函数LGPerson创建一个对象p和LGTeacher创建teacher对象如下

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        LGPerson *p = [LGPerson alloc];
        LGTeracher * teacher = [LGTeracher alloc];
        NSLog(@"%@-----%@",p,teacher);
    }
    return 0;
}

通过lldb调试如下图


lldb指令调试分析

根据调试过程,我们产生了一个疑问:
为什么上图中的p/x 0x001d80010000336d & 0x00007ffffffffff8ULLp/x 0x0000000100003340 & 0x00007ffffffffff8ULL 中的类信息打印出来都是LGTeracher

0x001d80010000336dteacher对象的isa指针地址,其&mask后得到的结果是 创建teacher的类LGTeracher

0x0000000100003340isa中获取的类信息所指的类的isa的指针地址,即 LGTeracher类的类 的isa指针地址,在Apple中,我们简称LGTeracher类的类为 元类

所以,两个打印都是LGTeracher的根本原因就是因为元类导致的

什么是元类?

对象isa指向也是一个对象,可以称为类对象元类就是类对象所属的元类是系统给的,其定义和创建都是由编译器完成,在这个过程中,的归属来自于元类

  • 对象的isa指向类
  • 类的isa指向元类
  • 元类的isa指向根元类(NSObject)
  • 根元类的isa指向本身

类的信息存在几份

通过以下代码验证

//MARK:--- 分析类对象内存 存在个数
void testClassNum(){
    Class class1 = [LGTeracher class];
    Class class2 = [LGTeracher alloc].class;
    Class class3 = object_getClass([LGTeracher alloc]);
    NSLog(@"\n%p-\n%p-\n%p-\n%p", class1, class2, class3);
}
NSLog打印出来的结果如下:
0x100003368-
0x100003368-
0x100003368-
0x100003368

从结果中可以看出,打印的地址都是同一个,所以NSObject只有一份,即NSObject(根元类)在内存中永远只存在一份

isa走向分析

附上经典的isa走位图


isa走位图

isa走位

isa的走向有以下几点说明:

  • 实例对象(Instance of Subclass)的 isa 指向 类(class)

  • 类对象(class) isa 指向 元类(Meta class)

  • 元类(Meta class)的isa 指向 根元类(Root metal class)

  • 根元类(Root metal class)的isa 指向它自己本身,形成闭环,这里的根元类就是NSObject

superclass走位

superclass(即继承关系)的走向也有以下几点说明:

类 之间 的继承关系:

  • 类(subClass)继承自 父类(superClass)

  • 父类(superClass)继承自 根类(RootClass),此时的根类是指NSObject

  • 根类继承自 nil,所以根类NSObject

元类也存在继承,元类之间的继承关系如下:

  • 子类的元类(metal SubClass) 继承自父类的元类(metal SuperClass)

  • 父类的元类(metal SuperClass) 继承自根元类(Root metal Class)

  • 根元类(Root metal Class) 继承于 根类(Root class),此时的根类是指NSObject

【注意】实例对象之间没有继承关系,类之间有继承关系

类的结构分析

引---内存偏移
int arr[4] = {1, 3, 5, 6};
int *p = arr;
        
for (int i=0; i<4; i++) {
    printf("p[%d] == %d\n", i, p[i]);
}
// output
p[0] == 1
p[1] == 3
p[2] == 5
p[3] == 6

通过lldb调试打印

// 1. 打印p指针
p p
(int *) $0 = 0x00007ffeefbff4b0

// 2. 打印数组的地址
p &arr
(int (*)[4]) $10 = 0x00007ffeefbff4b0

// 3. p 指向 arr 的地址
p arr 
(int [4]) $3 = ([0] = 1, [1] = 3, [2] = 5, [3] = 6)

// 4. 打印数组首元素的地址
p/x &arr[0] 等于 arr 的地址
(int *) $5 = 0x00007ffeefbff4b0

// 5. 打印数组首元素的值
p *arr
(int) $11 = 1

// 6. 打印数组后续元素的值, 最好带个括号,易读性强
(lldb) p * (arr + 1)
(int) $13 = 3
(lldb) p * (arr + 3)
(int) $14 = 6

通过数组的首地址,然后拿到偏移量就可以获取到其它的元素

objc_class & objc_object
通过我们之前下载的objc781的源码我们在源码中查找得到:

objc_object

objc_class

总结:
objc_class继承于objc_object,objc_class中有一个公用的isa,所以所有的对象都继承于objc_object,万物皆来源于objc_object
objc_class、objc_object、isa、object、NSObject等的整体的关系,如下图所示

整体关系图
类的结构分析

通过上述源码可知:
isa属性objc_class继承自objc_object,故objc_class含有isa属性,占8个字节。
superclass 属性:Class类型,Class是由objc_class定义的结构体,是一个指针,占8字节
cache属性:简单从类型class_data_bits_t目前无法得知,而class_data_bits_t是一个结构体类型,结构体的内存大小需要根据内部的属性来确定,而结构体指针才是8字节
bits属性:只有首地址经过上面3个属性的内存大小总和的平移,才能获取到bits

  • 计算cache类的内存大小
    进入cachecache_t的定义(只贴出了结构体中非static修饰的属性,主要是因为static类型的属性 不存在结构体的内存中),代码如下
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;

计算前两个属性的内存大小,有以下两种情况,最后的内存大小总和都是12字节

  • 【情况一】if流程
    buckets类型是struct bucket_t *,是结构体指针类型,占8字节
    maskmask_t类型,而mask_tunsigned int的别名,占4字节
  • 【情况二】elseif流程
    _maskAndBucketsuintptr_t类型,它是一个指针,占8字节
    _mask_unusedmask_t 类型,而 mask_tuint32_t 类型定义的别名,占4字节
    _flagsuint16_t类型,uint16_tunsigned short 的别名,占2个字节
    _occupieduint16_t类型,uint16_tunsigned short的别名,占 2个字节

总结:所以最后计算出cache类的内存大小 = 12 + 2 + 2 = 16字节

获取bits
所以有上述计算可知,想要获取bits的中的内容,只需通过类的首地址平移32字节即可

探索 属性列表,即 property_list
通过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};
        }
    }

通过lldb调试如下图:


获取属性列表属性的lldb调试过程

lldb指令解析:

  • p $3.properties()打印出属性数组
  • p $4.list获取属性数组的属性列表
  • p *$5获取属性列表的第一个属性

通过上述的打印看,只打印出属性,没有打印出成员变量。属性与成员变量的区别就是有没有set、get方法,那成员变量存储在哪里?

成员变量列表

通过查看objc_classbits属性中存储数据的类class_rw_t的定义发现,除了methodspropertiesprotocols方法,还有一个ro方法,其返回类型是class_ro_t,通过查看其定义,发现其中有一个ivars属性,我们可以做如下猜测:是否成员变量就存储在这个ivar_list_t类型的ivars属性中呢?

获取成员变量列表属性的lldb调试过程

通过调试得出 ivar_list_t类型的属性中不仅包含了成员变量还包括了我们之前的属性列表。

实例方法列表

通过lldb调试来获取方法列表,步骤如图所示

获取实例方法列表lldb调试流程

通过p $5.methods()获得具体的方法列表的list结构,其中methods也是class_rw_t提供的方法

通过打印p *$7count = 4可知,存储了4个方法,可以通过p $8.get(i)内存偏移的方式获取单个方法,i的范围是0-3

如果在打印 p $8.get(4),获取第五个方法,也会报错,提示数组越界

但是我们发现我们声明的类方法并不存在中,而实例方法存在中,那么类方法会不会存在元类中呢?下面我们探索一下。

类方法列表

获取类方法列表lldb调试流程

通过调试流程我们得以验证我们这之前的猜测是正确的,类方法的存储在元类的bits中 。

总结:

通过{}定义的成员变量,会存储在类的bits属性中,通过bits--> data() -->ro() --> ivars获取成员变量列表,除了包括成员变量,还包括属性定义的成员变量

通过@property定义的属性,也会存储在bits属性中,通过bits --> data() --> properties() --> list获取属性列表,其中只包含属性

类的实例方法存储在类的bits属性中,通过bits --> methods() --> list获取实例方法列表,例如LGPerson类的实例方法instanceMethod就存储在 LGPerson类的bits属性中

类的类方法存储在元类bits属性中,通过元类bits --> methods() --> list获取类方法列表

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

推荐阅读更多精彩内容