iOS底层原理探究06-类的底层原理下

开始本节内容之前需要对了解WWDC2020中对runtime的改动,主要是对clean memorydirty memory以及rorw有个初步的了解。
实际上内存页有分类,一般来说分为 clean memorydirty memory 两种,iOS 中也有 compressed memory 的概念。

Clean memory & dirty memory

对于一般的桌面操作系统,clean memory 可以认为是能够进行 Page Out 的部分。Page Out指的是将优先级低的内存数据交换到磁盘上的操作,但iOS 并没有内存交换机制,所以对 iOS 这样的定义是不严谨的。那么对于 iOS 来说,clean memory 指的是能被重新创建的内存,它主要包含下面几类:

  • app 的二进制可执行文件
  • framework 中的 _DATA_CONST
  • 文件映射的内存
  • 未写入数据的内存

内存映射的文件指的是当 app 访问一个文件时,系统会将文件映射加载到内存中,如果文件只读,那么这部分内存就属于 clean memory。另外需要注意的是,链接的 framework_DATA_CONST 并不绝对属于 clean memory,当app 使用到 framework 时,就会变成 dirty memory

未写入数据的内存也属于 clean memory,比如下面这段代码,只有写入了的部分才属于 dirty memory

int *array = malloc(20000 * sizeof(int));
array[0] = 32
array[19999] = 64

image.png

所有不属于 clean memory 的内存都是 dirty memory。这部分内存并不能被系统重新创建,所以 dirty memory始终占据物理内存,直到物理内存不够用之后,系统便会开始清理
Compressed memory

当物理内存不够用时,iOS 会将部分物理内存压缩,在需要读写时再解压,以达到节约内存的目的。而压缩之后的内存,就是所谓的 compressed memory。苹果最开始只是在 OS X 上使用这项技术,后来也在 iOS 系统上使用。

摘自:《iOS Memory 内存详解 》这篇文章对iOS的内存进行了详细解读,感兴趣的可以去看下

WWDC2020中对runtime的改动

接下来结合WWDC2020中对runtime的改动探究类的数据结构,
在磁盘上app二进制文件中类是这样的

image.png

首先这有个类对象本身它包含了最常被访问的信息指向元类超类方法缓存指针
它还有一个指向更多数据的指针,存储额外信息的地方叫做 class_ro_t,'RO'代表只读,它包含了类名方法协议实例变量的信息,Swift类和Objective-C共享这一基础结构,所以每个Swift类也有这些数据结构。
image.png

当类第一次从磁盘加载到内存中时,它一开始也是这样的但是一经使用 它们就会发生变化,这里就要结合clean memorydirty memory来看,clean memory是指加载后不会发生变更的内存class_ro_t就属于clean memory因为它是只读的,dirty memory是指在进程运行时会发生变化的内存,类结构一经使用就会变成 dirty memory因为Runtime会向它写入新的数据,例如 创建一个新的方法缓存并从类中指向它,dirty memoryclean memory要昂贵得多,只要进程在运行 它就必须一直存在 另一方面 clean memory 可以进行移除从而节省更多的内存空间,因为如果你需要clean memory系统可以从磁盘中重新加载macOS可以选择缓冲 dirty memory 但是iOS不能使用swap(内存交换机制iOS不支持),所以dirty memory 在iOS中代价很大,dirty memory是这个类被分成两部分的原因,可以保持清洁的数据越多越好,通过分离出那些永远不会改变的数据可以把大部分数据存储为clean memory。虽然有了这些数据足以让我们开始,但是运行时需要追踪每个类的更多信息,所以当一个类首次被使用运行时会为它分配额外的容量,这个运行时分配的容量是class_rw_t用于读取-编写数据,在这个数据结构中储存了只有运行时才会生成的新信息。
image.png

例如 所有的类都会链接成一个树状结构这是通过First SubclassNext Sibling Class 指针实现的,这允许运行时遍历当前使用的所有类这对于使方法缓存无效非常有用。
image.png

但是为什么方法和属性也在只读数据中时,这里还要有方法和属性呢?因为他们可以在运行时进行改变,当category被加载时它可以向类中添加新方法,而且程序员可以使用运行时的API动态的添加它们,因为 class_ro_t是只读的,所以需要在class_rw_t中追踪这些东西,现在结果是这样做会占用相当多的内存,在任何给定的设备中都有许多类在使用,Ben(苹果工程师的名字)iPhone上的整个系统中测量了大约30兆字节这些 class_rw_t 结构,那么如何缩小这些结构呢?记住我们需要这些东西存在read/write区因为运行时它们可能会被改变,但是通过检查实际设备上的使用情况发现大约只有10%的类真正地更改了它们的方法。
image.png

而且demangled name 这个字段只有Swift类才可能使用到,而且只有当有东西访问它们的Objective-C名称时才会用到,所以可以拆掉那些平时不用的部分
image.png

这将class_rw_t的大小减少了一半,对于那些确实需要额外信息的类,我们可以分配这些扩展中的一个并把它滑到类中供其使用
image.png

大约有90%的类从来不需要这些扩展数据,这在系统范围中可节省大约14MB的内存。
实际上可以在Mac上看到这一变化带来的影响,这只需要在终端上执行一个命令,下面我们看下邮件这个程序的情况

heap Mail | egrep 'class_rw|COUNT'

image.png

可以看到邮件app中使用了大约6000个这样的class_rw_t类型,但是其中只有大约十分之一 600多一点实际上需要使用这一扩展信息,所以通过这个改变节省了很多内存。
现在很多从类中读取数据的代码都必须同时处理那些有扩展数据和没有扩展数据的类当然运行时会为我们处理这一切,并且从外部看 一切都像往常一个工作,只是使用了更少的内存。

摘自《WWDC 2020 Advancements in the Objective-C runtime》

了解完这些之后我们就可以继续类的探索了,这里有个小插曲,不知道你有没有注意到《iOS底层原理探究05-类的底层原理isa链&继承链&类的内存结构》里

image.png

firstSubclass为nil但是LGPerson明明是有子类的LGTeacher

image.png

这是为啥呢?其实原因很简单,因为LGTeacher类是懒加载的,当前还没加载
image.png

我们p一下LGTeacher之后,在输出LGPerson的数据firstSubclass已经被赋值了
下面继续探索类的数据结构,通过上面对WWDC2020的分析我们知道类的成员变量存在ro结构中,我们到ro里看下能不能找到
image.png

  • 1.获取class的data数据
  • 2.获取ro数据
  • 3.获取ro中的ivars成员变量数据
  • 4.看下成员变量输出的信息
    • name = 0x0000000100003d29 "subject"成员变量名称
    • type = 0x0000000100003eaa "@\"NSString\""成员变量类型
    • size = 8成员变量大小

有些同学可能会疑惑subject是哪儿冒出来的,它是LGPerson的一个承运变量

image.png

我们再多添加几个成员变量,研究一下

@interface LGPerson : NSObject{
    NSString *subject;
    int age;
    double height;
    NSObject * face;
}

image.png

前面的流程是一致的,后面的type里出现的i,d,@是啥意思,这些其实是类型编码。可以看下表对照也可以直接去官网看
Type Encodings

除了存储的地方不一样属性成员变量还有没有其他的区别呢,下面就来探索一下,通过xcrun编译生成C/C++源码看看在源码层面(xcrun的介绍和使用),他们有什么区别
下面我们把这个类编译一下看下底层有什么区别
//编译前

// 成员变量 vs 属性 VS 实例变量
@interface LGPerson : NSObject
{
    NSString *hobby; // 字符串
    int a;
    NSObject *objc;  // 结构体
}

@property (nonatomic, copy) NSString *nickName;
@property (nonatomic, strong) NSString *name;

@end

//编译后

...省略无关代码
#ifndef _REWRITER_typedef_LGPerson
#define _REWRITER_typedef_LGPerson
typedef struct objc_object LGPerson;
typedef struct {} _objc_exc_LGPerson;
#endif

extern "C" unsigned long OBJC_IVAR_$_LGPerson$_nickName;
extern "C" unsigned long OBJC_IVAR_$_LGPerson$_name;
struct LGPerson_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    NSString *hobby;
    int a;
    NSObject *objc;
    NSString *_nickName;
    NSString *_name;
};

// @property (nonatomic, copy) NSString *nickName;
// @property (nonatomic, strong) NSString *name;

/* @end */

// @implementation LGPerson

static NSString * _I_LGPerson_nickName(LGPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_LGPerson$_nickName)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);

static void _I_LGPerson_setNickName_(LGPerson * self, SEL _cmd, NSString *nickName) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct LGPerson, _nickName), (id)nickName, 0, 1); }

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; }
// @end
static struct /*_method_list_t*/ {
    unsigned int entsize;  // sizeof(struct _objc_method)
    unsigned int method_count;
    struct _objc_method method_list[8];
} _OBJC_$_INSTANCE_METHODS_LGPerson __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    8,
    {{(struct objc_selector *)"nickName", "@16@0:8", (void *)_I_LGPerson_nickName},
    {(struct objc_selector *)"setNickName:", "v24@0:8@16", (void *)_I_LGPerson_setNickName_},
    {(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 *)"nickName", "@16@0:8", (void *)_I_LGPerson_nickName},
    {(struct objc_selector *)"setNickName:", "v24@0:8@16", (void *)_I_LGPerson_setNickName_},
    {(struct objc_selector *)"name", "@16@0:8", (void *)_I_LGPerson_name},
    {(struct objc_selector *)"setName:", "v24@0:8@16", (void *)_I_LGPerson_setName_}}
};
...省略无关代码
  • 底层属性会被添加下划线_name变成一个以下划线开头的成员变量
  • 系统自动给属性生成了gettersetter方法
  • 属性 = 承运变量 + getter + setter
  • 最下面方法列表中有的东西还需要看一下例如{{(struct objc_selector *)"nickName", "@16@0:8", (void *)_I_LGPerson_nickName},,前面的nickName很明显是方法名也就是nickNamegetter的方法名,但是后面的@16@0:8这个怎么理解呢?
    • "@16@0:8"解析
      • 1:@ 表示返回值的类型编码即返回值是id类型
      • 2:16 表示整个这一串所占用的内存
      • 3:@ 表示第一个参数为id类型
      • 4: 0 表示从0号位置开始
      • 5::表示第二个参数类型SEL
      • 6:8表示从8号位置开始

这样ivar和property的区别就比较清晰了,但是有一个细节不知道你注意到了没nickNamesetter方法

static void _I_LGPerson_setNickName_(LGPerson * self, SEL _cmd, NSString *nickName) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct LGPerson, _nickName), (id)nickName, 0, 1); }

namesetter方法

static void _I_LGPerson_setName_(LGPerson * self, SEL _cmd, NSString *name) { (*(NSString **)((char *)self + OBJC_IVAR_$_LGPerson$_name)) = name; }
  • namesetter方法使用内存偏移赋值
  • nickNamesetter方法则是直接调用objc_setProperty
    这两个属性在OC中我们声明的时候有什么区别呢?
@property (nonatomic, copy) NSString *nickName;
@property (nonatomic, strong) NSString *name;

nickName使用的是copy,而name是用的strong是否是因为这个导致底层setter发生变化的呢。接下来我们再来研究下,因为属性getter 和 setter方法都是编译器自动生成的,所以我们到llvm源码中看看能不能找到答案。
关于llvm我们后面再补充(我网速有点慢,源码还没下下来)

objc_setProperty做了啥,objc_setProperty中最终都是调用reallySetProperty

image.png

来看下reallySetProperty的源码
reallySetProperty源码

reallySetProperty实质是深拷贝的流程,会重新生成一个字符串对象并指向这个新的字符串,直接地址偏移赋值直接改原来的对象并没有这个深拷贝的操作

解决上一篇遗留的问题

在《iOS底层原理探究05》中我们留下了一个问题,类方法存在哪里?下面我来把这个坑填上
其实类方法是存在元类中的,我直接这么说你肯定不信,那么下面我们就来验证一下,上代码

#import 

NS_ASSUME_NONNULL_BEGIN

@interface LGPerson : NSObject
- (void)sayNB;
+ (void)say666;
@end

@implementation LGPerson
- (void)sayNB{
    
}
+ (void)say666{
    
}

实例方法sayNB是在LGPerson类里保存的这个在《iOS底层原理探究05》里已经验证过了,接下来就看看say666方法是不是在LGPerson 元类

image.png

控制台输出是这样的接下来我添加上备注信息你就明白了

(lldb) x/4gx LGPerson.class    //打印LGPerson类的内存结构
0x100008778: 0x00000001000087a0 0x000000010036c140
0x100008788: 0x0000000100649dd0 0x0002a04000000003
(lldb) p/x 0x00000001000087a0 & 0x00007ffffffffff8ULL    //LGPerson类的isa & ISA_MASK获取到LGPerson元类
(unsigned long long) $1 = 0x00000001000087a0
(lldb) po 0x00000001000087a0
LGPerson    //验证一下 获取到的确实是LGPerson元类

(lldb) p/x 0x00000001000087a0+0x20    //内存偏移找到元类bits数据结构 这在上一篇中讲到过
(long) $3 = 0x00000001000087c0
(lldb) p (class_data_bits_t *)0x00000001000087c0 //   告诉lldb bits是class_data_bits_t类型的
(class_data_bits_t *) $4 = 0x00000001000087c0
(lldb) p $4->data()    //取出元类LGPerson的data
(class_rw_t *) $5 = 0x0000000100612780
(lldb) p *$5   //查看data的数据结构
(class_rw_t) $6 = {
  flags = 2684878849
  witness = 1
  ro_or_rw_ext = {
    std::__1::atomic = {
      Value = 4301561681
    }
  }
  firstSubclass = nil
  nextSiblingClass = 0x00007fff800fcec0
}
(lldb) p $6->methods()    //获取到元类LGPerson的方法列表
(const method_array_t) $7 = {
  list_array_tt = {
     = {
      list = {
        ptr = 0x00000001000086b0
      }
      arrayAndFlag = 4295001776
    }
  }
}
  Fix-it applied, fixed expression was: 
    $6.methods()
(lldb) p $7.list    //取list
(const method_list_t_authed_ptr) $8 = {
  ptr = 0x00000001000086b0
}
(lldb) p $8.ptr    //取prt
(method_list_t *const) $9 = 0x00000001000086b0
(lldb) p *$9  //查看method_list_t的数据结构
(method_list_t) $10 = {
  entsize_list_tt = (entsizeAndFlags = 27, count = 1)
}
(lldb) p $10.get(0).big()    //取方法列表中的第一个方法 获取到了 say666方法
(method_t::big) $11 = {
  name = "say666"
  types = 0x0000000100003ea2 "v16@0:8"
  imp = 0x0000000100003ba0 (KCObjcBuild`+[LGPerson say666])
}
(lldb) 

添加完注释流程还是蛮清晰的,验证了类方法确实存在元类

结论

实例方法保存在methods数据结构里,类方法实例方法的形式保存在元类中,本质上不存在类方法,类方法是存在元类中的实例方法,底层都是函数

使用Runtime API 验证方法存储位置

接下来我们使用runtimeAPI尝试打印LGPerson类的保存的所有方法

#import 
NS_ASSUME_NONNULL_BEGIN

@interface LGPerson : NSObject
{
    NSObject *objc; 
    NSString *nickName;
}
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) NSObject *obj;

- (void)sayHello;
+ (void)sayHappy;
@end
NS_ASSUME_NONNULL_END

下面是打印方法的代码

void lgObjc_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));
        
        LGLog(@"Method, name: %@", key);
    }
    free(methods);
}

运行结果

image.png

打印的结果中sayHappy并不在其中,因为我们知道它是类方法保存在元类
image.png

输出元类的方法列表打印出了sayHappy

- (void)sayHello;
+ (void)sayHappy;

我们用这三个方法检验一下
第一个方法验证sayHellosayHappy是否是元类实例方法
第一个方法验证sayHellosayHappy是否是元类类方法
第一个方法验证sayHellosayHappy是否在元类中有IMP实现

void lgInstanceMethod_classToMetaclass(Class pClass){
    
    const char *className = class_getName(pClass);
    Class metaClass = objc_getMetaClass(className);
    
    Method method1 = class_getInstanceMethod(pClass, @selector(sayHello));
    Method method2 = class_getInstanceMethod(metaClass, @selector(sayHello));

    Method method3 = class_getInstanceMethod(pClass, @selector(sayHappy));
    Method method4 = class_getInstanceMethod(metaClass, @selector(sayHappy));
    
    LGLog(@"%s - %p-%p-%p-%p",__func__,method1,method2,method3,method4);
}

void lgClassMethod_classToMetaclass(Class pClass){
    
    const char *className = class_getName(pClass);
    Class metaClass = objc_getMetaClass(className);
    
    Method method1 = class_getClassMethod(pClass, @selector(sayHello));
    Method method2 = class_getClassMethod(metaClass, @selector(sayHello));

//    - (void)sayHello;
//    + (void)sayHappy;
    Method method3 = class_getClassMethod(pClass, @selector(sayHappy));
    Method method4 = class_getClassMethod(metaClass, @selector(sayHappy));
    
    LGLog(@"%s-%p-%p-%p-%p",__func__,method1,method2,method3,method4);
}

void lgIMP_classToMetaclass(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));// 0
    // sel -> imp 方法的查找流程 imp_farw
    IMP imp3 = class_getMethodImplementation(pClass, @selector(sayHappy)); // 0
    IMP imp4 = class_getMethodImplementation(metaClass, @selector(sayHappy));

    NSLog(@"%p-%p-%p-%p",imp1,imp2,imp3,imp4);
    NSLog(@"%s",__func__);

}
image.png
  • lgInstanceMethod_classToMetaclass输出为0x1000081c0-0x0-0x0-0x100008158,-(void)sayHello是类的实例方法+(void)sayHappy不是元类的实例方法+(void)sayHappy不是类的实例方法+(void)sayHappy是元类的实例方法
  • lgClassMethod_classToMetaclass输出为0x0-0x0-0x100008158-0x100008158,-(void)sayHello不是类的类方法-(void)sayHello不是元类的类方法+(void)sayHappy是类的类方法+(void)sayHappy是元类的类方法
  • lgIMP_classToMetaclass输出为0x100003ac0-0x7fff202295c0-0x7fff202295c0-0x100003b00,-(void)sayHello在类中能找到IMP实现,-(void)sayHello在元类中能找到IMP实现,+(void)sayHappy在类中能找到IMP实现,+(void)sayHappy在元类中能找到IMP实现。

分析:第一点毋庸置疑跟我们之前的结论是一致的,但是第二点有点问题class_getClassMethod(metaClass, @selector(sayHello))有结果输出,+(void)sayHappy怎么是元类类方法呢,不是说类方法实例方法的形式存在元类中接下来我们来看下class_getClassMethod源码

/***********************************************************************
* class_getClassMethod.  Return the class method for the specified
* class and selector.
**********************************************************************/
Method class_getClassMethod(Class cls, SEL sel)
{
    if (!cls  ||  !sel) return nil;

    return class_getInstanceMethod(cls->getMeta(), sel);
}

可以看到源码的实现跟我们之前得到的结论是一致的,获取类方法的实现就是去元类里找同名的实例方法,那我们现在传进来的就是元类,通过cls->getMeta()获取元类又会得到什么呢,我们来看下cls->getMeta()的源码

// NOT identical to this->ISA when this is a metaclass
    Class getMeta() {
        if (isMetaClassMaybeUnrealized()) return (Class)this;
        else return this->ISA();
    }

如果当前传入的类元类直接返回当前类,如果不是元类返回当前类的isa,所以class_getClassMethod(metaClass, @selector(sayHello)),其实底层是找metaClass实例方法,当然可以找到。
第三点-(void)sayHello在类中能找到IMP实现和+(void)sayHappy在元类中能找到IMP实现这两个没啥问题 但是 -(void)sayHello在元类中能找到IMP实现和+(void)sayHappy在类中能找到IMP实现这两个就不正常了,而且不知道你注意到没他们两个的IMP实现地址是一样的

image.png

这是什么原因呢?
我们在看下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 version
    if (!imp) {
        return _objc_msgForward;
    }

    return imp;
}

查找IMP的过程就是通过SEL查找IMP的过程,也就是我们常说的方法查找流程,不知道你有没有注意到这一句代码

if (!imp) {
        return _objc_msgForward;
    }

如果找不到SEL对应的IMP就返回_objc_msgForward,这是个啥玩意儿呢?欢迎来到下一部分方法查找流程

你可能感兴趣的:(iOS底层原理探究06-类的底层原理下)