OC 类原理探索:类结构分析补充

422 阅读7分钟

OC 类原理探索 系列文章

  1. OC 类原理探索:类的结构分析
  2. OC 类原理探索:类结构分析补充
  3. 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 memoryclean memory要昂贵的多,只要进程在运行,他就必须一直存在;
  • macOS可以选择换出dirty memory,但因为iOS不使用swap所以 dirty memoryiOS中代价很大。

dirty memory是这类数据被分成两部分的原因,可以保持清洁的数据越多越好,通过分离出那些永远不会更改的数据,可以把大部分的类数据存储为clean memory

2. class_rw_t 的优化

当类第一次被加载到内存时的结构:

image.png

虽然这些数据足以让我们开始,但运行时需要追踪每个类的更多信息。 所以当一个类首次被使用,运行时会为它分配额外的存储容量,这个运行时分配的存储容量是class_rw_t用于读取-编写数据,看下面的结构:

image.png

在这个数据结构中,我们存储了只有在运行时才会生成的新信息。例如,所有的类都会连接成一个树状结构,这是通过使用First SubclassNext Sibling Class指针实现的,这允许运行时遍历当前使用的所有类,这对于使方法缓存无效非常有用。

但为什么方法和属性在只读数据中有,这里还有方法和属性呢?因为它们可以在运行时进行更改,当categry被加载时它可以向类中添加新的方法,也可以使用运行时API动态的添加它们,因为class_ro_t是只读的,所以需要在class_rw_t中追踪这些东西。

这样做的结果是会占用相当多的内存,在任何给定的设备中都有许多类在使用,在iPhone上的整个系统中,大约30兆字节这些class_rw_t结构,那么我们如何缩小这些结构呢?我们在读取-编写部分需要这些东西,因为它们可以在运行时进行更改,但是通过检查实际设备上的使用情况,发现大约只有10% 的类真正的更改了它们的方法。

而且只有Swift类会使用这个Demangled name字段,并且Swift类并不需要这一字段,除非有东西询问他们的Objective-C名称时才需要。

image.png

所以 我们可以拆掉那些平时不用的部分,这将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))得到了地址0x1000083b8class_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
  • imp1pClass的类方法,imp4metaClass的元类方法。
  • 为什么imp2imp3也有值呢,看源码分析一下。
__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,所以imp2imp3有值。

三、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

image.png

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]进入方法获取它的元类赋值给tclstcls = NSObject元类[NSObject class]进行比较显然不相等;
    • 进入循环tcls = NSObject元类取其父类是NSObject类tcls = NSObject类[NSObject class]进行比较相等,所以re1 = 1;
  • BOOL re3 = [(id)[SSLPerson class] isKindOfClass:[SSLPerson class]];解析:
    • [SSLPerson class]进入方法获取它的元类赋值给tclstcls = SSLPerson元类[SSLPerson class]进行比较显然不相等;
    • 进入第一次循环tcls = SSLPerson元类取其父类是根元类tcls = 根元类[SSLPerson class]进行比较还是不相等;
    • 第二次循环tcls = 根元类取其父类是NSObject类tcls = NSObject类[SSLPerson class]进行比较还是不相等;
    • 第三次循环tcls = NSObject类取其父类是niltcls = 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