底层研究 - 类的底层探索(中)

前言

底层研究 - 类的底层探索(上)中我们已经探索得知了类对象本质为objc_class结构体,同时类中的信息都存储在 class_rw_t 和 class_ro_t 中,那么接下来的重点就是探索下 class_rw_t 和class_ro_t以及使用runtime来获取类的基本信息。

1、clean memory & dirty memory

在WWDC 2020有一段介绍提到了关于class_rw_t 和 class_ro_t 之间的关系,推荐观看,十分牛掰!!o( ̄▽ ̄)d

Advancements in the Objective-C runtime

接下来我们结合视频和源码进行探索下,首先我们要先知道2个概念:

  • clean memory

clean memory 是指加载后不会发生改变的内存。它可以进行移除来节省更多的内存空间,需要时再从磁盘加载。
class_ro_t就是属于 clean memory

  • dirty memory

dirty memory是指在运行时会发生改变的内存。当类开始使用时,系统会在运行时为它分配一块额外的内存空间,也就是dirty memory,只要进程在运行,它就会一直存在,因此使用代价很高。

2、 ro & rw & rwe

  • class_ro_t

ro,也就是readonly,class_ro_t 是在编译的时候生成的。当类在编译的时候,类的属性,实例⽅法,协议这些内容就存在class_ro_t这个结构体⾥⾯了,这是⼀块clean memory,不允许被修改,但可以在不使用的时候被删除,需要的时候再从磁盘加载。

class_ro_t

  • class_rw_t

rw,即readwrite,class_rw_t是在运⾏的时候⽣成的。当一个类第一次被使用时,rumtime会为其分配额外的内存,即class_rw_t ,它会先将class_ro_t的内容 剪切 到class_rw_t中存储,整个内存中其实只有一份

通过 First SubclassNext Sibling Class 指针实现了把类连接成一个树状结构,这就决定了runtime能够遍历当前使用的所有类

class_rw_t

可以发现Methods、Properties、Protocols不仅存在于ro,也存在了rw中,这是因为在运行时它们是可以发生改变的,比如通过category添加方法或者通过runtime的api动态添加它们,系统需要去跟踪这些内容。

rw拥有Methods、Properties、Protocols

但按这种做法,会导致占用相当多的内存,那怎么取缩小这些结构呢?

在对bits的源码探索中,我们发现了 rw 提供三个方法method()、properties()、protocols()来分别返回方法,属性和协议,也就是说rw并没有直接存储这三者,而是存在于一个ro_or_rw_ext,因为rw属于dirty memory,使用开销大,因为把一些类的信息分离出来,能减小开销。

method()、properties()、protocols()

  • class_rw_ext_t

class_rw_ext_t可以减少内存的消耗。苹果在wwdc2020⾥⾯说过,只有⼤约10%左右的类需要动态修改。所以只有10%左右的类⾥⾯需要⽣成class_rw_ext_t这个结构体。这样的话,可以节约很⼤⼀部分内存。对于动态修改的类可以通过class_rw_t结构体中提供的ext()方法获取class_rw_ext_t。

内存结构之class_rw_ext_t

我们通过源码也可以发现,在methods方法中,也是先判断的是否存在rwe,有则从rwe获取方法,无则直接从ro中获取。

rwe的使用

那么 class_rw_ext_t 的⽣成的条件是什么呢?

  1. ⽤过runtime的Api进⾏动态修改的时候。
  2. 有分类的时候,且分类和本类都为⾮懒加载类的时候。实现了+load⽅法即为⾮懒加载类。

因此我们也能得到一个rw,ro,rwe的具体关系图


rw,ro,rwe的具体关系图

3、runtime的基本使用

  • 获取类的成员变量
-(void)class_copyIvarList:(Class)pClass {
    unsigned int  outCount = 0;
    Ivar *ivars = class_copyIvarList(pClass, &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(@"name = %s type = %s",cName,cType);
    }
    free(ivars);
}
  • 获取类的属性
-(void)class_copyPropertyList:(Class)pClass {
    unsigned int outCount = 0;
    objc_property_t *perperties = class_copyPropertyList(pClass, &outCount);
    for (int i = 0; i < outCount; i++) {
        objc_property_t property = perperties[i];
        const char *cName = property_getName(property);
        const char *cType = property_getAttributes(property);
        NSLog(@"name = %s type = %s",cName,cType);
    }
    free(perperties);
}
  • 获取类的方法
-(void)class_copyMethodList:(Class)pClass {
    unsigned int outCount = 0;
    Method *methods = class_copyMethodList(pClass, &outCount);
    for (int i = 0; i < outCount; i++) {
        Method method = methods[i];
        NSString *name = NSStringFromSelector(method_getName(method));
        const char *cType = method_getTypeEncoding(method);
        NSLog(@"name = %@ type = %s",name,cType);
    }
    free(methods);
}
  • 交换方法的实现,仅限当前类有效
-(void)class_replaceMethod{
     //参数:1.类Class 2.⽅法名SEL 3.⽅法的实现IMP 4.⽅法参数描述
     //返回:BOOL
    BOOL result = class_replaceMethod([self class], @selector(method1), [self
    methodForSelector:@selector(method2)], NULL);
    NSLog(@"class_replaceMethod:%d",result);
}

-(void)method1{
    NSLog(@"method1");
}

-(void)method2{
    NSLog(@"method2");
}

  • 分类交换方法
+(void)load{
    NSString *className = NSStringFromClass(self.class);
    NSLog(@"classname %@", className);
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = object_getClass((id)self);
 
        SEL originalSelector = @selector(systemMethod_PrintLog);
        SEL swizzledSelector = @selector(ll_imageName);
 
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

        //在这里通过class_addMethod()的验证,如果self实现了这个方法,class_addMethod()函数将会返回NO
        BOOL didAddMethod =
        class_addMethod(class,
                        originalSelector,
                        method_getImplementation(swizzledMethod),
                        method_getTypeEncoding(swizzledMethod));
        if (didAddMethod) {
            class_replaceMethod(class,
                                swizzledSelector,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

稍微讲解下:

  • dispatch_once
    dyld虽然能够保证调用Class的load时是线程安全的,但还是推荐使用dispatch_once做保护,防止极端情况下load被显示强制调用时,重复交换(第一次交换成功,下次又换回来了...),造成逻辑混乱。

  • class_addMethod
    给指定Class添加一个SEL的实现(或者说是SEL和指定IMP的绑定),如果该SEL在父类中有实现,则会添加一个覆盖父类的方法,添加成功返回YES,SEL已经存在或添加失败返回NO。
    因为iOS Runtime消息传递机制的影响,只执行method_exchangeImplementations操作时可能会影响到父类的方法,这也是先使用class_addMethod的原因。

  • class_replaceMethod

  1. 如果该Class不存在指定SEL,则class_replaceMethod的作用就和class_addMethod一样;
  2. 如果该Class存在指定的SEL,则class_replaceMethod的作用就和method_setImplementation一样。

4、总结

  1. clean memory是指加载后不会发生改变的内存
  2. dirty memory是指在运行时会发生改变的内存
  3. ro、rw、rwe具体流程
    (1) 当编译的时候,类会自动生成ro
    (2) 当类被使用时,会生成rw,并把ro 剪切 到rw中
    (3) 当类被修改时,rw内会新增rwe,把允许修改的内容转移到rwe,读取时优先读取rwe的内容
    (4) 内存不足时,ro 可以在不使用的时候被移除
  4. 常用的runtime方法:
    (1) 成员变量:class_copyIvarList、ivar_getName、ivar_getTypeEncoding
    (2) 属性:class_copyPropertyList、property_getName、property_getAttributes
    (3) 方法:class_copyMethodList、method_getTypeEncoding、class_replaceMethod、class_addMethod、method_exchangeImplementations
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,458评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,030评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,879评论 0 358
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,278评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,296评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,019评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,633评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,541评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,068评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,181评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,318评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,991评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,670评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,183评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,302评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,655评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,327评论 2 358

推荐阅读更多精彩内容