天天看點

從C/C++語言到Objective-C語言

從C/C++語言到Objective-C語言

Objective-C,通常寫作ObjC和較少用的Objective C或Obj-C,是擴充C的面向對象程式設計語言。是以有一定C/C++語言基礎了解和掌握Objective-C也會相應的快些。這回,我們将比較着學習Objective-C語言,掌握其文法并了解其思想。

文法

讓我們先來看看C++和Objective-C中對于類的宣言 :

C++

#include "BaseClass.h"

class MyClass : public BaseClass
{
public:
                    MyClass();
    virtual         ~MyClass();

    virtual int     GetValue() const;
    virtual void    SetValue(int     inValue);

    bool            IsValid() const;

    static MyClass* GetInstance();

private:
    int             mValue;
    static MyClass* sInstance;
};
           

Objective-C

<span style="font-size:18px;">#import "BaseClass.h"

@interface MyClass : BaseClass

{
    int         mValue;

}

- (int)         getValue;
- (void)        setValue:   (int)   inValue;
- (BOOL)        isValid;
</span>
+ (MyClass*)    getInstance;

@end
           

通過比較上面兩段代碼,從文法的角度上我們看到 Objective-C 語言有以下特點:

  • 用 #import 取代了 #include
#import 相當于 C/C++ 語言中的 #include+#pragma once。當頭檔案嵌套包含的時候,它的作用就發揮出來了。
當某一頭檔案已經被讀取後,又一次被 #include 的時候,#pragma once 這會跳過該次讀取。
比如我們在C/C++語言的頭檔案中常常這樣定義,就是為了實作 #pragma once 而做的 :      
#ifndef INCLUDED_BASECLASS_H
#include    "BaseClass.h"
#endif
           
  • 繼承的時候沒有限定符
繼承都是 public 的。      
  • 沒有建構和虛構函數
  • 成員變量/函數沒有限定符
成員變量預設是   private  protected  的,而函數是 public 的。      
  • 沒有const關鍵字
  • 沒有virtual關鍵字
Objective-C 中函數預設的就是 virtual 的。      

接下來再看看具體的實作 :

C++:

#include    "MyClass.h"
#include    <assert.h>

MyClass*    MyClass::sInstance = 0;

MyClass::MyClass() : mValue(0)
{
}

MyClass::~MyClass()
{
    mValue = -1;
}

int MyClass::GetValue() const
{
    return (mValue);
}

void MyClass::SetValue(int     inValue)
{
    assert(IsValid());
    mValue = inValue;
}

bool MyClass::IsValid() const
{
    return (0 <= inValue && inValue <= 1000);
}

MyClass* MyClass::GetInstance()
{
    if (sInstance == 0) {
        sInstance = new MyClass();
    }
    return (sInstance);
}
           

Objective-C:

#import "MyClass.h"

static MyClass* sInstance = 0;

@implementation MyClass

- (int)         getValue
{
    return (mValue);
}

- (void)     setValue:  (int)         inValue
{
    NSParameterAssert([self isValid]);
    mValue = inValue;
}

- (BOOL)            isValid
{
    return (0 <= inValue && inValue <= 1000);
}

+ (MyClass*)        getInstance
{
    if (sInstance == 0) {
        sInstance = [MyClass alloc];
    }
    return (sInstance);
}

@end
           
  • 執行個體方法
方法前面的“-”是執行個體方法(類似于C++中的類成員函數)      
  • 類方法
字首為“+”的是類方法(類似于C++中的靜态成員函數,或者是全局函數)      
  • 類變量
與C/C++語言中的靜态變量一樣,Objective-C 中的類變量就是以 static 聲明的變量。(隻在目前定義檔案中有效)
如果子類也想參照父類中的類變量的時候,須定義屬性參照方法(類方法)。(這與面向對象中的封裝概念有所背馳,降低了凝聚度)      
  • 單一繼承
Objective-C 與 Java 語言一樣,都是單一繼承。
如果想實作多重繼承,可以隻用類似Java 中 implements 的方法。(Objective-C 中叫做 protocol)      
  • 發送消息
Objective-C 中類似于C/C++中函數調用的地方都被稱作“發送消息”。調用某個函數,被稱為發送了某個消息。其形式如下圖所示      
從C/C++語言到Objective-C語言

Objective-C的發送消息

  • 方法,SEL,方法實作
Objective-C 中方法,SEL型,實作的關系如如下圖所示 :      
從C/C++語言到Objective-C語言

概念

SEL,IMP的定義

接下來,我們來看看 Objective-C 語言中的頭檔案 objc.h 的定義 :

// objc.h

typedef struct objc_class *Class;

typedef struct objc_object {
  Class isa;
} *id;

typedef struct objc_selector  *SEL;
typedef id      (*IMP)(id, SEL, …);
typedef signed char   BOOL;

#define YES             (BOOL)1
#define NO              (BOOL)0

#ifndef Nil
#define Nil 0   /* id of Nil class */
#endif

#ifndef nil
#define nil 0   /* id of Nil instance */
#endif
           
id

id和void *并非完全一樣。在上面的代碼中,id是指向struct objc_object的一個指針,這個意思基本上是說,id是一個指向任何一個繼承了Object(或者NSObject)類的對象。需要注意的是id是一個指針,是以你在使用id的時候不需要加星号。比如id foo=nil定義了一個nil指針,這個指針指向NSObject的一個任意子類。而id *foo=nil則定義了一個指針,這個指針指向另一個指針,被指向的這個指針指向NSObject的一個子類。

從C/C++語言到Objective-C語言
nil

nil和C語言的NULL相同,在objc/objc.h中定義。nil表示一個Objctive-C對象,這個對象的指針指向空(沒有東西就是空)。

Nil

首字母大寫的Nil和nil有一點不一樣,Nil定義一個指向空的類(是Class,而不是對象)。

SEL

SEL是“selector”的一個類型,表示一個方法的名字。比如以下方法:

-[Foo count] 和 -[Bar count] 使用同一個selector,它們的selector叫做count。

在上面的頭檔案裡我們看到,SEL是指向 struct objc_selector的指針,但是objc_selector是什麼呢?那麼實際上,你使用GNU Objective-C的運作時間庫和NeXT Objective-C的運作運作時間庫(Mac OS X使用NeXT的運作時間庫)時,它們的定義是不一樣的。實際上Mac OSX僅僅将SEL映射為C字元串。比如,我們定義一個Foo的類,這個類帶有一個- (int) blah方法,那麼以下代碼:

NSLog (@"SEL=%s", @selector(blah));
           

會輸出為 SEL=blah。說白了SEL就是傳回方法名。

這樣的機制大大的增加了我們的程式的靈活性,我們可以通過給一個方法傳遞SEL參數,讓這個方法動态的執行某一個方法;我們也可以通過配置檔案指定需要執行的方法,程式讀取配置檔案之後把方法的字元串翻譯成為SEL變量然後給相應的對象發送這個消息。

在 Objective-C 運作時庫中,selector 是作為數組來管理的。這都是從效率的角度出發:函數調用的時候,不是通過方法名字比較而是指針值的比較來查找方法,由于整數的查找和比對比字元串要快得多,是以這樣可以在某種程度上提高執行的效率。

這樣就必須保證所有類中的 selector 須指向同一實體(數組)。一旦有新的類被定義,其中的 selector 也需要映射到這個數組中。

實際情況下,總共有兩種 selector 的數組:預先定義好的内置selector數組和用于動态追加的selector數組。

  • 内置selector
簡單地說,内置的selector就是一個大的字元串數組。定義在objc-sel-table.h檔案中:      
#define NUM_BUILTIN_SELS 16371
/* base-2 log of greatest power of 2 < NUM_BUILTIN_SELS */
#define LG_NUM_BUILTIN_SELS 13

static const char * const _objc_builtin_selectors[NUM_BUILTIN_SELS] = {
    ".cxx_construct",
    ".cxx_destruct",
    "CGColorSpace",
    "CGCompositeOperationInContext:",
    "CIContext",
    "CI_affineTransform",
    "CI_arrayWithAffineTransform:",
    "CI_copyWithZone:map:",
    "CI_initWithAffineTransform:",
    "CI_initWithRect:",
    "CI_rect",
    "CTM",
    "DOMDocument",
    "DTD",
    ...

};
           
可以看到,數組的大小NUM_BUILTIN_SELS定義為16371。字元串按照字母順序排序,簡單的都是為了運作時檢索的速度(二分法查找)。
從定義好的 selector 名稱我們可以看到一些新的方法名稱,比如 CIConetext,CI開頭的方法是由Tiger開始導入的程式庫。
每次系統更新的時候,這個數組也是需要更新的。      
  • 動态追加selector
另一個用于動态追加的 selector,其定義在 objc-sel.m 和 objc-sel-set.m  檔案中
新的 selector 都被追加到 _buckets 成員中,其中追加和搜尋使用 Hash 算法。      
static struct __objc_sel_set *_objc_selectors = NULL;

struct __objc_sel_set {
    uint32_t _count;
    uint32_t _capacity;
    uint32_t _bucketsNum;
    SEL *_buckets;
};
           
IMP

從上面的頭檔案中我們可以看到,IMP定義為

id (*IMP) (id, SEL, …)。
           

這樣說來,IMP是一個指向函數的指針,這個被指向的函數包括id(“self”指針),調用的SEL(方法名),再加上一些其他參數。說白了IMP就是實作方法。

我們取得了函數指針之後,也就意味着我們取得了執行的時候的這段方法的代碼的入口,這樣我們就可以像普通的C語言函數調用一樣使用這個函數指針。當然我們可以把函數指針作為參數傳遞到其他的方法,或者執行個體變量裡面,進而獲得極大的動态性。我們獲得了動态性,但是付出的代價就是編譯器不知道我們要執行哪一個方法是以在編譯的時候不會替我們找出錯誤,我們隻有執行的時候才知道,我們寫的函數指針是否是正确的。是以,在使用函數指針的時候要非常準确地把握能夠出現的所有可能,并且做出預防。尤其是當你在寫一個供他人調用的接口API的時候,這一點非常重要。

方法的定義

在頭檔案 objc-class.h 中,有方法的定義 :

typedef struct objc_method *Method;

struct objc_method {
    SEL method_name;
    char *method_types;
    IMP method_imp;
};
           

這個定義看上去包括了我們上面說過的其他類型。也就是說,Method(我們常說的方法)表示一種類型,這種類型與selector和實作(implementation)相關。

最初的SEL是方法的名稱method_name。char型的method_types表示方法的參數。最後的IMP就是實際的函數指針,指向函數的實作。

Class的定義

Class(類)被定義為一個指向struct objc_class的指針,在objc/objc-class.h中它是這麼定義的:

struct objc_class {
  struct objc_class *isa;                /* metaclass */
  struct objc_class *super_class;        /* 父類 */
  const char *name;                      /* 類名稱 */
  long version;                          /* 版本 */
  long info;                             /* 類資訊 */
  long instance_size;                    /* 執行個體大小 */
  struct objc_ivar_list *ivars;          /* 執行個體參數連結清單 */
  struct objc_method_list **methodLists; /* 方法連結清單 */
  struct objc_cache *cache;              /* 方法的緩存 */
  struct objc_protocol_list *protocols;  /* protocol連結清單 */
};
           

由以上的結構資訊,我們可以像類似于C語言中結構體操作一樣來使用成員。比如下面取得類的名稱:

Class cls;
cls = [NSString class];

printf("class name %s\n", ((struct objc_class*)cls)->name);
           

發送消息與函數調用的不同

Objective-C的消息傳送如下圖所示 :

從C/C++語言到Objective-C語言

發送消息的過程,可以總結為以下内容 :

  • 首先,指定調用的方法
  • 為了方法調用,取得 selector
源代碼被編譯以後,方法被解釋為 selector。這裡的 selector 隻是單純的字元串。      
  • 消息發送給對象B
消息傳送使用到了 objc_msgSend 運作時API。這個API隻是将 selector 傳遞給目标對象B。      
  • 從 selector 取得實際的方法實作
首先,從對象B取得類的資訊,查詢方法的實作是否被緩存(上面類定義中的struct objc_cache *cache;)。如果沒有被緩
存,則在方法連結清單中查詢(上面類定義中的struct objc_method_list **methodLists;)。      
  • 執行
利用函數指針,調用方法的實作。這時,第一個參數是對象執行個體,第二個是 selector。      
  • 傳送傳回值
利用 objc_msgSend API 經方法的傳回值傳送回去。      

簡單地從上面發送消息的過程可以看到,最終還是以函數指針的方式調用了函數。為什麼特意花那麼大的功夫繞個大圈子呢?1

這些年,随着程式庫尺寸的擴大,動态連結庫的使用已經非常普遍。就是說,應用程式本身并不包括庫代碼,而是在啟動時或者運作過程中動态加載程式庫。這樣一來一方面可以減小程式大小,另一方面可以提升了代碼重用(不用再造輪子)。但是,随之帶來了向下相容的問題。

如果程式庫反複更新,添加新的方法的時候,開發者與使用者間必須保持一緻的版本,否則将産生運作時錯誤。一般,解決這個問題是,調用新定義的方法的時候,實作檢查目前系統中是否存在新方法的實作。如果沒有,跳過它或者簡單地産生警告資訊。 Objective-C中的respondsToSelector:方法就可以用來實作這樣的動作。

但是,這并不是萬全的解決方案。如果應用程式與新的動态程式庫(含有新定義的API)一起編譯後,新定義的API符号也被包含進去。而這樣的應用程式放到比較舊的系統(舊的動态程式庫)中運作的時候,因為找不到連結符号,程式将不能啟動。這就是 win32系統中常見的「DLL地域」。

為了解決這個問題,Objective-C 編譯得到的二進制檔案中,函數是作為 selector 來儲存的。就是說,不管調用什麼函數,二進制檔案中不會包含符号資訊。為了驗證 Objective-C 編譯的二進制檔案是否包含符号資訊,這裡用 nm 指令來檢視。

源代碼如下 :

int main (int argc, const char * argv[])
{
    NSString*   string;
    int         length;
    string = [[NSString alloc] initWithString:@"Objective-C"];
    length = [string length];

    return  0;
}
           

這裡調用了 alloc、initWithString:、length 等方法。

% nm Test
         U .objc_class_name_NSString
00003000 D _NXArgc
00003004 D _NXArgv
         U ___CFConstantStringClassReference
00002b98 T ___darwin_gcc3_preregister_frame_info
         U ___keymgr_dwarf2_register_sections
         U ___keymgr_global
0000300c D ___progname
000025ec t __call_mod_init_funcs
000026ec t __call_objcInit
         U __cthread_init_routine
00002900 t __dyld_func_lookup
000028a8 t __dyld_init_check
         U __dyld_register_func_for_add_image
         U __dyld_register_func_for_remove_image
...
           

可以看到,這裡沒有alloc、initWithString:、length3個方法的符号。是以,即使我們添加了新的方法,也可以在任何新舊系統中運作。當然,函數調用之前,需要使用 respondsToSelector: 來确定方法是否存在。正是這樣的特性,使得程式可以運作時動态地查詢要執行的方法,提高了 Objective-C 語言的柔韌性。

Target-Action Paradigm

Objective-C 語言中,GUI控件對象間的通信利用 Target-Action Paradigm。不像其他事件驅動的 GUI 系統實作的那樣,需要以回調函數的形式注冊消息處理函數(Win32/MFC,Java AWT, X Window)。Target-Action Paradigm 完全是面向對象的事件傳遞機制。

例如使用者點選菜單的事件,用Target-Action Paradigm來解釋就是,調用菜單中被設定目标的Action。這個Action對應的方法不一定需要實作。目标與Action的指定與方法的實作沒有關系,源代碼編譯的時候不會檢測,隻是在運作時确認(參考前面消息傳送的機制)。

運作時,通過respondsToSelector: 方法來檢查實作的情況。如果有實作,那麼使用performSelector:withObject:來調用具體的Action,像是下面的代碼:

// 目标對象
id target;
// 具體Action的 selector
SEL action;
...

// 确認目标是否實作Action
if ([target respondsToSelector:actioin]) {
    // 調用具體Action
    [target performSelector:action withObject:self];
}
           

通過這樣的架構,利用 setTarget: 可以更該其他的目标,或者 setAction: 變換不同的Action。實作動态的方法調用。

繼續閱讀