0. 前言
- 这是一本传说中不看完不让转正的书。瑟瑟发抖系列。
- 并不适合作为入门书籍,更像是实战经验总结。
1. 熟悉OC
1.1 尽量少在头文件中import
- 减少编译时间
- 避免循环引用,如果A B互相import,虽然不会造成死循环(不是include),但是两个文件里面有一个会编译失败
- 协议尽量单独拆分,方便别的文件进行引用
- 可以使用@class进行向前声明
1.2 多用语法糖(字面量语法)
use: @(1);
instead: [NSNumber numberWithInt:1];
类似的语法有:
NSString *str = @"1";
Person *person = people[0];
NSDictionary *dict = @{@"key":@"value"};
1.3 使用常量类型,少用Define
- define有被重复覆盖的可能
- define没有类型检查,即使被更改了类型也不会有警告
how to use:
- 在头文件中extern const
- 在.m中定义
- 使用k开头,与一般变量/常量进行区分
xxxxx.h
extern NSString *const kGlobalA;
xxxxx.m
NSString *const kGlobalA = @"test string";
2. 对象 消息 运行时
2.1 消息转发机制
在OC中,如果向某个对象传递消息,使用的是动态绑定技术
- msg_send
- 实例接受消息
- 当前不能处理,isa寻找父类
- 遵循继承链都不能处理,做消息转发(forwarding)【- +方法都可以进行转发】
2.2 Method Swizzling
通过交换IMP,将selector映射到不同的方法实现上。
//取出方法:
class_getInstanceMethod(Class class, SEL selector)
//交换方法:
class_exchangeImplementations(method1, method2)
2.3 理解类对象
- 对象是分配在堆空间上的,一个“*”代表一个内存地址
- 对象不能分配在栈空间上,例如以下就是非法的:
// 尝试分配在栈空间
NSString str = [NSString xxxxxx];
- id可以指代任意对象,本身就是一个指针
3. 接口与API设计
3.1 使用前缀来避免命名冲突
- 重名时,app的链接过程会报错。
- 重名发生在动态链接时,会引发crash
- 可以使用适当的前缀来避免库之间、app之间的重名问题。
需要注意的是,Apple保留其使用双字幕前缀的权利,所以我们最好选用三字母作为前缀
eg: 用项目名称的简写来命名,QQXXXViewController QRXXXXXView等
使用前缀的好处:
- 避免重名引发的编译问题/运行时问题
- 在回溯问题时,可以比较方便地定位代码块
3.2 重写对象的description方法
- 直接NSLog打印对象/在断点时po对象,看不到具体的属性信息
- 重写description方法,格式化输出对象信息
- 当然,最好保持原有的打印体验
// for NSLog
- (NSString *)description {
return [NSString stringWithFormat:@"<%@: @p, %@>
[self class], // 类名
self, // 地址
@{@"title" : self.title,
@"name" : self.name,
...}
"];
}
// for po in lldb
- (NSString *)debugDescription {
// detail description
}
3.3 尽量使用不可变对象
声明property时:
- 在.h中时指定 readonly
- 在.m中使用 raedwrite
3.4 使用合理的方法命名
- 清晰
- 可以稍长,但是不要啰嗦
// better
- (CGFloat)area;
// worse
- (CGFloat)calculateTheArea;
- 范围值为BOOL时,方法名一般以is或者has开头
- (BOOL)isEqualToString:
- (BOOL)hasPrefix:
- 方法中尽量不要使用缩略词,除非是默认熟知的词汇(eg,MD5)
3.5 为私用方法名加上前缀
Apple喜欢用单下划线作为私用方法的前缀。因此,我们应该尽量避免以“_”作为方法的开头
- 便于区分方法是否被外部调用(如果方法实现有调整,被外部使用的方法需要全量过一遍,对减包来说同理)
3.6 理解OC的错误模型
ARC下不是“异常安全的(Exception Safe)”
如果抛出异常,那么本应在作用域末尾释放的对象就不会自动释放了
如果想要异常安全,需要加上编译选项 -fobjc-arc-exceptions
- 异常应该只作用于极其严重的错误(比如放在某个重要基类的init方法中)
- OC中处理一般错误,可以返回nil/0,业务层做校验。一些比较重要的路径,可以asset一下。
- 对于一些自定义类,在处理错误的时候,可以使用delegate将错误信息返回给调用者
3.7 理解NSCopying
- 如果想让自己实现的类有copy操作,需要实现NSCopying协议。该协议只有一个实现方法:
- (id)copyWithZone:(NSZone *)zone;
- 当然,也有可变版本的mutableCopy方法:
- (id)mutableCopyWithZone:(NSZone *)zone;
- 可以根据自己的需要实现不同的协议
需要注意的是,Zone这个概念是个历史遗留问题。在以前的开发时,会将内存分为不同的区域(Zone),而对象会创建在特定的区域中。现在只有一个区(默认区,default zone)的概念。现在仍使用这个协议进行copy的重写,但是无需关心zone的概念了。
4. 分类与协议
4.1 使用委托与数据源/协议进行对象间通信
- delegate对象需要使用weak修饰(使用strong会造成retain cycle)
- 类内部调用委托方法进行信息传递的时候,最好使用一下
respondsToDelegate:
,判断一下delegate是否真的实现了这个协议方法,防止crash
4.2 使用分类来分离臃肿的代码
- 将类的方法划分成易于管理的小模块
- 可以覆盖主类的方法实现
- 不同分类间的方法可以重名,后编译的会被调用
4.3 使用分类来扩展第三方类时加上前缀
如 4.2
中所述,通过加上自己的前缀,可以避免方法/分类名重复
@interface NSString(ZK_Debug)
// some code
@end
4.4 勿在分类中声明变量
分类中无法声明新的属性,这与分类的实现是息息相关的。
// 分类结构体 源码实现
struct category_t {
const char *name;
classref_t cls;
struct method_list_t *instanceMethods; // 对象方法
struct method_list_t *classMethods; // 类方法
struct protocol_list_t *protocols; // 协议
struct property_list_t *instanceProperties; // 属性
// Fields below this point are not always present on disk.
struct property_list_t *_classProperties;
method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}
property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};
从源码基本可以看出我们平时使用categroy的方式,对象方法,类方法,协议,和属性都可以找到对应的存储方式。并且我们发现分类结构体中是不存在成员变量的,因此分类中是不允许添加成员变量的。分类中添加的属性并不会帮助我们自动生成成员变量,只会生成get set方法的声明,需要我们自己去实现。
一些iOS底层的知识,例如对runtime、category、block、runloop等的详细解析,推荐@xx_cc在简书上的这一系列文章
https://www.jianshu.com/notebooks/24110540
- 不要在分类中添加属性
- 可以在主类中定义,分类中使用
5. 内存管理
OC的内存管理是基于引用计数机制来实现的。有了ARC之后,几乎所有的内存管理工作都交给了编译器,开发者可以更关注于业务逻辑。
从MacOS 10.8开始,垃圾收集器(garbage collector)被正式废弃;对iOS来说,这个概念是从未被引入的。
5.1 理解引用计数
- retain 递增引用计数
- release 递减
- autorelease 等autorelease pool来进行递减操作
当引用计数为0时,会将对应内存标记为reuse,所有指向对象的引用都无效
- 计数为0,内存不一定立即被回收。
- 如果在执行后续代码时,未覆写对象内存,不会crash
- 一般在release后执行一下置空操作
[object release];
object = nil;
5.2 在dealloc中释放引用&解除监听
[[NSNotificationCenter defaultCenter] removeObserver:self];
5.3 善用Autorelease Pool降低内存峰值
- App生命周期中会自动持有一个autorelease pool
- 在遇到for里大量创建临时对象时,可以使用autorelease pool,来降低内存峰值
for (int i = 0; i < 100000; i++) {
@autoreleasepool {
// code here
}
}
5.4 使用Zombie Object来调试内存管理问题
启用Zombie Object后,在运行期,系统会将所有已经收回的实例转化为僵尸对象,而不会重新回收它们;这种对象的内存地址不会遭到覆写。在向僵尸对象发送消息时,会抛出异常,App crash。
实现方式:
- 通过对dealloc方法进行method swizzle。
- 目的是为了保留原类名(LOG的时候能区分出来具体是哪个类)
原理:
- 类似KVO,系统会自动创建以
_NSZombie_
开头的类,通过修改isa来指向新类 - 新类会将整个类结构copy一份
- 新类会响应原有类的所有selector,但是会打印出消息内容,app crash
6. Block & GCD
Block是可在C、C++、OC中使用的语法闭包,开发者可以将代码像对象一样传递。
GCD是一种与Block有关的技术,基于dispatch queue,提供对线程的抽象
6.1 理解block
// block的定义
return_type(^block_name)(paramters) {
//code here
}
以下是一个实例:
// define
int (^addBlock)(int a, int b) = ^(int a, int b) {
return a+b;
}
// imp
int sum = addBlock(a, b);
- 在block声明范围内的所有变量,均可以被block捕获;但是如果需要在block内改变它的值,则需要加上
__block
前缀。 - 要注意避免retain cycle,对于self来说,可以定义
__weak
- block可以分配在栈或堆上, 也可以是全局的
- 栈上的block可以通过
copy
,移至堆中。
6.2 为常用block进行typedef
如果我们有网络框架,回包数据通过block传递给上层,我们可能会这么写:
- (void)requestWithCompletion:(void(^)(NSData *data, NSError *error));
如果封装了若干个方法,回包结构又是一致的,我们可以typedef一下block,使方法看起来可读性更强。
typedef void(^completeBlock)(NSData *data, NSError *error);
- (void)requestWithCompletion:(completeBlock)completeBlock;
- 增强代码可读性
- 后续如果需要扩展block,直接修改定义的地方,可以通过触发编译错误的方式,快速定位所有需要修改的方法实现。
6.3 使用block来降低代码分散程度
对比的对象是delegate
。
如我们熟悉的tableView和早期的UIAlertView组件,都是使用delegate来完成一些相关逻辑的。
缺点在于,组件创建、展示和业务处理的代码,是分离开的。且同一种类型对象的业务逻辑,需要在delegate中使用if-else进行区分,久而久之代码比较臃肿。
- 开发者可以主动选择在什么线程上执行代码
- 在创建的时候声明后续业务逻辑,更完整也更简洁
6.4 多用GCD,少用同步锁
在多线程问题的时候,往往需要对一些数据进行“加锁”。
- @synchronize()
- NSLock
- NSRecursiveLock
- ...
可以使用GCD来进行优化。(对synchronize来说,不滥用即可)
- 使用串行同步队列
- 混合使用同步和异步GCD
- 加入队列和barrier,可以掌控同步时序,提升效率
6.5 掌握GCD及NSOperationQueue的使用时机
GCD:
- GCD并不是永远都是最佳解决方案
- GCD是基于C的API
NSOperationQueue:
- NSOperationQueue是比较重量级的封装,是OC对象。
- 面向对象封装,使用起来更直观简单
- 底层使用GCD实现
- 在各种设置项上更简单(线程池,并发数,线程优先级)
需要注意的是,NSNotificationCenter内部是使用NSOperationQueue的。
6.6 使用dispatch group
// 创建
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// mission1
dispatch_group_enter(group); //group引用计数+1
dispatch_async(queue, ^{
//code here
dispatch_group_leave(group); //group引用计数-1
});
// mission2
dispatch_group_enter(group);
dispatch_async(queue, ^{
//code here
dispatch_group_leave(group);
});
使用dispatch_group后,以下是两种继续执行的方式:
- 当前线程等待,直到group全部执行完再往下执行
//...
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
//code here...
- 当前线程继续往下执行;当group全部执行完成后,执行block
dispatch_group_notify(group, queue, ^{
// code here...
});
6.7 dispatch_once 只执行一次的线程安全代码
最典型的代表:【单例】
+ (instancetype)sharedInstance {
static QRAccountManager *__QRAccountManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
__QRAccountManager = [QRAccountManager new];
});
return __QRAccountManager;
}
-
dispatch_once_t
类似token,可以唯一标识block - 使用
dispatch_once
的效率是@synchronize
的两倍
6.8 不要使用dispatch_get_current_queue
- iOS6.0起已经废弃
- 该函数的行为往往与预期不一致
- 可以做调试,但是尽量避免使用。
7. 系统框架
7.1 熟悉系统框架
- Foundation
- 为什么是NS开头?将OC用作是NeXTSTEP操作系统(乔老板被赶出去建立的公司的产物,OSX的前身)的编程语言时确定的
- CoreFoundation:在里面,很多Foundation的功能可以找到对应的C语言API
- CFNetwork:对BSD socket的封装,提供网络接口
- CoreAudio
- AVFoundation
- CoreText:文字渲染和排版
在使用OC时,会经常用到C语言级别的API。好处是可以绕过OC的运行时,提升执行速度;缺点是,使用C语言级别API,需要自己管理内存。
一些其他的框架:
- Appkit
- UIKit
- CoreAnimation
- CoreGraphics
- QuartzCore
- MapKit
- SceneKit
- CoreML
- ...
7.2 多用Enumberator,少用for
- 几乎所有的collection都可以用这个套API
- 有一些快速遍历的接口(FastEnumberator)
- 本身就可以通过GCD来进行并发操作
7.3 toll-free bridging
toll-free bridging,无缝桥接,是Foundation和CoreFoundation之间的粘合剂。使用这个技术,可以将两个框架之间的对象进行转换
NSArray *anNSArray = @[@"1", @"2", @"3"];
CFArrayRef aCFArray = (__bridge CFArrayRef)anNSArray; // OC -> C
-
__bridge
表示ARC仍具有OC对象的使用权 -
__bridge_retain
则表示ARC需要交出OC对象的控制权,开发者自己使用CFRelease()来手动释放 -
__bridge_transfer
,CFArrayRef --> NSArray
7.4 构建缓存的时候选用NSCache而非NSDictionary
- NSCache在系统资源紧张的时候,会自动删除缓存(Dict则需要在内存警告中进行重写)
- NSCache会优先删除最久未使用的对象
- NSCache本身是线程安全的
开发者是可以手动控制缓存删除内容的时机:
- 缓存中的对象总数
- 所有对象的总开销
想通过调整“开销”来迫使缓存优先删除某对象,不是个好主意。
不想要这种不确定性,还是用dict,手动管理好内存峰值比较合适
_cache = [NSCache new];
_cache.countLimit = 100; // 对象上限
_cache.totalCostLimit = 5 * 1024 * 1024; // 开销上限,5M
7.5 精简initialize与load
+(void)load;
- App启动时,对于运行时的每个class和category,必定会调用此方法
- 仅调用一次
- 执行子类的
+load
前,必定会执行父类的+load
- 原类的优先于分类执行
- 并不完全遵从继承规则(子类没有实现
+load
,各级超类的+load
不会执行)
实现的时候,应该精简一些:
- 会在App启动的时候执行
- 尽量不要使用锁
- 不要在里面使用别的类,因为有可能还没有被加载
+(void)initialize;
- 程序首次使用某个类前调用(惰性调用,使用类时才调用)
- 仅调用一次
- 该方法是线程安全的
- 如果子类没有实现,会自动调用父类的方法(如果父类实现了)
- 一般在initialize里面加上
if (self == [XXXClass class])
,来特定
实现的时候,也应该精简一些:
- 不好掌控触发时机
- 不宜使用锁,避免阻塞线程
网友评论