Objective-C的runtime
在既有类中使用关联对象存放自定义数据
相关介绍
有时候需要在对象中存放相关信息。这个时候我们会从对象所属类中继承一个子类,然后改用这个子类对象。然而并非所有情况下都能这么做,有时候类的实例可能时又某种机制创建出来,而开发中无法令这种机制创建出自己写的子类实例。因此Objective-C提供了一项强大的特性解决此问题,即“关联对象”(Associated Object)
通过关联对象可以给已有类关联许多其他的对象,这些对象通过“键”来区分,在存储关联对象值的时候一并指明“存储策略”(storage policy),用以维护相应的“内存管理语义”
对象关联类型
关联类型 | 等效的@property属性 |
---|---|
OBJC_ASSOCIATION_COPY | copy |
OBJC_ASSOCIATION_ASSIGN | assign |
OBJC_ASSOCIATION_RETAIN | retain |
OBJC_ASSOCIATION_COPY_NONATOMIC | nonatomic copy |
OBJC_ASSOCIATION_RETAIN_NONATOMIC | nonatomic retain |
我们可以利用下列方法管理关联对象
-
用给定的键和存储策略为某个对象设置关联对象
void objc_setAssociatedObject(id object, void *key, id value, objc_AssociatedPolicy ploicy)
-
更具给定的键从某对象中获取关联的对象值
id objc_getAssociatedObject(id object, void *key)
-
移除指定对象的全部关联对象
void objc_removeAssociatedObjects(id object)
应用举例
开发过程中经常用到UIAlertView类,在用户点击按钮关闭视图的时候,需要用到委托协议 来处理动作。但是当一个视图中有多个UIAlerView视图的时候,这个时候我们就需要在协议方法里面判断UIAlertView的传入参数来确定到底是哪一个UIAlertView了,并且按钮回调的方法的代码和创建UIAlertView的代码并不在一起,这样就造成阅读起来也很不方便。我们像如果能给已知的UIAlertView类添加一个按钮点击回调的block,那么这个问题不就好解决了吗?
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"warmth warning" message:@"we are learning object associated of runtime" delegate:self cancelButtonTitle:@"cancel" otherButtonTitles:@"default", nil];
void (^associatedBlock)(NSInteger clickedBtn) = ^(NSInteger clickedBtn){
if (clickedBtn == 0)
{
NSLog(@"you clicked cancel button");
}
else
{
NSLog(@"you clicked default button");
}
};
objc_setAssociatedObject(alertView, associatedBlockPorpertyKey, associatedBlock, OBJC_ASSOCIATION_COPY);//绑定属性
[alertView show];
}
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
void (^associatedBlock)(NSInteger clickedBtn) = objc_getAssociatedObject(alertView, associatedBlockPorpertyKey);
associatedBlock(buttonIndex);
}
上面代码的实质就是为UIAlertView动态的添加了一个associatedBlock属性,在按钮点击的时候,我们先获取这个属性,然后执行associatedBlock()。这样即使有多个UIAlertView,而每个UIAlertView都带有属于自己的associatedBlock属性,因此在协议方法中我们就不用判断UIAlertView的参数来确定到底是那一个UIAlertView,而是直接去除associatedBlock属性执行即可。并且每个UIAlertView的代码和处理按钮的逻辑代码都写在一起,阅读起来也非常的方便。
理解动态绑定和静态绑定
由于Objective-C是C的超级,所以最好先理解C语言的函数调用方式。C语言使用“静态绑定”(static binding),也就是说在编译器就能决定运行时所应调用的函数。
void prinHelloWorld()
{
printf("Hello World");
}
void printGoodBye()
{
printf("Good Bye");
}
void doSomething(int type)
{
if (type == 0)
{
prinHelloWorld();
}
else
{
printGoodBye();
}
}
如果不考虑“内联”(inline),那么编译器在编译代码的时候就已经知道程序有printHelloWorld和printGoogBye这两个函数了,于是会直接生成调用这些函数的指令,而函数地址实际上是硬编码在指令中的,如是将上面的代码改写成如下会怎么样呢?
void prinHelloWorld()
{
printf("Hello World");
}
void printGoodBye()
{
printf("Good Bye");
}
void doSomething(int type)
{
void (*func)();
if (type == 0)
{
func = prinHelloWorld;
}
else
{
func = printGoodBye;
}
func();
}
这时就使用到“动态绑定”( dynamic binding)了,因为所有调用函数在运行期才能确定,编译器在这种情况下生成的指令与修改之前不一样。在修改前,if与else语句里面都有函数点用指令,而修改以后只有一个函数调用指令,不过待调用函数地址无法硬编码在指令中,而是在运行期读取出来。
在Objective-C中,如果像某个对象发送消息,那就使用动态绑定机制来决定那个方法。在底层,所有方法都试C语言的普通函数,然而对象收到消息之后究竟调用哪个方法完全取决于运行期环境。甚至可以在程序运行时改变这些特性使得Objective-C称为一门真正的动态语言。
方法调配技术和调试黑盒方法
大家都知道,如果使用nil去初始化一个数组的时候,程序必定会抛出异常
NSString *str = nil;
NSArray *arr = @[@"123", str];
但是,上面提到Objective-C对象在接受到消息之后,究竟会调用何种方法需要在运行期才能解析出来。因此我们可以利用runtime机制来交换两个方法的实现,从而达到上述代码不在抛出异常。这里我们需要给NSObject添加一个类目
#import <Foundation/Foundation.h>
@interface NSObject (SafeNS)
@end
@interface NSArray (SafeNS)
@end
#import "NSObject+SafeNS.h"
#import <objc/runtime.h>
@implementation NSObject (SafeNS)
+ (void)swapInstanceSel:(SEL)originalSel withSel:(SEL)swapSel
{
Method originalMethod = class_getInstanceMethod(self, originalSel);
Method swapMehod = class_getInstanceMethod(self, swapSel);
if (originalMethod && swapMehod)
{
method_exchangeImplementations(originalMethod, swapMehod);
}
}
+ (void)swapClassSel:(SEL)originalSel with:(SEL)swapSel
{
Method oriMethod = class_getClassMethod(self, originalSel);
Method swapMethod = class_getClassMethod(self, swapSel);
if (oriMethod && swapMethod)
{
method_exchangeImplementations(oriMethod, swapMethod);
}
}
@end
@implementation NSArray (SafeNS)
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
//swap instance methods
[self swapInstanceSel:@selector(initWithObjects:count:) withSel:@selector(swapInitWithObjects:count:)];
//swap class method
[self swapClassSel:@selector(arrayWithObjects:count:) with:@selector(swapArrayWithObjects:count:)];
[self swapClassSel:@selector(arrayWithObject:) with:@selector(swapArrayWithObject:)];
});
}
- (instancetype)swapInitWithObjects:(const id [])objects count:(NSUInteger)count
{
id objTmp[count];
for (int i = 0; i < count; i++)
{
if (!objects[i])//将nil对象转换成NSNull的对象,避免程序跑抛出异常
{
objTmp[i] = [NSNull null];
}
else
{
objTmp[i] = objects[i];
}
}
return [self swapInitWithObjects:objTmp count:count];//看上去程序好想是递归进入是循环,实质是调用了系统的方法
}
+ (instancetype)swapArrayWithObjects:(const id [])objects count:(NSUInteger)count
{
id objTmp[count];
NSLog(@"111");
for (int i = 0; i < count; i++)
{
if (!objects[i])
{
objTmp[i] = [NSNull null];
}
else
{
objTmp[i] = objects[i];
}
}
return [[self class] swapArrayWithObjects:objTmp count:count];
}
+ (instancetype)swapArrayWithObject:(id)anObject
{
if (!anObject)
{
anObject = [NSNull null];
}
return [[self class] swapArrayWithObject:anObject];
}
@end