在前一篇文章中,我们已经探讨了iOS底层原理--isa与类关联的原理,isa
包含了Class
类,从而将isa
与Class
类进行了关联。
那么,我们现在来看下类的结构分析。
首先我们还是看一段代码
//
// main.m
// KCObjc
//
// Created by Cooci on 2020/7/24.
//
#import <Foundation/Foundation.h>
#import "LGPerson.h"
#import <objc/runtime.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
// objc_alloc VS alloc
// NSObject LGPerson
// NSObject 根类
// sel 处理 llvm
// define ISA_MASK 0x00007ffffffffff8ULL
NSObject *objc1 = [NSObject alloc];
LGPerson *objc2 = [LGPerson alloc];
// LGPerson *objc3 = [LGPerson alloc];
NSLog(@"Hello, World! %@",objc2);
}
return 0;
}
- 我们将断点打到
objc2
处,然后查看objc2
的内存情况。
x/4gx
表示查看objc2的内存情况
- 通过上面的输出,我们看到了内存,然后我们知道,内存中第一位代表了对象的
isa
指针的地址,所以0x001d800100002205
这个表示isa
指针的地址。 - 通过iOS底层原理--isa与类关联的原理这篇文章,我们将
isa
于ISA_MASK
进行与运算可以得的isa
指针指向的类的地址,我们进行验证。
ISA_MASK
在x86_64中的地址0x00007ffffffffff8ULL
通过上面的图片我们可以看到,经过与运算后,我们得出了最后得到的地址中存放了
LGPerson
,至此我们可以得到结论。对象的isa
指针指向了类。-
然后我们再来一步,将类的
isa
拿出来和ISA_MASK
进行与运算,得到什么呢?我们验证一下。
看到输出结果,显示还是
LGPerson
,为什么呢?这就是所谓的元类。
元类(Meta Class)
我们都知道
对象的isa
是指向类
,类
的其实也是一个对象
,可以称为类对象
,其isa的位域指向苹果定义的元类
元类
是系统给的,其定义和创建都是由编译器自动完成,在这个过程中,类的归属来自于元类
元类
是类对象
的类,每个类都有一个独一无二的元类用来存储 类方法的相关信息。元类本身是没有名称的,由于与类相关联,所以使用了同类名一样的名称
上面这段话摘自Style_月月的文章iOS-底层原理 08:类 & 类结构分析,大家也可以去看看她的其他文章,都写的非常详细。
那么我们接着按照上面的流程进行探索
-
首先我们得到
LGPerson
的元类
-
此事可以看到,在
LGPerson
的元类中,我们仍然可以拿到isa
指针。那么我们用isa
于ISA_MASK
进行与运算,得到什么呢?请往下看。
可以看到,经过一系列的操作,LGPerson
的元类最终竟然指向了NSObject
。
而且NSObject
的首地址和isa
指针的地址相同,所以也可以说明NSObject
指向的是自己。
- 那我们看看
NSObject
的情况,
可以看到,这次打印的地址跟我们之前得到的NSObject
地址不一样,难道NSObject
在内存中有多份吗?那我们再打印一下完整的地址。
再和之前的地址对比一下,可以看到一样。
所以得出结论,LGPerson
元类的isa
指针并不是指向NSObject
,而是指向NSObject
的元类,即根元类,而NSObject
也是指向根元类,而根元类最终指向自己。
上面就是isa
和继承关系
的走势图,总结如下:
isa
- 实例对象
isa
指向类对象 - 类对象
isa
指向元类 - 元类
isa
指根(NSObject)元类 - 根(NSObject)元类指向自己
superclass(继承关系)
- 子类继承自父类
- 类才有继承关系,而实例对象不存在继承关系
- 所有类都继承
NSObject
,而NSObject
继承自nil
-
NSObject
的元类也继承自NSObject
objc_object & objc_class
在我们之前的分析中,通过clang
进行解析
#ifndef _REWRITER_typedef_LGPerson
#define _REWRITER_typedef_LGPerson
typedef struct objc_object LGPerson;
typedef struct {} _objc_exc_LGPerson;
#endif
extern "C" unsigned long OBJC_IVAR_$_LGPerson$_name;
struct LGPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS; //isa
NSString *_name;
};
可以看到LGPerson
是objc_object
类型的 ,LGPerson
的底层编译LGPerson_IMPL
中又包含了NSObject_IMPL
,NSObject_IMPL
是一个结构体,包含了Class isa
struct NSObject_IMPL {
Class isa;
};
Class
又是一个objc_class
类型的结构提体。
typedef struct objc_class *Class;
所以,objc_object
就是一个根类,而objc_class
定义了一个Class,这证明了每个对象都有isa
指针。
而从我们的源码,可以看出来objc_class
继承自objc_object
内存偏移
在接着往下分析之前,我们来了解一个知识 -- 内存偏移。
普通指针
先看下面的案例,打印a
、b
的值和地址。
int a = 10;
int b = 10;
//打印地址和
NSLog(@"%d --- %p",a,&a);
NSLog(@"%d --- %p",b,&b);
如下图所示
a、b都指向10,但是a、b的地址不一样,这是一种拷贝,属于
值拷贝
,也称为浅拷贝
。a,b的地址之间相差 4 个字节,这取决于a、b的类型
对象指针
LGPerson *p1 = [LGPerson alloc];
LGPerson *p2 = [LGPerson alloc];
NSLog(@"%@ --- %p",p1,&p1);
NSLog(@"%@ --- %p",p2,&p2);
打印结果如下所示:
- p1、p2 是指针,p1 是 指向
[LGPerson alloc]
创建的空间地址,即内存地址
,p2 同理
&p1、&p2是指向 p1、p2对象指针的地址
,这个指针 就是二级指针
数组指针
//数组指针
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);
打印结果如下:
-
&c
和&c[0]
都是取 首地址,即数组名等于首地址 -
&c
与&c[1]
相差4个字节,地址之间相差的字节数,主要取决于存储的数据类型 - 可以通过 首地址+偏移量取出数组中的其他元素,其中偏移量是数组的下标,内存中首地址实际移动的字节数 等于
偏移量 * 数据类型字节数
类结构
我们已经知道,类在底层最终会转化成objc_class
,objc_class
结构如下所示,
而
bits
有我们需要的所有信息,那么要拿到bits
,我们需要进行内存偏移。那么看看
bits
前面的东西有多大,进行多少偏移。
-
isa
属性:继承自objc_object的isa,占 8字节 -
superclass
属性:Class类型,Class是由objc_object定义的,是一个指针,占8字节 -
cache
,是一个cache_t
类型的结构体,所以我们来分析下cache
cache_t
我们来看看cache_t
的结构,进入cache类cache_t的定义(只贴出了结构体中非static修饰的属性,主要是因为static类型的属性 不存在结构体的内存中),有如下几个属性
struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
explicit_atomic<struct bucket_t *> _buckets; // 是一个结构体指针类型,占8字节
explicit_atomic<mask_t> _mask; //是mask_t 类型,而 mask_t 是 unsigned int 的别名,占4字节
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
explicit_atomic<uintptr_t> _maskAndBuckets; //是指针,占8字节
mask_t _mask_unused; //是mask_t 类型,而 mask_t 是 uint32_t 类型定义的别名,占4字节
#if __LP64__
uint16_t _flags; //是uint16_t类型,uint16_t是 unsigned short 的别名,占 2个字节
#endif
uint16_t _occupied; //是uint16_t类型,uint16_t是 unsigned short 的别名,占 2个字节
-
【情况一】if流程
buckets
类型是struct bucket_t *
,是结构体指针类型,占8
字节mask
是mask_t
类型,而mask_t
是unsigned int
的别名,占4
字节
-
【情况二】elseif流程
_maskAndBuckets
是uintptr_t
类型,它是一个指针,占8
字节_mask_unused
是mask_t类型,而
mask_t是
uint32_t类型定义的别名,占
4```字节_flags
是uint16_t
类型,uint16_t
是unsigned short
的别名,占2
个字节_occupied
是uint16_t
类型,uint16_t
是 unsigned short的别名,占
2```个字节
总结:所以最后计算出cache类的内存大小 = 12 + 2 + 2 = 16字节
所以最终要拿到bits
需要平移32位。
class_data_bits_t
- 首先打印出
LGPerson
- 拿出首地址
0x100002200
平移32位 得到0x100002220
,得到bits
- 获取
data()
- 获取
class_rw_t
- 获取属性列表
properties()
- 我们可以看到里面是一个
list
,获取其中的元素
可以看到list中的属性与我们LGPerson
中的属性一致。