关联对象
runtime的对象关联,如果有了解的话就会知道,他是通过一个key,将两个对象或者对象和某个类之间进行联系。这样就可以实现对象直接的传递,通信,及获取对应关联方法。
在runtime中,OC为我们提供特定的方法:
//关联对象
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)
其中的参数object
和value
分别为被关联和关联的对象,通过key
联系在一起。objc_AssociationPolicy
是一个枚举类型,指定发送策略,指定关联类型的属性:
OBJC_ASSOCIATION_ASSIGN = 0, <指定一个弱引用关联的对象,等效:@property assign>
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,<指定一个强引用关联的对象,等效:@property retain,nonatomic>
OBJC_ASSOCIATION_COPY_NONATOMIC = 3, <指定相关的对象复制,等效:@property copy,nonatomic>
OBJC_ASSOCIATION_RETAIN = 01401, <指定强参考,等效:@property retain>
OBJC_ASSOCIATION_COPY = 01403 <指定相关的对象复制,等效:@property copy>
使用实战
- 给对象添加联系
比如,我们为一个数据要增加一个说明,可以使用这种关联关系来运用:
const NSString *accociate_name_key = @"accociate_name_key";
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSArray *names = @[@"张三",@"李四",@"王五"];
NSString *describe = @"这是一张3年级的学生姓名表数据";
// 将两个联系在一起
objc_setAssociatedObject(names, &accociate_name_key, describe, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
// 通过数据对象,获取联系对象
NSString *obj = objc_getAssociatedObject(names, &accociate_name_key);
NSLog(@"%@",obj);
}
- 生成set / get方法
当使用category无法增加私有属性,但是可以通过该方法去关联生成setter/getter方法。这样类别在就可以实现定义属性和传值。
#import <UIKit/UIKit.h>
@interface UIViewController (Tool)
@property (copy, nonatomic)NSString *otherTitle;
@end
#import "UIViewController+Tool.h"
#import <objc/runtime.h>
static const NSString *associatedKey = @"associate_otherTitle_key";
@implementation UIViewController (Tool)
- (NSString *)otherTitle {
return objc_getAssociatedObject(self, &associatedKey);
}
- (void)setOtherTitle:(NSString *)otherTitle {
objc_setAssociatedObject(self, &associatedKey, otherTitle, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- 自定义响应事件
存在一种情况,当调用某些方法时,响应的事件需要到其他方法去处理,比如UIAlertView,当弹出框点击时,响应的事件需要去delegate中去看,如果代码量多的话,这需要找一段时间,但是通过关联的方式可以将delegate处理时间转移到同一个方法中,方便查看。
- (void)viewDidLoad {
[super viewDidLoad];
UIAlertView *alterView = [[UIAlertView alloc]initWithTitle:@"提示"
message:@"内容"
delegate:self
cancelButtonTitle:@"取消"
otherButtonTitles:@"确定", nil];
// 自定义响应方法
void (^block)(NSInteger) = ^(NSInteger buttonIndex) {
NSLog(@"响应方法");
};
objc_setAssociatedObject(alterView, &accociate_name_key, block, OBJC_ASSOCIATION_COPY);
}
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
void (^block)(NSInteger) = objc_getAssociatedObject(alertView, &accociate_name_key);
block(buttonIndex);
}
这样想要知道响应事件是什么就可以在同一个方法来解决处理,但是这种方式会涉及到循环引用的问题,因此记得需要去处理。
方法交换
对于系统的方法,调用何种方式在运行期可以去解析或者去改变,runtime提供的方法交互的黑科技,让我们在不用去继承子类的情况下,可以在运行期对特定的选择子去改变这个类本身的功能。如果运用的好的话,将是一个很大的用处,就是方法交换(Method Swizzling)。
方法交换涉及到的一些runtime方法:
// 获取方法实例对象
Method class_getInstanceMethod(Class cls, SEL name)
// 获取方法类对象
Method class_getClassMethod(Clas cls, SEL name)
// 获取方法名字符串
const char *method_getTypeEncoding(Method m)
// 获取方法所指向的IMP指针对象
IMP method_getImplementation(Method m)
// 设置方法所指向的IMP指针对象
IMP method_setImplementation(Method m, IMP imp)
// 交换两个方法IMP指向
void method_exchangeImplementations(Method m1, Method m2)
// 向类的方法列表添加方法
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char * types)
// 替换一个类的方法的IMP指针
IMP class_replaceMethod(Class cls, SEL _Nonnull name, IMP imp, const char * types)
每个类都有一个对应的方法列表,为了在传递消息的可以去对应查找,而方法列表中包含该列的所有选择子名,以及指向IMP指针。我们在进行方法交换的本质其实就是改变IMP指针的指向,从而当向原来的选择子传递消息时,可以去改变实现的功能。
那么能实现该方法的方式有三种:
- 通过
class_replaceMethod
去替换原来的方法的IMP指针的指向。 - 通过
method_exchangeImplementations
交换两个IMP指针的指向。 - 通过
method_setImplementation
重新设置IMP指针的实现。
class_replaceMethod 替换
用户可以去改变类的方法列表,可以新增选择子,也可以改变选择子的对应方法实现。class_replaceMethod
便是如此。通常都是在 load
方法中调用,该方法在系统运行开始时执行且只执行一次。
+ (void)load {
swizzleMethod([self class], @selector(viewWillAppear:), @selector(exchangeViewWillAppear:));
}
void swizzleMethod(Class class, SEL originalSelector, SEL swizzledSelector)
{
// 获取方法实例对象
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
// 在原类方法列表中,将要交换的方法名及IMP指针添加进去
BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
// 替换原来指针的指向
class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
}
- (void)exchangeViewWillAppear:(BOOL)animal {
// 这步必须加上,才能再跳回原来的viewWillAppear去执行后续方法
[self exchangeViewWillAppear:animal];
}
方法交换的本意是去为某些方法添加一些功能,比如埋点,数组越界,或者调试的时候用来统一处理一些事等等,使用方法交换就很方便。
方法交换一般在程序运行时只能调用一次,因此如果没有调用的话需要自己处理。通过以上的code可以实现当controller在viewWillAppear
时,都会进入exchangeViewWillAppear
,并且当在调用 [self exchangeViewWillAppear:animal];
,又真正去执行viewWillAppear
。从而实现可以在exchangeViewWillAppear
新增自定义的处理或功能。
method_exchangeImplementations 替换
该方法用法就比较简单,直接交换:
void swizzleMethod(Class class, SEL originalSelector, SEL swizzledSelector)
{
// 获取方法实例对象
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
// 交换对象IMP指针
method_exchangeImplementations(originalMethod, swizzledMethod);
}
method_setImplementation 替换
利用IMP指针去对方法对象进行操作实现方法交换,这种方法需要特别注意,系统使用的IMP指针是无返回值的,因为如果直接获取对象会出现编译失败或报错。因此处理的一种方式是自己根据选择子的类型和参数自己定义IMP指针的类型:
// 自定义有返回值的IMP
typedef id (*_IMP) (id, SEL, ...);
// 自定义无返回值的IMP
typedef void (*_VIMP) (id, SEL, ...);
void swizzleMethod(Class class, SEL originalSelector, SEL swizzledSelector)
{
// 获取方法实例对象
Method originalMethod = class_getInstanceMethod(class, originalSelector);
// 根据自定义类型_IMP,获取IMP指针对象
_IMP origialIMP = (_IMP)method_getImplementation(originalMethod);
// 重新设置IMP指针的实现方法
method_setImplementation(originalMethod, imp_implementationWithBlock(^(id target, SEL action) {
// 返回消息给原对象
origialIMP(target, action);
}));
}
通过重新指向IMP指针的方式去设置,实现交换的功能。
网友评论