iOS runtime(2)-class结构和消息转发机制

1. class结构

一. class结构

其实类对象和元类对象的结构是相同的,元类对象是一种特殊的类对象.由于类对象和元类对象结构相同,但我们为什么感觉类对象只有对象方法列表,元类对象只有类对象列表呢,原因是不需要的数据都变为nil.
下图是class结构图

Class结构.png

二. class_rw_t(可修改的)

class_rw_t里面的methods、properties、protocols是二维数组,是可读可写的,包含了类的初始内容、分类的内容.

class_rw_t的结构.png

三. class_ro_t(不可修改的)

class_ro_t里面的baseMethodList、baseProtocols、ivars、baseProperties是一维数组,是只读的,包含了类的初始内容.

class_ro_t的结构图.png

注:在runtime的过程中会将ro中的methods和分类中的methods合并到rw中的methods中,class的bits原来的指向是指向ro的,在runtime的过程中bits的指向由指向ro改变成指向rw

四. method_t

nmethod_t的结构体是对方法\函数的封装.

struct method_t{
      SEL name;      //函数名
      const char *types;    //编码(返回值类型、参数类型)
      IMP imp;         //指针函数的指针(函数地址)
};

IMP代表函数的具体实现

typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull,...);

SEL代表方法\函数名,一般叫做选择器,底层结构跟char *类似

  • 可以通过@selector()sel_registerName()获得.
  • 可以通过sel_getName()NSStringFromSelector()转成字符串.
  • 不同类中相同名字的方法,所对应的方法选择器是相同的.
typedef strct objc_selector *SEL;

types包含了函数返回值、参数编码的字符串

返回值 参数1 参数2 ..... 参数n

Type Encoding

Type Encoding.png

2. 方法缓存

Class内部结构中有个方法缓存(cache_t),用散列表(哈希表)来缓存曾经调用过的方法,可以提高方法的查找速度.

方法缓存图.png

散列表的原理:将key传递并计算出一个index(索引).

散列表原理.png

用key(selector)的值&_mask就是所需要的imp,如果取值&后selector和key值不相等,_mask-1后再做&的操作.存储的时候已经&_mask计算好了缓存在第几个位置,如果在计算的时候存储的位置有方法缓存,会做_mask-1后再&的操作.(_mask有个初始值,如果容量不足可以扩容,扩容的时候清空缓存).

3. 消息转发机制

一. objc_msgsend

OC的方法调用,也叫做消息机制,给方法调用者发送一条消息.

OC中的方法调用,其实都是转换成objc_msgsend函数调用的.

  • objc_msgsend的流程大致分为3个阶段:
    1.消息发送.
    2.动态方法解析.
    3.消息转发.

objc_msgSend执行流程 – 源码跟读流程

objc_msgSend执行流程.png

二. objc_msgSend执行流程01-消息发送

消息发送.png

如果调用的是父类的方法,会把方法缓存到当前类,如果调用的是自己的方法,会把方法的缓存到自己的类中.

三. objc_msgSend执行流程02-动态方法解析

动态解析的流程图.png
  1. 开发者可以实现以下方法,来动态添加方法实现.
  • +(BOOL)resolveInstanceMethod:(SEL)sel.
  • +(BOOL)resolveClassMethod:(SEL)sel.
  1. 动态解析过后,会重新走“消息发送”的流程.
  • 从receiverClass的cache中查找方法”这一步开始执行.

demo:

#import <Foundation/Foundation.h>

@interface CSPersion : NSObject
- (void)test;
@end

#import "CSPersion.h"
#import <objc/runtime.h>

void otherC(id self, SEL _cmd) {
    NSLog(@" %@-%s-%s",self,sel_getName(_cmd),__func__);
}

@implementation CSPersion

+ (BOOL)resolveInstanceMethod:(SEL)sel {
  
    if (sel == @selector(test)) {
        struct method_t *method = (struct method_t*)class_getInstanceMethod(self,@selector(other));
        class_addMethod([self class], sel, method->imp, method->types);
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
  
    if (sel == @selector(test)) {
        struct method_t *method = (struct method_t*)class_getInstanceMethod(self,@selector(other));
        class_addMethod([self class], sel,method_getImplementation(methd), method_getTypeEncoding(method));
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {

    if (sel == @selector(test)) {
        class_addMethod([self class], sel, (IMP)otherC, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

- (void)test {
    NSLog(@"test ...");
}
- (void)other {
    NSLog(@"other...");
}
@end

我们有三种方式进行方法动态解析,还是建议用第二种方式,第二种方式比较清晰.

四. objc_msgSend的执行流程03-消息转发

消息转发的意思是把消息发送给别人,交给能够处理消息的人.
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;方法返回的签名types不为nil时,就会调用- (void)forwardInvocation:(NSInvocation *)anInvocation ;.
生成NSMethodSignature

NSMethodSignature  *signature = [[NSMethodSignature signatureWithObjCTypes:"i@:i"]];
NSMethodSignature *signature = [[MJStudent alloc] init] methodSignatureForSelector:@selector(test:)];
消息转发流程.png
demo
@interface Cat : NSObject
- (int)test:(int)age;
@end

@implementation Cat
- (int)test:(int)age {
    NSLog(@"%s",__func__);
    return age * age;
}
@end
/** 消息发送 */
@interface Student : NSObject
- (void)test:(int)age;
@end

@implementation Student

//+ (BOOL)resolveInstanceMethod:(SEL)sel
//{
//    class_addMethod(<#Class  _Nullable __unsafe_unretained cls#>, <#SEL  _Nonnull name#>, <#IMP  _Nonnull imp#>, <#const char * _Nullable types#>)
//}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(test:)) {
        
        // 测试一
//        return [NSMethodSignature signatureWithObjCTypes:"v20@0:8i16"];
        
        // 测试二
//        return [NSMethodSignature signatureWithObjCTypes:"i@:i"];
        
        // 测试三
//        return [[[Cat alloc] init] methodSignatureForSelector:aSelector];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    // 参数顺序:receiver、selector、other arguments
    
    // 测试一
//    int age;
//    [anInvocation getArgument:&age atIndex:2];
//    NSLog(@"%d", age + 10);
    
    // 测试二
    // anInvocation.target == [[MJCat alloc] init]
    // anInvocation.selector == test:
    // anInvocation的参数:15
//    [[[Cat alloc] init] test:15];
    
    // 测试三
    [anInvocation invokeWithTarget:[[Cat alloc] init]];
    int ret;
    [anInvocation getReturnValue:&ret];
    NSLog(@"%d", ret);
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 2.消息转发
        Student *stu = [[Student alloc] init];
        [stu test:10];
    }
    return 0;
}

五. objc_msgSend-类方法消息转发

+ (id)forwardingTargetForSelector:(SEL)aSelector为nil时,会继续调用+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector,如果methodSignatureForSelector为nil,则会报一个非常经典的错误doesNotRecognizeSelector,我们可以看出从方法我们只有在methodSignatureForSelector为nil时才会报错.

@interface CSCat : NSObject
+ (void)test;
- (void)test;
@end

@implementation CSCat
+ (void)test {
    NSLog(@"%s", __func__);
}

- (void)test {
    NSLog(@"%s", __func__);
}
@end
/** 类方法的转发过程 */
@interface CSPerson : NSObject
+ (void)test;
@end

@implementation CSPerson

+ (id)forwardingTargetForSelector:(SEL)aSelector {
    // objc_msgSend([[MJCat alloc] init], @selector(test))
    // [[[MJCat alloc] init] test]
    // 该方法显示与注释后有不同的结果
//    if (aSelector == @selector(test)) {
//        return [[CSCat alloc] init];
//    }
    
    return [super forwardingTargetForSelector:aSelector];
}

+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(test)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }

    return [super methodSignatureForSelector:aSelector];
}

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

推荐阅读更多精彩内容