iOSiOS基础·OC高级篇

iOS 你所了解的runtime

2019-01-10  本文已影响0人  1江春水
起来搬砖啦

我们说Objective-C这门语言是一门动态语言,哪个特性来体现的呢,就是runtime运行时系统来体现的。

下面通过几个方面来介绍runtime运行时:

runtime介绍

先看看官方文档的介绍:

Objective-C语言从编译时间和链接时间到运行时推迟了尽可能多的决策。只要有可能,它就会动态地完成任务。这意味着该语言不仅需要编译器,还需要运行时系统来执行编译代码。运行时系统充当Objective-C语言的一种操作系统;这就是语言运作的原因。

Objective-C runtime运行时有两个版本,lagecy(遗产)和modern(现代),modern版本运行时随Objective-C2.0推出,Objective-C2.02.0包含了许多新功能,其中一点是当更改类的实例变量布局时,不必重新编译。

运行时系统的核心是消息传递和转发,以及在消息传递和转发中动态的加载类并查找有关对象的信息。

不管你是显式的调用了runtime的API还是直接写OC有关的代码,编译器都会生成相应的数据结构和函数。

runtime运行时系统是一个动态共享库,可以使用 #<objc/runtime>来查看内部的API及各种结构体定义,且runtime是开源的,你可以在这里下载。

消息发送

曾经有这么一个面试题:

- (instancetype)init {
    self = [super init];
    if (self) {
        NSLog(@"class = %@",NSStringFromClass([self class]));
        NSLog(@"class = %@",NSStringFromClass([super class]));
    }
    return self;
}
打印:
class = Person
class = Person

自己先想想为啥...
接着看:
举个例子:

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [self test];
}

- (void)test {
    NSLog(@"%@",NSStringFromSelector(_cmd));
}

@end

我们经常说在viewDidLoad方法里通过self调用test方法,[self test],其实确切的应该说成给 self 发送 test 消息 当编译完后该代码转换成了如下代码:

objc_msgSend(self,sel_registerName("test"));

需要设置下xcode,不让xcode检查是否使用objc_msgSend发送消息,要不然会报错;

项目target--build Setting --搜索objc_msgSend ---设置为NO即可

objc_msgSend定义如下:

id _Nullable objc_msgSend(id _Nullable self, SEL _Nonnull op, ...);

在想弄清楚runtime是怎么完成消息发送前,我们先来看下,OC对象和类在runtime中的定义:

/// An opaque type that represents an Objective-C class. 类的定义
typedef struct objc_class *Class;

/// Represents an instance of a class.代表类的一个实例
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

//类结构体
struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;//指向元类

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;//指向父类(类也是一个对象)
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;//name
    long version                                             OBJC2_UNAVAILABLE;//版本
    long info                                                OBJC2_UNAVAILABLE;//info
    long instance_size                                       OBJC2_UNAVAILABLE;//实例大小
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;//变量列表
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;//方法列表
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;//缓存
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;//协议列表
#endif
} OBJC2_UNAVAILABLE;

看完上边的实例及类的定义,加上我们的开发经验,其实已经知道消息发送的一个流程了:

  1. 通过实例self的isa指针找到当前所属类
  2. 在当前类的objc_cache中查找,有没有test方法
  3. cache中没有找到test方法,再去methodList中去查找,找到就执行IMP,没有找到通过isa指针去父类中去找,直到找到,找不到就报unrecognized selector xxx

方法查找首先去objc_cache中查找是为了提高效率,调用过的方法会存放在缓存中,不能每次调用一个方法都要查找一遍methodList,显然是不行的。缓存的时候会把test的method_name作为key,方法的method_imp实现作为value来存储。

其他的一些结构体声明:

id

id类型,就是一个objc_object结构体指针,指向任意对象

/// A pointer to an instance of a class.
typedef struct objc_object *id;
SEL

方法选择器,可以理解为方法ID,可以联想到OC语言一个类中方法不能重载

/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;
Method

一个方法有方法名,有types,有方法实现;types举例:@"v@:" v代表返回值为void,@ 表示参数为参数为对象类型,: 表示selector方法选择器 具体请移步这里

struct objc_method {
    SEL _Nonnull method_name  //方法名                               OBJC2_UNAVAILABLE;
    char * _Nullable method_types  //types(@"v@:")                          OBJC2_UNAVAILABLE;
    IMP _Nonnull method_imp   //方法实现IMP                               OBJC2_UNAVAILABLE;
}                                                
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
} 
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;//协议列表
} 
objc_property_t 属性
/// An opaque type that represents an Objective-C declared property.类声明的属性
typedef struct objc_property *objc_property_t;

//获取一个类中的属性列表
objc_property_t _Nonnull * _Nullable class_copyPropertyList(Class _Nullable cls, unsigned int * _Nullable outCount);
objc_cache

缓存,刚才讲过了,寻找类的方法先是去缓存中找的,内部有个 buckets实例变量,意思很明白,一个方法桶子

typedef struct objc_cache *Cache                             OBJC2_UNAVAILABLE;

struct objc_cache {
    unsigned int mask /* total = mask + 1 */                 OBJC2_UNAVAILABLE;
    unsigned int occupied                                    OBJC2_UNAVAILABLE;
    Method _Nullable buckets[1]                              OBJC2_UNAVAILABLE;
};

以上简单列举了下,runtime运行时系统其他的结构体声明,理解了这些在加上内部的函数,就能更灵活的运用到自己的项目当中,内部更具体的函数、结构体声明请移步官方文档。

消息转发

我们都知道,假如你调用了一个没有实现的方法,会报unrecognised selector xxx,其实即便你没有实现这个方法,通过runtime的消息转发机制,你仍然还有机会让你的程序不崩溃。

NSObject类中有这么几个方法,以下几个方法就是运行时系统在给你最后机会来自己处理消息转发:

1、
+ (BOOL)resolveClassMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
+ (BOOL)resolveInstanceMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

2、- (id)forwardingTargetForSelector:(SEL)aSelector OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

3、
- (void)forwardInvocation:(NSInvocation *)anInvocation OBJC_SWIFT_UNAVAILABLE("");
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector OBJC_SWIFT_UNAVAILABLE("");

挂
- (void)doesNotRecognizeSelector:(SEL)aSelector;

可以概括为三种方式:

动态添加方法实现

举例:

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    objc_msgSend(self,sel_registerName("test"));
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == NSSelectorFromString(@"test")) {
        BOOL added = class_addMethod([self class], NSSelectorFromString(@"test"), (IMP)testImp, "v@:");
        if (added) {
            return YES;
        }
        return NO;
    }
    //其他系统访问返回super调用
    return [super resolveInstanceMethod:sel];
}


void testImp(id self,SEL _cmd) {
    NSLog(@"method name = %@",NSStringFromSelector(_cmd));
}
@end

打印:
method name = test

来看下官方文档的说明:

Dynamically provides an implementation for a given selector for an instance method.动态提供一个给定实例的方法实现,说的很明白了。

一个OC方法默认有两个参数(self 和 SEL),使用class_addMethod函数来动态添加一个函数实现,并返回YES

返回实现该方法的对象

如果你实现了forwardingTargetForSelector这个方法,系统就会调用它,会去你返回的那个对象实例中寻找方法,然后调用。

现在我创建了一个Person类,里边声明了test方法并实现。

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

@implementation Person
- (void)test {
    NSLog(@"class_name = %@ , method_name = %@",[self class],NSStringFromSelector(_cmd));
}
@end

VC内部:
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    objc_msgSend(self,sel_registerName("test"));
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == NSSelectorFromString(@"test")) {
        return NO;
    }
    return [super resolveInstanceMethod:sel];
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == NSSelectorFromString(@"test")) {
        return [Person new];
    }
    return [super forwardingTargetForSelector:aSelector];
}
@end

打印:
class_name = Person , method_name = test

系统首先调用resolveInstanceMethod方法,发现没有找到方法实现,就会调用方法forwardingTargetForSelector,该方法内部返回Person对象实例,Person对象内部实现了test方法,然后调用了test方法。

看下系统给的解释:

如果对象实现(或继承)此方法,并返回非零(和非自身)结果,则返回的对象将用作新的接收器对象,并且消息调度将恢复到该新对象,如果这个方法返回self,会直接崩溃。

也就是系统会重新进行一次消息转发,消息的接收者变成了该方法返回的对象。

通过返回NSInvocation和MethodSignature完成一次完整转发

如果以上两个机会你都没有把握,系统就会开启一次完整的消息转发,步骤如下:

  1. 通过实现methodSignatureForSelector方法,实例化一个方法签名对象NSMethodSignature返回,这个方法签名对象是对方法method的一个描述
  2. runtime动态生成一个NSInvocation对象传递给forwardInvocation方法
  3. 通过实现forwardInvocation来指定一个对象,通过这个对象完成消息转发

看下面代码:

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    objc_msgSend(self,sel_registerName("test"));
}

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

//这个官方有示例
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL sel = [anInvocation selector];
    Person *p = [[Person alloc] init];
    if ([p respondsToSelector:sel]) {
        [anInvocation invokeWithTarget:p];
    } else {
        [super forwardInvocation:anInvocation];
    }
}
@end

打印:
class_name = Person , method_name = test

有关NSMethodSignature和NSInvocation类,内部很简单,可以点进去看下。

以上就是消息转发的一个流程。

runtime使用场景

结合自己项目中runtime使用场景来列举下:

  1. 对象绑定
  2. 方法交换
  3. 自定义对象归档返归档
  4. dic-model
  5. KVO自定义实现

1. 对象绑定

直接看例子:

@interface Person : NSObject
- (void)bind;
@end


NSString * const personSource = @"personSource";

@implementation Person
- (NSString *)description {
    NSString *str = objc_getAssociatedObject(self, &personSource);
    return [NSString stringWithFormat:@"%@-刚出生就拥有:%@",self.name,str];
}

- (void)bind {
    objc_setAssociatedObject(self, &personSource, @"北京二环两套房+500万", OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (void)dealloc {
    objc_removeAssociatedObjects(self);
}

//VC中
- (void)viewDidLoad {
    [super viewDidLoad];

    Person *p = [[Person alloc] init];
    p.name = @"小明";
    [p bind];
    NSLog(@"%@",p);
}

打印:
小明-刚出生就拥有:北京二环两套房+500万
@end

通过上边这个例子可以看出:现在是拼爹的时代,哈哈。

看下绑定对象API:

/** 
 * 通过给定key和绑定策略将value绑定到object上!
 * 
 * @param object 被绑定的对象
 * @param key 绑定value使用的key,注意:key-value一一对应
 * @param value 绑定的value,传空表示清空value
 * @param policy 绑定策略,用于绑定对象的内存管理
 * @see objc_setAssociatedObject
 * @see objc_removeAssociatedObjects
 */
void objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
                         id _Nullable value, objc_AssociationPolicy policy)
    OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0, 2.0);
    
  /** 
 * 根据key返回对象绑定的value值
 * @param object 被绑定的对象
 * @param key key
 * @return 绑定到对象上的value值
 * @see objc_setAssociatedObject
 */
id _Nullable objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
    OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0, 2.0);
 

//清除所有绑定到对象上的value,一般不用这个方法,使用objc_setAssociatedObject方法,value传递nil即可
void objc_removeAssociatedObjects(id _Nonnull object)
    OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0, 2.0);

绑定策略:

typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,//弱引用+原子性      
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,//强引用+非原子性
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,//copy+非原子性
    OBJC_ASSOCIATION_RETAIN = 01401, //强引用+原子性      
    OBJC_ASSOCIATION_COPY = 01403  //copy+原子性 
};

缓存策略用于绑定对象value内存管理用的,跟常见的属性修饰符管理完全相同。

2. 方法交换

方法交换,核心就是交换方法的实现(IMP),有图为证


屏幕快照 2019-01-10 18.33.39.png

简单说一下核心方法实现:

  1. 根据旧类class和SEL,获取要被替换的Method,即originMethod
  2. 根据新类class和SEL获取新加的Method,即cusMethod
  3. 如果没有originMethod,直接向旧类添加新方法,添加成功,就替换老方法的实现(IMP)
  4. 直接交换方法实现
void tt_swizzleMethodImplementation(Class originC,Class cusC ,SEL originSEL, SEL cusSEL) {
    Method originMethod = class_getInstanceMethod(originC, originSEL);
    Method cusMethod = class_getInstanceMethod(cusC, cusSEL);
    if (!originMethod) {
        BOOL added = class_addMethod(originC, cusSEL, method_getImplementation(cusMethod), method_getTypeEncoding(cusMethod));
        if (added) {
            class_replaceMethod(originC, originSEL, method_getImplementation(cusMethod), method_getTypeEncoding(cusMethod));
        }
    } else {
        method_exchangeImplementations(originMethod, cusMethod);
    }
}

这个我前一阵写过一个例子,就是通过方法交换来优化appDelegate内部的推送部分代码的,优化后你会发现,appDelegate推送部分代码非常简洁,里边写的比较详细,有需要的可以看下推送优化传送门,方法交换就此别过。

3. 自定义对象归档返归档

当存储自定义数据(model)时,需要用到归档反归档。
系统对归档的内部实现猜测是以key-value键值对存储的。通过实现系统的两个协议来告知系统你要存储的数据。需要实现NSCoding协议内的两个方法

- (void)encodeWithCoder:(NSCoder *)aCoder;
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder; 

一般如果model属性少的话,手写还是可以的,但是属性过多还要手写? 通过runtime来动态遍历出model的属性key,然后来赋值或取值。MJExtension内部有实现,而且一个宏搞定。

看下核心代码的实现:

- (void)tt_decode:(NSCoder *)decoder {
    unsigned int count;
    objc_property_t *properties = class_copyPropertyList([self class], &count);
    for (unsigned int i = 0; i < count; i++) {
        objc_property_t property = properties[i];
        const char *name = property_getName(property);
        NSString *propertyName = [NSString stringWithUTF8String:name];
        NSString *propertyValue = [decoder decodeObjectForKey:propertyName];
        if (nil != propertyValue) {
            [self setValue:propertyValue forKey:propertyName];
        }
    }
    free(properties);
}

- (void)tt_encode:(NSCoder *)encoder {
    unsigned int count;
    objc_property_t *properties = class_copyPropertyList([self class], &count);
    for (unsigned int i = 0; i < count; i++) {
        objc_property_t property = properties[i];
        const char *name = property_getName(property);
        NSString *propertyName = [NSString stringWithUTF8String:name];
        id value = [self valueForKey:propertyName];
        if (nil != value) {
            [encoder encodeObject:value forKey:propertyName];
        }
    }
    free(properties);
}

这块不多说,需要注意的是最后需要释放objc_property_t指针,因为这是在OC环境,对C语言不自动管理内存。

dic-model(字典转模型)

字典转模型也不多说,看看常用的开源框架就行,核心是遍历出对象的属性列表然后把以属性名(可以自定义)为key从字典取出对应的value赋值给对象属性。

KVO自定义实现

当你了解了KVO的底层原理后,也可以尝试着使用runtime运行时来写一个自己的KVO,当然有可能会有bug,即便自己实现一个KVO也没必要用到项目中,毕竟系统的或RAC很成熟了,自己写的目的其实很简单,通过自己实现可以更熟悉底层的实现,从而给自己带来一些思考。前一阵自己实现过一个,也有demo,有需要的自己去看吧,这里不多说。

面试题

回过头来看看那个面试题,我们知道[self class] 等价于

objc_msgSend(self, @selector(class));

那么[supser class]等价于

    objc_msgSend(self, @selector(class));
    struct objc_super superClass = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self))
    };
    objc_msgSendSuper(&superClass, _cmd);

通过上面转化后的代码我们就清楚了,消息的接收者都是self,super只是一个标识符而已。

以上就是runtime运行时的一个概括汇总吧,只有当你真正理解了runtime,才会领悟这门语言动态的含义。

奋然前行!

点赞是你的态度!

上一篇下一篇

猜你喜欢

热点阅读