前言
阳光明媚,天气凉爽,不出去浪一浪,在这探究类,我堕落了啊。言归正传,前面探究对象时发现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的父类是nilLWTeacher元类 继承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的内存地址是首地址+0x8cache的内存地址是首地址+0x10bits的内存地址是首地址+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中进行了处理,通过big()获取变量
文章最后补充了
类方法探究模块,已经对类方法的存储进行了探究
总结
类中有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中的变量列表里
类方法探究
对象的方法是存储在类中,那么类方法可能存储在元类中。按照这个思路探究下
类方法存储在元类中的方法列表里