1、元类MetaClass
首先,先来看看 类 是什么, 类 在OC中其实是一个指向objc_class的结构体指针,我们先看看结构体的构造,很多在OBJC2中即将废弃(单纯参考一下大概需要什么):
现有的为:
- superclass为父类指针,cache为缓存,bits中的
class_rw_t中存放property、protocol、实例methoed等数据,本节先着重分析 bits 数据
对象objc_object定义
这里又涉及到一个知识点
__has_feature(ptrauth_calls),我们下边再补充
OC对象有一个大家都熟悉的特性:消息发送机制
-
原理是OC对象在发送消息时,
运行时库会根据对象的isa指针,得到对象所属的类,这个类 包含了这个类的所有实例方法以及指向父类的指针,以便可以找到父类的实例方法。运行时库检查这个类和其父类的方法列表,找到与消息对应的方法。编译器会将消息转换为消息函数objc_msgSend进行调用。 -
OC的类其实也是一个对象,一个对象就要有一个它属于的类,意味着类也要有一个isa指针,指向他所属的类,所以,
元类就是类所属的类。- 当你给对象发送消息时,消息是在寻找这个对象的类的方法列表。
- 当你给类发消息时,消息是在寻找这个类的元类的方法列表。
元类定义
-
元类就是类所属的类,提供类的通用方法列表
-
isa指向上
根元类是终点:- 对象isa -> 类isa -> 元类isa -> 父元类isa -> 根元类isa -> 根元类
- 根类isa -> 根元类isa
-
继承关系上
NSObject是终点:- 元类 -> 父元类 -> 根元类 -> 根类NSObject
元类的意义
-
元类的设计主要是为了
复用消息机制- objc_msgSend(消息接受者isa, 消息方法名)
- 元类中存类方法,类中存实例方法,单一职责,各自找isa指针中的方法,省去了多余判断
-
底层中并不区分类方法和实例方法,底层都是实例方法,只是
类方法是元类的实例方法/*********************************************************************** * class_getClassMethod. Return the class method for the specified * class and selector. **********************************************************************/ Method class_getClassMethod(Class cls, SEL sel) { if (!cls || !sel) return nil; //底层类方法执行了实例方法,只是执行者为元类 return class_getInstanceMethod(cls->getMeta(), sel); }
__has_feature(ptrauth_calls)介绍
有些时候__has_feature(ptrauth_calls)与TARGET_OS_SIMULATOR一起使用, 需要先普及ARM64e概念
// ARM64 simulators have a larger address space, so use the ARM64e\
// scheme even when simulators build for ARM64-not-e.
//ARM64模拟器有更大的地址空间,所以使用ARM64e\
//即使在为ARM64-not-e构建模拟器时也是如此。
ARM64e是arm64e架构,用于Apple A12及更高版本A系列处理器 或 新型OS_SIMULATOR 模拟器的设备。
__has_feature(ptrauth_calls): 是判断编译器是否支持指针身份验证功能ptrauth_calls指针身份验证,针对arm64e架构;使用Apple A12或更高版本A系列处理器的设备(如iPhone XS、iPhone XS Max和iPhone XR或更新的设备)支持arm64e架构
2、类结构分析
普通指针
- a 和 b 为变量都指向10, 10是
系统开辟的固定内存空间, 其他需要10的值的变量都可以指向内存固定生成的10 - a 和 b 地址不一样, 这是一种拷贝, 属于
值拷贝, 可发现a, b地址相差4个字节,stra、strb地址相差8个字节,这取决于a、b和stra和strb的类型
对象指针
- p1/p2 是
一级指针, 指向的 alloc 开辟空间的内存地址 - &p1/&p2 是
二级指针, 指向对象的指针的地址(0x7ffeefbff3f8, 0x7ffeefbff3f0 为对象指针)
数组指针
- &c == &c[0] == 首地址, 其实他都是取的首地址, 数组地址其实就是数组第一个元素地址即数组名为
首地址 - &c[0]与&c[1]相差
4字节, 取决于数据类型 - 数组类型指针可以通过
首地址+偏移量得到其他元素(偏移量为数组下标) - 移动的字节数 等于 偏移量 * 数据类型字节数, 这个根据&c[0], &c[1]看出, 两者相差4
3、类的结构内存
-
bits中存放类信息,可以通过偏移获取(32字节即0x20)class_data_bits_t bits内容 -
计算偏移量需要分析 cache_t 大小:16字节
-
0x20是一种整型常量的表示方式。以0x开头的整型常量,代表后续字符为16进制表达。于是0x20也就是16进制的20,即10进制的32。另外,0x20作为单字节表示,可以用于字符型变量的赋值,用于char时,其代表ascii码值0x20,即字符空格' '。
好了,前期的准备工作做完了,让我们按步分析 bits 中的内容吧:
4、bits探索
4.1、bits结构
struct class_data_bits_t {
//与objc_class成为友元
friend objc_class;
// Values are the FAST_ flags above.
uintptr_t bits;
private:
bool getBit(uintptr_t bit) const
{
return bits & bit;
}
// Atomically set the bits in `set` and clear the bits in `clear`.
// set and clear must not overlap.
void setAndClearBits(uintptr_t set, uintptr_t clear)
{
ASSERT((set & clear) == 0);
uintptr_t newBits, oldBits = LoadExclusive(&bits);
do {
newBits = (oldBits | set) & ~clear;
} while (slowpath(!StoreReleaseExclusive(&bits, &oldBits, newBits)));
}
void setBits(uintptr_t set) {
__c11_atomic_fetch_or((_Atomic(uintptr_t) *)&bits, set, __ATOMIC_RELAXED);
}
void clearBits(uintptr_t clear) {
__c11_atomic_fetch_and((_Atomic(uintptr_t) *)&bits, ~clear, __ATOMIC_RELAXED);
}
public:
//存放类的实例方法、协议、属性等
class_rw_t* data() const {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
void setData(class_rw_t *newData)
{
ASSERT(!data() || (newData->flags & (RW_REALIZING | RW_FUTURE)));
// Set during realization or construction only. No locking needed.
// Use a store-release fence because there may be concurrent
// readers of data and data's contents.
uintptr_t newBits = (bits & ~FAST_DATA_MASK) | (uintptr_t)newData;
atomic_thread_fence(memory_order_release);
bits = newBits;
}
// Get the class's ro data, even in the presence of concurrent realization.
// fixme this isn't really safe without a compiler barrier at least
// and probably a memory barrier when realizeClass changes the data field
const class_ro_t *safe_ro() const {
class_rw_t *maybe_rw = data();
if (maybe_rw->flags & RW_REALIZED) {
// maybe_rw is rw
return maybe_rw->ro();
} else {
// maybe_rw is actually ro
return (class_ro_t *)maybe_rw;
}
}
......
}
friend:友元,C++中提供的修饰符,为了能通过一些函数方法访问其他类中受保护的私有成员
4.2、clean memory 与 dirty memory
- clean memory
- 加载后不会发生改变的内存
- 可以进行移除,因为需要时可以从磁盘中重新加载
- dirty memory : 进程运行时会发生改变的内存
- 会发生改变的内存
进程在运行它就必须一直存在
4.3、class_ro_t、class_rw_t 与 class_rw_ext_t
4.3.1、什么是 class_ro_t
-
当编译产生二进制文件后,类中会包含指向 MetaClass、SuperClass、Flags、Method cache的指针,和一个指向更多数据的指针,
这个存放额外信息的地方就叫做 class_ro_t -
ro 是 clean memory (read only),是编译时产生
-
当类第一次从磁盘加载到内存时,结构还是如此,但类结构一经使用就会变成 dirty memory,因为运行时会向它写入新的数据
ro的内容
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
//Size
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif
union {
const uint8_t * ivarLayout;
Class nonMetaclass;
};
explicit_atomic<const char *> name;
// With ptrauth, this is signed if it points to a small list, but
// may be unsigned if it points to a big list.
//方法列表
void *baseMethodList;
//协议
protocol_list_t * baseProtocols;
//成员变量
const ivar_list_t * ivars;
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
// This field exists only when RO_HAS_SWIFT_INITIALIZER is set.
_objc_swiftMetadataInitializer __ptrauth_objc_method_list_imp _swiftMetadataInitializer_NEVER_USE[0];
_objc_swiftMetadataInitializer swiftMetadataInitializer() const {
if (flags & RO_HAS_SWIFT_INITIALIZER) {
return _swiftMetadataInitializer_NEVER_USE[0];
} else {
return nil;
}
}
......
}
4.3.2、什么是 class_rw_t
-
当一个类首次被使用时,运行时会为它分配额外的存储容量用于读取-编写数据,
这个储存空间叫做 class_rw_t -
rw 是 dirty memory (read write),是运行时产生
-
rw 会把 ro 数据剪切进来
rw的内容
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;
......
public:
const class_ro_t *ro() const {
auto v = get_ro_or_rwe();
if (slowpath(v.is<class_rw_ext_t *>())) {
return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->ro;
}
return v.get<const class_ro_t *>(&ro_or_rw_ext);
}
void set_ro(const class_ro_t *ro) {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
v.get<class_rw_ext_t *>(&ro_or_rw_ext)->ro = ro;
} else {
set_ro_or_rwe(ro);
}
}
//实例方法
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};
}
}
};
First Subclass、Next Subling Class:运行时才会生成的信息,所有的类都会变成一个树状结构,就是通过First Subclass和Next Subling Class指针实现的,从而实现运行时遍历当前使用的所有类
4.3.3、什么是 class_rw_ext_t
-
如果按照原有思路 rw 中包含一份ro,当该 类有分类或者动态修改时又需要复制一整份ro内容用于修改及后续使用,但复制一整份ro内容明显会生成更多的dirty memory(真正改变自身方法的类大概只有10%)
-
为了节省内存,苹果将 被修改的内容单独存到了一个空间中,
这个单独存放被修改的内容的空间叫做 class_rw_ext_t,如此一来,就 不需要复制整个ro内容,而只需复制ro中被修改的内容即可 -
在 rw 中调用内容时需要判断是否存在 rwe,
如果有 rwe 则从 rwe 中获取数据(动态修改过),如果没有 rwe 则直接从ro中获取数据
rwe的内容
struct class_rw_ext_t {
DECLARE_AUTHED_PTR_TEMPLATE(class_ro_t)
class_ro_t_authed_ptr<const class_ro_t> ro;
method_array_t methods;
property_array_t properties;
protocol_array_t protocols;
char *demangledName;
uint32_t version;
};
-
Methods、Properties、protocols:包含这3个是因为它们可以在运行时进行修改,当category被加载时,它可以向类中添加新的方法,也可以通过runtime API添加它们 -
Demangled Name:这个是只有Swift才会使用的字段,因为整个数据结构OC与Swift是共享的,但是Swift类本身并不需要这个字段,是为了有人要访问Swift的OC名称的时候使用,利用率比较低
list_array_tt模板容器
method_array_t、property_array_t、protocol_array_t都是通过这个模板容器创建的//继承自list_array_tt 模板容器 class method_array_t : public list_array_tt<method_t, method_list_t, method_list_t_authed_ptr> { typedef list_array_tt<method_t, method_list_t, method_list_t_authed_ptr> Super; public: method_array_t() : Super() { } method_array_t(method_list_t *l) : Super(l) { } const method_list_t_authed_ptr<method_list_t> *beginCategoryMethodLists() const { return beginLists(); } const method_list_t_authed_ptr<method_list_t> *endCategoryMethodLists(Class cls) const; };list_array_tt<>模板容器结构:template <typename Element, typename List, template<typename> class Ptr> class list_array_tt { ...... }- Element:代表元素类型,List:代表容器类型
- 专门用来初始化容器(method_array_t、protocol_array_t、protocol_array_t)
ro、rw、rwe优化后结构
| class_ro_t (二进制文件中) | class_rw_t (类首次使用时) | class_rw_ext_t (有动态修改时) |
|---|---|---|
| flags | flags | methods |
| instanceStart | witness | properties |
| instanceSize | firstSubclass | protocols |
| reserved | nextSiblingClass | demangledName |
| name | version | |
| ivarLayout | 一份ro数据 | |
| weakIvarLayout | rwe数据(可选) | |
| ivars | ||
| baseMethodList | ||
| baseProtocols | ||
| baseProperties |
总结
- rw 中可以获取 Methods、Properties、Protocols,但实际
直接来源是rw中复制的ro或者rwe - 被修改过的内容才生成rwe,不修改不生成
- 通过终端命令查看一下class_rw占用情况(以苹果自带的Safari为例)
//Safari heap Safari | egrep 'class_rw|COUNT'- 我们看到class_rw_t一共占用162240字节,而class_rw_ext_t则占用了25056字节,相当于其
15%多
- 我们看到class_rw_t一共占用162240字节,而class_rw_ext_t则占用了25056字节,相当于其
4.4、根据 bits 结构验证打印 Properties、Methods、Protocols
1、根据偏移获取class_data_bits_t类型的 bits
x/4gx test其中首地址0x1006459400x100645940平移32字节为0x100645960- 因为
bits是class_data_bits_t类型, 我们要取地址所以class_data_bits_t *类型强转一下, 变成指针地址
2、获取 bits 中class_rw_t *类型的 data
p $2->data()是因为struct class_data_bits_t源码(->是因为当前的是指针, 结构体的话用.)
3、打印 data 下的 Properties 与 Methods
-
根据上面
class_rw_t(结构体)结构,里面提供一些属性properties,方法列表methods,协议列表protocols的方法、list_array_tt<> 提供对应容器 -
打印属性列表
验证打印步骤 :
-
打印方法列表
验证打印步骤 :
- 根据芯片英特尔和M1不同,英特尔大端模式用
.big(),M1小端模式用 small 提供的.getDescription()方法
struct method_t { static const uint32_t smallMethodListFlag = 0x80000000; method_t(const method_t &other) = delete; // The representation of a "big" method. This is the traditional // representation of three pointers storing the selector, types // and implementation. //big中存放了方法名和函数指针地址 struct big { //方法名称 SEL name; const char *types; //函数指针地址 MethodListIMP imp; }; private: bool isSmall() const { return ((uintptr_t)this & 1) == 1; } // The representation of a "small" method. This stores three // relative offsets to the name, types, and implementation. struct small { // The name field either refers to a selector (in the shared // cache) or a selref (everywhere else). RelativePointer<const void *> name; RelativePointer<const char *> types; RelativePointer<IMP> imp; bool inSharedCache() const { return (CONFIG_SHARED_CACHE_RELATIVE_DIRECT_SELECTORS && objc::inSharedCache((uintptr_t)this)); } }; ...... } - 根据芯片英特尔和M1不同,英特尔大端模式用
-
打印协议列表
协议列表的list结构, 获取data后p $3.protocols()即可
补充
- objc_object VS NSObject、objc_class VS Class、dispatch_objc VS dispatch,iOS底层实现使用带objc内容进行操作
- 当 类中有成员变量时,methods 中会有一个
.cxx_destruct方法,是ARC下自动生成的析构函数
MachOView(反编译)
Symbol Table → Symbols中可看到, 实际在底层多了个_OBJC_METACLASS(meta class : 元类)