OC 类原理探索 系列文章
- OC 类原理探索:类的结构分析
- OC 类原理探索:类结构分析补充
- OC 类原理探索:属性的底层原理
前言
上篇OC 类原理探索:类的结构分析对类结构
做了整体的分析,今天做一些补充和拓展。
一、WWDC 2020 类结构的优化
WWDC 2020 – runtime 优化 中介绍了类数据结构
的优化、方法列表
的优化和tagged pointer
格式的优化3
方面内容,我们今天只分析类数据结构
的优化。
1. clean memory 和 dirty memory 的区别
clean memory
clean memory
是指加载后不会发生更改的内存;class_ro_t
就属于clean memory
因为它是只读的;clean memory
可以进行移除从而节省更多的内存空间,因为如果你需要clean memory
,系统可以从磁盘中重新加载。
dirty memory
dirty memory
是指在进程运行时会发生更改的内存;- 类结构一经使用就会变成
dirty memory
,因为运行时会向它写入新的数据,例如创建一个新的方法缓存并从类中指向它; dirty memory
比clean memory
要昂贵的多,只要进程在运行,他就必须一直存在;macOS
可以选择换出dirty memory
,但因为iOS
不使用swap
所以dirty memory
在iOS
中代价很大。
dirty memory
是这类数据被分成两部分的原因,可以保持清洁的数据越多越好,通过分离出那些永远不会更改的数据,可以把大部分的类数据存储为clean memory
。
2. class_rw_t 的优化
当类第一次被加载
到内存时的结构:
虽然这些数据足以让我们开始,但运行时需要追踪每个类的更多信息。
所以当一个类首次被使用
,运行时会为它分配额外的存储容量,这个运行时分配的存储容量是class_rw_t
用于读取-编写
数据,看下面的结构:
在这个数据结构中,我们存储了只有在运行时才会生成的新信息。例如,所有的类都会连接成一个树状结构,这是通过使用First Subclass
和Next Sibling Class
指针实现的,这允许运行时遍历当前使用的所有类,这对于使方法缓存无效非常有用。
但为什么方法和属性
在只读数据中有,这里还有方法和属性呢?因为它们可以在运行时进行更改,当categry
被加载时它可以向类中添加新的方法,也可以使用运行时API
动态的添加它们,因为class_ro_t
是只读的,所以需要在class_rw_t
中追踪这些东西。
这样做的结果是会占用相当多的内存,在任何给定的设备中都有许多类在使用,在iPhone
上的整个系统中,大约30
兆字节这些class_rw_t
结构,那么我们如何缩小这些结构呢?我们在读取-编写
部分需要这些东西,因为它们可以在运行时进行更改,但是通过检查实际设备上的使用情况,发现大约只有10%
的类真正的更改了它们的方法。
而且只有Swift
类会使用这个Demangled name
字段,并且Swift
类并不需要这一字段,除非有东西询问他们的Objective-C
名称时才需要。
所以 我们可以拆掉那些平时不用的部分,这将class_rw_t
的大小减少了一半,
将需要动态更新的部分提取出来,存入class_rw_ext_t
。
大约90%
的类从来不需要这些扩展数据,这在系统范围内可节省大约14MB
的内存,这些内存可以用于更有效的用途,比如存储你的app
的数据。
二、API 方式解析类方法的存储
OC 类原理探索:类的结构分析 中用lldb
对类方法的存储进行了解析,今天用API
的方式来进行解析。
定义SSLPerson
类:
@interface SSLPerson : NSObject {
NSString *_hoby;
}
@property (nonatomic, strong) NSString *name;
@property (nonatomic, assign) int age;
- (void)eat;
+ (void)run;
@end
复制代码
1. 获取方法列表:class_copyMethodList
void logObjc_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, name: %@", key);
}
free(methods);
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
SSLPerson *person = [SSLPerson alloc];
Class pClass = object_getClass(person);
const char *className = class_getName(pClass);
Class metaClass = objc_getMetaClass(className);
logObjc_copyMethodList(pClass);
SSLog(@"*************");
logObjc_copyMethodList(metaClass);
}
return 0;
}
打印结果:
Method, name: eat
Method, name: name
Method, name: .cxx_destruct
Method, name: setName:
Method, name: age
Method, name: setAge:
*************
Method, name: run
复制代码
- 分别打印了
类
和元类
中的所有方法; metaClass
打印的run
属于元类
中方法,其他属于类
中方法。
2. 获取类方法:class_getInstanceMethod
void logInstanceMethod_classToMetaclass(Class pClass){
const char *className = class_getName(pClass);
Class metaClass = objc_getMetaClass(className);
Method method1 = class_getInstanceMethod(pClass, @selector(eat));
Method method2 = class_getInstanceMethod(metaClass, @selector(eat));
Method method3 = class_getInstanceMethod(pClass, @selector(run));
Method method4 = class_getInstanceMethod(metaClass, @selector(run));
NSLog(@" %p \n %p \n %p \n %p",method1,method2,method3,method4);
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
SSLPerson *person = [SSLPerson alloc];
Class pClass = object_getClass(person);
logInstanceMethod_classToMetaclass(pClass);
}
return 0;
}
打印结果:
0x1000083b8
0x0
0x0
0x100008350
复制代码
class_getInstanceMethod(pClass, @selector(eat))
得到了地址0x1000083b8
,class_getInstanceMethod(metaClass, @selector(run))
得到了地址0x100008350
。- 说明了
eat
属于类
中方法,run
属于元类
中方法。
3. 获取元类方法:class_getClassMethod
void logClassMethod_classToMetaclass(Class pClass){
const char *className = class_getName(pClass);
Class metaClass = objc_getMetaClass(className);
Method method1 = class_getClassMethod(pClass, @selector(eat));
Method method2 = class_getClassMethod(metaClass, @selector(eat));
Method method3 = class_getClassMethod(pClass, @selector(run));
Method method4 = class_getClassMethod(metaClass, @selector(run));
SSLog(@" %p \n %p \n %p \n %p",method1,method2,method3,method4);
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
SSLPerson *person = [SSLPerson alloc];
Class pClass = object_getClass(person);
logClassMethod_classToMetaclass(pClass);
}
return 0;
}
打印结果:
0x0
0x0
0x100008350
0x100008350
复制代码
class_getClassMethod
函数解析:如果是个普通类
就获取它的元类方法,如果已经是元类
了,直接获取方法;- 所以
class_getClassMethod(pClass, @selector(run))
和class_getClassMethod(metaClass, @selector(run))
得到了方法地址0x100008350
。
4. 获取类 imp :class_getMethodImplementation
void logIMP_classToMetaclass(Class pClass){
const char *className = class_getName(pClass);
Class metaClass = objc_getMetaClass(className);
IMP imp1 = class_getMethodImplementation(pClass, @selector(eat));
IMP imp2 = class_getMethodImplementation(metaClass, @selector(eat));// 0
// sel -> imp 方法的查找流程 imp_farw
IMP imp3 = class_getMethodImplementation(pClass, @selector(run)); // 0
IMP imp4 = class_getMethodImplementation(metaClass, @selector(run));
SSLog(@" %p \n %p \n %p \n %p",imp1,imp2,imp3,imp4);
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
SSLPerson *person = [SSLPerson alloc];
Class pClass = object_getClass(person);
logIMP_classToMetaclass(pClass);
}
return 0;
}
打印结果:
0x100003ba0
0x7fff203895c0
0x7fff203895c0
0x100003bb0
复制代码
imp1
是pClass
的类方法,imp4
是metaClass
的元类方法。- 为什么
imp2
和imp3
也有值呢,看源码分析一下。
__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
,所以imp2
和imp3
有值。
三、isKindOfClass 面试题
看下面代码及打印结果:
void logKindOfDemo(void){
BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
BOOL re3 = [(id)[SSLPerson class] isKindOfClass:[SSLPerson class]];
BOOL re4 = [(id)[SSLPerson class] isMemberOfClass:[SSLPerson 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)[SSLPerson alloc] isKindOfClass:[SSLPerson class]];
BOOL re8 = [(id)[SSLPerson alloc] isMemberOfClass:[SSLPerson class]];
NSLog(@" re5 :%hhd\n re6 :%hhd\n re7 :%hhd\n re8 :%hhd\n",re5,re6,re7,re8);
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
logKindOfDemo();
}
return 0;
}
打印结果:
re1 :1
re2 :0
re3 :0
re4 :0
re5 :1
re6 :1
re7 :1
re8 :1
复制代码
为什么会有这样的结果呢,接下来我们用源码进行方法分析。
1. 错误的源码
+ (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = self->ISA(); tcls; tcls = tcls->getSuperclass()) {
if (tcls == cls) return YES;
}
return NO;
}
- (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = [self class]; tcls; tcls = tcls->getSuperclass()) {
if (tcls == cls) return YES;
}
return NO;
}
复制代码
网上针对这段源码分析的文章很多,其实这是不对的,这段代码是老版的,OC 2.0
已经不用这段代码了。
2. 源码分析
OC 对象原理探索(一)中介绍过底层探索的三种方式,我们今天用汇编跟源码
的方式解析这道面试题。
打断点 -> 开启汇编调试 -> 运行项目 -> 汇编中找到底层方法objc_opt_isKindOfClass
:
isKindOfClass 源码分析
打开 objc4-818.2 源码,搜索objc_opt_isKindOfClass
方法:
BOOL
objc_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;
}
#endif
return ((BOOL(*)(id, SEL, Class))objc_msgSend)(obj, @selector(isKindOfClass:), otherClass);
}
复制代码
objc_opt_isKindOfClass
方法解析:对象
和类
都调用了objc_opt_isKindOfClass
方法;obj
先获取它的类
或者元类
赋值给tcls
,与otherClass
进行比较,如果相等直接返回YES
,如果不相等tcls
循环取其父类与otherClass
进行比较。
BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
解析:[NSObject class]
进入方法获取它的元类赋值给tcls
,tcls = NSObject元类
与[NSObject class]
进行比较显然不相等;- 进入循环
tcls = NSObject元类
取其父类是NSObject类
,tcls = NSObject类
与[NSObject class]
进行比较相等,所以re1 = 1
;
BOOL re3 = [(id)[SSLPerson class] isKindOfClass:[SSLPerson class]];
解析:[SSLPerson class]
进入方法获取它的元类赋值给tcls
,tcls = SSLPerson元类
与[SSLPerson class]
进行比较显然不相等;- 进入第一次循环
tcls = SSLPerson元类
取其父类是根元类
,tcls = 根元类
与[SSLPerson class]
进行比较还是不相等; - 第二次循环
tcls = 根元类
取其父类是NSObject类
,tcls = NSObject类
与[SSLPerson class]
进行比较还是不相等; - 第三次循环
tcls = NSObject类
取其父类是nil
,tcls = nil
与[SSLPerson class]
进行比较还是不相等,所以re3 = 0
。
re5 = 1
。re7 = 1
。
isMemberOfClass 源码分析
打开 objc4-818.2 源码,搜索isMemberOfClass
方法:
+ (BOOL)isMemberOfClass:(Class)cls {
return self->ISA() == cls;
}
- (BOOL)isMemberOfClass:(Class)cls {
return [self class] == cls;
}
复制代码
+ (BOOL)isMemberOfClass
方法解析:- 当前类的元类和
cls
进行比较;
- 当前类的元类和
- (BOOL)isMemberOfClass
方法解析:- 当前对象的类和
cls
进行比较;
- 当前对象的类和
BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
解析:[NSObject class]
的元类和[NSObject class]
进行比较不相等,所以re2 = 0
。
BOOL re4 = [(id)[SSLPerson class] isMemberOfClass:[SSLPerson class]];
解析:[SSLPerson class]
的元类和[SSLPerson class]
进行比较不相等,所以re4 = 0
。
BOOL re6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]];
解析:[NSObject alloc]
的类和[NSObject class]
进行比较相等,所以re6 = 1
。
BOOL re8 = [(id)[SSLPerson alloc] isMemberOfClass:[SSLPerson class]];
解析:-
[SSLPerson alloc]
的类和[SSLPerson class]
进行比较相等,所以re8 = 1
。
-