原本打算将AutoreleasePool和内存管理,Runloop,线程,事件分发放在一块介绍的,因为这些都有着密切的联系,但是考虑到这样的话文章会很长,看得都累,所以还是分开了。

1. AutoReleasePool 是什么

看过iOS内存管理的都知道,iOS内存管理使用的引用计数会在不使用某个对象的时候调用release 方法,将某个对象的引用计数减1,当某个对象当引用计数减到0的时候对象被释放,dealloc被调用。但是某些情况需要延迟释放,最常见的例子比如当你在一个方法中返回一个对象时就需要延迟释放,AutoreleasePool是一种内存自动回收机制,它可以延迟加入AutoreleasePool中的变量release的时机。在正常情况下,创建的变量会在超出其作用域的时候release,但是如果将变量加入AutoreleasePool,那么release将延迟执行。具体这些对象什么时候释放,在后续的介绍中将会介绍。这部分只要知道AutoReleasePool 是什么就可以了:

Autorelase Pool 提供了一种可以允许你向一个对象延迟发送release消息的机制。当你想放弃一个对象的所有权,同时又不希望这个对象立即被释放掉(例如在一个方法中返回一个对象时),Autorelease Pool 的作用就显现出来了。
2.从宏观看AutoReleasePool位于哪个位置

终于可以上图了:

在一个应用中有两种释放池局部释放池和RunLoop释放池,上图中展示的是RunLoop释放池,局部释放池比较简单,就是在autoreleasepool花括号结束的时候释放释放池里面的对象的,所以这里最关键的就是RunLoop释放池

@autoreleasepool {

}

RunLoop释放池会在进入主线程Runloop的时候新建一个Autoreleasepool,在从睡眠被唤醒的时候,通过pop后再push清空并重建一个Autoreleasepool,在退出RunLoop的时候pop掉Autoreleasepool

3. AutoReleasePool的内部结构与机制

首先AutoReleasePool是由多个AutoreleasePoolPage构成的双向链表。
每个AutoreleasePoolPage大小为4096字节,被分成两大部分:
AutoreleasePoolPage描述部分,它是由如下字段构成,大小为56字节:

magic 用来校验 AutoreleasePoolPage 的结构是否完整;
next 指向最新添加的 autoreleased 对象的下一个位置,初始化时指向 begin();
thread 指向当前线程;
parent 指向父结点,第一个结点的 parent 值为 nil;
child 指向子结点,最后一个结点的 child 值为 nil;
depth hiwat 不是我们的重点这里不介绍

剩余的用来存储加入到自动释放池的对象,它们其实只是指向这些对象的指针,对象还是存储在堆上。
当前正在使用的AutoreleasePoolPage被称为hotPage.

当我们在代码中使用

@autoreleasepool {

}

的时候会转化为:

void * atautoreleasepoolobj = objc_autoreleasePoolPush();
// do whatever you want
objc_autoreleasePoolPop(atautoreleasepoolobj);

3.1 objc_autoreleasePoolPush

objc_autoreleasePoolPush 内部很简单就是调用AutoreleasePoolPage类的push方法。

void *objc_autoreleasePoolPush(void) {
return AutoreleasePoolPage::push();
}

AutoreleasePoolPage::push会往当前使用的hotPage中添加一个POOL_BOUNDARY 自动释放池边界标记,这个标记作为返回值返回,作为pop的参数。

static inline void *push() {
return autoreleaseFast(POOL_BOUNDARY);
}

之后的完自动释放池添加对象最终也是调用autoreleaseFast。

static inline id *autoreleaseFast(id obj) {
AutoreleasePoolPage *page = hotPage();
if (page && !page->full()) {
return page->add(obj);
} else if (page) {
return autoreleaseFullPage(obj, page);
} else {
return autoreleaseNoPage(obj);
}
}

它分三种情况:

  • hotPage 不存在:
    比如刚开始的时候是没有hotPage的,它会创建一个然后调用setHotPage将新建的page设置为hotpage,如果没有POOL_BOUNDARY,先往AutoreleasePoolPage添加一个POOL_BOUNDARY,然后再将带加入释放池对象的指针添加到AutoreleasePoolPage。
static id *autoreleaseNoPage(id obj) {
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
setHotPage(page);

if (obj != POOL_BOUNDARY) {
page->add(POOL_BOUNDARY);
}

return page->add(obj);
}
  • hotPage 存在并且还没满

调用 page->add(obj) 方法将对象加入当前hotPage中,也就是将对象添加到next指向的位置,然后将next指向下一个空的位置。

id *add(id obj) {
id *ret = next;
*next = obj;
next++;
return ret;
}
  • hotPage 满了

调用 autoreleaseFullPage(obj, page) 方法,该方法会先查找 hotPage 的 child,如果有则将 child page 设置为 hotPage,如果没有则将创建一个新的 hotPage,之后在这个新的 hotPage 上执行 page->add(obj) 操作

static id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page) {
do {
if (page->child) page = page->child;
else page = new AutoreleasePoolPage(page);
} while (page->full());

setHotPage(page);
return page->add(obj);
}
3.2 objc_autoreleasePoolPop

objc_autoreleasePoolPop

objc_autoreleasePoolPop 里面只是简单调用AutoreleasePoolPage::pop方法,传入的参数一般是一个POOL_BOUNDARY,

void objc_autoreleasePoolPop(void *ctxt){
AutoreleasePoolPage::pop(ctxt);
}

AutoreleasePoolPage::pop 首先会找到POOL_BOUNDARY所在的页面的地址,然后调用releaseUntil,releaseUntil会针对每页的每一项调用memset重置,然后调用objc_release将对应的对象release。最后将当前页面以及子页面全部删除

static inline void pop(void *token) 
{
AutoreleasePoolPage *page = pageForPointer(token);
id *stop = (id *)token;

page->releaseUntil(stop);

if (page->child) {
// hysteresis: keep one empty child if page is more than half full
if (page->lessThanHalfFull()) {
page->child->kill();
} else if (page->child->child) {
page->child->child->kill();
}
}
}

3.3 autorelease

当某个对象调用autorelease的时候,最终会调用AutoreleasePoolPage::autorelease

static inline id autorelease(id obj) {
id *dest __unused = autoreleaseFast(obj);
return obj;
}

最终还是调用autoreleaseFast

3.4 AutoReleasePool drain 和 release区别

当我们向自动释放池pool发送 release 消息时,它会向池中的每一个发送了 autorelease 消息的对象发送一条 release 消息,并且自身也会销毁。当向它发送 drain 消息时,只会释放里面的对象,而不会销毁自己。

3.5 什么情况下会将对象放到自动释放池:
  1. 对象作为方法返回值时候
  2. 通过类方法创建对象的时候
  3. 使用如下的便捷语法来建立对象的时候
NSArray *array = @[@"abc",@"def"];
NSNumber *number = @123;

一般来讲,开一个线程执行某项任务,在任务执行完成后线程就会退出。如果我们需要让线程能不退出一致常驻随时处理事件这就需要消息循环来实现了。
Runloop 是 iOS中的消息循环,Android也有Looper/Handler一套消息循环机制,这到底是啥,以前玩过单片机的小伙伴肯定记得很清楚,我们在配置完硬件,对各个部件进行初始化完毕后,都会在main中执行一个死循环,然后等待硬件中断,软件终端,在对应的中断函数中处理外部事件,其实这个死循环就是我们今天要介绍的Runloop,它会保持整个应用进程处于存活状态,然后在每个消息循环中处理各个事件。

Runloop 的思想可以用下面的伪代码来表示:

int main(void) {
初始化();
while (不满足退出条件) {
休眠前处理待处理事件(message);
睡眠ZZzzzz... 等待被事件唤醒
message = 获取需要处理的事件();
处理事件(message)
}
return 0;
}

整个大致流程的回调分布

{
/// 1. 通知Observers,即将进入RunLoop
/// 此处有Observer会创建AutoreleasePool: _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
do {

/// 2. 通知 Observers: 即将触发 Timer 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);

/// 3. 通知 Observers: 即将触发 Source (非基于port的,Source0) 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);

/// 处理Block
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
/// 4. 触发 Source0 (非基于port的) 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
/// 处理Block
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);

/// 6. 通知Observers,即将进入休眠
/// 此处有Observer释放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);

/// 7. sleep to wait msg.
mach_msg() -> mach_msg_trap();

/// 8. 通知Observers,线程被唤醒
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);

/// 9. 如果是被Timer唤醒的,回调Timer
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);

/// 10. 如果是被dispatch唤醒的,执行所有调用 dispatch_async 等方法放入main queue 的 block
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);

/// 11. 如果如果Runloop是被 Source1 (基于port的) 的事件唤醒了,处理这个事件
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);
} while (...);

/// 10. 通知Observers,即将退出RunLoop
/// 此处有Observer释放AutoreleasePool: _objc_autoreleasePoolPop();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);
}

间歇性懒癌发作,用一张图来概括下:

需要注意的点:
  • CFRunLoopRef 基于C线程安全,NSRunLoop 基于 CFRunLoopRef 面向对象的API 是不安全的
  • RunLoop和线程的一一对应的,对应的方式是以key-value的方式保存在一个全局字典中,
  • 主线程的 Runloop 会在应用启动的时候启动,其他线程的 Runloop 需要我们手动创建并启动,RunLoop在第一次获取时创建,在线程结束时销毁,我们只能在一个线程的内部获取其 RunLoop,并且苹果系统不允许我们直接创建RunLoop对象,只能通过以下几个函数来获取RunLoop:
CFRunLoopRef CFRunLoopGetCurrent(void)
CFRunLoopRef CFRunLoopGetMain(void)
+(NSRunLoop *)currentRunLoop
+(NSRunLoop *)mainRunLoop
  • Runloop Mode是 Source,Timer 和 Observer 的集合,不同的 Mode 把不同组的 Source,Timer 和 Observer 隔绝开。Runloop 在某个时刻只能跑在一个 Mode 下,处理这一个 Mode 当中的 Source,Timer 和 Observer,这也是为啥在滑动屏幕的时候有时候定时器会失效的原因,这个会在后续介绍。
  • source0是内部事件 UIEvent、CFSocket都是source0,它区别于source1,它不是基于Port的,它不能主动触发事件。我们需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop处理这个事件。
  • source1由RunLoop和内核管理,source1带有mach_port_t和一个回调,可以接收内核消息并触发回调,它能主动唤醒 RunLoop 的线程。
  • source0 有公开的 API 可供开发者调用,source1 却只能供系统使用
  • timmer 包含一个时间长度和一个回调。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。
    NSTimer的创建通常有两种方式,一种是以timer开头,另一种是以schedued开头,第一种是没有添加到Runloop中的,需要我们自己去手动调用代码添加,第二种会自动以NSDefaultRunLoopModeMode添加到当前线程RunLoop中。

  • 一个 item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环

RunLoop 应用相关

  • NSTimer, GCD Timer,CADisplayLink

NSTimer 的执行必须依赖于 RunLoop,如果没有 RunLoop,NSTimer 是不会执行的。而GCD 的线程管理是通过系统来直接管理的。GCD Timer 是通过 dispatch port 给 RunLoop 发送消息,来使 RunLoop 执行相应的 block,如果所在线程没有 RunLoop,那么 GCD 会临时创建一个线程去执行 block,执行完之后再销毁掉,因此 GCD 的 Timer 是不依赖 RunLoop 的。CADisplayLink是一个执行频率和屏幕刷新相同的定时器,它也需要加入到RunLoop才能执行,通常情况下CADisaplayLink用于构建帧动画,看起来相对更加流畅。

子线程启动定时器需要按照如下方式启动Runloop

dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSTimer* timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(Timered:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
[[NSRunLoop currentRunLoop] run];
});

  • AutoreleasePool
    RunLoop的进入的时候会调用objc_autoreleasePoolPush()创建新的自动释放池。
    RunLoop的进入休眠的时候会调用objc_autoreleasePoolPop() 和 objc_autoreleasePoolPush() 销毁自动释放池,创建一个新的自动释放池。
    RunLoop即将退出时会调用objc_autoreleasePoolPop() 释放自动自动释放池内对象。

  • 触摸事件

当一个硬件事件发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。随后用 mach port 转发给需要的App进程。随后苹果注册的那个 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内部的分发。
_UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,

  • 屏幕刷新

在我们修改了View frame、或者调整了UI层级,或者手动设置了setNeedsDisplay/setNeedsLayout之后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去,_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv 这个Observer会监听RunLoop即将进入休眠和退出状态,一旦进入这两种状态则会遍历所有的UI更新并提交进行实际绘制更新。facebook推出的AsyncDisplayKit的机制和它也类似,它将UI排版和绘制运算尽可能放到后台,将UI的最终更新操作放到主线程,在主线程RunLoop中增加了一个Observer监听即将进入休眠和退出RunLoop两种状态,收到回调时遍历队列中的待处理任务更新界面。

  • GCD

当调用了dispatch_async(dispatch_get_main_queue())时libDispatch会向主线程RunLoop发送消息唤醒RunLoop,RunLoop从消息中获取block,并且在CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE回调里执行这个block。需要注意的是dispatch_async() 到其他线程是由libDispatch处理,并不涉及到RunLoop。

  • 支持异步方法调用
    我们使用 performSelector:onThread: 或者 performSelecter:afterDelay: 时,实际上系统会创建一个Timer并添加到当前线程的RunLoop中

  • 滑动屏幕导致定时器失效

一般而言

[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];

等效于

NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopDefaultModes];

当我们滚动ScrollView或者滑动屏幕的时候,RunLoop会切换到UITrackingRunLoopMode 模式,而定时器运行在defaultMode下面,系统一次只能处理一种模式的RunLoop,所以导致defaultMode下的定时器失效,所以这种情况的解决方式就是将定时器放到commonMode中,这样即使切换到UITrackingRunLoopMode也会被触发。

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

或者使用GCD定时器,它不会受RunLoop的影响:

// 获得队列
dispatch_queue_t queue = dispatch_get_main_queue();

// 创建一个定时器(dispatch_source_t本质还是个OC对象)
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);

// 设置定时器的各种属性(几时开始任务,每隔多长时间执行一次)
// GCD的时间参数,一般是纳秒(1秒 == 10的9次方纳秒)
// 比当前时间晚1秒开始执行
dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC));

//每隔一秒执行一次
uint64_t interval = (uint64_t)(1.0 * NSEC_PER_SEC);
dispatch_source_set_timer(self.timer, start, interval, 0);

// 设置回调
dispatch_source_set_event_handler(self.timer, ^{
});

// 启动定时器
dispatch_resume(self.timer);
  • 常驻线程

我们有时候需要创建一个在后台一直存在的程序,来做一些需要频繁处理的任务

- (void)run {
@autoreleasepool{
/*如果不加这句,会发现runloop创建出来就挂了,因为runloop如果没有CFRunLoopSourceRef事件源输入或者定时器,就会立马消亡。
下面的方法给runloop添加一个NSport,就是添加一个事件源,也可以添加一个定时器,或者observer,让runloop不会挂掉*/

[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];

[[NSRunLoop currentRunLoop] run];
}
}

当需要这个后台线程执行任务时,通过调用 [NSObject performSelector:onThread:..] 将这个任务扔到了后台线程的 RunLoop 中。

AFNetworking 就是通过这种方式接收 Delegate 回调


+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];

NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
// 这里主要是监听某个 port,目的是让RunLoop不会退出,确保该 Thread 不会被回收
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}

+ (NSThread *)networkRequestThread {
static NSThread *_networkRequestThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_networkRequestThread =
[[NSThread alloc] initWithTarget:self
selector:@selector(networkRequestThreadEntryPoint:)
object:nil];
[_networkRequestThread start];
});
return _networkRequestThread;
}
  • 观察Runloop状态
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFStringRef runLoopMode = kCFRunLoopDefaultMode;
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler
(kCFAllocatorDefault, kCFRunLoopBeforeWaiting, true, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity _) {
// TODO here
});
CFRunLoopAddObserver(runLoop, observer, runLoopMode);

1. 多线程时间分片模式:

多数现代操作系统使用时间分片模式来管理线程的,它会将CPU运行时间分割成一个个时间片,一般在10-100毫秒之间不等,当线程被分配到执行时间后,系统将会将该线程的堆栈以及寄存器加载到CPU,并将旧线程的堆栈和寄存器数据保存起来,这就是所谓的上下文切换,这个切换也是需要耗费时间的,如果将时间片分割得太短就会导致过度频繁的上下文切换,从而导致大量的CPU时间浪费在切换上。这也是自旋锁存在的原因,这个在后面会详细介绍。

今天介绍的主题是线程安全,之前我们说过同一进程的线程之间是共享资源的,并且多线程是并发执行的,在这种并发环境下资源共享带来的一个问题就是资源竞争,竞争的资源可以是:锁、网络连接、通知事件,磁盘、带宽,以及一切可以被称作“资源”的东西,如果没有对共享资源进行保护,就会导致多个线程对资源进行修改,从而导致资源状态不确定,也就是我们所说对数据竞态。

但是并不是所有情况下多线程访问共享资源都会存在这些问题。
以下这些情况就不存在线程安全的情况:

  • 多线程串行访问共享资源,这也是很多时候解决数据竞态的一种方案,将访问资源的操作串行化。
  • 多线程并行情况下但是这些线程都是访问共享资源而不去修改共享资源,比如多个线程同时读一个文件。

所以发生数据竞态的条件有两个:

  1. 至少有两个线程同时访问同一个资源
  2. 至少其中有一个是改变资源的状态,比如写操作
2. 基础概念:

1.临界区:指的是一块对公共资源进行访问的代码
2.竞态条件:当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件

3.死锁
是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去

死锁发生的条件:

  • 互斥条件:线程对资源的访问是排他性的,如果一个线程对占用了某资源,那么其他线程必须处于等待状态,直到资源被释放。
  • 请求和保持条件:线程T1至少已经保持了一个资源R1占用,但又提出对另一个资源R2请求,而此时,资源R2被其他线程T2占用,于是该线程T1也必须等待,但又对自己保持的资源R1不释放。
  • 不剥夺条件:线程已获得的资源,在未使用完之前,不能被其他线程剥夺,只能在使用完以后由自己释放。
  • 环路等待条件:在死锁发生时,必然存在一个“进程-资源环形链”,即:{p0,p1,p2,…pn},进程p0(或线程)等待p1占用的资源,p1等待p2占用的资源,pn等待p0占用的资源。

4.资源饥饿: 当一个线程一直无法得到自己的资源而一直无法进行后续的操作时,我们称这个线程会饥饿而死。

  1. 优先级反转:
    优先级反转是在高优级(假设为A)的任务要访问一个被低优先级任务(假设为C)占有的资源时被阻塞.
    而此时又有优先级高于占有资源的任务(C),而低于被阻塞的任务(A)的优先级的中间优先级任务(假设为B)进入时,这时候,占有资源的任务(C)就被挂起(占有的资源仍为它占有),因为占有资源的任务优先级很低,所以,它可能一直被另外的任务挂起.而它占有的资源也就一直不能释放,这样,引起任务A一直没办法执行.而比它优先低的任务却可以执行.
    解决方案:
  • 优先级继承:将低优先级任务的优先级提升到等待它所占有的资源的最高优先级任务的优先级.当高优先级任务由于等待资源而被阻塞时,此时资源的拥有者的优先级将会自动被提升.
  • 优先级天花板:将申请某资源的任务的优先级提升到可能访问该资源的所有任务中最高优先级任务的优先级
    两者区别:
    优先级继承,只有当占有资源的低优先级的任务被阻塞时,才会提高占有资源任务的优先级,而优先级天花板,不论是否发生阻塞都提升.
  1. 原子操作: 一条不可打断的操作,在单处理器环境下,一条汇编指令是原子操作,但一句高级语言的代码却不是原子的,因为它最终是由多条汇编语言完成
  2. 可重入:当子程序正在运行时,执行线程可以再次进入并执行它,仍然获得符合设计时预期的结果。与多线程并发执行的线程安全不同,可重入强调对单个线程执行时重新进入同一个子程序仍然是安全的
  3. 线程安全:指某个函数、函数库在多线程环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。
3. iOS多线程开发中的常用锁:
3.1 互斥锁

互斥锁在出现锁的争夺时,未获得锁的线程会主动让出时间片,阻塞线程并睡眠,CPU会通过上下文切换,让其它线程继续运行。互斥锁用于保证某个资源只允许一个线程访问。

3.1.1 NSLock

NSLock 内部封装了一个 pthread_mutex,属性为 PTHREAD_MUTEX_ERRORCHECK, 它比pthread_mutex多了错误提示,也正式这个原因它比pthread_mutex性能上要慢,但是由于它在内部使用了缓存机制,所以性能上不会相差很多。

NSLock *lock = [NSLock alloc] init];

// 加锁
[lock lock];
/*
* 被加锁的代码区间,在这里可以访问需要锁保护的资源
*/

// 解锁
[lock unlock];
3.2 递归锁
3.2.1 NSRecursiveLock

递归锁在被同一线程重复获取时不会产生死锁。它会记录上锁和解锁的次数,当二者平衡的时候,才会释放锁,其它线程才可以上锁成功

@property (nonatomic, strong) NSRecursiveLock *recursiveLock;
_recursiveLock = [[NSRecursiveLock alloc] init];

[_recursiveLock lock];
/*
* 被加锁的代码区间,在这里可以访问需要锁保护的资源
*/

[_recursiveLock unlock];
3.2.2 synchronized

@synchronized 结构在工作时为传入的对象分配了一个递归锁。


@try {
objc_sync_enter(obj);
// do work
} @finally {
objc_sync_exit(obj);
}

当调用 objc_sync_enter(obj) 时,它用 obj 内存地址的哈希值查找合适的 SyncData(包含传入对象和一个递归锁的结构体),然后将其上锁。当你调用 objc_sync_exit(obj) 时,它查找合适的 SyncData 并将其解锁。

@synchronized(object)指令使用的 object 为该锁的唯一标识,只有当标识相同时,才满足互斥

优点:使用起来十分简单不需要在代码中显式的创建锁对象,便可以实现锁的机制,并且不用担心忘记解锁的情况出现。
缺点:性能较差,一般用在多线程情况下访问属性的情况

注意:如果在 @sychronized(object){} 内部object 被释放或被设为nil,没有问题,但如果 object 一开始就是nil,则失去了锁的功能。

- (void)setIntegerValue:(NSInteger)intValue {
@synchronized (self) {
_intValue = intValue;
}
}
3.3 自旋锁:

自旋锁与互斥锁有点类似,只是自旋锁被某线程占用时,其他线程不会进入睡眠状态等待,而是一直轮询查询直到锁被释放。由于不涉及用户态与内核态之间的切换,它的效率远远高于互斥锁。
但是自旋锁也有很明显的不足:

  • 旋锁一直占用CPU,在未获得锁的情况下会占用着CPU一直运行,如果不能在很短的时间内获得锁,这无疑会使CPU效率降低。
  • 自旋锁可能会引起优先级反转问题。如果一个低优先级的线程获得锁并访问共享资源,这时一个高优先级的线程也尝试获得这个锁,自旋锁会处于忙等状态从而占用大量 CPU。此时低优先级线程无法与高优先级线程争夺 CPU 时间,从而导致任务迟迟完不成、无法释放 lock。

所以一般自旋锁只有在内核可抢占式比较适用,在单CPU且不可抢占式的内核下自旋锁只适用于锁使用者保持锁时间比较短的情况。

3.3.1 OSSpinLock
// 初始化
spinLock = OS_SPINKLOCK_INIT;
// 加锁
OSSpinLockLock(&spinLock);
// 解锁
OSSpinLockUnlock(&spinLock);

目前OSSpinLock存在优先级反转的问题,在使用的时候需要十分注意。
不再安全的 OSSpinLock

3.3.2 os_unfair_lock

iOS 10.+ 之后添加的,也是属于忙等锁。

#import <os/lock.h>

os_unfair_lock_t unfairlock = &(OS_UNFAIR_LOCK_INIT);

os_unfair_lock_lock(unfairlock);
/*
* 被加锁的代码区间,在这里可以访问需要锁保护的资源
*/
os_unfair_lock_unlock(unfairlock);
3.4 信号量

加锁时会把信号量的值减一,并判断是否大于零。如果大于零,立刻执行。如果等于零的时候将会等待,在资源使用结束的时候释放信号量让信号量增加1。并唤醒等待的线程。
信号量可以有更多的取值空间,用来实现更加复杂的同步,而不单单是线程间互斥。

3.4.1 dispatch_semaphore
dispatch_semaphore_t signal = dispatch_semaphore_create(1);
dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, 3.0f * NSEC_PER_SEC);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_semaphore_wait(signal, timeout);
/*

* 被加锁的代码区间,在这里可以访问需要锁保护的资源
*/

dispatch_semaphore_signal(signal);
});

3.5 条件锁

条件锁一般常用于生产者–消费者模式

3.5.1 NSCondition

NSCondition同样实现了NSLocking协议,可以当做NSLock来使用解决线程同步问题,用法完全一样。但是性能相对更差点,除了lock 和 unlock,NSCondition提供了更高级的用法wait/signal/broadcast:wait 进入等待状态,当其它线程中的该锁执行signal 或者 broadcast方法时,线程被唤醒,继续运行之后的方法。其中 signal 和 broadcast 方法的区别在于,signal 只是一个信号量,只能唤醒一个等待的线程,想唤醒多个就得多次调用,而 broadcast 可以唤醒所有在等待的线程。

@property (nonatomic, strong) NSCondition *condition;
_condition = [[NSCondition alloc] init];

- (void)conditionLockTest {
dispatch_queue_t queue = dispatch_queue_create("com.idealist.locktest", DISPATCH_QUEUE_CONCURRENT);
for (NSInteger i=0; i<10; i++) {
dispatch_async(queue, ^{
[self conditionAdd];
});
}
for (NSInteger i=0; i<10; i++) {
dispatch_async(queue, ^{
[self conditionRemove];
});
}
}

- (void)conditionAdd {
[_condition lock];
// 生产数据
NSObject *object = [NSObject new];
[_ticketsArr addObject:object];
[_condition signal];

[_condition unlock];
}

- (void)conditionRemove {

[_condition lock];
if (!_ticketsArr.count) {
[_condition wait];
}
[_ticketsArr removeObjectAtIndex:0];
[_condition unlock];
}
3.5.2 NSConditionLock

NSConditionLock 借助 NSCondition 来实现,内部持有一个 NSCondition 对象,以及 _condition_value 属性

lockWhenCondition:方法是当condition参数与当前condition相等时才可加锁
unlockWithCondition:方法是解锁之后修改 Condition 的值

// 设置条件
#define IDL_NO_DATA 100
#define IDL_HAS_DATA 101

// 初始化条件锁对象
@property (nonatomic, strong) NSConditionLock *conditionLock;
// 实例化
_conditionLock = [[NSConditionLock alloc] initWithCondition:IDL_NO_DATA];

// 调用测试方法
- (void)conditionLockTest {
dispatch_queue_t queue = dispatch_queue_create("com.idealist.conditionlocktest", DISPATCH_QUEUE_CONCURRENT);

for (NSInteger i=0; i<10; i++) {
dispatch_async(queue, ^{
[self conditionLockAdd];
});
}

for (NSInteger i=0; i<10; i++) {
dispatch_async(queue, ^{
[self conditionLockRemove];
});
}
}

- (void)conditionLockAdd {

[_conditionLock lockWhenCondition:IDL_NO_DATA];

NSObject *object = [NSObject new];
[_ticketsArr addObject:object];
[_condition signal];

[_conditionLock unlockWithCondition:IDL_HAS_DATA];
}

- (void)conditionLockRemove {

[_conditionLock lockWhenCondition:IDL_HAS_DATA];

if (!_ticketsArr.count) {
[_condition wait];
}
[_ticketsArr removeObjectAtIndex:0];
[_conditionLock unlockWithCondition:IDL_NO_DATA];
}
3.6 读写锁

读写锁把对共享资源的访问者划分成读和写,在多处理器系统中,它允许同时有多个读来访问共享资源,最大可能的读者数为实际的逻辑CPU数。但是写操作是排他性的。
所以如下情况可以并发进行:

  • 多个读,没有写操作
  • 一个写操作,多个读操作
3.6.1 dispatch_barrier_async / dispatch_barrier_sync

具体用法见iOS多线程总结[一]

  • 共同点:1、等待在它前面插入队列的任务先执行完;2、等待他们自己的任务执行完再执行后面的任务。
  • 不同点:1、dispatch_barrier_sync将自己的任务插入到队列的时候,需要等待自己的任务结束之后才会继续插入被写在它后面的任务,然后执行它们;2、dispatch_barrier_async将自己的任务插入到队列之后,不会等待自己的任务结束,它会继续把后面的任务插入队列,然后等待自己的任务结束后才执行后面的任务。
3.6.1 pthread_rwlock
#import <pthread.h>
__block pthread_rwlock_t rwlock;
pthread_rwlock_init(&rwlock,NULL);
pthread_rwlock_rdlock(&rwlock);
pthread_rwlock_unlock(&rwlock);
pthread_rwlock_wrlock(&rwlock);
3.7 atomic

atomic用于保证属性setter、getter的原子性操作,在getter和setter内部加了线程同步的锁,但是它并不能保证使用属性的过程是线程安全的

4. 其它保证线程安全的方式

使用串行队列来将访问资源的操作串行化

5. pthread的各种同步机制
6. 性能对比

在介绍多线程编程的时候我们需要明确一般会在什么场合上使用多线程,我们知道每个进程一定有一个线程–主线程,在这个线程中一般用于更新界面相关的任务,一般任务又可以分成耗时的和非耗时操作,计算密集型的任务和io密集型任务就属于耗时任务,比如读写数据库,读写磁盘文件,访问网络等,这些一般放在子线程中完成,但是一般在任务完成等时候都会将结果呈现在界面上,这时候就需要在主线程中完成。这个大家应该都知道,但是往往很多人会有误区,是不是线程越多越好,答案是否定的,创建的线程过多有如下问题:

  • 从空间角度来看:每个线程都需要占用一定的内存空间,如果开启大量的线程,会占用大量的内存空间,降低程序的性能
  • 线程切换需要上下文切换,这就需要耗费一定的时间,线程越多,CPU在调度线程上的开销就越大,同样会降低程序的性能
  • 线程越多,线程关系越复杂,线程竞争,线程管理,以及死锁等其他多线程问题发生的概率就会相应的增加。

因此合理得管理多线程是十分必要的工作。

下面将从:
1.多线程基本概念
2.线程通讯
3.线程同步,线程安全

三个大方面对iOS多线程技术进行一个简要的总结

1.多线程基本概念
1.1 多线程编程的基本概念:

进程: 进程是指系统中正在运行的一个应用程序,进程之间是独立的,有自己专用且受保护的内存空间。
线程: 是操作系统能够进行运算调度的最小单位,是一个CPU执行的一条无分叉的命令序列,进程是由至少一个线程(主线程)构成,同一进程中的多个线程将共享该进程中的全部系统资源,如虚拟地址空间、文件描述符等。但每个线程都拥有自己的栈,寄存器,本地存储
并行,串行: 是针对线程队列的,表示一次可以执行多少个线程,串行队列每次只能执行一个线程,并行队列可以同时执行多个线程。
同步,异步: 是针对线程行为的,指明线程的执行是否要等到任务返回后再往下执行。
线程安全: 指代码在多线程或者并发任务下能够被安全调用,而不会引起任何问题

1.2 iOS多线程实现方案对比

iOS 多线程方案有如下几种:

  • pthread
    语言: C 语言
    优点:跨平台,可移植
    缺点:需要自己管理线程生命周期,所以用得比较少

  • NSThread
    语言: OC 语言
    优点:是针对pthread的面向对象的封装
    缺点:需要自己管理线程生命周期,使用上还是显得比较麻烦,一般用于查看当前线程状态等不涉及线程周期的场景。

  • GCD
    语言: C 语言
    优点:能够充分发挥多核的特性,自动管理线程生命周期不需要手动管理

  • NSOperation
    语言: OC 语言
    优点:基于GCD 底层的面向对象封装,添加了线程依赖,并发数控制等功能

1.3 NSThread的使用

NSThread的创建:

/* 
param 1:要执行的方法,
param 2:提供selector的对象,通常是self,
param 3:传递给selector的参数
这种方式是默认start的
*/

[NSThread detachNewThreadSelector:(nonnull SEL)> toTarget:(nonnull id) withObject:(nullable id)]

/*
param 1:提供selector的对象,通常是self
param 2:要执行的方法
param 3:传递给selector的参数
*/

NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(doSomething) object:nil];

/*
param 1:调用的方法
param 2:传给selector方法的参数
*/

- (void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg

NSThread常见属性:

//只读属性,线程是否在执行
thread.isExecuting;
//只读属性,线程是否被取消
thread.isCancelled;
//只读属性,线程是否完成
thread.isFinished;
//是否是主线程
thread.isMainThread;
//线程的优先级,取值范围0.0到1.0,默认优先级0.5,1.0表示最高优先级,优先级高,CPU调度的频率高
thread.threadPriority;
//线程的堆栈大小,线程执行前堆栈大小为512K,线程完成后堆栈大小为0K
thread.stackSize;

NSThread常用方法:

[thread start]; 启动线程
[NSThread exit]; 退出线程
[NSThread isMainThread]; 当前线程是否为主线程
[NSThread isMultiThreaded]; 是否多线程
[NSThread mainThread]; 返回主线程的对象
[NSThread currentThread];(1 表示主线程,其他表示后台线程)
[NSThread sleepUntilDate:[NSDate date]]; (休眠到指定时间)
[NSThread sleepForTimeInterval:4.5]; (休眠指定时长)
1.4 GCD 的使用

使用GCD 需要明确:需要执行哪些操作,要投递到哪种分发队列,怎么执行这些任务串行还是并行。它有两个核心概念“任务”和“队列”,我们只需专注于想要执行的“任务” block,然后添加到适当的“队列”中,剩余的多线程生命周期管理都是GCD来替我们完成。

1.4.1 GCD 的队列类型

GCD 有两大类队列:

  • 串行队列(Serial Dispatch Queue):

串行队列每次只能执行一个任务,但是在应用中可以创建多个串行队列

dispatch_queue_t queue = dispatch_queue_create(“com.idealist.test”, DISPATCH_QUEUE_SERIAL);

iOS 默认创建的主线程就是串行队列,获取串行队列可以通过如下方法获取:

dispatch_get_main_queue()

串行队列的一个很重要的用途就是用于解决数据竞争,因为在串行队列中两个任务不可能并发运行,所以就没有可能会同时访问同一个临界区的风险。所以仅对于这些任务而言,这种运行机制能够保护临界区避免发生竟态条件

  • 并行队列(Concurrent Dispatch Queue):

并行队列和散弹枪一样每次可以同时执行多个任务,但是在系统中对同时执行的任务数是有限制的,这取决于CPU核数以及CPU负载等因素决定。

dispatch_queue_t queue = dispatch_queue_create(“com.idealist.test”, DISPATCH_QUEUE_CONCURRENT);

和串行队列一样iOS系统为创建并行队列增加了全局并行队列:

dispatch_get_global_queue()

全局队列有四个优先级:

DISPATCH_QUEUE_PRIORITY_HIGHT       高优先级
DISPATCH_QUEUE_PRIORITY_DEFAULT 默认先级
DISPATCH_QUEUE_PRIORITY_LOW 低先级
DISPATCH_QUEUE_PRIORITY_BACKGROUND 后台优先级

1.4.2 GCD 任务组 (dispatch_group)
  • “对于并行队列,以及多个串行、并行队列混合的情况我们如何知道所有任务都已经执行完了”
  • “如何在某些任务执行完毕后,执行一个操作“

遇到这种情况我们就可以使用GCD 任务组来解决,GCD 任务组 能够在任务组中的任务执行完毕后,执行某个任务。

//创建调度组
dispatch_group_t group = dispatch_group_create();
//将调度组添加到队列,执行 block 任务
dispatch_group_async(group, queue, block);
//阻塞当前线程,等待 group 关联的所有 block 执行完毕或者到达指定时间。如果到达指定时间后,所有任务并没有全部完成,那么 dispatch_group_wait 返回一个非 0 的数,可以根据这个返回值,判断是否等待超时。如果设置为 DISPATCH_TIME_FOREVER ,意思是永远等待,直到所有 block 执行完毕
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
//当调度组中的所有任务执行结束后,获得通知,统一做后续操作
dispatch_group_notify(group, dispatch_get_main_queue(), block);

//将任务放到任务组
void dispatch_group_enter(dispatch_group_t group);
//将任务从任务组取出
void dispatch_group_leave(dispatch_group_t group);
dispatch_group_async(group, queue, ^{ 
  // block
});

等价于

dispatch_group_enter(group);
dispatch_async(queue, ^{
  // block
  dispatch_group_leave(group);
});

dispatch_group 例子:

dispatch_group_t group =  dispatch_group_create();
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 执行1个耗时的异步操作
});

dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 执行1个耗时的异步操作
});

dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// 等前面的异步操作都执行完毕后,回到主线程...
});

1.4.3 任务派发函数
dispatch_sync(queue, ^{
// 这里放同步执行任务代码
});

dispatch_async(queue, ^{
// 这里放异步执行任务代码
});

dispatch_sync 函数将一个任务添加到一个队列中,会阻塞当前线程,直到该任务执行完毕。dispatch_async 不会等待任务执行完,当前线程会继续往下走,不会阻塞当前线程。

在使用的时候需要特别注意不要往当前队列中使用dispatch_sync抛任务,这样很容易造成死锁。
1.4.4 GCD 停止和恢复
dispatch_suspend(queue) //暂停某个队列  
dispatch_resume(queue) //恢复某个队列
1.4.5 GCD 其他方法
  • GCD 栅栏方法:dispatch_barrier_async

要异步执行多组操作,且前一组操作执行完之后,才能开始执行后一组操作。这种情况就需要用到栅栏来隔离。

dispatch_queue_t queue = dispatch_queue_create("com.dnduuhn.test", DISPATCH_QUEUE_CONCURRENT);

dispatch_async(queue, ^{
// 追加任务1
});
dispatch_async(queue, ^{
// 追加任务2
});

dispatch_barrier_async(queue, ^{
// 追加任务 barrier
});

dispatch_async(queue, ^{
// 追加任务3
});
dispatch_async(queue, ^{
// 追加任务4
});
  • GCD 延时执行方法:dispatch_after

延迟一段时间后执行某个操作:

dispatch_after函数传入的时间参数,并不是指在这时间之后开始执行处理,而是在指定时间之后将任务追加到队列中。并且这个时间不是绝对准确时间,但是可以满足对时间不是很严格的延迟要求。

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

});

  • GCD 一次性代码:dispatch_once

使用 dispatch_once 函数能保证某段代码在程序运行过程中只被执行1次,并且即使在多线程的环境下,dispatch_once也可以保证线程安全。

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 只执行1次的代码(这里面默认是线程安全的)
});
  • GCD 快速迭代方法:dispatch_apply

dispatch_apply按照指定的次数将指定的任务追加到指定的队列中,并等待全部队列执行结束。

如果是在串行队列中使用 dispatch_apply,那么就和 for 循环一样,按顺序同步执行。可这样就体现不出快速迭代的意义了。
我们可以利用并发队列进行异步执行。比如说遍历 0~5 这6个数字,for 循环的做法是每次取出一个元素,逐个遍历。dispatch_apply 可以 在多个线程中同时(异步)遍历多个数字。还有一点,无论是在串行队列,还是异步队列中,dispatch_apply 都会等待全部任务执行完毕

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(6, queue, ^(size_t index) {
// 迭代任务
});
  • GCD 信号量: Dispatch Semaphore

信号量是持有计数的信号,使用它控制对有限资源的使用和访问。假设有一间房子,它对应一个进程,房子里的两个人就对应两个线程。这个房子(进程)有很多资源,比如花园、客厅、卫生间等,是所有人(线程)共享的。但是有些地方,比卫生间,最多只能有1个人能进去。怎么办呢,在卫生间门口挂1把钥匙。进去的人(线程)拿着钥匙进去(信号量 -1),外面的人(线程)没有钥匙就在门口等待,直到里面的人出来并把钥匙重新放回门口(信号量+1),此时外面等待的人再拿着这个钥匙进去,所有人(线程)就按照这种方式依次访问卫生间这个有限的资源。门口的钥匙数量就称为信号量(Semaphore)。信号量为0时需要等待,信号量不为零时,减去1而且不等待。


dispatch_semaphore_create:创建一个Semaphore并初始化信号的总量
dispatch_semaphore_signal:解锁,释放一个信号量,使得信号量值加1
dispatch_semaphore_wait:加锁,信号量减去1,当信号总量为0时就会阻塞所在线程,否则就可以正常执行。

例子:

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
for (int i = 0; i < 10; i++) {
dispatch_async(queue, ^{
//加锁 信号量 -1
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

//访问临界区数据

//解锁 信号量 +1
dispatch_semaphore_signal(semaphore);
});
}
1.5 NSOperation 的使用

下图中列举了NSOperation 以及NSOperationQueue的一些重要概念:

1.5.1 创建操作

调用Operation的start方法将会在当前线程中执行,将operation添加到Queue不用调用start方法,会自动调度操作在对应线程中运行

NSInvocationOperation:

// 1.创建 NSInvocationOperation 对象
NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(operation1) object:nil];
// 2.调用 start 方法开始执行操作
[invocationOperation start];

NSBlockOperation:

NSBlockOperation 是否开启新线程,取决于操作的个数。如果添加的操作的个数多,就会自动开启新线程。

// 1.创建 NSBlockOperation 对象
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
// do something
}];

// 2.调用 start 方法开始执行操作
[op start];

通过 addExecutionBlock 添加额外操作,这些操作(包括blockOperationWithBlock中的操作)可以在不同的线程中并发执行。只有当所有相关的操作已经完成执行时,才视为操作完成

// 1.创建 NSBlockOperation 对象
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
// do something
}];

// 2.添加额外的操作
[op addExecutionBlock:^{
// do additional something
}];

// 3.调用 start 方法开始执行操作
[op start];
}
1.5.2 创建操作队列

获取主队列,凡是添加到主队列中的操作,都会放到主线程中执行

NSOperationQueue *queue = [NSOperationQueue mainQueue];

自定义队列创建方法:添加到这种队列中的操作,就会自动放到子线程中执行

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
1.5.3 设置操作队列属性

设置最大并发数等

1.5.4 将操作添加到操作队列

addOperation

[operationQueue addOperation:op1];
[operationQueue addOperation:op2];
[operationQueue addOperation:op3];

addOperationWithBlock

[operationQueue addOperationWithBlock:^{
// do something
}]
;

1.5.4 设置操作间依赖,及优先级,启动操作

KVC — Key Value Coding 键值编码

KVC 是一种不用通过调用 setter、getter 方法而是直接通过属性字符串名称key来访问属性的机制。

KVC 和 属性访问器的对比如下:

  • KVC是通过在运行时动态的访问和修改对象的属性而访问器是在编译时确定,单就这一点增加了访问属性的灵活性,
    但是用访问器访问属性的时候编译器会做预编译处理,访问不存在的属性编译器会报错,使用KVC方式的时候如果有错误只能在运行的时候才能发现。

  • 相比访问器KVC 效率会低一点。

  • KVC 可以访问对象的私有属性,修改只读属性
  • KVC 在字典转模型,集合类操作方面有着普通访问器不能提供的功能

总而言之 KVC的特点是在运行时访问,可以访问对象私有属性和修改可读属性,在字典转模型,集合类操作方面十分便捷。

下面是KVC 关键用法的总结:

1. 设置和访问策略及访问私有属性和可读属性
#import <Foundation/Foundation.h>

@interface Teacher : NSObject
@property (nonatomic, strong, readonly) NSString *readonlyValue;
@end

#import "Teacher.h"

@interface Teacher ()

@property (nonatomic, assign ,readwrite) NSInteger age;
@property (nonatomic, strong ,readwrite) NSString *name;
@property (nonatomic, assign ,readwrite) BOOL male;
@property (nonatomic, assign ,readwrite) BOOL isTest;

@end

@implementation Teacher

- (NSString *)description {
return [NSString stringWithFormat:@"name = %@ \
age = %ld \
readOnlyValue = %@ \
is Male = %d",

self.name, (long)self.age, self.readonlyValue, self.male];
}

int main(int argc, const char * argv[]) {
@autoreleasepool {

Teacher *teacher = [Teacher new];
[teacher setValue:@"linxiaohai" forKey:@"name"];
NSLog(@"%@",teacher);
[teacher setValue:@"linxiaohai1" forKey:@"_name"];
NSLog(@"%@",teacher);

//修改NSInteger要转换成NSNumber*
[teacher setValue:@(29) forKey:@"age"];
NSLog(@"%@",teacher);

//修改BOOL要转换成NSNumber*
[teacher setValue:@(YES) forKey:@"male"];
NSLog(@"%@",teacher);
[teacher setValue:@(NO) forKey:@"_male"];
NSLog(@"%@",teacher);

//修改只读属性
[teacher setValue:@"This is a readOnly Value" forKey:@"readonlyValue"];
NSLog(@"%@",teacher);
[teacher setValue:@"This is a readOnly Value 2" forKey:@"_readonlyValue"];
NSLog(@"%@",teacher);

//如果没有找到将会按照_is<Key> isKey 方式继续查找
NSLog(@"Test: %d", [[teacher valueForKey:@"test"] boolValue]);
[teacher setValue:@(YES) forKey:@"test"];
NSLog(@"%@",teacher);
NSLog(@"Test: %d", [[teacher valueForKey:@"test"] boolValue]);
}
return 0;
}

2019-06-21 11:27:36.860598+0800 KVC-Demo[42518:5605320] name = linxiaohai age= 0 readOnlyValue = (null) is Male = 0
2019-06-21 11:27:36.860744+0800 KVC-Demo[42518:5605320] name = linxiaohai age= 29 readOnlyValue = (null) is Male = 0
2019-06-21 11:27:36.860916+0800 KVC-Demo[42518:5605320] name = linxiaohai age= 29 readOnlyValue = (null) is Male = 0
2019-06-21 11:27:36.861087+0800 KVC-Demo[42518:5605320] name = linxiaohai age= 29 readOnlyValue = (null) is Male = 1
2019-06-21 11:27:36.861213+0800 KVC-Demo[42518:5605320] name = linxiaohai age= 29 readOnlyValue = This is a readOnly Value is Male = 1
2019-06-21 11:27:36.861348+0800 KVC-Demo[42518:5605320] name = linxiaohai age= 29 readOnlyValue = This is a readOnly Value 2 is Male = 1
2019-06-21 11:27:36.861422+0800 KVC-Demo[42518:5605320] male: 1
2019-06-21 11:27:36.877241+0800 KVC-Demo[42518:5605320] ===================================================
2019-06-21 11:27:36.877501+0800 KVC-Demo[42518:5605320] Test: 0
2019-06-21 11:27:36.877765+0800 KVC-Demo[42518:5605320] name = linxiaohai age= 29 readOnlyValue = This is a readOnly Value 2 is Male = 1
2019-06-21 11:27:36.877824+0800 KVC-Demo[42518:5605320] Test: 1
2019-06-21 11:27:36.877867+0800 KVC-Demo[42518:5605320] ===================================================
  • (void)setValue:(id)value forKey:(NSString *)key 规则:
首先搜索 setter 方法,有就直接赋值。
如果上面的 setter 方法没有找到,再检查类方法+ (BOOL)accessInstanceVariablesDirectly
返回 NO,则执行setValue:forUNdefinedKey:
返回 YES,则按_<key>,_<isKey><key><isKey>的顺序搜索成员名。
还没有找到的话,就调用setValue:forUndefinedKey:
  • (id)valueForKey:(NSString *)key 规则
首先查找 getter 方法,找到直接调用。如果是 boolintfloat 等基本数据类型,会做 NSNumber 的转换。
如果没查到,再检查类方法+ (BOOL)accessInstanceVariablesDirectly
返回 NO,则执行valueForUNdefinedKey:
返回 YES,则按_<key>,_is<Key>,<key>,is<Key>的顺序搜索成员名。
还没有找到的话,调用valueForUndefinedKey:

keyPath 用法:

Teacher *teacher = [Teacher new];
Student *student = [Student new];
[student setValue:@"Jimmy" forKey:@"name"];
[teacher setValue:student forKey:@"student"];
NSString *studentName = [teacher valueForKeyPath:@"student.name"];
NSLog(@"Student name: %@",studentName);
[teacher setValue:@"linxiaohai" forKeyPath:@"student.name"];
2. 异常处理

如果都没找到对应的key就会调用setValue:forUndefinedKey: 和valueForUndefinedKey:
下面是一个例子:

增加
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
if([key isEqualToString:@"undefineKey"]) {
NSLog(@"=========> %@",value);
}
}

在调用[teacher setValue:@"valueForUndefineKey" forKey:@"undefineKey"] 的时候就会打出如下log

=========> valueForUndefineKey


增加:
- (id)valueForUndefinedKey:(NSString *)key {
return [NSString stringWithFormat:@"This is A default Value for %@",key];
}

在调用
NSLog(@"Test: %@", [teacher valueForKey:@"undefineKey"]); 就会打出如下Log:

Test: This is A default Value for undefineKey

其他API

- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
// 这是集合操作的API,里面还有一系列这样的API,如果属性是一个NSMutableArray,那么可以用这个方法来返回

// 如果你在setValue方法时面给Value传nil,则会调用这个方法
- (void)setNilValueForKey:(NSString *)key;
3. 字典转模型

使用字典来初始化模型

NSDictionary *catogiresDic = @{
@"id" :@1,
@"image":@"@www.iPhone.com",
@"name":@"iPhone",
@"price":@"100.03"};
CategoryList *catogires = [[CategoryList alloc] init];
[catogires setValuesForKeysWithDictionary:catogiresDic];

通过key的集合来从一个model中取出值形成一个字典

NSDictionary *value = [catogires dictionaryWithValuesForKeys:@[@"name",@"image"]];
4. 集合操作

相比于普通对象NSArray 以及 NSSet 在使用上我们需自己实现一遍下面的方法:

//必须实现,对应于NSArray的基本方法count: 
-countOf<Key>
//这两个必须实现一个,对应于 NSArray 的方法 objectAtIndex: 和 objectsAtIndexes:

-objectIn<Key>AtIndex:
-<Key>AtIndexes:

//两个必须实现一个,类似 NSMutableArray 的方法 insertObject:atIndex: 和 insertObjects:atIndexes:

-insertObject:in<Key>AtIndex:
-insert<Key>:atIndexes:

//两个必须实现一个,类似于 NSMutableArray 的方法 removeObjectAtIndex: 和 removeObjectsAtIndexes:

-removeObjectFrom<Key>AtIndex:
-remove<Key>AtIndexes:

KVC对于数组而言最大的功能还是获取集合类的 count,max,min,avg,sum 这是一个很好用的功能

NSLog(@"count of book price : %@",[student valueForKeyPath:@"bookList.@count.price"]);
NSLog(@"min of book price : %@",[student valueForKeyPath:@"bookList.@min.price"]);
NSLog(@"avg of book price : %@",[student valueForKeyPath:@"bookList.@max.price"]);
NSLog(@"sum of book price : %@",[student valueForKeyPath:@"bookList.@sum.price"]);
NSLog(@"avg of book price : %@",[student valueForKeyPath:@"bookList.@avg.price"]);

对象运算符
通过这个特性能够一个对象集合的所有某个特定字端的值组成一个数组

一共有两种:

@distinctUnionOfObjects
@unionOfObjects

它们的返回值都是NSArray,区别是前者返回的元素都是唯一的,是去重以后的结果;后者返回的元素是全集。例子如下:

NSArray* arrDistinct = [arrBooks valueForKeyPath:@"@distinctUnionOfObjects.price"];
NSArray* arrUnion = [arrBooks valueForKeyPath:@"@unionOfObjects.price"];
5. 键值验证

KVC提供了验证Key对应的Value是否可用的方法,但是这个方法不会自动调用,必须在使用的时候手动调用:

- (BOOL)validateValue:(inoutid*)ioValue forKey:(NSString*)inKey error:(outNSError**)outError;

如下例子:

- (BOOL)validateValue:(inout id  _Nullable __autoreleasing *)ioValue forKey:(NSString *)inKey error:(out NSError * _Nullable __autoreleasing *)outError {
NSNumber *age = *ioValue;
if (age.integerValue == 10) {
return NO;
}
return YES;
}

NSNumber *age = @10;
NSError* error;
NSString *key = @"age";
BOOL isValid = [test validateValue:&age forKey:key error:&error];
KVO — Key Value Observer 键值观察
KVO 的使用

KVO 适合任何对象监听另一个对象的改变,这是一个对象与另外一个对象保持同步的一种方法。KVO 只能对属性做出反应,不会用来对方法或者动作做出反应

为某个对象的属性添加观察者:

[_person addObserver:self
forKeyPath:@"age"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionPrior
context:nil];

回调方法

- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSString *,id> *)change
context:(void *)context {
NSLog(@"%@对象的%@属性改变了:%@",object,keyPath,change);
}

移除观察者

[self.person removeObserver:self forKeyPath:@"age"];

这里比较重要的是option这个参数:

NSKeyValueObservingOptionNew = 0x01 change字典包括改变后的值
NSKeyValueObservingOptionOld = 0x02 change字典包括改变前的值
NSKeyValueObservingOptionInitial = 0x04 注册后立刻触发KVO通知
NSKeyValueObservingOptionPrior = 0x08
如果指定,则在每次修改属性时,会在修改通知被发送之前预先发送一条通知给观察者,
这与-willChangeValueForKey:被触发的时间是相对应的。
这样,在每次修改属性时,实际上是会发送两条通知。
设置相互关联的属性

假如有个 Person 类,类里有三个属性,fullName、firstName、lastName。这种情况如果需要观察名字的变化,就要分别添加 fullName、firstName、lastName 三次观察,非常麻烦。通过设置相互关联的属性就会在关联的属性发生变化的时候,另外的属性也受到通知。

@interface Person : NSObject

@property (nonatomic, strong) NSString *firstName;
@property (nonatomic, strong) NSString *lastName;
@property (nonatomic, strong) NSString *fullName;

@end

@implementation Person

+ (NSSet<NSString *> *)keyPathsForValuesAffectingFullName {
return [NSSet setWithObjects:@"firstName",@"lastName", nil];
}

@end

[_person addObserver:self
forKeyPath:@"fullName"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionPrior
context:nil];

[_person setValue:@"FistName" forKey:@"fistName"];
KVO 原理:
KVO 是通过 isa-swizzling 实现的,当某个类的对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中做如下的几方面工作:

1. 重写基类中任何被观察属性的 setter 方法,而通过重写就获得了 KVO 需要的通知机制

- (void)setName:(NSString *)newName {
[self willChangeValueForKey:@"name"]; // KVO在调用存取方法之前总调用
[super setValue:newName forKey:@"name"]; // 调用父类的存取方法
[self didChangeValueForKey:@"name"]; // KVO在调用存取方法之后总调用
}

KVO 在调用存取方法之前总是调用 willChangeValueForKey:,通知系统该 keyPath 的属性值即将变更。 当改变发生后,didChangeValueForKey: 被调用,通知系统该 keyPath 的属性值已经变更。 之后,observeValueForKey:ofObject:change:context: 也会被调用。

2. 让这个重写的类称为原来类的派生类:

除了完成上面提到的第一步操作的同时派生类还重写了class 方法以“欺骗”外部调用者它就是起初的那个类。然后系统将这个对象的 isa 指针指向这个新诞生的派生类,因此这个对象就成为该派生类的对象了,因而在该对象上对 setter 的调用就会调用重写的 setter,从而激活键值通知机制

3. 重写派生类的dealloc 方法释放资源

手动KVO (禁用自动KVO)
@interface Target : NSObject {
int age;
}

- (int) age;
- (void) setAge:(int)theAge;

@end

@implementation Target

- (id) init {
self = [super init];
if (nil != self) {
age = 10;
}
return self;
}

- (int) age {
return age;
}

- (void) setAge:(int)theAge {
[self willChangeValueForKey:@"age"];
age = theAge;
[self didChangeValueForKey:@"age"];
}

+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:@"age"]) {
return NO;
}
return [super automaticallyNotifiesObserversForKey:key];
}

@end
  1. 需要手动实现属性的 setter 方法,并在设置操作的前后分别调用 willChangeValueForKey: 和 didChangeValueForKey方法
  2. 实现类方法 automaticallyNotifiesObserversForKey,并在其中设置对该 key 不自动发送通知(返回 NO 即可)。这里要注意,对其它非手动实现的 key,要转交给 super 来处理。
查看某个对象有哪些属性被监听

如果我们想获取一个对象上有哪些观察者正在监听其属性的修改,则可以查看对象的observationInfo属性

id info = object.observationInfo;
NSLog(@"%@", [info description]);

一 . Category分类 Extension 扩展

分类是Objective-C 2.0之后添加的语言特性

它有如下功能:
  • 能够在不改变原来类内容的基础上,为类增加一些方法
  • 将类的实现分开写在几个分类里面
    • 可以把不同的功能组织到不同的Category里,从而减少单个文件的体积
    • 可以由多个开发者共同完成一个类
    • 可以按需加载想要的category
    • 可以声明类的私有方法
    • 可以模拟多继承。
分类使用过程中需要注意的点:
  • 分类只能增加方法,不能增加成员变量。
  • 分类方法实现中可以访问原来类中声明的成员变量。
  • 分类可以实现与原来类中相同的方法,但是这样做会覆盖掉原来的方法,实际上这里的覆盖和覆写类方法的覆盖是有区别的,这里的覆盖不是真正的替换,而是Category的方法被放到了新方法列表的前面,而原来类的方法被放到了新方法列表的后面,而运行时在查找方法的时候是顺着方法列表的顺序查找的,只要一找到对应名字的方法,就会停止查找,导致原来类中的方法不会被使用。
  • 当分类、原来类、原来类的父类中有相同方法时,方法调用的优先级:分类(最后参与编译的分类优先) –> 原来类 –> 父类
  • Category是在runtime时候加载,而不是在编译的时候。
  • Category不能添加成员变量因为在运行期,对象的内存布局已经确定,如果添加实例变量会破坏类的内部布局),但是可以结合关联对象来为分类添加属性
  • 可以在类的+load方法调用category中声明的方法,因为添加category到类的工作会先于+load方法的执行,load执行循序是先主类,然后分类,分类的顺序取决于在Build Phases的顺序。

总结来说类别可以添加实例方法,类方法,甚至可以实现协议,添加属性,但是不可以添加实例变量

Extension与Category区别

刚刚开始接触Objective C的时候最容易搞混的就是扩展与分类,最初感觉扩展就是匿名的分类一样,但是后面才知道其实扩展和分类是两码事

  • Extension 扩展
    Extension是类的一部分,它和头文件的@interface和实现文件里的@implement一起形成了一个完整的类。伴随着类的产生而产生,也随着类的消失而消失。
    Extension一般用来放置类的私有变量,必须有一个类的源码才能添加一个类的Extension

  • Category 分类
    是运行期决议的,类扩展可以添加实例变量,分类不能添加实例变量

推荐文章

深入理解Objective-C:Category
objc category的秘密
Objective-C封装技巧(一):Category和继承的博弈
YYCategories
iOS-Category

二. 协议 Protocol

学过Java的同学都知道接口这个概念,在Objective C 中对应的被称为协议也就是Protocol,Protocol只能定义公用的一套接口,但不能提供具体的实现方法。也就是说,它只告诉你要做什么,但具体怎么做,它不关心。

协议的作用很简单就是对某个或者某个系列的对象的行为起约束作用,就像给一个模版,后续只有满足这个模版的对象才能传入。

协议可用定义在单独.h文件中,也可用定义在某个类中:
  • 如果这个协议只用在某个类中,应该把协议定义在该类中
  • 如果这个协议用在很多类中,就应该定义在单独文件中
协议中方法声明的关键字
@required (默认) 要求实现,如果没有实现,会发出警告,但不报错
@optional 不要求实现,不实现也不会有警告
导入协议
@protocol 协议名称    告诉下面代码这是一个协议,但不知道协议里面有什么东西
#import “协议文件名.h” 需要知道协议里面的具体方法的时候需要通过这种方式导入
遵循协议
类遵守协议(这里可以写在外部接口,也可以写在内部扩展部分)
@interface 类名 : 父类名 <协议名称1, 协议名称2>

@end

@interface 类名()<协议名称1, 协议名称2>

@end

协议遵守协议

@protocol 协议名称 <其他协议名称1, 其他协议名称2>

@end
使用协议约束属性或者变量
@property中声明的属性也可用做一个遵守协议的限制
@property (nonatomic, strong) 类名<协议名称> *属性名;
@property (nonatomic, strong) Dog<MyProtocol> *dog;
@property (nonatomic, strong) id<协议名称> 属性名;
@property (nonatomic, strong) id<MyProtocol> dog2;
协议可以声明属性,但是不能添加实例变量

和分类一致的是协议也可以声明属性,但是一般很少这样做,一般协议中都是用于声明方法的,下面是协议声明属性的用法:

@protocol IDLTestProtocol <NSObject>

@property(nonatomic, strong, readwrite) NSString *test;

@end


@interface TestObject : NSObject<IDLTestProtocol>

@end


@implementation TestObject

@synthesize test/* 这个是供类外部使用的名称*/ = _helloWorld/* 这个是供类内部使用的名称*/;

- (instancetype)init {
self = [super init];
if (self) {
_helloWorld = @"linxiaohai";
}
return self;
}

@end

测试代码

TestObject *testObject = [[TestObject alloc] init];
IDLLogError(@"========>%@",testObject.test);

1. 内存区域分布

堆操作:
操作系统中有一个存放堆内空闲存储块地址和大小的链表,当程序员申请空间的时候,系统就会遍历整个链表,找到第一个比申请空间大的空闲块节点,系统会将该空闲块从空闲链表中删除,分配给程序,由于申请的空间不一定与找到的空闲块大小相同,多出来剩余的空闲区会被系统重新添加到空闲链表中。当我需要删除对象时,便会根据指针纪录的地址,将这一块区域重新加入到链表中

栈操作:
栈区的内存是系统自动申请的而且是有序的。我们在申请栈空间时就只能在栈的顶部进行申请,当程序执行某个方法(或者函数)时,会从内存中栈(stack)的区域分配出一块内存空间,这个内存空间被称之为帧(frame)用来储存程序在这个方法内声明的变量的值。当应用启动并运行 main 函数时,它的帧会被存在栈的底部。当 main 继续调用另外一个方法时,这个方法的帧又会继续被压入栈的顶部。被调用的方法还可以再调用其他方法,以此类推,会有帧继续被压入栈顶,在被调用的方法结束后,程序会将其帧从栈顶释放。

2. iOS 引用计数内存管理策略

引用计数是一种内存管理技术,是指将资源(可以是对象、内存或磁盘空间等等)的被引用次数保存起来,当被引用次数变为零时就将其释放的过程。从而实现资源自动管理的目的。它的做法是:当创建一个对象的实例并在堆上申请内存时,对象的引用计数就为1,在其他对象中需要持有这个对象时,就需要把该对象的引用计数加1,需要释放一个对象时,就将该对象的引用计数减1,对象的引用计数为0时对象的内存会被立刻释放。

* 当程序调用方法名以alloc、new、copy、mutableCopy开头的方法来创建对象时,该对象的引用计数加1,这种情况我们将拥有所创建的这个对象。
* 当有一个新的指针指向这个对象时(或者调用retain方法时),我们将其引用计数加 1,接收到此调用的对象通常保证在他接收到retain所在的方法中保持有效。
* 除了以alloc、new、copy、mutableCopy 开头的方式创建对象外,其他方式创建的对象都是会被添加到AutoReleasePool,该对象的引用计数不会+1,这种情况下我们不用负责释放对象。
* 当某个指针不再指向这个对象时(或者调用release方法时),我们将其引用计数减 1
* 当对象的引用计数变为 0 时,说明这个对象不再被任何指针指向了,这个时候我们就可以将对象销毁,回收内存。这时候会调用该对象的dealloc方法。

* 对于数组类型其引用计数是会自动的相应变化的:
1. 当一个对象被添加进数组时 ,对象的引用计数也会相应的增加。
2. 数组移除指定的对象或者时所有对象,其被移除的对象会 release
3. 当数组销毁时,所有对象均会 release。
3. iOS开发中的内存管理四个黄金法则
* 自己生成的对象,自己持有
* 非自己生成的对象,自己也能持有
* 不再需要自己持有的对象的时候,释放
* 非自己持有的对象无法释放
4. 有关引用计数的方法:
* —retain:将该对象的引用计数器加1,从而持有该对象,但是并不拥有对象的释放权利。
* —release:将该对象的引用计数器减1,注意只有在计数为0的时候才会释放,而不是说一旦release就释放。
* —autorelease:调用 autorelease 后,对象不会被立即释放,而是注册到 autoreleasepool 中,经过一段时间后 pool结束,此时调用release方法,引用计数减1。
* —retainCount:返回该对象的引用计数的值。
* dealloc: 当一个对象一个拥有者都没有的话,dealloc就会被自动调用,dealloc方法的角色是释放对象自己的内存,并且销毁他所拥有的资源,包括所有对象变量的拥有权。
5. iOS中的变量标识符 & 属性标识符

变量标识符

__strong                持有强引用的变量在超出其作用域时被废弃,随着强引用的失效,引用的对象会随之释放,从另一个角度讲只要还有一个强指针指向某个对象,这个对象就会一直存活
__weak 弱引用虽然持有对象,但是并不增加引用计数,这样就避免了循环引用的产生,如果对象没有被其他对象强引用,弱引用会被置为 nil,弱引用的实现原理是这样:
系统对于每一个有弱引用的对象,都维护一个表来记录它所有的弱引用的指针地址。这样,当一个对象的引用计数为 0 时,系统就通过这张表,找到所有的弱引用指针,
继而把它们都置成 nil
__unsafe_unretained 它和__weak有点类似,只不过在没有被其他对象强引用的时候它不会被置为 nil。如果它引用的对象被回收掉了,该指针就变成了野指针。
__unsafe_unretained修饰符的变量不属于编译器的内存管理对象,赋值时即不获得强引用也不获得弱引用。

__autoreleasing 替代autorelease方法

属性标识符

@property (assign/retain/strong/weak/unsafe_unretained/copy) PropertyType* propertyType

* assign 表明 setter 仅仅是一个简单的赋值操作,没有持有不持有这一说,通常用于基本的数值类型
* strong 表明属性定义一个持有者关系。当给属性设定一个新值的时候,首先对旧值进行 release ,对新值进行retain 然后进行赋值操作。
* weak 表明属性定义了一个持有者关系。当给属性设定一个新值的时候,这个值不会进行 retain,旧值也不会进行 release, 而是进行类似 assign 的操作。
不过当属性指向的对象被销毁时,该属性会被置为nil
* unsafe_unretained 的语义和 assign 类似,不过是用于对象类型的,表示一个非拥有(unretained)的,同时也不会在对象被销毁时置为nil的(unsafe)关系。
* copy 类似于 strong,不过在赋值时进行 copy 操作而不是 retain 操作。通常在需要保留某个不可变对象,并且防止它被意外改变时使用。

概括得讲:
strong 和 copy都会持有对象,一个是持有对象的本身,一个是持有对象的副本。
weak,unsafe_unretained 更像一个旁观者,它们不会对数据的引用计数起到任何的改变,它看着对象被持有,被销毁却无能为力,只不过weak会在对象被销毁的时候会将其置为nil。而unsafe_unretained不会,unsafe_unretained 在开发中用得比较少, 如果对性能有极高的要求方可以考虑使用 unsafe_unretained 替换 weak,因为weak 其实对性能还是有影响的,只不过少量使用的时候是不会察觉到的,但是在类似YYModel这种序列化,反序列化库如果大量使用weak,肯定会对性能有较大的影响,weak的最主要作用就是解决循环引用的问题。这个会在后面做介绍,其实这个已经在Block总结的时候已经介绍过了。

6. ARC规则

与Java 中 GC 不同,ARC 是编译器特性,而不是基于运行时的,ARC 背后的原理是依赖编译器的静态分析能力,通过在编译时找出合理的插入引用计数管理代码,而不是实时监控与回收内存。

需要注意的是ARC 所做的事情并不仅仅局限于在编译期找到合适的位置帮你插入合适的 release 等等这样的内存管理方法,其在运行时期也做了一些优化,比如:

  • 合并对称的引用计数操作。比如将 +1/-1/+1/-1 直接置为 0.
  • 巧妙地跳过某些情况下 autorelease 机制的调用。
    当返回值被返回之后,紧接着就需要被 retain 的时候,没有必要进行 autorelease + retain,直接什么都不要做就好了。

ARC 打开的情况下有如下限制:

* 不能使用retain/release/retainCount/autorelease
* 不能使用NSAllocateObject/NSDeallocateObject
* 须遵守内存管理的方法命名规则
* 不要显式调用dealloc
* 使用@autoreleasepool块替代NSAutoreleasePool
* 不能使用NSZone
* 对象型变量不能作为C语言结构体(struct、union)的成员: 要把对象类型添加到结构体成员中,可以强制转换为void *或是附加__unsafe_unretained修饰符。
7. 内存相关常见问题

内存问题有两种:

  • 释放得太早,还在使用中就释放:
如果某个对象有至少一个拥有者,那么就必须保留不能释放,否则的话其他对象或者方法仍然有指向这个对象的指针沦为野指针(空指针)。这称之为过早释放,这是十分危险的,因为当野指针指向的内存区域再次被某个新的对象使用时,野指针上的操作便会破坏这个新对象造成文件丢失或者崩溃。
  • 释放得太晚,已经不用了但是还没释放:
如果某个对象失去了拥有者(变成没有拥有者)那么应该将其释放掉,否则没有拥有者的对象会被孤立而程序找不到,并且始终占用着一块内存,导致内存泄漏
7.1 内存泄漏

ARC内存泄露常见场景:

  • 对象型变量作为C语言结构体,或者联合体(struct、union)的成员
struct Data {
NSMutableArray __unsafe_unretained *array;
};

__unsafe_unretained修饰符的变量不属于编译器的内存管理对象。如果管理时不注意赋值对象的所有者,便可能遭遇内存泄露或者程序崩溃。

  • 循环引用

循环引用常见有如下几种情况:

  1. 两个对象互相持有对象,这个可以设置弱引用解决,最常见的是block,但是需要注意并非所有的block都需要使用weak来打破循环引用,如果self没有持有block就不会造成循环引用。而有些地方之所以使用了__weak,是为了在[self dealloc]之后就不再执行了。

解决方案 1:在block外部对弱化self,在block内部强化已经弱化的weakSelf

@interface Test: NSObject {
id __weak obj_;
}

- (void)setObject:(id __strong)obj;
block持有self对象,这个要在block块外面和里面设置弱引用和强引用。

__weak __typeof(self) wself = self;
obj.block = ^{
__strong __typeof(wself) sself = wself;
[sself updateSomeThing];
}

解决方法 2: 通过将对象在block中设置为nil,但是这种需要注意的是block一定要被执行

__block TestObject *object = [[TestObject alloc] init…];
object.completionHandler = ^(NSInteger result) {
[object testMethod];
object = nil;
};
  1. NSTimer的target持有self
self.timmer = [NSTimer scheduledTimerWithTimeInterval:1.0 
target:self
selector:@selector(updateTime:)
userInfo:nil
repeats:YES];

NSTimer会造成循环引用,timer会强引用target即self,一般self又会持有timer作为属性,这样就造成了循环引用。
如果timer只作为局部变量,不把timer作为属性同样释放不了,因为在加入runloop的操作中,timer被强引用。而timer作为局部变量,是无法执行invalidate的,所以在timer被invalidate之前,self也就不会被释放。

解决方案:在恰当时机调用[timer invalidate]即可,这个需要根据业务来自己决定,但是放在dealloc中调用是无效的,因为循环引用的情况下dealloc是不会被调用的,所以[timer invalidate]也就不会被调用。

还有下面几种定时相关的情形也需要注意:

__weak __typeof(self) wself = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[wself commentAnimation];
});

__weak __typeof(self) wself = self;
timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC, 1 * NSEC_PER_SEC);
dispatch_source_set_event_handler(timer, ^{
[wself commentAnimation];
});

dispatch_resume(timer);
  1. 代理delegate

代理在一般情况下,需要使用weak修饰,我们常见的delegate 一般会是VC的属性,被VC持有,同时我们会将VC相关的属性作为delegate从而导致循环引用。
解决方案:delegate属性使用weak修饰

  1. NSNotification

使用block的方式增加notification,引用了self,在删除notification之前,self不会被释放

解决方案:在block内部使用弱引用解决

  • 对象被单例持有

我们在单例里面设置一个对象的属性,因为单例是不会释放的,所以单例会有一直持有这个对象的引用。

[Instanse shared].obj = self;
  • CF类型内存

注意以creat,copy作为关键字的函数都是需要释放内存的.

8. 内存泄漏的排查方法
  • 静态分析方法(Analyze)
  • 动态分析方法(Instrument工具库里的Leaks,Allocations)
  • 在可疑对象的dealloc方法中添加log进行查看
  • 使用三方开源库:

MLeaksFinder
PLeakSniffer
FBRetainCycleDetector
FBAllocationTracker
FBMemoryProfiler
介绍FBRetainCycleDetector,FBAllocationTracker,FBMemoryProfiler的文章

9. AutoreleasePool 与 RunLoop的关系

主线程的AutoreleasePool会在RunLoop进入的时候重新建立一个,在RunLoop退出休眠状态的时候也会进行释放后重新建立一个。在退出RunLoop的时候释放AutoreleasePool,具体见RunLoop总结

10. weak-strong dance

在7.1 介绍内存泄漏类型时候提到循环引用的一种解决方案是在block外部对弱化self,在block内部强化已经弱化的weakSelf,这也就是这里所说的 weak-strong dance,block外部对弱化self是为了避免循环引用,而在block内部强化已经弱化的weakSelf是为了避免外部_weak导致在运行block的时候self被释放。

Block源码地址

1. Block实质:

Block从C语言角度实质上是能够捕获上下文变量的匿名函数,在创建的时候会捕获所需要的上下文局部自动变量到闭包内部。Block的底层是作为C语言源代码来处理的,支持Block的编译器会将含有Block语法的源代码转换为C语言编译器能处理的源代码,当作C语言源码来编译。Block和__block 最终都会转换为一个C语言的结构体对象。

Block 在OC中的实现如下

struct Block_layout {
void *isa;
int flags;
int reserved;
void (*invoke)(void *, ...);
struct Block_descriptor *descriptor;
/* Imported variables. */
};

struct Block_descriptor {
unsigned long int reserved;
unsigned long int size;
void (*copy)(void *dst, void *src);
void (*dispose)(void *);
};

Block 需要了解如下几个方面:

(1) 我们怎么把带有Block的 Objective C 代码转化为 C 代码
(2) 转换后的代码结构是怎样的
(3) Block对局部自动变量,局部静态变量,全局变量,对象,block变量的捕获
(4) Block 与
block 的存储特性
(5) Block的循环引用
(6) Block的内存布局

我们使用的转换block的命令行如下:

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp

一个简单常见的例子:

#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
@interface Person : NSObject
@property(nonatomic) int testProperty;
@end

@implementation Person
@end

int globleIntValue = 2;
static int staticGlobleIntValue = 5;
int main(int argc, char * argv[]) {
int intValue = 5;
static int staticIntValue = 3;
__block int blockInt = 5;
Person *person = [Person new];
void (^blockTest)(int) = ^ (int value){
printf("Hello %d",value);
printf("Hello %d",intValue);
printf("Hello %d",staticIntValue);
printf("Hello %d",globleIntValue);
printf("Hello %d",staticGlobleIntValue);
printf("Hello %d",person.testProperty);
blockInt = 34;
};
blockTest(2222);
return 0;
}

转换后的代码:

int globleIntValue = 2;
static int staticGlobleIntValue = 5;

struct __Block_byref_blockInt_0 {
void *__isa;
__Block_byref_blockInt_0 *__forwarding;
int __flags;
int __size;
int blockInt;
};

struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int intValue;
int *staticIntValue;
Person *person;
__Block_byref_blockInt_0 *blockInt; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _intValue, int *_staticIntValue, Person *_person, __Block_byref_blockInt_0 *_blockInt, int flags=0) : intValue(_intValue), staticIntValue(_staticIntValue), person(_person), blockInt(_blockInt->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself, int value) {
__Block_byref_blockInt_0 *blockInt = __cself->blockInt; // bound by ref
int intValue = __cself->intValue; // bound by copy
int *staticIntValue = __cself->staticIntValue; // bound by copy
Person *person = __cself->person; // bound by copy

printf("Hello %d",value);
printf("Hello %d",intValue);
printf("Hello %d",(*staticIntValue));
printf("Hello %d",globleIntValue);
printf("Hello %d",staticGlobleIntValue);
printf("Hello %d",((int (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("testProperty")));
(blockInt->__forwarding->blockInt) = 34;
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->person, (void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);_Block_object_assign((void*)&dst->blockInt, (void*)src->blockInt, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);_Block_object_dispose((void*)src->blockInt, 8/*BLOCK_FIELD_IS_BYREF*/);}

static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
int main(int argc, char * argv[]) {

int intValue = 5;
static int staticIntValue = 3;

__attribute__((__blocks__(byref))) __Block_byref_blockInt_0 blockInt = {(void*)0,(__Block_byref_blockInt_0 *)&blockInt, 0, sizeof(__Block_byref_blockInt_0), 5};

Person *person = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("new"));

void (*blockTest)(int) = ((void (*)(int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, intValue, &staticIntValue, person, (__Block_byref_blockInt_0 *)&blockInt, 570425344));
((void (*)(__block_impl *, int))((__block_impl *)blockTest)->FuncPtr)((__block_impl *)blockTest, 2222);

return 0;
}

2. Block定义

Block类型变量,一般结合用typedef定义:

typedef int (^blockType)(int,int)
@property (nonatomic, asign, readonly) blockType block;

Block 变量可以作为自动变量,函数参数,函数返回值,静态局部变量,静态全局变量,全局变量
Block 定义:
block定义和普通的C语言函数定义类似,只不过多了一个^省去函数名称

^返回值类型 (参数列表) {
表达式
}

3. __block_impl 结构体

struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
}
void (^blk)(void) = ^ {

}

转换成:

void (*blk)(void) = &__main_block_impl_0(__main_block_func_0,&__main_block_desc_0_DATA,auto_peram1,auto_peram2);

blk() 实际上执行的是

(*blk->impl.FuncPtr)(blk);

一个block的组成:

struct __block_impl impl;
struct __main_block_desc_0 *Desc;
---->
捕获的自动变量和局部静态变量指针

4. Block的变量捕获

  • block接受的参数属于值传递,可以在block内修改,不对其进行捕获

  • 局部变量的捕获: 当定义block的时候,Block中所使用的自动变量值被保存到Block的结构体实例中,也就是Block自身中,并且保存后就不能改写该值。但是还是可以调用变更该对象的方法。比如NSMutableArray 的 addObject

  • static局部变量的捕获: 局部静态变量 作用域在block之外,这种情况下block中存储的是静态局部变量的地址,所以当FuncPtr指向的函数调用的时候会通过取地址中存储的值

  • 全局变量的捕获
    全局变量并没有被block所捕获,因为全局变量存放在全局区,随时都可以访问,所以当FuncPtr指向的函数调用的时候就会直接取全局变量的值使用。而局部变量超过作用域就会自动回收所以block需要在自身存放一份,以保证其能准确访问。

  • 对象类型的捕获:
    在没有调用copy的情况下,还没有调用block()的时候对象就已经释放了,说明在栈上的block并没有对所使用的对象强引用,对block 进行一次copy操作会发现在block没有release之前,所引用的对象没有被释放,所以堆上的block强引用了所使用的对象,对对象执行一次release之后,对象的引用计数依然没有成为0,因为block还引用着它。这是因为当对block进行copy操作的时候,block会执行内部的main_block_copy_0方法。main_block_copy_0方法执行_Block_object_assign根据变量的修饰符判断对捕获的对象的引用情况(retain或者弱引用)。而当block从堆中移除的时候,会调用与__main_block_copy_0对应的_Block_object_dispose函数,该函数会自动释放引用的auto变量。(栈上block访问了对象类型的auto变量的时候不会对其发生强引用)

block从栈上copy到堆上的时候,block内部会执行copy操作,_Block_object_assign函数回通过auto变量的修饰符判断发生强弱引用。block从堆中移除的时候,block内部会执行dispose,将引用的对象进行释放。

简而言之:
局部变量在block中使用的时候会被block捕获,auto变量是值捕获,而static变量是地址捕获。全部变量不会被捕获。当Block从栈复制到堆上时,block会对id类型的变量产生强引用

5. __block 说明符

在block中允许修改静态局部变量,静态全局变量和全局变量这几种类型,但是对于局部自动变量如果在block里面修改编译器会发出警告,这时候需要在局部自动变量之前添加block 说明符,block 可以指定任何类型的自动变量,通过__block修饰的变量将会变成一个结构体实例,只有这样这个值才能被block共享、并且不受栈帧生命周期的限制、在block被copy后,能够随着block复制到堆上

这时候被捕获的对象会转换为

struct __Block_byref_val_0 {
void *__isa;
__Block_byref_val_0 * __forwarding;
int __flags;
int __size;
// 这后面是__block变量
int val;
}

__main_block_impl_0中也会多出一个

__Block_byref_val_0 *val;

也就是说局部自动变量添加了block后该变量将会以Block_byref_val_0 形式被捕获

有时候block变量配置在堆上的情况下,也可以访问栈上的block变量,在这种情况下只要栈上的结构体实例成员变量forwarding指向堆上的结构体实例,那么不管是从堆上的block变量还是从栈上的block变量都能正确得访问。简而言之: block 变量结构体成员变量forwarding可以实现无论block变量配置在栈上还是在堆上都能正确得访问__block变量


block id strong obj = [[NSObject alloc] init]; 和 id __strong obj = [[NSObject alloc] init] 一样在block没有copy的时候是不持有的,copy后block会持有对象引用

id array = [[NSMutableArray alloc] init];
id __weak array2 = array;

array2 是弱引用,当变量作用域结束,array 所指向的对象内存被释放,array2 指向 nil

如果 weak 再改成 unsafe_unretained ,__unsafe_unretained 修饰的对象变量指针就相当于一个普通指针。使用这个修饰符有点需要注意的地方是,当指针所指向的对象内存被释放时,指针变量不会被置为 nil。所以当使用这个修饰符时,一定要注意不要通过悬挂指针(指向被废弃内存的指针)来访问已经被废弃的对象内存,否则程序就会崩溃。

如果 unsafe_unretained 再改成 autoreleasing 会怎样呢?会报错,编译器并不允许你这么干!如果你这么写
block id autoreleasing obj = [[NSObject alloc] init];
编译器就会报下面的错误,意思就是 block 和 autoreleasing 不能同时使用。

6. Block 存储属性

Block 有如下几种存储类型:

_NSConcreteGlobalBlock 程序的数据区域

  • Block当作全局变量使用时(block 字面量写在全局作用域时)
  • 当 block 字面量不获取任何外部变量时(只使用全局变量以及block参数的时候)
    _NSConcreteGlobalBlock不持有对象

_NSConcreteStackBlock 栈

  • 除了上述两中情况下Block配置在程序的数据区中以外(换种说法如果只用到外部局部变量、成员属性变量,且没有强指针引用的block就是StackBlock),Block语法生成的Block为_NSConcreteStackBlock类对象,且设置在栈上。配置在栈上的Block,如果其所属的变量作用域结束,该Block就被自动废弃。_NSConcreteStackBlock不持有对象

_NSConcreteMallocBlock 堆

  • 有强指针引用或copy修饰的成员属性引用的block,配置在全局变量上的Block,从变量作用域外也可以通过指针访问。但是设置在栈上的Block,如果其所属的作用域结束,该Block就被废弃;并且block变量的也是配置在栈上的,如果其所属的变量作用域结束,则该block变量也会被废弃。那么这时需要将Block和__block变量复制到堆上,才能让其不受变量域作用结束的影响。_NSConcreteMallocBlock持有对象。

整个存储区域如下图所示:

通过上述结果我们可以看出当block访问了auto变量的时候会变成NSStackBlock类型。而其他情况下是NSGlobalBlock类型,比较典型的是Block声明在全局区域,以及Block虽然不声明在全局区域但是Block不截获自动变量。
NSGlobalBlock类型存在于数据区,NSStackBlock存在于栈区。

而在ARC环境下原本NSGlobalBlock的block依然是NSGlobalBlock类型,而原本是NSStackBlock却变成了NSMallocBlock存放在堆区。这是因为当我们定义block的时候ARC默认为我们做了一次copy操作。

在开启 ARC 时,大部分情况下编译器通常会将创建在栈上的 block 自动拷贝到堆上,只有当
block 作为方法或函数的参数传递时,编译器不会自动调用 copy 方法这时候需要手动调用copy,但是比如Cocoa框架的方法且方法名中包含usingBlock等,或者GCD API的情况下,这些在方法或者函数中对传递过来的参数做了复制操作所以不需要copy

copy 的结果:

如果原先存储域处于_NSConcreteGlobalBlock 那么什么都不做
如果原先存储域处于 _NSConcreteMallocBlock 那么将会导致引用计数增加
如果原先存储域处于_NSConcreteStackBlock 那么会将block从栈复制到堆

下面是Block复制和释放的源码:

Block 复制

 void *_Block_copy(const void *arg) {

//1. 声明一个Block_layout结构体类型的指针,如果传入的BlockNULL就直接返回。
struct Block_layout *aBlock;
if (!arg) return NULL;

// 如果Block有值就强转成Block_layout的指针类型。
aBlock = (struct Block_layout *)arg;
//如果Block的flags表明该Block为堆Block
if (aBlock->flags & BLOCK_NEEDS_FREE) {
//对block的引用计数递增后返回Block
latching_incr_int(&aBlock->flags);
return aBlock;
}
//如果Block为全局Block就不做其他处理直接返回
else if (aBlock->flags & BLOCK_IS_GLOBAL) {
return aBlock;
}
// 如果是堆栈类型就对block执行一次copy
else {
//分配空间,这里的控件大小为descriptor->size
struct Block_layout *result =
(struct Block_layout *)malloc(aBlock->descriptor->size);
if (!result) return NULL;

//将Block从栈上复制到堆上
memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first

#if __has_feature(ptrauth_calls)
// Resign the invoke pointer as it uses address authentication.
result->invoke = aBlock->invoke;
#endif
// reset refcount
//将新Block的引用计数置零。
result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING); // XXX not needed
result->flags |= BLOCK_NEEDS_FREE | 2; // logical refcount 1
_Block_call_copy_helper(result, aBlock);
// Set isa last so memory analysis tools see a fully-initialized object.
//将堆Block的isa指针置为_NSConcreteMallocBlock,返回新Block
result->isa = _NSConcreteMallocBlock;
return result;
}
}

Block 释放

void _Block_release(const void *arg) {

struct Block_layout *aBlock = (struct Block_layout *)arg;
if (!aBlock) return;

//如果是Globle类型的 Block 就不做任何操作
if (aBlock->flags & BLOCK_IS_GLOBAL) return;
//如果Block flags 标志位指示 block 不需要释放 就直接返回
if (! (aBlock->flags & BLOCK_NEEDS_FREE)) return;

//判断aBlock的引用计数是否需要释放内存
if (latching_decr_int_should_deallocate(&aBlock->flags)) {
//释放block
_Block_call_dispose_helper(aBlock);
_Block_destructInstance(aBlock);
free(aBlock);
}
}

简而言之:
访问了auto变量的block是NSStackBlock类型,没有访问auto变量的block是NSGlobalBlock类型。而对NSStackBlock类型进行copy操作就会变为NSMallocBlock类型。

在如下情况下栈上的block会被复制到堆上:

在调用Block 的copy 实例方法的时候
Block 作为函数返回值返回时
将Block赋给带有__strong修饰符id 类型的类或者Block类型成员变量时(block作为强指针引用的时候也会自动调用copy)
在方法名中含有usingBlock的Cocoa框架方法,或者GCD API中传递Block时候

在谁都不持有Block的时候block将会被释放

copy 函数会持有截获的对象,以及所使用的block变量,dispose函数会释放截获的对象以及block变量,
所以Block中使用的赋给赋有strong修饰符的自动变量的对象和复制到堆上的block变量由于被堆上的Block所持有,因而可以超出其变量作用域而存在。

再简而言之:也就是block 被copy到堆上的时候,它所使用的strong类型的对象以及__block变量都会超出作用域而存在。

  1. __block 变量存储属性

当Block从栈复制到堆上的时候,它所使用的所有block变量也会被复制到堆上,并被Block持有。在多个Block中使用block变量的时候,因为最先会将所有的Block配置在栈上,所以block变量最初也会配置到栈上,在任何一个Block从栈上复制到堆上的时候,block变量也会一起从栈复制到堆上,并被该Block 持有,当剩下的Block从栈复制到堆的时候,被复制的Block持有block变量,并增加block变量的引用计数。
如果配置在堆上的Block被废弃,那么它所使用的__block变量也会被释放。





  1. 避免循环引用:

如果用self引用了block,block又捕获了self,这样就会有循环引用。因此,需要用weak来声明self,如果捕获到的是当前对象的成员变量对象,同样也会造成对self的引用,同样也要避免。

使用__weak来声明self
- (instancetype)init {
self = [super init];
if (self) {
__weak typeof(self) weakSelf = self;
self.blk = ^{
NSLog(@"%@", weakSelf.name);
};
}
return self;
}
- (void)configureBlock {
id tmpIvar = _ivar; //临时变量,避免了self引用
self.block = ^{
[tmpIvar msg];
}
}

当struct第一次被创建时,它是存在于该函数的栈帧上的,其Class是固定的_NSConcreteStackBlock。其捕获的变量是会赋值到结构体的成员上,所以当block初始化完成后,捕获到的变量不能更改。

当函数返回时,函数的栈帧被销毁,这个block的内存也会被清除。所以在函数结束后仍然需要这个block时,就必须用Block_copy()方法将它拷贝到堆上。这个方法的核心动作很简单:申请内存,将栈数据复制过去,将Class改一下,最后向捕获到的对象发送retain,增加block的引用计数。