《Effective ObjC-2.0》读书总结
📖 Effective 系列之 ObjC 专题摘要
对于 Effective-ObjC 2.0 书中的较为有价值的阅读点做归纳。考虑到全书通过讨论点的方式组织,因此摘要中同样映射原著结构并进一步进行相关的归纳。因为 Effective 系列书籍本身都属于面向对特定语言/技术栈有一定基础并且希望进阶对应技术栈能力的开发人员,因此在阅读本书之前,建议阅读了解 Apple 官方的 ObjC 语法文档以及 Runtime 文档。
1. 语言基础
话题 1: 关于 OC 这门语言
ObjC 与 C++ 相比, 前者的函数调用过程完全是动态的: C++主要通过 function calling, 而 ObjC 则是通过 messaging。前者在编译时决定,后者在运行时通过运行时 函数表进行派发调用。作为 C 语言的超集,提供了对底层 C 语言库集成的无缝支持。这里会涉及到几个细节点:
- 指针在栈中储存,指针所创建的对象在堆中维护
- 结构体(Structs) 以及其他的常见非 Object 类型在栈中储存
- 函数/闭包传入的是栈中的值的 Copy
延伸阅读点: Structs V.S. Class
话题 2: 头文件的管理
ObjC 中的一个最常见的规范既是: 尽可能在最深的层级导入头文件(.m), 因为如果一个头文件一旦在一个头文件导入,那么导入当前的头文件就会连带导入。当头文件中需要引入其他的头文件以使用其中的 class 作为函数出入参数, 或者属性时,可以使用延迟导入的技术:@class xxxx
来在.m 文件中实现对依赖头文件的引导。
内部的头文件处理方法如上,对于一个模块中需要统一对外暴露的头文件来说, 所有的对外头文件都应该统一合并在 umbrella header 中.
延伸阅读点: #import v.s. #include
话题 3-5: 语法规范
本章剩下的话题都围绕一些常见 ObjC 语法小技巧所展开, 这里坐一下进一步总结并提供链接,可以逐个翻阅。
- 当部分语言特征在 C 与 OC 同时支持时,使用 OC 规范,比如:
- 使用 YES/NO 而不是 true/false.
- 使用 Typed Constants 而非 #define.
- 使用便捷的 Literal Syntax 而不是 Constructor 函数来创建常见类型,比如 @“xxx”, @{}, @[]
2. ObjC 运行时
话题 6-7: 关于@Property
原先的 ivar 对像是通过地址偏移量的方式定位 reference 地址. 不过此处在动态更新新的 var 后会导致之前的偏移量失效. 因此通过 property 的方式进行封装。正常情况下从外部或者类内部都建议直接通过 property 进行 ivar 的操作,但也有部分场景依然需要直接使用 ivar:
- 读操作直接使用 ivar 时将提高调用效率,少一层 dispatch 操作。
- 在 setter 中需直接使用 ivar 而不是 property 避免循环调用。
- 建议在 init 与 deallocate 中使用 ivar,避免 subclass 中集成 synthesis call 的情况。
- 当 ivar 需要支持 lazy initialisation 时,需要在 getter 中设计懒加载逻辑处理 ivar 对象。
通常来说,property 或者 ivar 都是定义在类的默认头文件中,如果需要在分类(category)中对类绑定新的数据,则无法直接使用 property/ivar 的方式。此时,可以通过* objc_setAssociateObjects/objc_getAssociatedObjects* 对数据通过 ObjC runtime 提供的AssociateManager进行数据绑定。
延伸阅读点: NSHipster: Associate Objects, Patterns & Anti Patterns
话题 8: 关于 Equality
与 Java 等语言类似的,在 OC 中 ==
代表两个实例在指针层面上相等。当我们希望比较两个不同的实例时,需要正确的使用 isEqual
函数。在 collection 模型中,判断元素的独特性(uniqueness) 都会使用此函数。与 isEqual 函数紧密相关的还有 hash
函数。后者是前者的必要不充分条件。
延伸阅读点: NSHipster: Equality
话题 11-13: 常用 OC 运行时技术
正如在第一部分所提到的, ObjC 相比于 C/C++使用 static binding 的设计,本身采用的 dynamic binding:
“All methods are plain old C functions under the hood, but which one is invoked for a given message is decided entirely at runtime and can even change throughout the course of an app running, making Objective-C truly dynamic.”
那么既然运行时是 OC 如此重要的设计点, 那么无论是面试过程中考察 iOS 从业者的专业度还是实际项目开发的角度来看,往往都会涉及到 OC 运行时的一些常见技术。
关于函数调用 _objc_msgSend: _
在正常情况下的 ObjC 函数调用都会经过此方法并进一步路由到 messageForwarding 等函数组成的调用链,来判断一个函数签名的调用最后由哪一个具体的函数指针做执行:
- 首先当类被发现类的函数调用存在于缓存表中时,使用缓存表中的函数指针。
- 其次在缓存表未集中时,运行时给予类机会动态创建函数的机会:
(BOOL)resolveInstanceMethods:(SEL)selector
- 如果不添加动态执行函数,那么会进入下一步:
-(id)forwardingTargetForSelector:(SEL)selector
, 此处类可以返回一个可以执行 SEL 的对象。并交由这个对象继续函数调用链检查。 - 如果依然无法准确识别能够处理 SEL 的对象,那么会再执行最后一步:
-(void)forwardInvocation:(NSInvocation*)invocation
关于函数替换 method_swizzling:
函数替换时 OC 的另一个非常常见的技术,用于替换 class 中 selector table 对应的函数实现为自己的实现,但不需要集成当前类型或者修改当前的代码实现。函数替换的场景可以应用于集成层监听黑盒二进制模块的内部函数调用,并拦截插入自定义的操作。但值得注意的是多层的函数替换往往会导致业务代码难以维护并最终造成稳定性问题。
函数替换后,往往需要通过以下自循环的方式调用原本的函数指针:
- (NSString*)eoc_myLowercaseString {
NSString *lowercase = [self eoc_myLowercaseString];
NSLog(@"%@ => %@", self, lowercase);
return lowercase;
}
话题 14: Class 的本质
参考 ObjC4 中的class 源码, class 本质上为 objc_class 结构体的指针, 其集成了 isa 指针以及 superclass 指针。
typedef struct objc_class *Class;
struct objc_class : objc_object {
// Class ISA;
Class superclass;
// formerly cache pointer and vtable
cache_t cache;
// class_rw_t * plus custom rr/alloc flags
class_data_bits_t bits;
....
}
struct objc_object {
private:
isa_t isa;
...
}
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
...
}
struct class_rw_t {
uint32_t flags;
uint32_t version;
const class_ro_t *ro; // the rw table internally wraps readonly
method_array_t methods;
property_array_t properties;
protocol_array_t protocols;
Class firstSubclass;
Class nextSiblingClass;
...
}
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif
const uint8_t * ivarLayout;
const char * name;
method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
method_list_t *baseMethods() const {
return baseMethodList;
}
};
延伸阅读点: objc_class 结构, 可参考文章中的方式, 通过自己实现同样的 class 结构体并断点验证以下的结构.
3. 接口设计规范
话题 15: 命名空间
由于 ObjC 本身不会自动添加命名空间, 在不同模块同时被引入项目工程时,会存在一些常见命名的 class 因为同时被多个模块声明所导致冲突的情况, 比如 “utils”. 因此 ObjC 建议模块的维护者为自己的类给予命名空间. (建议三个字母) 比如: IAPUtils
话题 16-19: 函数规范
首先聊聊函数的命名, 对于 ObjC 本身的函数命名风格来说,NSObject 提供了极佳的规范。
- 在函数命名时采用驼峰格式,首字母小写
- 函数通常以(助)动词开头 (hasPrefix/isOpened/checkForUpdate)
- 在参数定义前强制使用 (可选)(${介系词} 或 ${副词}) + ${名词} 的形式: e.g.
[copyScriptingValue:forKey:withProperties:](https://developer.apple.com/documentation/objectivec/nsobject/1410291-copyscriptingvalue?language=objc)
其次, 在初始化一个类型的时候,往往需要我们通过指定初始化函数
来在这个函数中对当前类实例所涉及到的所有变量进行初始化赋值. 一个类可以有多个初始化函数,但都应最终调用指定初始化函数
来完成实例的最终创建。
于此同时,在日常打印日志诊断实例信息的时候,如果使用 NSObject 本身的 description 往往只能打印出当前类的地址。此时可以自己实现 description
函数以便在打印时得到更加完整的信息。
最后,因为 ObjC 中的类对象在作为参数在各个函数/闭包传递的过程中,都是指针的形式。因此如果不对指针所暴露的函数进行限制,往往会存在在当前函数执行的时候,引用对象被其他线程修改的情况从而引起数据一致性问题。因此建议函数传入参数,传出结果都是用不可变类型。
延伸阅读点: Copying With Objects 并了解 NSString/NSMutableString 在何种场景下会使用深/浅拷贝。
话题 20: 错误模型
由于 ObjC 本身中的内存管理机制难以在发生异常后对对象进行正常回收,因此异常只用来表达 fatal error。对于其他程序的非 fatal error,统一使用 NSError 模型来表达。 将 error 指针的指针传入函数后,由函数内部为 error 指针指向的地址赋值。
- (BOOL)doSomething:(NSError**)error;
NSError *error = nil;
BOOL ret = [object doSomething:&error];
if (error) { }
NSError 错误模型被广泛应用在近乎所有的 iOS 官方/非官方项目中,进一步总结一下这个核心模型的几个字段。这里值得注意的是 NSError 使用了 SecureCoding 协议,因此也支持序列化储存:
@interface NSError : NSObject <NSCopying, NSSecureCoding> {
NSInteger _code;
NSString *_domain;
NSDictionary *_userInfo;
}
4. 协议&分类
话题 23, 28: 协议
协议的概念类似于 Java 等语言中的 Interface,用于为特定场景下的不同模块声明各自对应的职责分工并在使用 delegate (协议实现方) 时不需要感知具体协议实现方的类型。 在协议作为 Delegate 模式使用的时候,往往有以下特点:
- 协议可提供必选/可选函数声明。
- 实现 delegate 的协议尽量不要持有宿主的引用。如果必须,则使用 weak 关键字,避免循环引用导致内存泄漏。
- 在宿主使用 delegate 对象时,往往不需要关注具体协议的实现方,因此 delegate 可采用匿名写法进行标注:
id <NSFetchedResultsSectionInfo> sectionInfo = sections[section];
。
话题 24-27: 分类的技巧
分类常用于解决以下场景的问题:
- Category: 当类中函数太多, 可以按照类别把函数放到各自的 Category 当中. 并进一步拆分为 XXX+${Category}.(h/m) 文件。
- Category: 当引入三方库中的模型,希望不需要集成直接扩展类函数的场景下。可使用 Category 进行拓展,并建议在函数名称前统一添加命名空间:
- (NSString*)abc_urlEncodedString;
。 - Class Extension: 一种特殊的分类,当对应的一部分函数需要暴露在工程内部但又不能够对外暴露时,可以在类拓展中定义 method。
Category 不支持添加新属性, 如果一定需要的话,可以通过话题 6-7: 关于@Property中提到的 associateObjects 接口进行 getter/setter 函数实现。
5. 内存管理
话题 29-30: ARC
做为 ObjC 语言中最核心的内存管理概念,ARC 与 MRC 相比,对引用的持有数量进行自动计数。当一个实例的引用数归零后,该对象则被回收. 详细的 ARC 原理可参考苹果官方的Automatic Reference Counting 文档。这里只阐述 ARC 中编译器层面的一些注意事项:
- 在 MRC 模式下的内存计数函数禁止在 ARC 模式下使用: retain/release/autorelease/dealloc。
- 上述的几个函数在 ARC 模式下无法被 override,同时编译器也通过优化绕过了上述函数的调用。
- 在 ARC 模式下的函数如果使用 alloc/new/copy/mutableCopy 打头,那么调用方便默认成为创建对象的 owner,将在调用方的 scope 中由编译器自动插入 release 代码,如下所示:
+ (EOCPerson*)newPerson {
EOCPerson *person = [[EOCPerson alloc] init]; return person;
/**
* The method name begins with 'new', and since 'person' * already has an unbalanced +1 retain count from the
* 'alloc', no retains, releases, or autoreleases are
* required when returning.
*/
}
+ (EOCPerson*)somePerson {
EOCPerson *person = [[EOCPerson alloc] init]; return person;
/**
* The method name does not begin with one of the "owning"
* prefixes, therefore ARC will add an autorelease when
* returning 'person'.
* The equivalent manual reference counting statement is:
*
*/
}
- (void)doSomething {
EOCPerson *personOne = [EOCPerson newPerson];
EOCPerson *personTwo = [EOCPerson somePerson];
/*
* At this point, 'personOne' and 'personTwo' go out of scope,
* therefore ARC needs to clean them up as required.
* - 'personOne' was returned as owned by this block of
* code, so it needs to be released.
* - 'personTwo' was returned not owned by this block of
* code, so it does not need to be released.
* The equivalent manual reference counting cleanup code is:
*
* [personOne release]; <<< 编译器自动插入
*
*/
}
- 属性常见 ARC 关键字: strong/retained, weak, copy, assigned, unsafe_unretained 建议点击链接理解区别,特别是 unsafe_unretained 以外的几个。
- ARC 只对 ObjC 类型生效,对于 CoreFoundation 中的类需要手动调用 CFRetain/CFRelease 进行管理。
话题 31-35, 52*: 常见技巧
过完了 ARC 的概念,那么我们再来看看常见的内存管理小技巧:
“合理释放资源”
- 对于非系统资源,非性能消耗性资源的释放可在 dealloc 函数中进行。(在 ARC 模式下,不需要调用 super dealloc)。
- 对于系统资源,性能消耗性资源,由于 dealloc 触发时机由 GC 决定,因此建议设置单独的生命周期管理函数,提前进行释放。
“Exception 引起的内存问题”
- 在 exception 发生时,默认 ARC 不会为@try/@catch 段自动添加 @finally 并在其中完成对对象的释放。(-fobjc-arc-exceptions 会造成编译器增加过多代码,增大包体积并影响性能)
- 因此在默认编译配置模式下,建议在@finally 中手动释放对象。(xxxx = nil;)
“Weak”
- 老生常谈,使用 Weak 关键字避免循环引用。
“自动释放池”
- ObjC 运行时的自动释放池默认存在于每个线程的 run loop 中,在进入 loop 时(开始执行)创建新的 release pool, 在当前 loop 退出时对自动释放池内的对象统一调用 release。
- 在 ARC 模式下,对于较长的循环语句 for/while loop,可在每次循环中使用@autoreleasePool 来及时为当前循环创建的对象进行释放。降低内存水位线。
延伸阅读点: iOS 自动释放池原理
“使用 Zombie 对象”
- 在对象被释放后对齐所在的地址进行调用时危险的,往往此时会存在野指针问题。ObjC 运行时通过提供编译器选项支持在 debug 模式下当对象被释放时,生成僵尸对象。僵尸对象在被调用时会说明自己在被释放前的类型以及当前什么函数被调用。
- 僵尸对象的原理是利用 ObjC 运行时特性,使用”NSZombie“作为模版创建 “_NSZombie_xxxx”类,并提供该类的 methodList:
// Obtain the class of the object being deallocated
Class cls = object_getClass(self);
// Get the class's name
const char *clsName = class_getName(cls);
// Prepend _NSZombie_ to the class name
const char *zombieClsName = "_NSZombie_" + clsName;
// See if the specific zombie class exists
Class zombieCls = objc_lookUpClass(zombieClsName);
// If the specific zombie class doesn't exist,
// then it needs to be created
if (!zombieCls) {
// Obtain the template zombie class called _NSZombie_
Class baseZombieCls = objc_lookUpClass("_NSZombie_");
// Duplicate the base zombie class, where the new class's
// name is the prepended string from above
zombieCls = objc_duplicateClass(baseZombieCls, zombieClsName, 0);
}
// Perform normal destruction of the object being deallocated
objc_destructInstance(self);
// Set the class of the object being deallocated
// to the zombie class
objc_setClass(self, zombieCls);
// The class of 'self' is now _NSZombie_OriginalClass
“留心 NSTimer 持有 target”
- NSTimer 会持有 target 对象,只有在手动 invalidate 或者对于非 repeat 模式下完成 fire 之后才会释放。
- 对于
repeats: YES
模式下的 timer,请切记手动 invalidate
6. 闭包与 GCD
闭包与 GCD 同为当代 ObjC 变成的支柱概念。GCD 作为很长一段时间的 iOS 官方异步处理主流框架,而闭包作为异步结果/任务执行的主要载体。因此这两个概念在书中的第六部分同时被介绍。
话题 37-40: 闭包及技巧
Block 的基本格式如下,本身作为变量可使用在函数出入参数中。可从其创建的作用域中获得上下文。
NSArray *array = @[@0, @1, @2, @3, @4, @5];
__block NSInteger count = 0;
[array enumerateObjectsUsingBlock:^(
NSNumber *number,
NSUInteger idx,
BOOL *stop
) {
if ([number compare:@2] == NSOrderedAscending) {
count++;
}
}];
// count = 2
其本质上也是一个对象,对象的结构如下,核心为 invoke 字段,指向具体 block 执行时的函数指针。
Block 默认在 Stack 中创建,当 Block 被进行 copy 操作或者作为函数参数进行传递时,Block 将从 Stack 中复制到 Heap。(通过这样的设计防止 Block 在作用域结束后被回收)
理解闭包(Block)之后,进一步整理闭包的使用技巧:
- 使用 typedef 为常见闭包定义别名,提升使用效率。
typedef void(^EOCCompletionHandler)(NSData *data, NSError *error);
- (void)startWithCompletionHandler: (EOCCompletionHandler)completion;
- 对于简单的异步调用函数,使用闭包作为 Handler 接受函数执行更新。避免使用 Delegate 设计模式造成的代码割裂感。(可以简单对比一下 URLSession 中创建 task 的函数如果通过执行完结回调以及 delegate 方式接受结果的开发体验上的差异)
- 对于闭包来说,往往需要注意使用 WEAK 闭包来避免循环引用造成的内存泄漏。
话题 41-42, 44-46: GCD
集中介绍了一些 GCD 使用时需要注意的事项:
- 对于使用 synchronize 关键字的场景,可以考虑使用 dispatch serial queue 代替;这样能够防止 synchronize 获取锁的过程中 block 当前线程或者导致死锁的可能性,又保证了对关键操作的线性执行。
- 对于 concurrent queue 来说,可以通过栅栏(barrier)强制设定在之前的闭包完成执行后再执行栅栏闭包中的任务。
- performSelector 函数由于无法被 ARC 对端返回值是否需要进行释放,因此会造成内存泄漏风险。 于此同时支持的参数格式数量都存在限制。因此更推荐使用 GCD dispatch 系列函数进行替代。
- 老生常谈:使用 dispatch_once 实现 ObjC 单列线程安全的懒加载。
- 可以将各类任务放入相同的 dispatch group 中,执行完毕后通过 dispatch_group_notify 函数监听执行结果:
dispatch_queue_t lowPriorityQueue =
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
dispatch_queue_t highPriorityQueue =
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_queue_t notifyQueue = dispatch_get_main_queue();
dispatch_group_t dispatchGroup = dispatch_group_create();
for (id object in lowPriorityObjects) {
dispatch_group_async(
dispatchGroup,
lowPriorityQueue,
^{ [object performTask]; }
);
}
for (id object in highPriorityObjects) {
dispatch_group_async(
dispatchGroup,
highPriorityQueue,
^{ [object performTask]; }
);
}
dispatch_group_notify(
dispatchGroup,
notifyQueue,
^{
// Continue processing after completing tasks
}
);
话题 43: GCD v.s. NSOperation
“✏️ 面试必考题: ….能对比一下 GCD 以及 Operation 的差异点么?”
如果在未来的职业生涯中,后续被面试官问到,差异点概括一下有以下这些
GCD | Operation Queue |
---|---|
轻量便捷 | 基于 GCD 实现 |
C API | ObjC API |
只能通过 Group 或者栅栏的方式间接支持 | 支持 Operation 之间生命依赖关系 |
没有直接的方式支持同样的能力 | 同一个 Queue 中的 operation 支持优先级排序 |
执行基本单位为闭包,定制能力有限 | 执行单位为 Operation 对象,可深度定制 (比如说支持自定义字段 KVO) |
需要自己实现 Cancel 检查 | 支持任务 Cancel |
7. 其他(碎碎念)
话题 47: iOS 常用框架
介于本书出版的年代较久远, Effective-ObjC 原文中只提到了诸如 Foundation,CoreFoundation, CoreAnimation 等框架的说明。此处针对 Apple 官网当前的Technologies 挑选 iOS 日常应用研发最可能涉及到的框架做总结:
名称 | 描述 |
---|---|
应用层 | |
StoreKit | 苹果应用内支付 |
Authentication Service | 苹果登录,登录凭证信息安全存储 |
Background Tasks | 后台任务处理 |
Bundle Resources | 提供 Bundle 中 Entitlement/Plist 文件格式的解析接口 |
Combine | 当代官方事件流处理框架,与 SwiftUI,Concurrent 搭配食用 |
Contacts (UI) | 获取通讯录联系人信息. 替代历史的 Address Book 框架 |
Core Data | 集合 iCloud/SQLite/KV 存储方案多合一的官方持续化缓存方案 |
Core Location | 地理位置,陀螺仪传感器库 |
Core Animation | 动画库 |
DocC | 编译过程中自动生成文档 |
Foundation | 官方数据类型/网络库/持续化缓存/文字,时间模型处理。 |
SwiftUI | Swift UI DSL 风格框架 |
UIKit | 传统 iOS (Swift/ObjC)UI 组件库 |
User Notifications | 推送消息自定义 |
WebKit | iOS 网络库 |
WidgetKit | iOS/MacOS 各类 widget 组件开发 |
XcodeKit | Xcode IDE 插件研发 |
XCTest | Xcode 提供的单元测试/UI 测试框架 |
底层 | |
ObjC Runtime | 底层 ObjC 运行时接口,为 NSObject 提供实现,支持各种运行时动态特性 |
CFNetwork/Network | 应用层/传输层 C 接口。 |
Core Foundation | Foundation 底层所使用的 C 实现,不支持 ARC |
JavaScriptCore | iOS 框架 JS 运行时支持 |
话题 48: 遍历的几种方式
“……‘回’字有四样写法,你知道么” 《孔乙己》
说来也巧 ObjC 中遍历也有四种方式实现:
- 传统的 for loop 三段式: a. 初始值,b. 终止条件, c. 增量方法
for (int i = 0; i < keys.count; i++) { }
- ObjC 1.0 所支持的 NSEnumerator, 其实与传统 for loop 相比主要是统一了 loop 的调用写法
- ObjC 2.0 开始所支持的 Fast Enumeration 对 for loop 进行了简化
for (id key in aDictionary) { }
- ObjC 2.0 进一步提供 Block-Based Enumeration (Swift Collection 中也同样提供), 在闭包执行时同时提供了 index / key / item 等信息。同时,Block-Based Enumeration 也支持并发遍历。这点的优势时其他几类遍历方法无法提供的 💯
话题 49: 双向桥接
“同样的事情发生在 Swift 与 ObjC 之间 ~”
由于 ObjC 本身也支持 C 语言模型以及函数的使用,并且 Foundation 中的大多数类本身也存在对等的 Core Foundation 实现,因此 Foundation 类中的类往往可以通过双向桥接(toll-free bridging)的方式转换为 CoreFoundation 的类进行使用。这里存在多种桥接方式:
- __bridge_retained 可以将 Objective-C 对象指针转换为 CF 对象指针,并且将该对象的引用计数增加 1,表示新创建或取得了该对象的所有权。
- **bridge_transfer 和 **bridge_retained 相反,可以将 CF 对象转换为 Objective-C 对象,并将其所有权转移给 ARC 管理,也就是将其引用计数减少 1。
- **bridge 用于在 Objective-C 对象和 Core Foundation 对象之间进行转换,但是并不管理对象的所有权。使用 **bridge 进行转换后,将返回一个 CF 类型的指针或者一个 Objective-C 类型的指针,但是不会修改对象的引用计数。如果转换前为 CF 类型,那么依然需要 CF 类型进行 CFRelease 操作, 如果转换前为 Foundation 类型,那么则自动交给 ARC 做处理。
拓展阅读点: when to use: bridge/bridge_transfer/__bridge_retain
话题 50: 缓存 v.s.字典
“鱼与熊掌”
NSDictionary 为广大 iOS 研发在初学阶段就会接触的数据模型,然而在特定场景下 NSCache 会是更加适合的替代 品。这两者的特性总结如下:
NSCache | NSDictionary |
---|---|
- NSCache 是一个可变类,与 NSDictionary 一样存储键值对,但具有更多的优势。 | - NSDictionary 是一个不可变类,这意味着它创建后其内容不能更改。这使它适合存储静态和不变的数据。 |
- 当设备内存紧张时,NSCache 会自动删除对象,帮助减轻应用程序对内存的压力,从而避免发生内存警告或崩溃。 | - 它使用哈希算法来快速找到键值对,从而快速访问值。 |
- NSCache 还具有可配置的总内存和最大对象数限制;当达到其中一个限制时,缓存会自动丢弃较旧的对象。 | - NSDictionary 是非常轻量级的类,比其他集合类消耗更少的内存 |
- NSCache 线程安全 | - NSDictionary 非线程安全 |
- 可以通过 countLimit / totalCostLimit 来控制 触发缓存时机。 | - 手动管理 |
- 实现重 | - 实现轻量 |
话题 51: ‘加载’与’初始化’函数
在 ObjC 中,每个类都有一个名为“load” 的方法和一个名为“initialize”的方法。这些方法在类被加载到内存时自动调用。类加载的顺序是 undefined 的,并且无法控制。但是,类的“+load”方法总是在“+initialize”方法之前调用:
- Load 往往在程序启动的时候调用 (non-lazy)。 Load 的执行线程为主线程。
- Initialize 则在 Class 收到第一条消息前调用 (lazy)。Initialze 的执行线程取决于当前 Class 第一次在什么线程,可以为主线程以外的其他线程。
- 注意如果需要定制以上两个函数,请让相关的同步执行链路尽可能轻量,保证程序的顺畅运行。