Objective-C Effective 技巧

时间:2021-09-04 16:31:07

1.除非有必要,否则不要引用头文件,一般来说应该利用@class使用前向声明,并在实现中引用头文件;如果实在无法使用,比如要声明某个类遵循一项协议,这种情况下,尽量把这条声明移到分类中,如果不行的话,就把协议单独放到一个头文件中,然后再引入

2.应该使用字面量语法来创建字符串、数值、数组、字典,这样做更加简明扼要;应该通过下标操作来访问数组或字典中的值;需要注意的是,采用字面量语法,若值有nil,会抛出异常。

3.不用使用预处理指令定义常量;在实现文件中用static const来定义内部常量(以k开通),这样就不会出现在全局符号表中;在头文件中使用extern来声明全局常量,并在相关实现文件中定义其值,他会出现在全局符号表,所以其名称应加以区别,通常用与之相关的类名做前缀

4.用NS_ENUM与NS_OPTIONS宏来定义枚举类型,并指明其底层数据类型,确保枚举类型底层数据类型的可控性; 其语法为:
typedef NS_ENUM(NSUInteger,xxx){};
typedef NS_OPTIONS(NSInteger,YYY){};

5.可以用@property语法来定义对象中所封装的数据,并通过atomic/nonatomic/readwrite/readonly/assign/unsafe_unretained/strong/weak/copy等特性来指定存储数据所需的正确定义,还可以在声明@property的时候利用getter=<name>和setter=<name>来指定其相应的方法名;并且在设置属性所对应的实例变量时,一定要遵从该属性所声明的语义

6.在对象内部读取数据时,应该直接通过实例变量来读,而写入数据时,则应通过属性来写;在初始化及dealloc方法中,应该通过实例变量来读取数据,防止子类因为对属性方法的重载导致的问题;如果使用惰性初始化技术的数据,必须通过属性来读取数据

7.若要想检测对象的等同性,请提供isEqual与hash方法,对于内置对象,已经提供了isEqualToArray/isEqualToDictionary/isEqualToString方法;并且要清楚hash相等只是一个必要而非充分的条件;不要盲目地逐个检测每条属性,而是应该按照具体需求来制定检测方案

8.所谓的类族就是把实现细节隐藏在一套简单的公共接口里面,基类实现类方法;系统框架中经常使用类族,所在要小心的是你所想要的类也许是该类的子类,故就不能用isMemberOfClass或==来做判断,而应该采用isKindOfClass来做判断

9.所谓关联对象就是把对象当成NSDictionary来存储key-value,类似给对象设置动态属性,用方法:objc_setAssociatedObject /objc_getAssociatedObject /objc_removeAssociatedObject全句方法来完成设置/获取/删除;他有相应的关联类型:
OBJC_ASSOCIATION_ASSIGN
OBJC_ASSOCIATION_RETAIN_NONATOMIC
OBJC_ASSOCIATION_COPY_NONATOMIC
OBJC_ASSOCIATION_RETAIN
OBJC_ASSOCIATION_COPY
想要注意的是,只有在其他做法不可行时才应选用关联对象,因为这种做法通常会引入难于查找的BUG,一般都可以继承实现子类的方法来解决

10.利用objc_msgSend/objc_msgSend_stret/objc_msgSend_fpret/objc_msgSendSuper来实现动态消息派发系统,当给某对象发送消息时,该系统会查出对应的方法,在该对象上调用其C代码

11.消息转发流程包含三个部分:resolveInstanceMethod->forwardingTargetForSelector->forwardInvocation,对应第一步可以实现对@dynamic属性的设置,第二步可以实现类似多继承的方式,第三步会封装NSInvocation对象,进行完整的消息派发流程;故步骤越往后,处理消息的代价越大,如果第一步处理完,就可以缓存,后续访问直接就可以返回而不需要启动消息转发流程,第二步可以以组合的方式来实现多继承,第三步由于要创建NSInvocation对象,消耗最大;需要用到辅助函数NSStringFromSelector来将方法名转成字符串,另外,利用class_addMethod来动态添加成员方法

12.利用方法调配技术,并结合class_getInstaneMethod和method_exchangeImplementations函数可以向类中新增或替换选择子所对应的方法实现,或者使用另一份实现来替换原有的;但是,只有调试程序的时候才需要在运行期修改方法实现,这种做法不宜滥用;需要注意的事,要包含<objc/runtime.h>头文件

13.每个实例都有一个指向Class对象的指针,而Class对象又有一个指向MetaClass的指针,并且每个对象都有相应的super来指向自己相应的基类,这样就构成了类的继承体系;如果对象类型无法在编译器确定,那么就应该使用类型信息查询方法来探知,而不要直接比较类对象,因为某些对象可能实现了消息转发功能

14.Apple宣称其保留使用所有"两字母前缀"的权利,故应选择与你的公司、应用程序或二者皆有关联之名作为类名的前缀,并在所有的代码中均使用这一前缀;若自己所开发的程序中用到了第三方库,则应为其中的名称加上前缀

15.在类中提供一个全能初始化方法,其他初始化方法均应调用此方法;若全能初始化方法与超类不同,则覆写超类中对应的方法;如果超类的初始化方法不使用子类,那么覆写这个超类方法,并在其中抛出异常(类方法NSException exceptionWithName)

16.如果想要@%打印出自定义的对象,需要实现其description方法返回一个有意义的字符串;若想在调试时利用po命令打印出更详尽的对象描述信息,则应实现debugDescription方法;需要注意的是,可以把待打印的信息放到字典里面(字面量语法),然后将字典对象的description方法所输出的内容包含在字符串里并返回,实现精简的信息输出方式

17.尽量创建不可变的对象,若某属性仅可于对象内部修改,则在分类中将其由readonly属性扩展为readwrite属性;并且不要把可变的collection作为属性公开,而应该提供相关的方法,以此修改对象中的可变collection

18.给私有方法的名称加上前缀,这样可以很容易地将其同公共方法区分开;并且不要单用一个下划线做私有方法的前缀,因为这种做法是预留给Apple公司使用的;需要注意的是,与公共方法不同,私有方法不出现在接口定义中,一般只在实现的时候声明,因为Objective-C中没有那种约束方式调用的机制用以限定谁能调用此方法、能在哪个对象上调用此方法以及何时能调用此方法

19.Objective-C只在极其罕见的情况下抛出异常,抛出以后就无须考虑恢复问题,而且应用程序此时也应该退出,也就是说,不同再编写复杂的"异常安全"代码了;那么对于不是非致命的错误,可以令方法返回nil/0,或使用NSError,以表明其中有错误发生;使用后者更加灵活,该对象封装了三条信息:Error domain(错误范围,类型为字符串)/Error code(错误吗,类型为整数)/User info(用户信息,类型为字典),可以把他以Delegate或输出参数的方式返回给使用者

20.要实现NSCopying协议,就是要实现方法 {-(id) copyWithZone:(NSZone*)zone};要实现NSMutableCopying协议,就是要实现方法 {-(id) mutableCopyWithZone:(NSZone*)zone};在函数中,可以这样写:XXX* copy = [[[self class] allocWithZone:zone] initXXX:YYY];需要注意的是,Foundation框架中的所有colletion类在默认情况下都执行浅拷贝,所有我们会遵照系统框架所使用的模式,在自定义的类中以浅拷贝的方式实现copyWithZone,如果真的要深拷贝,则另外单独起一个新方法;对了在Objective-C中,也是使用->语法,这样他就是直接访问变量,不再走属性那一套了

21.所谓的委托对象也就是C++中的Callback,在这里把支持的Callback定义成协议,在协议中把可能需要处理的事件定义成方法;当某对象需要从另外一个对象中获取数据时,可是使用委托模式,在这种情况下也称为数据源协议;若有必要,可实现含有位段的结构体,将委托对象是否能响应相关协议这一信息缓存其中;一般这样声明:
@property (nonatomic, weak) id<XXXProtocalDelegate> delegate;
可以将协议声明为optional的,比如:
@protocal XXXProtocalDelegate
@optional
-(void)AAFunction....
-(void)BBFunction....
@end
使用的时候,要判断是否有这样的方法,比如:
if([_delegate respondsToSelector: @selector(AAFunction....)])
{
  [_delegate AAFunction....];
}

22.使用分类机制吧类的实现代码划分成易于管理的小块;还应该吧私有的方法归入名叫Private的分类中,以隐藏实现细节;并且分类的名称和方法名都应该加上专用的前缀

23.正确的做法是把所有的属性都定义在主接口里,这里是唯一能够定义实例变量的地方,而属性只是定义实例变量及相关存取方法所用的语法糖,所以也应遵循同实例变量一样的规则;故分类则应该将其理解为一种手段,目标在于扩展类的功能,而非封装数据;故在class-continuation分类之外的其他分类中,可以定义存取方法,但尽量不要定义属性

24.class-continuation分类没有名字,和普通分类不同的是,它必须定义在其接续的那个类的实现文件里,这是唯一能声明实例变量的分类,而且此分类没有特定的实现文件,其中的方法都应该定义在类的主实现文件里;其中,实例变量定义在实现块里,与class-continuation分类等效,只是看个人喜好罢了;但是以下几种情况适用后者更
合适:
1)某属性在主接口为只读,而类内部要修改该属性,那就可以采用class-continuation分类将其扩展为可读写
2)若想使类遵循的协议不为人所知,可采用class-continuation分类,并在其中进行声明
3)把私有方法的原型声明放到class-continuation分类中
4)包装C++等异构系统,将其对象的声明和操作放到class-continuation分类中

25.协议可在某种程度上提供匿名类型,具体的对象类型可以淡化成遵从某协议的id类型,协议里规定了对象所应实现的方法;在这里所谓的匿名对象是用来隐藏类型名称(或类名)的,即如果具体类型不重要,重要的是对象能响应(定义在协议里)特定方法,那么就可以使用匿名对象来表示

26.autorelease此方法可以保证对象在跨越"方法调用边界"后一定存活,释放操作会在清空最外层的自动释放池时执行,除非你有自己的自动释放池,否则这个时机指的就是当前线程的下一次事件循环;

27.Clang编译器项目带有一个静态分析器,探知程序中引用计数出问题的地方;ARC就是利用这个机制来为我们自动添加保留与释放操作,故在ARC下调用retain/release/autorelease/dealloc是非法的;使用ARC无须担心内存管理问题,也可省去类中的许多样板代码,其内存管理语义是通过方法名来体现:alloc/new/copy/mutableCopy;需要注意的是CoreFunction对象不归ARC管理,开发者必须适时调用CFRetain/CFRelease

28.在dealloc方法里,应该做的事情就是释放指向其他对象的引用,并取消原来订阅的KVO或NSNotificationCenter等通知,不要做其他事情;如果对象持有文件描述符等系统资源,那么应该专门编写一个方法来释放此种资源,这样的类要和其使用者约定,用完资源后必须调用close方法;执行异步任务的方法不应在dealloc里调用,只能在正常状态下执行的哪些方法也不应在dealloc里调用,因为此时对象正处于回收的状态

29.捕获异常时,一定要注意将try内创立的对象清理干净,可以使用@try/@catch/@finally语法,在@finally中完成回收操作;在默认情况下,ARC不生成安全处理异常所需的清理代码,开启-fobjc-arc-exceptions可以生成,尽管这不会导致应用程序变大,但是会降低运行效率

30.避免保留环的方法是使用弱引用,有三种弱引用类型assign/unsafe_unretained/weak,assign通常只用于整体类型(int/float/结构体等);unsafe_unretained则多用于对象类型,他指向的对象被回收了就野了;weak是比较安全的,他会加到autoreleasepool中,他指向的对象被回收后自己会被置nil,避免了程序的崩溃,不过自动清空(autonilling)是随着ARC而引入的新特性,由运行期系统来实现

31.自动释放池@autoreleasepool{},当清空(drain)时,系统会向其中的对象发送release消息;在Cocoa和Cocoa Touch环境下,系统会自动一些线程,比如主线程或GCD机制中的线程,他们默认都有自动释放池,每次执行事件循环时,就会将其清空,故我们只需在main中加入自动释放池就可以;需要注意的是自动释放池排布在栈中,对象收到autorelease消息后,系统将其放入最顶端的池里;利用自动释放池的嵌套可以借助控制应用程序的内存峰值,而NSAutoreleaePool对象更加重量级,通常用来创建那种偶尔需要清空的池,故@autoreleaepool这种新式写法能创建出更为轻便的自动释放池

32.为了调试的目的,可以让系统在回收对象时,不降其真的回收,而是把它转化成僵尸对象,可以Edit Scheme->Run XXX->Diagnostics->Enable ZombieObjects来开启这个功能;系统会修改对象的isa指针,令其指向特殊的僵尸类,从而使该对象变成僵尸对象,该对象能响应所有的选择子,为:打印一条包含消息内容及其接受者的消息,然后终止应用程序

33.块类型的语法结构为:return_type (^block_name)(parameters),实际上就是其他语言中的闭包;默认情况下,块所捕获的变量是不可以在块里修改的,除非加上__block修饰符,如果块定义在类的实例方法中,那么除了可以访问类的所有实例变量之外,还可以使用self变量,而无须加__block;不过需要注意的是,self也是一个对象,因而块在捕获它时也会将其保留,如果self所指代的那个对象同时也保留了块就会出现保留环;定义块时,所占的内存是分配在栈上的,如果要让其分配到堆上享用ARC,就使用其copy方法,前者叫栈块,后者叫堆块,还有全局块,他不会捕捉任何状态,运行时也无须有状态来参与,故可以在全局内存中声明

34.以typedef重新定义块类型,可令块变量用起来更加简单;不妨为同一个块签名定义多个类型别名,如果要重构的代码使用了块类型的某个别名,那么只需要修改相应typedef中的块签名即可,无须改动其他的typedef

35.在创建对象时,可以使用内联的handler块将相关业务逻辑一并声明;在有多个实例需要监控时,如果采用委托模式,那么经常需要根据传入的对象来切换,若改用handler块来实现,则可直接将块与相关对象放在一起;需要注意的是,在设计API时,如果用到了handler块,那么可以增加一个参数,使调用者可通过此参数来决定应该把块安排在哪个队列上执行

36.如果块所捕获的对象直接或间接地保留了块本身,那么就得小心保留环问题了;一定要找个适当的时机解除保留环,即在调用handler后就将属性设置为nil,而不能把责任推给API的调用者

37.在GCD出现之前,有两者办法来实现同步机制,第一种办法采用@synchronized(self){}同步块,另一种就是采用NSLock对象及相应的lock/unlock方法;有种简单而高效的办法可以代替同步块或锁对象,那就是"串行同步队列",将读取操作及写入操作都安排在同一队列里,即可保证数据同步:
1)创建和获取:dispatch_queue_create/dispatch_get_global_queue/dispatch_get_main_queue
2)同步和异步执行:dispatch_sync/dispatch_async
3)同步和异步栅栏:dispatch_barrier_sync/dispatch_barrier_async
4)延迟执行:dispatch_after
5)执行一次:dispatch_once
6)执行指定次数:dispatch_apply

38.performSelector系统方法在内存管理方面容易有遗漏,他无法确定将要执行的选择子具体是什么,因而ARC编译器也就无法插入适当的内存管理方法,同时他所能处理的选择子太过局限了,选择子的返回值类型及发送给方法的参数个数都受到了现在;如果想把任务放在另一个线程上执行,那么最好不要用performSelector系列方法,而是应该把任务封装在块里面,然后调用大中枢派发机制的相关方法来实现

39.在解决多线程与任务管理问题时,派发队列并非唯一方案;操作队列提供了一套高层的Objective-C API,能实现纯GCD所具备的大部分功能,而且还能完成一些更为复杂的操作,比如:取消某个操作/指定操作间的依赖关系/指定操作的优先级/监控NSOperation对象的属性,若改用GCD实现,则需另外编写代码

40.一系列任务可归入一个dispatch group中,开发者可以在这组任务执行完毕时获得通知;通过dispatch group,可以在并发式派发队列里同时执行多项任务,此时GCD会根据系统资源状况来调度这些并发执行的任务,大大减少了开发者的编码量:
1)创建:dispatch_group_create
2)执行:dispatch_group_async
3)阻塞等待完成:dispatch_group_wait
4)异步回调完成:dispatch_group_notify

41.经常需要编写"只需执行一次的线程安全代码",通过GCD所提供的dispatch_once函数,就可以很容易实现此功能;标记应该声明为static或global,这样,在把只需执行一次的块传给dispatch_once函数时,传进去的标记也是相同的

42.dispatch_get_current_queue行为常常与预期不一致(由于派发队列是按层级来组织的,所以无法用单个队列对象来描述当前队列这个概念),此函数已经废弃;通常改用队列特定数据来解决死锁问题:dispatch_set_target_queue/dispatch_queue_set_specific/dispatch_queue_get_specific

43.有个与Foundation相伴的框架,叫CoreFoundation,虽然从技术上讲,CoreFoundation框架不是Objective-C框架,但它却是编写Objective-C应用程序时所应熟悉的重要框架,Foundation框架中的许多功能,都可以在此框架中找到对应的C语言API;CoreFoundation与Foundation不仅名字相似,而且还可以进行无缝桥接,比如NSString与CFString对象就互相转换;很多常见任务都能用框架来做,例如音频和视频处理、网络通信、数据管理等

44.在遍历collection的时候,除了可以利用for循环,还可以使用NSEnumerator来进行,首先从collection中根据objectEnumerator、keyEnumerator或reverseObjectEnumerator获得该对象,并结合while循环调用nextObject来进行遍历;还可以采用for..in语法;特别地还有基于块遍历方式,调用函数enumerateObjectsUsingBlock传入基于三个参数的块(所针对的对象、所针对的下标、指向代表终止的布尔值的指针)来遍历NSArray,调用函数enumerateKeysAndObjectsUsingBlock传入基于三个参数的块(所针对的key、所针对的value、指向代表终止的布尔值的指针)来遍历NSDictionary,调用函数enumerateObjectsUsingBlock传入基于两个参数的块(所针对的对象、指向代表终止的布尔值的指针)来遍历NSSet;注意的是,上述的两种enumerate函数,都有带options的版本:enumerateObjectsWithOptions/enumerateKeysAndObjectsWithOptions,其中第一个参数是NSEnumerationOptions类型的enum值,用以表明遍历方式

45.由于Foundation与CoreFoundation之间可以无缝桥接,故可以在CoreFoundation层面创建collection,指定许多回调函数,这些函数表示此collection应如何处理其元素。然后利用无缝桥接将其转换成具备特殊内存管理语义的Objective-C collection

46.在IOS中使用缓存,NSCache胜过NSDictionary,当系统资源耗尽时,他可以自动删减缓存;并且他不会拷贝键,而是会保留他;另外NSCache是线程安全的;有个类叫NSPurgeableData,他是NSMutableData的子类,并且实现了NSDiscardalbeContent协议,如果与NSCache配合使用会很强大,可以利用函数beginContentAccess/endContentAccess来操作data数据,在之间的操作代表现在还不应丢弃自己所占据的内存,如果将其加入到NSCache中,当该对象为系统所丢弃时,也会自动从缓存中移除

47.在加载阶段,如果类实现了load方法,那么系统会调用它,分类里也可以定义该方法,并且类的load方法要比分类的先调用,与其他方法不同,load方法不参与覆写机制,通常用于Debug某个类是否加载;首次使用某个类之前,系统会向其发送initialize消息,由于此方法遵从普通的覆写规则,所有通常应该在里面判断当前要初始化的是哪个类,通常用来设置内部数据,不应该在其中调用其他方法,即便是本类自己的方法,也最好别调用;load与initialize都应该实现得精简一些,这有助于保持应用程序的响应能力,也能减少引入依赖环的几率;需要注意的是,无法在编译期设定的全局变量(纯Objective-C对象),可以放在initialize方法里初始化

48.创建计时器的语法如下:
+(NSTimer*) scheduledTimerWithTimerInterval:(NSTimeInterval)seconds
target:(id)target
selector:(SEL)selector
userinfo:(id)userinfo
repeats:(BOOL)repeats
利用其invalidate方法可让计时器失效,如果是重复的,必须自己调用invalidate方法,才能让其停止;由于计时器会保留其目标对象,所有反复执行任务通常会导致应用程序出现依赖环,他可能是直接发生的,也可能是间接发生的,解决可以通过__weak弱引用和块来解决

49.Objective-C中选择符有两个含义:一种是用在代码中向对象发送消息时,代表了一个方法名;另一种是源代码被编译时,选择器会指向唯一标识以替代方法名,被编译后的选择器类型为SEL,当然了,也可以直接使用:
SEL setWidthHeight;
setWidthHeight = @selector(setWidth:height:);
但是在某些情况下,需要在运行时将一个字符串转化为一个选择器:
setWidthHeight = NSSelectorFromString(aBuffer);
反之也是可以行的:
NSString* method = NSStringFromSelector(setWidthHeight);

50.NSObject根类和它采纳的NSObject协议以及其他的"根"协议一起,为所有不作为代理对象的Cocoa对象指定了如下的接口和行为特质:
1)分配、初始化、复制:alloc/allocWithZone/init/initialize/load/new/copy/copyWithZone
2)对象的保持和清理:retain/release/autorelease/retainCount/dealloc
3)内省和比较:superclass/class/isKindOfClass/isMemberOfClass/isSubclassOfClass/respondsToSelector/instancesRespondToSelector/
conformsToProtocal/isEqual/description
4)编解码:encodeWithCoder/initWithCoder/classForCoder/replacementObjectForCoder/awakeAfterUsingCoder
5)消息转发:fowardInvocation
6)消息派发:performSelector

51.可以在@implementation中使用@syncthesize和@dynamic指令来触发特定编译器动作,不过这两个指令对于@property声明都不是必需的;可以使用函数class_copyPropetyList和protocal_copyPropertyList来将获得类或者协议类中的属性列表;可以通过下面的代码得到一个类中所有的属性:
id LenderClass = objc_getClass("Lender");
unsigned int outCount,i;
objc_property_t* properties = class_copyPropertyList(LenderClass, &outCount);
for(i=0; i<outCount; i++)
{
objc_property_t property = properties[i];
fprintf(stdout, "%s, %s\n", property_getName(property), property_getAttributes(property));
}

52.避免动态绑定的唯一办法就是取得方法的地址,并且直接像调用函数那也使用它。当一个方法会被连续调用很多次,而且希望节省每次
调用方法都要发送消息的开销时,使用方法地址来调用方法就显得很有效;利用NSObject类中methodForSelector方法,可以获得一个指向
方法实现的指针,并可以使用该指针直接调用方法实现,methodForSelector返回的指针和赋值的变量类型必须完全一致,第一个参数为接受
消息的对象,第二个参数为选择子,这两个参数在方法中是隐藏参数,但使用时必须显示地给出;需要注意的是对象本身用self,方法本身
可以用_cmd