iOS 底层探索05——iOS类的结构分析(下)

这是我参与更文挑战的第7天,活动详情查看: 更文挑战

类的结构

在iOS开发中几乎每一个对象都是的实例,之前我们已经分析过一部分的结构, 主要包括以下几项内容:

  1. Class
  2. MetaClass
  3. isa
  4. SuperClass
  5. property
  6. instance method
  7. ivar
  8. class method

有兴趣的朋友可以看上一篇文章iOS 底层探索04——iOS类的结构分析(上),这篇文章将在上一篇的基础上从一个更高的维度来探究一下关于相关的知识;

clean Memory & dirty memory

参考视频资料

Advancements in the Objective-C runtime – WWDC 2020 – Videos – Apple Developer

iOS Memory 内存详解

iOS中clean Memory/dirty Memory

虚拟内存简述

我们都知道iOS系统和绝大多数操作系统都采用虚拟内存技术,通过映射关系将虚拟内存和物理内存甚至硬盘进行一一映射;为了方便映射和管理,虚拟内存和物理内存都被分割成相同大小的单位,物理内存的最小单位被称为帧(Frame),虚拟内存的最小单位被称为页(Page)。在物理内存不足时通过 Page Out来进行释放,需要使用的时候再从硬盘加载到物理内存,一旦释放再从CPU页表中查找失败的时候,就会触发page fault 并将这部分释放掉的数据从硬盘中重新加载到内存;

clean Memory

虽然page fault 因为重新加载到内存会影响性能,但是至少可以缓解一部分物理内存的压力;这部分可以从硬盘中重新加载的内存数据我们称为Clean Memory,主要包括:

  1. 系统framework
  2. 应用的二进制执行文件.
  3. 内存数据映射到文件。

相较于Dirty Memory操作系统更希望数据是Clean Memory;

dirty memory

如果数据是运行时候产生的,一旦释放就很难再次恢复,这部分不能从硬盘中重新加载的数据我们称为Dirty Memory;操作系统希望这部分数据越少越好;

Class被首次加载到内存

当类第⼀次从磁盘加载到内存时,它的结构如下图
001.jpg

Class被runtime使用

当一个类首次被通过runtime使用时,由于系统可能会对他进行以下操作
通过runtime进行更改;
通过 category向类中添加新的方法;
通过runtime API手动向类中添加属性和方法
class_ro_t 是只读的,所以我们需要在class_rw_t中来储存这些信息,
Class被runtime使用后,它的结构如下图

002.jpg

Class 被动态更新

当一个类需要动态更新时,会将动态更新的部分提取出来,存⼊class_rw_ext_t,它的结构如下图

003.jpg

类的整体结构

class的结构演变图如下
004.jpg
class的结构关系图如下

005.png

firstSubclass

所有的class都会通过firstSubclass(首个子类)和nextSiblingClass(兄弟类)指针链接,这样运行时会遍历当前使用的所有类来进行处理,但要注意class是懒加载的,只有使用过对应的firstSubclass类,他的里面才会有值;

类的结构

属性 & 成员变量 & 方法

005.jpg

  1. 为了观察成员变量、属性、实例变量的区别,最好的办法就是使用clang将其重写为c++;代码如下

clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-14.2.0 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.2.sdk GCPerson.m -o GCPerson.cpp

重写后的代码如下图

image.png

image.png
2. 通过关键字搜索后,观察成员变量和属性的区别,发现底层都是成员变量,只是属性的成员变量多了下划线_,属性添加了set方法和get方法;2如图所示
3. 主要有:

  • 成员变量:基本数据类型的成员变量;
  • 实例变量:对象类型的成员变量 (对象类型的)
  • 属性:带下划线成员变量 + setter方法 + getter ⽅法
  • 方法
  1. method 在objc中的结构如下
    struct big {
        SEL name;
        const char *types;
        MethodListIMP imp;
    };
复制代码
  1. 在观察方法时,我们发现一些奇怪的字符,这些都是成员变量的类型编码,通过command + shift + 0 跳转文档,搜索ivar_getTypeEncoding(),跳转到官网

image.png
看到如下所示的编码信息
Objective-C type encodings

Code Meaning
c A char
i An int
s A short
l A longl is treated as a 32-bit quantity on 64-bit programs.
q A long long
C An unsigned char
I An unsigned int
S An unsigned short
L An unsigned long
Q An unsigned long long
f A float
d A double
B A C++ bool or a C99 _Bool
v A void
* A character string (char *)
@ An object (whether statically typed or typed id)
# A class object (Class)
: A method selector (SEL)
[array type] An array
{name=type…} A structure
(name=type…) A union
bnum A bit field of num bits
^type A pointer to type
? An unknown type (among other things, this code is used for function pointers)
  1. 方法中还有一些数字相关的信息,例如@16@0:8

    • @:id返回值类型;
    • 16:所有入参数占用的内存之和;
    • @:第一个参数的类型;
    • 0:第一个参数的起始位置;
    • ::第二个参数的类型;
    • 8:第二个参数的起始位置;
  2. 编码的格式如下:

[返回值类型][入参的总size][第一个参数类型][第一个参数的起始位置][第二个参数类型][第二个参数的起始位置][第三个参数类型][第三个参数的起始位置]....[第n个参数类型][第n个参数起始位置]依次向后输出所有的参数;

  1. 还可以使用以下方法objc_copyIvar_copyProperies来判断某个Class里面的ivarproperty的区别;
void objc_copyIvar_copyProperies(Class pClass){
    unsigned int count = 0;
    Ivar *ivars = class_copyIvarList(pClass, &count);
    for (unsigned int i=0; i < count; i++) {
        Ivar const ivar = ivars[i];
        //获取实例变量名
        const char*cName = ivar_getName(ivar);
        NSString *ivarName = [NSString stringWithUTF8String:cName];
        NSLog(@"class_copyIvarList:%@",ivarName);
    }
    free(ivars);
    unsigned int pCount = 0;
    objc_property_t *properties = class_copyPropertyList(pClass, &pCount);
    for (unsigned int i=0; i < pCount; i++) {
        objc_property_t const property = properties[i];
        //获取属性名
        NSString *propertyName = [NSString stringWithUTF8String:property_getName(property)];
        //获取属性值
        NSLog(@"class_copyProperiesList:%@",propertyName);
    }
    free(properties);
}
复制代码

objc_setProperty

  1. 在查看改写后的属性set方法时,我们发现有些属性有objc_setProperty,有些是通过内存偏移赋值的形式,为什么会有这两种区别呢?
  2. OC里面的 set方法写法太多,不可能为每个OCset方法都添加一个底层实现,set方法的本质就是为某块内存区域进行赋值,为了方便实现属性赋值,objcOC底层之间添加了一些中间方法objc_setProperty方法;底层则可以对objc_setProperty方法进行不同的实现;
  3. 为了让上层的所有的ivarset方法能直接调用到objc_setProperty,LLVM在编译时期就将上层的set方法sel->IMP的IMP重定向到了objc_setProperty
  4. LLVM源码中搜索objc_setProperty关键词找到对应的方法实现,找到GetPropertySetFunction()方法,经过阅读LLVM源码,最终发现copy修饰的属性会在底层添加上objc_setProperty方法;
  5. 为什么objc_setProperty在只存在于copy修饰的时候?当copy修饰的时候,objc_setProperty会执行reallySetProperty()方法来对zone进行内存拷贝,实现内存复制的效果;没有使用copy修饰的,底层是简单的的内存偏移后赋值,没有copy的效果;

类方法

  1. 借助MachOView查看FunctionStarts段的Functions,可以看到类方法是存在的;

005.jpg

006.jpg
2. 对象方式存在class中,类方法如果和对象方法一样都存在在class当中,调用的时候就无法根据方法名单纯的区分调用的是类方法还是对象方法,所以类方法应该不是存在class当中,对象方法是存在class当中,类方法会不会存在MetaClass中呢?
3. 经过模仿对象方法的查找过程,成功从MetaClass中获取到了类方法;
在LLDB中模仿获取对象方法的步骤来获取MetaClass中的method,最终成功找到了类方法;

(lldb) x/4gx metaClass     //步骤1获取metaClass的地址
0x100004638: 0x00000001003540f0 0x00000001003540f0
0x100004648: 0x000000010034b360 0x0000e03100000000
(lldb) p/x 0x100004638 + 0x20    //步骤2结构体指针偏移32个字节
(long) $1 = 0x0000000100004658
(lldb) p (class_data_bits_t *)$1    //步骤3
(class_data_bits_t *) $2 = 0x0000000100004658
(lldb) p $2->data()    //步骤4获取data
(class_rw_t *) $3 = 0x0000000100719a60
(lldb) p *$3    //重要步骤5
(class_rw_t) $4 = {
  flags = 2684878849
  witness = 0
  ro_or_rw_ext = {
    std::__1::atomic<unsigned long> = {
      Value = 4294983992
    }
  }
  firstSubclass = nil
  nextSiblingClass = 0x00007fff884e2cd8
}
(lldb) p $4.methods()  //步骤6 获取methods
(const method_array_t) $5 = {
  list_array_tt<method_t, method_list_t, method_list_t_authed_ptr> = {
     = {
      list = {
        ptr = 0x0000000100004180
      }
      arrayAndFlag = 4294984064
    }
  }
}
(lldb) p $5.list    //步骤7获取list
(const method_list_t_authed_ptr<method_list_t>) $6 = {
  ptr = 0x0000000100004180
}
(lldb) p $6.ptr    //步骤8获取ptr
(method_list_t *const) $7 = 0x0000000100004180
(lldb) p *$7      // 重要步骤9还原数据
(method_list_t) $8 = {
  entsize_list_tt<method_t, method_list_t, 4294901763, method_t::pointer_modifier> = (entsizeAndFlags = 27, count = 1)
}
(lldb) p $8.get(0).big()  //步骤10通过big打印method的信息
(method_t::big) $9 = {
  name = "sayHi"
  types = 0x0000000100003ed2 "v16@0:8"
  imp = 0x0000000100003a10 (KCObjcBuild`+[GCPerson sayHi] at main.m:30)
}
(lldb) 
复制代码

Method相关API总结

  1. class_getClassMethod(pClass, @selector(sayHello)); //获取传入类的元类的实例方法;不管传入的是class 还是MetaClass,都是找的传入class的MetaClass的instanceMethod,所以是只找类方法;
  2. class_getInstanceMethod(pClass, @selector(sayHello));//获取当前传入类的实例方法,传入元类获取的是类方法,传入类获取的是实例方法
  3. class_copyMethodList(pClass, &count);//拷贝当前类的方法,父类的方法不算;
  4. class_getMethodImplementation(pClass, @selector(sayHello))//获取当前类的某个方法实现,如果当前类找不到,会触发_objc_msgForward;

isKindOfClass && isMemberOfClass

isKindOfClass:

  1. returns YES if the receiver is an instance of the specified class or an instance of any class that inherits from the specified class.
  2. 方法调用者是传入的类的实例对象,或者调用者是传入类的继承者链中的类的实例对象,则返回YES。
  3. 底层调用为objc_opt_isKindOfClass(id _Nullable obj, Class _Nullable cls)、但是生效版本为OBJC_AVAILABLE(10.15, 13.0, 13.0, 6.0, 5.0);当使用objc_opt_isKindOfClass(id obj, Class otherClass)时不再区分类方法还是对象方法,第一个参数id obj是调用的类,第二个参数otherClass是比较的类;从obj的类开始不断的向取父类的isa与otherClass做比较;
  4. 仔细观看发现objc_opt_isKindOfClass(id obj, Class otherClass)-iskindofClass以及+iskindofClass的实现基本一致;

+isKindOfClass 注意点

+isKindOfClass本来是判断类对象是否与元类或元类的父类相等,这里由于NSObject元类superClass又指向了NSObject类,所以这里用
objcBOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];

正常情况下元类的父类还是元类,是永远不可能跟类相等的,但是刚好nsobject的元类还是nsobject的类,所以就相等了;

isMemberOfClass:

returns YES if the receiver is an instance of the specified class.
方法调用者必须是传入的类的实例对象才返回YES。

void gcisKindofDemo(void){
    //iskindofclass:方法调用者是传入的类的实例对象,或者调用者是传入类的继承者链中的类的实例对象,则返回YES。解释-
    //iskindofclass:方法调用者的isa或者isa的superclass =传入的类。解释二
    //class对象 与 metaclass 或者与super metaclass做比较,
    //isMemberOfClass:方法调用者必须是传入的类的实例对象才返回YES。
    BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];       //类对象是否与元类或元类的父类相等,本来元类的父类还是元类,是永远不可能跟类相等的,但是刚好nsobject的元类还是nsobject的类,所以就相等了;
    BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];     //左边是元类的member,右边是类,所以不想等
    BOOL re3 = [(id)[GCPerson class] isKindOfClass:[GCPerson class]];       //左边是GCPerson元类的实例对象或者GCPerson元类的父类的对象,右边是GCPerson的类对象,所以不会相等;
    BOOL re4 = [(id)[GCPerson class] isMemberOfClass:[GCPerson class]];     //左边是GCPerson 元类的member,右边是gcperson类,所以不想等;
    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]];       //左边的isa是类,右边也是类,相等;
    BOOL re6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]];     //左边的isa是类,右边也是类,相等
    BOOL re7 = [(id)[GCPerson alloc] isKindOfClass:[NSObject class]];       //左边的isa是gcperson类,右边也是gcperson类,相等;
    BOOL re8 = [(id)[GCPerson alloc] isMemberOfClass:[GCPerson class]];     //左边的isa是gcperson类,右边也是gcperson类,相等;
    NSLog(@" re5 :%hhd\n re6 :%hhd\n re7 :%hhd\n re8 :%hhd\n",re5,re6,re7,re8);
}

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