读《编写高质量iOS与OS X代码的52个有效方法》

又看了一遍《编写高质量iOS与OS X代码的52个有效方法》这本书,做一个简单的总结,其中runtime和GCD那些的不是太详细,要想很详细估计写的东西比篇文字都多,但恰巧又是iOS的重点和难点,这个需要后续有机会会单独拿出文章来写。暂时先多看看专门的博客。另外,由于笔者水平有限,如有不妥之处欢迎指正,一起学习进步。

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

消息结构和函数调用的区别:

使用消息结构的语言,其运行时所应执行的代码由运行环境来决定;而使用函数调用的语言,则由编译器决定。另外,CGRect结构体,储存在栈区

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

如果不是非用不可,尽量用@class(向前声明)代替#import,加快编译速度。

3. 多用字面量语法,少用与之等价的方法
id object1 = @"1";
id object2 = nil;
id object3 = @"3";
    
NSArray *arrayA = [NSArray arrayWithObjects:object1, object2, object3, nil];
NSArray *arrayB = @[object1, object2, object3];
    
复制代码

上面的代码我们发现,arrayA虽然能创建出来,但是其中却只有object1一个对象,原因在于“arrayWithObjects:”方法会依次处理各个参数,直到发现nil为止,由于object2是nil,所以该方法会提前结束,按字面量语法创建arrayB时会抛出异常。 所以:

使用字面量语法创建数组、字典...更安全,语法更简洁。

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

仅在“编译单元(.m文件内)”内使用

define ANIMATION_DURATION 0.3 // 不建议
static NSTimeInterval const kANIMATION_DURATION = 0.3; // 建议

复制代码

需要外露使用

// EOCAnimatedView.h
extern const NSTimeInterval EOCAnimateViewAnimationDuration;

// EOCAnimatedView.m
const NSTimeInterval EOCAnimateViewAnimationDuration = 0.3;
复制代码

上述宏定义,预处理指令会把源代码中的ANIMATION_DURATION字符串替换成0.3。假设此指令声明在某个头文件中,那么所有引入了这个头文件的代码,其ANIMATION_DURATION都会被替换。

涉及知识点:
  • 常量命名:若常量局限于某“编译单元”(也就是实现文件.m)之内,则在前面加上字母k;若常量在类之外可见,则通常以类名为前缀。

  • 宏:

    • 作用:在预编译阶段替换;
    • 其他:在预编译阶段执行,不做编译检查,不会报编译错误;
  • const:

    • 作用:仅仅用来修饰右边的变量,被const修饰的变量是只读的;
    • 其他:在编译阶段执行,会做编译检查,会报编译错误;
  • static的作用:

    • 修饰局部变量:1. 延长局部变量的声明周期,程序结束才会销毁;2.局部变量只会生成一份内存,只会初始化一次;3. 改变局部变量的作用域;
    • 修饰全局变量:1. 只能在本文件中访问,修改全局变量的作用域,生命周期不会改变;2. 避免重复定义全局变量;
  • extern:

    • 作用:只是用来获取全局变量(包括全局静态变量)的值,不能用于定义变量;
    • 原理:先在当前文件中查找有没有全局变量,没有找到,才会去其他文件查找;
  • static与const联合使用:

    • 作用:声明一个只读的静态变量;(在每个文件都需要定义一份静态全局变量)
    • 使用场景:在一个文件中经常使用的字符串常量;
  • extern与const联合使用:

    • 作用:只需要定义一份全局变量,多个文件共享;
    • 使用场景:在多个文件中经常使用同一个字符串常量;
5. 用枚举表示状态、选项、状态码

在处理枚举类型的switch语句中不要实现default分支。这样的话,加入新枚举之后,编译器就会提示开发者:switch语句并未处理所有枚举。

6. 理解“属性”这一概念
@interface EOCPersion: NSObject {
    @public
    NSString *_firstName;
    NSString *_lastName;
    @private
    NSString *_someInternalData;
}
@end
复制代码

编写时使用实例变量有个问题:就是对象布局在编译期就已经固定了,只要碰到访问_firstName变量的代码,编译器就把其替换为“偏移量”(offset),这个偏移量是“硬编码”(hardcode),表示该变量距离存放对象的内存区域的起始地址有多远,这样做目前没问题,但是如果又加了一个实例变量,如:

@interface EOCPersion: NSObject {
    @public
    NSString *_dateOfBirth;
    NSString *_firstName;
    NSString *_lastName;
    @private
    NSString *_someInternalData;
}
@end
复制代码

那么原来_firstName的偏移量现在却指向_dateOfBirth了,把偏移量硬编码于其中的那些代码都会读取到错误的值。所以,修改之后必须重新编译,否则就会出错。Obejcet-C的做法是,把实例变量当做一种存储偏移变量所用的“特殊变量”,交由“类对象”保管,偏移量会在运行期查找,如果类的定义变了,那么存储的偏移量也就变了,这样的话,无论何时访问实例变量,总能使用正确的偏移量。另外还有一种解决办法就是,尽量不要直接访问实例变量,而应该通过存取方法来做。

atomic一定安全吗?

具备atomic特质的获取方法会通过锁定机制来确保其操作的原子性,但这无法保证绝对的“线程安全”,例如:一个线程在连续多次读取某个属性值的过程中有别的线程在同时改写该值,那么即便将属性声明为atomic,也还是会读到不同的属性值。

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

在对象之外访问实例变量时,总是应该通过属性来做,然后在对象内部访问实例变量是该如何?作者建议

在读取实例变量的时候采用直接访问的形式,而在设置实例变量的时候通过属性来做。

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

重写hash的一种思路,相对高效

- (NSUInteger)hash
{
    NSInteger firstNameHash = [_firstName hash];
    NSInteger lastNameHash = [_lastName hash];
    NSInteger ageHash = _age;
    return firstNameHash ^ lastNameHash ^ ageHash;
}

复制代码

一般重写“isEqual:”方法

- (BOOL)isEqualToPerson:(EOCPerson *)otherPerson
{
    if (self == otherPerson) return YES;
    if (![_firstName isEqualToString:otherPerson.firstName]) return NO;
    if (![_lastName isEqualToString:otherPerson.lastName]) return NO;
    if (_age != otherPerson.age) return NO;
    
    return YES;
}

-(BOOL)isEqual:(id)object
{
    if ([self class] == [object class]) {
        return [self isEqualToPerson:(EOCPerson *)object];
    } else {
        return [super isEqual:object];
    }
}
复制代码
9. 以“类簇模式”隐藏实现细节
10. 在既有类中使用关联对象存放自定义数据

可以把关联对象想象成NSDictionary,只不过设置关联对象时用到的键(key)是个“不透明的指针”。

11. 理解objc_msgSend的作用

消息发送机制已经说的够多了,不再重复,这里只摘了书中一段:

如果某函数的最后一项操作是调用另外一个函数,那么久可以运用“尾调用优化”技术。编译器会生成调转至另一函数所需要的指令码,而且不会向调用堆栈中推入新的“栈帧”。只有当某函数的最后一个操作仅仅是调用其他函数而不会将其返回值另做他用时,才能执行“尾调用优化”。这项优化对objc_msgSend非常关键,如果不这么做的话,那么每次调用Object-C方法之前,都需要为调用objc_msgSend函数准备“栈帧”,大家在“栈踪迹”中可以看到这种“栈帧”。此外,若是不优化,还会过早的发生“栈溢出”现象。

12. 理解消息转发机制

基本就是消息转发的大体流程,需要注意的一点就是步骤越往后,处理消息的代价就越大,最好能在第一步就处理完,这样的话,运行期系统就看可以将此方法缓存起来,如果这个类的实例稍后还受到同样的选择子(方法选择器),那么根本无须启动消息转发流程。 详细文章可参考iOS 消息发送与转发详解

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

利用动态特性,运行时动态添加、交换方法

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

尽量使用类型信息查询方法来确定对象类型,而不要直接比较类对象,因为某些对象可能实现了消息转发功能。11~14 全是老成长谈的runtime机制,这个一时半会且三言两语是无法讲解清楚的(单拿出一整篇文章来讲都不见得能面面俱到),请移步,这篇讲的稍微详细点

15. 用前缀避免命名空间冲突
16. 提供“全能初始化方法”
17. 实现description方法
  • 实现description方法返回一个有意义的字符串,用以描述该实例;
  • 若想在调试时打印出更详尽的对象描述信息,则应实现debugDescription方法;
18. 尽量使用不可变对象

这个意思是说:

尽量把对外公布出来的属性设为只读,而且只有在确有必要的时候才将属性对外公布。

尽管把属性设置成readOnly之后,可以防止外部乱改属性值,但并不是完全不能改,外界仍然可以通过“键值编码”(KVC)技术设置这个属性值。

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

别怕麻烦,要见名知意,方法命名可参考系统方法名。

20. 为私有方法名加前缀

作者建议:

编写类的实现代码时,经常要写一些只在内部使用的方法。应该为私有方法的名称前加上某些前缀,这有助于调试,因为据此很容易就能把公共方法和私有方法区别开。可以用“p_”打头。(不要单单只用一个下划线做私有方法的前缀,因为这种做法是预留给苹果大大用的)。

21. 理解Object-C错误模型

当出现“不那么严重的错误”时,Object-C语言所用的编程范式为:令方法返回nil/0,或者使用NSError

NSError对象里封装了三条信息:

  • Error domain(错误范围,其类型为字符串);
  • Error code(错误码,其类型为整数);
  • User info(用户信息,其类型为字典)
22. 理解NSCoding协议

想要实现复制功能,需要遵从NSCoding协议,实现copuWithZone:方法;

深拷贝:

深拷贝:在拷贝对象自身时,将其底层数据也一并复制过去。Foundation框架中所有collection类在默认情况下都执行浅拷贝。

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

这里主要是讲了使用delegate的场景以及相关的注意事项,比如使用weak防止循环引用等。这里有一个优化点:(不过笔者很少这么干,不是很少,是压根没有?)

我们通常把委托对象能否响应某个协议方法这一信息缓存起来,以优化程序效率。 将方法响应能力缓存起来的最佳途径是实用“位段”数据类型。这是一项乏人问津的C语言特性,但在此处用起来却正合适。

// 协议声明部分
@class EOCNetworkFetcher;

@protocol EOCNetworkFetcherDelegate
@optional
- (void)networkFetcher:(EOCNetworkFetcher *)fetcher didReceiveData:(NSData *)data;
- (void)networkFetcher:(EOCNetworkFetcher *)fetcher didFailWithError:(NSError *)error;
- (void)networkFetcher:(EOCNetworkFetcher *)fetcher didUpdateProgressTo:(float )progress;

@end

在.m文件中 定义一个结构体_delegateFlags用来缓存方法响应能力

@interface EOCNetworkFetcher ()
{
    struct {
        unsigned int didReceiveData : 1;
        unsigned int didFailWithError : 1;
        unsigned int didUpdateProgressTo : 1;
    } _delegateFlags;
}
@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:)];
}

// 在使用的时候 直接判断
if (_delegateFlags.didUpdateProgressTo) {
        [_delegate networkFetcher:self didUpdateProgressTo:currentProgress];
    }

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

使用分类机制可以把类的实现代码划分成易于管理的小块;

25. 总是为第三方类的分类名称增加前缀
  • 向第三方类中添加分类时,总应该给其名称加上你专用的前缀;
  • 向第三方类中添加分类时,总应该给其中的方法名加上你专用的前缀;
26. 勿在分类中声明属性

把封装数据所用的全部属性都定义在主接口里。不建议在分类中添加属性,虽然通过关联对象能够解决在分类中不能合成实例变量的问题。但不是最理想的,且在内管理上容易出错,分类只是一种手段,目的是在于扩展类的功能,而非封装数据。

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

其实就是类扩展

  • 通过“class-continuation分类”向类中新增实例变量;
  • 如果某属性在主接口中声明为“只读”,而类的内部又要用设置方法修改此属性,那么就在“class-continuation分类”中将其扩展为“可读写”。
  • 把私有方法的原型声明在“class-continuation分类”里面;
  • 若想使类所遵循的协议不为人所知,则可于“class-continuation分类”中声明。
28. 通过协议提供佚名对象
29. 理解引用计数
- (void)setFoo:(id)foo
{
    [foo retain];
    [_foo release];
    _foo = foo;
}
复制代码

此方法将保留新值释放旧值,然后更新实例变量,令其指向新值。顺序很重要。假如还未保留新值就先把旧值释放了,而且这两个值又指向同一个对象,那么,限执行的release操作就有可能导致系统将此对象永久回收。而后续的retain操作则无法令这个已经彻底回收的对象复生,于是实例变量就成了悬挂指针。

30. 以ARC简化引用计数
  1. ARC在调用这些方法时,并不通过普通的Object-C消息派发机制,而是直接调用其底层的C语言版本,这样性能更好,因为保留及释放操作需要频繁执行,所以直接调用底层函数能节省很多CPU周期,比如ARC会调用与retain等价的底层函数objc_retain。
  1. 在编译期,ARC会把能够相互抵消的retain、release、autorelease操作简约。如果发现在同一个对象上执行多次的“保留”与“释放”操作,那么ARC有时可以成对的移除这两个操作;
  2. ARC也包含运行期组件。用于检测当前方法返回之后即将执行的那段代码。如果发现那段代码要在返回的对象上执行retain操作,则设置全局数据结构中的一个标志位,而不执行autorelease操作。
  3. ARC最在dealloc方法中自动生成.cxx_destrucet方法,释放对象
31. 在dealloc方法中只释放引用并解除监听

简单说就是在dealloc方法里,只应该释放引用,或者移除监听者(KVO或NSNotificationCenter),不要做其他事。

32. 编写“异常安全代码”时留意内存管理问题
  • 捕获异常时,一定要注意将try块内所创建的对象清理干净;
  • 在默认情况下,ARC不生成安全处理异常所需的清理代码,开启编译器标志后(-fobjc-arc-exceptions),可生成这种代码,不过会导致应用程序变大,而且会降低运行效率。
33. 以弱引用避免保留环

使用weak。weak所指向的实例回收后,weak属性指向nil,而unsafe_unreatined属性仍然指向那个已经回收的实例。

34. 以“自动释放池块”降低内存峰值
for (int i = 0; i < 100000; i++) {
        [self doSomethingWithInt:i];
    }
复制代码

如果“doSomethingWithInt:”方法要创建临时对象,那么这些对象很可能放在自动释放池里。但是,这些对象在调用完方法之后就不再使用了,他们也依然处于存活状态,因为目前还在自动释放池里,等待线程执行下一次事件循环时才会清空。这就意味着在执行for循环时,会持续有新对象创建出来,加入到自动释放池中。所有这些对象要等到for循环执行完之后才会释放,所以会造成内存高峰,即:内存用量持续上涨,然后突然下降。

NSArray *databaseRecords = /* ... */;
    NSMutableArray *people = [NSMutableArray array];
    for (NSDictionary *record in databaseRecords) {
        @autoreleasepool {
            EOCPerson *person = [[EOCPerson alloc] initWithRecord:record];
            [people addObject:person];
        }
    }
复制代码
35. 用“僵尸对象”调试内存管理问题
  1. 僵尸对象:已经被销毁的对象(不能再使用的对象);
  1. 野指针:指向僵尸对象(不可用内存)的指针
  2. 给野指针发消息会报EXC_BAD_ACCESS错误
  • 系统在回收对象时,可以不将其真的回收,而是把它转成僵尸对象;
  • 系统会修改对象的isa指针,令其指向特殊的僵尸类,从而使该对象变为僵尸对象。僵尸对象能够响应所有的选择器,响应方式:打印一条包含消息内容及其接受者的消息,然后终止应用程序。

开启“僵尸对象”的检测

在Xcode中设置Edit Scheme -> Diagnostics -> Zombie Objects

从汇编的调用顺序可以阿盖总结僵尸对象生成过程:

//1、获取到即将deallocted对象所属类(Class)
Class cls = object_getClass(self);

//2、获取类名
const char *clsName = class_getName(cls)

//3、生成僵尸对象类名
const char *zombieClsName = "_NSZombie_" + clsName;

//4、查看是否存在相同的僵尸对象类名,不存在则创建
Class zombieCls = objc_lookUpClass(zombieClsName);
if (!zombieCls) {
     //5、获取僵尸对象类 _NSZombie_
 Class baseZombieCls = objc_lookUpClass(“_NSZombie_");

     //6、创建 zombieClsName 类
 zombieCls = objc_duplicateClass(baseZombieCls, zombieClsName, 0);
}
//7、在对象内存未被释放的情况下销毁对象的成员变量及关联引用。
   objc_destructInstance(self);

//8、修改对象的 isa 指针,令其指向特殊的僵尸类
objc_setClass(self, zombieCls);
复制代码

那么Zombie Object是如何被触发的?

//1、获取对象class
Class cls = object_getClass(self);

//2、获取对象类名
const char *clsName = class_getName(cls);

//3、检测是否带有前缀_NSZombie_
if (string_has_prefix(clsName, "_NSZombie_")) {
//4、获取被野指针对象类名
const char *originalClsName = substring_from(clsName, 10);
 
//5、获取当前调用方法名
const char *selectorName = sel_getName(_cmd);
&emsp;&emsp;
//6、输出日志
NSLog(''*** - [%s %s]: message sent to deallocated instance %p", originalClsName, selectorName, self);

&emsp;//7、结束进程
&emsp;abort();
复制代码

详情参考

36. 不要使用retainCount
  • 对象的保留计数看似有用,实则不然,因为任何给定时间点上的“绝对保留计数”都无法反映对象生命期的全貌;
  • ARC下调用该方法会编译报错。
37. 理解“块”这一概念

块和函数类似,只不过是直接定义在另一个函数里,和定义它的那个函数共享同一范围内的东西。

block的内存管理:

  • 默认情况下block的内存是在栈中(不需要手动去管理block内存),它不会对所引用的对象进行任何操作;
  • 如果对block进行了copy操作, block的内存会搬到堆里面,它会对所引用的对象做一次retain操作。

为什么加上**__block**就可以修改外部的变量了?

真正的原因是这样的: 我们都知道:Block不允许修改外部变量的值,这里所说的外部变量的值,指的是栈中指针的内存地址。__block 所起到的作用就是只要观察到该变量被 block 所持有,就将“外部变量”在栈中的内存地址放到了堆中。进而在block内部也可以修改外部变量的值。

38. 为常用的块型创建typedef

以typedef重新定义块类型,可令变量用起来更加简单

39. 用handler块降低代码的分散程度
40. 用块引用其所属对象时不要出现保留环
41. 多用派发队列,少用同步锁

为啥@synchronized(self)效率低?

同步行为针对的对象是self。然而共用同一个锁的那些同步块,都必须按顺序执行,若是在self对象频繁加锁,那么程序可能要等另一段与此无关的代码执行完毕,才能继续执行当前代码。

手动实现atomic特性,通常会这么写:

- (NSString *)someString
{
    @synchronized(self) {
        return _someString;
    }
}

- (void)setSomeString:(NSString *)someString
{
    @synchronized(self) {
        _someString = someString;
    }
}
复制代码

这么写虽然能提供某种程度的“线程安全”,但却无法保证访问该对象时绝对的线程安全,在同一个线程上多次调用获取方法(getter),每次获取到的结果却未必相同。在两次访问操作之间,其他线程可能会写入新的属性值。

优化后:

- (NSString *)someString
{
    __block NSString *localSomeString;
    dispatch_sync(__syncQueue, ^{
        localSomeString = _someString;
    });
    return localSomeString;
}

- (void)setSomeString:(NSString *)someString
{
    dispatch_barrier_async(__syncQueue, ^{
        _someString = someString;
    });
}
复制代码

42. 多用GCD,少用performSelector

performSelector系列方法有局限性。如:具备延后执行功能的那些方法(performSelector: withObject: afterDelay:)都无法处理带有两个参数的选择器。而能够指定执行线程的那些方法(performSelector: onThread: withObject: waitUntilDone:)也不是特别通用,如果要用这些方法,就得把许多参数都打包到字典里,然后在受调用的方法里将其提取出来,这样会增加开销且可能出bug。

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

使用NSOperation及NSOperationQueue的好处如下:

  • 取消某个操作。在运行任务之前,可以在NSOperation对象上调用cancel方法,该方法会设置对象内的标志位,用以表名此任务不需要执行。
  • 指定操作间的依赖关系
  • 通过键值观察机制(KVO)监控NSOperation对象的属性。比如isCancelled(是否已经取消)、isFinished(是否已完成);
  • 指定操作的优先级。GCD的队列确实有优先级,不过那是针对整个队列来说的,而不是针对每个块来说的;
  • 重用NSOperation对象
44. 通过Dispatch Group机制,根据系统资源状况来执行任务
  • 使用场景:这个比较适合比如下载多张图片然后再合成一张图片,或者请求C需要依赖请求A和请求B的结果的情况;
  • 注意事项:调用dispatch_group_enterdispatch_group_leave须成对出现;
  • dispathc_apply所用的队列可以是并发队列,然而dispathc_apply会持续阻塞,知道所有任务都执行完毕。
45. 使用dispathc_once来执行只需要运行一次的线程安全代码

单例

46. 不要使用dispathc_get_current_queue
  1. dispathc_get_current_queue函数行为常常与开发者所预期的不同。
  1. dispathc_get_current_queue函数用于解决由不可重入的代码所引发的死锁,然而能用此函数解决的问题,通常也能改用“队列特定数据”来解决。
47. 熟悉系统框架
48. 多用块枚举,少用for循环

“块枚举法”本身就能通过GCD来并发执行遍历操作。

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

主要讲了下CF的一些底层函数,比如CFDictionary

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

相比NSDictionary,优势在于:自动删减功能,而且是“线程安全的”,它与字典不同,并不会拷贝键。

51. 精简initialize与load方法

load是在编译阶段执行,是方法地址执行,不走消息发送机制。首次使用某个类之前,系统会向其发送initialize消息; load方法:

执行顺序:父类 -> 子类 -> 分类。都执行

initialize方法:(执行顺序)

  1. 分类会覆盖本类。
  1. 父类 -> 子类
  2. 如果子类没有执行initialize 那么父类的initialize可能会执行多次。
52. 别忘了NSTimer会保留其目标对象

书中是用分类扩展,用“块”来实现的。也可以通过关联对象来实现。具体可参考利用RunTime解决由NSTimer导致的内存泄漏

转载于:https://juejin.im/post/5afa42be6fb9a07aad17a8bd

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值