第一章 熟悉Objective-C
1.OC的起源
oc使用了消息結(jié)構(gòu)而非函數(shù)調(diào)用藐守。使用消息結(jié)構(gòu)的語言挪丢,其運行時所執(zhí)行的代碼由運行環(huán)境決定,而使用函數(shù)調(diào)用的語言卢厂,則由編譯器決定乾蓬。
OC對象所占內(nèi)存總是分配在“堆空間”,而不會分配在“椛骱悖”上任内。分配在堆中的內(nèi)存必須直接管理,而分配棧上的用于保存變量的內(nèi)存 則會在其棧幀彈出時自動清理巧号。
當(dāng)我們看到一個變量類型是已知的族奢,就分配在棧里面姥闭,比如丹鸿,int、double等棚品。其他未知的類型靠欢,比如自定義的類型廊敌,因為系統(tǒng)不知道需要多大,所以程序自己申請门怪,這樣就分配在堆里面百度文庫
2.在類的頭文件中盡量少引入其他頭文件
- 除非有必要骡澈,否則不要引入頭文件。一般來說掷空,應(yīng)在某個類的頭文件里使用向前聲明來提及別的類肋殴,并在實現(xiàn)文件中引入哪些類的頭文件。這樣做可以盡量降低類之間的耦合坦弟。
- 有時無法使用向前聲明护锤,比如要聲明某個類遵循一項協(xié)議。這種情況下酿傍,盡量把“該類遵循某協(xié)議”的這條聲明移至“class-continuation”分類中烙懦。如果不行的話,就把協(xié)議單獨放在一個頭文件中赤炒,然后將其引入氯析。
3.多用字面量語法,少用與之等價的方法
- 應(yīng)該使用字面量語法來創(chuàng)建字符串莺褒、數(shù)值掩缓、數(shù)組、字典癣朗。與創(chuàng)建此類對象的常規(guī)方法相比拾因,這么做更加簡明扼要。(即使用NSStringstr = @"string"*這種方式來創(chuàng)建)
- 應(yīng)該通過取下標(biāo)操作來訪問數(shù)組下標(biāo)或字典中的鍵值所對應(yīng)的元素旷余。(即使用array[1]這種方式)
- 用字面量語法創(chuàng)建數(shù)組或字典時绢记,若值中有nil,則會拋出異常正卧。因此蠢熄,務(wù)必確保值里不含nil。
4.多用類型常量炉旷,少用#define預(yù)處理指令
應(yīng)該使用
static constant NSTimeInterval kAnimationDuration = 0.3;
而不要使用
#define Animation_duration 0.3
如果想要在該常量的編譯單元之外使用签孔,那么使用這種形式
//In the header file
extern NSString *constant EOCStringConstant;//這里用UIKIT_EXTERN會比較好
//In the implementation file
NSString *const EOCStringConstant = @"Value";`
常量定義應(yīng)從右至左解讀,EOCStringConstant就是一個常量窘行,而這個常量是指針饥追,指向NSString對象。(大概意思應(yīng)該是罐盔,const修飾的是常量但绕,那么EOCStringConstant是一個常量,而*是一個指針,最后捏顺,它的類型是NSString六孵。)
- 不要用預(yù)處理指令定義常量。這樣定義出來的常量不含類型信息幅骄,編譯器只是會在編譯前據(jù)此執(zhí)行查找與替換操作劫窒。即時有人重新定義了常量值,編譯器也不會產(chǎn)生警告信息拆座,這將導(dǎo)致應(yīng)用程序中的常量值不一致主巍。
- 在實現(xiàn)文件中使用static const來定義“只有在編譯單元內(nèi)可見的常量”。由于此類常量不在全局符號表中挪凑,所以無須為其名稱加前綴煤禽。
- 在頭文件中使用extern來聲明全局常量,并在相關(guān)實現(xiàn)文件中定義其值岖赋。這種常量要出現(xiàn)在全局符號表中檬果,所以其名稱應(yīng)加以區(qū)隔,通常用與之相關(guān)的類名做前綴恳啥。
5.用枚舉表示狀態(tài)、選項丹诀、狀態(tài)碼
- 應(yīng)該用枚舉來表示狀態(tài)機(jī)的狀態(tài)钝的,傳遞給方法的選項以及狀態(tài)碼等值铆遭,給這些值起個易懂的名字硝桩。
- 如果把傳遞給某個方法的選項表示為枚舉類型,而多個選項又可同時使用枚荣,那么就將各個選項值為2的冪,以便通過按位或操作將其組合起來。
- 用NS_ENUM與NS_OPTIONS宏來定義枚舉類型,并指明其底層數(shù)據(jù)類型。這樣做可以確保枚舉是用開發(fā)者所選的底層數(shù)據(jù)類型實現(xiàn)出來的,而不會采用編譯器所選的類型椭员。
- 在處理枚舉類型的switch語句中不要實現(xiàn)default分支。這樣的話研铆,加入新枚舉之后,編譯器就會產(chǎn)生還有未處理的枚舉。(xcode7.3實踐,反正不加default會有警告集嵌。咽块。。這可能是版本問題?)
第二章 對象、消息院崇、運行期
6.理解“屬性“這一概念
- 可以使用@property語法來定義對象中所封裝的數(shù)據(jù)
- 通過“特性”來指定存儲數(shù)據(jù)所需的正確語義(即copy,atomic等)
- 在設(shè)置屬性所對應(yīng)的實例變量時,一定要遵從該屬性所聲明的語義(比如垦梆,NSString使用copy修飾的時候绞旅,那么在set方法內(nèi)部應(yīng)該使用[string copy])
- 開發(fā)iOS程序時應(yīng)該使用nonatomic屬性,因為atomic屬性會嚴(yán)重影響性能
7.在對象內(nèi)部盡量直接范文實例變量
- 在對象內(nèi)部讀取數(shù)據(jù)時晃琳,應(yīng)該直接通過實例變量來讀围段,而寫入數(shù)據(jù)時灸芳,則應(yīng)通過屬性來寫蕊肥。(直接訪問實例變量比較快谒获,但是使用屬性會遵循屬性的內(nèi)存管理語義,會觸發(fā)鍵值觀察晴埂,出錯的時候通過斷點比較好排查)
- 在初始化方法及dealloc方法中,總是應(yīng)該直接通過實例變量來讀寫數(shù)據(jù)寻定。
- 有時會使用懶加載的初始化方法來配置某份數(shù)據(jù)儒洛,這種情況下,需要通過屬性來讀取數(shù)據(jù)
8.理解“對象等同性”這一概念
- 若想檢測對象的等同性狼速,請?zhí)峁癷sEqual”與hash方法
- 相同的對象必須具有相同的哈希碼琅锻,但是兩個哈希碼相同的對象卻未必相同
- 不要盲目地逐個檢測每條屬性,而是應(yīng)該依照具體需求來指定檢測方案
- 編寫hash方法時向胡,應(yīng)該使用計算速度快而且哈希碼碰撞幾率低的算法
9. 以“類簇模式”隱藏實現(xiàn)細(xì)節(jié)
- 子類應(yīng)該繼承自類簇中的抽象基類
若要編寫NSArray類簇的子類恼蓬,則需令其繼承自不可變數(shù)組的基類或可變數(shù)組的基類。 - 子類應(yīng)該定義自己的數(shù)據(jù)存儲方式僵芹。
開發(fā)編寫NSArray子類時处硬,經(jīng)常在這個問題上受阻。子類必須用一個實例變量來存放數(shù)組中的對象拇派。這似乎與大家預(yù)想的不同荷辕,我們以為NSArray自己肯定會保存那些對象,所以在子類中就無須再存一份了件豌。但是大家要記住疮方,NSArray本身只不過是包在其他隱藏對象外面的殼,它僅僅定義了所有數(shù)組都需具備的一些接口茧彤。對于這個自定義的數(shù)組子類來說骡显,可以用NSArray來保存其實例 - 子類應(yīng)該覆寫超類文檔中指明需要覆寫的方法。
在每個抽象基類中曾掂,都有一些子類必須覆寫的方法惫谤。比如說,想要編寫NSArray的子類珠洗,就需要實現(xiàn)count及其“objectAtIndex:”方法石挂。像lastObject這種方法則無須實現(xiàn),因為基類可以根據(jù)前兩個方法實現(xiàn)出這個方法险污。
在類簇中實現(xiàn)子類所需遵循的規(guī)范一般都會定義于基類的文檔之中痹愚,編碼前應(yīng)該先看看富岳。 - 類簇模式可以把實現(xiàn)細(xì)節(jié)隱藏在一套簡單的公共接口后面(有點像協(xié)議)
- 系統(tǒng)框架中經(jīng)常使用類簇
- 從類簇的公共抽象基類中繼承子類時要小心,若有開發(fā)文檔拯腮,則應(yīng)先閱讀
10. 在既有類中使用關(guān)聯(lián)對象存放自定義數(shù)據(jù)
- 可以通過“關(guān)聯(lián)對象”機(jī)制來把兩個對象連起來(就是runtime里面的objc_getAssociateObject與objc_getAssociatedObject關(guān)聯(lián)的參考)
- 定義關(guān)聯(lián)對象時可指定內(nèi)存管理語義窖式,用以模仿定義屬性時所采用的“擁有關(guān)系”與“非擁有關(guān)系”。
- 只有在其他做法不可行時才應(yīng)選用非關(guān)聯(lián)對象动壤,因為這種做法通常會引入難以查找的bug萝喘。
11. 理解objc_msgSend的作用
C語言的函數(shù)調(diào)用方式:C語言使用“靜態(tài)綁定”,也就是說琼懊,在編譯期就能決定運行時所應(yīng)調(diào)用的函數(shù)阁簸。編譯器在編譯代碼的時候就已經(jīng)知道程序中有哪些函數(shù)了。于是會直接生成調(diào)用這些函數(shù)的命令哼丈。
??動態(tài)綁定:在OC中启妹,如果向某對象傳遞消息,那就會使用動態(tài)綁定機(jī)制來決定需要調(diào)用的方法醉旦。在底層饶米,所有的方法都是普通的C語言函數(shù),然而對象收到消息之后车胡,究竟該調(diào)用哪個方法則取決于運行期決定檬输,甚至可以在程序運行時改變,這些特性使得OC成為一門真正的動態(tài)語言匈棘,類似如下
void printHello(){
printf("Hello, world!");
}
void printGoodbye(){
print("goodbye,world!\n");
}
void doTheThing(int type){
void (*fnc)();
if(type == 0){
fnc = printHello;
}else{
fnc = printGoodby;
}
}
一般的方法調(diào)用:
id returnValue = [someObject messageName:parameter]
本例中丧慈,somObject叫做“接收者”(receiver),messageName叫做“選取器”(selector)主卫。選取器與參數(shù)合起來稱為“消息”(message)伊滋。編譯器看到此消息后,將其轉(zhuǎn)換為一條標(biāo)準(zhǔn)的C語言函數(shù)調(diào)用队秩,所調(diào)用的函數(shù)乃是消息傳遞機(jī)制中的核心函數(shù)笑旺,叫做objc_msgSend,“原型”如下
void objc_msgSend(id self, SEL cmd,...)
??這是個“參數(shù)個數(shù)可變的函數(shù),能接受兩個或兩個以上的參數(shù)馍资。第一個參數(shù)代表接收者筒主,第二個代碼選取器,后續(xù)參數(shù)就是消息中的那些參數(shù)鸟蟹,其順序不變乌妙。選取器指的就是方法名〗ㄔ浚“選取器”與“方法”這兩個詞經(jīng)常交替使用藤韵。編譯器會把剛才那個例子中的消息轉(zhuǎn)換為如下函數(shù):
id returnValue = objc_msgSend(someObject,
@selector(messageName:),
parameter);
objc_msgSend函數(shù)會一句接收者與選取器的類型來調(diào)用適當(dāng)?shù)姆椒ā榱送瓿纱瞬僮餍芫摲椒ㄐ枰诮邮照咚鶎俚念愔兴褜て洹胺椒斜怼痹笏遥绻苷业脚c選取器名稱相符的方法欲险,就調(diào)至其實現(xiàn)代碼。沒有就沿著繼承體系向上查找匹涮。如果最終找不到天试,就執(zhí)行“消息轉(zhuǎn)發(fā)”操作。
還有另外一些邊界情況然低,將交由oc運行環(huán)境中的另一些函數(shù)來處理:
objc_msgSend_stret:如果待發(fā)送消息要返回結(jié)構(gòu)體喜每,那么可交由此函數(shù)處理。
objc_msgSend_fpret:如果消息返回的是浮點數(shù)雳攘,那么可交由此函數(shù)處理带兜。
objc_msgSendSuper:如果需要給超類發(fā)消息,例如[super message:parameter],那么就交由此函數(shù)處理吨灭。
消息接收者刚照、選取器及參數(shù)構(gòu)成。給某對象“發(fā)送消息”也就相當(dāng)于在該對象上“調(diào)用方法”
發(fā)給某對象的全部消息都要由“動態(tài)消息派發(fā)系統(tǒng)”來處理沃于,該系統(tǒng)會查出對應(yīng)的方法涩咖,并執(zhí)行其代碼
12. 理解消息轉(zhuǎn)發(fā)機(jī)制
消息轉(zhuǎn)發(fā)分為兩大階段海诲,第一階段先征詢接收者所屬的類繁莹,看其是否能動態(tài)添加方法,以處理當(dāng)前這個“未知的選取器”特幔,這叫做“動態(tài)方法解析"咨演。第二階段涉及“完整的消息轉(zhuǎn)發(fā)機(jī)制”,如果runtime已經(jīng)把第一階段執(zhí)行完了蚯斯,那么接收者自己無法再以動態(tài)添加的方法來相應(yīng)包含該選取器的消息了薄风。此時,運行期系統(tǒng)會請求接收者以其他手段來處理與消息相關(guān)的方法調(diào)用拍嵌。這又細(xì)分為兩部遭赂,首先,看有沒其他對象處理横辆。有就給它撇他。即“備援的接收者”。沒有狈蚤,則啟動“完整的消息轉(zhuǎn)發(fā)機(jī)制”困肩。runtime會把與消息有關(guān)的全部細(xì)節(jié)都封裝到NSInvocation對象,再給接收者最后一次機(jī)會脆侮。
動態(tài)方法解析:
對象在收到無法解讀的消息后锌畸,首先將調(diào)用其所屬類的下列表方法
+ (BOOL)resolveInstanceMethod:(SEL)selecotor
??該方法的參數(shù)就是那個未知的選取器,其返回值表示這個類能否新新增一個實例方法用以處理這個選取器靖避。在繼續(xù)往下執(zhí)行轉(zhuǎn)發(fā)機(jī)制之前潭枣,本類有機(jī)會新增一個處理此選取器的方法比默。加入尚未實現(xiàn)的方法不是實例方法,而是類方法卸耘,那么會調(diào)用另一個叫resolveClassMethod:
的方法
??使用這種方法的前提是退敦,相關(guān)方法的實現(xiàn)代碼已經(jīng)寫好,只等著運行時蚣抗,動態(tài)插在類里就可以了侈百。此方法常用來實現(xiàn)@dynamic屬性。
備援接收者
當(dāng)前接收者還有第二次機(jī)會能處理未知的選取器翰铡,在這一步中钝域,runtime系統(tǒng)會問它,能不能把這條消息轉(zhuǎn)給其他接收者來處理锭魔。其對應(yīng)處理方法:
- (id)forwardingTargetForSelector:(SEL)selector
??方法參數(shù)代表未知的選取器例证,若當(dāng)前接收者能找到備援對象,則將其返回迷捧,找不到织咧,就返回nil。通過此方案漠秋,我們可以用“組合”來模擬出“多重集成”的某些特性笙蒙。在一個對象內(nèi)部,可以還有其他對象庆锦,改對象經(jīng)由此方法將能夠處理某選取器的相關(guān)內(nèi)部對象返回捅位,這樣的話,看起來就是該對象處理的搂抒。
注意艇搀,我們無法操作經(jīng)由這一步所轉(zhuǎn)發(fā)的消息,如果想在發(fā)送給備援接收者之前修改消息內(nèi)容求晶,那就得通過完整的消息轉(zhuǎn)發(fā)機(jī)制來做焰雕。
完整的消息轉(zhuǎn)發(fā)
如果轉(zhuǎn)發(fā)算法已經(jīng)來到這一步的話,那么唯一能做的就是啟用完整的消息轉(zhuǎn)發(fā)機(jī)制芳杏。首先創(chuàng)建NSInvocation對象矩屁,把尚未處理的那條消息有關(guān)的全部細(xì)節(jié)全部封裝與其中。此對象包含選取器蚜锨,目標(biāo)(target)及參數(shù)档插。在觸發(fā)NSInvocation對象時,“消息派發(fā)系統(tǒng)”將親自出馬亚再,把消息指派給目標(biāo)對象郭膛。
此步驟會調(diào)用下列方法來轉(zhuǎn)發(fā)消息:
- (void)forwardInvocation:(NSInvocation *)invocation
這個方法可以實現(xiàn)的很簡單:只需改變調(diào)用目標(biāo),使消息在新目標(biāo)上得以調(diào)用即可氛悬。然而這樣實現(xiàn)出來的方法與“備援接收者”方案所實現(xiàn)的方法等效则剃。比較有用的實現(xiàn)方法:在觸發(fā)消息前耘柱,先以某種方式改變消息內(nèi)容,比如追加另一個參數(shù)棍现,或者改換選取器等调煎。
實現(xiàn)此方法時,若發(fā)現(xiàn)某調(diào)用操作不應(yīng)由本類處理己肮,則需調(diào)用超類的同名方法士袄。這樣的話,繼承體系中的每個類都有機(jī)會處理此調(diào)用請求谎僻,直至NSObject娄柳。如果最后調(diào)用了NSObject類方法,那么該方法還會繼而調(diào)用doesNotRecognizeSelector:
以拋出異常艘绍,此異常表明選取器最終未能得到處理赤拒。
- 若對象無法響應(yīng)某個選取器,則進(jìn)入消息轉(zhuǎn)發(fā)流程
- 通過運行期的動態(tài)方法解析功能诱鞠,我們可以在需要用到某個方法時再將其加入類中
- 對象可以把其無法解讀的某些選取器交給其他對象來處理
- 經(jīng)過上述兩部之后挎挖,如果還是沒辦法處理,那就只能啟動完整的消息轉(zhuǎn)發(fā)機(jī)制(目前能看出的用處:1. 理解了錯誤的時候拋出異常的地方航夺,2. 用于實現(xiàn)類似多繼承的效果)
13. 用"方法轉(zhuǎn)換“調(diào)試”黑盒方法“
使用** 方法轉(zhuǎn)換 **可以讓我們既不需要源代碼蕉朵,也不需要通過繼承子類來覆寫方法就能改變這個類本身的功能。這樣一來敷存,新功能將在本類的所有實例中生效墓造,而不是僅限于腹寫了相關(guān)方法的那些子類實例堪伍。
??類的方法列表會把選取器的名稱映射到相關(guān)的方法實現(xiàn)之上锚烦,使得“動態(tài)消息派發(fā)系統(tǒng)”能夠據(jù)此找到應(yīng)該調(diào)用的方法。這些方法均以函數(shù)指針的形式來表示帝雇,這種指針叫IMP涮俄,原型如下
id (*IMP)(id,SEL, ...)
NSString類可以響應(yīng)uppercaseString等選取器。
?#emsp;想交換方法實現(xiàn)尸闸,可用下列函數(shù):
void method_exchangeImplementations(Method m1, Method m2)
此函數(shù)的兩個參數(shù)表示待交換的兩個方法實現(xiàn)彻亲,而方法實現(xiàn)則可通過下列函數(shù)獲得:
Method class_getInstanceMethod(Class aClass, SEL aSelector)
此函數(shù)根據(jù)給定的選擇從類中去除與之相關(guān)的方法。執(zhí)行下列代碼吮廉,即可交換比如lowercaseString和uppercaseString方法實現(xiàn):
Method originalMethod = class_getInstanceMethod([NSStringclass], @selector(lowercaseString));
Method swappedMethod = class_getInstanceMethod([NSStringclass], @selector(uppercaseString));
method_exchangeImplementations(originalMethod, swappedMethod);
如果想要在調(diào)用lowercaseString時記錄某些信息苞尝,這時可以通過交換方法來實現(xiàn)
??新方法可以添加到NSString的一個category鐘:
@interface NSString (EOCMyAdditions)
- (NSString *)eoc_myLowercaseString;
@end
上述新方法將與原有的lowercaseString方法呼喚
??新方法的實現(xiàn)可以這樣寫:
@implementation NSString (EOCMyAdditions)
- (NSString *)eoc_myLowercaseString{
NSString *lowercase = [self eoc_myLowercaseString];
NSLog(@"%@ = > %@", self, lowercase);
return lowercase;
}
@end
這段代碼看上去好像會是死循環(huán),不過要記住宦芦,此方法是準(zhǔn)備和lowercaseString方法呼喚的宙址。所以,在運行期调卑,eoc_myLowercaseString選取器實際上對應(yīng)于原有的lowercaseString方法實現(xiàn)抡砂。最后通過以下來交換實現(xiàn):
Method originalMethod = class_getInstanceMethod([NSStringclass], @selector(lowercaseString));
Method swappedMethod = class_getInstanceMethod([NSStringclass], @selector(eoc_myLowercaseString));
method_exchangeImplementations(originalMethod, swappedMethod);
- 在運行期大咱,可以向類中新增或替換選取器所對應(yīng)的方法實現(xiàn)
- 使用另一份實現(xiàn)來替換原有的方法實現(xiàn),這道工序叫做** 方法轉(zhuǎn)換 **注益,開發(fā)者常用詞技術(shù)向原有實現(xiàn)中添加新功能
- 一般來說碴巾,只有調(diào)試程序的時候才需要在運行期修改方法實現(xiàn),這種做法不宜濫用
14. 理解類對象的用意
- 每個實例都有一個指向Class對象的指針丑搔,用以表明其類型厦瓢,而這些Class對象則構(gòu)成了類的繼承體系
- 如果對象類型無法再編譯期確定,那么就應(yīng)該使用類型信息來查詢方法探知(isEquelto)
- 盡量使用類型信息查詢方法來確定對象類型啤月,而不要直接比較類的對象旷痕,因為某些對象可能實現(xiàn)了消息轉(zhuǎn)發(fā)功能