Runtime的基本運用

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函數代碼,最終在程序運行時轉成了底層的runtimec語言代碼 ;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機制方法的小例子,分別是:

  1. 獲取person類的全部變量;
  2. 獲取person類的全部方法;
  3. 改變person類的私有變量name的值;
  4. 爲person的category類增長一個新屬性;
  5. person類添加一個方法;
  6. 交換person類的2個方法的功能;

(我的習慣,喜歡爲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;
}  
  • method_name :方法選擇器@selector(),類型爲SEL。 相同名字的方法下,即便在不一樣類中定義,它們的方法選擇器也相同。
  • method_types:方法類型,是個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

相關文章
相關標籤/搜索