提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 前言
- 熟悉系统框架
- 多用块枚举,少用for循环
- for循环
- 使用Objective-C 1.0的NSEnumerator遍历
- 快速遍历
- 基于块的遍历方式
- 对自定义其内存管理语义的collection使用无缝桥接
- 构建缓存时选用NSCache而非NSDictionary
- 精简initialize与load的实现代码
- 别忘了NSTimer会保留其目标对象
前言
系统框架:
如果不使用Foundation,就必须自己编写根类,同时还要编写collection,事件循环,以及其他会用到的类。此外,如果不使用系统框架,也就无法使用Objective-C开发Mac OS X及iOS应用程序。
提示:以下是本篇文章正文内容,下面案例可供参考
熟悉系统框架
系统框架里提供了许多经常使用的类,例如collection。如果不了解系统框架所提供的内容,那么就可能把其中已经实现过的东西又改了一遍。如果我们直接使用框架中的类,应用程序就可以得益于新版系统库所带来的改进,开发者无需手动更新代码
- 框架是将一系列代码封装为动态库,并在其中放入描述其接口的头文件。有时iOS平台构建的第三方库所使用的是静态库,这是因为iOS应用程序不允许在其中包含动态库。严格上说,这些东西不算真正的框架,但是经常被视为框架。但是,iOS所有的平台系统框架仍然适用动态库。
- Cocoa本身不是框架,但是里面集成了一批创建应用程序时经常会用到的框架。
- 开发者会碰到的主要框架就是Foundation,像NSObject,NSArray,NSDictionary等类都在其中。Foundation中的类,使用NS这个前缀,此前缀是在Objective-C语言作用操作系统NeXTSTEP操作系统的编程语言时首度确定的。
- Foundation框架提供了collection等基础核心功能,而且提供了字符串处理这样的复杂功能。NSLinguisticTagger可以解析字符串并找到其中的全部名次,动词,代词等。
- CoreFoundation框架不算Objective-C框架,但是他是编写Objective-C音乐现场时应该熟悉的重要框架,Foundation框架中的与多功能,都可以在此框架中找到对应的C语言API。“无缝桥接”库吧CoreFoundation中的C语言数据结构平滑转为Foundation中的Objective-C对象,可以反向转换。无缝桥接技术是用一些很复杂的代码实现的,这些代码可以使运行期系统吧CoreFoundation框架中的对象视为普通的Ovbjective-C对象。
系统库 | 介绍 |
---|---|
CFNetwork | 提供C语言级别的网络通信能力,将“BDS套接字”抽象成为易于使用的网络接口。Foundation将该框架中的部分内容封装为Objective-C语言的接口,以便进行网络通信 |
CoreAudio | 提供的C语言API可以用来操作设备上的音频硬件。这个框架比较难用。这套API可以抽象出另一套Objective-C式API |
AVFoundation | 提供的Objective-C对象可以用来回放并录制音频及视频。 |
CoreData | 提供的Objective-C接口可以将对象放入数据库,便于持久保存。Coredata会处理数据的获取及存储等事宜,可以跨Mac OS X及iOS平台 |
CoreText | 提供的C语言接口可以高效的执行文字排版及渲染操作 |
- Objective-C编程的一项重要特点就是:经常需要使用底层的C语言级API。好处是:可以绕过Objective-C的运行期系统,提升速度。
- 使用UI框架编写Mac OS X或iOS应用程序,这两个平台的核心UI框架分别叫做AppKit及UIKit,他们都提供了狗站在Foundation与Corefoundation之上的Objective-C类。
- CoreAnimation是用Objective-C语言写成的,提供了一些工具,UI框架用这些工具来渲染图形并播放动画。CoreAnimation本身不算框架,他是QuarzCore框架中的一部分。然而在框架的国度里,CoreAnimation算作“一等公民”。
- CoreGraphics框架以C语言写成,提供了2D渲染所必备的数据结构与函数。
- 还有很多框架构建在UI框架之上,MapKit可以为iOS程序提供地图功能。Social框架为Mac OS X及iOS程序提供了社交网络功能。
多用块枚举,少用for循环
我们经常需要列举collection中的元素,可以使用标准的C语言循环,Objective-C的NSEnumerator和Objective-C的快速遍历。引入“块”这个概念后,又出来了几种新的遍历方式。采用这几种新的方式来遍历collection时,可以传入块,collection中的每个元素都要放在块里运行一遍,这样通常会大幅简化编码过程。
collection包括NSArray,NSDictionary,NSSet这几个频繁使用的类型。也适用于自定义的collection。
for循环
- 便利数组的第一种方法就是采用老式的for循环。这是一个很基本的方法,功能非常有限。
NSArray* array = [NSArray arrayWithObjects:@"111", @"222", @"333" ,nil];
for (int i = 0; i < array.count; i++){
NSLog(@"%@", array[i]);
}
NSDictionary* dictionary = [NSDictionary dictionaryWithObjects:@[@"111", @"222", @"333", ] forKeys:@[@"1", @"2", @"3"]];
NSArray* keys = [dictionary allKeys];
for (int i = 0; i < keys.count; i++){
NSLog(@"%@", dictionary[keys[i]]);
}
NSSet* set = [[NSSet alloc] initWithObjects:@"111", @"222", @"333", nil];
NSArray* array = [set allObjects];
for (int i = 0; i < set.count; i++){
NSLog(@"%@", array[i]);
}
- 根据定义,字典与set都是“无序的”。所以无法根据下标直接访问。于是我们先获取字典里的所有键或者set里的所有对象,然后在获取到的有序数组上遍历。这样做会有额外开销。
使用Objective-C 1.0的NSEnumerator遍历
- NSEnumerator是一个抽象基类。方法nextObject返回枚举里的下一个对象。每次调用此方法时, 内部数据都会更新,使得下次调用方法时能返回下一个对象
NSArray* array = [NSArray arrayWithObjects:@"111", @"222", @"333" ,nil];
NSEnumerator* enumerator = [array objectEnumerator];
id object;
while ((object = [enumerator nextObject]) != nil) {
NSLog(@"%@", object);
}
- 遍历字典的方式和数组和set不太一样,先根据给定的键把对应的值提取出来。
NSDictionary* dictionary = [NSDictionary dictionaryWithObjects:@[@"111", @"222", @"333", ] forKeys:@[@"1", @"2", @"3"]];
NSEnumerator* enumerator = [dictionary keyEnumerator];
id key;
while (( key = [enumerator nextObject]) != nil) {
id value = dictionary[key];
NSLog(@"%@", value);
}
NSSet* set = [[NSSet alloc] initWithObjects:@"111", @"222", @"333", nil];
NSEnumerator* enmuator = [set objectEnumerator];
id object;
while ((object = [enmuator nextObject]) != nil) {
NSLog(@"%@", object);
}
- 使用NSEnumerator还有一个好处,就是有多种“枚举器”可供使用。反向遍历数组所用的枚举器,如果用它来遍历,就可以按反方向来迭代collection中的元素了。
NSArray* array = [NSArray arrayWithObjects:@"111", @"222", @"333" ,nil];
NSEnumerator* enumerator = [array reverseObjectEnumerator];
id object;
while ((object = [enumerator nextObject]) != nil) {
NSLog(@"%@", object);
}
快速遍历
- Objective-C2.0引入了快速遍历。和使用NSEnumerator遍历差不多,语法更为简洁,为for循环开设了in关键字。这个关键字大幅简化遍历collection的语法。
NSArray* array = [NSArray arrayWithObjects:@"111", @"222", @"333" ,nil];
for (id object in array) {
NSLog(@"%@", object);
}
- 如果某个类的对象支持快速遍历,那么就可以说自己遵从名为NSFastEnumerator的协议,令开发者可以采用此语法来迭代该对象。
NSDictionary* dictionary = [NSDictionary dictionaryWithObjects:@[@"111", @"222", @"333", ] forKeys:@[@"1", @"2", @"3"]];
for (id key in dictionary) {
id value = dictionary[key];
NSLog(@"%@", value);
}
NSSet* set = [[NSSet alloc] initWithObjects:@"111", @"222", @"333", nil];
for (id object in set) {
NSLog(@"%@", object);
}
NSArray* array = [NSArray arrayWithObjects:@"111", @"222", @"333" ,nil];
for (id object in [array reverseObjectEnumerator]) {
NSLog(@"%@", object);
}
基于块的遍历方式
- (void)enumerateObjectsUsingBlock:(void(^)(id object, NSUInteger idx, BOOL* stop))block
- 遍历数组和set时,每次迭代都要执行block参数传入的块,这个块有三个参数,当前迭代所针对的对象,所针对的下标,指向布尔值的指针。通过第三个参数所提供的机制,开发者可以终止遍历操作。
NSArray* array = [NSArray arrayWithObjects:@"111", @"222", @"333" ,nil];
[array enumerateObjectsUsingBlock:^(id _Nonnull object, NSUInteger idx, BOOL * _Nonnull stop) {
if (shouldStop){
*stop = YES;
}
}];
- 这种写法遍历时技能获取对象,也能知道其下标。此方法还提供了一种优雅的机制,用于终止遍历操作。开发者可以通过设定stop值来实现。
- 此方法的好处在于:遍历时可以直接从块里获取更多信息。在遍历数组时,可以知道当前所针对的下标。遍历有序set时也一样。遍历字典时无序额外编码,即可同时获取键与值,省去了根据给定键获取对应值这一步。
- 能够修改块的方法签名,避免进行类型转换操作,从效果上讲,相当于把本来需要执行的类型转换操作交给方法签名来做。因为id类型相当特殊,他可以为其他类型覆写。如果原来的块签名吧键和值都定义成为NSObject*,这么些就不可以了。
- NSEnumeratorOptions是一个enum,各种取之可用“按位或”连接,用以表明遍历方式。
- 块枚举拥有其他遍历方法都具备的优势,而且还能带来更多好处。与快速遍历相比,他要多用一些代码,但能提供遍历时针对的下标,在遍历字典时也能提供键与值,而且还有选项可以开启并发迭代功能。
对自定义其内存管理语义的collection使用无缝桥接
- Objective-C的系统库里包含相当多的collection类,其中有各种数组,各种字典,各种set。CoreFoundation框架定义了一套C语言API,用于操作表达这些collection及其他各种collection的数据结构。
- “无缝桥接”可以在NSArray和CFArray类型之间平滑转换。使用“无缝桥接”计数,可以在定义在Foundation框架中的Objective-C类和定义于CoreFoundation框架中的C数据结构之间相互转换
- __bridge意在告诉ARC如何处理转换所设计的Objective-C对象。他本身的意思是:ARC仍然具备这个歌Objective-C对象的所有权。__bridge_retained和他相反,意味着ARC要交出对象的所有权。
- 需要加上CFRelease()释放内存。
- 在Foundation中的字典对象中,键的内存管理语义为“拷贝”,值的语义为“保留”,除非使用强大的无缝桥接计数,否则无法改变语义。
CFMutableDictionaryRef CFDictionaryCreatMutable(
CFAllocatorRef allocator,
CFIndex capacity,
const CFDictionaryKeyCallBacks* keyCallBacks,
const CFDictionaryValueCallBacks* valueCallBacks )
- 第一个参数表示要使用的内存器,CoreFoundation对象里的数据结构需要占用内存,分配器负责分配及回收这些内存。
- 第二个参数定义字典的大小。
- 最后两个参数定义了回调函数,用于指示字典里的键和值杂居遇到各种事件时应该执行何种操作。这两个参数都是指向结构体的指针。
-
version参数目前应设置为0。这个参数可以用于检验新版与旧版数据结构之间是否兼容。结构体中的其余成员都是函数指针,他们定义了当各种事件发生时应该采用哪个函数来执行相关任务。
-
retain是一个函数指针,其所指向的函数接受两个参数,其类型分别是CFAllocatorRef和const void*。传给此函数的value参数表示即将加入字典中的键或值,返回的void*表示要加到字典里的最终值。
-
在CoreFoundation层面创建collection时,可以指定许多回调函数,这些函数表示此collection应如何处理其元素,然后运用无缝桥接技术,将其转换成具备特殊内存管理语义的Objective-C collection
构建缓存时选用NSCache而非NSDictionary
- 开发应用程序时,经常需要缓存图片。一般情况第一时间会想到使用NSDictionary来缓存,因为这个类很实用。其实NSCache类更好,这个类是专为处理这种情况而生的。
- 当系统资源快要耗尽时,NScache会自动删减缓存。如果使用字典,就有自己编写挂钩,在系统第内存的时候通知手工删减缓存。NSCache会自动删减。NSCache还会自动删减“最久未被使用的”对象。
- NSCache不会拷贝键,是保留它。原因在于:很多时候,键都是由不支持拷贝操作的对象来充当的,因此,NSCache不会自动拷贝键。在键不支持拷贝的情况下,该类比字典方便。
- NSCache是线程安全的,在开发者不自己编写加锁代码的前提下,多个线程可以同时访问NSCache。对缓存来说,线程安全很重要。
- 开发者可以操纵缓存删减其内容的时机。有两个与系统资源相关的尺度可供调整,其一是缓存中的总数,其二是所有对象的总开销。开发者在将对象加入缓存时,可以指定“开销值”。当对象的总数或者总开销超过上限时,缓存就可能会删减其中的对象了。在系统资源趋于紧张时,也会这样做。
- 向缓存中添加对象时,只有在能很快计算出“开销值”的情况下,才一个考虑采用这个尺度。如果计算过程很复杂,就不值得这样做了。
- 还有一个类叫做“NSPurgeableData,和NACache搭配起来使用,效果很好。此类是NSMuatbleData的子类,实现了NSDiscardableContenet协议。
- 如果要访问某个NSPurgeableData对象,可以调用其beginContentAccess方法,告诉他现在还不应该丢弃自己的内存,用完之后嗲用endContentAccess方法,告诉他们可以对其内存了。这些调用可以嵌套,他们就像递增和递减引用计数所用的方法那样。只有对象的”音引用计数“为0时才可以丢弃。
精简initialize与load的实现代码
-
有时候类必须先初始化才能使用。NSObject类有两个方法实现初始化操作。
-
+(void)load
-
对于加入运行期的每个类和分类来说,必会调用此方法。而且仅调用一次。当包含类或分类的程序库载入系统时,就会执行此方法,这通常就是指应用程序启动的时候。
-
load方法的问题在于,执行该方法时,运行期系统处于“脆弱状态”,执行load方法之前,必定先执行所有超类的load方法。如果代码还依赖了其他程序库,那么程序库里的相关类的load方法也必定会先执行。然而,根据某个给定的程序库,却无法判断出其中各个类的载入顺序。因此,load方法里使用其他类是不安全的。
-
在EOCClassB的load方法里受用EOCClassA不太安全,因为无法确定在执行EOCClassB的load方法之前,EOCClaseA有没有加载好。
-
load方法不遵从那套继承规则。如果某个类本身没有实现load方法,那么不管其各级超类是否实现此方法,系统都不会嗲用。
-
load方法必须要实现的精简一些,也就是尽量减少其所执行的操作。 因为整个程序在load方法时都会阻塞。如果load方法中包含繁杂的代码,那么应用程序在执行期间就会无响应。不要在里边等待锁,也不用调用可能加锁的方法。
-
+(void)initialize
-
这个方法会在程序首次启动该类之前调用且仅调用一次。他是由运行期系统调用的,不能有代码直接调用。他是“惰性调用的”,至于当当程序用到了这个类时,才会调用。应用程序无序把每个类的initialize都执行一遍,应用程序必须阻塞并等着所有类的load方法执行完毕
-
在运行期系统执行此方法时,时处于正常状态的,从运行期系统完整度上来讲,此时可以安全使用并调用任意类中的任意方法。运行期系统也能确保initialize方法一定会在“线程安全的环境”中执行。只有执行initialize的那个线程可以操作类或实例。其他线程都要先阻塞,等initialize执行完
-
initialize和其他消息一样,如果某个类未实现它,而超类实现了,那么运行超类的实现代码。
-
initialize也遵循通常的继承规则。
-
这两个方法的实现代码要尽量精简。在里面设置一些状态,是本类能够正常运转就可以了,不要执行那种耗时太久或需要加锁的任务。
-
对于某个类来说,任何线程都可能成为初次用到他的那个线程,并导致其初始化。如果这个线程碰巧是UI线程,那么初始化期间就会一直阻塞,导致应用程序无响应。
-
类在首次使用前,肯定要初始化,但编写程序不能令代码依赖特定的时间点。
-
如果某个类的实现代码很复杂,那么其中可能会直接或间接用到其他的类,如果那些类未初始化,系统会逼他初始化。然而,本类的初始化方法此时尚未运行完毕。其他类在initialize方法时,也可能会依赖本类中的某些数据,这些数据此时也没有初始化好。
-
initialize方法只应该用来设置内部数据,不应该在其中调用其他方法,即便是本类自己的方法也最好不要调用。
别忘了NSTimer会保留其目标对象
- 计时器要和“运行循环”(run loop)相关联,运行循环时会触发任务。穿件NSTimer可以将其“预先安排”在当前的运行循环中,也可以先创建好,然后由开发者自己来调度。无论哪种方式,只有把计算器放在运行循环里,他才能正常触发任务。
+ (NSTimer*)scheduledTimerWithTimeInterval:(NSTimeInterval)seconds target:(nonnull id)aTarget selector:(nonnull SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo
- 用这个方法创建出来的计时器,会在指定的间隔时间后执行任务。也可以令其反复执行任务,直到开发者手动关闭。target和selector表示计时器在那个对象上调用哪个方法。计时器会自动保留其目标对象,等到自身“失效”后释放此对象。调用invalidate
方法可以令计时器失效。执行完相关任务后,一次性计时器也会失效。 - 由于计时器会保留其目标对象,反复执行任务通常会导致应用程序出现问题。设置成重复执行的那种计时器,很容易引入“保留环”。
- 当指向EOClass实例的最后一个外部引用一走之后,该实例仍然会继续存活。因为计时器还保留着它。计时器对象也不可能为系统所释放,因为实例中还有一个强引用正指向他。处理计时器外,因为没有别的引用真心爱指向这个实例了,于是这个实例就永远消失了。除了这个实例外,有没有其他引用指向计时器,于是,内存就泄漏了。
- 将计时器封装成块,在调用计时器时,把它作参数传入。这个参数可以用来存放“不透明值”,只要计时器还有效,就会一直保留它。传入的参数要通过copy方法将block拷贝到“堆”上,否则等到稍后要执行它的时候,该块可能已经无效了。计时器现在的target是一个NSTimer类对象,是一个单例,因此计时器是否会保留它,其实无所谓了。此处仍然有保留环,但是因为类对象无需回收,所以不用担心。