Runtime运行时机制

Runtime简介

Objective-C是一门动态性比较强的编程语言,跟C、C++等语言有着很大的不同,允许很多操作推迟到程序运行时再进行。
Objective-C的动态性是由Runtime API来支撑和实现的,Runtime API提供的接口基本都是C语言的,源码由C\C++\汇编语言编写。
学习 Runtime 机制前需要先了解OC 对象本质,否则看不懂以下内容。
平时编写的OC代码,底层都是转换成了Runtime API进行调用。

class的结构
//struct objc_class 结构
struct objc_class{
  Class isa;
  Class superclass;
  Cache_t cache;//方法缓存
  class_data_bit_t bit;//用于获取类的具体信息
}
// bit & FAST_DATA_MASK 得到类的具体信息
struct class_rw_t {  // rw表示可读写
    uint32_t flags;
    uint32_t version;
    const class_ro_t *ro;
    method_array_t methods; //method_t 二维数组
    property_array_t properties; //属性二维数组
    protocol_array_t protocols; //协议二维数组
    Class firstSubclass;
    Class nextSiblingClass;
    char *demangledName;
}
//struct class_ro_t 的结构
struct class_ro_t { //ro表示只读
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif
    const uint8_t * ivarLayout;
    const char * name; //类名
    method_list_t * baseMethodList; //方法列表
    protocol_list_t * baseProtocols; //协议列表
    const ivar_list_t * ivars; //成员变量列表
    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;
}

class_rw_t里面的methods、properties、protocols是二维数组,是可读可写的,包含了类的初始内容、分类的内容
class_ro_t里面的baseMethodList、baseProtocols、ivars、baseProperties是一维数组,是只读的,包含了类的初始内容

//method_t结构
struct method_t{
  SEL name;//函数名
  const char *type;//编码(包含返回值类型,参数类型)
  IMP imp;//函数指针(指向函数的指针)
}
  • SEL代表方法\函数名,一般叫做选择器,底层结构跟char *(字符串)类似
    不同类中相同名字的方法,所对应的方法选择器是相同的

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

方法缓存

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

//cathe_t结构
struct cahe_t{
  struct bucket_t *buckets;//散列表
  mask_t _mask;//散列表的长度 - 1
  mask_t _occupied;//已经缓存的方法数量
}
//buket_t结构
struct_t buket_t{
  cahe_ket_t _key;//SEL作为key
  IMP _imp;//函数的内存地址
}
objc_msgSend执行流程

OC中的方法调用,其实都是转换为objc_msgSend(receiver,message)函数的调用。
objc_msgSend的执行流程可以分为3大阶段:消息发送;动态方法解析;消息转发。

  • objc_msgSend第一阶段:消息发送


    消息发送流程图
  • objc_msgSend第二阶段:动态方法解析


    动态方法解析流程图

开发者可以实现以下方法,来动态添加方法实现

+ (BOOL)resolveClassMethod:(SEL)sel
+ (BOOL)resolveInstanceMethod:(SEL)sel

动态解析对象方法

void c_other(id self, SEL _cmd)
{
    NSLog(@"c_other:%@ - %@", self, NSStringFromSelector(_cmd));
}

- (void)other
{
    NSLog(@"%s", __func__);
}

+ (BOOL)resolveClassMethod:(SEL)sel
{
    if (sel == @selector(test)) {
        // 参数 类对象,sel, 方法实现地址,方法参数编码(包含返回值,参数)
        // 方式1:直接添加方法实现
        // class_addMethod(object_getClass(self), sel, (IMP)c_other, "v16@0:8");
        // 方式2: 获取其他方法添加
        // 获取其他方法
        Method method = class_getInstanceMethod(self, @selector(other));

        // 动态添加test方法的实现
        class_addMethod(self, sel,
                        method_getImplementation(method),
                        method_getTypeEncoding(method));
        return YES;
    }
    return [super resolveClassMethod:sel];
}

动态解析类方法

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    if (sel == @selector(test)) {
        // 获取其他方法
        struct method_t *method = (struct method_t *)class_getInstanceMethod(self, @selector(other));

        // 动态添加test方法的实现
        class_addMethod(self, sel, method->imp, method->types);

        // 返回YES代表有动态添加方法
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

动态解析过后,会重新走“消息发送”的流程
“从receiverClass的cache中查找方法”这一步开始执行

  • objc_msgSend第三阶段:消息转发


    image.png

开发者可以实现以下方法实现消息转发

- (id)forwardingTargetForSelector:(SEL)aSelector
//或
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
- (void)forwardInvocation:(NSInvocation *)anInvocation

消息转发

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    if (aSelector == @selector(test)) {
        // Cat 对象实现了 test 方法
        // objc_msgSend([[MJCat alloc] init], aSelector)
        return [[Cat alloc] init];
    }
    return [super forwardingTargetForSelector:aSelector];
}
// 方法签名:返回值类型、参数类型
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    if (aSelector == @selector(test)) {
        return [NSMethodSignature signatureWithObjCTypes:"v16@0:8"];
    }
    return [super methodSignatureForSelector:aSelector];
}

// NSInvocation封装了一个方法调用,包括:方法调用者、方法名、方法参数
//    anInvocation.target 方法调用者
//    anInvocation.selector 方法名
//    [anInvocation getArgument:NULL atIndex:0]
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
//    anInvocation.target = [[Cat alloc] init];
//    [anInvocation invoke];
// 上面两句等同于下面这句
    [anInvocation invokeWithTarget:[[Cat alloc] init]];
}
Category的加载处理过程

我们日常写的分类的底层结构如下

struct category_t {
    const char *name;//类名
    classref_t cls;
    struct method_list_t *instanceMethods;//对象方法列表
    struct method_list_t *classMethods;//类方法列表
    struct protocol_list_t *protocols; //协议列表
    struct property_list_t *instanceProperties;
    // Fields below this point are not always present on disk.
    struct property_list_t *_classProperties;

    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }
    property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};
  • category的加载处理过程
    1.通过runtime加载某个类的category数据
    2.把所有的category的方法、属性、协议数据合并到一个大数组中
    后面参与编译的category数据,会在数组的前面
    3.将合并后的分类数据(方法、属性、协议),插到类原来数据的前面
Runtime应用

runtime 常用 API 链接

  • 查看对象的私有成员变量,然后用 KVC 赋值改变私有成员变量的值
// 打印查看某个类的所有成员变量
- (void)lookIvarsFromClass:(Class)cls{
    unsigned int count;
    Ivar *ivars = class_copyIvarList(cls, &count);
    for (int i = 0; i < count; i ++) {
        Ivar ivar = ivars[i];
        NSLog(@"%s",ivar_getName(ivar));
    }
    free(ivars);
}
//改变placeholderLabel字体颜色
- (void)changePlaceholderColor{
    [self.textField setValue:[UIColor redColor] forKeyPath:@"_placeholderLabel.textColor"];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [super touchesBegan:touches withEvent:event];
    // 查看UITextField的所有成员变量
    [self lookIvarsFromClass:[UITextField class]];
}
  • 动态添加属性
    为 Person 对象动态添加属性(间接实现属性效果)
// Person+kj.m 分类 文件
@implementation Person (KJ)

static char nameKey;

- (void)setName:(NSString *)name{
    objc_setAssociatedObject(self, &nameKey, name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (NSString *)name{
    return  objc_getAssociatedObject(self, &nameKey);
}

@end
  • 字典转模型(MJExtension 框架)利用 Runtime遍历所有的属性或成员变量,然后利用 KVC设值
+ (instancetype)objectWithJson:(NSDictionary *)json{
    id obj = [[self alloc] init];
    
    unsigned int count;
    Ivar *ivars = class_copyIvarList(self, &count);
    // 遍历 obj 成员变量
    for (int i = 0; i < count; i ++) {
        Ivar ivar = ivars[i];
        NSMutableString *ivarName = [NSMutableString stringWithUTF8String:ivar_getName(ivar)];
        // 删掉前面下划线
        [ivarName deleteCharactersInRange:NSMakeRange(0,1)];
        // KVC 赋值
        [self setValue:json[ivarName] forKeyPath:ivarName];
    }
    
    free(ivars);
    return obj;
}
  • 利用消息转发解决找不到方法App闪退问题
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    // 本来能调用的方法
    if ([self respondsToSelector:aSelector]) {
        return [super methodSignatureForSelector:aSelector];
    }
    // 找不到方法
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}

// 找不到的方法,都会来到这里
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    NSLog(@"找不到%@方法", NSStringFromSelector(anInvocation.selector));
}
  • 替换方法实现(Method Swizzling) hook 系统方法,自定义处理自己的逻辑
#import "UIViewController+KJ.h"
#import <objc/runtime.h>

@implementation UIViewController (KJ)

// 类加载时调用,只会调用一次
+ (void)load{
    Method hookMethod = class_getInstanceMethod(self, NSSelectorFromString(@"hookViewDidLoad"));
    Method originalMethod = class_getInstanceMethod(self, @selector(viewDidLoad));
    method_exchangeImplementations(hookMethod, originalMethod);
}

- (void)hookViewDidLoad{
    // 调用原来的方法,咋看是循环调用,其实方法实现被交换了,这样只会调用原来的
    [self hookViewDidLoad];
    NSLog(@"加载了%@",[self class]);
    // 自定义业务,判断 class 统一埋点点处理等等
}

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

推荐阅读更多精彩内容

  • 我们常常会听说 Objective-C 是一门动态语言,那么这个「动态」表现在哪呢?我想最主要的表现就是 Obje...
    Ethan_Struggle阅读 2,159评论 0 7
  • 参考链接: http://www.cnblogs.com/ioshe/p/5489086.html 简介 Runt...
    乐乐的简书阅读 2,125评论 0 9
  • 本文详细整理了 Cocoa 的 Runtime 系统的知识,它使得 Objective-C 如虎添翼,具备了灵活的...
    lylaut阅读 788评论 0 4
  • 转载:http://yulingtianxia.com/blog/2014/11/05/objective-c-r...
    F麦子阅读 719评论 0 2
  • 本文转载自:http://yulingtianxia.com/blog/2014/11/05/objective-...
    ant_flex阅读 737评论 0 1