1、什麼是runtime(也就是所謂的「運行時」,由於是在運行時實現的。) html
1.runtime是一套底層的c語言API(包括不少強大實用的c語言類型,c語言函數); [runtime運行系統] git
2.實際上,平時咱們編寫的oc代碼,底層都是基於runtime實現的; [OC語言的動態性] github
運行時系統 (runtime system),對於C語言,函數的調用在編譯的時候會決定調用哪一個函數。對於OC的函數,屬於動態調用過程,在編譯的時候並不能決定真正調用哪一個函數,只有在真正運行的時候纔會根據函數的名稱找到對應的函數來調用。 runtime就是OC辛苦的幕後工做人員。(編譯器會自動幫助咱們編譯成runtime代碼。)函數
動態特性,使得它在語言層面上支持程序的可擴展性。只有在程序運行時,纔會去肯定對象的類型,並調用類與對象相應的方法。利用runtime機制讓咱們能夠在程序運行時動態修改類的具體實現、包括類中的全部私有屬性、方法。這也是本文runtime例子的出發點。學習
咱們所敲入的代碼轉化爲運行時的runtime函數代碼,最終在程序運行時轉成了底層的runtime的c語言代碼 ;atom
舉例:spa
當某個對象使用語法[receiver message]來調用某個方法時,其實[receiver message]被編譯器轉化爲:指針
id objc_msgSend ( id self, SEL op, ... );
也就是說,咱們平時編寫的oc代碼,方法調用的本質,就是在編譯階段,編譯器轉化爲向對象發送消息。code
Demo地址:https://github.com/mengzhihun6orm
2、runtime的幾種使用方法
咱們經過繼承於NSObject的person類,來對runtime進行學習。
本文共有6個關於runtime機制方法的小例子,分別是:
(我的習慣,喜歡爲6個例子添加按鈕各自的行爲方法,並分別執行相應的行爲,以此看清各個runtime函數的具體功能所帶來的效果。)
首先,建立新的項目,並在項目中新建一個普通的OC類:person類(繼承於NSObject),爲了不後面與其餘方法函數搞混,咱們把完整的person類編寫齊全,用於後面使用runtime的幾種方法:
①JGPerson.h以下:
#import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @interface JGPerson : NSObject @property (nonatomic, copy) NSString *name; //屬性變量 @property (nonatomic, assign) int age; -(void)func1; -(void)func2; @end NS_ASSUME_NONNULL_END
②JGPerson.m以下:
#import "JGPerson.h" @implementation JGPerson { double _score; //實例變量 } - (instancetype)init { if (self = [super init]) { self.name = @"小明"; self.age = 22; _score = 66.5; } return self; } //person的2個普通方法 -(void)func1 { NSLog(@"執行func1方法。"); } -(void)func2 { NSLog(@"執行func2方法。"); } - (NSString *)description { return [NSString stringWithFormat:@" %d歲的 %@考了%.1f分",self.age, self.name, _score]; } @end
從person類的描述中,咱們能夠看到person類含有一個可供外類使用的共有屬性age,以及一個外界不能夠訪問私有屬性name,可是,有木有想過,其實在外類,name也是能夠訪問的。OC裏面,經過runtime系統,蘋果容許不受這些私有屬性的限制,對私有屬性私有方法等進行訪問、添加、修改、甚至替換系統的方法。
那麼,爲項目的故事板添加6個按鈕;
在使用runtime的地方,咱們都須要包含頭文件: (本文幾個例子中,都只須要在ViewController.m中包含.)
#import <objc/runtime.h>
1.獲取person類的全部變量
將第一個按鈕關聯到ViewController.h,添加行爲並命名其方法爲:「getAllVariable」:
/*1.獲取person全部的成員變量*/ - (IBAction)Func1:(id)sender {}
在ViewController.m中的實現以下:
/*1.獲取person全部的成員變量*/ - (IBAction)Func1:(id)sender { unsigned int count = 0; //獲取類的一個包含全部變量的列表,IVar是runtime聲明的一個宏,是實例變量的意思. Ivar *allIvariables = class_copyIvarList([JGPerson class], &count); for (int i = 0; i < count; i++) { //遍歷每個變量,包含名稱和類型(此處沒有星號「*」) Ivar ivar = allIvariables[i]; //獲取成員變量名稱 const char *VariableName = ivar_getName(ivar); //獲取成員變量類型 const char *VariableType = ivar_getTypeEncoding(ivar); NSLog(@"名稱:%s => 類型:%s",VariableName, VariableType); } }
獲得的輸出以下:(i表示類型爲int d表示類型爲double)
2019-04-03 15:59:17.415756+0800 RuntimeDemo[20661:192149] 名稱:_score => 類型:d 2019-04-03 15:59:17.415868+0800 RuntimeDemo[20661:192149] 名稱:_age => 類型:i 2019-04-03 15:59:17.415939+0800 RuntimeDemo[20661:192149] 名稱:_name => 類型:@"NSString"
分析:Ivar,一個指向objc_ivar結構體指針,包含了變量名、變量類型等信息。
能夠看到,私有屬性name可以訪問到了。 在有些項目中,爲了對某些私有屬性進行隱藏,某些.h文件中沒有出現相應的顯式建立,而是如上面的person類中,在.m中進行私有建立,可是咱們能夠經過runtime這個有效的方法,訪問到全部包括這些隱藏的私有變量。
拓展:
①class_copyIvarList可以獲取一個含有類中全部成員變量的列表,列表中包括屬性變量和實例變量。須要注意的是,若是如本例中,age返回的是"_age",可是若是在person.m中加入:
@synthesize age;
那麼控制檯第二行返回的是"(Name: age) ----- (Type:i) ;"(由於@property是生成了"_age",而@synthesize是執行了"@synthesize age = _age;",關於OC屬性變量與實例變量的區別、@property、@synthesize的做用等具體的知識,有興趣的童鞋能夠自行了解。)
②若是單單須要獲取屬性列表的話,可使用函數:class_copyPropertyList();只是返回的屬性變量僅僅是「age」,作爲實例變量的name是不被獲取的。
而class_copyIvarList()函數則可以返回實例變量和屬性變量的全部成員變量。
2.獲取person類的全部方法
將第二個按鈕關聯到ViewController.h,添加行爲並命名其方法爲:「getAllMethod」:
/*2.獲取person全部方法*/ - (IBAction)Func2:(id)sender {}
在ViewController.m中的實現以下:
/*2.獲取person全部方法*/ - (IBAction)Func2:(id)sender { unsigned int count = 0; //獲取方法列表,全部在.m文件顯式實現的方法都會被找到,包括setter+getter方法; Method *allMethods = class_copyMethodList([JGPerson class], &count); for (int i = 0; i < count; i++) { //Method,爲runtime聲明的一個宏,表示對一個方法的描述 Method md = allMethods[i]; //獲取SEL:SEL類型,即獲取方法選擇器@selector() SEL sel = method_getName(md); //獲得sel的方法名:以字符串格式獲取sel的name,也即@selector()中的方法名稱 const char *methodName = sel_getName(sel); NSLog(@"Method%d:%s",i+1, methodName); } }
控制檯輸出:
2019-04-03 16:09:18.052985+0800 RuntimeDemo[20818:197062] Method1:eat 2019-04-03 16:09:18.053088+0800 RuntimeDemo[20818:197062] Method2:.cxx_destruct 2019-04-03 16:09:18.053154+0800 RuntimeDemo[20818:197062] Method3:description 2019-04-03 16:09:18.053212+0800 RuntimeDemo[20818:197062] Method4:name 2019-04-03 16:09:18.053273+0800 RuntimeDemo[20818:197062] Method5:setName: 2019-04-03 16:09:18.053333+0800 RuntimeDemo[20818:197062] Method6:init 2019-04-03 16:09:18.053389+0800 RuntimeDemo[20818:197062] Method7:run 2019-04-03 16:09:18.053443+0800 RuntimeDemo[20818:197062] Method8:setAge: 2019-04-03 16:09:18.053502+0800 RuntimeDemo[20818:197062] Method9:age
控制檯輸出了包括set和get等方法名稱。【備註:.cxx_destruct方法是關於系統自動內存釋放工做的一個隱藏的函數,當ARC下,且本類擁有實例變量時,纔會出現;】
分析:Method是一個指向objc_method結構體指針,表示對類中的某個方法的描述。在API中的定義:
typedef struct objc_method *Method;
而objc_method結構體以下
:
truct objc_method { SEL method_name OBJC2_UNAVAILABLE; char *method_types OBJC2_UNAVAILABLE; IMP method_imp OBJC2_UNAVAILABLE; }
方法選擇器@selector(),類型爲SEL
。
相同名字的方法下,即便在不一樣類中定義,它們的方法選擇器也相同。char
指針,存儲着方法的參數類型和返回值類型。method_imp:指向
方法的具體實現的指針,數據類型爲IMP,本質上是一個函數指針。 在第五個按鈕行爲「增長一個方法」部分會提到。SEL:數據類型,表示方法選擇器,能夠理解爲對方法的一種包裝。在每一個方法都有一個與之對應的SEL類型的數據,根據一個SEL數據「@selector(方法名)」就能夠找到對應的方法地址,進而調用方法。
所以能夠經過:獲取Method結構體->獲得SEL選擇器名稱->獲得對應的方法名,這樣的方式,便於認識OC中關於方法的定義。
3.改變person對象的私有變量name的值.
將第三個按鈕關聯到ViewController.h,添加行爲並命名其方法爲:「changeVariable」:
/*3.改變person的name變量屬性*/ - (IBAction)Func3:(id)sender {}
在ViewController.m中建立一個person對象,記得初始化
@implementation ViewController { JGPerson *_person; } - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. _person = [[JGPerson alloc] init]; }
在ViewController.m中的實現以下:
/*3.改變person的name變量屬性*/ - (IBAction)Func3:(id)sender { NSLog(@"改變以前的Penson:%@",_person); unsigned int count = 0; Ivar *allList = class_copyIvarList([JGPerson class], &count); Ivar ivar = allList[2]; //從第一個例子getAllVariable中輸出的控制檯信息,咱們能夠看到name爲第2個實例屬性; // object_setIvar(<#id _Nullable obj#>, <#Ivar _Nonnull ivar#>, <#id _Nullable value#>) object_setIvar(_person, ivar, @"小剛"); //name屬性小明被強制改成小剛。 NSLog(@"改變以後的Penson:%@",_person); }
控制檯輸出:
2019-04-03 16:20:51.312155+0800 RuntimeDemo[20975:202727] 改變以前的Penson: 22歲的 小明考了66.5分 2019-04-03 16:20:51.312235+0800 RuntimeDemo[20975:202727] 改變以後的Penson: 22歲的 小剛考了66.5分
4.爲person的category類增長一個新屬性:
如何在不改動某個類的前提下,添加一個新的屬性呢?
答:能夠利用runtime爲分類添加新屬性。
在iOS中,category也就是分類,是不能夠爲本類添加新的屬性的,可是在runtime中咱們可使用對象關聯,爲person類進行分類的新屬性建立:
分類.h文件
#import "JGPerson.h" NS_ASSUME_NONNULL_BEGIN @interface JGPerson (JGCategory) @property (nonatomic, assign) float height; //新屬性 @end NS_ASSUME_NONNULL_END
分類.m文件
#import "JGPerson+JGCategory.h" #import <objc/runtime.h> const char *str = "personKey"; //作爲key,字符常量 必須是C語言字符串; @implementation JGPerson (JGCategory) - (void)setHeight:(float)height { NSNumber *num = [NSNumber numberWithFloat:height]; /* 第一個參數是須要添加屬性的對象; 第二個參數是屬性的key; 第三個參數是屬性的值,類型必須爲id,因此此處height先轉爲NSNumber類型; 第四個參數是使用策略,是一個枚舉值,相似@property屬性建立時設置的關鍵字,可從命名看出各枚舉的意義; objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy); */ objc_setAssociatedObject(self, str, num, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (float)height { NSNumber *number = objc_getAssociatedObject(self, str); return [number floatValue]; } @end
接下來,咱們能夠在ViewController.m中對person的一個對象進行height的訪問了,
將第四個按鈕關聯到ViewController.h添加行爲並命名其方法爲:「addVariable」:
/* 4.添加新的屬性*/ - (IBAction)Func4:(id)sender { /* 如何在不改動某個類的前提下,添加一個新的屬性呢? 答:能夠利用runtime爲分類添加新屬性。 */ //給新屬性height賦值 _person.height= 168; //給新屬性height賦值 //訪問新屬性值 NSLog(@"%f",[_person height]); }
點擊按鈕4、再點擊按鈕1、二獲取類的屬性、方法。
2019-04-03 17:27:02.265178+0800 RuntimeDemo[21710:227208] 168.000000 2019-04-03 17:27:09.185091+0800 RuntimeDemo[21710:227208] 名稱:_score => 類型:d 2019-04-03 17:27:09.185190+0800 RuntimeDemo[21710:227208] 名稱:_age => 類型:i 2019-04-03 17:27:09.185265+0800 RuntimeDemo[21710:227208] 名稱:_name => 類型:@"NSString" 2019-04-03 17:27:11.165884+0800 RuntimeDemo[21710:227208] Method1:func1 2019-04-03 17:27:11.165984+0800 RuntimeDemo[21710:227208] Method2:func2 2019-04-03 17:27:11.166057+0800 RuntimeDemo[21710:227208] Method3:.cxx_destruct 2019-04-03 17:27:11.166123+0800 RuntimeDemo[21710:227208] Method4:description 2019-04-03 17:27:11.166176+0800 RuntimeDemo[21710:227208] Method5:name 2019-04-03 17:27:11.166228+0800 RuntimeDemo[21710:227208] Method6:setName: 2019-04-03 17:27:11.166293+0800 RuntimeDemo[21710:227208] Method7:init 2019-04-03 17:27:11.166460+0800 RuntimeDemo[21710:227208] Method8:height 2019-04-03 17:27:11.166551+0800 RuntimeDemo[21710:227208] Method9:setHeight: 2019-04-03 17:27:11.166661+0800 RuntimeDemo[21710:227208] Method10:setAge: 2019-04-03 17:27:11.166771+0800 RuntimeDemo[21710:227208] Method11:age
分析:能夠看到分類的新屬性能夠在per對象中對新屬性height進行訪問賦值。
獲取到person類屬性時,依然沒有height的存在,可是卻有height和setHeight這兩個方法;由於在分類中,即便使用@property定義了,也只是生成set+get方法,而不會生成_變量名,分類中是不容許定義變量的。
使用runtime中objc_setAssociatedObject()和objc_getAssociatedObject()方法,本質上只是爲對象per添加了對height的屬性關聯,可是達到了新屬性的做用;
使用場景:假設imageCategory是UIImage類的分類,在實際開發中,咱們使用UIImage下載圖片或者操做過程須要增長一個URL保存一段地址,以備後期使用。這時能夠嘗試在分類中動態添加新屬性MyURL進行存儲。
5.爲person類添加一個新方法
將第五個按鈕關聯到ViewController.h,添加行爲並命名其方法爲:「addMethod」:
/*5.添加新的方法試試(這種方法等價於對Father類添加Category對方法進行擴展):*/ - (IBAction)Func5:(id)sender {}
在ViewController.m中的實現以下:
/*5.添加新的方法試試(這種方法等價於對Father類添加Category對方法進行擴展):*/ - (IBAction)Func5:(id)sender { class_addMethod([_person class], @selector(NewMethod),(IMP)myAddingFunction, 0); [_person performSelector:@selector(NewMethod)]; } //具體的實現(方法的內部都默認包含兩個參數Class類和SEL方法,被稱爲隱式參數。) int myAddingFunction(id self, SEL _cmd) { NSLog(@"已新增方法:NewMethod"); return 1; }
控制檯輸出:
2019-04-03 16:42:14.522734+0800 RuntimeDemo[21232:211364] 已新增方法:NewMethod
6.交換person類的2個方法的功能
將第六個按鈕關聯到ViewController.h,添加行爲並命名其方法爲:「replaceMethod」:
/* 6.交換兩種方法以後(功能對調*/ - (IBAction)Func6:(id)sender {}
在ViewController.m中的實現以下:
/* 6.交換兩種方法以後(功能對調*/ - (IBAction)Func6:(id)sender { Method method1 = class_getInstanceMethod([JGPerson class], @selector(func1)); Method method2 = class_getInstanceMethod([JGPerson class], @selector(func2)); method_exchangeImplementations(method1, method2); [_person func1]; }
控制檯輸出:
2019-04-03 16:48:14.902453+0800 RuntimeDemo[21328:214212] 執行func2方法。
交換方法的使用場景:項目中的某個功能,在項目中須要屢次被引用,當項目的需求發生改變時,要使用另外一種功能代替這個功能,且要求不改變舊的項目(也就是不改變原來方法實現的前提下)。那麼,咱們能夠在分類中,再寫一個新的方法(符合新的需求的方法),而後交換兩個方法的實現。這樣,在不改變項目的代碼,而只是增長了新的代碼的狀況下,就完成了項目的改進,很好地體現了該項目的封裝性與利用率。
注:交換兩個方法的實現通常寫在類的load方法裏面,由於load方法會在程序運行前加載一次。
參考:https://www.cnblogs.com/azuo/p/5505782.html