前言
上一篇文章中ios 底层原理 03–isa 的走位图&类的结构探索对类结构进行了整体大概流程的分析。今天我们对类结构进行些补充,以及过程中出现的疑问进行说明,所以知识点比较散
WWDC2020上关于类的优化
链接如下Advancements in the Objective-C runtime
先上结论:
Dirty Memory跟Clean Memory的区别
Clean Memory
clean memory
加载后不会发生改变的内存class_ro_t
就属于clean memory
,因为它是只读
的不会对其内存进行修改clean memory
是可以进行移除
的,从而节省更多的内存空间,因为如果你需要clean memory
的时候系统可以从磁盘中重新加载
Dirty Memory
dirty memory
是指在进程运行时会发生改变的内存- 类结构一经使用就会变成
dirty memory
,因为运行时会向它写入新的数据。例如创建一个新的方法缓存并从类中指向它,初始化类相关的子类和父类 - dirty memory是类数据被分成两部分的主要原因
Dirty Memory跟Clean Memory的区别
dirty memory
要比clean memory
昂贵的多,只要进行运行它就必须一直存在,通过分离出那些不会被改变的数据,可以把大部分的类数据存储为clean memory
,这是苹果追求的
class_rw_t 优化
当一个类第一次被创建跟使用时,runtime
会为它分配额外的存储容量,运行时分配的存储容量就是class_rw_t
。class_rw_t
用于读-写
数据,在这个数据结构中存储的是只有运行时
才会生成的新数据 所有的类都是通过
firstSubclass
和nextSiblingClass
指针实现的树
结构,这样运行时会遍历当前使用的所有类
问题1:从上图可以看出 rw
跟 ro
中都有 Methods
方法Protocols
协议Properties
属性。这是为啥呢?
- 因为它们可以在
运行时
进行更改 - 当
category
被加载时,它可以向类中添加新的方法
- 通过
runtime
API手动向类中添加属性
和方法
class_ro_t
是只读
的,所以我们需要在class_rw_t
中来跟踪这些东西
问题 2:class_rw_t
结构在系统中,占用很多的内存,怎么优化结构?
- 我们在
rw
部分需要这些东西,是因为它们在运行时
可以被修改,但是大约90%的类是不需要修改的而且只有在swift
中才会使用这个demangledName
字段,但是swift
类很多时候也不需要这个字段,除了访问它们Objective-C
名称时才需要。所以可以拆除那些我们平时不常用的部分放在class_rw_ext_t
里面。
总结
class_rw_t
优化,其实就是对class_rw_t
不常用的部分进行了剥离
。如果需要用到这部分就从class_rw_ext_t(扩展记录)
中分配一个,滑到类中供其使用
类方法是否在元类中
对象(实例)方法
是存放在类
中的,这时候类
相对于元类
来说就是类对象
,在对象中的类方法
==类对象
的实例方法
,类对象
的实例方法
应该是元类
中
举例子说明
LGPerson *person
的减号方法 [person sayhello] 是存放在LGPerson
中的
这时候LGPerson
相对于LGPerson 的元类(用 XXX代替)
来说就是XXX对象
,在对象*person中的 +加号方法 init
==LGPerson
的减号方法init
,所以XXX 的对象
的减号方法
应该在XXX
中
在自定义的类中添加方法名相同的实例方法
和类方法
,编译运行不会报错。如果都是放在类
中,那么编译器肯定会出问题,因为识别不了哪个是我需要的方法,我们猜想类方法
不在类中,那么可能在元类
中。在上一篇文章中Meta Class(元类)中 我们说过原理,下面我们通过代码来验证一下。
class_getInstanceMethod得到类的实例方法
编写一段代码 其中
sayClassMethod
为类方法sayInstanceMethod
为对象方法
可以看出 类
的实例方法
中有sayInstanceMethod
方法 元类
的实例方法
中有sayClassMethod
方法
class_getClassMethod 得到类的类方法
可以看出 类
的类方法
中有sayClassMethod
方法 元类
的类方法
中有sayClassMethod
方法
总结
实例方法
在类
中,类方法
在元类
中- 编译器自动生成
元类
,目的是存放类方法
关于 2个 API的问题
sayInstanceMethod
怎么在LGPerson
类中怎么没有找到
源码分析:class_getClassMethod
就是去元类
中,查找实例方法
。打印结果sayInstanceMethod
不存在元类
中,找不到。sayClassMethod
在元类
中,可以找到。在底层不存在类方法
,都是按普通方法
进行查找。
class_getMethodImplementation
为啥会有指针地址 imp1和imp4应该是nil
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
= nil
时,系统会返回_objc_msgForward
,所以imp1
和imp4
才有函数指针地址,而且地址相同
成员变量,实例变量,属性的区别
成员变量
- 在
Objective-C
中写在类声明的大括号中的变量称之为成员变量,例如int a
,NSObject *obj
- 成员变量用于
类内部
,无需与外界接触的变量
@interface LGPerson : NSObject
{
NSObject *objc; //成员变量 实例变量
NSString *nickName;//成员变量
}
复制代码
实例变量
- 变量的数据类型不是
基本数据类型
且是一个类则称这个变量为实例变量,例如NSObject *obj
成员变量
包含实例变量
成员变量和属性的区别
- 成员变量:在底层没有其他操作只是变量的
声明
- 属性:系统会自动在底层添加了
_属性名变量
,同时生成setter
和getter
方法
SEL和IMP关系
- SEL:方法编号 SEL 相当于书本的目录名称(第几页的大纲)
- IMP:函数指针地址 IMP 相当于书的页码(第几页地址)
setter方法的底层实现
在探究属性和成员变量的区别时,发现属性setter方法有的是通过objc_setProperty
实现的,有的是直接内存偏移
获取变量地址,然后赋值
通过探索 llvm
可以得出
/// 对象 sel 偏移量 值 原子性 是否copy
void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy)
{
// 是否是 Copy
bool copy = (shouldCopy && shouldCopy != MUTABLE_COPY);
// 是否是 可变的Copy
bool mutableCopy = (shouldCopy == MUTABLE_COPY);
reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy);
}
复制代码
发现他实际调用了 reallySetProperty
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
if (offset == 0) {
// 修改 ISA
object_setClass(self, newValue);
return;
}
id oldValue;
// 定位(首地址+偏移量)
id *slot = (id*) ((char*)self + offset);
// Copy相关操作
if (copy) {
newValue = [newValue copyWithZone:nil];
} else if (mutableCopy) {
newValue = [newValue mutableCopyWithZone:nil];
} else {
if (*slot == newValue) return;// 相同的话不做操作
// 引用计数+1
newValue = objc_retain(newValue);
}
// 原子性相关
if (!atomic) {
oldValue = *slot;
*slot = newValue;
} else {
// 加锁
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;
slotlock.unlock();
}
// reloase 旧值 引用计数 -1
objc_release(oldValue);
}
复制代码
结论
copy
修饰的属性使用objc_setProperty
方式实现,其它属性使用内存偏移
实现- 苹果没有把所有的setter方法全部写在底层,因为如果底层需要维护,修改起来特别麻烦。搞了个
适配器中间层
,中间层的作用是供上层的setter
调用,中间层对属性的修饰符
进行判断走不同的流程,调用底层的方法实现
中间层的优点:底层变化上层不受影响
,上层变化底层也不会受影响
getter方法的底层实现
在探究属性和成员变量的区别时,基本上通过内存偏移
获取变量地址获取值。 只有少数条件下才通过objc_getProperty
方式实现
通过探索 llvm
可以得出
结论
-
ARC:
atomic+ copy
修饰的属性使用objc_getProperty
方式实现,其它属性使用内存偏移
实现 -
MRC:
atomic+ retain
修饰的属性使用objc_getProperty
方式实现,其它属性使用内存偏移
实现