类的底层探究(上)

一、类的本质以及类的对象

Class class1 = [ LGPerson class ];
Class class2 = [ LGPerson alloc ]. class ;
Class class3 =  object_getClass ([ LGPerson alloc ]);
NSLog (@ "\n%p-\n%p-\n%p" , class1 , class2 , class3 );

我们通过不同的创建类对象的方式可以发现,类对象(类)的地址始终是一样的,那这个类的底层结构是什么样子的呢?接下来我们来探究一下。我们在objc的源码中可以找到关于类的定义:

  我们可以看到类的本质就是objc_class 结构体,他 继承自objc_object, 而objc_object 是实例对象的结构,也就是说类其实也是一个对象,那么既然是一个对象,其中就应该含有isa指针。接下来,我们看一下isa指针的指向问题。

1.isa指针指向

首先,我们通过一段代码来分析isa指针指向:

void lgTestNSObject(void){
    // NSObject实例对象
    NSObject *object1 = [NSObject alloc];
    // NSObject类对象
    Class class = object_getClass(object1);
    // NSObject元类(根元类)
    Class metaClass = object_getClass(class);
    
    NSLog(@"NSObject实例对象:%p",object1);
    NSLog(@"NSObject类对象:%p",class);
    NSLog(@"NSObject元类(根元类):%p",metaClass);
}

 

通过对实例对象的isa指针的读取获取到了类对象的地址,接着通过对类对象的isa指针获取了元类对象的指针。同时我们可以发现,类对象和元类对象虽然不同,但名称是相同的。接下来,我们看看元类对象的isa指针的指向问题。

通过对元类对象的isa指针走向发现,元类对象的isa指针指向了一个NSobject,这个应该是NSobject的元类对象,由于Nsobject是所有对象的根类,所以这个指向的Nsobject就是根元类,而我们通过对根元类的isa指针的探索发现,根元类依旧指向了自己。

由此我们可以得出isa指针的指向关系:

  • 实例对象的isa指向类对象
  • 类对象的isa指向元类对象
  • 元类对象的isa指向根元类对象
  • 根元类对象的isa指向自己

这里我们做一个小补充:X/4g -p找到isa指针的内存地址,再与isa的掩码相与,然后po这个&之后的地址就可以得到类对象的地址,然后我们打印这个类对象即可得到。

  • 元类对象就是objc_class结构
  • 根元类就是NSobject

二、类和元类的继承关系

在分析类的结构的时候,我们知道类的本质就是objc_class,他的内部有一个superClass的成员,也就是说我们可以通过superclass找到父类。接着,我们通过以下代码来看了解类和元类之间的继承关系。

// LGPerson  -- 元类的父类就是父类的元类
    Class pMetaClass =  objc_getMetaClass ( "LGPerson" );
    Class psuperClass =  class_getSuperclass ( pMetaClass );
    NSLog (@ "%@ - %p" , pMetaClass , pMetaClass );
    NSLog (@ "%@ - %p" , psuperClass , psuperClass );
   
    // LGTeacher继承自LGPerson
    // LGTeacher元类的父类 就是 LGPerson(LGPerson的元类)
    Class tMetaClass =  objc_getMetaClass ( "LGTeacher" );
    Class tsuperClass =  class_getSuperclass ( tMetaClass );
    NSLog (@ "%@ - %p" , tsuperClass , tsuperClass );
   
    // NSObject的父类
    Class nsuperClass =  class_getSuperclass ( NSObject . class );
    NSLog (@ "%@ - %p" , nsuperClass , nsuperClass );
   
    // 根元类的父类 -- NSObject
    Class rnsuperClass =  class_getSuperclass ( metaClass );
    NSLog (@ "%@ - %p" , rnsuperClass , rnsuperClass );

 这里由于xcode无法更新,所以我们这里直接给出类对象和元类对象的指向:(父类的元类就是元类的父类,根元类的父类是Nsobject)Nsobject是万类之祖
类对象的superclass

  • 子类的superclass指向父类
  • 父类的superclass指向根类
  • 根类的superclass指向nil,没有父类

元类对象的superclass

  • 子类元类的superclass指向父类元类
  • 父类元类的superclass指向根元类
  • 根元类的superclass指向根类(NSobject)

通过isa指针和superclass的指向分析,我们可以得到一个经典的图:(Class(类),Metaclass(元类),Rootclass(根类),Rootclass‘s metaclass(根元类))

 

isa的指向图:

 

superclass的指向图:

 

 

 三、类结构体中的class_data_bits_t

首先,我们通过class_data_bit_t 的源码来看一下:

在class_data_bit_t 中只有一个bits成员,没有什么实质性的东西,我们从其所提供的方法看看,然后我们可以看到两个方法

这两个方法返回的结构分别为class_rw_t 、class_ro_t,并且class_ro_t是存储在class_rw_t中的。

通过对这两个结构体的源码阅读可以发现,类中的所有信息都是在class_rw_t中的,也就是说类中所有的信息都是通过class_data_bits_t中来获取的。

接着,我们从代码部分来探索一下class_data_bits_t:

@ interface Person :  NSObject {
    NSString * hobby ;
}

@ property ( nonatomic , copy )  NSString * name ;
@ property ( nonatomic , assign )  int age ;

+ ( void ) classMetheod ;
- ( void ) instanceMetheod ;
@ end

我们可以根据类的内存结构来推算出class_data_bits_t的存储位置,在class_data_bits_t之前有32个字节(isa : 8 cache_t : 16 superclass: 8),我们在类对象地址平移32位即可。我们将拿到的class_data_bits_t进行(class_data_bits_t)的强转,有了这个我们可以先从源码中来看一下class_rw_t提供的方法:

通过一层层的深入我们发现,最终获取到了method_list_t类型的数据,进入method_list_t可以看到这是一个entsize_list_tt的模版类。

从源码中可以看出entsize_list_tt的模版类,类似一个范型类,表明了存储的元素类型和容器类型(Element:表示元素类型 List:表示容器类型 FlagMask:标记位),并且在内部发现了get 方法来获取元素,可以实例化出method_list_t、ivar_list_t、property_list_t三种类型。

那么就可以通过刚才获取的  method_list_t来获取列表中存储的方法是什么,但是这里只能看到获取的是 method_t的类型,但是没有其他有用的信息,再去method_t中查看源码

method_t 可以看到这里定义了两个结构体big 和 small,从源码来看就是根据电脑的系统是大端模式和小端模式存储不同的数据类型,电脑是大端就可以通过 method_t 提供的 big方法来获取方法的信息,电脑是小端就可以通过 method_t 提供的 small方法来获取方法的信息。

此时就可以看到了类对象中存储的所有方法,里面包含了属性自动生成的setter、getter,定义的实例方法、.cxx_destruct,对于.cxx_destruct方法,当类中有成员变量时就会生成该方法,用于是ARC 模式下释放成员变量,此时还发现了这里没有类方法,基于之前的对于类的结构分析,此时应该明白,类方法是存储在元类对象的。我们接着来探究一下类方法:

通过元类对象的class_rw_t 去获取类方法,我们可以看到了类方法确实存储元类对象里面。

我们通过前面读取方法的方式来继续读取属性列表

这里又看到了  entsize_list_tt模版,套路就不重复了,看一下 property_t结构,就是一个单纯的结构体:

分别打印一下,里面的元素:

可以看到,这里只有属性,没有成员变量,也就是说成员变量是存在别的地方。

我们继续来获取成员变量

在源码中可以看到在 class_ro_t中存储了 ivar_list_t ,而  class_ro_t又存储在  class_rw_t中,所以我们还是从 class_rw_t开始获取:

获取成员变量列表:

可以看到,这里包含了属性成员变量(_age、_name)和手动添加的成员变量(hobby).

五 元类的作用

前面提到了,实例方法和类方法是分别通过类和元类来存储,那么为什么要这么做呢,难道单单是为了存储类方法么,难道通过类来存储类方法实例方法不能实现么?

  • 实例的属性和方法存储在类对象中,类的属性和方法存储在元类对象中,之所以这么设计,其实是为了统一方法调用的接口,保证单一职责原则

其实,设计元类的目的,就是想要复用消息机制,因为在OC中万物皆对象,消息机制(objc_msgSend)中需要传入消息的接收者 isa和方法名,如果没有元类,类方法和实例方法都存储在类对象中,其中类方法和实例方法重名,那么就需要额外增加一系列的判断,比如判断接收者是类还是实例等,这样就会影响消息发送的效率,有了元类之后,就可以直接通过 isa 快速寻找到方法去调用,并且类和元类分别负责实例方法和类方法,也更符合单一职责的原则,通过这种操作,就可以可以完美复用消息机制,也更利于维护消息机制

这个设计的理念在系统中体现的非常明显 例如class_getClassMethod这个函数,其实底层调用的也是class_getInstanceMethod,这也说明了,其实实例方法和类方法并没有区别,从而更加体现了元类不是为了存储类方法,而是为了能够复用消息机制。



 

posted on 2022-04-26 21:40  suanningmeng98  阅读(79)  评论(0编辑  收藏  举报