准备工作
内存偏移
普通指针代码分析:
int main(int argc, const char * argv[]) {
@autoreleasepool {
int a = 10;
int b = 10;
int *a_p = &a;
int *b_p = &b;
NSLog(@"%d -- %p -- %p",a,&a,&a_p);
NSLog(@"%d -- %p -- %p",b,&b,&b_p);
}
//打印结果
内存偏移[86667:1854956] 10 -- 0x7ffeefbff47c -- 0x7ffeefbff470
内存偏移[86667:1854956] 10 -- 0x7ffeefbff478 -- 0x7ffeefbff468
- 好明显
a
和b
虽然值是一样的,但是它们的地址却不一样。这就是我们常说的深拷贝咯
。 -
a
和b
的地址刚好相差了4
个字节,这取决于a
的的类型。 - 地址大小比较:
a > b > a_p > b_p
,由于是局部变量,他们都存放在栈区
!
注意:栈区的地址是由高到低的。
对象指针代码分析:
int main(int argc, const char * argv[]) {
@autoreleasepool {
XXPerson *p1 = [LGPerson alloc];
XXPerson *p2 = [LGPerson alloc];
NSLog(@"%@ -- %p",p1,&p1);
NSLog(@"%@ -- %p",p2,&p2);
//打印结果
内存偏移[86667:1854957] <LGPerson: 0x100796fb0> -- 0x7ffeefbff460
内存偏移[86667:1854957] <LGPerson: 0x10078b730> -- 0x7ffeefbff458
}
-
p1
和p2
的内存地址是不一样的,alloc
开辟的内存在堆区
。 -
&p1
和&p2
的内存地址也不一样的,好明显是指向了两个不同的地址。
注意:堆区的地址是由低到高的。
数组指针代码分析:
int main(int argc, const char * argv[]) {
@autoreleasepool {
int c[4] = {1,2,3,4};
int *d = c;
NSLog(@"%p - %p - %p",&c,&c[0],&c[1]);
NSLog(@"%p - %p - %p",d,d+1,d+2);
for (int i = 0; i<4; i++) {
int value = *(d+i);
NSLog(@"%d",value);
}
}
//打印结果
内存偏移[86667:1854956] 0x7ffeefbff490 - 0x7ffeefbff490 - 0x7ffeefbff494
内存偏移[86667:1854956] 0x7ffeefbff490 - 0x7ffeefbff494 - 0x7ffeefbff498
//循环打印结果
内存偏移[86667:1854956] 1
内存偏移[86667:1854956] 2
内存偏移[86667:1854956] 3
内存偏移[86667:1854956] 4
- 数组的地址就是数组元素内存中的
首地址
,&c,&c[0]
指向同一个地址。 - 数组中元素地址的间隔是
元素数据类型
决定的。 - 数组里面的元素可以通过
地址+n
取址的方式取出来,如*(d+i)
; - 数组元素不相同用
首地址+偏移量方式
,根据当前变量的偏移值(需要前面类型大小相加)
图解如下:
数组地址偏移
类isa走位的分析
通过之前的文章可以知道对象本质是结构体
,结构体的第一个成员变量就是isa
。那么类的结构是什么有什么?类有isa指向嘛?如果有他们之间的关系是怎么样的?那么针对这些问题我们进行以下的分析。
isa的走位图(官方的)
isa走位图iOS不同架构下的isa掩码
- 拿到类的信息我之前总结过有三种,用掩码来获取是比较快的而且直接的。
- 从
objc4
的源码可以拿到:
x86_64:define ISA_MASK 0x00007ffffffffff8ULL
arm64:define ISA_MASK 0x0000000ffffffff8ULL
arm64(simulators):define ISA_MASK 0x007ffffffffffff8ULL
类对象的内存个数
int main(int argc, char * argv[]) {
@autoreleasepool {
Class class1 = [MyPersion class];
Class class2 = [MyPersion alloc].class;
Class class3 = object_getClass([MyPersion alloc]);
Class class4 = [MyPersion alloc].class;
NSLog(@"\n-%p-\n-%p-\n-%p-\n-%p-",class1,class2,class3,class4);
}
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
打印结果:
2021-06-18 15:57:27.779872+0800 test[1355:490263]
-0x102e21818-
-0x102e21818-
-0x102e21818-
-0x102e21818-
得出结论:类的内存地址分配都是一样的,每个类只有一个内存块,这根对象的内存分配不一样。
对象的类isa指向(元类的引出)
我自己创建了一个persion类继承于NSObject类,代码分析如下:
分析:
-
0x0000000100ea1a30
是XXPersion
类的地址,当我们找出XXPersion
类的isa
指向的类的地址是0x0000000100ea1a08
,也同样与指向了XXPersion
类。上面已经说到了类只会开辟一个内存空间,那么现在是不是矛盾了?还是其中有一个不是XXpersion
类? - 其不然,
0x0000000100ea1a30
输出的是XXPersion
类的地址,0x0000000100ea1a08
指向是XXPersion
的元类
。
根元类的引出
参照以上的想法,元类会不会也有isa,isa指向是什么?很简单,那就实践实践一下呗!请以下操作:
分析:
-
0x00000001e7afb260
地址是XXPersion
的元类isa
指向的类地址,发现是NSObject
类,然后再找NSObject
类的isa
指向哪个类,发现地址还是0x00000001e7afb260
,并指向了自己。
总结:
经过上面代码的层层分析,我们验证么isa的走位图的isa走位流程:objc(对象) --> class(类) --> metaClass(元类) --> rootMetaClass(根元类) --> rootMetaClass(根元类自己)。
isa走位图:
类的继承链分析
用oc代码看看继承链的原理,创建XXTeacher类继承于XXPerson类,如下图:
分析:
NSObject
的父类打印的结果是nil
。XXTeacher
的元类的父类是XXPerson
的元类(XXPerson
的元类的地址和XXPerson
类的地址不一样)。XXPerson
的元类的父类是NSObject
的元类,NSObject
的元类的父类是NSObject
(和NSObject
类的地址一样)
-
XXTeacher
继承XXPerson
,XXPerson
继承NSObject
,NSObject
的父类是nil
-
XXTeacher
元类 继承XXPerson
元类,XXPerson
继承 根元类,根元类继承NSObject
类之间的继承流程图如下:
类之间的继承图
官方isa走位图和继承图的还原:
还原图类的结构分析
查看objc4(818版本)
的源码,找到了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; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
Class getSuperclass() const {
#if __has_feature(ptrauth_calls)
# if ISA_SIGNING_AUTH_MODE == ISA_SIGNING_AUTH
if (superclass == Nil)
return Nil;
......... //源码位置为objc-runtime-new.h文件第1688行-2173行
//省略了好多代码
分析
- 类的结构中也有隐藏的
isa
,占用8
个字节 -
Class superclass
是类的父类,占用8
个字节 -
cache_t cache
是类的缓存空间,占用16个
字节 -
class_data_bits_t
保存类的数据,如属性
、方法
等信息。
探究cache_t
内存大小
我们在开发过程中看类主要看类的属性和方法,上面所述类的属性和方法都存放在class_data_bits_t
结构体中,那么我们必须要知道class_data_bits_t
结构体的地址,所以分析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个字节,现在的objc版本只能进入这个判断
#endif
uint16_t _occupied; //占用2个字节
};
explicit_atomic<preopt_cache_t *> _originalPreoptCache; //占用8个字节
};
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
// _bucketsAndMaybeMask is a buckets_t pointer
// _maybeMask is the buckets mask
static constexpr uintptr_t bucketsMask = ~0ul;
static_assert(!CONFIG_USE_PREOPT_CACHES, "preoptimized caches not supported");
.........忽略与结构体内存无关的代码
//源码位置为objc-runtime-new.h文件第338行-550行
分析:
- 结构体中
static
修饰的静态变量、调用的方法
已经其他相关的运算
都不占用结构体的内存,所以我省略了好多的代码。 -
typedef unsigned long uintptr_t
是无符号长整形,占用8
个字节。 -
preopt_cache_t *
是结构体指针,占用8
个字节。 -
uint16_t
是无符号16位整形,占用2
个字节。 -
mask_t
是uint32_t
类型的,占用4
个字节。
cache_t
的内存大小为:uintptr_t
内存大小8
个字节+union
内存大小8
个字节 =16
字节
分析class_data_bits_t bits
结构体
综上所述,class_data_bits_t bits
结构体记录的是类的属性、成员变量以及方法。所以必须要了解结构体里面有什么哦!上代码:
struct class_data_bits_t {
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);
//此处省略部分代码
public:
class_rw_t* data() const {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
void setData(class_rw_t *newData)
{
//此处省略部分代码
class_rw_t
结构体
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;
//此处省略部分代码
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};
}
//此处省略部分代码
分析:
是不是很迷茫?当我们看到那么多代码时候该怎么做?首先我们需要看这个结构提供了什么方法跟属性,这是非常重要的!!!
-
class_rw_t
是结构体类型,提供了获取属性列表
,方法列表
,协议列表
的方法。通过实例来验证下方法,属性,变量是不是在class_rw_t
中,在XXPerson
类中添加属性和方法 以及成员变量,请继续往下看验证流程。
获取类的属性、方法操作流程分析
获取类的属性
1.创建好XXPersion类,如图所示:
2.断点,进行lldb调试,如图所示:
获取属性1
获取属性2
步骤:
-
x/4gx XXPerson.class
格式化输出XXPerson.class
,拿到类的首地址:0x0000000100004530
-
p/x 0x0000000100004530 + 0x20
首地址偏移32
个字节(ISA8字节、superclass8字节、cache16字节)
,拿到类对象属性地址 -
p (class_data_bits_t *)0x0000000100004550
将地址转化成class_data_bits_t
类型,为了使用class_data_bits_t
的函数 -
p $21->data()
使用class_data_bits_t
的data()
函数,拿到class_rw_t
类型的地址 -
p $22->properties()
通过properties()
函数获取XXPerson
的成员变量 -
p $23.list
和p $24.ptr
解析出property_list_t
的地址 -
p *$25
通过取地址的方式获取成员变量property_list_t
-
p $26.get(0)
与p $26.get(1)
通过c++函数单个获取类的成员变量name
、nickName
、age
注意: - 最后获取属性的
get()
方法是迭代器
,系统自带的方法。 - 发现
属性列表中
只有3
个,那么定义的hobby
成员变量去哪里了? -
方法列表
的方法也是同样获取的,请接着往下操作。
获取类的方法
获取类方法1获取属性方法2
步骤:
NSObject.class
-> class_data_bits_t
-> class_rw_t
-> method_array_t
-> method_list_t
-> method_t
-> big
成员变量与类方法的获取
成员变量获取
观察发现class_rw_t
还有一个获取class_ro_t *
的方法,会不会在class_ro_t
中,源码查看class_ro_t
的类型。
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<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;
//省略了部分代码(objc-runtime-new.h文件第1037行-1171行)
}
分析:
-
class_ro_t
是结构体类型,有一个ivar_list_t * ivars
变量 -
ivar_list_t * ivars
变量存储着类的成员变量。 - 系统会给属性自动生成一个带
_属性名变量
,存储在class_ro_t
中的变量列表里
lldb调试
类成员变量获取
步骤: -
x/4gx LGPerson.class
格式化输出LGPerson.class
,获取到首地址 -
p/x 0x100008408 + 0x20
首地址偏移32
字节(ISA8字节、superclass8字节、cache_t16字节)
,拿到包含类属性方法成员变量的对象class_data_bits_t
的地址 -
p (class_data_bits_t *)$1
将地址转换为class_data_bits_t
,为了使用class_data_bits_t
的函数 -
p $2->data()
使用class_data_bits_t的data()
函数,拿到class_rw_t
类型的地址 -
p $3->ro
使用class_rw_t
的ro
函数,拿到class_ro_t
类型的地址 -
p *$4
取lass_ro_t
类型地址的值,拿到了class_ro_t
对象 -
p $5.ivars
使用ivars
函数获取class_ro_t
对象的ivars
,得到了指向ivar_list_t
地址的指针 -
p *$6
通过取地址的方式获实例变量数组ivar_list_t,entsize_list_tt<ivar_t, ivar_list_t, 0, PointerModifierNop> = (entsizeAndFlags = 32, count = 4)
,可以看到有4
个ivars
- 通过
c++
函数get()
单个获取类的实例方法:
p $7.get(0):
(ivar_t) $8 = {
offset = 0x0000000100008370
name = 0x0000000100003eec "hobby"
type = 0x0000000100003f60 "@"NSString""
alignment_raw = 3
size = 8
}
ivars获取流程源码:NSObject.class
->class_data_bits_t
->class_rw_t
->class_ro_t
->ivar_list_t
->ivar_t
类方法获取
对象的方法是存储在类中,那么类方法可能存储在元类
中。那么实践一下咯。
lldb调试
步骤:
-
x/4gx XXPerson.class
格式化打印类XXPerson
,得到类的首地址 -
p/x 0x00000001000083e0 & 0x00007ffffffffff8
将isa
指针和ISA_MASK
做与操作,拿到XXPerson的metaClass(元类)
-
x/4gx 0x00000001000083e0
,格式化打印XXPerson
的metaClass(元类)
,拿到元类的首地址 -
p/x 0x1000083e0 + 0x20
,将元类的首地址偏移32个
字节(ISA8字节、superclass8字节、cache_t16字节)
,那多元类的class_data_bits_t
对象地址 -
p (class_data_bits_t *)0x0000000100008400
将地址转化为class_data_bits_t
对象,方便调用函数 -
p $3->data()
调用class_data_bits_t
的data
函数,拿到class_rw_t
对象 -
p $4->methods()
获取class_rw_t
的methods
方法列表 -
p $5.list
和p $5.ptr
拿到指向method_list_t
地址的指针 -
p *$7
取地址,拿到了method_list_t
对象,count
为1
,entsize_list_tt<method_t, method_list_t, 4294901763, method_t::pointer_modifier>
= (entsizeAndFlags = 27, count = 1), 有一个类方法
通过c++
函数get()
与big()
单个获取类的类方法:
p $7.get(0).big():
(method_t::big) $9 = {
name = "sayNB"
types = 0x0000000100003f74 "v16@0:8"
imp = 0x0000000100003ce0 (KCObjcBuild+[XXPerson sayNB]) } **类方法获取流程:
NSObject.class->
metaClass->
class_data_bits_t->
class_rw_t->
method_array_t->
method_list_t->
method_t->
big`*
总结:
附上类探究的整个流程图:
学习过程的确艰辛!但是学到知识让我非常兴奋!通过对类结构的深入学习,使得我对底层的理解更加深刻了,自身的专业知识又有了进一步的提升。让我们继续加油吧💪🏻!
网友评论