前言

最近又看了一遍 Objective-C高级编程 iOS与OS X多线程和内存管理 这本书。对一些问题有了一些新的认识。这篇文章将讲讲下面几个话题:

  1. ARC,MRC 与 引用计数
  2. Block的实现
  3. GCD与runloop

ARC, MRC 与 引用计数

引用计数

Objective C的生成的数据会存放在栈或者堆上面。对于存放在栈上面的数据(比如局部int变量等)在不需要时,系统会自动释放内存空间;对于存放在堆上面的数据(比如对象等)会使用引用计数来决定是否释放内存空间。

创建一个对象时,会malloc一块对象所需大小的内存空间;当有一变量持有该对象时(一般是有一指针指向该内存时)该对象的引用计数加一;当持有该对象的变量不再持有它时(一般是指针被销毁,或指向其他地方)该对象的引用计数减一。当对象的引用计数为0时,free这一块内存

MRC和ARC实质上都是对这个引用计数进行操作

手动引用计数 MRC

手动引用计数要确保在对象从被创建到销毁的一生中,该对象的引用计数也从1变成0

与对象引用计数相关的函数只有3个:

  1. alloc/new/copy/mutableCopy
  2. retain
  3. release

alloc/new/copy/mutableCopy

这几个函数生成一个对象,并且该对象的引用计数为1

retain

retain函数将对象的引用计数加1

release

release函数将对象的引用计数减一

examples

autorelease函数
1
2
3
4
5
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]  // 1
id obj = [[NSObject alloc] init]; // 2
[obj autorelease]; // 3
[id dosomething]; // 4
[pool drain]; // 5
  1. 注释2处,使用alloc方法新建一个对象,该对象的引用计数为1
  2. 注释3处,调用了autorelease函数;该函数并没有更改对象的引用计数,但是它将对象注册到自动释放池中
  3. 注释4处,因为注释3没有减少引用计数,所以对象的引用计数还是1。所以这个地方对象依然存在,可以正常使用对象
  4. 注释5处,因为在注释3时将对象注册进pool。此时[pool drain]方法会去调用对象的release方法,将对象引用计数减1;此时引用计数为0,那么释放内存空间

所以从上面看出:**autorelease函数并没有改变对象的引用计数,只是简单的将对象与释放池挂钩;当pool drain时才调用对象的release函数**。

所以 autorelease函数实质就是延迟对象的release方法;可以让你放心使用对象,但又防止了你忘记释放对象

array类对象
1
id obj = [NSMutableArray array];

这里不需要调用对象的release方法。因为在内部的实现是:

1
2
3
4
5
+ (id)array {
id obj = [[NSMutableArray alloc] init];
[obj autorelease];
return obj;
}

array类方法返回的对象,该对象的引用计数为1;但是已经注册到自动释放池中了,会在释放池结束时调用release方法

MRC总结

对象一定成对的存在 allocrelease方法;或者成对的存在retainrelease 方法。只有这样才能对引用计数有增有减,才能保证对象能正确的释放。

如果没有成对的出现,但是程序又是正确的,那么这3个方法的调用肯定在某函数的内部。比如[pool drain] 或者 [NSMutableArray array] 等函数中。

自动引用计数 ARC

ARC不再需要你来手动调用retainrelease 函数了,系统会自己调用这些函数。那么ARC需要注意的是什么呢:

  1. __strong
  2. __weak
  3. __autorelease

使用以上三个修饰符对对象进行修饰,那么就会自动增减引用计数

__strong

会自动release

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
id obj = [[NSObject alloc] init];
}

// 转化之后

id obj = objc_msgSend(NSObject, @selector(alloc));
objc_msgSend(obj, @selector(init));
objc_release(obj);

/*--------------------------分割符------------------*/
{
id __strong obj = obj2;
// do something
}

// 转化之后

id obj = obj2;
[obj retain];
// do something
[obj release];

可以看到__strong会持有对象,并自动调用release将引用计数减一

会阻止对象注册到release pool中,并持有对象

1
2
3
4
5
6
7
8
9
10
11
12
{
id __strong obj = [NSMutableArray array];
}

// 转化之后

id obj = objc_msgSend(NSMutableArray, @selector(alloc));
objc_msgSend(obj, @selector(init));
return objc_autoreleaseReturnValue(obj);

objc_retainAutoreleasedReturnValue(obj);
objc_release(obj);

可以看到,__strong会将即将注册到pool的对象抢过来,自己持有,并释放。

总结

__strong修饰符会持有对象,并自动释放。有时会抢过pool的对象,来自己释放

__autorelease

不常用,基本原理:

  1. 对于已经/或即将注册到pool的对象,没有变化。会由pool来释放该对象
  2. 对于其他对象,会将对象retain,并强制注册到pool,由pool来release

__weak

weak变量不对引用计数产生影响。来看一下代码:

1
2
id __weak obj1 = obj;
id __weak obj2 = obj;

APP管理一个全局的weak表,上面这两行代码,会往该表中插入2行记录:

每行记录的key是对象的首地址,也就是 obj ;value是存放weak变量的地址,也就是&obj1&obj2

  1. 当weak变量obj1被废弃时,将weak表中的value为&obj1的记录删除
  2. 当对象被废弃时,这时候obj1,obj2应该指向nil。这时就将weak表中,key为obj的记录的key置成nil

所以寻找weak变量对应的对象地址时,其实就是寻找weak表中的key值

使用__weak变量时
1
2
3
4
5
6
7
8
9
10
11
12
13
{
id __weak o = obj;
NSLog(@"%@", o);
}

// 转化之后

id o;
objc_initWeak(&o, obj);
id tmp = objc_loadWeakRetained(&o);
objc_autorelease(tmp);
NSLog(@"%@", tmp);
objc_destoryWeak(&o);

可以看到使用weak变量时,其实是将该weak变量指向的对象交由一个tmp变量持有,并将该对象注册到pool中。这样可以保证对象万一被释放了,也至少还有tmp持有他,可以正常使用。

Block的实现

使用clang的-rewrite-objc选项,可以将OC代码转换成C++代码。这样就可以看到Block的C++的实现

不截获任何变量的Block

1
2
3
4
5
int main() {
void (^blk)(void) = ^{printf("Block\n");};
blk();
return 0;
}

经过 clang -rewrite-objc main.m 转化之后,生成main.cpp 。来看看Block在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
// 只截取关键代码
struct __main_block_impl_0 {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
struct __main_block_desc_0 *Desc;

__main_block_impl_0(void *fp) {
isa = &_NSConcreteStackBlock;
FuncPtr = fp;
}
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("Block\n");
};

int main() {
struct __main_block_impl_0 tmp = __main_block_impl_0(__main_block_func_0);
struct __main_block_impl_0 *blk = &tmp;
(*blk->FuncPtr)(blk);
return 0;
}

可以发现Block实际上是一个结构体,blk变量是指向__main_block_impl_0结构体的指针。该结构体中有一个函数指针 FuncPtr,执行Block时,其实就是执行该结构体中函数指针对应的函数。

截获变量但不修改的Block

1
2
3
4
5
6
int main() {
int val = 10;
void (^blk)(void) = ^{printf("%d", val);};
blk();
return 0;
}

相同的方法,来看一下转化之后的结果:

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
struct __main_block_impl_0 {
void *fp;
int Flags;
int Reserved;
void *FuncPtr;
struct __main_block_desc_0 *Desc;

int val;

__main_block_impl_0(void *fp, int _val) : val(_val) {
isa = &_NSConcreteStackBlock;
FuncPtr = fp;
}
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int val = __cself->val;
printf("%d", val);
}

int main() {
int val = 10;
struct __main_block_impl_0 = __main_block_impl_0(__main_block_func_0, val);
struct __main_block_impl_0 *blk = &tmp;
(*blk->FuncPtr)(blk);

return 0;
}

可以看到,对于截获但是未曾改变的变量val,Block结构体会将该变量拷贝到结构体中的同名成员变量中。所以当main函数里面定义的val之后无论如何改变,都不会影响到Block中存储的val变量;因为他们两个是不同的两个变量。

截获__block变量

当要在Block里面改变截获的变量时,需要使用**__block**修饰符。

全局变量

1
2
3
4
int global_val = 2;
int main() {
// do something
}

对于全局变量,Block可以直接访问,不需要做任何的改变

静态局部变量

1
2
3
4
5
6
7
8
int main() {
static int static_val = 3;
void (^blk)(void) = ^{
static_val += 1;
}

return 0;
}

来看转化之后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct __main_block_impl_0 {
// 一些其他成员
int *static_val;

__main_block_impl_0(void *fp, int *_static_val) : static_val(_static_val) {
isa = &_NSConcreteStackBlock;
FuncPrt = fp;
}
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int *static_val = __cself->static_val;
*static_val += 1;
}

int main() {
static int static_val = 3;
blk = &__main_block_impl_0(__main_block_func_0, &static_val);

return 0;
}

可以看到,对于静态局部变量,Block的结构体中存放的是指向该变量的指针,所以可以通过指针来改变该静态局部变量。这是因为,静态局部变量即使其作用域已经结束,但是它并不会被销毁,其内存空间是一直存在的

局部变量

1
2
3
4
5
int main() {
__block int val = 10;
void (^blk)(void) = ^{val = 1;};
return 0;
}

在Block中改变局部自动变量需要使用__block修饰符;转化成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
struct __Block_byref_val_0 {
void *__isa;
__Block_byref_val_0 *__forwarding;
int __flags;
int __size;
int val;
};

struct __main_block_impl_0 {
// 其他成员变量

__Block_byref_val_0 *val;

__main_block_impl_0(void *fp, __Block_byref_val_0 *_val) : val(_val->__forwarding) {
//
}
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_val_0 *val = __cself->val;
(val->forwarding->val) = 1;
}

int main() {
__Block_byref_val_0 val = {
0,
&val,
0,
sizeof(__Block_byref_val_0),
10
};

blk = &__main_block_impl_0(__main_block_func_0, &val);
return 0;
}

可以看到__block变量val其实是一个结构体 __Block_byref_val_0。在里面有val这个变量。

因为正常生成的Block都是在栈上面的,但是在合适的情况下面,栈上的Block会被拷贝到堆上;与此同时,Block所拥有的__block结构体也会被拷贝到堆上,相当于Block持有__block变量。

当block变量还在栈上时,它的forwarding指向自己;当被拷贝到堆上时,forwarding指向堆上的block变量。在使用时全部使用forwarding,这样可以保证改变的是同一个变量。

使用这么一个结构体来存放局部变量,可以保证局部变量可以时时刻刻被更改,也可以保证栈上局部变量被释放后,堆上的局部变量依然可以访问

其他

在堆上的Block或者block变量才谈论对象的retain以及release;在栈上的Block和block会被栈自动释放,也不存在对象的retain和release

Block会在一下情况下被复制到堆上面:

  1. 显式调用Block的copy方法
  2. Block作为函数返回值返回
  3. 将Block赋给__strong的id类型或者Block的类的成员变量时 (这时候要注意self和Block的循环引用)
  4. 方法名中含有usingBlock的Cocoa框架方法,或者GCD中使用的Block

GCD 与 runloop

  1. GCD操作的是queue,然后由queue来确定block该由哪个线程来执行。
  2. runloop对应的则直接是线程。一个线程可以有0-1个runloop在上面执行。

runloop

runloop实际上就是一个运行在特定线程上面的do-while循环。在do-while循环里面,可以处理多种事件:

  1. port-based event,多半是系统事件
  2. 其他线程传递给当前线程来直线的block
  3. Timer,定时事件
  4. Observer,当runloop状态变化时,执行相应的监听器

有事件来时,runloop就执行事件;没有事件,就进入休眠,但是并没有退出do-while循环。只有当runloop到达限定时间,或者手动退出runloop;这个do-while循环才会结束

主线程与runloop

APP的主线程一直运行着一个runloop循环,这个runloop由系统运行,并且已经往该runloop中添加了一些事件。

硬件事件响应

当发生硬件事件时,也就是触摸,锁屏,摇动等事件时;这些事件都是系统事件,也就是port-based的类型。那么runloop会使用_UIApplicationHandleEventQueue()该函数来处理这一类事件

手势识别

_UIApplicationHandleEventQueue()函数中,会识别出需要待处理的手势。当runloop进入休眠之前,会出发Observer,在这个Observer中会调用_UIGestureRecognizerUpdateObserver()函数,该函数会处理手势(也就是这行手势的回调函数)

界面更新

每当runloop即将进入休眠,或者即将退出时,会执行一个Observer。在这个Observer里面会查看每个View的frame等属性,如果需要重新绘制,那么开始重新绘制的过程。(layout -> display -> draw 等等)

所以当其他的事件执行时间过长,超过16ms的话,就会推迟进入休眠,也就会推迟Observer的触发,也就会推迟界面的更新。那么就会造成卡顿的现象。

参考