第四节课 类的原理分析(上)
isa分析到元类
p/x p //拿到当前指针地址
我们通过
x p
指令可以验证下,看到打印出的地址与我们拿到的地址一模一样。
接下来我们
x/4gx 0x000000010292f480
0x001d800100008365
就是我们的对象的isa
p/x 0x001d800100008365 & 0x00007ffffffffff8 po 0x0000000100008360 //就是我们当前的类
其实最后还是这么个地址,有那么一丢丢好奇,再次x/4g
会得到什么,干就完了~
可以看到,打印出来的结果里跟上面的指针对象拥有同样的内存结构,那第一个地址是啥?还是
isa
嘛?又一次得到
LGPerson
,0x0000000100008360
是LGPerson
,0x0000000100008338
还是LGPerson
,小朋友,你是否有很多问号??猜想:类会和我们的对象无限开辟,内存不止有一个类
接下来就来验证下我们的猜想
Class class1 = [LGPerson class];
Class class2 = [LGPerson alloc].class;
Class class3 = object_getClass([LGPerson alloc]);
Class class4 = [LGPerson alloc].class;
NSLog(@"\n%p \n%p \n%p \n%p",class1,class2,class3,class4);
<----输出结果---->
0x100008360
0x100008360
0x100008360
0x100008360
我们可以看到最终输出的类都是0x100008360
,那就证明之前的0x0000000100008338
它不是类,那么它是个什么?接下来我们就需要使用烂苹果来分析了
在这里我们看到了熟悉的东西,但是并没有看到
0x0000000100008338
这个东西
当我们看到符号表的时候看到了一个奇奇怪怪的东西
这个METACLASS
实际上并不是我们通过代码创建的,这个是系统或者编译器帮我们生成好的,这个东西就叫做元类
。
我们得到了一个大概的流程:对象isa -> 类isa -> 元类
isa走位图和继承链
isa走位图
上面我们通过isa分析出了一个元类,那我们就想,元类之后会不会还有东西?
继续用上面元类的isa
po打印输出的是NSObject,&上我们的掩码,又是他本身,这个东西就是我们的根元类
对象isa -> 类isa -> 元类 -> 根元类
我们看到了NSObject有点奇怪,那我们就跟直接打印的对比下
我们发现,通过
NSObject.class
获取到的地址与刚才po
的地址并不相同,但是我们继续往下x/4g
的时候发现isa
的位置又与po
的地址相同了。就相当于是NSObject.class的isa -> NSObject的元类
,只有两层根类 isa -> 根元类 isa
根据上面我们分析的过程可以看到
isa
的链路如上图虚线部分。同时我们也可以使用代码来进行验证一下
NSObject *object1 = [NSObject alloc];
// NSObject类
Class class = object_getClass(object1);
// NSObject元类
Class metaClass = object_getClass(class);
// NSObject根元类
Class rootMetaClass = object_getClass(metaClass);
// NSObject根根元类
Class rootRootMetaClass = object_getClass(rootMetaClass);
NSLog(@"\n%p 实例对象\n%p 类\n%p 元类\n%p 根元类\n%p 根根元类",object1,class,metaClass,rootMetaClass,rootRootMetaClass);
// LGPerson元类
Class pMetaClass = object_getClass(LGPerson.class);
Class psuperClass = class_getSuperclass(pMetaClass);
NSLog(@"%@ - %p",psuperClass,psuperClass);
<----输出结果---->
0x100547790 实例对象
0x7fff9103a118 类
0x7fff9103a0f0 元类
0x7fff9103a0f0 根元类
0x7fff9103a0f0 根根元类
NSObject - 0x7fff9103a0f0
继承链
根据上面的打印结果,我们发现任何对象,元类的父类都是根元类,有点烧脑了吧?
接下来我们写了一个LGTeacher继承自LGPerson,再次通过上面代码进行打印
// LGTeacher -> LGPerson -> NSObject
// 元类也有一条继承链
Class tMetaClass = object_getClass(LGTeacher.class);
Class tsuperClass = class_getSuperclass(tMetaClass);
NSLog(@"%@ - %p",tsuperClass,tsuperClass);
<----输出结果---->
LGPerson - 0x100008338
元类的继承链如下图实线部分
[图片上传失败...(image-743b09-1624451069459)]
我们继续分析NSObject特殊情况
// NSObject 根类特殊情况
Class nsuperClass = class_getSuperclass(NSObject.class);
NSLog(@"%@ - %p",nsuperClass,nsuperClass);
// 根元类 -> NSObject
Class rnsuperClass = class_getSuperclass(metaClass);
NSLog(@"%@ - %p",rnsuperClass,rnsuperClass);
<----输出结果---->
(null) - 0x0
NSObject - 0x7fff9103a118
根据上述结果,我们推出了图中RootClass(class) -> nil
的这一步
以及RootClass(meta) -> RootClass(class)
这一步
小结:
-
NSOject
对象的元类
与根元类
是同一个
-
元类间
也存在着继承的关系
,跟类是一样的 根元类的父类指向了NSObject
-
NSObject的父类是(null)
,地址为0x0,即NSObject没有父类
指针和内存平移
如果要想获取对象内存中的变量,底层实现方式是对象的首地址+偏移值。下面探究下内存偏移
我们平时遇到的指针分为三种:普通指针、对象指针、数组指针
普通指针
int a = 10;
int b = 10;
NSLog(@"%d -- %p",a,&a);
NSLog(@"%d -- %p",b,&b);
<----输出结果---->
10 -- 0x7ffeefbff41c
10 -- 0x7ffeefbff418
- 我们发现,
值是一样的
,但是指针地址却是不同的
,这就是我们所说的copy:值拷贝
-
a
的地址是0x7ffeefbff41c
,b
的地址是0x7ffeefbff418
,相差4字节
,主要取决于a的类型
对象指针
HZMPerson *p1 = [HZMPerson alloc];
HZMPerson *p2 = [HZMPerson alloc];
NSLog(@"%@ -- %p",p1,&p1);
NSLog(@"%@ -- %p",p2,&p2);
<----输出结果---->
<HZMPerson: 0x1007049f0> -- 0x7ffeefbff410
<HZMPerson: 0x100704650> -- 0x7ffeefbff408
我们发现,对象的地址不同
,地址指向的空间也不同
数组指针
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);
<----输出结果---->
0x7ffeefbff430 - 0x7ffeefbff430 - 0x7ffeefbff434
0x7ffeefbff430 - 0x7ffeefbff434 - 0x7ffeefbff438
-
数组的地址
就是数组元素中的首地址
,即&c
和&c[0]
都是首地址
- 数组中每个元素之间的
地址间隔
,由当前元素的数据类型决定
的 - 数组的
元素地址
可以通过首地址+n*类型大小
方式,这种方式是数组中的元素类型必须相同
。 - 数组元素不相同用
首地址+偏移量
方式,根据当前变量的偏移值(需要前面类型大小相加)
小结:
- 内存地址就是内存元素的首地址
- 内存偏移可以根据首地址+ 偏移值方法获取相对应变量的地址
源码分析类的结构
在之前的探索时我们发现isa
是Class
类型的。Class
类型是objc_class *
,objc_class
是一个结构体。 所有的Class
底层实现都是objc_class
。又来到了我们的源码分析部分,在 objc4-818.2
中全局搜索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_class
这个结构体内部存储了几个成员变量,怎么去探究它们呢?类的地址是知道的,那么就根据上面探究过的首地址+偏移值
来获取里面的成员变量的地址
,然后获取值
。但是偏移值需要知道当前变量之前的所有成员变量的大小
- Class ISA://结构体指针占
8字节
继承自objc_object - Class superclass:结构体指针占
8字节
- cache_t cache:
??母鸡
- class_data_bits_t bits;
计算 cache 类的内存大小
接下来我们就先看cache
的内存大小,查看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
是结构体类型
,有两个成员变量_bucketsAndMaybeMask
和一个联合体
_bucketsAndMaybeMask
是 uintptr_t
无符长整型占8字节
联合体里面有两个成员变量结构体
和 _originalPreoptCache
,联合体的内存大小由成员变量中的最大变量类型决定
_originalPreoptCache
是结构体指针占8字节
结构体中有_maybeMask
、_flags
、_occupied
。 _maybeMask
是uint32_t
占 4字节
,_flags
和_occupied
是uint16_t
各占2字节
,结构体大小是8字节
cache_t
的内存大小是 8+8
或者是8+4+2+2
都是16字节
,所以cache
就是16字节
*isa
的内存地址是首地址,前面已经探究了
-
superclass
的内存地址是首地址+0x8 -
cache
的内存地址是首地址+0x10 -
bits
的内存地址是首地址+0x20
具体cache
里面的内容我们后续再进行讲解,我们先看看bits
这个东西
获取bits
所以有上述计算可知,想要获取bits
的中的内容,只需通过类
的首地址平移32字节
即可
以下是通过lldb命令调试的过程
ps:获取类的首地址有两种方式
- 通过
p/x LGPerson.class
直接获取首地址 - 通过
x/4gx LGPerson.class
,打印内存信息获取
从$2
指针的打印结果中可以看出bits
中存储的信息,其类型是class_rw_t
,也是一个结构体类型。但我们还是没有看到属性列表、方法列表等
,需要继续往下探索
探索 属性列表,即 property_list
通过查看class_rw_t
定义的源码发现,结构体中有提供相应的方法去获取 属性列表、方法列表等,如下所示
在获取bits
并打印bits
信息的基础上,通过class_rw_t
提供的方法,继续探索 bits
中的属性列表
,以下是lldb探索的过程图示
p $3.properties()
命令中的propertoes
方法是由class_rw_t
提供的,方法中返回的实际类型为property_array_t
由于
list
的类型是property_list_t
,是一个指针,所以通过p *$6
获取内存中的信息,同时也证明bits
中存储了property_list
,即属性列表p $7.get(2)
,想要获取LGPerson
中的下一个成员变量,发现提示数组越界了
,说明 property_list 中只有两个属性
目前我们的LGPerson中只有两个属性,那么我们添加一些其他的东西后,property_list
会有什么变化呢?
我们新增了一个成员变量,重复上面步骤结果发现,还是只能取出两个属性,由此可得出property_list
中只有属性,没有成员变量与方法。那成员变量哪去了?
通过查看objc_class
中bits
属性中存储数据的类class_rw_t
的定义发现,除了methods、properties、protocols
方法,还有一个ro
方法,其返回类型是class_ro_t
,通过查看其定义,发现其中有一个ivars
属性,我们大胆猜测:是否成员变量就存储在这个ivar_list_t
类型的ivars
属性中呢?下图是lldb命令的调试流程
通过{}定义的成员变量,会存储在类的bits
属性中,通过bits -> data() ->ro() -> ivars
获取成员变量列表,除了包括成员变量,还包括属性定义的成员变量
通过@property
定义的属性,也会存储在bits
属性中,通过bits -> data() -> properties() -> list
获取属性列表,其中只包含属性
探索methods_list
上面我们新增了成员变量,现在我们添加两个方法来看看
继续上面的lldb调试
但是我们发现直到我们取到越界,打印出来的方法都是空的方法,这是为啥?
这个其实是因为
property_t
的底层是存在两个成员变量的
struct property_t {
const char *name;
const char *attributes;
};
而我们的method_t
则没有
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;
};
但是我们可以看见 有一个结构体big
,里面则存在着成员变量name
,我们取一下试试
可以取出来了,但是发现并没有类方法。在之前,我们曾提及了元类,类对象
的isa指向就是元类
,元类
是用来存储类的相关信息
的,所以我们大胆的猜一下:是否类方法存储在元类的bits中呢?可以通过lldb命令来验证我们的猜测。下图是lldb命令的调试流程
通过打印,我们看到了之前的类方法,证明了我们的猜想成立
类
的实例方法存储在类的bits属性中
,类方法存储在元类的bits属性中
。通过bits/元类bits -> methods() -> list
获取实例方法列表