本文章简括了《Effective Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法》书中的一些使用规范,具体使用方法可参考书中相应内容,或自行google。
熟悉Objective-C对象、消息及运行期
1)类的头文件中尽量少引入其它头文件
A)某个类头文件包含另一个类属性,用@class表明该属性是个类即可(向前声明)。
B)如果无法使用向前声明,如声明某个类遵循一项协议,尽量把协议移至分类或单独起文件。
2)多用字面语法
A)NSNumber *charNumber = @‘a’ 总比 NSNumber *charNumber = [NSNumber numberWithChar:@‘a’] 简洁的多
B)NSArray/ NSDictionary 也一样。取值可直接使用下标,且如果数为nil时会崩溃,容易排查。(如果使用arrayWith/dictionaryWith 遇到第一个nil值后面的值就会忽略,影响会更大)
3)多用类型常量,少用#define预处理指令
A)例如:static const NSTimeInterval kAnimationDuration = 0.3
规范:实现文件中用k开头,类之外可见用类名为前缀。
注意点:一定要用static 和 const 两个关键字声明。static表明仅在定义此变量的编译单元中可见。如果不加static,编译器会为它创建一个外部符号。此时若是另一个编译单元也声明了同样的变量,则会出错。
B)如果需要对外公开这个变量,可以这样使用:
头文件 :extern NSString *const MyTestNotification;
实现文件:NSString *const MyTestNotification = @“ThisMyTestNotification”
4)用枚举表示状态/选项/状态码
A)可以借助系统的宏(具备向后兼容能力):
NS_ENUM(NSUIntefer, MyState){**, **, **};
NS_OPTIONS(NSUInteger, MyState){** =1<<0, **=1<<1};
注意:用switch语句的枚举,不要使用default分支。此时编译器会警告有的状态未处理。
5)在对象内部尽量直接访问实例变量
A)在对象内部读取数据时,应该直接通过实例变量来读,而写入数据时,则应通过属性来写。
B)在初始化init和dealloc方法中,总是直接访问实例变量来读取数据。
6)在自有类中使用关联对象存放自定义数据
A)可以通过关联对象机制把两个对象连接起来。
B)只有在其它做法不可行的时候才使用关联对象,因为关联对象容易引起难查的bug
7)消息转发机制
8)方法交换技术method swizzling
A)一般来说,只有调试程序的时候才需要在运行期间修改方法实现,这种做法不宜滥用。
9)类对象
A)尽量使用类型信息查询方法(isKindOfClass)来确定对象类型,而不要直接比较类对象。因为某些对象可能实现了消息转发功能。
接口与API设计
1)使用前缀避免命名空间冲突
A)给类名加上适当的前缀,并且在所有代码中使用这一前缀。
B)若开发的程序中用到了第三方库,则应为其中的名称加上前缀。
2)提供全能的初始化方法
A)在类中提供一个全能初始化方法,并于文档里指明。其它初始化方法均应调用此方法。
B)如果全能初始化方法和超类不同,则需要覆写超类中的对应方法。
C)如果超类的初始化方法不适合用于子类,那么应该覆写这个超类方法,并在其中抛出异常。
3)尽量使用不可变对象
A)若属性仅可内部修改,则在分类中将readonly改成readwrite属性。
B)不要把可变的集合作为属性公开,而应该提供相关方法,以此修复对象中的可变集合属性。
4)为私有方法加前缀名
A)不要单用下划线作为前缀。苹果公司框架内部使用的就是下划线,避免继承框架的类时覆盖超类的方法。
5)OC的错误处理
A)只有发生可使整个应用程序崩溃的严重错误时,才应使用异常。
B)在错误不严重的情况下,可以指派代理来处理错误,也可以把错误信息放在NSError对象中,作为输出参数返回给调用者。
6)理解NSCopying协议
A)实现copyWithZone:方法, 系统方法以应用程序唯一的“默认区”为参数(以前的程序内存有多个分区)
B)[NSMutableArray copy] => NSArray; [NSArray mutableCopy] => NSMutableArray
C)如果自定义的对象分为可变和不可变版本,需要同时实现NSCopying和NSMutableCopying协议
D)复制对象时需要决定使用深拷贝还是浅拷贝,一般情况下应该尽量执行浅拷贝。
E)如果所写的对象需要深拷贝,可以考虑新增一个专门执行深拷贝的方法。
协议与分类
1)通过委托与数据源进行通信
A)若有必要,可实现含有位段的结构体,将委托对象是否能响应相关协议方法这一信息缓存至其中(如网络请求中通过代理方法传递数据进度)。
2)将类的实现代码分散到各个分类中,便于管理
A)根据回溯信息的分类名称,便于调试。
B)应该将“私有”方法归入名叫Private的分类,以隐藏实现细节。
3)向第三方类中添加分类时,应给其名称和其中 的方法名加上专用前缀。
A)为了避免覆盖,出现难以排查的bug。
4)分类中的属性
A)把封装数据所用的全部属性都定义在主接口里。
B)分类中不推荐定义属性,因为分类无法合成与属性相关的实例变量。即使可以使用关联对象来解决问题,也不推荐。
5)用分类隐藏实现细节
A)新增实例变量/只读扩展为读写/声明私有方法原型/遵守不为外部知道的协议
6)使用匿名对象
内存管理
1)ARC
A)以alloc/new/copy/mutableCopy开头的方法,其返回的对像归调用者所有,返回创建对象时不会执行[object release]。
B)CoreFOundation对象不归ARC管理,所以应适当使用CFRetain/CFRelease
2)dealloc用法
A)这个方法只用来释放其它对象的应用、取消KVO、通知,不要做其它事。
B)如果对象持有文件描述符等系统资源,则应该专门编写一个方法释放该资源。如和使用者约定,用完资源后必须调用close方法。
C)异步任务不要放在dealloc,正常状态下执行的方法也不要放在dealloc
D)对于开销较大的如套接字、文件描述符、大块内存等,应该单独实现另一个方法,当程序用完这些资源时就调用此方法。因为不能指望dealloc会在某个特定时期调用甚至不会调用,且在等到dealloc执行时,这些资源就会保留过长。
3)异常
A)捕获对象时要将try中创立的对象清理干净。
B)默认情况下,ARC不生成处理异常所需要的代码,样板代码多、影响性能等副作用。开启编译标志-fobjc-arc-exceptions后,可以生成这种代码。
4)保留环
A)A、B相互作为属性,其中之一应置weak。不建议使用unsafe_unretained,否则继续使用会崩溃。
5)自动释放池
A)自动释放池排布在栈中,对象收到autorelease消息后,系统将其放入最顶端。
B)合理运用释放池可以降低应用程序的内存峰值。
C)@autoreleasepool这种新式写法能创建出更为轻便的自动释放池。
6)僵尸对象
A)系统回收对象时,可以不将其真的收回,而是转化为僵尸对象。通过环境变量NSZombieEnabled可开启此功能。
B)系统会修改对象的isa指针,令其指向僵尸类(从而变为了僵尸对象)。僵尸类能够响应所有选择子,方式为打印一条包含消息内容及其接受者的消息,并终止程序。
块与大中枢派发
1)块
A)块也是一个对象,块可以分配在栈、堆、全局的。栈上的离开创建区域之后会被销毁。
2)用handler块降低代码分散程度
A)创建对象时,可以使用内联的handler块把相关业务逻辑一起声明。特别是有多个实例需要监控时,用handler可以直接将块与相关对象放在一起。
B)设计API时如果用到了handler块,那么可以增加一个参数,使调用者可通过此参数来决定应该把块安排在哪个队列上执行。
3)多用派发队列,少用同步锁
A)派发队列可用来表述同步语意,这种做法比@synchronized块或NSLOck块对象更简单。例如:
- (NSString *)someString{
__block NSString *tempStr;
dispatch_syns(_syncQueue, ^{ tempStr = _someString});
return tempStr;
}
B)将同步和异步派发结合起来,可实现与普通加锁机制一样的同步行为,而这么做却不会阻塞执行异步派发的线程。
C)使用同步队列及栅栏块,可以令同步行为更加高效
void dispatch_barrier_async(queue, block); void dispatch_barrier_sync(queue, block);
备注:barrier块必须单独执行,不能与其他block并行。这只对并发队列有意义。并发队列如果发现接下来要执行的block是个barrier block,那么就一直要等到当前所有并发的block都执行完毕,才会单独执行这个barrier block代码块,等到这个barrier block执行完毕,再继续正常处理其他并发block。
4)多用GCD,少用performSelector系列方法
A) 在内存管理方面容易疏失。无法确定将要执行的选择子具体是什么,所以ARC无法插入适当的内存管理方法。
B)能处理的选择子过于局限,返回值类型以及参数都收到限制。
C)如果想把任务放在另一个线程上,最好不要用 performSelector,应该封装到块里。
6)适当选择GCD和操作队列的使用时机
A)操作队列实现了高层的OC API,能实现一些复杂的功能和操作,如
取消操作、指定操作间的依赖关系、通过键值观测监控NSOperation对象的属性、指定优先级、重用NSOperation对象等。GCD主要针对整个队列间,操作队列主要针对队列中的每个块。
7)通过dispatch Group机制,根据系统资源状况来执行任务
A)一系列任务可归入一个dispatch group中,开发者可在这组任务执行完毕获得通知。
相关API:
dispatch_group_create(); // 创建组
dispatch_group_async(group, queue, block); // 在组中异步执行队列中的block
dispatch_group_enter(group); // 加入组
dispatch_group_leave(group); //和enter成对出现,否则改group的任务永远执行不完
dispatch_group_wait(group, timeout); // 等待组。DISPATCH_TIME_FOREVER 表示永久等待
dispatch_group_notify(group, queue, block); // 组中任务执行完毕,会执行block
dispatch_apply(count, queue, block(size)); // 重复执行block共count次,是一个持续阻塞函数,直到任务执行完(可在异步dispatch_async中调用该方法)
8)使用dispatch_once来执行只需要运行一次的线程安全代码
dispatch_once(token, block);
A)注意标记token应该声明在static或global中。
9)不要使用dispatch_get_current_queue
A)该函数的行为常常与开发者所预期不同,因此iOS6.0已废弃,只能做调试使用。
B)该函数用于解决由不可重入的代码所引发的死锁。可以用标记队列dispatch_queue_set_specific、递归锁NSRecursiveLock来解决。
熟悉系统框架
1)多用块枚举,少用for循环
A)遍历collection有四种方式。最基本的办法是for循环,其次是NSEnumerator遍历法及快速遍历法,最新最先进的是“块枚举法”。
B)“块枚举”本身通过GCD来并发执行遍历操作。
2)对自定义内存管理语义的collection使用无缝桥接
A)在CoreFoundation层面创建collection时,可以指定许多回调函数,这些函数表示此collection应该如何处理其函数。运用无缝桥接,可实现特殊内存管理语义。例如可以使OC中的字典实现键值均保留。
3)构建缓存时选用NSCache,而不是NSDictionary
A)好处之一是系统资源将要耗尽时,NSCache可以自动删减缓存。且线程安全,保留键值。
B)可以给NSCache对象设置上线,用以限制缓存中的对象个数以及总成本。但这些限制并不可靠,NSCache只是”参考”。
C)将NSPurgeableData(NSMutableData的子类)与NSCache搭配使用,可实现自动清除数据的功能。即当NSPurgeableData对象所占内存为系统丢弃时,该对象自身也可以从缓存中移除。
D)只有那种“重新计算起来很费事”的数据,才值得放入缓存
4)精简initialize与load的实现代码
A)运行期的类和分类,必定会调用+load方法,且只调用一次。
B)给定的程序库,无法判断各个类的载入顺序,因此,在+load方法中使用其它类是不安全的,因为不确定这个其它类是否已经载入了。程序执行+load方法时都会阻塞。
C)+load方法不像普通方法,它不遵循继承规则。如果某个类未实现+load方法,那么它的超类不管是否实现了该方法,都不会被调用。类的+load会比分类的+load先执行。
D)+load的真正用途在于调试程序。时下编写OC代码,不需要使用它。
E)+initialize 属于懒加载,首次使用该类之前调用,且只调用一次。在+initialize中可以放心调用任意类的任意方法。如果某个类未实现它,但其超类实现了,那么就会运行超类的实现代码。
F )无法在编译期设定的全局常量,可以放在initialize方法里初始化。如全局可变数组,单例使用前需要执行某些操作。(PS:使用时,用if(self == [MyClass class])做一下判断,否则可能会执行多变)
5)别忘了 NSTimer会保留其目标对象
A)可以扩充NSTimer的功能,用”块”打破保留环。
网友评论