开始本节内容之前需要对了解WWDC2020中对runtime的改动,主要是对clean memory
和dirty memory
以及ro
、rw
有个初步的了解。
实际上内存页有分类,一般来说分为 clean memory
和 dirty memory
两种,iOS
中也有 compressed memory
的概念。
Clean memory & dirty memory
对于一般的桌面操作系统,clean memory
可以认为是能够进行 Page Out
的部分。Page Out
指的是将优先级低的内存数据交换到磁盘上的操作,但iOS
并没有内存交换机制,所以对 iOS
这样的定义是不严谨的。那么对于 iOS
来说,clean memory
指的是能被重新创建的内存,它主要包含下面几类:
-
app
的二进制可执行文件 -
framework
中的_DATA_CONST
段 - 文件映射的内存
- 未写入数据的内存
内存映射的文件指的是当 app
访问一个文件时,系统会将文件映射加载到内存中,如果文件只读
,那么这部分内存就属于 clean memory
。另外需要注意的是,链接的 framework
中 _DATA_CONST
并不绝对属于 clean memory
,当app
使用到 framework
时,就会变成 dirty memory
。
未写入数据的内存也属于 clean memory
,比如下面这段代码,只有写入了的部分才属于 dirty memory
。
int *array = malloc(20000 * sizeof(int));
array[0] = 32
array[19999] = 64
所有不属于
clean memory
的内存都是 dirty memory
。这部分内存并不能被系统重新创建
,所以 dirty memory
会始终占据物理内存
,直到物理内存
不够用之后,系统便会开始清理
。Compressed memory
当物理内存不够用时,iOS
会将部分物理内存压缩
,在需要读写时再解压
,以达到节约内存
的目的。而压缩之后的内存,就是所谓的 compressed memory
。苹果最开始只是在 OS X
上使用这项技术,后来也在 iOS 系统
上使用。
摘自:《iOS Memory 内存详解 》这篇文章对iOS的内存进行了详细解读,感兴趣的可以去看下
WWDC2020中对runtime的改动
接下来结合WWDC2020中对runtime的改动探究类的数据结构,
在磁盘上app二进制文件中类是这样的
首先这有个类对象本身它包含了
最常被访问的信息
指向元类
、超类
和方法缓存指针
它还有一个指向更多数据的指针,存储额外信息的地方叫做
class_ro_t
,'RO'代表只读,它包含了类名
、方法
、协议
和实例变量
的信息,Swift
类和Objective-C
类共享
这一基础结构,所以每个Swift类也有这些数据结构。当类第一次从磁盘加载到内存中时,它一开始也是这样的但是
一经使用
它们就会发生变化
,这里就要结合clean memory
和 dirty memory
来看,clean memory
是指加载后不会发生变更的内存class_ro_t
就属于clean memory
因为它是只读的,dirty memory
是指在进程运行时会发生变化的内存,类结构一经使用就会变成 dirty memory
因为Runtime
会向它写入新的数据,例如 创建一个新的方法缓存
并从类中指向它,dirty memory
比 clean memory
要昂贵得多,只要进程在运行 它就必须一直存在 另一方面 clean memory
可以进行移除从而节省更多的内存空间,因为如果你需要clean memory
系统可以从磁盘中重新加载
,macOS
可以选择缓冲 dirty memory
但是iOS不能使用swap(内存交换机制iOS不支持)
,所以dirty memory
在iOS中代价很大,dirty memory
是这个类被分成两部分的原因,可以保持清洁的数据越多越好,通过分离出那些永远不会改变的数据可以把大部分数据存储为clean memory
。虽然有了这些数据足以让我们开始,但是运行时需要追踪每个类的更多信息,所以当一个类首次被使用运行时会为它分配额外的容量,这个运行时分配的容量是class_rw_t
用于读取-编写数据,在这个数据结构中储存了只有运行时才会生成的新信息。例如 所有的类都会链接成一个树状结构这是通过
First Subclass
和 Next Sibling Class
指针实现的,这允许运行时遍历当前使用的所有类这对于使方法缓存无效非常有用。但是为什么方法和属性也在只读数据中时,这里还要有方法和属性呢?因为他们可以在运行时进行改变,当
category
被加载时它可以向类中添加新方法,而且程序员可以使用运行时的API
动态的添加它们,因为 class_ro_t
是只读的,所以需要在class_rw_t
中追踪这些东西,现在结果是这样做会占用相当多的内存,在任何给定的设备中都有许多类在使用,Ben(苹果工程师的名字)
在iPhone
上的整个系统中测量了大约30兆字节这些 class_rw_t
结构,那么如何缩小这些结构呢?记住我们需要这些东西存在read/write区
因为运行时它们可能会被改变,但是通过检查实际设备上的使用情况发现大约只有10%
的类真正地更改了它们的方法。而且
demangled name
这个字段只有Swift
类才可能使用到,而且只有当有东西访问它们的Objective-C名称
时才会用到,所以可以拆掉那些平时不用的部分这将
class_rw_t
的大小减少了一半,对于那些确实需要额外信息的类,我们可以分配这些扩展中的一个并把它滑到类中供其使用大约有90%
的类从来不需要这些扩展数据,这在系统范围中可节省大约14MB的内存。
实际上可以在Mac
上看到这一变化带来的影响,这只需要在终端上执行一个命令,下面我们看下邮件
这个程序的情况
heap Mail | egrep 'class_rw|COUNT'
可以看到
邮件app
中使用了大约6000
个这样的class_rw_t
类型,但是其中只有大约十分之一 600
多一点实际上需要使用这一扩展信息,所以通过这个改变节省了很多内存。现在很多从类中读取数据的代码都必须同时处理那些有扩展数据和没有扩展数据的类当然运行时会为我们处理这一切,并且从外部看 一切都像往常一个工作,只是使用了更少的内存。
了解完这些之后我们就可以继续类的探索了,这里有个小插曲,不知道你有没有注意到《iOS底层原理探究05-类的底层原理isa链&继承链&类的内存结构》里
firstSubclass
为nil但是LGPerson
明明是有子类的LGTeacher
的
这是为啥呢?其实原因很简单,因为LGTeacher类是懒加载的,当前还没加载
我们p一下
LGTeacher
之后,在输出LGPerson
的数据firstSubclass
已经被赋值了下面继续探索类的数据结构,通过上面对WWDC2020的分析我们知道类的成员变量存在
ro
结构中,我们到ro
里看下能不能找到- 1.获取class的data数据
- 2.获取
ro
数据 - 3.获取
ro
中的ivars成员变量
数据 - 4.看下成员变量输出的信息
-
name = 0x0000000100003d29 "subject"
成员变量名称 -
type = 0x0000000100003eaa "@\"NSString\""
成员变量类型 -
size = 8
成员变量大小
-
有些同学可能会疑惑subject
是哪儿冒出来的,它是LGPerson
的一个承运变量
我们再多添加几个成员变量,研究一下
@interface LGPerson : NSObject<NSCoding,NSObject>{
NSString *subject;
int age;
double height;
NSObject * face;
}
前面的流程是一致的,后面的type里出现的
i
,d
,@
是啥意思,这些其实是类型编码。可以看下表对照也可以直接去官网看除了存储的地方不一样
属性
和成员变量
还有没有其他的区别呢,下面就来探索一下,通过xcrun编译生成C/C++源码看看在源码层面(xcrun的介绍和使用),他们有什么区别下面我们把这个类编译一下看下底层有什么区别
//编译前
// 成员变量 vs 属性 VS 实例变量
@interface LGPerson : NSObject
{
NSString *hobby; // 字符串
int a;
NSObject *objc; // 结构体
}
@property (nonatomic, copy) NSString *nickName;
@property (nonatomic, strong) NSString *name;
@end
//编译后
...省略无关代码
#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$_nickName;
extern "C" unsigned long OBJC_IVAR_$_LGPerson$_name;
struct LGPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *hobby;
int a;
NSObject *objc;
NSString *_nickName;
NSString *_name;
};
// @property (nonatomic, copy) NSString *nickName;
// @property (nonatomic, strong) NSString *name;
/* @end */
// @implementation LGPerson
static NSString * _I_LGPerson_nickName(LGPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_LGPerson$_nickName)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);
static void _I_LGPerson_setNickName_(LGPerson * self, SEL _cmd, NSString *nickName) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct LGPerson, _nickName), (id)nickName, 0, 1); }
static NSString * _I_LGPerson_name(LGPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_LGPerson$_name)); }
static void _I_LGPerson_setName_(LGPerson * self, SEL _cmd, NSString *name) { (*(NSString **)((char *)self + OBJC_IVAR_$_LGPerson$_name)) = name; }
// @end
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[8];
} _OBJC_$_INSTANCE_METHODS_LGPerson __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
8,
{{(struct objc_selector *)"nickName", "@16@0:8", (void *)_I_LGPerson_nickName},
{(struct objc_selector *)"setNickName:", "v24@0:8@16", (void *)_I_LGPerson_setNickName_},
{(struct objc_selector *)"name", "@16@0:8", (void *)_I_LGPerson_name},
{(struct objc_selector *)"setName:", "v24@0:8@16", (void *)_I_LGPerson_setName_},
{(struct objc_selector *)"nickName", "@16@0:8", (void *)_I_LGPerson_nickName},
{(struct objc_selector *)"setNickName:", "v24@0:8@16", (void *)_I_LGPerson_setNickName_},
{(struct objc_selector *)"name", "@16@0:8", (void *)_I_LGPerson_name},
{(struct objc_selector *)"setName:", "v24@0:8@16", (void *)_I_LGPerson_setName_}}
};
...省略无关代码
- 底层
属性
会被添加下划线
如_name
变成一个以下划线开头的成员变量 - 系统自动给
属性
生成了getter
、setter
方法 -
属性
=承运变量
+getter
+setter
- 最下面方法列表中有的东西还需要看一下例如
{{(struct objc_selector *)"nickName", "@16@0:8", (void *)_I_LGPerson_nickName},
,前面的nickName
很明显是方法名也就是nickName
的getter
的方法名,但是后面的@16@0:8
这个怎么理解呢?- "@16@0:8"解析
- 1:@ 表示返回值的类型编码即返回值是
id
类型 - 2:16 表示整个这一串所占用的内存
- 3:@ 表示第一个参数为id类型
- 4: 0 表示从0号位置开始
- 5::表示第二个参数类型SEL
- 6:8表示从8号位置开始
- 1:@ 表示返回值的类型编码即返回值是
- "@16@0:8"解析
这样ivar和property的区别就比较清晰了,但是有一个细节不知道你注意到了没nickName
的setter
方法
static void _I_LGPerson_setNickName_(LGPerson * self, SEL _cmd, NSString *nickName) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct LGPerson, _nickName), (id)nickName, 0, 1); }
name
的setter
方法
static void _I_LGPerson_setName_(LGPerson * self, SEL _cmd, NSString *name) { (*(NSString **)((char *)self + OBJC_IVAR_$_LGPerson$_name)) = name; }
-
name
的setter
方法使用内存偏移赋值 -
nickName
的setter
方法则是直接调用objc_setProperty
这两个属性在OC
中我们声明的时候有什么区别呢?
@property (nonatomic, copy) NSString *nickName;
@property (nonatomic, strong) NSString *name;
nickName
使用的是copy
,而name
是用的strong
是否是因为这个导致底层setter
发生变化的呢。接下来我们再来研究下,因为属性
的getter 和 setter
方法都是编译器自动生成的,所以我们到llvm
源码中看看能不能找到答案。
关于llvm我们后面再补充(我网速有点慢,源码还没下下来😭)
objc_setProperty
做了啥,objc_setProperty
中最终都是调用reallySetProperty
来看下
reallySetProperty
的源码reallySetProperty
实质是深拷贝
的流程,会重新生成一个字符串对象并指向这个新的字符串,直接地址偏移赋值直接改原来的对象并没有这个深拷贝
的操作
解决上一篇遗留的问题
在《iOS底层原理探究05》中我们留下了一个问题,类方法
存在哪里?下面我来把这个坑填上
其实类方法
是存在元类
中的,我直接这么说你肯定不信,那么下面我们就来验证一下,上代码
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface LGPerson : NSObject<NSCoding,NSObject>
- (void)sayNB;
+ (void)say666;
@end
@implementation LGPerson
- (void)sayNB{
}
+ (void)say666{
}
实例方法sayNB
是在LGPerson类
里保存的这个在《iOS底层原理探究05》里已经验证过了,接下来就看看say666
方法是不是在LGPerson 元类
中
控制台输出是这样的接下来我添加上备注信息你就明白了
(lldb) x/4gx LGPerson.class //打印LGPerson类的内存结构
0x100008778: 0x00000001000087a0 0x000000010036c140
0x100008788: 0x0000000100649dd0 0x0002a04000000003
(lldb) p/x 0x00000001000087a0 & 0x00007ffffffffff8ULL //LGPerson类的isa & ISA_MASK获取到LGPerson元类
(unsigned long long) $1 = 0x00000001000087a0
(lldb) po 0x00000001000087a0
LGPerson //验证一下 获取到的确实是LGPerson元类
(lldb) p/x 0x00000001000087a0+0x20 //内存偏移找到元类bits数据结构 这在上一篇中讲到过
(long) $3 = 0x00000001000087c0
(lldb) p (class_data_bits_t *)0x00000001000087c0 // 告诉lldb bits是class_data_bits_t类型的
(class_data_bits_t *) $4 = 0x00000001000087c0
(lldb) p $4->data() //取出元类LGPerson的data
(class_rw_t *) $5 = 0x0000000100612780
(lldb) p *$5 //查看data的数据结构
(class_rw_t) $6 = {
flags = 2684878849
witness = 1
ro_or_rw_ext = {
std::__1::atomic<unsigned long> = {
Value = 4301561681
}
}
firstSubclass = nil
nextSiblingClass = 0x00007fff800fcec0
}
(lldb) p $6->methods() //获取到元类LGPerson的方法列表
(const method_array_t) $7 = {
list_array_tt<method_t, method_list_t, method_list_t_authed_ptr> = {
= {
list = {
ptr = 0x00000001000086b0
}
arrayAndFlag = 4295001776
}
}
}
Fix-it applied, fixed expression was:
$6.methods()
(lldb) p $7.list //取list
(const method_list_t_authed_ptr<method_list_t>) $8 = {
ptr = 0x00000001000086b0
}
(lldb) p $8.ptr //取prt
(method_list_t *const) $9 = 0x00000001000086b0
(lldb) p *$9 //查看method_list_t的数据结构
(method_list_t) $10 = {
entsize_list_tt<method_t, method_list_t, 4294901763, method_t::pointer_modifier> = (entsizeAndFlags = 27, count = 1)
}
(lldb) p $10.get(0).big() //取方法列表中的第一个方法 获取到了 say666方法
(method_t::big) $11 = {
name = "say666"
types = 0x0000000100003ea2 "v16@0:8"
imp = 0x0000000100003ba0 (KCObjcBuild`+[LGPerson say666])
}
(lldb)
添加完注释流程还是蛮清晰的,验证了类方法
确实存在元类
中
结论
实例方法
保存在类
的methods
数据结构里,类方法
以实例方法
的形式保存在元类
中,本质上不存在类方法
,类方法
是存在元类
中的实例方法,底层都是函数
。
使用Runtime API 验证方法存储位置
接下来我们使用runtime
的API
尝试打印LGPerson
类的保存的所有方法
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface LGPerson : NSObject
{
NSObject *objc;
NSString *nickName;
}
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) NSObject *obj;
- (void)sayHello;
+ (void)sayHappy;
@end
NS_ASSUME_NONNULL_END
下面是打印方法的代码
void lgObjc_copyMethodList(Class pClass){
unsigned int count = 0;
Method *methods = class_copyMethodList(pClass, &count);
for (unsigned int i=0; i < count; i++) {
Method const method = methods[I];
//获取方法名
NSString *key = NSStringFromSelector(method_getName(method));
LGLog(@"Method, name: %@", key);
}
free(methods);
}
运行结果
打印的结果中
sayHappy
并不在其中,因为我们知道它是类方法
保存在元类
中输出
元类
的方法列表打印出了sayHappy
- (void)sayHello;
+ (void)sayHappy;
我们用这三个方法检验一下
第一个方法验证sayHello
和sayHappy
是否是类
和元类
的实例方法
第一个方法验证sayHello
和sayHappy
是否是类
和元类
的类方法
第一个方法验证sayHello
和sayHappy
是否在类
和元类
中有IMP
实现
void lgInstanceMethod_classToMetaclass(Class pClass){
const char *className = class_getName(pClass);
Class metaClass = objc_getMetaClass(className);
Method method1 = class_getInstanceMethod(pClass, @selector(sayHello));
Method method2 = class_getInstanceMethod(metaClass, @selector(sayHello));
Method method3 = class_getInstanceMethod(pClass, @selector(sayHappy));
Method method4 = class_getInstanceMethod(metaClass, @selector(sayHappy));
LGLog(@"%s - %p-%p-%p-%p",__func__,method1,method2,method3,method4);
}
void lgClassMethod_classToMetaclass(Class pClass){
const char *className = class_getName(pClass);
Class metaClass = objc_getMetaClass(className);
Method method1 = class_getClassMethod(pClass, @selector(sayHello));
Method method2 = class_getClassMethod(metaClass, @selector(sayHello));
// - (void)sayHello;
// + (void)sayHappy;
Method method3 = class_getClassMethod(pClass, @selector(sayHappy));
Method method4 = class_getClassMethod(metaClass, @selector(sayHappy));
LGLog(@"%s-%p-%p-%p-%p",__func__,method1,method2,method3,method4);
}
void lgIMP_classToMetaclass(Class pClass){
const char *className = class_getName(pClass);
Class metaClass = objc_getMetaClass(className);
IMP imp1 = class_getMethodImplementation(pClass, @selector(sayHello));
IMP imp2 = class_getMethodImplementation(metaClass, @selector(sayHello));// 0
// sel -> imp 方法的查找流程 imp_farw
IMP imp3 = class_getMethodImplementation(pClass, @selector(sayHappy)); // 0
IMP imp4 = class_getMethodImplementation(metaClass, @selector(sayHappy));
NSLog(@"%p-%p-%p-%p",imp1,imp2,imp3,imp4);
NSLog(@"%s",__func__);
}
-
lgInstanceMethod_classToMetaclass
输出为0x1000081c0-0x0-0x0-0x100008158
,-(void)sayHello
是类的实例方法
,+(void)sayHappy
不是元类的实例方法
,+(void)sayHappy
不是类的实例方法
,+(void)sayHappy
是元类的实例方法
。 -
lgClassMethod_classToMetaclass
输出为0x0-0x0-0x100008158-0x100008158
,-(void)sayHello
不是类的类方法
,-(void)sayHello
不是元类的类方法
,+(void)sayHappy
是类的类方法
,+(void)sayHappy
是元类的类方法
。 -
lgIMP_classToMetaclass
输出为0x100003ac0-0x7fff202295c0-0x7fff202295c0-0x100003b00
,-(void)sayHello
在类中能找到IMP实现,-(void)sayHello
在元类中能找到IMP实现,+(void)sayHappy
在类中能找到IMP实现,+(void)sayHappy
在元类中能找到IMP实现。
分析:第一点毋庸置疑跟我们之前的结论是一致的,但是第二点有点问题class_getClassMethod(metaClass, @selector(sayHello))
有结果输出,+(void)sayHappy
怎么是元类
的类方法
呢,不是说类方法
以实例方法
的形式存在元类
中接下来我们来看下class_getClassMethod
的源码
/***********************************************************************
* class_getClassMethod. Return the class method for the specified
* class and selector.
**********************************************************************/
Method class_getClassMethod(Class cls, SEL sel)
{
if (!cls || !sel) return nil;
return class_getInstanceMethod(cls->getMeta(), sel);
}
可以看到源码的实现
跟我们之前得到的结论
是一致的,获取类方法
的实现就是去元类里找同名的实例方法
,那我们现在传进来的就是元类,通过cls->getMeta()
获取元类又会得到什么呢,我们来看下cls->getMeta()
的源码
// NOT identical to this->ISA when this is a metaclass
Class getMeta() {
if (isMetaClassMaybeUnrealized()) return (Class)this;
else return this->ISA();
}
如果当前传入的类
是元类
会直接返回当前类
,如果不是元类
返回当前类的isa
,所以class_getClassMethod(metaClass, @selector(sayHello)),其实底层是找metaClass
的实例方法
,当然可以找到。
第三点-(void)sayHello
在类中能找到IMP实现和+(void)sayHappy
在元类中能找到IMP实现这两个没啥问题 但是 -(void)sayHello
在元类中能找到IMP实现和+(void)sayHappy
在类中能找到IMP实现这两个就不正常了,而且不知道你注意到没他们两个的IMP实现地址是一样的
这是什么原因呢?
我们在看下
class_getMethodImplementation
的源码
__attribute__((flatten))
IMP class_getMethodImplementation(Class cls, SEL sel)
{
IMP imp;
if (!cls || !sel) return nil;
lockdebug_assert_no_locks_locked_except({ &loadMethodLock });
imp = lookUpImpOrNilTryCache(nil, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER);
// Translate forwarding function to C-callable external version
if (!imp) {
return _objc_msgForward;
}
return imp;
}
查找IMP
的过程就是通过SEL
查找IMP
的过程,也就是我们常说的方法查找流程
,不知道你有没有注意到这一句代码
if (!imp) {
return _objc_msgForward;
}
如果找不到SEL
对应的IMP
就返回_objc_msgForward
,这是个啥玩意儿呢?欢迎来到下一部分方法查找流程
。