前言
OC类的探索 对类结构进行了整体大概流程的分析。今天我们对类结构进行些补充,以及过程中出现的疑问进行说明。
一.工具&资料
LLVM
Advancements in the Objective-C runtime
WWDC 2020 - 类优化
自己研究都是对代码产生了好奇,然后进行摸索,加上不断的总结规律,才能加以验证,但是WWDC 这个可以直接给开发者提供思路、代码讲解的地方,其重要性不言而喻,比较懒,直接上干货:
Clean Memory 与 Dirty 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昂贵的多,只要进行运行它就必须一直存在,通过分离出那些不会被改变的数据,可以把大部分的类数据存储为clean memory,这是苹果追求的
class_rw_t 优化
- 当一个类首次被使用时,runtime会为它分配额外的存储容量,运行时分配的存储容量就是class_rw_t。class_rw_t用于读取-编写数据,在这个数据结构中存储的是只有运行时才会生成的新数据。 上图:
所有的类都会链接成一个树状结构这是通过firstSubclass和nextSiblingClass指针实现的,这样运行时会遍历当前使用的所有类。
问题:为什么方法,属性在class_ro_t中时,class_rw_t还要有方法,属性呢?
- 因为它们可以在运行时进行更改
- 当category被加载时,它可以向类中添加新的方法
- 通过runtime API手动向类中添加属性和方法
- class_ro_t 是只读的,所以我们需要在class_rw_t中来跟踪这些东西
问题:class_rw_t结构在苹果手机中,占用很多的内存,那么如何去缩小这些结构呢?
- 我们在读取—编写部分需要这些东西,因为它们在运行时可以被修改,但是大约10%的类是需要修改它们的方法
- 而且只有在swift中才会使用这个demangledName字段,但是swift类并不需要这个字段,除非是访问它们Objective-C名称时才需要。
因此我们可以拆除那些我们平时不常用的部分。图解如下:
结果:这样class_rw_t的大小会减少一半
对那些需要修改内存的,需要额外信息的类,我们可以分配这些扩展记录中的一个,并把它滑到类中供其使用。图解如下:
小结
class_rw_t优化,其实就是对class_rw_t不常用的部分进行了剥离。如果需要用到这部分就从扩展记录中分配一个,滑到类中供其使用。现在大家对类应该有个更清楚的认识。
二.变量及编码
成员变量
在Objective-C中写在类声明的大括号中的变量称之为成员变量,例如int a,NSObject *obj
成员变量用于类内部,无需与外界接触的变量
实例变量
变量的数据类型不是基本数据类型且是一个类则称这个变量为实例变量,例如 NSObject *obj
成员变量包含实例变量
小结
成员变量:在底层没有其他操作只是变量的声明
属性:系统会自动在底层添加了_属性名变量,同时生成setter和getter方法
三.官方类型编码
编码链接
也可以通过一下方法获取
打开xcode--> command+shift+0--> 搜索ivar_getTypeEncoding--> 点击Type Encodings
类型编码
Code | Meaning |
---|---|
c | A char |
i | An int |
s | A short |
l | A long |
l | is treated as a 32-bit quantity on 64-bit programs. |
q | A long long |
C | An unsigned char |
I | An unsigned int |
S | An unsigned short |
L | An unsigned long |
Q | An unsigned long long |
f | A float |
d | A double |
B | A C++ bool or a C99 _Bool |
v | A void |
* | A character string (char *) |
@ | An object (whether statically typed or typed id) |
# | A class object (Class) |
: | A method selector (SEL) |
[array type] | An array |
{name=type…} | A structure |
(name=type…) | A union |
bnum | A bit field of num bits |
^type | A pointer to type |
? | An unknown type (among other things, this code is used for function pointers) |
注意:
- OC 不支持 long double类型,@encode(long double)将会返回 d ,和double类型的编码一致。
- c 数组,在经过编码后,会在类型前加上数组的个数,如存储12个float类型的指针数组,将会返回 [12^f]
- 类对象会被当作结构体来处理,如@encode(NSObject)将会返回{NSObject=#}
方法的类型编码
runtime系统预备了一些编码,这些编码在协议中声明方法时会用到。编码列表如下:
Code | Meaning |
---|---|
r | const |
n | in |
N | inout |
o | out |
O | bycopy |
R | byref |
V | oneway |
可以通过以下代码进行校验:
NSLog(@"char --> %s",@encode(char));
输出结果:
char --> c
四.SEL和IMP关系
- SEL:方法编号
- IMP:函数指针地址
- SEL 相当于书本的目录名称
- IMP 相当于书的页码
如图:
五.setter方法底层实现方式
在探究属性和成员变量的区别时,发现属性setter方法有的是通过objc_setProperty实现的,有的是直接内存偏移获取变量地址,然后赋值
objc_setProperty和内存偏移
源码探索
@interface MHPerson : NSObject
{
NSString * newName;
NSObject * objc;
}
@property(nonatomic, copy)NSString * name;
@property(nonatomic,strong)NSString * hobby;
@property(nonatomic,assign)NSInteger age;
@end
@implementation MHPerson
@end
通过编译命令 编译main.m 生成cpp文件
clang -rewrite-objc main.m -o main.cpp
找到代码
static NSString * _I_MHPerson_name(MHPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_MHPerson$_name)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);
static void _I_MHPerson_setName_(MHPerson * self, SEL _cmd, NSString *name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct MHPerson, _name), (id)name, 0, 1); }
static NSString * _I_MHPerson_hobby(MHPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_MHPerson$_hobby)); }
static void _I_MHPerson_setHobby_(MHPerson * self, SEL _cmd, NSString *hobby) { (*(NSString **)((char *)self + OBJC_IVAR_$_MHPerson$_hobby)) = hobby; }
static NSInteger _I_MHPerson_age(MHPerson * self, SEL _cmd) { return (*(NSInteger *)((char *)self + OBJC_IVAR_$_MHPerson$_age)); }
static void _I_MHPerson_setAge_(MHPerson * self, SEL _cmd, NSInteger age) { (*(NSInteger *)((char *)self + OBJC_IVAR_$_MHPerson$_age)) = age; }
然后添加了几个属性,修改了几次顺序 , 引用objc_setProperty函数:
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);
然后copy属性会通过objc_setProperty实现的:
static void _I_MHPerson_setName_(MHPerson * self, SEL _cmd, NSString *name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct MHPerson, _name), (id)name, 0, 1); }
其他属性是通过内存偏移实现的。
那么setter和getter方法在编译期函数地址就已经确定,怎么确定是编译期呢?通过烂苹果查看可执行文件的函数表:
没有体现,那么只好在oc源码里找一下,发现了这么多东西:
void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy)
{
bool copy = (shouldCopy && shouldCopy != MUTABLE_COPY);
bool mutableCopy = (shouldCopy == MUTABLE_COPY);
reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy);
}
void objc_setProperty_atomic(id self, SEL _cmd, id newValue, ptrdiff_t offset)
{
reallySetProperty(self, _cmd, newValue, offset, true, false, false);
}
void objc_setProperty_nonatomic(id self, SEL _cmd, id newValue, ptrdiff_t offset)
{
reallySetProperty(self, _cmd, newValue, offset, false, false, false);
}
void objc_setProperty_atomic_copy(id self, SEL _cmd, id newValue, ptrdiff_t offset)
{
reallySetProperty(self, _cmd, newValue, offset, true, true, false);
}
void objc_setProperty_nonatomic_copy(id self, SEL _cmd, id newValue, ptrdiff_t offset)
{
reallySetProperty(self, _cmd, newValue, offset, false, true, false);
}
哇,好像调用的都是一个方法reallySetProperty,里面有offset、atomic、copy、mutablecopy,代码如下:
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
if (offset == 0) {
object_setClass(self, newValue);
return;
}
id oldValue;
id *slot = (id*) ((char*)self + offset);
if (copy) {
newValue = [newValue copyWithZone:nil];
} else if (mutableCopy) {
newValue = [newValue mutableCopyWithZone:nil];
} else {
if (*slot == newValue) return;
newValue = objc_retain(newValue);
}
if (!atomic) {
oldValue = *slot;
*slot = newValue;
} else {
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;
slotlock.unlock();
}
objc_release(oldValue);
}
其原理就是新值retain,旧值release
llvm探索
这里好像找不出什么,那么去llvm里面看一下:
找到了这个,有点懵,搜getSetPropertyFn,然后:
llvm::FunctionCallee CGObjCMac::GetPropertySetFunction() {
return ObjCTypes.getSetPropertyFn();
}
然后:
根据switch条件PropertyImplStrategy类型调用GetPropertySetFunction(),PropertyImplStrategy类型有两种GetSetProperty或者SetPropertyAndExpressionGet,下一步只要知道什么时候给策略赋值:
/// Pick an implementation strategy for the given property synthesis.
PropertyImplStrategy::PropertyImplStrategy(CodeGenModule &CGM,
const ObjCPropertyImplDecl *propImpl) {
const ObjCPropertyDecl *prop = propImpl->getPropertyDecl();
ObjCPropertyDecl::SetterKind setterKind = prop->getSetterKind();
IsCopy = (setterKind == ObjCPropertyDecl::Copy);
IsAtomic = prop->isAtomic();
HasStrong = false; // doesn't matter here.
// Evaluate the ivar's size and alignment.
ObjCIvarDecl *ivar = propImpl->getPropertyIvarDecl();
QualType ivarType = ivar->getType();
auto TInfo = CGM.getContext().getTypeInfoInChars(ivarType);
IvarSize = TInfo.Width;
IvarAlignment = TInfo.Align;
// If we have a copy property, we always have to use getProperty/setProperty.
// TODO: we could actually use setProperty and an expression for non-atomics.
if (IsCopy) {
Kind = GetSetProperty;
return;
}
// Handle retain.
if (setterKind == ObjCPropertyDecl::Retain) {
// In GC-only, there's nothing special that needs to be done.
if (CGM.getLangOpts().getGC() == LangOptions::GCOnly) {
// fallthrough
// In ARC, if the property is non-atomic, use expression emission,
// which translates to objc_storeStrong. This isn't required, but
// it's slightly nicer.
} else if (CGM.getLangOpts().ObjCAutoRefCount && !IsAtomic) {
// Using standard expression emission for the setter is only
// acceptable if the ivar is __strong, which won't be true if
// the property is annotated with __attribute__((NSObject)).
// TODO: falling all the way back to objc_setProperty here is
// just laziness, though; we could still use objc_storeStrong
// if we hacked it right.
if (ivarType.getObjCLifetime() == Qualifiers::OCL_Strong)
Kind = Expression;
else
Kind = SetPropertyAndExpressionGet;
return;
// Otherwise, we need to at least use setProperty. However, if
// the property isn't atomic, we can use normal expression
// emission for the getter.
} else if (!IsAtomic) {
Kind = SetPropertyAndExpressionGet;
return;
// Otherwise, we have to use both setProperty and getProperty.
} else {
Kind = GetSetProperty;
return;
}
}
// If we're not atomic, just use expression accesses.
if (!IsAtomic) {
Kind = Expression;
return;
}
// Properties on bitfield ivars need to be emitted using expression
// accesses even if they're nominally atomic.
if (ivar->isBitField()) {
Kind = Expression;
return;
}
// GC-qualified or ARC-qualified ivars need to be emitted as
// expressions. This actually works out to being atomic anyway,
// except for ARC __strong, but that should trigger the above code.
if (ivarType.hasNonTrivialObjCLifetime() ||
(CGM.getLangOpts().getGC() &&
CGM.getContext().getObjCGCAttrKind(ivarType))) {
Kind = Expression;
return;
}
// Compute whether the ivar has strong members.
if (CGM.getLangOpts().getGC())
if (const RecordType *recordType = ivarType->getAs<RecordType>())
HasStrong = recordType->getDecl()->hasObjectMember();
// We can never access structs with object members with a native
// access, because we need to use write barriers. This is what
// objc_copyStruct is for.
if (HasStrong) {
Kind = CopyStruct;
return;
}
// Otherwise, this is target-dependent and based on the size and
// alignment of the ivar.
// If the size of the ivar is not a power of two, give up. We don't
// want to get into the business of doing compare-and-swaps.
if (!IvarSize.isPowerOfTwo()) {
Kind = CopyStruct;
return;
}
llvm::Triple::ArchType arch =
CGM.getTarget().getTriple().getArch();
// Most architectures require memory to fit within a single cache
// line, so the alignment has to be at least the size of the access.
// Otherwise we have to grab a lock.
if (IvarAlignment < IvarSize && !hasUnalignedAtomics(arch)) {
Kind = CopyStruct;
return;
}
// If the ivar's size exceeds the architecture's maximum atomic
// access size, we have to use CopyStruct.
if (IvarSize > getMaxAtomicAccessSize(CGM, arch)) {
Kind = CopyStruct;
return;
}
// Otherwise, we can use native loads and stores.
Kind = Native;
}
小结
- copy修饰的属性使用objc_setProperty方式实现,其它属性使用内存偏移实现
- 苹果没有把所有的setter方法全部写在底层,因为如果底层需要维护,修改起来特别麻烦。搞了个适配器中间层,中间层的作用是供上层的setter调用,中间层对属性的修饰符进行判断走不同的流程,调用底层的方法实现
- 中间层的优点:底层变化上层不受影响,上层变化底层也不会受影响
六.getter方法底层实现方式
探寻
印象中setter方法和getter方法是同时存在的,那么既然objc_setProperty存在,objc_getProperty是不是也存在呢?经过一系列操作发现:
static NSString * _I_MHPerson_name(MHPerson * self, SEL _cmd) { typedef NSString * _TYPE;
return (_TYPE)objc_getProperty(self, _cmd, __OFFSETOFIVAR__(struct MHPerson, _name), 1); }
源码改成了这样
@property(atomic, copy)NSString * name;
- get方法是否通过objc_getProperty映射,受属性修饰符影响:当属性同时被atomic和copy修饰,get走objc_getProperty映射,反之走内存平移。
- 同时Retain和atomic也影响是否通过objc_getProperty映射,反之走内存平移。
LLVM
日常搜代码,发现就在set方法的正上方
所以按照刚刚找objc_setProperty的思路,发现也是在上方1109-1284,此处省略 太长了,等等,突然想起来点什么:
// If we have a copy property, we always have to use getProperty/setProperty.
// TODO: we could actually use setProperty and an expression for non-atomics.
if (IsCopy) {
Kind = GetSetProperty;
return;
}
这里对set/get都适用
小结
- atomic+ copy修饰的属性使用objc_getProperty方式实现,其它属性使用内存偏移实现
七.总结
仰之弥高,钻之弥坚,谨以此共勉。
比较笨,如果有什么疏漏或好的学习思路,欢迎指点交流。