《Effective Objective-C 2.0》 - 编写高质量iOS与OS X代码的52个有效方法 简介

原著作者是 Matt Galloway
出版社是机械工业出版社
共7章52个小节

第1章 熟悉Objective-C

1.了解Objetive-C语言的起源

* 为什么要了解OC的起源?
* OC语言的特性是什么?跟其他语言有什么区别?

OC是C语言的一个超集,是一门面向对象的语言
OC是一门使用“消息结构”的动态语言,只有在运行时才会检查对象类型

2.在类的头文件中尽量少引入其他头文件

* 为什么?
* 怎么做?

避免了在编译期暴露太多细节
使引入头文件的时机尽量延后,减少编译时间
避免循环引用头文件
使用@class向前声明

3. 多用字面量语法,少用与之等价的方法

*什么是字面量语法?

字面量语法就是使用“@”的语法糖

NSString *str = @"abc";
NSArray *arr = @[@"a", @"b"];
NSDictionary *dict = @[@"key1":@"v1", @"key2":@"v2"];

4. 多用类型常量,少用#define预处理指令

当要定义常量的时候,使用"const"代替#define,可以更清晰表明数据类型
使用"static const"定义当前编译单元内可用常量
使用"extern const"暴露全局常量

xx.h
extern NSString *const SHPersonNick; // 暴露出去

xx.m
static NSString *const SHPersonName = @"Name"; // 当前.m可用
NSStrng *const SHPersonNick = @"Nick"; // 全局常量,其他文件不可存在同名常量

5. 用枚举表示状态、选项、状态码

使用NS_ENUM、NS_OPTIONS来定义状态、选项,编译器会根据系统结构使用不同的实现。
在处理枚举的switch语句中不要使用default分支,这样编译器能自动检查是否已经处理了所有的枚举。

typedef NS_ENUM(NSUInteger, SHColor) {
    SHColorRed,
    SHColorBlue,
    SHColorGreen
};

switch (color) {
    case SHColorRed:
    //...
    break;
    
    case SHColorBlue:
    //...
    break;
    
    case SHColorGreen:
    //...
    break;
}

第2章 对象、消息、运行期

6. 理解“属性”这一概念

能自动生成实例变量、setter和getter方法。
可以设置原子性、读写权限、内存管理语义。

@property(nonatomic, copy) NSString *name;
@property(nonatomic, assign, readonly) int age;

7. 在对象内部尽量直接访问实例变量

读用实例,写用setter

*为什么?

读取需要速度,写可能需要处理。

self.name = @"Tom";
NSLog(@"name = %@", _name);

但是如果使用了延迟加载,就是用getter进行读取。

- (NSString *)name {
  if (!_name) {
     _name = @"Sam";  
  }
  return _name;
}

NSLog(@"name = %@", self.name);

8. 理解“对象同等性”这一概念

就是使用“==”操作符来比较对象。
可以覆写isEqual:和hash方法实现自定义类的等同比较。

*实现了isEqual:为什么还要实现hash?

当isEqual:判断两对象相等,hash也必须返回同一个值。
但是两个对象拥有同样的hash值,isEqual:方法未必判断为相等。
实现hash是为了解决在colleciton中使用对象时的索引问题。
好的hash算法能带来性能上的提升(速度高,哈希碰撞率低)。
其实就是hash表算法,所以就可以理解相同的对象必有相同的hash值。

9. 以“类族模式”隐藏实现细节

创建一个具有“公共接口”的类,作为抽象基类。
实现“工厂模式”。
使用枚举定义抽象基类,根据枚举值创建不同的的对象。

typedef NS_ENUM(NSUInteger, SHEmployeeType) {
    SHEmployeeTypeDeveloper,
    SHEmployeeTypeDesigner,
    SHEmployeeTypeFinace
};

+ (SHEmployee *)employeeWithType:(SHEmployeeType)type {
    switch (type) {
        case SHEmployeeTypeDeveloper:
            return [SHEmployeeDeveloper new];
            break;
        
        case SHEmployeeTypeDesigner:
            return [SHEmployeeDesigner new];
            break;
            
        case SHEmployeeTypeFinace:
            return [SHEmployeeFinance new];
            break;
    }   
}

大部分collection类都是类族,例如NSArray

NSArray *a = [NSArray array]; // a对象的类并不是NSArray

if ([a isMemberOfClass:[NSArray class]]) {
    // 不能进入
}

if ([a isKindOfClass:[NSArray class]]) {
    // 能进入
}

10. 在既有类中使用关联对象存放自定义数据

就是使用运行时添加关联对象,此法也可以在分类中实现“模拟添加属性”。

 #import <objc/runtime.h>

const char nameKey;

- (void)setName:(NSString *)name {
    objc_setAssociatedObject(self, &nameKey, name, OBJC_ASSOCIATION_COPY);
}

- (NSString *)name {
    return objc_getAssociatedObject(self, &nameKey);
}


11. 理解objc_msgSend的作用

OC是使用“消息结构”的语言,调用对象方法的实质是使用objc_msgSend给对象发送消息。
消息由“动态消息派发系统”处理,寻找方法实现并执行。

12. 理解消息转发机制

在对象方法缓存中寻找方法实现,若无,在对象类中寻找。
若当前对象无法响应方法调用,就进入消息转发流程。

  1. 调用resolveInstanceMethod调用备用方法。
  2. 若没有,调用forwardingTargetForSelector方法寻找备用接受者。
  3. 若没有,调用forwardInvocation启用完整的消息转发机制,指定目标接受者,此方法效果上“2”类似。
  4. 若没有,抛出“消息未能处理”。

ps:core data中的字段属性就是标注了@dynamic, 使用了消息转发机制,自己实现了字段属性的setter、getter

13. 用“方法调配技术”调试“黑盒方法”

使用运行时方法method_exchangeImplementations(Method m1, Method m2),改变方法实现。
此方法也可以实现hook(给方法加钩子)。

Method m1 = objc_getInstanceMethod([A class], @selector(doSomething));
Method m2 = objc_getInstanceMethod([A class], @selector(doOtherthing));
objc_exchangeImplementations(m1, m2);

14. 理解“类对象”的用意

*什么是类对象?
  1. 每个对象结构体都有一个“isa”指针,指向对象所属的类。
  2. 类的结构体存放的是此类的“元数据(metadata)”,存放有类的实例方法、实例变量。
  3. 类的结构体上也有一个“isa”指针,指向“元类(metaclass)”,存放了类的类方法(可以理解成类对象的实例方法),说明类也是Objective-C对象。
  4. 结构体上还有一个指针“super_class”指向此类的超类(父类)。
              NSObject类--isa--> NSObject元类
                |                |
                |超类             |超类
A类对象 --isa--> A类 -- isa --> A元类

第3章 接口与API设计

15. 用前缀避免命名空间冲突

OC没有命名空间,项目内的类名不能重复,所以可以使用前缀进行区分。
原则上苹果公司保留所有2位字符开头的前缀,所以最好使用3位或以上的前缀。

16. 提供“全能初始化方法”

即提供包含了所需参数的初始化方法。
注意如果超类的初始化方法不适合子类,在子类中必须覆写。

- (id)initWithString:(NSString *)string;

17. 实现description方法

实现了description方法,可以在控制台打印该实例。
如果想在调试的时候输出更详细的信息,可以实现debugDescription。

- (NSString *)description {
    return [NSString stringWithFormat:@"%@ - %@", _name, _desc];
}

18. 尽量使用不可变对象

在头文件中,如果不是需要暴露出去的属性,最有所有权修饰符定为"readonly",然后在实现文件中设置可读写。
可以提供修改方法来修改不可写属性。

//SHPerson.h
@property(nonatomic, copy, readonly) NSString *name;
- (void)modifyName:(NSString *)name;

//SHPerson.m
@property(nonatomic, copy) NSString *name;

- (void)modifyName:(NSString *)name {
    _name = [NSString stringWithFormat:@"name: %@", name];
}

19. 使用清晰而协调的命名方式

注意类名、方法名、变量名、常量名的命名方式。
方法名尽量详尽易懂,尽可能描述这个方法做了什么。

20. 为私有方法名加前缀

更好区分公共方法、私有方法。
苹果公司保留使用下划线开头的方法名。

- (void)p_doSomePrivateThing;

21. 理解Objective-C错误模型

在发生严重错误的时候跑出NSException,停止程序

if (/* terrible error */) {
    @throw [NSException exceptionWithName:@"Exp"
                                    reason:@"There was an error"
                                  userInfo:nil];
}

22. 理解NSCopying协议

实现NSCopying协议和其中的方法"copyWithZone:",调用copy方法的时候回调用此方法
zone是过时的东西,不必理会。
对于collection类来说,“copyWithZone:”对应的是浅拷贝copy方法;如果要进行深拷贝,可以实现“NSMutableCopying”协议和"mutableCopyWithZone:"方法,对应mutableCopy方法。

//SHPerson.h
@interface SHPerson : NSObject <NSCopying>

@proeprty(nonatomic, copy) NSString *name;
@property(nonatomic, copy) NSString *nick;

@end

//SHPerson.m
- (id)copyWithZone:(NSZone *)zone {
    SHPerson *person = [][SHPerson alloc] init];
    person.name = _name;
    person.nick = _nick;
}

第4章 协议与分类

23. 通过委托与数据源协议进行对象间通信

可以参考UITableView的UITableViewDataSource、UITableViewDelegate协议。

24. 将类的实现代码分散到便于管理的数个分类之中

不多说,就是拆分逻辑,降低耦合。

25. 总是为第三方类的分类名称加前缀

防止和系统原有的类冲突。

26. 勿在分类中声明属性

在分类中声明属性也并不能自动生成实例变量和setter、getter方法
如果要使用,可以使用runtime运行时的关联对象模拟setter、getter

//xx.h
 #import "PCBitch.h"

@interface PCBitch (Asshole)

@property(nonatomic, copy) NSString *assholeName;

@end

//xx.m
 #import "PCBitch+Asshole.h"
 #import <objc/runtime.h>

static char assholeNameChar;

@implementation PCBitch (Asshole)

- (void)setAssholeName:(NSString *)assholeName {
    objc_setAssociatedObject(self, &assholeNameChar, assholeName, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)assholeName {
    return objc_getAssociatedObject(self, &assholeNameChar);
}

@end


27. 使用"class-continuation"分类隐藏实现细节

就是使用“内扩展”,隐藏不必要暴露的属性、方法。

28. 通过协议提供匿名对象

参考代理模式的实现。

@property(nonatomic, weak) id<SHPersonDelegate> delegate; //隐藏了类型,就是无论delegate是什么类,反正只要实现了SHPersonDelegate协议就可以了 

第5章 内存管理

29. 理解引用计数

通过递增递减的计数器来管理内存,若计数等于0,对象会被销毁,系统会标记此内存可回收使用。
苹果是通过一个计数表类保存各个对象的计数和内存地址的。

30. 以ARC简化引用计数

Automatic Reference Counting技术,编译器自动管理引用技术,再也不用手动写retain、release啦。
需要注意Core Foundation对象不归ARC管理,需要手动进行分配和释放。

31. 在dealloc方法中只释放引用并解除监听

在dealloc方法中必须释放资源、取消KVO或者Notification通知,不然再次接收到通知时会crash,因为观察者已经被销毁释放了。
除此之外的方法不要调用。

- (void)dealloc {
    CFRelease(coreFoundationObject);
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

32. 编写“异常安全代码”时留意内存管理问题

捕获异常的时候要清理干净try内创建的对象。

33. 以弱引用避免保留环

就是使用__weak关键字修饰互相引用的其中一个对象,避免互相强引用。

34. 以“自动释放池块”降低内存峰值

系统在runloop中会创建一个autoreleasepool,所以每次循环结束都能自动回收对象,但是例如当使用循环创建大量对象并使用完成之后,并不会立刻释放对象,造成内存高峰。

for (int i=0; i<9999; i++) {
    @autoreleasepool { // 这样就能每创建一个,就释放旧的一个
        SHPerson *p = [[SHPerson alloc] init];
        [p doSomething];
    }
}

35. 用“僵尸对象”调试内存管理问题

在Xcode的环境变量中设置NSZomebieEnabled开启,可以使对象计数为0的时候不会被系统真正回收,而是成为僵尸对象。
系统会修改对象的isa指针,指向一个僵尸类,从而改变为僵尸对象。

36. 不要使用retainCount

第6张 块(Block)与大中枢派发(GCD)

37. 理解“块”这一概念

  1. block类似于函数指针。
  2. 在block声明的范围能,能捕获所有变量。
  3. 如果变量是对象,block会自动保留,知道block被释放。
  4. block也可以有引用计数(堆block)。
  5. block会把捕获的变量都拷贝一份。
  6. block分为全局block、栈block和堆block。
  7. 定义的时候,block默认分配在栈中,所以如果编译器覆盖了该块内存,而调用此block会发生崩溃。
  8. 鉴于上述的情况,需要对block进行copy操作,把这个block移到堆内存中,并具有引用计数。
  9. 创建一个block属性的时候,使用copy策略,可以把set进来的block放入堆中。
  10. 全局block在编译器定义,不能捕获任何变量,有点像静态常量。

38. 为常用的块类型创建typedef

typedef void(^doSomethingBlock)(void) DoSomethingBlock;

DoSomethingBlock block = ^(void) {
    // do something
}

39. 用handler块降低代码分散程度

使用block比delegate能使代码更紧凑,逻辑更通顺。

- (void)doSomethingWithParam:(id)param completion:(void (^)(void))completion;

[object doSomethingWithParam:param completion:^(void) {
    // do something after completion
}];

40. 用块引用所属对象时不要出现保留环

使用__weak修饰,解决reatain cycle问题。

__weak typeof(self) weakSelf = self;

[self setDoSometingBlock:^(void) { // self持有block
    [weakSelf doOneThing]; // block持有self
}];

41. 多用派发队列,少用同步锁

使用派发队列代替同步锁,能减少crash几率。

42. 多用GCD,少用performSelector系列方法

由于performSelector对选择子的返回类型、参数个数有限制,如果要使用跨线程调用方法,最好使用GCD。

43. 掌握GCD及操作队列的使用时机

44. 通过Dispatch Group机制,根据系统资源情况来执行任务

45. 使用dispatch_once来执行只需要运行一次的线程安全代码

46. 不要使用dispatch_get_current_queue

此函数已被废弃。

第7张 系统框架

47. 熟悉系统框架

48. 多用块枚举,少用for循坏

遍历一个collection有4种方式:for循环、NSEnumerator、快速遍历、块枚举。
块枚举比for循环提供更多的信息。

NSArray *list = ...;

// for循环
for (int i=0; i<list.count; i++) {
    
}

// 快速遍历
for (id object in list) {

}

// NSEnumerator
NSEnumerator *enum = [list objectEnumerator];
id object;
while (object = [enum nextObject]) {
    
}

// 块枚举
[list enumerateObjectUsingBlock:^(id object, NSUInteger index, BOOL stop) {
    
}]

49. 对自定义其内存管理语义的colletion使用无缝桥接

50. 构建缓存时选用NSCache而非NSDictinary

51. 精简initialize与load的实现

  1. 加入了运行期的每个类和分类都会调用load方法,先调用类的,再调用分类。
  2. 在load方法中调用其他类是不安全的,因为不确保其他类都已经被加载了。
  3. 如果一个类没有实现load,不过其超类有没有实现,系统都不会调用。
  4. initialize该方法会在程序首次调用此类的时候调用,是惰性调用。
  5. initialize和普通方法一样,如果类没有实现,就会到超类中寻找。
  6. 可以在initialize方法中初始化无法在编译器确定的全局变量。

52. 别忘了NSTimer会保留其目标对象

// _timer会保留self,产生retain cycle
_timer = 
[NSTimerscheduledTimerWithTimeInterval:1.0
                                 target:self
                               selector:@selector(doSomething)
                               userInfo:nil
                               repeast:YES];
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容