Runtime详解(下)

Runtime应用

1.Runtime 交换方法

应用场景:当第三方框架或者系统原生方法功能不能满足我们的时候,我们可以在保持系统原有功能的基础上,添加额外的功能。

需求:加载一张图片直接用系统的[UIImage imageNamed:@""];是无法知道到底有没有加载成功。给系统的imageNamed添加额外功能,(是否加载图片成功,以及加载未完成的时候,用模糊的该照片代替)

方法一:继承系统的类,重写方法:(每次使用都需要导入)

方法二:使用runtime,交换方法

实现步骤:

1)给系统的方法添加分类

(2)自己实现一个带有扩展功能的方法

(3)交换方法,只需要交换一次

下面是案例代码:

- (void)viewDidLoad {
    [super viewDidLoad];
    // 方案一:先搞个分类,定义一个能加载图片并且能打印的方法+ (instancetype)imageWithName:(NSString *)name;
    // 方案二:交换 imageNamed 和 ln_imageNamed 的实现,就能调用 imageNamed,间接调用 ln_imageNamed 的实现。
    UIImage *image = [UIImage imageNamed:@"123"];
}

#import <objc/message.h>
@implementation UIImage (Image)
/**
 load方法: 把类加载进内存的时候调用,只会调用一次
 方法应先交换,再去调用
 */
+ (void)load {

    // 1.获取 imageNamed方法地址
    // class_getClassMethod(获取某个类的方法)
    Method imageNamedMethod = class_getClassMethod(self, @selector(imageNamed:));
    // 2.获取 ln_imageNamed方法地址
    Method ln_imageNamedMethod = class_getClassMethod(self, @selector(ln_imageNamed:));

    // 3.交换方法地址,相当于交换实现方式;「method_exchangeImplementations 交换两个方法的实现」
    method_exchangeImplementations(imageNamedMethod, ln_imageNamedMethod);
}

/**
 看清楚下面是不会有死循环的
 调用 imageNamed => ln_imageNamed
 调用 ln_imageNamed => imageNamed
 */
// 加载图片 且 带判断是否加载成功
+ (UIImage *)ln_imageNamed:(NSString *)name {

    UIImage *image = [UIImage ln_imageNamed:name];
    if (image) {
        NSLog(@"runtime添加额外功能--加载成功");
    } else {
        NSLog(@"runtime添加额外功能--加载失败");
    }
    return image;
}

/**
 不能在分类中重写系统方法imageNamed,因为会把系统的功能给覆盖掉,而且分类中不能调用super
 所以第二步,我们要 自己实现一个带有扩展功能的方法.
 + (UIImage *)imageNamed:(NSString *)name {

 }
 */
@end

总结:我们所做的就是在方法调用流程第三步的时候,交换两个方法地址指向。而且我们改变指向要在系统的imageNamed:方法调用前,所以将代码写在了分类的load方法里,最后当运行的时候系统的方法就会去找我们实现的方法。

2.动态添加属性

给一个类声明属性,其实本质就是给这个类添加关联,并不是直接把这个值的内存空间添加到类存空间。

给系统的类添加属性的时候,可以使用runtime动态添加属性。

注解:系统NSObject添加一个分类,我们知道在分类中不能添加成员属性的,虽然我们用了@property,但是仅仅是自动生成get和set方法的声明,并没有带下滑线的属性和方法实现生成。我们可以通过runtime就可以做到给它方法的实现。

需求:给系统NSObject动态添加属性name字符串。

案例如下:

@interface NSObject (Property)

// @property分类:只会生成get,set方法声明,不会生成实现,也不会生成下划线成员属性
@property NSString *name;
@property NSString *height;
@end

@implementation NSObject (Property)

- (void)setName:(NSString *)name {

    // objc_setAssociatedObject(将某个值跟某个对象关联起来,将某个值存储到某个对象中)
    // object:给哪个对象添加属性
    // key:属性名称
    // value:属性值
    // policy:保存策略
    objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (NSString *)name {
    return objc_getAssociatedObject(self, @"name");
}

// 调用
NSObject *objc = [[NSObject alloc] init];
objc.name = @"123";
NSLog(@"runtime动态添加属性name==%@",objc.name);
//结果如下:
2016-02-17 19:37:10.530 runtime[12761:543574] runtime动态添加属性--name == 123

其实给属性赋值的本质,就是让属性与一个对象产生关联,所以要给NSObject的分类的name属性赋值就是让那个name和NSObject产生关联,而Runtime可以做到这一点。

下面再举个例子:

关联对象(objective-C Associated objects)给分类增加属性

关联对象Runtime提供了几个接口:

//关联对象
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
//获取关联的对象
id objc_getAssociatedObject(id object, const void *key)
//移除关联的对象
void objc_removeAssociatedObjects(id object)

参数注释:

id object:被关联的对象
const void *key:关联的key,要求唯一
id value:关联的对象
objc_AssociationPolicy policy:内存管理的策略

内存管理的策略

typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,           /**< Specifies a weak reference to the associated object. */
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object. 
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   /**< Specifies that the associated object is copied. 
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_RETAIN = 01401,       /**< Specifies a strong reference to the associated object.
                                            *   The association is made atomically. */
    OBJC_ASSOCIATION_COPY = 01403          /**< Specifies that the associated object is copied.
                                            *   The association is made atomically. */
};

下面实现一个UIViewCategory添加自定义属性defaultColor

#import "ViewController.h"
#import "objc/runtime.h"

@interface UIView (DefaultColor)

@property (nonatomic, strong) UIColor *defaultColor;

@end

@implementation UIView (DefaultColor)

@dynamic defaultColor;

static char kDefaultColorKey;

- (void)setDefaultColor:(UIColor *)defaultColor {
    objc_setAssociatedObject(self, &kDefaultColorKey, defaultColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (id)defaultColor {
    return objc_getAssociatedObject(self, &kDefaultColorKey);
}

@end

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    
    UIView *test = [UIView new];
    test.defaultColor = [UIColor blackColor];
    NSLog(@"%@", test.defaultColor);
}

@end

结果如下:

打印结果:
2018-04-01 15:41:44.977732+0800 ocram[2053:63739] UIExtendedGrayColorSpace 0 1

从打印结果来看:我们成功在分类上添加一个属性,实现了它的setter和getter方法。

通过关联对象实现的属性的内存管理也是有ARC管理的,所以我们只需要给定适当的内存策略就行了,不需要操心对象的释放。

3.方法魔法:(俗称黑魔法)-method swizzling

简单的说就是进行方法交换

在Objective-C中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是selector的名字。利用Objective-C的动态特性,可以实现在运行时偷换selector对应的方法实现,达到给方法挂钩的目的。

每一个类都有一个方法列表,存放着方法的名字实现的映射关系,selector的本质就是方法名,IMP有点类似函数指针,指向具体的method实现,通过selector就可以找到对应的IMP。

交换方法的几种实现方式:

(1)利用method_exchangeImplementations 交换两个方法的实现

(2)利用class_replaceMethod替换方法的实现。

(3)利用method_setImplementation来直接设置某个方法的IMP。

目前已更新实例汇总:

.替换ViewController生命周期方法

.解决获取索引、添加、删除元素越界崩溃问题

.防止按钮重复暴力点击

.全局更换控件初始效果

.App热修复

.全局修改导航栏后退(返回)按钮

Method Swizzling通用方法封装

我们可以将Method Swizzling功能封装为类方法,作为NSObject的类别。

#import <Foundation/Foundation.h>
#import <objc/runtime.h>
@interface NSObject (Swizzling) 

+ (void)methodSwizzlingWithOriginalSelector:(SEL)originalSelector
                         bySwizzledSelector:(SEL)swizzledSelector;
@end
#import "NSObject+Swizzling.h"
@implementation NSObject (Swizzling)

+ (void)methodSwizzlingWithOriginalSelector:(SEL)originalSelector bySwizzledSelector:(SEL)swizzledSelector{
    Class class = [self class];
    //原有方法
    Method originalMethod = class_getInstanceMethod(class, originalSelector);
    //替换原有方法的新方法
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
    //先尝试給源SEL添加IMP,这里是为了避免源SEL没有实现IMP的情况
    BOOL didAddMethod = class_addMethod(class,originalSelector,
                                        method_getImplementation(swizzledMethod),
                                        method_getTypeEncoding(swizzledMethod));
    if (didAddMethod) {//添加成功:说明源SEL没有实现IMP,将源SEL的IMP替换到交换SEL的IMP
        class_replaceMethod(class,swizzledSelector,
                            method_getImplementation(originalMethod),
                            method_getTypeEncoding(originalMethod));
    } else {//添加失败:说明源SEL已经有IMP,直接将两个SEL的IMP交换即可
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}
@end

解析:为什么要添加didAddMethod判断?

先尝试添加原SEL其实是为了做一层保护,因为如果这个类如果没有实现originalSelector,但其父类实现了,那class_getInstanceMethod会返回父类的方法。这样method_exchangeImplementations替换的是父类的那个方法。这样method_exchangeImplementations替换的是父类的那个方法,这当然不是我们想要。所以我们先尝试添加orginalSelector,如果已经存在,再用method_exchangeImplement把原方法的实现跟新的方法实现给交换掉。

大概的意思就是我们可以通过class_addMethod为一个类添加方法

(包括方法名称(SEL)和方法的实现(IMP)),返回值为BOOL类型,表示方法是否成功添加。需要注意的地方是class_addMethod会添加一个覆盖父类的实现,但不会取代原有类的实现。也就是说如果class_addMethod返回YES,说明子类中没有方法originalSelector,通过class_addMethod为其添加了方法originalSelector,并使其实现(IMP)为我们想要替换的实现。

class_addMethod(class,originalSelector,
                                        method_getImplementation(swizzledMethod),
                                        method_getTypeEncoding(swizzledMethod));

同时再将原有的实现(IMP)替换到swizzledMethod方法上,

class_replaceMethod(class,swizzledSelector,
                            method_getImplementation(originalMethod),
                            method_getTypeEncoding(originalMethod));

从而实现了方法的交换,并且未影响父类方法的实现。反之如果class_addMethod返回NO,说明子类中本身就具有方法originalSelector的实现,直接调用交换即可。

method_exchangeImplementations(originalMethod, swizzledMethod);

实例1:替换ViewController

App跳转到某具有网络请求的界面时,为了用户体验效果常会添加加载栏或进度条来显示当前请求情况或进度。这种界面都会存在这样一个问题,在请求较慢时,用户手动退出界面,这时候需要去除加载栏。
当然可以依次在每个界面的viewWillDisappear方法中添加去除方法,但如果类似的界面过多,一味的复制粘贴也不是方法。这时候就能体现Method Swizzling的作用了,我们可以替换系统的viewWillDisappear方法,使得每当执行该方法时即自动去除加载栏。

#import "UIViewController+Swizzling.h"
#import "NSObject+Swizzling.h"
@implementation UIViewController (Swizzling)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self methodSwizzlingWithOriginalSelector:@selector(viewWillDisappear:) bySwizzledSelector:@selector(sure_viewWillDisappear:)];
    });
}

- (void)sure_viewWillDisappear:(BOOL)animated {
    [self sure_viewWillDisappear:animated];
    [SVProgressHUD dismiss];
}

⚠️补充知识点

(1)为什么方法交换调用+load方法中?
在Objective-C runtime会自动调用两个类方法,分别为+load与+ initialize。+load 方法是在类被加载的时候调用的,也就是一定会被调用。而+initialize方法是在类或它的子类收到第一条消息之前被调用的,这里所指的消息包括实例方法和类方法的调用。也就是说+initialize方法是以懒加载的方式被调用的,如果程序一直没有给某个类或它的子类发送消息,那么这个类的+initialize方法是永远不会被调用的。此外+load方法还有一个非常重要的特性,那就是子类、父类和分类中的+load方法的实现是被区别对待的。换句话说在 Objective-C runtime自动调用+load方法时,分类中的+load方法并不会对主类中的+load方法造成覆盖。综上所述,+load 方法是实现 Method Swizzling 逻辑的最佳“场所”。
(2)为什么方法要在dispatch_once中执行?
方法交换应该要线程安全,而且保证在任何情况下(多线程环境,或者被其他人手动再次调用+load方法)只交换一次,防止再次调用又将方法交换回来。除非只是临时交换使用,在使用完成后又交换回来。 最常用的解决方案是在+load方法中使用dispatch_once来保证交换是安全的。之前有读者反馈+load方法本身即为线程安全,为什么仍需添加dispatch_once,其原因就在于+load方法本身无法保证其中代码只被执行一次。
 
实例2.防止按钮重复暴力点击
程序中大量按钮没有做连续响应的校验,连续点击出现了很多不必要的问题,例如发表帖子操作,用户手快点击多次,就会导致同一帖子发布多次。
#import <UIKit/UIKit.h>
//默认时间间隔
#define defaultInterval 1
@interface UIButton (Swizzling)
//点击间隔
@property (nonatomic, assign) NSTimeInterval timeInterval;
//用于设置单个按钮不需要被hook
@property (nonatomic, assign) BOOL isIgnore;
@end
#import "UIButton+Swizzling.h"
#import "NSObject+Swizzling.h"

@implementation UIButton (Swizzling)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self methodSwizzlingWithOriginalSelector:@selector(sendAction:to:forEvent:) bySwizzledSelector:@selector(sure_SendAction:to:forEvent:)];
    });
}

- (NSTimeInterval)timeInterval{
    return [objc_getAssociatedObject(self, _cmd) doubleValue];
}
- (void)setTimeInterval:(NSTimeInterval)timeInterval{
    objc_setAssociatedObject(self, @selector(timeInterval), @(timeInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    
}
//当按钮点击事件sendAction 时将会执行sure_SendAction
- (void)sure_SendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event{
    if (self.isIgnore) {
        //不需要被hook
        [self sure_SendAction:action to:target forEvent:event];
        return;
    }
    if ([NSStringFromClass(self.class) isEqualToString:@"UIButton"]) {
        self.timeInterval =self.timeInterval == 0 ?defaultInterval:self.timeInterval;
        if (self.isIgnoreEvent){
            return;
        }else if (self.timeInterval > 0){
            [self performSelector:@selector(resetState) withObject:nil afterDelay:self.timeInterval];
        }
    }
    //此处 methodA和methodB方法IMP互换了,实际上执行 sendAction;所以不会死循环
    self.isIgnoreEvent = YES;
    [self sure_SendAction:action to:target forEvent:event];
}
//runtime 动态绑定 属性
- (void)setIsIgnoreEvent:(BOOL)isIgnoreEvent{
    // 注意BOOL类型 需要用OBJC_ASSOCIATION_RETAIN_NONATOMIC 不要用错,否则set方法会赋值出错
    objc_setAssociatedObject(self, @selector(isIgnoreEvent), @(isIgnoreEvent), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (BOOL)isIgnoreEvent{
    //_cmd == @select(isIgnore); 和set方法里一致
    return [objc_getAssociatedObject(self, _cmd) boolValue];
}
- (void)setIsIgnore:(BOOL)isIgnore{
    // 注意BOOL类型 需要用OBJC_ASSOCIATION_RETAIN_NONATOMIC 不要用错,否则set方法会赋值出错
    objc_setAssociatedObject(self, @selector(isIgnore), @(isIgnore), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (BOOL)isIgnore{
    //_cmd == @select(isIgnore); 和set方法里一致
    return [objc_getAssociatedObject(self, _cmd) boolValue];
}
- (void)resetState{
    [self setIsIgnoreEvent:NO];
}
@end

 

实例3.全局修改导航栏(返回)按钮

iOS默认的返回按钮样式如下,默认为蓝色左箭头,文字为上一界面标题文字。

闲话少说,我们创建基于UINavigationItem的类别,在其load方法中替换方法backBarButtonItem

#import "UINavigationItem+Swizzling.h"
#import "NSObject+Swizzling.h"
static char *kCustomBackButtonKey;
@implementation UINavigationItem (Swizzling)
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self methodSwizzlingWithOriginalSelector:@selector(backBarButtonItem)
                               bySwizzledSelector:@selector(sure_backBarButtonItem)];

    });
}

- (UIBarButtonItem*)sure_backBarButtonItem {
    UIBarButtonItem *backItem = [self sure_backBarButtonItem];
    if (backItem) {
        return backItem;
    }
    backItem = objc_getAssociatedObject(self, &kCustomBackButtonKey);
    if (!backItem) {
        backItem = [[UIBarButtonItem alloc] initWithTitle:@"" style:UIBarButtonItemStylePlain target:nil action:NULL];
        objc_setAssociatedObject(self, &kCustomBackButtonKey, backItem, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    return backItem;
}
@end

这里进行将返回按钮的文字清空操作,其他需求样式大家也可随意替换,现在再次运行程序,就会发现所有的返回按钮均只剩左箭头,并右滑手势依然有效。如图所示

 

4.KVO实现

提供了一种当其它对象属性被修改的时候能通知当前对象的机制。

KVO的实现依赖于 Objective-C 强大的 Runtime,当观察某对象 A 时,KVO 机制动态创建一个对象A当前类的子类,并为这个新的子类重写了被观察属性 keyPath 的 setter 方法。setter 方法随后负责通知观察对象属性的改变状况。

Apple 使用了 isa-swizzling 来实现 KVO 。当观察对象A时,KVO机制动态创建一个新的名为:NSKVONotifying_A的新类,该类继承自对象A的本类,且 KVONSKVONotifying_A 重写观察属性的 setter 方法,setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象属性值的更改情况。

KVO 的键值观察通知依赖于 NSObject 的两个方法:willChangeValueForKey:didChangeValueForKey: ,在存取数值的前后分别调用 2 个方法:
被观察属性发生改变之前,willChangeValueForKey:被调用,通知系统该 keyPath 的属性值即将变更;
当改变发生后, didChangeValueForKey: 被调用,通知系统该keyPath 的属性值已经变更;之后, observeValueForKey:ofObject:change:context:也会被调用。且重写观察属性的setter 方法这种继承方式的注入是在运行时而不是编译时实现的。
KVO 为子类的观察者属性重写调用存取方法的工作原理在代码中相当于:
- (void)setName:(NSString *)newName { 
      [self willChangeValueForKey:@"name"];    //KVO 在调用存取方法之前总调用 
      [super setValue:newName forKey:@"name"]; //调用父类的存取方法 
      [self didChangeValueForKey:@"name"];     //KVO 在调用存取方法之后总调用
}

5.消息转发(热更新)解决Bug(JSPatch)

JSPatch 是一个 iOS 动态更新框架,只需在项目中引入极小的引擎,就可以使用 JavaScript 调用任何 Objective-C 原生接口,获得脚本语言的优势:为项目动态添加模块,或替换项目原生代码动态修复 bug。

 

6.实现NSCoding的自动归档和自动解档

runtime提供的函数遍历Model自身所有属性,并对属性进行encodedecode操作。
核心方法:在Model的基类中重写方法:

- (id)initWithCoder:(NSCoder *)aDecoder {
    if (self = [super init]) {
        unsigned int outCount;
        Ivar * ivars = class_copyIvarList([self class], &outCount);
        for (int i = 0; i < outCount; i ++) {
            Ivar ivar = ivars[i];
            NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
            [self setValue:[aDecoder decodeObjectForKey:key] forKey:key];
        }
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder {
    unsigned int outCount;
    Ivar * ivars = class_copyIvarList([self class], &outCount);
    for (int i = 0; i < outCount; i ++) {
        Ivar ivar = ivars[i];
        NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
        [aCoder encodeObject:[self valueForKey:key] forKey:key];
    }
}

上面就是Runtime的知识点以及常用场景,博客会持续更改,欢迎指正。

posted @ 2018-06-15 11:34  国孩  阅读(546)  评论(0编辑  收藏  举报