《Effective Objective-C 2.0》——Object, Protocol and Categories  

这是《Effective Objective-C 2.0》英文版的Chapter2和Chapter4的笔记

理解Property

  • Property能够带来以下几点好处:
  1. 声明Property后,编译器会在编译期间自动为你在代码中添加一个以下划线为前缀的实例变量,和该实例变量的getter/setter方法。这样在外部调用时,就可以通过getter/setter来读和写对象的实例变量了,而不是直接使用实例变量本身。
  2. 外部可以用.的形式使用一个Property,这样比较接近大多数编程语言的调用风格,编译器会自动把它转换成中括号调用的形式。
  • @synthesize告诉编译器给实例变量起一个其他的名字,但最好就用默认的。
1
2
3
4
@implementation EOCPerson
@synthesize firstName = _myFirstName;
@synthesize lastName = _myLastName;
@end
  • @dynamic告诉编译器不用自动生成实例变量的getter/setter方法,编译器认为程序员会自己实现这两个方法,所以其他地方还是可以调用的,编译期间不会报错,但在运行时如果没有,就会报错了。
1
2
3
@implementation EOCPerson
@synthesize firstName, lastName;
@end
  • nonatomicatomic说的是对实例变量的读写是否为原子操作,默认情况下是atomic,即原子操作原子操作是一个线程在对变量读/写时,其他线程也想对该变量读/写则必须等待,线程A正在进行setter,线程B想要setter或getter都必须等待,保证变量不会被损坏。指定atomic只能保证在读/写的执行过程是线程安全的,在使用的变量时就不能保证是线程安全的了。

  • atomic使用了锁来保证原子操作,所以比较耗性能,所以在声明Property时最好指定为nonatomic

关于atomic的内部实现可以看这篇文章:线程安全类的设计

关于atomic线程安全与不安全可以看这篇文章:iOS多线程到底不安全在哪里?

  • 在Property中指定的内存管理方式,在类中的任何地方给该实例变量赋值时,也同样要遵循。尤其是在initializer方法中,不能通过Property的形式调用实例变量,只能直接用内部的实例变量,所以在赋值时要注意。

  • 总结在一起,一个比较好的类的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
@interface EOCPerson : NSObject<NSCopying>

// 定义对象的不可变性
@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
@property (nonatomic, strong, readonly) NSSet *friends;

- (instancetype)initWithFirstName:(NSString *)firstName
andLastName:(NSString *)lastName;
- (void)addFriend:(EOCPerson *)person;
- (void)removeFriend:(EOCPerson *)person;
@end

@interface EOCPerson()
@property (nonatomic, copy, readwrite) NSString *firstName;
@property (nonatomic, copy, readwrite) NSString *lastName;
@end
@implementation EOCPerson {
NSMutableSet *_internalFriends;
}

- (NSSet *)friends {
return [_internalFriends copy];
}

- (void)addFriend:(EOCPerson *)person {
[_internalFriends addObject:person];
}

- (void)removeFriend:(EOCPerson *)person {
[_internalFriends removeObject:person];
}

- (instancetype)initWithFirstName:(NSString *)firstName andLastName:(NSString *)lastName {
if (self = [super init]) {
// 采用property中指定的内存管理方式
_firstName = [firstName copy];
_lastName = [lastName copy];
_internalFriends = [[NSMutableSet alloc] init];
}
return self;
}

- (NSString *)description {
return [NSString stringWithFormat:@"%@",
@{@"firstName": _firstName,
@"lastName": _lastName
}];
}

- (NSString *)debugDescription {
return [NSString stringWithFormat:@"<%@: %p, %@>",
[self class],
self,
@{@"firstName": _firstName,
@"lastName": _lastName
}];
}

在内部直接读取实例变量

  • 在类的内部直接读取实例变量,而不是通过property来获得,这样读取效率会高些。但是对于懒初始化的变量,还是要用getter方法。
  • 在类的内部使用property提供的setter方法给变量赋值,这样可以统一对变量的控制。
  • 但是在initializersdealloc中,要直接读和写实例变量。因为子类可能重写setter方法做一些针对该子类的特殊处理,如果父类在初始化方法中用了setter,就会污染了父类。

理解对象的相等性

  • 判断两个对象是否相等的两个核心方法是isEqualhash,两个对象相等,那他们的hash值一定相等,但是hash值相等的两个对象,则不一定相等。
  • 对于hash方法不要对所有对象都返回一样值的,这样会降低比较的效率。根据上一条,如果hash值都不相等就可以不用再去调用isEqual比较了。尤其是对于NSSet,为了保证插入对象的唯一性,每次插入对象时都要去遍历已有的对象集合,而set的做法就是用hash作为索引,如果所有对象的hash值都一样,那就丧失了set的特性。

关于set内部实现的算法可以参考:NSArray,NSDictionary,NSSet 当中的算法知识

  • 书中给出的计算hash值的最佳实践:
1
2
3
4
5
6
- (NSUInteger)hash {
NSUInteger firstNameHash = [_firstName hash];
NSUInteger lastNameHash = [_lastName hash];
NSUInteger ageHash = _age;
return firstNameHash ^ lastNameHash ^ ageHash;
}

使用Delegate和Data Source协议进行对象间交流

  • 如果要频繁判断一个delegate对象是否实现了某些方法,可以使用bitfield struct缓存判断结果来提高性能。可以将该struct定义在类扩展中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#import "EOCNetworkFetcher.h"

@interface EOCNetworkFetcher() {
struct {
unsigned int didReceiveData : 1;
unsigned int didFailWithError : 1;
unsigned int didUpdateProgressTo: 1;
} _delegateFlags;
}
@end

@implementation EOCNetworkFetcher

- (void)setDelegate:(id<EOCNetworFetcherDelegate>)delegate {
_delegate = delegate;
_delegateFlags.didReceiveData = [delegate respondsToSelector:@selector(didReceiveData:)];
_delegateFlags.didFailWithError = [delegate respondsToSelector:@selector(didFailWithError:)];
_delegateFlags.didUpdateProgressTo = [delegate respondsToSelector:@selector(didUpdateProgressTo:)];
}

@end

用Categories将类中的方法按功能分割

  • 创建一个叫Private的category类来隐藏私有方法的实现细节,尤其在提供给别人library用时。可以不将该private category导出去,这样调用者是看不见这些私有方法的。

总是给第三方类的Category加上前缀

  • 因为Category中的方法会覆盖原有的方法,所以如果不是给自己写的类实现Category,最好在给该Category和其中定义的方法命名时,名字前加上前缀。
1
2
3
4
5
6
7
8
9
@interface NSString (ABC_HTTP)

// Encode a string with URL encoding
- (NSString *)abc_urlEncodedString;

// Decode a URL encoded string
- (NSString *)abc_urlDecodedString;

@end

避免在Categories中使用properties

  • 应该尽量避免在category中声明properties,因为category不支持给类添加实例变量,所以只声明properties编译器会报错,找不到实例变量的setter和getter方法。
  • 可以通过associated object在category中声明properties,但是也应尽量避免这样做。因为property中会指明weak/strong等内存管理方式,在用associated object实现setter方法时也要指明变量的内存管理方式,两个必须是一直的。在后续的开发中,如果需要修改property的内存管理方式,而那时你可能已经忘记了该property是这样实现的,在associated object处就没有对应的改,就有可能发生错误。
  • 最好在category中直接声明实例变量的getter方法,而不要通过property readonly的方式去做。

本文作者:意林
本文链接:http://www.shinancao.cn/2019/05/07/iOS-EffectiveObjC-2
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 许可协议。转载请注明出处!