[iOS] Effective Objective-C ——熟悉OC、類舵鳞、對(duì)象震檩、運(yùn)行期

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)里面有方法列表。


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;
對(duì)象空間分配

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è)栗子:


image

先來看如果是傳統(tǒng)C代碼改鲫,新建一個(gè)MyObject集成NSObject诈皿,在編譯的時(shí)候會(huì)計(jì)算它們實(shí)例變量的偏移量,類似于students的偏移量是4钩杰,那么編譯時(shí)代碼中所有用到student的地方都會(huì)被hard code硬改寫為4纫塌。

如果蘋果發(fā)了新版本,給NSObject新加了兩個(gè)屬性:


image

那么在運(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)存更快

  • 用.的方式訪問:

    1. 寫入優(yōu)點(diǎn)直接借用了property的修飾符醒串,不用自己再實(shí)現(xiàn)一遍
    2. 寫入可以觸發(fā)KVO
    3. 讀/寫可以在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ì)這樣做

  1. 集成成員的hash值是否和目標(biāo)hash值相等, 如果相同進(jìn)入Step 2, 如果不等, 直接判斷不相等

  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ì)象:

  1. void objc_setAssociatedObject(id object, void*key, id value, objc_AssociationPolicy policy)
    此方法以給定的鍵和策略為某對(duì)象設(shè)置關(guān)聯(lián)對(duì)象值匕得。

  2. id objc_getAssociatedObject(id object, void*key)
    此方法根據(jù)給定的鍵從某對(duì)象中獲取相應(yīng)的關(guān)聯(lián)對(duì)象值。

  3. void objc_removeAssociatedObjects(id object)
    此方法移除指定對(duì)象的全部關(guān)聯(lián)對(duì)象。

這里的policy就是對(duì)應(yīng)屬性的修飾符哈~

關(guān)聯(lián)policy

※ 關(guān)聯(lián)對(duì)象應(yīng)用

  1. 作為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)引用啦乞榨。

  1. 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ā)機(jī)制

消息轉(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, ...)
默認(rèn)正常的方法列表

我們可以修改這個(gè)列表的指向后频,這個(gè)過程就叫做方法調(diào)配梳庆。例如我們可以改成下面這樣:


改imp指針指向

通過這個(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ì)象類翘骂。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市帚豪,隨后出現(xiàn)的幾起案子碳竟,更是在濱河造成了極大的恐慌,老刑警劉巖狸臣,帶你破解...
    沈念sama閱讀 217,542評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件莹桅,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡烛亦,警方通過查閱死者的電腦和手機(jī)诈泼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來煤禽,“玉大人铐达,你說我怎么就攤上這事∶使” “怎么了瓮孙?”我有些...
    開封第一講書人閱讀 163,912評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)选脊。 經(jīng)常有香客問我杭抠,道長(zhǎng),這世上最難降的妖魔是什么恳啥? 我笑而不...
    開封第一講書人閱讀 58,449評(píng)論 1 293
  • 正文 為了忘掉前任偏灿,我火速辦了婚禮,結(jié)果婚禮上钝的,老公的妹妹穿的比我還像新娘菩混。我一直安慰自己忿墅,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,500評(píng)論 6 392
  • 文/花漫 我一把揭開白布沮峡。 她就那樣靜靜地躺著疚脐,像睡著了一般。 火紅的嫁衣襯著肌膚如雪邢疙。 梳的紋絲不亂的頭發(fā)上棍弄,一...
    開封第一講書人閱讀 51,370評(píng)論 1 302
  • 那天,我揣著相機(jī)與錄音疟游,去河邊找鬼呼畸。 笑死,一個(gè)胖子當(dāng)著我的面吹牛颁虐,可吹牛的內(nèi)容都是我干的蛮原。 我是一名探鬼主播,決...
    沈念sama閱讀 40,193評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼另绩,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼儒陨!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起笋籽,我...
    開封第一講書人閱讀 39,074評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤蹦漠,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后车海,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體笛园,經(jīng)...
    沈念sama閱讀 45,505評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,722評(píng)論 3 335
  • 正文 我和宋清朗相戀三年侍芝,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了研铆。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,841評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡州叠,死狀恐怖棵红,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情留量,我是刑警寧澤,帶...
    沈念sama閱讀 35,569評(píng)論 5 345
  • 正文 年R本政府宣布哟冬,位于F島的核電站楼熄,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏浩峡。R本人自食惡果不足惜可岂,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,168評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望翰灾。 院中可真熱鬧缕粹,春花似錦稚茅、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至绘面,卻和暖如春欺税,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背揭璃。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工晚凿, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人瘦馍。 一個(gè)月前我還...
    沈念sama閱讀 47,962評(píng)論 2 370
  • 正文 我出身青樓歼秽,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親情组。 傳聞我的和親對(duì)象是個(gè)殘疾皇子燥筷,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,781評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容