iOS内存管理与多线程

在iOS中我们如何进行内存管理

目录



自动引用计数(ARC)

OC内存管理

在OC中,内存管理的引用技术采用ARC(自动引用计数),MRC(手动引用计数)的老版本现在不再使用。

OC的内存管理主要分为四个步骤:

  1. 创建 [ new / alloc / copy / mutableCopy ]
  2. 持有 [ retain ]
  3. 释放 [ release]
  4. 废弃 [ dealloc ]

事实上,在OC的内存管理中,是不用去考虑 计数 问题的,OC对象的内存管理,只需要注意几点就可以了:

  1. 对象的创建者持有对象
  2. 非对象的创建者也可以持有对象
  3. 不需要持有对象的时候需要释放对象
  4. 非自己持有的对象是无法去释放的

在iOS开发中,上述内存管理的方法均不是来自OC自身,而是来自苹果官方的的 Cocoa框架 内的 Foundation框架类 中的 NSObject, NSObject 中包含 + alloc类方法, - retain - release - dealloc实力方法

与其他语言的引用计数不同的是,OC的采用的是散列表作为引用计数的管理,其中内存块地址作为键值。这种设计能够保证即使内存块被损坏,只有这张表还在,就能顺通摸瓜找到对应的内存块地址

autoRelease(自动释放池)

作用类似C语言的 局部变量

autoRelease的使用:首先生成NSAutoReleasePool对象,而这个对象的属性则类似C语言的局部变量,调用对象的autoRelease实例方法,在NSAutoReleasePool结束生存周期后,所有调用过autoRelease实例方法的对象都将执行release操作。

autoRelease常用于处理大量数据时,避免出现内存不足而闪退的现象。例如APP中同时处理大量图片等。

OC中,实际上对象调用autoRelease都是调用的NSObject类的实例方法,会将对象加入正在运行的NSAutoReleasePool种。

ARC规则

OC中,通常编译单位可设置ARC有效/无效,因此项目中可以出现 ARC有效/无效 混合的情况。新项目中,通常均会使用ARC有效。

xcode4.2及其以上版本中,文件会默认ARC有效的

修饰符

ARC有效时,对象类型必须附加所有权修饰符:

  1. __strong
  2. __weak
  3. __unsafe_unretained
  4. __autoreleasing


  • __strong

__strong是对象在不明确所有权修饰符时的默认修饰符。与strong一样,表示强引用,在ARC中,使用__strong后,则不必考虑retain和release的问题了,它自动做好处理的。
因为默认会使用__strong,使用通常创建对象时可以不加上__strong

  • __weak(iOS5及其以上)

__strong并不能做到完美的内存管理,当两个对象互相强引用对方的时候,就会造成 循环引用,同用一个对象强引用自身的时候也会造成自引用(也是循环引用)循环引用会导致对象不能正常得到释放而产生内存泄露,这个时候就需要__weak来解决了。__weak为对象提供弱引用,弱引用不能持有对象实例,因此不会产生循环引用的问题。

另外,__weak在持有对象的被废弃的时候,弱引用会自动失效并被赋值为nil,可以通过检验弱引用是否为nil,就可以知道那个对象是否有效。

  • __unsafe_unretained

在作用上与__weak类似,在低于iOS5的版本也被用于代替__weak。但是__unsafe_unretained赋值给__strong对象时,是必须保证__strong对象存在的,不然会导致崩溃。

  • __autoreleasing

在iOS中,ARC有效时,会使用@autoreleasepool块来表示autoRelease:

1
2
3
@autoreleasepool {
id __autoreleasing test = [[NSObject alloc] init];
}

__autoreleasing的作用则是 [test autorelease]

规则

ARC有效时,必须遵循以下规则:

  1. 禁止retain、release,autoRelease,retainCount [ARC会自动处理]
  2. 禁止NSAllocateObject、NSDeallocateObject [使用alloc]
  3. 遵守内存管理的命名规则
  4. 禁止显式调用dealloc [ARC会自动调用dealloc]
  5. 使用@autoreleasepool{}代替NSAutoReleasePool
  6. 禁止NSZone [iOS有着更好的处理]
  7. 对象类型变量不能成为C语言结构体成员
  8. 不要显式转换idvoid * [不得不用就要加上 __brdige]


ARC属性

ARC有效时,属性声明的属性与所有权修饰符的对应关系

  • assign ===> __unsafe_unretained
  • unsafe_unretained ===> __unsafe_unretained
  • retain ===> __strong
  • strong ===> __strong
  • copy ===> __strong
  • weak ===> __weak

ARC实现

简单理解,

__autoreleasing则是 [test autorelease]

__strong则是在对应地方插入好相应的retain和release方法;

__weak,使用weak表,将赋值对象的地址作为key值,weak修饰对象的地址作为value值,储存在表中,当对象被释放后,则将weak修饰对象赋值nil然后从表中移除。(使用weak修饰对象会比较消耗cpu资源,所以请在正确的地方使用)

Block

匿名的内联代码集合体,常用在代码回调上

功能上与其他语言的 闭包 , lambda表达式 相似

Block的使用

基本结构如下:

1
2
3
returnType (^blockName)(parameterTypes) = ^(parameters) {
statements
};

Block的创建方式

1
2
3
4
5
6
7
8
9
10
11
1. 申明Block并创建
typedef void (^testBlock)(int);
testBlock _block;

2. 属性定义Block
@property (nonatomic, copy) void (^testBlock)(int);

3. 局部Block
void (^testBlock)(int) = ^(int x) {
NSLog(@"test");
};

Block作为一个函数的参数

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

1. 先定义
typedef void (^parameterBlock)(int);

- (void)test:(parameterBlock)blk {
//statements
}


2. 省略定义
- (void)test:(void (^)(int))blk {
//statements
}

Block截取变量

  • 截取局部变量

Block截取局部变量是直接进行了 值拷贝 操作,因此其截取的值是无法被修改的.
如果企图修改,XCode会报错,

但是可以用__Block修饰准备在Block中改变一个变量的值

1
2
3
4
_block int x = 0;
void (^myBlock)() = ^{
++x;
}

这样修饰后,这个值就可以被修改了。原因是Block直接复制了其内存地址

  • 全局变量和静态变量

截取全局变量和静态变量时是直接对对象进行引用,因此可以直接修改变量。

Block循环引用

Block在使用的时候很容易出现循环引用。
如图:

简单解释就是:某个对象持有了Block,但是Block又在代码块中调用了那个对象。

通常的解决方案为 打破互相的强引用

下面是Self-Block循环引用解决方法例子:

1
2
3
4
5
__weak typeof(self) weakSelf = self;
^ {
weakSelf.xxxxxx;
[weakSelf yyyy];
};

为了防止Block可能出现先于对象被释放的情况时,可以再加上一层strong。

1
2
3
4
5
6
__weak typeof(self) weakSelf = self;
^ {
__strong typeof(self) strongSelf = weakSelf;
weakSelf.xxxxxx;
[weakSelf yyyy];
};

Block对象模型

Block数据结构

  • isa指针 用于实现对象相关功能
  • invoke 函数指针,代码块实际上是个函数
  • descriptor Block的描述信息,比如size等等
  • variable Block所截取的对象

Block用三种,在Block创建的时候,isa指针指向对应的类
全局Block(_NSConcreteGlobalBlock)
栈Block(_NSConcreteStackBlock)
堆Block(_NSConcreteMallocBlock)
(isa指向的三个类)

Block外部变量的复制

在没用_block修饰的外部变量或者对象,Block内部截取时,是直接进行了”值拷贝”,直接复制其数据结构存在新的内存地址中,并用被其内存结构中的variable所引用。如果是对象,则复制其指针。

如果是用_block修饰了的外部变量和对象被Block截取的时候。Block会直接复制其对象或者值的引用地址,直接对其内存数据进行操作。

多线程

进程

  • 创建进程:创建所有必要的管理信息,创建完成后才会加入下一步
  • 就绪:获得了一切需要资源和管理信息
  • 运行:运行状态进程数小于等于系统处理器的总数
  • 阻塞:等待,睡眠
  • 终止:进程结束运行

线程

线程是进程的基本执行单元,进程的任务都是在线程中执行的。其中主线程负责主要的进程任务,在iOS开发中,主线程通常为UI线程。

多线程

多线程常用地方:

  • 网络请求
  • 图片加载

任务执行方式:

  • 串行
  • 并行

多线程执行原理

  • 单核操作系统主要是通过时间片来实现的,两个线程在不同的时间片上交替执行,完成多线程。

  • 多核操作系统是把线程分配给不同的处理器来执行,完成真正的并行。

优缺点

  • 优点:

    1. 简化了编程模型:高效的处理大型任务或者零散任务
    2. 更加轻量级
    3. 提高了效率和资源利用率
  • 缺点:

    1. 加强了程序设计的复杂性
    2. 占用内存空间(不要乱用多线程)
    3. 增加了CPU的调度开销,增加了CPU占有率

iOS中多线程的实现技术方案

  • pThread
  • NSThread
  • GCD
  • NSOperation

pThread

多平台的多线程操作API,基于C语言的。

1
2
3
4
5
6
7
8
9
10
void *fun(void *data) {
NSLog(@"fun");
return NULL;
}

- (void)test {
//创建线程
pthread_t t;
pthread_create(&t, NULL, fun, NULL);
}

ps:pThead在iOS中并不常用

NSThread

经过苹果封装,完全面向对象。

三种创建方式:

  1. 方式一
1
2
3
//单独设置NSThread变量是因为方便调试跟踪
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
[thread start];
  1. 方式二
1
2
3
4
5
6
[NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil];

//也可以直接使用Block
[NSThread detachNewThreadWithBlock:^{
[self run];
}];
  1. 方式三
1
2
3
4
//这个函数来自NSObject
[self performSelectorInBackground:@selector(run) withObject:nil];

[self performSelector:@selector(run) onThread:([NSThread new]) withObject:nil waitUntilDone:nil];

设置优先级:

setThreadPriority的值是在0~1之间的double值。值越高,执行该线程的概率越高。

1
2
3
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];

[thread setThreadPriority:0.5];

NSThread锁

1
2
3
4
@synchronized(self)
{
// Everything between the braces is protected by the @synchronized directive.
}
  • @synchronized,代表这个方法加锁, 相当于不管哪一个线程(例如线程A),运行到这个方法时,都要检查有没有其它线程例如B正在用这个方法,有的话要等正在使用synchronized方法的线程B运行完这个方法后再运行此线程A,没有的话,直接运行。它包括两种用法:synchronized 方法和 synchronized 块。
1
2
3
4
//ticketsCondition 为一个NSCondition对象
[ticketsCondition lock];
//content
[ticketsCondition unlock];
  • NSCodition是一种特殊类型的锁,我们可以用它来同步操作执行的顺序。等待某个NSCondition的线程一直被lock,知道其他线程给那个condition发送了信号。

NSOperation

对于GCD的封装。

使用方式:

  • NSInvocationOperation
1
2
NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:SEL object:nil];
[operation start];
  • NSBlockOperation
1
2
3
4
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"test");
}];
[operation start];
  • NSBlockOperation
    用于添加异步操作
1
2
3
4
5
6
7
8
9
10
11
 NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"operation");
}];

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:operation];
NSLog(@"main");

result:
DEMO[8360:12240295] main
DEMO[8360:12240397] operation
  • 自定义NSOperation
    创建一个子类继承于NSOperation
  • setMaxConcurrentOperationCount最大线程数
1
[queue setMaxConcurrentOperationCount:1]

GCD

iOS开发中比较常用的多线程技术,苹果官方说法为:异步执行任务的技术之一。

首先明白 线程,任务,队列 的区别

常见GCD使用
  • 简单使用DEMO
1
2
3
4
5
6
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// get Data
dispatch_async(dispatch_get_main_queue(), ^{
//Update UI
});
});
  • dispatch_get_main_queue

    由系统提供的主线程

  • dispatch_get_global_queue

    由系统提供的全局并发队列

1
2
3
4
5
6
7
8
9
10
//  long identifier : 优先级
// 系统提供了三个优先级列举
// #define DISPATCH_QUEUE_PRIORITY_HIGH 2
// #define DISPATCH_QUEUE_PRIORITY_DEFAULT 0
// #define DISPATCH_QUEUE_PRIORITY_LOW (-2)
//
//
// unsigned long flags : 通常填 0 或者 NULL
//
dispatch_get_global_queue(long identifier, unsigned long flags)
  • dispatch_queue_create

    创建队列

1
2
dispatch_queue_t queue = dispatch_queue_create("name", DISPATCH_QUEUE_CONCURRENT);
//DISPATCH_QUEUE_CONCURRENT,则这个队列是并发队列
1
2
dispatch_queue_t queue = dispatch_queue_create("name", DISPATCH_QUEUE_SERIAL);
//DISPATCH_QUEUE_SERIAL,则这个队列是串行队列,一次在一个线程只执行一个任务。

当需要用多行程处理的数据会造成数据竞争的时候就使用DISPATCH_QUEUE_SERIAL更加安全。但是DISPATCH_QUEUE_SERIAL每次都会生成一个新的线程去处理程序,会造成计算压力。如果不是处理一些互不竞争的数据则正常使用DISPATCH_QUEUE_CONCURRENT吧

  • dispatch_set_target_queue

作用有二:

  1. 用于改变队列的优先级,将目标队列与指定的队列优先级一致。
  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
25
26
27
dispatch_queue_t queue_1 = dispatch_queue_create("queue_1", NULL);
dispatch_queue_t queue_2 = dispatch_queue_create("queue_2", NULL);

dispatch_async(queue_1, ^{
NSLog(@"queue_1");
});
dispatch_async(queue_2, ^{
NSLog(@"queue_2");
});

dispatch_set_target_queue(queue_1, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0));

dispatch_async(queue_1, ^{
NSLog(@"1");
});
dispatch_async(queue_2, ^{
NSLog(@"2");
});


/*
结果:
queue_1
queue_2
2
1
*/

让指定队列来成为目标队列的执行阶层

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
dispatch_queue_t queue_1 = dispatch_queue_create("queue_1", NULL);
dispatch_queue_t queue_2 = dispatch_queue_create("queue_2", NULL);
dispatch_queue_t queue_3 = dispatch_queue_create("queue_2", NULL);

dispatch_async(queue_1, ^{
NSLog(@"queue_1");
});
dispatch_async(queue_2, ^{
NSLog(@"queue_2");
});
dispatch_async(queue_3, ^{
NSLog(@"queue_3");
});

dispatch_queue_t queue = dispatch_queue_create("queue", NULL);

dispatch_set_target_queue(queue_1, queue);
dispatch_set_target_queue(queue_2, queue);
dispatch_set_target_queue(queue_3, queue);

dispatch_async(queue_1, ^{
NSLog(@"queue_1");
});
dispatch_async(queue_2, ^{
NSLog(@"queue_2");
});
dispatch_async(queue_3, ^{
NSLog(@"queue_3");
});

/*
结果:
queue_3
queue_2
queue_1
queue_1
queue_2
queue_3


让无序的三个串行队列变成有序的队列,可以有效避免几个串行队列的并行计算
*/
  • dispatch_groupd

    用于处理并发线程时,完全结束时的逻辑处理

1
2
3
4
5
6
7
8
9
10
11
dispatch_queue_t queue = dispatch_queue_create("dispachTest", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{
//todo1
});
dispatch_group_async(group, queue, ^{
//todo2
});
dispatch_group_notify(group, queue, ^{
//finish
});
  • dispatch_barrier_async

    插入任务

    常用于并发队列中避免出现数据竞争的情况。比如在多线程读取数据,这时同时也需要写入数据,此时就有可能造成数据问题,因此使用dispatch_barrier_async就可以有效避免这个问题,并发队列会先等待dispatch_barrier_async执行完成后再继续执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
dispatch_queue_t queue = dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT);
__block NSInteger x = 0;
dispatch_async(queue, ^{
NSLog(@"%ld",x);
});
dispatch_async(queue, ^{
NSLog(@"%ld",x);
});
dispatch_barrier_async(queue, ^{
x += 1;
NSLog(@"%ld",x);
//此时写入数据,其它队列会等待这个写入完成。
});
dispatch_async(queue, ^{
NSLog(@"%ld",x);
});
  • dispatch_suspend dispatch_resume
1
2
3
4
5
//线程队列挂起
dispatch\_suspend(queue);

//线程队列恢复
dispatch\_resume(queue);
  • dispatch_after

    延迟多少秒执行,但是这个并不是准确的时间,可用于大致的情况下。

1
2
3
4
5
6
//该方法的第一个参数是开始时间,第二个参数是多少时间后,第三个参数是要执行的block。  
//dispatch_after的真正含义是在多少秒后把任务添加进队列中,并不是表示在6秒后执行

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
//
});
  • dispatch_semaphore

    信号量

    就是一种可用来控制访问资源的数量的标识,设定了一个信号量,在线程访问之前,加上信号量的处理,则可告知系统按照我们指定的信号量数量来执行多个线程。

其实,这有点类似锁机制了,只不过信号量都是系统帮助我们处理了,我们只需要在执行线程之前,设定一个信号量值,并且在使用时,加上信号量处理方法就行了。

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
    // create(value) 中的value表示最大允许的线程数量
dispatch_semaphore_t semaphore = dispatch_semaphore_create(3);

dispatch_queue_t queue = dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT);

// 信号量 先 “降” 后 “升”
dispatch_async(queue, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"run 1");
sleep(2);
NSLog(@"finish 1");
dispatch_semaphore_signal(semaphore);
});
dispatch_async(queue, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"run 2");
sleep(2);
NSLog(@"finish 2");
dispatch_semaphore_signal(semaphore);
});
dispatch_async(queue, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"run 3");
sleep(2);
NSLog(@"finish 3");
dispatch_semaphore_signal(semaphore);
});
dispatch_async(queue, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"run 4");
sleep(2);
NSLog(@"finish 4");
dispatch_semaphore_signal(semaphore);
});
}

结果:
run 2
run 1
run 3
finish 2
finish 1
finish 3
run 4
finish 4

// 因为线程最好只允许3个,所以在1,2,3执行完成后才会执行4.

相关使用范例

异步加载图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
self.imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
[self.view addSubview:self.imageView];

NSString *url = @"https://ss0.bdstatic.com/5aV1bjqh_Q23odCf/static/superman/img/logo/bd_logo1_31bdc765.png";

dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"开始异步下载图片");
NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:url]];
UIImage *image = [[UIImage alloc] initWithData:data];
sleep(3);
if(image) {
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"主线程显示图片");
[self.imageView setImage:image];
});
}
});

创建单例

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

static SingleTest *instanceObj = nil;

+ (instancetype)instance {

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instanceObj = [[SingleTest alloc] init];
});

return instanceObj;
}
MarkDown整理 iOS-组件化

Comments

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×