文章主要提取一些总结性意见,没有对具体技术点做分析,概念性比较强。适合有一定内存管理、runtime底层源码了解的小伙伴。
Part1:让自己习惯Objective-C
1、视 Objective-C 为一门动态语言
Objective-C是动态语言,C++是静态语言。静态语言的执行效率和安全性比动态语言高,但灵活性没有动态语言高。
- 消息查找和转发机制(函数是否实现、协议是否实现等)
- 类型检验(弱类型校验、空类型校验等)
2、在头文件中减少其他头文件的引用
头文件中,过多的 import 会影响编译效率,如果有新的 import 链路,则会导致整个链路上的文件都需要重新编译。
- 如果可以,在(.h)使用 @class 标记类,在(.m)中 import 文件;
- 避免“类循环依赖”;
- 在设计类时,尽量多采用协议,避免 #import 过多,引入不必要的依赖;
- 如果(.h)中有多个类的定义,尽量采用模块方式,只针对引进所需要的类;
3、尽量使用 const、enum、static 来替换预处理 #define
- 过多的 #define 会影响程序的编译速度;
- 过度的 #define 会隐藏实现细节,不利于调试;(例如对某个业务方法的包装)
- 宏定义 #define 会隐藏变量类型,不利于使用;
- 对于字符串
static NSString * const AFURLSessionManagerLockName = @"com.alamofire.networking.session.manager.lock";
- 对于方法
static dispatch_group_t url_session_manager_completion_group() {
static dispatch_group_t af_url_session_manager_completion_group;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
af_url_session_manager_completion_group = dispatch_group_create();
});
return af_url_session_manager_completion_group;
}
- 对于整型
static NSUInteger const AFMaximumNumberOfAttemptsToRecreateBackgroundSessionUploadTask = 3;
也可使用枚举
typedef NS_ENUM(NSInteger, UITableViewCellStyle) {
UITableViewCellStyleDefault, // Simple cell with text label and optional image view (behavior of UITableViewCell in iPhoneOS 2.x)
UITableViewCellStyleValue1, // Left aligned label on left and right aligned label on right with blue text (Used in Settings)
UITableViewCellStyleValue2, // Right aligned label on left with blue text and left aligned label on right (Used in Phone/Contacts)
UITableViewCellStyleSubtitle // Left aligned label on top and left aligned label on bottom with gray text (Used in iPod).
};
4、优先使用对象字面量语法而非等效方法
- 尽量使用对象字面量语法来创建字符串、数字、数组和字典等;
- 对象字面量语法特性完全是向下兼容;
- 在数字和字典中,要使用关键字和索引做下标来获取数据;
- 使用对象字面量与法时,容器类的不可为nil,否则运行时会抛异常;
使用字面量语法特性
NSNumber* number = @1;
NSArray* array = @[@"1", @"2"];
NSDictionary* dict = @{
@"key1" : @"value1",
@"key2" : @"value2"
}
不使用这类复杂的,不利于阅读的方法
NSNumber* number = [NSNumber numberWithInt:1];
NSArray* array = [NSArray arrayWithObjects:@"1", @"2", nil];
NSDictionary* dict = [NSDictionary dictionaryWithObjectsAndKeys:@"value1", @"key1", @"value2", @"key2", nil];
容器类的值不能为nil
parameters:@{
@"key1" : [NSString excludeNil:value1],
@"key2" : [NSString excludeNil:value2],
@"key3" : [NSString excludeNil:value3],
@"key4" : [NSString excludeNil:value4],
@"key5" : [NSString excludeNil:value5],
}];
5、处理隐藏的返回类型,优先选择实例类型而非id
- 方法的返回类型,使用
instanceType
而不要使用id
; - 将类型的校验提前到编译期,避免在运行时因为类型不匹配造成的崩溃;
-
instanceType
不能完全替代id
;(例如:不能直接用instanceType
来作为类型声明变量)
6、尽量使用模块方式与多类建立复合关系
- 对于#include 和 #import,其根本就是简单的复制、粘贴,将目标.h文件中的内容一字不落地复制到当前文件中,#import 可以避免多次的重复引用;
- 以预编译头文件(.pch)的方法,虽然可以缩短编译时间,但后期维护会非常困难,不建议广泛使用;
- 开启模块功能,不仅仅会加快编译速度,在链接框架的表现也是非常优秀的;(
Enable Mudules (C and Objective-C)
默认是开启的,目前只支持系统Module,暂不支持自定义Module——需要确定时限) - 启用模块功能之后,编译器会隐式地把所有 #import 都转换成 @import;
导入模块
@import UIKit;
@import MapKit;
导入模块中的部分内容
@import UIKit.UIView
7、明解 Objective-C++ 中的有所为而所有不为、
- C++ 和 Objective-C 在定义结构上一样,但是后者的继承是封闭的;
- Objective-C 接口中定义的 C++ 类是全局范围的,而不是 Objective-C 类的内嵌类;
- C++ 和 Objective-C 的对象模型不能直接兼容。与 Objective-C 不同的是,C++对象是静态类型的,有运行时系统多态的特殊情况;
- C++ 和 Objective-C 有词汇歧义和冲突;
- C++ 和 Objective-C 两者功能上有限制。Objective-C++没有为 Objective-C 类增加 C++ 的功能,也没有为 C++ 类增加 Objective-C 的功能。
Part2:数据类型、集合和控制语句
1、C 语言和 Objective-C 语言的关系是充分而非必要条件
- C 语言的基本语法在 Objective-C 语言中是可用的;
- 与 C 语言相比,Objective-C 语言又定义了新的基本数据类型,如 BOOL 等;
- Objective-C 值对象比 C 类型变量具有封装常用操作的优势,但在数值计算中,使用 C 类型标量更为简洁;
2、高度警惕空指针和野指针的袭击
- 空指针(NULL指针):没有存储任何内存地址的指针,未被具体初始化的指针;
- 野指针(悬垂指针):指向“垃圾”内存、不可用内存的指针;
- 利用野指针发送消息,很容易造成崩溃;
- 利用空指针发送消息,不会有任何问题;
3、在 64 位环境下尽可能利用标记指针
-
指针地址对齐
- 指针8字节对齐
- 内存16字节对齐(一个对象所占最小内存为16字节)
64 位地址,iOS采用的是小端序,所以内存地址的前面会有很多0
标记(Tagged)指针,64位的空间仅用来存储地址,会有很多浪费,所以就会忘指针中塞入一些特殊属性或信息,例如 isa 指针;
标记(Tagged)指针对 NSNumber 的优化,对于NSNumber这类小对象,不会直接给他分配内存,而是将他的值存入到指针里面
分析案例
NSNumber* number3 = @3;
NSNumber* number4 = @4;
NSNumber* number9 = @9;
NSLog(@"number3 pointer is %p", number3)
NSLog(@"number4 pointer is %p", number4)
NSLog(@"number9 pointer is %p", number9)
64位结果
number3 pointer is 0xb000000000000032
number4 pointer is 0xb000000000000042
number9 pointer is 0xb000000000000092
- 前4位:0xb
- 后4位:0x2:指针的Tag
- 中间56位:存储就是这个number的值
long类型8字节,64位,所以长整型的值有可能超过标记(Tagged)指针所能表示的范围
numberBig pointer is 0x109426a0
- 0x109426a0:表示8位地址,是一个指针
- 后四位:0x0,可以用此来判断系统是否给标记(Tagged)指针分配内存
- 标记(Tagged)指针对 isa 的优化,在 64 位环境下,isa 指针的标记位只用到了 33 位,有 19 位用来保存对象的引用计数。所以对象的引用计数超过19位之后,才会将引用计数保存到外部的引用计数表;
4、紧急兼容 32 位和 64 位环境下代码编写事项
苹果已经抛弃了32位架构
- 不要将长整型数据赋值给整型;
- 使用 NSInteger 来处理 32 位和 64 位之间的转换;
- 创建数据结构要注意固定大小和对齐;
5、清楚常量字符串和一般字符串的区别
常量会占用一块特殊的代码块,加载到内存时会映射到一块常量存储区,以加快访问速度。
- 由于编译器的优化,相同内容的常量字符串的地址值是完全相同的;
- 如果使用常量字符串来初始化一个字符串,那么这个字符串也将是相同的常量;
[[NSString alloc] initWithString:s2]
,alloc 出来的那个对象会被立即释放,然后将 s2 的地址直接赋值给等号左边; - 对常量字符串永远不要 release;
- 标记(Tagged)指针对字符串的优化,对于字符串长度小于一定长度(可能是9)的字符串,会直接将值存在这个标记(Tagged)指针中,类似于 NSNumber 的存储逻辑;
6、在访问集合时要优先使用快速枚举
- 使用快速枚举,要尽可能使用枚举新的写法;
- 和直接使用NSEnumerator 相比,使用快速枚举更高效、简洁;
- 使用快速枚举更安全,枚举会监控枚举对象的变化,如果在枚举过程中对象发生变化会抛出一个异常;
- 多个枚举可以同时进行,因为在循环中被循环对象是禁止修改的;
7、有序对象事宜存于数组,无序对象事宜存于集合
- 数组可维持持续性,故事宜存储有序的对象,但每一项必须是Objective-C对象。集合不维持次序,故事宜存储无序对象;
- 统一数组可以保存不同的对象,但不能存储 float、int、double 等基本类型和 nil,否则存储基本类型都会被置为 0,不能存储 nil 是因为数组必须用 nil 来结尾(就像 C 字符串中的 '\0';
- 使用枚举是访问数组中的对象的一种比较快的方法;
- 使用数组和字典类可以直接将其内从容写入磁盘进行持久化;
8、存在公共键时,字典是对象之间传递信息的绝佳方式
- 字典的 key 可以是任何对象遵循 NSCoping 协议的对象,但只有 key 为字符串时才满足 NSKeyValueCoding 协议,所以一般情况下 key 都用字符串表示;
- 字典不仅可以作为无需对象的集合,还可以作为有序对象的集合;
- 字典可作为有序对象的集合,主要依赖于键值可采用有序(一般不这么用);
- 存在公共键时,字典是在对象之间传递各种信息的绝佳方式;
9、明智而谨慎地使用 BOOL 类型
- 整型转为BOOL型时,使用三元操作符,以保证返回 YES 或 NO值;
- 整型转换为 BOOL 型的时候要避免直接合 YES 作比较;BOOL的类型实际为
unsigned char
,如果直接对整型、指针等类型强行转为 BOOL时,有可能出现值不为0,但被转为 NO 的情况;(有待确认) - BOOL 值进行逻辑运算(&&、|||、!不但有效,而且还可以确保返回值安全地转为 BOOL 型,无需三元操作符);
Part3:内存管理
1、理解内存和 Objective-C 内存管理规则
理解内存
内存可以看做是内存控制器与 CPU 之间的桥梁
内存两个指标:内存容量和内存速度(内存带宽)
理解alloc/new/copy/retain/weak/release/autorelease/retaincount相关原理
理解pagefault、symbol、对象原理、虚拟地址表、编译与link过程
- 内存可以看做是内存控制器与 CPU 之间的桥梁,内存也就是相当于“仓库”;
- Objective-C 内存管理模式基于对象的“所有权”上。任何对象都会被一个或多个使用者引用,只要对象还有一个使用者,该对象就会继续存在。如果对象没有使用者了,系统就要自动销毁它;
- 对象所有权策略是基于引用引用计数实现的,每一个对象有一个 retaincount 变量。
- 理解循环引用的几个场景,养成良好的习惯,避免造成循环引用;
- MLeaksFinder 工具对内存泄露的检查;
2、内存管理讲究“好借好还,再借不难”
目前开发环境已经从 MRR 过渡到了 ARC 机制下,对于 Objective-C 对象,我们不需要手动去管理内存了
系统工作的简单过程:CPU 接收到指令后
- 向 CPU 中的一级缓存(L1 Cache)寻找数据,一级缓存是与 CPU 同级运行的,容量小,命中率不高;
- 如果未命中,则向二级缓存(L2 Cache)中寻找;
- 若仍未命中,则向三级缓存(L3 Cache)中寻找,(如果有三级缓存的话)
- 然后就是 内存 -> 硬盘中寻找;
- 对于 C 方法,直接操作“堆”内存上的方法,还是需要程序员手动管理的,例如:
malloc
-free
- 对于一些
Core Graphic
中的对象,要习惯手动释放; - 对于一些持续生成大量对象的方法,我们可以考虑将他们加入 autoreleasepool 中,避免内存出现短暂的暴增现象;
3、区别开 alloc、init、retain、release和dealloc之间的区别
以下建议都是在 MRR 模式下的一些建议,有助于在 ARC 环境下对内存的理解
- alloc 是创建变量,dealloc 是释放变量,retain 是计数加1,release 是计数减1;
- 分配过程(alloc 和 init)不仅进行对象的内存分配,还会对它的 isa 指针和 refcount 初始化;
- 对象赋值时尽量采用 autorelease 而不是 retain;
4、优先选用存取方式来简化内存管理
以下建议都是在 MRR 模式下的一些建议,有助于在 ARC 环境下对内存的理解
- 使用存取方法管理内存,可大大降低管理方面出现的问题;
- 在代码中,管理实例变量尽可能避免全部都用 retain 和 release,降低错误的发生概率;
- 在 init 方法和 dealloc 两处地方,不要使用存取方法来设置实例变量;
- 实现方法的重置。有两种选择,一种是使用简便构造函数创建一个新的 NSNumber 对象,因此没有必要发送任何 retain 或 release 消息;另一种使用 alloc 创建 NSNumber 实例,要相应地使用 release。两种方法都使用了类的 set 存取方法;
5、对象销毁或者被移除一定考虑所有权的释放
以下建议都是在 MRR 模式下的一些建议,有助于在 ARC 环境下对内存的理解
- 从集合中移除对象,集合要释放对被移除对象的所有权;
- 防止出现福对象被释放前而子对象没有被释放;
- 释放对象前,要确保其他对象对该对象的所有权已经释放;
- 在 Objective-C 中,是否负责对象的释放,需要看如何获取的对象,即要看对象的所有权策略;
6、明智而谨慎地使用的 dealloc
- 任何时候,都不要直接调用另一个对象的 dealloc 方法;
- 不许在 dealloc 的最后一行调用父类的 dealloc;
- 不要尝试管理系统资源。应用程序终止时,对象的 dealloc 可能不会被调用。因为进程的内存是自动清除退出,让操作系统清理资源比调用所有的内存管理方法更为有效;
Part4:定制 init 和 dealloc
1、了解对象的 alloc 和 init
- alloc 方法使用应用程序默认的虚存区。区是一个按页对齐的内存区域,用于存放应用程序分配的对象和数据;
- alloc 分配过程不进进行对象的内存分配,还会初始化对象的 isa 指针和 refcount 属性;
- 子类可以不采用带参数的初始化方法,而是实现一个简单的 init 方法,并在初始化后马上使用“set”存取方法,将对象设置为有用的初始状态;
- 工厂方法则可以避免为可能没有用欧冠的对象盲目分配内存;
2、直接访问实例变量的 init 方法
以下建议仅供参考,在相对早期的开发习惯中比较常见
- 应始终从内初始化方法来直接访问实例变量,因为在设置属性时,该对象的其余部分可能尚未完全初始化;(现在更多的建议是对象的懒加载,或者是通过自动生成的 set 方法进行赋值)
- 父类可能无法正确初始化的对象,并返回 nil,故要经常检查、确保 self 不为 nil,然后再执行自己的初始化;
- 继承一个类要使用多个初始化方法时,写初始化方法,要老驴到重写父类指定的初始值设定项来执行自己的初始化,或添加自己的附加初始值设定项;
3、初始化方法必须以 init
前缀开头
- 如果一个对象没有实现自己的初始化方法,Cocoa 就会调用其最近的祖先对象的方法;
- 对于不需要初始化其他数据的自雷,重载 init 方法就可以了,但是常见的情况是初始阶段需要根据外部的数据来设置对象的初始状态;
4、从 init
方法得到的对象可能是不想要的
-
init
方法得到的对象可能不是预期的、正在被初始化的对象; -
init
方法并不是一定能执行其他对象请求的初始化; - 在创建对象时,通常应该在处理之前检查返回值是否为 nil;
- 一旦对象被初始化了,就不应该再进行初始化,否则会抛出异常;
5、实现 init
方法的唯一性或指定性并非“不可能”
相关技术点
msg_send 中隐藏的两个参数:self 和 SEL
runtime 消息的查找和转发流程
- 实现
init
方法的唯一性或者指定行并非“不可能”,确保最终出口方法; - 调用 super 的初始化方法可以确保继承链上方的类定义的实例变量都率先得到初始化;
- 在创建子类时需要关注通过集成得到的初始方法,大部分时间需要扩展自己的特性,一般都需要重载
init
方法;
6、init
方法有“轻重级别”之分
- 进行对象的初始化,要注意轻重之分;
- 指定初始化最终一定会调用 super 的某一个初始化方法,最终也一定会调用到根类的
init
方法,如果没有,可能会初始化失败,编译器也会抛出相应警告;
总结
“故常无,欲以观其妙;常有,欲以观其徼”
《道德经》——开篇
在《道德经》里,天地是从“无”中诞生的,而“有”又是天地相互作用的产物,代表着万物,所以对于存在的事物,我们观摩其外观、轮廓,对于不存在的(或看不见、摸不着的)事物,我们观摩其奥妙、玄妙。同样的,在 objc编程之道——上篇
中主要讲述的是“有”这一部分,譬如“尽量使用 const、enum、static 来替换预处理 #define”、“优先使用对象字面量语法而非等效方法”、“定制 init 和 dealloc”等。对于内存管理,我们可以直接通过控制台、符号表来追踪相关信息;而对于runtime,我们也可以追踪消息查找和转发的过程,所以是“常有,欲以观其徼”,我们从静态代码层面来规范我们的代码,提高我们的编码质量。在 objc编程之道——下篇
中主要描述了设计、思想,也就是“常无,欲以观其妙”。
本文参考:
《编写高质量代码:改善Objective-C程序的61个建议》(可在微信读书上获取)