美文网首页
类的结构分析

类的结构分析

作者: Y丶舜禹 | 来源:发表于2020-09-14 10:53 被阅读0次
    isa走位及继承关系图

    神图镇楼,相信做过iOS开发的同学一定非常熟悉这张经典图,每次看这张图都有不一样的体会,今天我们就借这张图,引出我们对的探索,主要探索的是 类的结构分析以及isa的走向和 继承.

    准备工作

    objc781源码
    创建两个类
    ZGPerson继承NSObject,定义一些方法和属性

    @interface ZGPerson : NSObject{
        NSInteger age;
    }
    @property (nonatomic, copy) NSString *name;
    - (void) say666;
    + (void) say888;
    @end
    
    - (void)say666
    {}
    + (void)say888
    {}
    @end
    

    ZGTeacher继承ZGPerson

    @interface ZGTeacher : ZGPerson
    
    @end
    

    输出两个对象

    int main(int argc, char * argv[]) {
        NSString * appDelegateClassName;
        @autoreleasepool {
            
            ZGPerson *person = [ZGPerson alloc];
            ZGTeacher *teacher = [ZGTeacher alloc];
        
            NSLog(@"person %@\n teacher %@",person,teacher);
            // Setup code that might create autoreleased objects goes here.
            appDelegateClassName = NSStringFromClass([AppDelegate class]);
        }
        return UIApplicationMain(argc, argv, nil, appDelegateClassName);
    }
    
    LLDB调试

    利用LLDB我们打印内存,先介绍几个常用指令

    x/4gx: 以16进制形式打印地址内容,读取416字节内容
    p/x: 打印变量的16进制格式
    po: 打印变量的 description 方法

    调试结果如下图所示

    LLDB调试内存信息

    在我们调试的过程中发现p/x 0x001d8001000012d5 & 0x00007ffffffffff8ULLp/x 0x00000001000012a8 & 0x00007ffffffffff8ULL中打印的信息都是ZGPerson,但是两者的含义却不相同。

    • 0x001d8001000012d5person对象isa指针所存储的的信息
    • 0x00000001000012a8 是isa中获取的类信息所指的类的isa的指针地址,即ZGPerson类的isa指针地址,我们称ZGPerson这样的类的类为元类
    元类

    什么是元类?,主要有以下几点说明:

    • 对象的isa 是指向类,类的其实也是一个对象,可以称为类对象,其isa的位域指向苹果定义的元类
    • 元类是系统给的,其定义和创建都是由编译器完成,在这个过程中,的归属来自于元类
    • 元类是类对象 的类,每个类都有一个独一无二元类用来存储 类方法的相关信息。
    • 元类本身是没有名称的,由于与类相关联,所以使用了同类名一样的名称
    isa指针指向
    isa指针指向

    由上图中LLDB调试信息我们得出isa的指向:

    • person实例对象的 isa指向了 ZGPerson
    • ZGPerson类对象的isa 指向了 ZGPerson 元类
    • ZGPerson 元类对象的 isa 指向了 NSObject
    • NSObject 类对象的 isa 指向了自己

    类的继承关系如下图:


    类的继承关系

    的继承关系链:ZGTeacher(子类) --> ZGPerson(父类) --> NSObject(根类)-->nil

    元类的继承关系链:ZGTeacher(子元类) --> ZGPerson(父元类) --> NSObject(根元类)-->NSObject(根类)--> nil

    为什么对象都有isa?

    理清了isa走位和继承问题,又来了一个新的疑问:为什么 对象都有isa属性呢?这里就不得不提到两个结构体类型:objc_class& objc_object
    我们在终端使用 clang命令:

    clang -rewrite-objc main.m -o main.cpp

    在生成的.cpp文件中我们注意到NSObject的底层编译是NSObject_IMPL结构体,其中Classisa指针的类型,是由objc_class定义的类型,
    objc_class是一个结构体。在iOS中,所有的Class都是以objc_class为模板创建的

    struct NSObject_IMPL {
        Class isa;
    };
    
    
    typedef struct objc_class *Class;
    

    我们在objc781的源码中搜索objc_class :发现在文件objc-runtime-new.h

    新版objc_class定义

    可以发现结构体objc_class是继承objc_object结构体的(在c++中结构体是可以继承的),而我们搜索objc_object {发现位于 objc-privat.h的源码发现了isa的定义!由此就可以解释为什么OC中所有的对象都有一个isa的指针!

    objc_object定义

    总结:

    • 结构体类型objc_class 继承自objc_object类型,其中objc_object也是一个结构体,且有一个isa属性,所以objc_class也拥有了isa属性
    • mian.cpp底层编译文件中,NSObject中的isa在底层是由Class 定义的,其中class的底层编码来自objc_class类型,所以NSObject也拥有了isa属性
    • NSObject 是一个类,用它初始化一个实例对象objcobjc 满足 objc_object 的特性(即有isa属性),主要是因为isa是由 NSObjectobjc_class继承过来的,而objc_class继承自objc_objectobjc_objectisa属性。所以对象都有一个isaisa表示指向,来自于当前的objc_object
    • objc_object(结构体) 是 当前的 根对象,所有的对象都有这样一个特性objc_object,即拥有isa属性

    类的结构体分析

    类的结构体分析我们主要分析类的结构体里面到底存储了哪些信息?
    因为我们在分析类的结构体的时候需要用到内存偏移的知识,所以我们先补充一下内存偏移.
    我们从普通指针对象指针数组指针逐个分析内存的偏移.

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            // insert code here...
            
            //普通指针
            int a = 10; //变量
            int b = 10;
            NSLog(@"%d -- %p", a, &a);
            NSLog(@"%d -- %p", b, &b);
            
            //对象指针
            ZGPerson *p1 = [ZGPerson alloc];
            ZGPerson *p2 = [ZGPerson alloc];
            NSLog(@"%@ -- %p", p1, &p1);
            NSLog(@"%@ -- %p", p2, &p2);
            
            //数组指针
            int c[4] = {1, 2, 3, 4};
            int *d = c;
            NSLog(@"%p -- %p - %p", &c, &c[0], &c[1]);
            NSLog(@"%p -- %p - %p", d, d+1, d+2);
            // Setup code that might create autoreleased objects goes here.
        }
        
        return 0;
    }
    

    打印结果

    打印结果
    普通指针 普通指针打印结果
    • a、b都指向10,但是a、b的地址不一样,这是一种拷贝,属于值拷贝,也称为浅拷贝

    • a,b的地址之间相差 4 个字节,这取决于a、b的类型

    普通指针内存情况如图:


    普通指针

    对象指针

    对象指针打印结果
    • p1、p2是指针,p1是 指向 [ZGPerson alloc]创建的空间地址,即内存地址,p2 同理

    • &p1、&p2是 指向p1、p2对象指针的地址,这个指针 就是二级指针

    对象指针内存情况如图:

    对象指针

    数组指针

    数组指针打印结果
    • &c&c[0]都是取 首地址,即数组名等于首地址
    • &c&c[1]相差4个字节,地址之间相差的字节数,主要取决于存储的数据类型
    • 可以通过 首地址+偏移量取出数组中的其他元素,其中偏移量是数组的下标,内存中首地址实际移动的字节数 等于 偏移量 * 数据类型字节数

    数组指针内存情况如图:


    数组指针

    类结构分析
    接下来我们对objc781objc-runtime-new.h文件中objc_class结构体的源码进行分析,

    struct objc_class : objc_object {
        // Class ISA;
        Class superclass;
        cache_t cache;             // formerly cache pointer and vtable
        class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    
        ...省略部分代码
    

    通过首地址+内存偏移我们可以打印objc_class结构体中存储的信息.

    • isa属性:继承自objc_object,占8个字节
    • superclass属性:Class类型指针,指针8个字节
    • cache: cache_t结构体类型,结构体的内存大小有结构体本身的属性决定,详情可参考本人的另一篇文章结构体与内存对齐
    • bits:我们得到上面3个属性的大小之后,通过首地址偏移就可以得到bits的内容

    现在不能确定大小的就是结构体cache的内存大小,我们进入cache的源码中看一下

    struct cache_t {
    #if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
        explicit_atomic<struct bucket_t *> _buckets;
        explicit_atomic<mask_t> _mask;
    #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
        explicit_atomic<uintptr_t> _maskAndBuckets;
        mask_t _mask_unused;
    
    ...一些static属性
    
    #if __LP64__
        uint16_t _flags;
    #endif
        uint16_t _occupied;
    
    ...一些static属性
    

    由于结构体中static的属性不占内存大小,所以我们只要关注上面的几个属性的内存大小

    • _flags属性:uint16_t类型,uint16_tunsigned short的别称,占2个字节
    • _occupied属性:uint16_t类型,uint16_tunsigned short的别称,占2个字节
    • 下面我们看if-else条件中各属性的大小
      if流程:
      _buckets属性:struct bucket_t *类型的结构体指针类型,占8个字节
      _mask属性:mask_t类型,mask_tunsigned int 的别名,占4字节
      elif流程:
      _maskAndBuckets属性:uintptr_t类型,是一个指针,指针占8个字节
      _mask_unused属性:mask_t类型,mask_tunsigned int的别名,占4字节

    总结:cache属性的内存大小:8+4+2+2 = 16字节,所以我们想获取bits属性中的内容我们需要偏移8+8+16字节。

    bits属性内容
    通过LLDB调试bits内容如下图所示

    LLDB调试bits

    其中data()获取数据是objc_class提供的方法

    struct objc_class : objc_object {
        // Class ISA;
        Class superclass;
        cache_t cache;             // formerly cache pointer and vtable
        class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    
        class_rw_t *data() const {
            return bits.data();
        }
     ...省略部分代码
     
    

    通过LLDB获取方法列表

    LLDB获取方法列表
    • class_rw_t中的methods()函数,可以获取到方法列表.
      *list函数获取列表的首地址,打印地址的值后,用get查看到类中的所有方法。通过下图能看到类中定义的say666方法、属性namesettergetter方法,还有一个系统生成的.cxx_destruct方法:

    • 类中还定义了一个类方法+ (void)say888,在这里是看不到的。因为类方法存在元类中。

    通过LLDB获取属性列表

    LLDB获取属性列表
    • class_rw_t中的properties()函数,可以获取到属性列表.
      *list函数获取列表的首地址,打印地址的值后,用get查看到类中的所有属性。不过却看不到成员属性,调用p $6.get(2)会报错数组越界

    成员属性
    那么对象的成员属性到底存放在哪里呢?经过探索我们终于发现~

    LLDB获取成员属性
    • 原来成员变量存在ro中,通过函数ro()获取,最终我们在roivars属性中发现了我们的成员变量age

    至此,大功告成!

    总结一下我们这篇文章的内容

    • isa指针指向及继承关系
    • 探索为什么对象都有isa
    • 类的结构体分析
    • 补充知识-内存偏移

    相关文章

      网友评论

          本文标题:类的结构分析

          本文链接:https://www.haomeiwen.com/subject/xnnnektx.html