一、对象
所占的内存总是分配在堆空间
中,绝对不会分配在栈上。
NSString *someString = @"The string";
someString 是一个指针,它的值是个内存地址,它指向的对象@"The string"保存在堆上,而someString自身是保存在栈上的。&someString 可以拿到someString自身的内存地址。
二、每次在头文件引入其他头文件之前,都先考虑是否可以用向前声明取代,即先在头文件中使用:
// .h文件
@class ClassToBeImported;
然后在实现文件中去引用:
// .m文件
#import ClassToBeImported
三、尽量使用以下字面量语法
NSString *someString = @"someString";
NSNumber *intNumber = @1;
NSNumber *floatNumber = @2.5f;
NSNumber *doubleNumber = @3.14159;
NSNumber *boolNumber = @YES;
NSNumber *charNumer = @'a';
四、少用#define预处理指令,多用类型常量
// 少用
#define ANIMATION_DURATION 0.3
// 多用
static const NSTimeInterval kAnimationDuration = 0.3;
命名的规则:如果常量只在本类使用,则在前面加上k;如果要在类之外可见。则以此类名为前缀。
五、用枚举表示状态、选项、状态码。在处理枚举类型的switch语句中不要实现default分支。这样加入新枚举后编译器才会提醒:switch语句未处理所有枚举。
六、指定为copy的字符串,意味着编译器自动生成的setter方法是实现了通过copy来赋值的。如果通过直接调用来赋值,则需要手动进行copy:_firstName = firstName.copy;
(设置属性所对应的实例变量时,一定要遵从该属性所声明的语义)。
atomic基本上不用,因为它并不能保证线程安全,而且开销较大。
七、在对象内部的时候,尽量通过直接访问的形式来访问实例变(懒加载除外),而在设置实例变量的时候通过属性来做。
self.firstName = firstName;
if (_firstName) {}
但是在初始化函数里面设置值的时候,一般通过直接访问来设置,因为子类有可能复写了setter方法。
#import "Person.h"
@implementation Person
- (instancetype)initWithFirstName:(NSString *)firstName {
self = [super init];
if (self) {
_firstName = firstName.copy;
}
return self;
}
@end
#import "SmithPerson.h"
@implementation SmithPerson
- (void)setFirstName:(NSString *)firstName {
if (![firstName isEqualToString:@"Smith"]) {
[NSException raise:NSInvalidArgumentException format:@"First name must be Smith"];
}
[super setFirstName:firstName];
}
@end
原书里的例子貌似会出现死循环:
- (void)setLastName:(NSString *)lastName {
if (...) {
...
}
self.lastName = lastname; // 循环了,而且应该是lastName
}
八、要检测对象的等同性,需要提供isEqual:和hash方法。
如果两个两个对象相同,那么它们的hash值必须相等,但是hash值相等的两个对象未必相同。
九、类族的概念有点类似Java的抽象类。
如果不希望某个基类被实例化,可以提供一个公共的类方法来根据传来的参数生成相应的子类实例,从而隐藏具体的实现细节。
+ (instancetype)personWithType:(PersonType)personType {
switch (personType) {
case PersonTypeSmith:
return [[PersonSmith alloc] init];
case PersonTypeUnderwood:
return [[PersonUnderwood alloc] init];
}
}
类似UIButton的buttonWithType:。
类族创建出来的对象在判断它的类时要额外注意。
系统框架中有很多类族。大部分的collection类都是类族。比如NSArray:
NSArray *tmpArray = [[NSArray alloc] init];
if (tmpArry.class == NSArray.class) {
}
这个判断不会为YES,打点会看到其实tmpArray的类型是_NSArray0。类似的还有NSURLSeesion,这也是为什么AFNetWork在实现NSURLSession的方法替换时要先创建一个实例出来,再调用object.class才能拿到真正的类:
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration ephemeralSessionConfiguration];
NSURLSession * session = [NSURLSession sessionWithConfiguration:configuration];
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wnonnull"
NSURLSessionDataTask *localDataTask = [session dataTaskWithURL:nil];
#pragma clang diagnostic pop
IMP originalAFResumeIMP = method_getImplementation(class_getInstanceMethod([self class], @selector(af_resume)));
Class currentClass = [localDataTask class];
调用isMemberofClass也会返回NO,但isKindofClass会返回YES。
十、关联属性
#import <objc/runtime.h>
// 设置关联对象
objc_setAssociatedObject(<#id _Nonnull object#>, <#const void * _Nonnull key#>, <#id _Nullable value#>, <#objc_AssociationPolicy policy#>);
// 读取关联对象
objc_getAssociatedObject(<#id _Nonnull object#>, <#const void * _Nonnull key#>);
// 移除全部的关联对象
objc_removeAssociatedObjects(<#id _Nonnull object#>);
objc_AssociationPolicy:
关联类型 | 等效的@property属性 |
---|---|
OBJC_ASSOCIATION_ASSIGN | assign |
OBJC_ASSOCIATION_RETAIN_NONATOMIC | nonatomic, retain |
OBJC_ASSOCIATION_COPY_NONATOMIC | nonatomic, copy |
OBJC_ASSOCIATION_RETAIN | retain |
OBJC_ASSOCIATION_COPY | copy |
十一和十二、objc_msgSend和消息转发
十三、Method_Swizzing
Runtime浅析
十四、类对象
Runtime浅析
确定对象类型的时候不要直接比较类对象:[object class] == [EOCSomeClass class],而应该使用issKinfOfClass
方法,因为后者可以正确处理消息转发。
十五、前缀
类的命名需要加前缀,而且前缀应该是三个字母,因为苹果会使用两个字母的。
分类和分类中的方法需要加前缀。
类的实现文件中所用的纯C函数及全局变量需要加前缀。
NSArray * AFQueryStringPairsFromDictionary(NSDictionary *dictionary) {
return AFQueryStringPairsFromKeyAndValue(nil, dictionary);
}
static NSArray * AFHTTPRequestSerializerObservedKeyPaths() {
static NSArray *_AFHTTPRequestSerializerObservedKeyPaths = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_AFHTTPRequestSerializerObservedKeyPaths = @[NSStringFromSelector(@selector(allowsCellularAccess)), NSStringFromSelector(@selector(cachePolicy)), NSStringFromSelector(@selector(HTTPShouldHandleCookies)), NSStringFromSelector(@selector(HTTPShouldUsePipelining)), NSStringFromSelector(@selector(networkServiceType)), NSStringFromSelector(@selector(timeoutInterval))];
});
return _AFHTTPRequestSerializerObservedKeyPaths;
}
十六、全能初始化方法
如果创建类实例的方法不止一种,那就提供一个全能初始化方法,让其他的初始化方法最终都调用它。
如果父类的初始化方法不适用于子类,那么子类应该复写该方法,并在其中抛出异常。比如不想EOCSomeClass通过[[EOCSomeClass alloc] init]来创建实例,而是通过[[EOCSomeClass alloc] initWIthType: type]来创建,那就复写EOCSomeClass的init方法,并在里面抛出异常。
- (instancetype)init {
@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Must use initWithUrl: or initWithUrl:destinationPath: instead" userInfo:nil];
}
十七、实现description方法
可以借助NSDictionary的description方法。
- (NSString *)description {
return [NSString stringWithFormat:@"<%@ : %p, %@>", self.class, self, @{NSStringFromSelector(@selector(firstName)) : _firstName}];
}
十八、尽量使用不可变的对象
只在确有必要时才将属性对外公布,而且尽量将对外的属性设为只读:readonly。
如果希望可以在对象内部可以修改而外部不能改动,可以在分类里面重新将该属性声明为readwrite
//.h
@interface Person : NSObject
@property (nonatomic, copy, readonly) NSString *number;
@end
// .m
@interface Person()
@property (nonatomic, copy, readwrite) NSString *number;
@end
但是这种情况可能会碰到一个问题:在对象内部写入某属性时,对象外的观察者也在读取该属性。要避免这个问题,需要将数据存取操作都设为同步操作。
关于容器对象,可以在内部创建私有的可变容器,然后返回只读的该容器的拷贝:
@interface Person : NSObject
@property (nonatomic, strong, readonly) NSSet *friends;
@end
@implementation Person {
NSMutableSet *_interalFriends;
}
- (NSSet *)friends {
return [_interalFriends copy];
}
@end
应该默认读到的容器都是不可变的。不应该把可变的容器作为属性公开,而是提供相关的方法来修改对象中的可变容器。
十九、二十、命名
给私有方法加上前缀,可以是p_,不建议用单个,因为苹果公司是这么命名私有方法的,如果我们也用单个,可能无意间复写了它的方法。
二十一、错误模型
常见的NSError用法:通过方法的输出参数返回给调用者:
- (BOOL)doSomething:(NSError **)error;
看以下的代码:
void changeName(Person *person) {
person = [[Person alloc] init];
person.firstName = @"B";
}
int main(int argc, char * argv[]) {
@autoreleasepool {
Person *person = [[Person alloc] init];
person.firstName = @"A";
changeName(person);
}
}
在main函数中创建一个Person,调用changeName方法修改firstName后,main函数里面的person的firstName依然还是A,也就是changeName没法修改main函数里面的person的属性。这个很好理解,因为changeName里面创建了新的person。从定义上来理解,是因为传给changeName的person本质是一个指针,可以看成是一个整型,传给changeName的只是这个指针值的一个拷贝,person = [[Person alloc] init]仅仅修改了changeName里面的person的值,不会影响到外面main函数的。外面的person指向的一直是一开始创建的,它的值没有变。
再看看下面的:
void changeName(Person **person) {
*person = [[Person alloc] init];
(*person).firstName = @"B";
}
int main(int argc, char * argv[]) {
@autoreleasepool {
Person *person = [[Person alloc] init];
person.firstName = @"A";
changeName(&person);
}
}
这里changeName接收的是person这个指针自己本身的地址,拿到这个地址后可以修改person这个指针的值,所以changeName里面也修改了main函数的person这个指针值,它指向了在changeName里面创建的新的Person实例,这样changeName执行完成后,main函数里person指向的值已经不是一开始创建的实例了,而是在changeName里面创建的实例。
二十二、NSCopying
要想自己的类支持拷贝操作,则需要实现NSCopying协议,然后实现- (id)copyWithZone:(NSZone *)zone方法,copy方法的内部会调用copyWithZone。
关于浅拷贝和深拷贝:
Foundation框架里面所有的容器类在默认情况下都是浅拷贝,也就是拷贝之后两个容器里面的数据都是相同的,并没有复制数据,而深拷贝则会复制每个数据。
二十三、协议
如果协议中的方法是可选的,可以用位段来记录该协议是否实现了某个方法,避免每次调用的时候都要判断。
@protocol EOCNetworkFetcherDelegate <NSObject>
@optional
- (void)networkFetcher:(EOCNetworkFetcher *)fetcher didReceiveData:(NSData *)data;
- (void)networkFetcher:(EOCNetworkFetcher *)fetcher didFailWithError:(NSError *)error;
- (void)networkFetcher:(EOCNetworkFetcher *)fetcher didUpdateProgressTo:(float)progress;
@end
@interface EOCNetworkFetcher() {
struct {
unsigned int didReceiveData : 1;
unsigned int didFailWithError : 1;
unsigned int didUpdateProgressTo : 1;
} _delegateFlags;
}
@property (nonatomic, weak) id<EOCNetworkFetcherDelegate> delegate;
@end
@implementation EOCNetworkFetcher
- (void)setDelegate:(id<EOCNetworkFetcherDelegate>)delegate {
_delegate = delegate;
_delegateFlags.didReceiveData = [delegate respondsToSelector:@selector(networkFetcher:didReceiveData:)];
_delegateFlags.didFailWithError = [delegate respondsToSelector:@selector(networkFetcher:didFailWithError:)];
_delegateFlags.didUpdateProgressTo
= [delegate respondsToSelector:@selector(networkFetcher:didUpdateProgressTo:)];
}
二十四、把类的实现代码分散到便于管理的数个分类中
将私有方法归入名叫Private的分类中
二十五、为第三类的分类名称加前缀
二十六、不要在分类中什么属性
分类的机制,应将其理解为一中手段,目标在于拓展类的功能,而非封装数据。属性的意思是有数据在支持它,属性是用来封装数据的。不要为了存取方法什么一个属性。
二十七、使用分类来隐藏实现的细节
分类可实现属性对外声明为只读,但对内为只读写。另外在Object-C++中也可以将引用到的C++类隐藏到分类当中。
二十八、通过协议提供匿名对象
二十九、