1、指针平移
在介绍类的内存结构之前先介绍一下指针平移,在后面会用到相关知识。

这张图的意思呢就是定义一个整型数组
intArray,里面的元素分别为10,2,6,4,8,可以利用intArray[0]方式取到元素10,其他的元素依次类推,这个大家都知道。还可以采用下面的方式:
简单解释一下:
int * p = intArray就是定义一个指针p指向数组intArray,我们知道数组的地址就是首元素的地址,所以*p取得值就是第一个元素,p+1就是指针往后移动一位也就是指向第二个元素,*(p+1)取得就是第二个元素,依次可以取到数组中所有的元素,这就是指针平移的用法。
2、class内存结构
class的指针平移跟上面讲到的平移一样,想要平移指针首先要知道class每个属性占的字节数,然后才能平移到我们想要的位置,我们先来回顾一下class的结构(818版本):

从定义可以看到
class里面总共有4个属性,分别是isa、superclass、cache和bits。
- 获取
isa,从class首地址开始取8字节就可以了,因为isa是一个指针,指针的大小固定8字节。 - 获取
superclass,从class首地址平移8字节后再取8字节就可以了 - 获取
cache,从class首地址平移16字节后再取cache所占字节数就可以了 - 获取
bits,从class首地址平移16+cache所占字节数就可以了
从上面的分析知道如果知道cache的字节数,就可以通过平移的方式来获取class各个属性的值了。我们先看一下cache的大小
2.1 cache大小
cache是cache_t类型,看一下cache_t的定义(818版本):

这个定义里面有很多
static类型的属性,static类型属性是不存在结构体里面的,所以我们只需关注红色部分就可以了。其实就是两部分:
_bucketsAndMaybeMask和union(1)
_bucketsAndMaybeMask是uintptr_t类型,看一下定义:

就是无符号长整型,
8字节。(2)
union通过前面的介绍,里面的struct和_originalPreoptCache是共用内存的,我们只看一个就可以了,最简单的是_originalPreoptCache,就是一个preopt_cache_t指针,所以也是8字节。
总结:通过分析我们知道了cache的大小就是16字节。
2.2 bits分析
class里面属性我们已经知道isa和superclass,还剩cache和bits,cache看名字就知道是缓存用的,所以接下来我们就先研究一下bits。
用x/8gx查看一下MlqqObject内存结构:

在图中可以轻松找到bits的起始地址,我们在验证一下对不对,按照图中方法转化一下:

能正常打印出来是
class_data_bits_t类型指针,说明我们移位没问题。
我们只拿一个指针没什么用,关键是要看指针里面指向的内容是什么,在objc_class的可以找到下面的方法:
class_rw_t *data() const {
return bits.data();
}
可以看到bits有一个data()方法,我们也可以调用,看看里面返回什么内容:

确实是返回了一个
class_rw_t类型的指针,接着看一下这个指针的内容是什么
看到这里还是没能看到我们想看的内容,既然是类的结构,那总要看到跟类相关的方法、属性等内容吧。我们打印的$3变量是
class_rw_t结构体,我可以在这个结构体里面找到相关的方法:1、获取method的函数:
const method_array_t methods() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->methods;
} else {
return method_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseMethods()};
}
}
2、获取properties的函数:
const property_array_t properties() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->properties;
} else {
return property_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProperties};
}
}
3、获取protocols的函数:
const protocol_array_t protocols() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->protocols;
} else {
return protocol_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProtocols};
}
}
2.3 获取method,properties和protocols
在获取之前先来看一下MlqqObject定义的方法和属性:

2.3.1 获取methods
通过class_rw_t源码我们知道里面有获取methods的方法,既然有方法就好办了,我们来获取一下方法,通过变量$3调用methods()方法

得到变量$4,可以看到$4里面有list,就是一个数组,我们用$4.list取到里面的list得到变量$5:

这里解释一下:ptr指的是指针,指向的类型是method_list_t,所以我们可以强转一下类型:

打印一下$6的值:

我这里用的是objc-818.2版本看到的是这个格式跟之前版本略有不同,这里面总共有6个方法,可以通过变量$7取出来里面的值:

取出来变量$8没有信息,但是可以看到是一个method_t类型,搜一下源码可以看到method_t定义中有很多方法,我们这里打印一下其描述信息看一下,先看一下方法:
objc_method_description *getDescription() const {
return isSmall() ? getSmallDescription() : (struct objc_method_description *)this;
}

在打印一下变量$9的值:

这样我们就取出来第一个方法了,按着这步骤我们分别取一下其他的值:

这样可以将类里面的方法全部遍历出来了。但是我们没有看到
+ (void)hobbyTest这个类方法。超过数组数量就会发生越界。
2.3.2 获取properties
我们在来看看获取properties方式,前面跟获取methods一样,只是在将methods()方法换成properties()方法,其他的获取方式都是一样的:

这样我们就可以看到所有的属性了,但是我们没有看到
hobby这个成员变量。
2.3.2 获取protocols
这个跟上面两个获取的方式一样,大家可以自己动手试试。
总结:这篇要介绍的已经介绍完了,还有两个问题:
1、+ (void)hobbyTest这个静态方法我们没看到存在哪里
2、hobby这个成员变量我们也没看到存在哪里
2.4 获取成员变量
其实成员变量是存在结构体class_rw_t的ro里面,获取方式如图:

获取结构体
class_ro_t的ivars:

这样就可以取到类的成员变量了
2.5 获取类方法
通过上面的分析,我们在MlqqObject.class没有找到+ (void)hobbyTest这个类方法,那么我们可以看看其元类里面有没有,根据前面的知识先打印其元类的内存结构:

接下来就是获取
methods,参考2.3.1就不一步步展示了,直接看结果:
这里验证了类方法是存在元类里面的
2.6 类方法归属分析
为了探索类方法的归属问题,我们需要两个方法:
方法1:获取类及元类的实例方法
void instanceMethod_classToMetaclass(Class mClass){
const char *className = class_getName(mClass);
Class metaClass = objc_getMetaClass(className);
Method method1 = class_getInstanceMethod(mClass, @selector(bitsTest));
Method method2 = class_getInstanceMethod(metaClass, @selector(bitsTest));
Method method3 = class_getInstanceMethod(mClass, @selector(hobbyTest));
Method method4 = class_getInstanceMethod(metaClass, @selector(hobbyTest));
NSLog(@"%s - %p-%p-%p-%p",__func__,method1,method2,method3,method4);
}
方法2:获取类及元类的类方法
void classMethod_classToMetaclass(Class mClass){
const char *className = class_getName(mClass);
Class metaClass = objc_getMetaClass(className);
Method method5 = class_getClassMethod(mClass, @selector(bitsTest));
Method method6 = class_getClassMethod(metaClass, @selector(bitsTest));
Method method7 = class_getClassMethod(mClass, @selector(hobbyTest));
Method method8 = class_getClassMethod(metaClass, @selector(hobbyTest));
NSLog(@"%s-%p-%p-%p-%p",__func__,method5,method6,method7,method8);
}
2.6.1 实例方法
我们在main函数里面先调方法1:
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
MlqqObject * objc = [[MlqqObject alloc] init];
Class mClass = object_getClass(objc);
instanceMethod_classToMetaclass(mClass);
NSLog(@"Hello, World!");
}
return 0;
}
来看看打印的结果:

method1:获取类MlqqObject的实例方法bitsTest,有值method2:获取类MlqqObject的元类的实例方法bitsTest,无值method3:获取类MlqqObject的实例方法hobbyTest,无值method4:获取类MlqqObject的元类的实例方法hobbyTest,根据上面的分析知道,类方法是存在元类里面的,所以有值
2.6.2 类方法
再获取类方法,在main函数里面调方法2:
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
MlqqObject * objc = [[MlqqObject alloc] init];
Class mClass = object_getClass(objc);
classMethod_classToMetaclass(mClass);
NSLog(@"Hello, World!");
}
return 0;
}
看看结果:

method5:获取类MlqqObject的类方法bitsTest,无值method6:获取类MlqqObject的元类的类方法bitsTest,无值method7:获取类MlqqObject的类方法hobbyTest,有值method8:获取类MlqqObject的元类的类方法hobbyTest,也有值
那么现在问题来了,为什么MlqqObject的元类中既有实例方法hobbyTest又有类方法hobbyTest呢,我们接着往下分析
2.6.3 class_getClassMethod分析
我们来看看class_getClassMethod的源码:
/***********************************************************************
* class_getClassMethod. Return the class method for the specified
* class and selector.
**********************************************************************/
Method class_getClassMethod(Class cls, SEL sel)
{
if (!cls || !sel) return nil;
return class_getInstanceMethod(cls->getMeta(), sel);
}
从上面的源码我们可以得出结论:
获取类方法的本质就是获取元类的实例方法
再来看看getMeta()的源码:
// NOT identical to this->ISA when this is a metaclass
Class getMeta() {
if (isMetaClassMaybeUnrealized()) return (Class)this;
else return this->ISA();
}
也就是说只要
cls是元类,cls->getMeta()得到的就是cls本身,并不会再去找cls的元类了,看到这里我们在来看method4和method8,metaClass都是MlqqObject的元类,所以class_getClassMethod(metaClass, @selector(hobbyTest))等价于class_getInstanceMethod(metaClass, @selector(hobbyTest)),这就是MlqqObject的元类中既有实例方法hobbyTest又有类方法hobbyTest的原因
这里简单介绍一下lldb里面参数打印的规则:
1、这里都是用的p命令,因为底层都没有描述方法,所以不能用po
2、指针类型用->方式调用方法,例如:$1->data()
3、结构体类型用.方式调用方法,例如:$3.properties()
4、指针类型,可以用*的方式取值,例如:*$2
5、$1、$2、$3、$4等都是临时变量,方便调用