Runtime梳理(五)

挖就挖底層.png

繼上Runtime梳理(四)

通過前面的學習,我們了解到Objective-C的動態特性:Objective-C不僅需要一個編譯器,還需要一個運行時系統來執行編譯的代碼,這個運行時系統就像一個操作系統一樣:它讓所有的工作可以正常的運行。這個運行時系統即Objc Runtime。Objc Runtime其實是一個Runtime庫。

這個庫主要做了兩件事:

  1. 封裝:在這個庫中,對象可以用C語言中的結構體表示,而方法可以用C函數來實現,另外再加上了一些額外的特性。這些結構體和函數被runtime函數封裝后,我們就可以在程序運行時創建,檢查,修改類、對象和它們的方法了。
  2. 找出方法的最終執行代碼:當程序執行[object doSomething]時,會向消息接收者(object)發送一條消息(doSomething),runtime會根據消息接收者是否能響應該消息而做出不同的反應。這將在后面詳細介紹。

復習幾個定義:isa super cache version

第一部分:概念性的新東西

新東西 1 :

NSString *string = [[NSString alloc] init]; 
  • 注意流程:
1.  [NSString alloc]先被執行。因為NSString沒有+alloc方法,于是去父類NSObject去查找。
2.  檢測NSObject是否響應+alloc方法,發現響應,于是檢測NSString類,并根據其所需的內存空間大小開始分配內存空間,然后把isa指針指向NSString類。同時,+alloc也被加進cache列表里面。
3.  接著,執行-init方法,如果NSString響應該方法,則直接將其加入cache;如果不響應,則去父類查找。
4.  在后期的操作中,如果再以[[NSString alloc] init]這種方式來創建數組,則會直接從cache中取出相應的方法,直接調用。

補充:

  • objc_object 結構體指針:當創建一個特定類的實例對象時,分配的內存包含一個objc_object數據結構,然后是類的實例變量的數據。NSObject類的alloc和allocWithZone:方法使用函數class_createInstance來創建objc_object數據結構。id 也是這類指針

新東西 2 :

struct objc_cache {
    unsigned int mask /* total = mask + 1 */                 OBJC2_UNAVAILABLE;
    unsigned int occupied                                    OBJC2_UNAVAILABLE;
    Method buckets[1]                                        OBJC2_UNAVAILABLE;
}; 
  • 這段代碼的解讀:
    1. mask:一個整數,指定分配的緩存bucket的總數。在方法查找過程中,Objective-C runtime使用這個字段來確定開始線性查找數組的索引位置。指向方法selector的指針與該字段做一個AND位操作(index = (mask & selector))。這可以作為一個簡單的hash散列算法。
    2. occupied:一個整數,指定實際占用的緩存bucket的總數。
    3. buckets:指向Method數據結構指針的數組。這個數組可能包含不超過mask+1個元素。需要注意的是,指針可能是NULL,表示這個緩存bucket沒有被占用,另外被占用的bucket可能是不連續的。這個數組可能會隨著時間而增長。

補充:元類(Meta Class)

NSString *string = [[NSString string];
  +string消息發送給了NSString類,而這個NSString也是一個對象。既然是對象,那么它也是一個objc_object指針,它包含一個指向其類的一個isa指針。那么這些就有一個問題了,這個isa指針指向什么呢?為了調用+string方法,這個類的isa指針必須指向一個包含這些類方法的一個objc_class結構體。這就引出了meta-class的概念

meta-class是一個類對象的類。
當我們向一個對象發送消息時,runtime會在這個對象所屬的這個類的方法列表中查找方法;而向一個類發送消息時,會在這個類的meta-class的方法列表中查找。
meta-class之所以重要,是因為它存儲著一個類的所有類方法。每個類都會有一個單獨的meta-class,因為每個類的類方法基本不可能完全相同。

新東西 3 :

// 獲取類中指定名稱實例成員變量的信息
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 ) ;
// 獲取整個成員變量列表
Ivar * class_copyIvarList ( Class cls, unsigned int *outCount );
  • class_getInstanceVariable函數,它返回一個指向包含name指定的成員變量信息的objc_ivar結構體的指針(Ivar)。
  • class_getClassVariable函數,目前沒有找到關于Objective-C中類變量的信息,一般認為Objective-C不支持類變量。注意,返回的列表不包含父類的成員變量和屬性。
  • Objective-C不支持往已存在的類中添加實例變量,因此不管是系統庫提供的提供的類,還是我們自定義的類,都無法動態添加成員變量。但如果我們通過運行時來創建一個類的話,又應該如何給它添加成員變量呢?這時我們就可以使用class_addIvar函數了。不過需要注意的是,這個方法只能在objc_allocateClassPair函數與objc_registerClassPair之間調用。另外,這個類也不能是元類。成員變量的按字節最小對齊量是1<<alignment。這取決于ivar的類型和機器的架構。如果變量的類型是指針類型,則傳遞log2(sizeof(pointer_type))。
  • class_copyIvarList函數,它返回一個指向成員變量信息的數組,數組中每個元素是指向該成員變量信息的objc_ivar結構體的指針。這個數組不包含在父類中聲明的變量。outCount指針返回數組的大小。需要注意的是,我們必須使用free()來釋放這個數組。

有個注意點:

// 替代方法的實現
IMP class_replaceMethod ( Class cls, SEL name, IMP imp, const char *types );
// 返回方法的具體實現
IMP class_getMethodImplementation ( Class cls, SEL name );
IMP class_getMethodImplementation_stret ( Class cls, SEL name );
// 類實例是否響應指定的selector
BOOL class_respondsToSelector ( Class cls, SEL sel );

class_addMethod的實現會覆蓋父類的方法實現,但不會取代本類中已存在的實現,如果本類中包含一個同名的實現,則函數會返回NO。如果要修改已存在實現,可以使用method_setImplementation。一個Objective-C方法是一個簡單的C函數,它至少包含兩個參數—self和_cmd。所以,我們的實現函數(IMP參數指向的函數)至少需要兩個參數,如下所示:
void myMethodIMP(id self, SEL _cmd) { // implementation .... }

新東西 4:

參數types是一個描述傳遞給方法的參數類型的字符數組,這就涉及到類型編碼(后面再講)

  1. class_getInstanceMethod、class_getClassMethod函數,與class_copyMethodList不同的是,這兩個函數都會去搜索父類的實現。
  2. class_copyMethodList函數,返回包含所有實例方法的數組,如果需要獲取類方法,則可以使用class_copyMethodList(object_getClass(cls), &count)(一個類的實例方法是定義在元類里面)。該列表不包含父類實現的方法。outCount參數返回方法的個數。在獲取到列表后,我們需要使用free()方法來釋放它。
  3. class_replaceMethod函數,該函數的行為可以分為兩種:如果類中不存在name指定的方法,則類似于class_addMethod函數一樣會添加方法;如果類中已存在name指定的方法,則類似于 4. method_setImplementation一樣替代原方法的實現。
    class_getMethodImplementation函數,該函數在向類實例發送消息時會被調用,并返回一個指向方法實現函數的指針。這個函數會比method_getImplementation(class_getInstanceMethod(cls, name))更快。返回的函數指針可能是一個指向runtime內部的函數,而不一定是方法的實際實現。例如,如果類實例無法響應selector,則返回的函數指針將是運行時消息轉發機制的一部分。
  4. class_respondsToSelector函數,我們通常使用NSObject類的respondsToSelector:或instancesRespondToSelector:方法來達到相同目的。

補充:
runtime還提供了兩個函數來供CoreFoundation的tool-free bridging使用:

Class objc_getFutureClass ( const char *name );
void objc_setFutureClass ( Class cls, const char *name );

新東西 5 :

// 獲取已注冊的類定義的列表
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 );

代碼解析:
獲取類定義的方法有三個:objc_lookUpClass, objc_getClass和objc_getRequiredClass。如果類在運行時未注冊,則objc_lookUpClass會返回nil,而objc_getClass會調用類處理回調,并再次確認類是否注冊,如果確認未注冊,再返回nil。而objc_getRequiredClass函數的操作與objc_getClass相同,只不過如果沒有找到類,則會殺死進程。

objc_getMetaClass函數:如果指定的類沒有注冊,則該函數會調用類處理回調,并再次確認類是否注冊,如果確認未注冊,再返回nil。不過,每個類定義都必須有一個有效的元類定義,所以這個函數總是會返回一個元類定義,不管它是否有效。

第二部分:關聯對象

1. 關聯對象

把關聯對象想象成一個Objective-C對象(如字典),這個對象通過給定的key連接到類的一個實例上。不過由于使用的是C接口,所以key是一個void指針(const void *)。我們還需要指定一個內存管理策略,以告訴Runtime如何管理這個對象的內存。這個內存管理的策略可以由以下值指定:

OBJC_ASSOCIATION_ASSIGN
OBJC_ASSOCIATION_RETAIN_NONATOMIC
OBJC_ASSOCIATION_COPY_NONATOMIC
OBJC_ASSOCIATION_RETAIN
OBJC_ASSOCIATION_COPY

就是這樣的,前面我們講過的,只是沒有這種認識罷了

  • 當宿主對象被釋放時,會根據指定的內存管理策略來處理關聯對象。如果指定的策略是assign,則宿主釋放時,關聯對象不會被釋放;而如果指定的是retain或者是copy,則宿主釋放時,關聯對象會被釋放。我們甚至可以選擇是否是自動retain/copy。當我們需要在多個線程中處理訪問關聯對象的多線程代碼時,這就非常有用了。
  • 我們將一個對象連接到其它對象所需要做的就是下面兩行代碼:
static char myKey;
objc_setAssociatedObject(self, &myKey, anObject, OBJC_ASSOCIATION_RETAIN);
  • 在這種情況下,self對象將獲取一個新的關聯的對象anObject,且內存管理策略是自動retain關聯對象,當self對象釋放時,會自動release關聯對象。另外,如果我們使用同一個key來關聯另外一個對象時,也會自動釋放之前關聯的對象,這種情況下,先前的關聯對象會被妥善地處理掉,并且新的對象會使用它的內存。
  • 案例:動態地將一個Tap手勢操作連接到任何UIView中,并且根據需要指定點擊后的實際操作。這時候我們就可以將一個手勢對象及操作的block對象關聯到我們的UIView對象中。這項任務分兩部分。首先,如果需要,我們要創建一個手勢識別對象并將它及block做為關聯對象。
- (void)setTapActionWithBlock:(void(^)(void))block
{
    UITapGestureRecognizer *gesture = objc_getAssociatedObject(self, &kDTActionHandlerTapGestureKey);
    if (!gesture)
    {
        gesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(__handleActionForTapGesture:)];
        [self addGestureRecognizer:gesture];
        objc_setAssociatedObject(self, &kDTActionHandlerTapGestureKey, gesture, OBJC_ASSOCIATION_RETAIN);
    }
    objc_setAssociatedObject(self, &kDTActionHandlerTapBlockKey, block, OBJC_ASSOCIATION_COPY);
}
````檢測了手勢識別的關聯對象。如果沒有,則創建并建立關聯關系。同時,將傳入的塊對象連接到指定的key上。注意block對象的關聯內存管理策略。手勢識別對象需要一個target和action,所以接下來我們定義處理方法:
  • (void)__handleActionForTapGesture:(UITapGestureRecognizer *)gesture
    {
    if (gesture.state == UIGestureRecognizerStateRecognized)
    {
    void(^action)(void) = objc_getAssociatedObject(self, &kDTActionHandlerTapBlockKey);
    if (action)
    {
    action();
    }
    }
    }
解說:檢測手勢識別對象的狀態,因為我們只需要在點擊手勢被識別出來時才執行操作。關聯對象使用起來并不復雜。它讓我們可以動態地增強類現有的功能。我們可以在實際編碼中靈活地運用這一特性

#### 2. 移除關聯:

對比:
`
// 設置關聯對象
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 );`

#### 3. 映射對象
案例:假定這樣一個場景,我們從服務端兩個不同的接口獲取相同的字典數據,但這兩個接口是由兩個人寫的,相同的信息使用了不同的字段表示。我們在接收到數據時,可將這些數據保存在相同的對象中。對象類如下定義:

@interface MyObject: NSObject
@property (nonatomic, copy) NSString * name;
@property (nonatomic, copy) NSString * status;
@end

接口A、B返回的字典數據如下所示:JSON
`@{@"name1": "張三", @"status1": @"start"}
@{@"name2": "張三", @"status2": @"end"}`

> 通常的方法是寫兩個方法分別做轉換,不過如果能靈活地運用Runtime的話,可以只實現一個轉換方法,為此,我們需要先定義一個映射字典(全局變量)
static NSMutableDictionary *map = nil;
@implementation MyObject
+ (void)load
{
    map = [NSMutableDictionary dictionary];
    map[@"name1"]                = @"name";
    map[@"status1"]              = @"status";
    map[@"name2"]                = @"name";
    map[@"status2"]              = @"status";
}
@end

上面的代碼將兩個字典中不同的字段映射到MyObject中相同的屬性上,這樣,轉換方法可如下處理:
  • (void)setDataWithDic:(NSDictionary *)dic
    {
    [dic enumerateKeysAndObjectsUsingBlock:^(NSString *key, id obj, BOOL *stop) {
    NSString *propertyKey = [self propertyForKey:key];
    if (propertyKey)
    {
    objc_property_t property = class_getProperty([self class], [propertyKey UTF8String]);
    // TODO: 針對特殊數據類型做處理
    NSString *attributeString = [NSString stringWithCString:property_getAttributes(property) encoding:NSUTF8StringEncoding];
    ...
    [self setValue:obj forKey:propertyKey];
    }
    }];
    }
這部分前面講過了,但是是實現有關前提:一個屬性能否通過上面這種方式來處理的前提是其支持KVC。

### 第三部分 :消息機制(發送與轉發)
#### 1. SEL

> SEL又叫選擇器,是表示一個方法的selector的指針,Objective-C在編譯時,會依據每一個方法的名字、參數序列,生成一個唯一的整型標識(Int類型的地址),這個標識就是SEL。那么:Objective-C同一個類(及類的繼承體系)中,不能存在2個同名的方法,即使參數類型不同也不行。相同的方法只能對應一個SEL。這也就導致Objective-C在處理相同方法名且參數個數相同但類型不同的方法方面的能力很差。
舉例:
`- (void)setWidth:(int)width;
- (void)setWidth:(double)width;`
這樣的定義被認為是一種編譯錯誤
糾正:
`-(void)setWidthIntValue:(int)width;
-(void)setWidthDoubleValue:(double)width;
`

但是:不同的類可以擁有相同的selector,這個沒有問題。不同類的實例對象執行相同的selector時,會在各自的方法列表中去根據selector去尋找自己對應的IMP。
sel:
sel_registerName函數
Objective-C編譯器提供的@selector()
NSSelectorFromString()方法

#### 2. IMP
> IMP實際上是一個函數指針,指向方法實現的首地址。`id (*IMP)(id, SEL, ...)`SEL就是為了查找方法的最終實現IMP的。由于每個方法對應唯一的SEL,因此我們可以通過SEL方便快速準確地獲得它所對應的IMP.

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

#### 3. Method 
1. 結構: 

SEL method_name OBJC2_UNAVAILABLE; // 方法名
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE; // 方法實現

解析:結構體中包含一個SEL和IMP,實際上相當于在SEL和IMP之間作了一個映射。有了SEL,我們便可以找到對應的IMP

2. 一些常見方法相關操作函數:

// 調用指定方法的實現
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 );


發現:
  - method_invoke函數,返回的是實際實現的返回值。參數receiver不能為空。這個方法的效率會比method_getImplementation和method_getName更快。
  -  method_getName函數,返回的是一個SEL。如果想獲取方法名的C字符串,可以使用sel_getName(method_getName(method))。
  -  method_getReturnType函數,類型字符串會被拷貝到dst中。
  -  method_setImplementation函數,注意該函數返回值是方法之前的實現。

#### 4. 方法選擇器
1. 函數

// 返回給定選擇器指定的方法的名稱
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系統中注冊一個方法名以獲取方法的選擇器

2. 方法調用流程
消息直到運行時才綁定到方法實現上。編譯器會將消息表達式[receiver message]轉化為一個消息函數的調用,即objc_msgSend。這個函數將消息接收者和方法名作為其基礎參數,有兩種形式:
`objc_msgSend(receiver, selector)
objc_msgSend(receiver, selector, arg1, arg2, ...)`
這個函數完成了動態綁定的所有事情:

> 首先它找到selector對應的方法實現。因為同一個方法可能在不同的類中有不同的實現,所以我們需要依賴于接收者的類來找到的確切的實現。
它調用方法實現,并將接收者對象及方法的所有參數傳給它。
最后,它將實現返回的值作為它自己的返回值。

3. 隱藏參數
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在實際中用得比較少。

#### 5. 消息轉發

> 一個對象能接收一個消息時,就會走正常的方法調用流程。但如果一個對象無法接收指定消息時,又會發生什么事呢?默認情況下,如果是以[object message]的方式調用方法,如果object無法響應message消息時,編譯器會報錯。但如果是以perform…的形式來調用,則需要等到運行時才能確定object是否能接收message消息。如果不能,則程序崩潰。`if ([self respondsToSelector:@selector(method)]) { [self performSelector:@selector(method)];}`
發生:當一個對象無法接收某一消息時,就會啟動所謂”消息轉發(message forwarding)“機制,通過這一機制,我們可以告訴對象如何處理未知的消息。默認情況下,對象接收到未知的消息,會導致程序崩潰,通過控制臺,我們可以看到以下異常信息:
`unrecognized selector sent to instance 0x100111940
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[SUTRuntimeMethod method]: unrecognized selector sent to instance 0x100111940'`
這段異常信息實際上是由NSObject的”doesNotRecognizeSelector”方法拋出的。

呵呵:
消息轉發機制基本上分為三個步驟:1.動態方法解析 2.備用接收者 3.完整轉發

1. 動態方法解析

對象在接收到未知的消息時,首先會調用所屬類的類方法+resolveInstanceMethod:(實例方法)或者+resolveClassMethod:(類方法)。在這個方法中,我們有機會為該未知消息新增一個”處理方法”“。不過使用該方法的前提是我們已經實現了該”處理方法”,只需要在運行時通過class_addMethod函數動態添加到類里面就可以了。如下代碼所示:

void functionForMethod1(id self, SEL _cmd) {
NSLog(@"%@, %p", self, _cmd);
}

  • (BOOL)resolveInstanceMethod:(SEL)sel {
    NSString *selectorString = NSStringFromSelector(sel);
    if ([selectorString isEqualToString:@"method1"]) {
    class_addMethod(self.class, @selector(method1), (IMP)functionForMethod1, "@:");
    }
    return [super resolveInstanceMethod:sel];
    }

不過這種方案更多的是為了實現@dynamic屬性。

2. 備用接收者

如果在上一步無法處理消息,則Runtime會繼續調以下方法:`- (id)forwardingTargetForSelector:(SEL)aSelector`

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

使用這個方法通常是在對象內部,可能還有一系列其它對象能處理該消息,我們便可借這些對象來處理消息并返回,這樣在對象外部看來,還是由該對象親自處理了這一消息。如下代碼所示:

@interface SUTRuntimeMethodHelper : NSObject

  • (void)method2;
    @end
    @implementation SUTRuntimeMethodHelper
  • (void)method2 {
    NSLog(@"%@, %p", self, _cmd);
    }
    @end

pragma mark -

@interface SUTRuntimeMethod () {
SUTRuntimeMethodHelper *_helper;
}
@end
@implementation SUTRuntimeMethod

  • (instancetype)object {
    return [[self alloc] init];
    }
  • (instancetype)init {
    self = [super init];
    if (self != nil) {
    _helper = [[SUTRuntimeMethodHelper alloc] init];
    }
    return self;
    }
  • (void)test {
    [self performSelector:@selector(method2)];
    }
  • (id)forwardingTargetForSelector:(SEL)aSelector {
    NSLog(@"forwardingTargetForSelector");
    NSString *selectorString = NSStringFromSelector(aSelector);
    // 將消息轉發給_helper來處理
    if ([selectorString isEqualToString:@"method2"]) {
    return _helper;
    }
    return [super forwardingTargetForSelector:aSelector];
    }
    @end

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

3. 完整消息轉發
如果在上一步還不能處理未知消息,則唯一能做的就是啟用完整的消息轉發機制了。此時會調用以下方法:

`- (void)forwardInvocation:(NSInvocation *)anInvocation
`

運行時系統會在這一步給消息接收者最后一次機會將消息轉發給其它對象。對象會創建一個表示消息的NSInvocation對象,把與尚未處理的消息有關的全部細節都封裝在anInvocation中,包括selector,目標(target)和參數。我們可以在forwardInvocation方法中選擇將消息轉發給其它對象。

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

1. 定位可以響應封裝在anInvocation中的消息的對象。這個對象不需要能處理所有未知消息。
2.使用anInvocation作為參數,將消息發送到選中的對象。anInvocation將會保留調用結果,運行時系統會提取這一結果并將其發送到消息的原始發送者。
不過,在這個方法中我們可以實現一些更復雜的功能,我們可以對消息的內容進行修改,比如追回一個參數等,然后再去觸發消息。另外,若發現某個消息不應由本類處理,則應調用父類的同名方法,以便繼承體系中的每個類都有機會處理此調用請求。

還有一個很重要的問題,我們必須重寫以下方法:
`- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector`

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

完整的示例如下所示:
  • (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
    if (!signature) {
    if ([SUTRuntimeMethodHelper instancesRespondToSelector:aSelector]) {
    signature = [SUTRuntimeMethodHelper instanceMethodSignatureForSelector:aSelector];
    }
    }
    return signature;
    }
  • (void)forwardInvocation:(NSInvocation *)anInvocation {
    if ([SUTRuntimeMethodHelper instancesRespondToSelector:anInvocation.selector]) {
    [anInvocation invokeWithTarget:_helper];
    }
    }
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;  }
`

### 第四部分: Method Swizzling(新知識點)
Method Swizzling是什么鬼? 我也不知道,感覺很牛逼就對了

其實呢:Method Swizzling是改變一個selector的實際實現的技術。通過這一技術,我們可以在運行時通過修改類的分發表中selector對應的函數,來修改方法的實現。

例如,我們想跟蹤在程序中每一個view controller展示給用戶的次數:當然,我們可以在每個view controller的viewDidAppear中添加跟蹤代碼;但是這太過麻煩,需要在每個view controller中寫重復的代碼。創建一個子類可能是一種實現方式,但需要同時創建UIViewController, UITableViewController, UINavigationController及其它UIKit中view controller的子類,這同樣會產生許多重復的代碼。

那么直接上代碼:

@implementation UIViewController (Tracking)

  • (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
    Class class = [self class];
    // When swizzling a class method, use the following:
    // Class class = object_getClass((id)self);
    SEL originalSelector = @selector(viewWillAppear:);
    SEL swizzledSelector = @selector(xxx_viewWillAppear:);
    Method originalMethod = class_getInstanceMethod(class, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
    BOOL didAddMethod = class_addMethod(class, originalSelector,
    method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
    if (didAddMethod) {class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
    } else
    {
    method_exchangeImplementations(originalMethod, swizzledMethod);
    }
    });
    }

pragma mark - Method Swizzling

  • (void)xxx_viewWillAppear:(BOOL)animated {
    [self xxx_viewWillAppear:animated];
    NSLog(@"viewWillAppear: %@", self);
    }

method swizzling修改了UIViewController的@selector(viewWillAppear:)對應的函數指針,使其實現指向了我們自定義的xxx_viewWillAppear的實現。這樣,當UIViewController及其子類的對象調用viewWillAppear時,都會打印一條日志信息。

##### Swizzling應該總是在+load中執行

在Objective-C中,運行時會自動調用每個類的兩個方法。+load會在類初始加載時調用,+initialize會在第一次調用類的類方法或實例方法之前被調用。這兩個方法是可選的,且只有在實現了它們時才會被調用。由于method swizzling會影響到類的全局狀態,因此要盡量避免在并發處理中出現競爭的情況。+load能保證在類的初始化過程中被加載,并保證這種改變應用級別的行為的一致性。相比之下,+initialize在其執行時不提供這種保證—事實上,如果在應用中沒為給這個類發送消息,則它可能永遠不會被調用。

##### Swizzling應該總是在dispatch_once中執行
與上面相同,因為swizzling會改變全局狀態,所以我們需要在運行時采取一些預防措施。原子性就是這樣一種措施,它確保代碼只被執行一次,不管有多少個線程。GCD的dispatch_once可以確保這種行為,我們應該將其作為method swizzling的最佳實踐。

#####選擇器、方法與實現
在Objective-C中,選擇器(selector)、方法(method)和實現(implementation)是運行時中一個特殊點,雖然在一般情況下,這些術語更多的是用在消息發送的過程描述中。

以下是Objective-C Runtime Reference中的對這幾個術語一些描述:

1. Selector(typedef struct objc_selector *SEL):用于在運行時中表示一個方法的名稱。一個方法選擇器是一個C字符串,它是在Objective-C運行時被注冊的。選擇器由編譯器生成,并且在類被加載時由運行時自動做映射操作。
2. Method(typedef struct objc_method *Method):在類定義中表示方法的類型
3. Implementation(typedef id (*IMP)(id, SEL, …)):這是一個指針類型,指向方法實現函數的開始位置。這個函數使用為當前CPU架構實現的標準C調用規范。每一個參數是指向對象自身的指針(self),第二個參數是方法選擇器。然后是方法的實際參數。

理解這幾個術語之間的關系最好的方式是:一個類維護一個運行時可接收的消息分發表;分發表中的每個入口是一個方法(Method),其中key是一個特定名稱,即選擇器(SEL),其對應一個實現(IMP),即指向底層C函數的指針。

為了swizzle一個方法,我們可以在分發表中將一個方法的現有的選擇器映射到不同的實現,而將該選擇器對應的原始實現關聯到一個新的選擇器中。

##### 調用_cmd
` - (void)xxx_viewWillAppear:(BOOL)animated {
    [self xxx_viewWillAppear:animated];
    NSLog(@"viewWillAppear: %@", NSStringFromClass([self class]));
}`
咋看上去是會導致無限循環的。但令人驚奇的是,并沒有出現這種情況。在swizzling的過程中,方法中的[self xxx_viewWillAppear:animated]已經被重新指定到UIViewController類的-viewWillAppear:中。在這種情況下,不會產生無限循環。不過如果我們調用的是[self viewWillAppear:animated],則會產生無限循環,因為這個方法的實現在運行時已經被重新指定為xxx_viewWillAppear:了。

##注意事項
Swizzling通常被稱作是一種黑魔法,容易產生不可預知的行為和無法預見的后果。雖然它不是最安全的,但如果遵從以下幾點預防措施的話,還是比較安全的:

1. 總是調用方法的原始實現(除非有更好的理由不這么做):API提供了一個輸入與輸出約定,但其內部實現是一個黑盒。Swizzle一個方法而不調用原始實現可能會打破私有狀態底層操作,從而影響到程序的其它部分。
2. 避免沖突:給自定義的分類方法加前綴,從而使其與所依賴的代碼庫不會存在命名沖突。
3. 明白是怎么回事:簡單地拷貝粘貼swizzle代碼而不理解它是如何工作的,不僅危險,而且會浪費學習Objective-C運行時的機會。閱讀Objective-C Runtime Reference和查看<objc/runtime.h>頭文件以了解事件是如何發生的。
4. 小心操作:無論我們對Foundation, UIKit或其它內建框架執行Swizzle操作抱有多大信心,需要知道在下一版本中許多事可能會不一樣。

### 第四部分:Runtime 總補充

1. super
`@interface MyViewController: UIViewController
@end
@implementation MyViewController
- (void)viewDidLoad {
    [super viewDidLoad];
// do something
    ...
}
@end`

super的工作:
首先我們需要知道的是super與self不同。self是類的一個隱藏參數,每個方法的實現的第一個參數即為self。而super并不是隱藏參數,它實際上只是一個”編譯器標示符”,它負責告訴編譯器,當調用viewDidLoad方法時,去調用父類的方法,而不是本類中的方法。而它實際上與self指向的是相同的消息接收者。為了理解這一點,我們先來看看super的定義:
`struct objc_super { id receiver; Class superClass; };`
這個結構體有兩個成員:
receiver:即消息的實際接收者
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))`

通俗版本:
`@interface MyClass : NSObject
@end
@implementation MyClass
- (void)test {
    NSLog(@"self class: %@", self.class);
    NSLog(@"super class: %@", super.class);
}
@end`

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 );
`

通過這幾個函數,我們可以了解到某個類所有的庫,以及某個庫中包含哪些類。如下代碼所示:
`
NSLog(@"獲取指定類所在動態庫");NSLog(@"UIView's Framework: %s", class_getImageName(NSClassFromString(@"UIView")));NSLog(@"獲取指定庫或框架中所有類的類名");const char ** classes = objc_copyClassNamesForImage(class_getImageName(NSClassFromString(@"UIView")), &outCount);for (int i = 0; i < outCount; i++) { NSLog(@"class name: %s", classes[i]);}
`
> 我們來看一下輸出:
`2014-11-08 12:57:32.689 [747:184013] 獲取指定類所在動態庫
2014-11-08 12:57:32.690 [747:184013] UIView's Framework: /System/Library/Frameworks/UIKit.framework/UIKit
2014-11-08 12:57:32.690 [747:184013] 獲取指定庫或框架中所有類的類名
2014-11-08 12:57:32.691 [747:184013] class name: UIKeyboardPredictiveSettings
2014-11-08 12:57:32.691 [747:184013] class name: _UIPickerViewTopFrame
2014-11-08 12:57:32.691 [747:184013] class name: _UIOnePartImageView
2014-11-08 12:57:32.692 [747:184013] class name: _UIPickerViewSelectionBar
2014-11-08 12:57:32.692 [747:184013] class name: _UIPickerWheelView
2014-11-08 12:57:32.692 [747:184013] class name: _UIPickerViewTestParameters

......`

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 );`

mp_implementationWithBlock函數:參數block的簽名必須是method_return_type ^(id self, method_args …)形式的。該方法能讓我們使用block作為IMP。如下代碼所示:
`@interface MyRuntimeBlock : NSObject
@end
@implementation MyRuntimeBlock
@end
`


`
// 測試代碼
IMP imp = imp_implementationWithBlock(^(id obj, NSString *str) { NSLog(@"%@", str);});class_addMethod(MyRuntimeBlock.class, @selector(testBlock:), imp, "v@:@");MyRuntimeBlock *runtime = [[MyRuntimeBlock alloc] init];[runtime performSelector:@selector(testBlock:) withObject:@"hello world!"];
`

4. 弱引用操作
`// 加載弱引用指針引用的對象并返回
id objc_loadWeak ( id *location );
// 存儲__weak變量的新值
id objc_storeWeak ( id *location, id obj );
`
1. objc_loadWeak函數:該函數加載一個弱指針引用的對象,并在對其做retain和autoreleasing操作后返回它。這樣,對象就可以在調用者使用它時保持足夠長的生命周期。該函數典型的用法是在任何有使用__weak變量的表達式中使用。
2. objc_storeWeak函數:該函數的典型用法是用于__weak變量做為賦值對象時。
3. 這兩個函數的具體實施在此不舉例,有興趣的小伙伴可以參考《Objective-C高級編程:iOS與OS X多線程和內存管理》中對__weak實現的介紹。

5. 宏定義
在runtime中,還定義了一些宏定義供我們使用,有些值我們會經常用到,如表示BOOL值的YES/NO;而有些值不常用,如OBJC_ROOT_CLASS。在此我們做一個簡單的介紹。

布爾值 `#define YES  (BOOL)1 #define NO   (BOOL)0 `
這兩個宏定義定義了表示布爾值的常量,需要注意的是YES的值是1,而不是非0值。

 空值 `#define nil  __DARWIN_NULL #define Nil  __DARWIN_NULL`
其中nil用于空的實例對象,而Nil用于空類對象。

分發函數原型 `#define OBJC_OLD_DISPATCH_PROTOTYPES  1`
該宏指明分發函數是否必須轉換為合適的函數指針類型。當值為0時,必須進行轉換

Objective-C根類 `#define OBJC_ROOT_CLASS `
如果我們定義了一個Objective-C根類,則編譯器會報錯,指明我們定義的類沒有指定一個基類。這種情況下,我們就可以使用這個宏定義來避過這個編譯錯誤。該宏在iOS 7.0后可用。
其實在NSObject的聲明中,我們就可以看到這個宏的身影,如下所示:

__OSX_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0)
OBJC_ROOT_CLASS
OBJC_EXPORT
@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}


局部變量存儲時長: #define NS_VALID_UNTIL_END_OF_SCOPE 
該宏表明存儲在某些局部變量中的值在優化時不應該被編譯器強制釋放。

我們將局部變量標記為id類型或者是指向ObjC對象類型的指針,以便存儲在這些局部變量中的值在優化時不會被編譯器強制釋放。相反,這些值會在變量再次被賦值之前或者局部變量的作用域結束之前都會被保存。
 
代碼原出處:

[Objective-C Runtime1](http://southpeak.github.io/blog/2014/10/25/objective-c-runtime-yun-xing-shi-zhi-lei-yu-dui-xiang/)
[Objective-C Runtime2](http://southpeak.github.io/blog/2014/10/30/objective-c-runtime-yun-xing-shi-zhi-er-:cheng-yuan-bian-liang-yu-shu-xing/)
[Objective-C Runtime3](http://southpeak.github.io/blog/2014/11/03/objective-c-runtime-yun-xing-shi-zhi-san-:fang-fa-yu-xiao-xi-zhuan-fa/)
[Objective-C Runtime4](http://southpeak.github.io/blog/2014/11/06/objective-c-runtime-yun-xing-shi-zhi-si-:method-swizzling/)
[Objective-C Runtime5](http://southpeak.github.io/blog/2014/11/08/objective-c-runtime-yun-xing-shi-zhi-wu-:xie-yi-yu-fen-lei/)
[Objective-C Runtime6](http://southpeak.github.io/blog/2014/11/09/objective-c-runtime-yun-xing-shi-zhi-liu-:shi-yi/)

向原著致敬!
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,345評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,494評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,283評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,953評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,714評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,410評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,940評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,776評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,210評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,654評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373

推薦閱讀更多精彩內容

  • 轉至元數據結尾創建: 董瀟偉,最新修改于: 十二月 23, 2016 轉至元數據起始第一章:isa和Class一....
    40c0490e5268閱讀 1,751評論 0 9
  • 我們常常會聽說 Objective-C 是一門動態語言,那么這個「動態」表現在哪呢?我想最主要的表現就是 Obje...
    Ethan_Struggle閱讀 2,223評論 0 7
  • 這篇文章完全是基于南峰子老師博客的轉載 這篇文章完全是基于南峰子老師博客的轉載 這篇文章完全是基于南峰子老師博客的...
    西木閱讀 30,578評論 33 466
  • 轉載:http://yulingtianxia.com/blog/2014/11/05/objective-c-r...
    F麥子閱讀 753評論 0 2
  • 微信公眾號:西風谷 閱讀量:62 點贊:5 在某高校中,有一地名曰生活區。 普天之下莫非王土,率土之濱莫非王臣...
    Ricardo_Clown閱讀 338評論 0 0