一篇文章拿下《Effective Objective-C 2.0编写高质量iOS与OS X代码的52个有效方法》
Posted 程序员大咖
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一篇文章拿下《Effective Objective-C 2.0编写高质量iOS与OS X代码的52个有效方法》相关的知识,希望对你有一定的参考价值。
关键时刻,第一时间送达!
最近在重温这本OC经典之作《Effective Objective-C 2.0编写高质量ios与OS X代码的52个有效方法》,这篇文章算是重温之后的产物吧,读完这篇文章你将快速读完这本书,由于个人能力有限,难免有一些遗漏或者错误,请各位看官不吝赐教!谢谢!同时如果有任何问题也可以在下方留言,欢迎一起交流进步!另外由于篇幅原因,书中一些基础知识的介绍文中就省略掉了。
目录
第一条:了解Objective-C语言的起源
Objective-C从Smalltalk语言是从Smalltalk语言演化而来,
Smalltalk是消息语言的鼻祖。
Objective-C是C语言的超集,在C语言基础上添加了面向对象等特性,可能一开始接触时你会觉得语法有点奇怪,那是因为Objective-C使用了动态绑定的消息结构,而Java,C++等等语言使用的是函数调用。
消息结构与函数调用的关键区别在于:函数调用的语言,在编译阶段由编译器生成一些虚方法表,在运行时从这个表找到所要执行的方法去执行。而使用了动态绑定的消息结构在运行时接到一条消息,接下来要执行什么代码是运行期决定的,而不是编译器。
第二条: 在类的文件中尽量少引用其他头文件
如果需要引用一个类文件时,只是需要使用类名,不需要知道其中细节,可以用@class xx.h,这样做的好处会减少一定的编译时间。如果是用的#import全部导入的话,会出现a.h import了b.h,当c.h 又import a.h时,把b.h也都导入了,如果只是用到类名,真的比较浪费,也不够优雅
有时候无法使用@class向前声明,比如某个类要遵循一项协议,这个协议在另外一个类中声明的,可以将协议这部分单独放在一个头文件,或者放在分类当中,以降低引用成本。
第三条:多用字面量语法,少用与之等价的方法
1.多使用字面量语法来创建字符串,数组,字典等。
传统创建数组方法:
NSArray *languages = [NSArray arrayWithObjects:@"php", @"Objective-C", someObject, @"Swift", @"Python", nil];
NSString *Swift = [languages objectAtIndex:2];
NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:@"key", @"value", nil];
NSString *value = [languages objectForKey:@"key"];
字面量:
NSArray *languages = @[@"PHP", @"Objective-C", someObject, @"Swift", @"Python"];
NSString *Swift = languages[2];
NSDictionary *dict = @{@"key" : @"value"};
NSString *value = languages[@"key"];
这样做的好处:使代码更简洁,易读,也会避免nil问题。比如languages数据中 someObject 如果为nil时,字面量语法就会抛出异常,而使用传统方法创建的languages数组值确是@[@"PHP", @"Objective-C"];因为字面量语法其实是一种语法糖,效果是先创建了一个数组,然后再把括号中的对象都加到数组中来。
不过字面量语法有一个小缺点就是创建的数组,字符串等等对象都是不可变的,如果想要可变的对象需要自己多执行一步mutableCopy,例如
NSMutableArray *languages = [@[@"PHP", @"Objective-C", @"Swift", @"Python"] mutableCopy];
第六条:理解“属性”这一概念
这一条讲的是属性的基本概念,以及属性的各种修饰符,这些就不多啰嗦了,这里强调一下:
定义对外开放的属性时候尽量做到暴露权限最小化,不希望被修改的属性要加上readonly。
atomic 并不能保证多线程安全,例如一个线程连续多次读取某个属性的值,而同时还有别的线程在修改这个属性值得时候,也还是一样会读到不同的值。atomic 的原理只是在 setter and getter 方法中加了一个@synchronized(self),所以iOS开发中属性都要声明为nonatomic,因为atomic严重影响了性能,但是在Mac OSX上开发却通常不存在这个性能问题
说一下下面的哪个属性声明有问题
@property (nonatomic, strong) NSArray *arrayOfStrong;
@property (nonatomic, copy) NSArray *arrayOfCopy;
@property (nonatomic, strong) NSMutableArray *mutableArrayOfStrong;
@property (nonatomic, copy) NSMutableArray *mutableArrayOfCopy;
具体运行示例点击查看
答案是正常应该这样声明
@property (nonatomic, copy) NSArray *arrayOfCopy;
@property (nonatomic, strong) NSMutableArray *mutableArrayOfStrong;
第七条:在对象内部尽量直接访问实例变量
在类内读取属性的数据时,应该通过直接实例变量来读,这样不经过Objecit-C的方法派发,编译器编译后的代码结果是直接访问存实例变量的那块内存中的值,而不会生成走方法派发的代码,这样的速度会更快。
给属性写入数据时,应该通过属性的方式来写入,这样会调用setter 方法。但是在某种情况下初始化方法以及dealloc方法中,总是应该直接通过实例变量来读写数据,这样做是为了避免子类复写了setter方法造成的异常。
使用了懒加载的属性,应该一直保持用属性的方式来读取写入数据。
第八条:理解“对象等同性”这一概念
思考下面输出什么?
NSString *aString = @"iphone 8";
NSString *bString = [NSString stringWithFormat:@"iphone %i", 8];
NSLog(@"%d", [aString isEqual:bString]);
NSLog(@"%d", [aString isEqualToString:bString]);
NSLog(@"%d", aString == bString);
答案是110
==操作符只是比较了两个指针,而不是指针所指的对象
第九条:以“类族模式”隐藏实现细节
为什么下面这段if 永远为false
id maybeAnArray = @[];
if ([maybeAnArray class] == [NSArray class]) {
//Code will never be executed
}
因为[maybeAnArray class] 的返回永远不会是NSArray,NSArray是一个类族,返回的值一直都是NSArray的实体子类。大部分collection类都是某个类族中的’抽象基类’
所以上面的if想要有机会执行的话要改成
id maybeAnArray = @[];
if ([maybeAnArray isKindOfClass [NSArray class]) {
//Code probably be executed
}
这样判断的意思是,maybeAnArray这个对象是否是NSArray类族中的一员
** 使用类族的好处:可以把实现细节隐藏再一套简单的公共接口后面 **
第十条:在既有类中使用关联对象存放自定义数据
这条讲的是objc_setAssociatedObject和objc_getAssociatedObject,如何使用在这里就不多说了。值得强调的一点是,用关联对象可能会引入难于查找的bug,毕竟是在runtime阶段,所以可能要看情况谨慎选择
第十一条:理解“objc_msgSend”的作用
之前在了解Objective-C语言的起源有提到过,Objective-C是用的消息结构。这条就是让你理解一下怎么传递的消息。
在Objective-C中,如果向某个对象传递消息,那就会在运行时使用动态绑定(dynamic binding)机制来决定需要调用的方法。但是到了底层具体实现,却是普通的C语言函数实现的。这个实现的函数就是objc_msgSend,该函数定义如下:
void objc_msgSend(id self, SEL cmd, ...)
这是一个参数个数可变的函数,第一参数代表接收者,第二个参数代表选择子(OC函数名),后续的参数就是消息(OC函数调用)中的那些参数
举例来说:
id return = [git commit:parameter];
上面的Objective-C方法在运行时会转换成如下函数:
id return = objc_msgSend(git, @selector(commit), parameter);
objc_msgSend函数会在接收者所属的类中搜寻其方法列表,如果能找到这个跟选择子名称相同的方法,就跳转到其实现代码,往下执行。若是当前类没找到,那就沿着继承体系继续向上查找,等找到合适方法之后再跳转 ,如果最终还是找不到,那就进入消息转发的流程去进行处理了。
说过了OC的函数调用实现,你会觉得消息转发要处理很多,尤其是在搜索上,幸运的是objc_msgSend在搜索这块是有做缓存的,每个OC的类都有一块这样的缓存,objc_msgSend会将匹配结果缓存在快速映射表(fast map)中,这样以来这个类一些频繁调用的方法会出现在fast map 中,不用再去一遍一遍的在方法列表中搜索了。
第十二条:理解消息转发机制
关于这条这看看这篇文章:iOS理解Objective-C中消息转发机制附Demo
第十三条:用“方法调配技术”调试“黑盒方法”
这条讲的主要内容就是 Method Swizzling,通过运行时的一些操作可以用另外一份实现来替换掉原有的方法实现,往往被应用在向原有实现中添加新功能,比如扩展UIViewController,在viewDidLoad里面增加打印信息等。具体例子可以点击我查看
第十四条:理解“类对象”的用意
Objective-C类是由Class类型来表示的,它实际上是一个指向objc_class结构体的指针。它的定义如下:
typedef struct objc_class *Class;
在<objc/runtime.h>中能看到他的实现:
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY; ///< 指向metaClass(元类)
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE; ///< 父类
const char *name OBJC2_UNAVAILABLE; ///< 类名
long version OBJC2_UNAVAILABLE; ///< 类的版本信息,默认为0
long info OBJC2_UNAVAILABLE; ///< 类信息,供运行期使用的一些位标识
long instance_size OBJC2_UNAVAILABLE; ///< 该类的实例变量大小
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; ///< 该类的成员变量链表
struct objc_method_list **methodLists OBJC2_UNAVAILABLE; ///< 方法定义的链表
struct objc_cache *cache OBJC2_UNAVAILABLE; ///< 方法缓存
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; ///< 协议链表
#endif
} OBJC2_UNAVAILABLE;
此结构体存放的是类的“元数据”(metadata),例如类的实例实现了几个方法,具备多少实例变量等信息。
这里的isa指针指向的是另外一个类叫做元类(metaClass)。那什么是元类呢?元类是类对象的类。也可以换一种容易理解的说法:
当你给对象发送消息时,runtime处理时是在这个对象的类的方法列表中寻找
当你给类发消息时,runtime处理时是在这个类的元类的方法列表中寻找
我们来看一个很经典的图来加深理解:
可以总结为下:
每一个Class都有一个isa指针指向一个唯一的Meta Class
每一个Meta Class的isa指针都指向最上层的Meta Class,这个Meta Class是NSObject的Meta Class。(包括NSObject的Meta Class的isa指针也是指向的NSObject的Meta Class,也就是自己,这里形成了个闭环)
每一个Meta Class的super class指针指向它原本Class的 Super Class的Meta Class (这里最上层的NSObject的Meta Class的super class指针还是指向自己)
最上层的NSObject Class的super class指向 nil
第十五条:用前缀避免命名空间冲突
Objective-C没有类似其他语言那样的命名空间机制(namespace),比如说PHP中的
<?php
namespace RootSubsubnamespace;
这就会导致当你不小心实现了两个相同名字的类,或者把两个相对独立的库导入项目时而他们又恰好有重名的类的时候该类所对应的符号和Meta Class符号定义了两次。所以很容易产生这种命名冲突,让程序的链接过程中出现出现重复的符号造成报错。
为了避免这种情况,我们要尽量在类名,以及分类和分类方法上增加前缀,还有一些宏定义等等根据自己项目来定吧
第十六条:提供“全能初始化方法”
如果创建类的实例的方式不止一种,那么这个类就会有多个初始化方法,这样做很好,不过还是要在其中选定一个方法作为全能初始化方法,剩下的其余的初始化方法都要调用它,这样做的好处是以后如果初始化的逻辑更改了只需更改一处即可,或者是交给子类覆写的时候也只覆写这一个方法即可~
举个例子来说:可以看一下NSDate的实现在NSDate.h中NSDate类中定义了一个全能初始化方法:
- (instancetype)initWithTimeIntervalSinceReferenceDate:(NSTimeInterval)ti NS_DESIGNATED_INITIALIZER;
其余的类似初始化方式定义在NSDate (NSDateCreation) 分类中
- (instancetype)initWithTimeIntervalSinceNow:(NSTimeInterval)secs;
- (instancetype)initWithTimeIntervalSince1970:(NSTimeInterval)secs;
- (instancetype)initWithTimeInterval:(NSTimeInterval)secsToBeAdded sinceDate:(NSDate *)date;
在NSDate文档中有一条:If you want to subclass NSDate to obtain behavior different than that provided by the private or public subclasses, you must do these things:然后其中要做的有一步就是
Override [initWithTimeIntervalSinceReferenceDate:
](apple-reference-documentation://hcslylvSCo), one of the designated initializer methods`
这个是我们组织代码过程中应该学习的地方!
第十七条:实现description方法
这条讲的是可以通过覆写description方法或者debugDescription方法来在NSLog打印时或者LLDB打印时输出更多的自定义信息。(数据和字典的可以通过覆写descriptionWithLocale:方法)
友情提示:不要在description中使用 NSLog("%@",self);,不然会掉进无底深渊啊
这里我有一个有趣的想法,不过还没完全实现,就是想通过覆写description能把任何一个对象的属性值名称,属性值都一一完整的记录下来。
第十八条:尽量使用不可变对象
这条主要讲尽量使用不可变的对象,也就是在对外属性声明的时候要尽量加上readonly修饰,默认是readwrite,这样一来,在外部就只能读取该数据,而不能修改它,使得这个类的实例所持有的数据更加安全。如果外部想要修改,可以提供方法来进行修改。
不要把可变的collection作为属性公开,而应提供相关方法,以此修改对象中的可变collection(这条个人感觉一般在常用、重要的类才有必要,毕竟也增加了不少代码量)
比如例子:
//Language.h
@property (nonatomic, strong) NSSet *set;
应该改为
//Language.h
@property (nonatomic, strong, readonly) NSSet *languages;
- (void)addLanguage:(NSString *)language;
- (void)removeLanguage:(NSString *)language;
//**.m
@implementation Language {
NSMutableSet *mutableLanguages;
}
- (NSSet *)languages {
return [_mutableLanguages copy];
}
- (void)addLanguage:(NSString *)language {
[_mutableLanguages addObject:language];
}
- (void)removeLanguage:(NSString *)language {
[_mutableLanguages removeObject:language];
}
第十九条:使用清晰而协调的命名方式
这条不用太强调了,具体也可以参照一下我之前拟的Objective-C编程规范及建议,后续可能会不断补充更新
第二十条:为私有方法名加前缀
这条讲的是应该为类内的私有方法增加前缀,以便区分,这个感觉因人而异吧,感觉只要你不随便把私有方法暴露在.h文件都能接受,曾遇到过这样的同事,感觉其不太适合写程序吧。
第二十一条:理解Objective-C错误模型
很多语言都有异常处理机制,Objective-C也不例外,Objective-C也有类似的@throw,不过在OC中使用@throw可能会导致内存泄漏,可能是它被设计的使用场景的问题。建议@throw只用来处理严重错误,也可以理解为致命错误(fatal error),那么处理一般错误的时候(nonfatal error)时可以使用NSError。
第二十二条:理解NSCopying协议
在OC开发中,使用对象时经常需要拷贝它,我们会通过copy/mutbleCopy来完成。如果想让自己的类支持拷贝,那必须要实现NSCopying协议,只需要实现一个方法:
- (id)copyWithZone:(NSZone*)zone
当然如果要求返回对象是可变的类型就要用到NSMutableCopying协议,相应方法
- (id)mutableCopyWithZone:(NSZone *)zone
在拷贝对象时,需要注意拷贝执行的是浅拷贝还是深拷贝。深拷贝在拷贝对象时,会将对象的底层数据也进行了拷贝。浅拷贝是创建了一个新的对象指向要拷贝的内容。一般情况应该尽量执行浅拷贝。
第二十三条:通过委托与数据源协议进行对象间通信
这条讲的也比较基础,就是基本的delegate,protocal使用。
有一点稍微说一下:当某对象需要从另外一个对象中获取数据时,可以使用委托模式,这种用法经常被称为“数据源协议”(Data source Protocal)类似 UITableview的UITableViewDataSource
另外在Swift中有一个很重要的思想就是面向协议编程。当然OC中也可以用协议来降低代码耦合性,必要的时候也可以替代继承,因为遵循同一个协议的类可以是任何,不必是同一个继承体系下。
第二十四条:将类的实现代码分散到便于管理的数个分类之中
这条主要说的是通过分类机制,可以把类分成很多歌易于管理的小块。也是有一些前提的吧,可能是这个类业务比较复杂,需要瘦身,需要解耦等等。作者还推荐把私有方法统一放在Private分类中,以隐藏实现细节。这个个人觉得视情况而定吧。
第二十五条:总是为第三方类的分类名称加前缀
向第三方类的分类名称加上你专用的前缀,这点不必多说 以上是关于一篇文章拿下《Effective Objective-C 2.0编写高质量iOS与OS X代码的52个有效方法》的主要内容,如果未能解决你的问题,请参考以下文章 死磕SpringBoot59篇干货总结一次拿下SpringBoot!