上一篇文章中我们探索了类的结构,留下了一个疑问就是class_ro_t
和class_rw_t
的区别,我们从这个区别开始。
class_ro_t
和class_rw_t
的区别
class_ro_t
class_ro_t
存储了当前类在编译期就已经确定的属性
、方法
和遵循的协议
,里面没有category
的方法。运行时添加的方法存储在运行时生成的class_rw_t
中。
ro
标示的是read only
,是无法进行修改的,我们看一下它的定义:
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif
union {
const uint8_t * ivarLayout;
Class nonMetaclass;
};
explicit_atomic<const char *> name;
// With ptrauth, this is signed if it points to a small list, but
// may be unsigned if it points to a big list.
void *baseMethodList;//方法列表的获取方法
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;//成员变量列表
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;//属性列表
// This field exists only when RO_HAS_SWIFT_INITIALIZER is set.
_objc_swiftMetadataInitializer __ptrauth_objc_method_list_imp _swiftMetadataInitializer_NEVER_USE[0];
_objc_swiftMetadataInitializer swiftMetadataInitializer() const {
if (flags & RO_HAS_SWIFT_INITIALIZER) {
return _swiftMetadataInitializer_NEVER_USE[0];
} else {
return nil;
}
}
const char *getName() const {
return name.load(std::memory_order_acquire);
}
static const uint16_t methodListPointerDiscriminator = 0xC310;
#if 0 // FIXME: enable this when we get a non-empty definition of __ptrauth_objc_method_list_pointer from ptrauth.h.
static_assert(std::is_same<
void * __ptrauth_objc_method_list_pointer *,
void * __ptrauth(ptrauth_key_method_list_pointer, 1, methodListPointerDiscriminator) *>::value,
"Method list pointer signing discriminator must match ptrauth.h");
#endif
method_list_t *baseMethods() const {
#if __has_feature(ptrauth_calls)
method_list_t *ptr = ptrauth_strip((method_list_t *)baseMethodList, ptrauth_key_method_list_pointer);
if (ptr == nullptr)
return nullptr;
// Don't auth if the class_ro and the method list are both in the shared cache.
// This is secure since they'll be read-only, and this allows the shared cache
// to cut down on the number of signed pointers it has.
bool roInSharedCache = objc::inSharedCache((uintptr_t)this);
bool listInSharedCache = objc::inSharedCache((uintptr_t)ptr);
if (roInSharedCache && listInSharedCache)
return ptr;
// Auth all other small lists.
if (ptr->isSmallList())
ptr = ptrauth_auth_data((method_list_t *)baseMethodList,
ptrauth_key_method_list_pointer,
ptrauth_blend_discriminator(&baseMethodList,
methodListPointerDiscriminator));
return ptr;
#else
return (method_list_t *)baseMethodList;
#endif
}
///省略代码
};
复制代码
class_rw_t
类中的属性、方法还有遵循的协议等信息都保存在 class_rw_t
中,它是可读可写的:
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint16_t witness;
#if SUPPORT_INDEXED_ISA
uint16_t index;
#endif
explicit_atomic<uintptr_t> ro_or_rw_ext;
Class firstSubclass;
Class nextSiblingClass;
///省略代码
const method_array_t methods() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->methods;
} else {
return method_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseMethods()};
}
}
const property_array_t properties() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->properties;
} else {
return property_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProperties};
}
}
const protocol_array_t protocols() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->protocols;
} else {
return protocol_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProtocols};
}
}
};
复制代码
class_rw_t
生成在运行时,在编译期间,class_ro_t
结构体就已经确定,objc_class
中的bits
的data
部分存放着该结构体的地址。在runtime
运行之后,具体说来是在运行runtime
的realizeClass
方法时,会生成class_rw_t
结构体,该结构体包含了class_ro_t
,并且更新data部分,换成class_rw_t
结构体的地址。
WWDC2020中类结构优化
二进制类在磁盘中的表现是:
类对象本身,包含最常访问的信息:指向元类,超类和方法缓存的指针,在类结构之中有指向包含更多数据的结构体class_ro_t
的指针,包含了类的名称,方法,协议,实例变量等等编译期确定的信息。其中 ro 表示 read only 的意思。
当类被Runtime
加载之后,类的结构会发生一些变化在了解这些变化之前,我们需要知道2个概念:
**Clean Memory:**加载后不会发生更改的内存块,class_ro_t
属于Clean Memory
,因为它是只读的。
**Dirty Memory:**运行时会进行更改的内存块,类一旦被加载,就会变成Dirty Memory
,例如,我们可以在 Runtime 给类动态的添加方法。
Dirty Memory
比Clean Memory
要昂贵的多,因为它需要更多的内存信息,并且只要进程正在运行,就必须保留它。对于我们来说,越多的Clean Memory
显然是更好的,因为它可以节约更多的内存。我们可以通过分离出永不更改的数据部分,将大多数类数据保留为Clean Memory
,应该怎么做呢?
我们先看一下,类加载后的结构
在类加载到 Runtime 中后会被分配用于读取/写入数据的结构体class_rw_t
。
事实证明,class_rw_t
会占用比class_ro_t
占用更多的内存,在 iPhone 中,我们在系统测量了大约 30MB 的这些class_rw_t
结构。应该如何优化这些内存呢?通过测量实际设备上的使用情况,我们发现大约 10% 的类实际会存在动态的更改行为,如动态添加方法,使用 Category 方法等。因此,我们能可以把这部分动态的部分提取出来,我们称之为class_rw_ext_t
,所以,结构会变成这个样子。
经过拆分,可以把 90% 的类优化为Clean Memory
,在系统层面,取得效果是节省了大约 14MB 的内存,使内存可用于更有效的用途。
更多内容可以查看苹果方法视频
小结
class_ro_t存放的是编译期间就确定的;而class_rw_t是在runtime时才确定,它会先将class_ro_t的内容拷贝过去,然后再将当前类的分类的这些属性、方法等拷贝到其中。所以可以说class_rw_t是class_ro_t的超集,当然实际访问类的方法、属性等也都是访问的class_rw_t中的内容。
Type Encodings
在前面我们执行clang
将main.m
文件转换成main.cpp
文件的时候,我们发现有一些编码,例如
{(struct objc_selector *)"setNickName:", "v24@0:8@16", (void *)_I_JSPerson_setNickName_},
中的v24@0:8@16
,这其实就是运行时中的encode
编码,它的编码表如下:
v24@0:8@16
的含义也就是:
v
:void
24
:占用的内存@
: 对象类型参数self
0
:上面参数从0
位置开始:
:SEL
8
:SEL
从8
位置开始@
:对象类型,实际传入的第一个参数16
:从16
位置开始
setter
方法底层
我们首先定义一个JSPerson
类:
@interface JSPerson : NSObject
{
NSString *hobby; //
int a;
NSObject *objc; //
}
@property (nonatomic, copy) NSString *nickName;
@property (atomic, copy) NSString *acnickName;
@property (nonatomic) NSString *nnickName;
@property (atomic) NSString *anickName;
@property (nonatomic, strong) NSString *name;
@property (atomic, strong) NSString *aname;
@end
@implementation JSPerson
@end
复制代码
通过clang
命令将其转换成转换成c++
代码:
clang -rewrite-objc main.m -o main.cpp
我们在main.cpp
全局搜索JSPerson
,定位到类的方法的代码:
// @implementation JSPerson
static NSString * _I_JSPerson_nickName(JSPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_JSPerson$_nickName)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);
static void _I_JSPerson_setNickName_(JSPerson * self, SEL _cmd, NSString *nickName) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct JSPerson, _nickName), (id)nickName, 0, 1); }
extern "C" __declspec(dllimport) id objc_getProperty(id, SEL, long, bool);
static NSString * _I_JSPerson_acnickName(JSPerson * self, SEL _cmd) { typedef NSString * _TYPE;
return (_TYPE)objc_getProperty(self, _cmd, __OFFSETOFIVAR__(struct JSPerson, _acnickName), 1); }
static void _I_JSPerson_setAcnickName_(JSPerson * self, SEL _cmd, NSString *acnickName) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct JSPerson, _acnickName), (id)acnickName, 1, 1); }
static NSString * _I_JSPerson_nnickName(JSPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_JSPerson$_nnickName)); }
static void _I_JSPerson_setNnickName_(JSPerson * self, SEL _cmd, NSString *nnickName) { (*(NSString **)((char *)self + OBJC_IVAR_$_JSPerson$_nnickName)) = nnickName; }
static NSString * _I_JSPerson_anickName(JSPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_JSPerson$_anickName)); }
static void _I_JSPerson_setAnickName_(JSPerson * self, SEL _cmd, NSString *anickName) { (*(NSString **)((char *)self + OBJC_IVAR_$_JSPerson$_anickName)) = anickName; }
static NSString * _I_JSPerson_name(JSPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_JSPerson$_name)); }
static void _I_JSPerson_setName_(JSPerson * self, SEL _cmd, NSString *name) { (*(NSString **)((char *)self + OBJC_IVAR_$_JSPerson$_name)) = name; }
static NSString * _I_JSPerson_aname(JSPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_JSPerson$_aname)); }
static void _I_JSPerson_setAname_(JSPerson * self, SEL _cmd, NSString *aname) { (*(NSString **)((char *)self + OBJC_IVAR_$_JSPerson$_aname)) = aname; }
// @end
复制代码
可以看到,各个属性的get
和set
方法如上,可以发现,不同属性的set
方法执行的方法不一定相同,比如:
name
的set
方法:static void _I_JSPerson_setName_(JSPerson * self, SEL _cmd, NSString *name) { (*(NSString **)((char *)self + OBJC_IVAR_$_JSPerson$_name)) = name; }
nickName
的set
方法:static void _I_JSPerson_setNickName_(JSPerson * self, SEL _cmd, NSString *nickName) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct JSPerson, _nickName), (id)nickName, 0, 1); }
name
的set
方法内存平移
,nickName
的set
方法调用的是objc_setProperty
,这两种方式的是怎么确定的呢,什么情况下属性的set
方法调用objc_setProperty
方法呢?探究这个问题我们就需要使用LLVM
了,我们从github
上下载LLVM
的源码,下载地址。
LLVM
源码里我们全局搜索objc_setProperty
,查找调用这个方法的地方,发现了getSetPropertyFn
方法,它的返回值是CGM.CreateRuntimeFunction(FTy, "objc_setProperty");
:
llvm::FunctionCallee getSetPropertyFn() {
CodeGen::CodeGenTypes &Types = CGM.getTypes();
ASTContext &Ctx = CGM.getContext();
// void objc_setProperty (id, SEL, ptrdiff_t, id, bool, bool)
CanQualType IdType = Ctx.getCanonicalParamType(Ctx.getObjCIdType());
CanQualType SelType = Ctx.getCanonicalParamType(Ctx.getObjCSelType());
CanQualType Params[] = {
IdType,
SelType,
Ctx.getPointerDiffType()->getCanonicalTypeUnqualified(),
IdType,
Ctx.BoolTy,
Ctx.BoolTy};
llvm::FunctionType *FTy =
Types.GetFunctionType(
Types.arrangeBuiltinFunctionDeclaration(Ctx.VoidTy, Params));
return CGM.CreateRuntimeFunction(FTy, "objc_setProperty");
}
复制代码
接下来搜索关键字getSetPropertyFn()
,同样是查找调用的地方:
llvm::FunctionCallee GetPropertySetFunction() override {
return ObjCTypes.getSetPropertyFn();
}
复制代码
这里只是一个中间调用的方法,我们继续搜索关键字GetPropertySetFunction()
,找调用的地方:
void
CodeGenFunction::generateObjCSetterBody(const ObjCImplementationDecl *classImpl,
const ObjCPropertyImplDecl *propImpl,
llvm::Constant *AtomicHelperFn) {
//省略代码
switch (strategy.getKind()) {
case PropertyImplStrategy::Native: {
// We don't need to do anything for a zero-size struct.
if (strategy.getIvarSize().isZero())
return;
Address argAddr = GetAddrOfLocalVar(*setterMethod->param_begin());
LValue ivarLValue =
EmitLValueForIvar(TypeOfSelfObject(), LoadObjCSelf(), ivar, /*quals*/ 0);
Address ivarAddr = ivarLValue.getAddress(*this);
// Currently, all atomic accesses have to be through integer
// types, so there's no point in trying to pick a prettier type.
llvm::Type *bitcastType =
llvm::Type::getIntNTy(getLLVMContext(),
getContext().toBits(strategy.getIvarSize()));
// Cast both arguments to the chosen operation type.
argAddr = Builder.CreateElementBitCast(argAddr, bitcastType);
ivarAddr = Builder.CreateElementBitCast(ivarAddr, bitcastType);
// This bitcast load is likely to cause some nasty IR.
llvm::Value *load = Builder.CreateLoad(argAddr);
// Perform an atomic store. There are no memory ordering requirements.
llvm::StoreInst *store = Builder.CreateStore(load, ivarAddr);
store->setAtomic(llvm::AtomicOrdering::Unordered);
return;
}
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;
}
}
}
复制代码
这里是个switch
语句,调用的条件取决于strategy.getKind()
,我们接下来就搜索PropertyImplStrategy
找一下类型是什么时候设置的,在PropertyImplStrategy
的定义中我们找到了答案:copy
修饰的属性会有Kind = GetSetProperty
,也就是set
方法会调用objc_setProperty
// 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;
}
复制代码
我们写代码验证一下:
@property (nonatomic, copy) NSString *nickName;
@property (atomic, copy) NSString *acnickName;
@property (nonatomic) NSString *nnickName;
@property (atomic) NSString *anickName;
复制代码
我们定义四个属性,然后用clang
将其转换成c++
代码:
static NSString * _I_JSPerson_nickName(JSPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_JSPerson$_nickName)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);
static void _I_JSPerson_setNickName_(JSPerson * self, SEL _cmd, NSString *nickName) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct JSPerson, _nickName), (id)nickName, 0, 1); }//copy
extern "C" __declspec(dllimport) id objc_getProperty(id, SEL, long, bool);
static NSString * _I_JSPerson_acnickName(JSPerson * self, SEL _cmd) { typedef NSString * _TYPE;
return (_TYPE)objc_getProperty(self, _cmd, __OFFSETOFIVAR__(struct JSPerson, _acnickName), 1); }
static void _I_JSPerson_setAcnickName_(JSPerson * self, SEL _cmd, NSString *acnickName) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct JSPerson, _acnickName), (id)acnickName, 1, 1); }//copy
static NSString * _I_JSPerson_nnickName(JSPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_JSPerson$_nnickName)); }
static void _I_JSPerson_setNnickName_(JSPerson * self, SEL _cmd, NSString *nnickName) { (*(NSString **)((char *)self + OBJC_IVAR_$_JSPerson$_nnickName)) = nnickName; }//strong
static NSString * _I_JSPerson_anickName(JSPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_JSPerson$_anickName)); }
static void _I_JSPerson_setAnickName_(JSPerson * self, SEL _cmd, NSString *anickName) { (*(NSString **)((char *)self + OBJC_IVAR_$_JSPerson$_anickName)) = anickName; }//strong
复制代码
小结
set
方法在底层并不是定义了很多set
方法调用,而是采用内存平移
或调用objc_setProperty
方法。- 使用
copy
修饰的属性的set
方法调用的是objc_setProperty
方法。 - 没有
copy
修饰的属性的set
方法是内存平移
。
isKindOfClass
vsisMemberOfClass
我们平时开发经常会使用isKindOfClass
和isMemberOfClass
方法来判断对象的类型,从一个例子开始。首先定义两个继承关系的类:
@interface JSPerson : NSObject{
NSString *hobby;
}
@property (nonatomic, copy) NSString *name;
@property (nonatomic) int age;
@end
@implementation JSPerson
@end
@interface JSTeacher : JSPerson
@property (nonatomic, copy) NSString *hobby;
- (void)teacherSay;
@end
@implementation JSTeacher
- (void)teacherSay{
NSLog(@"%s",__func__);
}
@end
复制代码
我们定义一个方法,用来打印方法的结果:
void jsKindofDemo(void){
BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]]; //
BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]]; //
BOOL re3 = [(id)[JSPerson class] isKindOfClass:[JSPerson class]]; //
BOOL re4 = [(id)[JSPerson class] isMemberOfClass:[JSPerson class]]; //
NSLog(@" re1 :%hhd\n re2 :%hhd\n re3 :%hhd\n re4 :%hhd\n",re1,re2,re3,re4);
BOOL re5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]]; //
BOOL re6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]]; //
BOOL re7 = [(id)[JSPerson alloc] isKindOfClass:[JSPerson class]]; //
BOOL re8 = [(id)[JSPerson alloc] isMemberOfClass:[JSPerson class]]; //
NSLog(@" re5 :%hhd\n re6 :%hhd\n re7 :%hhd\n re8 :%hhd\n",re5,re6,re7,re8);
}
复制代码
打印结果如下:
re1 :1
re2 :0
re3 :0
re4 :0
re5 :1
re6 :1
re7 :1
re8 :1
复制代码
re5
–re8
符合我们平时使用的思想,对象判断是否是类,re1
–re4
是什么情况呢,我们看isKindOfClass
底层源码:
// Calls [obj isKindOfClass]
BOOL
objc_opt_isKindOfClass(id obj, Class otherClass)
{
#if __OBJC2__//我们只看这里 objc2
if (slowpath(!obj)) return NO;
Class cls = obj->getIsa();//获取对象(类对象)的类(元类)
if (fastpath(!cls->hasCustomCore())) {
for (Class tcls = cls; tcls; tcls = tcls->getSuperclass()) {//遍历父类
if (tcls == otherClass) return YES;
}
return NO;
}
#endif
return ((BOOL(*)(id, SEL, Class))objc_msgSend)(obj, @selector(isKindOfClass:), otherClass);
}
复制代码
通过源码和上一篇中isa
的走位和继承链,我们可以看出
-
实例对象调用:1、获取对象的类。2、依次查找对象的类和父类,如果和传入的类相等返回
YES
,遍历结束未找到返回NO
即查找顺序:
对象的类->父类->根类(NSObject)->nil
-
类对象:1、获取到类的元类。2、依次查找元类及元类的父类,如果和传入的类相等返回
YES
,遍历结束未找到返回NO
即查找顺序:
元类->元类父类->根元类->根类(NSObject)->nil
通过上面的结论,我们看上面的例子:
re1
:传入是NSObject
的类对象,首先找它的元类即根元类,根元类的父类是NSObject
=传入的第二个参数,所以re1
=1
re3
:传入是JSPerson
的类对象,首先找它的元类,依次找元类的父类到根元类,最后到根类,没有类=[JSPerson class]
,所以re3
=0
re5
:传入是NSObject
的实例对象,找它的类就是NSObject
=[NSObject class]
,所以re5
=1
re7
:传入是JSPerson
的实例对象,找它的类就是JSPerson
=[JSPerson class]
,所以re7
=1
我们继续看isMemberOfClass
的源码
//类方法
+ (BOOL)isMemberOfClass:(Class)cls {
return self->ISA() == cls;
}
//实例方法
- (BOOL)isMemberOfClass:(Class)cls {
return [self class] == cls;
}
复制代码
可以看出:
- 实例对象:判断实例对象的类和传入的类是否相等,即
类
是否相等 - 类对象:判断类的元类是否和传入的类相等,即看
元类
通过上面结论我们继续看例子:
re2
:传入是NSObject
的类对象,首先找它的元类即根元类,根元类不是NSObject
类,所以re2
=0
re4
:传入是JSPerson
的类对象,首先找它的元类,元类!=[JSPerson class]
,所以re4
=0
re6
:传入是NSObject
的实例对象,找它的类就是NSObject
=[NSObject class]
,所以re6
=1
re8
:传入是JSPerson
的实例对象,找它的类就是JSPerson
=[JSPerson class]
,所以re8
=1
本文我们主要研究了类的ro
和rw
的区别,属性的set
方法,以及isKindOfClass
vsisMemberOfClass
的源码,类的探究就到这里了,有遗漏的话后面会补充。