Objective-C對象模型及應用

本文主要介紹Objective-C對象模型的實現細節,以及Objective-C語言對象模型中對isa swizzling和method swizzling的支持。但願本文能加深你對Objective-C對象的理解。html

ISA指針

Objective-C是一門面向對象的編程語言。每個對象都是一個類的實例。在Objective-C語言的內部,每個對象都有一個名爲isa的指針,指向該對象的類。每個類描述了一系列它的實例的特色,包括成員變量的列表,成員函數的列表等。每個對象均可以接受消息,而對象可以接收的消息列表是保存在它所對應的類中。ios

在XCode中按Shift + Command + O, 而後輸入NSObject.h和objc.h,能夠打開NSObject的定義頭文件,經過頭文件咱們能夠看到,NSObject就是一個包含isa指針的結構體,以下圖所示:git

 

按照面向對象語言的設計原則,全部事物都應該是對象(嚴格來講Objective-C並無徹底作到這一點,由於它有象int, double這樣的簡單變量類型)。在Objective-C語言中,每個類實際上也是一個對象。每個類也有一個名爲isa的指針。每個類也能夠接受消息,例如[NSObject alloc],就是向NSObject這個類發送名爲alloc消息。github

在XCode中按Shift + Command + O, 而後輸入runtime.h,能夠打開Class的定義頭文件,經過頭文件咱們能夠看到,Class也是一個包含isa指針的結構體,以下圖所示。(圖中除了isa外還有其它成員變量,但那是爲了兼容非2.0版的Objective-C的遺留邏輯,你們能夠忽略它。)編程

由於類也是一個對象,那它也必須是另外一個類的實列,這個類就是元類(metaclass)。元類保存了類方法的列表。當一個類方法被調用時,元類會首先查找它自己是否有該類方法的實現,若是沒有,則該元類會向它的父類查找該方法,直到一直找到繼承鏈的頭。安全

元類(metaclass)也是一個對象,那麼元類的isa指針又指向哪裏呢?爲了設計上的完整,全部的元類的isa指針都會指向一個根元類(root metaclass)。根元類(root metaclass)自己的isa指針指向本身,這樣就行成了一個閉環。上面提到,一個對象可以接收的消息列表是保存在它所對應的類中的。在實際編程中,咱們幾乎不會遇到向元類發消息的狀況,那它的isa指針在實際上不多用到。不過這麼設計保證了面向對象的乾淨,即全部事物都是對象,都有isa指針。服務器

咱們再來看看繼承關係,因爲類方法的定義是保存在元類(metaclass)中,而方法調用的規則是,若是該類沒有一個方法的實現,則向它的父類繼續查找。因此,爲了保證父類的類方法能夠在子類中能夠被調用,因此子類的元類會繼承父類的元類,換而言之,類對象和元類對象有着一樣的繼承關係。網絡

我很想把關係說清楚一些,可是這塊兒確實有點繞,下面這張圖或許可以讓你們對isa和繼承的關係清楚一些app

該圖中,最讓人困惑的莫過於Root Class了。在實現中,Root Class是指NSObject,咱們能夠從圖中看出:編程語言

  1. NSObject類包括它的對象實例方法。

  2. NSObject的元類包括它的類方法,例如alloc方法。

  3. NSObject的元類繼承自NSObject類。

  4. 一個NSObject的類中的方法同時也會被NSObject的子類在查找方法時找到。

類的成員變量

若是把類的實例當作一個C語言的結構體(struct),上面說的isa指針就是這個結構體的第一個成員變量,而類的其它成員變量依次排列在結構體中。排列順序以下圖所示(圖片來自《iOS 6 Programming Pushing the Limits》):

爲了驗證該說法,咱們在XCode中新建一個工程,在main.m中運行以下代碼:

#import <UIKit/UIKit.h>

@interface Father : NSObject {
   int _father;
}

@end

@implementation Father

@end

@interface Child : Father {
   int _child;
}

@end

@implementation Child

@end


int main(int argc, char * argv[])
{

 Child * child = [[Child alloc] init];
 @autoreleasepool {
     // ...
 }
}

咱們將斷點下在 @autoreleasepool 處,而後在Console中輸入p *child,則能夠看到Xcode輸出以下內容,這與咱們上面的說法一致。

(lldb) p *child
(Child) $0 = {
 (Father) Father = {
   (NSObject) NSObject = {
     (Class) isa = Child
   }
   (int) _father = 0
 }
 (int) _child = 0
}

可變與不可變

由於對象在內存中的排布能夠當作一個結構體,該結構體的大小並不能動態變化。因此沒法在運行時動態給對象增長成員變量。

相對的,對象的方法定義都保存在類的可變區域中。Objective-C 2.0並未在頭文件中將實現暴露出來,但在Objective-C 1.0中,咱們能夠看到方法的定義列表是一個名爲 methodLists的指針的指針(以下圖所示)。經過修改該指針指向的指針的值,就能夠實現動態地爲某一個類增長成員方法。這也是Category實現的原理。同時也說明了爲何Category只可爲對象增長成員方法,卻不能增長成員變量。

須要特別說明一下,經過objc_setAssociatedObject 和 objc_getAssociatedObject方法能夠變相地給對象增長成員變量,但因爲實現機制不同,因此並非真正改變了對象的內存結構。

除了對象的方法能夠動態修改,由於isa自己也只是一個指針,因此咱們也能夠在運行時動態地修改isa指針的值,達到替換對象整個行爲的目的。不過該應用場景較少。

系統相關API及應用

isa swizzling的應用

系統提供的KVO的實現,就利用了動態地修改isa指針的值的技術。在蘋果的文檔中能夠看到以下描述:

Key-Value Observing Implementation Details

Automatic key-value observing is implemented using a technique called isa-swizzling.

The isa pointer, as the name suggests, points to the object’s class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.

When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.

You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.

Method Swizzling API說明

Objective-C提供瞭如下API來動態替換類方法或實例方法的實現:

  • class_replaceMethod 替換類方法的定義

  • method_exchangeImplementations 交換2個方法的實現

  • method_setImplementation 設置1個方法的實現

這3個方法有一些細微的差異,給你們介紹以下:

  • class_replaceMethod在蘋果的文檔(以下圖所示)中能看到,它有兩種不一樣的行爲。當類中沒有想替換的原方法時,該方法會調用class_addMethod來爲該類增長一個新方法,也由於如此,class_replaceMethod在調用時須要傳入types參數,而method_exchangeImplementations和method_setImplementation卻不須要。

  • method_exchangeImplementations 的內部實現至關於調用了2次method_setImplementation方法,從蘋果的文檔中能清晰地瞭解到(以下圖所示)

從以上的區別咱們能夠總結出這3個API的使用場景:

  • class_replaceMethod, 當須要替換的方法可能有不存在的狀況時,能夠考慮使用該方法。

  • method_exchangeImplementations,當須要交換2個方法的實現時使用。

  • method_setImplementation 最簡單的用法,當僅僅須要爲一個方法設置其實現方式時使用。

以上3個方法的源碼在這裏,感興趣的同窗能夠讀一讀。

使用示例

咱們在開發客戶端的筆記功能時,須要使用系統的UIImagePickerController。可是,咱們發現,在iOS6.0.2系統下,系統提供的UIImagePickerController在iPad橫屏下有轉屏的Bug,形成其方向錯誤。具體的Bug詳情能夠見這裏

爲了修復該Bug,咱們須要替換UIImagePickerController的以下2個方法

- (BOOL)shouldAutorotate;
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation;

咱們先實現了一個名爲ImagePickerReplaceMethodsHolder的類,用於定義替換後的方法和實現。以下所示:

// ImagePickerReplaceMethodsHolder.h
@interface ImagePickerReplaceMethodsHolder : NSObject

- (BOOL)shouldAutorotate;
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation;

@end

// ImagePickerReplaceMethodsHolder.m
@implementation ImagePickerReplaceMethodsHolder

- (BOOL)shouldAutorotate {
   return NO;
}

- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
   return UIInterfaceOrientationPortrait;
}


@end

開源界的使用

有少許不明真相的同窗覺得蘋果在審覈時會拒絕App使用以上API,這實際上是對蘋果的誤解。使用如上API是安全的。另外,開源界也對以上方法都適當的使用。例如:

  • 著名的網絡庫AFNetworking。AFNetworking網絡庫(v1.x版本)使用了class_replaceMethod方法(AFHTTPRequestOperation.m文件第105行)

  • Nimbus。Nimbus是著名的工具類庫,它在其core模塊中提供了NIRuntimeClassModifications.h文件,用於提供上述API的封裝。

  • 國內的大衆點評iOS客戶端。該客戶端使用了他們本身開發的基於Wax修改而來的WaxPatch,WaxPatch能夠實現經過服務器更新來動態修改客戶端的邏輯。而WaxPatch主要是修改了wax中的wax_instance.m文件,在其中加入了class_replaceMethod來替換原始實現,從而實現修改客戶端的原有行爲。

總結

經過本文,咱們瞭解到了Objective-C語言的對象模型,以及Objective-C語言對象模型中對isa swizzling和method swizzling的支持。本文也經過具體的實例代碼和開源項目,讓咱們對該對象模型提供的動態性有了更加深入的認識。

相關文章
相關標籤/搜索