iOS底层 - 类的结构分析

iOS开发底层探究之路

类的结构分析

类分析初探

首先我们先创建一个类LGPerson,再创建一个继承于LGPerson的子类LcrTeacher

@interface LGPerson : NSObject
{
    NSString *hobby;
}
@property (nonatomic, copy) NSString *nickName;
@property (nonatomic, strong) NSString *name;
-(void)sayHello;
+(void)say88;
@end
#import "LGPerson.h"
@implementation LGPerson
-(void)sayHello{    
}
+(void)say88{   
}
@end

#import "LGPerson.h"
@interface LcrTeacher : LGPerson
@end
#import "LcrTeacher.h"
@implementation LcrTeacher
@end

在main.m文件中我们创建两个对象:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        //ISA_MASK  0x00007ffffffffff8ULL
        LGPerson *person = [LGPerson alloc];
        LcrTeacher *teacher = [LcrTeacher alloc];
        
        NSLog(@"%@ ----  %@",person,teacher);
    }
    return 0;
}

利用lldb调试我们打印出下面图片中一些信息:

类结构初探

如上图所示,12处的打印结果都为LGPerson1处是因为person对象的指针指向类,所以当此对象的指针地址&ISA_MASK时,所得到的就是LGPerson类地址,打印即为LGPerson。2 处打印的地址是类的isa&ISA_MASK后得到的元类的信息,那么这个元类到底是什么?

元类

对象的isa指向的是所对应的类的信息,类其实也是一个对象,它的内存信息中保存的位域信息所指向的就是由苹果定义的元类
1.元类是由系统定义创建的,这个过程是在编译期间完成的。
2.元类的类对象,元类中保存着类的相关类方法信息
3.元类是没有自己的名字,由于与当前类相关联,所以苹果直接拿当前类名命名元类。

由此,上面的12处打印的都为LGPerson就是元类导致的。
通过lldb可以调试元类信息链,即isa走向,对象--->类--->元类--->根元类--->根类(NSObject):

isa走位图分析

上图可见:

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

著名isa及Superclass走位图

isa流程图.png

结合上面的工程,将我们自定义的类信息放上去,isaSuperclass走向一目了然:

  • isa走向:

    • teacher走位链:teacher对象) ---> LcrTeacher) --->LcrTeacher元类) --->NSObject根元类)--->NSObject自己) 。
    • person 走位链:person对象)---> LGPerson) ---> LGPerson元类) --->NSObject根元类)--->NSObject自己) 。
  • Superclass走向:

    • 的走向链:Lcrteacher(子类)---> LGPerson(父类) ---> NSObject(根类) --->nil
    • 元类的走向链:LcrTeacher(子类元类) ---> LGPerson(父类元类) ---> NSObject(根元类) ---> NSObject(根类) ---> nil

objc_object & objc_class

在继续下面类的结构分析之前,我们先来了解objc_object 和 objc_class两个概念,以及它们之间是怎样的关系?

objc_class:
在上篇文章iOS底层 - isa与类关联的原理 中,通过clang编译过的main.cpp文件中可以看到类的底层结构:

struct NSObject_IMPL {
    Class isa;
};
typedef struct objc_class *Class;

NSObject 被编译成了一个NSObject_IMPL结构体,里面有一个Class类型的isa,类型Class 是由objc_class 定义的。
在源码objc4-781源码中我们搜索objc_class,找到最新源码:

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

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

    void setInfo(uint32_t set) {
        ASSERT(isFuture()  ||  isRealized());
        data()->setFlags(set);
    }

    void clearInfo(uint32_t clear) {
        ASSERT(isFuture()  ||  isRealized());
        data()->clearFlags(clear);
    }
//略
}

objc_class的源码中看出objc_class 是继承自objc_object的。

objc_object:
查看源码,在objc.h及objc_private.h两个文件中分别能找到objc_object的源码实现:

struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};
struct objc_object {
private:
    isa_t isa;

public:

    // ISA() assumes this is NOT a tagged pointer object
    Class ISA();
 //略
}

那么objc_classobjc_object 以及 NSObject对象到底是什么关系?

  • objc_class 继承自objc_object,所以objc_class中有一个继承过来的isa属性。
  • 通过clang编译的main.cpp 中看出NSObject_IMPL 中有一个Class类型的isa,而Class是由objc_class定义的,而objc_class 又继承自objc_object,所以可知所有以NSObject所创建的对象都有一个isa属性。
  • objc_object是一个对象创造的根类,所有对象创建之后都带有objc_object的特性,带有一个isa属性。

总结

  • 所有的对象++元类都有isa
  • 所有的对象都是由objc_object继承来的。
  • 简单概括就是万物皆对象,万物皆来源于objc_object,有以下两点结论:
    • 所有以 objc_object为模板 创建的对象,都有isa属性
    • 所有以objc_class为模板,创建的类,都有isa属性
  • 在结构层面可以通俗的理解为上层OC底层的对接:
    • 下层是通过 结构体 定义的 模板,例如objc_classobjc_object
    • 上层 是通过底层的模板创建的 一些类型,例如LGPerson
      可以用下图表示出:
      objc_object & objc_class & isa

类的结构分析

上面分析可知类是以objc_class为模版创建的,那么我们就从objc_class入手研究类底层结构:

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

    class_rw_t *data() const {
        return bits.data();
    }
//省略下方方法
}
  • 继承于objc_object,默认带有一个isa,大小为8字节。
  • 父类superclass,objc_class定义的一个Class类型的结构体指针,大小也为8字节。
  • cache 类的缓存信息,结构体cache_t类型,我们在下面会计算cache属性大小。(cache是一个结构体,而不是结构体指针,不能简单地认为大小8字节)
  • bits 存放类的一些信息,class_data_bits_t 结构体类型,接下来我们就针对这个bits里存放的信息进行一步步探究。

cache大小

struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
    explicit_atomic<struct bucket_t *> _buckets;  //8
    explicit_atomic<mask_t> _mask;    //4
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    explicit_atomic<uintptr_t> _maskAndBuckets; // 8
    mask_t _mask_unused; // 4
//过滤一些静态的,因为静态不计入结构体的大小
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    // _maskAndBuckets stores the mask shift in the low 4 bits, and
    // the buckets pointer in the remainder of the value. The mask
    // shift is the value where (0xffff >> shift) produces the correct
    // mask. This is equal to 16 - log2(cache_size).
    explicit_atomic<uintptr_t> _maskAndBuckets;
    mask_t _mask_unused;

    static constexpr uintptr_t maskBits = 4;
    static constexpr uintptr_t maskMask = (1 << maskBits) - 1;
    static constexpr uintptr_t bucketsMask = ~maskMask;
#else
#error Unknown cache mask storage type.
#endif
    
#if __LP64__
    uint16_t _flags;          //2
#endif
    uint16_t _occupied;   //2
//省略下面的方法,结构体大小只和属性有关
}

有上面代码可得,cache大小为16,所以8 + 8 + 16 ,想获得bts里的信息的话,我们可利用指针偏移32来获取类信息中的bits,这样就可以访问类相关的一些信息了。

lldb调试查看类信息bits

其中data()方法获取信息,是objc_class提供的方法:

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

目前已经拿到了bits信息,但是还不够,我们暂时还看不到一些具体的类的信息,所以我们继续往下探索:

bits 查看属性列表 property_list

由于上方获取到的bits是class_rw_t类型结构体, 查看class_rw_t 源码,我们可以找到类的一些相关信息:


class_rw_t相关类的信息

可以看到,方法列表methods、属性列表properties、协议列表protocols等类的信息。
既然有这些信息,我们不妨试着去获取它们,并且将它们打印:

bits的属性列表获取

所以,通过点方法.properties(),就可以获取到类的属性列表,再通过p *$6 打印$6里的数据,可以获得property_list_t类型的属性列表,进而打印.get()方法,获取每一个属性。.get()一直获取属性的话,会造成数组越界的结果,因为类的属性个数是固定的,文章头部的LGPerson有两个属性(nickNamename),所以此处只能.get()到这两个属性.

bits 查看属性列表 method_list

@interface LGPerson : NSObject
{
    NSString *hobby;
}
@property (nonatomic, copy) NSString *nickName;
@property (nonatomic, strong) NSString *name;

-(void)sayHello;
+(void)say88;

@end

下面再利用lldb调试,是否也能像属性列表一样方式去查看类的方法列表:

bits获取方法列表

由上图可知,可以利用.methods()获取bits信息中的方法列表,获取到的也是一个method_list_t类型的方法列表信息,然后利用.get()方法一个个获取到方法。LGPerson 共有6个方法,实例方法sayHello.cxx_destructname属性的setgetnickName属性的setget

最后留下两个问题:成员变量hobby在哪里?以及类方法+say88又在哪里?下篇文章我们一一揭晓。

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