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
等都是临时变量,方便调用