一、WWDC关于runtime的优化
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
| 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号位置开始,:代表SEL8代表SEL从8号位置开始。 -
v24@0:8@16:v代表返回值void,24代表所占总内存,@默认参数id self,0代表self从0号位置开始,:代表SEL8代表SEL从8号位置开始。@代表第三个参数id类型,16代表从16号位置开始。
三、成员变量和属性
上篇文章分析了属性存储在class_rw_t的properties()中,成员变量存储在ro()中。
- 成员变量:包含在类的
{}中的变量。 - 实例变量:实例变量是特殊的成员变量,对象类型就是实例变量。普通的成员变量就指基本数据类型了。
NSString修饰的是成员变量不是实例变量因为本质上它是字符串,底层不是结构体。 - 属性:用
@property修饰的。
3.1 成员变量、属性、实例变量
那么他们之间有什么关系呢?这就需要我们用clang查看下底层结构了。
原代码如下:
#import <Foundation/Foundation.h>
@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代码生成器的时候导致的。
总结:
-
clangcopy/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:获取类对象,循环获取父类判断是否与传进来的参数是否相同。
ret5NSObject实例对象的class返回NSObject类对象,类对象cls相同,返回YES
ret7HPObject实例对象的class返回HPObject类对象,类对象cls相同,返回YES+ isKindOfClass:获取类对象的isa,循环判断父类是否与传进来的参数相同。
ret1[NSObject class]返回的是NSObject类对象本身。cls参数也是类对象本身。tcls是元类。元类一直找到根元类,最终找到NSObject类(跟元类继承自NSObject)。所以返回YES。
ret3cls参数是HPObject类。HPObject元类一直找到根元类的父类NSObject也与cls不等。返回NO。- isMemberOfClass:判断类对象是否和参数相同。
ret6实例对象class返回NSObject类对象,与cls相同,返回YES。
ret8实例对象class返回HPObject类对象,与cls相同,返回YES。+ isMemberOfClass:判断类对象的isa是否和参数相同。
ret2NSObject元类 不等于NSObject类,返回NO。
ret4HPObject元类 不等于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只是进行值比较不需要进行优化,所以底层没有重绑定- 在底层没有类方法,都是实例方法。