­

30分鐘學會Objective-C

注: 本文首發於我的個人博客:https://evilpan.com/2019/04/05/objc-basics/

請原諒我的標題黨。但是如果你有其他語言的學習經驗,要學習Objective-C的語法特性其實並不困難。正如我之前在軟件開發的一些"心法"一文中所說,編程語言只是一個工具,工具本身不是目的,關鍵是要看你用來做什麼。

而我學習Objective-C的理由也很簡單,就是為了逆向破解iOS和macOS程序。不需要研究高深的語法糖和特性,只需要會寫簡單的應用,以及會看別人的代碼。所以,你沒看錯,30分鐘足矣。

什麼是Objective-C

Objective-C,簡稱OC,是一種通用、高級、面向對象的編程語言。它擴展了標準的ANSI C編程語言,
將Smalltalk式的消息傳遞機制加入到ANSI C中。當前主要支持的編譯器有GCC和Clang(採用LLVM作為後端)。
Objective-C的商標權屬於蘋果公司,蘋果公司也是這個編程語言的主要開發者。
蘋果在開發NeXTSTEP操作系統時使用了Objective-C,之後被OS X和iOS繼承下來。
現在Objective-C與Swift是OS X和iOS操作系統、及與其相關的API、Cocoa和Cocoa Touch的主要編程語言。

Objective-C是C語言的嚴格超集。這意味着任何C語言程序不經修改就可以直接通過Objective-C編譯器,
在Objective-C中使用C語言代碼也是完全合法的。Objective-C被描述為蓋在C語言上的薄薄一層,
因為Objective-C的原意就是在C語言主體上加入面向對象的特性。OC項目中常用的拓展名如下:

擴展名 內容類型
.h 頭文件。頭文件包含類,類型,函數和常數的聲明。
.m 源代碼文件。這是典型的源代碼文件擴展名,可以包含 Objective-C 和 C 代碼。
.mm 源代碼文件。帶有這種擴展名的源代碼文件,除了可以包含Objective-C和C代碼以外還可以包含C++代碼。僅在你的Objective-C代碼中確實需要使用C++類或者特性的時候才用這種擴展名。

Hello, World!

學習任何一門語言之前,基本都需要做的就是編寫並運行一個HelloWorld程序,對於OC而言則是如下:

#import <Foundation/Foundation.h>    int main (int argc, const char * argv[])  {      @autoreleasepool {          NSLog (@"Hello, World!");      }      return 0;  }

使用clang進行編譯:

clang -framework Foundation hello.m -o hello

運行:

$ ./hello  2019-04-05 09:33:22.579 hello[75742:3312942] Hello, World!

So easy!我們學習Objective-C時記住要重點關注概念而不是具體的語言細節,避免陷入學而無用的境地。

關鍵概念

消息傳遞

Objective-C最大的特色是承自Smalltalk的消息傳遞模型(message passing),
此機制與今日C++式之主流風格差異甚大。 Objective-C里,與其說對象互相調用方法,
不如說對象之間互相傳遞消息更為精確。此二種風格的主要差異在於調用方法/消息傳遞這個動作。
C++里類別與方法的關係嚴格清楚,一個方法必定屬於一個類別,而且在編譯時(compile time)
就已經緊密綁定,不可能調用一個不存在類別里的方法。但在Objective-C,類別與消息的關係比較鬆散,
調用方法視為對對象發送消息,所有方法都被視為對消息的回應。所有消息處理直到運行時(runtime)
才會動態決定,並交由類別自行決定如何處理收到的消息。也就是說,一個類別不保證一定會回應收到的消息,
如果類別收到了一個無法處理的消息,程序只會拋出異常,不會出錯或崩潰。

C++里,送一個消息給對象(或者說調用一個方法)的語法如下:

obj.method(argument);

Objective-C則寫成:

[obj method: argument];

此二種風格各有優劣。C++強制要求所有的方法都必須有對應的動作,且編譯期綁定使得函數調用非常快速。
缺點是僅能藉由virtual關鍵字提供有限的動態綁定能力。Objective-C天生即具備鴨子類型之動態綁定能力,
因為運行期才處理消息,允許發送未知消息給對象。可以送消息給整個對象集合而不需要一一檢查每個對象的類型,
也具備消息轉送機制。同時空對象nil接受消息後默認為不做事,所以送消息給nil也不用擔心程序崩潰。

字符串

作為C語言的超集,Objective-C 支持 C 語言字符串方面的約定。也就是說,單個字符被單引號包括,
字符串被雙引號包括。然而,大多數Objective-C通常不使用C語言風格的字符串。
反之,大多數框架把字符串傳遞給NSString對象。NSString類提供了字符串的類包裝,
包含了所有你期望的優點,包括對保存任意長度字符串的內建內存管理機制,支持Unicode,printf風格的格式化工具,
等等。因為這種字符串使用的非常頻繁,Objective-C提供了一個助記符@可以方便地從常量值創建NSString對象。
如下面的例子所示:

// 從一個C語言字符串創建Objective-C字符串  NSString*  fromCString = [NSString stringWithCString:"A C string"  encoding:NSASCIIStringEncoding];  // 使用助記符@  NSString* name = @"PANN";  NSString* line = [NSString stringWithFormat:@"Hello, %sn", @"String"];

類(class)

類是面向對象語言中最重要的一個概念,Objective-C同樣支持類。下圖是一個名為MyClass的類聲明介紹:
class.png

聲明

遵循C語言的規範,類聲明一般定義在.h頭文件中。類聲明以關鍵字@interface作為開始,@end作為結束。
其中類方法前的+號表示類方法,-號表示實例方法。一個對應的C++類定義如下:

public MyClass : NSObject {    protected:    int count;    id data;    NSString *name;    public:    id intWithString(NSString *aName);    static MyClass *createMyClassWithString(NSString *aName);  };

實現

遵循C語言的規範,類實現一般定義在對應的.m文件中。類實現包含了公開方法的實現,
以及定義私有(private) 變量及方法。 以關鍵字@implementation作為區塊起頭,@end結尾。
上述類的一個實現如下:

@implementation MyClass {    NSString *secret;    -(id) initWithString: (NSString*)aName {      self.name = aName;      return 0;    }    +(MyClass)createMyClassWithString:(NSString*)aName {      MyClass * my = [[MyClass alloc] init];      my.name = aName;      return my;    }  }

頭文件(類聲明)中定義的屬性默認為protected,方法為public。而類實現中定義的屬性為private。
當然也可以使用@public、@private等助記符來覆蓋默認行為。

實例化

實例化即創建對象。Objective-C創建對象需通過alloc以及init兩個消息。alloc的作用是分配內存,
init則是初始化對象。 init與alloc都是定義在NSObject里的方法,父對象收到這兩個信息並做出正確回應後,
新對象才創建完畢。如上述類中:

MyClass * my = [[MyClass alloc] init];

在Objective-C 2.0里,若創建對象不需要參數,則可直接使用new:

MyClass * my = [MyClass new];

僅僅是語法上的精簡,效果完全相同。

若要自己定義初始化的過程,可以重寫init方法,來添加額外的工作。(用途類似C++ 的構造函數constructor),
如下:

- (id) init {      if ( self=[super init] ) {   // 必須調用父類的init          // do something here ...      }      return self;  }

方法(method)

在上節介紹類的時候已經見過了一些方法的定義和使用,第一次接觸Objective-C的人肯定會覺得很奇怪(比如我就覺得這語法比Golang還奇葩),
但是只要接收了這種設定,還是可以慢慢習慣的。

聲明

下圖為Objective-C內置數組類型的insertObject方法聲明:

method.png

方法實際的名字(insertObject:atIndex:)是所有方法標識關鍵的級聯,包含了冒號。冒號表明了參數的出現。
如果方法沒有參數,你可以省略第一個(也是唯一的)方法標識關鍵字後面的冒號。本例中,這個方法有兩個參數。
該函數轉換成類似的C++表示如下:

void insertObject:atIndex:(id anObject, NSUInteger index);

調用

調用一個方法實際上就是傳遞消息到對應的對象。這裡消息就是方法標識符以及傳遞給方法的參數信息。
發送給對象的所有消息都會動態分發,這樣有利於實現Objective-C類的多態行為。
也就是說,如果子類定義了跟父類的具有相同標識符的方法,那麼子類首先收到消息,
然後可以有選擇的把消息轉發(也可以不轉發)給他的父類。

消息被中括號( [ 和 ] )包括。括號中接收消息的對象在左邊,消息及其參數在右邊。
例如,給myArray變量傳遞消息insertObject:atIndex:消息,可以使用如下的語法:

[myArray insertObject:anObj atIndex:0];

消息允許嵌套。也就是說,假如你有一個myAppObject對象,該對象有getArray方法獲取數組,
有getObjectToInsert方法獲取元素,那麼嵌套的消息可以寫成:

[[myAppObject getArray] insertObject:[myAppObject getObjectToInsert] atIndex:0];

屬性(attribute)

屬性沒什麼好說的,和C++的類屬性類似。不過在Objective-C 2.0引入了新的語法以聲明變量為屬性,
並包含一可選定義以配置訪問方法的生成。屬性總是為公共的,其目的為提供外部類訪問(也可能為只讀)
類的內部變量的方法。屬性可以被聲明為「readonly」,即只讀的,也可以提供儲存方法包括「assign」,
「copy」或「retain」(簡單的賦值、複製或增加1引用計數)。默認的屬性是原子的,
即在訪問時會加鎖以避免多線程同時訪問同一對象,也可以將屬性聲明為「nonatomic」(非原子的),
避免產生鎖。

定義屬性的例子如下:

@interface Person : NSObject {      @public          NSString *name;      @private          int age;  }    @property(copy) NSString *name;  @property(readonly) int age;    -(id)initWithAge:(int)age;  @end

synthesize

屬性的訪問方法由@synthesize關鍵字來實現,它由屬性的聲明自動的產生一對訪問方法。
另外,也可以選擇使用@dynamic關鍵字表明訪問方法為手動提供。

@implementation Person  @synthesize name;  @dynamic age;    -(id)initWithAge:(int)initAge  {      age = initAge; // 注意:直接賦給成員變量,而非屬性      return self;  }    -(int)age  {      return 18; // 注意:並非返回真正的年齡  }  @end

訪問

屬性可以利用傳統的消息表達式、點表達式或"valueForKey:"/"setValue:forKey:"方法對來訪問。如下:

Person *aPerson = [[Person alloc] initWithAge: 53];  // 修改屬性  aPerson.name = @"Steve";  [aPerson setName: @"Steve"];  // 讀取屬性  NSString *tmp;  tmp = [aPerson name]; // 消息表達式  tmp = aPerson.name;   // 點表達式  tmp = aPerson->name;  // 直接訪問成員變量  tmp = [aPerson valueForKey:@"name"]; // property訪問

協議(Protocol)

協議是一組沒有實現的方法列表,任何的類均可採納協議並具體實現這組方法。簡而言之就是接口,
可以類比Java的interface,或者C++的純虛函數,表述一種is-a的概念。

協議以關鍵字@protocol作為區塊起始,@end結束,中間為方法列表。如下:

@protocol Mutex  - (void)lock;  - (void)unlock;  @end

若要聲明實現該協議,可以使用尖括號<>,如下:

@interface SomeClass : SomeSuperClass <Mutex>  @end

一旦SomeClass表明他採納了Mutex協議,SomeClass就有義務實現Mutex協議中的兩個方法:

@implementation SomeClass  - (void)lock {    // 實現lock方法  }  - (void)unlock {    // 實現unlock方法  }  @end

動態類型

類似於Smalltalk,Objective-C具備動態類型:即消息可以發送給任何對象實體,無論該對象實體的公開接口中有沒有對應的方法。
雖然Objective-C具備動態類型的能力, 但編譯期的靜態類型檢查依舊可以應用到變量上。
以下三種聲明在運行時效力是完全相同的, 但是三種聲明提供了一個比一個更明顯的類型信息,
附加的類型信息讓編譯器在編譯時可以檢查變量類型,並對類型不符的變量提出警告。

下面三個方法,差異僅在於參數的形態:

- setMyValue1:(id) foo;  - setMyValue2:(id <aProtocol>) foo;  - setMyValue3:(NSNumber*) foo;

Objective-C中的id類型類似於void指針,但是被嚴格限制只能使用在對象上。

消息轉發

一個對象收到消息之後,他有三種處理消息的可能手段,第一是回應該消息並運行方法,若無法回應,
則可以轉發消息給其他對象,若以上兩者均無,就要處理無法回應而拋出的例外。只要進行三者之其一,
該消息就算完成任務而被丟棄。若對"nil"(空對象指針)發送消息,該消息通常會被忽略,
只不過對於某些編譯器選項可能會拋出異常。

Objective-C運行時在Object中定義了一對方法:

轉發方法

- (retval_t) forward:(SEL) sel :(arglist_t) args; // with GCC  - (id) forward:(SEL) sel :(marg_list) args; // with NeXT/Apple systems

響應方法

- (retval_t) performv:(SEL) sel :(arglist_t) args;  // with GCC  - (id) performv:(SEL) sel :(marg_list) args; // with NeXT/Apple systems

GCC和NeXT/Apple編譯器的區別是返回值和參數類型不同。

希望實現轉發的對象只需用新的方法覆蓋以上方法來定義其轉發行為而無需重寫響應方法performv::
因為後者只是單純的對響應對象發送消息並傳遞參數。其中,SEL類型是Objective-C中消息的類型。

類別(Category)

Objective-C借用並擴展了Smalltalk實現中的"分類"概念,用以幫助達到分解代碼的目的。

一個分類可以將方法的實現分解進一系列分離的文件。程序員可以將一組相關的方法放進一個分類,
使程序更具可讀性。舉例來講,可以在字符串類中增加一個名為"拼寫檢查"的分類,
並將拼寫檢查的相關代碼放進這個分類中。

分類中的方法是在運行時被加入類中的,這一特性允許程序員向現存的類中增加方法,
而無需持有原有的代碼, 或是重新編譯原有的類。
例如若系統提供的字符串類的實現中不包含拼寫檢查的功能,可以增加這樣的功能而無需更改原有的字符串類的代碼。

在運行時,分類中的方法與類原有的方法並無區別,其代碼可以訪問包括私有類成員變量在內的所有成員變量。
若分類聲明了與類中原有方法同名的函數,則分類中的方法會被調用。因此分類不僅可以增加類的方法,
也可以代替原有的方法。這個特性可以用於修正原有代碼中的錯誤,更可以從根本上改變程序中原有類的行為。
若兩個分類中的方法同名,則被調用的方法是不可預測的。

分類的聲明如下:

@interface ClassName (CategoryName)    @end

下面是一個具體的例子,通過MyAdditions分類,動態的給NSString類中添加getCopyRightString方法:

#import <Foundation/Foundation.h>    @interface NSString(MyAdditions)  +(NSString *)getCopyRightString;  @end    @implementation NSString(MyAdditions)    +(NSString *)getCopyRightString {     return @"Copyright evilpan.com 2019";  }    @end    int main(int argc, const char * argv[]) {     NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];     NSString *copyrightString = [NSString getCopyRightString];     NSLog(@"Accessing Category: %@", copyrightString);       [pool drain];     return 0;  }

小結

現在,我們已經了解了Objective-C語言的基本語法和關鍵概念,可以開始自己編寫簡單的程序了。
一門語言只是一個工具,常用常新,如果不使用,學得再深也很容易遺忘。
當然,本文介紹的Objective-C特性只是一小部分,但我們仍然可以先用起來,
等遇到具體語法或者API時候再查閱文檔(如spec、[tutorialspoint][tp]等)即可。
使用得越多,需要查閱文檔但頻率也會越少,學習沒有捷徑可言。

[wiki]: https://zh.wikipedia.org/wiki/Objective-C
[w3]: http://www.runoob.com/w3cnote/objective-c-tutorial.html
[tp]: https://www.tutorialspoint.com/objective_c/objective_c_overview.htm