前言
? ? 要啥前言超埋,直接就是擼搏讶。
1、OC與C
? ? ? OC是C的超集霍殴,所有C語言的特性媒惕,在OC上都可以使用。同時掌握這兩門語言来庭,對于提升OC的代碼效率非常重要妒蔚。尤其是理解C語言的內(nèi)存模型,有助于理解OC的“引用計數(shù)”機制的工作原理月弛,不過現(xiàn)在ARC已經(jīng)很普遍了肴盏,很多剛上道的OCer 基本不太理解引用計數(shù),根本不用嘛帽衙。Swift的出現(xiàn)更加讓OC尷尬菜皂,總而言之废麻,還是看自己的理解吧戒祠。OC使用動態(tài)綁定的消息結(jié)構(gòu)滩字,也就是說只有在運行時,才會檢查對象蚁堤,收到消息,執(zhí)行哪段代碼由運行期決定而非編譯器杖虾。這點與JAVA完全不同条辟。
2、頭文件引入
? ? 這個是老梗了胳施,相信老的OCer都應該了解溯祸。我再復述一遍吧。
? ? 當在a.h頭文件中需要用到b類文件時舞肆,偷懶的童鞋會直接#import "b.h"焦辅,這樣做就有可能引起某些問題了。
編譯時間增加椿胯,當編譯a文件的時候筷登,會將b文件全部編譯。其實在編譯a時不需要知道b文件的全部細節(jié)哩盲,只要知道一個類名就好前方。?
避免頭文件的循環(huán)引用,當a的頭文件#import "b.h",同時在b的頭文件#import "a.h"廉油,這樣就產(chǎn)生了兩個類的循環(huán)引用惠险,當然使用#import 而非#include指令不會導致死循環(huán),但卻意味著兩個類里有一個無法正確編譯抒线。
? ? 針對以上兩個問題班巩,產(chǎn)生了一個名詞“向前聲明”。即通過在a的頭文件中使用@class b嘶炭;就可以在a文件中使用b類抱慌。在a.m文件中再#import "b.h"。這樣就可以減少編譯的時間眨猎,也可以防止了頭文件的循環(huán)引用抑进。
? ? 還有一種情況需要在頭文件中,必須要#import睡陪,即a類繼承自b類或者遵循某類協(xié)議单匣,那么該類需要完整定義,且不能使用向前聲明宝穗。根據(jù)這種情況户秤,可以考慮將某些代碼放入分類(class continuation)中,比如是否遵循某個協(xié)議(或者講協(xié)議的定義放入單獨的頭文件)
3逮矛、多用字面量語法鸡号,少用與之等價的方法
? ?寫OC時,經(jīng)常會用到幾個類须鼎,他們屬于Foundation鲸伴,NSString府蔗、NSNumber、NSArray汞窗、NSDictionary姓赤,針對這幾個類,推薦多用字面量語法進行定義仲吏,減少alloc不铆,init方式初始化。例如
NSString *demoString = @"Demo";
NSNumber *two??????? = @2;
NSArray? *cityArray? = @[@"北京",@"天津"];
NSDictionary *cityDic= @{@"name":@"北京",@"code":@"010"};
NSString *capital??? = cityArray[0];
NSString *capitalName= cityDic[@"name"];
字面量語法實際上是一種“語法糖”裹唆,可另程序更易讀誓斥,減少出錯率。但也有他的局限性许帐。在用NSArray時劳坑,需要先確保值中沒有nil,如果有成畦,則會拋出異常距芬。
4、多用類型變量循帐,少用#define預處理
? ?定義一個動畫執(zhí)行時間為0.3秒框仔,也許有人會這么做:
#define ANIMATION_DURATION 0.3
? 這么寫首先沒有定義出這個常量的類型,duration應該是時間惧浴,但是沒有明確指出。另外如果其他類引用了這個類奕剃,那么所有的ANIMATION_DURATION都會替換成了0.3衷旅,有可能會產(chǎn)生小問題。
? 解決這個問題可以定義一個類型為NSTimeInterval的常量:
static const NSTimeInterval kAnimationDuration 0.3
?這樣可以更清晰標明這個屬性的類型纵朋,也容易讓其他人明白其作用柿顶。需要注意的是命名規(guī)范,弱常量局限于實現(xiàn)文件之內(nèi)操软,則在前面加字幕k嘁锯,如果供外部類使用,需要加類名前綴聂薪。
5家乘、用枚舉表示狀態(tài)、選項藏澳、狀態(tài)碼
? 每一個狀態(tài)都用一個便于理解的值來表示仁锯,寫出來的代碼更容易理解。
? 專門說一下翔悠,如果選項是可組合的业崖,更需要用枚舉了野芒。各選項之間通過“按位或操作符”來組合,使用方式如下:
typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) {
????UIViewAutoresizingNone???????????????? = 0,
????UIViewAutoresizingFlexibleLeftMargin?? = 1 << 0,
????UIViewAutoresizingFlexibleWidth??????? = 1 << 1,
????UIViewAutoresizingFlexibleRightMargin? = 1 << 2,
????UIViewAutoresizingFlexibleTopMargin??? = 1 << 3,
????UIViewAutoresizingFlexibleHeight?????? = 1 << 4,
????UIViewAutoresizingFlexibleBottomMargin = 1 << 5
};
? 這樣双炕,每個選項都可以啟用或禁用狞悲,在做判斷的時候,用“按位與運算符”判斷是否啟用:
enum UIViewAutoresizing resize = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
if (resize & UIViewAutoresizingFlexibleHeight) {
????//處理height
}
?6妇斤、理解屬性的概念
? ?屬性特質(zhì)
原子性:如果屬性具有nonatomic摇锋,標明屬性不使用同步鎖,未聲明atomic趟济,標明屬性是原子的乱投。區(qū)別在于atomic特質(zhì)的獲取方法會通過鎖定機制確保其操作的原子性,即多個線程讀同一屬性顷编,無論何時總能看到有效值戚炫。如果不加鎖的情況,當某一線程正在改寫值時媳纬,另外一個線程把未修改的屬性值取出來双肤,結(jié)果可能不對。iOS的同步鎖開銷較大會帶來性能問題钮惠,盡量少用茅糜。
? ? ? ? 讀寫權(quán)限:具有readwrite權(quán)限的屬性擁有g(shù)etter和setter方法,如果該屬性由@synthesize實現(xiàn)素挽,則編譯器會自動生成這兩個方法蔑赘。
? ? ? ? ? ? ? ? ? ? ? ? ? 具有readonly權(quán)限的屬性只擁有g(shù)etter方法,
? ? ? ? 內(nèi)存管理語義:
? ? ? ? ? ? ? ? ? ? ? ? ?assign:“設置方法”只會針對“純量類型”的簡單賦值操作预明。
? ? ? ? ? ? ? ? ? ? ? ? ?strong:此特質(zhì)表明屬性定義了一種“擁有關系”缩赛,為這種屬性設置新值時,會先保留新值撰糠,并釋放舊值酥馍,然后再將新值賦值上去。
? ? ? ? ? ? ? ? ? ? ? ? ?weak : 此特質(zhì)表明屬性定義了一種“非擁有關系”阅酪,為這種屬性設置新值時旨袒,既不保留新值,也不釋放舊值术辐,同assign類似砚尽,然而在屬性所指的對象被銷毀時,屬性值也會清空
? ? ? ? ? ? ? ? ? ? ? ? ?unsafe_unretained: 此特質(zhì)和assign相同辉词,但是只適用于對象類型尉辑,表達一種“非擁有關系”,當目標對象被銷毀時较屿,屬性值不會被清空隧魄,同weak不同卓练。
? ? ? ? ? ? ? ? ? ? ? ? copy: 此特質(zhì)與strong類似,然而設置方法并不保留新值购啄,而是將其copy襟企,當屬性為NSString類型時,經(jīng)常用此特質(zhì)狮含,保護其封裝性顽悼。因為傳遞的值很可能是NSMutableString,即NSString的子類几迄,若是不拷貝字符串蔚龙,那么設置完后,可能會在不知情的情況下遭人篡改映胁,所以這時就要拷貝一份“不可變的”字符串木羹。
? ? ? ? 方法名:getter = 指定獲取方法的名稱,如果某屬性是BOOL類型解孙,你想為其getter方法添加is前綴坑填,就可以通過這個指定。
? ? ? ? ? ? ? ? ? ? ? setter = 指定設置屬性的名稱弛姜,不常用脐瑰。
7、對象內(nèi)部盡量直接訪問實例變量
? ? 屬性訪問和直接訪問的區(qū)別:
直接訪問不需要經(jīng)過OC的方法派發(fā)廷臼,所以訪問實例變量的速度比較快
直接訪問不會調(diào)用“設置方法”苍在,會繞過屬性定義的內(nèi)存管理語義,比如一個屬性定義為copy荠商,如果直接訪問寂恬,并不會拷貝屬性,而是保留新值结啼,釋放舊值掠剑。
如果直接訪問實例變量屈芜,不會觸發(fā)KVO郊愧。
通過屬性訪問有助于排查與之相關的錯誤。
8井佑、理解“對象等同性”的概念
? ? 提到“等同性”属铁,可能第一個想法就是"==",但是"=="比較出來的結(jié)果未必是我們想要的結(jié)果躬翁,因為他比較的是兩個指針本身焦蘑,而不是所指的對象。一般來說盒发,兩個不同類型的對象總是不相等的例嘱,如果已經(jīng)知道兩個受測對象是相同的狡逢,那么可以看下如下代碼:
NSString *foo = @"ZRC123";
NSString *foo1 = [NSString stringWithFormat:@"ZRC %@",@"123"];
BOOL equalA = foo == foo1; //equalA = NO
BOOL equalB = [foo isEqual:foo1]; // equalB = YES
BOOL equalC = [foo isEqualToString:foo1]; //equalC = YES
? ? 從上述代碼不難看出“==”與“isEqual”之間的區(qū)別,NSString實現(xiàn)了自己獨有的等同性判斷方法拼卵,名叫"isEqualToString"奢浑,傳遞的參數(shù)必須是NSString,否則為undefined腋腮。該方法要比“isEqual”快雀彼,后者要執(zhí)行其他方法。
? ? NSObject協(xié)議中有兩個用于判斷等同性的關鍵方法:
? ? -(BOOL)isEqual:(id)object;
? ? -(NSUInteger)hash;
? ?如果“isEqual”判定兩個對象相同即寡,則hash值返回的值必定相同徊哑,但如果hash值返回相同,那么“isEqual”未必會認為兩個對象相同聪富。
? ?如果經(jīng)常需要判斷等同性莺丑,很可能需要自己去實現(xiàn)一個判斷方法,無需檢測參數(shù)類型善涨,可以大大提升檢測速度窒盐。在編寫判定方法時,也應一并復寫“isEqual”方法钢拧。常用的復寫“isEqual”蟹漓,是判斷受測參數(shù)與接受該消息的參數(shù)是否屬于同類,如果屬于源内,調(diào)用自己編寫的判定方法葡粒,如果不同,則調(diào)用超類膜钓。
等同性執(zhí)行深度
? ?創(chuàng)建等同性判斷方法時嗽交,需要決定是根據(jù)整個對象來判斷,還是根據(jù)其中幾個字段來判斷颂斜。NSArray判斷是先根據(jù)數(shù)組所含個數(shù)是否相同夫壁,若相同,則在每個位置的的兩個對象調(diào)用isEqual沃疮,如果每個位置的對象均相等盒让,這就叫做“深度等同性判斷”。
容器中可變類的等同性
? ? 還有一種情況需要注意司蔬,就是在容器中放入可變類對象邑茄,就不應該再改變其哈希碼了,舉個例子:
NSMutableSet *set = [NSMutableSet new];
NSMutableArray *arrayA = [@[@1,@2] mutableCopy];
[set addObject:arrayA];
NSLog(@"set is %@",set);
//output set is {((1,2))}
? ? 現(xiàn)在set中含有一個數(shù)組對象俊啼,數(shù)組中有兩個元素肺缕,再向set中添加另外一個數(shù)組,此數(shù)組與前一個數(shù)組對象相同,順序也相同:
NSMutableArray *arrayB = [@[@1,@2] mutableCopy];
[set addObject:arrayB];
NSLog(@"set is %@",set);
//output set is {((1,2))}
? ? ?此時set中仍然只有一個對象同木,因為已有的數(shù)組與新添加的數(shù)組相同浮梢,所以set不會改變。這次我們添加一個和set中已有的數(shù)組不同的數(shù)組:
NSMutableArray *arrayC = [@[@1] mutableCopy];
[set addObject:arrayC];
NSLog(@"set is %@",set);
//output set is {((1),(1,2))}
? ? ?如我們所料彤路,set中現(xiàn)在又兩個數(shù)組了黔寇。此時我們改變arrayC,另其和最早的數(shù)組相同
[arrayC addObject:@2];
NSLog(@"set is %@",set);
//output set is {((1,2),(1,2))}
set中居然可以包含兩個相同的數(shù)組斩萌,根據(jù)set語義是不允許這樣的缝裤。此時如果我們copy此set,那就更糟了:
NSSet *setB = [set copy];
NSLog(@"setB is %@",setB);
//output setB is {((1,2))}
復制過來的set中又只剩一個對象了颊郎,此set看上去好像由一個空set開始憋飞,通過逐個向其中添加新對象創(chuàng)造出來的。并不是不可以這么做姆吭,只是如果這么做榛做,就要小心其隱患,并用相應的代碼處理可能發(fā)生的問題内狸。
9检眯、理解objc_msgSend的作用
? ? 對象調(diào)用方法是Objective-C經(jīng)常使用的,用OC的術(shù)語來說昆淡,這叫做“消息傳遞”锰瘸,消息有名稱和選擇器(selector),可以接受參數(shù)而且可能有返回值昂灵。在OC中避凝,如果向某對象傳遞消息,會使用動態(tài)綁定機制來決定需要調(diào)用哪個方法眨补。對象收到消息后管削,調(diào)用哪個方法是運行時決定的,甚至可以再運行時改變撑螺。給對象發(fā)送消息可以這樣來寫:
id retureValue = [someObject messageName:param];
? ? 在上邊這個例子中含思,someObject作為接收者(receiver),messageName作為選擇器和param合起來稱為消息甘晤。編譯器看到此消息后含潘,轉(zhuǎn)換為消息傳遞機制的核心函數(shù),叫做“objc_msgSend”安皱,方法原型如下:
void objc_msgSend(id self, SEL cmd,...)
? ? 這是個“函數(shù)參數(shù)可變的函數(shù)”调鬓,能接受兩個及以上的參數(shù)艇炎,第一個參數(shù)代表接收者酌伊,第二個參數(shù)代表選擇器(SEL是選擇器的類型),后續(xù)參數(shù)就是消息的實際參數(shù)。編譯器會把之前的例子轉(zhuǎn)換成:
id returnValue = objc_msgSend(someObject ,?@selector(messageName),param);
為了完成此操作居砖,消息中心會在接收者的類中搜尋方法(list of method)虹脯,如果能找到實現(xiàn)的代碼,則跳至代碼的實現(xiàn)奏候,若是找不到循集,則沿著繼承體系繼續(xù)往上找,找到合適的方法再跳轉(zhuǎn)蔗草。如果最后還是沒找到咒彤,那就執(zhí)行“消息轉(zhuǎn)發(fā)”操作。前面描述的只是部分調(diào)用過程咒精,比如方法緩存之類的都沒有詳細說明镶柱,后續(xù)會進行詳細說明。其他的邊界情況模叙,則需要交由OC運行環(huán)境中的另外的函數(shù)進行處理歇拆。每個類里都有一張表格,選擇器的名稱作為查表的key范咨,objc_msgSend正是通過表格來尋找應該執(zhí)行的方法并跳轉(zhuǎn)的故觅。
10、了解消息轉(zhuǎn)發(fā)機制
? ? 當對象在收到無法解讀的消息之后會發(fā)生什么渠啊?
? ? 我們會遇到消息轉(zhuǎn)發(fā)流程處理的消息输吏,只是未加留意過,如果在控制臺中顯示以下信息替蛉,說明你曾向某個對象發(fā)送了一條無法解讀的消息评也,從而啟動了消息轉(zhuǎn)發(fā)機制,并轉(zhuǎn)發(fā)給了NSObject默認的實現(xiàn)方式灭返。
-[NSCFNumber lowercaseString]: unrecognized selector sent to instance 0x87
? ??上面這段信息是由NSObject的“doesNotRecognizeSelector”方法拋出的盗迟,此異常表明,消息接受者的類型時NSCFNumber,而該接受者無法理解名為“l(fā)owercaseString”的選擇器熙含。這個例子以程序崩潰告終罚缕,不過在編寫自己的類時,可在轉(zhuǎn)發(fā)過程中掛鉤怎静,用以執(zhí)行預定的邏輯邮弹,不讓程序崩潰。
? ? 消息轉(zhuǎn)發(fā)分為兩個階段蚓聘。第一階段是詢問接收者所屬的類腌乡,是否能動態(tài)添加方法,來處理當前未知的選擇器夜牡,叫做“動態(tài)方法解析”与纽。第二階段涉及“完整的消息轉(zhuǎn)發(fā)機制”。如果運行期已經(jīng)把第一階段執(zhí)行完了,那么接受者就無法再用動態(tài)新增方法的手段來響應包含該選擇器的消息了急迂。此時系統(tǒng)會請求接受者以其他手段來處理與消息相關的方法調(diào)用影所。這又細分為兩小步:首先,請接受者看看有沒有其他對象能處理這條消息僚碎,如果有猴娩,則轉(zhuǎn)發(fā)此消息給這個對象;如果沒有“備援的接受者”勺阐,則啟動完整的消息轉(zhuǎn)發(fā)機制卷中,運行期系統(tǒng)把完整的消息封裝成NSInvocation中,再給接受者最后一次機會渊抽。
動態(tài)方法解析
? ? 對象在收到無法解析的消息時仓坞,首先會調(diào)用以下方法:
+ (BOOL)resolveInstanceMethod:(SEL)selector
? ? 該方法的參數(shù)就是未知的選擇器,返回值為BOOL腰吟,表示是否能新增一個實例方法來處理這個選擇器无埃。假如未實現(xiàn)的方法不是實例方法而是類方法,那么運行期系統(tǒng)會調(diào)用“resolveClassMethod”毛雇。使用這種方法的前提是嫉称,相關方法的實現(xiàn)代碼已經(jīng)寫好,只需要在運行時動態(tài)插在里面就可以了灵疮,此方案常用來處理@dynamic屬性织阅。下面的代碼演示了如何使用“resolveInstanceMethod”來實現(xiàn):
void testResolveInstanceMethod(id self, SEL _cmd, NSString *name);
+(BOOL)resolveInstanceMethod:(SEL)sel{
????NSString *methodString = NSStringFromSelector(sel);
????if ([methodString hasPrefix:@"test"]) {
????????class_addMethod(self, sel, (IMP)testResolveInstanceMethod, "B@:@");
????????return YES;
????}
????return [super resolveInstanceMethod:sel];
}
備援接收者
? ? 當前接收者還有第二次機會處理未知的選擇器,在這一步中震捣,運行期系統(tǒng)會詢問他荔棉,能否將這條消息轉(zhuǎn)發(fā)給其他接收者處理,對應的處理方法如下:
-(id)forwardingTargetForSelector:(SEL)selector
? ? 若當前的接收者能找到備援對象蒿赢,則將其返回润樱,若找不到,返回nil羡棵。通過此方案壹若,我們可以用“組合”來模擬出“多重繼承”的某些特性。在一個對象內(nèi)部皂冰,可能含有一系列的對象店展,該對象可經(jīng)由此方法將能夠處理某選擇器的對象返回,這樣在外界看來秃流,好像是由該對象親自處理的赂蕴。需要注意的是,我們無法操作經(jīng)由這一步所轉(zhuǎn)發(fā)的消息舶胀。如果想在發(fā)送給備援接收者之前修改消息概说,那就只能通過完整的消息轉(zhuǎn)發(fā)機制來做了碧注。
完整的消息轉(zhuǎn)發(fā)
? ? 如果轉(zhuǎn)發(fā)算法到達這一步,那么只能通過完整的消息轉(zhuǎn)發(fā)機制了席怪。首先創(chuàng)建NSInvocation對象,把尚未處理的消息全部封裝到里面纤控,包含選擇器挂捻、目標和參數(shù),在觸發(fā)NSInvocation對象時船万,“消息派發(fā)系統(tǒng)”將親自觸發(fā)刻撒,把消息派發(fā)給對象。調(diào)用如下方法:
- (void)forwardInvocation:(NSInvocation*)invocation;
? ? 這個方法實現(xiàn)比較簡單耿导,只需要改變調(diào)用目標声怔,使消息在新目標可以被調(diào)用即可。通過這種方式和備援接收者方式相同舱呻,比較有用的實現(xiàn)方式為:在觸發(fā)消息前醋火,先以某種方式改變消息內(nèi)容。
? ? 實現(xiàn)此方法時箱吕,若發(fā)現(xiàn)某調(diào)用操作不應由本類處理芥驳,則需調(diào)用超類的同名方法,這樣的話茬高,繼承體系中的每個類都有機會處理此請求兆旬,直至NSObject。如果到了NSObject還未處理怎栽,那么該方法會調(diào)用“doNotRecognizeSelector”以拋出異常丽猬,表明選擇器最終未被處理。
11熏瞄、“方法調(diào)配技術(shù)”–swizzling
? ?在上一節(jié)中脚祟,我們介紹了,對象收到消息之后强饮,具體調(diào)用什么方法需要在運行期執(zhí)行愚铡。類的方法列表會把選擇器的名稱映射到相關的方法實現(xiàn)之上,使得動態(tài)消息派發(fā)系統(tǒng)能夠找到具體實現(xiàn)的方法胡陪。這些函數(shù)均以函數(shù)指針的方式來表示沥寥,這種指針叫做IMP,原型如下
? ?id (*IMP) (id, SEL , ...)?
? ?NSString類可以響應lowercaseString,uppercaseString等選擇器柠座,映射表中的每個選擇器都對應到不同的IMP上邑雅,如下所示:
持續(xù)更新中