Fork me on GitHub
An Guoli's Blog

Runtime应用

Runtime原理探究参考另一篇文章 Runtime原理探究, 本文主要整理Runtime在实际开发中的应用.

一. 交换方法(method swizzing)

涉及到的方法
load class_getInstanceMethod class_getClassMethod method_exchangeImplementations method_getImplementation class_addMethod class_replaceMethod

让你快速上手Runtime 袁峥

  • 开发使用场景:系统自带的方法功能不够,给系统自带的方法扩展一些功能,并且保持原有的功能。
  • 方式一:继承系统的类,重写方法.
  • 方式二:使用Runtime,交换方法.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    @implementation ViewController
    - (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    // 需求:给imageNamed方法提供功能,每次加载图片就判断下图片是否加载成功。
    // 步骤一:先搞个分类,定义一个能加载图片并且能打印的方法+ (instancetype)imageWithName:(NSString *)name;
    // 步骤二:交换imageNamed和imageWithName的实现,就能调用imageWithName,间接调用imageWithName的实现。
    UIImage *image = [UIImage imageNamed:@"123"];
    }
    @end
    @implementation UIImage (Image)
    // 加载分类到内存的时候调用
    + (void)load
    {
    // 交换方法
    // 获取imageWithName方法地址
    Method imageWithName = class_getClassMethod(self, @selector(imageWithName:));
    // 获取imageWithName方法地址
    Method imageName = class_getClassMethod(self, @selector(imageNamed:));
    // 交换方法地址,相当于交换实现方式
    method_exchangeImplementations(imageWithName, imageName);
    }
    // 不能在分类中重写系统方法imageNamed,因为会把系统的功能给覆盖掉,而且分类中不能调用super.
    // 既能加载图片又能打印
    + (instancetype)imageWithName:(NSString *)name
    {
    // 这里调用imageWithName,相当于调用imageName
    UIImage *image = [self imageWithName:name];
    if (image == nil) {
    NSLog(@"加载空的图片");
    }
    return image;
    }
    @end

Objective-C中的Runtime LeeJay

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL originalSelector = @selector(willMoveToSuperview:);
SEL swizzledSelector = @selector(myWillMoveToSuperview:);
Method originalMethod = class_getInstanceMethod(self, originalSelector);
Method swizzledMethod = class_getInstanceMethod(self, swizzledSelector);
BOOL didAddMethod = class_addMethod(self,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(self,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
- (void)myWillMoveToSuperview:(UIView *)newSuperview
{
NSLog(@"WillMoveToSuperview: %@", self);
[self myWillMoveToSuperview:newSuperview];
}

  • 1.如果我现在想检查一下项目中有没有内存循环,怎么办?是不是要重写dealloc函数,看下dealloc有没有执行,项目小的时候,一个一个controller的写,还不麻烦,如果项目大,要是一个一个的写,估计你会疯掉的。这时候方法交换就派上用场了,你就可以尝试用自己的方法交换系统的dealloc方法,几句代码就搞定了。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    #import "UIViewController+Dealloc.h"
    #import <objc/runtime.h>
    @implementation UIViewController (Dealloc)
    + (void)load
    {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
    Method method1 = class_getInstanceMethod(self, NSSelectorFromString(@"dealloc"));
    Method method2 = class_getInstanceMethod(self, @selector(my_dealloc));
    method_exchangeImplementations(method1, method2);
    });
    }
    - (void)my_dealloc
    {
    NSLog(@"%@销毁了", self);
    [self my_dealloc];
    }
    @end
  • 2.数组越界,向数组中添加一个nil对象等等,都会造成闪退,我们可以用自己的方法交换数组相对应的方法。下面是一个交换数组addObject:方法的栗子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    #import "NSMutableArray+Category.h"
    #import <objc/runtime.h>
    @implementation NSMutableArray (Category)
    + (void)load
    {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
    SEL originalSelector = @selector(addObject:);
    SEL swizzledSelector = @selector(lj_AddObject:);
    // NSMutableArray是类簇,真正的类名是__NSArrayM
    Method originalMethod = class_getInstanceMethod(objc_getClass("__NSArrayM"), originalSelector);
    Method swizzledMethod = class_getInstanceMethod(objc_getClass("__NSArrayM"), swizzledSelector);
    BOOL didAddMethod = class_addMethod(self,
    originalSelector,
    method_getImplementation(swizzledMethod),
    method_getTypeEncoding(swizzledMethod));
    if (didAddMethod)
    {
    class_replaceMethod(self,
    swizzledSelector,
    method_getImplementation(originalMethod),
    method_getTypeEncoding(originalMethod));
    }
    else
    {
    method_exchangeImplementations(originalMethod, swizzledMethod);
    }
    });
    }
    - (void)lj_AddObject:(id)object
    {
    if (object != nil)
    {
    [self lj_AddObject:object];
    }
    }
    @end

PS:我不太建议大家平时开发的时候使用这类数组安全操作的做法,不利于代码的调试,如果真的加入了nil对象,你可能就不会那么容易找出问题在哪,还是在项目发布的时候使用比较合适。

Runtime Method Swizzling开发实例汇总(持续更新中) 卖报的小画家Sure

Method Swizzling通用方法封装

在列举之前,我们可以将Method Swizzling功能封装为类方法,作为NSObject的类别,这样我们后续调用也会方便些。

1
2
3
4
5
6
7
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
@interface NSObject (Swizzling)
+ (void)methodSwizzlingWithOriginalSelector:(SEL)originalSelector
bySwizzledSelector:(SEL)swizzledSelector;
@end

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#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替换的是父类的那个方法,这当然不是我们想要的。所以我们先尝试添加 orginalSelector,如果已经存在,再用 method_exchangeImplementations 把原方法的实现跟新的方法实现给交换掉。
    如果理解还不够透彻,我们可以进入runtime.h中查看class_addMethod源码解释:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    /**
    * Adds a new method to a class with a given name and implementation.
    *
    * @param cls The class to which to add a method.
    * @param name A selector that specifies the name of the method being added.
    * @param imp A function which is the implementation of the new method. The function must take at least two arguments—self and _cmd.
    * @param types An array of characters that describe the types of the arguments to the method.
    *
    * @return YES if the method was added successfully, otherwise NO
    * (for example, the class already contains a method implementation with that name).
    *
    * @note class_addMethod will add an override of a superclass's implementation,
    * but will not replace an existing implementation in this class.
    * To change an existing implementation, use method_setImplementation.
    */

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

1
2
3
class_addMethod(class,originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));

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

1
2
3
class_replaceMethod(class,swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));

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

1
method_exchangeImplementations(originalMethod, swizzledMethod);

实例一:替换ViewController生命周期方法

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#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];
}

代码如上,这样就不用考虑界面是否移除加载栏的问题了。补充一点,通常我们也会在生命周期方法中设置默认界面背景颜色,因若背景颜色默认为透明对App的性能也有一定影响,这大家可以在UIKit性能优化那篇文章中查阅。但类似该类操作也可以书写在通用类中,所以具体使用还要靠自己定夺。

实例二:解决获取索引、添加、删除元素越界崩溃问题

对于NSArray、NSDictionary、NSMutableArray、NSMutableDictionary不免会进行索引访问、添加、删除元素的操作,越界问题也是很常见,这时我们可以通过Method Swizzling解决这些问题,越界给予提示防止崩溃。

这里以NSMutableArray为例说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#import "NSMutableArray+Swizzling.h"
#import "NSObject+Swizzling.h"
@implementation NSMutableArray (Swizzling)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[objc_getClass("__NSArrayM") methodSwizzlingWithOriginalSelector:@selector(removeObject:) bySwizzledSelector:@selector(safeRemoveObject:) ];
[objc_getClass("__NSArrayM") methodSwizzlingWithOriginalSelector:@selector(addObject:) bySwizzledSelector:@selector(safeAddObject:)];
[objc_getClass("__NSArrayM") methodSwizzlingWithOriginalSelector:@selector(removeObjectAtIndex:) bySwizzledSelector:@selector(safeRemoveObjectAtIndex:)];
[objc_getClass("__NSArrayM") methodSwizzlingWithOriginalSelector:@selector(insertObject:atIndex:) bySwizzledSelector:@selector(safeInsertObject:atIndex:)];
[objc_getClass("__NSArrayM") methodSwizzlingWithOriginalSelector:@selector(objectAtIndex:) bySwizzledSelector:@selector(safeObjectAtIndex:)];
});
}
- (void)safeAddObject:(id)obj {
if (obj == nil) {
NSLog(@"%s can add nil object into NSMutableArray", __FUNCTION__);
} else {
[self safeAddObject:obj];
}
}
- (void)safeRemoveObject:(id)obj {
if (obj == nil) {
NSLog(@"%s call -removeObject:, but argument obj is nil", __FUNCTION__);
return;
}
[self safeRemoveObject:obj];
}
- (void)safeInsertObject:(id)anObject atIndex:(NSUInteger)index {
if (anObject == nil) {
NSLog(@"%s can't insert nil into NSMutableArray", __FUNCTION__);
} else if (index > self.count) {
NSLog(@"%s index is invalid", __FUNCTION__);
} else {
[self safeInsertObject:anObject atIndex:index];
}
}
- (id)safeObjectAtIndex:(NSUInteger)index {
if (self.count == 0) {
NSLog(@"%s can't get any object from an empty array", __FUNCTION__);
return nil;
}
if (index > self.count) {
NSLog(@"%s index out of bounds in array", __FUNCTION__);
return nil;
}
return [self safeObjectAtIndex:index];
}
- (void)safeRemoveObjectAtIndex:(NSUInteger)index {
if (self.count <= 0) {
NSLog(@"%s can't get any object from an empty array", __FUNCTION__);
return;
}
if (index >= self.count) {
NSLog(@"%s index out of bound", __FUNCTION__);
return;
}
[self safeRemoveObjectAtIndex:index];
}
@end

对应大家可以举一反三,相应的实现添加、删除等,以及NSArray、NSDictionary等操作,因代码篇幅较大,这里就不一一书写了。
这里没有使用self来调用,而是使用objc_getClass(“__NSArrayM”)来调用的。因为NSMutableArray的真实类只能通过后者来获取,而不能通过[self class]来获取,而method swizzling只对真实的类起作用。这里就涉及到一个小知识点:类簇。补充以上对象对应类簇表。
类簇表

实例三:防止按钮重复暴力点击

程序中大量按钮没有做连续响应的校验,连续点击出现了很多不必要的问题,例如发表帖子操作,用户手快点击多次,就会导致同一帖子发布多次。

1
2
3
4
5
6
7
8
9
#import <UIKit/UIKit.h>
//默认时间间隔
#define defaultInterval 1
@interface UIButton (Swizzling)
//点击间隔
@property (nonatomic, assign) NSTimeInterval timeInterval;
//用于设置单个按钮不需要被hook
@property (nonatomic, assign) BOOL isIgnore;
@end

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#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

实例四:全局更换控件初始效果

以UILabel为例,在项目比较成熟的基础上,应用中需要引入新的字体,需要更换所有Label的默认字体,但是同时,对于一些特殊设置了字体的label又不需要更换。乍看起来,这个问题确实十分棘手,首先项目比较大,一个一个设置所有使用到的label的font工作量是巨大的,并且在许多动态展示的界面中,可能会漏掉一些label,产生bug。其次,项目中的label来源并不唯一,有用代码创建的,有xib和storyBoard中的,这也将浪费很大的精力。这时Method Swizzling可以解决此问题,避免繁琐的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#import "UILabel+Swizzling.h"
#import "NSObject+Swizzling.h"
@implementation UILabel (Swizzling)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self methodSwizzlingWithOriginalSelector:@selector(init) bySwizzledSelector:@selector(sure_Init)];
[self methodSwizzlingWithOriginalSelector:@selector(initWithFrame:) bySwizzledSelector:@selector(sure_InitWithFrame:)];
[self methodSwizzlingWithOriginalSelector:@selector(awakeFromNib) bySwizzledSelector:@selector(sure_AwakeFromNib)];
});
}
- (instancetype)sure_Init{
id __self = [self sure_Init];
UIFont * font = [UIFont fontWithName:@"Zapfino" size:self.font.pointSize];
if (font) {
self.font=font;
}
return __self;
}
- (instancetype)sure_InitWithFrame:(CGRect)rect{
id __self = [self sure_InitWithFrame:rect];
UIFont * font = [UIFont fontWithName:@"Zapfino" size:self.font.pointSize];
if (font) {
self.font=font;
}
return __self;
}
- (void)sure_AwakeFromNib{
[self sure_AwakeFromNib];
UIFont * font = [UIFont fontWithName:@"Zapfino" size:self.font.pointSize];
if (font) {
self.font=font;
}
}
@end

这一实例个人认为使用率可能不高,对于产品的设计这些点都是已经确定好的,更改的几率很低。况且我们也可以使用appearance来进行统一设置。

实例六:App异常加载占位图通用类封装(更新于:2016/12/01)

详情可见文章:《零行代码为App添加异常加载占位图》

在该功能模块中,使用Runtime Method Swizzling进行替换tableView、collectionView的reloadData方法,使得每当执行刷新操作时,自动检测当前组数与行数,从而实现零代码判断占位图是否显示的功能,同样也适用于网络异常等情况,详细设置可前往阅读。

实例七:全局修改导航栏后退(返回)按钮(更新于:2016/12/05)

在真实项目开发中,会全局统一某控件样式,以导航栏后退(返回)按钮为例,通常项目中会固定为返回字样,或者以图片进行显示等。

iOS默认的返回按钮样式如下,默认为蓝色左箭头,文字为上一界面标题文字。
image.png
这里我们仍可以通过Runtime Method Swizzling来实现该需求,在使用Method Swizzling进行更改之前,必须考虑注意事项,即尽可能的不影响原有操作,比如对于系统默认的返回按钮,与其对应的是有界面边缘右滑返回功能的,因此我们进行统一更改后不可使其功能废弃。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#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

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

Runtime基本知识点以及应用场景 deft_mkjing

交换方法实现的需求场景:自己创建了一个功能性的方法,在项目中多次被引用,当项目的需求发生改变时,要使用另一种功能代替这个功能,要求是不改变旧的项目(也就是不改变原来方法的实现)。
可以在类的分类中,再写一个新的方法(是符合新的需求的),然后交换两个方法的实现。这样,在不改变项目的代码,而只是增加了新的代码 的情况下,就完成了项目的改进。
交换两个方法的实现一般写在类的load方法里面,因为load方法会在程序运行前加载一次,而initialize方法会在类或者子类在 第一次使用的时候调用,当有分类的时候会调用多次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// Load的时候如果下面的方法是-方法,那么是无效的,类方法对实例方法无法操作
+ (void)load
{
Method eatM = class_getClassMethod(self, sel_registerName("eat:"));
Method sleepM = class_getClassMethod(self, @selector(sleep:));
method_exchangeImplementations(eatM, sleepM);
}
// 如果要在自己的方法里面调用另个一个方法,直接调用自己的方法名就好了
+ (void)eat:(NSString *)food
{
NSLog(@"%@大口吃%@",NSStringFromClass([self class]),food);
}
// 如果这样调用直接死循环了
+ (void)sleep:(NSString *)name
{
// [self eat:@"屎"]; 死循环
NSLog(@"%@睡了%@",NSStringFromClass([self class]),name);
[self sleep:@"屎"];
}
objc_msgSend(objc_getClass("Dog"), sel_registerName("eat:"),@"aaa");
// 打印如下
// 2016-12-19 17:25:15.699 RuntimeKJ[12219:363270] Dog睡了aaa
// 2016-12-19 17:25:15.700 RuntimeKJ[12219:363270] Dog大口吃屎
// 先是调用eat的方法,但是由于方法的调换,先调用了sleep方法,在sleep方法里面继续调用sleep,实际上调用的是eat方法,这样就完成的方法调换

二. 给分类添加属性

涉及到的方法:
objc_getAssociatedObject objc_setAssociatedObject

让你快速上手Runtime 袁峥
原理:给一个类声明属性,其实本质就是给这个类添加关联,并不是直接把这个值的内存空间添加到类存空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
// 给系统NSObject类动态添加属性name
NSObject *objc = [[NSObject alloc] init];
objc.name = @"YoonAngel";
NSLog(@"%@",objc.name);
}
@end
// 定义关联的key
static const char *key = "name";
@implementation NSObject (Property)
- (NSString *)name
{
// 根据关联的key,获取关联的值。
return objc_getAssociatedObject(self, key);
}
- (void)setName:(NSString *)name
{
// 第一个参数:给哪个对象添加关联
// 第二个参数:关联的key,通过这个key获取
// 第三个参数:关联的value
// 第四个参数:关联的策略
objc_setAssociatedObject(self, key, name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end

iOS-Runtime-实践篇 Jerry4me

在分类只能对原类扩充方法, 并不能扩充属性, 你可以创建一个分类, 然后在分类中敲几个@property, 然后用第二节的方法打印下原类的property看看存不存在? 答案显然是不存在这个属性.

那么我们可以使用runtime中的一个叫关联对象的办法, 给分类添加一个property, 并且打印原类的property列表是真真切切存在的. 上代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Person+RunningMan.h
@interface Person (RunningMan)
/** 速度(km/h) */
@property (nonatomic, assign) CGFloat speed;
@end
// Person+RunningMan.m
#import <objc/objc-runtime.h>
@implementation Person (RunningMan)
- (CGFloat)speed
{
id value = objc_getAssociatedObject(self, _cmd);
return [value doubleValue];
}
- (void)setSpeed:(CGFloat)speed
{
objc_setAssociatedObject(self, @selector(speed), @(speed), OBJC_ASSOCIATION_ASSIGN);
}
@end

好的, 我们看看加了这个分类之后再利用第二节的办法打印下瞧瞧~

1
2
3
4
5
6
7
8
9
10
11
12
13
14
2016-09-04 17:26:00.403 Runtime-实践篇[13795:1050331] ivarName:_name, ivarValue:Kobe
2016-09-04 17:26:00.404 Runtime-实践篇[13795:1050331] ivarName:_age, ivarValue:18
2016-09-04 17:26:00.405 Runtime-实践篇[13795:1050331] ivarName:_weight, ivarValue:110
2016-09-04 17:26:00.405 Runtime-实践篇[13795:1050331] ivarName:_address, ivarValue:值为nil
2016-09-04 17:26:00.405 Runtime-实践篇[13795:1050331] propertyName:speed, propertyValue:0
2016-09-04 17:26:00.405 Runtime-实践篇[13795:1050331] propertyName:address, propertyValue:值为nil
2016-09-04 17:26:00.405 Runtime-实践篇[13795:1050331] propertyName:weight, propertyValue:110
2016-09-04 17:26:00.405 Runtime-实践篇[13795:1050331] methodName:speed, argumentsCount:0
2016-09-04 17:26:00.406 Runtime-实践篇[13795:1050331] methodName:setWeight:, argumentsCount:1
2016-09-04 17:26:00.406 Runtime-实践篇[13795:1050331] methodName:setSpeed:, argumentsCount:1
2016-09-04 17:26:00.406 Runtime-实践篇[13795:1050331] methodName:weight, argumentsCount:0
2016-09-04 17:26:00.446 Runtime-实践篇[13795:1050331] methodName:setAddress:, argumentsCount:1
2016-09-04 17:26:00.447 Runtime-实践篇[13795:1050331] methodName:address, argumentsCount:0
2016-09-04 17:26:00.447 Runtime-实践篇[13795:1050331] methodName:.cxx_destruct, argumentsCount:0

看到了嘛? speed这个属性乖乖的在那儿呢.

其实关联对象这个技术就是用哈希表实现的, 将一个类映射到一张哈希表上, 然后根据key找到关联对象, 所以严格说, 关联对象跟本类没有任何联系, 它不是储存在类的内部的. 它的底层原理就不多介绍了, 不属于本文的范畴, 大家感兴趣的可以到以下两篇文章里面看看

Associated Objects

Objective-C Associated Objects 的实现原理

Objective-C中的Runtime LeeJay
类别不可以添加属性,我们可以在类别中设置关联,举个栗子:
Person+Category.h 文件

1
2
3
4
#import "Person.h"
@interface Person (Category)
@property (nonatomic, copy) NSString *name;
@end

Person+Category.m 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#import "Person+Category.h"
#import <objc/runtime.h>
@implementation Person (Category)
static char *key;
- (void)setName:(NSString *)name
{
objc_setAssociatedObject(self,
key,
name,
OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)name
{
return objc_getAssociatedObject(self, key);
}
@end

当然你也可以这么写

Person+Category.m 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#import "Person+Category.h"
#import <objc/runtime.h>
@implementation Person (Category)
- (void)setName:(NSString *)name
{
objc_setAssociatedObject(self,
@selector(name),
name,
OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)name
{
return objc_getAssociatedObject(self, _cmd);
}
@end

objc_setAssociatedObjectobjc_getAssociatedObject传入的参数key:要求是唯一并且是常量,可以使用static char,然而一个更简单方便的方法就是:使用选择子。由于选择子是唯一并且是常量,你可以使用选择子作为关联的key。(PS:_cmd表示当前调用的方法,它就是一个方法选择器SEL,类似self表示当前对象)

Objecive-C runtime实践-给Category添加属性
在开发中经常遇到需要添加hud的情形,每次添加的代码都在10行左右,遂新建一个ViewController的Category来添加hud。由于分类不能直接添加属性,就考虑到了runtime。
下面讲具体的实施步骤
新建工程--新建文件,选择Objective-C File 如下图:

导入MBProgressHUD, 现在项目结构如图:

在UIViewController+HUD.h中导入MBProgressHUD.h,

1
2
3
4
5
#import <UIKit/UIKit.h>
#import "MBProgressHUD.h"
@interface UIViewController (HUD)
@end

在UIViewController+HUD.m中导入runtime.h,并添加hud属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#import "UIViewController+HUD.h"
#import <objc/runtime.h>
@interface UIViewController(Private)
@property (nonatomic, strong) MBProgressHUD *progressHud;
@end
@implementation UIViewController (HUD)
const char *kHudKey = "ProgressHUD_Key";
@end

接下来是关键的一步-设置hud的setter与getter方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (void)setProgressHud:(MBProgressHUD *)progressHud
{
objc_setAssociatedObject(self, kHudKey, progressHud, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (MBProgressHUD *)progressHud
{
MBProgressHUD *hud = objc_getAssociatedObject(self, kHudKey);
if (!hud) {
UIView *view = self.navigationController.view;
hud = [[MBProgressHUD alloc] initWithView:view];
hud.removeFromSuperViewOnHide = YES;
[view addSubview:hud];
self.progressHud = hud;
}
return hud;
}

在setter中设置连接, 在getter中初始化。
好了,接下来就可以正常使用属性了,现在,我们对hud进行扩展。
写两个基本的show、hide方法,其余的实现在其基础上变化即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#pragma mark - Private
- (void)showHudWithMessage:(NSString *)message model:(MBProgressHUDMode)mode
{
if (self.progressHud.taskInProgress) {
return;
}
self.progressHud.taskInProgress = YES;
self.progressHud.mode = mode ?: MBProgressHUDModeIndeterminate;
self.progressHud.labelText = message;
self.progressHud.labelFont = [UIFont systemFontOfSize:15];
[self.progressHud show:YES];
[self.progressHud hide:YES afterDelay:15];
}
- (void)hideHudWithMessage:(NSString *)message image:(UIImage *)image
{
self.progressHud.customView = [[UIImageView alloc] initWithImage:image];
self.progressHud.mode = MBProgressHUDModeCustomView;
self.progressHud.labelText = message;
self.progressHud.labelFont = [UIFont systemFontOfSize:15];
[self.progressHud show:YES];
[self performSelector:@selector(hideHud) withObject:nil afterDelay:0.7];
}

再实现下面的方法大概就够用了:

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)showHud;
- (void)showHudWithMessage:(NSString *)message;
- (void)showDeterminateHudWithMessage:(NSString *)message;
- (void)changeHudProgress:(double)precent;
- (void)hideHud;
- (void)hideHudWithSuccessMessage:(NSString *)message;
- (void)hideHudWithErrorMessage:(NSString *)message;

具体的实现直接看代码吧!
https://github.com/Xigtun/RuntimeDemo

防止按钮重复暴力点击

同上

按钮防止被重复点击的方法 (iOS)

按钮防止被重复点击的方法 (iOS)

避免一个button被多次点击(共总结了3种)
第一种:每次在点击时先取消之前的操作
将这段代码放在你按钮点击的方法中,例如:

1
2
3
4
5
- (void)buttonClicked:(id)sender{
//点击按钮后先取消之前的操作,再进行需要进行的操作
[[selfclass]cancelPreviousPerformRequestsWithTarget:selfselector:@selector(buttonClicked:)object:sender];
[selfperformSelector:@selector(buttonClicked: )withObject:senderafterDelay:0.2f];
}

第二种:点击后设为不可被点击的状态,几秒后恢复:

1
2
3
4
5
6
7
-(void)buttonClicked:(id)sender{
self.button.enabled =NO;
[selfperformSelector:@selector(changeButtonStatus)withObject:nilafterDelay:1.0f];//防止重复点击
}
-(void)changeButtonStatus{
self.button.enabled =YES;
}

第三种:使用runtime,一劳永逸我这设的是0.5秒内不会被重复点击
1.导入objc / runtime.h(可以放在PCH文件里)

2.创建uicontrol或UIButton的的分类!

创建分类文件:

2.1 打开Xcode中,新建文件,选择OC文件

2.2 在第二个界面,File名为UIControl+UIControl_buttonCon,将文件类型File Type选为Category类,在类里选继承的类别,这里咱们选的Class是UIButton

注:若用Unbutton分类,则会对对Unbutton创建的按钮反应。

2.3 分类创建完毕对分类进行操作
.h文件

1
2
3
4
5
6
#import
#define defaultInterval.5//默认时间间隔
@interfaceUIControl (UIControl_buttonCon)
@property(nonatomic,assign)NSTimeIntervaltimeInterval;//用这个给重复点击加间隔
@property(nonatomic,assign)BOOLisIgnoreEvent;//YES不允许点击NO允许点击
@end

.m文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#import"UIControl+UIControl_buttonCon.h"
@implementationUIControl (UIControl_buttonCon)
- (NSTimeInterval)timeInterval{
return[objc_getAssociatedObject(self,_cmd)doubleValue];
}
- (void)setTimeInterval:(NSTimeInterval)timeInterval{
objc_setAssociatedObject(self,@selector(timeInterval),@(timeInterval),OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
//runtime动态绑定属性
- (void)setIsIgnoreEvent:(BOOL)isIgnoreEvent{
objc_setAssociatedObject(self,@selector(isIgnoreEvent),@(isIgnoreEvent),OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (BOOL)isIgnoreEvent{
return[objc_getAssociatedObject(self,_cmd)boolValue];
}
- (void)resetState{
[selfsetIsIgnoreEvent:NO];
}
+ (void)load{
staticdispatch_once_tonceToken;
dispatch_once(&onceToken, ^{
SELselA =@selector(sendAction:to:forEvent:);
SELselB =@selector(mySendAction:to:forEvent:);
MethodmethodA =class_getInstanceMethod(self, selA);
MethodmethodB =class_getInstanceMethod(self, selB);
//将methodB的实现添加到系统方法中也就是说将methodA方法指针添加成方法methodB的返回值表示是否添加成功
BOOLisAdd =class_addMethod(self, selA,method_getImplementation(methodB),method_getTypeEncoding(methodB));
//添加成功了说明本类中不存在methodB所以此时必须将方法b的实现指针换成方法A的,否则b方法将没有实现。
if(isAdd) {
class_replaceMethod(self, selB,method_getImplementation(methodA),method_getTypeEncoding(methodA));
}else{
//添加失败了说明本类中有methodB的实现,此时只需要将methodA和methodB的IMP互换一下即可。
method_exchangeImplementations(methodA, methodB);
}
});
}
- (void)mySendAction:(SEL)action to:(id)target forEvent:(UIEvent*)event{
if([NSStringFromClass(self.class)isEqualToString:@"UIButton"]) {
self.timeInterval=self.timeInterval==0?defaultInterval:self.timeInterval;
if(self.isIgnoreEvent){
return;
}elseif(self.timeInterval>0){
[selfperformSelector:@selector(resetState)withObject:nilafterDelay:self.timeInterval];
}
}
//此处methodA和methodB方法IMP互换了,实际上执行sendAction;所以不会死循环
self.isIgnoreEvent=YES;
[selfmySendAction:actionto:targetforEvent:event];
}
@end

三. 字典转模型

涉及到的方法:
class_copyIvarList ivar_getName ivar_getTypeEncoding

让你快速上手Runtime 袁峥

模型属性,通常需要跟字典中的key一一对应,提供一个分类,专门根据字典生成对应的属性字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@implementation NSObject (Log)
// 自动打印属性字符串
+ (void)resolveDict:(NSDictionary *)dict{
// 拼接属性字符串代码
NSMutableString *strM = [NSMutableString string];
// 1.遍历字典,把字典中的所有key取出来,生成对应的属性代码
[dict enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
// 类型经常变,抽出来
NSString *type;
if ([obj isKindOfClass:NSClassFromString(@"__NSCFString")]) {
type = @"NSString";
}else if ([obj isKindOfClass:NSClassFromString(@"__NSCFArray")]){
type = @"NSArray";
}else if ([obj isKindOfClass:NSClassFromString(@"__NSCFNumber")]){
type = @"int";
}else if ([obj isKindOfClass:NSClassFromString(@"__NSCFDictionary")]){
type = @"NSDictionary";
}
// 属性字符串
NSString *str;
if ([type containsString:@"NS"]) {
str = [NSString stringWithFormat:@"@property (nonatomic, strong) %@ *%@;",type,key];
}else{
str = [NSString stringWithFormat:@"@property (nonatomic, assign) %@ %@;",type,key];
}
// 每生成属性字符串,就自动换行。
[strM appendFormat:@"\n%@\n",str];
}];
// 把拼接好的字符串打印出来,就好了。
NSLog(@"%@",strM);
}
@end

  • 字典转模型的方式一:KVC

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    - @implementation Status
    + (instancetype)statusWithDict:(NSDictionary *)dict
    {
    Status *status = [[self alloc] init];
    [status setValuesForKeysWithDictionary:dict];
    return status;
    }
    @end
  • KVC字典转模型弊端:必须保证,模型中的属性和字典中的key一一对应。

  • 如果不一致,就会调用[<Status 0x7fa74b545d60> setValue:forUndefinedKey:]
    报key找不到的错。
  • 分析:模型中的属性和字典的key不一一对应,系统就会调用setValue:forUndefinedKey:报错。
  • 解决:重写对象的setValue:forUndefinedKey:,把系统的方法覆盖,
    就能继续使用KVC,字典转模型了。

    1
    2
    3
    4
    - (void)setValue:(id)value forUndefinedKey:(NSString *)key
    {
    }
  • 字典转模型的方式二:Runtime

  • 思路:利用运行时,遍历模型中所有属性,根据模型的属性名,去字典中查找key,取出对应的值,给模型的属性赋值。

  • 步骤:提供一个NSObject分类,专门字典转模型,以后所有模型都可以通过这个分类转。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
// 解析Plist文件
NSString *filePath = [[NSBundle mainBundle] pathForResource:@"status.plist" ofType:nil];
NSDictionary *statusDict = [NSDictionary dictionaryWithContentsOfFile:filePath];
// 获取字典数组
NSArray *dictArr = statusDict[@"statuses"];
// 自动生成模型的属性字符串
// [NSObject resolveDict:dictArr[0][@"user"]];
_statuses = [NSMutableArray array];
// 遍历字典数组
for (NSDictionary *dict in dictArr) {
Status *status = [Status modelWithDict:dict];
[_statuses addObject:status];
}
// 测试数据
NSLog(@"%@ %@",_statuses,[_statuses[0] user]);
}
@end
@implementation NSObject (Model)
+ (instancetype)modelWithDict:(NSDictionary *)dict
{
// 思路:遍历模型中所有属性-》使用运行时
// 0.创建对应的对象
id objc = [[self alloc] init];
// 1.利用runtime给对象中的成员属性赋值
// class_copyIvarList:获取类中的所有成员属性
// Ivar:成员属性的意思
// 第一个参数:表示获取哪个类中的成员属性
// 第二个参数:表示这个类有多少成员属性,传入一个Int变量地址,会自动给这个变量赋值
// 返回值Ivar *:指的是一个ivar数组,会把所有成员属性放在一个数组中,通过返回的数组就能全部获取到。
/* 类似下面这种写法
Ivar ivar;
Ivar ivar1;
Ivar ivar2;
// 定义一个ivar的数组a
Ivar a[] = {ivar,ivar1,ivar2};
// 用一个Ivar *指针指向数组第一个元素
Ivar *ivarList = a;
// 根据指针访问数组第一个元素
ivarList[0];
*/
unsigned int count;
// 获取类中的所有成员属性
Ivar *ivarList = class_copyIvarList(self, &count);
for (int i = 0; i < count; i++) {
// 根据角标,从数组取出对应的成员属性
Ivar ivar = ivarList[i];
// 获取成员属性名
NSString *name = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 处理成员属性名->字典中的key
// 从第一个角标开始截取
NSString *key = [name substringFromIndex:1];
// 根据成员属性名去字典中查找对应的value
id value = dict[key];
// 二级转换:如果字典中还有字典,也需要把对应的字典转换成模型
// 判断下value是否是字典
if ([value isKindOfClass:[NSDictionary class]]) {
// 字典转模型
// 获取模型的类对象,调用modelWithDict
// 模型的类名已知,就是成员属性的类型
// 获取成员属性类型
NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
// 生成的是这种@"@\"User\"" 类型 -》 @"User" 在OC字符串中 \" -> ",\是转义的意思,不占用字符
// 裁剪类型字符串
NSRange range = [type rangeOfString:@"\""];
type = [type substringFromIndex:range.location + range.length];
range = [type rangeOfString:@"\""];
// 裁剪到哪个角标,不包括当前角标
type = [type substringToIndex:range.location];
// 根据字符串类名生成类对象
Class modelClass = NSClassFromString(type);
if (modelClass) { // 有对应的模型才需要转
// 把字典转模型
value = [modelClass modelWithDict:value];
}
}
// 三级转换:NSArray中也是字典,把数组中的字典转换成模型.
// 判断值是否是数组
if ([value isKindOfClass:[NSArray class]]) {
// 判断对应类有没有实现字典数组转模型数组的协议
if ([self respondsToSelector:@selector(arrayContainModelClass)]) {
// 转换成id类型,就能调用任何对象的方法
id idSelf = self;
// 获取数组中字典对应的模型
NSString *type = [idSelf arrayContainModelClass][key];
// 生成模型
Class classModel = NSClassFromString(type);
NSMutableArray *arrM = [NSMutableArray array];
// 遍历字典数组,生成模型数组
for (NSDictionary *dict in value) {
// 字典转模型
id model = [classModel modelWithDict:dict];
[arrM addObject:model];
}
// 把模型数组赋值给value
value = arrM;
}
}
if (value) { // 有值,才需要给模型的属性赋值
// 利用KVC给模型中的属性赋值
[objc setValue:value forKey:key];
}
}
return objc;
}
@end

Objective-C中的Runtime LeeJay

字典转模型
利用Runtime,遍历模型中所有成员变量,根据模型的属性名,去字典中查找key,取出对应的value,给模型的属性赋值,实现的思路主要借鉴MJExtension。

NSObject+Property.h文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#import <Foundation/Foundation.h>
@protocol KeyValue <NSObject>
@optional
/**
* 数组中需要转换的模型类
*
* @return 字典中的key是数组属性名,value是数组中存放模型的Class(Class类型或者NSString类型)
*/
+ (NSDictionary *)objectClassInArray;
/**
* 将属性名换为其他key去字典中取值
*
* @return 字典中的key是属性名,value是从字典中取值用的key
*/
+ (NSDictionary *)replacedKeyFromPropertyName;
@end
@interface NSObject (Property) <KeyValue>
+ (instancetype)objectWithDictionary:(NSDictionary *)dictionary;
@end

NSObject+Property.m文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
#import "NSObject+Property.h"
#import <objc/runtime.h>
@implementation NSObject (Property)
+ (instancetype)objectWithDictionary:(NSDictionary *)dictionary
{
id obj = [[self alloc] init];
// 获取所有的成员变量
unsigned int count;
Ivar *ivars = class_copyIvarList(self, &count);
for (unsigned int i = 0; i < count; i++)
{
Ivar ivar = ivars[i];
// 取出的成员变量,去掉下划线
NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
NSString *key = [ivarName substringFromIndex:1];
id value = dictionary[key];
// 当这个值为空时,判断一下是否执行了replacedKeyFromPropertyName协议,如果执行了替换原来的key查值
if (!value)
{
if ([self respondsToSelector:@selector(replacedKeyFromPropertyName)])
{
NSString *replaceKey = [self replacedKeyFromPropertyName][key];
value = dictionary[replaceKey];
}
}
// 字典嵌套字典
if ([value isKindOfClass:[NSDictionary class]])
{
NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
NSRange range = [type rangeOfString:@"\""];
type = [type substringFromIndex:range.location + range.length];
range = [type rangeOfString:@"\""];
type = [type substringToIndex:range.location];
Class modelClass = NSClassFromString(type);
if (modelClass)
{
value = [modelClass objectWithDictionary:value];
}
}
// 字典嵌套数组
if ([value isKindOfClass:[NSArray class]])
{
if ([self respondsToSelector:@selector(objectClassInArray)])
{
NSMutableArray *models = [NSMutableArray array];
NSString *type = [self objectClassInArray][key];
Class classModel = NSClassFromString(type);
for (NSDictionary *dict in value)
{
id model = [classModel objectWithDictionary:dict];
[models addObject:model];
}
value = models;
}
}
if (value)
{
[obj setValue:value forKey:key];
}
}
// 释放ivars
free(ivars);
return obj;
}
@end

参考

Associated Objects

Objective-C Runtime

Objective-C Runtime 1小时入门教程

Objective-C的hook方案(一): Method Swizzling

iOS开发进阶

Effective Objective-C 2.0
附上本文的所有demo下载链接,【GitHub】
作者:LeeJay
链接:http://www.jianshu.com/p/3e050ec3b759

根据字典自动生成属性代码

http://www.jianshu.com/p/2ad0ebfd5a63

日常开发中,我们拿到接口文档,会根据接口返回的数据来写模型。
在之前我都是根据返回的字典一个个key这样对照着来创建模型属性。后面遇到项目中有时候返回的数据里面要写成模型属性的key实在是太多,写起来花时间容易写错又没什么技术含量。
这时候就应该动用开发人员该有的程序思想了,干脆让它自动生成不就好了。废话不多说上代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (void)createPropertyCode
{
NSMutableString *codes = [NSMutableString string];
// 遍历字典
[self enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull value, BOOL * _Nonnull stop) {
NSString *code;
if ([value isKindOfClass:[NSString class]]) {
code = [NSString stringWithFormat:@"@property (nonatomic, strong) NSString *%@;",key];
} else if ([value isKindOfClass:NSClassFromString(@"__NSCFBoolean")]) {
code = [NSString stringWithFormat:@"@property (nonatomic, assign) BOOL %@;",key];
} else if ([value isKindOfClass:[NSNumber class]]) {
code = [NSString stringWithFormat:@"@property (nonatomic, assign) NSInteger %@;",key];
} else if ([value isKindOfClass:[NSArray class]]) {
code = [NSString stringWithFormat:@"@property (nonatomic, strong) NSArray *%@;",key];
} else if ([value isKindOfClass:[NSDictionary class]]) {
code = [NSString stringWithFormat:@"@property (nonatomic, strong) NSDictionary *%@;",key];
}
// @property (nonatomic, strong) NSString *source;
[codes appendFormat:@"\n%@\n",code];
}];
NSLog(@"%@",codes);
}

这个方法利用NSDictionary类里面的这个方法帮我们遍历字典里面所有的key和value,期间要做的事情写到block中,也就是帮我们自动生成属性代码。

1
- (void)enumerateKeysAndObjectsUsingBlock:(void (^ _Nonnull)(KeyType _Nonnull key, ObjectType _Nonnull obj, BOOL * _Nonnull stop))block

至于怎么用,可以写成NSDictionary的一个分类。然后用字典对象直接调用就好。像这样[dict createPropertyCode]。就可以帮我们打印出属性代码,然后复制粘贴就好。在面对属性超多的模型时,是不是方便许多了。
当然你也可以根据需要做一些调整,这里也只是提供一个开发中的小技巧,让机器帮我们做事往往

Runtime基本知识点以及应用场景

8.使用Runtime和KVC字典转模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
+ (instancetype)configModelWithDict:(NSDictionary *)jsonDict replaceDict:(NSDictionary *)replaceDict
{
id obj = [[self alloc] init];
unsigned int count = 0;
// 获取变量列表
Ivar *ivarLists = class_copyIvarList(self, &count);
// 遍历逐个进行使用
for (NSInteger i = 0; i < count; i ++)
{
// 获取变量对象
Ivar ivar = ivarLists[i];
const char *name = ivar_getName(ivar);
const char *coding = ivar_getTypeEncoding(ivar); // 判断类型
// 获取自己写的属性变量字符串 _name
NSString *nameStr = [[NSString stringWithUTF8String:name] substringFromIndex:1];
NSString *codingstr = [NSString stringWithUTF8String:coding];
// 根据字符串在原生字典取值
id value = jsonDict[nameStr];
// 如果未取到值 说明字段已经修改了
if (!value) {
if (replaceDict) {
// 然后把修改之前的原生字段拿出来进行取值
NSString *originValue = replaceDict[nameStr];
// 再赋值
value = jsonDict[originValue];
}
}
// 避免属性数量大于数据数量的时候,如果多出来的属性是对象类型的那正好是null,无影响,如果多出来的属性是普通类型的,那会把nil赋值过去,直接崩溃
if ([codingstr isEqualToString:@"f"] || [codingstr isEqualToString:@"d"]) {
value = @(0.0);
}
// kvc进行模型组装 这里的value类型和property里面给的属性效果是一致的,如果属性是BOOL,你强行给字符串,实际类型还是BOOL
[obj setValue:value forKey:nameStr];
}
return obj;
}

上面就是转换的核心代码,分析下主要功能参数
1.通过class_copyIvarList拿到属性列表的数组,ivargetName这方法拿到属性C类型字符去掉,转换成OC

2.这里会有个问题,如果自己建的model字段和Json返回的字段完全一致,那么就问题不大,但是由于可读性的关系,我们一般都会做一次映射,这就是replaceDict存在的意义,用例如下:
当你的属性名字是SubName,但是Json返回的字典key是sub_name,显然是不同的,需要映射,我们根据runtime拿到的key也是SubName,那么你根据字典取值,就会出现空值的问题,因此
replaceDict就用到了@{@”SubName”:@”sub_name”},只要映射好传进去,我们里面就能进一步做判断了

3.直接看代码注释

1
2
3
4
5
6
7
8
9
/*
* 1.首先属性小于数据源的时候是肯定没问题的
* 2.当属性大于数据源的时候,属性是对象,打印出来就是nil,但是如果属性是基本数据类型,直接崩溃
* 一定要这么判断是否是基本数据类型
* 避免属性数量大于数据数量的时候,如果多出来的属性是对象类型的那正好是null,无影响,如果多出来的属性是普通类型的,那会把nil赋值过去,直接崩溃
* if ([codingstr isEqualToString:@"f"] || [codingstr isEqualToString:@"d"]) {
* value = @(0.0);
}
*/

这里的F,D什么类型可以参考官方文档类型type类型文档

理解了字段和原理,调用代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 1.URL
NSString *githubAPI = @"https://api.github.com/users/Tuccuay";
// 2.创建请求对象
// 对象内部已经包含了请求方法和请求头(GET)
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:githubAPI]];
// 3.创建session单例
NSURLSession *session = [NSURLSession sharedSession];
// 4.根据会话对象发送请求
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSDictionary *jsonDict = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:nil];
MKJModel *mkj = [MKJModel configModelWithDict:jsonDict replaceDict:@{@"mkjLogin":@"login",
@"mkjID":@"id",
@"mkjAvatar_url":@"avatar_url",
@"mkjGravatar_id":@"gravatar_id",
@"mkjUrl":@"url",
@"mkjHtml_url":@"html_url",
@"mkjFollowers_url":@"followers_url",
@"mkjFollowing_url":@"following_url",
@"mkjGists_url":@"gists_url",
@"mkjStarred_url":@"starred_url",
}];
NSLog(@"%@",mkj);
}];
// 5.task resume
[dataTask resume];

四. 动态添加一个类

涉及方法 objc_allocateClassPair class_addIvar objc_registerClassPair class_getInstanceVariable objc_disposeClassPair class_addMethod performSelector

iOS-Runtime-实践篇 Jerry4me

所有runtime代码都是基于C的函数, 所以要用到runtime的函数必须导入

1
#import <objc/objc-runtime.h> // 模拟器

或者

1
2
#import <objc/runtime.h> // 真机
#import <objc/message.h> // 真机

就像KVO一样, 系统是在程序运行的时候根据你要监听的类, 动态添加一个新类继承自该类, 然后重写原类的setter方法并在里面通知observer的.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 创建一个类(size_t extraBytes该参数通常指定为0, 该参数是分配给类和元类对象尾部的索引ivars的字节数。)
Class clazz = objc_allocateClassPair([NSObject class], "GoodPerson", 0);
// 添加ivar
// @encode(aType) : 返回该类型的内部表示字符串, 如@encode(int) -> i
class_addIvar(clazz, "_name", sizeof(NSString *), log2(sizeof(NSString *)), @encode(NSString *));
class_addIvar(clazz, "_age", sizeof(NSUInteger), log2(sizeof(NSUInteger)), @encode(NSUInteger));
// 注册该类
objc_registerClassPair(clazz);
// 创建实例对象
id object = [[clazz alloc] init];
// 设置ivar
[object setValue:@"Tracy" forKey:@"name"];
Ivar ageIvar = class_getInstanceVariable(clazz, "_age");
object_setIvar(object, ageIvar, @18);
// 打印对象的类和内存地址
NSLog(@"%@", object);
// 打印对象的属性值
NSLog(@"name = %@, age = %@", [object valueForKey:@"name"], object_getIvar(object, ageIvar));
// 当类或者它的子类的实例还存在,则不能调用objc_disposeClassPair方法
object = nil;
// 销毁类
objc_disposeClassPair(clazz);

运行结果为

1
2
2016-09-04 17:04:08.328 Runtime-实践篇[13699:1043458] <GoodPerson: 0x1002039b0>
2016-09-04 17:04:08.329 Runtime-实践篇[13699:1043458] name = Tracy, age = 18

这样, 我们就在程序运行时动态添加了一个继承自NSObject的GoodPerson类, 并为该类添加了name和age成员变量. 这里我们需要注意的是, 添加成员变量的class_addIvar方法必须要在objc_allocateClassPairobjc_registerClassPair之间调用才行, 这里涉及到OC中类的成员变量的偏移量, 如果在类注册之后再addIvar的话会破坏原来类成员变量正确的偏移量, 这样的话会导致你访问的那个成员变量并不是你想访问的成员变量, 如图 :

在类中新增另一个实例变量前后的数据布局图

大家可以试试把class_addIvar方法放在objc_registerClassPair方法之后执行, 看看会发生什么? (用KVC赋值和取值直接报错, 用getIvar的话取值为null)

Objective-C中的Runtime LeeJay

一、动态的创建一个类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 创建一个名为People的类,它是NSObject的子类
Class People = objc_allocateClassPair([NSObject class], "People", 0);
// 为该类添加一个eat的方法
class_addMethod(People, NSSelectorFromString(@"eat"), (IMP) eatFun, "v@:");
// 注册该类
objc_registerClassPair(People);
// 创建一个People的实例对象p
id p = [[People alloc] init];
// 调用eat方法
[p performSelector:@selector(eat)];

五. 打印一个类的所有ivar, property 和 method

iOS-Runtime-实践篇

这个还是比较简单的, 应该直接看代码都能看懂

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
Person *p = [[Person alloc] init];
[p setValue:@"Kobe" forKey:@"name"];
[p setValue:@18 forKey:@"age"];
// p.address = @"广州大学城";
p.weight = 110.0f;
// 1.打印所有ivars
unsigned int ivarCount = 0;
// 用一个字典装ivarName和value
NSMutableDictionary *ivarDict = [NSMutableDictionary dictionary];
Ivar *ivarList = class_copyIvarList([p class], &ivarCount);
for(int i = 0; i < ivarCount; i++){
NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivarList[i])];
id value = [p valueForKey:ivarName];
if (value) {
ivarDict[ivarName] = value;
} else {
ivarDict[ivarName] = @"值为nil";
}
}
// 打印ivar
for (NSString *ivarName in ivarDict.allKeys) {
NSLog(@"ivarName:%@, ivarValue:%@",ivarName, ivarDict[ivarName]);
}
// 2.打印所有properties
unsigned int propertyCount = 0;
// 用一个字典装propertyName和value
NSMutableDictionary *propertyDict = [NSMutableDictionary dictionary];
objc_property_t *propertyList = class_copyPropertyList([p class], &propertyCount);
for(int j = 0; j < propertyCount; j++){
NSString *propertyName = [NSString stringWithUTF8String:property_getName(propertyList[j])];
id value = [p valueForKey:propertyName];
if (value) {
propertyDict[propertyName] = value;
} else {
propertyDict[propertyName] = @"值为nil";
}
}
// 打印property
for (NSString *propertyName in propertyDict.allKeys) {
NSLog(@"propertyName:%@, propertyValue:%@",propertyName, propertyDict[propertyName]);
}
// 3.打印所有methods
unsigned int methodCount = 0;
// 用一个字典装methodName和arguments
NSMutableDictionary *methodDict = [NSMutableDictionary dictionary];
Method *methodList = class_copyMethodList([p class], &methodCount);
for(int k = 0; k < methodCount; k++){
SEL methodSel = method_getName(methodList[k]);
NSString *methodName = [NSString stringWithUTF8String:sel_getName(methodSel)];
unsigned int argumentNums = method_getNumberOfArguments(methodList[k]);
methodDict[methodName] = @(argumentNums - 2); // -2的原因是每个方法内部都有self 和 selector 两个参数
}
// 打印method
for (NSString *methodName in methodDict.allKeys) {
NSLog(@"methodName:%@, argumentsCount:%@", methodName, methodDict[methodName]);
}

打印结果为 :

1
2
3
4
5
6
7
8
9
10
11
2016-09-04 17:06:49.070 Runtime-实践篇[13723:1044813] ivarName:_name, ivarValue:Kobe
2016-09-04 17:06:49.071 Runtime-实践篇[13723:1044813] ivarName:_age, ivarValue:18
2016-09-04 17:06:49.071 Runtime-实践篇[13723:1044813] ivarName:_weight, ivarValue:110
2016-09-04 17:06:49.072 Runtime-实践篇[13723:1044813] ivarName:_address, ivarValue:值为nil
2016-09-04 17:06:49.072 Runtime-实践篇[13723:1044813] propertyName:address, propertyValue:值为nil
2016-09-04 17:06:49.072 Runtime-实践篇[13723:1044813] propertyName:weight, propertyValue:110
2016-09-04 17:06:49.073 Runtime-实践篇[13723:1044813] methodName:setWeight:, argumentsCount:1
2016-09-04 17:06:49.073 Runtime-实践篇[13723:1044813] methodName:weight, argumentsCount:0
2016-09-04 17:06:49.074 Runtime-实践篇[13723:1044813] methodName:setAddress:, argumentsCount:1
2016-09-04 17:06:49.074 Runtime-实践篇[13723:1044813] methodName:address, argumentsCount:0
2016-09-04 17:06:49.074 Runtime-实践篇[13723:1044813] methodName:.cxx_destruct, argumentsCount:0

前面2节主要是熟悉runtime的函数调用, 毕竟有许多函数前缀objc, class, object等等. 其实这里面也有规律 :

  • objc_: 高于类的操作, 例如添加类, 注册类, 销毁类还有许多高于一个类本身的操作一般都是objc开头
  • class : 对类的内部进行修改的, 例如添加ivar, 添加property, 添加method等等
  • object : 对某个对象进行修改, 例如设置ivar值, 获取ivar值, 设置property值, 获取property值, 调用某个method等等
  • ivar, property, method : 这三个方法大家可以手动去敲敲看一看

六. 动态添加方法

涉及方法:
performSelector resolveInstanceMethod class_addMethod

让你快速上手Runtime 袁峥

  • 开发使用场景:如果一个类方法非常多,加载类到内存的时候也比较耗费资源,需要给每个方法生成映射表,可以使用动态给某个类,添加方法解决。
  • 经典面试题:有没有使用performSelector,其实主要想问你有没有动态添加过方法。
  • 简单使用
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    @implementation ViewController
    - (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    Person *p = [[Person alloc] init];
    // 默认person,没有实现eat方法,可以通过performSelector调用,但是会报错。
    // 动态添加方法就不会报错
    [p performSelector:@selector(eat)];
    }
    @end
    @implementation Person
    // void(*)()
    // 默认方法都有两个隐式参数,
    void eat(id self,SEL sel)
    {
    NSLog(@"%@ %@",self,NSStringFromSelector(sel));
    }
    // 当一个对象调用未实现的方法,会调用这个方法处理,并且会把对应的方法列表传过来.
    // 刚好可以用来判断,未实现的方法是不是我们想要动态添加的方法
    + (BOOL)resolveInstanceMethod:(SEL)sel
    {
    if (sel == @selector(eat)) {
    // 动态添加eat方法
    // 第一个参数:给哪个类添加方法
    // 第二个参数:添加方法的方法编号
    // 第三个参数:添加方法的函数实现(函数地址)
    // 第四个参数:函数的类型,(返回值+参数类型) v:void @:对象->self :表示SEL->_cmd
    class_addMethod(self, @selector(eat), eat, "v@:");
    }
    return [super resolveInstanceMethod:sel];
    }
    @end

动态添加方法实现

iOS-Runtime-实践篇

好了, 绕来绕去又回到了runtime强大的消息转发身上了, 当一个方法没有实现的时候, OC会怎么做的呢? 还记得那四个步骤吗, 不记得也没关系, 我们看代码!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/*
Person类只有- (void)noIMPMethod方法的声明,
没有他的实现, 一般来说程序运行, 调用noIMPMethod这个方法, 肯定要报错的,
我们可以在这个方法里动态添加该方法的实现
*/
// 用来实现noIMPMethod方法实现的函数
void otherFunction(id self, SEL cmd)
{
NSLog(@"动态处理了noIMPMethod方法的实现");
}
// 第一步, 对象在收到无法解读的消息后, 首先调用其所属类的这个类方法
// 返回YES则结束消息转发, 返回NO则进入下一步
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
// 如果是noIMPMethod方法
if([NSStringFromSelector(sel) isEqualToString:@"noIMPMethod"]){
// 动态添加方法实现
class_addMethod([self class], sel, (IMP)otherFunction, "v@:");
return YES;
} else {
return [super resolveInstanceMethod:sel];
}
}

程序运行结果 :

1
2
2016-09-04 17:38:24.301 Runtime-实践篇[13856:1054351] 动态处理了noIMPMethod方法的实现
代码应该也很明了, 当判断到无法解读的SEL后, 可以给该SEL动态添加方法的实现.

ps : 消息转发的另外3个方法会在下文放上, 因为本例子用不上所以就不放上来了

Objective-C中的Runtime LeeJay

二、动态的给某个类添加方法

1
2
3
4
5
6
7
8
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
if ([NSStringFromSelector(sel) isEqualToString:@"doSomething"])
{
class_addMethod(self, sel, (IMP) doSomething, "v@:@");
}
return YES;
}

动态的给某个类添加方法,class_addMethod的参数:
self:给哪个类添加方法
sel:添加方法的方法编号(选择子)
IMP:添加方法的函数实现(函数地址)
types 函数的类型,(返回值+参数类型) v:void @:对象->self :表示SEL->_cmd

Runtime基本知识点以及应用场景

动态添加方法

通常做法都是在resolve方法内部指定sel的IMP,前提是该方法未实现会被拦截下来,就能实现动态创建的过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
void run (id self, SEL _cmd,NSNumber *meter,NSString *name)
{
// implementation .....
NSLog(@"%@跑了%@",name,meter);
}
//对实现(abc)的前两个参数的说明
//每个方法的内部都默认包含两个参数,被称为隐式参数
//id类型self(代表类或对象)和SEL类型的_cmd(方法编号)
//class_addMethod函数参数的含义:
//第一个参数Class cls, 类型
//第二个参数SEL name, 被解析的方法
//第三个参数 IMP imp, 指定的实现
//第四个参数const char *types,方法的类型,具体参照类型的codeType那张图,但是要注意一点:Since the function must take at least two arguments—self and _cmd, the second and third characters must be “@:” (the first character is the return type).译为:因为函数必须至少有两个参数self和_cmd,第二个和第三个字符必须是“@:”。如果想要再增加参数,就可以从实现的第三个参数算起,看下面的例子就明白。
// 当调用有未实现的实例方法的时候会进到这里来
+ (BOOL) resolveInstanceMethod:(SEL)aSEL
{
// 多参数就是"run:"无参数就是run
if (aSEL == @selector(run:))
{
class_addMethod([self class], aSEL, (IMP) run, "v@:@:@");// 增加了2个对象类型参数 增加了@
return YES;
}
// return [super resolveInstanceMethod:aSEL];
return YES;
}
// 当调用类方法未实现的时候+ (BOOL)resolveClassMethod:(SEL)sel 在这里拦截
+ (BOOL)resolveClassMethod:(SEL)sel
{
NSLog(@"类方法未实现");
return NO;
}

七. 消息转发

动态消息转发
和上面一样我们创建的对象调用未实现的方法时,类和实例变量的内部是可以这样进行转发的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#pragma mark - 对象方法
// 没有实现firstMethod的方法
// 1. 在没有找到方法时,会先调用此方法,和DynicmicInsatance的方法一样就可以动态添加方法3
// 返回YES 表示响应的Selector的实现已经被找到并添加到子类中了
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
// if (sel == @selector(touch)) {
// class_addMethod([self class], sel, (IMP)touch, "v@:");
// }
return YES;
}
// 2.第二步
// 第一步里面返回之后没有添加新方法,该方法就会被调用,在这个方法中,我们可以指定一个可以返回一个响应方法的独享
// 不能返回Self 死循环
- (id)forwardingTargetForSelector:(SEL)aSelector
{
return nil;
}
// 3.第三步
// 如果上面返回了nil则该方法会被调用,给系统一个需要的编码
// 如果这里放回的是nil,那是无法执行下一波的,下次无法得到处理
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
// 4.第四步
// 调用转发消息方法
- (void)second
{
NSLog(@"对象方法first方法未被调用,消息转发成了second方法");
}
// 该方法不进行重写就直接进入第五步
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
[anInvocation setSelector:@selector(second)];
[anInvocation invokeWithTarget:self];
}
// 5.第五步
// 如果没有调用第四步的转发,那么会进入异常
- (void)doesNotRecognizeSelector:(SEL)aSelector
{
NSLog(@"无法处理的消息:%@",NSStringFromSelector(aSelector));
}

  • 先在这里拦截resolveInstanceMethod
  • 第一步未动态添加的话就调用forwardingTargetForSelector
  • 第二步返回nil来到这里调用methodSignatureForSelector签名
  • 重定向消息指针,实现消息转发
  • 如果没有实现第四步就doesNotRecognizeSelector异常

八. 更换方法调用者

iOS-Runtime-实践篇 Jerry4me

试想一下, 一个腿部残疾的人, 他想跑, runtime知道他自己跑不了, 于是就让他的狗替代他去跑了(person没有run方法的声明和实现, dog有run方法的声明和实现)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 第一步, 对象在收到无法解读的消息后, 首先调用其所属类的这个类方法
// 返回YES则结束消息转发, 返回NO则进入下一步
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
return NO;
}
// 第二步, 动态方法解析失败, 则调用这个方法
// 返回的对象将处理该selector, 返回nil则进入下一步
- (id)forwardingTargetForSelector:(SEL)aSelector
{
return nil;
}
// 第三步, 在这里返回方法的消息签名
// 返回YES则进入下一步, 返回nil则结束消息转发
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
if ([NSStringFromSelector(aSelector) isEqualToString:@"run"]){
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
} else {
return [super methodSignatureForSelector:aSelector];
}
}
// 第四步, 最后一次处理该消息的机会
// 这里处理不了这个invocation就会结束消息转发
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
// 在这我们修改调用该方法的对象
Dog *dog = [[Dog alloc] init];
// 让dog去调用该方法
[anInvocation invokeWithTarget:dog];
}

那么我们通过((void(*)(id, SEL))objc_msgSend)((id)p, @selector(run)); // 这里强转是为了不让编译器报参数过多的错误方法调用person的run方法, 得到的输出为 :

1
2016-09-04 17:49:42.634 Runtime-实践篇[13939:1059419] 是狗在跑步

同样, 其实可以在第二部就把这件事做了, 只需返回dog实例即可, 大家可以亲手操作试试

九. 更改特定方法的实现

iOS-Runtime-实践篇 Jerry4me

一条狗在吃着骨头, 然后他的主人一把把一个球扔得远远的, 碍于主人的淫威之下, 狗就不得不停下来跑去捡球了(更改[dog eat]方法的实现为[dog run])

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 第一步, 对象在收到无法解读的消息后, 首先调用其所属类的这个类方法
// 返回YES则结束消息转发, 返回NO则进入下一步
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
return NO;
}
// 第二步, 动态方法解析失败, 则调用这个方法
// 返回的对象将处理该selector, 返回nil则进入下一步
- (id)forwardingTargetForSelector:(SEL)aSelector
{
return nil;
}
// 第三步, 在这里返回方法的消息签名
// 返回YES则进入下一步, 返回nil则结束消息转发
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
if ([NSStringFromSelector(aSelector) isEqualToString:@"eat"]){
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
} else {
return [super methodSignatureForSelector:aSelector];
}
}
// 第四步, 最后一次处理该消息的机会
// 这里处理不了这个invocation就会结束消息转发
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
// 在这我们修改选择子为run
[anInvocation setSelector:@selector(run)];
// 让dog去调用该方法
[anInvocation invokeWithTarget:self];
}

((void(*)(id, SEL))objc_msgSend)((id)dog, @selector(eat));的输出结果为 :

1
2016-09-04 17:56:53.238 Runtime-实践篇[14037:1063170] 是狗在跑步

demo在这里
Github

  1. 动态添加一个类
  2. 打印一个类的所有ivar, property 和 method
  3. 给分类增加属性
  4. 动态添加方法实现
  5. 更换方法调用者
  6. 更改特定方法的实现

十. 归档

涉及方法:
class_copyIvarList ignoredIvarNames objc_setAssociatedObject objc_getAssociatedObject

Objective-C中的Runtime LeeJay

五、归档
大家都知道在归档的时候,需要先将属性一个一个的归档,然后再将属性一个一个的解档,3-5个属性还好,假如100个怎么办,那不得写累死。有了Runtime,就不用担心这个了,下面就是如何利用Runtime实现自动归档和解档。
NSObject+Archive.h文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#import <Foundation/Foundation.h>
@interface NSObject (Archive)
/**
* 归档
*/
- (void)encode:(NSCoder *)aCoder;
/**
* 解档
*/
- (void)decode:(NSCoder *)aDecoder;
/**
* 这个数组中的成员变量名将会被忽略:不进行归档
*/
@property (nonatomic, strong) NSArray *ignoredIvarNames;
@end

NSObject+Archive.m文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#import "NSObject+Archive.h"
#import <objc/runtime.h>
@implementation NSObject (Archive)
- (void)encode:(NSCoder *)aCoder
{
unsigned int outCount = 0;
Ivar *ivars = class_copyIvarList([self class], &outCount);
for (unsigned int i = 0; i < outCount; i++)
{
Ivar ivar = ivars[i];
NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
if ([self.ignoredIvarNames containsObject:key])
{
continue;
}
id value = [self valueForKey:key];
[aCoder encodeObject:value forKey:key];
}
free(ivars);
}
- (void)decode:(NSCoder *)aDecoder
{
unsigned int outCount = 0;
Ivar *ivars = class_copyIvarList([self class], &outCount);
for (unsigned int i = 0; i < outCount; i++)
{
Ivar ivar = ivars[i];
NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
if ([self.ignoredIvarNames containsObject:key])
{
continue;
}
id value = [aDecoder decodeObjectForKey:key];
[self setValue:value forKey:key];
}
free(ivars);
}
- (void)setIgnoredIvarNames:(NSArray *)ignoredIvarNames
{
objc_setAssociatedObject(self,
@selector(ignoredIvarNames),
ignoredIvarNames,
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSArray *)ignoredIvarNames
{
return objc_getAssociatedObject(self, _cmd);
}
@end

然后再去需要归档的类实现文件里面写上这几行代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@implementation Person
- (void)encodeWithCoder:(NSCoder *)aCoder
{
[self encode:aCoder];
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder
{
if (self = [super init])
{
[self decode:aDecoder];
}
return self;
}
@end

这几行代码都是固定写法,你也可以把它们定义成宏,这样就可以实现一行代码就归档了,思路源自MJExtension!

Runtime基本知识点以及应用场景 deft_mkjing

runtime实现归档和解档

1
2
self.fish = [NSKeyedUnarchiver unarchiveObjectWithFile:self.path];
[NSKeyedArchiver archiveRootObject:self.fish toFile:self.path];

当外部调用上面归档解档的代码时会走如下方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
- (NSArray *)ignoredNames
{
return @[];
}
// 解档的时候调用
- (instancetype)initWithCoder:(NSCoder *)aDecoder
{
if (self == [super init]) {
[self mkj_initWithCoder:aDecoder];
}
return self;
}
// 归档的时候调用
- (void)encodeWithCoder:(NSCoder *)aCoder
{
[self mkj_encodeWithCoder:aCoder];
}
#pragma mark - 普通模式下的归档和解档
//实现NSCoding协议中的归档方法,需要一个个列出属性来
//- (void)encodeWithCoder:(NSCoder *)aCoder {
// [aCoder encodeObject:self.name forKey:@"name"];
// [aCoder encodeObject:self.age forKey:@"age"];
//}
//
//
////实现NSCoding协议中的解档方法
//- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder {
// if (self = [super init]) {
// self.name = [aDecoder decodeObjectForKey:@"name"];
// self.age = [aDecoder decodeObjectForKey:@"age"];
// }
// return self;
//}
- (NSArray *)ignoredProperty
{
return @[];
}
- (void)mkj_encodeWithCoder:(NSCoder *)aCoder
{
Class selfClass = self.class;
while (selfClass && selfClass != [NSObject class]) {
unsigned int count = 0;
Ivar *ivars = class_copyIvarList(selfClass, &count);
for (NSInteger i = 0; i < count; i ++)
{
Ivar ivar = ivars[i];
const char *ivarName = ivar_getName(ivar);
NSString *ivarStr = [[NSString stringWithUTF8String:ivarName] substringFromIndex:1];
if ([self respondsToSelector:@selector(ignoredProperty)]) {
// 如果归档key为空
if ([[self ignoredProperty] containsObject:ivarStr]) {
continue;
}
}
id value = [self valueForKey:ivarStr];
[aCoder encodeObject:value forKey:ivarStr];
}
free(ivars);
selfClass = [selfClass superclass];
}
}
- (void)mkj_initWithCoder:(NSCoder *)aDecoder
{
Class selfClass = self.class;
while (selfClass && selfClass != [NSObject class]) {
unsigned int count = 0;
Ivar *ivars = class_copyIvarList(selfClass, &count);
for (NSInteger i = 0; i < count; i ++)
{
Ivar ivar = ivars[i];
const char *ivarName = ivar_getName(ivar);
NSString *ivarStr = [[NSString stringWithUTF8String:ivarName] substringFromIndex:1];
if ([self respondsToSelector:@selector(ignoredProperty)]) {
// 如果归档key为空
if ([[self ignoredProperty] containsObject:ivarStr]) {
continue;
}
}
id value = [aDecoder decodeObjectForKey:ivarStr];
[self setValue:value forKey:ivarStr];
}
free(ivars);
selfClass = [selfClass superclass];
}
}

十一

轻松学习之二——iOS利用Runtime自定义控制器POP手势动画 J_雨
利用runtime获取系统手势的target和action
class_copyIvarList

十二

runtime运用 -从一个模型中找出所有属性赋值给另外一个模型 醋溜草莓便当

objc_property_t class_copyPropertyList property_getName

十三

热修复 JSPatch