类的底层探索(下)

对象的实例方法和成员变量是存储的类对象的结构体class_rw_t中,在class_rw_t中properties里没有成员变量,那么成员变量储存在哪里呢?

实例变量储存位置

在Apple官方WWDC视频中有介绍类的运行时数据变化

  • 首先class包括Metaclass元类,Superclass超类,Method cache方法缓存,还有一个指向更多数据的指针,存储额外信息的地方,叫做class_ro_t(ro表示只读)
  • class_ro_t包括像类名称,方法,协议和实例变量的信息,swift类和OC类共享这一基础结构
  • 当类第一次从磁盘加载到内存时,一经使用,就会发生变化,class_ro_t 是clean memory (指创建后不会更改的内存),class_rw_t是dirty memory(进程运行时会发生改变的内存),类结构一经使用就变成了dirty memory,因为运行时会向它写入新的数据,例如,创建一个新的方法缓存并从类中指向它。dirty memory 比 clean memory昂贵的多,只要进程在运行,dirty memory就一直存在,而clean memory可以从系统中移除,当你需要的时候可以从磁盘中重新加载。dirty memory是数据分为两部分的主要原因,可以保持clean memory中的数据越多越好。通过分离出那些永远不可能更改的数据,可以把大部分数据储存为clean memory。
  • 运行时需要追踪更多类的信息,所以当一个类首次被使用,运行时会为它分配额外的储存空间classs_rw_t用于读取-编写数据,在这个数据结构中,存储了只有运行时才会生成的新信息,例如,所有的类都会链接成一个树状结构,这是通过使用class_rw_t中First Subclass和Next Sibling Class指针实现的,这允许运行时遍历当前使用的所有的类,这会使方法缓存失效。
  • 但为什么方法和属性也在ro数据中时,class_rw_t也存在方法和属性,是因为他们在运行时可以被改变,当category被加载的时候,它可以向类添加新的方法,也可以通过运行时的API动态添加,因为class_ro_t是只读的,所以需要在class_rw_t中去追踪。那这样的结果就是会使用更多的内存,在任何给定的设备都有很多类在使用,Apple官方在一些iPhone设备上测定,大约30M字节这些class_rw_t,那么该怎么改善呢?


    image.png
  • 因为在读取-编写时,需要这些数据,但是通过检查实际设备这些数据的使用情况,Apple官方发现大约10%的类真正改变了他们的方法,所以拆掉那些平时不用的类,class_rw_t就缩小了将近一半。比如Demangled Name这个swift类只有在询问oc内容时才会用到。


    image.png
  • 对于那些确实需要额外信息的类,可以分配这些扩展记录中的一个,并把它划到其他类中使用,大约90%到类都不需要这些扩展信息,可以节省大约14M的内存用于其他用途,我们在Mac上可以通过heap命令,这个命令可以查看正在运行的进程所使用的堆内存


    image.png

    heap Mail | egrep 'class_rw|COUNT'
  • 通过上图可以看出,有690个需要class_rw_t,但是只有143是必须要使用扩展的,这样可以清楚的计算出通过这个改变节省的内存,大约节省了1M字节的四分之一,这还只是对于Mail app来讲,如果在系统范围内进行推广,那么节省的内存时非常可观的
  • 从很多类中获取数据的代码必须同时处理有扩展和没有扩展的类,这些运行时都会做到,而且通过这个改变更加节省内存。之所以这样,是因为读取这些结构的代码,都在运行时并且同步更新。
    了解这些背景之后,那么我们就来探索一下class_ro_t中的内容。先找到对应的class_rw_t


    class_rw_t

    class_rw_t中找到了ro方法

    class_ro_t中的ivars就是我们的成员变量

    得到了一个ivar_list_t继承自entsize_list_tt也是一个容器


    里面有name属性生成的_name成员变量也有我们在extension中定义的_hobby成员变量

    为什么成员变量要放在类里面而成员变量的值要放在实例对象里面呢?
    类的本质是一个结构体,相当于一个模板,就有方法成员变量属性等等,在创建实例对象的时候,值是不一样的,所以值要放在实例对象里面。

类方法储存在哪里呢?

先找到掩码0x00007ffffffffff8ULL,通过地址位移获取我们的元类内存地址,用类对象的isa指针地址 & 掩码获得元类的地址


intel的找这个x86的掩码
获取元类存储的类方法

从图中在元类方法中的方法列表中找到了类方法。


TTPerson声明了test的类方法

为什么要设计元类?

  • 为了复用消息机制。objc_msgSend(id,sel)
  • 在OC的世界里一切皆对象(借鉴于Smalltalk),metaclass的设计就是要为满足这一点。
  • 在OC中Class也是一种对象,它对应的类就是metaclass,metaclass也是一种对象,它的类是root metaclass,在往上根元类(root metaclass)指向自己,形成了一个闭环,一个完备的设计。
  • 如果不用metaClass,会将消息传递复杂化,要去判断消息接受者是类对象还是实例对象,sel是类方法还是实例方法。
  • 元类对象就去储存类方法,类对象就储存实例方法,实例对象就去储存实例变量的值,实行单一职责,提高了消息发送的效率,也更容易维护。

获取类的属性和成员变量

//获取类的属性
- (void)tt_copy_propertyList:(Class)tclass{
    unsigned int outCount = 0;
    objc_property_t *properties = class_copyPropertyList(tclass, &outCount);
    for (int i = 0; i<outCount; i++) {
        objc_property_t property = properties[I];
        const char *cName = property_getName(property);
        const char *cType = property_getAttributes(property);
        NSLog(@"%u name = %s type = %s",outCount,cName,cType);
    }
    free(properties);
}

//获取类的成员变量
- (void)tt_copy_ivars:(Class)tclass{
    unsigned int outCount = 0;
    Ivar *ivars = class_copyIvarList(tclass, &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(@"%u name = %s type = %s",outCount,cName,cType);
    }
    free(ivars);
    
}
//获取类的方法
- (void)tt_copy_methodList:(Class)tclass{
    unsigned int outCount = 0;
    Method *methods = class_copyMethodList(tclass, &outCount);
    for (int i = 0; i < outCount; i++) {
        Method method = methods[I];
        const char *cName = method_getName(method);
        const char *cType = method_getTypeEncoding(method);
        NSLog(@"%u name = %s type = %s",outCount,cName,cType);
    }
    free(methods);
}
获取成员变量,属性,方法

//name = test type = v16@0:8
v返回值类型void
16 v8个长度,消息接收者8个长度

  • @:固定写法,@消息接收者,:方法名

// name = setName: type = v24@0:8@16
24 v8个字节,@0:8占8个字节@是一个NSString的对象参数从第16个字节开始占用8字节,共24

返回值类型

  • c char
  • I int
  • s short
  • l long 咋爱64位处理器上也是按照32位处理
  • q long long
  • C unsigned char
  • I unsigned int
  • S unsigned short
  • L unsigned long
  • Q unsigned long long
  • f float
  • d double
  • B 代表C++中的bool或者C99的_Bool
  • v void
  • '*' char *
  • @ 代表对象类型
  • '#' 代表类对象(class)
  • : 代表方法selector(SEL)
  • [array type]代表array
  • {name=type....}代表结构体
  • (name=type...)代表union
  • bnum A bit field of num bits
  • ^type A pointer of type
  • ? An unknown type

T类型,V_name成员变量

  • R readOnly
  • C copy
  • & retain
  • N nonatomic
  • G<name> The property defines a custom getter selector name.The name follows the G(for example,GcustomGetter)
  • S <name> The property defines a custom setter selector name.
  • D @dynamic
  • W __weak
  • P The property is eligible for garbage collection
  • t<encodeing>Specifies the type using ole-style encoding
-(void)methodTest:(Class)pClass {
    const char *className = class_getName(pClass);
    Class metaClass = objc_getMetaClass(className);
    
    Method method1 = class_getInstanceMethod(pClass, @selector(instanceMethod));
    Method method2 = class_getInstanceMethod(metaClass, @selector(instanceMethod));
    Method method3 = class_getInstanceMethod(pClass, @selector(classMethod));
    Method method4 = class_getInstanceMethod(metaClass, @selector(classMethod));
    NSLog(@"%p - %p - %p - %p",method1,method2,method3,method4);
}

-(void)impTest:(Class)pClass {
    const char *className = class_getName(pClass);
    Class metaClass = objc_getMetaClass(className);
    
    IMP imp1 = class_getMethodImplementation(pClass, @selector(instanceMethod));
    IMP imp2 = class_getMethodImplementation(metaClass, @selector(instanceMethod));
    IMP imp3 = class_getMethodImplementation(pClass, @selector(classMethod));
    class_getClassMethod(<#Class  _Nullable __unsafe_unretained cls#>, <#SEL  _Nonnull name#>)
    IMP imp4 = class_getMethodImplementation(metaClass, @selector(classMethod));
    NSLog(@"%p - %p - %p - %p",imp1,imp2,imp3,imp4);
    //isa---
    
    //isa --
}

Method method3 = class_getInstanceMethod(pClass, @selector(classMethod));获取元类方法通过class_getInstanceMethod方法也可以获取到,这是为什么呢?


class_getClassMethod实现

发现class_getClassMethod内部其实也是返回了class_getInstanceMethod方法,所以通过class_getInstanceMethod也可获取到,所以进一步体现了元类的设计不仅仅是为了存储类方法,更是为了消息的复用。

获取方法实现

-(void)impTest:(Class)pClass {
    const char *className = class_getName(pClass);
    Class metaClass = objc_getMetaClass(className);
    
    IMP imp1 = class_getMethodImplementation(pClass, @selector(instanceMethod));
    IMP imp2 = class_getMethodImplementation(metaClass, @selector(instanceMethod));
    IMP imp3 = class_getMethodImplementation(pClass, @selector(classMethod));
    IMP imp4 = class_getMethodImplementation(metaClass, @selector(classMethod));
    NSLog(@"%p - %p - %p - %p",imp1,imp2,imp3,imp4);
}
//打印结果:
//0x1007df700 - 0x1007df700 - 0x1007df700 - 0x1007df700

从打印结果看出,从类里面获取实例方法实现可以找到,从元类获取实例方法的实现也可以找到,从元类里面去获取类方法实现也可以找到,在类里面获取类方法实现也可以找到。

//寻找方法实现的实现,当找不到的时候,会进行消息转发,所以上述代码找不到对应的方法实现的时候,返回的内存地址是一样的。
__attribute__((flatten))
IMP class_getMethodImplementation(Class cls, SEL sel)
{
    IMP imp;

    if (!cls  ||  !sel) return nil;

    lockdebug_assert_no_locks_locked_except({ &loadMethodLock });

    imp = lookUpImpOrNilTryCache(nil, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER);

    // Translate forwarding function to C-callable external version
    if (!imp) {
        return _objc_msgForward;
    }

    return imp;
}

总结

  • 成员变量储存在类对象的class_rw_t结构体中的class_ro_t中,类方法储存在元类中,而设计元类也是为了复用消息机制,因为元类储存类方法,类对象储存实例方法和成员变量,实例对象储存成员变量的值,这样单一职责,提高了消息服用的效率,减少了消息传递的复杂性。
  • class_rw_t是dirty memory,class_ro_t是clean memory,因为类一旦使用就需要动态修改,需要在class_rw_t中去追踪更多的信息,就会使用更多的内存,为了节省内存使用,拆掉平时不用的90%的类,增加了class_rw_ext_t,这里储存需要更改的必要扩展。
  • class_ro_t必要可以从内存中移除,需要的时候再从磁盘中重新开辟。class_rw_t从类使用时创建,一直存在,直到进程销毁。
  • class_rw_ext_t可以减少内存的消耗。苹果在wwdc2020里面说过,只有大约10%左右的类需要动 态修改。所以只有10%左右的类里面需要生成class_rw_ext_t这个结构体。这样的话,可以节约很 大一部分内存。
  • class_rw_ext_t生成的条件:
    第一:用过runtime的Api进行动态修改的时候。 第二:有分类的时候,且分类和本类都为非懒加载类的时候。实现了+load方法即为非懒加载类。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,504评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,434评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,089评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,378评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,472评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,506评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,519评论 3 413
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,292评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,738评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,022评论 2 329
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,194评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,873评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,536评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,162评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,413评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,075评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,080评论 2 352

推荐阅读更多精彩内容