Effective Objective-C

一、对象所占的内存总是分配在堆空间,绝对不会分配在栈上。

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和消息转发

Runtime浅析


十三、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++类隐藏到分类当中。


二十八、通过协议提供匿名对象


二十九、

猜你喜欢

转载自blog.csdn.net/weixin_34290000/article/details/86813059