前言
RunTime
我第一次接触概念还是在17年,那时候有个需求,点击按钮(不是全部)都要先检查用户是否认证(大概如此)下面会介绍如何实现。当时iOS
老大说他来写,然后我recode
他的代码,研究才知道有这个东西,后来面试也有遇到不少大佬问到,今天就来简单总结一下。
RumTime API
Runtime
是一套C语言的API,封装了很多动态性相关的函数,可以在程序运行时动态的创建对象,方法和属性,修改类和对象的方法,属性等。先简单介绍常用的API,就开始说实战。
objc相关
函数 | 说明 |
---|---|
objc_getClass | 获取Class对象 |
objc_allocateClassPair | 动态创建一个类 |
objc_registerClassPair | 注册一个类 |
objc_disposeClassPair | 销毁一个类 |
objc_setAssociatedObject | 为实例对象关联对象 |
objc_getAssociatedObject | 获取实例对象的关联对象 |
class相关
函数 | 说明 |
---|---|
class_getSuperclass | 获取父类 |
class_addIvar | 动态添加成员变量 |
class_addProperty | 动态添加属性方法 |
class_addMethod | 动态添加方法 |
class_replaceMethod | 动态替换方法 |
class_getInstanceVariable | 获取实例变量 |
class_getClassVariable | 获取类变量 |
class_getInstanceMethod | 获取实例方法 |
class_getClassMethod | 获取类方法 |
class_getMethodImplementation | 获取父类方法实现 |
class_getInstanceSize | 获取实例大小 |
class_copyMethodList | 获取类的方法数组 |
object相关
函数 | 说明 |
---|---|
object_getClassName | 获取对象的类名 |
object_getClass | 获取对象的类 |
object_getIvar | 获取对象成员变量的值 |
object_setIvar | 设置对象成员变量的值 |
method相关
函数 | 说明 |
---|---|
method_getName | 获取方法名 |
method_getImplementation | 获取方法的实现 |
method_getTypeEncoding | 获取方法的类型编码 |
method_setImplementation | 设置方法的实现 |
method_exchangeImplementations | 替换方法的实现 |
property相关
函数 | 说明 |
---|---|
property_getName | 获取属性名 |
property_getAttributes | 获取属性的特性列表 |
ivar相关
函数 | 说明 |
---|---|
ivar_getName | 获取成员变量名称 |
ivar_getOffset | 获取偏移量 |
ivar_getTypeEncoding | 获取类型编码 |
protocol相关
函数 | 说明 |
---|---|
protocol_getName | 获取协议名称 |
protocol_addProperty | 协议添加属性 |
protocol_getProperty | 获取协议属性 |
protocol_copyPropertyList | 拷贝协议的属性列表名 |
runtime 的实战
KVO
KVO
的全称是(Key-Value Observing),俗称“键值监听",可以用于监听某个对象属性值的改变。
[self.person addObserver:self
forKeyPath:@"age"
options:NSKeyValueObservingOptionNew
context:nil];
本质:利用Runtime
API动态生成一个子类,并且让instance
对象的isa
指向这个全新的子类;
KVC
KVC
全程是(key-Value coding),俗称“键值编码",允许开发者通过Key名直接访问对象的属性,或者给对象的属性赋值。不需要调用明确的存取方法,这样就可以在运行时动态访问和修改对象的属性,而不是在编译时确定。
[textField setValue:[UIColor greenColor] forKeyPath:@"_placeholderLabel.textColor"];
上述例子就是通过KVC
修改占位文字颜色,可惜在iOS13
中已经禁止textfield
通过KVC
获取和修改私有属性。
字典转模型
在OC中,字典转模型一般我们用第三方库MJExtension
,YYModel
来运用。
基本原理就是利用Runtime
可以获取模型中所有属性这一特性,来对要进行转换的字典进行遍历,利用KVC
的- (nullable id)valueForKeyPath:(NSString *)keyPath;
和- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
方法去取出模型属性并作为字典中相对应的key
,来取出其所对应的value
,并把value
赋值给模型属性。
下面来个简单的字典转模型的例子
- (void)transformDict:(NSDictionary *)dict {
Class class = self.class;
// count:成员变量个数
unsigned int count = 0;
// 获取成员变量数组
Ivar *ivars = class_copyIvarList(class, &count);
// 遍历所有成员变量
for (int i = 0; i < count; i++) {
// 获取成员变量
Ivar ivar = ivars[i];
// 获取成员变量名字
NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 成员变量名转为属性名(去掉下划线 _ )
key = [key substringFromIndex:1];
// 取出字典的值
id value = dict[key];
// 如果模型属性数量大于字典键值对数理,模型属性会被赋值为nil而报错
if (value == nil) continue;
// 利用KVC将字典中的值设置到模型上
[self setValue:value forKeyPath:key];
}
//释放指针
free(ivars);
}
自动归档解档
当我们需要将一个对象进行归档时,都要让该对象的类遵守NSCoding
协议,再实现归档和接档方法。
基本原理就是利用Runtime
可以获取实例成员变量,然后再通过KVC
进行操作。
// 归档
- (void)encodeWithCoder:(NSCoder *)coder {
// count:成员变量个数
unsigned int count = 0;
// 获取成员变量数组
Ivar *ivars = class_copyIvarList([self class], &count);
// 遍历所有成员变量
for (int i = 0; i < count; i++) {
// 获取成员变量
Ivar ivar = ivars[I];
// 获取成员变量名字
NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 获取成员变量的值
id value = [self valueForKey:key];
// 编码
[coder encodeObject:value forKey:key];
}
//释放指针
free(ivars);
}
// 解档
- (instancetype)initWithCoder:(NSCoder *)coder {
self = [super init];
if (self) {
// count:成员变量个数
unsigned int count = 0;
// 获取成员变量数组
Ivar *ivars = class_copyIvarList([self class], &count);
// 遍历所有成员变量
for (int i = 0; i < count; i++) {
// 获取成员变量
Ivar ivar = ivars[I];
// 获取成员变量名字
NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 获取成员变量的值
id value = [coder decodeObjectOfClasses:[NSSet setWithObject:[self class]] forKey:key];
// KVC
[self setValue:value forKey:key];
}
//释放指针
free(ivars);
}
return self;
}
Method Swizzling
Method Swizzing
在程序运行时,通过改变 selector
所在 Class
(类)的 method list
(方法列表)的映射从而改变方法的调用。其实质就是交换两个方法的 IMP
(方法实现)。不建议开发者滥用。
+ (void)jj_MethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL
{
if (!cls) NSLog(@"传入的交换类不能为空");
// 原始的方法
Method oriMethod = class_getInstanceMethod(cls, oriSEL);
// 想要交换的方法
Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
//添加方法看是不是属于本类实现的,oriSEL对应的是oriMethod,如果是本类有的oriMethod,是不会添加成功。
BOOL success = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
if (success) {
// 成功,cls中不存在oriMethod,也就是存在父类
class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
}else{
当前oriMethod是当前cls的,直接交换方法
method_exchangeImplementations(oriMethod, swiMethod);
}
}
全局页面统计
添加一个UIViewController
的分类,通过load
方法和dispatch_once_t
来保证只加载一次,UIViewController
的viewWillAppear
方法,交换为swizz_viewWillAppear
方法。
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
SEL originalSEL = @selector(viewWillAppear:);
SEL swizzledSEL = @selector(swizz_viewWillAppear:);
// 交换方法
[JJRuntimeTool jj_MethodSwizzlingWithClass:class oriSEL:originalSEL swizzledSEL:swizzledSEL];
});
}
- (void)swizz_viewWillAppear:(BOOL)animated
{
// 这里是调用交换方法的viewWillAppear,不是递归
[self swizz_viewWillAppear:animated];
NSLog(@"统计页面: %@", [self class]);
}
防止按钮多次点击事件
这里我们要配合分类UIControl
,使用关联对象来添加属性。delayInterval
来控制按钮点击几秒后才可以继续响应事件。
@interface UIControl (Swizzling)
// 是否忽略事件
@property (nonatomic, assign) BOOL ignoreEvent;
// 延迟多少秒可继续执行
@property (**nonatomic, assign) NSTimeInterval delayInterval;
@end
设置属性的set
和get
方法。
- (void)setIgnoreEvent:(BOOL)ignoreEvent
{
objc_setAssociatedObject(self, @"associated_ignoreEvent", @(ignoreEvent), OBJC_ASSOCIATION_ASSIGN);
}
- (BOOL)ignoreEvent
{
return [objc_getAssociatedObject(self, @"associated_ignoreEvent") boolValue];
}
- (void)setDelayInterval:(NSTimeInterval)delayInterval
{
objc_setAssociatedObject(self, @"associated_delayInterval", @(delayInterval), OBJC_ASSOCIATION_ASSIGN);
}
- (NSTimeInterval)delayInterval
{
return [objc_getAssociatedObject(self, @"associated_delayInterval") doubleValue];
}
这里的实现方法也是通过交换响应事件sendAction:to:forEvent:
方法来实现延迟响应事件。
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
SEL originalSEL = @selector(sendAction:to:forEvent:);
SEL swizzledSEL = @selector(swizzl_sendAction:to:forEvent:);
[JJRuntimeTool jj_MethodSwizzlingWithClass:class oriSEL:originalSEL swizzledSEL:swizzledSEL];
});
}
- (void)swizzl_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event
{
// 如果忽略响应,就return
if (self.ignoreEvent) return;
if (self.delayInterval > 0) {
//添加了延迟,ignoreEvent就设置为YES,让上面来拦截。
self.ignoreEvent = YES;
// 延迟delayInterval秒后,让ignoreEvent为NO,可以继续响应
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(self.delayInterval * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
self.ignoreEvent = NO;
});
}
// 调用系统的sendAction方法
[self swizzl_sendAction:action to:target forEvent:event];
}
总体上实现流程,和前言写的需求实现方式差不多。
防止数组越界崩溃
交换数组的objectAtIndex
方法,检测是否越界,来防止崩溃。
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = NSClassFromString(@"__NSArrayM");
SEL originalSEL = @selector(objectAtIndex:);
SEL swizzledSEL = @selector(swizz_objectAtIndex:);
[JJRuntimeTool jj_MethodSwizzlingWithClass:class oriSEL:originalSEL swizzledSEL:swizzledSEL];
});
}
- (id)swizz_objectAtIndex:(NSUInteger)index
{
if (index < self.count) {
return [self swizz_objectAtIndex:index];
}else {
@try {
return [self swizz_objectAtIndex:index];
} @catch (NSException *exception) {
NSLog(@"------- %s Crash Bacause Method %s --------- \n", class_getName(self.class), __func__ );
NSLog(@"%@", [exception callStackSymbols]);
return nil;
} @finally {
}
}
}
消息转发机制拦截崩溃
如果调用objc_msgSend
后,找不到IMP
实现方法,就会来到消息转发机制resolveInstanceMethod
,这时候,我们可以动态添加一个方法,让sel
指向我们的动态实现的IMP
,来防止崩溃。
void testFun(){
NSLog(@"test Fun");
}
+(BOOL)resolveInstanceMethod:(SEL)sel{
if ([super resolveInstanceMethod:sel]) {
return YES;
}else{
class_addMethod(self, sel, (IMP)testFun, "v@:");
return YES;
}
}
Aspects
面向切面编程Aspects
,不修改原来的函数,可以在函数的执行前后插入一些代码。这个是我在公司的老项目发现用的库,觉得有意思,也写下来。
核心是方法aspect_hookSelector
。
/**
作用域:针对所有对象生效
selector: 需要hook的方法
options:是个枚举,主要定义了切面的时机(调用前、替换、调用后)
block: 需要在selector前后插入执行的代码块
error: 错误信息
*/
+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error;
/**
作用域:针对当前对象生效
*/
- (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error;
AspectOptions
AspectOptions
是个枚举,用来定义切面的时机,即原有方法调用前、调用后、替换原有方法、只执行一次(调用完就删除切面逻辑)
typedef NS_OPTIONS(NSUInteger, AspectOptions) {
AspectPositionAfter = 0, /// 原有方法调用后
AspectPositionInstead = 1, /// 替换原有方法
AspectPositionBefore = 2, /// 原有方法调用前执行
AspectOptionAutomaticRemoval = 1 << 3 /// 执行完之后就恢复切面操作,即撤销hook
};
AspectsDemo
我们想在当前控制器调用viewWillAppear
后,进行操作内容。
- (void)viewDidLoad {
[super viewDidLoad];
[self aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionAfter usingBlock:^{
NSLog(@"CCCCCC");
} error:nil];
}
- (void)viewWillAppear:(BOOL)animated
{
NSLog(@"AAAAAA");
[super viewWillAppear:animated];
NSLog(@"BBBBBB");
}
运行结果:
022-01-14 15:32:49.962692+0800 AspectsDemo[68165:356953] AAAAAA
2022-01-14 15:32:49.962785+0800 AspectsDemo[68165:356953] BBBBBB
2022-01-14 15:32:49.962847+0800 AspectsDemo[68165:356953] CCCCCC
附上gitHub地址:Aspects