runtime的实战应用和Aspects使用

565

前言

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中,字典转模型一般我们用第三方库MJExtensionYYModel来运用。

基本原理就是利用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来保证只加载一次,UIViewControllerviewWillAppear方法,交换为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

设置属性的setget方法。

- (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

参考资料

  1. iOS Runtime详细介绍及实战使用
  2. Runtime实现iOS字典转模型
  3. Aspects深度解析-iOS面向切面编程
  4. iOS 开发:『Runtime』详解(二)Method Swizzling
  5. iOS开发·runtime原理与实践: 方法交换篇
  6. 浅谈 iOS swizzle