iOS 探索系列相关文章 :
在前面的几篇文章对
iOS
对象的原理进行了探索, 那么类在底层的实现又是什么样子的呢? 类是以什么样的形式存在的? 他的结构又是什么样子的呢? 接下来进行对类的一些相关内容探索, 看一下他的真面目。
类的定义
说到
类
, 我相信看这篇文章的人都不会陌生, 那么什么是类呢?
类 (Class) 是面向对象程序设计 (OOP, Object-Oriented Programming) 实现信息封装的基础。类是一种用户自定义的数据类型,
也称类类型。每个类包含数据说明和一组操作数据或传递信息的函数(或者方法)。类的实例称为对象。
在 iOS
中, 我们知道大多数情况下我们使用的类都是从 NSObject
这个基类所派生出来的, 在 OC
的底层, 我们的类到底是什么样子的呢? 接下来开始我们的探索:
- 首先在我们之前的源码里面去尝试去全局搜索一下
objc_class
, 搜索发现了一个objc_class
的结构体实现:
// Class
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
// 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;
/* Use `Class` instead of `struct objc_class *` */
可以看出 Class
本质其实是一个 objc_class
类型的 结构体指针, 但是有一个东西需要注意 OBJC2_UNAVAILABLE
, 从该宏定义的名字可以看出, 这个定义在 OBJC2
中已经废弃掉了。
关于
OBJC2_UNAVAILABLE
的宏定义, 我在objc
源码中找到了 :/* OBJC2_UNAVAILABLE: unavailable in objc 2.0, deprecated in Leopard */ #if !defined(OBJC2_UNAVAILABLE) # if __OBJC2__ # define OBJC2_UNAVAILABLE UNAVAILABLE_ATTRIBUTE # else /* plain C code also falls here, but this is close enough */ # define OBJC2_UNAVAILABLE \ __OSX_DEPRECATED(10.5, 10.5, "not available in __OBJC2__") \ __IOS_DEPRECATED(2.0, 2.0, "not available in __OBJC2__") \ __TVOS_UNAVAILABLE __WATCHOS_UNAVAILABLE __BRIDGEOS_UNAVAILABLE # endif #endif
- 接下来继续在上一步的搜索结果中查找, 在
objc-runtime-new
中又发现了新的声明
// objc_class (一个隐藏的 ISA), 继承自 objc_object
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
class_rw_t *data() {
return bits.data();
}
......省略
}
// objc_object (包含 isa)
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
可以看到这是一个新的关于 objc_class
定义, 发现他继承自 objc_object
, 并且 isa
就是从 objc_object
继承过来的, 并没有自己去定义。那么从类的结构其实也验证了之前的 isa 分析的结论就是: 类也有自己的 isa
, 并且类的 isa
是指向的元类, 类其实也是一种对象。
万物皆对象
每个实例对象都有个 isa 指针, 指向对象的 (Class)类; 而类对象里也有个 isa 指针, 指向的是 meteClass(元类)。关于 isa 的指向可以去看看我之前的文章 iOS探索--isa的初始化和指向分析
类的结构分析
在上面的过程中我们找到了关于类的定义, 接下来具体看看类的结构是什么样子的? 每一部分的作用又是什么? (注意, 这里内存占用情况默认为 64位情况下)
1. Class isa
// Class (结构体指针)
typedef struct objc_class *Class;
类对象中的 isa
指针, 用于关联 元类
, 关于这一点在之前的 isa
指向流程图中可以看出来。这里的 Class
类型是一个指针, 所以 isa
占用 8 字节。
2. Class superclass
根据名字应该可以看出来表示 当前类的父类
,同样是 Class
类型, 所以 superclass
也占 8 个字节。
3. cache_t cache
缓存, 用于缓存已经调用的方法, 可以加速方法的调用, 具体分析我们放到以后。接下来分析一下他的内存占用情况:
//
struct cache_t {
struct bucket_t *_buckets; // 8
mask_t _mask; // 4
mask_t _occupied; // 4
......函数
}
//
#if __LP64__
typedef uint32_t mask_t; // x86_64 & arm64 asm are less efficient with 16-bits
#else
typedef uint16_t mask_t;
#endif
//
#ifndef _UINT32_T
#define _UINT32_T
typedef unsigned int uint32_t;
#endif /* _UINT32_T */
cache_t
结构体包含 一个结构体指针和两个 mask_t
类型的成员变量, 且 mask_t
在 64位情况下为 int 类型。所以 cache
部分总共占用内存为 16 位。
4. class_data_bits_t bits
bits
也是一个结构体类型, 当我们去查看 objc_class
里面的函数时, 会发现很多地方都跟 bits
有关。然后尝试查看 bits
的函数实现, 发现了一个有趣的东西, 内容如下:
//
class_rw_t* data() {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
//
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint32_t version;
const class_ro_t *ro;
method_array_t methods;
property_array_t properties;
protocol_array_t protocols;
Class firstSubclass;
Class nextSiblingClass;
char *demangledName;
#if SUPPORT_INDEXED_ISA
uint32_t index;
#endif
/*
...函数
*/
}
在 class_data_bits_t
的 data()
函数返回了一个 class_rw_t
类型的指针对象, 然后在 class_rw_t
结构体里面看到了对 methods
、properties
、protocols
等的声明。难道我们的类声明的方法、属性等存储在这里面吗, 接下来一起着重对这个 bits
来进行研究看看。
类的结构探究
1. 准备
开始探究之前, 先回到我们的 objc
源码里面, 新建一个 target
, 新建一个 Person
类, 然后开始我们的探索
// 类
@interface Person : NSObject {
NSString *name;
}
@property (nonatomic, copy) NSString *sex;
@property (nonatomic, assign) NSInteger age;
+ (void)person_sayHello;
- (void)person_study;
@end
// main.m
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
Person *person = [[Person alloc] init];
Class pClass = object_getClass(person);
NSLog(@"%@", pClass);
}
return 0;
}
类和元类的创建时机
开始之前我们先看一个东西, 在方法执行之前打一个断点, 然后对 Person
类进行打印, 看一下是否存在。
如上图所示, 此时 person
对象还没有被创建, 但是我们对 Person
的类对象进行打印, 发现已经存在在内存当中了。所以在类进行 alloc
操作之前, 类和元类就已经存在了, 说明类和元类在编译时期就已经被创建了。
2. bits 探索
想要去探索 bits
, 首先要解决如何找到他的问题, 然后才能在内存中对其结构进行一一分析。在上面的结构分析中我们知道, objc_class
内部一共包含 isa
、superclass
、cache
和 bits
组成, 前面的三位总共占 32 位, 那么接下来就借助指针平移来找到 bits
。
2.1 找到 bits 的内存地址
通过指针平移后得到新的地址, 然后进行打印验证得出 bits
的指针地址。
2.2 关于 class_rw_t
找到了 bits
, 我们上面提到过, bits
可以通过 data()
函数返回一个 class_rw_t
类型的指针对象。所以为了去查看 class_rw_t
的结构, 继续往下面走
上面通过 data()
函数得到了 class_rw_t
类型的指针, 然后又通过打印发现, 果然是我们想要找的内容, 在里面也发现了我们想要寻找的 methods
、properties
和 protocols
。接下来一起看看类的 属性 和 方法是否在这里面。
// 关于指针前面的 " * " 符号
// 对于指针而言, 星号一般出现的场合, 一个是指针定义时, 另一个是使用指针时。
1. int *p
指针定义时前面的星号, 目的是告诉编译器变量 p 是一个指针
2. *p + 1
使用指针时, 可以理解为 指针的值, 比如上面这里就是 指针指向的值 加上 1 。
1. properties 分析
首先直接打印 properties
, 发现内部存储的是一个 list_array_tt
类型的东西, 然后打印他的 list
, 发现他是一个 property_list_t
类型的指针, 进行值打印, 又出来一个 entsize_list_tt
, 下面是 entsize_list_tt
的声明
// 泛型
template <typename Element, typename List, uint32_t FlagMask>
// entsize_list_tt 的声明
struct entsize_list_tt {
uint32_t entsizeAndFlags;
uint32_t count;
Element first;
uint32_t entsize() const {
return entsizeAndFlags & ~FlagMask;
}
uint32_t flags() const {
return entsizeAndFlags & FlagMask;
}
Element& getOrEnd(uint32_t i) const {
assert(i <= count);
return *(Element *)((uint8_t *)&first + i*entsize());
}
Element& get(uint32_t i) const {
assert(i < count);
return getOrEnd(i);
}
size_t byteSize() const {
return byteSize(entsize(), count);
}
/*
一些函数...
*/
}
如上, 关于 template
好像是 C++
里面的东西, 用来做泛型编程的, 有兴趣的可以去查询一下。 entsizeAndFlags
和 count
可以直接打印出来, 重要的是 first
, 然后发现有两个函数 getOrEnd() 和 get()
, 尝试调用之后发现真的可以得到与我们的属性相关的东西。但是, 没有找到类的成员变量。
2. methods
打印结果如上图, 过程跟 第1步的 properties
一样, 这里不做描述了。可以看到定义的 实例方法 和 属性的 getter,setter
方法都可以找到, 除此之外还有一个 C++
的析构函数 .cxx_destruct
。但是, 类方法没有在里面
3. 关于 ro
为了寻找 类方法
和 成员变量
, 继续对 class_rw_t
的结构进行查看, 发现最有可能在的地方只有可能是 ro
。
// ro
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif
const uint8_t * ivarLayout;
const char * name;
method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
method_list_t *baseMethods() const {
return baseMethodList;
}
};
在其内部同样发现了关于方法和属性的东西 baseMethodList
、baseProperties
, 还有一个新的东西 ivars
, 接下来来打印一下里面到底有什么 (过程省略, 直接上图):
从打印结果可以看出, 不只是声明的成员变量, 属性自动生成的带 "_" 的成员变量也在其中, 所以成员变量存在于 ro
下面的 ivars
中。
注意: 此处省略了对 baseMethodList
和 baseProperties
的打印, 有兴趣的可以去自己试一下, 你会发现这里同样存储着类的 实例方法 和 属性。
4. rw 与 ro
关于 rw
与 ro
, 这里猜测 rw
意思为 read write
, ro
为 read only
。因为动态性的特性, OC
在编译期保存了一份 类
的数据结构到 ro
中, 然后又在运行时存储另外一份到 rw
中, 给 runtime
去动态的修改使用。
另外, 我们在 rw
中可以发现 ro
是 const
类型也就是不可变的, 所以存储的 ro
中的任何东西都是不能够改变的, 而 rw
中的 methods 、properties、protocols
则是可以改变的。这也证明了 类
为什么不可以动态添加属性, 因为在添加属性时会伴随着添加成员变量, 而 ivar
存储在 ro
中, 是无法改变的。
3. 类方法的存储位置
前面找到了 属性 、实例方法 和 成员变量 , 那么 类方法 到底存储在哪里呢? 接下来我们借助 runtime
的 API 来一起测试一下。
1. 作为实例方法
//
// + (void)person_sayHello;
// - (void)person_study;
//
const char *className = object_getClassName(pClass);
Class metaClass = objc_getMetaClass(className);
Method method1 = class_getInstanceMethod(pClass, @selector(person_sayHello));
Method method2 = class_getInstanceMethod(metaClass, @selector(person_sayHello));
Method method3 = class_getInstanceMethod(pClass, @selector(person_study));
Method method4 = class_getInstanceMethod(metaClass, @selector(person_study));
NSLog(@"%p-%p-%p-%p",method1,method2,method3,method4);
打印结果:
0x0-0x100001108-0x100001170-0x0
根据上面打印结果可以总结如下:
person_sayHello
是 元类对象的实例方法, 所以存在于元类当中, 不存在类中person_study
是类对象的实例方法, 所以存在于类当中, 不存在元类中
2. 作为类方法
//
// + (void)person_sayHello;
// - (void)person_study;
//
const char *className = object_getClassName(pClass);
Class metaClass = objc_getMetaClass(className);
Method method1 = class_getClassMethod(pClass, @selector(person_sayHello));
Method method2 = class_getClassMethod(metaClass, @selector(person_sayHello));
Method method3 = class_getClassMethod(pClass, @selector(person_study));
Method method4 = class_getClassMethod(metaClass, @selector(person_study));
NSLog(@"%p-%p-%p-%p",method1,method2,method3,method4);
打印结果:
0x100001108-0x100001108-0x0-0x0
根据结果可以看出, 不管是类还是元类, 作为类方法去查找 person_study
方法时都无法获取到, 而去查找 person_sayHello
方法时又都找到了, 这里就有问题了, 那么我们进入到 class_getClassMethod
方法内部实现里去找一下答案:
Method class_getClassMethod(Class cls, SEL sel) {
if (!cls || !sel) return nil;
return class_getInstanceMethod(cls->getMeta(), sel);
}
//
Class getMeta() {
if (isMetaClass()) return (Class)this;
else return this->ISA();
}
//
inline Class
objc_object::ISA()
{
assert(!isTaggedPointer());
#if SUPPORT_INDEXED_ISA
if (isa.nonpointer) {
uintptr_t slot = isa.indexcls;
return classForIndex((unsigned)slot);
}
return (Class)isa.bits;
#else
return (Class)(isa.bits & ISA_MASK);
#endif
}
可以看到 class_getClassMethod
方法的内部其实也是在调用 class_getInstanceMethod
方法, 不同点在于 getMeta()
方法, 可以看到如果是元类调用会直接返回; 如果是类调用的话, 会返回 ISA()
, 也就是元类, 所以不管是在类中查找 person_sayHello
方法, 还是在元类中查找, 其结果是一样的。
所以, 类方法是存储在元类中的, 实例方法是存储在类中的
总结
本次探索对类进行了一系列的研究, 总结如下:
- 我们发现类其实也是一个对象, 并且类和元类是在编译时就创建的。
- 对类的结构进行了分析, 并且在
class_rw_t
和ro
里面找到了我们的属性
和实例方法
, 且仅在ro
中找到了我们的成员变量
。 - 然后后面又对
类方法
的存储位置进行了探索, 发现类方法是存在元类中的。
最后希望本次的探索对你有所启发, 如果有不对的地方还请各位指出, 谢谢大家。