iOS运行时(runtime)探究四:实际运用

代码下载

github下载地址
CSDN下载地址

一、Runtime为分类添加属性

这里写图片描述
这里写图片描述
在oc中分类是不能添加属性的,可是有的时候在分类中添加属性又显得有必要,那么就可以通过运行时进行动态的添加属性。

场景说明:希望能够方便地对UIView添加点击事件。

问题分析:这个问题解决的最佳方式应该是使用分类而不是继承,因为继承不能使整个UIView体系中的所有类都具备点击能力。所以就得在分类中添加一个UITapGestureRecognizer点击手势的属性,并添加一个block的属性来保存点击的回调,再在点击手势的响应方法中读取出这个block回调来执行。

代码示例:


/**
 *  添加点击手势
 *
 *  @param action 手势执行的动作
 */
- (void)tapAction:(void (^)())action
{
    //获取点击手势属性
    UITapGestureRecognizer *tapGesture = objc_getAssociatedObject(self, &TapGesture);

    //是否获取到点击手势
    if (!tapGesture) {
        //创建点击手势
        tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapGestureAction:)];
        //把手势添加到视图
        [self addGestureRecognizer:tapGesture];
        //添加点击手势属性
        objc_setAssociatedObject(self, &TapGesture, tapGesture, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }

    //添加点击手势响应代码块属性
    objc_setAssociatedObject(self, &TapGestureAction, action, OBJC_ASSOCIATION_RETAIN);
}

/**
 *  点击手势执行的方法
 *
 *  @param sender 点击手势
 */
- (void)tapGestureAction:(UITapGestureRecognizer *)sender
{
    void (^action)() = objc_getAssociatedObject(self, &TapGestureAction);
    if (action) {
        action();
    }
}

二、Runtimer 解决NSTimer内存泄漏

这里写图片描述
这里写图片描述

iOS 中定时器需要加入到Runloop中,所以Runloop就会占有定时器的资源Target对象,通常定时器的Target对象就是创建定时器的对象,所以在不需要定时器的时候需要释放掉定时器中的资源,否则会造成内存泄漏。

场景说明:合理释放定时器的资源,解决内存泄漏。

问题分析:在定时任务完成后是需要invalidate释放掉资源,并把定时器置空。可是问题是在定时任务还没完成的时候,当定时器的Target销毁时,如果没有释放定时器资源的话Runloop会占有Target对象,造成内存泄漏,无法销毁。基于这样的情况,我制定了如下策略:把定时器的Target设置为第三方对象,第三方对象通过代理来回调定时任务或者通过运行时来交换定时器的方法,这样Runloop就不会占有定时器的对象了,就可以在创建定时器的对象的-dealoc:方法中释放掉定时器的资源并置空定时器。

- (void)dealloc
{
    //释放NSTimer的资源并置空NSTimer
    if (self.timer.isValid) {
        [self.timer invalidate];
        self.timer = nil;
    }

    NSLog(@"%@ 销毁了!", NSStringFromClass([self class]));
}

#pragma mark - 自定义方法
#define Timer_Key       "timer"
#define WeakSelf_Key    "weakSelf"
/**
 *  设置UI
 */
- (void)settingUi
{
    self.title = @"倒计时";

    NSTimer *timer;
    switch (self.type) {
        //产生内存泄漏
        case TimerTableViewControllerTypeMemoryLeak:
        {
            //NSTimer占用了self,如果不提前释放释放NSTimer占用的资源,就会造成self无法销毁,直到定时任务完成。
            timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerAction:) userInfo:nil repeats:YES];
        }
            break;
        //运行时解决内存泄漏
        case TimerTableViewControllerTypeRuntime:
        {
            //把NSTimer的Target对象设置为第三方对象
            NSObject *target = [[NSObject alloc] init];
            //为此对象动态添加方法
            class_addMethod([target class], @selector(timerAction:), (IMP)timerMethed, "V@:");
            timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:target selector:@selector(timerAction:) userInfo:nil repeats:YES];
            //为此对象动态添加属性,执行定时任务的对象与定时任务所需参数
            objc_setAssociatedObject(target, Timer_Key, timer, OBJC_ASSOCIATION_RETAIN);
            //此属性必须为弱引用,否则会引起循环引用
            objc_setAssociatedObject(target, WeakSelf_Key, self, OBJC_ASSOCIATION_ASSIGN);
        }
            break;
        //代理解决内存泄漏
        case TimerTableViewControllerTypeDelegate:
        {
            //把NSTimer的Target对象设置为第三方对象
            TimerTarget *timerTarget = [[TimerTarget alloc] init];
            timerTarget.delegate = self;
            timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:timerTarget selector:@selector(timerAction:) userInfo:nil repeats:YES];
        }
            break;

        default:
            break;
    }
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

    self.timer = timer;
}

#pragma mark - 触摸点击方法
void timerMethed(id self, SEL _cmd)
{
    //取出定时任务的执行对象和参数
    id ctr = objc_getAssociatedObject(self, WeakSelf_Key);
    NSTimer *timer = objc_getAssociatedObject(self, Timer_Key);

    //执行定时任务
    [ctr performSelector:_cmd withObject:timer];
}
- (void)timerAction:(NSTimer *)timer
{
    //finished记录定时任务释放完成
    BOOL finished = YES;

    //执行定时任务
    for (TimeModel *timeModel in self.dataArr) {
        if (timeModel.time > 0) {
            timeModel.time--;
            finished = NO;
        }
    }

    //定时任务完成,释放NSTimer的资源并置空NSTimer
    if (finished) {
        [timer invalidate];
        timer = nil;
    }
}

#pragma mark - <TimerTargetDelegate>代理方法
- (void)timerTargetDelegate:(TimerTarget *)timerTarget
{
    [self timerAction:self.timer];
}
#import <Foundation/Foundation.h>

@class TimerTarget;
@protocol TimerTargetDelegate <NSObject>

- (void)timerTargetDelegate:(TimerTarget *)timerTarget;

@end

@interface TimerTarget : NSObject

@property (weak, nonatomic) id delegate;

- (void)timerAction:(NSTimer *)timer;

@end
#import "TimerTarget.h"

@implementation TimerTarget

- (void)timerAction:(NSTimer *)timer
{
    if ([self.delegate respondsToSelector:@selector(timerTargetDelegate:)]) {
        [self.delegate timerTargetDelegate:self];
    }
}

@end

三、Runtime实现Method Swizzling

这里写图片描述

Method Swizzling是改变一个selector的实际实现的技术。通过这一技术,我们可以在运行时通过修改类的分发表中selector对应的函数,来修改方法的实现。

场景说明:在程序中每一个view controller展示给用户的次数记录下来。

问题分析:我们可以在每个view controller的viewDidAppear中添加跟踪代码;但是这太过麻烦,需要在每个view controller中写重复的代码。创建一个子类可能是一种实现方式,但需要同时创建UIViewController, UITableViewController, UINavigationController及其它UIKit中view controller的子类,这同样会产生许多重复的代码。这种情况下,我们就可以使用Method Swizzling。

主要代码:

+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class myClass = [self class];

        SEL oldSel = @selector(viewDidAppear:);
        SEL newSel = @selector(replaceViewDidAppear:);

        Method oldMethod = class_getInstanceMethod(myClass, oldSel);
        Method newMethod = class_getInstanceMethod(myClass, newSel);

        IMP newImp = method_getImplementation(newMethod);

        //class_addMethod会覆盖父类的方法实现,但不会取代本类中已存在的实现,如果本类中包含一个同名的实现,则函数会返回NO。
        BOOL added = class_addMethod(myClass, oldSel, newImp, method_getTypeEncoding(newMethod));
        //如果没有实现
        if (added) {
            //class_replaceMethod该函数的行为可以分为两种:如果类中不存在name指定的方法,则类似于class_addMethod函数一样会添加方法;如果类中已存在name指定的方法,则类似于method_setImplementation一样替代原方法的实现。
            class_replaceMethod(myClass, newSel, method_getImplementation(oldMethod), method_getTypeEncoding(newMethod));
        }
        else
        {
            //直接交换方法
            method_exchangeImplementations(oldMethod, newMethod);
        }

        //在NSUserDefaults创建一个字典用于记录数据
        NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
        NSMutableDictionary *dic = [NSMutableDictionary dictionaryWithCapacity:1];
        [userDefaults setObject:dic forKey:Statistics_Controllers];
        [userDefaults synchronize];
    });
}

- (void)replaceViewDidAppear:(BOOL)animated
{
    //由于方法的实现已经交换,千万要注意这个地方不能调用被替换的方法,否则会进入死循环
    [self replaceViewDidAppear:animated];

    //取出之前存储的字典对象
    NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
    NSMutableDictionary *dic = [NSMutableDictionary dictionaryWithDictionary:[userDefaults objectForKey:Statistics_Controllers]];
    NSString *className = NSStringFromClass([self class]);
    NSInteger index = [dic[className] integerValue] + 1;
    dic[className] = @(index);

    //以控制器的名称作为key来存储相关的访问次数
    [userDefaults setObject:dic forKey:Statistics_Controllers];
    [userDefaults synchronize];
}

Swizzling应该总是在+load中执行

在Objective-C中,运行时会自动调用每个类的两个方法。+load会在类初始加载时调用,+initialize会在第一次调用类的类方法或实例方法之前被调用。这两个方法是可选的,且只有在实现了它们时才会被调用。由于method swizzling会影响到类的全局状态,因此要尽量避免在并发处理中出现竞争的情况。+load能保证在类的初始化过程中被加载,并保证这种改变应用级别的行为的一致性。相比之下,+initialize在其执行时不提供这种保证–事实上,如果在应用中没为给这个类发送消息,则它可能永远不会被调用。

Swizzling应该总是在dispatch_once中执行

与上面相同,因为swizzling会改变全局状态,所以我们需要在运行时采取一些预防措施。原子性就是这样一种措施,它确保代码只被执行一次,不管有多少个线程。GCD的dispatch_once可以确保这种行为,我们应该将其作为method swizzling的最佳实践。

猜你喜欢

转载自blog.csdn.net/jiuchabaikaishui/article/details/52816479