1. WWDC 类结构的优化
WWDC 2020-Runtime优化:
- 类结构的优化
- 方法列表的优化
Tagged Pointer格式的优化
下面我们只针对第一项“类结构的优化”进行详细说明
1.1 Clean Memory & Dirty Memory
1.1.1 Clean Memory
- 加载后不会再改变的内存
class_ro_t是只读的,属于Clean Memory- 可移除,从而节省更多内存空间。如果你有需要,系统可以从磁盘中重新加载
1.1.2 Dirty Memory
- 进程运行时会发生变化的内存
- 类结构体一旦被使用就是
Dirty Memory,因为运行时会写入新的数据,例如:方法缓存 Dirty Memory是类数据被分为两部分的原因
Dirty Memory比Clean Memory更昂贵,因为在进程运行的整个过程中,都需要被保留。通过分离出那些永远不会改变的数据,将大部分的类数据存储为Clean Memory
1.2 类结构的优化
类第一次从磁盘被加载到内存时的结构
- 类对象包含了最常用的信息:元类、父类、以及方法的缓存。它还有一个指针,指向额外信息
class_ro_t,其中ro表示只读 class_ro_t中包含了类名、方法、协议、实例变量和属性等信息
当一个类首次被使用,运行时会为它分配额外的存储容量,即:class_rw_t
- 其中
rw表示可读写
通过firstSubclass和nextSiblingClass,所有的类都会链接成一个树状结构
- 允许运行时遍历当前使用的所有类
为什么方法和属性,在class_ro_t和class_rw_t各存一份?
- 可以在运行时进行更改
- 当
category被加载时,它可以向类中添加新的方法 - 使用
Runtime API动态添加属性和方法 - 因为
class_ro_t是只读的,所以需要在class_rw_t中来跟踪这些东西
class_rw_t结构在设备中,占用很多的内存,那么我们如何去缩小这些结构呢?
在实践中发现,大约只有10%的类,真正的更改了它们的方法
所有,我们可以将不常用的部分进行拆分
class_rw_t的大小可减少一半
对于确实需要额外信息的类,我们可以分配这些扩展记录中的一个,并把它滑到类中供其使用
如果原来的代码直接访问class_rw_t结构,由于结构内存布局发生了变化,可能产生崩溃。苹果推荐使用Runtime API,这样底层的细节会由系统处理
2. 成员变量 & 实例变量 & 属性
2.1 成员变量与属性的区别
- 成员变量:在底层只是变量的声明
- 属性:系统会自动在底层添加
_属性名变量,同时生成setter和getter方法
2.2 成员变量与实例变量的区别
- 实例变量是一种特殊的成员变量
- 成员变量为基本数据类型
- 实例变量为对象类型,例如:
NSObject类型 NSString为常量类型,属于成员变量
2.3 属性的底层实现
打开main.m文件,写入以下代码:
#import <Foundation/Foundation.h>@interface LGPerson : NSObject{NSString *desc;NSObject *obj;}@property (strong,nonatomic) NSString *name;@end@implementation LGPerson@end
生成cpp文件
clang -rewrite-objc main.m -o main.cpp
打开main.cpp文件,查看属性的底层实现
extern "C" unsigned long OBJC_IVAR_$_LGPerson$_name;struct LGPerson_IMPL {struct NSObject_IMPL NSObject_IVARS;NSString *desc;NSObject *obj;NSString *_name;};// @property (strong,nonatomic) NSString *name;static NSString * _I_LGPerson_name(LGPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_LGPerson$_name)); }static void _I_LGPerson_setName_(LGPerson * self, SEL _cmd, NSString *name) { (*(NSString **)((char *)self + OBJC_IVAR_$_LGPerson$_name)) = name; }
- 在底层代码中,属性被优化掉
- 生成
_开头的成员变量 +getter/setter方法
3. 编码
底层的方法列表中,生成的编码含义是什么?
static struct /*_method_list_t*/ {unsigned int entsize; // sizeof(struct _objc_method)unsigned int method_count;struct _objc_method method_list[4];} _OBJC_$_INSTANCE_METHODS_LGPerson __attribute__ ((used, section ("__DATA,__objc_const"))) = {sizeof(_objc_method),4,{{(struct objc_selector *)"name", "@16@0:8", (void *)_I_LGPerson_name},{(struct objc_selector *)"setName:", "v24@0:8@16", (void *)_I_LGPerson_setName_},{(struct objc_selector *)"name", "@16@0:8", (void *)_I_LGPerson_name},{(struct objc_selector *)"setName:", "v24@0:8@16", (void *)_I_LGPerson_setName_}}};
3.1 Code对照表
编码中Code与数据类型的对照表
- 可查看官方文档:Type Encodings
3.2 编码的含义
编码中,除了Code之外,还有数字,它们组合在一起的含义是什么?
案例1
name属性的getter方法
NSString * _I_LGPerson_name(LGPerson * self, SEL _cmd)-------------------------@16@0:8
@16:@表示返回值类型为id类型,16表示方法所占用的内存为16字节@0:@表示第一个参数类型为id类型,0表示参数1存储的起始位置,占8字节:8::表示第二个参数类型为SEL类型,8表示参数2存储的起始位置,占8字节- 参数分别为
objc_msgSend的两个隐含参数,id类型的self,SEL类型的_cmd
案例2
name属性的setter方法
void _I_LGPerson_setName_(LGPerson * self, SEL _cmd, NSString *name)-------------------------v24@0:8@16
v24:v表示返回值类型为void类型,24表示方法所占用的内存为24字节@0:@表示第一个参数类型为id类型,0表示参数1存储的起始位置,占8字节:8::表示第二个参数类型为SEL类型,8表示参数2存储的起始位置,占8字节@16:@表示第三个参数类型为id类型,16表示参数3存储的起始位置,占8字节
3.3 使用代码查看类型编码
void lgTypes(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 person{char *name;int age;}Person;NSLog(@"struct --> %s",@encode(Person));typedef union union_type{char *name;int a;}Union;NSLog(@"union --> %s",@encode(Union));int a = 2;int *b = {&a};NSLog(@"int[] --> %s",@encode(typeof(b)));}-------------------------//输出结果:char --> cint --> ishort --> slong --> qlong long --> qunsigned char --> Cunsigned int --> Iunsigned short --> Sunsigned long --> Qfloat --> fbool --> Bvoid --> vchar * --> *id --> @Class --> #SEL --> :int[] --> [3i]struct --> {person=*i}union --> (union_type=*i)int[] --> ^i
@encode:获取指定类型的编码字符串
扩展内容
为什么底层生成属性的getter/setter方法有同样的两份?
使用MachOView查看MachO文件
- 以
setName:为例:在符号表中,同样的方法,因符号的功能及种类不同,出现了两份
使用objdump命令,按符号功能查看
objdump --macho --syms KCObjcBuild | grep setName:-------------------------0000000100003d20 l F __TEXT,__text -[LGPerson setName:]0000000100003d20 l d *UND* -[LGPerson setName:]
F:表示Functiond:表示Debug
使用nm命名,按符号种类查看
nm -pa KCObjcBuild | grep setName:-------------------------0000000100003d20 t -[LGPerson setName:]0000000100003d20 - 01 0000 FUN -[LGPerson setName:]
t:表示符号存储在代码段(__TEXT.__text),小写表示符号为本地符号(local)-:表示调试符号
4. setter方法的底层实现
4.1 准备工作
打开main.m文件,写入以下代码:
#import <Foundation/Foundation.h>@interface LGPerson : NSObject@property (strong,nonatomic) NSString *name;@property (copy,nonatomic) NSString *nick;@end@implementation LGPerson@end
生成cpp文件
clang -rewrite-objc main.m -o main.cpp
4.2 底层实现的差异
在OC中,使用不同修饰符定义属性,例如:strong和copy。会导致setter方法在底层实现的差异
name属性的setter方法
static void _I_LGPerson_setName_(LGPerson * self, SEL _cmd, NSString *name) { (*(NSString **)((char *)self + OBJC_IVAR_$_LGPerson$_name)) = name; }
- 使用内存平移方式
nick属性的setter方法
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);static void _I_LGPerson_setNick_(LGPerson * self, SEL _cmd, NSString *nick) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct LGPerson, _nick), (id)nick, 0, 1); }
- 调用
objc_setProperty函数,相当于所有属性setter方法的抽取封装,因为setter方法的本质都是修改内存中的数据
找到setter方法和objc_setProperty在底层的关联时机:
方法在编译阶段已经确定函数地址
所以setter方法和objc_setProperty的关联,在编译时期由llvm完成
4.3 探索llvm源码
打开llvm-project源码
搜索objc_setProperty关键字
打开CGObjCMac.cpp文件
4.3.1 getSetPropertyFn函数

- 找到
objc_setProperty函数的创建 - 由下至上反推,找出函数的创建时机
搜索getSetPropertyFn关键字
4.3.2 GetPropertySetFunction函数

- 找到中间层的调用,继续向上层反推
搜索GetPropertySetFunction关键字
打开CGObjC.cpp文件
4.3.3 generateObjCSetterBody函数

- 当策略为
GetSetProperty或SetPropertyAndExpressionGet时,有可能触发GetPropertySetFunction函数
判断strategy.getKind()的策略值
搜索GetSetProperty关键字
4.3.4 PropertyImplStrategy函数

- 使用
copy修饰,Kind设置GetSetProperty
4.4 探索objc源码
找到objc_setProperty的函数实现
打开objc-accessors.mm文件
4.4.1 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);}
- 调用
reallySetProperty函数
4.4.2 reallySetProperty函数
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);}
- 底层触发内存拷贝的逻辑,和
strong使用内存平移赋值是有很大差异的
总结:
objc_setProperty的函数实现,在objc4-818.2源码中
5. Runtime API
5.1 class_copyMethodList
class_copyMethodList:获取类的实例方法列表
5.1.1 获取方法列表
封装objc_copyMethodList方法
void objc_copyMethodList(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));NSLog(@"Method:%@", key);}free(methods);}
打印类的方法列表
objc_copyMethodList(LGPerson.class);-------------------------Method:sayNBMethod:nickMethod:setNick:Method:initMethod:nameMethod:.cxx_destructMethod:setName:
打印元类的方法列表
objc_copyMethodList(objc_getMetaClass(class_getName(LGPerson.class)));-------------------------Method:good
5.2 class_getInstanceMethod
class_getInstanceMethod:获取指定类中的实例方法
5.2.1 验证实例方法
封装instanceMethod_classToMetaclass函数
void instanceMethod_classToMetaclass(Class pClass){const char *className = class_getName(pClass);Class metaClass = objc_getMetaClass(className);Method method1 = class_getInstanceMethod(pClass, @selector(sayNB));Method method2 = class_getInstanceMethod(metaClass, @selector(sayNB));NSLog(@"类的sayNB实例方法:%p", method1);NSLog(@"元类的sayNB实例方法:%p", method2);Method method3 = class_getInstanceMethod(pClass, @selector(good));Method method4 = class_getInstanceMethod(metaClass, @selector(good));NSLog(@"类的good实例方法:%p", method3);NSLog(@"元类的good实例方法:%p", method4);}
传入LGPerson类对象
instanceMethod_classToMetaclass(LGPerson.class);-------------------------类的sayNB实例方法:0x1000081d0元类的sayNB实例方法:0x0类的good实例方法:0x0元类的good实例方法:0x100008168
- 类中存储了
sayNB实例方法 - 元类中存储了
good实例方法
总结:
- 在
OC底层,所有方法都是实例方法 - 类方法,元类中存储的实例方法
5.3 class_getClassMethod
class_getClassMethod:获取指定类中的类方法
5.3.1 验证类方法
封装classMethod_classToMetaclass函数
void classMethod_classToMetaclass(Class pClass){const char *className = class_getName(pClass);Class metaClass = objc_getMetaClass(className);Method method1 = class_getClassMethod(pClass, @selector(sayNB));Method method2 = class_getClassMethod(metaClass, @selector(sayNB));NSLog(@"类的sayNB类方法:%p", method1);NSLog(@"元类的sayNB类方法:%p", method2);Method method3 = class_getClassMethod(pClass, @selector(good));Method method4 = class_getClassMethod(metaClass, @selector(good));NSLog(@"类的good类方法:%p", method3);NSLog(@"元类的good类方法:%p", method4);}
传入LGPerson类对象
classMethod_classToMetaclass(LGPerson.class);-------------------------类的sayNB类方法:0x0元类的sayNB类方法:0x0类的good类方法:0x100008158元类的good类方法:0x100008158
- 类中存储了
good类方法 - 元类中存储了
good类方法
疑问:
- 为什么元类中存储了
good类方法?
5.3.2 探索objc源码
找到class_getClassMethod函数
Method class_getClassMethod(Class cls, SEL sel){if (!cls || !sel) return nil;return class_getInstanceMethod(cls->getMeta(), sel);}
- 调用
class_getInstanceMethod函数 - 本质:查找当前类对象所属元类中的实例方法
找到getMeta函数
Class getMeta() {if (isMetaClassMaybeUnrealized()) return (Class)this;else return this->ISA();}
- 传入的类对象是元类,返回自己
所以,元类中存储good类方法,其实找到的还是元类中的good实例方法
5.4 class_getMethodImplementation
class_getMethodImplementation:获取指定方法的函数实现地址
5.4.1 SEL & IMP
SEL与IMP的关系
SEL:书的⽬录名称(⽅法编号)IMP: ⽬录的⻚码(函数指针地址)
查找流程:
- ⾸先明⽩我们要找到书本的什么内容(
SEL⽬录⾥⾯的名称) - 通过名称找到对应的⻚码(通过
SEL找IMP) - 通过⻚码去定位具体的内容(通过
IMP找到函数实现)
5.4.2 验证方法IMP
封装imp_classToMetaclass函数
void imp_classToMetaclass(Class pClass){const char *className = class_getName(pClass);Class metaClass = objc_getMetaClass(className);IMP imp1 = class_getMethodImplementation(pClass, @selector(sayNB));IMP imp2 = class_getMethodImplementation(metaClass, @selector(sayNB));NSLog(@"类的sayNB方法的IMP:%p", imp1);NSLog(@"元类的sayNB方法的IMP:%p", imp2);IMP imp3 = class_getMethodImplementation(pClass, @selector(good));IMP imp4 = class_getMethodImplementation(metaClass, @selector(good));NSLog(@"类的good方法的IMP:%p", imp3);NSLog(@"元类的good方法的IMP:%p", imp4);}
传入LGPerson类对象
imp_classToMetaclass(pClass);-------------------------类的sayNB方法的IMP:0x100003aa0元类的sayNB方法的IMP:0x7fff202405c0类的good方法的IMP:0x7fff202405c0元类的good方法的IMP:0x100003ab0
疑问:
- 元类中没有
sayNB实例方法,为什么存在方法IMP? - 类中没有
good类方法,为什么存在方法IMP? - 元类中
sayNB和类中good,两个方法IMP为什么相同?
5.4.3 探索objc源码
找到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 versionif (!imp) {return _objc_msgForward;}return imp;}
- 如果找不到方法
IMP,返回_objc_msgForward函数地址
找到_objc_msgForward的定义
OBJC_EXPORT void_objc_msgForward(void /* id receiver, SEL sel, ... */ )OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
Runtime的消息转发机制
所以,当方法IMP不存在时,触发Runtime的消息转发机制,返回_objc_msgForward函数地址,所以IMP地址相同
6. isKindOfClass
6.1 方法的作用
打开objc4-818.2源码
查看汇编代码
- 无论调用类对象还是实例对象的
isKindOfClass方法,入口函数统一为objc_opt_isKindOfClass
找到objc_opt_isKindOfClass函数
BOOLobjc_opt_isKindOfClass(id obj, Class otherClass){#if __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;}#endifreturn ((BOOL(*)(id, SEL, Class))objc_msgSend)(obj, @selector(isKindOfClass:), otherClass);}
- 获取对象isa指向的类,和传入的
Class对比 - 遍历对象isa指向类的父类,和传入的
Class对比
isKindOfClass方法的作用:
- 类对象
◦ 元类 vs Class
◦ 遍历:元类的父类 vs Class
- 实例对象
◦ 类 vs Class
◦ 遍历:类的父类 vs Class
6.2 验证类对象与实例对象
封装isKindOfClass函数
void isKindOfClassDemo(){BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];BOOL re2 = [(id)[LGPerson class] isKindOfClass:[LGPerson class]];NSLog(@"NSObject类对象:%hhd", re1);NSLog(@"LGPerson类对象:%hhd", re2);BOOL re3 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]];BOOL re4 = [(id)[LGPerson alloc] isKindOfClass:[LGPerson class]];NSLog(@"NSObject实例对象:%hhd", re3);NSLog(@"LGPerson实例对象:%hhd", re4);}
调用isKindOfClassDemo函数
isKindOfClassDemo();-------------------------NSObject类对象:1LGPerson类对象:0NSObject实例对象:1LGPerson实例对象:1
7. isMemberOfClass
7.1 方法的作用
打开objc4-818.2源码
查看汇编代码
- 调用指定对象的
objc_msgSend函数
找到isMemberOfClass方法
+ (BOOL)isMemberOfClass:(Class)cls {return self->ISA() == cls;}- (BOOL)isMemberOfClass:(Class)cls {return [self class] == cls;}
- 类方法:获取类的元类,和传入的
cls对比 - 实例方法:获取对象所属的类,和传入的
cls对比
7.2 验证类对象与实例对象
封装isMemberOfClassDemo方法
void isMemberOfClassDemo(){BOOL re1 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];BOOL re2 = [(id)[LGPerson class] isMemberOfClass:[LGPerson class]];NSLog(@"NSObject类对象:%hhd", re1);NSLog(@"LGPerson类对象:%hhd", re2);BOOL re3 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]];BOOL re4 = [(id)[LGPerson alloc] isMemberOfClass:[LGPerson class]];NSLog(@"NSObject实例对象:%hhd", re3);NSLog(@"LGPerson实例对象:%hhd", re4);}
调用isMemberOfClassDemo函数
isMemberOfClassDemo();-------------------------NSObject类对象:0LGPerson类对象:0NSObject实例对象:1LGPerson实例对象:1
总结
WWDC 2020-Runtime优化
- 类结构的优化
- 方法列表的优化
Tagged Pointer格式的优化
WWDC-类结构的优化
Clean Memory
◦ 加载后不会再改变的内存
◦ class_ro_t是只读的,它就属于Clean Memory
◦ 可移除,从而节省更多内存空间。如果你有需要,系统可以从磁盘中重新加载
Dirty Memory
◦ 进程运行时会发生变化的内存
◦ 类结构体一旦被使用就是Dirty Memory,因为运行时会写入新的数据,例如:方法缓存
◦ Dirty Memory是类数据被分为两部分的原因
- 方法和属性在
class_ro_t和class_rw_t各存一份
◦ 可以在运行时进行更改
◦ 当category被加载时,它可以向类中添加新的方法
◦ 用Runtime API动态添加属性和方法
◦ 因为class_ro_t是只读的,所以需要在class_rw_t中来跟踪这些东西
- 优化
class_rw_t空间
◦ 拆分class_rw_ext_t,存储方法、属性、协议等不常用的部分
成员变量与属性的区别
- 成员变量:在底层只是变量的声明
- 属性:系统会自动在底层添加
_属性名变量,同时生成setter和getter方法
成员变量与实例变量的区别
- 实例变量是一种特殊的成员变量
- 成员变量为基本数据类型
- 实例变量为对象类型,例如:
NSObject类型 NSString为常量类型,属于成员变量
属性的底层实现
- 在底层代码中,属性被优化掉
- 生成
_开头的成员变量 +getter/setter方法
编码
Code对照表:Type Encodings@encode:获取指定类型的编码字符串
setter方法的底层实现
- 在
OC中,使用不同修饰符定义属性,例如:strong和copy。会导致setter方法在底层的实现产生差异
◦ strong:内存平移
◦ copy:调用objc_setProperty函数
objc_setProperty
◦ setter方法和objc_setProperty的关联,在编译时期由llvm完成
◦ objc_setProperty的函数实现,在objc4-818.2源码中
◦ 底层触发内存拷贝的逻辑,和strong使用内存平移赋值是有很大差异的
SEL与IMP的关系
SEL:书的⽬录名称(⽅法编号)IMP: ⽬录的⻚码(函数指针地址)
Runtime API
class_copyMethodList:获取类的实例方法列表class_getInstanceMethod:获取指定类中的实例方法
◦ 在OC底层,所有方法都是实例方法
◦ 类方法,元类中存储的实例方法
class_getClassMethod:获取指定类中的类方法
◦ 本质:查找当前类对象所属元类中的实例方法
◦ 传入getMetah函数的类对象是元类,返回自己
class_getMethodImplementation:获取指定方法的函数实现地址
◦ 当方法IMP不存在时,触发Runtime的消息转发机制,返回_objc_msgForward函数地址
isKindOfClass
- 类对象
◦ 元类 vs Class
◦ 遍历:元类的父类 vs Class
- 实例对象
◦ 类 vs Class
◦ 遍历:类的父类 vs Class
isMemberOfClass
- 类方法:获取类的元类,和传入的
cls对比 - 实例方法:获取对象所属的类,和传入的
cls对比
