一、WWDC关于runtime的优化
WWDC2020-10163
1.1类数据结构的变化
1.1.1 class on disk
- 类对象本身包含最常访问的信息:元类,超类和方法缓存的指针。还有指向更多数据的结构体
class_ro_t
的指针。 -
class_ro_t
拥有类的名称,方法,协议,实例变量等编译期确定的信息。 -
Swift
类和OC
类共享这一基础结构。
ro
表示Read Only
,rw
表示Read Write
。
1.1.2 class in memory
当类被第一次加载进内存的时候他们的结构也是这样,一旦被使用就有了clean memory
与dirty memory
。
-
clean memory
:在程序运行时会不会发生改变的内存。class_ro_t
属于Clean Memory
(只读)。 -
dirty memory
:在程序运行时会发生更改的内存。类结构一经使用就会变成dirty memory
。例如,可以在Runtime
给类动态的添加方法。
dirty memory
比clean memory
昂贵的多,进程一经运行它就必须一直存在。clean memory
可以进行移除从而节省更多内存空间。如果需要clean memory
系统可以从磁盘中重新加载。
macOS
可以选择换出dirty memory
,iOS
不使用swap
。所以在iOS
中dirty memory
代价很大。dirty memory
是类数据被分成两部分的原因。
可以通过分离出永不更改的数据部分,将大多数类数据保留为Clean Memory
。
在类加载到 Runtime
中后,才会分配用于读取/写入数据的结构体 class_rw_t
,此时类的结构会变为:
-
class_ro_t
:只读,存放的是编译期间就确定的字段信息。 -
class_rw_t
:读写,在Runtime
时期才创建,会先将class_ro_t
的内容拷贝一份,再将类的分类的属性、方法、协议等信息添加进去。
为什么这么设计?
因为Objective-C
是动态语言,可以在运行时更改方法,属性等。并且分类可以在不改变类设计的前提下,将新方法添加到类中。
class_rw_t
会占用比 class_ro_t
更多的内存,在视频中苹果给的测试数据是大概10%
的类实际会存在动态的更改行为(动态添加方法、使用 Category
方法等)。
1.1.3 变化
所以苹果将动态的部分提取出来,称为class_rw_ext_t
,这个时候结构如下:
这样就将
class_rw_t
拆分出了部分。对于真正需要扩展数据的类会保留上面的结构,对于不需要的如下:
相当于没有
class_rw_ext_t
这部分数据。这样经过拆分,可以把 90%
的类优化为 Clean Memory
,在系统层面节省了内存。
验证:
➜ ~ heap WeChat | egrep 'class_rw|COUNT'
COUNT BYTES AVG CLASS_NAME TYPE BINARY
2970 95040 32.0 Class.data (class_rw_t) C libobjc.A.dylib
287 13776 48.0 Class.data.extended (class_rw_ext_t) C libobjc.A.dylib
2970
个class_rw_t
但是实际上只有287
个需要class_rw_ext_t
。
小结:经过拆分class_rw_t
为两部分:class_rw_t + class_rw_ext_t
。对于需要扩展数据的类会保留class_rw_ext_t
,对于不需要的不会生成class_rw_ext_t
部分数据。这些数据保存在class_ro_t
中。这样就将类中数据优化为了Clean Memory
。
1.2相对方法地址优化
1.2.1 method lists
在认知中每个类都有一个方法列表,以便Runtime
查找和消息发送,结构如下:
每个方法包含三部分:
-
methodName/Selector
:方法名称或选择器。 -
方法类型编码(method type encoding)
:方法类型编码标识。 -
IMP
:方法实现的函数指针。
在 64
位系统中,一个方法占用了 24
字节的空间:
这个时候寻址如下:
库的地址取决于动态链接库加载之后的位置(
ASLR
),而动态链接器需要修正真实的指针地址,也是一种代价。
1.2.2 relative method lists
由于方法地址仍旧在当前二进制地址空间区域内,所以方法列表并不需要使用 64
位的寻址范围空间,它们仅需要根据自身地址以及偏移量就可以找到其他方法位置。所以可以使用32
位相对偏移来代替绝对 64
位地址。优化后方法与内存地址的寻址:
这么做的好处:
- 加载后不需要进行修正指针地址:无论将库加载到内存中的任何位置,偏移量始终是相同的。
- 可以保存在只读存储器中,更加安全。
- 使用
32
位偏移量在64
位平台上所需的内存量减少了一半。
1.2.3 Swizzling relative method lists
⚠️:相对方法地址会存在另外一个问题, Method Swizzling
如何处理呢?
Method Swizzling
替换的是 2
个函数指针的指向。函数可以在任意地方实现,但使用了上述的相对寻址优化之后,Method Swizzling
就无法工作了。
官方给出的解决方案是全局映射表,在映射表中维护 Swizzles
方法对应的实现函数指针地址。由于 Method Swizzling
的操作并不常见,所以这个表不会变得很大,新的 Method Swizzling
如下:
macOS Big Sur
、iOS14
、tvOS14
、watchOS7
这个时候如果直接访问底层数据会将两个32
位地址作为一个64
位地址访问,还是需要通过API
去访问。
小结:
- 相对方法地址加载后不需要进行指针修正,更加安全,占用内存量减半。
- 对于
Method Swizzling
提供了全局映射表解决地址交换问题。
1.3 Tagged Pointer Format Changes
1.3.1Tagged Pointer是什么?
Tagged Pointer
是一种特殊标记的对象,通过在其最后一个 bit
位设置为特殊标记位,并将数据直接保存在指针自身中。
在 64
位系统中,有 64
位空间可以表示一个对象指针。由于内存对齐,通常没有真正使用到所有这些位。对象必须位于指针大小倍数的地址中,低位和高位均被 0
填充,因此只用到了中间部分的位,出现了大量的内存浪费。
基于上面的问题,按照
Tagged Pointer
的思路,可以将低位设置为 1
加以区分。
并且可以在最低位之后的 3
位,赋予类型意义(在剩余的字段中,记录所包含的数据)。3
位可以表示 7 种数据类型:
OBJC_TAG_NSAtom = 0,
OBJC_TAG_1 = 1,
OBJC_TAG_NSString = 2,
OBJC_TAG_NSNumber = 3,
OBJC_TAG_NSIndexPath = 4,
OBJC_TAG_NSManagedObjectID = 5,
OBJC_TAG_NSDate = 6,
OBJC_TAG_7 = 7
在Intel x86
架构中,Tagged Pointer
对象的表示如下:
OBJC_TAG_7
类型的Tagged Pointer
是个例外,它可以将后 8 位作为扩展字段。基于此可以多支持 256
种类型的 Tagged Pointer
,如 UIColors
或 NSIndexSets
之类的对象。
// Tagged pointer layout and usage is subject to change on different OS versions.
// Tag indexes 0..<7 have a 60-bit payload.
// Tag index 7 is reserved.
// Tag indexes 8..<264 have a 52-bit payload.
// Tag index 264 is reserved.
#if __has_feature(objc_fixed_enum) || __cplusplus >= 201103L
enum objc_tag_index_t : uint16_t
#else
typedef uint16_t objc_tag_index_t;
enum
#endif
{
// 60-bit payloads
OBJC_TAG_NSAtom = 0,
OBJC_TAG_1 = 1,
OBJC_TAG_NSString = 2,
OBJC_TAG_NSNumber = 3,
OBJC_TAG_NSIndexPath = 4,
OBJC_TAG_NSManagedObjectID = 5,
OBJC_TAG_NSDate = 6,
// 60-bit reserved
OBJC_TAG_RESERVED_7 = 7,
// 52-bit payloads
OBJC_TAG_Photos_1 = 8,
OBJC_TAG_Photos_2 = 9,
OBJC_TAG_Photos_3 = 10,
OBJC_TAG_Photos_4 = 11,
OBJC_TAG_XPC_1 = 12,
OBJC_TAG_XPC_2 = 13,
OBJC_TAG_XPC_3 = 14,
OBJC_TAG_XPC_4 = 15,
OBJC_TAG_NSColor = 16,
OBJC_TAG_UIColor = 17,
OBJC_TAG_CGColor = 18,
OBJC_TAG_NSIndexSet = 19,
OBJC_TAG_NSMethodSignature = 20,
OBJC_TAG_UTTypeRecord = 21,
// When using the split tagged pointer representation
// (OBJC_SPLIT_TAGGED_POINTERS), this is the first tag where
// the tag and payload are unobfuscated. All tags from here to
// OBJC_TAG_Last52BitPayload are unobfuscated. The shared cache
// builder is able to construct these as long as the low bit is
// not set (i.e. even-numbered tags).
OBJC_TAG_FirstUnobfuscatedSplitTag = 136, // 128 + 8, first ext tag with high bit set
OBJC_TAG_Constant_CFString = 136,
OBJC_TAG_First60BitPayload = 0,
OBJC_TAG_Last60BitPayload = 6,
OBJC_TAG_First52BitPayload = 8,
OBJC_TAG_Last52BitPayload = 263,
OBJC_TAG_RESERVED_264 = 264
};
#if __has_feature(objc_fixed_enum) && !defined(__cplusplus)
typedef enum objc_tag_index_t objc_tag_index_t;
#endif
在arm64
中结构如下:
当为
OBJC_TAG_7
类型时:
arm64
中:最高位代表 Tagged Pointer
标识位,次 3
位标识 Tagged Pointer
的类型,接下去的位来表示包含的数据(可能还有扩展类型字段)(iOS13
中是这样的)
在 ARM64
中,为什么要用最高位代表的 Tagged Pointer
标记,而不与 Intel x86
一样使用低位标记?
实际是对 objc_msgSend
的微小优化,在对objc_msgSend
查找指针时候的一个case
。在最高位能一次排查Tagged Pointer 指针
和 nil
两种类型,节省了一个case
的逻辑。
具体内容可以查看:WWDC2013 - Advances in Objective-C
1.3.2Tagged Pointer 优化点
在iOS14
中最高位仍然是标志位(msg_send
),将tag
位挪到了最后面:
why?
对于普通指针动态链接会忽略指针的前8
位。这是由于Top Byte Ignore
的ARM
特性。对于一个对齐指针,底部3
位总是0
,那么为添加7
(扩展标签)以将低位设置为1
。
这意味着实际上可将下面的指针放入一个扩展标签指针的Paylaod
中:
这样就开启了
tagged pointer
的引用二进制文件中的常量数据的能力。例如字符串或其他数据结构,这样就不占用dirty memory
内存了。
⚠️:使用苹果提供的API
访问底层数据结构。
小结:
- 将
tag
标记位从高位挪到了低位。 flag
与Extended
不变,仍然在高位。flag
在高位是为了优化objc_msgSend
查找效率(一次同时排查Target pointer与nil
)-
tag
放入低位是因为对于一个对齐指针低3
位总是0
,这样做了后可以将真正的指针放入Payload
中,这样就不占用dirty memory
内存空间了。
二、Objective-C type encodings
2.1 Objective-C type encodings
Objective-C 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) |
⚠️Objective-C does not support the long double type. @encode(long double) returns d, which is the same encoding as for double.
2.2 Objective-C method encodings
runtime
系统为类型限定符提供了另外的编码:
Code | Meaning |
---|---|
r | const |
n | in |
N | inout |
o | out |
O | bycopy |
R | byref |
V | oneway |
可以通过API
获取编码:
ivar_getTypeEncoding(Ivar _Nonnull)
method_getTypeEncoding(<#Method _Nonnull m#>)
2.3代码验证
苹果还提供了一个指令@encode
,可以将具体的类型表示成字符串编码:
void HPEncodeTypes(void) {
NSLog(@"char --> %s",@encode(char));
NSLog(@"int --> %s",@encode(int));
NSLog(@"short --> %s",@encode(short));
NSLog(@"long --> %s",@encode(long));
NSLog(@"long long --> %s",@encode(long long));
NSLog(@"unsigned char --> %s",@encode(unsigned char));
NSLog(@"unsigned int --> %s",@encode(unsigned int));
NSLog(@"unsigned short --> %s",@encode(unsigned short));
NSLog(@"unsigned long --> %s",@encode(unsigned long long));
NSLog(@"float --> %s",@encode(float));
NSLog(@"bool --> %s",@encode(bool));
NSLog(@"void --> %s",@encode(void));
NSLog(@"char * --> %s",@encode(char *));
NSLog(@"id --> %s",@encode(id));
NSLog(@"Class --> %s",@encode(Class));
NSLog(@"SEL --> %s",@encode(SEL));
int array[] = {1,2,3};
NSLog(@"int[] --> %s",@encode(typeof(array)));
typedef struct HPStruct{
char *name;
int age;
}hpStruct;
NSLog(@"struct --> %s",@encode(hpStruct));
typedef union HPUnion{
char *name;
int a;
}hpUnion;
NSLog(@"union --> %s",@encode(hpUnion));
int a = 2;
int *b = {&a};
NSLog(@"int[] --> %s",@encode(typeof(b)));
}
输出:
char --> c
int --> i
short --> s
long --> q
long long --> q
unsigned char --> C
unsigned int --> I
unsigned short --> S
unsigned long --> Q
float --> f
bool --> B
void --> v
char * --> *
id --> @
Class --> #
SEL --> :
int[] --> [3i]
struct --> {HPStruct=*i}
union --> (HPUnion=*i)
int[] --> ^i
在方法编码中不仅有编码还有数字,那么他们代表什么意思呢?
-
@16@0:8
:@
代表id
类型返回值,16
代表所占用的总内存,@
默认参数id self
,0
代表self
从0
号位置开始,:
代表SEL
8
代表SEL
从8
号位置开始。 -
v24@0:8@16
:v
代表返回值void
,24
代表所占总内存,@
默认参数id self
,0
代表self
从0
号位置开始,:
代表SEL
8
代表SEL
从8
号位置开始。@
代表第三个参数id
类型,16
代表从16
号位置开始。
三、成员变量和属性
上篇文章分析了属性存储在class_rw_t
的properties()
中,成员变量存储在ro()
中。
- 成员变量:包含在类的
{}
中的变量。 - 实例变量:实例变量是特殊的成员变量,对象类型就是实例变量。普通的成员变量就指基本数据类型了。
NSString
修饰的是成员变量不是实例变量因为本质上它是字符串,底层不是结构体。 - 属性:用
@property
修饰的。
3.1 成员变量、属性、实例变量
那么他们之间有什么关系呢?这就需要我们用clang
查看下底层结构了。
原代码如下:
#import
@interface HPObject : NSObject {
NSString *sex;
NSObject *obj;
}
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) NSString *name1;
@property (atomic, strong) NSString *name2;
@property (atomic, copy) NSString *name3;
@end
@implementation HPObject
@end
转换后如下:
#ifndef _REWRITER_typedef_HPObject
#define _REWRITER_typedef_HPObject
typedef struct objc_object HPObject;
typedef struct {} _objc_exc_HPObject;
#endif
extern "C" unsigned long OBJC_IVAR_$_HPObject$_name;
extern "C" unsigned long OBJC_IVAR_$_HPObject$_name1;
extern "C" unsigned long OBJC_IVAR_$_HPObject$_name2;
struct HPObject_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *sex;
NSObject *obj;
NSString *_name;
NSString *_name1;
NSString *_name2;
NSString *_name3;
};
// @property (nonatomic, copy) NSString *name;
// @property (nonatomic, strong) NSString *name1;
// @property (atomic, strong) NSString *name2;
// @property (atomic, copy) NSString *name3;
- 底层已经没有属性了,生成了对应带下划线的成员变量。
-
HPObject
继承了NSObject
的isa
。
继续往下看可以看到:
static NSString * _I_HPObject_name(HPObject * self, SEL _cmd) {
return (*(NSString **)((char *)self + OBJC_IVAR_$_HPObject$_name));
}
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);
static void _I_HPObject_setName_(HPObject * self, SEL _cmd, NSString *name) {
objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct HPObject, _name), (id)name, 0, 1);
}
static NSString * _I_HPObject_name1(HPObject * self, SEL _cmd) {
return (*(NSString **)((char *)self + OBJC_IVAR_$_HPObject$_name1));
}
static void _I_HPObject_setName1_(HPObject * self, SEL _cmd, NSString *name1) {
(*(NSString **)((char *)self + OBJC_IVAR_$_HPObject$_name1)) = name1;
}
static NSString * _I_HPObject_name2(HPObject * self, SEL _cmd) {
return (*(NSString **)((char *)self + OBJC_IVAR_$_HPObject$_name2));
}
static void _I_HPObject_setName2_(HPObject * self, SEL _cmd, NSString *name2) {
(*(NSString **)((char *)self + OBJC_IVAR_$_HPObject$_name2)) = name2;
}
extern "C" __declspec(dllimport) id objc_getProperty(id, SEL, long, bool);
static NSString * _I_HPObject_name3(HPObject * self, SEL _cmd) {
typedef NSString * _TYPE;
return (_TYPE)objc_getProperty(self, _cmd, __OFFSETOFIVAR__(struct HPObject, _name3), 1);
}
static void _I_HPObject_setName3_(HPObject * self, SEL _cmd, NSString *name3) {
objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct HPObject, _name3), (id)name3, 1, 1);
}
- 属性还生成了对应的
getter
和setter
方法。 -
_I_HPObject_setName_
和_I_HPObject_setName3_
是通过objc_setProperty
去设置值的,而_I_HPObject_setName1_
与_I_HPObject_setName2_
是通过内存平移进行赋值的。
所以这里为什么会有objc_setProperty
与内存平移赋值两种形式?在后面会具体分析。
⚠️:属性 = 带下划线成员变量 + setter + getter 方法
3.2 代码验证属性与成员变量
可以通过runtime
的API
获取属性和成员变量进行验证:
void HPObjc_copyIvar_copyProperies(Class pClass){
unsigned int count = 0;
Ivar *ivars = class_copyIvarList(pClass, &count);
for (unsigned int i=0; i < count; i++) {
Ivar const ivar = ivars[i];
//获取实例变量名
const char*cName = ivar_getName(ivar);
NSString *ivarName = [NSString stringWithUTF8String:cName];
printf("\n%s",[ivarName UTF8String]);
}
free(ivars);
unsigned int pCount = 0;
objc_property_t *properties = class_copyPropertyList(pClass, &pCount);
for (unsigned int i=0; i < pCount; i++) {
objc_property_t const property = properties[i];
//获取属性名
NSString *propertyName = [NSString stringWithUTF8String:property_getName(property)];
printf("\n%s",[propertyName UTF8String]);
}
free(properties);
}
结果:
sex
obj
_name
_name1
_name2
_name3
name
name1
name2
name3
与查看底层结构结果一致。
四、setter&getter的底层原理
上面分析道setter
方法有objc_setProperty
与内存平移赋值两种形式?
setter
方法的本质是对内存区域赋值,所有的上层调用调用到底层都是同样的原理。显示底层对每一个setter
都实现一次不现实,所以就有了个中间层objc_setProperty
。在ivar
入口处会对sel->imp
重定向到objc_setProperty
。在编译时期就已经处理完毕。
4.1 LLVM源码分析
4.1.1 objc_setProperty
在llvm
源码中直接搜索objc_setProperty
,在CGObjCMac.cpp
中会搜索到类似下面的代码:
const char *name;
if (atomic && copy)
name = "objc_setProperty_atomic_copy";
else if (atomic && !copy)
name = "objc_setProperty_atomic";
else if (!atomic && copy)
name = "objc_setProperty_nonatomic_copy";
else
name = "objc_setProperty_nonatomic";
在llvm::FunctionCallee getSetPropertyFn()
方法中最终返回了:
return CGM.CreateRuntimeFunction(FTy, "objc_setProperty");
继续搜索getSetPropertyFn
的调用方:
llvm::FunctionCallee GetPropertySetFunction() override {
return ObjCTypes.getSetPropertyFn();
}
GetPropertySetFunction
是一个中间层,继续查找发现在CGObjC.cpp
的CodeGenFunction::generateObjCSetterBody
中发现调用了:
是否调用是根据
PropertyImplStrategy
类型来决定的。类型定义如下:
enum StrategyKind {
/// The 'native' strategy is to use the architecture's provided
/// reads and writes.
Native,
/// Use objc_setProperty and objc_getProperty.
GetSetProperty,
/// Use objc_setProperty for the setter, but use expression
/// evaluation for the getter.
SetPropertyAndExpressionGet,
/// Use objc_copyStruct.
CopyStruct,
/// The 'expression' strategy is to emit normal assignment or
/// lvalue-to-rvalue expressions.
Expression
};
那么PropertyImplStrategy
是什么时候赋值的呢?在PropertyImplStrategy
中有如下代码:
PropertyImplStrategy(CodeGenModule &CGM,
const ObjCPropertyImplDecl *propImpl);
查看它的实现:
-
copy
。 -
retain + nonatomic
。 -
retain + atomic
。
结论:当属性使用copy
/retain
修饰的时候底层会调用objc_setProperty
,默认是走的native
分支(内存平移)。
验证:
//objc_setProperty
@property (nonatomic,copy) NSString *name3;
@property (nonatomic,retain) NSString *name4;
@property (atomic,copy) NSString *name5;
@property (atomic,retain) NSString *name6;
//不能有objc_setProperty
@property (atomic,strong) NSString *name7;
@property (nonatomic,strong) NSString *name8;
符合预期。
4.1.2 objc_getProperty
既然有set
那么对应的就有get
:
llvm::FunctionCallee getGetPropertyFn() {
CodeGen::CodeGenTypes &Types = CGM.getTypes();
ASTContext &Ctx = CGM.getContext();
// id objc_getProperty (id, SEL, ptrdiff_t, bool)
CanQualType IdType = Ctx.getCanonicalParamType(Ctx.getObjCIdType());
CanQualType SelType = Ctx.getCanonicalParamType(Ctx.getObjCSelType());
CanQualType Params[] = {
IdType, SelType,
Ctx.getPointerDiffType()->getCanonicalTypeUnqualified(), Ctx.BoolTy};
llvm::FunctionType *FTy =
Types.GetFunctionType(
Types.arrangeBuiltinFunctionDeclaration(IdType, Params));
return CGM.CreateRuntimeFunction(FTy, "objc_getProperty");
}
最终定位到void CodeGenFunction::generateObjCGetterBody
方法中:
case PropertyImplStrategy::GetSetProperty: {
llvm::FunctionCallee getPropertyFn =
CGM.getObjCRuntime().GetPropertyGetFunction()
也是GetSetProperty
分支。
-
copy
。 -
retain + atomic
。
结论:copy
/retain + atomic
修饰的情况下会调用objc_getProperty
,否则内存平移。(真的是这样么?)
那么直接验证下:
@interface HPObject : NSObject
@property (nonatomic, copy) NSString *name;
@property (atomic, copy) NSString *name1;
@property (nonatomic,retain) NSString *name2;
@property (atomic,retain) NSString *name3;
@property (nonatomic, strong) NSString *name4;
@property (atomic, strong) NSString *name5;
@end
@implementation HPObject
@end
验证发现只有atomic + copy
/atomic + retain
修饰的属性才走objc_getProperty
逻辑,否则是内存平移。为什么与分析的源码结果不一致?
在源码中有如下错误信息说明需要
atomic + copy
,条件是getPropertyFn
为空。再继续看类型赋值的逻辑有如下代码:
这样就说明对于这块是有特殊逻辑的。
之后又在源码中发现了以下代码:
说明
atomic + copy/retain
就是objc_getProperty
。这就与验证结果一致了。
同样的set
:
同样的
set
需呀copy/retain
修饰才调用objc_setProperty
,否则内存平移,这里也就验证了。
@interface HPSubObject : HPObject
//objc_getProperty
@property (atomic,copy) NSString *name1;
@property (atomic,retain) NSString *name2;
//objc_setProperty
@property (nonatomic,copy) NSString *name3;
@property (nonatomic,retain) NSString *name4;
@property (atomic,copy) NSString *name5;
@property (atomic,retain) NSString *name6;
//不能有objc_setProperty
@property (atomic,strong) NSString *name7;
@property (nonatomic,strong) NSString *name8;
//不能有objc_getProperty
@property (nonatomic,copy) NSString *name9;
@property (nonatomic,retain) NSString *name10;
@end
clang
验证:
验证通过。
小结:
- 对于
set
方法来说,copy/retain
修饰的属性会重定向到objc_setProperty
的实现,否则是内存平移。 - 对于
get
方法来说,atomic + copy
/atomic +retain
修饰的属性会重定向到objc_getProperty
的逻辑,否则是内存平移。
4.1.3 真机验证
上面是通过clang
编译来验证的,那么在真机情况下是否也这样呢?
@interface HPObject : NSObject
//objc_getProperty + objc_getProperty
@property (atomic,copy) NSString *name1;
@property (atomic,retain) NSString *name2;
//有objc_setProperty,不能有objc_getProperty
@property (nonatomic,copy) NSString *name3;
@property (nonatomic,retain) NSString *name4;
//不能有objc_setProperty以及objc_getProperty
@property (atomic,strong) NSString *name5;
@property (nonatomic,strong) NSString *name6;
@end
使用手机运行,然后打符号断点。要打以下符号断点:
objc_setProperty_atomic_copy
objc_setProperty_atomic
objc_setProperty_nonatomic_copy
objc_setProperty_nonatomic
objc_setProperty
objc_getProperty
验证结果:
验证结果与
clang
验证差异点:
-
nonatomic + copy
会走get
。 -
nonatomic + retain
会走set
。 -
atomic + strong
会走set + get
。
代码验证结论:atomic
/copy
修饰的属性会走objc_setProperty + objc_getProperty
,否则是内存平移。
4.1.4 为什么clang与真机验证结果不一致?
首先需要明白一点的是clang
验证的结论是通过llvm
源码分析得出来的,而clang
就是llvm
编译后的产物,所以这两者肯定能对的上。
StrategyKind
在llvm
中涉及到类型赋值如上图所示4
部分,得到的确定结论是copy
/atomic
会生成get + set
。
那么
retain/strong nonatomic
的逻辑set
是怎么被过滤掉的。
源码中有以下代码:
意味着要调用objc_getProperty
就要先进行声明,但是clang
的调用在声明之前:
猜测是执行clang
代码生成器的时候导致的。
总结:
-
clang
copy/retain
会调用objc_setProperty
,否则内存平移。copy/retain + atomic
会调用objc_getProperty
,否则内存平移。
- 真机:
atomic
/copy
会调用objc_setProperty + objc_getProperty
,否则内存平移。(以真机为准)。
4.2 objc源码分析
通过llvm
分析已经知道了内存平移和set/getProperty
的区别,那么为什么要这么做呢?
4.2.1 objc_setProperty
objc_setProperty
:
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);
}
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);
}
底层调用的是copyWithZone
,所以copy
方法是需要拷贝内存的。所以这块
才做了区分。如果直接内存平移的话就是对原始值的修改,而copy
是需要开辟新内存的。
4.2.2 objc_getProperty
id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
if (offset == 0) {
return object_getClass(self);
}
// Retain release world
id *slot = (id*) ((char*)self + offset);
if (!atomic) return *slot;
// Atomic retain release world
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
id value = objc_retain(*slot);
slotlock.unlock();
// for performance, we (safely) issue the autorelease OUTSIDE of the spinlock.
return objc_autoreleaseReturnValue(value);
}
可以看到内部有加锁操作。所以要求atomic
修饰。
五、API方式解析类方法存储
5.1 class_copyMethodList 获取类的方法列表
void HP_MethodList(Class pClass){
unsigned int count = 0;
Method *methods = class_copyMethodList(pClass, &count);
for (unsigned int i=0; i < count; i++) {
Method const method = methods[i];
//获取方法名
NSString *key = NSStringFromSelector(method_getName(method));
printf("\nMethod Name:%s",[key UTF8String]);
}
free(methods);
}
调用:
printf("\n类的方法:");
HP_MethodList(HPObject.class);
printf("\n元类的方法:");
HP_MethodList(object_getClass(HPObject.class));
输出:
类的方法:
Method Name:additions1InstanceMethod
Method Name:instanceMethod
Method Name:additions2InstanceMethod
Method Name:name
Method Name:.cxx_destruct
Method Name:setName:
Method Name:age
Method Name:setAge:
元类的方法:
Method Name:additions1ClassMethod
Method Name:additions2ClassMethod
Method Name:classMethod
5.2 方法验证
5.2.1 实例方法验证
void HP_InstanceMethodVerify(Class pClass) {
const char *className = class_getName(pClass);
Class metaClass = objc_getMetaClass(className);
Method method1 = class_getInstanceMethod(pClass, @selector(instanceMethod));
Method method2 = class_getInstanceMethod(metaClass, @selector(instanceMethod));
Method method3 = class_getInstanceMethod(pClass, @selector(classMethod));
Method method4 = class_getInstanceMethod(metaClass, @selector(classMethod));
printf("%p-%p-%p-%p",method1,method2,method3,method4);
}
调用:
HP_InstanceMethodVerify(HPObject.class);
输出:
0x1000080e0-0x0-0x0-0x1000080c0
说明类有instanceMethod
方法,元类有classMethod
方法。
5.2.2 类方法验证
void HP_ClassMethodVerify(Class pClass){
const char *className = class_getName(pClass);
Class metaClass = objc_getMetaClass(className);
Method method1 = class_getClassMethod(pClass, @selector(instanceMethod));
Method method2 = class_getClassMethod(metaClass, @selector(instanceMethod));
Method method3 = class_getClassMethod(pClass, @selector(classMethod));
Method method4 = class_getClassMethod(metaClass, @selector(classMethod));
printf("%p-%p-%p-%p",method1,method2,method3,method4);
}
调用:
HP_ClassMethodVerify(HPObject.class);
输出:
0x0-0x0-0x1000080c8-0x1000080c8
这个结果的第4
个为什么也有呢?按照前面的验证方式元类类方法在元类中应该是实例方法,那么它就不应该存在类方法classMethod
。
元类为什么有类方法呢?
既然结果不符合预期那就查看下源码:
Method class_getClassMethod(Class cls, SEL sel)
{
if (!cls || !sel) return nil;
return class_getInstanceMethod(cls->getMeta(), sel);
}
Class getMeta() {
if (isMetaClassMaybeUnrealized()) return (Class)this;
else return this->ISA();
}
可以看到class_getClassMethod
的底层调用的是class_getInstanceMethod
。如果cls
本身是元类则使用cls
本身调用,否则就找cls
的元类。
⚠️:在底层没有类方法,都是实例方法。所谓的加减号只是为了上层区分而已。
实例对象之间没有关系,类才有继承关系。对象没有。
5.3 IMP验证
void HP_IMPVerify(Class pClass) {
const char *className = class_getName(pClass);
Class metaClass = objc_getMetaClass(className);
IMP imp1 = class_getMethodImplementation(pClass, @selector(instanceMethod));
IMP imp2 = class_getMethodImplementation(metaClass, @selector(instanceMethod));// 0
// sel -> imp 方法的查找流程
IMP imp3 = class_getMethodImplementation(pClass, @selector(classMethod)); // 0
IMP imp4 = class_getMethodImplementation(metaClass, @selector(classMethod));
printf("%p-%p-%p-%p",imp1,imp2,imp3,imp4);
}
调用:
HP_IMPVerify(HPObject.class);
输出:
0x100003be0-0x1002eb640-0x1002eb640-0x100003bd0
按理解imp2
与imp3
是不应该存在的,为什么这里存在并且他们还相同。这里其实就涉及到消息转发了,返回的是_objc_msgForward
的imp
:
(lldb) p _objc_msgForward
(void (*)()) $23 = 0x00000001002eb640 (libobjc.A.dylib`_objc_msgForward)
class_getMethodImplementation
源码实现如下:
__attribute__((flatten))
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
没有实现的时候就直接返回了_objc_msgForward
。会进行消息转发。
六、 isKindOf
有一道经典面试题如下:
void KindOfTest(void) {
BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
BOOL re3 = [(id)[HPObject class] isKindOfClass:[HPObject class]];
BOOL re4 = [(id)[HPObject class] isMemberOfClass:[HPObject class]];
NSLog(@" re1 :%d\n re2 :%d\n re3 :%d\n re4 :%d\n",re1,re2,re3,re4);
BOOL re5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]];
BOOL re6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]];
BOOL re7 = [(id)[HPObject alloc] isKindOfClass:[HPObject class]];
BOOL re8 = [(id)[HPObject alloc] isMemberOfClass:[HPObject class]];
NSLog(@" re5 :%d\n re6 :%d\n re7 :%d\n re8 :%d\n",re5,re6,re7,re8);
}
要解答这道题,首先要清楚isKindOfClass
与isMemberOfClass
的实现。
源码如下:
isKindOfClass
- (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = [self class]; tcls; tcls = tcls->getSuperclass()) {
if (tcls == cls) return YES;
}
return NO;
}
+ (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = self->ISA(); tcls; tcls = tcls->getSuperclass()) {
if (tcls == cls) return YES;
}
return NO;
}
isMemberOfClass
- (BOOL)isMemberOfClass:(Class)cls {
return [self class] == cls;
}
+ (BOOL)isMemberOfClass:(Class)cls {
return self->ISA() == cls;
}
class
+ (Class)class {
return self;
}
- (Class)class {
return object_getClass(self);
}
- isKindOfClass
:获取类对象,循环获取父类判断是否与传进来的参数是否相同。
ret5
NSObject
实例对象的class
返回NSObject
类对象,类对象cls
相同,返回YES
ret7
HPObject
实例对象的class
返回HPObject
类对象,类对象cls
相同,返回YES
+ isKindOfClass
:获取类对象的isa
,循环判断父类是否与传进来的参数相同。
ret1
[NSObject class]
返回的是NSObject
类对象本身。cls
参数也是类对象本身。tcls
是元类。元类一直找到根元类,最终找到NSObject
类(跟元类继承自NSObject
)。所以返回YES
。
ret3
cls
参数是HPObject
类。HPObject
元类一直找到根元类的父类NSObject
也与cls
不等。返回NO
。- isMemberOfClass
:判断类对象是否和参数相同。
ret6
实例对象class
返回NSObject
类对象,与cls
相同,返回YES
。
ret8
实例对象class
返回HPObject
类对象,与cls
相同,返回YES
。+ isMemberOfClass
:判断类对象的isa
是否和参数相同。
ret2
NSObject
元类 不等于NSObject
类,返回NO
。
ret4
HPObject
元类 不等于HPObject
类,返回NO
。
所以输出应该为:YES、NO、NO、NO、YES、YES、YES、YES
。
验证:
re1 :1
re2 :0
re3 :0
re4 :0
re5 :1
re6 :1
re7 :1
re8 :1
结果与分析的一致,那么逻辑真的是这样的么?源码调试一下。
发现isKindOfClass
调用的是objc_opt_isKindOfClass
:
// Calls [obj isKindOfClass]
BOOL
objc_opt_isKindOfClass(id obj, Class otherClass)
{
#if __OBJC2__
if (slowpath(!obj)) return NO;
//元类
Class cls = obj->getIsa();
//// class or superclass has default new/self/class/respondsToSelector/isKindOfClass
if (fastpath(!cls->hasCustomCore())) {//cache中有缓存
//这里与+ isKindOfClass 相同
for (Class tcls = cls; tcls; tcls = tcls->getSuperclass()) {
if (tcls == otherClass) return YES;
}
return NO;
}
#endif
//objc1走objc_msgSend
return ((BOOL(*)(id, SEL, Class))objc_msgSend)(obj, @selector(isKindOfClass:), otherClass);
}
-
+ - isKindOfClass
底层调用的都是objc_opt_isKindOfClass
。 - 获取调用方的
isa
(循环获取父类一直到nil
)与参数比较是否相等。对象返回类与参数比较,类返回元类与参数比较。 - 这里会判断缓存,做了优化。
class
实际调用的是objc_opt_class
:
// Calls [obj class]
Class
objc_opt_class(id obj)
{
#if __OBJC2__
if (slowpath(!obj)) return nil;
//获取isa
Class cls = obj->getIsa();
if (fastpath(!cls->hasCustomCore())) {//cache中有缓存
//是元类返回obj,否则返回cls
return cls->isMetaClass() ? obj : cls;
}
#endif
return ((Class(*)(id, SEL))objc_msgSend)(obj, @selector(class));
}
-
+ - class
底层调用的都是objc_opt_class
。 - 获取调用方的
isa
判断是否是元类,是元类返回调用方,否则返回cls
。也就是说对象调用返回的是类,类调用返回的也是类。这也就是class
方法的本质返回类。 - 这里会判断缓存,做了优化。
而isMemberOfClass
的调用仍然是+ isKindOfClass
与- isKindOfClass
。
⚠️ class
与isKindOfClass
的底层实现再次说明:在底层没有类方法,都是实例方法。
小节:
+ - isKindOfClass
底层实现是objc_opt_isKindOfClass
,对象调用时是类与参数比较,类调用时元类与参数比较。(在这个过程中会找父类直到->NSObject->nil
)+ - class
底层调用的都是objc_opt_class
,无论谁调用都是返回类+ - isMemberOfClass
调用的仍然是+ - isKindOfClass
,对象调用是类与参数比较,类调用是元类与参数的比较。由于isMemberOfClass
只是进行值比较不需要进行优化,所以底层没有重绑定- 在底层没有类方法,都是实例方法。