我们在iOS底层原理-类的探究分析(上)对类底层从两个方向的进行了分析,得出了类的走位图。这篇我们从类的内部结构进行探索,看看类的结构到底是什么样的?
类的结构
Objective-C对象的本质是结构体,底层都是objc_class,从类的定义来看objc_class的结构(忽略类里面所有的方法),结构代码如下:
struct objc_class : objc_object {
objc_class(const objc_class&) = delete;
objc_class(objc_class&&) = delete;
void operator=(const objc_class&) = delete;
void operator=(objc_class&&) = delete;
// Class ISA;
Class superclass;
cache_t cache;
class_data_bits_t bits;
}
复制代码
从代码的结构可以看出,类主要有isa
,superclass
、cache
和bits
组成,isa在上一篇做过分析,superclass是父类,cache是缓存,bits是所有关于类的数据储存的地方,根据这些定义我们画个简单的结构图
类的结构图
本篇主要分析一下bits
里存放的类的数据,要分析bits,还是可以通过读取内存地址的方式去获得bits的数据,通过获取类的首地址,经过平移得到bits
的地址;我们知道isa
和superclass
都是结构体指针,所占内存都是8个字节,但cache
所占内存大小是多少并不清楚,先看一下cache_t
的结构
cache_t结构
在objc4-818.2
的源码中objc-runtime-new.h
可以找到cache_t
的定义,由于代码过多这里就不粘贴了,分析里面的结构可以看到包含大量的方法函数
和static修饰的变量
,我们知道方法
存储在方法区
,static修饰的变量
存储在全局区
,不占用结构体内存,所以最终可以得到下面cache_t
结构:
struct cache_t {
private:
explicit_atomic<uintptr_t> _bucketsAndMaybeMask; // 8字节
union {
struct {
explicit_atomic<mask_t> _maybeMask; // 4字节
#if __LP64__
uint16_t _flags; // 2字节
#endif
uint16_t _occupied; // 2字节
};
explicit_atomic<preopt_cache_t *> _originalPreoptCache; // 8字节
};
}
复制代码
从上面的结构可以看到cache_t里uintptr_t
类型的_bucketsAndMaybeMask
和union
联合体,联合体内包含一个struct
和preopt_cache_t *
类型,我们分析一下这个结构所占内存大小,_bucketsAndMaybeMask
由uintptr_t
修饰占用8字节,_maybeMask
是mask_t
修饰对应uint32_t
,占用4字节,_occupied
和_flags
个占用2字节,_originalPreoptCache
是preopt_cache_t *
结构体指针,占用8字节,由于union是互斥的,内存占用只取其一,所以cache_t
总共占用8 + 8 = 16
字节。
bits数据分析
根据class_data_bits_t
定义,我们可以获取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 *>(&ro_or_rw_ext)->methods;
} else {
return method_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->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 *>(&ro_or_rw_ext)->properties;
} else {
return property_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->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 *>(&ro_or_rw_ext)->protocols;
} else {
return protocol_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProtocols};
}
}
复制代码
下面我们已实际代码lldb调试进行验证。
lldb调试
首先定义LGPerson类,包含属性、方法和协议。
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@protocol LGPersonDelegate <NSObject>
- (void)say666;
@end
@interface LGPerson : NSObject<LGPersonDelegate>
@property (nonatomic, copy) NSString *name;
@property (nonatomic) int age;
@property (nonatomic, strong) NSString *hobby;
- (void)saySomething;
- (void)say666;
@end
NS_ASSUME_NONNULL_END
复制代码
接一下来通过lldb调试查看属性、方法存储是类中的位置,在运行代码处加个断点。
- p/x LGPerson.class,获取LGPerson类的首地址
0x00000001000087b8
- 根据之前的计算,获取bits地址,需要经过首地址平移
8 + 8 + 16 = 32
字节,也就是加上0x20
,0x00000001000087b8 + 0x20 = 0x00000001000087d8
p (class_data_bits_t *)0x00000001000087d8
输出class_data_bits_t
p $1->data()
得到class_rw_t
,p *$2
看到class_rw_t
结果p $3.properties()
获取类的属性,可以看到property_list_t
结构的list
p $4.list,p $5.ptr
可以拿到property_list_t *
结构的地址$6
p *$6
输出property_list_t
类型的属性列表,可以count的数量- 再可以一个个的数据输出,
p $7.get(0)
获取属性列表对应的值,可以看到我们定义的类中的属性都可以拿到。
下面的图例就是实际的输出结果
同样的方式也可以类中的方法列表
和协议列表
需要注意的问题是,获取methods直接通过get()方法返回的是个空,因为methods没有description方法,需要调用.big()才可以正常输出。
协议列表
关于协议列表和属性、方法之前逻辑一致,就是获取协议时需要特殊的调用,这个后续更新(待补充)…
总结
通过上面对类的结构分析,可以看到类的内部的组成,cache缓存类的方法,bits存储类的属性、方法和协议等,通过内存平移我们可以获取bits的数据,进而了解对应的内部结构,lldb调试对上面分析进行验证。