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

类的分析

  • 准备工作,我们先创建两个类继承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进行调试

    iOS原理探索04--类结构的分析_第1张图片
    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走位 & 继承关系

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

iOS原理探索04--类结构的分析_第2张图片
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。

举例说明

iOS原理探索04--类结构的分析_第3张图片
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等的整体的关系,如下图所示

iOS原理探索04--类结构的分析_第4张图片
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 _buckets;// 是一个结构体指针类型,占8字节
    explicit_atomic _mask;//是mask_t 类型,而 mask_t 是 unsigned int 的别名,占4字节
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    explicit_atomic _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 = {
     = {
      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 = {
    entsizeAndFlags = 26
    count = 4
    first = {
      name = "sayHello"
      types = 0x0000000100000f85 "v16@0:8"
      imp = 0x0000000100000d80 (KCObjc`-[LGPerson sayHello])
    }
  }
}
(lldb) 
iOS原理探索04--类结构的分析_第5张图片
截屏2020-09-18 17.49.44.png
  • 关于类的属性
(lldb) p $3.properties()
(const property_array_t) $7 = {
  list_array_tt = {
     = {
      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 = {
    entsizeAndFlags = 16
    count = 1
    first = (name = "lg_name", attributes = "T@\"NSString\",C,N,V_lg_name")
  }
}

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

你可能感兴趣的:(iOS原理探索04--类结构的分析)