iOS底層原理探究-Runtime

Runtime

0. 概述

Objective-C Runtime 使得C具備了面向對象的能力,在程序運行時建立,檢查,修改類,對象和它們的方法。Runtime 是 C和彙編寫的,這裏 www.opensource.apple.com/source/objc…能夠下載Apple維護的開源代碼,GUN也有一個開源的Runtime版本,它們都努力保持一致。 Apple官方的runtime編程指南

一、Runtime 函數

Runtime 系統是由一系列的函數和數據結構組成的公共接口動態共享庫,在/user/includeobjc 目錄下能夠看到頭文件,能夠用到其中一些函數經過C語言實現Objective-C中同樣的功能。蘋果官方文檔 developer.apple.com/library/mac… 裏有詳細的Runtime 函數文檔。


2. Class 和 NSObject 基礎數據結構

objc/runtime.h 中objc_class 結構體的定義以下:
html

struct objc_class {
Class isa OBJC_ISA_AVAILABILITY; //isa指針指向Meta Class,由於Objc的類的自己也是一個Object,爲了處理這個關係,runtime就創造了Meta Class,當給類發送[NSObject alloc]這樣消息時,其實是把這個消息發給了Class Object

#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE; // 父類
const char *name OBJC2_UNAVAILABLE; // 類名
long version OBJC2_UNAVAILABLE; // 類的版本信息,默認爲0
long info OBJC2_UNAVAILABLE; // 類信息,供運行期使用的一些位標識
long instance_size OBJC2_UNAVAILABLE; // 該類的實例變量大小
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; // 該類的成員變量鏈表
struct objc_method_list **methodLists OBJC2_UNAVAILABLE; // 方法定義的鏈表
struct objc_cache *cache OBJC2_UNAVAILABLE; // 方法緩存,對象接到一個消息會根據isa指針查找消息對象,這時會在methodLists中遍歷,若是cache了,經常使用的方法調用時就可以提升調用的效率。
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 協議鏈表
#endif

} OBJC2_UNAVAILABLE;複製代碼

objc_ivar_list 和 objc_method_list 的定義
編程

//objc_ivar_list結構體存儲objc_ivar數組列表
struct objc_ivar_list {
     int ivar_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
     int space OBJC2_UNAVAILABLE;
#endif
     /* variable length structure */
     struct objc_ivar ivar_list[1] OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;

//objc_method_list結構體存儲着objc_method的數組列表
struct objc_method_list {
     struct objc_method_list *obsolete OBJC2_UNAVAILABLE;
     int method_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
     int space OBJC2_UNAVAILABLE;
#endif
     /* variable length structure */
     struct objc_method method_list[1] OBJC2_UNAVAILABLE;
} 
複製代碼

2.2 objc_object 和 id

objc_object 是一個類的實例結構體,objc/objc.h 中 objc_object是一個類的實例結構體定義以下:
設計模式

struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};

typedef struct objc_object *id;複製代碼

向object發送消息時,Runtime 庫會根據object的isa指針找到這個實例object所屬於的類,而後在類的方法列表以及父類的方法列表中尋找對應的方法運行。id 是一個objc_object結構類型的指針,這個類型的對象能轉換成任何一種對象。
數組

2.3 objc_cache

objc_class 結構體中cache字段用於緩存調用過的method。cache指針指向objc_cache結構體,這個結構體定義以下:
緩存

struct objc_cache {
unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE; //指定分配緩存bucket的總數。runtime使用這個字段肯定線性查找數組的索引位置
unsigned int occupied OBJC2_UNAVAILABLE; //實際佔用緩存bucket總數
Method buckets[1] OBJC2_UNAVAILABLE; //指向Method數據結構指針的數組,這個數組的總數不能超過mask+1,可是指針是可能爲空的,這就表示緩存bucket沒有被佔用,數組會隨着時間增加。
};

複製代碼

2.4 Meta Class

meta class 是一個類對象的類,當向對象發送消息時,runtime 會在這個對象所屬類方法列表中查找發送消息對應的方法,但當向類發送消息時,runtime就會在這個類的meta class方法列表中查找。全部的meta class,包括Root class,SuperClass, SubClass的isa都指向Root clas的meta class,這樣可以造成一個閉環。

meta class 關係圖

3.Runtime 類與對象操做函數

Runtime 有不少函數能夠操做類和對象。類相關的是class爲前綴,對象相關相關的函數是 objc 或者 object 爲前綴。
bash

3.1類相關操做函數

name

// 獲取類的類名
const cahr * class_getName (Class cls);

複製代碼

super_class 和 meta_class

// 獲取類的父類
Class class_getSuperclass (Class cls);

// 判斷給定的Class是不是一個meta class
BOOL class_isMetaClass (Class cls);複製代碼

instance_size

// 獲取實例大小
size_t class_getInstanceSize (Class cls);複製代碼

3.2 成員變量(ivars)及屬性

3.2.1 成員變量操做函數

// 獲取類中指定名稱實例成員變量的信息
Ivar class_getInstanceVariable (Class cls, const char *name);

// 獲取類成員變量的信息
Ivar class_getClassVariable (Class cls, const char *name);

// 添加成員變量
BOOL class_addIvar (Class cls, const char *name, size_t size, uint8_t alignment, const char *types);  //只能向在runtime時建立的類添加成員變量,這個方法只能在objc_allocateClassPair函數與objc_registerClassPair之間調用。另外,這個類也不能是元類。

// 獲取整個成員變量列表
Ivar * class_copyIvarList (Class cls, unsigned int *outCount); // 必須使用free()來釋放這個數組
複製代碼

測試成員變量
數據結構

//成員變量
- (void)testIvar {
    BOOL isSuccessAddIvar = class_addIvar([NSString class], "_phone", sizeof(id), log2(sizeof(id)), "@");
    if (isSuccessAddIvar) {
        NSLog(@"Add Ivar success");
    }else{
        NSLog(@"Add Ivar error");
    }
    unsigned int outCount;
    Ivar *ivarList = class_copyIvarList([People class], &outCount);
    for (int i = 0; i < outCount; i++) {
        Ivar ivar = ivarList[i];
        const char *ivarName = ivar_getName(ivar);
        const char *type = ivar_getTypeEncoding(ivar);
        ptrdiff_t offset = ivar_getOffset(ivar);
        NSLog(@"ivar:%s, offset:%zd, type:%s", ivarName, offset, type);
    }
}複製代碼

3.2.2 屬性操做函數

// 獲取指定的屬性
objc_property_t class_getProperty(Class cls, const char *name);

// 獲取屬性列表
objc_property_t * class_copyPropertyList(Class cls, unsigned int *outCount);

// 爲類添加屬性
BOOL class_addProperty (Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount);

// 替換類的屬性
void class_replaceProperty (Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount); 複製代碼

針對ivar來操做的,不過它只操做那些property的值,包括擴展中的property。架構

測試屬性app

- (void)testProperty {    
    objc_property_attribute_t attribute1 = {"T", "@\"NSString\""};
    objc_property_attribute_t attribute2 = {"C", ""};
    objc_property_attribute_t attribute3 = {"N", ""};
    objc_property_attribute_t attribute4 = {"V", "_addProperty"};
    objc_property_attribute_t attributesList[] = {attribute1, attribute2, attribute3, attribute4};
    BOOL isSuccessAddProperty = class_addProperty([People class], "addProperty", attributesList, 4);
    if (isSuccessAddProperty) {
        NSLog(@"Add Property Success");
    }else{
        NSLog(@"Add Property Error");
    }
    unsigned int outCount;
    objc_property_t * propertyList = class_copyPropertyList([People class], &outCount);
    for (unsigned int i = 0; i < outCount; i++) {
        objc_property_t property = propertyList[i];
        const char *propertyName = property_getName(property);
        const char *attribute = property_getAttributes(property);
        NSLog(@"propertyName: %s, attribute: %s", propertyName, attribute);
        unsigned int attributeCount;
        objc_property_attribute_t *attributeList = property_copyAttributeList(property, &attributeCount);
        for (unsigned int i = 0; i < attributeCount; i++) {
            objc_property_attribute_t attribute = attributeList[i];
            const char *name = attribute.name;
            const char *value = attribute.value;
            NSLog(@"attribute name: %s, value: %s",name,value);
        }
    }
}  複製代碼

運行結果
框架

2018-05-01 17:14:52.957653+0800 RuntimeDemo[24515:910260] Add Property Success
2018-05-01 17:14:52.957871+0800 RuntimeDemo[24515:910260] propertyName: addProperty, attribute: T@"NSString",C,N,V_addProperty
2018-05-01 17:14:52.958034+0800 RuntimeDemo[24515:910260] attribute name: T, value: @"NSString"
2018-05-01 17:14:52.958175+0800 RuntimeDemo[24515:910260] attribute name: C, value:
2018-05-01 17:14:52.958309+0800 RuntimeDemo[24515:910260] attribute name: N, value:
2018-05-01 17:14:52.958452+0800 RuntimeDemo[24515:910260] attribute name: V, value: _addProperty
2018-05-01 17:14:52.958575+0800 RuntimeDemo[24515:910260] propertyName: name, attribute: T@"NSString",C,N,V_name
2018-05-01 17:14:52.958732+0800 RuntimeDemo[24515:910260] attribute name: T, value: @"NSString"
2018-05-01 17:14:52.958850+0800 RuntimeDemo[24515:910260] attribute name: C, value:
2018-05-01 17:14:52.958983+0800 RuntimeDemo[24515:910260] attribute name: N, value:
2018-05-01 17:14:52.959096+0800 RuntimeDemo[24515:910260] attribute name: V, value: _name
2018-05-01 17:14:52.959225+0800 RuntimeDemo[24515:910260] propertyName: age, attribute: T@"NSNumber",&,N,V_age
2018-05-01 17:14:52.959319+0800 RuntimeDemo[24515:910260] attribute name: T, value: @"NSNumber"
2018-05-01 17:14:52.959420+0800 RuntimeDemo[24515:910260] attribute name: &, value:
2018-05-01 17:14:52.959646+0800 RuntimeDemo[24515:910260] attribute name: N, value:
2018-05-01 17:14:52.959847+0800 RuntimeDemo[24515:910260] attribute name: V, value: _age
2018-05-01 17:14:52.960024+0800 RuntimeDemo[24515:910260] propertyName: sex, attribute: TQ,N,V_sex
2018-05-01 17:14:52.960186+0800 RuntimeDemo[24515:910260] attribute name: T, value: Q
2018-05-01 17:14:52.960365+0800 RuntimeDemo[24515:910260] attribute name: N, value:
2018-05-01 17:14:52.960584+0800 RuntimeDemo[24515:910260] attribute name: V, value: _sex
2018-05-01 17:14:52.960737+0800 RuntimeDemo[24515:910260] propertyName: address, attribute: T@"NSString",C,N,V_address
2018-05-01 17:14:52.960928+0800 RuntimeDemo[24515:910260] attribute name: T, value: @"NSString"
2018-05-01 17:14:52.961101+0800 RuntimeDemo[24515:910260] attribute name: C, value:
2018-05-01 17:14:52.961274+0800 RuntimeDemo[24515:910260] attribute name: N, value:
2018-05-01 17:14:52.961463+0800 RuntimeDemo[24515:910260] attribute name: V, value: _address複製代碼
T 是固定的,放在第一個 @」NSString」 表明這個property是一個字符串對象 & 表明強引用,其中與之並列的是:’C’表明Copy,’&’表明強引用,’W’表示weak,assign爲空,默認爲assign。R 表明readOnly屬性,readwrite時爲空 N 區分的nonatomic和atomic,默認爲atomic,atomic爲空,’N’表明是nonatomic V_exprice V表明變量,後面緊跟着的是成員變量名,表明這個property的成員變量名爲_exprice

property_getAttributes 說明

3.2.3 協議相關函數

// 添加協議
BOOL class_addProtocol ( Class cls, Protocol *protocol );
 
// 返回類是否實現指定的協議
BOOL class_conformsToProtocol ( Class cls, Protocol *protocol );
 
// 返回類實現的協議列表
Protocol * class_copyProtocolList ( Class cls, unsigned int *outCount );
複製代碼

測試協議

@protocol PeopleProcol <NSObject>
@end

- (void)testProtocol {
    // 添加協議
    Protocol *p = @protocol(PeopleProcol);
    if (class_addProtocol([People class], p)) {
        NSLog(@"Add Protoclol Success");
    }else{
        NSLog(@"Add protocol Fail");
    }
    if (class_conformsToProtocol([People class], p)) {
        NSLog(@"實現了 PeopleProcol 協議");
    }else{
        NSLog(@"沒有實現 PeopleProcol 協議");
    }
    unsigned int outCount;
    Protocol *__unsafe_unretained *protocolList = class_copyProtocolList([People class], &outCount);
    for (unsigned int i = 0; i < outCount; i++) {
        Protocol *p = protocolList[i];
        const char *protocolName = protocol_getName(p);
        NSLog(@"協議名稱: %s",protocolName);
    }
} 複製代碼

運行結果

2018-05-01 17:29:12.580433+0800 RuntimeDemo[25007:940310] Add Protoclol Success
2018-05-01 17:29:12.580591+0800 RuntimeDemo[25007:940310] 實現了 PeopleProcol 協議
2018-05-01 17:29:12.580707+0800 RuntimeDemo[25007:940310] 協議名稱: PeopleProcol 複製代碼

3.2.4 版本號

- (void)testVersion {
    int version = class_getVersion([People class]);
    NSLog(@"version %d",version);
    
    class_setVersion([People class], 10086);
    
    int nerVersion = class_getVersion([People class]);
    NSLog(@"nerVersion %d",nerVersion);
} 

複製代碼

運行結果

2018-05-01 17:38:29.593821+0800 RuntimeDemo[25266:956588] version 0
2018-05-01 17:38:29.593972+0800 RuntimeDemo[25266:956588] nerVersion 10086 
複製代碼

3.3 動態建立類和對象

3.3.1. 動態建立類

// 建立一個新類和元類
Class objc_allocateClassPair (Class superclass, const char *name, size_t extraBytes);

// 銷魂一個類及其相關聯的類
void objc_disposeClassPair (Class cls);

// 在應用中註冊由objc_allocateClassPair建立類
void objc_registerClassPair (Class cls); 

複製代碼

其中:

(1)objc_allocateClassPair函數:若是咱們要建立一個根類,則superclass指定爲Nil。extraBytes一般指定爲0,該參數是分配給類和元類對象尾部的索引ivars的字節數。

(2)爲了建立一個新類,咱們須要調用objc_allocateClassPair。而後使用諸如class_addMethod,class_addIvar等函數來爲新建立的類添加方法、實例變量和屬性等。完成這些後,咱們須要調用objc_registerClassPair函數來註冊類,以後這個新類就能夠在程序中使用了。

(3)實例方法和實例變量應該添加到類自身上,而類方法應該添加到類的元類上。

(4)objc_disposeClassPair只能銷燬由objc_allocateClassPair建立的類,當有實例存在或者它的子類存在時,調用這個函數會拋出異常。

測試代碼:

- (void)testAddClass {
    Class TestClass = objc_allocateClassPair([NSObject class], "myClass", 0);
    if (class_addIvar(TestClass, "myIvar", sizeof(NSString *), sizeof(NSString *), "@")) {
        NSLog(@"Add Ivar Success");
    }
    class_addMethod(TestClass, @selector(method1:), (IMP)method0, "v@:");
    // 註冊這個類到runtime纔可以使用
    objc_registerClassPair(TestClass);
    
    // 生成一個實例化對象
    id myObjc = [[TestClass alloc] init];
    NSString *str = @"qiuxuewei";
    //給剛剛添加的變量賦值
    //object_setInstanceVariable(myobj, "myIvar", (void *)&str);在ARC下不容許使用
    [myObjc setValue:str forKey:@"myIvar"];
    [myObjc method1:10086];
}
- (void)method1:(int)a {
}
void method0(id self, SEL _cmd, int a) {
    Ivar v = class_getInstanceVariable([self class], "myIvar");
    id o = object_getIvar(self, v);
    NSLog(@"%@ \n int a is %d", o,a);
} 複製代碼

運行結果:

2018-05-01 22:30:30.159096+0800 RuntimeDemo[31292:1162987] Add Ivar Success
2018-05-01 22:30:30.159344+0800 RuntimeDemo[31292:1162987] qiuxuewei 
 int a is 10086 

複製代碼

3.3.2. 動態建立對象

// 建立類的實例
id class_createInstance (Class cls, size_t extraBytes);

// 在指定位置建立類實例
id objc_constructInstance (Class cls, void *bytes);

// 銷燬類實例
void * objc_destructInstance (id obj);  複製代碼

class_createInstance函數:建立實例時,會在默認的內存區域爲類分配內存。extraBytes參數表示分配的額外字節數。這些額外的字節可用於存儲在類定義中所定義的實例變量以外的實例變量。該函數在ARC環境下沒法使用。

調用class_createInstance的效果與+alloc方法相似。不過在使用class_createInstance時,咱們須要確切的知道咱們要用它來作什麼。

測試代碼

- (void)testCreteInstance {
    id testInstance = class_createInstance([NSString class], sizeof(unsigned));
    id str1 = [testInstance init];
    NSLog(@"%@",[str1 class]);
    id str2 = [[NSString alloc] initWithString: @"Test"];
    NSLog(@"%@",[str2 class]);
} 

複製代碼

運行結果:
2018-05-01 23:43:25.941205+0800 RuntimeDemo[32783:1223167] NSString
2018-05-01 23:43:25.941364+0800 RuntimeDemo[32783:1223167] __NSCFConstantString 複製代碼

3.3.3. 其餘類和對象相關的操做函數


// 獲取已註冊的類定義的列表
int objc_getClassList(Class *buffer, int bufferCount);

// 建立並返回一個指向全部已註冊類的指針列表
Class * objc_copyClassList (unsigned int * outCount);

// 返回指定類的類定義
Class objc_lookUpClass ( const char *name );
Class objc_getClass ( const char *name );
Class objc_getRequiredClass ( const char *name );
 
// 返回指定類的元類
Class objc_getMetaClass ( const char *name );  複製代碼

對象

// 返回指定對象的一份拷貝
id object_copy ( id obj, size_t size );
 
// 釋放指定對象佔用的內存
id object_dispose ( id obj );

// 修改類實例的實例變量的值
Ivar object_setInstanceVariable ( id obj, const char *name, void *value );
 
// 獲取對象實例變量的值
Ivar object_getInstanceVariable ( id obj, const char *name, void **outValue );
 
// 返回指向給定對象分配的任何額外字節的指針
void * object_getIndexedIvars ( id obj );
 
// 返回對象中實例變量的值
id object_getIvar ( id obj, Ivar ivar );
 
// 設置對象中實例變量的值
void object_setIvar ( id obj, Ivar ivar, id value );

// 返回給定對象的類名
const char * object_getClassName ( id obj );
 
// 返回對象的類
Class object_getClass ( id obj );
 
// 設置對象的類
Class object_setClass ( id obj, Class cls );  

複製代碼

獲取類的定義

// 獲取已註冊的類定義的列表
int objc_getClassList (Class *)  

複製代碼

3.3.4. 應用實例

1. Json 轉 Model

操做函數

- (instancetype)initWithDict:(NSDictionary *)dict {
    if (self = [self init]) {
        NSMutableArray <NSString *>*keys = [NSMutableArray array];
        NSMutableArray <NSString *>*attributes = [NSMutableArray array];
        
        unsigned int outCount;
        objc_property_t * propertyList = class_copyPropertyList([self class], &outCount);
        for (unsigned int i = 0; i < outCount; i++) {
            objc_property_t property = propertyList[i];
            const char *name = property_getName(property);
            NSString *propertyName = [NSString stringWithCString:name encoding:NSUTF8StringEncoding];
            [keys addObject:propertyName];
            
            const char *attribute = property_getAttributes(property);
            NSString *attributeName = [NSString stringWithCString:attribute encoding:NSUTF8StringEncoding];
            [attributes addObject:attributeName];
        }
        free(propertyList);
        for (NSString *key in keys) {
            if ([dict valueForKey:key]) {
                [self setValue:[dict valueForKey:key] forKey:key];
            }
        }
    }
    return self;
}複製代碼
2. 快速歸解檔

遵循 NSCoding 協議
// 歸檔
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    if (self = [super init]) {
        unsigned int outCount;
        Ivar * ivarList = class_copyIvarList([self class], &outCount);
        for (unsigned int i = 0; i < outCount; i++) {
            Ivar ivar = ivarList[i];
            NSString *key = [NSString stringWithCString:ivar_getName(ivar) encoding:NSUTF8StringEncoding];
            [self setValue:[aDecoder decodeObjectForKey:key] forKey:key];
        }
    }
    return self;
}
// 解檔
- (void)encodeWithCoder:(NSCoder *)aCoder {
    unsigned int outCount;
    Ivar * ivarList = class_copyIvarList([self class], &outCount);
    for (unsigned int i = 0; i < outCount; i++) {
        Ivar ivar = ivarList[i];
        NSString *key = [NSString stringWithCString:ivar_getName(ivar) encoding:NSUTF8StringEncoding];
        [aCoder encodeObject:[self valueForKey:key] forKey:key];
    }
}複製代碼
測試

- (void)testCoder {
    NSString *key = @"peopleKey";
    People * people = [[People alloc] init];
    people.name = @"邱學偉";
    people.age = @18;
    NSData *peopleData = [NSKeyedArchiver archivedDataWithRootObject:people];
    [[NSUserDefaults standardUserDefaults] setObject:peopleData forKey:key];
    
    NSData *testData = [[NSUserDefaults standardUserDefaults] objectForKey:key];
    People *testPeople = [NSKeyedUnarchiver unarchiveObjectWithData:testData];
    NSLog(@"%@",testPeople.name);
} 

複製代碼
3. 關聯對象

// 關聯對象
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);

複製代碼

參數說明

id object : 被關聯的對象 
const void *key : 關聯的key, set和get 需統一
id value : 關聯的對象
objc_AssociationPolicy policy : 內存管理的策略複製代碼

objc_AssociationPolicy policy的enum值有:、

typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {

    OBJC_ASSOCIATION_ASSIGN = 0,           /**< Specifies a weak reference to the associated object. */
    
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object. 
                                            *   The association is not made atomically. */
    
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   /**< Specifies that the associated object is copied. 
                                            *   The association is not made atomically. */
    
    OBJC_ASSOCIATION_RETAIN = 01401,       /**< Specifies a strong reference to the associated object.
                                            *   The association is made atomically. */
    
    OBJC_ASSOCIATION_COPY = 01403          /**< Specifies that the associated object is copied.
                                            *   The association is made atomically. */

};  

複製代碼

應用實例

//
//  People+Category.h
//  RuntimeDemo
//
//  Created by 邱學偉 on 2018/5/3.
//  Copyright © 2018年 邱學偉. All rights reserved.
//

#import "People.h"
@interface People (Category)
/**
 新增屬性
 */
@property (nonatomic, copy) NSString *blog;
@end

複製代碼

//
//  People+Category.m
//  RuntimeDemo
//
//  Created by 邱學偉 on 2018/5/3.
//  Copyright © 2018年 邱學偉. All rights reserved.
//

#import "People+Category.h"
#import <objc/runtime.h>
@implementation People (Category)
static const char * cPeopleBlogKey = "cPeopleBlogKey";
- (NSString *)blog {
    return objc_getAssociatedObject(self, cPeopleBlogKey);
}
- (void)setBlog:(NSString *)blog {
    objc_setAssociatedObject(self, cPeopleBlogKey, blog, OBJC_ASSOCIATION_COPY);
}
@end

複製代碼

4. 方法與消息

4.1 SEL

SEL 又叫方法選擇器, 是表示一個方法的selector的指針,其定義以下

typedef  struct objc_selector *SEL;

複製代碼

方法的selector用於表示運行時方法的名字,Objective-C在編譯時,會根據每個方法的名字,參數序列,生成一個惟一的整型標示(Int類型的地址),這個標識就是SEL. 以下

+ (void)load {
    SEL sel = @selector(testMethod);
    NSLog(@"Programmer sel = %p",sel);
}
- (void)testMethod {
    NSLog(@"testMethod");
}複製代碼

兩個類之間,無論它們是父類與子類的關係,仍是之間沒有這種關係,只要方法名相同,那麼方法的SEL就是同樣的。每個方法都對應着一個SEL。因此在Objective-C同一個類(及類的繼承體系)中,不能存在2個同名的方法,即便參數類型不一樣也不行。相同的方法只能對應一個SEL。這也就致使Objective-C在處理相同方法名且參數個數相同但類型不一樣的方法方面的能力不好. 固然,不一樣的類能夠擁有相同的selector,這個沒有問題。不一樣類的實例對象執行相同的selector時,會在各自的方法列表中去根據selector去尋找本身對應的IMP。

本質上,SEL只是一個指向方法的指針(準確的說,只是一個根據方法名hash化了的KEY值,能惟一表明一個方法),它的存在只是爲了加快方法的查詢速度。這個查找過程咱們將在下面討論。

咱們能夠在運行時添加新的selector,也能夠在運行時獲取已存在的selector,咱們能夠經過下面三種方法來獲取SEL:

(1)sel_registerName函數

(2)Objective-C編譯器提供的@selector()

(3)NSSelectorFromString()方法

4.2 IMP

IMP 是一個函數指針,指向方法實現的首地址。

id (*IMP)(id,SEL,...) 

複製代碼

這個函數使用當前CPU架構實現的標準的C調用約定。第一個參數是指向self的指針(若是是實例方法,則是類實例的內存地址;若是是類方法,則是指向元類的指針),第二個參數是方法選擇器(selector),接下來是方法的實際參數列表。

前面介紹過的SEL就是爲了查找方法的最終實現IMP的。因爲每一個方法對應惟一的SEL,所以咱們能夠經過SEL方便快速準確地得到它所對應的IMP,查找過程將在下面討論。取得IMP後,咱們就得到了執行這個方法代碼的入口點,此時,咱們就能夠像調用普通的C語言函數同樣來使用這個函數指針了。

經過取得IMP,咱們能夠跳過Runtime的消息傳遞機制,直接執行IMP指向的函數實現,這樣省去了Runtime消息傳遞過程當中所作的一系列查找操做,會比直接向對象發送消息高效一些。

4.3 Method

Method 用於表示類定義中的方法,定義以下:

typedef struct objc_method *Method;

struct objc_method {
     SEL method_name OBJC2_UNAVAILABLE; // 方法名
     char *method_types OBJC2_UNAVAILABLE; //是個char指針,存儲着方法的參數類型和返回值類型
     IMP method_imp OBJC2_UNAVAILABLE; // 方法實現,函數指針
}複製代碼

該結構體中包含一個SEL和IMP,實際上至關於在SEL和IMP之間做了一個映射。有了SEL,咱們即可以找到對應的IMP,從而調用方法的實現代碼。

4.4 objc_method_description

objc_method_description定義了一個Objective-C方法,其定義以下:

struct objc_method_description { SEL name; char *types; };複製代碼

4.5 Method 相關操做函數

// 調用指定方法的實現
    id method_invoke (id receiver, Method m, ...);
    
    // 調用返回一個數據結構的方法的實現
    void method_invoke_stret (id receiver, Method m, ...);
    
    // 獲取方法名
    SEL method_getName (Method m);
    
    // 獲取方法的實現
    IMP method_getImplementation (Method m);
    
    // 獲取描述方法參數和返回值類型的字符串
    const char * method_getTypeEncoding (Method m);
    
    // 獲取方法的返回值類型的字符串
    char * method_copyReturnType ( Method m );
    
    // 獲取方法的指定位置參數的類型字符串
    char * method_copyArgumentType ( Method m, unsigned int index );
    
    // 經過引用返回方法的返回值類型字符串
    void method_getReturnType ( Method m, char *dst, size_t dst_len );
    
    // 返回方法的參數的個數
    unsigned int method_getNumberOfArguments ( Method m );
    
    // 經過引用返回方法指定位置參數的類型字符串
    void method_getArgumentType ( Method m, unsigned int index, char *dst, size_t dst_len );
    
    // 返回指定方法的方法描述結構體
    struct objc_method_description * method_getDescription ( Method m );
    
    // 設置方法的實現
    IMP method_setImplementation ( Method m, IMP imp );
    
    // 交換兩個方法的實現
    void method_exchangeImplementations ( Method m1, Method m2 ); 

複製代碼

(1)method_invoke函數,返回的是實際實現的返回值。參數receiver不能爲空。這個方法的效率會比method_getImplementation和method_getName更快。

(2)method_getName函數,返回的是一個SEL。若是想獲取方法名的C字符串,可使用sel_getName(method_getName(method))。

(3)method_getReturnType函數,類型字符串會被拷貝到dst中。

(4)method_setImplementation函數,注意該函數返回值是方法以前的實現。

4.6 方法選擇器

// 返回給定選擇器指定的方法的名稱
const char * sel_getName ( SEL sel );

// 在Objective-C Runtime系統中註冊一個方法,將方法名映射到一個選擇器,並返回這個選擇器
SEL sel_registerName ( const char *str );

// 在Objective-C Runtime系統中註冊一個方法
SEL sel_getUid ( const char *str );

// 比較兩個選擇器
BOOL sel_isEqual ( SEL lhs, SEL rhs );

複製代碼

sel_registerName函數:在咱們將一個方法添加到類定義時,咱們必須在Objective-C Runtime系統中註冊一個方法名以獲取方法的選擇器。

4.7 方法調用流程

1. 消息發送

在Objective-C中,消息直到運行時才綁定到方法實現上。編譯器會將消息表達式[receiver message]轉化爲一個消息函數的調用,即objc_msgSend。這個函數將消息接收者和方法名做爲其基礎參數,如如下所示:

objc_msgSend(receiver, selector)
複製代碼

若是消息中還有其餘參數,則該方法的形式以下所示:

objc_msgSend(receiver, selector,arg1, arg2, ...);複製代碼

這個函數完成了動態綁定的全部事情:

(1)首先它找到selector對應的方法實現。由於同一個方法可能在不一樣的類中有不一樣的實現,因此咱們須要依賴於接收者的類來找到的確切的實現。

(2)它調用方法實現,並將接收者對象及方法的全部參數傳給它。

(3)最後,它將實現返回的值做爲它本身的返回值。

消息的關鍵在於結構體 objc_class, 這個結構體有兩個字段是咱們在分發消息的時候關注的:

  1. 指向父類的指針。
  2. 一個類的方法分發表,即methodLists

當咱們建立一個新對象時,先爲其分配內存,並初始化其成員變量。其中isa指針也會被初始化,讓對象能夠訪問類及類的繼承體系。

下圖演示了這樣一個消息的基本框架:

消息的基本框架
當消息發送給一個對象時,objc_msgSend經過對象的isa指針獲取到類的結構體,而後在方法分發表裏面查找方法的selector。若是沒有找到selector,則經過objc_msgSend結構體中的指向父類的指針找到其父類,並在父類的分發表裏面查找方法的selector。依此,會一直沿着類的繼承體系到達NSObject類。一旦定位到selector,函數會就獲取到了實現的入口點,並傳入相應的參數來執行方法的具體實現。若是最後沒有定位到selector,則會走消息轉發流程

2. 隱藏參數

objc_msgSend 有兩個隱藏參數

  1. 消息接收對象
  2. 方法的selector

這兩個參數爲方法的實現提供了調用者的信息。之因此說是隱藏的,是由於他們在定義方法的源代碼中沒有聲明。他們是在編譯時被插入實現代碼的。

雖然這些參數沒有顯示聲明,但在代碼中仍然能夠引用它們。咱們可使用self來引用接收者對象,使用_cmd 來引用選擇器。以下代碼:

- strange
{
    id  target = getTheReceiver();
    SEL method = getTheMethod();
    if ( target == self || method == _cmd )
        return nil;
    return [target performSelector:method];
}
複製代碼

固然,這兩個參數咱們用的比較多的是self,_cmd 在實際中用得比較少。

3. 獲取方法地址

Runtime中方法的動態綁定讓咱們寫代碼時更具靈活性,如咱們能夠把消息轉發給咱們想要的對象,或者隨意交換一個方法的實現等。不過靈活性的提高也帶來了性能上的一些損耗。畢竟咱們須要去查找方法的實現,而不像函數調用來得那麼直接。固然,方法的緩存必定程度上解決了這一問題。

咱們上面提到過,若是想要避開這種動態綁定方式,咱們能夠獲取方法實現的地址,而後像調用函數同樣來直接調用它。特別是當咱們須要在一個循環內頻繁地調用一個特定的方法時,經過這種方式能夠提升程序的性能。

NSObject類提供了methodForSelector:方法,讓咱們能夠獲取到方法的指針,而後經過這個指針來調用實現代碼。咱們須要將methodForSelector:返回的指針轉換爲合適的函數類型,函數參數和返回值都須要匹配上。

這裏須要注意的就是函數指針的前兩個參數必須是id和SEL。

固然這種方式只適合於在相似於for循環這種狀況下頻繁調用同一方法,以提升性能的狀況。另外,methodForSelector:是由Cocoa運行時提供的;它不是Objective-C語言的特性。

- (void)testCommonMethod {
    for (int i = 0; i < 10000; i++) {
        [self logMethod:i];
    }
    //執行時長: Test Case '-[RuntimeDemoTests testCommonMethod]' passed (2.311 seconds).
}

- (void)testRuntimeMethod {
    void(*logM)(id, SEL, int);
    IMP imp = [self methodForSelector:@selector(logMethod:)];
    logM = (void(*)(id, SEL, int))imp;
    for (int i = 0; i < 10000; i++) {
        logM(self, @selector(logMethod:), i);
    }
    //執行時長: Test Case '-[RuntimeDemoTests testRuntimeMethod]' passed (2.199 seconds).
}
複製代碼複製代碼

4. 消息轉發

當一個對象能接收一個消息時,就會走正常的方法調用流程。但若是一個對象沒法接收指定消息時,又會發生什麼事呢?默認狀況下,若是是以[object message]的方式調用方法,若是object沒法響應message消息時,編譯器會報錯。但若是是以perform…的形式來調用,則須要等到運行時才能肯定object是否能接收message消息。若是不能,則程序崩潰。

一般,當咱們不能肯定一個對象是否能接收某個消息時,會先調用respondsToSelector:來判斷一下。以下代碼所示:

// perform方法要求傳入參數必須是對象,若是方法自己的參數是int,直接傳NSNumber會致使獲得的int參數不正確
if ([self respondsToSelector:@selector(logMethod:)]) {
        [self performSelector:@selector(logMethod:) withObject:[NSNumber numberWithInt:10086]];
    }
複製代碼複製代碼

當一個對象沒法接收某一消息時,就會啓動所謂」消息轉發(message forwarding)「機制,經過這一機制,咱們能夠告訴對象如何處理未知的消息。默認狀況下,對象接收到未知的消息,會致使程序崩潰,經過控制檯,咱們能夠看到如下異常信息:

unrecognized selector sent to instance 0x100111940

這段異常信息其實是由NSObject的」doesNotRecognizeSelector」方法拋出的。不過,咱們能夠採起一些措施,讓咱們的程序執行特定的邏輯,而避免程序的崩潰。

消息轉發機制基本上分爲三個步驟:

(1)動態方法解析

(2)備用接收者

(3)完整轉發

1. 動態方法解析
對象在接收到未知的消息時,首先會調用所屬類的類方法+resolveInstanceMethod:(實例方法)或者+resolveClassMethod:(類方法)。在這個方法中,咱們有機會爲該未知消息新增一個」處理方法」「。不過使用該方法的前提是咱們已經實現了該」處理方法」,只須要在運行時經過class_addMethod函數動態添加到類裏面就能夠了。以下代碼所示:
複製代碼複製代碼
void functionForMethod (id self, SEL _cmd) {
    NSLog(@"functionForMethod");
}
/// 調用未實現對象方法
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSString *selName = NSStringFromSelector(sel);
    if ([selName isEqualToString:@"methodNull"]) {
        class_addMethod(self.class, sel, (IMP)functionForMethod, "v@:");
        return YES;
    }
    return [super resolveClassMethod:sel];
}
/// 調用未實現類方法
+ (BOOL)resolveClassMethod:(SEL)sel {
    NSString *selName = NSStringFromSelector(sel);
    if ([selName isEqualToString:@"methodNull"]) {
        //v@:表示返回值和參數,能夠在蘋果官網查看Type Encoding相關文檔 https://developer.apple.com/library/mac/DOCUMENTATION/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html
        class_addMethod(self.class, sel, (IMP)functionForMethod, "v@:");
        return YES;
    }
    return [super resolveClassMethod:sel];
}
複製代碼複製代碼
2. 備用接受者
若是在上一步沒法處理消息,則Runtime會繼續調如下方法:
複製代碼複製代碼
- (id)forwardingTargetForSelector:(SEL)aSelector
複製代碼複製代碼

若是一個對象實現了這個方法,並返回一個非nil的結果,則這個對象會做爲消息的新接收者,且消息會被分發到這個對象。固然這個對象不能是self自身,不然就是出現無限循環。固然,若是咱們沒有指定相應的對象來處理aSelector,則應該調用父類的實現來返回結果。

使用這個方法一般是在對象內部,可能還有一系列其它對象能處理該消息,咱們即可借這些對象來處理消息並返回,這樣在對象外部看來,仍是由該對象親自處理了這一消息。以下代碼所示:

#import "People.h"
#import <objc/runtime.h>

@interface XWPeople : NSObject
- (void)people2log;
@end
@implementation XWPeople
- (void)people2log {
    NSLog(@"people2log");
}
@end

@interface People () <NSCoding> {
}
@property (nonatomic, strong) XWPeople *xw_people;
@end
@implementation People

//// 1 級處理
void functionForMethod (id self, SEL _cmd) {
    NSLog(@"functionForMethod");
}
/// 調用未實現對象方法
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSString *selName = NSStringFromSelector(sel);
    if ([selName isEqualToString:@"methodNull"]) {
        class_addMethod(self.class, sel, (IMP)functionForMethod, "v@:");
        return YES;
    }
    return [super resolveClassMethod:sel];
}
/// 調用未實現類方法
+ (BOOL)resolveClassMethod:(SEL)sel {
    NSString *selName = NSStringFromSelector(sel);
    if ([selName isEqualToString:@"methodNull"]) {
        //v@:表示返回值和參數,能夠在蘋果官網查看Type Encoding相關文檔 https://developer.apple.com/library/mac/DOCUMENTATION/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html
        class_addMethod(self.class, sel, (IMP)functionForMethod, "v@:");
        return YES;
    }
    return [super resolveClassMethod:sel];
}

/// 2 級處理
- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSString *selName = NSStringFromSelector(aSelector);
    if ([selName isEqualToString:@"people2log"]) {
        return self.xw_people;
    }
    return [super forwardingTargetForSelector:aSelector];
}
- (XWPeople *)xw_people {
    if(!_xw_people){
        _xw_people = [[XWPeople alloc] init];
    }
    return _xw_people;
}
@end
複製代碼複製代碼

這一步合適於咱們只想將消息轉發到另外一個能處理該消息的對象上。但這一步沒法對消息進行處理,如操做消息的參數和返回值。

3. 完整轉發

若是在上一步還不能處理未知消息,則惟一能作的就是啓用完整的消息轉發機制了。此時會調用如下方法:

- (void)forwardInvocation:(NSInvocation *)anInvocation

複製代碼複製代碼

運行時系統會在這一步給消息接收者最後一次機會將消息轉發給其它對象。對象會建立一個表示消息的NSInvocation對象,把與還沒有處理的消息有關的所有細節都封裝在anInvocation中,包括selector,目標(target)和參數。咱們能夠在forwardInvocation方法中選擇將消息轉發給其它對象。

forwardInvocation:方法的實現有兩個任務:

(1)定位能夠響應封裝在anInvocation中的消息的對象。這個對象不須要能處理全部未知消息。

(2)使用anInvocation做爲參數,將消息發送到選中的對象。anInvocation將會保留調用結果,運行時系統會提取這一結果並將其發送到消息的原始發送者。

不過,在這個方法中咱們能夠實現一些更復雜的功能,咱們能夠對消息的內容進行修改,好比追回一個參數等,而後再去觸發消息。另外,若發現某個消息不該由本類處理,則應調用父類的同名方法,以便繼承體系中的每一個類都有機會處理此調用請求。

還有一個很重要的問題,咱們必須重寫如下方法:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
複製代碼複製代碼

消息轉發機制使用從這個方法中獲取的信息來建立NSInvocation對象。所以咱們必須重寫這個方法,爲給定的selector提供一個合適的方法簽名。

完整的示例以下所示:

//
//  People.m
//  RuntimeDemo
//
//  Created by 邱學偉 on 2018/4/27.
//  Copyright © 2018年 邱學偉. All rights reserved.
//

#import "People.h"
#import <objc/runtime.h>

@interface XWPeople : NSObject
- (void)people2log;
- (void)people3log;
@end
@implementation XWPeople
- (void)people2log {
    NSLog(@"people2log");
}

- (void)people3log {
    NSLog(@"people3log");
}
@end

@interface People () <NSCoding> {
}
@property (nonatomic, strong) XWPeople *xw_people;
@end
@implementation People

//// 1 級處理
void functionForMethod (id self, SEL _cmd) {
    NSLog(@"functionForMethod");
}
/// 調用未實現對象方法
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSString *selName = NSStringFromSelector(sel);
    if ([selName isEqualToString:@"methodNull"]) {
        class_addMethod(self.class, sel, (IMP)functionForMethod, "v@:");
        return YES;
    }
    return [super resolveClassMethod:sel];
}
/// 調用未實現類方法
+ (BOOL)resolveClassMethod:(SEL)sel {
    NSString *selName = NSStringFromSelector(sel);
    if ([selName isEqualToString:@"methodNull"]) {
        //v@:表示返回值和參數,能夠在蘋果官網查看Type Encoding相關文檔 https://developer.apple.com/library/mac/DOCUMENTATION/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html
        class_addMethod(self.class, sel, (IMP)functionForMethod, "v@:");
        return YES;
    }
    return [super resolveClassMethod:sel];
}

/// 2 級處理
- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSString *selName = NSStringFromSelector(aSelector);
    if ([selName isEqualToString:@"people2log"]) {
        return self.xw_people;
    }
    return [super forwardingTargetForSelector:aSelector];
}
- (XWPeople *)xw_people {
    if(!_xw_people){
        _xw_people = [[XWPeople alloc] init];
    }
    return _xw_people;
}

/// 3 級處理
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
    if (!signature) {
        if ([XWPeople instancesRespondToSelector:aSelector]) {
            signature = [XWPeople instanceMethodSignatureForSelector:aSelector];
        }
    }
    return signature;
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    if ([XWPeople instancesRespondToSelector:anInvocation.selector]) {
        [anInvocation invokeWithTarget:self.xw_people];
    }
}

@end
複製代碼複製代碼

NSObject的forwardInvocation:方法實現只是簡單調用了doesNotRecognizeSelector:方法,它不會轉發任何消息。這樣,若是不在以上所述的三個步驟中處理未知消息,則會引起一個異常。

從某種意義上來說,forwardInvocation:就像一個未知消息的分發中心,將這些未知的消息轉發給其它對象。或者也能夠像一個運輸站同樣將全部未知消息都發送給同一個接收對象。這取決於具體的實現。

四、消息轉發與多重繼承

回過頭來看第二和第三步,經過這兩個方法咱們能夠容許一個對象與其它對象創建關係,以處理某些未知消息,而表面上看仍然是該對象在處理消息。經過這種關係,咱們能夠模擬「多重繼承」的某些特性,讓對象能夠「繼承」其它對象的特性來處理一些事情。不過,這二者間有一個重要的區別:多重繼承將不一樣的功能集成到一個對象中,它會讓對象變得過大,涉及的東西過多;而消息轉發將功能分解到獨立的小的對象中,並經過某種方式將這些對象鏈接起來,並作相應的消息轉發。

不過消息轉發雖然相似於繼承,但NSObject的一些方法仍是能區分二者。如respondsToSelector:和isKindOfClass:只能用於繼承體系,而不能用於轉發鏈。便若是咱們想讓這種消息轉發看起來像是繼承,則能夠重寫這些方法,如如下代碼所示:

- (BOOL)respondsToSelector:(SEL)aSelector   {
       if ( [super respondsToSelector:aSelector] )         
       		return YES;     
       else {          
       		/* Here, test whether the aSelector message can     *            
      		 * be forwarded to another object and whether that  *            
      		* object can respond to it. Return YES if it can.  */      
       }
      return NO;  
}複製代碼

4.6 Method Swizzling

4.6.1 概述

Objective-C 中的 Method Swizzling 是一項異常強大的技術,它能夠容許咱們動態地替換方法的實現,實現 Hook 功能,是一種比子類化更加靈活的「重寫」方法的方式。

4.6.2 原理

Objective-C同一個類(及類的繼承體系)中,不能存在2個同名的方法,即便參數類型不一樣也不行。因此下面兩個方法在 runtime 看來就是同一個方法:

- (void)viewWillAppear:(BOOL)animated;
- (void)viewWillAppear:(NSString *)string;
複製代碼複製代碼

而下面兩個方法倒是能夠共存的

- (void)viewWillAppear:(BOOL)animated;
+ (void)viewWillAppear:(BOOL)animated;
複製代碼複製代碼

由於實例方法和類方法是分別保存在類對象和元類對象中的。

原則上,方法的名稱 name 和方法的實現 imp 是一一對應的,而 Method Swizzling 的原理就是動態地改變它們的對應關係,以達到替換方法實現的目的

原有方法和實現的對應關係以下圖:

原有方法和實現的對應關係

經過runtime可實現: 

runtime 調整的對應關係

在OC語言的runtime特性中,調用一個對象的方法就是給這個對象發送消息。是經過查找接收消息對象的方法列表,從方法列表中查找對應的SEL,這個SEL對應着一個IMP(一個IMP能夠對應多個SEL),經過這個IMP找到對應的方法調用。

在每一個類中都有一個Dispatch Table,這個Dispatch Table本質是將類中的SEL和IMP(能夠理解爲函數指針)進行對應。而咱們的Method Swizzling就是對這個table進行了操做,讓SEL對應另外一個IMP。

4.6.3 使用注意

  • Swizzling應該總在+load中執行:Objective-C在運行時會自動調用類的兩個方法+load和+initialize。+load會在類初始加載時調用,和+initialize比較+load能保證在類的初始化過程當中被加載
  • Swizzling應該老是在dispatch_once中執行:swizzling會改變全局狀態,因此在運行時採起一些預防措施,使用dispatch_once就可以確保代碼無論有多少線程都只被執行一次。這將成爲method swizzling的最佳實踐。
  • Selector,Method和Implementation:這幾個之間關係能夠這樣理解,一個類維護一個運行時可接收的消息分發表,分發表中每一個入口是一個Method,其中key是一個特定的名稱,及SEL,與其對應的實現是IMP即指向底層C函數的指針。

4.6.4 應用實例

1. 替換方法實現
#import "ViewController+Method.h"
#import <objc/runtime.h>

@implementation ViewController (Method)
+ (void)load {
    [super load];
    [self exchangeMethod];
}

/// runtime 交換方法
+ (void)exchangeMethod {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        
        SEL originSel = @selector(viewWillAppear:);
        SEL swizzledSel = @selector(xw_viewWillAppear:);
        
        Method originMethod = class_getInstanceMethod(class, originSel);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSel);
        
        //先嚐試給源方法添加實現,這裏是爲了不源方法沒有實現的狀況
        BOOL isAddMethod = class_addMethod(class, originSel, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
        if (isAddMethod) {
            class_replaceMethod(class, swizzledSel, method_getImplementation(originMethod), method_getTypeEncoding(originMethod));
        }else{
            method_exchangeImplementations(originMethod, swizzledMethod);
        }
    });
}
- (void)xw_viewWillAppear:(BOOL)animation {
    [self xw_viewWillAppear:animation];
    NSLog(@"xw_viewWillAppear - %@",self);
}
@end
複製代碼
二、Method Swizzling類簇

在咱們項目開發過程當中,常常由於NSArray數組越界或者NSDictionary的key或者value值爲nil等問題致使的崩潰,咱們能夠嘗試使用前面知識對NSArray、NSMutableArray、NSDictionary、NSMutableDictionary等類進行Method Swizzling,可是結果發現Method Swizzling根本就不起做用,到底爲何呢?

這是由於Method Swizzling對NSArray這些的類簇是不起做用的。由於這些類簇類,實際上是一種抽象工廠的設計模式。抽象工廠內部有不少其它繼承自當前類的子類,抽象工廠類會根據不一樣狀況,建立不一樣的抽象對象來進行使用。例如咱們調用NSArray的objectAtIndex:方法,這個類會在方法內部判斷,內部建立不一樣抽象類進行操做。

因此也就是咱們對NSArray類進行操做其實只是對父類進行了操做,在NSArray內部會建立其餘子類來執行操做,真正執行操做的並非NSArray自身,因此咱們應該對其「真身」進行操做。

下面咱們實現了防止NSArray由於調用objectAtIndex:方法,取下標時數組越界致使的崩潰:

#import "NSArray+ MyArray.h"
#import "objc/runtime.h"
@implementation NSArray MyArray)
+ (void)load {
    Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
    Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(my_objectAtIndex:));
    method_exchangeImplementations(fromMethod, toMethod);
}

- (id)my_objectAtIndex:(NSUInteger)index {
    if (self.count-1 < index) {
        // 這裏作一下異常處理,否則都不知道出錯了。
        @try {
            return [self my_objectAtIndex:index];
        }
        @catch (NSException *exception) {
            // 在崩潰後會打印崩潰信息,方便咱們調試。
            NSLog(@"---------- %s Crash Because Method %s ----------\n", class_getName(self.class), __func__);
            NSLog(@"%@", [exception callStackSymbols]);
            return nil;
    }
        @finally {}
    } else {
        return [self my_objectAtIndex:index];
    }
}
@end
複製代碼複製代碼

常見類簇真身:

類簇

5. Protocol 和 Category

5.1 Category

指向分類的結構體的指針

typedef struct objc_category *Category;

struct objc_category {
     char *category_name OBJC2_UNAVAILABLE; // 分類名
     char *class_name OBJC2_UNAVAILABLE; // 分類所屬的類名
     struct objc_method_list *instance_methods OBJC2_UNAVAILABLE; // 實例方法列表
     struct objc_method_list *class_methods OBJC2_UNAVAILABLE; // 類方法列表,Meta Class方法列表的子集
     struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 分類所實現的協議列表
}
複製代碼複製代碼

示例代碼

#import <UIKit/UIKit.h>
#import "AppDelegate.h"

@interface NSObject (Fuck)
+ (void)foo;
@end

@implementation NSObject (Fuck)
- (void)foo {
    NSLog(@"我是Foo %@",[self class]);
}
@end

int main(int argc, char * argv[]) {
    @autoreleasepool {
        
        [NSObject foo];
        [[NSObject new] foo];
        
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

複製代碼複製代碼

輸出:

2018-05-04 16:23:26.643100+0800 RuntimeDemo[48558:2377362] 我是Foo NSObject
2018-05-04 16:23:26.644354+0800 RuntimeDemo[48558:2377362] 我是Foo NSObject
複製代碼複製代碼

objc runtime加載後NSObject的Sark Category被加載,頭文件+(void)foo沒有IMP,只會出現一個warning。被加到Class的Method list裏的方法只有-(void)foo,Meta Class的方法列表裏沒有。

執行[NSObject foo]時,會在Meta Class的Method list裏找,找不着就繼續往super class裏找,NSObject Meta Clas的super class是NSObject自己,這時在NSObject的Method list裏就有foo這個方法了,可以正常輸出。

執行[[NSObject new] foo]就簡單的多了,[NSObject new]生成一個實例,實例的Method list是有foo方法的,因而正常輸出。

若是換作其餘類就會報錯了

5.2 Protocol

Protocol其實就是一個對象結構體

typedef struct objc_object Protocol;
複製代碼複製代碼

操做函數:

// 返回指定的協議
Protocol * objc_getProtocol ( const char *name );
// 獲取運行時所知道的全部協議的數組
Protocol ** objc_copyProtocolList ( unsigned int *outCount );
// 建立新的協議實例
Protocol * objc_allocateProtocol ( const char *name );
// 在運行時中註冊新建立的協議
void objc_registerProtocol ( Protocol *proto ); //建立一個新協議後必須使用這個進行註冊這個新協議,可是註冊後不可以再修改和添加新方法。
// 爲協議添加方法
void protocol_addMethodDescription ( Protocol *proto, SEL name, const char *types, BOOL isRequiredMethod, BOOL isInstanceMethod );
// 添加一個已註冊的協議到協議中
void protocol_addProtocol ( Protocol *proto, Protocol *addition );
// 爲協議添加屬性
void protocol_addProperty ( Protocol *proto, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount, BOOL isRequiredProperty, BOOL isInstanceProperty );
// 返回協議名
const char * protocol_getName ( Protocol *p );
// 測試兩個協議是否相等
BOOL protocol_isEqual ( Protocol *proto, Protocol *other );
// 獲取協議中指定條件的方法的方法描述數組
struct objc_method_description * protocol_copyMethodDescriptionList ( Protocol *p, BOOL isRequiredMethod, BOOL isInstanceMethod, unsigned int *outCount );
// 獲取協議中指定方法的方法描述
struct objc_method_description protocol_getMethodDescription ( Protocol *p, SEL aSel, BOOL isRequiredMethod, BOOL isInstanceMethod );
// 獲取協議中的屬性列表
objc_property_t * protocol_copyPropertyList ( Protocol *proto, unsigned int *outCount );
// 獲取協議的指定屬性
objc_property_t protocol_getProperty ( Protocol *proto, const char *name, BOOL isRequiredProperty, BOOL isInstanceProperty );
// 獲取協議採用的協議
Protocol ** protocol_copyProtocolList ( Protocol *proto, unsigned int *outCount );
// 查看協議是否採用了另外一個協議
BOOL protocol_conformsToProtocol ( Protocol *proto, Protocol *other );

複製代碼複製代碼

6. 補充

6.1 Super

在Objective-C中,若是咱們須要在類的方法中調用父類的方法時,一般都會用到super,以下所示:

@interface MyViewController: UIViewController
@end
@implementation MyViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    // do something
    ...
}
@end
複製代碼複製代碼

super與self不一樣。self是類的一個隱藏參數,每一個方法的實現的第一個參數即爲self。而super並非隱藏參數,它實際上只是一個」編譯器標示符」,它負責告訴編譯器,當調用viewDidLoad方法時,去調用父類的方法,而不是本類中的方法。而它實際上與self指向的是相同的消息接收者。爲了理解這一點,咱們先來看看super的定義

struct objc_super { id receiver; Class superClass; };
複製代碼複製代碼

這個結構體有兩個成員:

(1)receiver:即消息的實際接收者

(2)superClass:指針當前類的父類

當咱們使用super來接收消息時,編譯器會生成一個objc_super結構體。就上面的例子而言,這個結構體的receiver就是MyViewController對象,與self相同;superClass指向MyViewController的父類UIViewController。

接下來,發送消息時,不是調用objc_msgSend函數,而是調用objc_msgSendSuper函數,其聲明以下:

id objc_msgSendSuper ( struct objc_super *super, SEL op, ... );
複製代碼複製代碼

該函數第一個參數即爲前面生成的objc_super結構體,第二個參數是方法的selector。該函數實際的操做是:從objc_super結構體指向的superClass的方法列表開始查找viewDidLoad的selector,找到後以objc->receiver去調用這個selector,而此時的操做就是以下方式了:

objc_msgSend(objc_super->receiver, @selector(viewDidLoad))
複製代碼複製代碼

因爲objc_super->receiver就是self自己,因此該方法實際與下面這個調用是相同的

objc_msgSend(self, @selector(viewDidLoad))
複製代碼複製代碼

以下:

+ (void)load {
    [super load];
    NSLog(@"self class: %@", self.class);
    NSLog(@"super class: %@", super.class);
}
複製代碼複製代碼

輸出:

2018-05-04 15:19:45.264902+0800 RuntimeDemo[47032:2208798] self class: ViewController
2018-05-04 15:19:45.265792+0800 RuntimeDemo[47032:2208798] super class: ViewController

複製代碼複製代碼

6.2 庫相關操做

庫相關的操做主要是用於獲取由系統提供的庫相關的信息,主要包含如下函數:

// 獲取全部加載的Objective-C框架和動態庫的名稱
const char ** objc_copyImageNames ( unsigned int *outCount );

// 獲取指定類所在動態庫
const char * class_getImageName ( Class cls );

// 獲取指定庫或框架中全部類的類名
const char ** objc_copyClassNamesForImage ( const char *image, unsigned int *outCount );
複製代碼複製代碼

經過這幾個函數,咱們能夠了解到某個類全部的庫,以及某個庫中包含哪些類。以下代碼所示:

- (void)testImage {
    NSLog(@"獲取指定類所在動態庫");
    NSLog(@"UIView's Framework: %s", class_getImageName(NSClassFromString(@"UIView")));
    NSLog(@"獲取指定庫或框架中全部類的類名");
    unsigned int outCount;
    const char ** classes = objc_copyClassNamesForImage(class_getImageName(NSClassFromString(@"UIView")), &outCount);
    for (int i = 0; i < outCount; i++) {
        NSLog(@"class name: %s", classes[i]);
    }
}
複製代碼複製代碼

輸出:

2018-05-04 15:30:51.342342+0800 RuntimeDemo[47333:2253385] 獲取指定類所在動態庫
2018-05-04 15:30:51.342499+0800 RuntimeDemo[47333:2253385] UIView's Framework: /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/UIKit.framework/UIKit 2018-05-04 15:30:51.342620+0800 RuntimeDemo[47333:2253385] 獲取指定庫或框架中全部類的類名 2018-05-04 15:30:51.343164+0800 RuntimeDemo[47333:2253385] class name: UIGestureKeyboardIntroduction 2018-05-04 15:30:51.343269+0800 RuntimeDemo[47333:2253385] class name: _UIPreviewPresentationPlatterView 2018-05-04 15:30:51.343364+0800 RuntimeDemo[47333:2253385] class name: UIKeyboardUISettings 2018-05-04 15:30:51.343456+0800 RuntimeDemo[47333:2253385] class name: _UIFocusScrollManager 2018-05-04 15:30:51.343550+0800 RuntimeDemo[47333:2253385] class name: _UIPickerViewTopFrame 2018-05-04 15:30:51.343655+0800 RuntimeDemo[47333:2253385] class name: _UIOnePartImageView 2018-05-04 15:30:51.343749+0800 RuntimeDemo[47333:2253385] class name: _UIPickerViewSelectionBar 。。。。。。。。。。。。。。。。。。。。。 複製代碼複製代碼

6.3 塊操做

咱們都知道block給咱們帶到極大的方便,蘋果也不斷提供一些使用block的新的API。同時,蘋果在runtime中也提供了一些函數來支持針對block的操做,這些函數包括:

// 建立一個指針函數的指針,該函數調用時會調用特定的block
IMP imp_implementationWithBlock ( id block );

// 返回與IMP(使用imp_implementationWithBlock建立的)相關的block
id imp_getBlock ( IMP anImp );

// 解除block與IMP(使用imp_implementationWithBlock建立的)的關聯關係,並釋放block的拷貝
BOOL imp_removeBlock ( IMP anImp );
複製代碼複製代碼

imp_implementationWithBlock函數:參數block的簽名必須是method_return_type ^(id self, method_args …)形式的。該方法能讓咱們使用block做爲IMP。以下代碼所示:

- (void)testBlock {
    IMP imp = imp_implementationWithBlock(^(id obj, NSString *str) {
        NSLog(@"testBlock - %@",str);
    });
    class_addMethod(self.class, @selector(testBlock:), imp, "v@:@");
    [self performSelector:@selector(testBlock:) withObject:@"邱學偉!"];
}
複製代碼複製代碼

輸出:

2018-05-04 15:41:47.221228+0800 RuntimeDemo[47587:2282146] testBlock - 邱學偉!
複製代碼複製代碼

6.4 弱引用操做

操做函數:

// 加載弱引用指針引用的對象並返回
id objc_loadWeak ( id *location );

// 存儲__weak變量的新值
id objc_storeWeak ( id *location, id obj );
複製代碼複製代碼

objc_loadWeak函數:該函數加載一個弱指針引用的對象,並在對其作retain和autoreleasing操做後返回它。這樣,對象就能夠在調用者使用它時保持足夠長的生命週期。該函數典型的用法是在任何有使用__weak變量的表達式中使用。

objc_storeWeak函數:該函數的典型用法是用於__weak變量作爲賦值對象時。

相關文章
相關標籤/搜索