30分鐘學會Objective-C
阿新 • • 發佈:2020-03-07
> 注: 本文首發於我的個人部落格:https://evilpan.com/2019/04/05/objc-basics/
請原諒我的標題黨。但是如果你有其他語言的學習經驗,要學習**Objective-C**的語法特性其實並不困難。正如我之前在[軟體開發的一些"心法"][1]一文中所說,程式語言只是一個工具,工具本身不是目的,關鍵是要看你用來做什麼。
而我學習**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而言則是如下:
```m
#import
int main (int argc, const char * argv[])
{
@autoreleasepool {
NSLog (@"Hello, World!");
}
return 0;
}
```
使用clang進行編譯:
```bash
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++裡,送一個訊息給物件(或者說呼叫一個方法)的語法如下:
```cpp
obj.method(argument);
```
Objective-C則寫成:
```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
// 從一個C語言字串建立Objective-C字串
NSString* fromCString = [NSString stringWithCString:"A C string"
encoding:NSASCIIStringEncoding];
// 使用助記符@
NSString* name = @"PANN";
NSString* line = [NSString stringWithFormat:@"Hello, %s\n", @"String"];
```
## 類(class)
類是面嚮物件語言中最重要的一個概念,Objective-C同樣支援類。下圖是一個名為MyClass的類宣告介紹:
![class.png][imgClass]
### 宣告
遵循C語言的規範,類宣告一般定義在.h標頭檔案中。類宣告以關鍵字@interface作為開始,@end作為結束。
其中類方法前的+號表示類方法,-號表示例項方法。一個對應的C++類定義如下:
```cpp
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結尾。
上述類的一個實現如下:
```c
@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裡的方法,父物件收到這兩個資訊並做出正確迴應後,
新物件才建立完畢。如上述類中:
```c
MyClass * my = [[MyClass alloc] init];
```
在Objective-C 2.0裡,若建立物件不需要引數,則可直接使用new:
```c
MyClass * my = [MyClass new];
```
僅僅是語法上的精簡,效果完全相同。
若要自己定義初始化的過程,可以重寫init方法,來新增額外的工作。(用途類似C++ 的建構函式constructor),
如下:
```c
- (id) init {
if ( self=[super init] ) { // 必須呼叫父類的init
// do something here ...
}
return self;
}
```
## 方法(method)
在上節介紹類的時候已經見過了一些方法的定義和使用,第一次接觸Objective-C的人肯定會覺得很奇怪(比如我就覺得這語法比Golang還奇葩),
但是隻要接收了這種設定,還是可以慢慢習慣的。
### 宣告
下圖為Objective-C內建陣列型別的insertObject方法宣告:
![method.png][imgMethod]
方法實際的名字(insertObject:atIndex:)是所有方法標識關鍵的級聯,包含了冒號。冒號表明了引數的出現。
如果方法沒有引數,你可以省略第一個(也是唯一的)方法標識關鍵字後面的冒號。本例中,這個方法有兩個引數。
該函式轉換成類似的C++表示如下:
```cpp
void insertObject:atIndex:(id anObject, NSUInteger index);
```
### 呼叫
呼叫一個方法實際上就是傳遞訊息到對應的物件。這裡訊息就是方法識別符號以及傳遞給方法的引數資訊。
傳送給物件的所有訊息都會動態分發,這樣有利於實現Objective-C類的多型行為。
也就是說,如果子類定義了跟父類的具有相同識別符號的方法,那麼子類首先收到訊息,
然後可以有選擇的把訊息轉發(也可以不轉發)給他的父類。
訊息被中括號( [ 和 ] )包括。括號中接收訊息的物件在左邊,訊息及其引數在右邊。
例如,給myArray變數傳遞訊息insertObject:atIndex:訊息,可以使用如下的語法:
```c
[myArray insertObject:anObj atIndex:0];
```
訊息允許巢狀。也就是說,假如你有一個myAppObject物件,該物件有getArray方法獲取陣列,
有getObjectToInsert方法獲取元素,那麼巢狀的訊息可以寫成:
```c
[[myAppObject getArray] insertObject:[myAppObject getObjectToInsert] atIndex:0];
```
## 屬性(attribute)
屬性沒什麼好說的,和C++的類屬性類似。不過在Objective-C 2.0引入了新的語法以宣告變數為屬性,
幷包含一可選定義以配置訪問方法的生成。屬性總是為公共的,其目的為提供外部類訪問(也可能為只讀)
類的內部變數的方法。屬性可以被宣告為“readonly”,即只讀的,也可以提供儲存方法包括“assign”,
“copy”或“retain”(簡單的賦值、複製或增加1引用計數)。預設的屬性是原子的,
即在訪問時會加鎖以避免多執行緒同時訪問同一物件,也可以將屬性宣告為“nonatomic”(非原子的),
避免產生鎖。
定義屬性的例子如下:
```c
@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關鍵字表明訪問方法為手動提供。
```c
@implementation Person
@synthesize name;
@dynamic age;
-(id)initWithAge:(int)initAge
{
age = initAge; // 注意:直接賦給成員變數,而非屬性
return self;
}
-(int)age
{
return 18; // 注意:並非返回真正的年齡
}
@end
```
### 訪問
屬性可以利用傳統的訊息表示式、點表示式或"valueForKey:"/"setValue:forKey:"方法對來訪問。如下:
```c
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結束,中間為方法列表。如下:
```c
@protocol Mutex
- (void)lock;
- (void)unlock;
@end
```
若要宣告實現該協議,可以使用尖括號<>,如下:
```c
@interface SomeClass : SomeSuperClass
@end
```
一旦SomeClass表明他採納了Mutex協議,SomeClass就有義務實現Mutex協議中的兩個方法:
```c
@implementation SomeClass
- (void)lock {
// 實現lock方法
}
- (void)unlock {
// 實現unlock方法
}
@end
```
## 動態型別
類似於Smalltalk,Objective-C具備動態型別:即訊息可以傳送給任何物件實體,無論該物件實體的公開介面中有沒有對應的方法。
雖然Objective-C具備動態型別的能力, 但編譯期的靜態型別檢查依舊可以應用到變數上。
以下三種宣告在執行時效力是完全相同的, 但是三種宣告提供了一個比一個更明顯的型別資訊,
附加的型別資訊讓編譯器在編譯時可以檢查變數型別,並對型別不符的變數提出警告。
下面三個方法,差異僅在於引數的形態:
```
- setMyValue1:(id) foo;
- setMyValue2:(id ) foo;
- setMyValue3:(NSNumber*) foo;
```
> Objective-C中的id型別類似於void指標,但是被嚴格限制只能使用在物件上。
## 訊息轉發
一個物件收到訊息之後,他有三種處理訊息的可能手段,第一是迴應該訊息並執行方法,若無法迴應,
則可以轉發訊息給其他物件,若以上兩者均無,就要處理無法迴應而丟擲的例外。只要進行三者之其一,
該訊息就算完成任務而被丟棄。若對"nil"(空物件指標)傳送訊息,該訊息通常會被忽略,
只不過對於某些編譯器選項可能會丟擲異常。
Objective-C執行時在Object中定義了一對方法:
**轉發方法**:
```c
- (retval_t) forward:(SEL) sel :(arglist_t) args; // with GCC
- (id) forward:(SEL) sel :(marg_list) args; // with NeXT/Apple systems
```
**響應方法**:
```c
- (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實現中的"分類"概念,用以幫助達到分解程式碼的目的。
一個分類可以將方法的實現分解進一系列分離的檔案。程式設計師可以將一組相關的方法放進一個分類,
使程式更具可讀性。舉例來講,可以在字串類中增加一個名為"拼寫檢查"的分類,
並將拼寫檢查的相關程式碼放進這個分類中。
分類中的方法是在執行時被加入類中的,這一特性允許程式設計師向現存的類中增加方法,
而無需持有原有的程式碼, 或是重新編譯原有的類。
例如若系統提供的字串類的實現中不包含拼寫檢查的功能,可以增加這樣的功能而無需更改原有的字串類的程式碼。
在執行時,分類中的方法與類原有的方法並無區別,其程式碼可以訪問包括私有類成員變數在內的所有成員變數。
若分類聲明瞭與類中原有方法同名的函式,則分類中的方法會被呼叫。因此分類不僅可以增加類的方法,
也可以代替原有的方法。這個特性可以用於修正原有程式碼中的錯誤,更可以從根本上改變程式中原有類的行為。
若兩個分類中的方法同名,則被呼叫的方法是不可預測的。
分類的宣告如下:
```c
@interface ClassName (CategoryName)
@end
```
下面是一個具體的例子,通過MyAdditions分類,動態的給NSString類中新增getCopyRightString方法:
```c
#import
@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]等)即可。
使用得越多,需要查閱文件但頻率也會越少,學習沒有捷徑可言。
[1]: https://www.cnblogs.com/pannengzhi/p/6820325.html
[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
[imgClass]: https://img2020.cnblogs.com/blog/676200/202003/676200-20200307103719917-1536134381.png
[imgMethod]: https://img2020.cnblogs.com/blog/676200/202003/676200-20200307103750781-329866