我正在参加「启航计划」

— 此文献给一位深夜奋战在一线的字节跳动面试官 :)

缘起: 2017年10月17日, 用Kindle3拜读完 Matt Galloway的<<Effective Objective-C 2.0>>即<<编写高质量iOS与 OS X代码的52个有用办法>>后就在简书上整理了这52个有用的Tips, 至今已近5年时间. 今天有幸跟一位字节的小伙伴畅聊技能, 很是开心. 最后让这位奋战在一线的字节跳动面试官同学安利一本对他的技能发展影响较大的书, 他遂推荐了这本. 面临简书这篇帖子5年一共218的阅览量. 不由一同感叹一本朴实的好书不应该知道的人那么少. OC真的被扔掉了吗? 扔掉OC的成本比幻想的高! OC作为一个久经考验并支撑iPhone改动世界的言语, 仍然有着顽强的生命力. 所以再次在, 这个我以为现在最好的中文技能社区之一, 发布此文. 与君共勉, 作为一个一般的MOP开发Coder, 有空读读这52个接地气办法, 真的很有用~ 简书原文链接

字节同学推荐_编写高质量Objective-C代码的52个有效方法

第1章 了解Objective-C

  1. 了解Objective-C言语的来源
  • Objective-C为C言语添加了面向目标特性,是其超集.Objective-C运用动态绑定的音讯结构,也便是说,在运行时才会查看目标类型.接收一条音讯之后,究竟应履行何种代码,由运行期环境而非编译器来决议.
  • 了解C言语的中心概念有助于写好Objective-C程序.尤其要把握内存模型与指针.
  1. 在类的头文件中尽量少引进其他头文件
  • 除非确有必要,不然不要引进头文件.一般来说,应该在某个类的头文件中运用向前声明来提及其他类,并在完成文件中引进那些类的头文件.这样做能够尽量下降类之间的耦合(coupling).
  • 有时无法运用向前声明,比如要声明某个类遵照一项协议.这种情况下,尽量把”该类遵照某协议”的这条声明移至”class-continuation分类中”.假如不行的话,就把协议独自放在一个头文件中,然后将其引进.
  1. 多用字面量语法,少用与之等价的办法
  • 应该运用字面量语法来创立字符串、数值、数组、字典.与创立此类目标的常规办法相比,这么做愈加简明扼要.
  • 应该经过取下标操作来访问数组下标或字典中的键所对应的元素.
  • 用字面量语法创立数组或字典时,若值中有nil,则会抛出反常.因而,有必要确保值里不含nil.
  1. 多用类型常量,少用#define预处理指令
  • 不要用预处理指令界说常量.这样界说出来的常量不含类型信息,编译器只是会在编译前据此履行查找与替换操作.即便有人从头界说了常量值.编译器也不会产生正告信息,这将导致运用程序中的常量值不一致.
  • 在完成文件中运用static const来界说”只在编译单元内可见的常量”(translation-unit-specific constant).由于此类常量不会再大局符号中标明,所以无需为其称号加前缀.
  • 在头文件中运用extern来声明大局常量,并在相关完成文件中界说其值.这种常量要呈现在大局符号表中,所以其称号应加以区隔,通常用与之相关的类名做前缀.
  1. 用枚举标明情况、选项、情况码
  • 应该用枚举来标明情况机的情况、传递给办法的选项以及情况码等值,给这些值起个易懂的姓名.
  • 假如把传递给某个办法的选项标明为枚举类型,而多个选项又可一起运用,那么就将各选项值界说为2的幂,以便经过按位或操作将其组合起来.
  • 用NS_ENUM与NS_OPTION宏来界说枚举类型,并指明其底层数据类型.这样做能够确保枚举是用开发者所选的底层数据类型完成出来的,而不会选用编译器所选的类型.
  • 在处理枚举类型的switch句子中不要完成default分支.这样的话,参加新枚举之后,编译器就会提示开发者:switch句子并未处理一切枚举.

第2章 目标、音讯、运行期

  1. 了解”特点”这一概念
  • 能够用@property语法来界说目标中所封装的数据.
  • 经过”特质”来指定存储数据所需的正确语义.
  • 在设置特点所对应的实例变量时,一定要遵照该特点所声明的语义.
  • 开发iOS程序时应该运用nonatomic特点,由于atomic特点会严峻影响功用.
  1. 在目标内部尽量直接访问实例变量
  • 在目标内部读取数据时,应该直接经过实例变量来读,而写入数据时,则应经过特点来写.
  • 在初始化办法及dealloc办法中,总是应该直接经过实例变量来读写数据.
  • 有时会运用惰性初始化技能配置某份数据,这种情况下,需求经过特点来读取数据.
  1. 了解”目标等同性”这一概念
  • 若想检测目标的等同性,请供给”isEqual:”与hash办法.
  • 相同的目标有必要具有相同的哈希码,可是两个哈希码相同的目标却未必相同.
  • 不要盲目地逐个监测每条特点,而是应该按照详细需求来拟定检测办法.
  • 编写hash办法时,应该运用核算速度快并且哈希码磕碰几率低的算法.
  1. 以”类族形式”躲藏完成细节
  • 类族形式能够把完成细节躲藏在一套简单的公共接口后面.
  • 体系结构中常常运用类族.
  • 从类族的公共笼统基类中承继子类时要留神,若有开发文档,则应首先阅览.
  1. 在既有类中运用相关目标存放自界说数据
  • 能够经过”相关目标”机制来把两个目标连起来.
  • 界说相关目标时可指定内存办理语义,用以仿照界说特点时所选用的”具有联系”与”非具有联系”.
  • 只要在其他做法不行行时才应选用相关目标,由于这种做法通常会引进难于查找的bug.
  1. 了解 objec_msgSend的效果
  • 音讯由接收者、挑选子及参数构成.给某目标”发送音讯”(invoke a message)也就相当于在该目标上”调用办法”(call a method).
  • 发给某目标的全部音讯都要由”动态音讯派发体系”(dynamic message dispatch system)来处理,该体系会查出对应的办法,并履行其代码
  1. 了解音讯转发机制
  • 若目标无法呼应某个挑选子,则进入音讯转发流程.
  • 经过运行期的动态办法解析功用,咱们能够在需求用到某个办法时再将其参加类中.
  • 目标能够把其无法解读的某些挑选子转交给其他目标来处理.
  • 经过上述两步之后,假如仍是没办法处理挑选子,那就发动完整的音讯转发机制.
  1. 用”办法分配技能”调试”黑盒办法”
  • 在运行期,能够向类中新增或替换挑选子所对应的办法完成.
  • 运用另一份完成来替换原有的办法完成,这道工序叫做”办法分配”,开发者常用此技能向原有完成中添加新功用.
  • 一般来说,只要调试程序的时候才需求在运行期修正办法完成,这种做法不宜滥用.
  1. 了解”类目标”的用意
  • 每个实例都有一个指向Class目标的指针,用以标明其类型,而这些Class目标则构成了类的承继体系.
  • 假如目标的类型无法在编译期确定,那么就应该运用类型信息查询办法来探知.
  • 尽量运用类型信息查询办法来确定目标类型.而不要直接比较类目标,由于某些目标或许完成了音讯转发功用.

第3章 接口与API设计

  1. 用前缀防止命名空间抵触
  • 挑选与你的公司、运用程序或二者皆有相关之称号作为类名的前缀,并在一切代码中均运用这一前缀.
  • 若自己所开发的运用程序库顶用到了第三方库,则应为其间的称号加上前缀.
  1. 供给”万能初始化办法”
  • 在类中供给一个万能初始化办法,并于文档里指明.其他初始化办法均应调用此办法.
  • 若万能初始化办法与超类不同,则需覆写超类中的对应办法.
  • 假如超类的初始化办法不适用于子类,那么应该覆写这个超类办法,并在其间抛出反常.
  1. 完成description办法
  • 完成description办法回来一个有意义的字符串,用以描绘该实例.
  • 若想在调试时打印出更翔实的目标描绘信息,则应完成debugDescription办法.
  1. 尽量运用不行变目标
  • 尽量创立不行变的目标.
  • 若某个特点仅可于目标内部修正,则在”class-continuation分类”中将其由readonly特点扩展为readwrite特点.
  • 不要把可变的collection作为特点揭露,而应供给相关办法,以此修正目标中的可变collection.
  1. 运用清晰而和谐的命名办法
  • 起名时应遵照规范的Objective-C命名规范,这样创立出来的接口更简单为开发者所了解.
  • 办法名要言简意赅,从左至右读起来要像个日常用语中的句子才好.
  • 办法名里不要运用缩略后的类型称号.
  • 给办法其名时的第一要务便是确保其风格于你自己的代码所要集成的结构相符.
  1. 为私有办法名加前缀
  • 给私有办法的称号加上前缀,这样能够很简单地将其同公共办法区别开来.
  • 不要单用一个下划线做私有办法的前缀,由于这种做法是预留给苹果公司用的.
  1. 了解Objective-C过错模型
  • 只要产生了可使整个运用程序崩溃的严峻过错时,才应运用反常.
  • 在过错不那么严峻的情况下,能够指使”托付办法”(delegate method)来处理过错,也能够把过错信息放在NSError目标里,经由”输出参数”回来给调用者.
  1. 了解NSCopying协议
  • 若想令自己所写的目标具有复制功用,则需完成NSCopying协议.
  • 假如自界说的目标分为可变版本,那么就要一起完成NSCopying与NSMutableArray协议.
  • 复制目标时需决议选用浅复制仍是深复制,一般情况下应该尽量履行浅复制.
  • 假如你所写的目标需求深复制,那么可考虑新增一个专门履行深复制的办法.

第4章 协议与分类

  1. 经过托付与数据源协议进行目标间通信要点
  • 托付形式为目标供给了一套接口,使其可由此将相关工作告知其他目标.
  • 将托付目标应该支撑的接口界说成协议,在协议中把或许需求处理的工作界说成办法.
  • 当某目标需求从另一个目标中获取数据时,能够运用托付形式.这种情形下,该形式亦称”数据源协议”(data source protocal).
  • 若有必要,可完成含有位段的结构体,将托付目标是否能呼应相关协议办法这一信息缓存至其间.
  1. 将类的完成代码涣散到便于办理的数个分类之中
  • 运用分类机制把类的完成代码划分红易于办理的小块.
  • 将应该视为”私有”的办法归入名叫Private的分类中,以躲藏完成细节.
  1. 总是为第三方类的分类称号加前缀
  • 向第三方类中添加分类时,总应给其称号加上你专用的前缀.
  • 向第三方类中添加分类时,总应给其间办法名加上你专用的前缀.
  1. 勿在分类中声明特点
  • 把封装数据所用的全部特点都界说在主接口里.
  • 在”class-continuation分类”之外的其他分类中,能够界说存取办法,但尽量不要界说特点(特点是用来封装数据的).
  1. 运用”class-continuation分类”躲藏完成细节
  • 经过”class-continuation分类”向类中新增实例变量.
  • 假如某特点在主接口中声明为”只读”,而类的内部又要用设置办法修正此特点,那么就在”class-continuation分类”中将其扩展为”可读写”.
  • 把私有办法的原型声明在”class-continuation分类”里边.
  • 若想使类所遵照的协议不为人知,则可于”class-continuation分类”中声明.
  1. 经过协议供给匿名目标
  • 协议可在某种程度上供给匿名类型.详细的目标类型能够淡化成遵照某协议的id类型,协议里规定了目标所应完成的办法.
  • 运用匿名目标来躲藏类型称号(或类名).
  • 假如详细类型不重要,重要的是目标能够呼应(界说在协议里的)特定办法,那么可运用匿名目标来标明.

第5章 内存办理

  1. 了解引证计数
  • 引证计数机制经过能够递加递减的计数器来办理内存.目标创立好之后,其保存计数至少为1.若保存计数为正,则目标继续存活.当保存计数降为0时,目标就被销毁了.
  • 在目标生命周期中,其他目标经过引证来保存或开释此目标.保存与开释操作别离会递加及递减保存计数.
  1. 以ARC简化引证计数 ps:实际上,ARC在调用(retain、release、autorelease、dealloc)这些办法时.并不经过一般的Objective-C音讯派发机制,而是直接调用其C言语版本.这样做功用更好,由于保存及开释操作需求频繁履行,所以直接调用底层函数能节省很多CPU周期.比方说,ARC会调用与retain等价的底层函数,objc_retain.这也是不能复写retain、release或autorelease的缘由,由于这些办法从来不会被直接调用.
  • 有ARC之后,程序员就无须(有必要,不是需求)担心内存办理问题了.运用ARC来编程,可省去类中的”样板代码”.
  • ARC办理目标生命周期的办法基本上就算是:在合适的地方刺进”保存”及”开释”操作.在ARC环境下,变量的内存办理语义能够经过修饰符指明,而本来则需求手艺履行”保存”及”开释”操作.
  • 有办法所回来的目标,其内存办理语义总是经过办法名来体现.ARC将此确定为开发者有必要恪守的规矩.
  • ARC只担任办理Objective-C目标的内存.尤其要留意:CoreFoundation目标不归ARC办理,开发者有必要当令调用CFretain/CFRelease.
  1. 在dealloc办法中只开释引证并免除监听
  • 在dealloc办法里,应该做的工作便是开释指向其他目标的引证,并取消本来订阅的”键值观察”(KVO)或NSNotificationCenter等告诉,不要做其他工作.
  • 假如目标持有文件描绘符等体系资源,那么应该专门编写一个办法来开释此种资源.这样的类要和其运用者约定:用完资源后有必要调用close办法.
  • 履行异步使命的办法不应再dealloc里调用;只能在正常情况下履行的那些办法也不应在dealloc里调用,由于此刻目标已处于正在收回的情况了.
  1. 编写”反常安全代码”时留意内存办理问题
  • 捕获反常时,一定要留意将try块内所创立的目标清理干净.
  • 在默认情况下,ARC不生成安全处理反常所需的清理代码.开启编译器标志后,可生成这种代码,不过会导致运用程序变大,并且会下降运行功率.
  1. 以弱引证防止保存环
  • 将某些引证设为weak,可防止呈现”保存环”.
  • weak引证能够主动清空,也能够不主动清空.主动清空(autonilling)是跟着ARC而引进的新特性,由运行期体系来完成.在具有主动清空功用的弱引证上,能够随意读取其数据,由于这种引证不会指向已收回过的目标.
  1. 以”主动开释池块”下降内存峰值
  • 主动开释池排布在栈中,目标收到aurorelease音讯后,体系将其放入最顶端的池里.
  • 合理运用主动开释池,可下降运用程序的内存峰值.
  • @autoreleasepool这种新式写法能创立出更为轻便的主动开释池.
  1. 用”僵尸目标”调试内存办理问题
  • 体系在收回目标时,能够不将其真的收回,而是把它转化为僵尸目标.经过环境变量NSZombieEnabled可开启此功用.
  • 体系会修正目标的isa指针,令其指向特别的僵尸类,从而使该目标变为僵尸目标.僵尸类能够呼应一切的挑选子,呼应办法为:打印一条包括音讯内容及其接收者的音讯,然后终止运用程序.
  1. 不要运用retainCount
  • 目标的保存计数看似有用,实则不然,由于任何给定时间点上的”肯定保存技能”(absolute retain count)都无法反映目标生命期的全貌.
  • 引进ARC之后,retainCount办法就正式废止了,在ARC下调用该办法会导致编译器报错.

第6章 块与大中枢派发

  1. 了解”块”这一概念
  • 块是C、C++、Objective-C中的词法闭包.
  • 块可接受参数,也能够回来值.
  • 块能够分配在栈或堆上,也能够是大局的.分配在栈上的块可复制到堆里,这样的话,就和规范的Objective-C目标相同,具有引证计数了.
  1. 为常用的块类型创立typedef
  • 以typedef从头界说块类型,可令块变量用起来愈加简单.
  • 界说新类型时应遵照现有的命名习气,勿使其称号与其他类型相抵触.
  • 不妨为同一个块签名界说多个类型别号.假如要重构的代码运用了块类型的某个别号,那么只要修正相应的typedef中的块签名即可,无须改动其他typedef.
  1. 用handler块下降代码涣散程度
  • 在创立目标时,能够运用内联的handler块将相关业务逻辑一并声明.
  • 在有多个实例需求监控时,假如选用托付形式,那么常常需求依据传入的目标来切换,而若改用handler块来完成,则可直接将块与相关目标放在一同.
  • 设计API时假如用到了handler块,那么能够添加一个参数,使调用者可经过此参数来决议应该把块组织在哪个行列上履行.
  1. 用块引证其所属目标时不要呈现保存环
  • 假如块所捕获的目标直接或直接地保存了块自身,那么就得留神保存环问题.
  • 一定要找个适当的机遇免除保存环,而不能把责任推给API的调用者.
  1. 多用派发行列,少用同步锁
  • 派发行列可用来表述同步语义(synchronization sematic),这种做法要比@synchronized块或NSLock目标更简单.
  • 将同步与异步派发结合起来,能够完成与一般加锁机制相同的同步行为,而这么做却不会堵塞履行异步派发的线程.
  • 运用同步行列及栅门块,能够令同步行为愈加高效.
  1. 多用GCD、少用performSelector系列办法
  • performSelector系列办法在内存办理方面简单有疏失.它无法确定即将履行的挑选子详细是什么,因而ARC编译器也就无法刺进适当的内存办理办法.
  • performSelector系列办法所能处理的挑选子太过于局限了,挑选子的回来值类型及发送给办法的参数个数都受到约束.
  • 假如想把使命放在另一个线程上履行,那么最好不要用performSelector系列办法,而是应该把使命封装到块里,然后调用大中枢派发机制的相关办法来完成.
  1. 把握GCD及操作行列的运用机遇
  • 在处理多线程与使命办理问题时,派发行列并非仅有方案.
  • 操作行列供给了一套高层的Objective-C API,能完成纯GCD所具有的绝大部分功用,并且还能完成更为复杂的操作,哪些操作若改用GCD来完成,则需别的编写代码.
  1. 经过Dispatch Group机制,依据体系资源情况来履行使命 (在并发行列中,履行使命所用的的并发线程数量,取决于各种要素,而GCD主要是依据体系资源情况来判定这些要素的.)
  • 一系列使命可归入一个dispatch group之中.开发者能够在这组使命履行结束时获得告诉.
  • 经过dispatch group,能够在并发式派发行列里一起履行多项使命.此刻GCD会依据体系资源情况来调度这些并发履行的使命.开发者若自己来完成此功用,则需求编写大量代码.
  1. 运用dispatch_once来履行只需履行一次的线程安全代码
  • 常常需求编写”只需求履行一次的线程安全代码”(thread-safe single-code execution).经过GCD所供给的dispatch_once函数,很简单就能完成此功用.
  • 标记应该声明在static或global效果域中,这样的话,在把只需履行一次的块传给dispatch_once函数时,传进去的标记也是相同的.
  1. 不要运用dispatch_get_current_queue
  • dispatch_get_current_queue函数的行为常常与开发者所预期的不同.此函数现已废弃,只应做调试之用.
  • 由于派发行列是按层级来组织的,所以无法单用某个行列目标来描绘”当前行列”这一概念.
  • dispatch_get_current_queue函数用于处理由不行重入的代码所引发的死锁,但是能用此函数处理的问题,通常也能改用”行列特定数据”来处理.

第7章 体系结构

  1. 了解体系结构
  • 许多体系结构都能够直接运用.其间最重要的是Foundation与CoreFoundation,这两个结构供给了构建运用程序所需的许多中心功用.
  • 很多常见使命都能用结构来做,例如音频与视频处理、网络通信、数据办理等.
  • 请记住:用纯C写成的结构与用Objective-C写成的相同重要,若想成为优秀的Objective-C开发者,应该把握C言语的中心概念.
  1. 多用块枚举,少用for循环
  • 遍历collection有四种办法.最基本的办法是for循环,其次是NSEnumenrator遍历法及快速遍历法,最新、最先进的办法则是”块枚举法”.
  • “块枚举法”,自身就能经过GCD来并发履行遍历操作,无须另行编写代码.而选用其他便利办法则无法简单完成这一点.
  • 若提早知道待遍历的collection含有何种目标,则应该修正块签名,指出目标的详细类型.
  1. 对自界说其内存办理语义的collection运用无缝桥接 ps:在运用Foundation结构中的字典目标是会遇到一个大问题,那便是其键的内存办理语义为”复制”,而值得语义却是”保存”.除非运用强大的无缝桥接技能,不然无法改动其语义.
  • 经过无缝桥接技能,能够在Foundation结构中的Objective-C目标与CoreFoundation结构中的C言语结构之间来回转换.
  • 在CoreFoundation层面创立collection时,能够指定许多回调函数,这些函数标明此collection应怎么处理其元素.然后,能够运用无缝桥接技能,将其转换成具有特别内存办理语义的Objective-C collection.
  1. 构建缓存是选用NSCache而非NSDicitionary
  • 完成缓存时应选用NSCache而非NSDictionary目标.由于NSCache能够供给高雅的主动删减功用,并且是”线程安全的”,此外,它与字典不同,并不会复制键.
  • 能够给NSCache目标设置上限,用以约束缓存中的目标总个数及”总成本”,而这些标准则界说了缓存删减其间目标的机遇.可是肯定不要把这些标准当成可靠的”硬约束”(hard limit),它们仅对NSCache起指导效果.
  • 将NSPurgeableData与NSCache搭配运用,可完成主动清除数据的功用,也便是说,当NSPurgeableData目标所占用内存为体系所丢掉时,该目标自身也会从缓存中移除.
  • 假如缓存运用妥当,那么运用程序的呼应速度就能进步.只要那种”从头核算起来很费事的”数据,才值得放入缓存,比如那些需求从网络获取或从硬盘读取的数据.
  1. 精简initialize与load的完成代码 ps:整数能够在编译期界说,但是可变数组不行,由于它是个Objective-C目标,所以创立实例之前有必要先激活运行期体系.留意,某些Objective-C目标也能够在编译期创立,例如NSString实例.但是创立下面这种目标会令编译器报错: static NSMutableArray * kSomeObjects = [NSMutableArray new];
  • 在加载阶段,假如完成了load办法,那么体系就会调用它.分类里也能够界说此办法,类的load办法要比分类中的先调用.与其他办法不同,load办法不参加覆写机制.
  • 初次运用某个类之前,体系会想起发送initialeze音讯.由于此办法遵照一般的覆写规矩,所以通常应该在里边判断当前要初始化的是哪个类.
  • load与initialize办法都应该完成得精简一些,这有助于保持运用程序的呼应能力,也能减少引进”依赖环”(interdependency cycle)的几率.
  • 无法在编译期设定的大局变量,能够放在initialize办法里初始化.
  1. 别忘了NSTimer会保存其目标目标
  • NSTimer目标会保存其目标,直到计时器自身失效停止,调用invalidate办法可令计时器失效,别的,一次性的计时器在触发完使命之后也会消失.
  • 反复履行使命的计时器(repeating timer),很简单引进保存环,假如这种计时器的目标目标又保存了计时器自身,那肯定会导致保存环.这种环状保存联系,或许是直接产生的,也或许是经过目标图里的其他目标直接产生的.
  • 能够扩大NSTimer的功用,用”块”来打破保存环.不过,除非NSTimer将来在公共接口里供给此功用,不然有必要创立分类,将相关完成代码参加其间.

PS:读书时做的纯手打笔记,如有过错,欢迎指正.

发文不易, 喜欢点赞的人更有好运气 :), 定期更新+关注不走失~

ps:欢迎参加笔者18年建立的研究iOS审阅及前沿技能的三千人扣群:662339934,坑位有限,备注“网友”可被群管经过~