前言
??本文是對《Effective Objective-C 2.0編寫高質量iOS和OSX代碼的52個有效方法》這本書的總結和個人學習備忘錄,僅供參考,詳情可購買電子書或者紙質書。
第一章 熟悉Objective-C
第一條:了解Objective-C語言的起源
??OC為C的超集,其使用“消息結構”而非“函數調用”,運行時所執行的代碼由運行環境來決定,使用函數調用的語言則由編譯器決定。
第二條:在類的頭文件里盡量少引用其他頭文件
??引用過多頭文件會增加編譯時間,如h文件中不需要知道類或協議的具體實現可以使用@class和@protocol讓編譯器識別該名字。也能解決兩個h文件互相引用引起的循環引用問題,雖然用#import不會像#include一樣導致死循環,但其中一個文件將無法正確編譯。
第三條:多用字面量語法,少用與之等價的方法
??OC以語法繁雜著稱,使用字面量可以使代碼更加簡潔,增加代碼的易讀性。但是除了字符串,字面量所創建的對象必須屬于Foundation框架,創建NSArray、NSDictionary和NSNumber子類需要使用非字面量語法,其子類不常見。
第四條:多用類型常量,少用#define預處理指令
??例:#define ANIMATION_DURATION 0.3f 該預處理指令會把所有碰到的ANIMATION_DURATION字符串替換成0.3,但此定義沒有類型信息,static const NSTimeInterval kAnimationDuration = 0.3f;使用此定義的常量包含類型信息,可以描述常量的含義。
??變量一定要同時用static和const聲明,避免其被修改。如果常量局限于某“編譯單元”(通常指某一類的實現文件,即.m),則在命名的時候前面加k,如外部可見,則通常命名前加類名。static修飾意味著該變量僅在當前編譯單元可見,如果不加則編譯器會為創建一個外部符號,在.h文件用使用extern關鍵字即可讓外部調用,此時常量在全局符號表中。如果同時使用static和const,編譯器并不會創建符號,會像#define一樣替換變量為常值。
第五條:用枚舉表示狀態、選項、狀態碼
??每一個狀態都用一個便于理解的值來與之對應,其代碼更易讀易懂。單選使用NS_ENUM,多選使用NS_OPTIONS宏。
第二章 對象、消息、運行期
第六條:理解“屬性”這一概念
??使用屬性,編譯器會自動幫你生成setter和getter方法,以及一個帶下劃線的成員變量。在實現文件使用@synthesize關鍵字可以指定實例變量的名字,在同時重寫setter和getter方法時,編譯器不會自動幫你生成成員變量,需要使用@synthesize來指定實例變量的名字。使用@dynamic關鍵字就不會生成成員變量以及存取方法。直接在.h{}里聲明成員變量,需要用@public關鍵字才能使外部調用,而且需要用類似于C++的->來取值,基本數據類型為值拷貝,對象類型為強引用。
??attribute也會影響屬性的存取方法,默認為atomic、readwrite、assign或strong(基本數據類型和對象類型)。
- assign 設置方法只會執行針對“純量類型”的簡單賦值操作。
- strong 該特質定義了一種“擁有關系”,為此屬性設置新值時,會保留新值,釋放舊值,然后將新值設置上去。如MRC下的賦值:[_oldVlue release]; _oldVlue = [newValue retain];
- weak 該特質定義了一種“非擁有關系”,為此屬性設置新值時,不會保留新值也不會釋放舊值,但在對象摧毀時,會將屬性值清空。
- unsafe_unretained 此特質語義與assign相同,但是assign用來修飾基本數據類型,unsafe_unretained用來修飾對象類型,表達一種“非擁有關系”,對象摧毀時不會清空。
- copy 與strong類似,但設置方法不保留新值,而是copy。在修飾NSSrting時,copy和strong相同,都是對原地址進行強引用,但修飾NSMutableString時,copy會copy出新的一份內存地址,并指向該地址。理論上實現NSCopying協議的對象都可以用copy修飾,賦值時會走copyWithZone方法,但一般用于string,看業務需求。
??getter=<name>指定獲取方法名,setter=<name>指定賦值方法名,不常見。
??如果在聲明文件中,使用了readOnly修飾屬性,在實現文件可以重寫該屬性并用readWrite修飾,就可以在實現文件中改寫該屬性。
??atomic會通過鎖定機制來確保其操作的原子性,但在一個線程多次讀取某屬性值的過程中有別的線程在修改該值,即使聲明atomic也會讀取到不同的值,而且使用同步鎖的開銷大,一般都使用nonatomic。atomic只是在setter和getter中,增加了同步線程鎖,保證數據的存取安全,但是(敲黑板),如果不涉及到setter和getter方法呢,例如對mutableArray的相關操作,atomic并沒有用。
第七條:在對象內部盡量直接訪問實例變量
??讀取數據時,直接使用成員變量,寫入數據時使用屬性。在初始化和dealloc中,應總是通過實例變量來讀寫數據。懶加載除外。無非就是節省了調用getter的方法的時間。
第八條:理解“對象等同性”這一概念
??等同性,即equal,比較兩個對象是否相同。NSObject中isEqual的默認實現是,當且僅當其指針值完全相等時,兩個對象才相等。當兩個對象equal時,hash也必須返回同一個值,但hash返回同一個值,兩個對象不一定相等。
??一般重寫對象isEqual方法時,都要重寫hash方法,否則就會違反Object.hashCode的通用約定,從而導致該類無法與所有基于散列值(hash)的集合類結合在一起正常運行,這樣的集合類包括HashMap(nsdictionary)、HashSet(nsset)、Hashtable(nsarray)。
第九條:以“類族模式”隱藏實現細節
??對外開放一個抽象類,例如UIButton,初始化時傳入一個Type,相關實現交由子類,隱去實現細節。系統框架中經常使用類族。
第十條:在既有類中使用關聯對象存放自定義數據
??在開發中偶爾會有在既有類中添加屬性的需求,一般做法是生成一個既有類的子類,然后在子類中添加相關屬性。但有時候該類是通過某一機制創建的,開發者通過該機制創建子類,這時候就用到了關聯對象(AssociatedObject);
??使用關聯對象,需要引入#import <objc/runtime.h>,其主要函數有三個
設置關聯對象 objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key, id _Nullable value, objc_AssociationPolicy policy)
獲取關聯對象 objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
刪除關聯對象 objc_removeAssociatedObjects(id _Nonnull object)
??可以將對象理解成一個dic,使用key來進行存取,object為當前操作的對象,key為泛型的鍵,一般用靜態變量來作為關鍵字,如:static const char objcKey;然后傳入&objcKey即可。也可以用當前方法名作為關鍵字,如在set方法中傳入:@selector(getterMethod),在getter方法中直接使用_cmd即可。value即需要關聯的值,policy有五種:
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
OBJC_ASSOCIATION_ASSIGN = 0, /**< Specifies a weak reference to the associated object. */
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object.
* The association is not made atomically. */
OBJC_ASSOCIATION_COPY_NONATOMIC = 3, /**< Specifies that the associated object is copied.
* The association is not made atomically. */
OBJC_ASSOCIATION_RETAIN = 01401, /**< Specifies a strong reference to the associated object.
* The association is made atomically. */
OBJC_ASSOCIATION_COPY = 01403 /**< Specifies that the associated object is copied.
* The association is made atomically. */
};
按需傳入即可。
第十一條:理解objc_msgSend的作用
??c語言中函數調用使用靜態綁定,即編譯期就能決定運行時所需調用的函數。例:
void callType(NSInteger type) {
if (type == 0) {
printBye();
} else {
printHello();
}
}
void printHello() {
printf("hello");
}
void printBye() {
printf("bye");
}
??如果不考慮內聯(inline),編譯器在編譯器已經知道兩個函數,會直接生成調用這些函數的指令,函數地址是硬編碼在指令中的。如果改下callType的代碼:
void callType(NSInteger type) {
void (*fun)(void);
if (type == 0) {
fun = printBye;
} else {
fun = printHello;
}
fun();
}
這時就需要使用動態綁定,該例子只有一個函數調用指令,該函數無法硬編碼到指令之中,智能在運行時確定。
??OC中,向對象發送消息,就會使用動態綁定機制來決定需要調用的方法。在底層,所有方法調用,都是c語言函數。在對象收到消息后,該調用哪個方法由運行時決定,甚至可以改變,這些特性使oc成為動態語言。
id returnValue = [object messageName:parameter];
??object即為消息接受者(receiver),messageName為選擇器(selector),messageName和parameter一起組成了消息。編譯器將其裝換為普通的函數調用,就是消息傳遞的核心函數objc_msgSend。
void objc_msgSend(id self, SEL cmd, ...);
??編譯器會把oc方法轉換成以下方法:
id returnValue = objc_msgSend(object, @selector(messageName:), parameter);
??隨后會在當前類的結構體中的方法列表(list of methods)中查找該方法,如果找不到就查找父類的方法列表,直到找到該方法并實現代碼。否則就是執行消息轉發(message forwarding)操作。
??第一次調用方法的時比較繁瑣,隨后objc_msgSend會將匹配結果緩存在快速映射表(fast map)里,每個類都有這一塊緩存,之后調用就會快一些。
- objc_msgSend_stret 如果待發送的消息要返回結構體,可用該函數。當CPU的寄存器容得下該消息返回類型時,該函數才能處理此消息。否則(如結構體太大),由另一個函數執行派發。此時,那個函數會通過在分配在棧上的變量來處理消息所返回的結構體。
- objc_msgSend_fpret 如果返回的是浮點數,可用該函數。該函數是為了處理X86等架構CPU的奇怪狀況。
- objc_msgSendSuper 對父類發送消息。
objc_msgSendSuper_stret(); objc_msgSend_fp2ret()
也是相同的作用
??每個對象的方法都相當于一個簡單的c函數:<return_type> Class_selector(id self, SEL _cmd, ...)//真正的函數可能不一樣
每個類里都有一張表,來存放函數的指針,并以選擇器的名稱作為key,所以在方法調用時才能找到對應的執行函數。該函數和objc_msgSend相似,是為了利用“尾調用優化(tail-call)”。
尾調用優化---------start(原文)
1、尾調用
就是在某一函數最后調用另一個函數
function f(x){
return g(x);
}
在函數的最后一步直接返回另一個函數
// 情況一
function f(x){
let y = g(x);
return y;
}
// 情況二
function f(x){
return g(x) + 1;
}
以上兩種因為有其他操作,都不屬于尾調用
function f(x) {
if (x > 0) {
return m(x)
}
return n(x);
}
尾調用不一定在函數的尾部,只要是最后一步操作即可。
2.尾調用優化
函數調用會在內存里形成一個“調用記錄”,又稱“調用幀(call frame)”,保存調用位置和內部變量等信息。
如果在函數A的內部調用函數B,那么在A的調用記錄上方就會形成一個B的調用記錄,B運行結束之后返回到A,B的調用記錄才會消失。如果在B的內部還調用函數C,那就會形成一個C的記錄棧,以此類推。所以的調用記錄,就形成了一個“調用棧”。
尾調用由于是函數的最后一步,所以不需要保留外層函數的調用記錄,因為調用位置、變量等信息不會再用到,直接用內部函數的調用記錄取代外部函數的調用記錄即可。
function f() {
let m = 1;
let n = 2;
return g(m + n);
}
f();
// 等同于
function f() {
return g(3);
}
f();
// 等同于
g(3);
上述代碼中,如果g函數不是尾調用,則需要保留f函數的調用位置以及變量m和n的值等信息。因為調用g之后,f函數已經結束了,所以執行到最后一步就不需要f函數的相關信息,就可以刪除掉f函數的調用記錄,只保留g函數的調用記錄即可。
以上就叫做“尾調用優化(Tail call optimization)”,即只保存內層函數的調用記錄。如果所有函數都是尾調用,則每次執行都只有一個內層函數的調用記錄,將大大的節省內存。
3.尾遞歸
函數調用自身稱為遞歸,如果尾調用自身則稱為尾遞歸。
遞歸函數很耗費內存,因為需要多次調用自身,會形成n個調用記錄,而且很容易形成“棧溢出(因為c語言沒有沒有內置檢查機制來保證復制到緩存區的的數據不得大于緩沖區的大小,所以在數據足夠大的時候就會形成棧溢出)”。但是尾遞歸只存在一個調用記錄,所以不會發生棧溢出。
int factorial(int n) {
if (n == 1) {
return 1;
}
return n * factorial(n - 1);
}
以上為階乘函數,最多要保存n個調用記錄,復雜度為O(n)。
int factorial(int n) {
return otherFactorial(n, 1);
}
int otherFactorial(int n, int total) {
if (n == 1) {
return total;
}
return otherFactorial(n - 1, total * n);
}
改成尾遞歸后,只保留一個調用記錄,復雜度為O(1)。
尾調用優化---------end
??在oc調用中,開發者并不需要關心這些,因為底層已經幫你處理好了。
??消息由接受者(receiver),選擇器(@selector),以及參數(parameter)構成,發送消息(invoke a message)就是在對象上調用方法(call a method)。
??對對象發送消息,都要由“動態消息派發系統(dynamic message dispath system)”來處理,找到對應函數并處理。
第十二條:理解消息轉發機制
??在對一個對象發送一個它無法識別的消息時,就會啟動“消息轉發機制”。
??消息轉發分為兩階段,第一階段先問接收者所屬的類,看其是否能動態的添加方法,以處理當前未知的選擇器(@selector),這叫做“動態方法解析(dynamic method resolution)”。第二階段涉及“完整的消息轉發機制”,分為兩小步。首先看有沒有其他對象能夠處理該對象,如果有,則運行期系統會把消息轉給該對象,轉發過程結束。如果沒有“備援的接收者(replacement receiver)”,則啟用完整的消息轉發機制,運行期系統會把與消息有關的全部細節全部封裝到NSInvocation對象,給接收者最后一次機會處理當前消息。
動態方法解析
??對象收到無法解讀的消息時,就會走+ (BOOL)resolveInstanceMethod:(SEL)sel
,如果是類方法,就會走+ (BOOL)resolveClassMethod:(SEL)sel
.使用該方法的前提是相關的代碼實現已經寫好,等運行時插入就好,常用來實現@dynamic屬性。
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
NSString *selName = NSStringFromSelector(sel);
if ([selName containsString:@"hello"]) {
class_addMethod(self, sel, (IMP)methodSayHello, "v@:@");
return YES;
} else if ([selName containsString:@"bye"]) {
class_addMethod(self, sel, class_getMethodImplementation(self, @selector(methodSayBye:)), "l@:d");
return YES;
}
return [super resolveInstanceMethod:sel];
}
void methodSayHello(id self, SEL _cmd, id value) {
printf("\n method say hello %ld \n", [value integerValue]);
}
- (NSInteger)methodSayBye:(double)value
{
NSLog(@"method say bye--%f", value);
return (NSInteger)value;
}
??以上就是添加方法的相關代碼,class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types)
,這就是添加方法的函數,cls為添加方法的類,name即方法名,imp為編譯器生成指向函數或者方法的指針,types為我們需要的返回值和參數。例:v@:@,“v”代表返回值為void,“@”為默認參數self,“:”為默認參數選擇器_cmd,最后一個“@”為參數為id類型。常用到的如下:
c A char
i An int
s A short
l A long l is treated as a 32-bit quantity on 64-bit programs.
q A long long
C An unsigned char
I An unsigned int
S An unsigned short
L An unsigned long
Q An unsigned long long
f A float
d A double
B A C++ bool or a C99 _Bool
v A void
* A character string (char *)
@ An object (whether statically typed or typed id)
# A class object (Class)
: A method selector (SEL)
備援接收器
??如果未在上一步為類增加方法,就會進入- (id)forwardingTargetForSelector:(SEL)aSelector
方法,可以返回一個對象讓該對象處理aSelector。此可以模擬“多重繼承”的某些特性。
完整的消息轉發
??如果以上都未能處理,系統將把所有調用的細節封裝成NSInvocation對象,這時候將會調用- (void)forwardInvocation:(NSInvocation *)anInvocation
方法。
??調用invocation對象之前,將會調用- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
,需要將所需的返回值和參數等信息傳進去,否則將不會調用invocation方法。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
[anInvocation setTarget:target];
[anInvocation setSelector:sel];
[anInvocation setArgument:argument atIndex:0];
[anInvocation invoke];
}
消息轉發全流程
??接收者每一步都有機會處理消息,步驟越往后,處理消息的代價越大。如果第一步就處理完消息,就能將方法緩存起來,下次調用的時候不需要走消息轉發機制。總而言之,消息處理的越早越好。
第十三條:用“方法調配技術”調試“黑盒方法”
??使用一份實現來替換原有的方法實現,這道工序叫做“方法調試”,常用來為已有的方法添加新功能。
??一般用來調試程序增加日志,不宜濫用。
??類的方法列表會把選擇器的名字映射到方法的實現上,這些方法均以函數指針的形式來表示,稱為IMP,原型為:id (*IMP)(id, SEL, ...)
.
+ (void)exchangeMethodForLog
{
Method baseMethod = class_getInstanceMethod([self class], @selector(uppercaseString));
Method changeMethod = class_getInstanceMethod([self class], @selector(addLog_uppercaseString));
method_exchangeImplementations(baseMethod, changeMethod);
}
- (NSString *)addLog_uppercaseString
{
NSLog(@"add log for uppercase");
return [self addLog_uppercaseString];
}
??這樣uppercaseString和addLog_uppercaseString互換了實現,每次調用uppercaseString實際上都會調用addLog_uppercaseString,而調addLog_uppercaseString則會走uppercaseString的實現。所以下面方法并不會死循環。
第十四條:理解“類對象”的用意
??每個Objective-C對象實例都是某款內存地址的指針,所以在聲明變量時,類型后面需要加一個*。
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
/// A pointer to an instance of a class.
typedef struct objc_object *id;
/// Represents an instance of a class.
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY; //isa指針,指向metaclass(該類的元類)
#if !__OBJC2__
Class super_class //指向objc_class(該類)的super_class(父類)
const char *name //objc_class(該類)的類名
long version //objc_class(該類)的版本信息,初始化為0,可以通過runtime函數class_setVersion和class_getVersion進行修改和讀取
long info //一些標識信息,如CLS_CLASS表示objc_class(該類)為普通類。ClS_CLASS表示objc_class(該類)為metaclass(元類)
long instance_size //objc_class(該類)的實例變量的大小
struct objc_ivar_list *ivars //用于存儲每個成員變量的地址
struct objc_method_list **methodLists //方法列表,與info標識關聯
struct objc_cache *cache //指向最近使用的方法的指針,用于提升效率
struct objc_protocol_list *protocols //存儲objc_class(該類)的一些協議
#endif
} OBJC2_UNAVAILABLE;
- Class是一個指向objc_class(類)結構體的指針,而id是一個指向objc_object(對象)結構體的指針。
- objec_object(對象)中isa指針指向的類結構稱為objec_class(該對象的類),其中存放著普通成員變量與對象方法 (“-”開頭的方法)。
-
objec_class(類)中isa指針指向的類結構稱為metaclass(該類的元類),其中存放著static類型的成員變量與static類型的方法 (“+”開頭的方法)。
如果一個SomeClass對象繼承自NSObject,則其繼承體系如圖所示:
繼承體系圖
??super_class指針確定繼承關系,isa指針描述實例所屬的類。根據布局關系圖即可執行“類型信息查詢”,對象是否可以響應某個選擇器、是否遵守某個協議、對象位于“類繼承體系”的哪部分。
在類繼承體系中查詢類型信息
??可以用類型信息查詢方法來檢視類繼承體系。比如可以用isMemberOfClass判斷對象是否是某個特定類的實例,isKindOfClass判斷對象是否是某個類或者其子類的實例。
??比較類是否相同時可以直接用“==”判斷,因為類對象為單例。但最好不要直接使用,用類型信息查詢可以正確處理那些使用了消息傳遞機制的對象。比如說某個對象可能把其所有收到的選擇器都轉發給另一個對象。這樣的某個對象叫做“代理(proxy)”,此種對象均以NSProxy作為根類。(NSProxy是一個虛類,用其子類來轉發消息的)
??通常情況下,在此種代理對象(NSProxy的子類)調用class方法獲取的是該代理對象本身所屬的類,而調用isKindOfClass方法時,因為消息全部被轉發到“接受代理的對象”,所有獲取的class為“接受代理對象”所屬的類,兩者會有些不一樣。
- 調用實例方法時,它會首先在自身isa指針指向的objc_class(類)的methodLists中查找該方法,如果找不到則會通過objc_class(類)的super_class指針找到其父類,然后從其methodLists中查找該方法,如果仍然找不到,則繼續通過 super_class向上一級父類結構體中查找,直至根class;
- 調用類方法時,它會首先通過自己的isa指針找到metaclass(元類),并從其methodLists中查找該類方法,如果找不到則會通過metaclass(元類)的super_class指針找到父類的metaclass(元類)結構體,然后從methodLists中查找該方法,如果仍然找不到,則繼續通過super_class向上一級父類結構體中查 找,直至根metaclass(元類);
- 運行的時候編譯器會將代碼轉化為objc_msgSend(obj, @selector (selectorName)),在objc_msgSend函數中首先通過obj(對象)的isa指針找到obj(對象)對應的class(類)。在class(類)中先去cache中通過SEL(方法的編號)查找對應method(方法),若cache中未找到,再去methodLists中查找,若methodists中未找到,則去superClass中查找,若能找到,則將method(方法)加入到cache中,以方便下次查找,并通過method(方法)中的函數指針跳轉到對應的函數中去執行。
??正常創建一個子類如圖所示,pair的意思就是說,每次創建不單單創建一個CustomString類,還會創建一個元類,CustomString類用來存儲實例相關的變量方法協議等,元類就用來存儲類變量方法等。
id obj = objc_allocateClassPair([NSString class], "CustomString", 0);
objc_registerClassPair(obj);
第三章 接口與API設計
??重用代碼應當設計成易于復用的形式,需用用到OC中常見的編程范式(paradigm)。
第十五條:用前綴避免命名空間沖突
??因為OC沒有其他語言內置的命名空間(namespace)機制,所以在命名時要設法避免重名問題,否則編譯時就會出錯。一般情況下加上公司或者個人的前綴作為區分,需要注意的是,C語言函數名字最好也加上前綴,否則在不同文件中定義了相同名字的函數也會報“重復符號錯誤”。
??在自己的開源庫中使用第三方庫,如果是直接拉進工程中,最好改下把第三方庫加上前綴。否則其他人在引用的時候,容易出現錯誤。如果均使用podspec進行相關依賴的話就會好很多。
第十六條 提供“全能初始化方法”
??為對象提供必要信息,以便其能完成工作的初始化方法叫做“全能初始化(designated initializer)”。
??如代碼所示,所有初始化方法都會調用一個全能初始化方法。這樣在底層數據變化時,無需修改其他方法,只需修改全能初始化方法即可。
- (instancetype)initWithName:(NSString *)name sex:(BOOL)sex age:(NSInteger)age address:(NSString *)address
{
if (self == [super init]) {
self.name = name;
self.sex = sex;
self.age = age;
self.address = address;
}
return self;
}
- (instancetype)initWithName:(NSString *)name age:(NSInteger)age
{
return [self initWithName:name sex:YES age:age address:@"hangzhou"];
}
- (instancetype)initWithName:(NSString *)name
{
return [self initWithName:name age:0];
}
第十七條 實現description方法
??一般使用NSLog打印對象時,只能打印出對象的類名和指針,并不能直觀的看到自己所需的信息,所以可以重寫NSObject的description方法,組裝并放回自己所需要的數據。比如可以用Dictionary的形式輸出,對比數據更加直接。
??description只是NSLog打印的數據,如果需要在控制臺里輸出相關的信息,需要重寫debugDescription方法,返回所需要的字符串。
第十八條 盡量使用不可變對象
??如網絡請求回來的狀態碼,原始數據,外部一般只需要讀取,這些數據就可以在h文件里設置成readonly,在m文件里重寫成readwrite即可。這樣,外部在修改值的時候就會報錯。不過,使用kvo或者performSelector可以繞過這層機制,不推薦這么做。
??一般來說,對外公開的collection(set array dic),盡量使用不可變的,如果需要修改其中的值,則可提供對應新增或者刪除的方法,盡量不要直接操作。
第十九條 使用清晰而協調的命名方式
??要遵守OC的命名規范,方法名要言簡意賅,要保持與框架相同的風格。點擊查看更多代碼規范Google Objective-C Style Guide。
方法命名
?1、如果方法的返回值是新創建的,則方法的首單詞應為返回值的類型,除非前面還有修飾詞,如:localizedString。屬性的存取不遵守此原則,一般認為此方法不會創建新對象,即便返回值為內部對象的copy,也認為是其對象本身。如:+ (NSString *)stringByAppendingString:(nonnull NSString *)string;
?2、需要把表示參數類型的名詞放在參數之前。如:+ (NSString *)stringByAppendingString:(nonnull NSString *)string;
?3、如果方法要在當前對象上進行操作,就應該包含動詞;如果執行操作時還需要其他參數,則應該在動詞之后加上名詞。如:- (void)insertString:(NSString *)aString atIndex:(NSUInteger)loc;
?4、不要使用str這樣的簡稱,使用全稱string
?5、BOOL屬性最好加上is,以BOOL為返回值的按場景加上is或者has等修飾詞
?6、get一般用于從調用者本身獲取某種數據的場景,如:- (BOOL)getCString:(char *)buffer maxLength:(NSUInteger)maxBufferCount encoding:(NSStringEncoding)encoding;
類與協議的命名
??保持類和協議命名的唯一性,并遵守框架的命名規范。最前面為公司或者個人的前綴,隨后為模塊的名稱,其次為業務名,最后為類型。如:AHC Scene Log ViewController
第二十條:為私有方法名添加前綴
??用前綴區分私有方法和公有方法可以明了的標識那些方法可用,那些不可用,并可以在有錯誤的時候方便找出錯誤的方法。
??因為OC不能像Java或者C++一樣將方法標價為private,所以在命名時可以對私有方法添加前綴以和公共方法做區分,方便修改和調試。如:- (BOOL)p_isRight;
??因為蘋果官方私有方法使用單一下劃線作為前綴,為了避免方法沖突,開發者需要避免使用單一下劃線作為私有方法前綴。
第二十一條:理解Objective-C錯誤模型
??OC中可以用try-catch來捕獲異常:
@try {
NSArray *array = [NSArray array];
if (array.count == 0) {
@throw [NSException exceptionWithName:@"ExceptionName" reason:@"The array's count is zero" userInfo:@{}];
}
} @catch (NSException *exception) {
NSLog(@"%@---%@---%@", exception.name, exception.description, exception.userInfo);
} @finally {
NSLog(@"finally");
}
??不過比較雞肋,內存溢出野指針都是UncaughtException類型,能捕獲到的都是數組越界、改變不可變對象等代碼中輕易可以避免的錯誤。對于oc思想來說,有錯誤還不崩潰等著過年呢?嗯,這很OC。This is a joke。。。
??自動引用計數在默認情況下不是“異常安全的”,throw之后的代碼將不會自動釋放。可以使用-objc-arc-exceptions打開編譯器的標志,用以生成“異常安全”的代碼。不過很麻煩,OC中并不常見,一般只用于出現極其錯誤的問題,就無需考慮恢復,直接拋出異常使程序退出。
??比如數組越界,如果取出不知名的對象,有可能引發更大的錯誤,不如直接拋出錯誤,讓開發者從根本上解決問題。
??所以一般exception只用來處理嚴重錯誤(fatal error 致命錯誤)。對于其他一般錯誤(nofatal error 非致命錯誤)時,OC一般另返回值為0或者nil來標識。如對象初始化,當入參不滿足條件時即可返回nil表示初始化錯誤。
??不過僅靠返回nil不能明了的告訴使用者具體的錯誤信息,所以可以使用NSError對象,把具體的錯誤原因返回出去。如:
{
NSError *error = nil;
NSData *data = [self dataFromFile:&error];
if (error) {
}
}
- (NSData *)dataFromFile:(NSError **)error
{
if (/* something wrong */) {
if (error) {
*error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorTimedOut userInfo:@@{NSLocalizedDescriptionKey:@"LocalizedDescription"}];
}
}
return nil;
}
??使用ARC時,編譯器會把(NSError **)轉換成(NSError *__autoreleasing *),指針所指的對象會在方法執行完畢之后自動釋放。
第二十二條:理解NSCopying協議
??當需要對一個對象使用copy時,需要實現NSCopying協議- (id)copyWithZone:(nullable NSZone *)zone;
,NSZone不是一個對象,而是使用C結構存儲了關于對象的內存管理的信息。之前開發時,會把內存分為不同的區(zone),對象會創建到對應的區里,防止對象釋放時內存過于碎片化。不過現在程序只有一個區(default zone),apple并不鼓勵開發者使用zone,而且絕大部分情況下開發者并不需要關心。所以并不需要關心zone的信息,在方法里生成你想生成的對象并賦值然后返回即可。
- (id)copyWithZone:(NSZone *)zone
{
id copyObj = [[self class] allocWithZone:zone];
/* 對copyObj進行屬性賦值等操作 */
return copyObj;
}
??如果需要生成可變的版本,實現- (id)mutableCopyWithZone:(nullable NSZone *)zone;
即可。[NSArray mutableCopy] => NSMutableArray ,[NSMutableArray copy] => NSArray,NSDictionary也是同理。復制對象時決定使用淺拷貝還是深拷貝,盡量使用淺拷貝。
第四章 協議與分類
??OC中的“協議(protocol)”和Java中的“接口(interface)”類似,把某些類應該實現的方法定義在協議文件里,然后實現委托。
??“分類(category)”可以方便的不通過繼承為類增加方法,不過也有一些坑需要先理解它來避免。
第二十三條:通過委托與數據源協議進行對象間通信
??“委托模式”:對象把對應的行為責任委托給另外一個類,如TableView的delegate中的- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath;
,tableView把點擊cell事件交予委托者(delegate)處理。
??“數據源模式(Data Source Pattern)”:數據由委托者(dataScource)傳入,以獲取相關必要參數。如TableView的DataSource中的- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section;
,外部將cell數量告訴tableView以展示。
??不過Apple官方并沒有嚴格的按照此模式,如delegate中的- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
明顯是數據源模式,卻放在了delegate協議里。好的學,不好的不要學。。
??在調用代理方法時候,一定要先判斷代理是否實現對應方法[_delegate respondsToSelector:@selector(methodName:)]
,如果有多個方法且會被多次調用,可以使用“位段”對代碼進行優化。
typedef struct {
unsigned int flagA : 1;
unsigned int flagB : 1;
}XDelegateFlag;
{
XDelegateFlag _deleagteFlag;
}
??在setDelegate時,用此結構體將對應的responds結果存起來,在調用之前判斷結構體的值即可,無需再判斷代理是否實現對應方法。
第二十四條:將類的實現代碼分散到便于管理的數個分類之中
??如果一個類功能龐大,其功能實現全堆放在一個實現文件中,則文件將不可避免的雜亂難以閱讀。此時,可以使用分類將該類按功能實現進行拆分,可以方便的開發和定位問題。
??如NSArray中:
@interface NSArray<__covariant ObjectType> : NSObject
@interface NSArray<ObjectType> (NSExtendedArray)
@interface NSArray<ObjectType> (NSArrayCreation)
@interface NSArray<ObjectType> (NSDeprecated)
??如果在開發公共庫時,如調用的方法不希望暴露出去,就可以創建Private分類,供開發者時候,且選擇不暴露該文件,即可隱去實現細節。
第二十五條:總是為第三方類的分類名稱加前綴
??在為既有類添加分類(Category)的時候,分類名及方法名需要加上你的前綴,以免與既有類的方法名相同,否則會覆蓋既有類的方法實現,導致很難排查的程序異常。
??所以在為第三方或者蘋果所有類添加分類時候,其名稱和方法名都需要加上專用的前綴。
第二十六條:勿在分類中聲明屬性
??在分類中可以聲明屬性,但并不會生成setter和getter方法及下劃線的成員變量。可以在.m文件中聲明@dynamic,并手動實現setter及getter方法,用association來存儲變量來實現添加屬性,但并不推薦使用。
??所以最好把所用屬性都聲明在主接口中,分類中只添加方法。
第二十七條:使用“call-continuation分類”隱藏實現細節
??“call-continuation分類”即延展:
@interface ClassName ()
@end
??工作中經常使用,用來隱藏私有屬性及方法,雖然OC的機制導致私有并不是真正的私有(比如可以用KVO或者performSelector來跳過檢測),但依然有其作用,比如在制作靜態的時候可以選擇不暴露屬性及方法名。這樣該類接口文件就會很明了,這樣使用者只需關心暴露的屬性及方法就可以,隱去實現細節。
??如果一個屬性只讀,就可以在接口文件定義為readonly,在延展中重新定義為readwrite并修改屬性值。如果遵循某個協議,也可以在延展中聲明,不需要暴露給外部。
第二十八條:通過協議提供匿名對象
??當不希望暴露實現細節或者提供的對象不確定時,可以選擇使用id類型的匿名對象。只需定義一個protocol,將外部所用到的實現定義在protocol中,給外部返回一個遵守該協議的id類型對象即可。如:
- (id <ProtocolName>)methodName:(id)params;
??這樣返回給外部的即是id類型的匿名對象,雖然也可以在運行時查出該對象,但能在一定程度上隱去具體細節。但使用匿名函數的前提是需要在協議中定義好外部所需要用的屬性及方法并由匿名對象實現,如果類型不敏感,即可以使用其來隱藏細節。
第五章 內存管理
??在ARC中,OC的內存管理變得尤為簡單,幾乎所有的內存管理均由編譯器來處理了,開發者只需要關注業務相關。但在使用C++對象及block時,需要稍微注意一下,Block需要注意內存管理的原因可以點擊這里來學習一下。
第二十九條:理解引用計數
??MRC中需要使用retain、release和autorelease來管理內存,在對象的retainCount為0的時候釋放內存。對象創建好之后其引用計數至少為1。但對象的retainCount并不是特別準,蘋果官方并不推薦使用,在ARC中蘋果官方已經禁止調用retainCount(可以強轉成CFTypeRef對象調用CFGetRetainCount來獲取引用計數)。因為現在均使用ARC,就不再多講。
第三十條:以ARC簡化引用計數
??ARC中的內存已經足夠安全,不需要調用MRC中的那些內存管理方法,在ARC中調用那些方法也會編譯錯誤。ARC也是基于引用計數的,引用計數還要執行,不過ARC會幫你執行。它直接調用底層的C語言來管理,比如obj_retain等函數,這樣效率更好,節省很多CPU周期。
??內存管理語義在方法名中已經表現出來,比如方法名以alloc、new、copy、mutableCopy
開頭時,則其返回的對象歸調用者所有。意思就是說,返回的對象引用計數必定為正數,在MRC中需要進行release或者autorelease操作。其他情況下,表示返回的對象不歸調用者所有,不用對其進行內存管理。如需要額外持有,需要進行retain操作。
??除了自動調用“保留”與“釋放”方法外,使用ARC還有其他好處,它可以執行一些手工操作很難或者無法完成的優化。比如,可以在編譯期把能夠互相抵消的retain、release、autorelease操作簡約。如果發現在同一個對象上執行了多個“保留”和“釋放”操作,ARC有時可以成對的移除這個操作。
??因為向后兼容性,用以兼容不使用ARC的代碼。為了優化代碼,在方法中返回自動釋放的對象時,此時不直接調用對象的autorelease方法,而是調用objc_autoreleaseReturnValue,此函數會檢視當前方法返回之后即將執行的那段代碼。如果這段代碼要在返回的對象上執行retain操作,則設置全局數據結構(此數據結構的具體內容因處理器而異)中的一個標志位,而不執行autorelease操作。與之相似,當需要保留此對象時,此時不直接執行retain,而是改為objc_retainAutoreleaseReturnValue函數。此函數會檢測剛才的標志位,若已經置位則不執行retain操作。設置并檢測標志位比調用autorelease和retain更快。
??使用ARC可以減少很多不需要的代碼,ARC管理內存的方法基本上就是在合適的地方插入“保留”和“釋放操作”。OC中的方法名語義很重要,要注意命名規范。ARC只負責OC對象內存,CoreFoundation對象需要使用(CFRetain/CFRelease)手動管理。
持續更新....