Runtime的幾個小例子(含Demo)


一、什么是runtime(也就是所謂的“運行時”,因為是在運行時實現的。) 

          1.runtime是一套底層的c語言API(包括很多強大實用的c語言類型,c語言函數);  [runtime運行系統]  

        2.實際上,平時我們編寫的oc代碼,底層都是基於runtime實現的;                             [OC語言的動態性] 

 運行時系統 (runtime system),對於C語言,函數的調用在編譯的時候會決定調用哪個函數。對於OC的函數,屬於動態調用過程,在編譯的時候並不能決定真正調用哪個函數,只有在真正運行的時候才會根據函數的名稱找到對應的函數來調用。 runtime就是OC辛苦的幕后工作人員。(編譯器會自動幫助我們編譯成runtime代碼。

動態特性,使得它在語言層面上支持程序的可擴展性。只有在程序運行時,才會去確定對象的類型,並調用類與對象相應的方法。利用runtime機制讓我們可以在程序運行時動態修改類的具體實現、包括類中的所有私有屬性、方法。這也是本文runtime例子的出發點。

我們所敲入的代碼轉化為運行時的runtime函數代碼,最終在程序運行時轉成了底層的runtimec語言代碼 ;

舉例:

當某個對象使用語法[receiver message]來調用某個方法時,其實[receiver message]被編譯器轉化為:

id objc_msgSend ( id self, SEL op, ... );  

也就是說,我們平時編寫的oc代碼,方法調用的本質,就是在編譯階段,編譯器轉化為向對象發送消息。

本次開發環境: 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個例子添加按鈕各自的行為方法,並分別執行相應的行為,以此看清各個runtime函數的具體功能所帶來的效果。)

 

首先,創建新的項目,並在項目中新建一個普通的OC類:person類(繼承於NSObject),為了避免后面與其他方法函數搞混,我們把完整的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(@"執行func1方法。"); } -(void)func2 { NSLog(@"執行func2方法。"); } //輸出person對象時的方法:
-(NSString *)description { return [NSString stringWithFormat:@"name:%@ age:%d",name,self.age]; } @end

從person類的描述中,我們可以看到person類含有一個可供外類使用的共有屬性age,以及一個外界不可以訪問私有屬性name,但是,有木有想過,其實在外類,name也是可以訪問的。OC里面,通過runtime系統,蘋果允許不受這些私有屬性的限制,對私有屬性私有方法等進行訪問、添加、修改、甚至替換系統的方法。

那么,為項目的故事板添加6個按鈕;

在使用runtime的地方,我們都需要包含頭文件:    (本文幾個例子中,都只需要在ViewController.m中包含.)

#import <objc/runtime.h>

 

1.獲取person類的所有變量

將第一個按鈕關聯到ViewController.h,添加行為並命名其方法為:“getAllVariable”:

- (IBAction)getAllVariable:(UIButton *)sender;   //獲取所有變量

在ViewController.m中的實現如下:

/*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文件中沒有出現相應的顯式創建,而是如上面的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”:

- (IBAction)getAllMethod:(UIButton *)sender;   //獲取所有方法

在ViewController.m中的實現如下:

/*2.獲取person所有方法*/
- (IBAction)getAllMethod:(UIButton *)sender {
unsigned
int count;
//獲取方法列表,所有在.m文件顯式實現的方法都會被找到,包括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方法是關於系統自動內存釋放工作的一個隱藏的函數,當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; } 
  • method_name :方法選擇器@selector(),類型為SEL。 相同名字的方法下,即使在不同類中定義,它們的方法選擇器也相同。
  • method_types:方法類型,是個char指針,存儲着方法的參數類型和返回值類型。
  • method_imp:指向方法的具體實現的指針,數據類型為IMP,本質上是一個函數指針。 在第五個按鈕行為“增加一個方法”部分會提到。

SEL:數據類型,表示方法選擇器,可以理解為對方法的一種包裝。在每個方法都有一個與之對應的SEL類型的數據,根據一個SEL數據“@selector(方法名)”就可以找到對應的方法地址,進而調用方法。

因此可以通過:獲取Method結構體->得到SEL選擇器名稱->得到對應的方法名,這樣的方式,便於認識OC中關於方法的定義。

 

 

3.改變person對象的私有變量name的值.

將第三個按鈕關聯到ViewController.h,添加行為並命名其方法為:“changeVariable”:

- (IBAction)changeVariable:(UIButton *)sender;//改變其中name變量

在ViewController.m中創建一個person對象,記得初始化

@implementation ViewController
{
    person *per;  //創建一個person實例
}
- (void)viewDidLoad {
    [super viewDidLoad];
    per = [[person alloc]init]; //記得要初始化...不然后果自己嘗試下
}

 

在ViewController.m中的實現如下:

/*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中我們可以使用對象關聯,為person類進行分類的新屬性創建:

①新建一個新的OC類:

 

命名為:PersonCategory ,點擊next:

②在出現的新類“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]; 
/* 
第一個參數是需要添加屬性的對象; 
第二個參數是屬性的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”:(記得:#import "person+PersonCategory.h")

- (IBAction)addVariable:(UIButton *)sender;

在ViewController.m中的實現如下:

/* 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()方法,本質上只是為對象per添加了對height的屬性關聯,但是達到了新屬性的作用;

使用場景:假設imageCategory是UIImage類的分類,在實際開發中,我們使用UIImage下載圖片或者操作過程需要增加一個URL保存一段地址,以備后期使用。這時可以嘗試在分類中動態添加新屬性MyURL進行存儲。

 

5.為person類添加一個新方法;

將第五個按鈕關聯到ViewController.h,添加行為並命名其方法為:“addMethod”:

- (IBAction)addMethod:(UIButton *)sender;

在ViewController.m中的實現如下:

/*5.添加新的方法試試(這種方法等價於對Father類添加Category對方法進行擴展):*/
- (IBAction)addMethod:(UIButton *)sender { /* 動態添加方法: 第一個參數表示Class cls 類型; 第二個參數表示待調用的方法名稱; 第三個參數(IMP)myAddingFunction,IMP一個函數指針,這里表示指定具體實現方法myAddingFunction; 第四個參數表方法的參數,0代表沒有參數; */ class_addMethod([per class], @selector(NewMethod), (IMP)myAddingFunction, 0); //調用方法 【如果使用[per NewMethod]調用方法在ARC下會報“no visible @interface”錯誤】
 [per performSelector:@selector(NewMethod)]; } //具體的實現(方法的內部都默認包含兩個參數Class類和SEL方法,被稱為隱式參數。)
int myAddingFunction(id self, SEL _cmd) { NSLog(@"已新增方法:NewMethod"); return 1; }

點擊按鈕后,控制台輸出:

2016-05-20 14:08:55.822 runtime運行時[1957:34730] 已新增方法:NewMethod

 

 6.交換person類的2個方法的功能:

 將第六個按鈕關聯到ViewController.h,添加行為並命名其方法為:“replaceMethod”:

- (IBAction)replaceMethod:(UIButton *)sender;

在ViewController.m中的實現如下:

/* 6.交換兩種方法之后(功能對調),可以試試讓蘋果亂套... */
- (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] 執行func2方法。

交換方法的使用場景:項目中的某個功能,在項目中需要多次被引用,當項目的需求發生改變時,要使用另一種功能代替這個功能,且要求不改變舊的項目(也就是不改變原來方法實現的前提下)。那么,我們可以在分類中,再寫一個新的方法(符合新的需求的方法),然后交換兩個方法的實現。這樣,在不改變項目的代碼,而只是增加了新的代碼的情況下,就完成了項目的改進,很好地體現了該項目的封裝性與利用率。

注:交換兩個方法的實現一般寫在類的load方法里面,因為load方法會在程序運行前加載一次。

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM