1. OC起源
※ 動(dòng)態(tài)綁定
首先是OC其實(shí)是基于“消息機(jī)制”的,具體可以參考:http://www.reibang.com/p/4f69804d0b4c
當(dāng)我們調(diào)用的時(shí)候:
Student *student = [Student new];
[student getName:name];
代碼會(huì)被翻譯為以下執(zhí)行:(還是類似函數(shù)調(diào)用,但是實(shí)際執(zhí)行的時(shí)候查方法列表來執(zhí)行方法)
objc_msgSend(student,@selector(getName:),name);
如何找到selector嘞抛虏?
就是通過NSObject也就是類的isa指針博其,isa指針結(jié)構(gòu)里面有方法列表。
這一切都是在運(yùn)行時(shí)也就是runtime發(fā)生的迂猴,在編譯的時(shí)候并不會(huì)確定你調(diào)用的是什么方法慕淡,這也是OC可以使用category來增加方法的基礎(chǔ)。而C語(yǔ)言的函數(shù)調(diào)用是靜態(tài)綁定的沸毁,也就是在編譯時(shí)就確定了你調(diào)用的是哪個(gè)函數(shù)峰髓。
在上面的objc_class結(jié)構(gòu)體中,ivars是objc_ivar_list(成員變量列表)指針息尺;methodLists是指向objc_method_list指針的指針携兵。在Runtime中,一個(gè)類objc_class結(jié)構(gòu)體大小是固定的搂誉,不可能往這個(gè)結(jié)構(gòu)體中添加數(shù)據(jù)徐紧,只能修改。
所以ivars指向的是一個(gè)固定區(qū)域炭懊,只能修改成員變量值并级,不能增加成員變量個(gè)數(shù)。methodList是一個(gè)二維數(shù)組凛虽,所以可以修改*methodLists的值來增加成員方法死遭,雖沒辦法擴(kuò)展methodLists指向的內(nèi)存區(qū)域,卻可以改變這個(gè)內(nèi)存區(qū)域的值(存儲(chǔ)的是指針)。
因此,可以動(dòng)態(tài)添加方法俏竞,不能添加成員變量篷朵。
※ 內(nèi)存分配
對(duì)象所占用的內(nèi)存總是在Heap堆里面的,指向?qū)ο蟮闹羔樖窃跅琒tack frame里面的糠聪。
如果我創(chuàng)建兩個(gè)對(duì)象:
NSString *someString = @"Hi";
NSString *anotherString = someString;
heap上面的內(nèi)存需要管理荒椭,而stack里面的在沒有用了以后他會(huì)自動(dòng)被清除掉,這也是為什么其實(shí)基礎(chǔ)變量int之類(定義時(shí)不用*的舰蟆,例如CGRect)的在stack上面的是不需要weak趣惠,只要assign就可以了,因?yàn)樗麄兊膬?nèi)存管理是由stack直接搞的所以不會(huì)有野指針問題身害。
2. 在.h文件中減少引入其他的.h文件
當(dāng)我們import ClassA.h的時(shí)候味悄,如果ClassA.h里面import ClassB.h了,那么編譯的時(shí)候就會(huì)一層一層的import塌鸯,但有可能最后由于if else之類的邏輯分支并沒有用到ClassB侍瑟,那么其實(shí)就白白增加了編譯的時(shí)間,所以其實(shí)ClassA.h文件里面只要聲明ClassB是一個(gè)class就可以了,不用把它的.h文件引入涨颜,在.m文件里面再引入费韭,這樣當(dāng)使用的時(shí)候才會(huì)真正引入。
// Class A.h
#import "ClassB.h"
應(yīng)改為:
// Class A.h
@class ClassB;
// Class A.m
#import "ClassB.h"
而且如果Class A中import了B庭瑰,而B又import A星持,就會(huì)造成循環(huán)引入,于是兩者中有一個(gè)無法正常編譯弹灭。
P.S. import有兩個(gè)作用:一是和include一樣督暂,完完全全的拷貝文件的內(nèi)容;二是可以自動(dòng)防止文件內(nèi)容的重復(fù)拷貝(即使文件被多次包含鲤屡,也只拷貝一份)损痰。
需要注意的是,不是所有狀況都可以用@class聲明然后不實(shí)際引入滴酒来,主要是在繼承以及遵從某協(xié)議的情況下卢未,需要知道具體接口有啥。delegate一般都會(huì)和自己的調(diào)用類放在一起堰汉,所以不會(huì)有這個(gè)問題辽社。
如果在.h文件中實(shí)現(xiàn)某個(gè)協(xié)議,這樣就必須要在.h文件中import那個(gè)協(xié)議翘鸭,而且協(xié)議不能用@protocal聲明但不實(shí)際import(這樣編譯器會(huì)報(bào)錯(cuò)無法找到定義)滴铅,所以最好不要這么做。盡量在.m文件中實(shí)現(xiàn)協(xié)議哈就乓。
3. 盡量使用字面量
屬于Foundation的NSString汉匙、NSArray、NSDictionary以及NSNumber是可以用字面量來賦值的生蚁,例如:
NSNumber *num = @23;
替換:
NSNumber *num = [NSNumber numberWithInt:23];
NSString *str = @"Hi";
替換:
NSString *str = [[NSString alloc] init];
NSArray *arr1 = @[@(1)];
替換:
NSArray * arr1 = [NSArray arrayWithObjects:@1, nil];
NSNumber *num = arr1[0];
替換:
NSNumber *num = [arr1 objectAtIndex:0];
NSDictionary *dict = @{@"key1": @"key2"};
NSString *obj = dict[@"key1"];
替換:
NSDictionary *dict = @{@"key1": @"key2"};
NSString *obj = [dict objectForKey:@"key1"];
這樣的好處是更加簡(jiǎn)潔噩翠,并且如果有nil的話會(huì)立刻crash,防止了一些容錯(cuò)導(dǎo)致有問題無法發(fā)現(xiàn)邦投。
需要注意的是伤锚,如果是NSMutableXXX可以利用mutableCopy,雖然增加了一個(gè)對(duì)象但是也是有點(diǎn)大于缺點(diǎn)的志衣。
NSMutableArray *muteArr = [@[@1] mutableCopy];
4. 少用define多用const
宏define在C中只是替換屯援,例如:
#define kCONTROL_BAR_HEIGHT 60
#define kSCREEN_WIDTH ([UIScreen mainScreen].bounds.size.width)
如果我在代碼中使用kSCREEN_WIDTH,那么它就會(huì)被替換為([UIScreen mainScreen].bounds.size.width)念脯,然鵝編譯時(shí)不會(huì)檢查這個(gè)替換的內(nèi)容是不是正確的狞洋,于是如果define所替代的公式是錯(cuò)的,編譯時(shí)也不會(huì)發(fā)現(xiàn)和二,很容易出錯(cuò)徘铝,所以最好不要用define。
尤其是define如果用來定義變量,都不會(huì)給出類型惕它,其實(shí)是非常不直觀而且容易出問題的怕午。
※ 定義const需要注意一下命名規(guī)范哈
如果可以在.m文件里面定義的常量就不要放到.h里面定義,因?yàn)橐坏﹦e的文件引入了這個(gè)含有const定義的.h淹魄,就會(huì)也定義了一個(gè)const郁惜,定義域相當(dāng)于類似全局變量了,很容易重復(fù)甲锡,這樣可能幾個(gè).h定義了同名const就會(huì)有奇怪的bug兆蕉。
如果在.m文件定義,它的作用域就在.m文件內(nèi)部缤沦,你可以用“k+大駝峰”的明明規(guī)范來命名虎韵,例如:
static const NSTimeInterval kAnimationDuration = 0.3;
但如果你的const必須放到.h文件,那么命名就不可以用k了缸废,需要加上所屬類名包蓝,確保不重復(fù)性,例如:
static const NSTimeInterval XXXClassAnimationDuration = 0.3;
static const NSTimeInterval MainViewControllerAnimationDuration = 0.3;
※ 關(guān)于static
可參考:http://www.reibang.com/p/4bfd96c57a6d
常量需要用static & const來定義企量,不能只用const测萎,因?yàn)槿绻挥胹tatic聲明的全局變量,聲明周期是到程序結(jié)束的届巩,其他文件可以通過extern引入這個(gè)變量硅瞧,作用域類似全局,當(dāng)其他文件中定義了同名const會(huì)報(bào)錯(cuò)duplicate symbol恕汇。
而static對(duì)于局部變量的作用是將其改成聲明周期到App結(jié)束腕唧,對(duì)于全局變量則是生命周期到App結(jié)束,但是只能在聲明它的文件中調(diào)用瘾英,也就是作用域局限在了聲明它的文件中四苇。所以即使其他文件定義了同名static const也不會(huì)報(bào)錯(cuò)。
※ 如何寫對(duì)外的const
如果是對(duì)外的常量表方咆,以及一些類似notification的名字之類的,是對(duì)外會(huì)使用的string蟀架,但是其實(shí)外面用的人而言瓣赂,他們并不需要知道string實(shí)際的字面量,只要用這個(gè)變量就可以了片拍,所以就實(shí)現(xiàn)聲明分離而言煌集,應(yīng)該是定義在.m文件里,聲明在.h文件捌省,不要直接在.h文件里面用static const這樣苫纤。
推薦的做法是:
.h文件:
extern NSString * const ClassXXXMMKVKeyVideoPlayStartCount;
.m文件:
// 不能加static哦,否則就不能extern找到啦
NSString * const ClassXXXMMKVKeyVideoPlayStartCount = @"ClassXXXVideoPlayStartCount";
注意如果對(duì)外,命名加上類名前綴哦卷拘!
const extern static的區(qū)別可以參考:https://www.cnblogs.com/qizhuo/p/6038186.html
5. 用枚舉表示選項(xiàng)喊废、狀態(tài)、狀態(tài)碼
typedef NS_ENUM(NSInteger, RateAlertChance) {
RateAlertChanceSubscription,
RateAlertChanceMeditationPlay,
RateAlertChanceMeditationFinish,
RateAlertChanceMusicPlay,
};
還有一種用法栗弟,是不用默認(rèn)的+1作為枚舉值:
typedef NS_ENUM(NSInteger, RateAlertChance) {
RateAlertChanceSubscription = 0,
RateAlertChanceMeditationPlay = 1 << 0,
RateAlertChanceMeditationFinish = 1 << 1,
RateAlertChanceMusicPlay = 1 << 2,
};
這樣就可以用| &之類的位操作了污筷,當(dāng)有可能兩種狀態(tài)共存的時(shí)候最好用這種。
注意如果用枚舉乍赫,switch就不要有default啦瓣蛀,確保處理所有狀態(tài)即可,否則多出一種狀態(tài)很奇怪雷厂。
6. 理解“屬性”這一概念
※ 實(shí)例變量如何尋址
首先OC的實(shí)例變量具有運(yùn)行時(shí)尋址惋增,如果增加了變量以后無需重新編譯的優(yōu)點(diǎn),具體可參考:http://quotation.github.io/objc/2015/05/21/objc-runtime-ivar-access.html
舉個(gè)栗子:
先來看如果是傳統(tǒng)C代碼改鲫,新建一個(gè)MyObject集成NSObject诈皿,在編譯的時(shí)候會(huì)計(jì)算它們實(shí)例變量的偏移量,類似于students的偏移量是4钩杰,那么編譯時(shí)代碼中所有用到student的地方都會(huì)被hard code硬改寫為4纫塌。
如果蘋果發(fā)了新版本,給NSObject新加了兩個(gè)屬性:
那么在運(yùn)行時(shí)NSObject已經(jīng)變了讲弄,所以它的子類也會(huì)自動(dòng)變措左,增加secretAry和secretImage,并且偏移量分別為4和8避除,如果代碼不重新編譯怎披,所有被替換為4的用到students的地方,取值其實(shí)都拿到了secretAry瓶摆,所以必須要重新編譯凉逛,讓代碼中被hard code數(shù)值的地方都更新為新的偏移量才能正常運(yùn)行。
但如果MyObject是第三方庫(kù)提供的打包后的product群井,那么還必須等待第三方庫(kù)打一個(gè)新的product才能正常運(yùn)行我們的程序状飞,這是非常麻煩的。
Objective-C是怎么做的呢书斜?
struct ivar_t {
int32_t *offset;
const char *name;
const char *type;
//...
};
每個(gè)變量都會(huì)有一個(gè)指向int的指針來存儲(chǔ)這個(gè)實(shí)例變量的偏移量诬辈,所以如果運(yùn)行時(shí)的偏移量改變以后,只要順著class找到他的實(shí)例變量list荐吉,并且通過指針找到真實(shí)存儲(chǔ)偏移量的int焙糟,改掉這個(gè)值就可以啦,雖然占用了空間样屠,給每個(gè)實(shí)例變量增加了一個(gè)offset的存儲(chǔ)穿撮,但是沒用offset替代掉變量增加了很多的靈活性缺脉,充分解決了重編譯問題。
※ @property等
如果我定義了一個(gè)@property悦穿,那么會(huì)自動(dòng)生成它的存取兩個(gè)方法(但readonly就不會(huì)了哈):
@property (nonatomic) NSString *entrance;
- (NSString *)entrance;
- (void)setEntrance:(NSString *)entrance;
當(dāng)我們用xxx.entrance獲取屬性值的時(shí)候其實(shí)就是調(diào)用的[xxx entrance]方法來得到攻礼,如果用xxx.entrance = @"ssss",就是調(diào)用了[xxx setEntrance:@"ssss"]咧党。
之前有寫過兩篇文章探討屬性:http://www.reibang.com/p/1313aac306b1以及http://www.reibang.com/p/e13259caf01e
我們創(chuàng)建property會(huì)自動(dòng)創(chuàng)建一個(gè)下劃線開頭的實(shí)例變量+set+get方法秘蛔,這個(gè)過程就叫做自動(dòng)合成(auto-synthesize)
//.h文件
@property (nonatomic) NSString *entrance;
會(huì)創(chuàng)建一個(gè)實(shí)例變量等同于:
//.m文件
@interface ViewController2 () {
NSString *_entrance;
}
@implementation ViewController2
@synthesize entrance = _entrance;
@end
如果你不希望自動(dòng)創(chuàng)建的變量名為“下劃線+屬性名”,可以強(qiáng)制改名:
@implementation ViewController2
@synthesize entrance = _entrance2;
- (void)viewDidLoad {
[super viewDidLoad];
self.entrance = @"ssss";
_entrance2 = @"ssss";
}
@end
這樣的話就不會(huì)自動(dòng)生成 _entrance變量了傍衡,只會(huì)有_entrance2變量深员。但是非常不推薦改名字,一個(gè)是為了統(tǒng)一規(guī)范蛙埂,大家都很容易懂倦畅,下意識(shí)就能看懂即使你不用self.xxx來獲取绣的;一個(gè)是其實(shí)沒有多大必要性叠赐。
但@synthesize不是只是用來改名字滴,它實(shí)際語(yǔ)義是如果你沒有手動(dòng)實(shí)現(xiàn)setter方法和getter方法屡江,那么編譯器會(huì)自動(dòng)為你加上這兩個(gè)方法芭概。
當(dāng)然我們可以自己覆寫set/get方法,沒有被覆寫的仍舊會(huì)被自動(dòng)生成惩嘉。如果你不希望自動(dòng)生成任何存取方法罢洲,就用@吧:
@implementation ViewController2
@dynamic entrance;
- (void)viewDidLoad {
[super viewDidLoad];
self.entrance = @"ssss";
}
@end
這段代碼會(huì)crash哦,因?yàn)闆]有自動(dòng)生成set方法文黎,我們也沒自己實(shí)現(xiàn)set惹苗,于是用self.entrance的時(shí)候就crash啦。
但編譯時(shí)是不會(huì)報(bào)錯(cuò)的耸峭,它相信運(yùn)行時(shí)會(huì)有相應(yīng)的set&get方法的桩蓉,只是運(yùn)行時(shí)如果沒有就會(huì)crash。
@dynamic的應(yīng)用場(chǎng)景主要是:CoreData以及category增加屬性:https://www.cnblogs.com/Ohero/p/4739089.html
property有很多修飾符劳闹,需要注意的是院究,假設(shè)你聲明了copy,那么如果你同時(shí)覆寫了set方法本涕,需要在里面真的去copy傳入的參數(shù)儡首,確保聲明的修飾符和你實(shí)際的操作是一致的!因?yàn)閯e人調(diào)用的時(shí)候看到聲明就會(huì)認(rèn)為是copy的偏友。
如果你寫了initWithXXX的方法,而且XXX的屬性也是copy修飾的对供,那么一定要在init方法里面真的copy哦位他,否則也是不一致的氛濒。
如果有些readonly的屬性是copy的也要聲明哦,即使不會(huì)自動(dòng)給他生成set方法鹅髓,但是init的時(shí)候作者就知道需要copy來初始化啦舞竿,并且外面調(diào)用的時(shí)候也會(huì)知道是copy過得,不會(huì)再次自己去copy一遍窿冯,重復(fù)copy沒啥必要很浪費(fèi)骗奖。
7. 在對(duì)象內(nèi)部盡量訪問實(shí)例變量
用實(shí)例變量訪問:
讀取優(yōu)點(diǎn)不用經(jīng)過函數(shù),直接讀內(nèi)存更快-
用.的方式訪問:
- 寫入優(yōu)點(diǎn)直接借用了property的修飾符醒串,不用自己再實(shí)現(xiàn)一遍
- 寫入可以觸發(fā)KVO
- 讀/寫可以在setter以及getter方法里面打斷點(diǎn)利于調(diào)試
最好是用實(shí)例變量直接讀取执桌,但賦值的時(shí)候通過.的方式。
但有兩點(diǎn)需要特別注意一下:
(1)在init方法里面不要用self.的方式給屬性賦值
因?yàn)橛锌赡茏宇惱^承了父類芜赌,并且覆寫了set屬性的方法仰挣,偷偷做了檢測(cè)或者拋個(gè)exception之類的,所以在init里面要直接用實(shí)例變量賦值
但如果在子類的init方法里面不能直接用父類的實(shí)例變量缠沈,就要用self.的方式賦值啦
(2)懶加載的時(shí)候要用self.的方式讀取膘壶,不要直接讀實(shí)例變量
因?yàn)閼屑虞d其實(shí)就是覆寫getter,看實(shí)例變量是不是nil洲愤,如果是nil就初始化實(shí)例變量颓芭,不為nil就直接返回實(shí)例變量。
如果直接讀實(shí)例變量不用self.柬赐,相當(dāng)于繞過了懶加載亡问,永遠(yuǎn)都不能初始化這個(gè)實(shí)例變量
8. 理解對(duì)象同等性
==比較的是指針是否相等,如果想自定義比較方式躺率,可以重寫isEqual玛界,需要注意的是,一定要考慮各種情況悼吱,例如不是同一種Class慎框、如果父類和子類比應(yīng)該返回什么等。
//.h文件
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) NSDate *birthday;
@end
//.m文件
- (BOOL)isEqual:(id)object {
if (self == object) {
return YES;
}
if (![object isKindOfClass:[Person class]]) {
return NO;
}
return [self isEqualToPerson:(Person *)object];
}
- (BOOL)isEqualToPerson:(Person *)person {
if (!person) {
return NO;
}
BOOL haveEqualNames = (!self.name && !person.name) || [self.name isEqualToString:person.name];
BOOL haveEqualBirthdays = (!self.birthday && !person.birthday) || [self.birthday isEqualToDate:person.birthday];
return haveEqualNames && haveEqualBirthdays;
}
isEqual在單獨(dú)比較的時(shí)候沒有問題后添,比如我們姓名和生日一致的兩個(gè)person對(duì)象如果用isEqual比較就是true的笨枯。但是如果把這兩個(gè)equal的person加入到一個(gè)set里面,仍舊是可以同時(shí)加進(jìn)去的遇西,這就是有問題的地方了馅精,因?yàn)橥粋€(gè)object是不可能作為兩個(gè)object加入到一個(gè)set里面的,set應(yīng)該保持唯一性粱檀,重復(fù)加入時(shí)應(yīng)該無效洲敢,仍舊只有那一個(gè)object。
※ 什么是hash
這個(gè)問題要從Hash Table這種數(shù)據(jù)結(jié)構(gòu)說起
首先我們看下如何在數(shù)組中查找某個(gè)成員
Step 1: 遍歷數(shù)組中的成員
Step 2: 將取出的值與目標(biāo)值比較, 如果相等, 則返回該成員
在數(shù)組未排序的情況下, 查找的時(shí)間復(fù)雜度是O(array_length)
為了提高查找的速度, Hash Table出現(xiàn)了
當(dāng)成員被加入到Hash Table中時(shí), 會(huì)給它分配一個(gè)hash值, 以標(biāo)識(shí)該成員在集合中的位置
通過這個(gè)位置標(biāo)識(shí)可以將查找的時(shí)間復(fù)雜度優(yōu)化到O(1), 當(dāng)然如果多個(gè)成員都是同一個(gè)位置標(biāo)識(shí), 那么查找就不能達(dá)到O(1)了
所以盡量不要大家hash值都一樣茄蚯,那樣其實(shí)根本沒有減少時(shí)間復(fù)雜度压彭。默認(rèn)的hash值就是對(duì)象的內(nèi)存地址睦优。
分配的這個(gè)hash值(即用于查找集合中成員的位置標(biāo)識(shí)), 就是通過hash方法計(jì)算得來的, 且hash方法返回的hash值最好唯一
和數(shù)組相比, 基于hash值索引的Hash Table查找某個(gè)成員的過程就是
Step 1: 通過hash值直接找到查找目標(biāo)的位置
Step 2: 如果目標(biāo)位置上有多個(gè)相同hash值得成員, 此時(shí)再按照數(shù)組方式進(jìn)行查找
所以數(shù)組元素比較的時(shí)候,如果hash值一樣壮不,才會(huì)繼續(xù)比isEqual汗盘,如果hash都不一樣就直接false了。故而询一,一樣的一定是hash值一致隐孽,但是hash值一致不一定equal哈。
所以上面的例子應(yīng)該覆寫hash健蕊,防止會(huì)往set中加入相同的object:
- (NSUInteger)hash {
return [self.name hash] ^ [self.birthday hash];
}
或者把name和birthday拼成一個(gè)字符串以后取hash值返回菱阵,只是不推薦這種,因?yàn)楫a(chǎn)生了新的字符串消耗绊诲,還是推薦上面這一種送粱。
關(guān)于isEqual和hash可以參考:http://www.reibang.com/p/915356e280fc
啥時(shí)候會(huì)調(diào)用hash可參考:https://www.cnblogs.com/YouXianMing/p/5397197.html(set以及dict的key,用于去重)
如果重寫isEqual方法掂之,一定要重寫hash方法抗俄。
重寫的hash方法一定要簡(jiǎn)單,因?yàn)槿绻愕膶?duì)象存在字典或者集中世舰,hash方法會(huì)頻繁的調(diào)用动雹。
相同的對(duì)象一定要返回相同的hash值,但是有相同的hash值的對(duì)象不一定是同一個(gè)對(duì)象跟压,這是就是產(chǎn)生了碰撞胰蝠,但是我們要讓產(chǎn)生這種情況的機(jī)會(huì)盡可能的少。
- hash方法與判等的關(guān)系?
為了優(yōu)化判等的效率, 基于hash的NSSet和NSDictionary在判斷成員是否相等時(shí), 會(huì)這樣做
集成成員的hash值是否和目標(biāo)hash值相等, 如果相同進(jìn)入Step 2, 如果不等, 直接判斷不相等
hash值相同(即Step 1)的情況下, 再進(jìn)行對(duì)象判等, 作為判等的結(jié)果
所以如果hash一致但是isEqual返回NO還是不等的哦震蒋,可以放進(jìn)set作為兩個(gè)不同對(duì)象~
簡(jiǎn)單地說就是:hash值是對(duì)象判等的必要非充分條件
補(bǔ)充一個(gè)知識(shí)點(diǎn)茸塞,之前面試的時(shí)候小哥哥問過的,hash如果沖突了怎么辦查剖,其實(shí)就會(huì)鏈表連起來~~ 我查了一下還有3種解決方式比如再算另外的一種hash之類的钾虐,可以參考:https://blog.csdn.net/Alexlee1986/article/details/81080449
※ isEqualToXXXClass
NSString的isEqualToString以及Array和Dict的類似方法其實(shí)都是沒有執(zhí)行類型檢查的,也就是它默認(rèn)了你傳入的就是NSString笋庄,不會(huì)再去判斷class是不是一致啦效扫,這樣可以增加執(zhí)行效率,并且語(yǔ)義更加清晰直砂。
所以如果自定義了class并且實(shí)現(xiàn)了isEqual菌仁,還是也實(shí)現(xiàn)一下isEqualToXXXClass會(huì)好一點(diǎn)~
- (BOOL)isEqualToPerson:(Person *)person {
if(!person) {
return NO;
}
BOOL haveEqualNames = (!self.name && !person.name) || [self.name isEqualToString:person.name];
BOOL haveEqualBirthdays = (!self.birthday && !person.birthday) || [self.birthday isEqualToDate:person.birthday];
return haveEqualNames && haveEqualBirthdays;
}
#pragma mark - NSObject
- (BOOL)isEqual:(id)object {
if(self == object) {
return YES;
}
if(![object isKindOfClass:[Person class]]) {
return [super isEqualTo:object];
}
return [self isEqualToPerson:(Person *)object];
}
這樣isEqual也可以簡(jiǎn)單的調(diào)用一下同類對(duì)比,如果不同類就調(diào)用super的isEqual方法來對(duì)比静暂。
※mutable class放入set之類的container需要注意hash
當(dāng)將一個(gè)mutable object放入set的時(shí)候济丘,那個(gè)瞬間就會(huì)去取他的hash值,之后即使你給這個(gè)mutable object做增刪改洽蛀,這個(gè)hash是不會(huì)再算一次了闪盔。
// 正常情況
NSMutableSet* setA = [NSMutableSet new];
NSArray* arrayA = @[@1, @2];
[setA addObject:arrayA];
NSArray* arrayB =@[@1];
[setA addObject:arrayB];
NSLog(@"setA : %@", setA);
輸出:
{((1,2), (1))}
也就是可能你放入了一個(gè)A弯院,然后放了一個(gè)B,然后又把A改的和B一樣泪掀,那么set里面就有兩個(gè)看起來一模一樣的B。
NSMutableSet* setA = [NSMutableSet new];
NSMutableArray* arrayA = [@[@1,@2] mutableCopy];
[setA addObject:arrayA];
NSMutableArray* arrayB = [@[@1] mutableCopy];
[setA addObject:arrayB];
[arrayB addObject:@2];
NSLog(@"setA : %@", setA);
輸出:
{((1,2), (1,2))}
這個(gè)時(shí)候如果你copy一下舊set得到一個(gè)新set颂碘,會(huì)發(fā)現(xiàn)又只剩下一個(gè)B了异赫。
NSSet* setB = [setA copy];
NSLog(@"setB : %@", setB);
輸出:
{((1,2))}
這個(gè)應(yīng)該是會(huì)新建一個(gè)set,然后一個(gè)一個(gè)往里面add头岔,于是如果有equal的就會(huì)加不進(jìn)去啦塔拳。
而如果你加入一個(gè)B,然后有放入一個(gè)和B一樣的B'峡竣,會(huì)發(fā)現(xiàn)set里面只有一個(gè)元素靠抑,不會(huì)有相同的兩個(gè)元素。
NSMutableSet* setA = [NSMutableSet new];
NSArray* arrayA = @[@1, @2];
[setA addObject:arrayA];
NSArray* arrayB =@[@1, @2];
[setA addObject:arrayB];
NSLog(@"setA : %@", setA);
輸出:
{((1,2))}
故而适掰,盡量不要將可變對(duì)象放到set里面去颂碧,因?yàn)楹罄m(xù)如果更改可變對(duì)象,可能會(huì)打破set的唯一性类浪。
可參考:http://www.reibang.com/p/e4ecb4dd14b9
※ NSMutableArray以及NSArray的hash&isEqual判斷
NSMutableSet* setA = [NSMutableSet new];
NSArray* arrayA = @[@1, @2];
[setA addObject:arrayA];
NSArray* arrayB =@[@1, @2];
[setA addObject:arrayB];
NSLog(@"setA : %@", setA);
BOOL same = [arrayA isEqual:arrayB];
NSLog(@"arrayA hash:%lu", (unsigned long)[arrayA hash]);
NSLog(@"arrayB hash:%lu", (unsigned long)[arrayB hash]);
NSLog(@"same: %@", same ? @"yes" : @"no");
輸出:
setA : {((1,2))}
arrayA hash:2
arrayB hash:2
same: yes
將arrayA和B都替換為[@[@1,@2] mutableCopy]载城,輸出結(jié)果一樣。所以其實(shí)array相等大概是通過元素?cái)?shù)以及每個(gè)元素是否相等來判斷的费就。
注意array木有重復(fù)性的check哈诉瓦,同一個(gè)元素也可以加的。
NSMutableArray* arrayA = [@[@1,@2] mutableCopy];
NSNumber *el3 = @3;
[arrayA addObject:el3];
[arrayA addObject:el3];
NSLog(@"arrayA : %@", arrayA);
輸出:
arrayA : (1,2,3,3)
9. 以“類族模式”隱藏實(shí)現(xiàn)細(xì)節(jié)
“類族”是一種很有用的模式力细,可以隱藏“抽象基類”背后的實(shí)現(xiàn)細(xì)節(jié)睬澡,OC系統(tǒng)框架中普遍使用此模式。比如UIKit中就有一個(gè)名為UIButton的類眠蚂,創(chuàng)建按鈕煞聪,則可以調(diào)用下面這個(gè)類方法:
+ (UIButton *)buttonWithType:(UIButtonType)type;
該方法所返回的對(duì)象,其類型取決于傳入的按鈕類型河狐,然而不管返回什么類型的對(duì)象米绕,他們都繼承同一個(gè)基類:UIButton,這么做的意義在于:UIButton類的使用者無需關(guān)心創(chuàng)建出來的按鈕具體屬于哪個(gè)子類馋艺,也不用考慮按鈕的繪制方式等實(shí)現(xiàn)細(xì)節(jié)栅干。
其實(shí)這也就是OC對(duì)抽象類的一種實(shí)現(xiàn)。
舉個(gè)例子:
typedef NS_ENUM(NSUInteger, CWGEmployeeType) {
CWGEmployeeTypeDeveloper,
CWGEmployeeTypeDesigner,
CWGEmployeeTypeFinance,
}
@interface CWGEmployee : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSUInteger salary;
// 創(chuàng)建對(duì)象
+(CWGEmployee *)employeeWithType : (CWGEmployeeType)type;
// 讓對(duì)象做工作
- (void)doADaysWork;
@end
@implementation CWGEmployee
+(CWGEmployee *)employeeWithType : (CWGEmployeeType)type {
switch (type) {
case CWGEmployeeTypeDeveloper:
return [CWGEmployeeDeveloper new];
break;
case CWGEmployeeTypeDesigner:
return [CWGEmployeeDesigner new];
break;
case CWGEmployeeTypeFinance:
return [CWGEmployeeFinance new];
break;
}
}
- (void)doADaysWork {
// Subclasses implement this.
}
@end
每個(gè)“實(shí)體子類”都繼承基類捐祠,例如:
@interface CWGEmployeeDeveloper : CWGEmployee
@end
@implementation CWGEmployeeDeveloper
- (void)doADaysWork {
[self writeCode];
}
@end
在這個(gè)例子中碱鳞,基類實(shí)現(xiàn)了一個(gè)“類方法”,該方法根據(jù)待創(chuàng)建的雇員類別分配好對(duì)應(yīng)的雇員類實(shí)例踱蛀。這種“工廠模式”是創(chuàng)建類族的辦法之一窿给。
還有一點(diǎn)需要注意:如果對(duì)象所屬的類位于某個(gè)類族中贵白,那么在查詢類型信息是就要當(dāng)心了,你可能覺得自己創(chuàng)建了某個(gè)類的實(shí)例崩泡,然而實(shí)際上創(chuàng)建的確實(shí)其子類的實(shí)例禁荒。在Employee這個(gè)例子中, [employee isMemberOfClass:[CWGEmployee class]]似乎會(huì)返回YES角撞,但是發(fā)回的確實(shí)NO呛伴,因?yàn)閑mployee并非Employee類的實(shí)例,而是其某個(gè)子類的實(shí)例谒所。
※ NSArray類族
系統(tǒng)框架有許多類族热康,大部分collection類都是類族,如NSArray和NSMutableArray劣领。有兩個(gè)抽象基類姐军,一個(gè)用于不可變數(shù)組,一個(gè)用于可變數(shù)組尖淘。盡管具備公共接口的類有兩個(gè)奕锌,但仍然可以合起來算作一個(gè)類族。不可變的類定義對(duì)所有數(shù)組都通用的方法德澈,可變的類則定義只適用于可變數(shù)組的方法歇攻。兩個(gè)類共屬同一類族,這意味著兩者在實(shí)現(xiàn)各自類型的數(shù)組時(shí)可以同用實(shí)現(xiàn)代碼梆造。
id maybeArray = /**...*/;
if ([maybeArray class] == [NSArray class]) {
}
NSArray是個(gè)類族缴守,其中if語(yǔ)句永遠(yuǎn)不可能為真。[maybeArray class]返回的類絕不可能是NSArray镇辉,因?yàn)橛蒒SArray的初始化方法所返回的那個(gè)實(shí)例其類型是隱藏在類族公共接口后面的某個(gè)內(nèi)部類型屡穗。
應(yīng)該用下面的類型信息查詢方法判斷:
if ([maybeArray isKindOfClass:[NSArray class]]) {
}
isMemberOfClass:判斷是否是這個(gè)類的實(shí)例
isKindOfClass:判斷是否是這個(gè)類或者這個(gè)類的子類的實(shí)例
我們經(jīng)常需要向類族中新增實(shí)體子類,不過這么做的時(shí)候得留心忽肛。在Employee這個(gè)例子中村砂,若是沒有“工廠方法”的源代碼,那就無法向其中新增雇員類別了屹逛。然而對(duì)于NSArray這樣的類族來說础废,還是有辦法新增子類的,但是需要遵守幾條規(guī)則罕模。規(guī)則如下:
子類應(yīng)該繼承自類族中的抽象基類评腺。
若要編寫NSArray類族的子類,則需令其繼承自不可變數(shù)組的基類或可變數(shù)組的基類淑掌。子類應(yīng)該定義自己的數(shù)據(jù)存儲(chǔ)方式蒿讥。
開發(fā)者編寫NSArray子類時(shí),經(jīng)常在這個(gè)問題上受阻。子類必須用一個(gè)實(shí)例變量來存放數(shù)組中的對(duì)象芋绸。這似乎與大家預(yù)想的不同媒殉,我們以為NSArray本身只不過是包在其他隱藏對(duì)象外面的殼,它僅僅定義了所有數(shù)組都需具備的一些接口摔敛。對(duì)于這個(gè)自定義的數(shù)組子類來說廷蓉,可以用NSArray來保存其實(shí)例。子類應(yīng)當(dāng)覆寫超類文檔中指明需要覆寫的方法马昙。
在每個(gè)抽象基類中苦酱,都有一些子類必須覆寫的方法。比如說给猾,想要編寫NSArray的子類,就需要實(shí)現(xiàn)“count” 及 “objectAtIndex:”方法颂跨。像lastObject這種方法則無須實(shí)現(xiàn)敢伸,因?yàn)榛惪梢愿鶕?jù)前面兩個(gè)方法實(shí)現(xiàn)出這個(gè)方法。
10. 在既有類中使用關(guān)聯(lián)對(duì)象存放自定義數(shù)據(jù)
如果我們希望給一些類保存一些數(shù)據(jù)恒削,可能會(huì)想到繼承這個(gè)類池颈,建個(gè)子類加一些屬性。OC提供了一種更好一點(diǎn)的實(shí)現(xiàn)“關(guān)聯(lián)對(duì)象”(Associated Object)钓丰,可以給現(xiàn)有類關(guān)聯(lián)數(shù)據(jù)躯砰,不用為了加屬性存數(shù)據(jù)增加新的子類。
關(guān)聯(lián)對(duì)象類似于携丁,每個(gè)對(duì)象其實(shí)都有一個(gè)dictionary用于讓開發(fā)者存儲(chǔ)對(duì)象相關(guān)的數(shù)據(jù)琢歇,最開始就是空的,當(dāng)你給他增加關(guān)聯(lián)對(duì)象的時(shí)候梦鉴,相當(dāng)于增加了一個(gè)鍵值對(duì)李茫。
于是,存取關(guān)聯(lián)對(duì)象的值就相當(dāng)于在NSDictionary對(duì)象上調(diào)用[object setObject:value forKey:key]與[object objectForKey:key]方法肥橙。然而兩者之間有個(gè)重要差別:設(shè)置關(guān)聯(lián)對(duì)象時(shí)用的鍵(key)是個(gè)“不透明的指針”(opaque pointer)魄宏。如果在兩個(gè)鍵上調(diào)用“isEqual:”方法的返回值是YES,那么NSDictionary就認(rèn)為二者相等存筏;然而在設(shè)置關(guān)聯(lián)對(duì)象值時(shí)宠互,若想令兩個(gè)鍵匹配到同一個(gè)值,則二者必須是完全相同的指針才行椭坚。
故而予跌,在設(shè)置關(guān)聯(lián)對(duì)象值時(shí),通常使用靜態(tài)全局變量做鍵藕溅。
※ 下列方法可以管理關(guān)聯(lián)對(duì)象:
void objc_setAssociatedObject(id object, void*key, id value, objc_AssociationPolicy policy)
此方法以給定的鍵和策略為某對(duì)象設(shè)置關(guān)聯(lián)對(duì)象值匕得。id objc_getAssociatedObject(id object, void*key)
此方法根據(jù)給定的鍵從某對(duì)象中獲取相應(yīng)的關(guān)聯(lián)對(duì)象值。void objc_removeAssociatedObjects(id object)
此方法移除指定對(duì)象的全部關(guān)聯(lián)對(duì)象。
這里的policy就是對(duì)應(yīng)屬性的修飾符哈~
※ 關(guān)聯(lián)對(duì)象應(yīng)用
- 作為alertView的delegate方法處理
正常的UIAlertView的用法如下:
- (void)askUserAQuestion {
UIAlertView *alert = [[UIAlertView alloc]
initWithTitle:@"Question"
message:@"What do you want to do?"
delegate:self
cancelButtonTitle:@"Cancel"
otherButtonTitles:@"Continue", nil];
[alert show];
}
// UIAlertViewDelegate protocol method
- (void)alertView:(UIAlertView *)alertView
clickedButtonAtIndex:(NSInteger)buttonIndex
{
if (buttonIndex == 0) {
[self doCancel];
} else {
[self doContinue];
}
}
這種方式其實(shí)delegate和創(chuàng)建alert的地方很有可能是分開的汁掠,看起來很不方便略吨,如果用關(guān)聯(lián)將處理的方法關(guān)聯(lián)給alert,那么delegate處理的時(shí)候直接取出方法調(diào)用就可以啦:
#import <objc/runtime.h>
static void *EOCMyAlertViewKey = "EOCMyAlertViewKey";
- (void)askUserAQuestion {
UIAlertView *alert = [[UIAlertViewalloc]
initWithTitle:@"Question"
message:@"What do you want to do?"
delegate:self
cancelButtonTitle:@"Cancel"
otherButtonTitles:@"Continue", nil];
void (^block)(NSInteger) = ^(NSInteger buttonIndex){
if (buttonIndex == 0) {
[self doCancel];
} else {
[self doContinue];
}
};
objc_setAssociatedObject(alert,
EOCMyAlertViewKey,
block,
OBJC_ASSOCIATION_COPY);
[alert show];
}
// UIAlertViewDelegate protocol method
- (void)alertView:(UIAlertView*)alertView
clickedButtonAtIndex:(NSInteger)buttonIndex
{
void (^block)(NSInteger) =
objc_getAssociatedObject(alertView, EOCMyAlertViewKey);
block(buttonIndex);
}
這里需要注意block的里面變量的內(nèi)存問題考阱,防止循環(huán)引用哈翠忠,因?yàn)檫@個(gè)block相當(dāng)于被object持有了,如果他又強(qiáng)持有了self的一些屬性就會(huì)循環(huán)引用啦乞榨。
- category的屬性存取
還有一個(gè)應(yīng)用是用于給現(xiàn)有的類增加屬性秽之,因?yàn)楸旧硎遣辉试S通過category增加屬性的,但是借用關(guān)聯(lián)對(duì)象可以把屬性值存給對(duì)象:
@property (nonatomic, strong) UIImageView *commonBackgoundImageView;
- (void)setCommonBackgoundImageView:(UIImageView *)commonBackgoundImageView {
objc_setAssociatedObject(self, @selector(commonBackgoundImageView), commonBackgoundImageView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (UIImageView *)commonBackgoundImageView {
return objc_getAssociatedObject(self, @selector(commonBackgoundImageView));
}
這種做法很有用吃既,但是只應(yīng)該在其他辦法行不通時(shí)才去考慮用它考榨。若是濫用,則很快就會(huì)令代碼失控鹦倚,使其難于調(diào)試河质。而且它容易產(chǎn)生retain cycle,不容易找到問題震叙,policy如果指定的有問題也比較麻煩掀鹅,所以其實(shí)還是一個(gè)萬(wàn)不得已才用的option吧,不要因?yàn)榭梢杂镁陀谩?/p>
11. 理解objc_msgSend的作用
傳統(tǒng)C語(yǔ)言的函數(shù)調(diào)用是硬編碼在代碼里面滴媒楼,類似于:
void A() {}
void B() {}
void doSth (bool isA) {
if (isA) {
A();
} else {
B();
}
}
假設(shè)A的函數(shù)地址是100乐尊,B函數(shù)地址是200,那么編譯完其實(shí)就是:
void doSth (bool isA) {
if (isA) {
jump to 100;
} else {
jump to 200;
}
}
但是OC是動(dòng)態(tài)綁定的划址,例如:
void A() {}
void B() {}
void doSth (bool isA) {
void (*fun)();
if (isA) {
fun = A;
} else {
fun = B;
}
fun();
}
這種情況下扔嵌,編譯時(shí)并沒有辦法知道A是啥,編譯器只知道A()猴鲫,而fun()到底是A函數(shù)還是B函數(shù)是在運(yùn)行時(shí)才確定的对人,這也就是動(dòng)態(tài)綁定。
OC中的消息機(jī)制就是基于動(dòng)態(tài)綁定拂共,當(dāng)我們調(diào)用object的方法的時(shí)候牺弄,其實(shí)是給他發(fā)了一個(gè)消息,而底層其實(shí)就是對(duì)象調(diào)用objc_msgSend宜狐。
// 消息傳遞機(jī)制的核心函數(shù)
void objc_msgSend(id self, SEL cmd, ...)
說明:
是一個(gè)“參數(shù)可變的函數(shù)”势告,能接受兩個(gè)或兩個(gè)以上的參數(shù)。
第一個(gè)參數(shù):接受者
第二個(gè)參數(shù):選擇器(SEL是選擇器的類型)
后續(xù)參數(shù)就是消息中的參數(shù)抚恒,順序不變咱台。
選擇器指的就是方法的名字。
舉個(gè)例子:
[self reportEvent];
等同于:
objc_msgSend(self, @selector(reportEvent));
完全可以把代碼中的函數(shù)調(diào)用替換成objc_msgSend俭驮,因?yàn)榈讓右彩沁@么干的回溺,但是注意需要把.m文件改成.mm即C語(yǔ)言混編春贸,以及把build settings里面的Enable Strict Checking of objc_msgSend Calls改為No
并且#import <objc/message.h>哈
OC中每個(gè)對(duì)象都有個(gè)isa指針,指向的結(jié)構(gòu)體里面有個(gè)methodList遗遵,存儲(chǔ)了方法名以及它所對(duì)應(yīng)的函數(shù)地址萍恕。所以當(dāng)我們給一個(gè)object發(fā)消息的時(shí)候,如果能找到與selector名稱相符的方法车要,就調(diào)至其實(shí)現(xiàn)代碼允粤。如果找不到就沿著繼承體系繼續(xù)向上查找,等找到合適的方法之后再跳轉(zhuǎn)翼岁。如果最終都找不到类垫,那就執(zhí)行“消息轉(zhuǎn)發(fā)”操作。
當(dāng)找到相符的方法之后琅坡,objc_msgSend會(huì)將匹配結(jié)果緩存在“快速映射表”里悉患,每個(gè)類都會(huì)有這么一塊緩存,如果稍后還向該類發(fā)送此消息榆俺,那么執(zhí)行起來就會(huì)很快了善炫。
而且其實(shí)OC是有尾調(diào)用優(yōu)化滴:http://www.reibang.com/p/9e3cd9b1095a?from=timeline&isappinstalled=0
12. 理解消息轉(zhuǎn)發(fā)機(jī)制
當(dāng)給object發(fā)送它無法解析的消息的時(shí)候就會(huì)觸發(fā)消息轉(zhuǎn)發(fā)機(jī)制:
-
消息轉(zhuǎn)發(fā)分為兩大階段:
第一階段:征詢接收者系馆,所屬的類割以,看其是否能動(dòng)態(tài)添加方法环形,以處理當(dāng)前這個(gè)“未知的選擇器”蹂安。這叫做“動(dòng)態(tài)方法解析”备韧。第二階段:涉及“完整的消息轉(zhuǎn)發(fā)機(jī)制”莉给。運(yùn)行時(shí)系統(tǒng)會(huì)請(qǐng)求接受者以其他手段來處理與消息相關(guān)的方法調(diào)用也糊。分兩小步:
step 1:請(qǐng)接收者看看有沒有其他對(duì)象能處理未知消息毫玖,若有掀虎,則運(yùn)行時(shí)系統(tǒng)會(huì)把消息轉(zhuǎn)給那個(gè)對(duì)象。這叫做“備援接收者”付枫。若沒有進(jìn)行第二步烹玉。
step 2:?jiǎn)?dòng)完整的消息轉(zhuǎn)發(fā)機(jī)制,運(yùn)行時(shí)系統(tǒng)會(huì)把與消息有關(guān)的全部細(xì)節(jié)都封裝到NSInvocation對(duì)象中阐滩,再給接收者最后一次機(jī)會(huì)二打,令其設(shè)法解決當(dāng)前的未知消息。
消息轉(zhuǎn)發(fā)掂榔,步驟越往后继效,處理消息的代價(jià)就越大,最好在第一步就處理完装获,這樣瑞信,運(yùn)行時(shí)系統(tǒng)可以將此方法緩存起來,如果類的實(shí)例稍后收到同名選擇器穴豫,那就無須啟動(dòng)消息轉(zhuǎn)發(fā)流程凡简。如果不修改消息內(nèi)容,則在第二步進(jìn)行消息轉(zhuǎn)發(fā)即可。
※ 應(yīng)用
// 頭文件
#import <Foundation/Foundation.h>
@interface EOCAutoDictionary : NSObject
@property (nonatomic, strong) NSString *string;
@property (nonatomic, strong) NSNumber *number;
@property (nonatomic, strong) NSDate *date;
@property (nonatomic, strong) id opaqueObject;
@end
// 實(shí)現(xiàn)文件
#import "EOCAutoDictionary.h"
#import <objc/runtime.h>
#import "EOCAutoHelper.h"
@interface EOCAutoDictionary()
@property (nonatomic, strong) NSMutableDictionary *backingStore;
@property (nonatomic, strong) EOCAutoHelper *heper;
@end
@implementation EOCAutoDictionary
@dynamic string, number, date, opaqueObject;
- (instancetype)init
{
self = [super init];
if (self) {
_backingStore = [NSMutableDictionary new];
_heper = [EOCAutoHelper new];
}
return self;
}
// 動(dòng)態(tài)方法解析
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
NSString *selectorString = NSStringFromSelector(sel);
if ([selectorString isEqualToString:@"autoHeplerMethod"]) {
// 備援接受者
return NO;
} else {
// 動(dòng)態(tài)方法解析
if ([selectorString hasPrefix:@"set"]) {
// 向類中動(dòng)態(tài)添加方法
// 參數(shù)說明:類秤涩,選擇器帜乞,待添加的函數(shù)指針,類型編碼(返回值類型@:參數(shù))
class_addMethod(self, sel, (IMP)autoDictionarySetter, "v@:@");
} else {
class_addMethod(self, sel, (IMP)autoDictionaryGetter, "@@:");
}
return YES;
}
return NO;
}
// 備援接受者
- (id)forwardingTargetForSelector:(SEL)aSelector
{
NSString *selectorString = NSStringFromSelector(aSelector);
if ([selectorString isEqualToString:@"autoHeplerMethod"]) {
// 返回一個(gè)內(nèi)部對(duì)象來代替實(shí)現(xiàn)方法
return _heper;
}
return [super forwardingTargetForSelector:aSelector];
}
id autoDictionaryGetter(id self, SEL _cmd)
{
EOCAutoDictionary *typeSelf = (EOCAutoDictionary *)self;
NSMutableDictionary *backingStore = typeSelf.backingStore;
NSString *key = NSStringFromSelector(_cmd);
return [backingStore objectForKey:key];
}
void autoDictionarySetter(id self, SEL _cmd, id value)
{
EOCAutoDictionary *typeSelf = (EOCAutoDictionary *)self;
NSMutableDictionary *backingStore = typeSelf.backingStore;
NSString *selectorString = NSStringFromSelector(_cmd);
NSMutableString *key = [selectorString mutableCopy];
// 刪除':'
[key deleteCharactersInRange:NSMakeRange(key.length - 1, 1)];
// 刪除'set'
[key deleteCharactersInRange:NSMakeRange(0, 3)];
// 將第一個(gè)字符串變?yōu)樾? NSString *lowercaseFirstChar = [[key substringToIndex:1] lowercaseString];
[key replaceCharactersInRange:NSMakeRange(0, 1) withString:lowercaseFirstChar];
if (value) {
[backingStore setObject:value forKey:key];
} else {
[backingStore removeObjectForKey:key];
}
}
@end
// 頭文件
#import <Foundation/Foundation.h>
@interface EOCAutoHelper : NSObject
- (void)autoHeplerMethod;
@end
// 實(shí)現(xiàn)文件
#import "EOCAutoHelper.h"
@implementation EOCAutoHelper
- (void)autoHeplerMethod
{
NSLog(@"EOCAutoHelper");
}
@end
// 使用
EOCAutoDictionary *dict = [EOCAutoDictionary new];
// 測(cè)試動(dòng)態(tài)方法解析
dict.date = [NSDate dateWithTimeIntervalSince1970:475372800];
NSLog(@"dict.date : %@",dict.date);
// 測(cè)試備援接收者
[dict performSelector:@selector(autoHeplerMethod)];
現(xiàn)在可以往EOCAutoDictionary加任何屬性啦溉仑,因?yàn)樗衐ynamic的屬性找不到setter/getter的時(shí)候就會(huì)調(diào)用resolveInstanceMethod挖函,resolveInstanceMethod又會(huì)自動(dòng)創(chuàng)建setter和getter,類似CALayer就是可以加任何的屬性浊竟,然后用key-value的方式讀取怨喘。
13.用“方法調(diào)配技術(shù)”調(diào)試“黑盒方法”
我們之前一直有提到方法列表振定,他其實(shí)也是類似key-value對(duì)必怜,key就是selector,value就是指向?qū)崿F(xiàn)的指針I(yè)MP:
id (*IMP)(id, SEL, ...)
我們可以修改這個(gè)列表的指向后频,這個(gè)過程就叫做方法調(diào)配梳庆。例如我們可以改成下面這樣:
通過這個(gè)方式我們無須增加子類,就可以改變現(xiàn)有類的方法卑惜。
交換方法實(shí)現(xiàn):
void method_exchangeImplementations(Method m1, Method m2)
參數(shù):表示待交換的兩個(gè)方法實(shí)現(xiàn)
獲取方法實(shí)現(xiàn):
Method class_getInstanceMethod(Class cls, SEL name)
參數(shù):類膏执,相關(guān)方法
例如我們可以交換String的lower和uppercase方法:
Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));
Method swappedMethod = class_getInstanceMethod([NSString class], @selector(uppercaseString));
method_exchangeImplementations(originalMethod, swappedMethod);
但其實(shí)我們很少會(huì)交換方法,畢竟方法就應(yīng)該和它實(shí)際做了什么相符合露久。
方法調(diào)配可以用于黑盒調(diào)試更米,在調(diào)用現(xiàn)有方法的時(shí)候增加日志等,避免了繼承子類再調(diào)用毫痕,畢竟系統(tǒng)類生成子類很麻煩征峦。
舉個(gè)例子~如果想在lowercaseString被調(diào)用的時(shí)候打印一些日志,可以:
// 新建NSString類分類消请,頭文件
#import <Foundation/Foundation.h>
@interface NSString (EOCMyAdditions)
- (NSString *)eoc_myLowercaseString;
@end
// 實(shí)現(xiàn)文件
#import "NSString+EOCMyAdditions.h"
@implementation NSString (EOCMyAdditions)
- (NSString *)eoc_myLowercaseString
{
NSString *lowercase = [self eoc_myLowercaseString];
NSLog(@"%@ => %@",self, lowercase);
return lowercase;
}
@end
// 使用
Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));
Method swappedMethod = class_getInstanceMethod([NSString class], @selector(eoc_myLowercaseString));
method_exchangeImplementations(originalMethod, swappedMethod);
NSString *string = @"This is the Stirng";
NSString *lowercaseString = [string lowercaseString];
輸出:
2018-08-18 23:17:24.038765+0800 Demo[12721:496759] This is the Stirng => this is the stirng
注意eoc_myLowercaseString的實(shí)現(xiàn)里面又調(diào)用了eoc_myLowercaseString不會(huì)造成死循環(huán)栏笆,因?yàn)閇self eoc_myLowercaseString]相當(dāng)于[self lowercaseString],這兩個(gè)方法已經(jīng)交換啦臊泰。
這種方法交換不要濫用哈也是蛉加,因?yàn)榻粨Q是永久的,可能改變了一些默認(rèn)行為缸逃,還是只用在黑盒調(diào)試比較好七婴。
14. 理解類對(duì)象
id是沒有類型的,但是它本身已經(jīng)是指針啦察滑,所以可以id str = @"ssss"
是不會(huì)報(bào)錯(cuò)滴打厘。
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
Class isa OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
typedef struct objc_class *Class;
// Class類
struct objc_class {
// metaClass 元類
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
// 父類
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
isa:是一個(gè)Class 類型的指針. 每個(gè)實(shí)例對(duì)象有個(gè)isa的指針,他指向?qū)ο蟮念悾鳦lass里也有個(gè)isa的指針, 指向meteClass(元類)贺辰。元類保存了類方法的列表户盯。當(dāng)類方法被調(diào)用時(shí)嵌施,先會(huì)從本身查找類方法的實(shí)現(xiàn),如果沒有莽鸭,元類會(huì)向他父類查找該方法吗伤。
同時(shí)注意的是:元類(meteClass)也是類,它也是對(duì)象硫眨。元類也有isa指針,它的isa指針最終指向的是一個(gè)根元類(root meteClass).根元類的isa指針指向本身足淆,這樣形成了一個(gè)封閉的內(nèi)循環(huán)。
Objective-C對(duì)象主要分為以下3類:
1> instance對(duì)象(實(shí)例對(duì)象)
2> class對(duì)象(類對(duì)象)存儲(chǔ)實(shí)例方法列表等信息
3> meta-class對(duì)象(元類對(duì)象)存儲(chǔ)類方法列表等信息
object的Class是ClassXXX礁阁,ClassXXX也是對(duì)象巧号,它的Class就是metaClass。
注意姥闭,一個(gè)類只會(huì)有一個(gè)類對(duì)象丹鸿,一個(gè)元類對(duì)象,可以有多個(gè)實(shí)例對(duì)象棚品。也就是說Class和metaClass是單例哈靠欢。所以可以用[object Class] == [NSString Class]來判斷是不是string~
如果我們發(fā)送消息給一個(gè)object:[object message];
它運(yùn)行原理便成了,運(yùn)行時(shí)通過object的isa指針铜跑,找到對(duì)應(yīng)的class门怪,因?yàn)閏lass里維護(hù)這方法列表及superClass的isa,如果class本身的方法列表里沒有找到message方法锅纺,便繼續(xù)通過superClass的isa往上查找薪缆,直到NSObject根,如果依然沒用伞广,就會(huì)進(jìn)行消息轉(zhuǎn)發(fā)(這里不詳述),最后如果轉(zhuǎn)發(fā)失敗疼电,就會(huì)崩潰嚼锄。
我們前面說過,class也是object蔽豺,因此区丑,當(dāng)類方法執(zhí)行時(shí),就會(huì)通過類的isa指針修陡,去MetaClass里找對(duì)應(yīng)方法沧侥,具體流程同上面object描述。
獲取元類可以用下面的方式哈:
// 必需要傳入類對(duì)象才能獲取元類對(duì)象
NSLog(@"meta-class: %p", object_getClass([obj class]));
// 通過類名獲取元類對(duì)象
NSLog(@"objcMetaClass: %p", objc_getMetaClass(className));
可參考:https://www.cnblogs.com/xgao/archive/2018/09/28/9708163.html
https://blog.csdn.net/zyx196/article/details/50780602
盡量使用類型信息查詢方法(isMemberOfClass魄鸦、isKindOfClass)宴杀,而不應(yīng)該直接比較兩個(gè)類對(duì)象是否等同,因?yàn)榍罢呖梢哉_處理那些使用了消息傳遞機(jī)制的對(duì)象拾因。
比方說旺罢,某個(gè)對(duì)象可能會(huì)把其收到的所有選擇子都轉(zhuǎn)發(fā)給另外一個(gè)對(duì)象旷余。這樣的對(duì)象叫做 代理類,此種對(duì)象均以 NSProxy 為根類扁达。
通常情況下正卧,如果在此種代理對(duì)象上調(diào)用 class 方法,那么返回的是代理類本身跪解,而非代理類轉(zhuǎn)發(fā)到的真正接收消息的類炉旷。然而,若是改用 “isKindOfClass:” 這樣的類型信息查詢方法叉讥,那么代理類就會(huì)把這條消息轉(zhuǎn)給 “接受代理的對(duì)象”(proxied object)窘行。也就是說,這條消息的返回值與直接在接受代理的對(duì)象上面查詢其類型所得的結(jié)果相同节吮。因此抽高,這樣查出來的類對(duì)象與通過 class 方法所返回的那個(gè)類對(duì)象不同,class 方法所返回的類是代理類透绩,而非真正處理代理類轉(zhuǎn)發(fā)的消息的對(duì)象類翘骂。