1.運行時 VS 編譯時
- 運行時 : 直到程序運行時才確定對象的具體信息
- 編譯時 : 在程序運行之前,編譯的時候,才確定對象的具體信息,并且不可改變
- runtime的強大之處在于它能在運行時創(chuàng)建類和對象
2.目的
目的:動態(tài)改變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.關聯(lián)
關聯(lián)對象
關聯(lián)對象操作函數包括以下:
// 設置關聯(lián)對象
void objc_setAssociatedObject ( id object, const void *key, id value, objc_AssociationPolicy policy );
// 獲取關聯(lián)對象
id objc_getAssociatedObject ( id object, const void *key );
// 移除關聯(lián)對象
void objc_removeAssociatedObjects ( id object );
關聯(lián)對象及相關實例已經在前面討論過了,在此不再重復。
屬性
屬性操作相關函數包括以下:
// 獲取屬性名
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 .關聯(lián)對象操作函數包括以下:
關聯(lián)對象是Runtime中一個非常實用的特性,不過可能很容易被忽視。
關聯(lián)對象類似于成員變量,不過是在運行時添加的。我們通常會把成員變量(Ivar)放在類聲明的頭文件中,或者放在類實現(xiàn)的@implementation后面。但這有一個缺點,我們不能在分類中添加成員變量。如果我們嘗試在分類中添加新的成員變量,編譯器會報錯。
我們可能希望通過使用(甚至是濫用)全局變量來解決這個問題。但這些都不是Ivar,因為他們不會連接到一個單獨的實例。因此,這種方法很少使用。
Objective-C針對這一問題,提供了一個解決方案:即關聯(lián)對象(Associated Object)。
我們可以把關聯(lián)對象想象成一個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
當宿主對象被釋放時,會根據指定的內存管理策略來處理關聯(lián)對象。
- 如果指定的策略是assign,則宿主釋放時,關聯(lián)對象不會被釋放;
- 如果指定的是retain或者copy,則宿主釋放時,關聯(lián)對象會被釋放
我們甚至可以選擇是否是自動retain/copy。當我們需要在多個線程中處理訪問關聯(lián)對象的多線程代碼時,這就非常有用了。
我們將一個對象連接到其它對象所需要做的就是下面兩行代碼:
static char myKey;
//一個對象連接到其它對象
objc_setAssociatedObject(self, &myKey, anObject, OBJC_ASSOCIATION_RETAIN);
在這種情況下,self對象將獲取一個新的關聯(lián)的對象anObject,且內存管理策略是自動retain關聯(lián)對象,當self對象釋放時,會自動release關聯(lián)對象。另外,如果我們使用同一個key來關聯(lián)另外一個對象時,也會自動釋放之前關聯(lián)的對象,這種情況下,先前的關聯(lián)對象會被妥善地處理掉,并且新的對象會使用它的內存。
id anObject = objc_getAssociatedObject(self, &myKey);
我們可以使用objc_removeAssociatedObjects函數來移除一個關聯(lián)對象,或者使用objc_setAssociatedObject函數將key指定的關聯(lián)對象設置為nil。
我們下面來用實例演示一下關聯(lián)對象的使用方法:
假定我們想要動態(tài)地將一個Tap手勢操作連接到任何UIView中,并且根據需要指定點擊后的實際操作。這時候我們就可以將一個手勢對象及操作的block對象關聯(lián)到我們的UIView對象中。這項任務分兩部分。首先,如果需要,我們要創(chuàng)建一個手勢識別對象并將它及block做為關聯(lián)對象。如下代碼所示:
注意:導入“#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對象的關聯(lián)內存管理策略
objc_setAssociatedObject(self, &kDTActionHandlerTapBlockKey, block, OBJC_ASSOCIATION_COPY);
}
這段代碼檢測了手勢識別的關聯(lián)對象。如果沒有,則創(chuàng)建并建立關聯(lián)關系。同時,將傳入的塊對象連接到指定的key上。
手勢識別對象需要一個target和action,所以接下來我們定義處理方法:
- (void)__handleActionForTapGesture:(UITapGestureRecognizer *)gesture {
if (gesture.state == UIGestureRecognizerStateRecognized) {
void(^action)(void) = objc_getAssociatedObject(self, &kDTActionHandlerTapBlockKey);
if (action){
action();
}
}
}
從上面的例子可以看到,關聯(lián)對象使用起來并不復雜。它可以動態(tài)地增強類現(xiàn)有的功能??梢栽趯嶋H編碼中靈活地運用這一特性。
完整代碼
//用法
#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,這個沒有問題。不同類的實例對象執(zhí)行相同的selector時,會在各自的方法列表中去根據selector去尋找自己對應的IMP。
工程中的所有的SEL組成一個Set集合,Set的特點就是唯一,因此SEL是唯一的。
因此,如果我們想到這個方法集合中查找某個方法時,只需要去 找到這個方法對應的SEL就行了,SEL實際上就是根據方法名hash化了的一個字符串,而對于字符串的比較僅僅需要比較他們的地址就可以了,可以說速度 上無語倫比??!但是,有一個問題,就是數量增多會增大hash沖突而導致的性能下降(或是沒有沖突,因為也可能用的是perfect hash)。但是不管使用什么樣的方法加速,如果能夠將總量減少(多個方法可能對應同一個SEL),那將是最犀利的方法。那么,我們就不難理解,為什么 SEL僅僅是函數名了。
4.IMP
IMP實際上是一個函數指針,指向方法實現(xiàn)的首地址。其定義如下:
id (*IMP)(id, SEL, ...)
這個函數使用當前CPU架構實現(xiàn)的標準的C調用約定。
- 第一個參數是指向self的指針(如果是實例方法,則是類實例的內存地址;如果是類方法,則是指向元類的指針)
- 第二個參數是方法選擇器(selector),接下來是方法的實際參數列表
前面介紹過的SEL就是為了查找方法的最終實現(xiàn)IMP的。由于每個方法對應唯一的SEL,因此我們可以通過SEL方便快速準確地獲得它所對應的 IMP,查找過程將在下面討論。取得IMP后,我們就獲得了執(zhí)行這個方法代碼的入口點,此時,我們就可以像調用普通的C語言函數一樣來使用這個函數指針 了。
通過取得IMP,我們可以跳過Runtime的消息傳遞機制,直接執(zhí)行IMP指向的函數實現(xiàn),這樣省去了Runtime消息傳遞過程中所做的一系列查找操作,會比直接向對象發(fā)送消息高效一些。
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; // 方法實現(xiàn)
}
可以看到該結構體中包含一個SEL和IMP,實際上相當于在SEL和IMP之間作了一個映射。有了SEL,便可以找到對應的IMP,從而調用方法的實現(xiàn)代碼。具體操作流程將在下面討論。
objc_method_description
objc_method_description定義了一個Objective-C方法,其定義如下:
struct objc_method_description { SEL name; char *types; };
6.方法相關操作函數
Runtime提供了一系列的方法來處理與方法相關的操作。包括方法本身及SEL。
方法操作相關函數包括下以:
//調用指定方法的實現(xiàn)
//返回的是實際實現(xiàn)的返回值。參數receiver不能為空。這個方法的效率會比method_getImplementation和method_getName更快
id method_invoke ( id receiver, Method m, ... );
// 調用返回一個數據結構的方法的實現(xiàn)
void method_invoke_stret ( id receiver, Method m, ... );
// 獲取方法名
//返回的是一個SEL。如果想獲取方法名的C字符串,可以使用sel_getName(method_getName(method))
SEL method_getName ( Method m );
// 返回方法的實現(xiàn)
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 );
// 設置方法的實現(xiàn)
//注意該函數返回值是方法之前的實現(xiàn)
IMP method_setImplementation ( Method m, IMP imp );
// 交換兩個方法的實現(xiàn)
void method_exchangeImplementations ( Method m1, Method m2 );
7.方法選擇器
選擇器相關的操作函數包括:
- sel_registerName函數:在我們將一個方法添加到類定義時,我們必須在Objective-C Runtime系統(tǒng)中注冊一個方法名以獲取方法的選擇器。
// 返回給定選擇器指定的方法的名稱
const char * sel_getName ( SEL sel );
// 在Objective-C Runtime系統(tǒng)中注冊一個方法,將方法名映射到一個選擇器,并返回這個選擇器
SEL sel_registerName ( const char *str );
// 在Objective-C Runtime系統(tǒng)中注冊一個方法
SEL sel_getUid ( const char *str );
// 比較兩個選擇器
BOOL sel_isEqual ( SEL lhs, SEL rhs );
8.方法調用流程
在Objective-C中,消息直到運行時才綁定到方法實現(xiàn)上。編譯器會將消息表達式[receiver message]轉化為一個消息函數的調用,即objc_msgSend。
- 這個函數將消息接收者和方法名作為其基礎參數,如以下所示:
objc_msgSend(receiver, selector)
- 如果消息中還有其它參數,則該方法的形式如下所示:
objc_msgSend(receiver, selector, arg1, arg2, ...)
這個函數完成了動態(tài)綁定的所有事情:
- 首先它找到selector對應的方法實現(xiàn)。因為同一個方法可能在不同的類中有不同的實現(xiàn),所以我們需要依賴于接收者的類來找到的確切的實現(xiàn)。
- 它調用方法實現(xiàn),并將接收者對象及方法的所有參數傳給它。
- 最后,它將實現(xiàn)返回的值作為它自己的返回值。
9.隱藏參數
objc_msgSend有兩個隱藏參數:
消息接收對象
方法的selector
這兩個參數為方法的實現(xiàn)提供了調用者的信息。之所以說是隱藏的,是因為它們在定義方法的源代碼中沒有聲明。它們是在編譯期被插入實現(xiàn)代碼的。
雖然這些參數沒有顯示聲明,但在代碼中仍然可以引用它們。我們可以使用self來引用接收者對象,使用_cmd來引用選擇器。如下代碼所示:
- strange{
id target = getTheReceiver();
SEL method = getTheMethod();
if ( target == self || method == _cmd ) {
return nil;
} else {
return [target performSelector:method];
}
}
當然,這兩個參數我們用得比較多的是self,_cmd在實際中用得比較少。