Effective Object-C 2.0笔记

2019-04-08  本文已影响0人  小强七号

熟悉Objective-C

1. Objective-C 语言的起源

Objective-C语言由Smalltalk演化而来,smalltalk是消息型语言的鼻祖。消息型语言运行时所执行的代码由运行环境来决定。函数调用的语言由编译器来决定。
Objective-C为C语言添加了面相对象特征,是C语言的超集。Objective-C使用动态绑定的消息结构,也就是说在运行时才回去检查对象类型。接受一条消息后,究竟应执行何种代码,由运行期环境而非编译器来决定。

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

除非确有必要,否则不要引入头文件。一般来说,应在某个类额头文件中使用向前声明来提及别的类,并在实现文件中引入那些类的头文件,这样做可以尽量降低类之间的耦合
有时无法使用向前声明,比如要声明某个类型遵循某一项协议。这种情况下,尽量把“该类遵循某协议”的这条声明移至“class-continuation分类”中。如果不行的话,就把该协议单独放在一个头文件中,然后将其引入。

3. 多用字面量语法,少用与之等价的方法

使用该字面量语法来创建字符串、数值、数组、字典。与创建此类对象的常规方法相比,这么做更加简明扼要。
应该通过取下标来访问数组下标或字典中的健所对应的元素
用字面语法创建数组或字典时,若值中有nil,则会抛出异常。因此,务必确保值里面不包含nill对象。

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

不要用预处理指令定义常量。这样定义出来的常量不包含类型信息,编译器只会在编译钱根据执行找与替换操作。几遍有人重定义了常量值,该编译器也不会产生警告信息,浙江导致应用中常量不一致。
在实现文件中庸static const来定义“只在编译单元内可见的常量”。由于该常量不在全局符号号表中,所以无需为其添加前缀。
在头文件中用extern来声明全局常量,并在相关实现文件中定义其值。这种常量要出现在全局符号表中,所以其名称应加以前缀用来区分,通常用用与之相关类名做前缀。

5. 用枚举表示状态、选项、状态码

应该用枚举来表示状态机的状态、传递给方法的选项以及状态码等值,给这些值起个易懂的名字。
如果传递个某个方法的选项表示枚举类型,而多个选项又可以同时使用,那么将各选项定义成2的幂,以便通过按位或操作将其组合起来。
用NS_ENUM和NS_OPTIONS宏来定义枚举的类型,并指明起底层数据类型。这样做可以明确确保枚举是用开发者所选的底层数据类型实现出来的,而不会采用编译器所选的类型。
在处理枚举类型中的switch语句不要实现default分支,这样的话,加入新枚举之后,编译器就会提示我们switch中有未处理的枚举。

对象、消息、运行期

6. 理解“属性”这一概念

属性是Objective-C的一项特性,用于封装对象中的数据。属性性质有:原子性读/写权限内存管理语义(assign、strong、weak、unsafe_unretained、copy)方法名(getter=<name>、setter=<name>)
可以用@property语法来定义对象中所封装的数据
通过“特质”来指定存储数据所需的正确语义
在设置属性所对应的实例变量时、一定要遵从该属性所声明的语义
开发iOS 12程序时使用nonatomic属性,因为atomic属性严重影响性能

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

属性访问直接访问的区别:

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

NSObject协议中有两个用于判断等同性的关键语句方法

要点:

9. 以“类族模式”隐藏实现细节

类族可以隐藏抽象基类背后实现的细节。比如UIButtonCocoa里大部分都是类族
类族规则:

要点:

10. 在已有类中使用关联对象存放自定义数据

关联对象(Associated Object)可以为已有类存储对象值,这些对象是通过来区分的,该键通常为全局静态变量

objc_setAssociatedObject(<#id  _Nonnull object#>, <#const void * _Nonnull key#>, <#id  _Nullable value#>, <#objc_AssociationPolicy policy#>)   

objc_getAssociatedObject(<#id  _Nonnull object#>, <#const void * _Nonnull key#>)    

objc_removeAssociatedObjects(<#id  _Nonnull object#>)

要点

11. 理解objc'_msgSend的作用

在objective-C中,如果向对象传递消息,那就会使用动态绑定机制来决定需要调用的方法。在底层,所有方法都是普通的C语言函数,然而对象收到消息后,该调用哪个方法取决于运行期间绝对,甚至可以在程序运行期间改变,这些特性使得Objective-C成为一门真正的动态语言。

objc_msgSend(id self,SEL cmd,...)
objc_msgSend_stret()
objc_msgSend_fpret()    
objc_msgSendSuper()

如果某函数的最后一项是调用另外一个函数,那么就可以运用“尾调用优化”技术。编译器会生成调用另一函数所需的指令码,而且不需要调用堆栈中推入新的“帧栈”。只有当某函数的最后一个操作仅仅是调用其他函数而且不会将其返值另作他用时,才能执行“尾调用优化”,这项技术对objc_msgSend非常关键,解决了帧栈溢出的问题。尾调用优化讲解
要点

12. 理解消息转发机制


消息转发大致分为两大阶段:

  1. 第一阶段先征询接受者,所属的类,看起是否能动态添加方法,已处理当前这个"未知选择子"
  2. 第二段设计"完整的消息转发机制",如果接受者无法动态的新增方法的手段来响应这个未选择子的消息,此时运行期系统会请求看看有没有其他对象来处理这条消息,若有,则运行期间会把消息传递给那个对象,于是消息转发结束,一切正常。若没有“备援的接受者”,则启动完整的消息转发机制,运行期系统会把与消息相关的全部细节封装到NSInvocation对象中,再给接受者最后一次机会,另其设法解决当前还未处理的这条消息。处理消息越到后面代价越大。
/// 对象收到无解读的消息后,首先将调用其类的下列类方法
+ (BOOL)resolveClassMethod:(SEL)sel 

///备援接受者
- (id)forwardingTargetForSelector:(SEL)aSelector

/// 完整的消息转发
- (void)forwardInvocation:(NSInvocation *)anInvocation

要点:

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

开发者可以为那些“完全不知道其具体实现”的黑盒方法增加日志记录功能,这非常有助于调试
要点:

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

在运行期间检查对象类型这一操作也叫类型信息查询,这个强大而有用的特性内置于Foundation框架中的NSObject协议里,凡是有公共根类(NSObject和NSProxy)继承而来的对象都要遵从此协议。在比较对象所属类的时候建议使用类型信息查询

struct objc_class {
    Class _Nonnull isa  ;/**< 该对象所属的类 元类metaClass*/
    
    Class _Nullable super_class;/**< 该对象所属类的超类 确立了继承关系 super_class*/
    const char * _Nonnull name;/**< 类名 */
    long version;/**< 版本信息 */
    long info;
    long instance_size;
    struct objc_ivar_list * _Nullable ivars;/**< 属性信息集合 */
    struct objc_method_list * _Nullable * _Nullable methodLists;/**< 方法信息集合 */
    struct objc_cache * _Nonnull cache;/**< 缓存信息集合 */
    struct objc_protocol_list * _Nullable protocols;/**< 遵从的协议集合 */
    
} OBJC2_UNAVAILABLE;

isMemberOfClass判断出对象是否为某个特定类的实例
isKindOfClass判断对象是否为某类或其派生类的实例

要点:

接口与API设计

15. 用前缀避免命名空间冲突

Objective-C语言没有内置的命名空间机制,因此我们开发的时候需要变相的实现命名空间(为所有名称前都加上适当的前缀,应用程序所有名称前都应该加上前缀)
要点:

16. 提供“全能初始化方法”

提供全能初始化方法,其余初始化方法都需要调用此全能初始化方法,只有在全能初始化方法中才可以存储数据。因此当底层数据放生改变时,只需要修改此方
法的代码就好,无需修改其他初始化方法。

要点:

17. 实现description方法

- (NSString *)description {
    
    return [NSString stringWithFormat:@"<%@:%p,width=%.2lf,height=%.2lf",[self class],self,_width,_height];
}
///控制台输出信息
2019-04-12 00:06:13.139550+0800 Effective Objective-C Demo[816:18588] <MJTestModel:0x600002edc240,width=100.00,height=100.00

18. 尽量使用不可变对象

在设计类的时候应该充分运用属性来封装数据,而在使用属性的时候可将其设置为只读,
要点:

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

方法命名:清晰的方法名是从左至右读起来好似一段文章。

  1. 如果方法的返回值是新创建的,那么方法名的首个词应该是返回值类型,除非前面还有修饰语,例如localizedString。然而属性的存储方法不遵守这种命名方法。
  2. 应该把参数类型的名词放在参数前面
  3. 如果方法要在当前对象上执行操作,那么应该包含动词;若执行操作时还需要参数,则应该在动词后面加上一个或者多个名词
  4. 不要使用str这种简称,应使用string这样的全称
  5. Boolean属性应加上is前缀。如果方法返回非属性的Boolean值,则应该根据其功能,选用has或is当前缀
  6. 将get这个前缀留给那些借助由“输出参数”来保存返回值的方法,比如说,把返回值填充到“C语言数组”里的那种方法就可以使用这个词做前缀

类与协议的命名:应该为类与协议加上前缀避免命名冲突,而且应该想给方法起名一样从左至右读起来通畅。

要点:

20. 为私有方法名加前缀

为私有方法加前缀有助于调试,而且还能与公开的方法区分开。
要点:

21. 理解?Objective-C错误模型

Objectview-C中“自动引用计数”在默认情况下不是“异常安全”的。因为抛出异常会导致本应在作用域末尾需要释放的对象不会自动释放,如果还要“异常安全”的代码,需要打开编译器标志-fobjc-arc-exceptions
Objective-C语言现在采用的办法是:只有在机器罕见的情况下抛出异常,异常抛出后,无需考虑恢复问题,而且app此时也应该退出。
Objective语言对于不那么严重的错误处理的方法是返回“nil/0”,或者使用NSError以表明其中有错误发生。
NSError:用法灵活,用次对象我们可以将错误原因高速调用者。里面封装了三条信息
1. Error domain(错误范围,字符创类型):通常用一个特有的全局变量来定义。比方说处理URL的子系统(URL-handing subsystem)再从URL中解析或取得数据时出错了,那么就使用NSURLErrorDomain来表示错误范围
2. Error code(错误码 整型):独有的错误码用以指明在某个范围了发生了何种错误
3. User info(用户信息,其类型为字典):有关错误额外信息,其中包含“本地化描述(localized description)”,或许还含有导致该错误信息发生的另一个错误
我们可以通过委托协议来传递错误,也可以经由方法的“输出参数”返回给调用者

要点:

22. 理解NSCopying协议

如果某个类想执行拷贝功能,则需要该类声明遵从NSCopying协议,并实现
-(id)copyWithZone:(NSZone *)zone方法
要点

协议与分类

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

Objective-C广发采用“委托模式”的编程设计模式来实现对象之间的通信,该模式的宗旨就是定义一套接口,某对象若想接受另一对象的委托,则需要遵从此接口,以便成为其“委托对象”。这“另一对象”则可以给其委托对象回传一些信息,也可以在发生相关事件时通知委托对象。此模式可以将数据与业务逻辑解耦。
在代理相关方法调用很多次时,可以使用位段数据数据类型进行优化

要点:

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

通过分类机制可以把类代码分成多个易于管理的小块,以便单独检视。

要点:

25. 总是为第三方的分类名钱加前缀

使用前缀可以大大降低自己为第三方库加分类的时候与第三方分类方法名重名的几率。
要点:

26. 勿在分类中声明属性

属性是封装数据的方式,而分类的目的在于扩张类的功能,而非封装数据。
要点

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

在“class-continuation分类”中或“实现快”中可以将其隐藏起来,只供本类使用。还可以在public声明为“只读”的属性扩展为“可读写”,以便在类的内部设置其值。

28. 通过协议提供匿名对象**

内存管理

29. 理解引用计数

Objective-C语言使用引用计数来管理内存,每个对象都有个可以递增或者递减的计数器。如果让某个对象继续存活,那么引用计数就递增;用完之后引用计数就递减。计数为0就销毁该对象。

引用计数工作原理:在引用计数架构下,对象有个计数器,用以表示当前有多少个事物想另此对象存活,这在?Objective-C中叫做“保留计数(引用计数)”。NSObject协议中声明了三个方法用于操作计数器,以递增或递减其值:

retain 递增引用计数
release 递减引用计数
autorelease 待稍后清理“自动释放池(autorelease pool)”时在递减其引用计数。

属性存储方法中的内存管理

- (void)setFoo:(id)foo {
      
      [foo retain];
      [_foo release]
      _foo = foo;
}

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

自动释放池:在Objective-C的引用计数架构中,自动释放池是一项很重要的特性。调用release会立刻递减引用计数而且还可能导致该对象被系统回收。然后有时候我们可以调用autorelease,此方法会在稍后递减引用计数,通常是下一回合“时间循环(evetn loop)”时递减,不过也有可能执行的更早。由此可见autorelease能延长对象生命期,使其在跨界方法调用边界后依然可以存活一段时间。

保留环(循环引用):使用引用技术机制时,经常要注意一个问题就是“循环引用”,这会导致“内存泄漏”。解决这个问题我们通常采用weak来打破循环解决此问题。

要点

30. 以ARC简化引用计数

使用ARC时引用计数还是在执行的,只不过保留与释放操作都是ARC为你添加。由于ARC自动执行retain、release、autorelease等操作,因此不能手动调用retain、release、autorelease、dealloc
使用ARC时必须遵从的方法命名规则:以alloc、new、copy、mutableCopy命名的方法返回的对象归调用者持有。如果不是以以上4种方法开头所返回的对象不归调用者持有,会调用autorelease来让它跨界方法调用边界后依然有效。

_myPerson = [ECOPerson personWithName:@"Bob Smith"];

上述代码调用了personWithName:方法返回新的ECOPerson对象,而此方法在返回对象之前为其调用了autorelease方法。由于_myPerson是strong类型,所以编译器在设置其值时还需要执行一次保留操作。因此前面那段代码等效如下:

EOCPerson * tem = [ECOPerson personWithName:@"Bob Smith"];
_myPerson =  [tem retain];

此时personWithName:方法里面autorelease与上述代码中retain都是多余的。实际上ARC在运气器检测到这一对多余操作,为了优化代码,在方法返回自动释放对象时,要执行一个特殊函数,此时不会直接调用autorelease,而是该调用objc_autoreleaseReturnValue函数。此函数会检测当前方法返回之后要立即执行的那段代码。若发现那段代码要在返回的对象上执行retain操作,则设置全局数据结构(此数据结构的具体内容因处理器而已)中的一个标志位,二不执行autorelease操作。与之相似入股方法返回了一个自动释放池对象,而调用方法的代码要保留此对象,那么此时不直接执行retain操作,而是改调用objc_retainAutoreleaseReturnValue函数。此函数检测刚才那个标志位,若位置已置位,则不执行retain操作。设置并检测标志位,比调用autorelease和retain更快。

变量的内存管理语义:ARC也出处理局部变量与实例变量的内存管理。默认情况下都是指向对象的强引用。
_strong:默认语义,保留此值
_unsafe_unretained:不保留次值,这么做不安全,因为等到下次再用它时,他可能已经被系统回收了
_weak:不保留此值,但是变量可以安全使用,因为系统如果把这个对象回收了,那么会将此对象置为nil
_autorelease:把对象“按引用传递” 给方法时,使用这个特殊的修饰符,此值在方法返回时自动释放

要点

31. 在dealloc方法中释放并解除监听

对象经历其生命周期后,被系统回收时需要执行dealloc方法,此方法有且只执行一次,也就是在引用计数为0的时候执行。一旦调用dealloc后对象就不再有效了。在此方法中会释放对象所拥有的引用,但是CoreFoundation对象需要手动释放,还有就是移除观测行为(observation behavior)。开销大或系统内稀缺的资源不一定在dealloc方法中释放。那些通常是在实现一方法。在调用完资源后立即调用刚刚实现的方法关闭资源。在dealloc中不要调用属性的存储方法,因为有人可能会覆写这些方法,并于其中做一些无法再回首阶段安全执行的操作。

要点:

32. 编写“异常安全代码”时留意内存管理问题

ARC开启安全处理异常的附加代码:-fobjc-arc-exceptions这个编译器标志。这个标志默认是关闭的,因为如果应用程序即将终止,那么发生内存泄漏已经无关紧要了,在应用即将终止的情况下还去添加安全处理异常所用的附加代码已经没有意义了。

要点:

33.以弱引用避免循环引用

避免循环引用最佳方式是弱引用,这种“非拥有关系”。
weak与unsafe_unretained的区别:weak在对象被回收后会置为nil,而unsafe_unretained则仍指向那个已经被回收的对象。显然用weak时更安全的做法

要点:

34. 以“自动释放池块”降低内存峰值

自动释放池嵌套用的好处就是可以降低内存峰值,自动释放池机制就像“栈(stack)”一样,系统创建好自动释放池之后,就将其推入栈中,而清空自动释放池,则相对于将其从栈中弹出。在对象上执行自动释放池,就等于将其放入栈顶的那个池中。

要点:

35. 用“僵尸对象”调试内存管理问题

僵尸对象是调试内存问题的最佳方式,僵尸对象的原理是实现代码深值于Objective-C运行期程序库、Foundation框架、CoreFoundation框架中。系统在将回收对象时,如果发现通过环境变量启用了僵尸对象功能,那么还将执行一个附加步骤,这一步就是把对象转换成僵尸对象,而不是彻底回收,然后再dealloc中通过swizzle生成僵尸对象的代码(NSZombieEnabled环境已打开),关键之处在于对象内存没有(通过调用free()方法)释放,因此这块代码不可复用。

要点

36. 不要使用retainCount

上一篇下一篇

猜你喜欢

热点阅读