iOS底层-类的底层原理(二)
前言
上一期我们探讨过类的本质,我们知道类的本质是结构体
,讲到类我们自然离不开类的各种属性
。平时在写属性的时候我们会用到各种关键词nonatomic
, atomic
,strong
, copy
,assign
等,我们又常说用这些修饰词跟oc的内存管理机制有关系。为什么是这样的,属性底层是怎么实现的呢?
探索
为方便下面的分析我们需要LLVM
源码(LLVM
是构架编译器(compiler
)的框架系统,以C++编写而成,用于优化以任意程序语言编写的程序的编译时间(compile-time
)、链接时间(link-time
)、运行时间(run-time
)以及空闲时间(idle-time
),对开发者保持开放,并兼容已有脚本。LLVM源码下载)。
- 利用
clang
分析oc代码底层实现
首先我们创建一个工程文件,在main文件里面定义一个类,并添加两组属性。
@interface Father : NSObject
@property (nonatomic, strong) NSString *aNikeName;
@property (nonatomic, copy) NSString *bNikeName;
@end
@implementation Father
@end
接着使用终端输入指令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main64.cpp
// aNikeName getter方法
static NSString * _I_Father_aNikeName(Father * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_Father$_aNikeName)); }
// aNikeName setter方法。(使用到了内存地址的平移)
static void _I_Father_setANikeName_(Father * self, SEL _cmd, NSString *aNikeName) { (*(NSString **)((char *)self + OBJC_IVAR_$_Father$_aNikeName)) = aNikeName; }
// bNikeName getter方法
static NSString * _I_Father_bNikeName(Father * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_Father$_bNikeName)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);
// bNikeName setter方法 (使用objc_setProperty)
static void _I_Father_setBNikeName_(Father * self, SEL _cmd, NSString *bNikeName) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct Father, _bNikeName), (id)bNikeName, 0, 1); }
观察源码发现两个属性的setter
方法存在差异,一个用到了内存地址的平移,另一个用到了objc_setProperty
。Why?我们可以肯定的是代码变成这样,发生在clang
编译之前。我们可以通过LLVM查找相关线索。
- 利用LLVM源码分析
用VSCode
将下载下来的LLVM
源码打开,因为上一步我们发现了objc_setProperty
这个关键词,我们可以以它为线索。全局搜索objc_setProperty
。
接着分析
getSetPropertyFn()
查找线索image
下一步分析
GetPropertySetFunction()
查找线索image
通过逆推我们发现在构造函数
PropertyImplStrategy strategy(CGM, propImpl)
内部,有一个枚举,枚举为GetSetProperty
和SetPropertyAndExpressionGet
时会调用GetPropertySetFunction()
。下面我们从构造函数出发找线索。image
从源码的PropertyImplStrategy这个枚举的注释中貌似发现了部分线索,再看下构造函数的实现
/// 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.
// 跟修饰词copy有关哦
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
修饰属性时,属性的setter
方法会用到objc_setProperty
函数方法。
源码分析过程中,我们发现底层不仅有objc_setProperty
还有objc_getProperty
。那么objc_getProperty
的底层逻辑是怎么走的呢,哪些情况属性的getter方法会调用呢?
首先LLVM
源码全局搜索objc_getProperty
接着查看函数
getGetPropertyFn()
实现image
继续探索我们发现下面一段代码
image
image
结论
- 当不使用
nonatomic
修饰属性,使用retain
、copy
修饰属性时,属性的getter
方法会调用objc_getProperty
。 - 当使用
retain
和copy
修饰属性时,属性的setter
方法会调用objc_setProperty
。
下面通过案例验证以上结果
首先我们创建一个工程文件,在main文件里面定义一个类,并添加六组属性。
@interface Father : NSObject
@property (nonatomic, strong) NSString *aNikeName;
@property (nonatomic, copy) NSString *bNikeName;
@property (nonatomic, retain) NSString *cNikeName;
@property (atomic, strong) NSString *dNikeName;
@property (atomic, copy) NSString *eNikeName;
@property (atomic, retain) NSString *fNikeName;
@end
@implementation Father
@end
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
接着使用终端输入指令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main64.cpp
// @implementation Father
// aNikeName getter方法 (使用内存平移)
static NSString * _I_Father_aNikeName(Father * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_Father$_aNikeName)); }
// aNikeName setter方法 (使用内存平移)
static void _I_Father_setANikeName_(Father * self, SEL _cmd, NSString *aNikeName) { (*(NSString **)((char *)self + OBJC_IVAR_$_Father$_aNikeName)) = aNikeName; }
// bNikeName getter方法 (使用内存平移)
static NSString * _I_Father_bNikeName(Father * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_Father$_bNikeName)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);
// bNikeName setter方法 (使用objc_setProperty)
static void _I_Father_setBNikeName_(Father * self, SEL _cmd, NSString *bNikeName) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct Father, _bNikeName), (id)bNikeName, 0, 1); }
// cNikeName getter方法 (使用内存平移)
static NSString * _I_Father_cNikeName(Father * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_Father$_cNikeName)); }
// cNikeName setter方法 (使用objc_setProperty)
static void _I_Father_setCNikeName_(Father * self, SEL _cmd, NSString *cNikeName) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct Father, _cNikeName), (id)cNikeName, 0, 0); }
// dNikeName getter方法 (使用内存平移)
static NSString * _I_Father_dNikeName(Father * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_Father$_dNikeName)); }
// dNikeName setter方法 (使用内存平移)
static void _I_Father_setDNikeName_(Father * self, SEL _cmd, NSString *dNikeName) { (*(NSString **)((char *)self + OBJC_IVAR_$_Father$_dNikeName)) = dNikeName; }
extern "C" __declspec(dllimport) id objc_getProperty(id, SEL, long, bool);
// eNikeName getter方法 (使用objc_getProperty)
static NSString * _I_Father_eNikeName(Father * self, SEL _cmd) { typedef NSString * _TYPE;
return (_TYPE)objc_getProperty(self, _cmd, __OFFSETOFIVAR__(struct Father, _eNikeName), 1); }
// eNikeName setter方法 (使用objc_setProperty)
static void _I_Father_setENikeName_(Father * self, SEL _cmd, NSString *eNikeName) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct Father, _eNikeName), (id)eNikeName, 1, 1); }
// fNikeName getter方法 (使用objc_getProperty)
static NSString * _I_Father_fNikeName(Father * self, SEL _cmd) { typedef NSString * _TYPE;
return (_TYPE)objc_getProperty(self, _cmd, __OFFSETOFIVAR__(struct Father, _fNikeName), 1); }
// fNikeName setter方法 (使用objc_setProperty)
static void _I_Father_setFNikeName_(Father * self, SEL _cmd, NSString *fNikeName) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct Father, _fNikeName), (id)fNikeName, 1, 0); }
// @end
由上面结果发现LLVM
源码探索出来调用objc_getProperty
和objc_setProperty
的条件与实际运行结果一致。
总结
- 使用
retain
和copy
修饰属性且不要使用nonatomic
修饰时生成的getter
方法会调用objc_getProperty
。 - 使用
retain
和copy
修饰的属性生成的setter
方法会调用objc_setProperty
。 - 以上两种情况以外的其他条件都是使用
内存平移
进行赋值。