這段時間因為工作需要,閱讀了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
,不過這兩個關鍵字僅限于使用在指針類型上。而在方法的聲明中,我們還可以使用不帶下劃線的nullable
和nonnull
,如下所示:
- (nullable id)itemWithName:(NSString * nonnull)name
在屬性聲明中,也增加了兩個相應的特性,因此上例中的items屬性可以如下聲明:
@property (nonatomic, copy, nonnull) NSArray * items;
當然也可以用以下這種方式:
@property (nonatomic, copy) NSArray * __nonnull items;
推薦使用nonnull這種方式,這樣可以讓屬性聲明看起來更清晰。
Nonnull區域設置(Audited Regions)
如果需要每個屬性或每個方法都去指定nonnull
和nullable
,是一件非常繁瑣的事。蘋果為了減輕我們的工作量,專門提供了兩個宏:NS_ASSUME_NONNULL_BEGIN和NS_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;
不用多想就清楚下面的數組中存的是什么,避免了 NSString
和 NSURL
的混亂。
自定義泛型類
比起使用系統的泛型容器,更好玩的是自定義一個泛型類,目前這里還沒什么文檔,但攔不住我們寫測試代碼,假設我們要自定義一個 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 實際上的意思是返回一個 UITableViewCell
或 UITableViewCell
子類的實例,于是新的 __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位環境下,UITableViewCellStyle
和 NSInteger
一樣有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
又太開放.