Yun's Blog

  • Home

  • Archives

EffectiveObjective-C2.0 笔记 - 第七部分

Posted on 2018-10-12 | Edited on 2018-11-05 | In iOS

EffectiveObjective-C2.0 笔记 - 第七部分

7. 系统框架

7.1 熟悉系统框架

1. 框架:将一系列代码封装成动态库,并在其中放入描述其接口的头文件。

平时我们第三方框架用的是静态库,因为iOS 应用程序不允许其中包含动态库。

2. Foundation、CoreFoundation 框架平时用的比较多,“无缝桥接” 可以将这两种框架的对象平滑转换。

3. 常用框架:

  • CFNetwork
  • CoreAudio
  • AVFoundation
  • CoreData
  • CoreText

7.2 多用块枚举,少用for 循环

遍历collection 有四种方式。最基本的办法就是for 循环,其次是NSEnumerator 遍历法及快速遍历法,最新、最先进的方式则是 “块枚举法”。

1. for 循环

简单粗暴,遍历数组还可以,但是对于遍历字典或者set,就不太友好。

2. 使用Objective-C 1.0 的NSEnumerator 来遍历

1
2
3
4
5
6
NSArray *array = @[@"A",@"B",@"C"];
NSEnumerator *enumerator = [array objectEnumerator];
NSString *string;
while ((string = [enumerator nextObject]) != nil) {
NSLog(@"%@",string);
}

这种遍历使用相对比较统一,数组、字典和set 都可以这样子写,并且还有多种 “枚举器” 可供使用,例如反向遍历数组的枚举器。

3. 快速遍历

1
2
3
for (<#type *object#> in <#collection#>) {
<#statements#>
}

for in 这个更加简洁,如果某个类的对象支持快速遍历,那么就可以宣称自己遵从名为NSFastEnumeration 的协议,从而令开发者可以采用此语法来迭代改对象。此协议只定义了一个方法:

1
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id __unsafe_unretained _Nullable [_Nonnull])buffer count:(NSUInteger)len;

由于NSEnumerator 对象也实现了NSFastEnumeration 协议,所以能用来执行快速遍历。但是快速遍历拿不到当前操作对象的下标。

1
2
3
4
NSArray *array = @[@"A",@"B",@"C"];
for (NSString *string in [array reverseObjectEnumerator]) {
NSLog(@"%@",string);
}

4. 基于块的遍历方式

1
2
3
4
5
6
7
8
NSArray *array = @[@"A",@"B",@"C"];
[array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {

}];

[array enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {

}];

“块枚举法” 本身就能通过GCD来并发执行遍历操作,无须另行编写代码。而采用其他遍历则无法轻易实现这一点。

此方式对于其他相比,在遍历时候可以直接在块中获取更多信息,而且这种对于字典的遍历也是非常友好的,一次性可以返回键和值。并且还可以支持反向遍历。

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

无缝桥接

使用 “无缝桥接” 计数,可以在定义于Foundation框架中的Objective-C类和定义于CoreFoundation框架中的C数据结构之间相互转换。

1. 三种转换方式

  • __bridge 只是声明类型转变,但是不做内存管理规则的转变
  • __bridge_retained 表示将指针类型转变的同时,将内存管理的责任由原来的Objective-C 交给Core Foundation 来处理,也就是ARC 转变成 MRC
  • __bridge_transfer 表示将管理的责任由Core Foundation 转交给Objective-C,即将MRC转变成ARC
  1. Foundation中字典对象无缝桥接:

Foundation中字典对象,对其键的内存管理语义为 “拷贝”,而值的语义是 “保留”。只能通过强大的无缝桥接技术,否则无法改变其语义。

CoreFoundation 框架的字典类型是CFDictionary,可变版本是CFMutableDictionary。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
//CFMutableDictionary 用CFDictionaryCreateMutable 来创建
//用CFDictionaryCreateMutable 定义
CFMutableDictionaryRef CFDictionaryCreateMutable (
CFAllocatorRef allocator,
CFIndex capacity,
const CFDictionaryKeyCallBacks *keyCallBacks,
const CFDictionaryValueCallBacks *valueCallBacks
);

/*CFAllocatorRef 表示将要使用的内存分配器,CoreFoundation 对象里的数据结构需要占用内存,而分配器负责分配及回收这些内存,一般传NULL,表示采用默认的分配器。

CFIndex 表示字典的初始大小,跟我们Foundation 字典的创建一样,并不限制最大容量 就是预先分配内存

最后两个参数都是指向结构体的指针,定义了很多回调函数,用于指示字典中的键和值遇到各种事件时应该执行何种操作。

CFDictionaryKeyCallBacks 的结构体定义
typedef struct {
CFIndex version;
CFDictionaryRetainCallBack retain;
CFDictionaryReleaseCallBack release;
CFDictionaryCopyDescriptionCallBack copyDescription;
CFDictionaryEqualCallBack equal;
CFDictionaryHashCallBack hash;
} CFDictionaryKeyCallBacks;

CFDictionaryValueCallBacks 的结构体定义
typedef struct {
CFIndex version;
CFDictionaryRetainCallBack retain;
CFDictionaryReleaseCallBack release;
CFDictionaryCopyDescriptionCallBack copyDescription;
CFDictionaryEqualCallBack equal;
} CFDictionaryValueCallBacks;

version 参数目前应设置为0,表示版本号;
其他参数都是函数指针,例如,字典加入了新的键与值,那么就会调用retain 函数,定义如下:
typedef const void *(*CFDictionaryRetainCallBack)(
CFAllocatorRef allocator,
const void *value
);
retain 是个函数指针,其所指向的函数接受两个参数,其类型分别是CFAllocatorRef、const void *。传给此函数的value 参数表示即将加入字典中的键或值。而返回的void * 则表示加到字典里的最终值。我们可以这样子实现:
const void *CustomCallback(CFAllocatorRef allocator,const void *value){
return value;
}
如果用它充当retain 回调函数来创建字典,那么该字典就不会 “保留” 键和值。然后再利用无缝桥接搭配起来,就可以创建特殊的NSDictionary 对象,跟我们普通的字典不一样。

开发者可以直接在CoreFoundation 层创建字典,于是就能修改内存管理语义,对键执行 “保留” 而非 “拷贝” 操作了。

7.4 构建缓存时选用NSCache而非 NSDictionay

1. 实现缓存时应选用NSCache而非NSDictionary 对象。

NSCache 是专门来处理缓存的,在系统资源将要耗尽时,它可以自动删减缓存。而且是线程安全的,此外,它与字典不同,并不会拷贝健。

2. 可以给NSCache 对象设置上限

可以给NSCache 对象设置上限,用以限制缓存中的对象总个数及总成本,而这些尺度则定义了缓存删减其中对象的时机。但是绝对不要把这些尺度当成可靠的 “硬限制”,它们仅对NSCache其指导作用。

3. NSPurgeableData 与 NSCache 搭配使用

NSPurgeableData类是NSMutableData的子类,而且实现了NSDiscardableContent协议。将NSPurgeableData 与 NSCache 搭配使用,可实现自动清除数据的功能,也就是说,当NSPurgeableData 对象所占内存为系统丢弃时,该对象也会从缓存中移除。

4. 如果缓存使用得当,那么应用程序的响应速度就能提高。

只有那种 “重新计算起来很费事的” 数据,才值得放入缓存,比如那些需要从网络获取或从磁盘读取的数据。

7.5 精简initalize与load的实现代码

1. 在加载阶段,如果类实现了load 方法,那么系统就会调用它。分类里也可以定义此方法,类的load 方法要比分类中的先调用。与其他方法不同,load 方法不参与覆写机制。

  • 对于加入运行期系统中的每个类及分类,必定会调用load这个方法,而且仅调用一次。意思就是程序启动的时候需要加载load方法,这个时候运行期系统也是出于 “脆弱状态”,在执行子类的load方法之前,必定会先执行所有超类的load 方法。

  • 如果load 代码还依赖了其他类,那类的load 也必然会先执行,我们无法判断每个类的载入顺序,所以load 方法使用其他类是不安全的。

  • load 方法不遵从继承规则,如果某个类没实现load 方法,那么不管其各级超类是否实现此方法,系统都不会调用。

  • load 方法要实现的精简点,因为应用程序在执行load 方法会阻塞。load 一般作为调试用,很少用来做初始化操作。

2. load 与initialize 方法都应该实现得精简一些,这有助于保持应用程序的响应能力,也能减少引入 “依赖环” 的几率。

  • 想执行与类相关的初始化操作,可以使用 +(void)initialize 这个方法,它跟load 有以下几个区别:

    • 这个方法是在首次使用这个类的时候调用,类似 “惰性调用” ,只有用到这个类才会调用。
  • 运行期在执行该方法的时候,是出于正常状态的,此时是可以安全调用任意类的任意方法,而且运行期系统会确保initialize 方法一定在 “线程安全的环境” 中执行。其他线程都要先阻塞,等initialize 执行完。
  • initialize 方法跟其他方法一样,某个类没有实现它,而超类方法实现了,那么就会运行超类的实现代码。
  • 也就是说initalize 与 load 的实现代码要精简些。

  • 若某个全局状态无法在编译期初始化,则可以放在initalize 里来做。(例如Objectice-C 对象,创建实例之前必须先激活运行期系统)

7.6 别忘了NSTimer会保留其目标对象

1. 计时器放在运行循环里,它才能正常触发任务。

1
+ scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:

2. 计时器保留环

计时器会保留其目标对象,等到自身 “失效” 时再释放此对象,调用invalidate 方法可令计时器失效,另外,一次性的计时器在触发完任务之后也会失效。设置成重复执行模式的计时器,要注意 “保留环” 问题。

3. 如何解决外界不调用invalidate方法也不产生 “保留环” 的问题。

可以用块来解决这个问题,其实就是将timer的target 对象不要指向持有timer的对象,这里用的方法是让timer 的taerget 指向自己。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//定义
+ (NSTimer *)my_scheduledTimerWithTimeInterval:(NSTimeInterval)ti
block:(void(^)())block
repeats:(BOOL)yesOrNo;

//实现
+ (NSTimer *)my_scheduledTimerWithTimeInterval:(NSTimeInterval)ti
block:(void(^)())block
repeats:(BOOL)yesOrNo {
return [self scheduledTimerWithTimeInterval:ti
target:self
selector:@selector(my_blockInvoke:)
userInfo:[block copy]
repeats:yesOrNo];
}

- (void)my_blockInvoke:(NSTimer *)timer {
void (^block) () = timer.userInfo;
if (block) {
block();
}
}

EffectiveObjective-C2.0 笔记 - 第六部分

Posted on 2018-10-07 | Edited on 2019-01-08 | In iOS

EffectiveObjective-C2.0 笔记 - 第六部分

6. 块与大中枢派发

6.1 理解 “块” 这一概念

一、块的基础知识

块是C、C++、Objective-C 中的词法闭包。

1. 块用 “^” 符号来表示,后面跟着一对花括号,括号里面是块的实现代码。

块其实就是个值,与 int,flout,Objective-C对象一样,而且自有其相关类型,可以赋值给变量;块类型的语法和函数指针类似。

1
2
3
4
5
6
7
8
9
^{
//block implementation herer
}

//块类型的语法结构如下
//return_type (^block_name)(parameters)
void (^oneBlock)() = ^{
//block implementation herer
}

2. 在声明块的范围内,所有变量都可以被其捕获。

默认情况下被块捕获的变量是不可以在块里修改的,不过可以在声明变量的时候加上__block 修饰符,这样子就可以在块内修改了。

如果块所捕获的变量是对象类型,那么就会自动保留它,在系统释放这个块的时候,也会将其一并释放。

块总能修改实例变量,所以在声明时无须加__block。不过如果通过读取或写入操作捕获了实例变量,那么也会自动把self 变量一并捕获了,因为实例变量是与self 所指代的实例关联在一起的。

如果 self 所指代的那个对象同时也保留了块,那么这种情况通常就会导致”保留环”。

二、块的内部结构

1. 块本身也是对象,在存放块对象的内存区域中,首个变量是指向Class 对象的指针(isa 指针)。

2. invoke 变量是这个函数指针,指向块的实现代码。

函数原型至少要接受一个void* 型的参数,此参数代表块。为什么要把块对象作为参数传进来呢,因为在执行块的时候,要从内存中把这些捕获到的变量读出来。

descriptor 变量是指向结构体的指针,这个结构体包含块的一些信息。

三、全局块、栈块及堆块

1. 定义块的时候,其所占的内存区域是分配在栈中,意思就是,块只在定义它的那个范围内有效。

1
2
3
4
5
6
7
8
9
10
11
void (^block)();
if(***){
block = ^(){
NSLog(@"Block A");
};
}else{
block = ^(){
NSLog(@"Block B");
};
}
block();
  • 栈块

定义在if else 语句中的两个块都分配在栈内存中,编译器会给每个块分配好栈内存,然而等离开了相应的范围之后,编译器有可能把分配给块内存覆写掉。所以这里执行block() 有危险。

  • 堆块

为了解决这个问题,可以给块发送copy 消息以拷贝之。这样子的话,就可以把块从栈复制到堆可。一旦复制到堆上,块就成了带引用计数的对象了,后续的复制操作都不会真的执行复制,只是递增块对象的引用计数。

  • 全局块

全局块声明在全局内存里,而且也不能被系统回收,相当于单例。由于运行该块所需的全部信息在编译期确定,所以可以把它作为全局块,这是一种优化技术:若把如此简单的块当成复杂的块来处理,那就会在复制及丢弃该块时执行一些无谓的操作。

1
2
3
void (^block)() = ^(){
NSLog(@"Block A");
};

6.2 为常用的块类型创建 typedef

1. 每个块都具备其 “固定类型”,因而可将其赋值给适当类型的变量。

2. 由于块类型的语法比较复杂难记,我们可以给块类型起个别名。

用C 语言中的 “类型定义” 的特性。typedef 关键字用于给类型起个易读的别名。

1
2
3
4
5
typedef int(^EOCSomeBlock)(BOOL flag, int value);

EOCSomeBlock block = ^(BOOL flag, int value){
//to do
};

6.3 块降低代码分散程度

场景:

异步方法执行完任务,需要以某种手段通知相关代码。经常使用的技巧是设计一个委托协议,令关注此事件的对象遵从该协议,对象成了delegate 之后,就可以在相关事件发生时得到通知了。

使用块来写的话,代码会更清晰,使得代码更加紧致。

6.4 用块引用其所属对象时不要出现保留环

1. 如果块所捕获的对象直接或间接地保留了块本身,那么就得当心保留环问题。

2. 一定要找个合适的时机解除保留环,而不能把责任推给API的调用者。

6.5 多用派发队列,少用同步锁

1. 同步锁

如果有多个线程要执行同一份代码,那么有时可能会出问题,这种情况下,通常要使用锁来实现某种同步机制。在GCD 出现之前,有两种办法:

  • 采用内置的 “同步块”(synchronization block)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    - (void)synchronizedMethod {
    @synchronized(self){
    //safe
    }
    }

    /*
    这种写法会根据给定的对象,自动创建一个锁,并等待块中的代码执行完毕,执行到代码结尾,锁就释放了。

    但是,滥用 @synchronized(self) 则会降低代码效率,因为共用同一个锁的那些同步块,都必须按顺序执行。
    */
  • 直接使用NSLock 对象,也可以使用NSRecursiveLock “递归锁”,线程能多次持有该锁,而且不会出现死锁。

    1
    2
    3
    4
    5
    6
    7
    _lock = [[NSLock alloc] init];

    - (void)synchronizedMethod {
    [_lock lock];
    //safe
    [_lock unlock];
    }

2. 对于上面两种方法,有些缺陷,同步块会导致死锁,直接使用锁对象,遇到死锁,就会非常麻烦。

3. GCD以更简单、更高效的形式为代码加锁。

例子:

属性是开发者经常需要同步的地方,可以使用atomic 特质来修饰属性,来保证其原子性,每次肯定可以从中获取到有效值,然而在同一个线程上多次调用获取方法(getter),每次获取到结果未必相同,在两次访问操作之间,其他线程可能会写入新的属性值。

使用 “串行同步队列”,将读取操作及写入操作都安排在同一个队列里,即可保证数据同步。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
_syncQueue = dispatch_queue_create("com.yun.syncQueue", NULL);

- (NSString *)name {
__block NSString *rstName;
dispatch_sync(_syncQueue, ^{
rstName = _name;
});
return rstName;
}

- (void)setName:(NSString *)name {
dispatch_sync(_syncQueue, ^{
_name = name;
});
}

上面是用串行同步队列来保证数据同步:把设置操作与获取操作都安排在序列化的串行同步队列里执行,这样子,所有针对属性的访问操作都是同步的了。

进一步优化,设置方法不一定非得是同步的,因为不需要返回值。这样子可以提高设置方法的执行速度,而读取操作与写入操作依然会按照顺序执行。

优化:多个获取方法可以并发执行,而获取方法与设置方法不能并发执行。

我们还可以使用并发队列来实现,现在都是在并发队列上面执行任务,但是顺序不能控制,我们可以用栅栏(barrier)来解决。

这两个函数可以向队列派发块,将其作为栅栏来使用:

1
2
dispatch_barrier_sync(dispatch_queue_t queue,^(void)block)
dispatch_barrier_async(dispatch_queue_t queue,^(void)block)

在队列中,栅栏块必须单独执行,不能与其他块并行,这只对并发队列有意义,因为串行队列中的块总是按照顺序逐个执行的。并发队列如果发现接下来要处理的块是栅栏块,那么就一直要等到当前所有的并发块都执行完毕,才会单独执行这个栅栏块。执行完栅栏块,再按照正常方式向下处理。
*/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
-----> 现在并发队列 还不能满足要求
_syncQueue1 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
- (NSString *)name {
__block NSString * rstName;
dispatch_sync(_syncQueue1, ^{
rstName = _name;
});
return rstName;
}

- (void)setName:(NSString *)name {
dispatch_async(_syncQueue1, ^{
_name = name;
});
}

-----> 转换写法 用栅栏块控制属性的设置方法 不能并行
_syncQueue1 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
- (NSString *)name {
__block NSString * rstName;
dispatch_sync(_syncQueue1, ^{
rstName = _name;
});
return rstName;
}

- (void)setName:(NSString *)name {
dispatch_barrier_async(_syncQueue1, ^{
_name = name;
});
}

6.6 多用GCD,少用performSelector 方法

建议用GCD 替代performSelector,对于performSelector 遇到的问题,我们都可以用GCD 解决

  1. performSelector 可以任意调用方法,还可以延迟调用,还可以指定运行方法所用的线程,这些由 Objective-C 的动态性决定。

  2. 但是如果是动态来调用performSelector 方法的时候,编译器都不知道执行的选择子是什么,必须到了运行期才能确定,这种情况在ARC下会报警告,因为编译器不知道方法名,所以不能运用ARC内存管理规则来判定返回值是否应该释放,对于这种情况ARC不会帮我们添加任何释放操作。

  3. performSelector 方法调用的时候对于返回类型只能是void或对象类型,对于有返回值的需要自己做多次转换,对于参数的也最多只能传2个,介于此performSelector 还是比较不方便的。

  4. 如果想把任务放在另一个线程上执行,那么最好不要用performSeletor 系列方法,而是应该把任务封装到块里,然后调用大中枢派发机制的相关方法来实现。

6.7 掌握GCD 及操作队列的使用时机

1. 使用NSOperation执行后台任务

  • GCD在很多地方特别优秀,但是在执行后台任务时,GCD不一定是最佳方式,还有一种技术叫做NSOperationQueue,开发者可以把操作以NSOperation子类的形式放在队列中,而这些操作也可以并发执行。

  • GCD是纯C的API,操作队列的则是Objective-C的对象。用NSOperationQueue类的“addOperationWithBlock” 方法搭配NSBlockOperation类操作队列,其语法与纯GCD方式非常类似。使用NSOperation及NSOperationQueue 的好处如下:

1) 取消某个操作。

如果使用操作队列,那么想取消操作是很容易的。运行任务之前,可以在NSOperation 对象调用cancel 方法,该方法会设置对象内的标识位,用以表明此任务不需执行,不过,已经启动的任务无法取消。若不是操作队列,而是把块安排到GCD 队列,那就无法取消了。那套架构是 “安排好任务之后就不管了”。开发者可以在应用层自己来实现取消功能,不过这样子做需要编写很多代码,而那些代码其实已经由操作队列实现好了。

2. 指定操作间的依赖关系。

一个操作可以依赖其他多个操作。开发者能够指定操作之间的依赖关系,使特定的操作必须在另外一个操作顺序执行完毕方可执行,比方说,从服务器下载并处理文件的动作,可以用操作来表示,而在处理其他文件之前,必须先下载 “清单文件”。后续的下载操作,都要依赖于先下载清单文件这一操作。如果操作队列允许并发的话,那么后续的多个下载操作就可以同时执行,但前提是它们所依赖的那个清单文件下载操作已经执行完毕。

3. 通过键值观测机制监控NSOperation对象的属性。

NSOperation 对象有许多属性都适合通过键值观测机制(KVO)来监听,比如可以通过isCancalled 属性来判断任务是否取消。如果想在某个任务变更期状态时得到通知,或是想用比GCD 更为精细的方式来控制所要执行的任务,那么键值观测机制会很有用。

4. 制定操作的优先级。

操作的优先级表示此操作与队列其他操作之间的优先关系。优先级高的操作先执行,优先级低的后执行。操作队列的调度算法已经比较成熟。反之,GCD 则没有直接实现此功能的办法,GCD 的队列有优先级,但是是针对整个队列来说的,而不是针对每个块来说的。对于优先级这一点,操作队列所提供的功能比GCD 更为便利。

5. 重用NSOperation 对象。

系统内置类一些NSOperation 的子类供开发者调用,要是不想用这些固有子类的话,那就得自己来创建了。这些类就是普通的Objective-C 对象,能够存放任何信息。对象在执行时可以充分利用存于其中的信息,而且还可以随意调用定义在类中的方法。这比派发队列中哪些简单的块要强大。这些NSOperation 类可以在代码中多次使用。

6.8 通过Dispatch Group机制,根据系统资源状况来执行任务

1. 一系列任务可归入一个dispatch group之中。开发者可以在这组任务执行完毕时获得通知。

2. 通过dispatch group,可以在并发式派发队列里同时执行多项任务。此时GCD会根据系统资源状况来调度这些并发执行的任务。开发者若自己实现此功能,则需编写大量代码。

6.9 使用dispatch_once来执行只需运行一次的线程安全代码

  1. 只需执行一次的线程安全代码时,使用dispatch_once。

经常需要编写 “只需执行一次的线程安全代码”。通常使用GCD 所提供的dispatch_once 函数,很容易就能实现此功能。

使用dispatch_once 可以简化代码,并且彻底保证线程安全

  1. 单例示例

对于单例我们创建唯一实例,之前都是用@synchronized 加锁来解决多线程的问题,GCD 提供了一个更加简单的方法来实现。

1
2
3
4
5
6
7
8
9
//单例
+(instancetype)shareInstance{
static EOCClass *shareInstance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
shareInstance = [EOCClass new];
});
return shareInstance;
}

6.10 不要使用dispatch_get_current_queue

  1. dispatch_get_current_queue 函数的行为常常与开发者所预期的不同。此函数已经废弃(iOS 6.0起废除),只应做调试之用。

  2. 由于派发队列是按层级来组织的,所以无法单用某个队列对象来描述 “当前队列” 这一概念。

  3. dispatch_get_current_queue 函数用于解决由不可重入的代码引发的死锁,然而能用此函数的解决的问题,通常也能改用 “队列特定数据” 来解决。

EffectiveObjective-C2.0 笔记 - 第五部分

Posted on 2018-10-03 | Edited on 2018-11-05 | In iOS

EffectiveObjective-C2.0 笔记 - 第五部分

5 内存管理

5.1 理解引用计数

1、引用计数

Objective-C 语言使用引用计数来管理内存,每个对象都有个可以递增递减的计数器,用以表示当前有多少个事物想令此对象继续存活下去。当这个计数器归零那么这个对象就会被释放。

查看引用计数的方法叫做 retainCount 但是实际并不建议使用这个方法调试代码

NSObject 协议声明下面三个方法用于操作计数器,以递增或递减其值:

  • retain 递增保留计数
  • release 递减保留计数
  • autorelease 待稍后清理 “自动释放池” 时,再递减保留计数

在调用release 之后,对象所占的内存可能会被回收,这样子在调用对象的方法就可能使程序崩溃,这里 “可能” 的意思是对象所占的内存在 “解除分配” (deallocated)之后,只是放回 “可用内存池”(avaiable pool)。若果执行方法时尚未覆写对象,那么对象仍然有效。

为避免在不经意间使用无效对象,一般在调用完release 之后都会清空指针,保证不会出现可能指向无效对象的指针,这种指针通常被称为 “悬挂指针”(dangling pointer)。

所有的对象最终都间接或直接的被一个根对象所引用,macOS 应用是 NSApplication 对象,iOS 则是 UIApplication 对象,这两个对象都是应用启动时创建的单例

2、自动释放池 autorelease

调用release 会立刻递减对象的保留计数(这里可能会令系统回收此对象),调用autorelease 方法,并不会马上减少对象的引用计数,而是在下一次 Event Loop(事件循环)时减少,以达到延迟释放对象的效果。

autorelease 能延长对象声明周期,使其在跨越方法调用边界后依然可以存活一段时间。

调用 release 并不会使对象被释放,对象释放被释放取决于引用计数是否为 0

5.2 以 ARC 简化引用计数

1. 内存泄漏:

没有正确的释放已经不再使用的内存。

2. ARC自用引用计数

1) ARC 只是自动为代码添加内存管理相关的代码

ARC 是通过在编译时在我们的代码中插入对应的内存管理代码,并且只适用于 Objective-C 的代码,使用ARC 时,引用计数实际上还是要执行的,只是保留与释放操作是由ARC 自动添加的。

2) 在 ARC 下,不允许调用内存管理方法 retain,release,autorelease,dealloc

ARC会自动执行以下等操作,所以在ARC下调用这些内存管理方法是非法的。

  • retain
  • release
  • autorelease
  • dealloc

ARC 在调用这些方法时,并不是普通的Objective-C 消息派发机制,而是直接调用其底层的C 语言函数,这样子性能会更好。

3. 使用ARC 时必须遵循的方法命名规则

将内存管理语义在方法名中表示出来,若方法名以下列词语开头,则返回的对象归调用者所有:

  • alloc
  • new
  • copy
  • mutableCopy

4. 变量的内存管理语义

1) ARC 也会处理局部变量与实例变量的内存管理。

2 )我们通常会给局部变量加上修饰符来打破 “块”(block)所引入的 “保留环”(retain cycle)。

变量内存管理语义修饰符

  • __strong:默认,强引用,表示需要保留这个值
  • __weak:弱引用,表示不保留这个值,并且如果系统回收这个对象,那么在获取此变量的值的时候会的到 nil
  • __unsafe_unretained:不安全的引用,不保留此值,系统回收这个对象的时候,不会清空变量的值
  • __autoreleasing:把对象“按引用传递”给方法时使用,表示此值在方法返回时自动释放

5. ARC 如何清理实例变量

对实例变量进行内存管理,必须在 “回收分配给对象的内存” 时生成必要的清理代码。凡事具备强引用的变量,都必须释放,ARC 会在dealloc 方法中插入这些代码。

  • ARC 只负责管理Objective-C 对象的内存

ARC 会借用Objective-C++ 的一项特性来生成清理代码,在回收对象时,待回收对象会调用所有C++ 对象的析构函数,编译器如果发现某个对象里含有C++ 对象,就会生成名为.cxx_desteuct 的方法,ARC 借助此特性,在该方法中生成清理内存所需的代码。

  • 对于非Objective-C 的对象,需要我们手动清理。

如 CoreFoundation 对象不归ARC 管理,开发者必须适时调用CFRetain/CFRelease。

6. 覆写内存管理方法

非ARC 时可以覆写内存管理方法,在ARC 下禁止覆写内存管理方法,会干扰到ARC 分析对象生命周期的工作。

5.3 在 dealloc 方法中只释放引用,并解除监听

对象在经历生命周期后,最终会为系统回收,这时候就要执行dealloc 方法。每个对象生命周期内,此方法只会调用一次,也就是保留计数为0 的时候,绝对不能自己调用dealloc 方法,运行期会在适当的时候调用,一旦调用,对象就不再有效了,后续的方法调用均是无效的。

dealloc 方法主要是释放对象所拥有的引用,也就是把Objective-C 对象都释放掉,ARC 会通过自动生成的.cxx_desteuct 方法,在dealloc 中为你自动添加这些释放代码。但是其他非Objective-C 对象就需要自己手动释放了。

1. dealloc 方法中需要做的事情:

  • 释放对象所拥有的引用,持有的对象(ARC 下自动加入施放代码)
  • 清理观察者
  • 清理通知
  • 如果不使用 ARC,那么需要调用 [super dealloc] 方法

2. dealloc 方法中不适合做的事情:

  • 释放开销较大或系统内稀缺的资源(文件描述符,套接字,大量内存等)

因为 dealloc 方法并不会在特定时机调用,一般对于使用这样资源的对象都需要提供名字类似 open 和 close 的方法处理申请和释放资源的行为

  • 执行异步任务

异步方法执行后,对象可能已经施放

  • 尽量不要去调用方法,包括属性的存取方法

在dealloc 里尽量不要去调用方法,包括属性的存取方法,因为在这些方法可能会被覆写,并在其中做一些无法在回收阶段安全执行的操作。

5.4 编写 “异常安全代码” 时留意内存管理问题

1. C++ 和 Objective-C 的异常互相兼容,可以相互抛出捕获

纯C 中没有异常,C++与Objective-C 都支持异常,在运行期系统中C++与Objective-C 异常相互兼容,也就是说,从其中一门语言里抛出的异常能用另外一门语言所编写的 “异常处理程序” 来捕获。

2. 捕获异常时,一定要注意将try 块内创建的对象清理干净。

Objective-C 错误模型表明,异常只应发生严重错误后抛出,发生异常如何管理内存很重要,在try 块中保留某个对象的,但是在释放它之前抛出异常了,这时候就无法正常释放了,这时候需要借助@finally 块来保证释放对象的代码一定会执行,且只执行一次。

3. 默认情况下,ARC 不生成安全处理异常所需的清理代码。

在ARC 不会自动生成处理异常中的代码,因为这样子需要加入大量的样板代码,以便追踪待清理的对象,从而在抛出异常时将其释放。可以这段代码会严重运行期的性能,还会增加应用程序的大小。

可以通过-fobjc-arc-exceptions 这个编译编织来开启这个功能,但是这个功能不应该作为生成这种安全处理异常所用的附加代码,应该是让代码处于Objective-C++模式。

5.5 以弱引用避免保留环

1. 相互引用和对象引用环

几个对象都以某种方式互相引用,从而形成 “环”,这种情况通常会泄漏内存,因为没有东西引用环中对象,这样子环里的对象互相引用,不会被系统回收,会导致内存泄漏。

2. 避免保留环的最佳方式就是弱引用

  • 非 ARC 的情况下使用 assign 或者 unsafe_unretained 来修饰弱引用属性
  • ARC 的情况下使用 weak 来修饰弱引用的属性,因为 weak 的属性在对象被释放后会自动设置为 nil

一般来说,如果不拥有某对象,就不要保留它,这条规则对collection 例外,collection 虽然不直接拥有其内容,但是它要代表自己所属的那个对象来保留这些元素。

5.6 以 “自动释放池块” 降低内存峰值

1. 释放对象有两种方式:

1) 一种是调用release 方法,使其保留计数立即递减

ARC下不能主动调用

2) 一种是调用autorelease 方法

将对象放入 “自动释放池” 中,自动释放池用于存放那些需要稍后某个时刻释放的对象,清空(drain)自动释放池时,系统会向其中的对象发送release 消息。

创建自动释放池,系统会自动创建一些线程,这些线程默认都有自动释放池,每次执行 “事件循环”时,都会将其清空。

1
2
3
@autoreleasepool {
//...
}

2. 内存峰值:

是指应用程序在某个特定时段内的最大内存用量。如:循环创建大量对象的时候

  • 对象有可能会放在自动释放池里面,需要等到线程执行下一次事件循环才会清空,这里会导致应用程序所占内存会持续增加,等到临时对象释放的时候,内存用量又会突然下降。我们现在就想把这个内存峰值给降低下来。
1
2
3
4
5
for (int i = 0;i < 100000;i++){
@autorelease{
NSObject *object = [NSObject new];
}
}

5.7用 “僵尸对象” 调试内存管理问题

1. 僵尸对象用于调试代码是否会使用到已经被销毁的对象

向已回收的对象发送消息是不安全的,是否崩溃这个是看对象所占的内存有没有为其他内容所覆写。

  • Cocoa 提供 “僵尸对象”(Zombie Object)这个非常方便的功能,开启后,运行期系统会把已经回收的实例转换成特殊的 “僵尸对象”,而不会真正回收它们。这个对象所在的核心内无法重用,因此不可能遭到覆写,僵尸对象收到消息后,会抛出异常。

2. XCODE 设置

Xcode Scheme 中的Enable Zombie Objects 选项,打开会将NSZombieEnabled 环境变量设成YES。

  • 系统在即将回收时,会执行一个附加步骤,将对象转换成僵尸对象,而不彻底回收。僵尸类是从名为NSZombie 的模版类复制出来的。NSZombie 类并未实现任何方法,此类没有超类,因此跟NSObject 一样,也是一个 “根类”,该类只有一个实例变量,叫做isa,所以发给他的消息都要经过 “完整的消息转发机制” 。

  • 在完整的消息转发机制中,forwarding 是核心,检查接受消息的对象所属的类名,若是NSZombie ,则表示消息接受者是僵尸对象,需要特殊处理。

  • 系统在回收对象时,可以不将其真的回收,而是把它转化成僵尸对象。通过环境变量NSZombieEnabled 可开启此功能。

  • 系统会修改对象的isa 指针,令其指向特殊的僵尸类,从而使该对象变成僵尸对象。僵尸类能够响应所有的选择子,响应方式为:打印一条包含消息内容及其接受者的消息,然后终止应用程序。

5.8 不要使用 retainCount

1. retainCount 在反映调用者有多少对象引用,以及调试内存管理都没有任何帮助

  • 每个对象都有一个计数器,其表明还有多少个其他对象想令此对象继续存活。在ARC retainCount 这个方法已经废弃了,但是在非ARC 中也不应该调用这个方法,因为这个保留计数只是返回某个时间点的值,并不会联系上下文给出真正有用的值。

2. retainCount 在 ARC 环境下将会编译错误

  • retainCount 可能永远不返回0,因为系统有时候会优化对象的释放行为,在保留计数为1的时候就把它回收了。

  • 不应该依靠保留计数的具体址来编码。

  • 对象的保留计数看似有用,实则不然,因为任何给定时间点上的 “绝对保留计数”(absolute retain count)都无法反映对象生命期的全貌。

  • 引入ARC 之后,retainCount 方式就正式废止了,在ARC 下调用方法会导致编译器报错。

3. 发布版本时,一定关闭此功能

EffectiveObjective-C2.0 笔记 - 第四部分

Posted on 2018-09-24 | Edited on 2018-11-05 | In iOS

EffectiveObjective-C2.0 笔记 - 第四部分

4 协议与分类

4.1 通过委托与数据源协议进行对象间通信

1. 协议(protocol)类似 java 的接口(interface)。Objective-C 不支持多重继承,但我们可以把某个类应该实现的方法定义在一系列的协议里面。

  1. Objective-C 可以使用 “委托模式”(Delegate pattern)的编程设计模式来实现对象间的通信:

定义一套接口,某对象若想接受另一个对象的委托,则需遵从此接口,以便成为其 “委托对象”(delegate)。Objective-C 一般利用 “协议” 机制来实现此模式。

定义协议:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//采用 “驼峰法” 命名,最后加上 Delegate 一词
@protocol EOCNetworkingFetcherDelegate<NSObject>

// 可选实现的方法
@optional
- (void)newworkingFetcher:(EOCNetworkingFetcher *)fetcher
didRecevieData:(NSData *)data;

// 必须实现的方法
@required
- (void)newworkingFetcher:(EOCNetworkingFetcher *)fetcher
didFailWithError:(NSError *)error;

@end

@interface EOCNetworkingFetcher : NSObject

// weak,避免循环引用
@property (nonatomic,weak) id delegate;

@end

3. 如果要在委托对象上调用可选方法,那么必须提前使用类型信息查询方法,判断这个委托对象能否响应相关的选择子。

1
2
3
4
NSData *data;
if([_delegate respondsToSelector:@selector(networkFetcher:didRecevieData:)]){
[_delegate networkFetcher:self didRecevieData:data];
}

4. delegate 里的方法也可以用于从委托对象中获取信息(数据源模式)。

5. 在实现委托模式和数据源模式的时,协议中的方法是可选的,我们就会写出大量这种判断代码:

1
2
3
if([_delegate respondsToSelector:@selector(networkFetcher:didRecevieData:)]){
[_delegate networkFetcher:self didRecevieData:data];
}

每次调用方法都会判断一次,其实除了第一次检测的结构有用,后续的检测很有可能都是多余的,因为委托对象本身没变,不太可能会一下子不响应,一下子响应的,所以我们这里可以把这个委托对象能否响应某个协议方法记录下来,以优化程序效率。
将方法响应能力缓存起来的最佳途径是使用 “位段”(bitfield)数据类型。我们可以把结构体中某个字段所占用的二进制位个数设为特定的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 协议
@protocol EOCNetworkingFetcherDelegate<NSObject>

@optional
- (void) didReceiveData:(NSData *)data;

@end

@interface EOCNetworkingFetcher ()
// 定义位段
struct {
unsigned int didReceiveData : 1;
} _delegateFlags
@end

// 设置
-(void)setDelegate:(id<EOCNetworkingFetcherDelegate>)delegate{
_delegate = delegate;
_delegateFlags.didReceiveData = [_delegate respondsToSelector:@selector(didRecevieData:)]
}

//使用
if(_delageteFlags.didReceiveData){
[_delegate didRecevieData:data];
}

4.2 将类的实现代码分散到便于管理的数个分类之中

1. 使用分类机制把类的实现代码划分成易于管理的小块。

2. 将应该视为 “私有” 的方法归入为叫Private 的分类中,以隐藏实现细节。

3. 分类原则上不能定义属性,当可以通过【关联对象】实现,参见《2.5-关联对象存放自定义数据》

4.3 总是为第三方类的分类名称加前缀

1. 分类机制常用于向无源码的既有类中新增新功能,但是在使用的时候要十分小心,不然很容易产生Bug。因为这个机制时在运行期系统加载分类时,将其方法直接加到原类中,这里要注意方法重名的问题,不然会覆盖原类中的同名方法。

2. 一般用前缀来区分各个分类的名称与其中所定义的方法。

3. 不要轻易去利用分类来覆盖方法,这里需要慎重考虑。

4.4 勿在分类中声明属性

1. 可以利用运行期的关联对象机制,为分类声明属性,但是这种做法要尽量避免。

因为除了 “class-continuation 分类” 之外,其他分类都无法向类中新增实例变量,因此,他们无法把实现属性所需的实例变量合成出来。

2. 利用关联对象机制可以解决分类中不能合成实例变量的问题。

自己实现存取方法,但是要注意该属性的内存管理语义(属性特质)。

1
2
3
4
5
6
7
8
9
@property (nonatomic,copy) NSString *name;
static const void *kViewControllerName = &kViewControllerName;
- (void)setName:(NSString *)name {
objc_setAssociatedObject(self, kViewControllerName, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)name {
NSString *myName = objc_getAssociatedObject(self, kViewControllerName);
return myName;
}

3. 在可以修改源代码的情况下,尽量把属性定义在主接口中。

这里是唯一能够定义实例变量的地方,属性只是定义实例变量及相关存取方法所用的 “语法糖”。

4.5 使用 ”class-continuation 分类“ 隐藏实现细节

“class-continuation 分类”必须定义在本身类的实现文件中,而且这里是唯一可以声明实例变量的分类(除关联对象机制)。

而且此分类没有特定的实现文件,这个分类也没有名字。这里可以定义实例变量的原因是 “ 稳固的ABI” 机制,我们无须知道对象的大小就可以直接使用它。

1
2
@interface EOCPerson ()
@end

私有的方法、协议、变量等,都可以定义在“class-continuation 分类”

1. 可以将不需要要暴露给外界知道的实例变量及方法写在 “class-continuation 分类” 中。

2. 可以利用 “class-continuation 分类” 把引用C++ 类的细节写到实现文件中,这样子别的类引用这个类就不会受到影响,甚至都不知道这个类底层实现混有C++ 代码。

编写Objective-C++ 代码时候,使用 “class-continuation 分类” 会十分方便。因为对于引用了C++的文件的实现文件需要用.mm 为扩展名,表示编译器应该将此文件按照Objective-C++ 来编译。C++ 类必须完全引入,编译器要完整地解析其定义才能得知这个C++ 对象的实例变量大小。如果把对C++ 类的引用写在头文件的话,其他引用到这个类也会引用到这个C++ 类,就也需要编译成Objective-C++ 才行,这样子很容易失控。

3. 使用 “class-continuation 分类” 还可以将头文件声明 “只读” 的属性扩展成 “可读写”,以便在类的内部可以设置其值。

我们通常不直接访问实例变量,而是通过设置方法来做,因为这样子可以触发 “键值观测” (Key-Value Observing,KVO)通知。

4. 若对象所遵循的协议只应视为私有,也可以同过“class-continuation 分类” 来隐藏。

4.6 通过协议提供匿名对象

1
@property (nonatomic,weak) id delegate;

该属性类型是id的,所以实际上任何类的都能充当这一属性,即便该类不继承NSObject 也可以,只要遵循EOCDelegae 协议就可以了,对于具备此属性的类来说,delegate 就是 “匿名的”。

1. 使用匿名对象来隐藏类型名称(或类名)。

2. 如果具体类型不重要,重要的是对象能够响应(定义在协议里的)特定方法,那么可使用匿名对象来表示。

EffectiveObjective-C2.0 笔记 - 第三部分

Posted on 2018-09-20 | Edited on 2018-11-05 | In iOS

EffectiveObjective-C2.0 笔记 - 第三部分

3 接口与 API 设计

3.1 用前缀避免命名空间冲突

1.如果发生命名冲突(naming clash),那么应用程序的链接过程就会出错,因为出现了重复符号(duplicate symbol)。

2.应该为所有名称都加上适当的前缀,最好是++三个字母以上++做前缀,因为Apple宣称其保留使用所有 “两字母前缀”。

3.在类的实现文件所有的纯C 函数及全局变量,也是容易命名冲突的,在编译好的目标文件中,这些要算做 “顶级符号”(top-level symbol)。

3.2 提供 “全能初始化方法”

1.“全能初始化方法”(designated initializer):为对象提供必要信息以便其能完成工作的初始化方法。

2.每个子类的全能初始化方法都应该调用其超类的对应方法,并逐层向上。

  • 在类中提供一个全能初始化方法,并于文档里指明。其他初始化方法均应调用此方法。
  • 若全能初始化方法与超类不同,则需覆写超类中的对应方法。
  • 如果超类的初始化方法不适用于子类,那么应该覆写这个超类方法,并在其中抛出异常。

3.3 实现 description 方法

1.在调用NSLog(@”object = %@”,onbject); 其实是调用了对象的description 方法。

2.在我们自定义类中,这样子打印输出信息有可能是这种object =,这个我们需要重写description 方法,让它返回我们需要的一些信息。

3.description 定义在NSObject 协议里面,因为NSObject 不是唯一的 “根类”,用继承不能很好的让其他类有这个方法

例如:NSProxy 也是遵从了NSObject 协议的 “根类”。

4.debugDescription 方法是开发者在调试器中以控制台命令打印对象时才调用的,默认是直接调用description 方法。

5.小技巧:可以在description 中用NSDictionary 的description 方法来输出,就是将信息用字典的形式来展示,这样子更加直观,也更加容易扩展。

3.4 尽量使用不可变对象

1.设计类的时候,用属性来封装数据,在用属性的时候,可将其声明为 “只读” ,避免外部不必要的修改

PS:如果把可变对象放到collection 之后又修改其内容,很容易会破坏set 的内部数据结构,使其失去固有的语义。

2.尽量把对外公布出来的属性设为只读,而且只在确有必要时才将属性对外公布。

3.当我们想外部暴露只读属性、内部需要修改属性,这样子通常是在内部将readonly 属性重新声明为readwrite。但是如果该属性是nonatomic 的,这样子做可能会产生 “竞争条件”(rece condition)。在对象内部写入某属性时,对象外的观察者也许正在读取该属性。若想避免此问题,我们可以在必要时通过 “派发队列”(dispatch queue)等手段,将所有的数据存取操作都设为同步操作。

4.虽然属性对外设置成readonly 了,但是外部仍能通过 “键值编码”(Key-Value Coding,KVC)技术设置这些属性值。[object setValue:@”abc” forKey:@”name”] ,这样子可以修改name 这个属性,KVC 会在类中查找 “setName:” 方法来修改属性值。

5.还可以通过类型信息查询功能,查出属性所对应的实例变量在内存中的偏移量,从此来人为设置这个实例变量的值。

注意点总结:

  • 尽量创建不可变的对象。
  • 若某属性仅可于对象内部修改,则在 “class-continuation 分类” 中将其由readonly 属性扩展成readwrite 属性。
  • 不要把可变的collection 作为属性公开,而应提供相关方法,以此修改对象中的可变collection。

3.5 使用清晰而协调的命名方式

1、方法与变量命名

方法和变量名使用 “驼峰式大小写命名法”:以小写字母开头,其后每个单词首字母大写。

1.方法名言简意赅,能准确表达方法功能,不易太长。

2. 如果方法的返回值是新创建的,那么方法名的首个词应该是返回值的类型

1
2
3
- stringWithString

- intValue

除非前面还有修饰语,例如localizedString。属性的存取方法不遵循这种命名方式。

1
2
3
- localizedStringWithFormat:

- initWith

3. 不要使用str这种简称,应该使用string 这样的全称。

4. Boolean 属性应加is前缀。如果某方法返回非属性的Boolean 值,那么应该根据其功能,选用has 或is 当前缀。

5. 将get 这个前缀留给那些借由 ”输出参数“ 来保存返回值的方法,比如说,把返回值填充到 ”C语言式数组“ 里的那种方法就可以使用这个词做前缀。

2、类与协议的命名

1. 类名也采用驼峰式命名法,不过其首字母需要大写,通常还会加三个以上前缀字母,避免命名空间冲突。

2. 命名应该协调一致,从其他框架继承子类,务必遵循其命名惯例。

UIView 子类末尾必须是View,委托协议末尾必须是Delegate。

3. 起名时应遵从标准的Objective-C 命名规范,这样子创建出来的接口更容易为开发者所理解。

3.6 为私有方法名加前缀

1. 给私有方法的名称加上前缀,这样可以很容易地将其同公共方法区分开。

一种方案是加前缀 p_

2. 不要单用一个下划线做私有方法的前缀,因为这种做法是预留给苹果公司用的。

3.7 理解Objective-C 错误模型

1. ARC 默认不是 “异常安全的”,如果抛出异常,那么应在作用域末尾释放的对象现在却不会自动释放了。

想要生成 “异常安全的” 代码,可以设置编译器的标志来实现 “-fobjc-arc-exceptions”。但是这样没有异常的情况也会执行这些代码。

2. 平常很难写出在抛出异常时不会导致内存泄漏的代码,Objective-C 语言现在采用的办法是:只在极其罕见的情况下抛出异常,抛出异常应用程序直接退出,不考虑修复问题,不用再写复杂的 “异常安全” 代码。

3. 只有发生了可使整个应用程序崩溃的严重错误时,才应使用异常。

4. 在 “不那么严重的错误”,令方法返回nil/0,或者是使用NSError,表明其中有错误发生。

创建对象时,可用 返回nil/0/-1

方法逻辑错误或者需要详细错误信息时,考虑 NSError

5. NSError 可以经由此对象,把导致错误的原因回报给调用者。

NSError 可封装3条信息:

  • Error domain (错误范围,其类型为字符串)
  • Error code (错误码,其类型为整数)
  • User info (用户信息,其类型为字典)

1)通过委托协议(delegate)来传递NSError

1
- (void) connection:(NSURLConnection *)connection didFailWithError:(NSError *)error;

2)经由方法的 “输出参数” 返回给调用者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 方法定义
// 返回 BOOL,容易判断(不需要查询 error 参数)。
// NSError ** 为指向指针(NSError *)的指针
- (BOOL) doSomething:(NSError **)error {
if(/*there was an error*/){
// error 参数不是nil,不然解引用异常
if(error){
// *error 解引用,即NSError *指针指向新对象。
*error = [NSError errorWithDomain:domain
code:code
userInfo:userInfo];
return NO;
}
}else{
return YES;
}
}

// 用法
NSError *error = nil; // 如果想获取 eroor 详情,不能为 nil
BOOL ret = [objecr doSomething:&error]
if(ret){
//to do
}

3.9 理解NSCopying 协议

1. 使用对象经常需要拷贝它,此操作通过copy 方法完成。如果想令自己的类支持拷贝操作,那就实现NSCopying 协议,该协议只有一个方法:

1
- (id)copyWithZone:(NSZone *)zone

不必担心zone参数

以前开发程序,会把内存分成不同的 “区”(zone),而对象会创建在不同区里面,现在不用了,每个程序只有一个区:“默认区”(default zone)。因此不必担心 zone 参数

2. NSMutableCopying 协议跟NSCopying 类似,也只有一个方法:

1
- (id)mutableCopyWithZone:(NSZone *)zone

3. 如果你的类分可变版本与不可变版本,这两个协议你都应该实现。

4. 注意:在可变对象上调用copy 方法返回另外一个不可变类的实例。

1
2
- [NSMutablArray copy] => NSArray
- [NSArray mutableCopy] => NSmutableArray

5. 在编写拷贝方法时,还要确定一个问题:应该执行 “深拷贝”(deep copy)还是 “浅拷贝”(shallow copy)。

6. 深拷贝是指在拷贝对象自身时,将其底层的数据也一并复制过去;浅拷贝只对拷贝对象的指针,并不会拷贝底层的数据。Foundation 框架中的所有collection 类默认都执行浅拷贝。

7. 没有专门定义深拷贝的协议,所以具体执行方式由每个类来确定。另外不要假设遵从了NSCopying 协议的对象都会执行深拷贝。绝大多数情况下,执行的都是浅拷贝。

8. 如果你所写的对象需要深拷贝,那么可以考虑新增一个专门执行深拷贝的方法。

1
- (id)initWithSet:(NSArray *)array copyItems:(BOOL)copyItems

EffectiveObjective-C2.0 笔记 - 第二部分

Posted on 2018-06-18 | Edited on 2018-10-10 | In iOS

EffectiveObjective-C2.0 笔记 - 第二部分

2.1 属性

1. “对象”(object)就是 “基本构造单元”(building block),开发者可以通过对象来存储并传递数据。

在对象直接传递数据并执行任务的过程就叫做 “消息传递”(Messaging)。

2. 程序运行起来后,为其提供相关支持的代码叫做”运行期环境”(runtime),它提供一些使得对象之间能够传递消息的重要函数。

理解运行期环境,可以帮你写出高效且易维护的代码。

3. Oc编译采用“应用程序二进制接口”(Application Binary Interface,ABI)

把实例变量当作一种存储偏移量所用的 “特殊变量”(speacial variable),交由 “类对象”(class object)保管。偏移量会在运行期查找,这样子总能找到正确的偏移量,这是稳固。

如果对象布局在编译器就固定了,访问变量时,编译器会使用 “偏移量”(offset)来计算,这个偏移量是 “硬编码”(hardcode),表示该变量距离存放对象的内存区域的起始地址有多远。 存在一个问题:如果代码使用了编译期计算出来的偏移量,那么修改类定义之后必须重新编译,否则就会出错。

@property

1. 用于声明属性,自动添加实例变量,以下划线开头,自动实现属性的读写方法。

2. 在实现文件中可以通过@synthesize 语法来指定实例变量的名字

1
2
3
@implementation EOCPerson
@synthesize name = _myName;
@end

3. @dynamic 关键字会告诉编译器:不要自动创建实现属性所用的实例变量,也不要为其创建存取方法

属性特质

原子性、读写权限、内存管理语义、方法名、其他。

1、原子性

  • atomic -默认

    • 原子性,会生成读写锁,读写安全(线程不一定安全),占用资源、效率一般。
  • nonatomic

    • 非原子、效率高、读写不安全

2、读写权限

  • readwrite -默认

    • 读写
  • readonly

    • 只读

3、内存管理

MRC时,有assign、retain、copy,ARC加入了strong、weak

  • assign -值类型默认

    • 简单赋值、用于值类型,如CGFloat、NSInteger等
  • strong (同retain -MRC) -引用类型默认

    • 强引用、用于引用类型

    • 赋值时,保留新值,新值引用计数+1,释放旧值(引用计数-1)。

    • 用于所有的实例变量和局部变量、其他常规对象引用。

    • 注意:可变对象应该使用strong,如NSMultiString,NSMultiArray

  • copy

    • 复制、用于引用类型

    • 赋值时,拷贝新值(新对象引用计数为1),释放旧值(引用计数-1),不改变新值(引用计数不变)。

    • copy的本质为复制该内存所存储的内容,重新创建一个对象赋给其相同的内容,对于实现了NSCopying协议的对象有效。

    • 用于不可变对象:NSString、block、NSArray、NSDictionary等

    • 注意:用于可变对象时,设置值后,变为不可变对象

  • weak

    • 弱引用、用于引用类型

    • 赋值时、单纯的引用新对象地址,不改变新对象(引用计数不变),不改变旧对象(引用计数不变)

    • 当引用对象释放后,其值置为nil

  • __unsafe_unretained

    • 类似assign、适用于引用类型、不安全的弱引用

    • 功能类似于weak、对象摧毁后,不置nil、不安全,可用weak代替

4、方法名

  • getter = methodname

    1
    @property (nonatomic, getter = isOn) BOOL on;
  • setter = methodname

    1
    @property (nonatomic, setter = setOnState) BOOL on;

5、其他

nonnull, null_resettable, nullable

2.2 对象访问

1. 对象访问有两种、一种是实例访问、一种是属性的读写方法访问。

  • 一般可以这样做:读取时,通过实例读取、写入时,通过属性方法写入。初始化时,都用实例。

  • 一般外部访问时,通过属性访问。

  • 内部访问时、无特殊情况,通过实例访问。

2. 具体情况应该根据他们的特点来定:

  • 实例访问不通过属性方法派发、效率高。

  • 实例访问、不触发“键值观测”,无法满足某些场景。

  • 初始化方法中,尽量实例访问,避免子类重写设置方法,导致出错。

  • 如果待初始化的实例声明在超类中,而我们又无法在子类直接访问此实例变量的话,那么就需要调用 “设置方法” 了。

  • 在 “惰性初始化”(lazy initialization),必须通过 “获取方法” 来访问属性,不然实例变量永远不会初始化。

1
2
3
4
5
6
7
-(EOCBrain *)brain{
if(!_brain){
_brain = [EOCBrain new];
}

return _brain;
}

2.3 对象同等性

1. == 与 isEqual

  • ==

    • == 用于值对象时,可以直接判断值是否相等。

    • == 用于引用对象时,是判断两个对象的指针是否相等(为同一个对象),不能判断其内容等同。

  • isEqual

    • 用于引用类型、判断内容是否等同、常需要ovewrite该方法。

2. NSObject 协议中有两个用于判断等同性的关键方法:

1
2
- (BOOL)isEqual:(id)object;
- (NSUInteger)hash;
  • 如果 “isEqual” 方法判定两个对象相等,那么其hash 方法也必须返回同一个值。

  • 但是,如果两个对象的hash 方法返回同一个值,那么 “isEqual” 方法未必会认为两者相等。

hash方法实现的一些情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 1-固定值
- (NSUInteger)hash {
return 12312312;
}
// 这种会对collection使用这个对象产生性能问题。因为在collection 在检索哈希表的时,会用对象的哈希码来做索引,在set 集合中,会根据哈希码把对象分装到不同的数组里面,在添加新对象的时候,要根据其哈希码找对与之对应的数组,依次检查其中各个元素,看数组已有的对象是否和将要添加的新对象相等,如果相等,就说明添加的对象已经在set 集合中了,是添加失败的。(如果所有对象的hash 值对一样,这样子set 集合只会有一个数组,所有数据都在一起了,每次插入数据都会遍历这个数组,这样子就会出现性能问题)

// 2-组合值
- (NSUInteger)hash {
NSString *stringToHash = [NSString stringWithFormat@"%@:%@",_firstName,_lastNmae];
return [stringToHash hash];
}
//这样子能在一定情况下保证返回不同的哈希码,但是这里会存在创建字符串的开销,会比返回单一值要慢

// 3-位运算
- (NSUInteger)hash {
return [self.firstName hash] ^ [self.lastNmae hash];
}

// ^为逐位逻辑运算符,它表示逐位非或(如果只有一个位为1,那么结果为1;否则为0。)。
//这样子可以保存较高的效率,又不会过于频繁的重复

3. 特定类所具有的等同性判定方法

1
isEqualToString、isEqualToArray、isEqualToDictionary

4. 容器中可变类的等同性

  • 如果要把某个对象放入colloection ,其 hash 方法的生成策略就应该保证在放入colloection 后,hash 值不再改变。不然会出现问题。

2.4 类族模式

1. “类族” (class cluster)是一种很有用的模式(pattern),可以隐藏 “抽象基类” (abstract base class)背后的实现细节。

2. 用户无须自己创建子类实例,只需要调用基类方法来创建即可。

3. 如何创建类族

  • 每个 “实体子类” 都从基类继承而来,“工厂模式” 是创建类族的办法之一,调用基类方法返回子类实例。

  • 如果对象所属的类位于某个类族中,那么查询其类型信息要注意,你可能觉得自己创建了某个类的实例,然后实际上创建的却是其子类的实例。

1
2
-(BOOL) isKindOfClass: classObj; //判断是否是这个类或者这个类的子类的实例
-(BOOL) isMemberOfClass: classObj; //判断是否是这个类的实例

2.5 关联对象存放自定义数据

1. 关联对象

可以给某个对象关联许多其他对象,这些对象通过“键”来区分。存储对象值的时候,可以指明“存储策略”,用以维护相应的“内存管理语义”。

1
2
3
4
5
OBJC_ASSOCIATION_ASSIGN --- assign
OBJC_ASSOCIATION_RETAIN_NONATOMIC --- nonatomic, retain
OBJC_ASSOCIATION_COPY_NONATOMIC --- nonatomic, copy
OBJC_ASSOCIATION_RETAIN --- retain
OBJC_ASSOCIATION_COPY --- copy

下列方法可以管理关联对象:

1
2
3
4
5
6
7
8
void objc_setAssociatedObject (id object, void *key, id value, objc_AssociationPolicy policy)
// 此方法以给定的键和策略为某对象设置关联对象值

id objc_getAssociatedObject(id object, void *key)
// 此方法根据给定的键从某个对象中获取相应的关联对象值

void objc_removeAssociatedObject(id object)
// 此方法移除指定对象的全部关联对象

若想令两个健匹配到相同的一个值,则二者必须是完全相同的指针才行。所以,在设置关联对象值时:通常使用静态全局变量做键。

2.6 消息

一. 消息传递(objc_msgSend)

1. 调用对象方法,在Objective-C 中叫做 “传递消息”(pass a message),消息有 “名称”(name)或“选择子”(selector),可以接受参数,而且可能还有返回值。

objc_megSend 的原型:

1
2
3
4
5
6
7
8
// 方法原型
// messageName 叫做 selector(选择子),选择子和参数合起来称为"消息"。
id returnValue = [receiveObject messageName:parameter];

// 所有方法都是普通的 C 语言函数,方法转为标准的 C 语言函数如下:
// 是一个 “参数个数可变的函数”,能够接受两个或两个以上的参数,
// 第一个参数代表接收者,第二个参数代表选择子,后续参数就是参数。
void objc_msgSend(id self,SEL cmd,...)

2. objc_megSend 函数会依据接收者和选择子来调用适当的方法:

  • 在接收者所属的类搜寻其 “方法列表”
  • 找不到的话,就沿着继承体系继续向上查找
  • 最终还是找不到相符的方法就执行 “消息转发”

3. 每个类里都有一张函数表,选择子的名称则是表的 “键”,对应的值都是指向函数的指针。objc_msgSend 等函数就是通过这个函数表来寻找应该执行的方法并执行跳转的。

4. objc_msgSend 会将匹配结果缓存在 “快速映射表”(fast map)里面,每个类都有这样子的一块缓存,接下来还向该类发送一样的消息,那么执行起来就很快了。

5. 这里有些特殊情况,需要由Objective-C 运行环境的另外一些函数来处理:

  • objc_msgSend_stret :如果待发送的消息要返回结构体,那么可以交由此函数处理。只有当CPU 寄存器能够容纳得下消息返回类型时,这个函数才能处理此消息。若是返回值无法容纳于CPU 寄存器(比如说返回的结构体太大了),那么就由另外一个函数执行派发。此时,那个函数会通过分配在栈上的某个变量来处理消息所返回的结构体。
  • objc_msgSend_fpret:如果消息返回的是浮点数,可以交由此函数处理。这个函数是为了处理x86 等架构CPU 中某些令人惊讶的奇怪状况。
  • objc_msgSendSuper:如果要给超类发消息,那么就交由此函数处理。

6. 如果某函数的最后一项操作是调用另外一个函数,那么就可以运用 “尾调用优化” 技术。编译器会生成跳转至另外一个函数所需的指令码,而且不会向调用栈推入新的 “栈帧”。

二、 消息转发

当对象接收到无法解读的消息后,就会启动 “消息转发”(message forwarding)机制,程序员可经由此过程告诉对象应该如何处理未知消息。

消息转发分为两大阶段:

1. 动态方法解析

第一阶段选征询接收者,所属的类,看其是否能动态添加方法,以处理当前这个 “未知的选择子“(unknown seletor),这叫做 ”动态方法解析“(dynamic method resolution)。

1
2
3
//表示这个类是否能新增一个方法来处理此选择子
+ (BOOL)resolveClassMethod:(SEL)sel
+ (BOOL)resolveInstanceMethod:(SEL)sel

2. 完整的消息转发机制

第二阶段涉及 ”完整的消息转发机制“(full forwarding mechanism)。

如果运行期系统已经把第一阶段执行完了,那么接收者自己就无法再以动态新增方法的手段来响应包含该选择子的消息了。这里的第二阶段又分为下面两小步:

  • 1) 备援的接收者

    首先,请接收者看看有没其他对象能处理这条消息;若有,则运行期系统会把消息转给那个对象,于是消息转发过程结束,一切正常。

1
- (id)forwardingTargetForSelector:(SEL)aSelector
  • 2) 若没有 ”备援的接收者“(replacement receiver),则启动完整的消息转发机制。

    运行期系统会把与消息有关的全部细节都封装到NSInvocation 对象中,再给接受者最后一次机会,令其设法解决当前还未处理的这条消息。

1
- (void)forwardInvocation:(NSInvocation *)anInvocation

2.7 方法调配

1. 不需要源代码,也不需要通过继承子类来覆写方法就能改变这个类本身的功能,新功能在本类的所有实例都生效,此方案称为 “方法调配”(method swizzling)。

2. 每个类有个方法列表(函数指针 IMP),各自映射到自己的方法实现,只要我们能操作这个函数指针的指向,我们就可以动态的增加替换原有的方法。

3. 互换两个已经写好的方法实现:

1
2
3
4
5
// 获取方法实现:
Method class_getInstanceMethod(Class cls, SEL name)

// 调换方法实现
void method_exchangeImplementations(Method m1, Method m2)

4. 为已有方法增加新功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

BOOL didAddMethod =
class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));

if (didAddMethod) {
class_replaceMethod(class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}

2.8 理解 “类对象” 的用意

1. 每个Objective-C 对象实例都是指向某块内存数据的指针。

2. Objective-C 对象所用的数据结构

1
2
3
4
5
struct objc_object {
Class isa;
};
/// A pointer to an instance of a class.
typedef struct objc_object *id;

每个对象结构体首个成员是Class 类的变量,定义了对象所属的类,通常称为 “is a” 指针。

3. Class 对象的数据结构定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;

Class 首个变量也是isa 指针,说明Class 本身也是Objective-C 对象,指向 “元类”(meta class)。

4. 在类继承体系中查询类型信息

1
2
3
4
5
// 判断对象是否为某个特定类的实例,不能判定 super 类
isMemberOfClass

// 判断出对象是否为某类或其派生类的实例
isKindOfClass

EffectiveObjective-C2.0 笔记 - 第一部分

Posted on 2018-06-16 | Edited on 2019-01-08 | In iOS

EffectiveObjective-C2.0 笔记 - 第一部分


1.1-了解Objective-C

了解Objective-C 语言的起源

1. Objective-C(以下简称Oc)是在C语言的基础上添加了面向对象特性。

Oc是C语言的超集(superset),因此C语言的所有功能特性都可以适用于Oc。

2. Oc是使用“消息结构”(messaging structure),而非常见的“函数调用”(function calling)。它们区别像这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// message structure 
Object *obj = [Object new];
[obj performWith:para1 and:para2]

// 其特性就是“运行时组件(Runtime)”,其本质上就是一种与开发者所编代码相链接的 “动态库”(dynamic libary),其代码能把开发者编写的所有程序粘合起来。
// 运行时所执行的代码由运行环境决定,动态特性明显,但是有些问题编译期间无法发现。
// 所有方法,都是运行时去查找,运行。接收消息的对象也要在运行时去查找。这时候就可能出问题。见后面。

// functions calling
Object *obj = new Object;
obj->perform(para1,para2);

// 与消息型相反,函数方法都有编译器编译的时候实现,可以预先发现一些潜在问题。
// 运行时所执行的代码由编译器决定;
// 如果是多态方法,运行时就会去“虚方法表(virtual table)”查找出具体哪一个函数;

3. Oc中的对象总是分配在“堆空间”(heap space),不会分配到“栈”(stack)上。

Oc 将堆内存管理抽象出来了,不需要用malloc 及free 来分配或释放对象所占内存,Oc
运行期环境把这部分工作抽象成一套内存管理架构,叫 ”引用计数“ 。一个例子

1
2
NSString *someString = @"The string";
NSString *anotherString = someString;

image

4. 能用C的结构体,不用对象,结构体比对象更有效率。

对象要分配空间,释放空间,而结构体不需要。结构体储存在栈空间。

5. 非对象类型(nonobject type),分配在栈上,在其栈帧弹出时自动清理。

1.2-头文件

核心点:在类的头文件中尽量少引用其他头文件

1. Oc 中编写类方式与 C和C++一样,使用头文件(header file)、实现文件(implementation file)来区隔代码。

2. 引用其他类,使用@class xxx的向前声明方式(forward declaring)。

也可以使用#import或#include,但是不够优雅,这里就要知道引用 类的具体细节,这里会引用到引用类的具体实现,会增加编译时间。使用@clss 还可以减少两个类之间的耦合。

3.应该将引入头文件的时机尽量延后(放在实现文件),只有确有需要的时候才引用,这样子可以减少类的使用者所需引用的头文件数量。缩短编译时间。

  • 可以使用@class时,首选@class。

  • 只有在迫不得已的时候才用#import (如:继承,实现协议)。

  • 协议建议放在单独的一个头文件。避免引入协议时,引入头文件中等其他内容。

  • 使用@class 可以减少.h中对其他类的依赖、减少链接到其他类所需要的时间,从而降低编译时间。

4. 两个类互相引用时: A类中引用B类,B类中也引用A类。必须用@class,不然会出现循环引用。

用#import 而不用 #include

  • import可以避免重复引用

  • 如果用#include的话,需要进行避免重复的宏定义

1
2
3
4
5
6
#ifndef HEADER
#define HEADER

xxx

#endif

1.3-字面量语法、常量、枚举

一、尽量用字面量语法,便于理解

1. 字面数值

1
2
3
4
5
// 字面量语法
NSNumber *itemNo = @1;

// 传统声明
NSNumber *itemNo = [NSNumber numberWithInt:1];

2. 字面量数组

不能有nil值,nil值为结尾标示

1
2
3
NSArray *arr1 = @[@"1",@"2"];

NSArray *arr2 = [NSArray arrayWithObjects:@"1",@"2",nil]; // 注意nil结尾

3. 字面量字典

不能有nil值,nil值为结尾标示

1
2
NSDictionary *dic = @{@"key1":@"val1",
@"key2":@"val2"};

4. 可变数组和字典

字面创建的都是不可变类型,如果想创建可变类型,需要mutableCopy

1
2
NSMutableDictionary *dic = [@{@"key1":@"val1",
@"key2":@"val2"} mutableCopy];

5. 字符串字面量创建的是常量,对象不在持有了也不会立马被释放

1
2
3
4
5
6
7
8
// Oc会做字符串的编译单元,而且会合并相同字符串的编译单元,来减少额外的消耗去链接这些编译单元。

NSString str1 = @“i am yun”;
NSString str2 = @“i am yun”;

// 此时,str1跟str2内存地址是一样的。

// 字符串常量创建后,不再修改。即使引用它的对象不再指向它,字符串常量也不会立即施放。

二、 多用类型常量,少用预处理

1. 不要用预处理命令定义常量,用静态常量代替

预处理命令定义的,不含有类型信息

2. 类内使用的常量,定义在实现文件,可用k做前缀

1
2
// in the implementation file
static const int kTimeItv = 1;

3. 如果需要其他类引用常量,在接口用extern定义,在实现文件实现,可用类名做前缀,如通知键值

1
2
3
4
5
// in the interface
extern const NSString *notiKey;

// in the implementation file
const NSString *notiKey = @"notiKey";

4. 编译器会在 “数据段”(data section)为字符串分配存储空间,这里在上面C 语言的内存模型有讲,数据段通常是指用来存放程序中已初始化的全局变量的一块内存区域。数据段属于静态内存分配。

三、 用枚举表示状态、选项、状态码

1. 用宏来定义枚举类型

这些宏具备向后兼容(backward compatibility)能力,如果目标平台编译器支持新标准,那就使用新式语法,否则改用旧式语法。

  • NS_ENUM宏 定义通用枚举
1
2
3
4
5
typedef NS_ENUM(NSInteger, NSWritingDirection) {
NSWritingDirectionNatural = -1, //值为-1
NSWritingDirectionLeftToRight = 0, //值为0
NSWritingDirectionRightToLeft = 1 //值为1
};
  • NS_OPTIONS宏 定义位移枚举
1
2
3
4
5
6
7
typedef NS_OPTIONS(NSUInteger, UISwipeGestureRecognizerDirection) {
UISwipeGestureRecognizerDirectionNone = 0, //值为0
UISwipeGestureRecognizerDirectionRight = 1 << 0, //值为2的0次方
UISwipeGestureRecognizerDirectionLeft = 1 << 1, //值为2的1次方
UISwipeGestureRecognizerDirectionUp = 1 << 2, //值为2的2次方
UISwipeGestureRecognizerDirectionDown = 1 << 3 //值为2的3次方
};

2. 在switch 语句中,最好不要有default 分支,这样子要做到处理所有样式,这样子在新家类型的时候,没有default 编译器会发出警告,让我们注意到。

3. 实现枚举所用的数据类型取决于编译器,不过其二进制位(bit)的个数必须能完全表示下枚举编号才行,一个字节含8个二进制位,所以至多能表示256(2^8^)个枚举变量。

iOS9 因为图片莫名闪退

Posted on 2018-05-21 | Edited on 2018-09-26 | In iOS

iOS9 因为图片莫名闪退

一般错误信息与图片相关,如

1
-[CUIStrucTuredThemeStore renditionWithKey:usingKeySignature:]

仅在 iOS9及一下系统出现

问题说明

ios9.3以下系统不支持非RGB 色域的图片,需要排查所有图片。

排查步骤

1. 获取ipa 文件

  1. 直接从 Xcode 导出

  2. 从 iTuns 下载(没找到在哪儿)

2. 获取Assets.car文件

  1. 解压ipa, 找到 Payload 中的 .app 文件, 显示包内容,找到 Assets.car 文件,拷贝到工作目录。

3. 获取asset.json文件

1
2
3
4
5
cd 工作目录

sudo xcrun --sdk iphoneos assetutil --info ./Assets.car > asset.json

输入密码,生成asset.json文件

4. 查找非 RBG图片

查询 asset.json文件中的”DisplayGamut” : “P3”,即为不能用的图片

补充

在查阅资料时发现, 很多资料都提到过在项目中运行一个脚本将P3图片进行转换, 由于此种方法朕没有实际验证过, 所以只做个摘录

1
2
3
4
5
#!/bin/bash DIRECTORY=$1 echo "----Passed Resources with xcassets folder argument is <$DIRECTORY>" echo "----Processing asset:"

find "$DIRECTORY" -name '*png' -print0 | while read -d $'\0' file; do echo "---------$file" sips -m "/System/Library/Colorsync/Profiles/sRGB Profile.icc" "$file" --out "$file" done

echo "----script successfully finished"

发布线上安装iOS应用(ipa)

Posted on 2018-05-21 | Edited on 2019-01-08 | In iOS

发布线上安装iOS应用(ipa)

发布 ipa 文件到线上,通过itms-services 协议访问安装。

一、适用对象

1. 企业证书,直接安装

所有人都可以下载安装,用户打开前,需要信任手动信任企业证书。

2. 个人/公司证书,给添加过 UDID 的设备安装

适用于部分设备安装(添加了 UDID 的设备)

二、操作步骤

本人采用Xcode 生成安装文件,步骤如下:

1. 打开Xcode,的 Organizer,选择需要发布的应用

2. 点击 Export,选择 Ad Hoc,点击 Next

3. 勾选如图选项:点击 Next

4. 填写相关项,需要 https 协议的链接,可用 github 仓库。

5. 根据情况执行后续操作,得到manifest.plist文件。上传到服务器,可选择 git。

6. 生成下载地址:如:

1
itms-services://?action=download-manifest&url=https://raw.githubusercontent.com/xxx/dis_ipa/master/manifest.plist

7. 手机上用 Safari 打开以上链接,进行安装。

iOS中的循环引用

Posted on 2018-04-20 | Edited on 2019-01-08 | In iOS

iOS中的循环引用

1. 概述

iOS内存中的分区有:堆、栈、静态区。其中,栈和静态区是操作系统自己管理回收,不会造成循环引用。在堆中的相互引用无法回收,有可能造成循环引用。

循环引用的实质:多个对象相互之间有强引用,不能施放让系统回收。

解决循环引用一般是将 strong 引用改为 weak 引用。

2. 循环引用场景分析及解决方法

1)父类与子类

如:在使用UITableView 的时候,将 UITableView 给 Cell 使用,cell 中的 strong 引用会造成循环引用。

1
2
3
4
5
6
7
8
9
10
11
// controller
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
TestTableViewCell *cell =[tableView dequeueReusableCellWithIdentifier:@"UITableViewCellId" forIndexPath:indexPath];
cell.tableView = tableView;
return cell;
}

// cell
@interface TestTableViewCell : UITableViewCell
@property (nonatomic, strong) UITableView *tableView; // strong 造成循环引用
@end

解决:strong 改为 weak

1
2
3
4
// cell
@interface TestTableViewCell : UITableViewCell
@property (nonatomic, weak) UITableView *tableView; // strong 改为 weak
@end

2)block

block在copy时都会对block内部用到的对象进行强引用的。

1
2
3
self.testObject.testCircleBlock = ^{
[self doSomething];
};

self将block作为自己的属性变量,而在block的方法体里面又引用了 self 本身,此时就很简单的形成了一个循环引用。

应该将 self 改为弱引用

1
2
3
4
5
__weak typeof(self) weakSelf = self;
self.testObject.testCircleBlock = ^{
__strong typeof (weakSelf) strongSelf = weakSelf;
[strongSelf doSomething];
};

在 ARC 中,在被拷贝的 block 中无论是直接引用 self 还是通过引用 self 的成员变量间接引用 self,该 block 都会 retain self。

  • 快速定义宏
1
2
3
4
5
// weak obj
/#define WEAK_OBJ(type) __weak typeof(type) weak##type = type;

// strong obj
/#define STRONG_OBJ(type) __strong typeof(type) str##type = weak##type;

3)Delegate

delegate 属性的声明如下:

1
@property (nonatomic, weak) id <TestDelegate> delegate;

如果将 weak 改为 strong,则会造成循环引用

1
2
3
4
5
6
7
8
9
// self -> AViewController
BViewController *bVc = [BViewController new];
bVc = self;
[self.navigationController pushViewController: bVc animated:YES];

// 假如是 strong 的情况
// bVc.delegate ===> AViewController (也就是 A 的引用计数 + 1)
// AViewController 本身又是引用了 <BViewControllerDelegate> ===> delegate 引用计数 + 1
// 导致: AViewController <======> Delegate ,也就循环引用啦

4)NSTimer

NSTimer 的 target 对传入的参数都是强引用(即使是 weak 对象)

解决办法: 《Effective Objective-C 》中的52条方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#import <Foundation/Foundation.h>

@interface NSTimer (YPQBlocksSupport)

+ (NSTimer *)ypq_scheduledTimeWithTimeInterval:(NSTimeInterval)interval
block:(void(^)())block
repeats:(BOOL)repeats;

@end


#import "NSTimer+YPQBlocksSupport.h"

@implementation NSTimer (YPQBlocksSupport)


+ (NSTimer *)ypq_scheduledTimeWithTimeInterval:(NSTimeInterval)interval
block:(void(^)())block
repeats:(BOOL)repeats
{
return [self scheduledTimerWithTimeInterval:interval
target:self
selector:@selector(ypq_blockInvoke:) userInfo:[block copy]
repeats:repeats];
}

- (void)ypq_blockInvoke:(NSTimer *)timer
{
void (^block)() = timer.userInfo;
if(block)
{
block();
}
}

@end

使用方式:

1
2
3
4
5
6
7
__weak ViewController * weakSelf = self;
[NSTimer ypq_scheduledTimeWithTimeInterval:4.0f
block:^{
ViewController * strongSelf = weakSelf;
[strongSelf afterThreeSecondBeginAction];
}
repeats:YES];

计时器保留其目标对象,反复执行任务导致的循环,确实要注意,另外在dealloc的时候,不要忘了调用计时器中的 invalidate方法。

123

Yun

25 posts
7 categories
20 tags
© 2019 Yun
Powered by Hexo v3.7.1
|
Theme – NexT.Gemini v6.4.1