The bottom layer of the OC underlying principle (05) class (below)

1. WWDC 2020 class optimization

  First, let's take a look at Apple's 2020 WWDC video . In the video, Apple's developers mainly made the following three modifications to the underlying data structure.

On disk, in your APP binary the class looks like this:

Xnip2022-07-04_22-26-30.png

The class object itself, which contains the most frequently accessed information, is pointers to metaclasses, superclasses, and method caches . In addition, there is a pointer to more data. The place to store additional information is called class_ro_t, roRepresents read-only. This structure contains information about class names, methods, protocols, and instance variables , as shown in the following figure:

Xnip2022-07-04_22-28-16.png

When classes are first loaded from disk into memory, they start out like this, but once used, they change, but before understanding these changes, it's worth understanding what is clean memoryand what dirty memorythe difference is:

  • clean memory: Refers to memory that does not change after loading.
  • dirty memory: Refers to memory that changes while the process is running.

class_ro_tbelongs clean memory, since it's read-only. The class structure changes as soon as it is used dirty memory, as the runtime writes new data to it (eg: create a new method cache and point to it from the class). dirty memoryis much clean memorymore expensive than, as long as the process is running, it has to exist, on the other hand clean memoryit can be removed, saving more memory space. Because clean memorythe system can be reloaded from disk if you need to. macOSThere is an option to swap out dirty memory, but since it iOS's not used swap, it 's expensive in the middle dirty memory. That's why this class data is split in two, the more data that can be kept clean the better, by separating out the data that never changes, most of the class data can be stored as , while this data is enough to get us started, But the runtime needs to track more information about each class. So, when a class is used for the first time, the runtime allocates additional storage capacity for it. This runtime allocated storage capacity is used for reading-writing data, as shown in the following figure:iOSdirty memoryclean memoryclass_rw_t

Xnip2022-07-04_22-35-36.png

First SubclassIn this data structure, we store new information ( , ) that is only generated at runtime Next Sibing Class, eg: all classes are linked into a tree-like structure. This is achieved by using First Subclassand Next Sibing Classpointers. This allows the runtime to traverse all classes currently in use, which is useful for making methods cacheable. But why are methods and properties both in class_ro_t(只读数据)and in class_rw_t(读写数据)? Because class_rw_tcan be changed at runtime, when categoryis loaded, it can add new methods to the class, and programmers can add them runtime APIdynamically . Since it class_ro_t's read-only, we need class_rw_tto keep track of these things in . But the result of the above is that it takes up quite a bit of memory, and there are many classes in use in any given device, so how do we shrink these structures? Remember, we class_rw_tneed these things in because these things can be changed at runtime. But by examining usage on actual devices, we found that only about 100 %10classes actually changed their methods, and only Swiftclasses would use the demangled namefield, and Swift classes don't need this field unless something asks for their Objective-Cname If needed, all we can remove those parts that are not normally used, which reduces class_rw_tthe size in half, as shown in the following image:

Xnip2022-07-04_22-42-32.png

For those classes that do need extra information, we can assign one of these extension records and slide it into the class for its use, as shown in the following image:

Xnip2022-07-04_22-43-43.png

大约 90% 的类从来不需要这些扩展数据,在系统范围内可节省大约14MB的内存,这些内存可以用于更有效的用途,比如存储你的App数据。这对于dirty memory来说,这是真正的节省内存,现在,很多从类中获取数据的代码,必须同时处理那些有扩展数据和没有扩展数据的类,当然,运行时会为你处理这一切,并且从外部看,一切都像往常一样工作,只是使用更少的内存,之所以会这样,是因为读取这些结构的代码,都在运行时内并且还会同时进行更新,坚持使用这些API 真的很重要,因为任何视图直接访问这些数据结构的代码都将在今年的OS版本中停止工作,如下图所示:

Xnip2022-07-04_22-46-01.png

因为数据结构已经发生了变化,而且该代码不知道新的布局,除了你自己的代码,也要注意那些依赖的外部代码,你可能正把它们带入你的 app 中,它们可能会在你没有意识到的情况下,访问这些数据结构。这些结构中的所有信息,都可以通过官方API获取,一些API如下图所示:

Xnip2022-07-04_22-48-19.png

当你使用这些API访问信息时,无论Apple在后台进行什么更改,这些APIS都将继续工作,所有的API都可以在Objective-C运行时说明文档中找到,而这个文档在developer.apple.com中。

总结

class_rw_t优化,其实就是对class_rw_t不常用的部分进行了剥离。如果需要用到这部分就从扩展记录中分配一个,滑到类中供其使用。现在大家对类应该有个更清楚的认识。

二. 类方法的探索方式

// YJPerson.h
@interface YJPerson : NSObject 
- (void)sayHello;
+ (void)eat;
@end

// YJPerson.m
@implementation YJPerson 
- (void)sayHello {}
+ (void)eat {}
@end

2.1 lldb 读取类方法

通过 类的底层探究上 我们知道 对象方法也就是实例方法是存放在类中的,那么 相对于 元类 来说就是 类对象,由此猜想类对象的方法是不是在 元类中呢?下面我们来通过实例探索:

Xnip2022-07-05_13-40-36.png

2.1 runtime API读取类的方法列表

/// 输出类的方法列表
/// @param cls 类
void yj_copyMehtodList(Class cls)
{
    unsigned int count = 0;
    Method *methods = class_copyMethodList(cls, &count);
    for (unsigned int i=0; i < count; i++) {
        Method const method = methods[i];
        // 获取方法名
        NSString *key = NSStringFromSelector(method_getName(method));
        YJLog(@"Method, name: %@", key);
    }
    free(methods);
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        YJPerson *person = [YJPerson alloc];
        YJLog(@"类的方法列表:")
        // 类
        Class pCls = object_getClass(person);
        // 获取类的 方法列表
        yj_copyMehtodList(pCls);
  
        YJLog(@"\n元类的方法列表:")
        // 元类
        const char *className = class_getName(pCls);
        Class metaCls = objc_getMetaClass(className);
        // 获取元类的 方法列表
        yj_copyMehtodList(metaCls);
    }
    return 0;
}

输出:

Xnip2022-07-05_14-34-19.png

类中的方法是 sayHello 方法,元类中的方法是 eat 方法,因此 类方法是在 元类 中的

三. 编码

3.1 官方编码说明

地址链接

Xnip2022-07-05_14-49-35.png

3.2 通过代码获取编码信息

Xnip2022-07-05_14-56-44.png

@encode() 获取一个给定类型的编码字符串

四. setter方法底层实现

OC对象本质与 isa 1.2.4 中发现属性setter方法有的是通过objc_setProperty实现的,有的是直接内存偏移获取变量地址,然后赋值。这是为什么呢?下面我们通过实例来探索

main.m 中创建 YYJPerson 代码为:

// main.m
@interface YYJPerson : NSObject
{
    NSString *yj_name;
    NSObject *yj_objc;
}
@property (nonatomic, copy) NSString *c_name;
@property (nonatomic, strong) NSString *s_name;
@property (nonatomic, assign) int age;
@end
@implementation YYJPerson
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
    }
    return 0;
}

使用clang 命令:clang -rewrite-objc main.m -o main.cpp生成 main.cpp 文件:

Xnip2022-07-05_16-07-42.png

我们发现,虽然这两个属性 c_names_name 都是 NSString 类型的,但是编译后的源码,c_name 属性的 setter 方法中使用的是objc_setProperty 函数赋值,而s_name属性的setter方法中使用的确实指针偏移赋值,这是为什么呢?

首先,我们先来探究一下为什么会需要 objc_setProperty 这个函数,因为在你编写类的代码时,底层并不知道你会定义什么样的属性以及方法,但是属性的settergetter 方法又要调用底层的接口,这就形成了一个矛盾,那该如何解决这个问题呢?由于所有属性的setter方法只有第二个参数_cmd是不一样的,因此,就出现了objc_setProperty函数,用来作为中间层,替换了属性setter方法的imp实现,这样就保证了底层代码不会因为属性的增多而增加多余的代码,如下图所示:

Xnip2022-07-05_16-18-46.png

接着,我们还需要探究的是objc_setProperty()调用流程,但是现在就出现了一个问题,就是我们是去那里找objc_setProperty()这个函数的调用流程呢,如果你去objc的源码中查找,就会发现objc的源码中有objc_setProperty()函数的声明以及实现,但问题是,这里是我们自定义的类中的属性的setter方法里面调用的objc_setProperty(),这肯定不是objc源码调用的,而是llvm在编译的时候调用的,但是为什么要在编译时刻如此修改呢?为什么不在运行时修改呢?因为要知道,在一个项目工程中,我们自定义的类是很多的,类中的属性也是很多的,如果我们全部交给运行时处理,效率就会很低,因此放在编译时修改属性的setter方法的imp是最明智的选择。现在使用VS Code打开llvm源码,搜索objc_setProperty,我们找到了如下的代码:

Xnip2022-07-05_16-36-16.png

全局搜索发现 其实是调用了 getSetPropertyFn() 这个函数,根据这个函数的名字,可以判断这个函数是用来获取 setProperty 这个函数的,因此我们需要知道在哪里调用了 getSetPropertyFn()这个函数,然后我们全局搜索getSetPropertyFn(),发现了以下的代码:

Xnip2022-07-05_16-39-55.png

全局搜索发现 其实是 GetPropertySetFunction函数调用了GetSetPropertyFn 这个函数,接着我们全局搜索GetPropertySetFunction(),看看GetPropertySetFunction在哪里调用了,然后发现了如下代码:

Xnip2022-07-05_16-49-00.png

Xnip2022-07-05_16-57-53.png

GetPropertySetFunction 函数的实际上调用的是generateObjCSetterBody函数,在这个函数中对strategy.getKind()函数调用后的枚举值进行比较,当枚举值为GetSetPropertySetPropertyAndExpressionGet并且UseOptimizedSetter(CGM)这个函数调用结果不为真的时候将会调用GetPropertySetFunction函数获取到setProperty这个函数,并在下面的函数中进行调用,因此我们需要知道strategygetKind函数是如何实现的,其代码如下:

Xnip2022-07-05_17-08-27.png

getKind()函数实际上是获取了类PropertyImplStrategy中的成员变量Kind的值,因此我们再来看看成员变量Kind是在何时进行赋值的,最后我们在PropertyImplStrategy的构造方法中找到了其成员变量Kind的赋值,代码如下所示:

Xnip2022-07-05_17-15-10.png

分析上图代码,我们就可以明白,当设置属性为Copy时,就会给Kind赋值为GetSetProperty,这就是objc_setProperty()函数底层的调用流程。 尽管底层是如此实现的,但是我们还是想做一个验证,编写代码如下:

// main.m 
@interface YYJPerson : NSObject
@property (nonatomic, copy) NSString *n_c_name;
@property (atomic, strong) NSString *a_c_name; 
@property (nonatomic) NSString *n_name; 
@property (atomic) NSString *_name; 
@end 

@implementation YYJPerson
@end

再次生成 main.cpp

Xnip2022-07-05_17-27-07.png

可以发现,只有使用 copy 修饰的属性才会在其setter方法中调用objc_setProperty函数。

五. 面试题

通过一个经典面试题来探究下isKindOfClassisMemberOfClass

// iskindOfClass & isMemberOfClass 类方法调用
BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
BOOL re2 = [(id)[YJPerson class] isKindOfClass:[YJPerson class]];
BOOL re3 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
BOOL re4 = [(id)[YJPerson class] isMemberOfClass:[YJPerson class]];
NSLog(@"\n re1 :%hhd\n re2 :%hhd\n re3 :%hhd\n re4 :%hhd\n", re1, re2, re3, re4);

// iskindOfClass & isMemberOfClass 实例方法调用
BOOL re5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]];
BOOL re6 = [(id)[YJPerson alloc] isKindOfClass:[YJPerson class]];
BOOL re7 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]];
BOOL re8 = [(id)[YJPerson alloc] isMemberOfClass:[YJPerson class]];
NSLog(@"\n re5 :%hhd\n re6 :%hhd\n re7 :%hhd\n re8 :%hhd\n",re5,re6,re7,re8);

输出结果:

Xnip2022-07-06_09-21-37.png

5.1 类方法 isKindOfClass 探索

我们来探索底层实现,首先在这儿打断点 Xnip2022-07-06_09-23-11.png

开启 Always Show Disassembly Xnip2022-07-06_09-24-26.png

运行项目: Xnip2022-07-06_09-32-18.png

通过断点调试,我们发现 isKindOfClass 底层调用的是 objc_opt_isKindOfClass。既然知道了调用的哪个方法,我们直接找到它:

BOOL objc_opt_isKindOfClass(id obj, Class otherClass)
{
#if __OBJC2__
    // obj为空的是小概率事件基本不会发生
    if (slowpath(!obj)) return NO;

    // 获取 obj 的 isa
    // 如果 obj 是实例对象,则 cls 就是类
    // 如果 obj 是类,则 cls 就是元类
    Class cls = obj->getIsa();
    
    // fastpath(!cls->hasCustomCore()) (类或者父类中大概率没有默认的isKindOfClass方法
    if (fastpath(!cls->hasCustomCore())) {
        // 先用 isa 返回的 cls 去比较,相等 return yes
        // 不相等 再循环获取 cls 的 superClass 去比较
        for (Class tcls = cls; tcls; tcls = tcls->getSuperclass()) {
            if (tcls == otherClass) return YES;
        }
        return NO;
    }
#endif
// 非OBJC2版本直接走消息转发
    return ((BOOL(*)(id, SEL, Class))objc_msgSend)(obj, @selector(isKindOfClass:), otherClass);
}

现在用的基本都是 OBJC2 版本。obj->getIsa() 获取 或者元类obj对象就获取obj就获取元类 然后就接着for循环,先用 getIisa 返回的 cls 去比较,相等就 return yes;不相等循环 获取 clssuperClass 去比较

objc_opt_class 底层实现:

Class objc_opt_class(id obj)
{
#if __OBJC2__
    // obj为空的是小概率事件基本不会发生
    if (slowpath(!obj)) return nil;
    Class cls = obj->getIsa();
    // 类或者父类中大概率没有默认的class方法
    if (fastpath(!cls->hasCustomCore())) {
        // 如果 cls 是元类就返回 obj,非元类就返回 cls
        return cls->isMetaClass() ? obj : cls;
    }
#endif
    return ((Class(*)(id, SEL))objc_msgSend)(obj, @selector(class));
}

objc_opt_class 其实就是获取 ,如果参数是 对象 则返回 ,如果是 就返回

5.2 对象方法 isKindOfClass 探索

断点发现,对象方法 isKindOfClass 在底层也是调用的 objc_opt_isKindOfClass

Xnip2022-07-06_10-28-40.png 这里就不重复分析了,参考 5.1

5.3 类方法 isMemberOfClass 探索

打断点: Xnip2022-07-06_10-17-59.png

运行项目: Xnip2022-07-06_10-21-05.png

发现这里是直接调用了 isMemberOfClass,找到这个方法实现:

+ (BOOL)isMemberOfClass:(Class)cls {
    // 获取自己的 isa,这里是类方法,isa 返回的肯定是元类
    // 用自己的元类 和 cls 比较
    return self->ISA() == cls;
}

直接获取元类 vs cls(需要比较的类),相同就返回YES,否则返回NO

5.4 对象方法 isMemberOfClass 探索

The breakpoint found that the object method is also called directly isMemberOfClass,Xnip2022-07-06_10-34-09.png

Find the implementation of this method:

- (BOOL)isMemberOfClass:(Class)cls {
    return [self class] == cls;
}

 vs  cls(classes that need to be compared), return if they are the same YES, otherwise returnNO

Summarize:

  • + isKindOfClassprocess. Class 元类vs cls(the class that needs to be compared), the difference continues to compare. Metaclass 父类vs cls, diff Continue to compare until found 根元类. 根元类vs cls, the difference continues to compare. 根类(NSObject)vs cls, if it is not the same, 根类(NSObject)the parent class of is nil, jump out of the loop and returnNO

  • - isKindOfClassprocess. Get the class to which the current object belongs, vs cls, the difference continues to compare. Class 父类vs cls, diff Continue to compare until found 根类(NSObject). 根类(NSObject)vs cls, if it is not the same, 根类(NSObject)the parent class is nil, jump out of the loop and returnNO

  • + isMemberOfClass process. Class 元类 vs  cls(classes that need to be compared), return if they are the same YES, otherwise returnNO

  • - isMemberOfClass process.  vs  cls(classes that need to be compared), return if they are the same YES, otherwise returnNO

Guess you like

Origin juejin.im/post/7117084846668644382