建议先看下Objc4-818底层探索(三):isa
先补充一些之前的知识点:
知识点1:关于掩码
isa 掩码以x86_64环境下为例
# define ISA_MASK 0x00007ffffffffff8ULL (ULL: unsigned long long无符号长整型 C++语法)
0x00007ffffffffff8
转二进制为
0001 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1000
其实就是
000...000...中间44个1..000
其他位抹零, 只保留中间44位, 即取到shiftcls类信息
知识点2:关于 __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架构
先熟悉下几个lldb
命令, 方便之后探索
-
x/4gx
: 读内存段, 以16进制读取4个内存片段 -
p/x
: 以16进制形式打印, 可用作打印地址 -
po
: 读出/输出值, 也可以用作打印对象
我们还是先看个例子
首先
x/4gx test
: 以16进制4个片段读取test
内存信息, 其中0x1007040c0
为首地址,0x011d8001000081ad
为isa
0x0000000100008180 & 0x00007ffffffffff8ULL
获取类信息, 返回(unsigned long long) $1 = 0x00000001000081a8
, 我们po
读取一下,po 0x00000001000081a8
返回SATest
接下来, 我们读取下类的片段
x/4gx 0x00000001000081a8
, 竟然发现首地址0x1000081a8
和isa0x0000000100008180
都发生了改变再做一次& 掩码得到类信息
0x011d8001000081ad & 0x00007ffffffffff8
获取类信息, 返回(unsigned long long) $3 = 0x0000000100008180
, 我们po
读取一下,po 0x0000000100008180
竟然还是返回SATest
, 难道说明一个类在内存当中会有多个地址 ???我们再重复之前操作, 发现第三次
po
得到的是NSObject
(再往后操作结果一样都是0x000000010036a140
,po
都是NSObject
), 这究竟是什么原因 ???
为了探索类是否在底层存多份, 或者系统会创建多个地址我们做下这个操作
SATest *test = [SATest alloc];
Class cls1 = [SATest class];
Class cls2 = [SATest alloc].class;
Class cls3 = object_getClass([SATest alloc]);
NSLog(@"cls1: %p", cls1);
NSLog(@"cls2: %p", cls2);
NSLog(@"cls3: %p", cls3);
使用系统方法获取类, 对比下, 系统方法得到的类信息为0x1000081b8
为类信息
- 第一次得到的类信息
0x00000001000081b8
VS0x1000081b8
, 相同没问题 - 第二次得到的类信息
0x0000000100008190
VS0x1000081b8
, 不同有问题
很显然第二次的不是类, 那么它是什么? 并且第三次为NSObject
又是为什么?
MachOView
这里我们要用反编译看一下究竟发生了什么
Symbol Table
→ Symbols
中可看到, 实际在底层多了个_OBJC_METACLASS
(meta class : 元类)
这里要涉及一个新的知识点元类
元类
元类
是苹果系统定义
的, 其定义和创建都是由编译器完成,在这个过程中,类的归属来自于元类
对象的
isa
→类
, 但类其实也是一个对象
,可以称为类对象
,而这个对象苹果系统就定义为元类
元类
本身是没有名称的,由于与类相关联,所以苹果系统给与了同类名一样的名称对象类的 isa
指向元类
,元类的isa
指向根元类
即NSObject
验证NSObject
之前自定义类的可看到满足isa
走位图, 这里再验证下NSObject
的isa
走向, 看下是否满足
可看到NSObject
的isa
走势为: NSObject
→根元类
→根元类自身
验证继承关系
isa
走位图没问题满足, 这里再验证下继承关系/父类链是否满足
自定义类的父类链继承
-
SATest
继承于NSObject
, 类→根类 (因为没有父类, 直接指向根类) -
NSObject
→null
,NSObject
没有父类, 指向nil
元类的父类链继承
0x1000080e0
元类
0x7fff80815fe0
根元类
0x7fff80816008
根类(根类是NSObject)
-
元类
继承于根元类
-
根元类
继承于根类
-
根类NSObject
继承于nil
当然我们也可以加个子类打印下继承链关系, 如图
objct_class
因为所有的类都是继承于objct_class
, 那么我们接下来看下objct_class
底层的实现
全局搜索objct_class
, 在objc-runtime-new.h
可以找到struct objc_class : objc_object
首先objct_class
结构体类型, 继承于objc_object
, 同时类结构里面默认一个Class ISA
同时包含Class superclass
, cache_t cache
, class_data_bits_t bits;
等等。
类结构分析
首先还是先看几个例子
普通指针
定义2个变量a, b = 10, 打印两个变量值以及内存地址
普通指针
定义2个变量a, b = 10, 打印两个变量值以及内存地址
a 和 b 为变量都指向10, 10是
系统开辟的固定内存空间
, 其他需要10的值的变量都可以指向内存固定生成的10a 和 b 地址不一样, 这是一种拷贝, 属于
值拷贝
, 也成深拷贝
, 可发现a, b地址相差 4 个字节,这取决于a、b的类型
对象指针
&p1/&p2 是
二级指针
, 指向对象的指针地址(0x7ffeefbff478, 0x7ffeefbff480 为对象指针)p1/p2 是
一级指针
, 指向的 [SATest alloc] 开辟空间的内存地址SATest为 [SATest alloc]创建内存空间, [SATest alloc]开辟空间的 isa指向SATest
数组指针
&arr == &arr[0] == 首地址, 其实他都是取的首地址, 数组地址其实就是数组第一个元素地址即数组名为
首地址
&arr[0]与%arr[1]相差
4
字节, 取决于数据类型
数组类型指针可以通过
首地址+偏移量
得到其他元素(偏移量为数组下标)移动的字节数 等于 偏移量 * 数据类型字节数, 这个根据&arr[0], &arr[1]看出, 两者相差4
bits探索
有了上面的概念, 便于我们理解之后的探索objc_class
中的类信息
objc-runtime-new.h
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; // 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();
}
...
}
因为我们之前在Clang
看到bits
里面存放着类信息
, 所以我们先探索下bits
。因为我们之前知道, 已知首地址, 可以通过平移方法, 得到我们
-
64位
下结构体指针类型
占8字节
, 即isa
占8字节
-
superclass
同理也是结构体指针类型
占8字节
, 即superclass
占8字节
接下来我们看下cache
, 大小
方法一:
因为cache
为cache_t
类型, 最简单的方法lldb命令 po
读一下cache_t
。
可看到cache_t
为16字节
方法二:
进入cache_t 底层
struct cache_t {
private:
explicit_atomic _bucketsAndMaybeMask;
union {
struct {
explicit_atomic _maybeMask;
#if __LP64__
uint16_t _flags;
#endif
uint16_t _occupied;
};
explicit_atomic _originalPreoptCache;
};
...
后面一些函数方法, 而方法不占用内存可以不看
还有一些static属性的内容, 在全局区也可以不看
}
先看下这个源码explicit_atomic
// Version of std::atomic that does not allow implicit conversions
// to/from the wrapped type, and requires an explicit memory order
// be passed to load() and store().
template
struct explicit_atomic : public std::atomic {
explicit explicit_atomic(T initial) noexcept : std::atomic(std::move(initial)) {}
operator T() const = delete;
T load(std::memory_order order) const noexcept {
return std::atomic::load(order);
}
void store(T desired, std::memory_order order) noexcept {
std::atomic::store(desired, order);
}
// Convert a normal pointer to an atomic pointer. This is a
// somewhat dodgy thing to do, but if the atomic type is lock
// free and the same size as the non-atomic type, we know the
// representations are the same, and the compiler generates good
// code.
static explicit_atomic *from_pointer(T *ptr) {
static_assert(sizeof(explicit_atomic *) == sizeof(T *),
"Size of atomic must match size of original");
explicit_atomic *atomic = (explicit_atomic *)ptr;
ASSERT(atomic->is_lock_free());
return atomic;
}
};
可看到explicit_atomic
的大小取决于传入的T
的大小
-
uintptr_t
定义typedef unsigned long uintptr_t;
,long
占8
字节 -
联合体
大小为内部成员最大
的大小, 成员有2个一个是结构体struct
, 一个是explicit_atomic
_originalPreoptCache;
我们先看下结构体
typedef uint32_t mask_t; // x86_64 & arm64 asm are less efficient with 16-bits
typedef unsigned int uint32_t;
-
uint32_t
下int
类型占4
字节 -
typedef unsigned short uint16_t;
short
类型占2
字节
所以结构体大小为4 + 2 + 2 = 8
字节
而后面的explicit_atomic
指针类型占8
字节
固cache
为8 + 8 = 16
字节
已知首地址
以及 ISA
占 8字节
, superclass
占8字节
, cache
占16字节
, 固bits
前面总共8+8+16 = 32
字节, 可通过首地址
平移32字节
获取bits
信息。
我们检查下是否可以真正读出来, 检测钱先看下class_data_bits_t
方便我们下面探索
-
x/4gx test
其中首地址0x1018c0170
-
0x1018c0170
平移32字节为0x1018c01a0
- 因为
bits
是class_data_bits_t
类型, 我们要取地址所以class_data_bits_t *
类型转一下, 变成指针地址 -
p $1->data()
这里要再看下struct objc_class : objc_object
源码(->
是因为当前的是指针, 结构体的话用·
)
struct objc_class : objc_object {
...
class_rw_t *data() const {
return bits.data();
}
}
可看到bit里面有data()函数方法(获取数据方法, class_rw_t
有多数据我们之后再讨论)。p $1->data()
读取下bits
里面数据, 看见返回(class_rw_t *) $2 = 0x00007fff3e24b6e0
。
-
p *$2
取一下$2
里面的内容, 可看到返回一些类信息(class_rw_t)
`
class_rw_t
我们接下来看下class_rw_t `, 源码比较多, 我们挑重点的看
struct class_rw_t {
...
const method_array_t methods() const {
auto v = get_ro_or_rwe();
if (v.is()) {
return v.get(&ro_or_rw_ext)->methods;
} else {
return method_array_t{v.get(&ro_or_rw_ext)->baseMethods()};
}
}
const property_array_t properties() const {
auto v = get_ro_or_rwe();
if (v.is()) {
return v.get(&ro_or_rw_ext)->properties;
} else {
return property_array_t{v.get(&ro_or_rw_ext)->baseProperties};
}
}
const protocol_array_t protocols() const {
auto v = get_ro_or_rwe();
if (v.is()) {
return v.get(&ro_or_rw_ext)->protocols;
} else {
return protocol_array_t{v.get(&ro_or_rw_ext)->baseProtocols};
}
}
};
可看到class_rw_t
(结构体)里面提供一些属性properties
, 方法列表methods
, 协议列表protocols
的方法。
那么我们再SATest.h
中定义一些成员变量, 属性, 方法打印看一下
@interface SATest : NSObject{
NSString *SAHobby;
}
@property (nonatomic, strong) NSString *SAName;
@property (nonatomic, assign) int SAAge;
- (void)sayHello;
- (void)sayNB;
+(void)sayGunDan;
@end
属性列表打印
先看属性列表打印情况
bits
数据信息在之前的例子我上面已经讲过了, 我们从读bits
数据信息$11
之后开始
-
p $12.properties()
获得的属性列表的list
结构, 其中list
中的ptr
就是属性数组的参数指针地址
。(p $12.properties()命令中的propertoes
方法是由class_rw_t
提供的, 方法中返回的实际类型为property_array_t
)
-p *$13.list.ptr
读一下指针地址指向内容, 可看到获得属性list信息, count = 2, 也符合我们建的2个属性
p $14.get(0)
可获取到SAName
对应属性(property_t) $15 = (name = "SAName", attributes = "T@"NSString",&,N,V_SAName")p $14.get(1)
可获取到SAAge
属性(property_t) $16 = (name = "SAAge", attributes = "Ti,N,V_SAAge")p $14.get(3)
数组越界, 因为我们只建立了2个属性
方法列表打印
先了解个知识点
struct class_rw_ext_t {
DECLARE_AUTHED_PTR_TEMPLATE(class_ro_t)
class_ro_t_authed_ptr ro;
method_array_t methods;
property_array_t properties;
protocol_array_t protocols;
char *demangledName;
uint32_t version;
};
struct property_t {
const char *name;
const char *attributes;
};
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.
struct big {
SEL name;
const char *types;
MethodListIMP imp;
};
...
}
想必与属性列表, 方法列表的相关内容name
, types
, imp
储存在struct big
里面(818新改动), 所以获取方法列表里面的信息也要稍微变一下
p $3.methods()
获得的方法列表的list结构
, 接下来仿照属性类型, 依次读取指针地址, 读取列表对应项方法列表留意下得到list之后, 不能直接读取, 新版818的
name
,types
,imp
存在big
中固p $5.get(0).big()
这样读取.cxx_destruct
由于底层是C++, 系统默认添加的方法有自定义的方法
sayNB
,sayHello
, 同时也有属性自动生成set, get方法("setSAName:", "SAName")方法列表超过范围也会报错
当然你要读协议列表
的list结构, 那里就p $3.protocols()即可
探索成员变量以及类方法存放位置
探索成员变量
打印过程中我们会发现, 成员变量
以及类方法
并没有在属性列表, 方法列表里面, 那它究竟在哪里存放的呢? 回过头我们再看struct class_rw_t
方法
其实我们发现, 在方法列表上面还有一个ro
方法, const class_ro_t *ro()
, 看下ro
底层
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif
union {
const uint8_t * ivarLayout;
Class nonMetaclass;
};
explicit_atomic 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;
...
};
可看到const ivar_list_t * ivars;
, 有一个ivars
属性(ivars
: 实例变量), 我们仿照下上面也读一下ro
-
p $3.ro()
获得的ro
的里面的信息 -
p $14.ivars
获得的ivars_list_t
即成员变量的列表里面
接下来我们仿照属性列表去读取, 发现实例变量
储存在ivars_list_t
里面, 同时也会发现还有属性的成员变量。这一点之前在Clang
时候我们看过, 属性在底层是以成员变量+set/get
方法 形式存放的。
通过XXXX {}定义的
成员变量
,会存储在类的bits
属性中,通过bits --> data() -->ro() --> ivars
获取成员变量列表,除了包括成员变量
,还包括属性的成员变量
通过@property定义的
属性
,存储在bits属性中,通过bits --> data() --> properties() --> list
获取属性列表,其中只包含property属性
探索类方法
所谓的对象/实例方法
, 类方法
其实是OC上层或者说苹果官方人为加入的概念, 其底层是都是函数, 不区分+
, -
。但实例方法与类方法还是有必要区分的, 则苹果将实例方法
存在类
里面, 而类方法
存在元类
里面。一方面避免对象存储太大会发生混乱, 一方面也是为了有个调用区分。
所以类方法
要在元类中查找。
x/4gx SATest.class
以4片段16进账形式打印SATest类
的内存段, 这里留意, 我们要取的是元类中的类信息, 所以要用类去打印。得到0x0000000100008238
即isa
用
0x0000000100008238
&0x00007ffffffffff8ULL
即:isa
&掩码
得到类信息0x0000000100008238
p (class_data_bits_t *)0x0000000100008258
, 字节平移32
位, 得到bits
, 并转换class_data_bits_t *
, 这里要留意下千万别忘平移, 不然获取的是系统给定isa
类方法, 几十W条。p $2->data()
读取bits
里面的data
p $4.methods()
读取方法列表p *$5.list.ptr
获取方法列表里面的信息p $6.get(0).big()
获取方法列表里面第一条数据, 可看到有
(lldb) p $6.get(0).big()
(method_t::big) $7 = {
name = "sayGunDan"
types = 0x0000000100003f6e "v16@0:8"
imp = 0x0000000100003d70 (SAObjcBuild`+[SATest sayGunDan])
}
综上也可看出
实例方法: 存在对应
类
的bits
中类方法: 存在对应
元类
的bits
中