一、什么是 Runtime(也就是所謂的“運行時”欢伏,因為是在運行時實現(xiàn)的曲聂。)
- 1.Runtime 是一套底層的c語言API(包括很多強大實用的c語言類型,c語言函數(shù)); [runtime運行系統(tǒng)]
- 2.實際上,平時我們編寫的oc代碼,底層都是基于 Runtime 實現(xiàn)的; [OC語言的動態(tài)性]
運行時系統(tǒng) (runtime system),對于C語言访惜,使用“靜態(tài)綁定”,函數(shù)的調(diào)用在編譯的時候就會決定運行時調(diào)用哪個函數(shù)腻扇。對于OC的函數(shù)债热,屬于“動態(tài)調(diào)用”過程,在編譯的時候并不能決定真正調(diào)用哪個函數(shù)幼苛,只有在真正運行的時候才會根據(jù)函數(shù)的名稱找到對應的函數(shù)來調(diào)用窒篱,甚至可以在運行時改變方法的調(diào)用。Runtime就是OC辛苦的幕后工作人員舶沿。(編譯器會自動幫助我們編譯成 Runtime 代碼墙杯。)
動態(tài)特性:使得它在語言層面上支持程序的可擴展性。只有在程序運行時括荡,才會去確定對象的類型畸冲,并調(diào)用類與對象相應的方法。利用runtime機制讓我們可以在程序運行時動態(tài)修改類的具體實現(xiàn)恕沫、包括類中的所有私有屬性婶溯、方法偷霉。這也是本文runtime例子的出發(fā)點类少。
我們所敲入的代碼轉化為運行時的runtime函數(shù)代碼硫狞,最終在程序運行時轉成了底層的runtime的c語言代碼;
舉例:
當某個對象使用語法[receiver message]來調(diào)用某個方法時残吩,其實[receiver message]被編譯器轉化為:
id objc_msgSend ( id self, SEL op, ... );
也就是說,我們平時編寫的oc代碼泣侮,方法調(diào)用的本質(zhì)活尊,就是在編譯階段,編譯器轉化為向對象發(fā)送消息深胳。
【本次開發(fā)環(huán)境: Xcode:7.2 iOS Simulator:iphone6 By: 啊左
本文Demo下載鏈接:runtime-Demo】
二舞终、runtime的幾種使用方法
我們通過繼承于NSObject的person類权埠,來對runtime進行學習。
本文共有6個關于runtime機制方法的小例子龙屉,分別是:
- 1.獲取person類的所有變量;
- 2.獲取person類的所有方法唆垃;
- 3.改變person類的私有變量name的值辕万;
- 4.為person的category類增加一個新屬性渐尿;
- 5.為person類添加一個方法砖茸;
- 6.交換person類的2個方法的功能凉夯;
(個人習慣劲够,喜歡為6個例子添加按鈕各自的行為方法再沧,并分別執(zhí)行相應的行為炒瘸,以此看清各個runtime函數(shù)的具體功能所帶來的效果顷扩。)
首先隘截,創(chuàng)建新的項目婶芭,并在項目中新建一個普通的OC類:person類(繼承于NSObject)犀农,為了避免后面與其他方法函數(shù)搞混呵哨,我們把完整的person類編寫齊全孟害,用于后面使用runtime的幾種方法:
①person.h如下:
#import <Foundation/Foundation.h>
@interface person : NSObject
@property (nonatomic,assign)int age; //屬性變量
-(void)func1;
-(void)func2;
@end
②person.m如下:
#import "person.h"
@implementation person{
NSString *name; //實例變量
}
//初始化person屬性
-(instancetype)init{
self = [super init];
if(self) {
name = @"Tom";
self.age = 12;
}
return self;
}
//person的2個普通方法
-(void)func1{
NSLog(@"執(zhí)行func1方法挨务。");
}-(void)func2{
NSLog(@"執(zhí)行func2方法谎柄。");
}
//輸出person對象時的方法:
-(NSString *)description{
return [NSString stringWithFormat:@"name:%@ age:%d",name,self.age];
}
@end
從person類的描述中,我們可以看到person類含有一個可供外類使用的共有屬性age绒障,以及一個外界不可以訪問私有屬性name,但是庐镐,有木有想過变逃,其實在外類,name也是可以訪問的名眉。OC里面损拢,通過runtime系統(tǒng)荆姆,蘋果允許不受這些私有屬性的限制胆筒,對私有屬性私有方法等進行訪問决乎、添加派桩、修改铆惑、甚至替換系統(tǒng)的方法员魏。
那么,為項目的故事板添加6個按鈕碌补;
在使用runtime的地方厦章,我們都需要包含頭文件:
#import <objc/runtime.h> //(在需要使用runtime的實現(xiàn)文件.m中包含即可.)
1.獲取person類的所有變量
將第一個按鈕關聯(lián)到ViewController.h袜啃,添加行為并命名其方法為:“getAllVariable”:
- (IBAction)getAllVariable:(UIButton *)sender; //獲取所有變量
在ViewController.m中的實現(xiàn)如下:
/*1.獲取person所有的成員變量*/
- (IBAction)getAllVariable:(UIButton *)sender {
unsigned int count = 0;
//獲取類的一個包含所有變量的列表群发,IVar是runtime聲明的一個宏熟妓,是實例變量的意思.
Ivar *allVariables = class_copyIvarList([person class], &count);
for(int i = 0;i<count;i++)
{
//遍歷每一個變量浪蹂,包括名稱和類型(此處沒有星號"*")
Ivar ivar = allVariables[i];
const char *Variablename = ivar_getName(ivar); //獲取成員變量名稱
const char *VariableType = ivar_getTypeEncoding(ivar); //獲取成員變量類型
NSLog(@"(Name: %s) ----- (Type:%s)",Variablename,VariableType);
}
}
點擊按鈕后告材,得到的輸出如下:(i表示類型為int)
2016-05-18 17:17:10.502 runtime運行時[10164:452725] (Name: name) ----- (Type:@"NSString")
2016-05-18 17:17:10.503 runtime運行時[10164:452725] (Name: _age) ----- (Type:i)
分析:Ivar坤次,一個指向objc_ivar結構體指針,包含了變量名、變量類型等信息斥赋。
可以看到缰猴,私有屬性name能夠訪問到了。 在有些項目中疤剑,為了對某些私有屬性進行隱藏滑绒,某些.h文件中沒有出現(xiàn)相應的顯式創(chuàng)建闷堡,而是如上面的person類中,在.m中進行私有創(chuàng)建,但是我們可以通過runtime這個有效的方法,訪問到所有包括這些隱藏的私有變量。
拓展:
① class_copyIvarList能夠獲取一個含有類中所有成員變量的列表,列表中包括屬性變量和實例變量。需要注意的是,如果如本例中,age返回的是"_age",但是如果在person.m中加入:@synthesize age;
那么控制臺第二行返回的是"(Name: age) ----- (Type:i) ;"
(因為@property是生成了"_age"箭昵,而@synthesize是執(zhí)行了"@synthesize age = _age;",關于OC屬性變量與實例變量的區(qū)別、@property蝇棉、@synthesize的作用等具體的知識板辽,有興趣的童鞋可以自行了解邑跪。)
②如果單單需要獲取屬性列表的話,可以使用函數(shù):class_copyPropertyList();只是返回的屬性變量僅僅是“age”,做為實例變量的name是不被獲取的视粮。
而class_copyIvarList()函數(shù)則能夠返回實例變量和屬性變量的所有成員變量。
2.獲取person類的所有方法
將第二個按鈕關聯(lián)到ViewController.h瞬铸,添加行為并命名其方法為:“getAllMethod”:
- (IBAction)getAllMethod:(UIButton *)sender; //獲取所有方法
在ViewController.m中的實現(xiàn)如下:
/*2.獲取person所有方法*/
- (IBAction)getAllMethod:(UIButton *)sender {
unsigned int count;
//獲取方法列表,所有在.m文件顯式實現(xiàn)的方法都會被找到,包括setter+getter方法菊值;
Method *allMethods = class_copyMethodList([person 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:%s)",methodname);
}
}
點擊按鈕后旬痹,控制臺輸出:
2016-05-19 17:05:19.880 runtime運行時[14054:678124] (Method:func1)
2016-05-19 17:05:19.881 runtime運行時[14054:678124] (Method:func2)
2016-05-19 17:05:19.881 runtime運行時[14054:678124] (Method:setAge:)
2016-05-19 17:05:19.881 runtime運行時[14054:678124] (Method:age)
2016-05-19 17:05:19.881 runtime運行時[14054:678124] (Method:.cxx_destruct)
2016-05-19 17:05:19.882 runtime運行時[14054:678124] (Method:description)
2016-05-19 17:05:19.882 runtime運行時[14054:678124] (Method:init)
控制臺輸出了包括set和get等方法名稱〗谠常【備注:.cxx_destruct方法是關于系統(tǒng)自動內(nèi)存釋放工作的一個隱藏的函數(shù)滨嘱,當ARC下囊扳,且本類擁有實例變量時弧轧,才會出現(xiàn);】
分析: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;
}
- method_name :方法選擇器@selector()捡偏,類型為SEL。 相同名字的方法下峡迷,即使在不同類中定義银伟,它們的方法選擇器也相同。
- method_types:方法類型绘搞,是個char指針彤避,存儲著方法的參數(shù)類型和返回值類型。
- method_imp:指向方法的具體實現(xiàn)的指針夯辖,數(shù)據(jù)類型為IMP琉预,本質(zhì)上是一個函數(shù)指針。 在第五個按鈕行為“增加一個方法”部分會提到蒿褂。
SEL:數(shù)據(jù)類型圆米,表示方法選擇器,可以理解為對方法的一種包裝啄栓。在每個方法都有一個與之對應的SEL類型的數(shù)據(jù)娄帖,根據(jù)一個SEL數(shù)據(jù)“@selector(方法名)”就可以找到對應的方法地址,進而調(diào)用方法昙楚。
因此可以通過:獲取 Method結構體->得到SEL選擇器名稱->得到對應的方法名 近速,這樣的方式,便于認識OC中關于方法的定義桂肌。
3.改變person對象的私有變量name的值.
將第三個按鈕關聯(lián)到ViewController.h数焊,添加行為并命名其方法為:“changeVariable”:
- (IBAction)changeVariable:(UIButton *)sender;//改變其中name變量
在ViewController.m中創(chuàng)建一個person對象,記得初始化
@implementation ViewController{
person *per; //創(chuàng)建一個person實例
}
- (void)viewDidLoad {
[super viewDidLoad];
per = [[person alloc]init]; //記得要初始化...不然后果自己嘗試下
}
在ViewController.m中的實現(xiàn)如下:
/*3.改變person的name變量屬性*/
- (IBAction)changeVariable:(UIButton *)sender {
NSLog(@"改變前的person:%@",per);
unsigned int count = 0;
Ivar *allList = class_copyIvarList([person class], &count);
Ivar ivv = allList[0]; //從第一個例子getAllVariable中輸出的控制臺信息崎场,我們可以看到name為第一個實例屬性佩耳。
object_setIvar(per, ivv, @"Mike"); //name屬性Tom被強制改為Mike。
NSLog(@"改變之后的person:%@",per);
}
點擊按鈕后谭跨,控制臺輸出:
2016-05-19 22:45:05.125 runtime運行時[1957:34730] 改變前的person:name:Tom age:12
2016-05-19 22:45:05.126 runtime運行時[1957:34730] 改變之后的person:name:Mike age:12
4.為person的category類增加一個新屬性:
如何在不改動某個類的前提下干厚,添加一個新的屬性呢李滴?
答:可以利用runtime為分類添加新屬性。
在iOS中蛮瞄,category所坯,也就是分類,是不可以為本類添加新的屬性的挂捅,但是在runtime中我們可以使用對象關聯(lián)芹助,為person類進行分類的新屬性創(chuàng)建:
(其實實際上并不會創(chuàng)建實例變量,只是通過 Runtime 為該類提供屬性的setter/getter 方法)
①新建一個新的OC類:
命名為:PersonCategory 闲先,點擊next:
②在出現(xiàn)的新類“person+PersonCategory.h”中状土,添加“height”:
#import "person.h"
@interface person (PersonCategory)
@property (nonatomic,assign)float height; //新屬性@end
“person+PersonCategory.m”類的代碼如下:
#import "person+PersonCategory.h"
#import <objc/runtime.h> //runtime API的使用需要包含此頭文件
const char * str = "myKey"; //做為key,字符常量 必須是C語言字符串伺糠;
@implementation person (PersonCategory)
-(void)setHeight:(float)height{
NSNumber *num = [NSNumber numberWithFloat:height];
/*
第一個參數(shù)是需要添加屬性的對象蒙谓;
第二個參數(shù)是屬性的key;
第三個參數(shù)是屬性的值,類型必須為id,所以此處height先轉為NSNumber類型训桶;
第四個參數(shù)是使用策略累驮,是一個枚舉值,類似@property屬性創(chuàng)建時設置的關鍵字舵揭,可從命名看出各枚舉的意義谤专;
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的訪問了琉朽,
將第四個按鈕關聯(lián)到ViewController.h添加行為并命名其方法為:“addVariable:”(記得:#import "person+PersonCategory.h")
- (IBAction)addVariable:(UIButton *)sender;
在ViewController.m中的實現(xiàn)如下:
/* 4.添加新的屬性*/
- (IBAction)addVariable:(UIButton *)sender {
per.height = 12; //給新屬性height賦值
NSLog(@"%f",[per height]); //訪問新屬性值
}
點擊按鈕四毒租、再點擊按鈕一、二獲取類的屬性箱叁、方法。
2016-05-20 15:39:54.432 runtime運行時[4605:178974] 12.000000
2016-05-20 15:39:56.295 runtime運行時[4605:178974] (Name: name) ----- (Type:@"NSString")
2016-05-20 15:39:56.296 runtime運行時[4605:178974] (Name: _age) ----- (Type:i)
2016-05-20 15:39:57.195 runtime運行時[4605:178974] (Method:func1)
2016-05-20 15:39:57.196 runtime運行時[4605:178974] (Method:func2)
2016-05-20 15:39:57.196 runtime運行時[4605:178974] (Method:setAge:)
2016-05-20 15:39:57.196 runtime運行時[4605:178974] (Method:age)
2016-05-20 15:39:57.196 runtime運行時[4605:178974] (Method:.cxx_destruct)
2016-05-20 15:39:57.197 runtime運行時[4605:178974] (Method:description)
2016-05-20 15:39:57.197 runtime運行時[4605:178974] (Method:init)
2016-05-20 15:39:57.197 runtime運行時[4605:178974] (Method:height)
2016-05-20 15:39:57.197 runtime運行時[4605:178974] (Method:setHeight:)
分析:可以看到分類的新屬性可以在per對象中對新屬性height進行訪問賦值惕医。
獲取到 person 類屬性時耕漱,依然沒有 height 的存在,但是卻有 height 和
setHeight 這兩個方法抬伺;因為在分類中螟够,即使使用 @property 定義了,也只是生成 set+get 方法峡钓,而不會生成_變量名妓笙,分類中是不允許定義變量的。
使用 runtime 中 objc_setAssociatedObject() 和
objc_getAssociatedObject() 方法能岩,本質(zhì)上只是為對象per添加了對height的屬性關聯(lián)寞宫,但是達到了新屬性的作用;
使用場景:假設 imageCategory 是 UIImage 類的分類拉鹃,在實際開發(fā)中辈赋,我們使用 UIImage 下載圖片或者操作過程需要增加一個 URL 保存一段地址鲫忍,以備后期使用。這時可以嘗試在分類中動態(tài)添加新屬性 MyURL 進行存儲钥屈。
5.為 person 類添加一個新方法悟民;
將第五個按鈕關聯(lián)到ViewController.h,添加行為并命名其方法為:“addMethod”:
- (IBAction)addMethod:(UIButton *)sender;
在ViewController.m中的實現(xiàn)如下:
/*5.添加新的方法試試(這種方法等價于對 Father 類添加 Category 對方法進行擴展):*/
- (IBAction)addMethod:(UIButton *)sender {
/* 動態(tài)添加方法:
第一個參數(shù)表示Class cls 類型篷就;
第二個參數(shù)表示待調(diào)用的方法名稱射亏;
第三個參數(shù)(IMP)myAddingFunction,IMP一個函數(shù)指針竭业,這里表示指定具體實現(xiàn)方法myAddingFunction鸦泳;
第四個參數(shù)表方法的參數(shù),0代表沒有參數(shù)永品;
*/
class_addMethod([per class], @selector(NewMethod), (IMP)myAddingFunction, 0);
//調(diào)用方法 【如果使用[per NewMethod]調(diào)用方法做鹰,在ARC下會報“no visible @interface"錯誤】
[per performSelector:@selector(NewMethod)];
}
//具體的實現(xiàn)(方法的內(nèi)部都默認包含兩個參數(shù)Class類和SEL方法,被稱為隱式參數(shù)鼎姐。)
int myAddingFunction(id self, SEL _cmd){
NSLog(@"已新增方法:NewMethod");
return 1;
}
點擊按鈕后钾麸,控制臺輸出:
2016-05-20 14:08:55.822 runtime運行時[1957:34730] 已新增方法:NewMethod
6.交換person類的2個方法的功能:
將第六個按鈕關聯(lián)到ViewController.h,添加行為并命名其方法為:“replaceMethod”:
- (IBAction)replaceMethod:(UIButton *)sender;
在ViewController.m中的實現(xiàn)如下:
/* 6.交換兩種方法之后(功能對調(diào))炕桨,可以試試讓蘋果亂套... */
- (IBAction)replaceMethod:(UIButton *)sender {
Method method1 = class_getInstanceMethod([person class], @selector(func1));
Method method2 = class_getInstanceMethod([person class], @selector(func2));
//交換方法
method_exchangeImplementations(method1, method2);
[per func1]; //輸出交換后的效果饭尝,需要對比的可以嘗試下交換前運行func1;
}
點擊按鈕后献宫,控制臺輸出:
2016-05-20 14:11:57.381 runtime運行時[1957:34730] 執(zhí)行func2方法钥平。
交換方法的使用場景:項目中的某個功能,在項目中需要多次被引用姊途,當項目的需求發(fā)生改變時涉瘾,要使用另一種功能代替這個功能,且要求不改變舊的項目(也就是不改變原來方法實現(xiàn)的前提下)捷兰。那么立叛,我們可以在分類中,再寫一個新的方法(符合新的需求的方法)贡茅,然后交換兩個方法的實現(xiàn)秘蛇。這樣,在不改變項目的代碼顶考,而只是增加了新的代碼的情況下赁还,就完成了項目的改進,很好地體現(xiàn)了該項目的封裝性與利用率驹沿。
注:交換兩個方法的實現(xiàn)一般寫在類的load方法里面艘策,因為load方法會在程序運行前加載一次。
(轉載請標明原文出處甚负,謝謝支持 ~ - ~)
? by:啊左~