前言
阳光明媚,天气凉爽,不出去浪一浪,在这探究类,我堕落了啊。言归正传,前面探究对象时发现isa
和类
之间的关联。那么今天就来探究下类
,看看类
有什么神奇之处。
准备工作
内存偏移
前一篇博客 IOS 底层原理之对象的本质&isa关联类 探究了如果要想获取对象内存中的变量,底层实现方式是对象的首地址
+偏移值
。下面探究下内存偏移
普通指针
int main(int argc, char * argv[]) {
@autoreleasepool {
int a = 10;
int b = 10;
int * p = &a;
int * q = &b;
NSLog(@"---%d----%p---%p",a,p,&p);
NSLog(@"---%d----%p---%p",b,q,&q);
}
return 0;
}
复制代码
2021-06-17 15:07:03.252239+0800 内存偏移[11954:347138] ---10----0x7ffedfe69c8c---0x7ffedfe69c80
2021-06-17 15:07:03.252592+0800 内存偏移[11954:347138] ---10----0x7ffedfe69c88---0x7ffedfe69c78
复制代码
a
和b
的值都是10
,但是a
和b
的地址不一样,这就是常说的深拷贝
a
的地址是0x7ffedfe69c8c
,b
的地址是0x7ffedfe69c88
,相差4
字节,主要取决于a
的类型a
>b
>p
>q
的地址,且它们全部在栈区
。
图解如下
对象指针
int main(int argc, char * argv[]) {
@autoreleasepool {
LWPerson * p1 = [LWPerson alloc];
LWPerson * p2 = [LWPerson alloc];
NSLog(@"---%@----%p",p1,&p1);
NSLog(@"---%@----%p",p2,&p2);
}
return 0;
}
复制代码
2021-06-17 16:00:37.697384+0800 内存偏移[12536:388334] ---<LWPerson: 0x600002000000>----0x7ffee357bc70
2021-06-17 16:00:37.697465+0800 内存偏移[12536:388334] ---<LWPerson: 0x600002000010>----0x7ffee357bc68
复制代码
alloc
开辟的内存在堆区
,局部变量
开辟的内存在栈区
- 堆区
低
地址 –>高
地址,栈区高
地址 –>低
地址
图解如下
数组指针
int main(int argc, 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++) {
//(d+i) 取地址里面的值
int value = *(d+i);
NSLog(@"value = %d",value);
}
}
return
}
复制代码
2021-06-17 16:32:13.132035+0800 内存偏移[12919:415236] 0x7ffeebfd9c80 - 0x7ffeebfd9c80 - 0x7ffeebfd9c84
2021-06-17 16:32:13.132122+0800 内存偏移[12919:415236] 0x7ffeebfd9c80 - 0x7ffeebfd9c84 - 0x7ffeebfd9c88
2021-06-17 16:32:13.132206+0800 内存偏移[12919:415236] value = 1
2021-06-17 16:32:13.132287+0800 内存偏移[12919:415236] value = 2
2021-06-17 16:32:13.132341+0800 内存偏移[12919:415236] value = 3
2021-06-17 16:32:13.132418+0800 内存偏移[12919:415236] value = 4
复制代码
- 数组的地址就是数组元素中的
首地址
,即&c
和&c[0]
都是首地址
- 数组中每个元素之间的
地址间隔
,根据当前元素的数据类型
决定的 - 数组的元素地址可以通过
首地址
+n
*类型大小
方式,这种方式是数组中的元素类型必须相同。 - 数组元素不相同用
首地址
+偏移量
方式,根据当前变量的偏移值(需要前面类型大小相加)
图解如下
总结
- 内存地址就是内存元素的
首地址
- 内存偏移可以根据
首地址
+偏移值
方法获取相对应变量的地址
类的分析(isa
)
探究对象时发现对象的isa
指向的是类
。万物皆对象,类也是个对象,类里面也有一个isa
,那么类的isa
是指向那个类呢?这个类就是苹果定义的元类
。
元类
元类
是编译器自动生成,苹果为什么要自动生成这么一个类呢?那就来探究下
类对象的内存个数
int main(int argc, char * argv[]) {
@autoreleasepool {
Class class1 = [LWPerson class];
Class class2 = [LWPerson alloc].class;
Class class3 = object_getClass([LWPerson alloc]);
Class class4 = [LWPerson alloc].class;
NSLog(@"\n-%p-\n-%p-\n-%p-\n-%p-",class1,class2,class3,class4);
}
return
}
复制代码
-0x100009510-
-0x100009510-
-0x100009510-
-0x100009510-
复制代码
源码分析:类对象的地址都是一样的,内存中每一个类只有一块内存,和普通的对象有明显的区别
元类探究
LWPerson
类有两个不一样
的地址,但是一个类对象只有一个地址。0x0000000100009500
是LWPerson
的类地址,那么0x00000001000094d8
这个类地址苹果把它叫做元类
总结
元类
是系统编译器自动创建的,和用户没关系- 对象的
isa
指向类
,类对象的isa
指向元类
- 类的
类名
和它关联类即元类
的类名
是一样的(只有关联元类才有类名)
isa 走位图
对象
,类
,元类
都有isa
, 具体isa
走向流程是如何的。 创建一个对象NSObject * obj = [NSObject alloc]
,结合lldb
进行探究
NSObject
的对象obj
的isa
–>NSObject
类NSObject
类的isa
–>根元类
根元类
的isa
–>根元类
(指向自己)
自定义LWPerson
类,创建一个对象LWPerson * person = [LWPerson alloc]
,结合lldb
进行探究
元类的isa
指向根元类
,疑问:为什么不是指向NSbOject
?注意NSbOject
类地址和根元类
类地址不一样,所以指向的是根源类
LWPerson
的对象person
的isa
–>LWPerson
类LWPerson
类的isa
–>LWPerson
类的元类
- 元类
isa
–>根元类
isa
走位流程图
总结
- 对象的
isa
指向类对象
- 类对象
isa
指向元类
- 元类
isa
指向根元类
- 根元类
isa
指向根元类
(指向自己)
类、元类、根元类的继承图
首先创建一个LWTeacher
类继承LWPerson
,探究下它们之间的继承关系。代码如下
int main(int argc, char * argv[]) {
@autoreleasepool {
Class tMetaClass = object_getClass(LWTeacher.class);//LWTeacher的元类
Class tMetaSuperClass = class_getSuperclass(tMetaClass);//LWTeacher的元类的父类
Class pMetaClass = object_getClass(LWPerson.class); //LWPerson的元类
Class pMeatSuperClass = class_getSuperclass(pMetaClass);//LWPerson的元类的父类
Class nMetaClass = object_getClass(NSObject.class);//NSObject的元类
Class nSuperClass = class_getSuperclass(NSObject.class);//NSObject的父类
Class nMetaSuperClass = class_getSuperclass(nMetaClass);//NSObject的元类的父类
NSLog(@"LWTeacher-%p",LWTeacher.class);
NSLog(@"LWPerson-%p",LWPerson.class);
NSLog(@"NSObject-%p",NSObject.class);
NSLog(@"%@ - %p - %@ - %p",tMetaClass,tMetaClass,tMetaSuperClass,tMetaSuperClass);
NSLog(@"%@ - %p - %@ - %p",pMetaClass,pMetaClass,pMeatSuperClass,pMeatSuperClass);
NSLog(@"%@ - %p - %@ - %p",nMetaClass,nMetaClass,nMetaSuperClass,nMetaSuperClass);
}
return
}
复制代码
2021-06-17 22:12:45.737286+0800 testClass[15587:581908] LWTeacher-0x100009618
2021-06-17 22:12:45.737359+0800 testClass[15587:581908] LWPerson-0x100009528
2021-06-17 22:12:45.737396+0800 testClass[15587:581908] NSObject-0x7fff8deb3118
2021-06-17 22:12:45.737434+0800 testClass[15587:581908] LWTeacher - 0x1000095f0 - LWPerson - 0x100009500
2021-06-17 22:12:45.737497+0800 testClass[15587:581908] LWPerson - 0x100009500 - NSObject - 0x7fff8deb30f0
2021-06-17 22:12:45.737540+0800 testClass[15587:581908] NSObject - 0x7fff8deb30f0 - NSObject - 0x7fff8deb3118
2021-06-17 22:12:45.737583+0800 testClass[15587:581908] - - (null)
复制代码
源码分析 NSObject
的父类打印的结果是nil
。LWTeacher
的元类的父类是LWPerson
的元类(LWPerson
的元类的地址和LWPerson
类的地址不一样)。LWPerson
的元类的父类是NSObject
的元类,NSObject
的元类的父类是NSObject
(和NSObject
类的地址一样)
LWTeacher
继承LWPerson
,LWPerson
继承NSObject
,NSObject
的父类是nil
LWTeacher
元类 继承LWPerson
元类,LWPerson
继承根元类
,根元类
继承NSObject
类之间继承流程图
isa的走位图和继承图
苹果官方提供isa
走位图和继承图
类结构分析
探究 IOS 底层原理之对象的本质&isa关联类 时,发现isa
是Class
类型的。Class
类型是objc_class *
,objc_class
是一个结构体。 所有的Class
底层实现都是objc_class
。在 objc4-818.2 中全局搜索objc_class
代码如下
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE; // OBJC2 不可用
复制代码
现在正常的都是OBJC2
,上面的定义的objc_class
在OBJC2
不可用
struct objc_class : objc_object {
...
// 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
//下面是一些方法省略
};
复制代码
源码分析objc_class
继承objc_object
,objc_object
里面只有一个成员变量isa
,isa
具体是作用已经探究过。下面的三个成员变量具体的作用还无从得知,怎么去探究它呢?类的地址是知道的,那么就根据上面探究过的首地址
+偏移值
来获取里面的成员变量的地址,然后获取值。但是偏移值
需要知道当前变量之前的所有成员变量的大小
isa
结构体指针占8字节
Class superclass
也是结构体指针占8字节
cache_t cache
是结构体类型的大小,由结构体内成员变量决定class_data_bits_t bits
知道前面三个成员变量大小,就可以得到bits
的地址
只要知道cache_t
的内存大小,objc_class
的所有的成员变量都可以获取到相应的地址,探究下cache_t
内存大小
typedef unsigned long uintptr_t;
#if __LP64__
typedef uint32_t mask_t; // x86_64 & arm64 asm are less efficient with 16-bits
#else
typedef uint16_t mask_t;
#endif
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
字节
探究类里面的各个成员变量,成员变量的内存地址如下
isa
的内存地址是首地址
,前面的博客已经探究了superclass
的内存地址是首地址
+0x8
cache
的内存地址是首地址
+0x10
bits
的内存地址是首地址
+0x20
今天主要探究成员变量bits
,bits
里面存储了哪些重要的信息
bits.data()
应该存储数据,data()
的类型是class_rw_t*
,源码分析下class_rw_t
类型
class_rw_t
是结构体类型,提供了获取属性列表
,方法列表
,协议列表
的方法。通过实例来验证下方法
,属性
,变量
是不是在class_rw_t
中,在LWPerson类
中添加属性
和方法
以及成员变量
属性探究
类中声明的属性name
,age
存储在属性列表preperty_list_t
里,p $7.get(index)
获取相应的属性
p $7.get(2)
时报错数组越界,属性列表里只有2
个属性,但是声明的变量height
存储在哪里?
文章最后
补充
模块下的变量探究
模块,已经对变量的存储进行了探究
方法探究
对象方法
存储在LWPerson
类中的方法列表method_list_t
里类方法
没有在LWPerson
的方法列表method_list_t
里,类方法
放在哪里呢?p $7.get(index)
在方法列表中获取不到具体的值,因为method_t
中进行了处理,通过bit()
获取变量
文章最后
补充
模块下的类方法探究
模块,已经对类方法的存储进行了探究
总结
类中有isa
、superclass
、chche
、bits
成员变量,在对bits
探究过程中发现bits
存储着大家熟悉的属性列表
、方法列表
、成员变量列表
、协议列表
等,打开了我们对类
的认知,后面会继续对类进行探究,敬请期待。
补充
变量探究
类属性列表
中没有存储变量
,变量
是类中自定义的不应该放在元类中,观察发现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;
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];
};
复制代码
class_ro_t
是结构体类型,有一个ivar_list_t * ivars
变量。是一个变量列表
,按逻辑里面应该存储变量
。
变量
的底层实现是ivar_t
。存储在class_ro_t
中的变量列表里- 系统会给
属性
自动生成一个带_属性名
变量,存储在class_ro_t
中的变量列表里
类方法探究
对象的方法是存储在类
中,那么类方法可能存储在元类
中。按照这个思路探究下
类方法
存储在元类
中的方法列表
里