Objective-C 2.0編程語言(一) 類與對象

Objective-C

Objective-C是對C的擴充,它最初的設計目的是給C語言一個通過簡單且直觀的方式而實現面向對象編程的能力。本系列用于自我查疑。

面向對象的程序都是圍繞對象而構建,一個對象把數據 和一些對這些數據的操作(即Methods)捆綁在一起,打包成一個獨立的編程單元。

例如你正在寫一個畫圖程序,這個程序允許用戶創建諸如線、弧線、矩形、文本、位圖等,那么你就可以創建關于這些基本圖形的很多類。例如一個矩形對象,應該有標識它的位置的實例變量和寬與高的信息,可能還會有別的實例變量來定義它的顏色、矩形是否被填充、直線樣式等。這個Rectangle類會有一些方法用來設置一個實例的位置、大小、顏色、填充狀態、直線樣式,還應該提供一個方法用來繪制自己。
在Objective-C中,Object的實例變量屬于Object的內部數據,通常要訪問這些數據只能通過對象的方法,你還可以通過作用域指示符(scope directives)為一個類的子類或別的對象指定訪問實例變量的權限。要獲取對象別的信息,必須提供一個相應的方法,例如,Rectangle類應該有專用的方法來獲取它的大小和位置。而且,對象只能看到為它自己而設計的方法,它不會把別的對象的方法運用到自己身上。就像一個函數隱藏局部變量一樣,對象不但隱藏實例變量,還會隱藏方法的實現。

一、OC中的類

編譯器為每個類定義一個類對象(Class_object)。Class_objectClass的編譯版本,而它所構建的對象被稱為類的實例。在你的程序中真正做工作的是類在運行時對象創建的那些實例。一個類的所有實例有同一套方法,而且有相同一套實例變量。每個對象都有自己的實例變量,但他們卻共享這套方法。根據約定,類名以大寫字母開頭,如Rectangle,而實例變量通常以小寫字母開頭,如myRect。

  • 繼承(Inheritance)
    類的定義通常是添加式的,一個新的類往往都基于另外一個類,而這個新類繼承了原來類的方法和實例變量。新類通常簡單地添加實例變量或者修改它所繼承的方法,它不需要復制繼承的代碼。繼承將這些類連接成一個只有一個根繼承關系樹。在OC中,寫基于功能框架的代碼時,這個根類通常是NSObject。每個類(除了根類)都有一個父類,而每個類,包括根類都可以成為任何數量子類的父類。
  • 抽象類
    有些類被設計為不能實例化,他們只能作為super類被其他類所繼承。這些類往往自身是不完整的,而只是包含了一些有用的代碼,這些代碼能夠被子類所繼承,從而減少子類的代碼量。NSObject類就是一個重要的抽象類。程序中經常會定義NSObject的子類并使用這些子類的實例,但從來沒有直接使用這個類的實例的。抽象類通常包含一些幫助定義應用程序架構的代碼,當你定義這些類的子類時,這些子類的實例能很好地適應這種應用程序架構,并能夠自動地和別的對象協作。由于抽象類只有定義了子類才能成為一個有用的類,因此它們常常被稱為抽象超類。
  • 類類型
    類實際上是一類對象的詳細說明,定義了一個數據類型。這個類型不但基于它所定義的數據結構,還取決于它所定義的一些行為,這些行為就是方法。因此,類名可以出現在任何C語言允許的類型說明符出現的地方,例如作為sizeof操作符的參數:Int i = sizeof(Rectangle);
  • 靜態類型匹配
    Rectangle *myRect;
    這種聲明對象的方式為編譯器提供了對象種類的信息,所以被稱為靜態類型匹配,靜態類型匹配使編譯器具備了一些類型檢查功能,例如如果一個對象接收一個沒有定義的消息時,可以發出警告,而如果把對象定義為id,就會放松這種限制。盡管這樣,它還是無法替代動態綁定的位置。一個對象可以被靜態地匹配為它自己的類或者它所繼承的類。例如因為繼承使得Rectangle成為一種Graphic,所以Rectangle實例可以靜態匹配為Graphic類:
    Graphic* myRect;
    這樣做之所以合法,是因為Rectangle本身就是一種Graphic,但它由于繼承了Shape和Rectangle的特性,所以又不止是一個Graphic。為了類型檢查,編譯器會把myRect當作Graphic,但在運行時環境中,它會被當作Rectangle對待。
  • 動態類型匹配
    id myRect;
    對象通常都是被定義成指針,上面的靜態匹配使得指針的含義更加明確,而id卻隱藏了這些信息。
    id類型是一種靈活的數據類型, 只表示它是一個對象,不能為編譯器提供例如實例變量,可執行操作等信息。所以每個對象在運行時必須提供這些信息。 而之所以能做到這點, 是因為每個對象都有一個isa 實例變量來標示這個對象所屬的類每個Rectangle對象都能告訴運行時系統它是一個矩形類,因此,動態類型匹配實際上發生在程序被執行時。
    不論何時,只要需要,運行時系統就能夠查明一個對象到底屬于哪個類,而這只需要查詢對象的isa實例變量就可以了。這個isa指針還為對象提供了一種稱為“自省”(introspection)的功能。編譯器會在數據機構中記錄關于類定義的信息為運行時環境所用。通過這種機制,你可以判斷一個對象是否提供了一個特定的方法,也可以獲取這個對象的超類(supperclass)的名字。當然也可以通過靜態類型匹配為編譯器提供有關對象所屬類的信息,這也是為什么類名可以作為類型名而使用了。
  • 類型自省
    實例在運行時可以獲取自己的類。例如,NSObject類中定義的isMemberOfClass(或isKindOfClass)方法可以檢查接收者是不是特定類的實例(或繼承特定類的實例)。

  • 消息語法

ObjC中使用[Receiver message]方式來進行方法調用,本質其實就是向Receiver發送message消息.而message告訴這個Receiver要做什么。

  • 給nil發送消息
    在ObjC中,給nil發送消息是合法的,但在運行時什么都不做,而發送給nil的消息帶有返回值也是合法的。
    ■如果一個方法返回一個對象、任何類型的指針、任何size小于或等于sizeof(void*)的類型,如float、 double、 long double、或者long long,那么給nil發送消息將返回0。
    ■如果一個方法返回一個數據結構,那么將這個消息傳遞給nil將為這個數據結構的每個成員都返回0.0。
    ■如果一個方法返回上述類型外的其它類型,那么將這個消息傳遞給nil,返回值為定義。
  • 多態動態綁定
    函數調用和消息的一個關鍵區別是,函數和它的參數是在編譯時綁定在一起的,而消息和接收者直到程序運行時,消息被發送才實現這種綁定。因此,響應一個消息的具體方法是在運行時才決定的,而不是在代碼被編譯的時候。Message啟動哪個Method取決于Message的Receiver,不同的Receiver可能有同名的不同Method實體,這就是多態。
    而編譯器要為一個Message找到正確的Method實體,它必須知道這個Receiver屬于什么對象,而這是一個對象只有在運行時接收到Message后才能夠真正獲取的信息. 因此Method實體的選擇是發生在運行時的。

當Message發出之后,運行時環境會查看Receiver以及它與消息同名的Method,它通過名字匹配找到這個Receiver的Method實體,并調用,同時傳遞一個指向Receiver實例變量的指針。Message中的方法名是用來選擇 Receiver的Method實體,因此,Message中的方法名也被稱為(selector)選擇器.
Method和Message的動態綁定,以及多態之間的協調合作,給了面向對象編程豐富的靈活性和強大的功能,因為每個對象都可以有一個方法的自己的版本,但僅僅是接收相同消息的對象不同,而這些都可以在程序運行時完成,即不同的對象以各自的方式響應相同的消息.
可以簡化編程接口,容許在類與類之間重用一些習慣性的命名.

這樣你可以編寫應用于任何不同種類對象的代碼,而不用去考慮到底是應用到什么樣的對象上.
這些對象甚至可以是尚未開發的,或者是由別的項目的程序員開發的.(ps.如果你寫了一個向id類型變量發送display消息的代碼,任何擁有display方法的對象都可能成為這個消息的接收者。)

  • 方法和選取器:
    選取器確定的是方法名,不是方法的實現, 這是多態和動態綁定的基礎.它使得向不同對象發送相同的消息成為現實.
  • 方法返回值和參數類型:
    消息機制是通過選取器找到方法的返回值類型和參數類型. 因此: 動態綁定需要同名方法的實現 擁有相同返回值類型和相同的參數類型;否則,運行時可能出現找不到對應方法的錯誤.(有一個例外,雖然同名靜態方法和實例方法擁有相同的選取器,但是它們可以有不同的參數類型和返回值類型。)
  // SEL和@selector區別:選擇器的類型是SEL.而 @selector指示符是用來引用選擇器的, 它返回類型是SEL.
    SEL response;      
    response = @selector(load:)

    // 1. 通過字符串來得到選取器:
    responseSEL = NSSelectorFromString(@"loadDataForTableView:");
    //  2 . 通過選擇器轉換來得到方法名: 
    NSString  *methodName = NSStringFromSelector(responseSEL);

1.1 類對象

一個類的定義會包含豐富的信息(詳見附錄),但大部分都是關于這個類的實例的,如:
■類名及超類
■關于實例變量的完整描述
■關于方法名及其參數的說明
■方法的實現
所有這些信息都被編譯被保存在一個運行時系統能夠訪問的數據結構中。編譯器創建一個而且只創建一個對象來表達這些信息,那就是類對象,因此,在OC中,所有的類自身也是一個對象。
雖然類對象保存了類的屬性,但它本身并不是一個類的實例。它沒有自己的實例變量,而且它也不能執行為類實例設計的方法。不過,類定義了可以包含只為類對象使用的方法,這就是類方法(靜態方法),這些方法不同于實例方法(動態方法)。另外,類對象會繼承所有超對象的類方法,就像類實例可以繼承所有超類實例的方法一樣。
在實際代碼中,類名就代表類對象,下面的例子中,Rectangle類使用繼承自NSObject的方法返回類的版本號:
Int versionNumber = [Rectangle version];
但只有在作為接收者接收一個消息時,類名才能代表類對象,在別的地方,你必須通過給類實例或給類發送class消息來獲得一個類id,如:
Id aClass = [anObject class];
Id rectClass = [Rectangle class];
如上例所述,和所有其它的對象一樣,類對象可以被轉化為id類型,而且類對象還可以更精確地轉化為類類型,如:
Class aClass = [anObject class];
Class rectClass = [Rectangle class];
所有的類都屬于類對象,使用Class類型和使用類名進行靜態類型匹配是等效的。因此,類對象也像類實例那樣,可以進行動態類型匹配、接收消息以及從別的類繼承方法。不同之處在于它們是由編譯器產生的,沒有自己的數據結構,它們是用于運行時系統產生類實例的代理。

  • 創建實例
    類對象的主要用途是用于創建新的實例,下面這個例子告訴Rectangle類創建一個Rectangle實例,并將其賦值給myRect變量:
    id myRect;
    myRect = [Rectangle alloc];
    alloc方法會為這個實例的每個實例變量動態分配內存,并將除連接該實例和它的類的 isa變量外的所有實例變量全部清零,一個對象要能真正派上用場,它還必須完整地初始化,這就是init方法的作用,通常init方法都緊挨著alloc方法,如:
    myRect = [[Rectangle alloc] init];
    在一個實例能夠接收消息前,對實例進行這樣的初始化是必須的。Alloc方法返回一個新的實例,然后init方法對這個事例進行初始化。每個類對象至少擁有一個方法(如alloc)才能讓它具備創建新對象的能力。而每個實例至少一個方法(如init)讓它能夠有用。
    而初始化方法通常帶有一些參數用來傳遞特定的值,而且用標簽來標示這些參數,例如initWithPositon: size:,但不管怎樣,它都是以init開頭。
  • 變量和類對象
    定義一個類,你可以為它定義實例變量,這個類的每個實例都保留一份這些實例變量的拷貝——每個對象管理自己的數據。
    但是你發現沒有,OC中并沒有類似實例變量那樣的所謂的“類變量”。而且類對象無法訪問任何實例的實例變量,它不能初始化、讀寫這些變量。為了讓類的所有實例共享數據,你必須定于外部變量(不是在類中定義的變量)。最簡單的辦法是像下面的代碼片斷那樣在類的實現文件中聲明一個變量:
int MCLSGlobalVariable;
@implementation MyClass
...
@end

更巧妙的做法是,你可以把變量聲明為靜態的,并提供類方法來管理它。靜態變量會把它的作用域限定在類范圍,而且僅限于當前文件的類的實現部分。并不像實例變量那樣,靜態變量不能被繼承,不能被子類直接操作。這種特性常常用于定于類的共享實例(如單一模式,參考“創建單例”部分)。靜態變量能夠幫助類對象提供一種比“工廠模式”創建實例更多的功能,而且更接近一個完整和通用的對象。

static MyClass *MCLSSharedInstance;
@implementation MyClass
+ (MyClass *)sharedInstance
{
// check for existence of shared instance
// create if necessary
return MCLSSharedInstance;
}
// implementation continues
  • 初始化類對象
    如果類對象不分配實例,它就可以向類的實例那樣初始化,盡管程序并不為類對象分配內存,但Objective-C確實也提供了一個初始化類對象的途徑,如果類使用靜態或全局變量,那么initialize是一個初始化這些變量的好地方。
    在類接受任何其它消息前,運行時環境會給類對象發送一個initialize消息,當然這發生在它的超類收到initialize消息之后。這就給這個類一個在被使用前建立運行時環境的機會。
    如果沒有初始化工作要做,你就沒有必要實現一個initialize方法來響應這個消息。因為繼承關系的存在,給一個沒有實現initialize的類發送initialize消息,這個消息將會被傳遞到它的超類,即使超類已經受到這個消息也是如此。
    例如,A類實現了initialize方法,而類B繼承自類A,但類B沒有實現initialize方法,在類B接收它的第一個消息之前,運行時環境會為它發送一個initialize消息。但由于類B沒有實現initialize方法,類A的initialize方法會被執行,因此,類A需要確保它的初始化邏輯只被執行一次。為了確保初始化邏輯只執行一次,請使用如下的模板實現initialize方法:
+ (void)initialize
{
  static BOOL initialized = NO;
  if (!initialized) {
  // Perform initialization here.
  ...
  initialized = YES;
  }
}
  • 根類方法
    所有的類和實例, 都需要有一個和runtime環境交互的接口。 類對象和實例都必須對自己有能力完成自省,并能夠匯報自己在繼承關系中所處的位置,所以要實現兩次:一次為實例提供可以和運行時環境交互的接口,另一次在類對象中復制這些接口。 而NSObject會提供這些接口,所以NSObject不需要實現兩次。類對象扮演另外一個角色,就是執行根類中定義的實例方法。當類對象接收一個不能響應消息時,運行時環境會判斷是否有根實例方法可以響應這個消息。類對象能夠執行的唯一的實例方法就是那些在根類中定義的方法,而且還必須是沒有類方法能完成這項工作時才會由根類方法來完成。有關類方法執行實例方法的特殊能力,請參考有關NSObject類的說明。
  • 代碼中的類名
    在代碼中,類名職能在兩種不同的上下文環境中出現,這兩種情況也反映了類名作為數據類型和對象的雙重角色:
    Rectangle * anObject;
    這里anObject被明確地定義為指向Rectangle的一個指針。編譯器會認為它具有Rectangle的數據結構和Rectangle的實例方法以及Rectangle所繼承的方法。靜態類型匹配可以讓編譯器做到更好的類型檢查并使得代碼更加清晰。但只有實例才能靜態地進行類型匹配,類對象不能,因為它們不屬于某個類的一種。
    而在發送消息的表達式中,作為消息的接收者,類名代表的是類對象,這種用法在前面的例子中已經多次展示過。只有作為消息接收者,類名才能代表一個類對象。在任何別的上下文環境中,必須要先通過發送一個class消息,獲取類對象的id,例如:
    if ( [anObject isKindOfClass:[Rectangle class]] )
    如果你不知道在編譯時類名稱,僅僅是用“ Rectangle”這個名稱作為參數,NSClassFromString將返回類對象:
    NSString *className;
    if ( [anObject isKindOfClass:NSClassFromString(className)] )
    如果className字符串不是一個有效的類名,返回nil。類名和全局變量存在同一個nameSpace,具有唯一性。

附錄1:

類的基礎數據結構:

typedef struct objc_class *Class;
struct objc_class { // 類的數據結構
    Class isa  OBJC_ISA_AVAILABILITY; // 
#if !__OBJC2__
    Class super_class                       OBJC2_UNAVAILABLE;  // 父類,如果該類已經是最頂層的根類(如NSObject或NSProxy),則super_class為NULL。
    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;  // 方法緩存
    struct objc_protocol_list *protocols    OBJC2_UNAVAILABLE;  // 協議鏈表
#endif
} OBJC2_UNAVAILABLE;

類實例數據結構:

struct objc_object {

    Class isa  OBJC_ISA_AVAILABILITY;

};
typedef struct objc_object *id;

可以看到,這個結構體只有一個字體,即指向其類的isa指針。因此id其實就是指向Class類型的指針。這樣,當我們向一個Objective-C對象發送消息時,運行時庫會根據實例對象的isa指針找到這個實例對象所屬的類。Runtime庫會在類的方法列表及父類的方法列表中去尋找與消息對應的selector指向的方法。找到后即運行這個方法。
當創建一個特定類的實例對象時,分配的內存包含一個objc_object數據結構,然后是類的實例變量的數據。NSObject類的alloc和allocWithZone:方法使用函數class_createInstance來創建objc_object數據結構。
另外還有我們常見的id,它是一個objc_object結構類型的指針。它的存在可以讓我們實現類似于C++中泛型的一些操作。該類型的對象可以轉換為任何一種對象,有點類似于C語言中void *指針類型的作用。

  1. isa:
    isa指針指向其類.
    NSArray *array = [NSArray array];
    顯而易見, array的數據結構中isa指針指向NSArray。
    而其實在這個例子中,+array消息發送給了NSArray類,這個NSArray也是一個對象,類對象。它也包含一個指向其類的一個isa指針。那么這些就有一個問題了,這個isa指針指向什么呢?即metaClass元類。 meta-class是一個類對象的類。當我們向一個對象發送消息時,runtime會在這個對象所屬的這個類的方法列表中查找方法;而向一個類發送消息時,會在這個類的meta-class的方法列表中查找。
    meta-class之所以重要,是因為它存儲著一個類的所有類方法。每個類都會有一個單獨的meta-class,因為每個類的類方法基本不可能完全相同。
    如果你有想法,再深入一下,meta-class也是一個類,也可以向它發送一個消息,那么它的isa又是指向什么呢?為了不讓這種結構無限延伸下去,Objective-C的設計者讓所有的meta-class的isa指向基類的meta-class,以此作為它們的所屬類。
  2. super_class:
    指向該類的父類,如果該類已經是最頂層的根類(如NSObject或NSProxy),則super_class為NULL。
  3. cache:
    用于緩存最近使用的方法。一個接收者對象接收到一個消息時,它會根據isa指針去查找能夠響應這個消息的對象。在實際使用中,這個對象只有一部分方法是常用的,很多方法其實很少用或者根本用不上。這種情況下,如果每次消息來時,我們都是methodLists中遍歷一遍,性能勢必很差。這時,cache就派上用場了。在我們每次調用過一個方法后,這個方法就會被緩存到cache列表中,下次調用的時候runtime就會優先去cache中查找,如果cache沒有,才去 methodLists中查找方法。這樣,對于那些經常用到的方法的調用,但提高了調用的效率。結構如下:
struct objc_cache {
/* mask :一個整數,指定分配的緩存bucket的總數。在方法查找過程中,
Objective-C runtime使用這個字段來確定開始線性查找數組的索引位
置。指向方法selector的指針與該字段做一個AND位操作(index = 
(mask & selector))。這可以作為一個簡單的hash散列算法。*/
    unsigned int mask /* total = mask + 1 */                 OBJC2_UNAVAILABLE;
  /*occupied:一個整數,指定實際占用的緩存bucket的總數。*/
    unsigned int occupied                                    OBJC2_UNAVAILABLE;
  /*buckets:指向Method數據結構指針的數組。這個數組可能包含不
超過mask+1個元素。需要注意的是,指針可能是NULL,表示這個緩
存bucket沒有被占用,另外被占用的bucket可能是不連續的。這個數
組可能會隨著時間而增長。*/
    Method buckets[1]                                        OBJC2_UNAVAILABLE;

};
  1. version:
    我們可以使用這個字段來提供類的版本信息。

附錄2

類與對象操作函數

runtime提供了大量的函數來直接操作類與對象數據結構,對照附錄1。類的操作方法大部分是以class為前綴的,而對象的操作方法大部分是以objc或object_為前綴。下面我們將根據這些方法的用途來分類討論這些方法的使用。

1. 類相關操作函數

我們可以回過頭去看看objc_class的定義,runtime提供的操作類的方法主要就是針對這個結構體中的各個字段的。

類名(name)

類名操作的函數主要有:
// 獲取類的類名
const char * class_getName ( Class cls );

  • 對于class_getName函數,如果傳入的cls為Nil,則返回一個char字符串。

父類(super_class)和元類(meta-class)

父類和元類操作的函數主要有:
// 獲取類的父類
Class class_getSuperclass ( Class cls );
// 判斷給定的Class是否是一個元類
BOOL class_isMetaClass ( Class cls );

  • class_getSuperclass函數,當cls為Nil或者cls為根類時,返回Nil。不過通常我們可以使用NSObject類的superclass方法來達到同樣的目的。
  • class_isMetaClass函數,如果是cls是元類,則返回YES;如果否或者傳入的cls為Nil,則返回NO。

實例變量大小(instance_size)

實例變量大小操作的函數有:
// 獲取實例大小
size_t class_getInstanceSize ( Class cls );

成員變量(ivars)及屬性

在objc_class中,所有的成員變量、屬性的信息是放在鏈表ivars中的。ivars是一個數組,數組中每個元素是指向Ivar(變量信息)的指針。runtime提供了豐富的函數來操作這一字段。大體上可以分為以下幾類:

  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 );
    // 獲取整個成員變量列表
    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()來釋放這個數組。
  1. 屬性操作函數,主要包含以下函數:
    // 獲取指定的屬性
    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 );
    這一種方法也是針對ivars來操作,不過只操作那些是屬性的值。我們在后面介紹屬性時會再遇到這些函數。

  2. 在MAC OS X系統中,我們可以使用垃圾回收器。runtime提供了幾個函數來確定一個對象的內存區域是否可以被垃圾回收器掃描,以處理strong/weak引用。這幾個函數定義如下:
    const uint8_t * class_getIvarLayout ( Class cls );
    void class_setIvarLayout ( Class cls, const uint8_t *layout );
    const uint8_t * class_getWeakIvarLayout ( Class cls );
    void class_setWeakIvarLayout ( Class cls, const uint8_t *layout );
    但通常情況下,我們不需要去主動調用這些方法;在調用objc_registerClassPair時,會生成合理的布局。在此不詳細介紹這些函數。

方法(methodLists)

方法操作主要有以下函數:
// 添加方法
BOOL class_addMethod ( Class cls, SEL name, IMP imp, const char *types );
// 獲取實例方法
Method class_getInstanceMethod ( Class cls, SEL name );
// 獲取類方法
Method class_getClassMethod ( Class cls, SEL name );
// 獲取所有方法的數組
Method * class_copyMethodList ( Class cls, unsigned int *outCount );
// 替代方法的實現
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 ....
    }
    與成員變量不同的是,我們可以為類動態添加方法,不管這個類是否已存在。

另外,參數types是一個描述傳遞給方法的參數類型的字符數組,這就涉及到類型編碼,我們將在后面介紹。

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

協議(objc_protocol_list)

協議相關的操作包含以下函數:
// 添加協議
BOOL class_addProtocol ( Class cls, Protocol *protocol );
// 返回類是否實現指定的協議
BOOL class_conformsToProtocol ( Class cls, Protocol *protocol );
// 返回類實現的協議列表
Protocol * class_copyProtocolList ( Class cls, unsigned int *outCount );

  • class_conformsToProtocol函數可以使用NSObject類的conformsToProtocol:方法來替代。
  • class_copyProtocolList函數返回的是一個數組,在使用后我們需要使用free()手動釋放。

版本(version)

版本相關的操作包含以下函數:
// 獲取版本號
int class_getVersion ( Class cls );
// 設置版本號
void class_setVersion ( Class cls, int version );

其它

runtime還提供了兩個函數來供CoreFoundation的tool-free bridging使用,即:
Class objc_getFutureClass ( const char *name );
void objc_setFutureClass ( Class cls, const char *name );
通常我們不直接使用這兩個函數。

2. 實例相關操作函數

實例操作函數主要是針對我們創建的實例對象的一系列操作函數,我們可以使用這組函數來從實例對象中獲取我們想要的一些信息,如實例對象中變量的值。這組函數可以分為三小類:

  1. 針對整個對象進行操作的函數,這類函數包含
    // 返回指定對象的一份拷貝
    id object_copy ( id obj, size_t size );
    // 釋放指定對象占用的內存
    id object_dispose ( id obj );
    有這樣一種場景,假設我們有類A和類B,且類B是類A的子類。類B通過添加一些額外的屬性來擴展類A。現在我們創建了一個A類的實例對象,并希望在運行時將這個對象轉換為B類的實例對象,這樣可以添加數據到B類的屬性中。這種情況下,我們沒有辦法直接轉換,因為B類的實例會比A類的實例更大,沒有足夠的空間來放置對象。此時,我們就要以使用以上幾個函數來處理這種情況,如下代碼所示:
    NSObject *a = [[NSObject alloc] init];
    id newB = object_copy(a, class_getInstanceSize(MyClass.class));
    object_setClass(newB, MyClass.class);
    object_dispose(a);

  2. 針對對象實例變量進行操作的函數,這類函數包含:
    // 修改類實例的實例變量的值
    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 );
    如果實例變量的Ivar已經知道,那么調用object_getIvar會比object_getInstanceVariable函數快,相同情況下,object_setIvar也比object_setInstanceVariable快。

  3. 針對對象的類進行操作的函數,這類函數包含:
    // 返回給定對象的類名
    const char * object_getClassName ( id obj );
    // 返回對象的類
    Class object_getClass ( id obj );
    // 設置對象的類
    Class object_setClass ( id obj, Class cls );

獲取類定義

Objective-C動態運行庫會自動注冊我們代碼中定義的所有的類。我們也可以在運行時創建類定義并使用objc_addClass函數來注冊它們。runtime提供了一系列函數來獲取類定義相關的信息,這些函數主要包括:
// 獲取已注冊的類定義的列表
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_getClassList函數:獲取已注冊的類定義的列表。我們不能假設從該函數中獲取的類對象是繼承自NSObject體系的,所以在這些類上調用方法是,都應該先檢測一下這個方法是否在這個類中實現。
    下面代碼演示了該函數的用法:
int numClasses;
Class * classes = NULL;
numClasses = objc_getClassList(NULL, 0);
if (numClasses > 0) {
    classes = malloc(sizeof(Class) * numClasses);
    numClasses = objc_getClassList(classes, numClasses);
    NSLog(@"number of classes: %d", numClasses);
    for (int i = 0; i < numClasses; i++) {
    Class cls = classes[i];
    NSLog(@"class name: %s", class_getName(cls));
  }
  free(classes);
}

輸出結果如下:
2014-10-23 16:20:52.589 RuntimeTest[8437:188589] number of classes: 1282
2014-10-23 16:20:52.589 RuntimeTest[8437:188589] class name: DDTokenRegexp
2014-10-23 16:20:52.590 RuntimeTest[8437:188589] class name: _NSMostCommonKoreanCharsKeySet
2014-10-23 16:20:52.590 RuntimeTest[8437:188589] class name: OS_xpc_dictionary
2014-10-23 16:20:52.590 RuntimeTest[8437:188589] class name: NSFileCoordinator
2014-10-23 16:20:52.590 RuntimeTest[8437:188589] class name: NSAssertionHandler
2014-10-23 16:20:52.590 RuntimeTest[8437:188589] class name: PFUbiquityTransactionLogMigrator
2014-10-23 16:20:52.591 RuntimeTest[8437:188589] class name: NSNotification
2014-10-23 16:20:52.591 RuntimeTest[8437:188589] class name: NSKeyValueNilSetEnumerator
2014-10-23 16:20:52.591 RuntimeTest[8437:188589] class name: OS_tcp_connection_tls_session
2014-10-23 16:20:52.591 RuntimeTest[8437:188589] class name: _PFRoutines
......還有大量輸出

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

  • objc_getMetaClass函數:如果指定的類沒有注冊,則該函數會調用類處理回調,并再次確認類是否注冊,如果確認未注冊,再返回nil。不過,每個類定義都必須有一個有效的元類定義,所以這個函數總是會返回一個元類定義,不管它是否有效。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容