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:@""];
}
#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 = @"";
NSLog(@"runtime动态添加属性name==%@",objc.name);
//结果如下: -- ::10.530 runtime[:] runtime动态添加属性--name ==
其实给属性赋值的本质,就是让属性与一个对象产生关联,所以要给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 = ,           /**< Specifies a weak reference to the associated object. */
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = , /**< Specifies a strong reference to the associated object.
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_COPY_NONATOMIC = ,   /**< Specifies that the associated object is copied.
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_RETAIN = ,       /**< Specifies a strong reference to the associated object.
                                            *   The association is made atomically. */
    OBJC_ASSOCIATION_COPY =           /**< Specifies that the associated object is copied.
                                            *   The association is made atomically. */
};
下面实现一个UIView的Category添加自定义属性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
结果如下:
打印结果:
-- ::44.977732+ ocram[:] UIExtendedGrayColorSpace
从打印结果来看:我们成功在分类上添加一个属性,实现了它的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为一个类添加方法
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
当然可以依次在每个界面的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方法中?
(2)为什么方法要在dispatch_once中执行?
实例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 == ?defaultInterval:self.timeInterval;
if (self.isIgnoreEvent){
return;
}else if (self.timeInterval > ){
[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的本类,且 KVO 为 NSKVONotifying_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自身所有属性,并对属性进行encode和decode操作。
核心方法:在Model的基类中重写方法:
- (id)initWithCoder:(NSCoder *)aDecoder {
    if (self = [super init]) {
        unsigned int outCount;
        Ivar * ivars = class_copyIvarList([self class], &outCount);
        for (int i = ; 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 = ; i < outCount; i ++) {
        Ivar ivar = ivars[i];
        NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
        [aCoder encodeObject:[self valueForKey:key] forKey:key];
    }
}
上面就是Runtime的知识点以及常用场景,博客会持续更改,欢迎指正。
Runtime详解(下)的更多相关文章
- 李洪强iOS经典面试题156 - Runtime详解(面试必备)
		李洪强iOS经典面试题156 - Runtime详解(面试必备) 一.runtime简介 RunTime简称运行时.OC就是运行时机制,也就是在运行时候的一些机制,其中最主要的是消息机制. 对于C ... 
- iOS开发-Runtime详解
		iOS开发-Runtime详解 简介 Runtime 又叫运行时,是一套底层的 C 语言 API,其为 iOS 内部的核心之一,我们平时编写的 OC 代码,底层都是基于它来实现的.比如: [recei ... 
- [js高手之路]深入浅出webpack教程系列3-配置文件webpack.config.js详解(下)
		本文继续接着上文,继续写下webpack.config.js的其他配置用法. 一.把两个文件打包成一个,entry怎么配置? 在上文中的webpack.dev.config.js中,用数组配置entr ... 
- SSL/TLS协议详解(下)——TLS握手协议
		本文转载自SSL/TLS协议详解(下)--TLS握手协议 导语 在博客系列的第2部分中,对证书颁发机构进行了深入的讨论.在这篇文章中,将会探索整个SSL/TLS握手过程,在此之前,先简述下最后这块内容 ... 
- .Net Attribute详解(下) - 使用Attribute武装枚举类型
		接上文.Net Attribute详解(上)-Attribute本质以及一个简单示例,这篇文章介绍一个非常实用的例子,相信你一定能够用到你正在开发的项目中.枚举类型被常常用到项目中,如果要使用枚举To ... 
- IE8"开发人员工具"使用详解下(浏览器模式、文本模式、JavaScript调试、探查器)
		来源: http://www.cnblogs.com/JustinYoung/archive/2009/04/03/kaifarenyuangongju2.html 在上一篇文章IE8“开发人员工具” ... 
- CSS2.1SPEC:视觉格式化模型之width属性详解(下)
		本文承接CSS2.1SPEC:视觉格式化模型之width属性详解(上),继续分析CSS视觉格式化模型中width以及相关值的计算问题: 注:与上节不同,本节的demo中由于出现了float,absol ... 
- Linux常用命令详解下
		Linux常用命令详解 目录 一.Linux常用命令 1.1.查看及切换目录(pwd.cd.ls.du) 1.2.创建目录和文件(mkdir.touch.ln) 1.3.复制.删除.移动目录和文件(c ... 
- Android Touch事件传递机制详解 下
		尊重原创:http://blog.csdn.net/yuanzeyao/article/details/38025165 资源下载:http://download.csdn.net/detail/yu ... 
- logback 配置详解(下)
		logback 常用配置详解(二) <appender> <appender>: <appender>是<configuration>的子节点,是负责写 ... 
随机推荐
- 关于Podfile,某个第三方指定源
			项目中有个指定了源,摸索好久Podfile编写方式,网上都没有 pod 'SDK名字', :source => '指定源' 其他的直接按原来的就可以了 
- git无法同步
			出现问题: fatal: destination path 'test' already exists and is not an empty directory. 解决方法如下: git init ... 
- 获取resource下文件
			Resource resource = new ClassPathResource(certPath);File file= resource.getFile(); 
- PHPExcel防止大数以科学计数法显示
			在使用PHPExcel来进行数据导出时,常常需要防止有些数字(如手机号.身份证号)以科学计数法显示,我们可以采用下面的方式来解决: setCellValueExplicit第三个参数用PHPExcel ... 
- 全栈开发工程师微信小程序 - 上
			全栈开发工程师微信小程序-上 实现swiper组件 swiper 滑块视图容器. indicator-dots 是否显示面板指示点 false indicator-color 指示点颜色 indica ... 
- Kali学习笔记21:缓冲区溢出实验(漏洞发现)
			上一篇文章,我已经做好了缓冲区溢出实验的准备工作: https://www.cnblogs.com/xuyiqing/p/9835561.html 下面就是Kali虚拟机对缓冲区溢出的测试: 已经知道 ... 
- MySql必备技能 不会的赶紧get一下  可以说很详细了
			1.Mysql服务 mysql服务如何开启: 下载了mysql数据库你的服务中会有mysql服务. 1.1: 1.2: 2.使用sql语句进行 建库.建表.等操作. 2.1:使用sql语句进行创建数据 ... 
- Docker - 参考信息
			初见 从 0 开始了解 Docker 可能是把Docker的概念讲的最清楚的一篇文章 Docker新手指南 8 个基本的 Docker 容器管理命令 Docker 核心技术与实现原理 在线教程 Doc ... 
- 学习笔记第六课  VB程序
			VB程序的特殊地方在于: 前几课学的破解方法,诸如设置API断点,修改关键CALL的返回值,MESSAGEBOX断点等,这些对于VB程序都是无效的. 这节课是设置VB的API断点,绕过报错弹窗来破解. ... 
- centos7系统配置记录SFTP操作日志
			1.修改ssh配置 [root@elk-node2 ~]# vim /etc/ssh/sshd_config 大概132行把下面这个句注释掉 #Subsystem sftp /usr ... 
