iOS runtime运行时的作用和应用场景

4,313 阅读6分钟

Runtime是什么?

  众所周知OC是一门高级编程语言,也是一门动态语言。有动态语言那也就有静态语言,静态语言---编译阶段就要决定调用哪个函数,如果函数未实现就会编译报错。如C语言。动态语言---编译阶段并不能决定真正调用哪个函数,只要函数声明过即使没有实现也不会报错。如OC语言。   高级编程语言想要成为可执行文件需要先编译为汇编语言再汇编为机器语言,机器语言也是计算机能够识别的唯一语言,但是OC并不能直接编译为汇编语言,而是要先转写为纯C语言再进行编译和汇编的操作,从OC到C语言的过渡就是由runtime来实现的。然而我们使用OC进行面向对象开发,而C语言更多的是面向过程开发,这就需要将面向对象的类转变为面向过程的结构体。   每当我面试的时候被问起Runtime相关知识的时候,总是只能回答个大体内容,具体的应用场景也是说的三三两两,所以我感觉是时候总结一波Runtime的应用场景了。

Runtime应用场景

场景1--动态扩展属性

  OC中类可以通过Category来直接扩展方法,但是却不能直接通过添加属性来扩展属性(以我项目中用到的一个为例)。

#import <UIKit/UIKit.h>

@interface UIView (SPUtils)

@property(nonatomic)CALayer * shadowLayer;

@end
#import "UIView+SPUtils.h"
#import <objc/runtime.h>
@implementation UIView (SPUtils)
-(void)setShadowLayer:(CALayer *)shadowLayer{
    objc_setAssociatedObject(self, @selector(shadowLayer), shadowLayer, OBJC_ASSOCIATION_RETAIN);
}
-(CALayer *)shadowLayer{
    return objc_getAssociatedObject(self, _cmd);
}

@end

场景2--交换方法用于统一处理某个方法

  在iOS新发布的时候在Scrollview的头部会系统默认多出一段空白,解决方法是设置其contentInsetAdjustmentBehavior属性为UIScrollViewContentInsetAdjustmentNever。但对于现存的项目来说挨个修改工作量无疑是巨大的,也容易出问题。这时候就用到Runtime了,用runtime来交换其初始化方法来统一设置这个属性就可以得到解决。

#import <UIKit/UIKit.h>

@interface UIScrollView (Inset)


@end

#import "UIScrollView+Inset.h"
#import "CYXRunTimeUtility.h"

@implementation UIScrollView (Inset)
+(void)load{
    [CYXRunTimeUtility swizzlingInstanceMethodInClass:[self class] originalSelector:@selector(initWithFrame:) swizzledSelector:@selector(m_initWithFrame:)];
}
- (instancetype)m_initWithFrame:(CGRect)frame {
    
    UIScrollView *scrollV = [self m_initWithFrame:frame];
    if (@available(iOS 11.0, *)) {
        scrollV.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
    }
    return scrollV;
}
@end

  实现交换方法的代码:

#import <Foundation/Foundation.h>

@interface CYXRunTimeUtility : NSObject
/**
 交换实例方法
 
 @param cls 当前class
 @param originalSelector originalSelector description
 @param swizzledSelector swizzledSelector description
 @return 返回
 */
+ (BOOL)swizzlingInstanceMethodInClass:(Class)cls originalSelector:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector;


/**
 交换类方法

 @param cls 当前class
 @param originalSelector originalSelector description
 @param swizzledSelector swizzledSelector description
 @return 成
 */
+ (BOOL)swizzlingClassMethodInClass:(Class)cls originalSelector:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector;

@end
#import "CYXRunTimeUtility.h"
#import <objc/runtime.h>

@implementation CYXRunTimeUtility

+ (BOOL)swizzlingInstanceMethodInClass:(Class)cls originalSelector:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector
{
    Class class = cls;
    
    Method originalMethod = class_getInstanceMethod(class, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
    
    BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
    if (didAddMethod)
    {
        class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
    }
    else
    {
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
    return didAddMethod;
}

+ (BOOL)swizzlingClassMethodInClass:(Class)cls originalSelector:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector
{
    Class class = cls;
    
    Method originalMethod = class_getClassMethod(class, originalSelector);
    Method swizzledMethod = class_getClassMethod(class, swizzledSelector);
    
    BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
    if (didAddMethod)
    {
        class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
    }
    else
    {
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
    return didAddMethod;
}

@end

场景3--遍历类属性--映射解析

  开发日常中我们对网络请求下来的数据进行解析是必然的操作,包括很多三方解析框架都是通过runtime来获取相关属性进行映射解析的。   下面是我自己利用runtime获取对象相关属性并进行简单深拷贝的例子(有不足之处,进攻参考):

#import <Foundation/Foundation.h>

@interface NSObject (MutableCopy)

-(id)getMutableCopy;

@end

#import "NSObject+MutableCopy.h"

@implementation NSObject (MutableCopy)
-(id)getMutableCopy{
    NSArray * keys = [self getObjcPropertyWithClass:[self class]];
    id objc = [[[self class] alloc] init];
    for (NSString * key in keys) {
        if ([self valueForKey:key] == nil) continue;
        [objc setValue:[self valueForKey:key] forKey:key];
        //[objc setValue:[[self valueForKey:key] getMutableCopy] forKey:key];
    }
    return objc;
}

- (NSArray<NSString *> *)getObjcPropertyWithClass:(id )objc{
    //(1)获取类的属性及属性对应的类型
    NSMutableArray * keys = [NSMutableArray array];
    NSMutableArray * attributes = [NSMutableArray array];
    /*
     * 例子
     * name = value3 attribute = T@"NSString",C,N,V_value3
     * name = value4 attribute = T^i,N,V_value4
     */
    unsigned int outCount;
    Class cls = [objc class];
    do {
        objc_property_t * properties = class_copyPropertyList(cls, &outCount);
        for (int i = 0; i < outCount; i ++) {
            objc_property_t property = properties[i];
            //通过property_getName函数获得属性的名字
            NSString * propertyName = [NSString stringWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
            [keys addObject:propertyName];
            //通过property_getAttributes函数可以获得属性的名字和@encode编码
            NSString * propertyAttribute = [NSString stringWithCString:property_getAttributes(property) encoding:NSUTF8StringEncoding];
            [attributes addObject:propertyAttribute];
        }
        //立即释放properties指向的内存
        free(properties);
        cls = [objc superclass];
        objc = [cls new];
    } while ([NSStringFromClass([objc superclass]) isEqualToString:@"NSObject"]);
    return [keys valueForKeyPath:@"@distinctUnionOfObjects.self"];
}
@end

场景4--修改isa指针,自己实现kvo

  面向对象中每一个对象都必须依赖一个类来创建,因此对象的isa指针就指向对象所属的类根据这个类模板能够创建出实例变量、实例方法等。   Apple 使用了 isa 混写(isa-swizzling)来实现 KVO 。当观察对象A时,KVO机制动态创建一个新的名为:NSKVONotifying_A 的新类,该类继承自对象A的本类,且 KVO 为 NSKVONotifying_A 重写观察属性的 setter 方法,setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象属性值的更改情况。   首先创建一个person类定义一个实例变量:

@interface Person : NSObject
{
    @public
    NSString * _name;
}

@property (nonatomic,copy) NSString *name;
@end

创建一个NSObject的Category用于给所有NSObject及其子类新增 添加监听方法:

@interface NSObject (KVO)
- (void)cyx_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
@end
NSString * const cyx_key = @"observer";

@implementation NSObject (KVO)
-(void)cyx_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context
{
    
    
    objc_setAssociatedObject(self, (__bridge const void *)(cyx_key), observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    
    //修改isa 指针
    object_setClass(self, [SonPerson class]);
}

  这里利用runtime修改isa指针,修改调用方法时寻找方法的类。这里我们修改到SonPerson类。并在SonPerson类里面实现监听方法。

extern NSString * const cyx_key;
@implementation SonPerson

-(void)setName:(NSString *)name{
    [super setName:name];
    
    NSObject * observer = objc_getAssociatedObject(self, cyx_key);
    
    [observer observeValueForKeyPath:@"name" ofObject:self change:nil context:nil];
}

  这里也用到了runtime 里面 objc_getAssociatedObject 和objc_setAssociatedObject动态存储方法。   好了那我们来用一下试一下效果吧。

#import "ViewController.h"
#import "Person.h"
#import "NSObject+KVO.h"
@interface ViewController ()

@property (nonatomic,strong) Person *p;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    Person * p = [[Person alloc] init];
    [p cyx_addObserver:self forKeyPath:@"name" options:0 context:nil];
    _p = p;
}
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"%@",_p.name);
}

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    
    static int i=0;
    i++;
    _p.name = [NSString stringWithFormat:@"%d",i];
    //_p -> _name = [NSString stringWithFormat:@"%d",i];
    
}

输出:

2018-04-21 11:15:17.974785+0800 04-响应式编程思想[1882:274712] 1
2018-04-21 11:15:18.293700+0800 04-响应式编程思想[1882:274712] 2
2018-04-21 11:15:18.687331+0800 04-响应式编程思想[1882:274712] 3
2018-04-21 11:15:19.036166+0800 04-响应式编程思想[1882:274712] 4
2018-04-21 11:15:19.396075+0800 04-响应式编程思想[1882:274712] 5
2018-04-21 11:15:19.699907+0800 04-响应式编程思想[1882:274712] 6
2018-04-21 11:15:19.981256+0800 04-响应式编程思想[1882:274712] 7

  demo:github.com/SionChen/Re…

场景5--利用runtime实现消息转发机制的三次补救

  这个参考我的另一篇文章:www.jianshu.com/p/1073daee5…

总结

  当然runtime的强大不仅仅是只能做这些事情,runtime还有很多用处等待我们大家去挖掘。