类对象的底层探索(下)

254 阅读5分钟

获取类对象中的成员变量

经过上篇文章的探索,我们知道属性列表,实例方法列表以及协议方法列表存放在类对象的class_rw_t这样的一个结构体中,那么成员变量存放在哪呢?我们继续进行探索,我们发现在class_rw_t中有一个返回class_ro_t结构体指针的方法: 未命名.png 我们还是通过上篇文章探索方法列表的方式对成员变量进行探索:

Person类的成员变量和属性如下: 截屏2022-04-25 下午10.52.33.png 未命名.png 也就是说成员变量存放在class_ro_t这么一个结构体中。那么为什么要将它们分开存放呢?

class_rw_t,class_ro_t,class_ro_ext_t

class_ro_t的结构体是在编译阶段生成的。当类从磁盘中加载出来的时候,属性,方法和协议方法等一些其他的信息都存在其中,这里边的东西是不可变的。class_rw_t则是在运行时阶段生成的,它会把ro中可能会更改的东西拿过来存储其中。当在运行时阶段,我们可能会动态的添加一些信息,比如方法,属性,这时候就会生成一个叫做class_ro_ext_t的结构体把需要更改的从class_ro_t中提取出来存放在里面。这样一来可变的存在一个地方,不可变的存在一个地方,而且也节省了空间。

苹果设计元类的原因

OC中方法调用的本质就是消息机制objc_msgSend(接收者,方法名),当我们调用方法的时候,系统就会去该接收者的isa指针指向的对象中去查找方法。如果接收者是实例对象,则会去类对象中根据方法名查找方法实现,如果接收者是类对象,则会去元类中进行查找。消息发送最需要的就是速度要快,如果我们在这个方法调用的底层写逻辑判断势必会影响执行速度。这也体现了单一职责,类对象存储实例实例方法以及成员变量等,元类存储类方法。所以苹果设计元类最大的目的就是复用消息机制

为什么成员变量的值要放在实例对象中,而成员变量放在类对象中。

因为类对象只有一个,而实例对象会有很多个,而这些实例对象的成员变量的值都可能会是不同的。所以类对象就好似一个模板,来规定如何去实例化一个实例对象。

通过runtime api获取成员变量,属性以及方法

类结构如下: 截屏2022-04-28 下午8.50.25.png

获取成员变量列表

- (void)getIvarList:(Class)cls {
    unsigned int outcount = 0;
    Ivar *ivars = class_copyIvarList(cls, &outcount);
    for (int i = 0; i < outcount; i++) {
        Ivar ivar = ivars[i];
        const char *cName = ivar_getName(ivar);
        const char *cType = ivar_getTypeEncoding(ivar);
        NSLog(@"name = %s type = %s", cName, cType);
    }
    free(ivars);
}

//调用
[self getIvarList:[Person class]];

结果:

2022-04-28 20:44:16.648612+0800 LearnTest[39000:5243491] name = _hobby type = @"NSString"
2022-04-28 20:44:16.648676+0800 LearnTest[39000:5243491] name = _age type = i
2022-04-28 20:44:16.648710+0800 LearnTest[39000:5243491] name = _name type = @"NSString"

获取属性列表

- (void)getIvarList:(Class)cls {
    unsigned int outCount = 0;
    Ivar *ivars = class_copyIvarList(cls, &outCount);
    for (int i = 0; i < outCount; i++) {
        Ivar ivar = ivars[i];
        const char *cName = ivar_getName(ivar);
        const char *cType = ivar_getTypeEncoding(ivar);
        NSLog(@"name = %s type = %s", cName, cType);
    }
    free(ivars);
}

// 调用
[self getPropertyList:[Person class]];

结果:

2022-04-28 21:04:44.981982+0800 LearnTest[39110:5258890] name = name attr = T@"NSString",&,N,V_name
2022-04-28 21:04:44.982026+0800 LearnTest[39110:5258890] name = age attr = Ti,N,V_age

获取方法列表

- (void)getMethodList:(Class)cls {
    unsigned int outCount = 0;
    Method *methods = class_copyMethodList(cls, &outCount);
    for (int i = 0; i < outCount; i++) {
        Method method = methods[i];
        SEL sel = method_getName(method);
        NSString *selString = NSStringFromSelector(sel);
        const char *type = method_getTypeEncoding(method);
        NSLog(@"name = %@, type = %s", selString, type);
    }
    free(methods);
}

// 调用:
[self getMethodList:[Person class]];

结果:

2022-04-28 21:30:28.098241+0800 LearnTest[39254:5275792] name = instanceMethod, type = v16@0:8
2022-04-28 21:30:28.098297+0800 LearnTest[39254:5275792] name = name, type = @16@0:8
2022-04-28 21:30:28.098353+0800 LearnTest[39254:5275792] name = setName:, type = v24@0:8@16
2022-04-28 21:30:28.098388+0800 LearnTest[39254:5275792] name = age, type = i16@0:8
2022-04-28 21:30:28.098414+0800 LearnTest[39254:5275792] name = setAge:, type = v20@0:8i16
2022-04-28 21:30:28.098449+0800 LearnTest[39254:5275792] name = .cxx_destruct, type = v16@0:8

那么我们要获取类方法呢,只需要将类对象换成元类对象即可:

调用:
[self getMethodList:objc_getMetaClass("Person")];

结果:

2022-04-28 21:39:12.461387+0800 LearnTest[39302:5282575] name = classMethod, type = v16@0:8

我们来看以下的例子,为啥获取类方法通过class_getInstanceMethod也可以获得? 截屏2022-04-28 下午10.04.03.png 我们来看看class_getClassMethod的源码: 截屏2022-04-28 下午10.09.34.png 可以看到,它也调用的是class_getInstanceMethod方法,只是参数变为了元类而已。所以说明在苹果的底层是不区分实例方法和类方法的,也更能说明苹果设计元类的目的并不只是为了存储类方法。

我们再来看以下的例子,为什么两个找不到方法的两个例子却返回了相同的地址:

截屏2022-04-28 下午10.28.33.png

我们再去找class_getMethodImplementation的源码: 截屏2022-04-28 下午10.32.27.png

注意那个_objc_msgForward方法,这个我们将会在消息发送的探索中进行讲解。