五、类原理之bits

1、指针平移

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

image.png

这张图的意思呢就是定义一个整型数组intArray,里面的元素分别为102648,可以利用intArray[0]方式取到元素10,其他的元素依次类推,这个大家都知道。还可以采用下面的方式:
image.png

简单解释一下:int * p = intArray就是定义一个指针p指向数组intArray,我们知道数组的地址就是首元素的地址,所以*p取得值就是第一个元素,p+1就是指针往后移动一位也就是指向第二个元素,*(p+1)取得就是第二个元素,依次可以取到数组中所有的元素,这就是指针平移的用法。

2、class内存结构

class的指针平移跟上面讲到的平移一样,想要平移指针首先要知道class每个属性占的字节数,然后才能平移到我们想要的位置,我们先来回顾一下class的结构(818版本):

image.png

从定义可以看到class里面总共有4个属性,分别是isasuperclasscachebits

  • 获取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版本):

image.png

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

image.png

就是无符号长整型,8字节。
(2)union通过前面的介绍,里面的struct_originalPreoptCache是共用内存的,我们只看一个就可以了,最简单的是_originalPreoptCache,就是一个preopt_cache_t指针,所以也是8字节。

总结:通过分析我们知道了cache的大小就是16字节。

2.2 bits分析

class里面属性我们已经知道isasuperclass,还剩cachebitscache看名字就知道是缓存用的,所以接下来我们就先研究一下bits
x/8gx查看一下MlqqObject内存结构:

image.png

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

能正常打印出来是class_data_bits_t类型指针,说明我们移位没问题。

我们只拿一个指针没什么用,关键是要看指针里面指向的内容是什么,在objc_class的可以找到下面的方法:

class_rw_t *data() const {
    return bits.data();
}

可以看到bits有一个data()方法,我们也可以调用,看看里面返回什么内容:

image.png

确实是返回了一个class_rw_t类型的指针,接着看一下这个指针的内容是什么
image.png

看到这里还是没能看到我们想看的内容,既然是类的结构,那总要看到跟类相关的方法、属性等内容吧。我们打印的$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定义的方法和属性:


image.png

2.3.1 获取methods

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

image.png

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

image.png

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

image.png

打印一下$6的值:

image.png

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

image.png

取出来变量$8没有信息,但是可以看到是一个method_t类型,搜一下源码可以看到method_t定义中有很多方法,我们这里打印一下其描述信息看一下,先看一下方法:

objc_method_description *getDescription() const {
        return isSmall() ? getSmallDescription() : (struct objc_method_description *)this;
    }
image.png

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

image.png

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

image.png

这样可以将类里面的方法全部遍历出来了。但是我们没有看到+ (void)hobbyTest这个类方法。
超过数组数量就会发生越界。

2.3.2 获取properties

我们在来看看获取properties方式,前面跟获取methods一样,只是在将methods()方法换成properties()方法,其他的获取方式都是一样的:

image.png

这样我们就可以看到所有的属性了,但是我们没有看到hobby这个成员变量。

2.3.2 获取protocols

这个跟上面两个获取的方式一样,大家可以自己动手试试。

总结:这篇要介绍的已经介绍完了,还有两个问题:
1、+ (void)hobbyTest这个静态方法我们没看到存在哪里
2、hobby这个成员变量我们也没看到存在哪里

2.4 获取成员变量

其实成员变量是存在结构体class_rw_tro里面,获取方式如图:

image.png

获取结构体class_ro_tivars
image.png

image.png

这样就可以取到类的成员变量了

2.5 获取类方法

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

image.png

接下来就是获取methods,参考2.3.1就不一步步展示了,直接看结果:
image.png

这里验证了类方法是存在元类里面的

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;
}

来看看打印的结果:

image.png

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;
}

看看结果:

image.png

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的元类了,看到这里我们在来看method4method8metaClass都是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等都是临时变量,方便调用

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容