1.运行时 VS 编译时
- 运行时 : 直到程序运行时才确定对象的具体信息
- 编译时 : 在程序运行之前,编译的时候,才确定对象的具体信息,并且不可改变
- runtime的强大之处在于它能在运行时创建类和对象
2.目的
目的:动态改变NSObject对象的某些信息(修改值:如,处理nil,Null属性值 ;添加属性)
C库介绍:
//库1 (objc/message.h): 获取这个类的所有属性,导入以下库
//说明:父类的属性并不会打印出来,通过这个方法,获取不到父类的属性
#import<objc/message.h>
//使用方法:
//打印NSObject对象的属性名和属性类型
- (void)delogNSObjectPropertyNameAndType{
unsigned int count = 0 ;
//获取到所有的NSObject对象的成员变量列表
Ivar *vars = class_copyIvarList([AddPropertyDTO class], &count);
for (int i = 0; i < count; i++) {
const char *propertyName = ivar_getName(vars[i]); //获取变量名
const char *propertyType = ivar_getTypeEncoding(vars[i]);//获取变量编码类型
printf("propertyName:%s propertyType:%s\n",propertyName,propertyType);
}
}
*********************AddPropertyDTO********************
#import <Foundation/Foundation.h>
@interface AddPropertyDTO : NSObject
@property (copy,nonatomic) NSString *propertyName;
@property (copy,nonatomic) NSString *propertyCount;
@end
#import "AddPropertyDTO.h"
@implementation AddPropertyDTO
@end
***********************LLDB打印信息*********************
propertyName:_propertyName propertyType:@"NSString"
propertyName:_propertyCount propertyType:@"NSString"
- 库2的重要函数说明:
//函数1 :
self 是要给哪个对象添加 变量
&AddProperty 是对这个变量的标记,获得这个变量也是需要这个key值得
hotelName 是 这个 变量的值
OBJC_ASSOCIATION_COPY 这个 是添加变量的策略,和属性的copy类似
/**
OBJC_ASSOCIATION_ASSIGN; //assign策略
OBJC_ASSOCIATION_COPY_NONATOMIC; //copy,nonatomic策略
OBJC_ASSOCIATION_RETAIN_NONATOMIC; //retain,nonatomic策略
OBJC_ASSOCIATION_RETAIN //retain策略
OBJC_ASSOCIATION_COPY //copy策略
*/
objc_setAssociatedObject(self, & AddProperty, hotelName, OBJC_ASSOCIATION_COPY)
//函数2:
//通过 & AddProperty 这个标记 获得 新添加的变量
objc_getAssociatedObject(self, & AddProperty)
//库2 (objc/runtime.h):利用NSObject 的Category添加扩展属性,不需要修改原有的属性对象
//说明:Category(类别)只可以添加方法,不可以添加属性,但是有了runTime之后就可以添加属性,如下
#import <objc/runtime.h>
//使用方法
//使用runtime运行时修改对象的值,当NSString类型属性的值为nil,Null时
- (void)runtimeModifyNSObjectProperty{
NSObject *object1 = [[NSObject alloc]init];
object1.hotName = nil;
NSLog(@"hotName属性值 = %@\n",object1.hotName);
NSObject *object2 = [[NSObject alloc]init];
object2.hotName = @"属性已经赋值";
NSLog(@"hotName属性值 = %@\n",object2.hotName);
}
***********************LLDB打印信息*********************
hotName属性值 = 属性未赋值
hotName属性值 = 属性已经赋值
//Category
********************* NSObject+AddProperty.h ********************
#import <Foundation/Foundation.h>
@interface NSObject (AddProperty)
@property (nonatomic,copy) NSString *hotName;
@end
********************* NSObject+AddProperty.m ********************
#import "NSObject+AddProperty.h"
#import <objc/runtime.h>
static void* AddPropertyKey = @"AddProperty";//属性对应的地址key
@implementation NSObject (AddProperty)
- (void)setHotName:(NSString *)hotName{
//添加 属性 !!!!重点
objc_setAssociatedObject(self, &AddPropertyKey, hotName, OBJC_ASSOCIATION_COPY);
}
- (NSString *)hotName{
//获取 属性 !!!重点
NSString *hotNameStr = objc_getAssociatedObject(self, &AddPropertyKey);
//对属性进行处理,返回对应值
if ([hotNameStr isEqualToString:@""]||hotNameStr==nil||hotNameStr.length == 0) {
return @"属性未赋值";
} else {
return hotNameStr;//objc_getAssociatedObject(self, &AddPropertyKey);
}
}
@end
2.关联
关联对象
关联对象操作函数包括以下:
// 设置关联对象
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 );
关联对象及相关实例已经在前面讨论过了,在此不再重复。
属性
属性操作相关函数包括以下:
// 获取属性名
const char * property_getName ( objc_property_t property );
// 获取属性特性描述字符串
const char * property_getAttributes ( objc_property_t property );
// 获取属性中指定的特性
char * property_copyAttributeValue ( objc_property_t property, const char *attributeName );
// 获取属性的特性列表
objc_property_attribute_t * property_copyAttributeList ( objc_property_t property, unsigned int *outCount );
1 .关联对象操作函数包括以下:
关联对象是Runtime中一个非常实用的特性,不过可能很容易被忽视。
关联对象类似于成员变量,不过是在运行时添加的。我们通常会把成员变量(Ivar)放在类声明的头文件中,或者放在类实现的@implementation后面。但这有一个缺点,我们不能在分类中添加成员变量。如果我们尝试在分类中添加新的成员变量,编译器会报错。
我们可能希望通过使用(甚至是滥用)全局变量来解决这个问题。但这些都不是Ivar,因为他们不会连接到一个单独的实例。因此,这种方法很少使用。
Objective-C针对这一问题,提供了一个解决方案:即关联对象(Associated Object)。
我们可以把关联对象想象成一个Objective-C对象(如字典),这个对象通过给定的key连接到类的一个实例上。不过由于使用的是C接口,所以key是一个void指针(const void *)。我们还需要指定一个内存管理策略,以告诉Runtime如何管理这个对象的内存。这个内存管理的策略可以由以下值指定:
OBJC_ASSOCIATION_ASSIGN
OBJC_ASSOCIATION_RETAIN_NONATOMIC
OBJC_ASSOCIATION_COPY_NONATOMIC
OBJC_ASSOCIATION_RETAIN
OBJC_ASSOCIATION_COPY
当宿主对象被释放时,会根据指定的内存管理策略来处理关联对象。
- 如果指定的策略是assign,则宿主释放时,关联对象不会被释放;
- 如果指定的是retain或者copy,则宿主释放时,关联对象会被释放
我们甚至可以选择是否是自动retain/copy。当我们需要在多个线程中处理访问关联对象的多线程代码时,这就非常有用了。
我们将一个对象连接到其它对象所需要做的就是下面两行代码:
static char myKey;
//一个对象连接到其它对象
objc_setAssociatedObject(self, &myKey, anObject, OBJC_ASSOCIATION_RETAIN);
在这种情况下,self对象将获取一个新的关联的对象anObject,且内存管理策略是自动retain关联对象,当self对象释放时,会自动release关联对象。另外,如果我们使用同一个key来关联另外一个对象时,也会自动释放之前关联的对象,这种情况下,先前的关联对象会被妥善地处理掉,并且新的对象会使用它的内存。
id anObject = objc_getAssociatedObject(self, &myKey);
我们可以使用objc_removeAssociatedObjects函数来移除一个关联对象,或者使用objc_setAssociatedObject函数将key指定的关联对象设置为nil。
我们下面来用实例演示一下关联对象的使用方法:
假定我们想要动态地将一个Tap手势操作连接到任何UIView中,并且根据需要指定点击后的实际操作。这时候我们就可以将一个手势对象及操作的block对象关联到我们的UIView对象中。这项任务分两部分。首先,如果需要,我们要创建一个手势识别对象并将它及block做为关联对象。如下代码所示:
注意:导入“#import <objc/runtime.h>”
- (void)setTapActionWithBlock:(void (^)(void))block {
UITapGestureRecognizer *gesture = objc_getAssociatedObject(self, &kDTActionHandlerTapGestureKey);
if (!gesture) {
gesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(__handleActionForTapGesture:)];
[self addGestureRecognizer:gesture];
objc_setAssociatedObject(self, &kDTActionHandlerTapGestureKey, gesture, OBJC_ASSOCIATION_RETAIN);
}
//注意block对象的关联内存管理策略
objc_setAssociatedObject(self, &kDTActionHandlerTapBlockKey, block, OBJC_ASSOCIATION_COPY);
}
这段代码检测了手势识别的关联对象。如果没有,则创建并建立关联关系。同时,将传入的块对象连接到指定的key上。
手势识别对象需要一个target和action,所以接下来我们定义处理方法:
- (void)__handleActionForTapGesture:(UITapGestureRecognizer *)gesture {
if (gesture.state == UIGestureRecognizerStateRecognized) {
void(^action)(void) = objc_getAssociatedObject(self, &kDTActionHandlerTapBlockKey);
if (action){
action();
}
}
}
从上面的例子可以看到,关联对象使用起来并不复杂。它可以动态地增强类现有的功能。可以在实际编码中灵活地运用这一特性。
完整代码
//用法
#import "UIView+Gesture.h"
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self.view setTapActionWithBlock:^{
NSLog(@"tap");
}];
}
@end
//.h文件
#import <UIKit/UIKit.h>
@interface UIView (Gesture)
- (void)setTapActionWithBlock:(void (^)(void))block;
@end
//.m文件
#import "UIView+Gesture.h"
#import <objc/runtime.h>
char kDTActionHandlerTapGestureKey;
char kDTActionHandlerTapBlockKey;
@implementation UIView (Gesture)
- (void)setTapActionWithBlock:(void (^)(void))block {
UITapGestureRecognizer *gesture = objc_getAssociatedObject(self, &kDTActionHandlerTapGestureKey);
if (!gesture) {
gesture = [[UITapGestureRecognizer alloc] initWithTarget:self
action:@selector(__handleActionForTapGesture:)];
[self addGestureRecognizer:gesture];
objc_setAssociatedObject(self, &kDTActionHandlerTapGestureKey, gesture, OBJC_ASSOCIATION_RETAIN);
}
objc_setAssociatedObject(self, &kDTActionHandlerTapBlockKey, block, OBJC_ASSOCIATION_COPY);
}
- (void)__handleActionForTapGesture:(UITapGestureRecognizer *)gesture {
if (gesture.state == UIGestureRecognizerStateRecognized) {
void(^action)(void) = objc_getAssociatedObject(self, &kDTActionHandlerTapBlockKey);
if (action){
action();
}
}
}
@end
3.方法和消息
SEL又叫选择器,是表示一个方法的selector的指针,其定义如下:
typedef struct objc_selector *SEL;
方法的selector用于表示运行时方法的名字。Objective-C在编译时,会依据每一个方法的名字、参数序列,生成一个唯一的整型标识(Int类型的地址),这个标识就是SEL。
如下 代码所示:
SEL sel1 = @selector(method);NSLog(@"sel : %p", sel1);
//输出为:
2016-08-15 17:14:45.128 Runtime选择器&方法[2825:257940] sel : 0x104c9ea22
两个类之间,不管它们是父类与子类的关系,还是之间没有这种关系,只要方法名相同,那么方法的SEL就是一样的。每一个方法都对应着一个SEL。所以在Objective-C同一个类(及类的继承体系)中,不能存在2个同名的方法,即使参数类型不同也不行。相同的方法只能对应一个SEL。这也就导致 Objective-C在处理相同方法名且参数个数相同但类型不同的方法方面的能力很差。
如在某个类中定义以下两个方法:
- (void)setWidth:(int)width;
- (void)setWidth:(double)width;
当然,不同的类可以拥有相同的selector,这个没有问题。不同类的实例对象执行相同的selector时,会在各自的方法列表中去根据selector去寻找自己对应的IMP。
工程中的所有的SEL组成一个Set集合,Set的特点就是唯一,因此SEL是唯一的。
因此,如果我们想到这个方法集合中查找某个方法时,只需要去 找到这个方法对应的SEL就行了,SEL实际上就是根据方法名hash化了的一个字符串,而对于字符串的比较仅仅需要比较他们的地址就可以了,可以说速度 上无语伦比!!但是,有一个问题,就是数量增多会增大hash冲突而导致的性能下降(或是没有冲突,因为也可能用的是perfect hash)。但是不管使用什么样的方法加速,如果能够将总量减少(多个方法可能对应同一个SEL),那将是最犀利的方法。那么,我们就不难理解,为什么 SEL仅仅是函数名了。
4.IMP
IMP实际上是一个函数指针,指向方法实现的首地址。其定义如下:
id (*IMP)(id, SEL, ...)
这个函数使用当前CPU架构实现的标准的C调用约定。
- 第一个参数是指向self的指针(如果是实例方法,则是类实例的内存地址;如果是类方法,则是指向元类的指针)
- 第二个参数是方法选择器(selector),接下来是方法的实际参数列表
前面介绍过的SEL就是为了查找方法的最终实现IMP的。由于每个方法对应唯一的SEL,因此我们可以通过SEL方便快速准确地获得它所对应的 IMP,查找过程将在下面讨论。取得IMP后,我们就获得了执行这个方法代码的入口点,此时,我们就可以像调用普通的C语言函数一样来使用这个函数指针 了。
通过取得IMP,我们可以跳过Runtime的消息传递机制,直接执行IMP指向的函数实现,这样省去了Runtime消息传递过程中所做的一系列查找操作,会比直接向对象发送消息高效一些。
5.Method
介绍完SEL和IMP,我们就可以来讲讲Method了。Method用于表示类定义中的方法,则定义如下:
typedef struct objc_method *Method;
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE; // 方法名
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE; // 方法实现
}
可以看到该结构体中包含一个SEL和IMP,实际上相当于在SEL和IMP之间作了一个映射。有了SEL,便可以找到对应的IMP,从而调用方法的实现代码。具体操作流程将在下面讨论。
objc_method_description
objc_method_description定义了一个Objective-C方法,其定义如下:
struct objc_method_description { SEL name; char *types; };
6.方法相关操作函数
Runtime提供了一系列的方法来处理与方法相关的操作。包括方法本身及SEL。
方法操作相关函数包括下以:
//调用指定方法的实现
//返回的是实际实现的返回值。参数receiver不能为空。这个方法的效率会比method_getImplementation和method_getName更快
id method_invoke ( id receiver, Method m, ... );
// 调用返回一个数据结构的方法的实现
void method_invoke_stret ( id receiver, Method m, ... );
// 获取方法名
//返回的是一个SEL。如果想获取方法名的C字符串,可以使用sel_getName(method_getName(method))
SEL method_getName ( Method m );
// 返回方法的实现
IMP method_getImplementation ( Method m );
// 获取描述方法参数和返回值类型的字符串
const char * method_getTypeEncoding ( Method m );
// 获取方法的返回值类型的字符串
char * method_copyReturnType ( Method m );
// 获取方法的指定位置参数的类型字符串
char * method_copyArgumentType ( Method m, unsigned int index );
// 通过引用返回方法的返回值类型字符串
//类型字符串会被拷贝到dst中
void method_getReturnType ( Method m, char *dst, size_t dst_len );
// 返回方法的参数的个数
unsigned int method_getNumberOfArguments ( Method m );
// 通过引用返回方法指定位置参数的类型字符串
void method_getArgumentType ( Method m, unsigned int index, char *dst, size_t dst_len );
// 返回指定方法的方法描述结构体
struct objc_method_description * method_getDescription ( Method m );
// 设置方法的实现
//注意该函数返回值是方法之前的实现
IMP method_setImplementation ( Method m, IMP imp );
// 交换两个方法的实现
void method_exchangeImplementations ( Method m1, Method m2 );
7.方法选择器
选择器相关的操作函数包括:
- sel_registerName函数:在我们将一个方法添加到类定义时,我们必须在Objective-C Runtime系统中注册一个方法名以获取方法的选择器。
// 返回给定选择器指定的方法的名称
const char * sel_getName ( SEL sel );
// 在Objective-C Runtime系统中注册一个方法,将方法名映射到一个选择器,并返回这个选择器
SEL sel_registerName ( const char *str );
// 在Objective-C Runtime系统中注册一个方法
SEL sel_getUid ( const char *str );
// 比较两个选择器
BOOL sel_isEqual ( SEL lhs, SEL rhs );
8.方法调用流程
在Objective-C中,消息直到运行时才绑定到方法实现上。编译器会将消息表达式[receiver message]转化为一个消息函数的调用,即objc_msgSend。
- 这个函数将消息接收者和方法名作为其基础参数,如以下所示:
objc_msgSend(receiver, selector)
- 如果消息中还有其它参数,则该方法的形式如下所示:
objc_msgSend(receiver, selector, arg1, arg2, ...)
这个函数完成了动态绑定的所有事情:
- 首先它找到selector对应的方法实现。因为同一个方法可能在不同的类中有不同的实现,所以我们需要依赖于接收者的类来找到的确切的实现。
- 它调用方法实现,并将接收者对象及方法的所有参数传给它。
- 最后,它将实现返回的值作为它自己的返回值。
9.隐藏参数
objc_msgSend有两个隐藏参数:
消息接收对象
方法的selector
这两个参数为方法的实现提供了调用者的信息。之所以说是隐藏的,是因为它们在定义方法的源代码中没有声明。它们是在编译期被插入实现代码的。
虽然这些参数没有显示声明,但在代码中仍然可以引用它们。我们可以使用self来引用接收者对象,使用_cmd来引用选择器。如下代码所示:
- strange{
id target = getTheReceiver();
SEL method = getTheMethod();
if ( target == self || method == _cmd ) {
return nil;
} else {
return [target performSelector:method];
}
}
当然,这两个参数我们用得比较多的是self,_cmd在实际中用得比较少。