Oracle学习

一.查询

1.1 查询一个列数据

1
2
3
4
5
SELECT
name
FROM
customers;
SQL

1.2 查询多个列数据

要从多个列查询数据,请指定一个以逗号分隔的列名称列表。

1
2
3
4
5
6
select 
customer_id,
name,
credit_limt
from
customers;

1.3 查询所有数据

查看所有列可以使用简写星号(*)来指示Oracle从表中返回所有列的数据

1
2
3
4
select
*
from
customers;

二. 排序: ORDER BY

ORDER BY子句可以用来对一列或多列的结果集按升序或降序进行排序。默认是ASC排序

1
2
3
select name, address, credit_limt
from customers
order by name ASC

三. 过滤重复行 : DISTINCT

SELECT DISTINCT可以用来过滤结果集中的重复行,确保SELECT子句中返回指定的一列或多列的值是唯一的。

3.1 distinct 应用一列:

1
select distinct first_name from CONTACTS order by first_name

3.2 disctinct 应用多列:

1
2
select distinct product_id, quantity from ORDER_ITEMS 
order by product_id

四. 符合查询条件: WHERE

WHERE子句指定SELECT语句返回符合条件的行记录。Where子句来指定过滤的条件返回符合查询条件的行记录。

以下示例仅返回名称为“Kingston”的产品:

1
2
3
4
5
6
SELECT 
product_name, description, list_price, category_id
FROM
products
WHERE
product_name = ‘Kiingston’;

4.1.选择符合某些条件的行

要组合条件,可以使用AND,OR和NOT逻辑运算符。

例如,要获取属于类别编号是4且标价大于500的所有主板,请使用以下语句:

1
2
3
4
5
6
7
8
SELECT
product_name,
list_price
FROM
products
WHERE
list_price > 500
AND category_id = 4;

4.2. 选择在两个值之间的值的行记录

要查找具有两个值之间的值的行,请在WHERE子句中使用BETWEEN运算符。

1
2
3
4
5
6
7
8
9
select 
product_name,
list_price
from
products
where
list_price between 650 and 680
order by
list_price;

4.3. 选择符合值列表中的行记录

要查询值列表中的行记录,可以使用IN运算符:

1
2
3
4
5
6
7
8
9
select
product_name,
list_price
from
products
where
cateroy_id IN (1, 4)
order by
product_name;

4.4. 选择包含值的行作为字符串的一部分: LIKE

1
2
3
4
5
6
7
8
9
select 
product_name,
list_price
from
products
where
product_name LIKE ‘Asus%’
order by
list_price;

五. 指定要返回的行数或百分比 : FETCH

获取前N行记录的示例:

以下语句返回库存量最高的前10个产品:

1
2
3
4
5
6
7
8
9
10
select 
product_name,
quanlity,
from
inventories
inner join products
using(product_id)
order by
quanlity desc
fetch next 5 rows only;

六. 授权

1
2
//授权创建同义词的权限给hrpuser用户
grant create any synonym to hrpuser
1
2
/删除公用同义词EF_COST_WBWORKNUMDATA
drop public synonym EF_COST_WBWORKNUMDATA
1
2
//为ythis用户的EF_IF_V_optTYPE表创建公用同义词EF_IF_V_optTYPE。
create public synonym EF_IF_V_optTYPE for ytis. EF_IF_V_optTYPE
1
2
3
4
//表权限
//给与hrpuser用户,EF_IF_V_optTYPE表的查询权限

grant select on EF_IF_V_optTYPE to hrpuser

首先连接到托管Oracle的服务器,然后以SYSTEM帐户身份连接到Oracle本身。

该SYSTEM帐户是安装Oracle时自动生成的少数预定义管理帐户之一。 SYSTEM能够执行大多数管理任务,我们就看看帐户管理。

6.1 创建用户

连接后SYSTEM,只需发出CREATE USER命令即可生成新帐户。

1
create books_admin(用户名) books(表明) identified by 123456(密码)

在这里,我们只是创建一个由指定的books_admin帐户进行IDENTIFIED身份验证的帐户password。

6.2 授权语句

books_admin创建新帐户后,我们现在可以开始使用该GRANT语句向帐户添加权限。 GRANT是一个非常强大的语句,有许多可能的选项,但核心功能是管理两个users和roles整个数据库的权限。

6.3 提供角色

通常,您首先要通过将帐户附加到各种角色来为用户分配权限,从角色开始CONNECT:

1
grant CONNECT(角色名称) TO books_admin(用户名);

在某些情况下,为了创建更强大的用户,您还可以考虑添加RESOURCE角色(允许用户为自定义模式创建命名类型)甚至DBA角色,这允许用户不仅创建自定义命名类型,还可以更改和销毁它们同样。

1
grant CONNECT, RESOURCE, DBA to books_admin(用户名);

6.4 分配权限

接下来,您需要确保用户具有实际连接到数据库并使用【创建会话的权限】grant create session。我们还将它与【所有权限】结合使用grant any privileges。

1
2
grant create session  to books_admin;//授予books_admin用户创建session的权限,即登陆权限
grant any privilege to books_admin(用户名);

我们还需要确保我们的新用户在系统中分配了磁盘空间来实际创建或修改表和数据,所以我们会grant tablespace这样:

1
2
//授予用户使用表空间的权限
grant unlimited tablespace to books_admin(用户名)

6.5 表权限

虽然在较新版本的Oracle中通常不需要,但某些较旧的安装可能要求您手动指定新用户对特定架构和数据库表的访问权限。

举例来说,如果我们希望我们的books_admin用户有执行能力SELECT,UPDATE,INSERT,和DELETE在功能books表中,我们可以执行以下GRANT语句:

1
grant select,insert,update,delete on books to books_admin

七. 同义词(synonyms)

Oracle的同义词(synonyms)从字面上理解就是别名的意思,和视图的功能类似,就是一种映射关系.

在Oracle中对用户的管理是使用权限的方式来管理的,也就是说,如果我们想使用数据库,我们就必须得有权限,但是如果是别人将权限授予了我们,我们也是能对数据库进行操作的,但是我们必须要已授权的表的名称前,键入该表所有者的名称,所以这就是比较麻烦的,遇到这种情况,我们该怎么办呢?创建个Oracle同义词吧!这样我们就可以直接使用同义词来使用表了。

在 user1 下 有表 table1,现在 user2 要使用 table1

1
2
grant select on table1 to user2;
select * from user1.table1 ;
1
2
create public synonym table1 for user1.table1
select * from table1; //不需要键入该表所有者的名称

7.1 同义词的概念

Oracle数据库中提供了同义词管理的功能。同义词是数据库方案对象的一个别名,经常用于简化对象访问和提高对象访问的安全性。在使用同义词时,Oracle数据库将它翻译成对应方案对象的名字。

与视图类似,同义词并不占用实际存储空间,只有在数据字典中保存了同义词的定义。在Oracle数据库中的大部分数据库对象,如表、视图、序列、存储过程、包等等,数据库管理员都可以根据实际情况为他们定义同义词。

7.2 Oracle同义词的分类

Oracle同义词有两种类型,分别是公用public同义词私有同义词

1)公用Oracle同义词:

由一个特殊的用户组Public所拥有。顾名思义,数据库中所有的用户都可以使用公用同义词。公用同义词往往用来标示一些比较普通的数据库对象,这些对象往往大家都需要引用。

2)私有Oracle同义词:

它是跟公用同义词所对应,他是由创建他的用户所有。当然,这个同义词的创建者,可以通过授权控制其他用户是否有权使用属于自己的私有同义词

7.3 创建同义词

1)创建公有Oracle同义词的语法:

create [public] synonym 同义词名称 for [username.]objectName;

1
create public synonym table_name for user.table_name;

2)创建一个远程数据库的对象的同义词

如果要创建一个远程的数据库上的某张表的同义词,需要先创建一个Database Link(数据库连接)来扩展访问,然后在使用如下语句创建数据库同义词:create synonym table_name for table_name@DB_Link;

学习参考

oracle 用户创建及权限设置 和 同义词

[Oracle同义词(synonyms)的概念作用、创建删除查看及Oracle的db link

](https://blog.csdn.net/a327736051/article/details/51419163)

Objective-C-(四)-多线程

介绍多线程前先来理解下进程和线程的概念:

进程:一个在前台正在运行的应用程序就是一个进程。比如打开的微信APP就是一个进程。

线程:微信APP可以聊天,发图片,而做这些事情都是要通过线程来做的。线程就是执行任务的基本单元,是CPU调度和分派的基本单位。一个进程可以有多个线程,线程是进程的一部分。

多线程就是多个线程并发处理任务的技术,可以充分利用多核CPU的资源,提升执行性能。

iOS中处理和操作线程的方案有PthreadsNSThreadGCDNSOperation && NSOperationQueuePthreads 比较底层,没有用过,就不说了,主要来说下NSThreadGCDNSOperation && NSOperationQueue

介绍之前先说下两个概念:同步/异步,串行/并发

  • 同步是指在执行任务的时候,会等待当前这个任务执行完毕后再继续向下执行。如果当前这个任务没有执行完毕,那么就会阻塞当前的线程直到这个任务执行完成。
  • 异步是指在执行任务的时候,不会等待当前这个任务执行完毕就会继续向下执行。即使当前这个任务没有执行完,也会立刻执行下面的任务。

同步异步的区别可以理解为:例如方法A内部中有个方法B,当前的方法A内部执行到了方法B,如果是同步,会等这个方法B执行完返回后才会向下执行,如果方法B没有返回,当前的线程就会一直卡住不向下执行;如果是异步的话这个方法B会立刻返回,然后继续向下执行。

1
2
3
4
5
6
7
8
9
10
//方法A
- (void)methodA {
....其他任务
[self methodB]
....其他任务
}
//方法B
- (void)methodB {
...
}
  • 串行:执行任务的时候按顺序执行,一次只能执行一个任务。当前的任务没有执行完,不会执行下一个。
  • 并发:执行任务的时候可以多个任务同时执行。

NSThread

NSThread是一个直接面向线程的类,提供了很多可以操作线程的方法。最简单的创建一个线程:

NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(doThead:) object:@"zzy"];

这样就创建了一个线程实例threadthread通过doThead:这个方法执行任务。但是这个时候创建的线程并没有启动执行任务,需要调用下start方法:[thread start];这样线程就开始执行任务了。object:@"zzy"参数会在线程执行doThead:方法的时候被作为参数传入。

我们也可以给一个线程设置一个名字作为标记:[thread setName:@"zzy"]; 通过[NSThread currentThread]获取当前线程的信息,打印出当前这个线程可以看到:

1
XXX[67222:788960] thread = <NSThread: 0x600000662600>{number = 3, name = zzy}

通过name我们可以找到我们设置的是哪个thread

NSThread 也提供了一些关于线程的状态信息,例如:executingfinishedcancelled,分别表示当前的线程正在执行,完成,取消的状态。但是cancelled只是一个标记状态,并不会取消线程,如果想强制退出当前的线程,可以通过[NSThread exit];

NSThread的使用比较简单,主要是注意通过NSThread创建的子线程中指定NSTimer计时器的情况。由于子线程中的runloop默认不启动,所以当添加一个定时器重复执行任务的时候要指定启动runloop。

1
2
3
4
5
- (void)doThead:(NSString *)object {    
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerRepeatAction:) userInfo:@{@"id":@"zzy"} repeats:YES];
[timer fire];
[[NSRunLoop currentRunLoop] run];
}

GCD

GCD是平时最常用的多线程处理的方案了,我们将任务放进block中追加到队列里,系统就会自动为我们创建相应的线程去处理任务,并且任务完成后在合适的时机销毁线程,整个过程中不需要我们去直接操作线程,所以使用起来比较方便。

GCD有三种队列:串行队列,并发队列,主队列(特殊的串行队列,队列中的任务一定会在主线程中执行)

GCD获取队列的方式有两种:

  • 第一种通过dispatch_queue_create(const char *_Nullable label, dispatch_queue_attr_t _Nullable attr);函数直接创建队列,第一个参数的队列的名称,方便调试使用,第二个是队列的类型。传NULLDISPATCH_QUEUE_SERIAL表示串行队列,传DISPATCH_QUEUE_CONCURRENT表示并发队列。

    1
    dispatch_queue_t queue = dispatch_queue_create("zzy", NULL);
  • 第一种是直接获取系统提供好的两个队列:

    • dispatch_get_main_queue:主队列,运行在主线程中的串行队列
    • dispatch_get_global_queue:全局队列,也就是并发队列,通过这个函数的第一个参数还可以指定队列的优先级。

GCD中平时常用的有以下几个函数:

  • dispathc_once:确保函数中的block只执行一次,而且是线程安全的。常用来实现单利对象。
  • dispatch_after:延迟指定的时间之后提交某个任务(这里是延迟某个时间提交任务,而不是延迟某个时间执行任务)。常用做一个定时操作。
  • dispatch_suspend&&dispatch_resume:暂停和恢复某个队列
  • dispatch_apply:循环将某些任务加入到某个队列当中
  • dispatch_set_target_queue:可以设置目标队列的优先级,让目标队列的优先级和指定的队列优先级一样。(由于dispatch_queue_create函数创建的队列没有提供设置优先级的参数,默认是默认的优先级,所以可以用这个函数来设置其他优先级,例如dispatch_set_target_queue(targetQueue, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0));
  • dispatch_barrier_async:在一个并发队列中,先并行处理一部分任务,然后同步执行dispatch_barrier_async中的任务(只执行当前block中的一个任务,其他任务不执行),dispatch_barrier_async任务执行完成后,然后再恢复当前队列并行执行的任务。常用来处理在多线程数据读取的时候插入写入操作(写入操作必须是线程安全的)。

这里主要介绍下平时处理异步任务的时候比较有用的任务组dispatch_group_t和信号量dispatch_semaphore_t

dispatch_group_t

dispatch_group_t是一个任务组,可以将几个并发任务一起放到任务组里面,当这几个并发任务都执行完成后,同步得到通知回调。涉及到两个常用的函数:dispatch_waitdispatch_notify

dispatch_wait是一个同步的函数,一旦被调用该函数就会一直处于调用的状态而不返回,直到dispatch group内的任务都执行完成或者经过dispatch_wait中第二参数指定的时间后它才会返回,否则它会一直阻塞当前的线程,无法继续执行。如果dispatch_wait函数返回值为0,说明group内的任务已经执行完毕;如果返回值不为0,说明经过了指定的时间group内的任务依然没有执行完。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (void)dispathGroup {
/**
使用dispatch_group_t也可以向group中指定不同优先级的队列,不一定非要是同一个队列,他们都归属同一个group
*/
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t defaultQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_queue_t highQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_group_async(group, defaultQueue, ^{
sleep(2);
NSLog(@"任务一");
});
dispatch_group_async(group, defaultQueue, ^{
sleep(2);
NSLog(@"任务二");
});
dispatch_group_async(group, highQueue, ^{
NSLog(@"高优先级任务");
});
dispatch_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"任务执行完成");
}

执行后:

1
2
3
4
2019-01-01 10:09:53.092345+0800 XXX[12671:1433314] 高优先级任务
2019-01-01 10:09:55.094704+0800 XXX[12671:1433327] 任务二
2019-01-01 10:09:55.094704+0800 XXX[12671:1433313] 任务一
2019-01-01 10:09:55.095105+0800 XXX[12671:1433235] 任务执行完成

可以看到dispatch_wait 会一直阻塞当前的线程,直到任务执行完成(这里指定的时间是永久等待,也可以自己定义临界时间)。

dispatch_notify提供了一个异步执行的函数,它不会阻塞当前的线程,但是会监听dispatch group的任务执行情况,一旦group内的都执行完成后就会调用dispatch_notify函数。dispatch_notify(object, queue, notification_block)函数提供了三个参数,第一个参数就是监听的dispatch group,另外两个参数提供了可以让某个任务在指定队列中执行的功能。也就说当dispatch group内的任务都完成后会通知dispatch_notify然后可以进行一些完成后的处理操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (void)dispathGroup {
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t defaultQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_queue_t highQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_group_async(group, defaultQueue, ^{
sleep(2);
NSLog(@"任务一");
});
dispatch_group_async(group, defaultQueue, ^{
sleep(2);
NSLog(@"任务二");
});
dispatch_group_async(group, highQueue, ^{
NSLog(@"高优先级任务");
});

dispatch_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"dispatch group执行完成");
});
NSLog(@"任务执行完成");
}

执行后:

1
2
3
4
5
2019-01-01 10:28:40.864817+0800 XXX[12801:1454449] 任务执行完成
2019-01-01 10:28:40.864821+0800 XXX[12801:1454557] 高优先级任务
2019-01-01 10:28:42.865759+0800 XXX[12801:1454555] 任务二
2019-01-01 10:28:42.865760+0800 XXX[12801:1454558] 任务一
2019-01-01 10:28:42.866070+0800 XXX[12801:1454449] dispatch group执行完成

dispatch_notify函数提供的这种获取dispatch group内任务完成后的通知,然后再异步执行处理很常用。

如果对同步、异步理解的不够深刻的话,使用dispatch_group_async函数的时候可能会导致一个错误的认识。比如在dispatch_group_async中添加几个网络请求的任务,会发现请求还没完成就执行了dispatch_notify回调。这是因为发起网络请求本身就是一个任务,在把这个任务通过block追加到dispatch_group_async中的队列后就算完成了,并不会等待网络请求的完成,因为网络请求本身也是异步的。所以在遇到这种异步操作情况的时候,可以用另外一对函数将来判定任务完成情况:dispatch_group_enter(添加到group中)、dispatch_group_leave(执行完成退出group)。

这两个函数必须同时存在,类似于引用计数,这是对group内的任务的递增和递减,如果只有递增没有递减,那么group的任务就会永远执行不完,也就一直不会回调dispatch_notify函数,或者dispatch_wait函数一直不会返回。

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
- (void)dispathGroup {
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t defaultQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_queue_t highQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);

dispatch_group_enter(group);
dispatch_async(defaultQueue, ^{
sleep(2);
NSLog(@"任务一");
dispatch_group_leave(group);
});

dispatch_group_enter(group);
dispatch_async(defaultQueue, ^{
sleep(2);
NSLog(@"任务二");
dispatch_group_leave(group);
});

dispatch_group_enter(group);
dispatch_async(highQueue, ^{
NSLog(@"高优先级任务");
dispatch_group_leave(group);
});

dispatch_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"dispatch group执行完成");
});
NSLog(@"任务执行完成");
}

dispatch_semaphore_t

dispatch_semaphore_t 信号量一共就三个函数,用起来比较简单,只是需要结合不同的场景去理解才能发挥很大的用处:

  • dispatch_semaphore_create : 创建一个信号量,并指定信号的初始化个数
  • dispatch_semaphore_wait:锁住当前的线程,等待信号的计数大于等于1,然后将计数减去1,并且该函数返回。如果信号是0,则该函数一直不返回,阻塞当前的线程。
  • dispatch_semaphore_signal:释放信号,让信号计数加1

在很多开源库中都使用dispatch_semaphore_t作为锁来处理多线程时对数据写入的保护。例如下面示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
- (void)dispatchSemaphore {
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
NSMutableArray *array = [NSMutableArray array];
for (int i = 0; i < 1000; i++) {
dispatch_async(queue, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
[array addObject:[NSNumber numberWithInt:i]];
dispatch_semaphore_signal(semaphore);
});
}
}

在一个异步并发线程中,对数组进行添加元素的操作。由于会有多个线程同时对array进行写入操作,如果不加dispatch_semaphore_t则很可能会导致内存访问错误导致程序终止。

加了dispatch_semaphore_t后,将信号的计数指定为1,每当执行一次dispatch_semaphore_wait函数后,信号就会减1,此时信号为0,则阻塞线程无法执行下面的[array addObject:[NSNumber numberWithInt:i]];方法,直到前面的线程执行完对数组的操作然后执行dispatch_semaphore_signal函数使信号加1,当前的线程才能访问数组,这样一来,每次对数组的访问操作都只能有一个线程,就保护了数据访问的安全。当然对本例也可以用其他方法来处理,例如指定一个串行队列。

通过dispatch_semaphore_t信号量和dispatch_group_t的组合,也能实现代替上面例子中使用dispatch_group_enterdispatch_group_leave的效果。

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
- (void)dispatchSemaphoreGroup {
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t defaultQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_group_async(group, defaultQueue, ^{
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_async(defaultQueue, ^{
sleep(2);
NSLog(@"任务一");
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
});

dispatch_group_async(group, defaultQueue, ^{
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_async(defaultQueue, ^{
sleep(2);
NSLog(@"任务二");
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
});

dispatch_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"dispatch group执行完成");
});
NSLog(@"任务执行完成");
}

在上面的代码中,每次追加block中的任务到group的时候,都先创建一个信号计数为0的信号量,然后开启一个block内部的异步任务的执行,在block的最后使用dispatch_semaphore_wait锁住当前的线程,让当前的block无法返回,则group的任务就一直无法完成。直到block内部的这个异步任务执行完成后释放信号,通过dispatch_semaphore_signal让信号加1,则block返回,group内的任务完成,再回调dispatch_notify函数。

信号量在网络请求的同步处理,资源竞争等情况下可以发挥很大的用处,可以使用信号量来解决这些问题。

NSOperation && NSOperationQueue

相对于GCD都是纯C的函数,NSOperation提供了一个更高层面面向对象的多线程处理方案。NSOperation是用来封装任务的一个类。我们把需要执行的任务封装进NSOperation实例当中,通过调用start方法它会自动去执行这些任务。它也提供了很多种的状态便于我们观测了解当前任务的执行状态,例如:isCancelledisReadyisExecutingisFinished等。NSOperation是一个抽象类,我们不能直接使用NSOperation类的去处理任务,系统提供了两个子类NSInvocationOperationNSBlockOperation方便我们使用,也可以自己创建一个NSOperation的子类去实现。

NSInvocationOperation提供了一种target - action模式响应的处理任务类,通过指定target,去对应执行action操作。例如:

1
2
3
4
5
6
7
8
- (void)invocationOperationAction {
NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(invocationOperationAction:) object:@"zzy"];
[operation start];
}

- (void)invocationOperationAction:(id)userInfo {
NSLog(@"userInfo = %@", userInfo);
}

NSBlockOperation则提供了block的方式去处理任务,通过把任务封装进block块中对应去执行。

1
2
3
4
NSBlockOperation *operaion = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"operation1");
}];
[operation start];

还可以给operation中添加多个任务:

1
2
3
4
5
6
7
[operaion addExecutionBlock:^{
NSLog(@"operation2");
}];
[operaion addExecutionBlock:^{
NSLog(@"operation3");
}];
[operation start];

只有当这些添加的Block中的任务都执行完,这个operation才算执行完成。

需要注意的是,如果通过手动执行NSOperation的start方法去启动任务的话,start方法是同步的。NSOperation提供了一个只读属性isAsynchronous(也可以通过concurrent)来标识当前NSOperation是否是异步执行,默认这个属性是NO。如同上面关于同步的解释,start方法会阻塞当前调用它的线程,直到operation的操作都完成。看下例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (void)blockOperationThread {
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(synchronousOperation) object:nil];
[thread start];
}

- (void)synchronousOperation {
NSBlockOperation *operaion = [NSBlockOperation blockOperationWithBlock:^{
sleep(2);
NSLog(@"operationBlock1");
NSLog(@"operation1 Thread = %@", [NSThread currentThread]);
}];
[operaion addExecutionBlock:^{
NSLog(@"operationBlock2");
NSLog(@"operation2 Thread = %@", [NSThread currentThread]);
}];
[operaion start];
NSLog(@"synchronousOperation....");
}

打印如下:

1
2
3
4
5
2019-01-01 11:17:30.504408+0800 XXX[11341:1269966] operationBlock2
2019-01-01 11:17:30.504966+0800 XXX[11341:1269966] operation2 Thread = <NSThread: 0x60400046be80>{number = 4, name = (null)}
2019-01-01 11:17:32.506224+0800 XXX[11341:1269987] operationBlock1
2019-01-01 11:17:32.506513+0800 XXX[11341:1269987] operation1 Thread = <NSThread: 0x600000474dc0>{number = 3, name = (null)}
2019-01-01 11:17:32.506980+0800 XXX[11341:1269987] synchronousOperation....

可以看到直到两秒之后才执行NSLog(@"synchronousOperation....");也就是说NSOperation的执行是同步的。同时我们通过打印执行NSOperation任务的线程发现,同一个operaion内添加的多个任务,执行这些任务的线程并不是同一个。也就是说虽然NSOperation是同步的,但是operaion内部添加的任务可能是在不同线程中并发执行的,这些任务执行完成后才会向下执行,有点类似GCD的dispatch_group

那么如何实现异步执行的NSOperation呢?可以通过两种方式:一种是通过创建NSOperation子类自己去实现异步操作,另外一种是通过NSOperationQueue。我们平时用的比较多的是通过使用NSOperationQueue来实现异步处理NSOperation。

NSOperationQueue

通过NSOperationQueue来实现NSOperation的执行比较简单,直接将NSOperation放进NSOperationQueue的队列中就可以了。NSOperationQueue会自动为NSOperation创建线程并且调用它的start方法启动任务。当然NSOperation添加到NSOperationQueue中后也不一定会立刻被执行,如果NSOperationQueue中有很多个operation,当前的operation会在队列排到它以后自动执行。

通过NSOperationQueue来执行operation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//将上面的代码改为NSOperationQueue来执行
- (void)synchronousOperation {
NSBlockOperation *operaion = [NSBlockOperation blockOperationWithBlock:^{
sleep(2);
NSLog(@"operationBlock1");
NSLog(@"operation1 Thread = %@", [NSThread currentThread]);
}];
[operaion addExecutionBlock:^{
NSLog(@"operationBlock2");
NSLog(@"operation2 Thread = %@", [NSThread currentThread]);
}];

// [operaion start];

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:operaion];
NSLog(@"synchronousOperation");
}

打印如下:

1
2
3
4
5
2019-01-01 11:42:41.789280+0800 XXX[11463:1298262] synchronousOperation
2019-01-01 11:42:41.789611+0800 XXX[11463:1298233] operationBlock2
2019-01-01 11:42:41.791127+0800 XXX[11463:1298233] operation2 Thread = <NSThread: 0x600000465680>{number = 4, name = (null)}
2019-01-01 11:42:43.790340+0800 XXX[11463:1298241] operationBlock1
2019-01-01 11:42:43.790851+0800 XXX[11463:1298241] operation1 Thread = <NSThread: 0x60400066a440>{number = 5, name = (null)}

可以看到operation不会阻塞当前的线程,会自动的异步去执行任务。

自定义NSOperation

通过自定义NSOperation可以实现更多的定制化任务处理,例如异步执行NSOperation。自定义NSOperation要实现以下几个方法:

  • start :在start方法中实现异步处理的操作,不论是调用异步处理函数还是说开启新的线程。而且要及时通过KVO更新operation当前的状态:isExecutingisFinished,以便于通知它的监测者。
  • main 官方文档中建议在这个方法中实现执行任务的操作。当然也可以不实现这个方法,直接在start中实现处理任务也可以。SDWebImage中的自定义operation就只实现了start方法。
  • isConcurrent是否是并发,返回YES即可。
  • isExecuting:是否正在执行
  • isFinished 是否完成,包括任务完成和取消,取消也是Finished

我自己没有实现过自定义NSOperation,之前看SDWebImage的时候看过里面自定义SDWebImageDownloaderOperation类实现了异步处理图片下载请求,写了很多东西,要控制各种状态,并自行手动实现KVO(因为以上几种状态都是readonly),感兴趣的可以参考SDWebImage

添加任务依赖

NSOperation可以指定任务间的依赖,一个任务A可以依赖于另外一个任务B,任务B没有执行完成,任务A不会开始执行。这里要注意的是,任务B不一定非要是执行成功,因为取消也算是完成isFinished

1
[operation2 addDependency:operaion1];

添加依赖一定要在operation 执行start方法或者添加到NSOperationQueue前,否则无效。

而且这两个operation不一定非要是在同一个NSOperationQueue中,不同的NSOperationQueue中的operation也可以指定依赖。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (void)blockOperationAction {
NSBlockOperation *operaion1 = [NSBlockOperation blockOperationWithBlock:^{
sleep(2);
NSLog(@"operation1");
NSLog(@"operation1 Thread = %@", [NSThread currentThread]);
}];

NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"operation2");
NSLog(@"operation2 Thread = %@", [NSThread currentThread]);
}];

[operation2 addDependency:operaion1];

NSOperationQueue *queue1 = [[NSOperationQueue alloc] init];
[queue1 addOperation:operaion1];

NSOperationQueue *queue2 = [[NSOperationQueue alloc] init];
[queue2 addOperation:operation2];
}

打印如下:

1
2
3
4
2019-01-01 12:29:44.008728+0800 XXX[11812:1350215] operation1
2019-01-01 12:29:44.009073+0800 XXX[11812:1350215] operation1 Thread = <NSThread: 0x60400047ca00>{number = 3, name = (null)}
2019-01-01 12:29:44.009623+0800 XXX[11812:1350214] operation2
2019-01-01 12:29:44.009877+0800 XXX[11812:1350214] operation2 Thread = <NSThread: 0x60400047cac0>{number = 4, name = (null)}

NSOperation还有很多功能:

  • 可以通过queuePriority属性指定在队列中的优先级;
  • 可以通过setCompletionBlock:方法设置任务完成的回调;
  • 可以手动cancel取消掉一个任务;也可以通过调用NSOperationQueuecancelAllOperations方法取消队列中所有的任务。
  • 可以通过NSOperationQueuesuspended来暂停队列中的任务。不过它只是暂停operation queue调度新的任务,并不会暂停正在执行的任务。
  • 还可以通过NSOperationQueuemaxConcurrentOperationCount属性来设置operation queue中任务的最大并发数。如果设置为1那就是串行执行。但是这里的串行执行顺序并不一定,要看当然operation的优先级和isReady的状态。

NSOperation相对于GCD来说,提供的对于任务的控制很丰富很灵活,可以做很多事情。

Objective-C-(三)Block实现解析

最近研究了下Block的实现代码,解惑了以前一直好奇的Block捕获外部变量,__block,Block回调等特性,在此记录下Block的实现原理。

最简单的没有变量捕获的block

准备工作:在工程中创建了一个Block.c的文件,在里面写了一个名为blockMain的函数,实现了一个简单的没有变量捕获的block:

1
2
3
4
5
6
7
8
void blockMain() {

void(^MyBlock)(void) = ^{
printf("block test");
};
MyBlock();

}

在这个文件目录下执行clang -rewrite-objc Block.c,会生成一个.cpp文件,打开文件就可以看到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
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};

struct __blockMain_block_impl_0 {
struct __block_impl impl;
struct __blockMain_block_desc_0* Desc;
__blockMain_block_impl_0(void *fp, struct __blockMain_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

static void __blockMain_block_func_0(struct __blockMain_block_impl_0 *__cself) {
printf("block test");
}

static struct __blockMain_block_desc_0 {
size_t reserved;
size_t Block_size;
} __blockMain_block_desc_0_DATA = { 0, sizeof(struct __blockMain_block_impl_0)};

void blockMain() {
void(*MyBlock)(void) = ((void (*)())&__blockMain_block_impl_0((void *)__blockMain_block_func_0, &__blockMain_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)MyBlock)->FuncPtr)((__block_impl *)MyBlock);

}

可以看到这个没有捕获变量的Block的底层实现主要是有三个结构体和一个函数。

__blockMain_block_impl_0就是我们在上面blockMain函数中写的MyBlock的底层实现,是一个结构体。这个结构体的命名是以block所在的函数名为开头,block在函数中出现的顺序为结尾,拼上_block_impl_组成,也即:__blockMain + _block_impl_ +0,(下面的函数名和结构体名称也是这样命名的)。这个结构体是由两个结构体成员变量struct __block_impl implstruct __blockMain_block_desc_0* Desc和一个构造函数__blockMain_block_impl_0所组成:

__block_impl impl

1
2
3
4
5
6
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
  • isa:isa指针,表明block也是一个对象,指向block所属的类型 (_NSConcreteGlobalBlock, _NSConcreteStackBlock, _NSConcreteMallocBlock)
    • _NSConcreteGlobalBlock:全局静态block,不会访问任何外部变量
    • _NSConcreteStackBlock:栈区block,当出了函数作用域后被销毁
    • _NSConcreteMallocBlock:堆去block,当引用计数为0时被销毁
  • Flags: 标志位,表示一些block的附加信息
  • Reserved:保留变量
  • FuncPtr:指向block实现函数的函数指针(block回调就是通过它进行回调的)

__blockMain_block_desc_0

1
2
3
4
static struct __blockMain_block_desc_0 {
size_t reserved;
size_t Block_size;
} __blockMain_block_desc_0_DATA = { 0, sizeof(struct __blockMain_block_impl_0)};
  • reserved:保留字段
  • Block_size:block的大小

并且初始化一个__blockMain_block_desc_0_DATA的结构体实例,后面给block赋值的时候直接传入。

__blockMain_block_func_0

1
2
3
static void __blockMain_block_func_0(struct __blockMain_block_impl_0 *__cself) {
printf("block test");
}

这是block的实现函数,在block的构造函数中被赋值给了FuncPtr函数指针。

__blockMain_block_func_0 函数中有一个__cself的参数,这个函数是结构体指针,指向的就是block自身的结构体实例。也即:在block执行的时候,block会将自身结构体当做参数传入执行函数,这也是为什么在执行block的时候,能够将block捕获的外部变量读取出来的原因:因为__cself指向的就是block结构体实例,而block结构体中追加了捕获的外部变量,所以就可以通过__cself获取到捕获的变量。这个例子没有用到__cself,下面会介绍。

block的赋值和执行:

1
2
3
4
5
6
void blockMain() {

void(*MyBlock)(void) = ((void (*)())&__blockMain_block_impl_0((void *)__blockMain_block_func_0, &__blockMain_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)MyBlock)->FuncPtr)((__block_impl *)MyBlock);

}

看到这些C++(其实是纯C)结构体和函数之间的互相转换时我是有些懵逼的,后来请教了一个C++比较好的同学,给出了如下解释:对于C语言来说,可以从内存层面重新解释内容,所以任何类型转换都可以转换,只要转换后在寻找函数或者变量的时候能够成功寻址就行。

所以对于上面的赋值和执行去掉类型转换,可以简单理解为如下:

1
2
3
4
5
 //创建一个__blockMain_block_impl_0类型的结构体,并且赋值给并且赋值给MyBlock结构体指针结构体指针
struct __blockMain_block_impl_0 *MyBlock = &__blockMain_block_impl_0((void *)__blockMain_block_func_0, &__blockMain_block_desc_0_DATA));

//调用 MyBlock
*MyBlock->impl.FuncPtr(MyBlock)

所以上面就很好理解了,第一个方法给block赋值的时候,通过block的赋值函数,第一个参数传入block的实现函数__blockMain_block_func_0,赋值给block内部的void *FuncPtr函数指针,让FuncPtr指向block的实现函数。第二个参数传入__blockMain_block_desc_0_DATA结构体实例,赋值给block内部的Desc成员变量,然后将block结构体实例的指针返回给*MyBlock保存。

当执行block的时候,通过*MyBlock结构体指针获取到FuncPtr函数指针,然后执行block的实现函数。

上面的block实现比较简单,没有捕获任何的变量,下面来看看捕获变量的block是什么样子的。

捕获外部变量可以根据外部变量的类型分为四种情况:

  • 基本类型的变量(int,float)
  • 静态全局变量
  • 静态局部变量
  • 对象类型

捕获基本类型变量的block

1
2
3
4
5
6
7
8
9
10
11
void blockMain() {

int a = 1;
int b = 2;
void(^MyBlock)(void) = ^
int c = a + b;
printf("%d", c);
};
MyBlock();

}

clang rewrite 转换之后代码如下:

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
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};

struct __blockMain_block_impl_0 {
struct __block_impl impl;
struct __blockMain_block_desc_0* Desc;
int a;
int b;
__blockMain_block_impl_0(void *fp, struct __blockMain_block_desc_0 *desc, int _a, int _b, int flags=0) : a(_a), b(_b) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __blockMain_block_func_0(struct __blockMain_block_impl_0 *__cself) {
int a = __cself->a; // bound by copy
int b = __cself->b; // bound by copy

int c = a + b;
printf("%d", c);
}

static struct __blockMain_block_desc_0 {
size_t reserved;
size_t Block_size;
} __blockMain_block_desc_0_DATA = { 0, sizeof(struct __blockMain_block_impl_0)};

void blockMain() {

int a = 1;
int b = 2;
void(*MyBlock)(void) = ((void (*)())&__blockMain_block_impl_0((void *)__blockMain_block_func_0, &__blockMain_block_desc_0_DATA, a, b));
((void (*)(__block_impl *))((__block_impl *)MyBlock)->FuncPtr)((__block_impl *)MyBlock);

}

跟上面的没有捕获外部变量的block相比,这个block的结构体内部多了两个成员变量int aint b,类型和名称跟外部的变量一模一样,然后在对block结构体赋值的时候,将捕获的这两个外部变量作为参数传入block的赋值函数,通过block的赋值函数将这两个成员变量进行了赋值。

可以看到,block捕获的外部变量其实是内部追加了跟外部变量同样的成员变量,然后对这些成员变量进行赋值,然后在__blockMain_block_func_0执行函数中,通过__cself(上面有介绍过这个参数的作用)读取出捕获的成员变量。所以block捕获外部的基本变量是一个值拷贝的过程,即使在block内部修改了这个变量的值,也不会影响外部的变量,修改了也没用,所以当我们对这个捕获的变量进行重新赋值的时候编译器会直接报错提醒我们。

捕获全局变量和静态局部变量的block

分别定义几种不同类型的变量,关于这几种变量的区别就不介绍了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static int a = 1;	//静态全局变量 		
int b = 2; //全局变量

void blockMain() {

static int c = 3; //静态局部变量
int d = 4; //自动变量

void(^MyBlock)(void) = ^{
a = 100; //静态全局变量,全局变量静,静态局部变量都可以在block内部修改
b = 100;
c = 100;
int e = a + b + c + d;
printf("%d", e);
};
MyBlock();

}

转换如下:

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
static int a = 1;  
int b = 2;

struct __blockMain_block_impl_0 {
struct __block_impl impl;
struct __blockMain_block_desc_0* Desc;
int *c;
int d;
__blockMain_block_impl_0(void *fp, struct __blockMain_block_desc_0 *desc, int *_c, int _d, int flags=0) : c(_c), d(_d) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __blockMain_block_func_0(struct __blockMain_block_impl_0 *__cself) {
int *c = __cself->c; // bound by copy
int d = __cself->d; // bound by copy

a = 100;
b = 100;
(*c) = 100;
int e = a + b + (*c) + d;
printf("%d", e);
}

static struct __blockMain_block_desc_0 {
size_t reserved;
size_t Block_size;
} __blockMain_block_desc_0_DATA = { 0, sizeof(struct __blockMain_block_impl_0)};

void blockMain() {

static int c = 3;
int d = 4;

void(*MyBlock)(void) = ((void (*)())&__blockMain_block_impl_0((void *)__blockMain_block_func_0, &__blockMain_block_desc_0_DATA, &c, d));
((void (*)(__block_impl *))((__block_impl *)MyBlock)->FuncPtr)((__block_impl *)MyBlock);

}

可以看到,首先静态变量和全局变量都是声明在函数外部的,作用域本来就是全局的,所以在block内部可以直接使用而且可以修改。

主要是看静态变量static int c,block在捕获这个静态变量的时候,如同普通基本类型的变量一样,被追加到block内部保存,不同的是,block内部保存的不是静态变量的值,而是静态变量的指针int *c;,在赋值的时候如下:

1
2
3
4
5
//结构体构造函数赋值
void(*MyBlock)(void) = ((void (*)())&__blockMain_block_impl_0((void *)__blockMain_block_func_0, &__blockMain_block_desc_0_DATA, &c, d));

//也即简单理解为:
int *c = &c

也即,block捕获的是静态变量的指针,所以在block内部不仅可以访问到这个静态变量,还可以进行修改,不需要加__block。

捕获对象类型的block

1
2
3
4
5
6
7
	NSObject *obj = [NSObject new];
__strong NSObject *obj1 = obj;
void(^MyBlock)(void) = ^{
NSLog(@"obj = %@", obj1);
};
MyBlock();
}

转换如下:

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
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};

struct __blockMain_block_impl_0 {
struct __block_impl impl;
struct __blockMain_block_desc_0* Desc;
NSObject *obj1;
__blockMain_block_impl_0(void *fp, struct __blockMain_block_desc_0 *desc, NSObject *_obj1, int flags=0) : obj1(_obj1) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

static void __blockMain_block_func_0(struct __blockMain_block_impl_0 *__cself) {
NSObject *obj1 = __cself->obj1; // bound by copy

NSLog((NSString *)&__NSConstantStringImpl__var_folders__s_s_d05ccd4qv0y698mt2mcqp80000gn_T_MallockBlock_7ea9ad_mi_0, obj1);
}

static void __blockMain_block_copy_0(struct __blockMain_block_impl_0*dst, struct __blockMain_block_impl_0*src) {_Block_object_assign((void*)&dst->obj1, (void*)src->obj1, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static void __blockMain_block_dispose_0(struct __blockMain_block_impl_0*src) {_Block_object_dispose((void*)src->obj1, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static struct __blockMain_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __blockMain_block_impl_0*, struct __blockMain_block_impl_0*);
void (*dispose)(struct __blockMain_block_impl_0*);
} __blockMain_block_desc_0_DATA = { 0, sizeof(struct __blockMain_block_impl_0), __blockMain_block_copy_0, __blockMain_block_dispose_0};

void blockMain() {

NSObject *obj = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("new"));
__attribute__((objc_ownership(strong))) NSObject *obj1 = obj;
void(*MyBlock)(void) = ((void (*)())&__blockMain_block_impl_0((void *)__blockMain_block_func_0, &__blockMain_block_desc_0_DATA, obj1, 570425344));
((void (*)(__block_impl *))((__block_impl *)MyBlock)->FuncPtr)((__block_impl *)MyBlock);

}

可以看到block的结构跟之前的差不多,只是多了一个__blockMain_block_copy_0函数和__blockMain_block_dispose_0函数。这两个函数是负责管理block捕获的对象的引用计数的,内部分别调用了_Block_object_assign函数,_Block_object_dispose函数。

_Block_object_assign函数相当于调用了retain方法,将捕获的对象赋值给block结构体内部的对象类型的成员变量(NSObject *obj1)。而_Block_object_dispose函数相当于调用了release方法,将赋值在block结构体内部成员变量的对象释放。

__blockMain_block_copy_0是在block从栈区拷贝到堆区的时候被调用,同时会retain它内部捕获的对象,__blockMain_block_dispose_0是在block被销毁时调用,同时会release它内部捕获的对象。

如同block捕获基本类型变量一样,对于对象类型的变量,block内部是追加一个同样内存修饰符的变量指向捕获的对象。具体来讲就是,如果外部是__strong NSObject *obj1 = obj;,block内部的成员变量就是__strong NSObject *obj1;,如果外部是__weak NSObject *obj1 = obj;,block内部的成员变量就是__weak NSObject *obj1;。所以block是否强引用捕获的对象,取决于捕获的这个变量的内存语义修饰符。这也是为什么通过__weak可以解决block循环引用的根本原因。

之前我在想,为什么这里不是直接使用对象的引用,这样的话就可以在block内部即能使用对象又可以修改对象。后来想了下,如果这样的话,就不能retain外部的对象了,这样的话对象的引用计数不会加1,那么如果对象出了当前方法的作用域后可能就会被释放了,这样是没有意义的。

__block

前面介绍了block的结构以及block是如何捕获外部变量值。由于block捕获的外部变量值不能修改,所以OC提供了__block修饰符让我们能够修改外部变量值。来看下__block是如何实现改变外部变量的。

1
2
3
4
5
6
7
8
9
10
11
12
13
void blockMain() {

__block int a = 1;
void(^MyBlock)(void) = ^{
a = 10;
printf("%d", a);
};
MyBlock();

a = 2;
printf("%d", a);

}

转换后:

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
// __block结构体
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding; //指向自身结构体实例的指针
int __flags;
int __size;
int a; //捕获的外部变量
};

struct __blockMain_block_impl_0 {
struct __block_impl impl;
struct __blockMain_block_desc_0* Desc;
__Block_byref_a_0 *a; // by ref
__blockMain_block_impl_0(void *fp, struct __blockMain_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

static void __blockMain_block_func_0(struct __blockMain_block_impl_0 *__cself) {
__Block_byref_a_0 *a = __cself->a; // bound by ref
//通过__forwarding找到自身实例,然后改变内部的变量a
(a->__forwarding->a) = 10;
printf("%d", (a->__forwarding->a));
}

static void __blockMain_block_copy_0(struct __blockMain_block_impl_0*dst, struct __blockMain_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __blockMain_block_dispose_0(struct __blockMain_block_impl_0*src) {_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}

static struct __blockMain_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __blockMain_block_impl_0*, struct __blockMain_block_impl_0*);
void (*dispose)(struct __blockMain_block_impl_0*);
} __blockMain_block_desc_0_DATA = { 0, sizeof(struct __blockMain_block_impl_0), __blockMain_block_copy_0, __blockMain_block_dispose_0};

void blockMain() {

__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 1};
void(*MyBlock)(void) = ((void (*)())&__blockMain_block_impl_0((void *)__blockMain_block_func_0, &__blockMain_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
((void (*)(__block_impl *))((__block_impl *)MyBlock)->FuncPtr)((__block_impl *)MyBlock);

(a.__forwarding->a) = 2;
printf("%d", (a.__forwarding->a));
}

代码跟之前的block略有不同,新增了一个__Block_byref_a_0的结构体和两个管理__block变量引用的函数__blockMain_block_copy_0__blockMain_block_desc_0作用类似于上一篇提到的block捕获对象类型变量,管理对象的引用计数)。可以看到我们声明的__block变量就变成了这样的结构体实例。byref 其实字面意思就是通过引用。同时block结构体内部会持有这样一个结构体指针指向这个__block变量结构体实例。

1
2
3
4
5
//通过 __block 声明,将之前的变量 int a 变成了 __Block_byref_a_0 a 的结构体实例
__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 1};

//将这个__block结构体实例指针赋值给block内部的 __Block_byref_a_0 *a 所持有
void(*MyBlock)(void) = ((void (*)())&__blockMain_block_impl_0((void *)__blockMain_block_func_0, &__blockMain_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));

所以我们用的__block修饰符原来是将变量变成了类似block结构体的结构体。

1
2
3
4
5
6
7
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};

这个结构体通过追加一个int a的成员变量保留了之前的变量 int a 的值(类似于block捕获外部变量)。然后还有一个__forwarding指针,这个指针是实现block能够更改捕获的外部变量值的核心。通过这个指针的定义和赋值可以看出来它指向的是自身结构体实例。为什么要这么做呢?

如果__block变量和block都是在栈区,那么__forwarding指针这个时候指向的是栈区的__block结构体实例,通过__forwarding指针获取到__block结构体变量,然后再获取到__block结构体变量内部的变量int a,这个时候就可以读取并且改变int a了。

但是由于栈区的变量在函数返回时就会被释放,所以如果我们想在超出变量作用域后继续使用block,会通过对block发送copy消息,将其拷贝到堆区进行持有。在block被拷贝到堆区的时候,内部持有的__block变量(例如此例中block结构体内部的struct __blockMain_block_desc_0* Desc;)也会被一并拷贝到堆区。那么这个时候在函数返回前__block结构体变量就会有两份,一份在栈区,一份是堆区。这个时候__block结构体变量内部的__forwarding指针就会指向堆区的自己,所以这个时候不论是在函数内部还是在超出函数作用域之外都能正确访问到修改后的__block变量值(即:内部的int a变量)。所以__forwarding指针的作用就是不论__block是在栈区还是堆区都能正确的访问到自身。

block的实现函数:

1
2
3
4
5
6
static void __blockMain_block_func_0(struct __blockMain_block_impl_0 *__cself) {
__Block_byref_a_0 *a = __cself->a; // bound by ref

(a->__forwarding->a) = 10;
printf("%d", (a->__forwarding->a));
}

这里通过__cself->a获取到__block变量结构体实例,然后通过 __block内部的__forwarding指针访问自身实例,并取出变量a 进行赋值,这样就实现了修改外部变量的目的。

block的实现大概就是这些,到这里应该就能弄清楚block到底是怎么实现的了。

Objective-C-(二)内存管理

由于Objective-C是基于C语言的,在了解Objective-C内存管理前应该先了解下C语言的内存模型。

简单回顾下C程序的占用空间的几个区域:

  • 程序代码区:存放程序执行代码的区域
  • 静态数据区:也称全局数据区,存放程序中的全局变量。例如:全局变量,静态变量,一般常量,字符串常量。静态数据区的内存是由程序终止时由系统自动释放。其中静态数据区具体又分为两块区域:
    • BSS段(Block Started by Symbol):未初始化的全局变量
    • 数据段(data segment):已初始化的全局变量
  • 堆区:由程序员手动管理分配和释放。通过malloc()callocfree()等函数操作的就是堆区的内存。
  • 栈区:函数的参数,局部变量等存放在栈区。栈区的内存由系统自动分配和释放。

在Objective-C中创建的对象都分配在堆区,内存管理针对的也是这块区域。

Objective-C内存管理的核心其实引用计数。系统通过对一个对象引用计数的计算来确认是否要释放对象。每个对象都有一个引用计数器,可以递增或者递减对象的引用计数,当引用计数大于0时,对象存活,反之当引用计数为0时,对象会被释放。Objective-C有两种内存管理机制:手动管理(MRC)和自动管理(ARC)。ARC的原理其实跟MRC是一致的,只是系统自动帮我们在合适的地方键入了内存管理的方法,避免了手动管理带来了麻烦和失误。目前基本上开发用的都是ARC。最开始学习iOS的时候也用过MRC,先介绍下MRC的机制。

MRC

操作对象的四种方式:

  • 生成并持有对象:alloc/new/copy/mutableCopy等, retainCount :+1
  • 持有对象:retain,retainCount :+1
  • 释放对象:release,retainCount :-1
  • 废弃对象:dealloc, 自动释放内存

内存管理的四个法则:

  • 自己生成的对象,自己持有
  • 非自己生成的对象,自己也能持有
  • 不再需要自己持有对象的时候释放对象
  • 非自己持有的对象无法释放

示例代码:

自己生成的对象,自己持有:

1
2
3
4
/**
* 以 alloc/new/copy/mutableCopy 等方法创建的对象归调用者持有
*/
id obj = [[NSObject alloc] init]; //创建一个NSObject对象返回给变量obj, 并且归调用者持有

非自己生成的对象,自己也能持有:

1
2
3
4
5
/**
* alloc/new/copy/mutableCopy 等方法以外的方式创建的对象不归调用者持有
*/
id obj = [NSMutableArray array]; // 非自己生成的对象,该对象存在,但不归调用者持有
[obj retain]; // 如果想持有该对象,需要执行retain方法

非自己生成的对象,且该对象存在是通过autorelease来实现的。autorelease提供了一种使得对象在超出生命周期后能正确的被释放(通过调用release方法)机制,以便于将对象返回给调用者,让调用者持有后再释放对象。否则对象还没来得及被调用者持有就被系统释放了。调用autorelease后对象不会立刻被释放,而是被注册到autoreleasepool中,然后当autoreleasepool结束被销毁的时候,才会调用对象的release方法释放对象。

不再需要自己持有对象的时候释放对象:

1
2
id obj = [[NSObject alloc] init];
[obj release]; // 释放自己生成并持有的对象

非自己持有的对象无法释放:

1
2
3
4
5
id obj = [NSMutableArray array]; 
[obj release]; //由于当前的调用者并不持有改对象,不能进行释放操作,否则导致程序崩溃。如果要释放该对象,需要先对对象进行retain操作。
/**
以上方法在Xcode9中经测试发现如果返回给obj的是NSMutableArray对象,会导致程序崩溃,但是如果是NSArray就不会。
*/

MRC下要注意属性的引用计数情况。虽然retainCount在获取引用计数的时候有时候不准确,但是也可以用来调试参考。例如我们给一个属性赋值如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
@interface MemoryRefenceVC ()
@property (nonatomic, copy) NSArray *array;
@end

@implementation MemoryRefenceVC

- (void)viewDidLoad {
[super viewDidLoad];

self.array = [[NSArray alloc] initWithObjects:@1, nil];
NSLog(@"array.retainCount = %ld", _array.retainCount);
}
@end

打印如下:

1
2018-12-12 17:25:57.607777+0800 XXX[9889:341414] array.retainCount = 2

我们创建了一个对象并且返回给调用者持有,为什么此时对象的引用计数是2呢?

因为在属性的赋值setter方法中,会对当前的对象多进行一次引用。

1
2
3
4
- (void)setArray:(NSArray *)array {
[array retain]; //进行了一次retain操作
_array = array;
}

所以此时对象的内存引用情况是:alloc创建时retainCount为1,setter方法中retain了一次引用计数加1,所以此时retainCount变为了2。

类似于如下操作:

1
2
NSArray *temp = [[NSArray alloc] initWithObjects:@1, nil]; 引用计数+1
self.array = temp; 引用计数+1

所以一般在使用属性赋值的时候一般这么写:

1
self.array = [[[NSArray alloc] initWithObjects:@2, nil] autorelease]; //用autorelease抵消一次retain操作

或者:

1
2
3
NSArray *temp = [[NSArray alloc] initWithObjects:@1, nil]; 
self.array = temp;
[temp release];

ARC

ARC是苹果引入的一种自动管理内存的机制,实现的方式就是在编译的时候在代码合适的位置自动键入内存管理的代码。

ARC下内存管理思想跟MRC一样,同样遵守上面的四个法则。只是ARC下已经没有了上面的retainreleaseautorelease等直接操作对象内存管理的方法。ARC下Objective-C采用所有权修饰符来管理对对象的引用情况。

  • __strong :变量的默认修饰符,默认不指定的话就是__strong__strong表明了一种强引用的关系,表示当前修饰的变量持有对象,类似于MRC下的retain
  • __weak:与__strong相反,__weak表明一种弱引用的关系,表示当前修饰的变量并不会持有该对象,当对象被系统释放后,__weak变量会自动置为nil,比较安全,常用于解决循环引用的情况。
  • __unsafe_unretained:同__weak一样,该修饰符同样不会持有对象,但是不同的是,当变量指向的对象被系统释放后,变量不会自动置为nil,该指针会变为野指针,如果再次访问该变量,会导致野指针访问错误。现在很少会用到该修饰符。
  • __autoreleasing:用于修饰引用传值的参数(id *, NSObject **),类似于调用autorelease方法,在函数返回该值时会被自动释放掉。常见于NSError的传递中:例如:error:(NSError *__autoreleasing *)error,传递error变量的引用,这样的话才可以在函数内部对error进行重新赋值然后返回给调用者,同时将内部的创建的error对象注册到Autorelease Pool中稍后释放。

具体用法就不举例了,平时写代码用的都是这些修饰符,不过大部分情况用的是__strong,默认省略了这个修饰符而已。

Autorelease Pool

MRC下,我们要使用自动释放池需要手动创建NSAutoreleasepool,并且要执行对象的autorelease方法和NSAutoreleasepool的 drain方法销毁自动释放池。ARC下我们只需要使用@autoreleasepool语法就可以代替MRC下的NSAutoreleasepool。Autorelease Pool 就是提供了一种延迟给对象发送release消息的机制。当你想放弃一个对象的所有权,但是又不想这个对象立刻被释放掉,就可以使用Autorelease Pool。

ARC下使用Autorelease Pool的场景:当在循环遍历中创建大量临时对象的时候,为了避免内存峰值可以使用Autorelease Pool来避免。例如:

1
2
3
4
5
6
for (int i = 0; i < 100; i++) {
@autoreleasepool {
NSData *data = UIImageJPEGRepresentation(image, 0.7f);
UIImage *image = [UIImage imageWithData:data];
}
}

如果不使用@autoreleasepool,for循环内部创建出的大量UIImage对象需要等到循环结束时才能释放,这样会导致内存暴涨。当指定了@autoreleasepool后,每次循环结束的时候对象就会被释放掉,避免了内存峰值。

或者在方法中执行一段非常消耗资源的操作时,可以用@autoreleasepool及时释放掉资源。例如SDWebImage中对图像进行的解码预渲染操作。

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
//摘自SDWebImage
- (nullable UIImage *)sd_decompressedImageWithImage:(nullable UIImage *)image {
if (![[self class] shouldDecodeImage:image]) {
return image;
}

// autorelease the bitmap context and all vars to help system to free memory when there are memory warning.
// on iOS7, do not forget to call [[SDImageCache sharedImageCache] clearMemory];
@autoreleasepool{

CGImageRef imageRef = image.CGImage;
// device color space
CGColorSpaceRef colorspaceRef = SDCGColorSpaceGetDeviceRGB();
BOOL hasAlpha = SDCGImageRefContainsAlpha(imageRef);
// iOS display alpha info (BRGA8888/BGRX8888)
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;

size_t width = CGImageGetWidth(imageRef);
size_t height = CGImageGetHeight(imageRef);

// kCGImageAlphaNone is not supported in CGBitmapContextCreate.
// Since the original image here has no alpha info, use kCGImageAlphaNoneSkipLast
// to create bitmap graphics contexts without alpha info.
CGContextRef context = CGBitmapContextCreate(NULL,
width,
height,
kBitsPerComponent,
0,
colorspaceRef,
bitmapInfo);
if (context == NULL) {
return image;
}

// Draw the image into the context and retrieve the new bitmap image without alpha
CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
CGImageRef imageRefWithoutAlpha = CGBitmapContextCreateImage(context);
UIImage *imageWithoutAlpha = [[UIImage alloc] initWithCGImage:imageRefWithoutAlpha scale:image.scale orientation:image.imageOrientation];
CGContextRelease(context);
CGImageRelease(imageRefWithoutAlpha);

return imageWithoutAlpha;
}
}

循环引用

循环引用是指几个对象(至少两个对象)之间互相持有强引用形成了一个闭环,导致在超出对象的生命周期后谁都释放不掉的情况。

导致循环引用的可能情况:

  • 使用Block互相持有
  • NSTimer强引用Target目标对象
  • 使用delegate

解决循环引用的方法:

  • 使用弱引用weak(__weak)
  • 当持有的实例完成任务后赋值为nil

僵尸对象

最近又看了下《Effective Objective-C 2.0》,关于僵尸对象的具体实现仔细研究了下,做个笔记记录下。

僵尸对象是iOS开发中常用的内存管理调试功能。当我们给一个已经释放的对象发送消息的时候,通过僵尸对象能够很方便的了解到这个消息的相关信息,包括当前调用对象所属的类,消息名称等信息,便于我们查找问题的根源。

僵尸对象的原理:在runtime期间当一个对象被释放后,它不会真正的被系统回收,而是被转化成一个特殊的僵尸对象。这个对象所占用的内存不会被释放,当再次给这个对象发送消息时,僵尸对象会抛出异常,并且描述出当前消息的相关内容。例如:

1
*** -[__NSArrayI indexOfObject:]: message sent to deallocated instance 0x60000022b500

僵尸对象是如何实现的呢?具体来讲分以下几个步骤:首先runtime会替换掉基类NSObject的dealloc方法,在dealloc方法中进行下面步骤:

  • 获取当前的类名ClassName
  • 通过拼接_NSZombie_前缀创建一个新的僵尸类名:_NSZombie_ClassName (后缀ClassName为当前的类名)
  • 通过_NSZombie_类拷贝出一个新的类,并且类名命名为_NSZombie_ClassName
  • 销毁当前的对象,但是不释放内存(不调用free()方法)
  • 将当前对象的isa指针指向新创建的僵尸类(变更对象所属的类)

_NSZombie_类以及从其拷贝出来的新的僵尸类都没有实现任何方法,所以当给僵尸对象发送消息后,会进入消息转发流程。___forwarding___函数是实现消息转发流程的核心函数,在这个函数中先检测当前接收消息的对象所属的类名,如果类名的前缀是_NSZombie_,表明当前的消息发送的对象是僵尸对象,然后就会做特殊处理:先打印出当前消息的相关信息,然后终止程序抛出异常。

伪代码如下:

dealloc方法中创建僵尸类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (void)createNSZombie {
//获取当前对象的类名
const char *className = object_getClassName(self);
//创建新的僵尸对象类名
const char *zombieClassName = strcat("_NSZombie_", className);
//根据僵尸对象类名获取僵尸对象类(`objc_lookUpClass` 相比 `objc_getClass`,当类没有注册时不会去调用类处理回调)
Class zombieClass = objc_lookUpClass(zombieClassName);

//如果不存在,先创建僵尸对象类
if (!zombieClass) {
//获取_NSZombie_类
Class baseZombieClass = objc_lookUpClass("_NSZombie_");
//这里使用的是`objc_duplicateClass`创建新的类,`objc_duplicateClass`是直接拷贝目标类生成新的类然后赋予新的类名,新的类和_NSZombie_类结构相同,本类的父类,实例变量和方法都和复制前一样。
zombieClass = objc_duplicateClass(baseZombieClass, zombieClassName, 0);
}

//销毁对象,但是不释放对象占用的内存
objc_destructInstance(self);

//重新设置当前对象所属的类,让其指向新创建的僵尸类
object_setClass(self, zombieClass);
}

消息转发的实现,我把整个forwarding函数的实现都摘录了,顺便回顾下消息转发的流程

以下代码摘自:Objective-C 消息发送与转发机制原理

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
int __forwarding__(void *frameStackPointer, int isStret) {
id receiver = *(id *)frameStackPointer;
SEL sel = *(SEL *)(frameStackPointer + 8);
const char *selName = sel_getName(sel);
Class receiverClass = object_getClass(receiver);

// 调用 forwardingTargetForSelector:
if (class_respondsToSelector(receiverClass, @selector(forwardingTargetForSelector:))) {
id forwardingTarget = [receiver forwardingTargetForSelector:sel];
if (forwardingTarget && forwarding != receiver) {
if (isStret == 1) {
int ret;
objc_msgSend_stret(&ret,forwardingTarget, sel, ...);
return ret;
}
return objc_msgSend(forwardingTarget, sel, ...);
}
}

// 僵尸对象
const char *className = class_getName(receiverClass);
const char *zombiePrefix = "_NSZombie_";
size_t prefixLen = strlen(zombiePrefix); // 0xa
if (strncmp(className, zombiePrefix, prefixLen) == 0) {
CFLog(kCFLogLevelError,
@"*** -[%s %s]: message sent to deallocated instance %p",
className + prefixLen,
selName,
receiver);
<breakpoint-interrupt>
}

// 调用 methodSignatureForSelector 获取方法签名后再调用 forwardInvocation
if (class_respondsToSelector(receiverClass, @selector(methodSignatureForSelector:))) {
NSMethodSignature *methodSignature = [receiver methodSignatureForSelector:sel];
if (methodSignature) {
BOOL signatureIsStret = [methodSignature _frameDescriptor]->returnArgInfo.flags.isStruct;
if (signatureIsStret != isStret) {
CFLog(kCFLogLevelWarning ,
@"*** NSForwarding: warning: method signature and compiler disagree on struct-return-edness of '%s'. Signature thinks it does%s return a struct, and compiler thinks it does%s.",
selName,
signatureIsStret ? "" : not,
isStret ? "" : not);
}
if (class_respondsToSelector(receiverClass, @selector(forwardInvocation:))) {
NSInvocation *invocation = [NSInvocation _invocationWithMethodSignature:methodSignature frame:frameStackPointer];

[receiver forwardInvocation:invocation];

void *returnValue = NULL;
[invocation getReturnValue:&value];
return returnValue;
} else {
CFLog(kCFLogLevelWarning ,
@"*** NSForwarding: warning: object %p of class '%s' does not implement forwardInvocation: -- dropping message",
receiver,
className);
return 0;
}
}
}

SEL *registeredSel = sel_getUid(selName);

// selector 是否已经在 Runtime 注册过
if (sel != registeredSel) {
CFLog(kCFLogLevelWarning ,
@"*** NSForwarding: warning: selector (%p) for message '%s' does not match selector known to Objective C runtime (%p)-- abort",
sel,
selName,
registeredSel);
} // doesNotRecognizeSelector
else if (class_respondsToSelector(receiverClass,@selector(doesNotRecognizeSelector:))) {
[receiver doesNotRecognizeSelector:sel];
}
else {
CFLog(kCFLogLevelWarning ,
@"*** NSForwarding: warning: object %p of class '%s' does not implement doesNotRecognizeSelector: -- abort",
receiver,
className);
}

// The point of no return.
kill(getpid(), 9);
}

这就是整个僵尸对象的实现过程。

拓展:iOS引用计数机制和JAVA的垃圾回收机制区别:

垃圾回收机制:垃圾回收(Garbage Collection)是 Java 虚拟机(JVM)垃圾回收器提供的一种用于在空闲时间不定时回收无任何对象引用的对象占据的内存空间的一种机制。就是GC(Garbage Collection)会在程序运行时不定时的去回收那些垃圾对象释放内存,由于GC在回收时会暂停到其他线程,所以可能会导致卡顿。

垃圾回收的具体回收算法就不详细介绍了,有兴趣的可以自己查阅。

比较两种机制的区别:

  • 引用计数是在编译期插入代码,对象引用计数一旦为0就被回收。GC是在运行的时候不定时的去扫描垃圾对象进行回收,会导致卡顿。
  • 引用计数会产生循环引用导致内存泄露,GC不会,它会把那些循环引用的对象一起回收掉。
  • GC的释放时机很难控制,而引用计数释放时机比较及时,时机可控。

OC内存管理大概就是这些,要想更深入的理解,可以了解下内存管理方法是如何实现的。下一篇写下OC的内存管理的实现原理。

关于截图的注意点(renderInContext && drawViewHierarchyInRect)

截图

关于截图有两个方法:

  • - (void)renderInContext:(CGContextRef)ctx: 作用于CALayer层的方法。将view的layer渲染到当前的绘制的上下文中。
  • - (BOOL)drawViewHierarchyInRect:(CGRect)rect afterScreenUpdates:(BOOL)afterUpdates: 作用于UIView的方法。对view进行一个快照,然后将快照渲染到当前的上下文中。

renderInContext就不说了,比较容易,只需要注意下UIGraphicsBeginImageContextWithOptions(CGSize size, BOOL opaque, CGFloat scale)函数中的的size即可。

关键是- (BOOL)drawViewHierarchyInRect:(CGRect)rect afterScreenUpdates:(BOOL)afterUpdates函数中多了一个描述位置的rect参数,容易弄不清这个rectUIGraphicsBeginImageContextWithOptions(CGSize size, BOOL opaque, CGFloat scale)函数中的size的关系。之前一直没有完全理解这两个参数的区别,搜了很多资料发现都没有人讲清楚这两个参数的含义,网上来来回回都是一些雷同的代码,这次仔细测试研究了下,总结了下这两个参数的含义。

一般截图的方法可以用例如下面这段代码,对当前控制的view进行截图:

1
2
3
4
UIGraphicsBeginImageContextWithOptions(CGSizeMake(CGRectGetWidth(self.view.frame), CGRectGetHeight(self.view.frame)), YES, 0.0);
[self.view drawViewHierarchyInRect:self.view.bounds afterScreenUpdates:NO];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

先介绍下这两个函数的含义:

UIGraphicsBeginImageContextWithOptions(CGSize size, BOOL opaque, CGFloat scale) :开启一个绘图的上下文

  • size : size是指绘图上下文的宽高,可以理解为要绘制图形的画布的大小
  • opaque : 是否透明,如果传YES,画布的背景色为黑色,NO的时候,画布背景色是白色
  • scale:指的是绘制出来的图片的像素比,决定了绘图图片的清晰度,一般填0.0,默认屏幕缩放比

- (BOOL)drawViewHierarchyInRect:(CGRect)rect afterScreenUpdates:(BOOL)afterUpdates: 将要截屏的view绘制到当前的上下文中。

rect:指定图片绘制的坐标

afterUpdates:截图的瞬间是否将屏幕当前的变更渲染进去

这个方法的是UIView的方法,截图的目标对象就是当前方法的调用者。

例如[self.view drawViewHierarchyInRect:self.view.bounds afterScreenUpdates:NO]; 就是对self.view这个对象进行截图,会把self.view 当前的这个view全部截取下来然后绘制到当前的上下文中生成图片,然后按照这个方法中指定的rect 为frame,以画布为父视图绘制到画布中去。

注意:截图截取的是drawViewHierarchyInRect这个方法调用的view,而渲染出来的效果(图片的位置和大小)是由UIGraphicsBeginImageContextWithOptions(CGSize size, BOOL opaque, CGFloat scale)中的size和- (BOOL)drawViewHierarchyInRect:(CGRect)rect afterScreenUpdates:(BOOL)afterUpdates中的rect共同决定的。

例如,我们要截取下面屏幕中的照片(请忽略被变形的照片…)

代码如下:

注:

self.view是当前控制器的view

self.screenshotImgV 是上面的图片的UIImageViewself.screenshotImgV的frame是:(50, 37, 275, 489)

为了方便理解,下面用画布来指代绘制的上下文

1
2
3
4
UIGraphicsBeginImageContextWithOptions(self.view.bounds.size, 1, 0.0);
[self.screenshotImgV drawViewHierarchyInRect:self.screenshotImgV.frame afterScreenUpdates:NO];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

如上指定画布的大小为当前self.view的大小,对self.screenshotImgV进行截图,同时设置截图后的图片在画布中的位置为自身的frame。

截图效果:

由于self.view的大小是大于当前self.screenshotImgV的大小,所以对self.screenshotImgV进行截图绘制后,并不能充满整个画布,只能占据其中的一部分。而占据的位置依据的就是drawViewHierarchyInRect方法中指定的rect。这个例子中指定的是self.screenshotImgV.frame,也即:(50, 37, 275, 489) 这个frame,所以就展示如上面效果。

下面我们来调整下指定的rect, 让其为self.screenshotImgV.bounds,看看效果。

1
2
3
4
UIGraphicsBeginImageContextWithOptions(self.view.bounds.size, 1, 0.0);
[self.screenshotImgV drawViewHierarchyInRect:self.screenshotImgV.bounds afterScreenUpdates:NO];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

截图效果:

如上图,由于rect用的是self.screenshotImgV.bounds,所以截图的坐标的xy变为了(0, 0),也就是图片的起始位置变为了最左上角。

下面我们来换一下,把画布的大小设置为self.screenshotImgV的大小,然后对self.view进行截图,看看效果:

1
2
3
4
UIGraphicsBeginImageContextWithOptions(self.screenshotImgV.bounds.size, 1, 0.0);
[self.view drawViewHierarchyInRect:self.screenshotImgV.frame afterScreenUpdates:NO];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

截图效果:

如上,由于rect指定为self.screenshotImgV.frame,只能从(50, 37)这个位置开始渲染,所以在截图的时候只能截取self.view中的一部分,另外一部分超出画布范围截取不到。如果frame的坐标是从(0,0)开始效果如何呢?

1
2
3
4
UIGraphicsBeginImageContextWithOptions(self.screenshotImgV.bounds.size, 1, 0.0);
[self.view drawViewHierarchyInRect:self.screenshotImgV.bounds afterScreenUpdates:NO];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

截图效果:

发现刚好把self.view完整的渲染出来了。这是因为UIGraphicsBeginImageContextWithOptions方法中的sizedrawViewHierarchyInRect中指定的rect.size大小相同,而且rectx,y都是(0,0),所以截取的self.view从左上角起始位置开始渲染,刚好能够把整个画布充满,全部渲染出来。

通过上面的例子,应你能弄清楚UIGraphicsBeginImageContextWithOptions(CGSize size, BOOL opaque, CGFloat scale)中的size- (BOOL)drawViewHierarchyInRect:(CGRect)rect afterScreenUpdates:(BOOL)afterUpdates中的rect的意义和关系了吧。

当然对于这个例子,如果我们想截取屏幕中的图片self.screenshotImgV,只需要把尺寸都指定为self.screenshotImgVsize既可:

1
2
3
4
UIGraphicsBeginImageContextWithOptions(self.screenshotImgV.bounds.size, 1, 0.0);
[self.screenshotImgV drawViewHierarchyInRect:self.screenshotImgV.bounds afterScreenUpdates:NO];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

截图效果:

裁剪图片

裁剪图片就是对当前的图片按照指定的大小范围生成一个新的图片,方法如下:

1
2
3
4
5
6
- (UIImage *)createImageWithRect:(CGRect)rect image:(UIImage *)clipImage {
CGImageRef imageRef = CGImageCreateWithImageInRect(clipImage.CGImage, rect);
UIImage *image = [UIImage imageWithCGImage:imageRef scale:clipImage.scale orientation:UIImageOrientationUp];
CGImageRelease(imageRef);
return image;
}

这里的rect就是指定的裁剪范围。方法比较简单,没有什么好解释的。主要注意的是,这里rect 包含的x,y,width,height应该是图片的绝对尺寸乘以图片的缩放因子,否则裁剪出来的图片是不对的。如果图片是1倍图会没什么问题,但是如果是2倍图或者3倍图,要么可能尺寸不对,要么截出来的图片很模糊。完整方法可改为:

1
2
3
4
5
6
7
8
9
10
- (UIImage *)createImageWithRect:(CGRect)rect image:(UIImage *)clipImage {
rect.origin.x *= clipImage.scale;
rect.origin.y *= clipImage.scale;
rect.size.width *= clipImage.scale;
rect.size.height *= clipImage.scale;
CGImageRef imageRef = CGImageCreateWithImageInRect(clipImage.CGImage, rect);
UIImage *image = [UIImage imageWithCGImage:imageRef scale:clipImage.scale orientation:UIImageOrientationUp];
CGImageRelease(imageRef);
return image;
}

其他注意点:

WKWebView

  • 对于WKWebView使用 renderInContext的方法进行截图的时候, 当WKWebView 执行UIGraphicsGetCurrentContext()的结果返回的nil,截图失败
  • 所以只能UIViewdrawViewHierarchyInRect的方法去截图

UIVisualEffectView,高斯模糊蒙版

  • 对于使用了UIBlurEffect进行高斯模糊的UIView进行截图的时候,如果对view.layer使用 renderInContext的方法进行截图,上面的高斯模糊蒙版会失真失效,可以改用drawViewHierarchyInRect方法,这样能够保持高斯模糊的效果。相对于renderInContext是对view的layer渲染到当前的上下文中,drawViewHierarchyInRect方法是对view进行一个快照,然后将快照渲染到当前的上下文中。

Objective-C-(一)对象、属性、方法

1.对象模型

Objective-C是一门面向对象的语言,对象是我们编程的基本单元,所有的操作都是通过对象。对象其实是对 数据行为 的封装。在OC中,数据的载体就是实例变量,我们可以通过属性便捷的访问到实例变量行为其实就是对象的方法,也可以称为发消息,方法内部可以传递数据和操作数据。

平时我们声明一个对象,例如:NSString *string = @"zzy"; 这条语句的意思是,创建了一个NSString类型的对象实例,实例的内容是zzy,并返回这个实例的内存地址给string这个变量保存,之后我们就可以通过变量string来操作这个实例。学过C语言的都知道,其实*string就是指这是一个NSString类型的指针,所以OC对象的本质其实就是指向某块内存地址的指针。

对象的结构

在OC中每个对象都是一个类的实例,对象的结构如下:

1
2
3
4
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};

可以看到对象是一个结构体,结构体当中有一个Class类型的成员变量isaClass的定义为typedef struct objc_class *Class;,这是一个指向objc_class结构体的一个结构体指针。而objc_class其实就是对象所属类的原型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;

可以看到Class中也存放了一个isa指针,所以Class也是一个对象,被称为类对象。同时Class结构体当中还存着:指向父类的super_class指针、类名、版本、实例对象的大小、实例变量列表、方法列表、缓存、协议等信息。通过类创建出来的实例对象所拥有的属性,方法,协议等都是存储在类结构体当中。

实例对象的isa指针指向其所属的类,类结构体当中存放着对象的属性和方法列表。类对象的isa指针指向其所属的元类,元类中存放着类对象的方法列表(类方法),而元类内部也有一个isa指针,指向的是基类的元类,而基类的元类的isa指针指向自身。用一个经典的图类表示整个关系链路:

不能向编译后得到的类中增加实例变量:因为编译后的类已经注册在 runtime 中,类结构体中的 objc_ivar_list 实例变量的链表 和 instance_size 实例变量的内存大小已经确定,同时runtime 会调用 class_setIvarLayoutclass_setWeakIvarLayout 来处理 strong weak 引用。所以不能向存在的类中添加实例变量。

运行时创建的类是可以添加实例变量,调用 class_addIvar 函数。但是得在调用 objc_allocateClassPair 之后,objc_registerClassPair 之前,否则类一旦注册到runtime中后就不能改变实例变量了。

2. 属性

对象中的数据是通过实例变量来保存的,OC提供了一种便捷的访问实例变量的方式:属性。属性的本质就是包括实例变量 + setter方法 + getter方法。实例变量的值保存在对象内部,通过“偏移量(offset)”来保存,即:该变量距离对象内存区域的起始地址的距离,这个是通过硬编码来标识的。当我们声明一个属性之后,系统会自动帮我们生成一个成员变量和属性,这个成员变量和属性的描述(类型,名称等)分别存放在类的ivar_listproperty_list当中,并且生成setter方法和getter方法追加到method_list当中,然后计算属性在对象内部的偏移量,并实现setter方法和getter方法。

声明属性的方式:@property (nonatomic, readwrite, copy) NSString *name; 这个时候编译器会自动帮我们合成实例变量_name。默认的实例变量名为属性名前面加下划线。如果不想用系统默认的实例变量名,也可以通过@synthesize关键字手动指定实例变量名,例如@synthesize name = _myName; 那么我们的实例变量就变成了_myName,不过一般为了规范没有人这么做。

如果不想让系统自动帮我们合成实例变量和settergetter方法,也可以通过关键字@dynamic声明(例如:@dynamic name;),然后自己手动去合成实例变量和相关存取方法。如果没有实现这些合成方法的话,那么是无法正常使用属性的。

目前使用@synthesize的一般场景:

  • 当我们手动实现了settergetter方法的时候,系统默认我们自己管理属性,所以就不再帮我们合成实例变量了,这个时候需要用@synthesize手动合成实例变量,不然编译器就会报错。
  • 当重写父类属性的时候,在子类中需要用@synthesize手动合成实例变量,否则无法使用。
  • 使用了@dynamic的时候。

属性包含的三种语义:

atomic && nonatomic :原子性 && 非原子性

在声明属性的时候,编译器默认的属性语义是atomic原子性的。atomicnonatomic的区别是前者在setter方法赋值的时候会进行加锁保证赋值过程的完整性(安全性),而后者不会使用同步锁。在iOS中,由于频繁加锁会导致性能问题,而且即使采用了atomic,在多线程操作的情况下,也并不能保证线程的安全性。如果要保证线程安全,需要专门进行其他加锁机制处理。所以一般情况下,我们平时声明属性用的都是nonatomic

readwrite && readonly :可读可写 && 只读

readwrite 表示属性可读可写。readonly表示属性只能读取不能修改,一般用于在.h中对外暴露的属性不想被别人修改时这么声明,然后在.m的extension中再重新定义为可读可写。编译器默认的属性语义为readwrite

内存管理语义:strong、weak、copy、assign、unsafe_unretained

  • strong 表示对属性所指对象的一种强引用的关系。当声明为strong时,在setter方法中会先持有新值,再释放旧值,然后再把新值赋值给实例变量。

    eg:

    1
    2
    3
    4
    5
    6
    @property (nonatomic, strong) NSMutableArray *array;
    - (void)setArray:(NSMutableArray *)array {
    [array retain];
    [_array release];
    _array = array;
    }
  • weak 表示对属性所指对象的一种弱引用的关系。当声明为weak时,在setter方法中即不会持有新值,也不会释放旧值,只是进行一次简单的赋值操作。但是当属性所指的对象被销毁时,属性值会自动置为nil 比较安全。

    eg:

    1
    2
    3
    4
    @property (nonatomic, weak) NSMutableArray *array;
    - (void)setArray:(NSMutableArray *)array {
    _array = array;
    }
  • copy 类似于strong,也表示对属性所指对象的一种强引用的关系,不同的是,copy语义在setter方法中并不是直接持有新值,而是会拷贝出一份不可变的副本持有,然后再赋值给实例变量。

    eg:

    1
    2
    3
    4
    5
    6
    7
    @property (nonatomic, copy) NSString *name;
    - (void)setName:(NSString *)name {
    NSString *copyName = [name copy];
    [_name release];
    _name = copyName;
    [copyName release]
    }

    copy语义一般是用于那些具有可变子类的类型如:NSArrayNSDictionaryNSString等。这些类族都有其对应的可变子类,如果声明一个不可变的NSString类型的属性,由于父类指针可以指向子类对象,在给属性赋值的时候,传递给setter方法的值有可能是一个可变子类NSMutableString的实例对象,那么在该属性值赋值完成后,由于属性所指的对象其实是个可变的字符串,就有可能被外界所篡改。所以为了保证属性的安全,在赋值的时候需要先copy出一份不可变的对象,然后再赋值。

  • assign 表示在 setter 方法赋值时只会进行简单的赋值操作,只用于修饰基本类型的数据(例如NSInteger、CGFloat等)。

  • unsafe_unretained 语义类似于assign,不同的是它用于修饰对象类型。如同它的字面意义一样,它不会持有属性所指的对象(类似于weak),但是当属性所指的对象被销毁时,属性值不会自动置为nil,所以它并不安全,可能会导致野指针。

assign也可以修饰对象,编译器不会报错,当修饰对象的时候,assignunsafe_unretained一样的效果。

3. 方法

在OC中,方法又被称为发消息,对象需要调用方法来传递数据,而方法是什么呢?例如一个方法:[self doSomething:@"something"];,编译器会将这个方法转为如下的C语言函数:

1
objc_msgSend(self, @selector(doSomething:), @"something");

objc_msgSend(id self, SEL cmd, ...) 这个函数就是OC消息传递的核心函数。我们平时调用的方法最终都会转为这个函数调用。这个函数接受两个及以上的参数,分别是:消息的接收者消息的签名selector消息的参数。消息的接收者就是该方法的调用者,消息的签名是一个SEL类型的数据,可以理解为方法名的包装,参数就是调用方法所传递的参数,按顺序传入。

当调用这个函数后,objc_msgSend会根据当前消息接收者的isa指针找到其所属的类,然后在类的方法列表中根据selector的名称找到对应的方法并执行,同时会将查找的结果缓存起来以供下次查找时快速的执行。如果找不到,就会根据super_class 指针沿着继承体系一直往上查找,直到找到合适的方法之后跳转到方法的实现并执行。如果直到找到基类还是找不到对应方法的话,那就开始执行消息转发机制。如果消息转发的过程中也没有找到的话,那就抛出异常程序终止。异常信息是常见的unrecognized selector send to instace xxxx

Selectors, Methods, IMP

这三个都跟方法有关,分别指代不同的概念。官方文档的描述如下:

  • Selectors(typedef struct objc_selector *SEL), 在runtime中Selectors用来表示一个方法的名称。Selectors在runtime中会被注册(或映射)为一个C类型的字符串。Selector由编译器产生并且在当类被加载进内存时由runtime自动进行名字和实现的映射。

  • Methods (typedef struct objc_method *Method), An opaque type that represents a method in a class definition: Methods用来代表方法的定义。

    1
    2
    3
    4
    5
    6
    typedef struct method_t *Method; 
    struct method_t {
    SEL name;
    const char *types;
    IMP imp;
    };

  • IMP (typedef id (*IMP)(id, SEL,...)),IMP 是一个函数指针,代表了方法执行的入口,指向具体的方法实现。

三者的关系:在runtime期间,类(Class)维护了一个消息分发的列表,列表的入口是一个MethodMethod中映射了一个键值对,key就是方法的名称selector,value就是指向这个方法实现的函数指针IMP。这也是方法查找的具体实现。

如果在runtime期间修改了方法列表,让已经存在的selector映射到另外一个IMP,然后用一个新的selector映射到原来的IMP,则可以达到动态修改方法实现的效果,这就是Method swizzling。关于Method swizzling的使用,这篇文章介绍的比较全面: Method Swizzling的各种姿势

关于category

category可以给一个类添加方法,但是需要注意以下几点:

  • 1.如果分类实现了和主类同样的方法,会覆盖主类的方法(load方法除外)
  • 2.如果两个不同的分类都实现了和主类同样的方法,那么会执行后编译的分类的方法(跟编译顺序有关,后加入的分类添加的方法加在在方法列表的前面,所以会先执行)
  • 3.如果分类实现了load方法,并不会覆盖主类的load方法,会先执行主类的load方法然后再执行分类的load方法(按照编译顺序)
  • 4.如果分类实现了initialize方法,会覆盖主类的initialize方法

消息转发

上面在介绍方法调用的实现流程时说到了消息转发,消息转发是发生在当给一个对象发送没有实现的方法时会启动消息转发。例如我们给一个对象发送没有实现的消息:

[obj performSelector:@selector(doWork:array:) withObject:@"somthing" withObject:@[@"work"]];

此时消息转发一共分为三个阶段:

第一个阶段:动态方法解析

runtime会先询问对象所属的类,看其能否动态的添加方法以处理当前未处理的消息。处理的方法有两个:

实例方法:+ (BOOL)resolveInstanceMethod:(SEL)sel

类方法:+ (BOOL)resolveClassMethod:(SEL)sel

方法的参数就是当前消息的selector,返回值是来标识当前的类是否能动态的添加方法来处理这个selector

eg:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@interface MessageInvokeObj : NSObject

@end

@implementation MessageInvokeObj

void doWork(id self, SEL cmd, NSString *work, NSArray *array) {
NSLog(@"work = %@, array = %@", work, array);
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSString *selectorString = NSStringFromSelector(sel);
if ([selectorString isEqualToString:@"doWork:array:"]) {
class_addMethod(self, sel, (IMP)doWork, "v@:@:@");
return YES;
}
return [super resolveInstanceMethod:sel];
}

@end

通过class_addMethod函数动态的添加一个方法来处理当前的消息。此时动态添加方法成功,消息转发结束。

第二个阶段:备援接收者

如果第一个阶段无法处理该消息的话,runtime会询问当前的类能否把这个消息转发给其他对象去处理,如果可以的话,就返回这个对象,否则就返回nil。

处理方法:- (id)forwardingTargetForSelector:(SEL)aSelector

eg:

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
@interface InvokeMethodObj : NSObject

@end

@implementation InvokeMethodObj

- (void)doWork:(NSString *)work array:(NSArray *)array {
NSLog(@"work = %@, array = %@", work, array);
}

@end

@interface MessageInvokeObj : NSObject

@end

@implementation MessageInvokeObj

- (id)forwardingTargetForSelector:(SEL)aSelector {
NSString *selString = NSStringFromSelector(aSelector);
if ([selString isEqualToString:@"doWork:array:"]) {
InvokeMethodObj *obj = [[InvokeMethodObj alloc] init];
return obj;
}
return [super forwardingTargetForSelector:aSelector];
}

@end

第三个阶段: forwardInvocation

最后一个阶段,runtime会将这个消息的所有信息封装到NSInvocation对象当中,包括消息的接收者target、消息的selector以及消息的所有参数。最后一次询问接收者能否处理,如果不能将抛出异常。如果可以,直接把消息指派给目标对象。

先返回正确的方法签名:- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector

然后执行消息转发:- (void)forwardInvocation:(NSInvocation *)anInvocation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@interface MessageInvokeObj : NSObject

@end

@implementation MessageInvokeObj

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSString *selString = NSStringFromSelector(aSelector);
if ([selString isEqualToString:@"doWork:array:"]) {
return [NSMethodSignature signatureWithObjCTypes:"v@:@:@"];
}
return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
InvokeMethodObj *obj = [[InvokeMethodObj alloc] init];
if ([obj respondsToSelector:anInvocation.selector]) {
[anInvocation invokeWithTarget:obj];
}
}

@end

OC对象模型大概就是这些,要想更深入的了解,需要读下runtime的源码。下一篇写下OC的内存管理…