美文网首页
iOS原理探索04--类结构的分析

iOS原理探索04--类结构的分析

作者: HardCabbage | 来源:发表于2020-09-21 10:17 被阅读0次

    类的分析

    • 准备工作,我们先创建两个类继承NSObjectLGPerson和继承LGPersonLGStudent
    //.h文件
    @interface LGPerson : NSObject
    {
        NSString *hobby;
    }
    @property (nonatomic, copy) NSString *lg_name;
    - (void)sayHello;
    + (void)sayBye;
    @end
    
    //.m文件
    @implementation LGPerson
    - (void)sayHello
    {
    }
    + (void)sayBye
    {
    }
    @end
    
    
    • main.m文件中如下设置
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            // insert code here...
            LGPerson *person = [LGPerson alloc];
            LGStudent *student = [LGStudent alloc];
            NSLog(@"Hello, World! %@ - %@",person,student);
        }
        return 0;
    }
    
    
    • 类结构的分析
      mian.m文件LGStudent *student = [LGStudent alloc];处打上断点运行一下项目工程,使用lldb进行调试

      lldb调试过程及输出结果
      • 首先我们可以根据lldb命令得到person的内存分布,我们知道0x001d8001000022ddperson的内存指针,接着使用这个指针&0x00007ffffffffff8ULL,可以获取类的相关信息$3 = 0x00000001000022d8
      • 接着po 0x00000001000022d8来打印类的信息,我们发现结果为LGPerson
      • x/4gx 0x00000001000022d8,来打印LGPerson的内存情况,我们同样的可以拿到类的isa指针地址0x00000001000022b0;
      • 下一步,p/x 0x00000001000022b0 & 0x00007ffffffffff8ULL,我们来获取类的信息,我们得到$5 = 0x00000001000022b0类的指针地址;
      • 下一步:通过po 0x00000001000022b0,发现结果还是LGPerson,这是为什么呢?这里先简单说一下这个LGPerson是元类;下面小节在详细说明元类的问题。

    二、元类,主要有以下几点说明:

    • 我们都知道 对象的isa是指向其实也是一个对象,可以称为类对象,其isa的位域指向苹果定义的元类

    • 元类系统设置的,其定义和创建都是由编译器完成,在这个过程中,类的归属来自于`元类。

    • 元类类对象的类,每个类都有一个独一无二的元类用来存储 类方法的相关信息。

    • 元类本身是没有名称的,由于与类相关联,所以使用了同类名一样的名称。

    总结:由上图的打印结果我们可以得出如下结论:对象 --> 类 --> 元类 --> NSobject, NSObject 指向自身

    三、isa走位 & 继承关系

    根据上面的探索以及各种验证,对象、类、元类、根元类的关系如下图所示

    isa走位 & 继承关系流程图
    isa的走向有以下几点说明:
    • 实例对象(Instance of Subclass)isa 指向 类(class)
    • 类对象(class) isa指向元类(Meta class)
    • 元类(Meta class)的isa指向根元类(Root metal class)
    • 根元类(Root metal class) 的isa 指向它自己本身,形成闭环,这里的根元类就是NSObject。

    superclass(即继承关系)的走向也有以下几点说明:

    • 类(subClass) 继承自 父类(superClass)
    • 父类(superClass) 继承自 根类(RootClass),此时的根类是指NSObject。
    • 根类 继承自 nil,所以根类即NSObject可以理解为万物起源,即无中生有。
    • 子类的元类(metal SubClass) 继承自 父类的元类(metal SuperClass)
    • 父类的元类(metal SuperClass) 继承自根元类(Root metal Class
    • 根元类(Root metal Class) 继承于根类(Root class),此时的根类是指NSObject。

    举例说明

    2251862-48a5603729fdf0a9.png

    isa 走位链

    • studentisa走位链:student(子类对象) --> LGStudent (子类)--> LGStudent(子元类) --> NSObject(根元类) --> NSObject(跟根元类,即自己)
      -personisa走位图:person(父类对象) --> CJLPerson (父类)--> CJLPerson(父元类) --> NSObject(根元类) --> NSObject(跟根元类,即自己)

    superclass走位链

    • 类的继承关系链:LGStudent(子类) --> CJLPerson(父类) --> NSObject(根类)--> nil
    • 元类的继承关系链:LGStudent(子元类) --> CJLPerson(父元类) --> NSObject(根元类)--> NSObject(根类)--> nil

    四、objc_class & objc_object

    在分析objc_class & objc_object我们先引入一个问题,为什么类和对象都有isa属性呢?
    我们先将main.m文件编译为main.cpp来分析一下这个问题。我们根据clang编译的c++源码可以看出NSObject的底层编译NSObject_IMPL结构体,并且对象含有Class isa,代码如下

    struct NSObject_IMPL {
        Class isa;
    };
    
    typedef struct objc_class *Class;
    
    
    struct objc_object {
        Class _Nonnull isa __attribute__((deprecated));
    };
    
    

    下面我们通过在objc4源码中搜索来探索objc_class & objc_object

    • objc_class在源码中搜索到两种相关源码
    • 第一种已经不再使用了,并且和我们使用Clang编译后的一样
    struct objc_class {
        Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
    
    } OBJC2_UNAVAILABLE;
    

    另外一种,我们选择主要的代码进行展示如下

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

    我们发现objc_class继承自objc_object,下面我们再看看objc_object源码

    • objc_object源码
    struct objc_object {
    private:
        isa_t isa;
    };
    

    总结:

    • 我们根据源码发现objc_object结构体定义了isa作为它的一个属性objc_class继承自objc_object,所以objc_class也拥有了isa属性。
    • mian.cpp底层编译文件中,NSObject中的isa在底层是由Class 定义的,其中class的底层编码来自 objc_class类型,所以NSObject也拥有了isa属性
    • NSObject是一个类,用它初始化一个实例对象objcobjc 满足 objc_object 的特性,所以对象都有一个 isaisa表示指向,来自于当前的objc_object
    • 所以所有的对象都是以 objc_object为模板继承过来的。
    • 因为对象是 来自NSObject(OC) ,但是真正到底层的 是一个objc_object(C/C++)的结构体类型,所以 objc_object对象的关系继承关系

    objc_classobjc_objectisaobjectNSObject等的整体的关系,如下图所示

    2251862-7b4c0996f92eb166.png

    类的方法

    我们根据下面类的底层实现源码来探索一下,类的实例方法存储在哪里,来具体探索一下类的结构是怎样的?

    struct objc_class : objc_object {
        // Class ISA;
        Class superclass;
        cache_t cache;             // formerly cache pointer and vtable
        class_data_bits_t bits;    
    
     // ......部分代码不在展示
    
    }
    
    • 在探索之前我们需要补充一下知识,关于内存偏移,我们先使用实例说明一下这个内存偏移
      【普通指针】
            //普通指针
            int a = 10; //变量
            int b = 10;
            NSLog(@"%d -- %p", a, &a);
            NSLog(@"%d -- %p", b, &b);
    
    输出的结果 截屏2020-09-21 09.37.28.png

    我们可以从控制台看出,a和b两个指针指向了同一片的存储着10的空间。

    • a、b都指向10,但是a、b的地址``不一样,这是一种拷贝,属于值拷贝,也称为浅拷贝

    • a,b的地址之间相差4个字节,这取决于a、b的类型
      【对象指针】

            LGPerson *p1 = [LGPerson alloc]; // p1 是指针
            LGPerson *p2 = [LGPerson alloc];
            NSLog(@"%d -- %p", p1, &p1);
            NSLog(@"%d -- %p", p2, &p2);
    

    输出结果

    截屏2020-09-21 09.42.12.png
    • p1、p2 是指针,p1是 指向 [LGPerson alloc]创建的空间地址,即内存地址,p2 同理

    • &p1、&p2是 指向 p1、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);
    
    输出结果 截屏2020-09-21 09.45.06.png
    • &c&c[0] 都是取这个数组的首地址,所以``数组名等同于首地址
    • &c&c[1] 相差4个字节,地址之间相差的字节数,主要取决于存储的数据类型可以通过首地址+偏移量取出数组中的其他元素,其中偏移量数组的下标内存中首地址实际移动的字节数 等于偏移量 * 数据类型字节数`;
    计算类结构的内存大小

    通过上面类结构的源码我们来计算一下类的大小
    -isa属性:继承自objc_objectisa,占 8字节;

    • superclass属性:Class类型,Class是由objc_object定义的,是一个指针,占8字节
    • cache属性:是cache_t结构体类型,我们应该按照计算结构体内存大小的规则来计算,而结构体指针才是8字节;下面代码可以计算出cache占16字节
    struct cache_t {
    #if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
        explicit_atomic<struct bucket_t *> _buckets;// 是一个结构体指针类型,占8字节
        explicit_atomic<mask_t> _mask;//是mask_t 类型,而 mask_t 是 unsigned int 的别名,占4字节
    #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
        explicit_atomic<uintptr_t> _maskAndBuckets;
        mask_t _mask_unused;
    //省略部分代码
    #if __LP64__
        uint16_t _flags; //是uint16_t类型,uint16_t是 unsigned short 的别名,占 2个字节
    #endif
        uint16_t _occupied; //是uint16_t类型,uint16_t是 unsigned short 的别名,占 2个字节
    //省略部分代码
    }
        
    
    • bits属性:只有首地址经过上面3个属性的内存大小总和的平移,才能获取到bits`;
    类结构中方法分析流程
    (lldb) p/x LGPerson.class
    (Class) $0 = 0x00000001000022f0 LGPerson
    (lldb) //平移32个字节
    error: '//平移32个字节' is not a valid command.
    (lldb) p/x 0x00000001000022f0 + 32
    (long) $1 = 0x0000000100002310
    (lldb) 获取bit 
    error: '获取bit' is not a valid command.
    (lldb) p (class_data_bits_t *)0x0000000100002310
    (class_data_bits_t *) $2 = 0x0000000100002310
    (lldb) 通过结构体的data()来获取bits
    error: '通过结构体的data()来获取bits' is not a valid command.
    (lldb) p $2 -> data()
    (class_rw_t *) $3 = 0x0000000102018a20
    (lldb) 获取methods()
    error: '获取methods()' is not a valid command.
    (lldb) p $3.methods()
    (const method_array_t) $4 = {
      list_array_tt<method_t, method_list_t> = {
         = {
          list = 0x0000000100002180
          arrayAndFlag = 4294975872
        }
      }
    }
      Fix-it applied, fixed expression was: 
        $3->methods()
    (lldb) 获取list
    error: '获取list' is not a valid command.
    (lldb) p $4.list
    (method_list_t *const) $5 = 0x0000000100002180
    (lldb) 打印list内容
    error: '打印list内容' is not a valid command.
    (lldb) p *$5
    (method_list_t) $6 = {
      entsize_list_tt<method_t, method_list_t, 3> = {
        entsizeAndFlags = 26
        count = 4
        first = {
          name = "sayHello"
          types = 0x0000000100000f85 "v16@0:8"
          imp = 0x0000000100000d80 (KCObjc`-[LGPerson sayHello])
        }
      }
    }
    (lldb) 
    
    截屏2020-09-18 17.49.44.png
    • 关于类的属性
    (lldb) p $3.properties()
    (const property_array_t) $7 = {
      list_array_tt<property_t, property_list_t> = {
         = {
          list = 0x0000000100002230
          arrayAndFlag = 4294976048
        }
      }
    }
      Fix-it applied, fixed expression was: 
        $3->properties()
    (lldb) p $7.list
    (property_list_t *const) $8 = 0x0000000100002230
    (lldb) p *$8
    (property_list_t) $9 = {
      entsize_list_tt<property_t, property_list_t, 0> = {
        entsizeAndFlags = 16
        count = 1
        first = (name = "lg_name", attributes = "T@\"NSString\",C,N,V_lg_name")
      }
    }
    

    总结:类的实例方法和类的属性都存在bits中,我们发现类的类方法和类的成员变量却没有打印,我们可以思考一下,它们存在哪里呢?类的类方法会不会存在元类里面呢?下一节我们接着探索一下这个内容。

    相关文章

      网友评论

          本文标题:iOS原理探索04--类结构的分析

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