ios 底层原理 04– 深入探究类的结构

前言

上一篇文章中ios 底层原理 03–isa 的走位图&类的结构探索对类结构进行了整体大概流程的分析。今天我们对类结构进行些补充,以及过程中出现的疑问进行说明,所以知识点比较散

WWDC2020上关于类的优化

链接如下Advancements in the Objective-C runtime
先上结论:

Dirty Memory跟Clean 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的区别

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

class_rw_t 优化

当一个类第一次被创建跟使用时,runtime会为它分配额外的存储容量,运行时分配的存储容量就是class_rw_tclass_rw_t用于读-写数据,在这个数据结构中存储的是只有运行时才会生成的新数据 图片[1]-ios 底层原理 04– 深入探究类的结构-一一网所有的类都是通过firstSubclassnextSiblingClass指针实现的结构,这样运行时会遍历当前使用的所有类
问题1:从上图可以看出 rwro中都有 Methods方法Protocols协议Properties属性。这是为啥呢?

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

问题 2:class_rw_t结构在系统中,占用很多的内存,怎么优化结构?

  • 我们在rw部分需要这些东西,是因为它们在运行时可以被修改,但是大约90%的类是不需要修改的而且只有在swift中才会使用这个demangledName字段,但是swift类很多时候也不需要这个字段,除了访问它们Objective-C名称时才需要。所以可以拆除那些我们平时不常用的部分放在class_rw_ext_t里面。

总结

class_rw_t优化,其实就是对class_rw_t不常用的部分进行了剥离。如果需要用到这部分就从class_rw_ext_t(扩展记录)中分配一个,滑到类中供其使用

类方法是否在元类中

对象(实例)方法是存放在中的,这时候相对于元类来说就是类对象,在对象中的类方法==类对象实例方法,类对象实例方法应该是元类

举例子说明LGPerson *person 的减号方法 [person sayhello] 是存放在 LGPerson 中的
这时候 LGPerson 相对于LGPerson 的元类(用 XXX代替) 来说就是 XXX对象,在对象*person中的 +加号方法 init == LGPerson减号方法init ,所以 XXX 的对象减号方法 应该在XXX

在自定义的类中添加方法名相同的实例方法类方法,编译运行不会报错。如果都是放在中,那么编译器肯定会出问题,因为识别不了哪个是我需要的方法,我们猜想类方法不在类中,那么可能在元类中。在上一篇文章中Meta Class(元类)中 我们说过原理,下面我们通过代码来验证一下。

class_getInstanceMethod得到类的实例方法

编写一段代码 其中

  • sayClassMethod为类方法
  • sayInstanceMethod 为对象方法

image.png
可以看出 实例方法中有sayInstanceMethod方法 元类实例方法中有sayClassMethod方法

class_getClassMethod 得到类的类方法

图片[3]-ios 底层原理 04– 深入探究类的结构-一一网
可以看出 类方法中有sayClassMethod方法 元类类方法中有sayClassMethod方法

总结

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

关于 2个 API的问题

sayInstanceMethod怎么在LGPerson类中怎么没有找到

图片[4]-ios 底层原理 04– 深入探究类的结构-一一网
源码分析:class_getClassMethod就是去元类中,查找实例方法。打印结果sayInstanceMethod不存在元类中,找不到。sayClassMethod元类中,可以找到。在底层不存在类方法,都是按普通方法进行查找。

class_getMethodImplementation为啥会有指针地址 imp1和imp4应该是nil

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,所以imp1imp4才有函数指针地址,而且地址相同

成员变量,实例变量,属性的区别

成员变量

  • Objective-C中写在类声明的大括号中的变量称之为成员变量,例如int aNSObject *obj
  • 成员变量用于类内部,无需与外界接触的变量
@interface LGPerson : NSObject
{
    NSObject *objc; //成员变量 实例变量
    NSString *nickName;//成员变量 
}
复制代码

实例变量

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

成员变量和属性的区别

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

SEL和IMP关系

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

setter方法的底层实现

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

通过探索 llvm 可以得出

/// 对象 sel 偏移量 值 原子性 是否copy
void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy) 
{
    // 是否是 Copy
    bool copy = (shouldCopy && shouldCopy != MUTABLE_COPY);
    // 是否是 可变的Copy
    bool mutableCopy = (shouldCopy == MUTABLE_COPY);
    reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy);
}
复制代码

发现他实际调用了 reallySetProperty

static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
    if (offset == 0) { 
        // 修改 ISA
        object_setClass(self, newValue);
        return;
    }

    id oldValue;
    // 定位(首地址+偏移量)
    id *slot = (id*) ((char*)self + offset);

    // Copy相关操作
    if (copy) {
        newValue = [newValue copyWithZone:nil];
    } else if (mutableCopy) {
        newValue = [newValue mutableCopyWithZone:nil];
    } else {
        if (*slot == newValue) return;// 相同的话不做操作
        // 引用计数+1
        newValue = objc_retain(newValue); 
    }

    // 原子性相关
    if (!atomic) {
        oldValue = *slot;
        *slot = newValue;
    } else {
      // 加锁
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();
    }

    // reloase 旧值 引用计数 -1
    objc_release(oldValue);
}
复制代码

结论

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

中间层的优点:底层变化上层不受影响上层变化底层也不会受影响

getter方法的底层实现

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

通过探索 llvm 可以得出

结论

  • ARC:atomic+ copy修饰的属性使用objc_getProperty方式实现,其它属性使用内存偏移实现

  • MRC:atomic+ retain修饰的属性使用objc_getProperty方式实现,其它属性使用内存偏移实现

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享