前言
之前有讲到类的结构,了解到元类、isa走向关系和继承关系,还从类的bits里拿到的方法和属性。这一篇我们继续深入研究,探索类里面还有什么。
探索DDAnimal.m的底层源码
@interface DDAnimal : NSObject {
NSString *_sex;
}
@property (nonatomic, copy) NSString *name; ///<
@property (nonatomic, strong) NSString *nickName; ///<
@property (nonatomic, assign) NSInteger height; ///<
@end
这是文件的上层代码,通过clang,将DDAnimal.m编译成DDAnimal.cpp文件。
clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk DDAnimal.m -o DDAnimal.cpp 编译后的代码
#ifndef _REWRITER_typedef_DDAnimal
#define _REWRITER_typedef_DDAnimal
typedef struct objc_object DDAnimal;
typedef struct {} _objc_exc_DDAnimal;
#endif
extern "C" unsigned long OBJC_IVAR_$_DDAnimal$_name;
extern "C" unsigned long OBJC_IVAR_$_DDAnimal$_nickName;
extern "C" unsigned long OBJC_IVAR_$_DDAnimal$_height;
struct DDAnimal_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *_sex;
NSString * _Nonnull _name;
NSString * _Nonnull _nickName;
NSInteger _height;
};
// @property (nonatomic, copy) NSString *name;
// @property (nonatomic, strong) NSString *nickName;
// @property (nonatomic, assign) NSInteger height;
/* @end */
#pragma clang assume_nonnull end
// @implementation DDAnimal
static NSString * _Nonnull _I_DDAnimal_name(DDAnimal * self, SEL _cmd) { return (*(NSString * _Nonnull *)((char *)self + OBJC_IVAR_$_DDAnimal$_name)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);
static void _I_DDAnimal_setName_(DDAnimal * self, SEL _cmd, NSString * _Nonnull name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct DDAnimal, _name), (id)name, 0, 1); }
static NSString * _Nonnull _I_DDAnimal_nickName(DDAnimal * self, SEL _cmd) { return (*(NSString * _Nonnull *)((char *)self + OBJC_IVAR_$_DDAnimal$_nickName)); }
static void _I_DDAnimal_setNickName_(DDAnimal * self, SEL _cmd, NSString * _Nonnull nickName) { (*(NSString * _Nonnull *)((char *)self + OBJC_IVAR_$_DDAnimal$_nickName)) = nickName; }
static NSInteger _I_DDAnimal_height(DDAnimal * self, SEL _cmd) { return (*(NSInteger *)((char *)self + OBJC_IVAR_$_DDAnimal$_height)); }
static void _I_DDAnimal_setHeight_(DDAnimal * self, SEL _cmd, NSInteger height) { (*(NSInteger *)((char *)self + OBJC_IVAR_$_DDAnimal$_height)) = height; }
// @end
这里可以看出类就是一个struct,所有的属性也被注释掉,变成了带下划线的成员变量,还有一个区别就是属性会创建set和get方法。
static void OBJC_CLASS_SETUP_$_DDAnimal(void ) {
OBJC_METACLASS_$_DDAnimal.isa = &OBJC_METACLASS_$_NSObject;
OBJC_METACLASS_$_DDAnimal.superclass = &OBJC_METACLASS_$_NSObject;
OBJC_METACLASS_$_DDAnimal.cache = &_objc_empty_cache;
OBJC_CLASS_$_DDAnimal.isa = &OBJC_METACLASS_$_DDAnimal;
OBJC_CLASS_$_DDAnimal.superclass = &OBJC_CLASS_$_NSObject;
OBJC_CLASS_$_DDAnimal.cache = &_objc_empty_cache;
}
在这个函数里也能看到DDAnimal的继承和isa的指向,同时也能看到METACLASS
(元类),这也印证了之前探索的isa走向和继承关系。
类型编码
{(struct objc_selector *)"setName:", "v24@0:8@16", (void *)_I_DDAnimal_setName_}
注意到了这样一行代码,这里应该是setName方法的定义,那么"v24@0:8@16"
是什么意思呢?
参考苹果官网类型编码图表
按照上图一一对比,可知:
v : 表示返回值,返回一个void;
24 : 表示这一串所占用的内存,总共24个字节;
@ : 表示第一个传入的参数,传入一个id类型的参数,这里表示的是self;
0 : 表示从0号位置开始存第一个参数;
: : 表示第二个传入的参数,传入一个method selector;
8 : 表示从8号位置开始存第二个参数;
@ : 表示第三个传入的参数,也是传入一个id类型的参数,这里表示的是一个NSString类型的;
16 : 表示从16号位置开始存第三个参数;
static void _I_DDAnimal_setName_(DDAnimal * self, SEL _cmd, NSString * _Nonnull name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct DDAnimal, _name), (id)name, 0, 1); }
对照方法的实现,正如我们所分析的,非常清晰,一目了然。
objc_setProperty() VS self + OBJC_IVAR_$_DDAnimal$_nickName
上面的set可以看出有的用了objc_setProperty(),有的又用的self + OBJC_IVAR_$_DDAnimal$_nickName
,先来看看self + OBJC_IVAR_$_DDAnimal$_nickName
是什么。
// 可以看出是通过__OFFSETOFIVAR__来赋值的,
extern "C" unsigned long int OBJC_IVAR_$_DDAnimal$_nickName __attribute__ ((used, section ("__DATA,__objc_ivar"))) = __OFFSETOFIVAR__(struct DDAnimal, _nickName);
// __OFFSETOFIVAR__的定义
#define __OFFSETOFIVAR__(TYPE, MEMBER) ((long long) &((TYPE *)0)->MEMBER)
((long long) &((TYPE *)0)->MEMBER)
ANSI C标准允许值为0的常量被强制转换成
任何一种类型的指针
,
并且转换结果是一个NULL指针
,因此((type *)0)的结果就是一个类型为type *的NULL指针
。
如果利用这个NULL指针来访问type的成员当然是非法的,
但&( ((type *)0)->MEMBER )的意图仅仅是计算MEMBER字段的地址
。
聪明的编译器根本就不生成访问type的代码
,
而仅仅是根据type的内存布局
和结构体实例首址
在编译期计算这个(常量)地址,
这样就完全避免了通过NULL指针访问内存的问题。
又因为首址为0,所以这个地址的值就是字段相对于结构体基址的偏移
。
以上方法避免了实例化一个type对象,并且求值在编译期进行,没有运行期负担。
这样一来就很好理解了,self + OBJC_IVAR_$_DDAnimal$_nickName
就是根据结构体的首地址加上成员相对应首地址的便宜,找到这片内存空间,然后直接赋值,非常清晰。
那么objc_setProperty()呢?
objc_setProperty()
在objc源码里能找到方法的声明和实现,但是却没法知道为什么有的会使用objc_setProperty(),有的却不用,很明显,这个应该是在编译期决定的,既然是编译期,那么我们就要去看看LLVM了(戴上痛苦面具,看看LLVM)。
在LLVM里搜索objc_setProperty,出来一长串,痛苦开始,挨个看它是在哪里调用的。仔细一看,搜索出来的结果挺多的,但是很多都是注释,除开注释,最终锁定在了这里。
return CGM.CreateRuntimeFunction(FTy, "objc_setProperty");
看函数名称,大致能理解成创建了一个"objc_setProperty"的runtime方法。
既然是创建,那肯定是要调用才创建,应该就是这里了。这个函数是getSetPropertyFn()
,继续看看这个函数是在哪里调用的
llvm::FunctionCallee CGObjCMac::GetPropertySetFunction() {
return ObjCTypes.getSetPropertyFn();
}
找到GetPropertySetFunction()
case PropertyImplStrategy::GetSetProperty:
case PropertyImplStrategy::SetPropertyAndExpressionGet: {
llvm::FunctionCallee setOptimizedPropertyFn = nullptr;
llvm::FunctionCallee setPropertyFn = nullptr;
if (UseOptimizedSetter(CGM)) {
// 10.8 and iOS 6.0 code and GC is off
setOptimizedPropertyFn =
CGM.getObjCRuntime().GetOptimizedPropertySetFunction(
strategy.isAtomic(), strategy.isCopy());
if (!setOptimizedPropertyFn) {
CGM.ErrorUnsupported(propImpl, "Obj-C optimized setter - NYI");
return;
}
}
else {
setPropertyFn = CGM.getObjCRuntime().GetPropertySetFunction(); // 这里调用了
if (!setPropertyFn) {
CGM.ErrorUnsupported(propImpl, "Obj-C setter requiring atomic copy");
return;
}
}
找到调用GetPropertySetFunction()
的位置,发现是在这两个条件下会调用,接下来要去找到这些条件是在哪里赋值的。
PropertyImplStrategy
::GetSetProperty
:PropertyImplStrategy
::SetPropertyAndExpressionGet
:
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;
}
}
}
找到PropertyImplStrategy
的实现(代码过长,这里只截取了GetSetProperty
、SetPropertyAndExpressionGet
相关的)。我们发现
- copy修饰的是GetSetProperty。
- retain修饰的:
-
- 在ARC环境下且不是atomic,
if (ivarType.getObjCLifetime() == Qualifiers::OCL_Strong)
情况,Kind = Expression
,否则Kind = SetPropertyAndExpressionGet
;
- 在ARC环境下且不是atomic,
-
- 如果不满足ARC环境或者是atomic,如果不是atomic,
Kind = SetPropertyAndExpressionGet
,否则Kind = GetSetProperty
。
- 如果不满足ARC环境或者是atomic,如果不是atomic,
小结
至此,我们也算是找了为什么会有两种set函数了。因为SetPropertyAndExpressionGet
和GetSetProperty
都会使用objc_setProperty(),所以copy
和retain
修饰的属性不论是nonatomic还是atomic在底层都会使用objc_setProperty()。
Tips: 在
PropertyImplStrategy
的实现里还有其他修饰符的赋值,代码过多,感兴趣的可以去看看。
objc_getProperty()
同样的方法我们去查找什么时候会使用objc_getProperty()。代码过多,就只贴出关键位置。
return CGM.CreateRuntimeFunction(FTy, "objc_getProperty");
llvm::FunctionCallee CGObjCMac::GetPropertyGetFunction() {
return ObjCTypes.getGetPropertyFn();
}
case PropertyImplStrategy::GetSetProperty: {
llvm::FunctionCallee getPropertyFn =
CGM.getObjCRuntime().GetPropertyGetFunction();
}
找到最终使用的地方,可以看出只有GetSetProperty
类型才会使用objc_getProperty
。结合上面找到的PropertyImplStrategy
初始化函数,copy和(atomic, retain)
修饰的kind = GetSetProperty
。
可是情况真的是这样吗?代码验证一下
// OC代码
@property (nonatomic, copy) NSString *name;
@property (atomic, retain) NSString *retainName;
// c++代码
static NSString * _Nonnull _I_DDAnimal_name(DDAnimal * self, SEL _cmd) { return (*(NSString * _Nonnull *)((char *)self + OBJC_IVAR_$_DDAnimal$_name)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);
static void _I_DDAnimal_setName_(DDAnimal * self, SEL _cmd, NSString * _Nonnull name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct DDAnimal, _name), (id)name, 0, 1); }
extern "C" __declspec(dllimport) id objc_getProperty(id, SEL, long, bool);
static NSString * _Nonnull _I_DDAnimal_retainName(DDAnimal * self, SEL _cmd) { typedef NSString * _Nonnull _TYPE;
return (_TYPE)objc_getProperty(self, _cmd, __OFFSETOFIVAR__(struct DDAnimal, _retainName), 1); }
static void _I_DDAnimal_setRetainName_(DDAnimal * self, SEL _cmd, NSString * _Nonnull retainName) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct DDAnimal, _retainName), (id)retainName, 1, 0); }
可以发现,两个属性都有objc_setProperty
,但是copy修饰的却没有objc_getProperty
。这是为什么呢?
case PropertyImplStrategy::GetSetProperty: {
llvm::FunctionCallee getPropertyFn =
CGM.getObjCRuntime().GetPropertyGetFunction();
if (!getPropertyFn) {
CGM.ErrorUnsupported(propImpl, "Obj-C getter requiring atomic copy");
return;
}
}
再看看源码,注意到下面还有个判断,判断方法是否生成成功,不成功则会提示"Obj-C getter requiring atomic copy"
,意思是需要atomic
修饰,去修改代码试一下。
// OC代码
@property (atomic, copy) NSString *name;
// c++代码
extern "C" __declspec(dllimport) id objc_getProperty(id, SEL, long, bool);
static NSString * _Nonnull _I_DDAnimal_name(DDAnimal * self, SEL _cmd) { typedef NSString * _Nonnull _TYPE;
return (_TYPE)objc_getProperty(self, _cmd, __OFFSETOFIVAR__(struct DDAnimal, _name), 1); }
发现name的getter也是使用objc_getProperty了。
小结
copy
和retain
修饰的属性必须是使用atomic,才会在底层使用objc_getProperty()。
补充
上面解释了什么情况会用objc_getProperty
,但是总感觉有点牵强,继续在llvm里找了下相关代码,还真的发现了疑似代码。
unsigned Attributes = PD->getPropertyAttributes();
if (mustSynthesizeSetterGetterMethod(IMD, PD, true /*getter*/)) {
bool GenGetProperty =
!(Attributes & ObjCPropertyAttribute::kind_nonatomic) &&
(Attributes & (ObjCPropertyAttribute::kind_retain |
ObjCPropertyAttribute::kind_copy));
std::string Getr;
if (GenGetProperty && !objcGetPropertyDefined) {
objcGetPropertyDefined = true;
// FIXME. Is this attribute correct in all cases?
Getr = "\nextern \"C\" __declspec(dllimport) "
"id objc_getProperty(id, SEL, long, bool);\n";
}
RewriteObjCMethodDecl(OID->getContainingInterface(),
PID->getGetterMethodDecl(), Getr);
Getr += "{ ";
// Synthesize an explicit cast to gain access to the ivar.
// See objc-act.c:objc_synthesize_new_getter() for details.
if (GenGetProperty) {
// return objc_getProperty(self, _cmd, offsetof(ClassDecl, OID), 1)
Getr += "typedef ";
const FunctionType *FPRetType = nullptr;
RewriteTypeIntoString(PID->getGetterMethodDecl()->getReturnType(), Getr,
FPRetType);
Getr += " _TYPE";
if (FPRetType) {
Getr += ")"; // close the precedence "scope" for "*".
// Now, emit the argument types (if any).
if (const FunctionProtoType *FT = dyn_cast<FunctionProtoType>(FPRetType)){
Getr += "(";
for (unsigned i = 0, e = FT->getNumParams(); i != e; ++i) {
if (i) Getr += ", ";
std::string ParamStr =
FT->getParamType(i).getAsString(Context->getPrintingPolicy());
Getr += ParamStr;
}
if (FT->isVariadic()) {
if (FT->getNumParams())
Getr += ", ";
Getr += "...";
}
Getr += ")";
} else
Getr += "()";
}
Getr += ";\n";
Getr += "return (_TYPE)";
Getr += "objc_getProperty(self, _cmd, ";
RewriteIvarOffsetComputation(OID, Getr);
Getr += ", 1)";
}
else
Getr += "return " + getIvarAccessString(OID);
Getr += "; }";
InsertText(startGetterSetterLoc, Getr);
}
这里看到有一个字符串拼接,拼接出来的字符串和c++代码里的极其相似。
extern "C" __declspec(dllimport) id objc_getProperty(id, SEL, long, bool);
return (_TYPE)objc_getProperty(self, _cmd, __OFFSETOFIVAR__(struct DDAnimal, _name), 1); }
都到这了,我们可以大胆猜测,c++里的代码就是这里拼接出来,淦。
那我们可以在这里找到刚刚的疑惑,GenGetProperty
是一个判断条件,不难看出,不是nonatomic
修饰,且必须是copy或者retain
就会使用objc_getProperty
。
同样的,也可以找到objc_setProperty
,大家可以自行查看。