iOS Runtime笔记

对象、类与元类

相信大家对上图应该不陌生,图中说明了OC中对象的本质以及对象、类与元类的关系,这个也是OC的基础,属于runtime的内容,那么什么是runtime呢?它有什么作用呢?现用的那些东西可以涉及到runtime呢?下面一一讲解。

1.runtime是什么?

说的简单点,runtime就是一套C语言的函数和结构体,OC的代码在编译后就会转化为对应的C代码(个人理解,可能存在误差啊!😂),OC被称之为运行时语言就是依赖于这个运行时系统,举个最简单的例子:

[p eat];

这句代码很常见吧!p对象执行eat方法,那么这句代码在编译后会被转化为什么呢?

objc_msgSend(p, sel_registerName("eat"));(精简版)
((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("eat"));(原版)

其实OC的方法执行本质上是发送消息(即消息发送机制)
那么常见的runtime函数还有哪些呢?

  • 消息发送(在<objc/message.h>中,直接使用#import导入即可)
objc_msgSend(id _Nullable self, SEL _Nonnull op, ...) 给对象发送消息
objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...) 给对象父类发送消息(我觉得理解成给对象发送父类的消息可能好点)
  • Class(在<objc/runtime.h>中)
    说到Class就不得不提文章开头的图片了,用于解释对象、类和元类的经典图,继承于NSObject的类对象都有一个isa指针成员变量指向它所属的类,而本身呢!其实也是一个Class结构体指针变量,结构体中也有一个名为isa的Class指针变量,这个isa就指向了该类所属的元类,我们常用的类方法就属于元类的。
NSObject的isa成员变量
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa  OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
Class是一个指向结构体objc_class的指针:
typedef struct objc_class *Class;
objc_class结构体
struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class    OBJC2_UNAVAILABLE; 父类
    const char * _Nonnull name    OBJC2_UNAVAILABLE;类名
    long version    OBJC2_UNAVAILABLE;版本
    long info    OBJC2_UNAVAILABLE;详情
    long instance_size    OBJC2_UNAVAILABLE;类的实例对象的大小
    struct objc_ivar_list * _Nullable ivars    OBJC2_UNAVAILABLE;类的成员变量列表
    struct objc_method_list * _Nullable * _Nullable methodLists    OBJC2_UNAVAILABLE;类的实例方法列表 是指向 ·objc_method_list 指针的指针,也就是说可以动态修改 *methodLists 的值来添加成员方法,这也是 Category 实现的原理)
    struct objc_cache * _Nonnull cache    OBJC2_UNAVAILABLE;Runtime 系统会把被调用的方法存到 cache 中下次查找的时候效率更高
    struct objc_protocol_list * _Nullable protocols    OBJC2_UNAVAILABLE;类的协议列表      
#endif

} OBJC2_UNAVAILABLE;

常见函数,更多函数前往<objc/runtime.h>查看

OBJC_EXPORT Class _Nullable object_getClass(id _Nullable obj)  获取对象的Class
OBJC_EXPORT Class _Nullable object_setClass(id _Nullable obj, Class _Nonnull cls) 设置对象所属的Class
OBJC_EXPORT Class _Nullable objc_getClass(const char * _Nonnull name) 根据C字符获取一个Class
OBJC_EXPORT Class _Nullable objc_getMetaClass(const char * _Nonnull name) 获取C字符对应的Class的元类
BOOL class_respondsToSelector(Class _Nullable cls, SEL _Nonnull sel) 类的对象是否响应方法(估计就是方法- (BOOL)respondsToSelector:(SEL)aSelector的底层实现)
OBJC_EXPORT Class _Nullable objc_allocateClassPair(Class _Nullable superclass, const char * _Nonnull name, size_t extraBytes) 创建一个新类
OBJC_EXPORT void objc_registerClassPair(Class _Nonnull cls) 注册一个新类(和上面的函数一起使用的)
OBJC_EXPORT void objc_disposeClassPair(Class _Nonnull cls) 注销一个类(慎用!!!)
  • Method(在<objc/runtime.h>和<objc/objc.h>中)
    直接看Method的结构体
typedef struct objc_method *Method;
struct objc_method {
    SEL _Nonnull method_name                                 OBJC2_UNAVAILABLE;
    char * _Nullable method_types                            OBJC2_UNAVAILABLE;
    IMP _Nonnull method_imp                                  OBJC2_UNAVAILABLE;
}   

显然方法中主要就两个东西SEL(方法选择器)IMP(method_types用于用于表示方法的返回值类型和参数类型),其实可以这么理解类中有一个方法分发表存放着SELIMP的映射关系,SEL就是一个C字符串,通过它找到的IMP是一个函数指针,指向方法的具体实现。

typedef struct objc_selector *SEL

#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ ); 
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 
#endif

常见的函数,更多函数前往<objc/runtime.h>和<objc/objc.h>查看

OBJC_EXPORT const char * _Nonnull sel_getName(SEL _Nonnull sel) 获取SEL对应的C字符串
OBJC_EXPORT SEL _Nonnull sel_registerName(const char * _Nonnull str)根据C字符串注册一个SEL
OBJC_EXPORT void method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2) 替换两个方法的IMP
OBJC_EXPORT IMP _Nonnull method_setImplementation(Method _Nonnull m, IMP _Nonnull imp) 设置方法的IMP
OBJC_EXPORT SEL _Nonnull method_getName(Method _Nonnull m) 获取方法的SEL
OBJC_EXPORT IMP _Nullable class_replaceMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types) 替换一个类的方法的IMP
OBJC_EXPORT BOOL class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types) 为类添加一个方法
  • Ivar(成员变量)和Property(属性)
    这两个东西很多人弄混了,其实这是两个不一样的东西,简单理解的话就是属性=成员变量+成员变量的set方法+成员变量的get方法,我之前针对这两个东西写过一篇文章《iOS-属性与实例变量(成员变量)》
    先看看Ivar指向的结构体,结构体中的变量很简单
typedef struct objc_ivar *Ivar;

struct objc_ivar {
    char * _Nullable ivar_name                               OBJC2_UNAVAILABLE;
    char * _Nullable ivar_type                               OBJC2_UNAVAILABLE;
    int ivar_offset                                          OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
}

常用的函数,更多函数前往<objc/runtime.h>查看

OBJC_EXPORT id _Nullable object_getIvar(id _Nullable obj, Ivar _Nonnull ivar) 获取成员变量的值
OBJC_EXPORT void object_setIvar(id _Nullable obj, Ivar _Nonnull ivar, id _Nullable value) 设置成员变量的值
OBJC_EXPORT Ivar _Nonnull * _Nullable class_copyIvarList(Class _Nullable cls, unsigned int * _Nullable outCount) 拷贝类的所有成员变量
OBJC_EXPORT const char * _Nullable ivar_getName(Ivar _Nonnull v) 获取成员变量的名称

OBJC_EXPORT objc_property_t _Nonnull * _Nullable class_copyPropertyList(Class _Nullable cls, unsigned int * _Nullable outCount) 获取属性列表
OBJC_EXPORT objc_property_t _Nullable class_getProperty(Class _Nullable cls, const char * _Nonnull name) 根据C字符串获取属性
OBJC_EXPORT const char * _Nonnull property_getName(objc_property_t _Nonnull property) 获取属性名
  • Category(类别)
    类别好像没有多少函数(<objc/runtime.h>里面基本没有),主要是类别的结构体
typedef struct objc_category *Category

struct objc_category {
    char * _Nonnull category_name                            OBJC2_UNAVAILABLE;
    char * _Nonnull class_name                               OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable instance_methods     OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable class_methods        OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
}  
  • Protocol(协议)
    协议好像没有对应的结构体,<objc/runtime.h>是这样声明的,相关函数用的少,后期再补上(🤔🤔🤔)。
@class Protocol;

2.runtime有什么作用?

runtime堪称黑魔法,他可以新建类,为类添加属性、成员变量和方法,甚至更改一个方法的实现,反正很多一开始以为不可能的事都可以通过runtime实现,下面说说具体的使用场景:

  • 类别添加属性
    一看到这个大家会想到面试中经常问的问题类别可以添加属性吗?类别可以添加成员变量吗?我的理解是类别可以添加属性但是不可以添加成员变量,至于原因呢?我觉得是结构体的objc_class中的成员列表变量ivars是一个指针,再把OC代码编译成C代码后,类在内存上的布局就改变不了。
    那么为什么可以添加属性呢?首先类的方法是可以添加的,因为objc_class中的实例方法列表变量methodLists是一个指向objc_method_list指针的指针,指针在内存中的布局是一样的,所以可以更改,然后再使用runtime关联对象来实现为类别添加属性,详细的可参考《iOS runtime 关联对象》

  • 方法交换
    相信大家在开发过程中经常遇到这样的问题:一开始使用系统方法很好很方便,可以慢慢迭代到后期发现系统方法不能满足要求,需要做额外操作,这时候咋办呢?最简单的就是集成然后重写方法再替换,但是这样需要改掉之前所有的,麻烦!直接快捷键查找再替换,Low!!而且不保险改到一半出问题了就尴尬了!再说以后再改还要再替换,工程越大替换成本越大。那可以添加分类,在分类中重写方法吗?不推荐!!原因简单的说它的初衷并不是让你去改变一个类。那咋办?这时候可以使用runtime,直接上代码:

#import "NSURL+testCategory.h"
#import <objc/runtime.h>

@implementation NSURL (testCategory)

+ (void)load
{
    Method oldMethod = class_getClassMethod(self, sel_registerName("URLWithString:"));
    Method newMethod = class_getClassMethod(self, sel_registerName("customURLWithString:"));
    method_exchangeImplementations(oldMethod, newMethod);
}

/**
 *  @author liyong
 *
 *  网址字符串转为可加载的url(主要针对有汉字的网址字符串)
 *
 *  @param URLString 网址字符串
 *
 *  @return
 */
+ (nullable instancetype)customURLWithString:(nonnull NSString *)URLString
{
    if ([URLString length] > 0)
    {
        NSString *encodingURLString = [URLString stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
        //注意!这地方不可以使用URLWithString会死循环,因为URLWithString的实现已经变成customURLWithString,而customURLWithString已经变成URLWithString
        NSURL *url = [NSURL customURLWithString:encodingURLString];
        if (!url)
        {
            NSLog(@"nil空了!!!");
            return nil;
        }
        
        return url;
    }
    
    NSLog(@"nil空了!!!");
    return nil;
}

@end

代码中使用customURLWithString替换了系统的URLWithString,注意点代码中有。
这里额外说一下load方法,我的理解比较简单,就是是app的可执行文件从存储空间加载到内存后,最先被CPU读取的代码或者说是指令,用于给开发者做预处理任务,而且这个方法在类别重写后不影响原类的执行。后来查资料发现load方法貌似比我想的要复杂的多!(😂说的简单点好理解)

但是这种方法交换也存在一种问题就是如果过度滥用方法交换的话,会导致动态能力过强,代码的可读性和维护性会下降,所以大家要注意点,然后就是多添加注释!不然会坑自己!

  • 模型的序列化与反序列化/JSON数据转成模型
    模型缓存在本地的业务相信大家都处理过,一般使用归档的方式将模型或者装有模型的容器类缓存到本地,而且模型的类需要遵循NSCoding协议,实现两个协议方法。
- (void)encodeWithCoder:(NSCoder *)aCoder;
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder; // NS_DESIGNATED_INITIALIZER

常见的实现方法如下:

@interface LYDog : NSObject <NSCoding>
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *color;
@end

@implementation LYDog
- (void)encodeWithCoder:(NSCoder *)aCoder
{
    [aCoder encodeObject:self.name forKey:@"name"];
    [aCoder encodeObject:self.color forKey:@"color"];
}

- (instancetype)initWithCoder:(NSCoder *)aDecoder
{
    if (self = [super init])
    {
        self.name = [aDecoder decodeObjectForKey:@"name"];
        self.color = [aDecoder decodeObjectForKey:@"color"];
    }
    
    return self;
}
@end

很正常的写法,但是当这个模型类的属性变多的时候呢?复制粘贴一个个修改?确实可以这么写,但是可以利用runtime+KVO实现另外一种方式:

@interface LYDog : NSObject <NSCoding>
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *color;
@end

@implementation LYDog

- (void)encodeWithCoder:(NSCoder *)aCoder
{  
    unsigned int propertyCount = 0;
    objc_property_t *propertyList = class_copyPropertyList([self class], &propertyCount);
    for (int index = 0; index < propertyCount; index++)
    {
        objc_property_t property = propertyList[index];
        const char * propertyNameChar = property_getName(property);
        NSString *propertyName = [NSString stringWithUTF8String:propertyNameChar];
        [aCoder encodeObject:[self valueForKey:propertyName] forKey:propertyName];
    }
    //必须free掉
    free(propertyList);
}

- (instancetype)initWithCoder:(NSCoder *)aDecoder
{
    if (self = [super init])
    {  
        unsigned int propertyCount = 0;
        objc_property_t *propertyList = class_copyPropertyList([self class], &propertyCount);
        for (int index = 0; index < propertyCount; index++)
        {
            objc_property_t property = propertyList[index];
            const char *propertyNameChar =  property_getName(property);
            NSString *propertyName = [NSString stringWithUTF8String:propertyNameChar];
            [self setValue:[aDecoder decodeObjectForKey:propertyName] forKey:propertyName];
        }
        //必须free掉
        free(propertyList);
    }
    return self;
}
@end

使用runtime获取类的属性列表循环列表根据属性名利用KVO为属性赋值或者序列化,代码固定,后期属性变化了也基本不需要更改代码。其实这个思想也可以用于把JSON字符串转模型,第三方类库JSONModel中好像就用到了,下次细细研读一下JSONModel的源码。

  • 动态方法解析+消息转发/消息重定向
    对一个对象发送了未实现的消息(即对象使用了一个未实现的方法)时,后果很简单,导致崩溃,而且控制台还会打印日志:
unrecognized selector sent to instance 0x**********

那么我们可不可以在程序闪退之前做一些什么呢?好点的能不能阻止闪退呢?利用runtime的动态消息解析就可以做到。
首先我们要知道向对象发送消息后的流程:

  1. 对象是否为nil对象,因为向nil对象发消息是不会crash的,而是被忽略。

2.根据SEL查找方法的IMP,首先从cache中查找,找到了就可以利用IMP找到对应的实现函数并执行。

3.若cache中没找到就在类的方法分发表中使用SEL查找IMP,找不到就去父类的方法分发表找,直到找到NSObject。
如果上面三步之后还找不到IMP的话,就会进入消息转发,转发流程大致如下:


消息转发流程

图中涉及到如下方法:
1.下面两个方法用于判断是否需要利用class_addMethod函数向对象动态添加方法

+ (BOOL)resolveInstanceMethod:(SEL)sel  针对对象方法
+ (BOOL)resolveClassMethod:(SEL)sel  针对类方法

2.若上面的方法返回NO就会进入下面的方法,该方法是runtime告诉我们是否需要将消息重定向给另外一个对象以避免crash

- (id)forwardingTargetForSelector:(SEL)aSelector

3.若上面的方法返回nil,就会进入下面的方法,用于返回方法编码(编码包含方法的返回值类型和参数类型等),若返回nil表示不处理,否则进入下一步

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector

4.下面的方法和1中可操作的内容差不多,添加方法或者修改方法的实现,但是这个方法弊端是消耗内存较大,要想达到相同的目的建议在1中操作。

- (void)forwardInvocation:(NSInvocation *)anInvocation

以上就是几个较大的runtime的使用场景,那么系统中又利用runtime做了哪些功能呢?

3.利用runtime实现的系统功能的解析

工作过程中有没有想过系统的哪些功能是使用runtime实现的呢?苹果开发出这么厉害的动态系统不可能不用吧?下面我们来分析几个

  • KVO
    KVO一部分实现原理就用到了runtime,当使用下面的方法为对象添加观察者,那么这个方法里面做了什么操作呢?方法中其实利用runtime为对象创建了一个子类,并且为子类重写了被观察属性的set方法。
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context

光说不行,我们可以测试一下,代码很简单为p对象的firstName属性添加观察者:

    self.p = [[LYPersonModel alloc] init];
    NSLog(@"修改前类:%@",  NSStringFromClass(object_getClass(self.p)));
    [self.p addObserver:self forKeyPath:@"firstName" options:NSKeyValueObservingOptionNew context:nil];
    NSLog(@"修改后类:%@", NSStringFromClass(object_getClass(self.p)));

结果
修改前

修改后

注意!差看不到p的isa指针的话可以先退出Xcode,再进一次,这可能是Xcode的bug

知道这一部分原理了,那么自己自定义一个简单的KVO应该不是难事了吧?大家可以自己试试,里面还有一些细节,下次补上demo

未完待续。。。。

参考文章
Runtime全方位装逼指南
#warning 不要在category中重写方法

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

推荐阅读更多精彩内容