YYModel源代碼閱讀--基礎知識

這段時間因為工作需要,閱讀了YYModel這個開源框架,至于它能做什么,最直白的講述就是JSON與Model之間的相互轉化。

源代碼在Github,大家可以自行git clone或者download。

接下來,筆者主要分析閱讀源代碼而引出的各種問題與知識點,不足之處請大家指正。

NS_ASSUME_NONNULL_BEGIN & NS_ASSUME_NONNULL_END

這組宏是成對使用的,不得不說我們自己寫代碼的時候使用的很少,以至于遺漏這個知識點,現在我們就來看看這兩個宏會引出什么問題。

這組宏會引出幾個關于Objective-C新特性的知識點:

  • Nullability Annotations

  • Lightweight Generics

  • __kindof

Nullability Annotations

我們都知道在swift中,可以使用!和?來表示一個對象是optional的還是non-optional,如view?和view!。而在 Objective-C中則沒有這一區分,view既可表示這個對象是optional,也可表示是non-optioanl。這樣就會造成一個問題:在 Swift與Objective-C混編時,Swift編譯器并不知道一個Objective-C對象到底是optional還是non-optional,因此這種情況下編譯器會隱式地將Objective-C的對象當成是non-optional。

為了解決這個問題,蘋果在Xcode 6.3引入了一個Objective-C的新特性:nullability annotations。這一新特性的核心是兩個新的類型注釋:** __nullable** 和 __nonnull 。從字面上我們可以猜到,__nullable表示對象可以是NULL或nil,而__nonnull表示對象不應該為空。當我們不遵循這一規則時,編譯器就會給出警告。

我們來看看以下的實例,

@interface TestNullabilityClass ()
@property (nonatomic, copy) NSArray * items;
- (id)itemWithName:(NSString * __nonnull)name;
@end
@implementation TestNullabilityClass
...
- (void)testNullability {
[self itemWithName:nil]; // 編譯器警告:Null passed to a callee that requires a non-null argument
}
- (id)itemWithName:(NSString * __nonnull)name {
return nil;
}
@end

不過這只是一個警告,程序還是能編譯通過并運行。

事實上,在任何可以使用const關鍵字的地方都可以使用__nullable__nonnull,不過這兩個關鍵字僅限于使用在指針類型上。而在方法的聲明中,我們還可以使用不帶下劃線的nullablenonnull,如下所示:

- (nullable id)itemWithName:(NSString * nonnull)name
在屬性聲明中,也增加了兩個相應的特性,因此上例中的items屬性可以如下聲明:

@property (nonatomic, copy, nonnull) NSArray * items;
當然也可以用以下這種方式:

@property (nonatomic, copy) NSArray * __nonnull items;
推薦使用nonnull這種方式,這樣可以讓屬性聲明看起來更清晰。

Nonnull區域設置(Audited Regions)

如果需要每個屬性或每個方法都去指定nonnullnullable,是一件非常繁瑣的事。蘋果為了減輕我們的工作量,專門提供了兩個宏:NS_ASSUME_NONNULL_BEGINNS_ASSUME_NONNULL_END。在這兩個宏之間的代碼,所有簡單指針對象都被假定為 nonnull,因此我們只需要去指定那些nullable的指針。如下代碼所示:

NS_ASSUME_NONNULL_BEGIN
@interface TestNullabilityClass ()
@property (nonatomic, copy) NSArray * items;
- (id)itemWithName:(nullable NSString *)name;
@end
NS_ASSUME_NONNULL_END

在上面的代碼中,items屬性默認是nonnull的,itemWithName:方法的返回值也是nonnull,而參數是指定為nullable的。

不過,為了安全起見,蘋果還制定了幾條規則:

  • typedef定義的類型的nullability特性通常依賴于上下文,即使是在Audited Regions中,也不能假定它為nonnull。

  • 復雜的指針類型(如id *)必須顯示去指定是nonnull還是nullable。例如,指定一個指向nullable對象的nonnull指針,可以使用”__nullable id * __nonnull”。

  • 我們經常使用的NSError **通常是被假定為一個指向nullable NSError對象的nullable指針。

兼容性

因為Nullability Annotations是Xcode 6.3新加入的,所以我們需要考慮之前的老代碼。實際上,蘋果已以幫我們處理好了這種兼容問題,我們可以安全地使用它們:

  • 老代碼仍然能正常工作,即使對nonnull對象使用了nil也沒有問題。

  • 老代碼在需要和swift混編時,在新的swift編譯器下會給出一個警告。

  • nonnull不會影響性能。事實上,我們仍然可以在運行時去判斷我們的對象是否為nil。

事實上,我們可以將nonnull/nullable與我們的斷言和異常一起看待,其需要處理的問題都是同一個:違反約定是一個程序員的錯誤。特別是,返回值是我們可控的東西,如果返回值是nonnull的,則我們不應該返回nil,除非是為了向后兼容。

Lightweight Generics

Lightweight Generics 輕量級泛型,輕量是因為這是個純編譯器的語法支持(LLVM 7.0),和 Nullability 一樣,沒有借助任何 objc runtime 的升級,也就是說,這個新語法在 Xcode 7 上可以使用且完全向下兼容(更低的 iOS 版本)

帶泛型的容器

這無疑是本次最重大的改進,有了泛型后終于可以指定容器類中對象的類型了:

NSArray *strings = @[@"sun", @"yuan"];
NSDictionary *mapping = @{@"a": @1, @"b": @2};

返回值的 id 被替換成具體的類型后,令人感動的代碼提示也出來了。

假如向泛型容器中加入錯誤的對象,編譯器會不開心的。

系統中常用的一系列容器類型都增加了泛型支持,甚至連 NSEnumerator 都支持了,這是非常 Nice 的改進。和 Nullability 一樣,我認為最大的意義還是豐富了接口描述信息,對比下面兩種寫法:

@property (readonly) NSArray *imageURLs;
@property (readonly) NSArray *imageURLs;

不用多想就清楚下面的數組中存的是什么,避免了 NSStringNSURL 的混亂。

自定義泛型類

比起使用系統的泛型容器,更好玩的是自定義一個泛型類,目前這里還沒什么文檔,但攔不住我們寫測試代碼,假設我們要自定義一個 Stack 容器類:

@interface Stack : NSObject
- (void)pushObject:(ObjectType)object;
- (ObjectType)popObject;
@property (nonatomic, readonly) NSArray *allObjects;
@end

這個 ObjectType 是傳入類型的 placeholder,它只能在 @interface 上定義(類聲明、類擴展、Category),如果你喜歡用 T 表示也 OK,這個類型在 @interface@end 區間的作用域有效,可以把它作為入參、出參、甚至內部 NSArray 屬性的泛型類型,應該說一切都是符合預期的。我們還可以給 ObjectType 增加類型限制,比如:

// 只接受 NSNumber * 的泛型
@interface Stack : NSObject
// 只接受滿足 NSCopying 協議的泛型
@interface Stack> : NSObject

若什么都不加,表示接受任意類型 ( id );當類型不滿足時編譯器將產生 error。
實例化一個 Stack,一切工作正常:

對于多參數的泛型,用逗號隔開,其他都一樣,可以參考 NSDictionary 的頭文件。

協變性和逆變性

當類支持泛型后,它們的 Type 發生了變化,比如下面三個對象看上去都是 Stack,但實際上屬于三個 Type:

Stack *stack; // Stack *
Stack *stringStack; // Stack
Stack *mutableStringStack; // Stack

當其中兩種類型做類型轉化時,編譯器需要知道哪些轉化是允許的,哪些是禁止的,比如,默認情況下:

Stack *stack;
Stack *stringStack;
Stack *mutableStringStack; 

stack = stringStack;
stack = mutableStringStack;
stringStack = stack;
stringStack = mutableStringStack;
mutableStringStack = stack;
mutableStringStack = stringStack

在Xcode中我們可以看到,不指定泛型類型的 Stack 可以和任意泛型類型轉化,但指定了泛型類型后,兩個不同類型間是不可以強轉的,假如你希望主動控制轉化關系,就需要使用泛型的協變性和逆變性修飾符了:

__covariant - 協變性,子類型可以強轉到父類型(里氏替換原則)
__contravariant - 逆變性,父類型可以強轉到子類型(WTF)

協變

@interface Stack<__covariant ObjectType> : NSObject

逆變

@interface Stack<__contravariant ObjectType> : NSObject

協變是非常好理解的,像 NSArray 的泛型就用了協變的修飾符。

__kindof

__kindof 這修飾符還是很實用的,解決了一個長期以來的小痛點,拿原來的 UITableView 的這個方法來說:

- (id)dequeueReusableCellWithIdentifier:(NSString *)identifier;

使用時前面基本會使用 UITableViewCell 子類型的指針來接收返回值,所以這個 API 為了讓開發者不必每次都蛋疼的寫顯式強轉,把返回值定義成了 id 類型,而這個 API 實際上的意思是返回一個 UITableViewCellUITableViewCell 子類的實例,于是新的 __kindof 關鍵字解決了這個問題:

- (__kindof UITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier;

既明確表明了返回值,又讓使用者不必寫強轉。再舉個帶泛型的例子,UIView 的 subviews 屬性被修改成了:

@property (nonatomic, readonly, copy) NSArray<__kindof UIView *> *subviews;

這樣,寫下面的代碼時就沒有任何警告了:

UIButton *button = view.subviews.lastObject;

NS_ENUM & NS_OPTIONS

枚舉是指將變量的值一一列舉出來,變量的值只限于列舉出來的值的范圍內。

枚舉本質上是一個整數,枚舉的作用是把值限定在指定的范圍內,并且增加代碼的可讀性。 枚舉的成員如果沒有顯示指定值,那么第一個成員的值總是0,后面成員的值依次遞增。枚舉可以直接用于比較。

一般我們聲明枚舉:

#import 

// 聲明枚舉類型
enum Direction {up, down, left = 10, right};

int main(int argc, const char * argv[]){
...
}
其中up = 0, down = 1, left = 10, right = 11。

我們會發現枚舉中一些不可自定義的部分,例如,枚舉名。

NS_ENUM 和 NS_OPTIONS 都不算太古老的宏,在iOS 6 / OS X Mountain Lion才開始有,它們都是代替 enum 的更好的辦法。

NS_ENUM

如果要在早期的iOS或OS X系統中使用這兩個宏,簡單定義一下就好

#ifndef NS_ENUM
#define NS_ENUM(_type, _name) enum _name : _type _name; enum _name : _type
#endif

在OS X 10.4 中的原始定義如下:

#define NS_ENUM(_type, _name) CF_ENUM(_type, _name)
#define NS_OPTIONS(_type, _name) CF_OPTIONS(_type, _name)

在之前枚舉可以這么定義:

typedef enum {
UITableViewCellStyleDefault,
UITableViewCellStyleValue1,
UITableViewCellStyleValue2,
UITableViewCellStyleSubtitle
};

或者

typedef NSInteger UITableViewCellStyle;

現在,有了統一的風格

typedef NS_ENUM(NSInteger, UITableViewCellSelectionStyle) {
UITableViewCellSelectionStyleNone,
UITableViewCellSelectionStyleBlue,
UITableViewCellSelectionStyleGray,
UITableViewCellSelectionStyleDefault
};

NS_ENUM 的第一個參數是用于存儲的新類型的類型。在64位環境下,UITableViewCellStyleNSInteger 一樣有8 bytes長。你要保證你給出的所有值能被該類型容納,否則就會產生錯誤。第二個參數是新類型的名字。大括號里面和以前一樣,是你要定義的各種值。

NS_OPTIONS

語法和 NS_ENUM 完全相同,但這個宏提示編譯器值是如何通過位掩碼 | 組合在一起的。

typedef NS_OPTIONS(NSUInteger, AMGResizing) {
AMGResizingNone = 0,
AMGResizingFlexibleWidth = 1 << 0,
AMGResizingFlexibleHeight = 1 << 1,
AMGResizingFlexibleUnicorn = 1 << 2
};

attribute((always_inline))

我們知道一般的函數調用都會通過call的方式來調用,這樣讓攻擊很容易對一個函數做手腳,如果是以inline的方式編譯的會,會把該函數的code拷貝到每次調用該函數的地方。而static會讓生成的二進制文件中沒有清晰的符號表,讓逆向的人很難弄清楚邏輯。

__attribute__((always_inline)) 的意思是強制內聯,所有加了__attribute__((always_inline)) 的函數再被調用時不會被編譯成函數調用而是直接擴展到調用函數體內,比如定義了函數
__attribute__((always_inline)) void a()

void b(){ 
a();
}

b 調用 a 函數的匯編代碼不會是跳轉到a執行,而是 a 函數的代碼直接在 b 內成為 b 的一部分。
#define __inline __attribute__((always_inline)) 的意思就是用
__inline 代替__attribute__((always_inline))
內聲明a的時候可以直接寫成__inline void a() 這樣比較方便因為__attribute__((always_inline)) 字多。

undef

這是預編譯指令,和#define搭配使用,意思是取消之前的宏定義。

#define PROC_ADD 
void main(void) 
{
#ifdef PROC_ADD 
// Do this code here then undefined it to run the code in the else 
// processing work 
#undef PROC_ADD 
#else 
// now that PROC_ADD has been undefined run this code 
// processing work 
#endif 
}

__unsafe_unretained

__unsafe_unretained是對對象的非zeroing的weak reference,意思是當對象所指向的內存被銷毀了,對象還存在,稱為“野指針”。

在iOS引入了Automatic Reference Count(ARC)之后,編譯器可以在編譯時對obj-c對象進行內存管理。大致規則如下:

alloc的要release;
retain/copy的要release;
NSAutoreleasePool在ARC中被禁止使用,替換成@autoreleasepool 函數體;
使用@ autoreleasepool,在函數入口的時候,autorelease pool入棧,正常退出時,autorelease pool出棧,從而釋放變量.
注意:@ autoreleasepool在非ARC模式下,也能使用,并據說使用@autoreleasepool比使用NSAutoreleasePool速度能快6倍, 明顯提升程序性能.

@package

為了強制一個對象隱藏其數據,編譯器限制實例變量范圍以限制其在程序中的可見性,但是為了提供靈活性,蘋果也讓開發者顯式設置范圍。

以下是這些關鍵字的使用范圍:

  • @private

The instance variable is accessible only within the class that declares it.

實例變量只能被聲明它的類訪問.

  • @protected

The instance variable is accessible within the class that declares it and within classes that inherit it. All instance variables without an explicit scope directive have @protected scope.

實例變量能被聲明它的類和子類訪問,所有沒有顯式制定范圍的實例變量都是.

  • @public

The instance variable is accessible everywhere.

實例變量可以被在任何地方訪問.

  • @package

Using the modern runtime, an @package instance variable has @public scope inside the executable image that implements the class, but acts like @private outside.使用modern運行時,一個@package實例變量在實現這個類的可執行文件鏡像中實際上是@public的,但是在外面就是@private【runtime需要再看一下蘋果文檔Runtime Programming Guide】

The @package scope for Objective-C instance variables is analogous to private_extern for C variables and functions. Any code outside the class implementation’s image that tries to use the instance variable gets a link error.

Objective-C中的@package與C語言中變量和函數的private_extern類似。任何在實現類的鏡像之外的代碼想使用這個實例變量都會引發link error

This scope is most useful for instance variables in framework classes, where @private may be too restrictive but @protected or @public too permissive.

這個類型最常用于框架類的實例變量,使用@private太限制,使用@protected或者@public又太開放.

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,106評論 6 542
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,441評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,211評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,736評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,475評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,834評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,829評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,009評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,559評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,306評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,516評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,038評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,728評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,132評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,443評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,249評論 3 399
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,484評論 2 379

推薦閱讀更多精彩內容