高性能iOS应用开发

最近读《高性能iOS应用开发》一书,重新了解了下iOS中内存模型与一些开发中注意事项,并摘抄书中知识点做了下笔记,另外原来不了解方面知识也做了相关资料搜索作笔记补充。(如有错误欢迎指正)

一. 开始

1. 移动应用的性能

1.1 定义性能

性能是非常模糊的术语,高性能有着多重的含义和丰富的解释方式。(需要测量和监控的)性能指标是其中的 一个关注点,(实际上收集数据的)测量是另一个关注点。

1.2 性能指标

性能指标是面向用户的各种属性。每个属性可能是一个或多个可测量工程参数的一个要素。

1.2.1 内存

内存涉及运行应用所需的 RAM 最小值,以及应用消耗的内存平均值和峰值。最小内存值 会严重限制硬件,而更高的内存平均值和峰值意味着更多的后台应用会被强制关闭。
同时还要确保没有泄漏内存。随时间流逝而持续增长的内存消耗意味着,应用很可能会因 为内存不足的异常而崩溃。

1.2.2 电量消耗

电量消耗不仅仅与计算 CPU 周期有关,还包括高效地使用硬件。除了要实现电量消耗最小化,还要确保不会影响用户体验。

1.2.3 初始化时间

应用在启动时应执行刚好够用的任务以完成初始化,从而满足用户的使用需求。执行这些 任务消耗的时间就是应用的初始化时间。 刚好够用是一个开放式用语——正确的平衡点取 决于应用的需要。

1.2.4 执行速度

一旦启动应用,用户总是希望它可以尽可能快地工作。一切必要的处理都应该在尽可能短 的时间内完成。

1.2.5 响应速度

每个应用都应该快速地响应用户交互。在应用中所做的一切优化和权衡最终都应该体现在 响应速度上。

1.2.6 本地存储

针对任何在服务器上存储数据或通过外部来源刷新数据的应用,开发人员应该对本地存储 的使用有所规划,以便应用具备离线浏览的能力。如果你的应用使用了本地存储,那么请提供一个清除数据的选项。

1.2.7 互操作性

用户可能会使用多个应用来完成某个任务,这就需要这些应用直接提供互操作的能力。

1.2.8 网络环境

移动设备会在不同网络环境下使用。为了确保能够提供最好的用户体验,你的应用应当适 应各种网络条件:

  • 高带宽稳定网络

  • 低带宽稳定网络

  • 高带宽不稳定网络

  • 低带宽不稳定网络

  • 无网络

1.2.9 安全

安全对移动应用来说是最重要的,因为敏感信息可能会在应用间共享。因此,对所有通信 以及本地数据和共享数据进行加密就显得尤为重要了。
实现安全需要更多的计算、内存和存储,但这与最大化运行速度、最小化内存和存储使用 的目标相冲突。

二. 核心优化

该部分讨论的优化包括以下方面:

  • 内存管理

  • 能耗

  • 并发编程

2. 内存管理

与(基于垃圾回收的)Java 运行时不同, Objective-C 和 Swift 的 iOS 运行时使用引用计数。 使用引用计数的负面影响在于,如果开发人员不够小心,那么可能会出现重复的内存释放 和循环引用的情况。

2.1 内存消耗

内存消耗指的是应用消耗的RAM。

iOS 的虚拟内存模型并不包含交换内存,与桌面应用不同,这意味着磁盘不会被用来分页 内存。最终的结果是应用只能使用有限的 RAM。 这些 RAM 的使用者不仅包括在前台运行 的应用,还包括操作系统服务,甚至还包括其他应用所执行的后台任务。

应用中的内存消耗分为两部分栈大小堆大小

2.1.1 栈大小

应用中新创建的每个线程都有专用的栈空间, 该空间由保留的内存和初始提交的内存组成。线程的最大栈空间很小,这就决定了以下的限制。

  • 可被递归调用的最大方法数

每个方法都有其自己的栈帧,并会消耗整体的栈空间。如果你调 用 main ,那么 main 将调用 method1 ,而 method1 又将调用 method2 , 这就存在三个栈帧了,且每个栈帧都会消耗一定字节的内存。

1
2
3
4
5
6
7
main() { 
method1();
}

method1() {
method2();
}

  • 一个方法中最多可以使用的变量个数

所有的变量都会载入方法的栈帧中,并消耗一定的栈空间。

  • 视图层级中可以嵌入的最大视图深度

渲染复合视图将在整个视图层级树中递归地调用 layoutSubViewsdrawRect 方法。如果层级过深,可能会导致栈溢出。

2.1.2 堆大小

每个进程的所有线程共享同一个堆。使用 NSString 、载入图片、创建或使用 JSON/XML 数据、使用视图等都会消耗大量的堆 内存。
与通过类创建的对象相关的所有数据都存放在堆中。类可能包含属性或值类型的实例变量(iVars), 如 int 、 char 或 struct 。但因为对象是在堆内创建的,所以它们只消耗堆内存。
当对象被创建并被赋值时,数据可能会从栈复制到堆。类似地, 当值仅在方法内部使用 时,它们也可能会被从堆复制到栈。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@interface AClass  
@property (nonatomic, assign) NSInteger anInteger;
@property (nonatomic, copy) NSString *aString;
@end

//一些其他的类
-(AClass *) createAClassWithInteger:(NSInteger)i string:(NSString *)s {
AClass *result = [AClass new];
result.anInteger = i;
result.aString = s;

}

-(void) someMethod:(NSArray *)items {
NSInteger total = 0;
NSMutableString *finalValue = [NSMutableString string];
for(AClass *obj in items) {
total += obj.anInteger;
[finalValue appendString:obj.aString];
}
}

如下总结:

  • i 的值在栈上。但赋值给属性时,它必须被复制到堆中,因为那是存储 result 的地方。
  • 虽然 NSString * 通过引用传递,但这个属性被标记为 copy 。这意味着它的值必须被复 制或克隆,这取决于 [-NSCopying copyWithZone:] 方法的实现。
  • 使用 anInteger 时,它的值必须先复制到栈然后才能进行进一步的处理。在本示例中,它的值加到 total。

2.2 内存管理模型

内存管理模型基于持有关系的概念。如果一个对象正处于被持有状态,那它占用的内存就 不能被回收。

一旦与某个对象相关的任务全部完成,那么就是放弃了持有关系。这一过程没有转移持有 关系,而是分别增加或减少了持有者的数量。当持有者的数量降为零时,对象会被释放。

这种持有关系计数通常被正式称为引用计数

eg.1 如:

1
2
3
4
5
6
7

NSString *message = @"Objective-C is a verbose yet awesome language";
NSString *messageRetained = [message retain];
[messageRetained release];
[message release];
NSLog(@"Value of message: %@", message);

  1. 创建对象、message 建立了持有关系,引用计数为 1。
  2. messageRetained 建立了持有关系,引用计数增加为 2。
  3. messageRetained 放弃了持有关系,引用计数降为 1。
  4. message 放弃了持有关系,引用计数降为 0。
  5. 严格来讲,此时 message 的值是未定义的。你仍然能像之前那样得到相同的值,因为它 对应的内存还没有被回收或重置

eg.2 方法中的引用计数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

//一个Person类的部分
//注:此 NSString 创建方法相当不科学,可能作者为了例子这样写,我会在小结处再做进一步解释

-(NSString *) address {
NSString *result = [[NSString alloc] initWithFormat:@"%@\n%@\n%@, %@", self.line1, self.line2, self.city, self.state];
return result;
}

-(void) showPerson:(Person *) p {
NSString *paddress = [p address];
NSLog(@"Person's Address: %@", paddress);
[paddress release];
}

  1. 首次创建对象,result 指向内存的引用计数为 1。
  2. 通过 paddress (指向 result )指向的内存的引用计数仍然是 1。 showPerson: 方法通过 address 按钮创建了对象,是对象的持有者。对象不应该被再次持有( retain )。
  3. 放弃持有关系;引用计数降为 0。

2.3 自动释放对象

自动释放对象让你能够放弃对一个对象的持有关系,但延后对它的销毁。当在方法中创建 一个对象并需要将其返回时,自动释放就显得非常有用。自动释放可以帮助在 MRC 中管 理对象的生命周期。

在上面例子中,没什么能表示 address 方法持有了返回的字符串。因此,方法的调用者 showPerson: 也不应该释放返回的字符串,这可能会导致发生内存泄漏。加入 [paddress release] 这行代码的目的是为了指明这种情况。

以下是两种可能的解决方案。

  • 不要使用 alloc 或相关的方法。

  • 对返回的对象使用延时释放。

修改代码如下:

eg.3 不要使用 alloc 或相关的方法

1
2
3
4
5
6
7
8
9
10
11

-(NSString *) address {
NSString *result = [NSString stringWithFormat:@"%@\n%@\n%@, %@", self.line1, self.line2, self.city, self.state];
return result;
}

-(void) showPerson:(Person *) p {
NSString *paddress = [p address];
NSLog(@"Person's Address: %@", paddress);
}

  1. 不要使用 alloc 方法。
  2. 由于 showPerson: 方法没有创建实体对象,因此不要在 showPerson: 方法中使用 release 方法。

但是,当使用第三方类库或者某个类有多个用于创建对象的方法 时,到底是哪个方法保持了持有关系并不明确。所以引入了 autorelease

eg.4 autorelease

1
2
3
4
5
6

-(NSString *) address {
NSString *result = [[[NSString alloc] initWithFormat:@"%@\n%@\n%@, %@", self.line1, self.line2, self.city, self.state] autorelease];
return result;
}

  1. 持有的对象(在上述示例中是 NSString )是 alloc 方法返回的。

  2. 确保没有内存泄漏,你必须在失去引用之前放弃持有关系。

  3. 但是,如果使用了 release ,那么对象的释放将发生在返回之前,因而方法将返回一个 无效的引用。

  4. autorelease 表明你想要放弃持有关系,同时允许方法的调用者在对象被释放之前使用对象。

结论如下:

当创建一个对象并将其从非 alloc 方法返回时,应使用 autorelease。 这样可以确保对象将被释放,并尽量在调用方法执行完成时立即释放。

2.4 自动释放池块 (@autoreleasepool)

自动释放池块是允许你放弃对一个对象的持有关系、但可避免它立即被回收的一个工具。它还能确保在块内创建的对象会在块完成时被回收。本地的块可以用来尽早地释放其中的对象,从而使内存用量保持在较低的水平。

main.m 文件中的 @autoreleasepool 代码段

1
2
3
4
5
6
//main.m
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([HPAppDelegate class]));
}
//end

那么,块中收到过 autorelease 消息的所有对象都会在 autoreleasepool 块结束时收到 release 消息

(注 : 根据一些网上一些文章解析, Autorelease对象是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop )

但在一些特定情况下,可能想创建自己的 autoreleasepool

  • 当你有一个创建了很多临时对象的循环时

在循环中使用 autoreleasepool 块可以为每个迭代释放内存。虽然迭代前后最终的内存 使用相同,但你的应用的最大内存需求可以大大降低。

eg.1 错误的 autoreleasepool 示范代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    //错误代码示范
{

@autoreleasepool {
NSUInteger *userCount = userDatabase.userCount;
for(NSUInteger *i = 0; i < userCount; i++){
Person *p = [userDatabase userAtIndex:i];
NSString *fname = p.fname;
if(fname == nil) {
fname = [self askUserForFirstName];
}
NSString *lname = p.lname;
if(lname == nil) {
lname = [self askUserForLastName];
}
//...
[userDatabase updateUser:p];
}
}
}

eg.2 正确 autoreleasepool 示范:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

{

@autoreleasepool {
NSUInteger *userCount = userDatabase.userCount;
for(NSUInteger *i = 0; i < userCount; i++){
@autoreleasepool {
Person *p = [userDatabase userAtIndex:i];
NSString *fname = p.fname;
if(fname == nil) {
fname = [self askUserForFirstName];
}
NSString *lname = p.lname;
if(lname == nil) {
lname = [self askUserForLastName];
}
//...
[userDatabase updateUser:p];
}
}
}
}

  1. eg1中这段代码,因为只有一个 autoreleasepool ,而且内存清理工作要在所有的循环 迭代完成之后才能进行。

  2. 这个示例中有两个 autoreleasepool ,内层的 autoreleasepool 确保在每次循环迭代完成 后清理内存,从而导致更少的内存需求。

2.5 自动引用计数

持续跟踪 retain 、 release 和 autorelease 并不容易。要想找出是谁在什么时间和地点向谁 发送了这些消息就更难了。
2011 年的全球开发者大会上介绍了解决这一问题的方案——ARC
ARC 是一种编译器特性。 它评估了对象在代码中的生命周期,并在编译时自动注入适合的 内存管理调用。编译器还会生成适合的 dealloc 方法。

ARC规则

  1. 不能实现或调用 retain 、 release 、 autorelease 或 retainCount 方法。 这一限制不仅针 对对象,对选择器同样有效。因此, [obj release] 或 @selector(retain) 是编译时的错误。

  2. 可以实现 dealloc 方法,但不能调用它们。不仅不能调用其他对象的 dealloc 方法,也不能调用超类。 [super dealloc] 是编译时的错误。 但你仍然可以对 Core Foundation 类型的对象调用 CFRetain 、 CFRelease 等相关方法。(注: CF 库需要手动内存管理)

  3. 不能调用 NSAllocateObject 和 NSDeallocateObject 方法。应使用alloc方法创建对象,运行时负责回收对象。

  4. 不能在 C 语言的结构体内使用对象指针。

  5. 不能在 id 类型和 void * 类型之间自动转换。如果需要,那么你必须做显示转换。

  6. 不能使用 NSAutoreleasePool ,要替换使用 autoreleasepool 块。

  7. 不能使用 NSZone 内存区域。

  8. 属性的访问器名称不能以 new 开头,以确保与 MRC 的互操作性。

2.6 引用类型

ARC 带来了新的引用类型:弱引用

  • 强引用
    强引用是默认的引用类型。 被强引用指向的内存不会被释放。 强引用会对引用计数加 1, 从而扩展对象的生命周期。

  • 弱引用
    弱引用是一种特殊的引用类型。它不会增加引用计数,因而不会扩展对象的生命周期。

2.6.1 变量限定符

ARC 为变量供了四种生命周期限定符

(注:由于书上内容解析不太深刻,找到个更好的解析)

Variable Qualifier Desc
__strong 是默认的。只要有强类型指针指向一个对象,那么该对象会一直”生存“下去。
__weak 表明一个不会维持所持对象生命期的引用。当没有强引用指向该对象时,弱引用会设置为nil。
__unsafe_unretained 指定一个引用,该引用不会维持所持对象的生命期,并且在没有强引用指向对象时也不会设置为nil。如果它所指向的对象已经被释放,那么它会成为一个野指针。
__autoreleasing 用以指示以引用(id*)传入的参数并在retun后自动释放。

eg.1 使用变量限定符

1
2
3
4
5
6

1. Person * __strong p1 = [[Person alloc] init];
2. Person * __weak p2 = [[Person alloc] init];
3. Person * __unsafe_unretained p3 = [[Person alloc] init];
4. Person * __autoreleasing p4 = [[Person alloc] init];

  1. 创建对象后引用计数为 1, 并且对象在 p1 引用期间不会被回收。
  2. 创建对象后引用计数为 0, 对象会被立即释放,且 p2 将被设置为 nil 。
  3. 创建对象后引用计数为 1, 对象会被立即释放,但 p3 不会被设置为 nil 。
  4. 创建对象后引用计数为 1, 当方法返回时对象会被立即释放。

2.7 僵尸对象

僵尸对象是用于捕捉内存错误的调试功能。
通常情况下,当引用计数降为 0 时对象会立即被释放,但这使得调试变得困难。如果开启 了僵尸对象, 那么对象就不会立即释放内存, 而是被标记为僵尸。 任何试图对其进行访 问的行为都会被日志记录, 因而你可以在对象的生命周期中跟踪对象在代码中被使用的位置。

2.9 循环引用

(注: 书上的例子非常不科学,写出来是没有循环引用问题的。循环引用开发中比较常见,所以这里不做更多笔记说明。)

Final 内存这块笔记小结:

读完内存模块这一章节了解到更多内存管理方面知识,有以下几点补充总结。

1. iOS中一共有哪些存储空间:

  1. 栈区(stack)
  2. 堆区(heap)
  3. 静态区
  4. 寄存器区
  5. 文字常量区
  6. 程序代码区

2. 栈区

栈区中的内存空间是由编译器自动释放的,一般来是存放参数局部变量等等。
在iOS开发中,栈空间的大小为1M。

3. 堆区

栈空间只有1M,那么我们很多时候需要的资源都会超过1M,所以由此也引出了堆。在iOS中,栈空间是每个程序都有一个的,而且互相不干扰,堆空间则是一个系统公共的,换句话说就是所有的应用程序都使用一个堆空间。
对于堆中的内存空间的操作,是通过链表来操作的

4. (MRC) stringWithFormat 和 initWithFormat 有何不同?

  1. initWithFormat是实例方法

只能经由过程 NSString* str = [[NSString alloc] initWithFormat:@”%@”,@”Hello World”] 调用,然则必须手动release来开释内存资料

  1. stringWithFormat是类方法

可以直接用 NSString* str = [NSString stringWithFormat:@”%@”,@”Hello World”] 调用,内存经管上是autorelease的,不需手动显式release

  1. Example:

别的国外有个贴子对此有专门评论辩论并且提出了一个常见错误:

1
2
3

label.text = [[NSString alloc] initWithFormat:@"%@",@"abc"];

最后在 dealloc 中将 labelrelease 掉然则仍然会产生内存泄漏!

原因在于:用

label.text = …

时,实际是隐式调用的label的setText办法,这会retain label内部的字符串变量text(哪怕这个字符串的内容跟传进来的字符串内容雷同,但体系仍然当成二个不合的字符串对象),所以最后release label时,实际上只开释了label内部的text字符串,然则最初用initWithFormat生成的字符串并未开释,终极造成了泄漏。

为什么会导致这样的情况呢?

initWithString申请的地址每次都是一样的,而initWithFormat的地址每次都不一样,这个说明什么?
说明initWithString的地址是静态的,而initWithFormat是动态的。

5. __autoreleasing

有以下代码:

1
2
3
4
5
6
NSError *error;
BOOL OK = [myObject performOperationWithError:&error];
if (!OK) {
//blablabla
//...
}

其中,error 是隐式调用:

1
NSError * __strong e;

方法的声明通常是:

1
- (BOOL)performOperationWithError:(NSError * __autoreleasing *)error;

因此编译器会重写代码:

1
2
3
4
5
6
7
8
NSError * __strong error;
NSError * __autoreleasing tmp = error;
BOOL OK = [myObject performOperationWithError:&tmp];
error = tmp;
if (!OK) {
//blablabla
//...
}

本地变量声明( __strong)和参数( __autoreleasing)之间的区别导致编译器创建临时变量。在获取__strong变量的地址时你可以通过将参数声明为 id __storng*来获得其原始指针。或者你可以将变量声明为 __autoreleasing。

问题:

  1. -(BOOL)performOperationWithError:(NSError * __autoreleasing *)error;中为什么需要用 __autoreleasing 变量限定符修饰?
  2. 本地变量声明( __strong)和参数( __autoreleasing)之间的区别导致编译器创建临时变量?

解决上述两个问题,首先得知道:

  1. __autoreleasing是什么?
  2. __autoreleasing作用什么?(为什么要使用__autoreleasing)
5.1 __autoreleasing 是什么?

__autoreleasing 是 ARC 下用于控制变量生命周期而引入的4个变量限定符之一。

5.2 __autoreleasing 作用什么?(为什么要使用__autoreleasing)?

ARC:

  1. 不能显式的调用dealloc,实现或调用 retain, release, retainCount,或 autorelease。
  2. 不能使用 NSAutoreleasePool 对象

    ARC 提供了 @autoreleasepool来代替。这比 NSAutoreleasePool更高效。

对比一下 MRC 与 ARC 下使用 autoreleasepool 的不同地方:

1
2
3
4
5
/* MRC  */
1 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
2 id obj = [[NSObject alloc] init];
3 [obj autorelease]; //对象调用 autorelease 方法就是将该对象注册到最近的 autoreleasepool 中
4 [pool drain];
1
2
3
4
/* ARC */
1 @autoreleasepool {
2 id __autoreleasing obj = [[NSObject alloc] init];
3 }

通过将对象赋值给带有 __autoreleasing 修饰符的变量来代替调用 autorelease 方法,即将对象注册到 autoreleasepool

所以:

被添加到autoreleasepool了,默认情况下要将 obj 指向的对象添加到autoreleasepool中是需要 __autoreleasing 修饰符去修饰 obj 的,那么ARC 下它应该就会进行一个编译转换,如:

1
id __autoreleasing tem = obj;

Reference

  1. https://blog.csdn.net/nineteen_/article/details/48782465

  2. http://www.isaced.com/post-240.html

  3. https://www.jianshu.com/p/245bdcb47e3b

  4. https://www.jianshu.com/p/0258ed2133ff

3. 能耗

(注:这部分没有太多可以做笔记的知识点,忽略)

4. 并发编程

4.1 线程

线程是运行时执行的一组指令序列。

每个进程至少应包含一个线程。在 iOS 中,进程启动时的主要线程通常被称作主线程。所 有的 UI 元素都需要在主线程中创建和管理。

Cocoa 编程不允许其他线程更新 UI 元素。这意味着,无论何时应用在后台线程执行了耗时 操作,比如网络或其他处理,代码都必须将上下文切换到主线程再更新 UI

4.2 线程开销

线程不仅仅有创建时的时间开销,还会消耗内核的内存,即应用的内存空间。

4.2.1 内核数据结构

每个线程大约消耗 1KB 的内核内存空间。这块内存用于存储与线程有关的数据结构和属 性。这块内存是联动内存(wired memory) ,无法被分页。

4.2.2 栈空间

主线程的栈空间大小为 1M, 而且无法修改。所有的二级线程默认分配 512KB 的栈空间。 注意,完整的栈并不会立即被创建出来。实际的栈空间大小会随着使用而增长。因此,即 使主线程有 1MB 的栈空间,某个时间点的实际栈空间很可能要小很多。

4.3 GCD

GCD 提供的功能列表:

  • 任务或分发队列,允许主线程中的执行、并行执行和串行执行。

  • 分发组,实现对一组任务执行情况的跟踪,而与这些任务所基于的队列无关。

  • 信号量。

  • 屏障,允许在并行分发队列中创建同步的点。

  • 分发对象和管理源,实现更为底层的管理和监控。

  • 异步 I/O, 使用文件描述符或管道。

注意:
当应用中有多个长耗时的任 务需要并行执行时,最好 . 对线程的创建过程加以控制。如果代码执行的时间过长,很有可能达到线程的限制 64 个, 2,3 即 GCD 的线程池上限。

4.4 操作与队列

NSOperation 封装了一个任务以及和任务相关的数据和代码, 而 NSOperationQueue 以先入 先出的顺序控制了一个或多个这类任务的执行。

NSOperationNSOperationQueue 都提供控制线程个数的能力。 可用 maxConcurrentOperationCount 属性控制队列的个数,也可以控制每个队列的线程个数。

NSOperationQueue 和 GCD API 快速比较。

  • GCD

    • 抽象程度最高。
    • 两种队列开箱即用: main 和 global 。
    • 可以创建更多的队列(使用 dispatch_queue_create )
    • 可以请求独占访问(使用 dispatch_barrier_sync 和 dispatch_barrier_async )。
    • 基于线程管理。
    • 硬性限制创建 64 个线程。
  • NSOperationQueue

    • 无默认队列。
    • 应用管理自己创建的队列。
    • 队列是优先级队列。
    • 操作可以有不同的优先级(使用 queuePriority 属性)。
    • 使用 cancel 消息可以取消操作。注意, cancel 仅仅是个标记。 如果操作已经开始执行,则可能会继续执行下去。
    • 可以等待某个操作执行完毕(使用 waitUntilFinished 消息)。

4.5 线程安全的代码

书中讨论了 atomicnonatomic 问题,与 @synchronized 的使用. 关于 atomicnonatomic 是否绝对安全,一些面试题或者博客有更好的解释。

归纳知识点如下:

所有的属性默认都是原子性的。

atomicnonatomic 的区别在于,系统自动生成的 getter/setter 方法不一样。如果你自己写 getter/setter,那 atomic/nonatomic/retain/assign/copy 这些关键字只起提示作用,写不写都一样。

对于atomic的属性,系统生成的 getter/setter 会保证 get、set 操作的完整性,不受其他线程影响。比如,线程 A 的 getter 方法运行到一半,线程 B 调用了 setter:那么线程 A 的 getter 还是能得到一个完好无损的对象。

nonatomic就没有这个保证了。所以,nonatomic的速度要比atomic快

不过atomic并不能保证线程安全。如果线程 A 调了 getter,与此同时线程 B 、线程 C 都调了 setter——那最后线程 A get 到的值,3种都有可能:可能是 B、C set 之前原始的值,也可能是 B set 的值,也可能是 C set 的值。同时,最终这个属性的值,可能是 B set 的值,也有可能是 C set 的值。

摘取 Mr.Peak 博客一个小结:

atomic的作用只是给getter和setter加了个锁,atomic只能保证代码进入getter或者setter函数内部时是安全的,一旦出了getter和setter,多线程安全只能靠程序员自己保障了。所以atomic属性和使用property的多线程安全并没什么直接的联系。另外,atomic由于加锁也会带来一些性能损耗,所以我们在编写iOS代码的时候,一般声明property为nonatomic,在需要做多线程安全的场景,自己去额外加锁做同步。

原文: 传送门

另外文中还有一些锁的概念,这里也不做太多扩展.

三. 性能

5. 应用生命周期

iOS 应用启动时会调用 UIApplicationMain 方法,并传入 UIApplicationDelegate 类的引用。 委托接收应用范围的事件, 并且有明确的生命周期,application:didFinishLaunchingWit hOptions: 方法表明应用已经启动。关键组件的初始化就发生在这个方法中,如崩溃上报、

网络、日志以及埋点的初始化。此外,初次启动或恢复前置状态以便后续启动时,还可能会执行一些一次性的初始化操作。

5.1 应用委托

应用委托通常是应用创建的第一个对象。它为应用提供一些环境变量,其中包括应用启动 的详细信息、远程通知、深层链接,等等。

如图启动流程:

5.2 应用启动

著名的 application:didFinishLaunchingWithOptions: 方法是应用启动时最核心的地方。此 处不能发生任何错误,且绝不能发生崩溃,否则应用将无法正常使用,直到下次升级。

应用有四种启动类型。

  • 首次启动

安装应用后的首次启动。此时没有之前的状态,也没有本地缓存。

这意味着将会出现以下两种情况中的一种:没有需要加载的内容(因此加载时间会缩 短),或者需要从服务器上下载初始数据(可能需要很长的加载时间)。

  • 冷启动

应用后续的启动。在启动期间,可能需要恢复原来的状态,例如,游戏中达到的最高等 级、消息应用中的聊天记录、新闻应用中上一次同步的文章、已登录用户的证书,或者 仅仅是用户已经使用过的引导图标记符。

  • 热(重)启动

这是指当应用处于后台,但并未被挂起或关闭时,用户切换至应用而触发的启动。在这 种情况下,当用户通过点击应用图标或深层链接,返回应用时,不会触发 启动时的回调,而是直接用 applicationDidBecomeActive: (或 application:openURL:so urce:annotation: )回调。

  • 升级后启动

应用升级以后的启动。通常而言,升级后的启动与冷启动没有差别。

6. 用户界面

6.1 视图控制器

视图控制器的生命周期。

视图初始化时会涉及两个方法—— loadViewviewDidLoad 。当添加一个新的视图控制器时, 通过 Xcode 生成的模板代码只有 viewDidLoad 方法。当视图控制器的 view 被请求时, loadView 方法会被调用,但因为它还未被创建,所以会是 nil 。

视图会通过以下三种方式加载:

  • 从 nibs

  • 使用故事板(使用 UIStoryboardSegue )

  • 使用自定义代码创建 UI

如果通过覆写loadView 方法创建了自定义 UI,你需要牢记以下几点。

  • 将 view 属性设置到视图层级的根上。

  • 确保视图正被其他的视图控制器所共享。

  • 不要调用 [super loadView]

书上剩下的知识点比较常见,如 TableViewUIWebView介绍等,不做更多笔记.

7 网络

8 数据共享

9 安全

后续章节没有更多好的知识点,本书比较好的还是前半段关于内存知识点,更多知识点可以学习日本大佬写的那本 内存管理的书,看完这本书与另外搜索的笔记让一些模糊知识点变得清晰

Author

Sylar

Posted on

2019-04-28

Updated on

2021-11-14

Licensed under

Comments