effective—Objective—C 第七章阅读笔记

系统框架

文章目录

熟悉系统框架

框架是指将一系列代码分装成动态库,并在其中放入描述其接口的头文件,这样做出来的东西叫做框架,所有的iOS平台的系统框架使用的是动态库。

还有个和Foundation相伴的一个框架,CoreFoundation,这个框架有很多的一个功能和Foundation有关系,这里我们介绍一下无缝桥接这个功能,他可以吧C语言的数据结构转化成我们的一个OC对象。

• CFNetwork 此框架提供了C语言级别的网络通信能力,它将 "BSD 套接字"(BSDsocket)抽象成易于使用的网络接口。而Foundation 则将该框架里的部分内容封装为Objective-C 语言的接口,以便进行网络通信,例如可以用 NSURLConnection 从URL

中下载数据。

• CoreAudio该框架所提供的C语言 API 可用来操作设备上的音频硬件。这个框架属于比较难用的那种,因为音频处理本身就很复杂。所幸由这套API 可以抽象出另外一套 Objective-C 式API,用后者来处理音频问题会更简单些。

• AVFoundation此框架所提供的 Objective-C对象可用来回放并录制音频及视频,比如能够在 UI 视图类里播放视频。

• CoreData此框架所提供的 Objective-C 接口可将对象放人数据库,便于持久保存°。CoreData 会处理数据的获取及存储事宜,而且可以跨越Mac OS X及iOS平台。

• CoreText此框架提供的C语言接口可以高效执行文字排版及渲染操作。

这里我们OC编程的底层调用了很多C语言级别的API,这样可以绕过OC的运行期的系统,从而提高执行速度。但是这里注意使用这个框架的时候,ARC是不负责这部分的内容的。

这里还有CoreAnimation和CoreGraphics两个框架。

要点

  • 许多系统框架都可以直接使用。其中最重要的是 Foundation 与 CoreFoundation,这两个框架提供了构建应用程序所需的许多核心功能。
  • 很多常见任务都能用框架来做,例如音频与视频处理、网络通信、数据管理等。
  • 请记住:用纯C写成的框架与用 Objective-C写成的一样重要,若想成为优秀的Objective-C 开发者,应该掌握C语言的核心概念。

多用块枚举

我们一般情况下我们有些时候都采用for循环,这是我以前的习惯,但是这里让我们多采用块的一个遍历顺序。

for循环

objc 复制代码
for (int i = 0; i < objects.count; i++) {
  
}

这种便利方式只能便利我们的一个有序数组,因为其他的collection都是不均匀的。

NSEnumerator遍历

objc 复制代码
NSArray* ary = @[@"123"];
NSEnumerator* en = [ary objectEnumerator];
id obj;
while ((obj = [en nextObject]) != nil) {
        }

这种写法的功能和标准的for循环类似,这种写法的优势主要是可以便利任何一种colleciton。

objc 复制代码
NSDictionary* ary = @{@"123":@"123"};
    NSEnumerator* en = [ary objectEnumerator];
    id obj;
    while ((obj = [en nextObject]) != nil) {
        id value = ary[obj];
        NSLog(@"%@", value);
    }

快速遍历

objc 复制代码
for (id object in ary) {
  
}

这里主要还是多了一个in这个关键字

objc 复制代码
NSDictionary* ary = @{@"123":@"123"};

for (id key in ary) {
     id value = ary[key];
}

这样更加方便,同时如果需要反向遍历数组,可以采用下面的这种写法:

objc 复制代码
NSArray* ary = @[@"1234", @"123"];
    NSEnumerator* en = [ary objectEnumerator];
    id obj;
    for (id key in [ary reverseObjectEnumerator]) {
        NSLog(@"%@", key);
    }

块枚举

objc 复制代码
[ary enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        
}];

这里的遍历其实和其他大致类似,我们这里三个变量中的第三个变量如果为YES,那么就会自动退出。

这种方式优秀的点在于可以获取块中更多的信息,

另外一个好处就是可以修改块的方法签名,以免进行类型转化操作。

objc 复制代码
[dicty enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
        
    }];

[dicty enumerateKeysAndObjectsUsingBlock:^(NSString*  _Nonnull key, NSString*  _Nonnull obj, BOOL * _Nonnull stop) {
        
    }];

这种写法成立的原因是因为id类型相当特殊,可以被其他类型所复写,这种写法的好处是可以检测出开发者是否调用了该对象不具备的方法,同时在发现这个问题的时候报错,如果知道他的一个具体类型的时候,就可以使用这种放指明对象。

要点

  • 遍历 collection 有四种方式。最基本的办法是 for 循环,其次是 NSEnumerator 遍历法及快速遍历法,最新、最先进的方式则是"块枚举法"。
  • "块枚举法"本身就能通过GCD 来并发执行遍历操作,无须另行编写代码。而采用其他遍历方式则无法轻易实现这一点。
  • 若提前知道待遍历的 collection 含有何种对象,则应修改块签名,指出对象的具体类型。

对自定义其内存管理语义的colleciton使用无缝桥接

objc 复制代码
NSArray* ansArray = @[@1, @3, @4];
CFArrayRef acfArray = (__bridge  CFArrayRef)ansArray;
NSLog(@"Size of array %li", CFArrayGetCount(acfArray));

首先我们得认识一下我们这里处理转化,无缝桥接。__ bridge 的意思是ARC仍然具备OC对象的所有权,而 __bridge retained则与之相反,以为值ARC要交出对象的所有权。那么这段代码后面就要加上 CFRelease.与之类似,反向转化可以通过bridge_transfer来实现。这三种方式都叫做桥式转化。

之所以会出现无缝衔接的内容,是因为:

可是,你也许会问:以纯 Objective-C来编写应用程序时,为何要用到这种功能呢?这是因为:Foundation 框架中的 Objective-C 类所具备的某些功能,是 CoreFoundation 框架中的C语言数据结构所不具备的,反之亦然。

创建CFMutableDictionary可以通过下列方法来指定建和值的内存管理语义

objc 复制代码
CMutableDictionaryRef CFDictionaryCreateMutable {
CFAllocatorRef allocator,
CIndex capacity,
const CFDictionaryKeyCallBacks *keyCallBacks,
const CFDictionaryValueCallBacks *valueCallBacks
}

第一个参数是内存分配器,这里对象的数据结构需要占用内存,而分配器负责分配和回收内存。通常情况下穿入NULL

第二个参数定义了字典的初始大小,他不会限制字典的最大容量

最后两个参数值得注意,它们定义了很多毁掉函数,用于指示字典中的建和值在遇到各种事件的时候应该怎么操作。两者都是指向结构体的指针,二者所对应的结构体:

objc 复制代码
struct CFDictionaryKeyCallBacks {
CIndex version;
CFDictionaryRetainCallBack retain;
CFDictionaryReleaseCallBack release;
CFDictionaryCopyDescriptionCallBack copyDescription;
CFDictionaryEqualCallBack equal;
CEDictionaryHashCallBack hash;
};
struct CFDictionaryValueCallBacks {
CIndex version;
CFDictionaryRetainCallBack retain;
CFDictionaryReleaseCallBack release;
CFDictionaryCopyDescriptionCallBack copyDescription;
CFDictionaryEqualCallBack equal;
};

version这个参数代表版本,这个参数用来检测新版和旧版书觉结构之间是否兼容。结构体中其他成员都是函数指针,他们定义了当各种事件发生时候应该采用那个函数,如果字典中添加了新的建和值,那么就会调用retain函数。

objc 复制代码
typedef const void* (*CFDictionaryRetainCallBack) (
CAllocatorRef allocator,
const void *value
) ;

由此可见,retain是一个函数指针,这个函数接受两个参数,其类型分别是 CAllocatorRef 和 const void * 返回的void则表示要加到字典里的最终值。

我们呢可以用下面的代码来实现这个回调函数:

objc 复制代码
const void* CustomCallback (CAllocatorRef allocator,
const void *value)
{
  return value;
}

这么写只是把即将加入字典中的值照样返回,如果用它来充当retain回调函数创建出一个字典,那么这个字典就不会保留建和值了。

下面我们来看一段代码:

objc 复制代码
#import <Foundation/Foundation.h>
#import <CoreFoundation/CoreFoundation.h>
const void* EOCRetainCallback (CFAllocatorRef allocator,
const void *value)
{
    return CFRetain (value) ;
}

void EOCReleaseCallback (CFAllocatorRef allocator,
                         const void *value) {
    CFRelease (value) ;
}
CFDictionaryKeyCallBacks keyCallbacks = {
    0,
    EOCRetainCallback,
    EOCReleaseCallback,
    NULL,
    CFEqual,
    CFHash
};
CFDictionaryValueCallBacks valueCallbacks = {
    0,
    EOCRetainCallback,
    EOCReleaseCallback,
    NULL,
    CFEqual
};
CFMutableDictionaryRef aCFDictionary = CFDictionaryCreateMutable (NULL,
    0,
    &keyCallbacks,
    & valueCallbacks) ;
    NSMutableDictionary *anNSDictionary = (__bridge_transfer NSMutableDictionary*) aCFDictionary;

在设定回调函数时,copyDescription 取值为NULL,因为采用默认实现就很好。而equal 与hash 回调函数分别设力 CFEqual 与CFHash,因为这二者所采用的做法与NSMutableDictionary 的默认实现相同。CFEqual 最终会调用 NSObject 的 "isEqual:"方法,

而 CFHash则会调用hash方法。由此可以看出无缝桥接技术更强大的一面。

键与值所对应的 retain 与 release 回调函数指针分别指向 EOCRetainCallback 与 EOCReleaseCallback 函数。为什么要这么做呢?回想一下,前面说过,在向 NSMutableDictionary 中加入键和值时,字典会自动"拷贝"键并"保留"值。如果用作键的对象不支持拷贝操作,那会如何呢?此时就不能使用普通的 NSMutableDictionary 了,假如用了,会导致下面这种运行期错误:

这个图说名原先的NetWorkFetcher没实现我们的一个NSCpoying。开发者可以直接在Core Foundation层创建字典,这样可以处理键保留而不是拷贝的操作。

要点

  • 通过无缝桥接技术,可以在 Foundation 框架中的 Objective-C对象与 CoreFoundation框架中的C语言数据结构之间来回转换。
  • 在 CoreFoundation 层面创建 collection 时,可以指定许多回调函数,这些函数表示此collection 应如何处理其元素。然后,可运用无缝桥接技术,将其转换成具备特殊内存管理语义的 Objective-C collection。

构建缓存的时候选用NSCache而非NSDictionary

NSCache 胜过 NSDictionary 之处在于,当系统资源将要耗尽时,它可以自动删减缓存。如果采用普通的字典,那么就要自己编写挂钩,在系统发出"低内存"(low memory)通知时手工删减缓存。而NSCache 则会自动删减,由于其是Foundation 框架的一部分,所以与开发者相比,它能在更深的层面上插入挂钩。此外,NSCache 还会先行删减"最久未使用的"(lease recently used)对象。若想自己编写代码来字典添加此功能,则会十分复杂。

NSCache 并不会"拷贝"键,而是会"保留"它。此行为用 NSDictionary 也可以实现,然而需要编写相当复杂的代码(参见第49条)。NSCache 对象不拷贝键的原因在于:很多时候,键都是由不支持拷贝操作的对象来充当的。因此,NSCache 不会自动拷贝键,所以说,

在键不支持拷贝操作的情况下,该类用起来比字典更方便。另外,NSCache 是线程安全的。而 NSDictionary则绝对不具备此优势,意思就是:在开发者自己不编写加锁代码的前提下,多个线程便可以同时访问 NSCache。对缓存来说,线程安全通常很重要,因为开发者可能要在某个线程中读取数据,此时如果发现缓存里找不到指定的键,那么就要下载该键所对应的数据了。而下载完数据之后所要执行的回调函数,有可能会放在背景线程中运行,这样的活,就等于是用另外一个线程来写入缓存了。

这里还介绍了另一个类别:NSPurgeableData,如果想使用这个类中的对象,需要调用beginContentAccess这个方法,用完之后就要调用endContentAccess这个方法,这种方式和引用计数大致一致。这里其实是我们的一个离线缓存的内容。

要点

  • 实现缓存时应选用NSCache 而非 NSDictionary 对象。因为 NSCache 可以提供优雅的自动删减功能,而且是"线程安全的",此外,它与字典不同,并不会拷贝键。
  • 可以给 NSCache 对象设置上限,用以限制缓存中的对象总个数及"总成本",而这些尺度则定义了缓存删减其中对象的时机。但是绝对不要把这些尺度当成可靠的"硬限制"(hard limit),它们仅对NSCache 起指导作用。
  • 将 NSPurgeableData 与 NSCache 搭配使用,可实现自动清除数据的功能,也就是说,当 NSPurgeableData 对象所占内存为系统所丢弃时,该对象自身也会从缓存中移除。
  • 如果缓存使用得当,那么应用程序的响应速度就能提高。只有那种"重新计算起来很费事的"数据,才值得放入缓存,比如那些需要从网络获取或从磁盘读取的数据。

精简initialize和load的实现代码

有些时候,类必须先执行某些初始化操作,然后才能正常使用

objc 复制代码
+ (void) load

对于加入运行期系统的每个类及分类来说,必定会调用这个方法,在执行load方法的时候,整个系统都处于一个脆弱的状态

objc 复制代码
#import <Foundation/Foundation.h>
#import "EOCClassA.h"//< From the same library
@interface EOCClassB : NSObject
@end
@implementation EOCClassB
+ (void) load (
NSLog (@"Loading EOCClassB") ;
EOCClassA *object = [EOCClassAnew];
// Use 'object'
}
@end

这段代码使用NSlog没有问题,但是这里在ClassB中使用ClassA,会出现一个问题,可能ClassA还没有加载好,从而会导致问题。

如果想执行某一个类的初始化操作,我们可以复写下面这个方法:

objc 复制代码
+ (void)initialize

下面罗列一下两者的区别:

对于每个类来说,该方法会在程序首次用该类之前调用,且只调用一次。它是由运行期系统来调用的,绝不应该通过代码直接调用。其虽与load 相似,但却有几个非常重要的微妙区别。首先,它是"惰性调用的",也就是说,只有当程序用到了相关的类时,才会调用。因此,如果某个类一直都没有使用,那么其 initialize 方法就一直不会运行。这也就等于说,应用程序无须先把每个类的 initialize 都执行一遍,这与load方法不同,对于load来说,应用程序必须阻塞并等着所有类的load 都执行完,才能继续。

此方法与load还有个区别,就是运行期系统在执行该方法时,是处于正常状态的,因此,从运行期系统完整度上来讲,此时可以安全使用并调用任意类中的任意方法。而且,运行期系统也能确保 initialize 方法一定会在"线程安全的环境"(thread-safe environment) 中执行,这就是说,只有执行 initialize 的那个线程可以操作类或类实例。其他线程都要先阻塞,

等着 initialize 执行完。

最后一个区别是:initialize 方法与其他消息一样,如果某个类未实现它,而其超类实现了,那么就会运行超类的实现代码。这听起来并不稀奇,但却经常为开发者所忽视。

这个方法应该只能用来设置内部数据,不应该在其中调用其他方法,即便是本类自己的方法,最好也别调用,不然很容易出现报错.

这里有两段代码:

objc 复制代码
+ (void)initialize
{
    if (self == [ViewController class]) {
        static NSMutableArray* ary = [NSMutableArray new];
    }
}
//上面这段代码会在编译期报错
+ (void)initialize
{
    if (self == [ViewController class]) {
        NSMutableArray* ary = [NSMutableArray new];
    }
}
//这段代码可以正常执行
static NSMutableArray* ary
+ (void)initialize
{
    if (self == [ViewController class]) {
        ary = [NSMutableArray new];
        NSLog(@"12333");
    }
}
//这段代码也可以正常执行

这里其实和我们的一个static这个关键字又关系。后面会总结一下这个几个关键字。

要点

  • 在加载阶段,如果类实现了load方法,那么系统就会调用它。分类里也可以定义此方法,类的load方法要比分类中的先调用。与其他方法不同,load 方法不参与覆写机制。
  • 首次使用某个类之前,系统会向其发送 initialize 消息。由于此方法遵从普通的覆写规
    则,所以通常应该在里面判断当前要初始化的是哪个类。
  • load 与 initialize 方法都应该实现得精简一些,这有助于保持应用程序的响应能力,也
    能减少引入"依赖环"(interdependency cycle)『的几率。
  • 无法在编译期设定的全局常量,可以放在 initialize 方法里初始化。

NSTimer会保留目标对象

NSTimer存在的问题是会保留目标对象,会出现保留环的问题,这里我们应该怎么解决这个问题。

上面这段代码是我们经常出现的样式,但是这种代码会出现一个保留环的问题,我们现在该怎么解决保留环的问题。

这个问题可以通过block来解决:

objc 复制代码
+ (NSTimer*)ecoScheduled :(NSTimeInterval)interval block:(void(^)())block repeats:(BOOL)repeats {
    return [self scheduledTimerWithTimeInterval:interval target:self selector:@selector(blockInvoke:) userInfo:[block copy] repeats:YES];
}
+ (void) blockInvoke:(NSTimer*)timer {
    void (^block)() = timer.userInfo;
    if (block) {
        block();
    }
}
//上面这部分代码放在分类中

- (void)start {
    [NSTimer ecoScheduled:1 block:^{[self doPoll];} repeats:YES];
}

但是这里还有一个一点问题就是下面的代码block捕获了self变量,这里我们需要采用弱引用来打破这个环:

objc 复制代码
- (void)start {
    __weak id weakSelf = self;
    [NSTimer ecoScheduled:1 block:^{
        id strongSelf = weakSelf;
        [strongSelf doPoll];} repeats:YES];
}

要点

  • NSTimer 对象会保留其目标,直到计时器本身失效为止,调用 invalidate 方法可令计
    时器失效,另外,一次性的计时器在触发完任务之后也会失效。
  • 反复执行任务的计时器(repeating timer),很容易引入保留环,如果这种计时器的目
    标对象又保留了计时器本身,那肯定会导致保留环。这种环状保留关系,可能是直接
    发生的,也可能是通过对象图里的其他对象间接发生的。
  • 可以扩充 NSTimer 的功能,用"块"来打破保留环。不过,除非 NSTimer 将来在公
    共接口里提供此功能,否则必须创建分类,将相关实现代码加人其中。
相关推荐
不会敲代码的狗25 分钟前
Acwing-基础算法课笔记之基础算法(差分)
笔记·算法
LuckyLay1 小时前
Golang学习笔记_29——抽象工厂模式
笔记·学习·golang·抽象工厂模式
嗷嗷哦润橘_3 小时前
Deepseek系列从v3到R易背面经版
开发语言·人工智能·笔记·deepseek
S19014 小时前
Django学习笔记(第一天:Django基本知识简介与启动)
笔记·学习·django
海绵波波1076 小时前
.gitignore中忽略node_modules
笔记
故里人间1658 小时前
Vue笔记(八)
笔记
派森先生10 小时前
《麻省理工公开课:线性代数》 中文学习笔记
笔记·学习·线性代数
故里人间16510 小时前
Vue笔记(七)
笔记
天天爱吃肉821811 小时前
第六篇:数字逻辑的“矩阵革命”——域控制器中的组合电路设计
笔记·线性代数·矩阵·汽车