IOS底层原理之类原理补充分析

1,898 阅读14分钟

前言

IOS底层原理之类结构分析类结构进行了整体大概流程的分析。今天我们对类结构进行些补充,以及过程中出现的疑问进行说明,所以知识点比较散。

准备工作

WWDC 2020 - 类优化

WWDC的重要性想必大家都知道,现在就类的优化这一块内容和大家一起分享一下,具体的链接已经给到大家,有兴趣的可以去看看

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是类数据被分成两部分的主要原因

dirty memory要比clean memory昂贵的多,只要进行运行它就必须一直存在,通过分离出那些不会被改变的数据,可以把大部分的类数据存储为clean memory,这是苹果追求的

class_rw_t 优化

当一个类首次被使用时,runtime会为它分配额外的存储容量,运行时分配的存储容量就是class_rw_tclass_rw_t用于读取-编写数据,在这个数据结构中存储的是只有运行时才会生成的新数据 图解如下

image.png

所有的都会链接成一个树状结构这是通过firstSubclassnextSiblingClass指针实现的,这样运行时会遍历当前使用的所有类

问题:为什么方法属性class_ro_t中时,class_rw_t还要有方法属性呢?

  • 因为它们可以在运行时进行更改
  • category被加载时,它可以向类中添加新的方法
  • 通过runtime API手动向类中添加属性方法
  • class_ro_t 是只读的,所以我们需要在class_rw_t中来跟踪这些东西

问题:class_rw_t结构在苹果手机中,占用很多的内存,那么如何去缩小这些结构呢?

  • 我们在读取—编写部分需要这些东西,因为它们在运行时可以被修改,但是大约10%的类是需要修改它们的方法
  • 而且只有在swift中才会使用这个demangledName字段,但是swift类并不需要这个字段,除非是访问它们Objective-C名称时才需要。

因此我们可以拆除那些我们平时不常用的部分。图解如下

image.png

结果:这样class_rw_t的大小会减少一半

对那些需要修改内存的,需要额外信息的类,我们可以分配这些扩展记录中的一个,并把它滑到类中供其使用。图解如下

image.png

总结

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

类方法的探索方式

类比猜想

  • 对象方法也就是实例方法是存放在类中的,那么相对于元类来说就是类对象。在对象中的类方法,就相当于是类对象中的实例方法类对象的实例方法应该存放在元类
  • 在自定义的类中添加方法名相同的实例方法类方法,编译运行不会报错。如果都是放在类中,那么编译器肯定会出问题,因为识别不了哪个是我需要的方法,我们猜想类方法不在类中,那么可能在元类
  • 有人可能觉着这种方法不靠谱,但是很多伟大的发现都是靠猜想+验证的方式得出来的哦

通过这种类比猜想+lldb验证的方式。探究出原来类方法确实是在元类中,lldb验证结果如下 image.png

runtime API

runtime的方式是最暴力也是最直接的,下面提供几种runtime验证方式

通过类的方法列表获取方法名

@interface LWPerson : NSObject
- (void)sayHello;
+ (void)sayHelloword;
@end

@implementation LWPerson
- (void)sayHello { NSLog(@"sayHello");}
+ (void)sayHelloword{NSLog(@"sayHelloword");}
@end

int main(int argc, char * argv[]) {
    @autoreleasepool {
    objc_copyMethodList(LWPerson.class);
    
    //获取元类
    Class pMetaClass = object_getClass(LWPerson.class);
    objc_copyMethodList(pMetaClass);
    
    }
    return 0;
}

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----- name: %@", key);
    }
    free(methods);
}
2021-06-19 21:54:10.953898+0800 testClass[10554:463640] Method----- name: sayHello
2021-06-19 21:54:10.953980+0800 testClass[10554:463640] Method----- name: sayHelloword

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

方法名判断

int main(int argc, char * argv[]) {
    @autoreleasepool {
    objc_copyMethodList(LWPerson.class);
  
    objc_MethodClass(LWPerson.class);
    
    }
    return 0;
}

void objc_MethodClass(Class pClass){
    const char * className = class_getName(pClass);
    //获取元类
    Class metaClass = objc_getMetaClass(className);
    //判断类中是否有sayHello方法
    Method method1 = class_getInstanceMethod(pClass, @selector(sayHello));
    //判断元类中是否有sayHello方法
    Method method2 = class_getInstanceMethod(metaClass, @selector(sayHello));
    //判断类中是否有sayHelloword方法
    Method method3 = class_getInstanceMethod(pClass, @selector(sayHelloword));
    //判断元类中是否有sayHelloword方法
    Method method4 = class_getInstanceMethod(metaClass, @selector(sayHelloword));
 
    NSLog(@"%p-%p-%p-%p",method1,method2,method3,method4);
}
2021-06-19 22:27:48.240079+0800 testClass[11096:489432] 0x100008110-0x0-0x0-0x1000080a8

sayHello 方法是在中,sayHelloword元类

总结

  • 实例方法在类中,类方法元类
  • 编译器自动生成元类,目的是存放类方法

变量及编码

成员变量和实例变量

  • Objective-C中写在类声明的大括号中的变量称之为成员变量,例如int aNSObject *obj
  • 成员变量用于类内部,无需与外界接触的变量

实例变量

  • 变量的数据类型不是基本数据类型且是一个则称这个变量为实例变量,例如 NSObject *obj
  • 成员变量包含实例变量

成员变量和属性的区别

  • 成员变量:在底层没有其他操作只是变量的声明
  • 属性:系统会自动在底层添加了_属性名变量,同时生成settergetter方法

IOS 底层原理之对象的本质&isa关联类 已经对属性和变量进行了底层的探究

编码

SELIMP关系

  • SEL:方法编号
  • IMP:函数指针地址
  • SEL 相当于书本的目录名称
  • IMP 相当于书的页码

SELIMP关系图 image.png

官方类型编码

image.png

重要Objective-C 不支持long double类型,@encode(long double)返回d,和double类型的编码值一样(官方提供我就翻译下)

类型编码图的获取途径:打开xcode--> command+shift+0--> 搜索ivar_getTypeEncoding--> 点击Type Encodings

代码实现类型编码

我们可以通过编译器指令@encode()来获取一个给定类型的编码字符串,下表列举了各种类型的类型编码

 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));
2021-06-20 00:06:15.731450+0800 testClass[12583:564098] char --> c
2021-06-20 00:06:15.731509+0800 testClass[12583:564098] int --> i
2021-06-20 00:06:15.731537+0800 testClass[12583:564098] short --> s
2021-06-20 00:06:15.731560+0800 testClass[12583:564098] long --> q
2021-06-20 00:06:15.731581+0800 testClass[12583:564098] long long --> q

@encode() 获取一个给定类型的编码字符串,不用记,用到直接打印或者按图查找

setter方法底层实现方式

在探究属性成员变量的区别时,发现属性setter方法有的是通过objc_setProperty实现的,有的是直接内存偏移获取变量地址,然后赋值

objc_setProperty内存偏移

首先定义一个LWPerson类,自定义属性和成员变量,生成.cpp文件。代码如下

@interface LWPerson : NSObject
{
    NSString * newName;
    NSObject * objc;
}

@property(nonatomic,  copy)NSString * LWName;
@property(nonatomic,strong)NSString * LWNickname;
@property(nonatomic,assign)NSInteger age;
@end

@implementation LWPerson

@end

查看生成.cpp文件。代码如下 image.png

LWName属性底层是通过objc_setProperty实现的,LWNicknameage是通过内存偏移实现的

settergetter方法在编译期函数地址就已经确定,怎么确定是编译期呢?查看可执行文件的函数表

image.png

既然在编译期就已经确定,objc_setProperty只能通过LLVM源码查看,在LLVM源码中全局搜索objc_setProperty,然后查询重要信息

image.png

CGM.CreateRuntimeFunction(FTy, "objc_setProperty"),创建了objc_setProperty方法。为什么在getSetPropertyFn()创建呢?由下层往上层推理,全局搜索getSetPropertyFn()

image.png

调用getSetPropertyFn()的中间层是GetPropertySetFunction(),因为需要判断是否走getSetPropertyFn(),加一个中间层过度。全局搜索GetPropertySetFunction()

image.png

根据switch条件PropertyImplStrategy类型调用GetPropertySetFunction()PropertyImplStrategy类型有两种GetSetProperty或者SetPropertyAndExpressionGet,下一步只要知道什么时候给策略赋值

image.png

image.png

copy修饰的属性使用objc_setProperty方式实现 。retain修饰的属性也是使用objc_setProperty方式实现。但是retain一般在MRC模式下使用,现在使用的基本是在ARC模式,retain这种修饰暂时忽略。通过实例再次确认

@interface LWPerson : NSObject
@property(nonatomic,  copy)NSString * LWNameA;
@property(atomic,     copy)NSString * LWNameB;
@property(atomic          )NSString * LWNameC;
@property(nonatomic       )NSString * LWNameD;
@end

@implementation LWPerson

@end

查看生成.cpp文件。代码如下

image.png

LWNameALWNameB使用的objc_setProperty方式实现,其它的属性通过内存偏移实现赋值

objc_setProperty函数的实现在哪里?objc4-818.2 全局搜索objc_setProperty

image.png

image.png

reallySetProperty的源码实现,其原理就是新值retain,旧值release

总结

  • copy修饰的属性使用objc_setProperty方式实现,其它属性使用内存偏移实现
  • 苹果没有把所有的setter方法全部写在底层,因为如果底层需要维护,修改起来特别麻烦。搞了个适配器中间层,中间层的作用是供上层的setter调用,中间层对属性的修饰符进行判断走不同的流程,调用底层的方法实现
  • 中间层的优点:底层变化上层不受影响上层变化底层也不会受影响

层级关系图 image.png

getter方法底层实现方式

在探究属性成员变量的区别时,发现属性getter方法,基本上通过内存偏移获取变量地址获取值。 只有少数条件下才通过objc_getProperty方式实现

objc_getProperty内存偏移

LLVM源码中全局搜索objc_getProperty

image.png

CGM.CreateRuntimeFunction(FTy, "objc_getProperty"),创建了objc_getProperty方法。为什么在getGetPropertyFn()创建呢?由下层往上层推理,全局搜索getGetPropertyFn()

image.png

调用getGetPropertyFn()的中间层是GetPropertyGetFunction(),因为需要判断是否走getSetPropertyFn(),加一个中间层过度。全局搜索GetPropertyGetFunction()

image.png

根据switch条件PropertyImplStrategy类型调用GetPropertyGetFunction()PropertyImplStrategy类型是GetSetProperty,下一步只要知道什么时候给策略赋值 image.png

image.png

image.png

copy+atomic修饰的属性使用objc_getProperty方式实现 。retain+ atomic修饰的属性也是使用objc_getProperty方式实现。但是retain一般在MRC模式下使用,现在使用的基本是在ARC模式,retain这种修饰暂时忽略。通过实例再次确认(但是还没找到方法调用和atomic之间关联的地方,后面继续在找LLVM寻找)

@interface LWPerson : NSObject
@property(nonatomic,  copy)NSString * nonatomicName;
@property(atomic,     copy)NSString * atomicName;
@property(nonatomic,  retain)NSObject * retainStr;
@property(atomic,     retain)NSObject * retainAtomicStr;
@end

@implementation LWPerson

@end

查看生成.cpp文件。代码如下

image.png

  • atomicNameretainAtomicStr是通过objc_getProperty方式实现,其它的属性通过内存偏移获取值
  • atomicNamenonatomicName的区别在于前者atomic修饰的,后者是nonatomic修饰的
  • LLVM中的代码显示如果你想要通过objc_getProperty方法实现, ARC下属性的修饰符必须是atomic+ copyMRC下属性的修饰符必须是atomic + retain
  • retain修饰的属性setter方法实现是通过objc_getProperty方式

objc_getProperty底层实现以及层级图objc_setProperty的类似,在这省略。有兴趣的可以去探究下

总结

  • ARCatomic+ copy修饰的属性使用objc_getProperty方式实现,其它属性使用内存偏移实现
  • MRCatomic+ retain修饰的属性使用objc_getProperty方式实现,其它属性使用内存偏移实现

总结

类越探究越多,越探究越细。总是有探究不完的感觉,但是探究过程中许多知识能连接起来了,豁然开朗的感觉。再接再厉,继续加油。

补充

objc_object objc_class 对象

  • objc_class 继承 objc_object
  • 对象的底层实现都是以objc_object为模板创建的, 对象objc_object不是继承关系

面试题

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

BOOL reg1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
BOOL reg2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
BOOL reg3 = [(id)[LWPerson class] isKindOfClass:[LWPerson class]];
BOOL reg4 = [(id)[LWPerson class] isMemberOfClass:[LWPerson class]];
NSLog(@" reg1 :%hhd reg2 :%hhd reg3 :%hhd reg4 :%hhd",reg1,reg2,reg3,reg4);

BOOL reg5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]];
BOOL reg6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]];
BOOL reg7 = [(id)[LWPerson alloc] isKindOfClass:[LWPerson class]];
BOOL reg8 = [(id)[LWPerson alloc] isMemberOfClass:[LWPerson class]];
NSLog(@" reg5 :%hhd reg6 :%hhd reg7 :%hhd reg8 :%hhd",reg5,reg6,reg7,reg8);
2021-06-20 15:41:30.839265+0800 KCObjcBuild[3949:139738]  reg1 :1 reg2 :0 reg3 :0 reg4 :0
2021-06-20 15:41:30.839826+0800 KCObjcBuild[3949:139738]  reg5 :1 reg6 :1 reg7 :1 reg8 :1

Xcode中把macOS的版本调到10.15以下,或者iOS的版本13.0以下。查看下汇编代码

image.png

源码分析:isKindOfClassisMemberOfClass底层的实现都是objc_msgSend消息转发。通过SEL找到对应的IMP

Xcode中把macOS的版本调到10.15以上,或者iOS的版本13.0以上。查看下汇编代码

image.png

源码分析:isKindOfClass底层的实现objc_opt_isKindOfClassclass底层的实现objc_opt_class,isMemberOfClass还是走消息转发

objc4-818.2 全局搜索isKindOfClassisMemberOfClassobjc_opt_isKindOfClassobjc_opt_class

isKindOfClass 底层实现

+ (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;
}

isMemberOfClass 底层实现

+ (BOOL)isMemberOfClass:(Class)cls {
    return self->ISA() == cls;
}

- (BOOL)isMemberOfClass:(Class)cls {
    return [self class] == cls;
}
  • + isKindOfClass 流程。 类的元类 vs cls(需要比较的类),不同继续比较 。元类的父类 vs cls,不同继续比较直到找到根元类根元类 vs cls,不同继续比较。根类(NSObject) vs cls,如果还不相同则根类(NSObject)的父类为nil,跳出循环返回NO

  • - isKindOfClass 流程。获取当前对象所属类, vs cls,不同继续比较 。类的父类 vs cls,不同继续比较直到找到根类(NSObject)根类(NSObject) vs cls,如果还不相同则根类(NSObject)的父类为nil,跳出循环返回NO

  • + isMemberOfClass 流程。 类的元类 vs cls(需要比较的类),相同就返回YES,否则返回NO

  • - isMemberOfClass 流程。 vs cls(需要比较的类),相同就返回YES,否则返回NO

objc_opt_isKindOfClass 底层实现

OBJC_EXPORT BOOL
objc_opt_isKindOfClass(id _Nullable obj, Class _Nullable cls)
	OBJC_AVAILABLE(10.15, 13.0, 13.0, 6.0, 5.0);

isKindOfClass的底层实现objc_opt_isKindOfClass支持的版本masOS大于10.15iOS大于13.0

// Calls [obj isKindOfClass]
BOOL
objc_opt_isKindOfClass(id obj, Class otherClass)
{
#if __OBJC2__ //现在基本上都用OBJC2版本
    //slowpath(!obj)  obj为空的是小概率事件基本不会发生
    if (slowpath(!obj)) return NO;
    // 获取类或者是元类:obj是对象就获取类,如果obj是类就获取元类
    Class cls = obj->getIsa();
    //fastpath(!cls->hasCustomCore()) (类或者父类中大概率没有默认的isKindOfClass方法)
    if (fastpath(!cls->hasCustomCore())) {
        for (Class tcls = cls; tcls; tcls = tcls->getSuperclass()) {
            if (tcls == otherClass) return YES;
        }
        return NO;
    }
#endif //OBJC版本直接走消息转发
    return ((BOOL(*)(id, SEL, Class))objc_msgSend)(obj, @selector(isKindOfClass:), otherClass);
}

基本上都用OBJC2版本,obj->getIsa()获取或者元类obj对象就获取obj就获取元类 然后就接着for循环,for里面的代码和isKindOfClass逻辑一样就不进行分析了,可以直接看 isKindOfClass的分析

objc_opt_class 底层实现

OBJC_EXPORT Class _Nullable
objc_opt_class(id _Nullable obj)
	OBJC_AVAILABLE(10.15, 13.0, 13.0, 6.0, 5.0);

class的底层实现objc_opt_class支持的版本masOS大于10.15iOS大于13.0

// Calls [obj class]
Class
objc_opt_class(id obj)
{
#if __OBJC2__
    if (slowpath(!obj)) return nil;
    // 获取类或者是元类:obj是对象就获取类,如果obj是类就获取元类
    Class cls = obj->getIsa();
    //(类或者父类中大概率没有默认的class方法)
    if (fastpath(!cls->hasCustomCore())) {
        //cls是类 返回cls,如果cls是元类,obj是类,返回obj还是类
        return cls->isMetaClass() ? obj : cls;
    }
#endif
    return ((Class(*)(id, SEL))objc_msgSend)(obj, @selector(class));
}

源码分析:objc_opt_class 的实现,其实就是获取,如果参数是对象则返回,如果是就返回

验证面试题

  • reg1: 比较的是 NSObject vs NSObject,返回 1
  • reg2: 比较的是 根元类 vs NSObject,返回 0
  • reg3: 比较的是 NSObject vs LWPerson,返回 0
  • reg4: 比较的是 LWPerson元类 vs LWPerson,返回 0
  • reg5: 比较的是 NSObject vs NSObject,返回 1
  • reg6: 比较的是 NSObject vs NSObject,返回 1
  • reg7: 比较的是 LWPerson vs LWPerson,返回 1
  • reg8: 比较的是 LWPerson vs LWPerson,返回 1

总结

  • + isKindOfClass方法:元类-->元类的父类-->直到找到根元类-->根类(NSObject)cls分别进行比较
  • - isKindOfClass方法:-->类的父类-->直到找到根类(NSObject)cls分别进行比较
  • + isMemberOfClass方法:元类 vs cls
  • - isMemberOfClass方法: vs cls

runtime API问题

class_getClassMethod

int main(int argc, char * argv[]) {
    @autoreleasepool {
      obj_classToMetaclass(LWPerson.class);
    }
    return 0;
}
void obj_classToMetaclass(Class pClass){
    
    const char *className = class_getName(pClass);
    Class metaClass = objc_getMetaClass(className);
    // - (void)sayHello;
    Method method1 = class_getClassMethod(pClass, @selector(sayHello));
    Method method2 = class_getClassMethod(metaClass, @selector(sayHello));


    // + (void)sayHelloword;
    Method method3 = class_getClassMethod(pClass, @selector(sayHelloword));
    Method method4 = class_getClassMethod(metaClass, @selector(sayHelloword));
    
    NSLog(@"%p-%p-%p-%p",method1,method2,method3,method4);
}
2021-06-20 18:46:00.897204+0800 testClass[6574:263479] 0x0-0x0-0x1000080b8-0x1000080b8

源码分析:sayHello怎么在LWPerson类中怎么没有找到。实例方法不是在类中的吗?奇怪,是不是class_getClassMethod的问题

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就是去元类中,查找实例方法。打印结果sayHello不存在元类中,找不到。sayHelloword元类中,可以找到。在底层不存在类方法,都是按普通方法进行查找。

class_getMethodImplementation

int main(int argc, char * argv[]) {
    @autoreleasepool {
      obj_IMPClassToMetaclass(LWPerson.class);
    }
    return 0;
}
void obj_IMPClassToMetaclass(Class pClass){
    
    const char *className = class_getName(pClass);
    Class metaClass = objc_getMetaClass(className);

    IMP imp1 = class_getMethodImplementation(pClass, @selector(sayHello));
    IMP imp2 = class_getMethodImplementation(metaClass, @selector(sayHello));
    
    IMP imp3 = class_getMethodImplementation(pClass, @selector(sayHelloword));
    IMP imp4 = class_getMethodImplementation(metaClass, @selector(sayHelloword));

    NSLog(@"%p-%p-%p-%p",imp1,imp2,imp3,imp4);
}
2021-06-20 19:01:53.112778+0800 testClass[6782:274233] 0x100001a80-0x7fff69233580-0x7fff69233580-0x100001a90

源码分析:结果怎么都有函数指针地址,imp2imp3nil才对啊。探究下class_getMethodImplementation源码

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 = nil时,系统会返回_objc_msgForward,所以imp2imp3才有函数指针地址,而且地址相同