版權(quán)聲明:本文源自簡書【九昍】历等,歡迎轉(zhuǎn)載胸完,轉(zhuǎn)載請務(wù)必注明出處: http://www.reibang.com/p/abb3b69172b9
在平時(shí)開發(fā)的時(shí)候經(jīng)常用到[NSString isEqualToString]這個(gè)方法蠕趁,但是卻不清楚它具體是怎么實(shí)現(xiàn)的铜涉,所以決定看下系統(tǒng)源碼早直,了解一下它的內(nèi)部實(shí)現(xiàn)做院。
根據(jù)蘋果官方Guide:【Concepts in Objective-C Programming: Introspection 】
可以得知如果要重寫isEqual: 則需要同時(shí)重寫hash嘹裂。這讓我想當(dāng)然的以為isEqual內(nèi)部會(huì)使用hash做一次判斷眠寿,然后再進(jìn)行字符串比較,不過事實(shí)并非如此焦蘑。
通過翻看static Boolean __CFStringEqual(CFTypeRef cf1, CFTypeRef cf2) 的源碼盯拱,可以得到下面的信息
static Boolean __CFStringEqual(CFTypeRef cf1, CFTypeRef cf2) {
CFStringRef str1 = (CFStringRef)cf1;
CFStringRef str2 = (CFStringRef)cf2;
const uint8_t *contents1;
const uint8_t *contents2;
CFIndex len1;
contents1 = (uint8_t *)__CFStrContents(str1);
contents2 = (uint8_t *)__CFStrContents(str2);
len1 = __CFStrLength2(str1, contents1);
//****X 首先判斷長度是否相等
if (len1 != __CFStrLength2(str2, contents2)) return false;
contents1 += __CFStrSkipAnyLengthByte(str1);
contents2 += __CFStrSkipAnyLengthByte(str2);
//****X 根據(jù)兩個(gè)字符串是否為Unicode編碼分別做判斷,判斷方式是逐個(gè)取字符做對比
if (__CFStrIsEightBit(str1) && __CFStrIsEightBit(str2)) { // 都不是Unicode
return memcmp((const char *)contents1, (const char *)contents2, len1) ? false : true;
} else if (__CFStrIsEightBit(str1)) { /* One string has Unicode contents */
CFStringInlineBuffer buf;
CFIndex buf_idx = 0;
CFStringInitInlineBuffer(str1, &buf, CFRangeMake(0, len1));
for (buf_idx = 0; buf_idx < len1; buf_idx++) {
if (__CFStringGetCharacterFromInlineBufferQuick(&buf, buf_idx) != ((UniChar *)contents2)[buf_idx]) return false;
}
} else if (__CFStrIsEightBit(str2)) { /* One string has Unicode contents */
CFStringInlineBuffer buf;
CFIndex buf_idx = 0;
CFStringInitInlineBuffer(str2, &buf, CFRangeMake(0, len1));
for (buf_idx = 0; buf_idx < len1; buf_idx++) {
if (__CFStringGetCharacterFromInlineBufferQuick(&buf, buf_idx) != ((UniChar *)contents1)[buf_idx]) return false;
}
} else { /* Both strings have Unicode contents */
CFIndex idx;
for (idx = 0; idx < len1; idx++) {
if (((UniChar *)contents1)[idx] != ((UniChar *)contents2)[idx]) return false;
}
}
return true;
}
既然hash方法不是用來提高isEqual:方法的效率的例嘱,那它的作用是什么狡逢?
來看一下isEqual:和hash方法的實(shí)現(xiàn):
[NSObject hash]的實(shí)現(xiàn)
uintptr_t _objc_rootHash(id obj)
{
return (uintptr_t)obj;
}
+ (NSUInteger)hash {
return _objc_rootHash(self);
}
[NSObject isEqual:]的實(shí)現(xiàn)
- (BOOL)isEqual:(id)obj {
return obj == self;
}
可以看出,對于NSObject無論是hash還是isEqual方法拼卵,都是用對象地址作為依據(jù)奢浑,所以對于NSObject,如果hash值相同isEqual:的返回值就是YES腋腮。
如果我們需要實(shí)現(xiàn)自定義的isEqual該怎么做雀彼,首先看下apple官方demo的實(shí)現(xiàn)
- (BOOL)isEqual:(id)other {
if (other == self)
return YES;
if (!other || ![other isKindOfClass:[self class]])
return NO;
return [self isEqualToWidget:other];
}
- (BOOL)isEqualToWidget:(MyWidget *)aWidget {
if (self == aWidget)
return YES;
if (![(id)[self name] isEqual:[aWidget name]])
return NO;
if (![[self data] isEqualToData:[aWidget data]])
return NO;
return YES;
}
官方的推薦是實(shí)現(xiàn)一個(gè)isEqualToType:方法,然后在isEqual內(nèi)部調(diào)用isEqualToType方法即寡,并且在isEqual內(nèi)部檢查對象類型及合法性徊哑。
當(dāng)修改完isEqual以后,如果不修改hash方法聪富,那么此時(shí)若有兩個(gè)不同的MyWidget對象莺丑,并且他們的name、data相同,此時(shí)他們的hash仍然是取的自身地址梢莽,此時(shí)hash不相等萧豆,不滿足對象相等hash一定相等的原則。
@implementation Model
- (id)copyWithZone:(nullable NSZone *)zone {
Model *result = [[[self class] allocWithZone:zone] init];
result.firstName = self.firstName;
result.lastName = self.lastName;
return result;
}
- (NSUInteger)hash {
NSLog(@"%@ call %s", self, __func__);
return NSUINTROTATE([_firstName hash], NSUINT_BIT / 2) ^ [_lastName hash];
}
- (BOOL)isEqual:(id)other {
NSLog(@"%@ call %s", self, __func__);
if (other == self)
return YES;
if (!other || ![other isKindOfClass:[self class]])
return NO;
return [self isEqualToModel:other];
}
- (BOOL)isEqualToModel:(Model *)other {
if (self == other)
return YES;
if (![(id)[self firstName] isEqual:[other firstName]])
return NO;
if (![[self lastName] isEqual:[other lastName]])
return NO;
return YES;
}
@end
-----------------------------------------------------------------
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Model *model1 = [[Model alloc] init];
model1.firstName = @"A";
model1.lastName = @"B";
Model *model2 = [[Model alloc] init];
model2.firstName = @"A";
model2.lastName = @"B";
NSMutableDictionary *mutDict = [NSMutableDictionary dictionary];
[mutDict setObject:@"" forKey:model1];
NSLog(@"**********************************");
[mutDict setObject:@"" forKey:model2];
程序運(yùn)行后打印的log如下:
2016-12-19 16:03:27.236 test[28108:2027961] <Model: 0x618000024bc0> call -[Model hash]
2016-12-19 16:03:27.236 test[28108:2027961] <Model: 0x610000024860> call -[Model hash]
2016-12-19 16:03:27.236 test[28108:2027961] **********************************
2016-12-19 16:03:27.237 test[28108:2027961] <Model: 0x618000024c60> call -[Model hash]
2016-12-19 16:03:27.237 test[28108:2027961] <Model: 0x610000024860> call -[Model isEqual:]
首先解釋下為什么第一次setObject:forKey:調(diào)用了兩次hash方法第一次調(diào)用hash方法是為了獲取hash值從而做進(jìn)一步判斷昏名,這是因?yàn)槲覀儧]有指定字典的Capacity涮雷,這種情況下Capacity的值為0,這時(shí)候我們向字典里添加元素轻局,字典空間不夠用洪鸭,會(huì)重新申請空間,此時(shí)需要在新申請的控件通過hash確定對象的地址嗽交,所以第二次調(diào)用hash這個(gè)方法卿嘲。
從上面的log可以看到在以hash表為基礎(chǔ)的對象(NSDictionary、NSSet等)中存儲(chǔ)數(shù)據(jù)時(shí)夫壁,若hash相等則會(huì)調(diào)用對象的isEqual方法進(jìn)一步判斷對象是否相等拾枣,從而確定對象是否已經(jīng)存在,如果我們只修改isEqual方法盒让,則可能會(huì)出現(xiàn)兩個(gè)isEqual的對象由于hash值不相等導(dǎo)致被誤判為兩個(gè)不相等對象的情況梅肤,這就是為什么必須要同時(shí)修改isEqual:和hash方法最重要的原因。