不鋪墊那麼多,單刀直入吧:runtime是一個C和匯編寫的動態庫,就像是一個小小的系統,將OC和C緊密關聯在一次,這個系統主要做兩件事情。
1,封裝C語言的結構體和函數,讓開發者在運行時創建,檢查或者修改類,對象和方法等
2,傳遞消息,找出方法的最終執行代碼
也就是說我們寫的OC代碼在運行的時候都會轉為運行時代碼
通過runtime的學習能夠更好理解OC的這種消息發送機制,並且我也認為對runtime的學習是對深入學習iOS必不可少的坎,比如你有可能通過閱讀一些第三方框架來提高自己的編程技巧,在這些第三方框架中就會有大量運行時代碼。掌握了runtime我們能夠簡單做些什麼事情呢?
1,遍歷對象的所有屬性
2,動態添加/修改屬性,動態添加/修改/替換方法
3,動態創建類/對象
4,方法攔截使用(給方法添加一個動態實現,甚至可以講該方法重定向或者打包給lisi)
聽起來跟黑魔法一樣。其實runtime就素有黑魔法之稱!我們就從成員變量開始我們對runtime的學習吧。
成員變量是我們在定義一個類中其中重要的成分,主要是想描述這個類實例化後具備了什麼屬性,特點,等等。就像定義了一個Person類,Person類具備了name,age,gender等各種屬性來描述這個類。舉了這個稍微符合的例子來輔助說明成員變量是干嘛用的,但是卻還是不能說明成員變量到底本質是什麼?在runtime.h文件中的成員變量是一個指向objc_ivar類型的結構體指針:
/// An opaque type that represents an instance variable. typedef struct objc_ivar *Ivar;
在這個objc_ivar這個結構體定義如下:
struct objc_ivar { char *ivar_name OBJC2_UNAVAILABLE;//成員變量的名字 char *ivar_type OBJC2_UNAVAILABLE;//成員變量的類型 int ivar_offset OBJC2_UNAVAILABLE; #ifdef __LP64__ int space OBJC2_UNAVAILABLE; #endif }
對成員變量進行操作的主要有以下幾種方式:
Ivar *class_copyIvarList(Class cls, unsigned int *outCount) //獲取所有成員變量 const char *ivar_getName(Ivar v) //獲取某個成員變量的名字 const char *ivar_getTypeEncoding(Ivar v) //獲取某個成員變量的類型編碼 Ivar class_getInstanceVariable(Class cls, const char *name) //獲取某個類中指定名稱的成員變量 id object_getIvar(id obj, Ivar ivar) //獲取某個對象中的某個成員變量的值 void object_setIvar(id obj, Ivar ivar, id value) //設置某個對象的某個成員變量的值
下面通過建立一個Person類來理解runtime中提供的這些函數,首先我們定義一個Person類,並且重寫它的description方法:
Person.h中:
@interface Person : NSObject { NSString *clan;//族名 } @property(nonatomic,copy)NSString *name; @property(nonatomic,copy)NSString *gender; @property(nonatomic,strong)NSNumber *age; @property(nonatomic,assign)NSInteger height; @property(nonatomic,assign)double weight;
Person.m
-(NSString *)description { unsigned int outCount; Ivar *IvarArray = class_copyIvarList([Person class], &outCount);//獲取到Person中的所有成員變量 for (unsigned int i = 0; i < outCount; i ++) { Ivar *ivar = &IvarArray[i]; NSLog(@"第%d個成員變量:%s,類型是:%s",i,ivar_getName(*ivar),ivar_getTypeEncoding(*ivar));// 依次獲取每個成員變量並且打印成員變量名字和類型 } return nil; }
在程序入口創建Person實例類並且調用description方法可以看到打印台打印:
ivar_getTypeEncoding函數獲取到的是成員變量的類型編碼。類型編碼是蘋果對數據類型對象類型規定的另一個表現形式,比如"@"代表的是對象,":"表示的是SEL指針,"v"表示的是void。具體可以看蘋果官方文檔對類型編碼的具體規定:戳我!!!
通過runtime來給對象賦值和獲取對象的值:
Person.m中實現了兩個分別對實例化person對象賦值和取值方法:
+ (Person *)personWithName:(NSString *)name age:(NSNumber *)age gender:(NSString *)gender clan:(NSString *)clan { Person *p = [Person new]; unsigned int outCount; Ivar *IvarArray = class_copyIvarList([Person class], &outCount); object_setIvar(p, IvarArray[0], clan); object_setIvar(p, IvarArray[1], name); object_setIvar(p, IvarArray[2], gender); object_setIvar(p, IvarArray[3], age); return p; } - (void)personGetPersonMessage { unsigned int outCount; Ivar *IvarArray = class_copyIvarList([Person class], &outCount); for (NSInteger i = 0; i < 4; i ++) { NSLog(@"%s = %@",ivar_getName(IvarArray[i]),object_getIvar(self,IvarArray[i])); } }
在viewDidLoad中:
Person *person = [Person personWithName:@"張三" age:@26 gender:@"man" clan:@"漢"]; [person personGetPersonMessage];
可以看到打印台打印:
成功的對Person對象進行設置值和取值操作。
屬性在runtime中定義如下:
/// An opaque type that represents an Objective-C declared property. typedef struct objc_property *objc_property_t; /// Defines a property attribute typedef struct { const char *name; /**< The name of the attribute */ const char *value; /**< The value of the attribute (usually empty) */ } objc_property_attribute_t;
屬性的本質是一個指向objc_property的結構體指針。跟成員變量一樣,runtime中一樣為屬性定義了一系列對屬性的操作函數:
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount) //獲取所有屬性的列表 const char *property_getName(objc_property_t property) //獲取某個屬性的名字 const char *property_getAttributes(objc_property_t property) //獲取屬性的特性描述 objc_property_attribute_t *property_copyAttributeList(objc_property_t property, unsigned int *outCount) //獲取所有屬性的特性
獲取person實例對象中所有屬性的特性描述:
Person.m中:
- (void)getAttributeOfproperty { unsigned int outCount; objc_property_t *propertyList = class_copyPropertyList([Person class], &outCount); for (NSInteger i = 0; i < outCount; i ++) { NSLog(@"屬性:%s,它的特性描述:%s",property_getName(propertyList[i]),property_getAttributes(propertyList[i])); } }
獲取屬性列表只會獲取有property屬性聲明的變量,所有當調用getAttributeOfproperty的時候打印台打印:
特性描述主要說明的是該屬性的修飾符,具體的代表意義如下:
屬性類型 name值:T value:變化 編碼類型 name值:C(copy) &(strong) W(weak) 空(assign) 等 value:無 非/原子性 name值:空(atomic) N(Nonatomic) value:無
在運行時runtime下我們可以獲取到所有的成員變量,以及類的私有變量。所有runtime的重要應用就是字典轉模型,復雜歸檔。
復雜對象歸檔平常我們需要類遵循<NSCoding>協議,重寫協議中編碼和解碼的兩個方法,創建NSKeyarchive對象將類中的成員變量進行逐一編碼和解碼。
runtime下基本是同樣的操作,但是我們可以利用runtime提供的函數獲取變量的名字和所對應的成員變量,開啟循環進行快速歸檔(要記得普通情況下我們可以要逐一的寫),同樣是以Person類為例;
Person.m中:
-(instancetype)initWithCoder:(NSCoder *)aDecoder { self = [super init]; if (self) { unsigned int outCount; Ivar *ivarList = class_copyIvarList([Person class], &outCount); for (NSInteger i = 0; i < outCount; i ++) { Ivar ivar = ivarList[i]; NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)]; [self setValue:[aDecoder decodeObjectForKey:ivarName] forKey:ivarName]; } } return self; } -(void)encodeWithCoder:(NSCoder *)aCoder { unsigned int outCount; Ivar *ivarlist = class_copyIvarList([self class], &outCount); for (NSInteger i = 0; i < outCount; i ++) { Ivar ivar = ivarlist[i]; NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)]; [aCoder encodeObject:[self valueForKey:ivarName] forKey:ivarName]; } }
另一個重要的應用便是字典轉模型,將字典中的數據賦值給模型中對應的屬性。大概思路是先通過class_copyPropertyList獲取到所有的屬性,再通過property_getName獲取到變量對應的名字作為key值,通過key值查看字典中是否有對應的value,若是有的話則給屬性賦值。
以上的操作都是基於對象具有的屬性通過runtime獲取屬性的一些信息,比如名字,屬性的值,屬性的特性描述等。通過runtime還可以給對象動態添加變量,也就是添加關聯。還記得分類和延展的區別嗎?延展可以為類添加屬性和方法,而分類只能為類添加方法。有個面試題:不使用繼承的方式如何給系統類添加一個公共變量?我們知道在延展裡面為類添加的變量是私有變量,外界無法訪問的。如果對runtime有了解的人也許就知道這是想考驗應聘人對runtime的了解。
runtime下提供了三個函數給我們能夠進行關聯對象的操作:
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:要添加成員變量的對象 key:添加成員變量對應的key值 value:要添加的成員變量 policy:添加的成員變量的修飾符 */
我們以給NSDictionary添加一個NSString類型的公共變量funnyName為例:
在NSDictionary分類MyDict.h新增加兩個屬性其中一個字符串一個block中:
@property(nonatomic,copy)NSString *funnyName; @property(nonatomic,copy)void(^dictAction)(NSString *str);
一般情況下如果我們只聲明了這些變量在外面使用的時候就會報錯,所有需要我們手動實現他們的set和get方法(可別以為是因為我們沒有實現它們的set和方法才報錯了哦,@property修飾的屬性可是會自動生成get和set方法)
那應該如何實現它們的set和get方法呢:
-(void)setFunnyName:(NSString *)funnyName { objc_setAssociatedObject(self, @selector(funnyName), funnyName, OBJC_ASSOCIATION_COPY_NONATOMIC); } -(NSString *)funnyName { return objc_getAssociatedObject(self, @selector(funnyName)); } -(void)setDictAction:(void (^)(NSString *))dictAction { objc_setAssociatedObject(self, @selector(dictAction), dictAction, OBJC_ASSOCIATION_COPY_NONATOMIC); } -(void (^)(NSString *))dictAction { return objc_getAssociatedObject(self, @selector(dictAction)); }
在我們程序中就可以使用字典新增加的兩個屬性了:
NSDictionary *dict = [NSDictionary new]; dict.funnyName = @"SoFunny"; NSLog(@"dict.funnyName = %@",dict.funnyName); void(^action)(NSString *str) = ^(NSString *str){ NSLog(@"打印了這個字符串:%@",str); }; //設置block dict.dictAction = action; //調用dict的action dict.dictAction(@"新增加變量dicAction");
在打印台可以看見打印成功打印到我們想要的東西:
初嘗runtime,若是有什麼表述不當的地方還請指出。後續將繼續更新runtime的學習。
戳我看代碼