JY's Den

想了好久


  • 首页

  • 标签

  • 分类

  • 归档

map、fitter、reduce、flatMap

发表于 2017-04-08 |

在 Swift 高阶使用中,map、fitter、reduce 是很常见的操作,能使代码干净整洁。在理解其概念之前,先要明白泛型的概念。

泛型

定义 computeIntArray 函数,接收一个数组,并返回其元素的2倍组成的另一个新的数组。但此函数只支持 Int 类型。如果换成其他类型则不适用,所以这里考虑用泛型来解决。

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
func computeIntArray(xs: [Int], transform: (Int) -> Int) -> [Int] {
var result: [Int] = []
for x in xs {
result.append(transform(x))
}
return result
}

func doubleArray(xs: [Int]) -> [Int] {
return computeIntArray(xs: xs) { x in x * 2 }
}

doubleArray(xs: [1, 2, 3])


// 换成 bool,则会出现编译错误
func isEvenArray(xs: [Int]) -> [Bool] {
return computeIntArray(xs: xs) { x in x % 2 == 0 }
}

// 使用泛型支持各种类型,进行抽象,对于任何 Element 的数组和 transform: Element -> T 函数,它都会生成一个 T 类型数组
func map<Element, T>(xs: [Element], transform: (Element) -> T) -> [T] {
var result: [T] = []
for x in xs {
result.append(transform(x))
}
return result
}

func genericComputeArray<T>(xs: [Int], transform: (Int) -> T) -> [T] {
return map(xs: xs, transform: transform)
}

genericComputeArray(xs: [2, 3, 4]) { x in x * 2 }

map

map函数能够被数组调用,还接收一个闭包参数,将数组中的每一个元素依次作用于该闭包,并返回一个新的数组。

上面的例子中,将泛型函数写成全局函数固然能够完成任务,但是为了避免写入顶层函数实现。将此函数定义为 Array 的扩展更合适。

1
2
3
4
5
6
7
8
9
10
11
extension Array {
func map<T>(tranform: (Element) -> T) -> [T] {
var result: [T] = []
for x in self {
result.append(tranform(x))
}
return result
}
}

[2, 3, 4].map { x in x * x }

fitter

与 map 函数类似,fitter 函数也可以接收一个闭包作为参数,同样可以避免函数为顶层实现:

1
2
3
4
5
6
7
8
9
10
11
12
extension Array {
func filter(includeElement: (Element) -> Bool) -> [Element] {
var result: [Element] = []
for x in self where includeElement(x) {
result.append(x)
}
return result
}
}

// ["HelloWorld.swift"]
["HelloWorld.swift", "HelloWorld.md"].filter {x in x.hasSuffix("swift")}

reduce

reduce 函数将变量初始化为某个值,对数组中的每一项进行遍历,最后一某种方式更新结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
extension Array {
func reduce<T>(initial: T, combine: (T, Element) -> T) -> T {
var result = initial
for x in self {
result = combine(result, x)
}
return result
}
}

// 输出:1
[1, 2, 3].reduce(initial: 0) { result, x in result + x }

// 也可以用运算符作为最后一个参数。这里初始化值为:1
// 输出:7
[1, 2, 3].reduce(initial: 1, combine: +)

flatMap

flatMap 与 map 类似,区别是若元素值不为 nil 的情况下,flatMap 能将可选类型转换为非可选类型:

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
let array = ["123", "", "4567"]
let anotherArray = array.map { (string: String) -> Int? in
let length = string.characters.count
guard length > 0 else {
return nil
}

return string.characters.count
}

// [Optional(3), nil, Optional(4)]
print(anotherArray)


let anotherArray2 = array.flatMap{ (string: String) -> Int? in
let length = string.characters.count
guard length > 0 else {
return nil
}

return string.characters.count
}

// [3, 4]
print(anotherArray2)

Any 与 泛型

尽量避免使用 Any 类型,因为使用 Any 类型会避开 swift 的类型系统。比如将 noOp 函数返回值设为 0 会导致类型错误。此外,任何调用 noOpAny 的函数都不知道返回值会被转换为何种类型。而结果就是可能导致各种各样的运行时错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func noOp<T>(x: T) -> T {
return x
}

func noOpAny(x: Any) -> Any {
return x
}

let result = noOp(x: 3)
let ret = noOpAny(x: "3")

// oOp中,我们可以清楚返回值和输入值完全一样,而 noOpAny 则不一样,返回是任意类型。甚至和原来输入值不同的类型。
func noOpWrong(x: Any) -> Any {
return 0
}

let s = noOpWrong(x: "string")

读书笔记

发表于 2017-03-21 |

这篇文章合集是整理了一些读书笔记,做了一些目录索引。


《Effective Objective-C 2.0》

Effective Objective-C 2.0 第一章

Effective Objective-C 2.0 第二章

Effective Objective-C 2.0 第三章

Effective Objective-C 2.0 第四章

Effective Objective-C 2.0 第五章

Effective Objective-C 2.0 第六章

Effective Objective-C 2.0 第七章

Effective-Objective-C读书笔记(7)

发表于 2017-03-21 |

系统框架

第47条:熟悉系统框架

Fundation、CoreFundation、CFNetwork(封装 BSD socket,抽象成易于使用的网络接口)、CoreAudio、AVFoundation、CoreData、CoreText 等。

第48条:多用 block 枚举,少用 for 循环

遍历 collection 可以使用 for 循环、NSEnumerator、快速遍历法。block 枚举本身通过 GCD 来并发这行遍历操作,无需另外编写代码。而其它遍历方式无法轻易实现这一点。如果已知待遍历的 collection 含有何种对象,则应该修改签名,指出对象的具体类型。

第49条:对自定义内存管理语句 collection 使用无缝桥接

使用__bridge进行桥接 Foundation 与 CoreFoundation 框架中的 C 怨言数据结构之间转换:

1
2
NSArray *anArray = @[@1, @2, @3];
CFArrayRef aCFArray = (__bridge CFArrayRef)(anArray);

转换涉及到的内存管理:

  • __bridge:告诉ARC 如何处理 Objective-C 对象,ARC 仍然具备这个对象的所有权。
  • __bridge_retained:ARC 将交出对象的所有权。
  • __bridge_transfer:反向转换,比如将 CFArrayRef 转换为 NSArray *。并令 ARC 获得对象所有权。
  • 使用 CoreFoundation 框架最后一定要调用与之对应的CFRelease()。

第50条:构建缓存时选用 NSCache 而非 NSDictionary

对比:

  • 当系统资源耗尽,可以自动删减缓存。使用NSDictionary需要自己 hook,在内存低警告的时候手动删减缓存。NSCache 是线程安全的,NSDictionary 不具备这种优势。
  • 可以给 NSCache 对象设置上限,用于限制缓存中的对象总个数。
  • 将 NSPurgeableData 对象与 NSCache 搭配使用,可以实现自动清除数据的功能。也就是说,当 NSPurgeableData 对象锁斩内存被系统所丢弃,该对象自身也可会从缓存中移除。

第51条:精简 initialize 与 load 的实现代码

对于加入runtime系统中的每个类和分类,都会调用这个方法,并且只调用一次。如果分类和类里面都定义了load方法,会先调用类里的,在调用分类里的。load方法再调用前,会加载父类的load方法。执行该load方法时,系统还处于“脆弱状态”(fragile state),根据某个指定的程序库,却无法判断出其中各个类的载入顺序。因此,在load方法中使用其他类是不安全的。比如说:

1
2
3
4
5
6
7
@interface EOCClassB
+ (void)load {
NSLog(@"Loading EOCClassB");
// 此处代码不安全,也许 EOCClassA 方法并没有加载
EOCClassA *obj = [EOCClassA new];
}
@end

该类不能在里面等待锁,也不要调用可能会加锁的方法。凡是在类加载之前执行某些任务的,基本都不太对。其真正的用途是仅仅在调试程序,比如可以在分类里编写此方法,用来判断该分类是否已经正确载入系统中。

想要执行与类初始化有关的操作,还要覆写+ (void)initialize。它与+ (void)load方法都是由 runtime 调用的,并且只调用一次。但是有一些区别:

  • initialize是惰性调用的,也就是程序用到相关的类时,才会调用。也就是说应用程序无需将每个类的initialize都执行一遍,这与load方法不同。对于load,应用程序必须阻塞并等着所有类的load都执行完,才能继续。
  • runtime 系统在执行该方法时,是处于正常状态的。因此,此时可以安全使用并调用任意类中的任意方法。而且 runtime 系统能确保initialize方法在“线程安全的环境”中执行。也就是说只有执行initialize的那个线程可以操作类或类实例。其他线程都要先阻塞,等着initialize执行完。
  • initialize与其他消息一样,如果某个类未实现它,其父类实习那了,那么就会运行父类的实现代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@interface EOCBaseClass: NSObject
@end

@implementation EOCBaseClass
+ (void)initialize {
NSLog(@"%@ initialize", self);
}
@end


@interface EOCSubClass: NSObject
@end

@implementation EOCSubClass
@end

首次使用 EOCSubClass 时,会输出:

1
2
EOCBaseClass initialize
EOCSubClass initialize

与其他方法一样(load 除外), initialize 也遵循常用的继承规则。所以在初始化 EOCSubClass 时,由于子类未覆写initialize方法,因此还要把父类的实现代码再运行一遍。鉴于此通常会这样实现 initialize 方法:

1
2
3
4
5
6
7
+ (void)initialize {
if (self == [EOCBaseClass class]) {
NSLog(@"%@ initialize", self);

// 初始化内部数据,不应该在这里调用其它方法。
}
}

总结:

  • 再加载阶段,如果类实现了 load 方法,那么系统就会调用它。类的 load 方法比分类的要先调用。与其他方法不同,load 方法不参与覆写机制。
  • 首次使用某个类,系统会向其发送 initialize 消息,此方法遵从普通对象的覆写规则,所以要在初始化方法中判断初始化的是哪个类。
  • load、initialize 方法应该实现的更精简一些,避免循环引用。
  • 常量,基本类型可以在编译期定义。无法编译期设定的全局常量(Objective-C 对象),可以放在 initialize 方法里面初始化。

第52条:NSTimer 会保留目标对象

  • NSTimer 对象会保留其目标对象,直到计数器本身失效位置,调用 invalidate 方法可令计时器失效。另外,一次性的计时器在触发完任务之后也会失效。
  • 反复执行任务的计时器,容易产生循环引用,因为 NSTimer 保留其目标实例,目标实例变量又持有 NSTimer,导致循环引用。
  • 扩充 NSTimer 的功能,通过 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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// NSTimer+EOCBlockSupport.h
@interface NSTimer (EOCBlockSupport)
+ (NSTimer *)eoc_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
block:(void (^)())block
repeats:(BOOL)repeats;
@end

// NSTimer+EOCBlockSupport.m
@implementation NSTimer (EOCBlockSupport)
+ (NSTimer *)eoc_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
block:(void (^)())block
repeats:(BOOL)repeats {
return [self scheduledTimerWithTimeInterval:interval
target:self
selector:@selector(eoc_blockInvoke:)
userInfo:[block copy]
repeats:repeats];
}

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

@end


// EOCClass.h
@interface NSTimer : NSObject
@end

// EOCClass.m
@implementation EOCClass {
NSTimer *_pollTimer;
}

- (void)startPolling {
// 通过 wealSelf 来避免实例变量 _pollTimer 持有 NSTimer,NSTimer 又持有 block,block 又持有 self 的三重循环引用。
__weak EOCClass *wealSelf = self;
_pollTimer = [NSTimer eoc_scheduledTimerWithTimeInterval:5.0
block:^{
EOCClass *strongSelf = wealSelf;
[strongSelf p_doPoll];
} repeats:YES];
}

- (void)p_doPoll {}

@end

Effective-Objective-C读书笔记(6)

发表于 2017-03-21 |

GCD

第37条:理解 Block

block 的语法结构如下:

1
2
3
4
5
6
7
8
return_type (^block_name)(parameters)

int (^addBlock)(int a, int b) = ^(int a, int b){
return a + b;
};

// 调用方式
int a = addBlock(2 + 5); // 7

如果 block 定义在 Objective-C 类的实例方法中,除了可以访问类的所有实例变量外,还可以使用 self 变量。block 总能修改实例变量,所以在声明时无需加 _block。不过如果通过读取或写入操作捕获了实例变量,那么也会自动把 self 所只带的实例关联在一起。比如:

1
2
3
4
5
6
7
8
9
- (void)anInstanceMethod {
__block int additional = 5;
void (^someBlock)() = ^{
// block 修改外部变量,需要使用 `__block` 修饰
additional = 100;
_anInstanceMethod = @"Something";
NSLog(@"_anInstanceMethod = %@", _anInstanceMethod);
};
}

如果某个类实例正在执行 anInstanceMethod 方法。那么 self 变量就指向此实例。由于 block 中没有明确使用 self 变量,所以很容易忘记 self 变量被 block 捕获。直接访问实例变量和通过 self->_anInstanceMethod = @"Something"; 来访问是等效的。一定不能忘记,self 也是对象,block 在捕获它时也会将其保留,如果 self 所指代的对象同时保留了 block,这样就会导致循环引用。

block 的内部结构:

在存放 block 内存区域中,首个变量 isa 指针指向 Class 对象的指针。其余内存里含有 block 对象正常运转所需要的信息。详细解释如下:

  • isa:指向 Class 对象的指针。
  • invoke 变量,指向 block 的实现代码。函数原型至少接受一个 void * 类型参数,此参数代表 block,void * 其实是一种代替函数指针的语法结构,可以将原来用标准 C 实现的代码封装成简明易用的接口。
  • descriptor变量,指向结构体指针。每个 block 中都包含结构体,声明了 block 对象的总体大小,copy与dispose辅助函数对应的函数指针。辅助函数在拷贝及丢弃 block 对象时运行,其中还会执行一些操作。比如前者要保留捕获的对象,后者将其释放。 block 对象会所捕获的变量拷贝一份,放在 descriptor 变量的后面,注意,拷贝的不是对象本身,而是对象的指针变量。在执行 block 时,要通过 invoke 函数把内存中捕获的变量读取出来。

在定义 block 的时候,都是分配在栈内存的,然而等离开相应的作用于之后,编译器有可能把分配的 block 的内存覆写掉,虽然可以编译,但是程序运行起来有时正确,有时错误。可以通过将 block 拷贝到堆上,这样 block 就变成带有引用计数的对象,后续复制就不会真正的复制,只是增加其医用计数。当引用计数为0,堆上的 block 对象,被系统回收。栈上的 block 原本就会被系统自动回收。示例:

1
2
3
4
5
6
7
8
9
10
void ^(block);
if (/* condition */) {
block = [^{
NSLog(@"Block A");
} copy];
} else {
block = [^{
NSLog(@"Block B");
} copy];
}

还有一种是全局 block,这种 block 不会捕捉任何状态(比如外围变量等),整个 block 所用的内存区域,在编译期间已经确定了。因此全局 block 可以声明在全局内存里,不需要在每次调用的时候于栈中创建,全局 block 的拷贝操作是个空操作,因此这种 block 绝不可能被系统回收。这种 block 相当于单例。

1
2
3
void (^block)() = ^ {
NSLog(@"This is a block");
};

由于该 block 所需要的信息在编译期就能确定,就可以把它做成全局 block。使用全局 block 可以优化程序,减少复制、丢弃该 block 时执行的不必要的操作。

总结:

  • block 可以分配在栈上或堆上,也可以是全局的,分配在栈上的 block 可拷贝至堆中,这样就具备引用计数了。全局的 block 对象可以用来优化程序调用。

第38条:为常用 block 创建 typedef

使用 typedef 来重新定义 block 结构,可以使代码简洁易懂,此时要避免其名称与别的类型相冲突。

1
2
typedef void(^EOCCompletionHandler)(NSData *data, NSError *error);
- (void)startWithCompletionHandler:(EOCCompletionHandler)completion;

第39条:使用 handler block 降低代码分散程度

使用 handler block 来作为回调函数,通常比使用委托代理更加简洁清晰。在设计 handler 的时候,建议成功和失败的 handle 分开写,理由是避免全部逻辑写到一起,造成 block 过长。

第40条:block 引用所属对象时不要出现循环引用

使用 block 一定要注意循环引用的问题,一定要在适当的时机解除循环引用,而不能把责任推给 API 调用者。有一个办法可以避免破坏代码的封装性,类似于 TWRequest 框架所做的那样,一旦调用 completion handler 之后,就没有必要继续在保留它。比如:

1
2
3
4
5
6
- (void)p_requestCompleted {
if (_completionHandler) {
_completionHandler(_downloadedData);
}
self._completionHandler = nil;
}

第41条:多用 GCD,少用同步锁

不要滥用 @synchronized(self),会降低代码效率。可以使用 NSLock、或者 NSRecursiveLock(递归锁,线程能够多次持有该锁,而不会出现死锁现象)。在使用 atomic 修饰属性的时候,setter、getter 方法是这样的:

1
2
3
4
5
6
7
8
9
10
11
- (NSString *)someString {
@synchronized(self) {
return _someString;
}
}

- (void)setSomeString:(NSString *)someString {
@synchronized(self) {
_someString = someString;
}
}

这种方式能保证线程安全,但是无法保证该对象是绝对的线程安全,比如在同一线程上多次调用 getter,在两次访问期间,其他线程也可能会写入新的值。造成获取结果未必相同。

可以使用串行同步队列将读取操作与写入操作都安排在同一队列里,即可保证数据同步:

然而还可以通过 dispatch_barrier_async 来提升同步行为的效率。在队列里面,barrier block 必须单独执行,不能与其他的 block 并发执行。因为串行队列当中的 block 总是按照顺序逐个来执行的。并发队列发现接下来的任务是barrier block,就等当前所有的并发任务处理完毕后,才单独执行 barrier block。等到barrier block执行完毕,才按照正常方式继续向下执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 在并发队列当中,读取操作是普通的 block 实现,写入则是用 `barrier block` 来实现,读取操作可以并行,但写入操作必须单步执行。
_syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
- (NSString *)someString {
__block NSString *localSomeString;
dispatch_sync(localSomeString, ^{
localSomeString = _someString;
});

return localSomeString;
}

- (void)setSomeString:(NSString *)someString {
dispatch_barrier_async(_syncQueue, ^{
_someSting = someString;
});
}

总结:将同步与异步操作结合起来,可以实现普通加锁一样的机制,这样做却不会阻塞执行派发的线程。多使用barrier block,可以令同步行为更加高效。

第42条:多用 GCD,少用 performSelector 系列方法

总结:

  • performSelector 在内存管理方面会容易有疏失,因为无法确定要执行的selector具体是什么,ARC 编译器无法插入适当的内存管理方法。
  • performSelector 系列方法处理 selector 过于局限,方法和参数个数都受到限制。
  • 最好使用 GCD 来代替 performSelector。

第43条:掌握 GCD 的使用时机

在处理多线程与任务管理的问题中,可以使用 GCD、NSOperation 技术。GCD 是基于 C 的 API,NSOperation ,NSOperation 则是 Objective-C 对象。NSOperation 提供了高层的 Objective-C API ,具备实现 GCD 的大多数功能。两者需要具体问题具体分析,每种都有各自的适用场景。比如:NSOperation可以实现取消某个操作、设置操作之间的依赖关系、通过键值观察机制检测 NSOperation 对象属性、指定操作优先级等。GCD 虽然也有优先级,不过这是针对于整个队列来说,而不是针对某个块来说。

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

一个任务可归为一个 dispatch group 只用,可以在并发队列里同时执行多项任务,此时 GCD 会根据资源情况来调度执行这些并发执行的任务。

第45条:使用 dispatch_once 来执行只需要运行一次的线程安全代码

使用 dispatch_once 编写线程安全的代码。标记应该声明在 static、global 作用域中,这样只需执行一次的 block 传给 dispatch_once 函数时,传进去的标记也是相同的。

1
2
3
4
5
6
static dispatch_once_t onceToken;
static EOCClass *sharedInstance = nil;
dispatch_once(&onceToken, ^{
// code to be executed once
sharedInstance = [[self alloc] init];
});

第46条:不要使用 dispatch_get_current_queue

dispatch_get_current_queue 函数已经废弃,只应做调试使用。由于派发队列是按照层级来组织的,所以无法单用某个队列对象来描述“当前队列”这个概念。为了解决死锁问题,可以用“队列特定数据”来替代dispatch_get_current_queue。

Effective-Objective-C读书笔记(5)

发表于 2017-03-21 |

内存管理

第29条:理解引用计数

通过引用计数来管理内存,当对象使用 new、alloc、retain,创建的时候,其 引用计数+1。当使用release的时候,引用计数-1。当其引用计数为0的时候,对象会被释放,同时需要将指针置 nil,防止出现悬垂指针。

自动释放池:当对象调用 release 会立即递减对象的引用计数,有时候可以不调用它。改为调用 autorelease,此方法会在稍后递减计数,通常会在下一次事件循环的时候递减。此方法可以保证对象在跨越方法调用边界后一定存活。使用 autorelease 可以延长对象生命周期。

1
2
3
4
5
6
7
- (NSString *)stringValue {
NSString *str = [[NSString alloc] initWithFormat:@"I am this: %@", self];
return [str autorelease];
}

// stringValue 返回调用者时,此对象必然存活。所以可以这样调用
NSString *str = [self stringValue];

由于返回的 str 对象稍后将自动释放,所以无需再执行内存管理操作。但是如果要持有该对象的话(比如要将其设置给实例变量),那就需要保留,并稍后释放:

1
_instanceVariable = [[self stringValue] retain];

第30条:用 ARC 简化引用计数

  • 在 ARC 环境中,引用计数实际上还是要执行的,只不过引用计数现在是由 ARC 自动添加。
  • 不能使用 retain、release、autorelease、dealloc 方法。
  • 若方法名以 alloc、new、copy、mutableCopy 开头,则返回的对象归调用者所有。
  • 不使用 ARC 情况下,在实现单例类的时候,因为单例不可以释放,所有我们经常个覆写 release 方法,将其替换为 空操作。
  • ARC 下会借用 Objective-C++ 中的一项特性来清理对象,回收 Objective-C++ 对象时,待回收对象会调用 C++ 对象的析构函数。编译器如果发现对象里含有 C++ 对象,就会生成 .cxx_destruct 的方法。在此方法中生成清理内存的代码。
  • ARC 只负责管理 Objective-C 对象内存,CoreFoundation 对象不归 ARC 管理,开发者必须适时调用 CFRetain/CFRelease。

第31条:在 dealloc 方法中只释放引用并解除监听

  • 在 dealloc 方法中,ARC 会通过自动生成 .cxx_destruct 方法,并自动释放所有的 Objective-C 对象。
  • 在此方法中移除通知
  • 如果不使用 ARC ,那么最后还需要调用 [super dealloc]。ARC 会自动执行此操作。
  • 如果对象持有文件描述符等资源,应该撰文写一个方法来释放此种资源。这样的类要和使用者约定:用完资源后必须调用 close 方法。
  • 执行异步任务的方法不应该在 dealloc 里调用。只能在正常状态下执行的那些方法也不应该在 dealloc 里调用,因为此时对象已处在正在回收的状态了。

第32条:编写异常安全代码时要注意内存管理问题

  • 在非 ARC 下,使用@try...@catch来捕获异常,当异常发生时,程序直接执行@catch中的语句。忽略了@try中的内存管理语句。最好的做法是将@try中的内存管理语句放在 @finally 当中。
  • 默认情况下,ARC 不生成安全处理异常的所需要的清理代码。开启编译器-fobjc-arc-exceptions标志后,可以生成这种代码,不过会导致应用程序变大。降低运行效率。

第33条:用弱引用避免循环引用

  • 将某些引用设置为 weak,避免循环引用。
  • weak 引用可以自动清空,也可以不自动清空。自动清空是随着 ARC 而引入的新特性,由运行时系统来实习那。在自动清空功能的弱引用上,可以随意读取其数据,因为这种引用不会指向已经回收过的对象。

第34条:使用 autoreleasepool 来降低内存峰值

  • 当程序当中需要创建大量的临时对象(比如循环),会造成内存增长,可以考虑将临时代码放在autoreleasepool当中,那么在循环中自动释放的对象就会放在这个池,而不是线程的主池里面。
  • autoreleasepool存放在栈中,对象收到 autorelease 消息后,系统将其放入最顶端的池里。

第35条:用僵尸对象调试内存管理问题

  • 向已经回收的对象发送消息是不安全的,因为这有时可以,有时不行。完全取决于对象所占内存有没有为其他内容所覆写,而这块内存有没有移作他用,又无法确定。此时需要开启僵尸对象进行检查。
  • 系统在回收对象的时候,可以不将其真的回收,而是把它转化为僵尸对象。通过 NSZombieEnabled 可开启此功能。

第36条:不要使用 retainCount

  • 不要使用 retainCount 来查看对象是否还被持有,因为任何给定时间点上 绝对引用计数 都无法反应对象生命周期的全貌。引用计数可能处在自动释放池中,retainCount 可能不准确。
  • ARC 后,retainCount 就被废止了。

Effective-Objective-C读书笔记(4)

发表于 2017-03-21 |

协议与分类

第23条:通过委托与数据源协议进行对象间通信

  • 如果有必要,可以实现含有位段的结构体,将委托对象能否响应相关协议方法这一信息缓存其中。

第24条:使用分类来分散代码

  • 使用分类将类的实现代码划分为易于管理的小块
  • 将视为“私有”的方法归入叫 Private 的分类中,隐藏实现细节。

第25条:为第三方分类添加前缀

  • 向第三方类中添加分类时,应该给其名称、方法名添加专用的前缀。

第26条:不要在分类中添加属性

  • 尽量不要在分类中添加属性,因为属性是为了封装数据的。而分类目的在于扩展类的功能,而非封装数据。
  • 虽然这样定义,但是也可以在分类当中创建只读属性。
1
2
3
@interface NSCalendar (EOCCalandar)
@property (nonatomic, strong, readonly) NSArray *eoc_allMonths;
@end

第27条:使用匿名分类(或者说称为扩展)来隐藏实现细节

  • 通过匿名分类,把私有的方法、属性声明在匿名分类中。
  • 若某属性在主接口中声明为 “readonly”,类内部又要用设置方法修改此属性,可以在匿名分类中将其扩展为 “readwrite”。

第28条:通过协议提供匿名对象

  • 提供协议来隐藏具体类内部的实现。
  • 具体的对象类型可以淡化为遵从某协议的 id 类型,协议里规定了对象所应该实现的方法,可以使用匿名对象来表示。

Effective-Objective-C读书笔记(3)

发表于 2017-03-21 |

接口与 API 设计

第15条:用前缀避免命名空间冲突

选择你的公司、应用程序或二者皆有关联的名称作为类的前缀,并在所有的代码中均使用这一前缀。

程序中使用到第三方库,则应为其中的名称加上前缀。如果命名出现冲突,会出现以下错误:

1
2
3
duplicate symbol _OBJC_METACLASS_$_***Class in:
...
...

第16条:提供“指定初始化方法”

在类当中提供一个指定初始化方法,并与文档里面说明,其他初始化方法均应该调用此方法。

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
// JYRectangle.h
@interface JYRectangle : NSObject<NSCoding>
@property (nonatomic, assign, readonly) float width;
@property (nonatomic, assign, readonly) float height;

- (id)initWithWidth:(float)width
andHeight:(float)height;
- (void)methodMustBeOverride;
@end

// JYRectangle.m
@implementation JYRectangle
- (id)initWithWidth:(float)width andHeight:(float)height {
if (self = [super init]) {
_width = width;
_height = height;
}
return self;
}

- (instancetype)init {
return [self initWithWidth:5.0 andHeight:5.0];
}

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
if (self = [super init]) {
_width = [aDecoder decodeFloatForKey:@"width"];
_height = [aDecoder decodeFloatForKey:@"height"];
}
return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeFloat:_width forKey:@"width"];
[aCoder encodeFloat:_height forKey:@"height"];
}

- (void)methodMustBeOverride {
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:@"This method must be override !"
userInfo:nil];
}

@end

如果子类想要提供不同的函数实现,需要覆写父类的方法。父类不提供实现的时候,在其中抛出异常,强制子类覆写此方法。

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

// JYSquare.h
@interface JYSquare : JYRectangle
- (id)initWithDimension:(float)dimension;
@end


// JYSquare.m
@implementation JYSquare
- (id)initWithDimension:(float)dimension {
return [super initWithWidth:dimension andHeight:dimension];
}

- (id)initWithWidth:(float)width andHeight:(float)height {
float dimension = MAX(width, height);
return [self initWithDimension:dimension];
}

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
if ([super initWithCoder:aDecoder]) {
// JYSquare's specific initializer
}
return self;
}

- (void)methodMustBeOverride {
// If subclass is not override this method, will throw exception
}
@end

第17条:实现 description 方法

实现 description 方法返回一个有意义的字符串,用来描述实例,也可以通过 debugDescription 方法返回更详细的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
- (NSString *)description {
return [NSString stringWithFormat:@"<%@: %p, %@>",
[self class],
self,
@{@"title", _title,
@"name", _name,}
];
}

// 只有在控制台使用 LLDB 的 po 命令有效
- (NSString *)debugDescription {
return [NSString stringWithFormat:@"<%@: %p, %@>", [self class], self, _title];
}

第18条:尽量使用不可变对象

  • 将对外公布的属性设置为 readonly,在匿名分类中将属性重新定义为readwrite。即使属性被声明为readonly,然而在对象外部,还可以通过 KVC 技术来设置属性的值。比如[someProperty setValue:@"abc" forKey:@"someValue"];,KVC 会在类中查找 setSomeProperty: 方法,我们最好不要通过这种 hack 方式来设置属性,不然会造成莫名的问题。
  • 尽量创建不可变对象。
  • 不要将 collection 作为属性公开,而是应该提供相关方法,以此修改对象中的不可变对象。

第19条:清晰的命名

  • 继承自类,命名应该与类保持一致,比如从 UIView 中继承的子类,其末尾的词必须是个 View。
  • 方法和变量名都使用驼峰式命名。
  • 保持与自己代码或集成框架代码相符。

第20条:为私有方法添加前缀

  • 私有方法建议使用 p_privateMethodName 开头,与公共方法区分开。
  • OC 中没有办法标记私有方法,所有根据这一条命名私有方法,增强代码可读性。
  • 不要单用下划线作为私有方法的前缀,这种是预留给苹果公司使用的。

第21条:Objective-C 中的错误类型

  • 异常只有程序发生严重错误时候才能抛出,比如定义一个抽象类,子类必须覆写父类的方法,可以在父类的方法里面抛出异常。这样子类如果没有覆写父类方法,就会抛出异常。
  • 其他非致命的错误,Objective-C 中所用的编程范式为:令方法返回 nil/0,或者使用 NSError。
  • 收到 NSError 消息时,必须优先处理错误。

第21条:NSCopying 协议

自己的类想要支持 copy 操作,必须要实现 NSCopying 协议。该协议有以下方法:

1
2
// NSZone:以前开发程序会把内存分成不同的区,对象存在某个区里面。现在每个程序只有一个区:默认区,不用担心 zone 参数
- (id)copyWithZone:(nullable NSZone *)zone;
  • 复制对象时决定采用深拷贝韩式浅拷贝,一般情况下应该尽量执行浅拷贝。
  • 自定义对象分为可变和不可变版本,需要同时实现 NSCopying 与 NSMutableCopying。

Effective-Objective-C读书笔记(2)

发表于 2017-03-21 |

对象、消息、runtime

Objective-C 语言中,“对象”是“基本的构造单元”,在对象之间传递数据并执行任务,这个过程叫做消息传递。为其提供相关支持的代码叫做Objective-C runtime,它提供了一些使得对象间能够传递消息的重要函数。

第6条:理解属性这一概念

属性是 Objective-C 中用于封装对象的数据。在 Objective-C 语言中,很少像 C++、Java 那样在接口内部声明实例变量,例如:

1
2
3
4
5
6
7
@interface EOCPerson : NSObject
@public
NSString *_firstName;
NSString *_lastName;
@private
NSString *_someInternalData;
@end

不像 C++、Java 那样,在这里可以定义实例变量的作用域。在 Objective-C 中,这种写法的问题是:对象布局在编译期间就已经固定了。只要访问_firstName,编译器就把其替换为偏移量(这个偏移量是硬编码,表示该变量距离存放对象的内存区域的起始地址有多远)。如果在 _firstName 前面又多添加一个实例变量 NSDate *_dateOfBirth;,这样 _firstName 偏移量就会改变,指向 _dateOfBirth,偏移量硬编码于其中就会读到错误的值。此时内存的布局如图所示:

如果代码使用了编译期计算出来的偏移量,那么在修改类定义之后必须重新编译,否则就会出错。然而这种将实例变量声明在@interface接口中暴露出类的接口,更好的方式是通过@property 语法来实现。可以像以下代码来声明属性:

1
2
3
4
@interface EOCPerson : NSObject
@property NSString *firstName;
@property NSString *lastName;
@end

使用@property属性,编译器会在编译期自动做以下几件事情:

  • 自动合成这些属性的 getter、setter 方法,开发者并不可见这些合成方法的源代码。
  • 向类中添加适当类型的实例变量,并在属性名前加_,作为实例变量的名字。

如果你不喜欢以_开头的实例变量名,可以通过@synthesize语法来指定实例变量的名字:

1
2
3
4
@interface EOCPerson
@synthesize firstName = _myFirstName;
@synthesize lastName = _myLastName;
@end

上面会将实例变量命名为_myFirstName和_myLastName。

若不想令编译器自动合成存取方法,也可以自己实现。通过使用@dynamic关键字,告诉编译器:不要自动创建属性所用的实例变量,也不要为其创建存取方法。而且,在编译期间访问属性代码时,即使编译器没有定义存取方法,也不会报错,它相信这些方法能在运行期找到。比如 Core Data 中 NSManagedObject的子类。

属性特质
  • atomic:修饰属性会给属性的设置方法加同步锁,iOS 同步锁开销大,会影响性能。若自定义方法,应该遵守与属性特质相符的原子性。然而这并不能保证其线程安全,需要更深层的锁机制才行。
  • nonatomic:不使用同步锁,
  • readwrite:编译器生成对应的getter、setter方法。
  • readonly:编译器只生成getter方法,可以在class continuation中将其定义为 readwrite 属性,保持属性在外部是 readonly 的。
  • assign:只针对基本“纯量类型”(scalar type):例如 CGFloat、NSInteger
  • strong:属性为拥有关系,设置新值时,会 retain 新值,release 旧值,然后再将新值设置设置上去。
  • weak:属性为非拥有关系,既不 retain 新值,也不 release 旧值,对象销毁的时候,属性值会清空(置 nil)
  • unsage_unretained:与 weak 相似,但是对象销毁的时候,修饰的属性并不自动清空,所以是不安全的。
  • copy:与 strong 类似,设置方法是将其 copy。用此方法保持属性的封装性。
  • getter=name:指定 getter 的方法名,例如 UISwitch 中,@property (nonatomic, getter=isOn) BOOL on;
  • setter=name:指定 setter 方法名,不常见。

第7条:在对象内部尽量直接访问实例变量

  • 在对象内部读取数据时,应该直接通过实例变量来读,而写入数据,则应通过属性来写。
  • 在初始化方法及 dealloc 方法中,总是应该直接通过实例变量来读写数据。
  • 在使用惰性初始化时,通过属性来获取数据

第8条:理解”对象同等性”

1
2
3
4
5
NSString *foo = @"Badge 123";
NSString *bar = [NSString stringWithFormat:@"Badge %i", 123];
BOOL equalA = (foo == bar); // NO
BOOL equalB = [foo isEqual:bar]; // YES
BOOL equalC = [foo isEqualToString:bar]; // YES

在判断对象是否相等,需要覆写 isEqual和 isEqualToString 方法:

  • 检测对象同等性,需要提供isEqual与hash方法。
  • 相同的对象必须具有相同的 hash 码,而两个相同 hash 码的对象不一定相同。

第9条:以“类簇模式”隐藏实现细节

  • 使用类簇可以把公共的接口隐藏在父类里面,比如 buttonWithType:方法,根据类型返回不同的 button 实例,其类型是隐藏在类簇的公共接口后面的某个内部类型。也可以称为门面模式。
  • 系统的常用框架经常使用类簇,比如 典型的有 UIButton,collection 类等。

第10条:在既有类中使用关联对象存放自定义数据

Objective-C 可以通过关联对象给某对象关联许多其他对象,这些对象通过key来区分,还可以指明存储策略,用来维护相应的内存管理语句。其由objc_AssociationPolicy的枚举所定义。可以把关联对象理解为一个 NSDictionary,拥有对应的存取值方法,与之不同的是,存取关联对象的值是个不透明的指针。

  • 可以通过关联对象将两个对象连接起来。
  • 定义关联对象可以指定其内存管理语句,用来模仿定义属性时所采用的拥有关系与非拥有关系。
  • 关联对象之间的关系并没有正式定义,其内存管理语句是在关联的时候才定义的,使用时要小心。

第11条:理解 objc_msgSend 的作用

在 C 语言中,大部分程序是静态绑定的。也就是说程序在编译期间就能得到运行时所调用的函数。然而当 C 程序中存在函数指针的时候,编译期就无法得知该函数的定义,直到运行时才能决定。这就是动态绑定。在 Objective-C 中就使用动态绑定机制来决定调用的方法。在底层,所有的方法都是普通的 C 函数实现,调用哪个函数都是由运行时来改变。这种特性使得 Objective-C 称为动态的语言。发送消息可以这样写:

1
2
3
4
// someObject:消息的接收者
// messageName:称为选择子(selector)
// selector 和参数结合起来称为`消息`
id returnValue = [someObject messageName:parameter];

编译器看到消息后,将其转换为一条标准的 C 语言函数调用,其原型如下:

1
2
3
4
5
// objc_msgSend:是可变参数的函数
// self:消息接收者
// SEL:selector(也就是选择子)
// 后续参数就是消息中的参数,其顺序不变
void objc_msgSend(id self, SEL cmd, ...)

上面的函数经过编译器转换后变成:

1
id returnValue = objc_msgSend(someObject, @selector(messageName:), paramter);

objc_msgSend会根据接收者与选择自的类型调用适当的方法。该方法会在接受者所属的类中搜索其方法列表(list of method),如果能找到与 selector 对应的方法,就跳至其实现代码。如果找不到,就沿着继承体系继续向上寻找,等找到匹配的方法后再跳转。如果还是找不到,就执行消息转发操作。

执行消息查找需要很多步骤,所幸的是 objc_msgSend 会将匹配的结果缓存到快速映射表(fast map)当中,下次查找执行就会很快。过程看起来很耗时,但是实际上,消息派发(message dispatch)并不是应用程序瓶颈所在。

上面只是将消息调用过程,当然还有一些特殊情况:

  • objc_msgSend_stret:如果带发送的消息返回结构体,可交由此函数处理。(这并不是绝对的,只有 CPU 寄存器能容纳下消息返回类型是,该函数才能处理此消息。若返回值无法容纳与 CPU 寄存器中,比如返回的结构体太大,就交个另一个函数进行派发。此时,函数通过分配在栈上的某个变量来处理消息返回的结构体。)
  • objc_msgSend_fpret:消息返回浮点数,则交个此函数处理,这是针对某些架构的 CPU 中(比如 x86)做出特殊处理,这种情况下使用 objc_msgSend 并不合适。
  • objc_msgSend_Super:给父类发送消息,如 [super message:parameter],另外有两个与:objc_msgSend_stret、objc_msgSend_fpret等效的函数,用于处理法给 super 的相应消息。

上面提到,objc_msgSend 一旦找到相应函数的实现,就会进行跳转。能这样做的原因是,Objective-C 对象每个方法都可以认为是简单的 C 函数,其原型如下:

1
<return_type> Class_selector(id self, SEL _cmd, ...)

其工作原理是:每个类中都要有一张表格,其中的指针指向这种函数,selector 作为查找表格所用的 key。这里要注意:原型与 objc_msgSend 函数很像,这是为了利用尾递归优化技术,这项优化非常关键,如果不这么做,在查找函数的过程当中就会频繁的调用堆栈,插入新的栈帧,造成栈溢出。而尾递归优化可以避免这一现象。

第12条:理解消息转发机制

在编译期间向类发送器无法解读的消息,并不会报错,因为在运行时可以继续向类中添加方法,所以编译器此时无法确定该类中到底会不会有某个方法实现。如果某个对象收到无法解读的消息,runtime 就会触发消息转发机制。我们已经遇到过消息转发流程所处理的消息了,比如这样:

1
2
-[__NSCFNumber lowercaseString]: unrecongnized selector sent to instance 0x87
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[__NSCFNumber lowercaseString]: unrecongnized selector sent to instance 0x87'

上面这段异常就是 NSObject 的 doesNotRecongnizeSelector: 方法所抛出的,表明 NSNumber 没有 lowercaseString 方法。__NSCFNumber 是为了实现桥接而使用的内部类。我们在编写程序的过程中,可以在消息转发的过程中设置钩子,用以执行预定的逻辑,不应该使程序崩溃。

消息转发分为两个阶段:

  • 1.动态方法解析(dynamic method resolution):先问消息接收者,所属的类,看其能否动态添加方法来处理当前未知的消息。
  • 2.完整消息转发(full forwarding mechanism):此时第一阶段已经完成,无法执行动态方法解析。runtime 使用其他手段来处理消息。

上面的完整消息转发又包括两步:

  • a).首先,请消息接收者查看有没有其他对象能处理这条消息,若有 runtime 则执行消息转发给该对象,一切正常。反之则执行 b)
  • b).没有备援的消息接收者,则启动完整的消息转发机制,runtime 会将消息封装到 NSInvocation 对象中,再给消息接收者最后一次机会,令其解决当前未处理的消息。

动态方法解析:

对象收到无法解读的消息,会调用其所属类的类方法:+ (BOOL)resolveInstanceMethod:(SEL)selector。该方法表示该类能否新增实例方法来处理 SEL。继续执行转发机制之前,本类有机会新增一个处理此 selector 的方法。如果未实现的方法不是实例方法而是类方法,runtime 就会调用另外一个方法 resolveClassMethod:。

备援接收者:

当前接收者有第二次机会处理未知的选择子,runtime 会询问能否将消息转给其他接收者来处理。该步骤通过以下处理方法:

1
2
// 参数代表未知的 selector,若找到备援对象,将其返回,没有找到,返回 nil
- (id)forwardingTargetForSelector:(SEL)selector

注意:我们无法操作经由这一步所转发的消息。若想在发送给备援接收者之前先修改消息内容,就必须通过消息转发机制来做。

完整的消息转发:

消息转发来到这一步,唯一能做的就是启动完整的消息转发机制。首先创建 NSInvocation 对象,把尚未处理的消息封装其中(包括 selector、target、参数),消息派发系统将消息派发给目标对象。此步骤调用下列方法来转发消息:

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

此方法会按照继承体系来寻找,继承体系中每个类都有机会处理此调用请求,直到 NSObject。如果最后调用了 NSObject 的方法,该方法还会继而调用 doesNotRecongnizeSelector: 以抛出异常,表明此 selector 未被处理。

整个流程图如下:

第13条:method swizzling

类的方法列表会把 selector 的名称映射到相关方法的实现上,这样动态消息派发系统就能根据此找到应该调用的方法。该方法用 IMP 指针来表示,其原型:

1
id (*IMP)(id, SEL, ...)

比如 NSString 类的部分方法表:

也可以把方法表中的 IMP 进行交换:

有以下方法的 API:

1
2
3
4
5
// 交换两个方法的实现
void method_exchangeImplementations(Method m1, Method m2)

// 获取待交换两个参数的方法
Method class_getInstanceMethod(Class aClass, SEL aSelector)

举个例子,当调用 lowercaseString 的时候,输出日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@interface NSString (EOCMyAdditions)
- (NSString *)eoc_myLowercaseString;
@end


@implementation NSString (EOCMyAdditions)
- (NSString *)eoc_myLowercaseString {
// 这段代码看似会陷入递归调用的死循环,不要忘记这个方法已经与 lowercaseString 互换了,其实是调用 lowercaseString: 方法的实现
NSString *lowercase = [self eoc_myLowercaseString];
NSLog(@"%@ => %@", self, lowercase);
return lowercase;
}
@end

// 调用方式
Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));
Method swappedMethod = class_getInstanceMethod([NSString class], @selector(eoc_myLowercaseString));
method_exchangeImplementations(originalMethod, swappedMethod);

NSString *string = @"This iS a stRiNg";
NSString *lowercaseString = [string lowercaseString]; // This iS a stRiNg => this is a string

交换后其方法表如下:

第13条:理解“类对象”

Objective-C 中有个特殊的类型叫做 id,它只带任意的 Objective-C 对象类型。一般情况下,应该指名下次接收者的具体类型,这样向其发送无法解读的消息,那么编译器就会产生警告。而 id 类型则不然,编译器假定它能相应所有消息。比如:

1
2
3
// 指定具体的类型的好处是:该类实例上调用其所没有的方法时,编译器会得知此情况并发出警告。
NSString *pointerVariable = @"Some string";
id genericTypedString = @"Some string";

id类型被定义在运行期程序库的头文件里:

1
2
3
4
5
6
7
8
9
10
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

/// Represents an instance of a class.
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};

/// A pointer to an instance of a class.
typedef struct objc_object *id;

由此可见,每个对象的结构体的首个成员是 Class 类的变量,该变量定义了结构体所属的类,称为 is a 指针,如上面的例子中对象“是一个” (is a)NSString 指针,所以其 is a 指针就指向 NSString。Class 对象在 runtime.h 中可以找到:

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

此结构体存放类的元数据(metadata),其结构分析如下:

  • isa:指向 Class 所属的类型,也就是 metaclass,用来表示类对象本身具备的元数据。“类方法”就定义在这里,因为这些方法可以理解成类对象的实例方法。每个类仅有一个类对象,而每个“类对象”仅有一个与之相关的“元类”。没错,类可以理解为 metaclass 的实例。
  • super_class:指向 Class 的父类。
  • name:类名
  • version:版本号
  • info:存放额外的信息
  • instance_size:Class 实例的大小。
  • methodLists:上面提到的方法表。
  • cache:方法缓存
  • protocols:协议表

假如,有个名为 SomeClass 的子类从 NSObject 继承而来,继承结构如下图:

Effective Objective-C读书笔记(1)

发表于 2017-03-21 |

熟悉 Objective-C

Objective-C 中的消息

Objective-C 起源于 Smalltalk,所以借鉴了 Smalltalk 的消息结构(messaging structure),而非像 C++ 那样的函数调用(function calling)。
主要区别:使用消息结构的语言,其运行时所应执行的代码由运行环境来决定。而使用函数调用的语言,由编译器决定。如果示例代码是多态的,那么在运行时就会按照虚方法表(virtual table)来查看到底该执行哪个函数实现。而采用消息结构的语言,不论是否是多态,总是在运行时才会查找要执行的方法。编译器不关心接收消息的对象是哪种类型。接收消息的对象问题也要在运行时处理。这个过程叫做动态绑定。

Objective-C 的工作都是由 运行期组件(runtime component)而非编译器来完成,运行期组件本质就是一种与开发者所编写代码相连接的动态库,这样只需更新运行期组件,就可以提升应用程序性能。而那种许多工作都在编译期完成的语言,想要获得类似的性能提升,则要重新编译应用程序代码。Objective-C 是 C 的“超集”,必须要理解 C 语言的内存模型。举个例子:

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

对象是存储在堆内存当中,anotherString 指向了 string 变量,它们两个共享一块存储区域,并不会拷贝对象。其内存空间分配是这样的:

需要注意的是:分配在堆中的内存必须直接管理,而分配在栈上用于保存变量的内存则会在栈上弹出时自动清理。Objective-C 将内存管理抽象出来了,不用像 C 那样使用 malloc、free 来回收内存,Objective-C 在运行时把这部分工作抽象为一套内存管理的框架,叫做引用计数。

在 Objective-C 程序当中,遇到不含*的变量,它们可能使用栈空间。比如CGRect等等。

类的头文件中尽量少引入其他头文件

更优雅的做法是使用 @class **** 来引入某个类,这叫做向前声明(forward declaring)该类。在.m文件中import该类。这样就隐藏了该类的所有接口实现。将引入头文件的时机尽量延后,只有在需要时才引入,这样就会减少类的使用者所需引入的头文件数量。减少编译时间。还可以避免两个互相import导致循环引用的问题。

如果某些类需要遵循某个协议,那么该协议必须要有完整的定义。且不能使用向前声明,这时候可以将该协议单独放在一个头文件当中。特例是委托协议就不用单独写一个头文件,协议只有与接受委托的类放在一起才有意义。这种情况下,最好能在实现文件中声明这个类实现了该委托协议,并把这段实现代码放在分类当中。这样只要在实现文件中引入包含委托协议的头文件即可,而不需将其放在公共头文件里。好处是降低依赖程度,缩短编译时间,代码清晰容易维护。

总结一下:优先使用向前声明来进行解耦,如果无法使用向前声明,尽量把该类遵循某个协议移至该类的分类当中。实在不行的话,就把协议单独放在一个头文件,然后将其引入。

多使用字面量,少用与之等价的方法

在使用字面量的时候,多使用字面数值,代码更加整洁:

1
2
3
4
5
6
7
8
9
10
// 不使用字面量
NSNumber *someNumber = [NSNumber numberWithInt:1];

// 使用字面量
NSNumber *intNumber = @1;
NSNumber *boolNumber = @YES;

int x = 5;
int y = 6.32f;
NSNumber *number = @(x * y);

字面量数组:使用字面量数组更加直观,易于理解,并能尽可能早发现程序设计中的问题。

1
2
3
4
5
6
id obj1 = /* ... */;
id obj2 = /* ... */;
id obj3 = /* ... */;

NSArray *arrayA = [NSArray arrayWithObjects:obj1, obj2, obj3, nil];
NSArray *arrayB = @[obj1, obj2, obj3];

如果 obj2 为 nil,字面量语法创建的数组 arrayB 会崩溃,而 arrayA 虽然能创建出对象,但是只包含 obj1,因为arrayWithObjects:会依次处理各个参数,直到发现nil为止。使用字面量语法更加安全,更快的发现错误。

字典字面量:与上面的数组类似,字典在遇到值为nil时抛出异常

1
2
3
4
5
6
7
8
NSDictionary *dict1 = [NSDictionary dictionaryWithObjectsAndKeys:@"value1", @"key1", @"value2", @"key2", nil];
NSDictionary *dict2 = @{
@"key1" : @"value1",
@"key2" : @"value2",
};

[dict1 objectForKey:@"value1"];
NSString *key1 = dict2[@"value1"];

注意上面的dictionaryWithObjectsAndKeys:的参数是 <Object> : <key> 的形式。很显然使用字面量更加简洁。

字面量语法也有局限性:就是除了字符串意外,所创建出来的对象必须属于 Foundation 框架才行。如果自定义了这些类的子类,则无法使用字面量语法创建对象。当然很少有人这么做,因为 NSArray、NSDictionary 都是已定型的 “子族”,无需再改动。

使用字面量创建的对象都是不可变的,虽然多创建一个对象,但好处还是多于缺点的:

1
NSMutableArray *mutable = [@[@1, @2] mutableCopy];

多使用类型常量,少用 #define 预处理指令

不要使用#define预处理指定定义常量,这样定义出来的类型不含类型信息。编译器只是会在编译器根据此执行查找与替换操作。即使有人重新定义了常量值,编译器也不警告。可以利用编译器特性来定义。比如:

1
2
3
@implementation ViewController
static const NSTimeInterval kAnimationDuration = 0.3;
@end

在实现文件中使用static const来定义”只在编译单元内可见的常量”(每个.m为一个编译单元),此类常量不在全局符号表当中,所以无需加前缀。

在头文件中使用extern来声明全局变量,这种常量出现在全局符号表当中。所以其名称应该加以区分,通常用类名作为前缀:

1
2
3
extern NSString *const EOCLoginManagerDidLoginNotification;

NSString *const EOCLoginManagerDidLoginNotification = @"EOCLoginManagerDidLoginNotification";

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

使用 enum 的时候需要注意,应该用枚举来表示状态机的状态、传递给方法的选项以及状态码等值,给这个值起个通俗易懂的名字。

如果把传递给某个方法的选项表示为枚举类型,而多个选项可同时使用,那么将各个选项定义为2的幂,以便通过按位或操作将其组合起来。
在使用 NS_ENUM 和 NS_OPTHONS 宏来定义枚举类型,要指名其数据类型,这样做可以确保枚举是用开发者所选的数据类型实现出来的,而不会采用编译器所选的类型。NS_ENUM 、NS_OPTHONS 其实内部做了这样一件事情:

其实就是判断编译器是否支持新式枚举,编译器按 C++ 模式编译,那么enum定义枚举的时候,其展开方式与NS_ENUM相同。枚举值使用按位或运算来组合的时候,C++ 认为运算结果的数据类型应该是枚举的底层类型,也就是NSUInteger,C++ 不允许将这个底层类型进行隐式转换,其展开方式为NS_OPTHONS。鉴于此,凡是需要按位或操作来组合的枚举都应该使用NS_OPTHONS来定义。若是枚举不需要互相组合,则应使用NS_ENUM来定义。通常我们会这样使用:

在处理switch语句中,不要实现default 分支。在枚举值改变后,编译器就会提醒开发者:switch语句并未处理所有枚举。

视频H.264压缩格式

发表于 2017-03-17 |

摄像头采集到的原始数据,需要将图片进行压缩处理,而 H.264恰好保证了良好的压缩

压缩算法只压缩图像改变的部分(需要找到上一帧和下一帧的区别)。

软编码:ffmpeg编码,进行 h.264编码后进行传输。
h.264分为 I、P、B帧

RTMP\RTSP当中依然包含 h.264 数据

摄像头采集到的是 RGBA 数据,需要将数据进行编码为 H.264 的流(encode,这里面可以通过硬件编码或者软件编码,就变得很小,适合进行网络传输)

收到网络传输的 h.264 数据,需要还原成一帧一帧的图片,这个过程叫做 decode(硬件解码(VideoToolBox)、软件解码(FFMpeg))。

将解码后的 h.264 数据(解码之后就成为 YUV 数据),交给硬件去播放。

音频:采集(PCM,无损压缩格式) -> 编码(压缩算法:g711(打电话),adpcm,aac(进行网络传输)) -> 解码 -> 将解码 pcm 交给硬件进行播放
使用 AudioQueue、openAL 进行播放视频

OpenCL:进行并行计算、深度学习等
OpenGL:3D、2D 游戏

码率:
采样率:设备在1秒钟对音频或者视频的采样次数。

H264编码原理:I/B/P

I 帧:帧内编码帧,I 帧表示关键帧,可以理解为这一帧画面的完整保留
解码时使用本帧数据就可以完成。解码时 I 帧就可以完全还原图像
P 帧:是 I 帧的参考帧,是 I 帧后面间隔1~2秒后的参考帧,也是 B 帧的参考帧
B 帧:双向差别帧,需要前面的 I 帧和后面的 P 帧进行压缩

H264 NAL 头文件解析(重要)

PCM -> aac 编码规则

在实际的音频编码中,假设网络每次接收到的音频数据为1280,
而在编码中我们设定输入的 PCM 数据为1024,实际的输出为768
那么如果输入的 PCM 数据小于实际接收到的音频数据,那么下次的数据
就会从下一帧中开始截取对应长度的音频,凑够1024长度的数据量。来进行编码
参考图片:PCM编码为 aac 规则

123…6

JY

52 日志
15 标签
GitHub
© 2019 JY
由 Hexo 强力驱动
|
主题 — NexT.Mist v5.1.4