iOS runtime程序员

iOS Runtime笔记

2018-02-28  本文已影响24人  LoveY34
对象、类与元类

相信大家对上图应该不陌生,图中说明了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_msgSend(id _Nullable self, SEL _Nonnull op, ...) 给对象发送消息
objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...) 给对象父类发送消息(我觉得理解成给对象发送父类的消息可能好点)
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) 注销一个类(慎用!!!)
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) 为类添加一个方法
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) 获取属性名
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;
}  
@class Protocol;

2.runtime有什么作用?

runtime堪称黑魔法,他可以新建类,为类添加属性、成员变量和方法,甚至更改一个方法的实现,反正很多一开始以为不可能的事都可以通过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方法貌似比我想的要复杂的多!(😂说的简单点好理解)

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

- (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实现的呢?苹果开发出这么厉害的动态系统不可能不用吧?下面我们来分析几个

- (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中重写方法

上一篇 下一篇

猜你喜欢

热点阅读