分类 IOS 下的文章

[!] CocoaPods could not find compatible versions for pod "multi_image_picker":
In Podfile:

multi_image_picker (from `.symlinks/plugins/multi_image_picker/ios`)

Specs satisfying the multi_image_picker (from .symlinks/plugins/multi_image_picker/ios) dependency were found, but they required a higher minimum deployment target.

pod 里的platform :ios, '8.0' 版本低了,改成9.0 即可

在向队列中添加任务时,可以直接在对应的函数中添加 block。但是如果想对任务进行操作,比如监听任务、取消任务,就需要获取对应的 block。
创建block

object-c
创建 block 有两种方式,第一种方式如下:
dispatch_block_t dispatch_block_create(dispatch_block_flags_t flags, dispatch_block_t block);

在该函数中,flags 参数用来设置 block 的标记,block 参数用来设置具体的任务。flags 的类型为 dispatch_block_flags_t 的枚举,用于设置 block 的标记,定义如下:
DISPATCH_ENUM(dispatch_block_flags, unsigned long,

DISPATCH_BLOCK_BARRIER = 0x1,
DISPATCH_BLOCK_DETACHED = 0x2,
DISPATCH_BLOCK_ASSIGN_CURRENT = 0x4,
DISPATCH_BLOCK_NO_QOS_CLASS = 0x8,
DISPATCH_BLOCK_INHERIT_QOS_CLASS = 0x10,
DISPATCH_BLOCK_ENFORCE_QOS_CLASS = 0x20,

);

创建 block 的另一种方式如下:
dispatch_block_t dispatch_block_create_with_qos_class(dispatch_block_flags_t flags,

dispatch_qos_class_t qos_class, int relative_priority,
dispatch_block_t block);

相比于 dispatch_block_create 函数,这种方式在创建 block 的同时可以指定了相应的优先级。dispatch_qos_class_t 是 qos_class_t 的别名,定义如下:

if __has_include(<sys/qos.h>)

typedef qos_class_t dispatch_qos_class_t;

else

typedef unsigned int dispatch_qos_class_t;

endif

而 qos_class_t 是一种枚举,有以下类型:

QOS_CLASS_USER_INTERACTIVE:user interactive 等级表示任务需要被立即执行,用来在响应事件之后更新 UI,来提供好的用户体验。这个等级最好保持小规模。
QOS_CLASS_USER_INITIATED:user initiated 等级表示任务由 UI 发起异步执行。适用场景是需要及时结果同时又可以继续交互的时候。
QOS_CLASS_DEFAULT:default 默认优先级
QOS_CLASS_UTILITY:utility 等级表示需要长时间运行的任务,伴有用户可见进度指示器。经常会用来做计算,I/O,网络,持续的数据填充等任务。这个任务节能。
QOS_CLASS_BACKGROUND:background 等级表示用户不会察觉的任务,使用它来处理预加载,或者不需要用户交互和对时间不敏感的任务。
QOS_CLASS_UNSPECIFIED:unspecified 未指明

事例:
dispatch_queue_t concurrentQuene = dispatch_queue_create("concurrentQuene", DISPATCH_QUEUE_CONCURRENT);

dispatch_block_t block = dispatch_block_create(0, ^{

NSLog(@"normal do some thing...");

});
dispatch_async(concurrentQuene, block);

//
dispatch_block_t qosBlock = dispatch_block_create_with_qos_class(0, QOS_CLASS_DEFAULT, 0, ^{

NSLog(@"qos do some thing...");

});
dispatch_async(concurrentQuene, qosBlock);

swift 3.0
swift 3.0 中使用了 DispatchWorkItem 对任务进行了封装。可以在 DispatchWorkItem 初始化方法中指定任务的内容和优先级。事例如下:
let concurrentQueue = DispatchQueue.init(label: "concurrentQueue", qos: .default, attributes: .concurrent, autoreleaseFrequency: .inherit, target: nil);

let block = DispatchWorkItem.init(block: {

print("normal do some thing...")

})
concurrentQueue.async(execute: block);

let qosBlock = DispatchWorkItem.init(qos: .default, flags: .noQoS, block: {

print("qos do some thing...")

})

监听 block 执行结束
有时我们需要等待特定的 block 执行完成之后,再去执行其他任务。有两种方法可以获取到指定 block 执行结束的时机。

object-c
在 object-c 中,第一种为使用下面的函数:
long dispatch_block_wait(dispatch_block_t block, dispatch_time_t timeout);

该函数会阻塞当前线程进行等待。传入需要设置的 block 和等待时间 timeout。timeout 参数表示函数在等待 block 执行完毕时,应该等待多久。如果执行 block 所需的时间小于 timeout,则返回 0,否则返回非 0 值。此参数也可以取常量 DISPATCH_TIME_FOREVER,这表示函数会一直等待 block 执行完,而不会超时。可以使用 dispatch_time 函数和 DISPATCH_TIME_NOW 常量来方便的设置具体的超时时间。
如果 block 执行完成,dispatch_block_wait 就会立即返回。不能使用 dispatch_block_wait 来等待同一个 block 的多次执行全部结束;这种情况可以考虑使用 dispatch_group_wait 来解决。也不能在多个线程中,同时等待同一个 block 的结束。同一个 block 只能执行一次,被等待一次。
注意:因为 dispatch_block_wait 会阻塞当前线程,所以不应该放在主线程中调用。
事例
dispatch_queue_t concurrentQuene = dispatch_queue_create("concurrentQuene", DISPATCH_QUEUE_CONCURRENT);

dispatch_async(concurrentQuene, ^{

dispatch_queue_t allTasksQueue = dispatch_queue_create("allTasksQueue", DISPATCH_QUEUE_CONCURRENT);

dispatch_block_t block = dispatch_block_create(0, ^{
    NSLog(@"开始执行");
    [NSThread sleepForTimeInterval:3];
    NSLog(@"结束执行");
});

dispatch_async(allTasksQueue, block);
// 等待时长,10s 之后超时
dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC));
long resutl = dispatch_block_wait(block, timeout);
if (resutl == 0) {
    NSLog(@"执行成功");
} else {
    NSLog(@"执行超时");
}

});

输入结果:

开始执行
结束执行
执行成功

swift 3.0
在 swift 3.0 中,只需调用 DispatchWorkItem 的实例方法即可等待任务结束,如下:
public func wait()
public func wait(timeout: DispatchTime) -> DispatchTimeoutResult
public func wait(wallTimeout: DispatchWallTime) -> DispatchTimeoutResult

事例:
let concurrentQueue = DispatchQueue.init(label: "concurrentQueue", qos: .default, attributes: .concurrent, autoreleaseFrequency: .inherit, target: nil);

concurrentQueue.async(execute: {

let allTesksQueue = DispatchQueue.init(label: "allTesksQueue", qos: .default, attributes: .concurrent, autoreleaseFrequency: .inherit, target: nil);

let workItem = DispatchWorkItem.init(block: {
    print("开始执行")
    sleep(3)
    print("结束执行")
})

allTesksQueue.async(execute: workItem)

// 设置超时时间,超时时间为 10s
let timeout = DispatchTime.now() + 10
let result = workItem.wait(timeout: timeout)

if result == .success {
    print("执行成功")
} else {
    print("执行超时")
}

})

输入结果同 object-c。
第二种监听 block 执行完成的方法如下:

object-c
在 object-c 中可用下面的函数:
void dispatch_block_notify(dispatch_block_t block,

    dispatch_queue_t queue, 
    dispatch_block_t notification_block);

该函数接收三个参数,第一个参数是需要监视的 block,第二个参数是监听的 block 执行结束之后要提交执行的队列 queue,第三个参数是待加入到队列中的 block。 和 dispatch_block_wait 的不同之处在于:dispatch_block_notify 函数不会阻塞当前线程。
例如:
NSLog(@"---- 开始设置任务 ----");
dispatch_queue_t serialQueue = dispatch_queue_create("com.fyf.serialqueue", DISPATCH_QUEUE_SERIAL);

// 耗时任务
dispatch_block_t taskBlock = dispatch_block_create(0, ^{

NSLog(@"开始耗时任务");
[NSThread sleepForTimeInterval:2.f];
NSLog(@"完成耗时任务");

});

dispatch_async(serialQueue, taskBlock);

// 更新 UI
dispatch_block_t refreshUI = dispatch_block_create(0, ^{

NSLog(@"更新 UI");

});

// 设置监听
dispatch_block_notify(taskBlock, dispatch_get_main_queue(), refreshUI);
NSLog(@"---- 完成设置任务 ----");

输出:

---- 开始设置任务 ----
---- 完成设置任务 ----
开始耗时任务
完成耗时任务
更新 UI

swift 3.0
在 swift 3.0 中,可以使用 DispatchWorkItem 的实例方法进行设置,方法如下:
public func notify(qos: DispatchQoS = default, flags: DispatchWorkItemFlags = default, queue: DispatchQueue, execute: @escaping @convention(block) () -> Swift.Void)

public func notify(queue: DispatchQueue, execute: DispatchWorkItem)

事例:
print("---- 开始设置任务 ----")

let tasksQueue = DispatchQueue.init(label: "com.fyf.tasksQueue", qos: .default, attributes: .concurrent, autoreleaseFrequency: .inherit, target: nil);

let refreshUIWorkItem = DispatchWorkItem.init(block: {

print("更新 UI")

})

let tasksWorkItem = DispatchWorkItem.init(block: {

print("开始耗时任务")
sleep(3)
print("完成耗时任务")

})

tasksWorkItem.notify(queue: DispatchQueue.main, execute: refreshUIWorkItem)
tasksQueue.async(execute: tasksWorkItem)

print("---- 完成设置任务 ----")

输出:

---- 开始设置任务 ----
---- 完成设置任务 ----
开始耗时任务
完成耗时任务
更新 UI

任务的取消
iOS8 后 GCD 支持对 dispatch block 的取消。方法如下:

object-c
可以使用下面的函数取消 dispatch block:
void dispatch_block_cancel(dispatch_block_t block);

这个函数用异步的方式取消指定的 block。取消操作使将来执行 dispatch block 立即返回,但是对已经在执行的 dispatch block 没有任何影响。当一个 block 被取消时,它会立即释放捕获的资源。如果要在一个 block 中对某些对象进行释放操作,在取消这个 block 的时候,需要确保内存不会泄漏。
例子:
dispatch_queue_t serialQueue = dispatch_queue_create("com.fyf.serialqueue", DISPATCH_QUEUE_SERIAL);

// 耗时任务
dispatch_block_t firstTaskBlock = dispatch_block_create(0, ^{

NSLog(@"开始第一个任务");
[NSThread sleepForTimeInterval:1.5f];
NSLog(@"结束第一个任务");

});

// 耗时任务
dispatch_block_t secTaskBlock = dispatch_block_create(0, ^{

NSLog(@"开始第二个任务");
[NSThread sleepForTimeInterval:2.f];
NSLog(@"结束第二个任务");

});

dispatch_async(serialQueue, firstTaskBlock);
dispatch_async(serialQueue, secTaskBlock);

// 等待 1s,让第一个任务开始运行
[NSThread sleepForTimeInterval:1];

dispatch_block_cancel(firstTaskBlock);
NSLog(@"尝试过取消第一个任务");

dispatch_block_cancel(secTaskBlock);
NSLog(@"尝试过取消第二个任务");

输出:

开始第一个任务
尝试过取消第一个任务
尝试过取消第二个任务
结束第一个任务

可见 dispatch_block_cancel 对已经在执行的任务不起作用,只能取消尚未执行的任务。

swift 3.0
在 swift 3.0 中,可以使用 DispatchWorkItem 的实例方法进行设置,方法如下:
public func cancel()

事例:
let tasksQueue = DispatchQueue.init(label: "com.fyf.tasksQueue");

let firstWorkItem = DispatchWorkItem.init(block: {

print("开始第一个任务")
sleep(2)
print("结束第一个任务")

})

let secondWorkItem = DispatchWorkItem.init(block: {

print("开始第二个任务")
sleep(2)
print("结束第二个任务")

})

tasksQueue.async(execute: firstWorkItem)
tasksQueue.async(execute: secondWorkItem)

// 等待 1s,让第一个任务开始运行
sleep(1)

firstWorkItem.cancel()
print("尝试过取消第一个任务")
secondWorkItem.cancel()
print("尝试过取消第二个任务")

输出内容同 object-c。

相信大家学习iOS视图部分的时候,跟它俩没少纠缠,尤其是bounds很多朋友都糊了,bounds确实比较难理解。这两个概念很重要,因为它们是奠定整个视图层次结构的基础。所以我们需要了解的透彻一点。
首先,我们来看一下iOS特有的坐标系,在iOS坐标系中以左上角为坐标原点,往右为X正方向,往下是Y正方向如下图:
160f59848cafa40f.jpeg

bounds和frame都是属于CGRect类型的结构体,系统的定义如下,包含一个CGPoint(起点)和一个CGSize(尺寸)子结构体。
struct CGRect {

CGPoint origin;
CGSize size;

};
origin决定了view的起点,size决定View的尺寸。

1.frame
frame是每个view必备的属性,表示view在父view坐标系统中的位置和大小,参照点是父视图的坐标系统。
示例代码:
UIView *viewA = [[UIView alloc] initWithFrame:CGRectMake(50, 50, 300, 300)];
[viewA setBackgroundColor:[UIColor blueColor]];
[self.view addSubview:viewA];
NSLog(@"viewA - %@",NSStringFromCGRect(viewA.frame));

UIView *viewB = [[UIView alloc] initWithFrame:CGRectMake(50, 50, 200, 200)];
[viewB setBackgroundColor:[UIColor yellowColor]];
[viewA addSubview:viewB];
NSLog(@"viewB - %@",NSStringFromCGRect(viewB.frame));

UIView *viewC = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
[viewC setBackgroundColor:[UIColor redColor]];
[self.view addSubview:viewC];
NSLog(@"viewC - %@",NSStringFromCGRect(viewC.frame));
打印结果:
2018-01-14 21:35:16.196389+0800 frame & bounds[1485:121325] viewA - {{50, 50}, {300, 300}}
2018-01-14 21:35:16.196647+0800 frame & bounds[1485:121325] viewB - {{50, 50}, {200, 200}}
2018-01-14 21:35:16.196802+0800 frame & bounds[1485:121325] viewC - {{100, 100}, {200, 200}}
效果图:
160f59848d8c17ed.png
以上可以看出,viewB和viewC的起点重合,但是从打印结果来看,viewB的起点为(50,50),而viewCde起点为(100,100)。原因就是frame中的位置是以父视图的坐标系为标准来确定当前视图的位置,viewB的父视图为viewA,viewC的父视图为self.view,而由于viewA的起点为(50,50),所以viewB与viewC起点才会重合。

2.bounds
bounds也是每个view都有的属性,这个属性我们一般不进行设置,表示view在本地坐标系统中的位置和大小。参照点是本地坐标系统。如果我们对上例打印bounds,将会得到以下结果:
2018-01-14 22:03:44.385207+0800 frame & bounds[1635:140821] viewA - {{0, 0}, {300, 300}}
2018-01-14 22:03:44.385482+0800 frame & bounds[1635:140821] viewB - {{0, 0}, {200, 200}}
2018-01-14 22:03:44.385646+0800 frame & bounds[1635:140821] viewC - {{0, 0}, {100, 100}}
因为我们并没有设置bounds值,那么,bounds到底有什么作用呢。这里强调,每个视图都有自己的坐标系,且这个坐标系默认以自身的左上角为坐标原点,所有子视图以这个坐标系的原点为基准点。bounds的位置代表的是子视图看待当前视图左上角的位置,bounds的大小代表当前视图的大小。原则如下:

更改bounds中的位置对于当前视图没有影响,相当于更改了当前视图的坐标系,对于子视图来说当前视图的左上角已经不再是(0,0), 而是改变后的坐标,坐标系改了,那么所有子视图的位置也会跟着改变。
更改bounds的大小,bounds的大小代表当前视图的长和宽,修改长宽后,中心点继续保持不变, 长宽进行改变,通过bounds修改长宽看起来就像是以中心点为基准点对长宽两边同时进行缩放。

以下给出例子详细讨论。
3.两者的区别
3.1 origin的区别
如下图:
160f59848e5ce396.jpeg
此时,如果我们把ViewA的bounds改为(0,100),结果如下:
请输入图片描述
我们始终要清楚,bounds的位置代表的是子视图看待当前视图左上角的位置。
bounds遵守的原则一中,更改bounds中的位置对于当前视图(ViewA)没有影响,相当于更改了ViewA的坐标系,但是子视图(ViewB)不同,对于ViewB来说ViewA的左上角已经不再是(0,0), 而是(0,100),所以对于ViewB来说,ViewA坐标系的原点其实是在红色箭头所指处的上方100处,而此时ViewB的frame.origin为(200,100),所以ViewB的上边与ViewA上边重合。
如果我们更改ViewA的bounds为(200,0),同理(可以自己思考试试),结果如下:
请输入图片描述
3.2 size的区别
frame的size直接决定了view的大小,而bounds的size修改后,view的中心点不变,长宽以中心点进行缩放。
如下例:
UIView *viewA = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 320, 240)];
[viewA setBackgroundColor:[UIColor grayColor]];
[self.view addSubview:viewA];

UIView *viewB = [[UIView alloc] initWithFrame:CGRectMake(100, 50, 160, 120)];
[viewB setBackgroundColor:[UIColor blueColor]];
[viewA addSubview:viewB];

//viewB设置size(320,160)
[viewB setBounds:CGRectMake(0, 0, 320, 240)];
结果如下:
请输入图片描述
请输入图片描述
第二个图为设置了size之后的结果,viewB左上点距离viewA显然不为(100,50),而是进行了基于viewB视图中心点的缩放操作。
4.总结

frame不管对于位置还是大小,改变的都是自己本身。
frame的位置是以父视图的坐标系为参照,从而确定当前视图在父视图中的位置。
frame的大小改变时,当前视图的左上角位置不会发生改变,只是大小发生改变。
bounds改变位置时,改变的是子视图的位置,自身没有影响;其实就是改变了本身的坐标系原点,默认本身坐标系的原点是左上角。
bounds的大小改变时,当前视图的中心点不会发生改变,当前视图的大小发生改变,看起来效果就想缩放一样。

写这篇文章的缘由是第一次面试时被问到了block循环引用的问题,当时回答的不是很好,首先要明确的是,block是否用copy修饰决定不了循环引用的产生,在此再一次进行补强,有不对的地方还请多多指教。
1.block为什么要用copy修饰
1.1 内存堆栈理解
内存栈区
由编译器自动分配释放,存放函数的参数值,局部变量的值等,不需要程序员来操心。其操作方式类似于数据结构中的栈。
内存堆区
一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。尽管后边苹果引入了ARC机制,但是ARC的机制其实仅仅是系统帮助程序员添加了retain,release,autorelease代码,并不是说系统就可以自动管理了。他的系统管理的原理还是MRC,并没有本质区别。注意内存堆区与数据结构中的堆是两回事,分配方式倒是类似于链表。

1.2 block作用域
首先,block是一个对象,所以block理论上是可以retain/release的。但是block在创建的时候它的内存是默认是分配在栈(stack)上,而不是堆(heap)上的。所以它的作用域仅限创建时候的当前上下文(函数, 方法...),当你在该作用域外调用该block时,block占用的内存已经释放,无法进行访问,程序就会崩溃,出现野指针错误。

1.3 三种block
NSGlobalBlock:全局的静态block,没有访问外部变量,存储在代码区(存储方法或者函数)。他直到程序结束的时候,才会被被释放。但是我们实际操作中基本上不会使用到不访问外部变量的block。
void(^testOneBlock)() = ^(){

  NSLog(@"我是全局的block");

};
NSLog(@"testOneBlock=%@",testOneBlock);
//控制台输出
2017-06-10 09:45:09.767 ReactiveCocoa[871:14517] testOneBlock=<__NSGlobalBlock__: 0x1045982d0>
//全局block,他会随程序销毁而销毁
NSStackBlock:保存在栈中的block,没有用copy去修饰并且访问了外部变量。但是必须要在MRC的模式下控制台才会输出NSStackBlock类型。
//需要MRC模式
int a = 5;
void(^testTwoBlock)() = ^(){

  NSLog(@"%d",a);

};
NSLog(@"testTwoBlock=%@",testTwoBlock);
//控制台输出
2017-06-10 09:45:09.768 ReactiveCocoa[871:14517] testTwoBlock=<__NSStackBlock__: 0x7fff5b668770>
//栈区block,函数调用完毕就会销毁

NSMallocBlock:保存在堆中的block,此类型blcok是用copy修饰出来的block,它会随着对象的销毁而销毁,只要对象不销毁,我们就可以调用的到在堆中的block。
int a = 5;
self.block1 = ^(NSString str, UIColor color){

  NSLog(@"%d",a);

};
NSLog(@"block1=%@",self.block1);
//控制台输出
2017-06-10 10:02:35.107 ReactiveCocoa[1075:19674] block1=<__NSMallocBlock__: 0x60000004ee50>
//用copy修饰的不会函数调用完就结束,随对象销毁才销毁,这种是在开发中正确使用block的姿势
第三种block在有些情况下会造成block的循环引用,将在下面进行讨论。

1.4 另一种理解方式:函数返回

关于函数返回,在一个函数的内部,return的时候返回的都是一个拷贝,不管是变量、对象还是指针都是返回拷贝,但是这个拷贝是浅拷贝。在这里我需要理解以下两点:

对于直接返回一些基本类型的变量来说,直接返回值的拷贝就好,没有问题。
对于返回一些非动态分配(new/malloc)得到的指针就可能出现问题,因为尽管你返回了这个指针地址。但是这个指针可能指向的栈内存,栈内存在函数执行完毕后就自动销毁了。如果销毁之后你再去访问,就会访问坏内存会导致程序崩溃。

明确上边两点之后,我们再来说,在MRC下,如果一个block作为参数,没有经过copy就返回。后果是什么呢?由于return的时候返回的是浅拷贝,也就是说返回的是对象的地址,因为在返回后这个block对应的栈内存就销毁了。如果你多次调用这个block就会发现,程序会崩溃。崩溃原因就是上边所说,block占用的空间已经释放了,你不可以进行访问了。
解决方案:就是在返回的时候,把block进行拷贝作为参数进行返回。这样做的好处是返回的那个block存储空间是在堆内,堆内的空间需要程序员自己去释放,系统不会自动回收,也就不会出现访问已释放内存导致的崩溃了。也就是我们在MRC下需要使用copy修饰符的原因。(此处是否是通过深复制在堆中申请内存不求甚解,在此标记,继续深究)

1.5 ARC下block用什么修饰
首先前面讲的内容都是在MRC下,MRC下block需要用copy修饰,但是在ARC下使用copy或strong修饰其实都一样,因为block的retain就是用copy来实现的。

2.block循环引用
在开始之前我们需要明确一点:是不是所有的block,使用self都会出现循环引用?其实不然,系统和第三方框架的block绝大部分不会出现循环引用,只有少数block以及我们自定义的block会出现循环引用。而我们只要抓住本质原因就可以了,如下:
如果block没有直接或者间接被self存储,就不会产生循环引用。就不需要用weak self。(retainCount无法变为0)

2.1 直接强引用:self -> block -> self
由于block会对block中的对象进行持有操作,就相当于持有了其中的对象,而如果此时block中的对象又持有了该block,则会造成循环引用。如下
typedef void(^block)();

@property (copy, nonatomic) block myBlock;
@property (copy, nonatomic) NSString *blockString;

  • (void)testBlock {

    self.myBlock = ^() {
        //其实注释中的代码,同样会造成循环引用
        NSString *localString = self.blockString;
        //NSString *localString = _blockString;
        //[self doSomething];
    };

    }

注:以下调用注释掉的代码同样会造成循环引用,因为不管是通过self.blockString还是_blockString,或是函数调用[self doSomething],因为只要block中用到了对象的属性或者函数,block就会持有该对象而不是该对象中的某个属性或者函数。

16102f74ffb5f56f.png
2.2 间接强引用:self -> 某个类 -> block -> self
间接强引用中,self并没有直接拥有block属性。来看下面一个例子:
这是一个持有block的view: XXSubmitBottomView
typedef void(^BtnPressedBlock)(UIButton *btn);
@interface XXSubmitBottomView : UIView
@property(strong,nonatomic)UILabel *allPriceLab;
@property(strong,nonatomic)UIButton *submittBtn;
@property(nonatomic, weak)XXConfirmOrderController *currentVc;
@property(nonatomic, weak)XXConfimOrderModel *model;
@property(nonatomic, copy)BtnPressedBlock block;
-(void)submittBtnPressed:(BtnPressedBlock)block;
这是一个持有bottomView属性的控制器: XXConfirmOrderController

@interface XXConfirmOrderController ()
@property(nonatomic, strong) XXConfimOrderTableView *tableView;
@property(nonatomic, strong) XXSubmitBottomView *bottomView;
@property(nonatomic, strong) XXConfimOrderModel *confimModel;
@end

@implementation XXConfirmOrderController
-(void)viewDidLoad{

[super viewDidLoad];
self.title = @"确认下单";
self.view.backgroundColor = DDCJ_Gray_Color;

//UI
[self.view addSubview:self.tableView];
[self.view addSubview:self.bottomView];

//Data
[self loadData];

}

下面是self.bottomView的懒加载以及block的回调处理

-(XXSubmitBottomView *)bottomView{

if (!_bottomView) {
    _bottomView = [[XXSubmitBottomView alloc] initWithFrame:CGRectMake(0, self.view.height - 50, Width, 50)];
    _bottomView.currentVc = self;

warning self.bottomView.block self间接持有了BtnPressedBlock 必须使用weak!

    WEAKSELF  //ps: weakSelf的宏定义#define WEAKSELF typeof(self) __weak weakSelf = self;
    [_bottomView submittBtnPressed:^(UIButton *btn) {
        NSLog(@"do提交订单");
        MBProgressHUD *hud = [MBProgressHUD showMessage:@"加载中..." toView:weakSelf.view];
        NSMutableDictionary *dynamic = [NSMutableDictionary dictionary];
        [dynamic setValue:weakSelf.confimModel.orderRemark forKey:@"orderRemark"];
        if (weakSelf.agreementId) {
            [dynamic setValue:weakSelf.agreementId forKey:@"agreementId"];
        }
        if (weakSelf.isShoppingCartEnter) {
            [dynamic setValue:@"0" forKey:@"orderOrigin"];
        }else{
            [dynamic setValue:@"1" forKey:@"orderOrigin"];
        }
                
        [[APIClientFactory sharedManager] requestConfimOrderWithDynamicParams:dynamic success:^(NSMutableArray *dataArray) {
        
            [hud hideAnimated:YES];                
            [weakSelf handlePushControllerWithModelList:dataArray];
        
        } failure:^(NSError *error) {
            [hud hideAnimated:YES];
            [MBProgressHUD showError:error.userInfo[@"message"]];
        }];
    }];
}

return _bottomView;

}

此处的控制器self并没有直接持有block属性,但是却强引用了bottomView,bottomView强引用了block属性,这就造成了间接循环引用。block回调内必须使用[weak self]来打破这个循环,否则就会导致这个控制器self永远都不会被释放掉产生常驻内存。

2.3 实际开发中的循环引用
使用通知(NSNotifation),调用系统自带的Block,在Block中使用self会发生循环引用。
1609d03395dc02ab.png
1609d03392aee4e0.png
1609d033935d46da.png

注:自定义的block出现循环引用时都会出现警告,所以出问题时容易解决。但在这里,在block中的确出现了循环引用,也的确没有出现警告,这才是我们真正需要注意的,也是为什么我们需要理解block循环引用的原因。

2.4 解决办法
一般性解决办法
__weak typeof(self) weakSelf = self;
复制代码
通过__weak的修饰,先把self弱引用(默认是强引用,实际上self是有个隐藏的__strong修饰的),然后在block回调里用weakSelf,这样就会打破保留环,从而避免了循环引用,如下:
self -> block -> weakSelf
self -> 某个类 -> block ->weakSelf

提醒:__block与__weak都可以用来解决循环引用,但是,__block不管是ARC还是MRC模式下都可以使用,可以修饰对象,还可以修饰基本数据类型。__weak只能在ARC模式下使用,也只能修饰对象(NSString),不能修饰基本数据类型(int)。__block对象可以在block中被重新赋值,__weak不可以。

@weakify
@weakify(self)
self.myBlock = ^() {

  NSString *localString = self.blockString;

};
16102f74ff984806.png
2.5 weak的缺陷
缺陷
如果我想在Block中延时来运行某段代码,这里就会出现一个问题,看这段代码:

  • (void)viewDidLoad {

    [super viewDidLoad];
    MitPerson*person = [[MitPerson alloc]init];
    __weak MitPerson * weakPerson = person;
    person.mitBlock = ^{
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            [weakPerson test];
        });
    };
    person.mitBlock();

    }

直接运行这段代码会发现[weakPerson test];并没有执行,打印一下会发现,weakPerson已经是 Nil 了,这是由于当我们的viewDidLoad方法运行结束,由于是局部变量,无论是MitPerson和weakPerson都会被释放掉,那么这个时候在Block中就无法拿到正真的person内容了。

解决办法一

  • (void)viewDidLoad {

    [super viewDidLoad];
    MitPerson*person = [[MitPerson alloc]init];
    __weak MitPerson * weakPerson = person;
    person.mitBlock = ^{
        __strong MitPerson * strongPerson = weakPerson;
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            [strongPerson test];
        });
    };
    person.mitBlock();

    }

这样当2秒过后,计时器依然能够拿到想要的person对象。
深入理解

首先了解一些概念:
堆里面的block(被copy过的block)有以下现象:
1.block内部如果通过外面声明的强引用来使用,那么block内部会自动产生一个强引用指向所使用的对象。
2.block内部如果通过外面声明的弱引用来使用,那么block内部会自动产生一个弱引用指向所使用的对象。

这段代码的目的:
首先,我们需要在Block块中调用,person对象的方法,既然是在Block块中我们就应该使用弱指针来引用外部变量,以此来避免循环引用。但是又会出现问题,什么问题呢?就是当我计时器要执行方法的时候,发现对象已经被释放了。

接下来就是为了避免person对象在计时器执行的时候被释放掉:那么为什么person对象会被释放掉呢?因为无论我们的person强指针还是weakPerson弱指针都是局部变量,当执行完ViewDidLoad的时候,指针会被销毁。对象只有被强指针引用的时候才不会被销毁,而我们如果直接引用外部的强指针对象又会产生循环引用,这个时候我们就用了一个巧妙的代码来完成这个需求。

首先在person.mitBlock引用外部weakPerson,并在内部创建一个强指针去指向person对象,因为在内部声明变量,Block是不会强引用这个对象的,这也就在避免的person.mitBlock循环引用风险的同时,又创建出了一个强指针指向对象。

之后再用GCD延时器Block来引用相对于它来说是外部的变量strongPerson,这时延时器Block会默认创建出来一个强引用来引用person对象,当person.mitBlock作用域结束之后strongPerson会跟着被销毁,内存中就仅剩下了延时器Block强引用着person对象,2秒之后触发test方法,GCD Block内部方法执行完毕之后,延时器和对象都被销毁,这样就完美实现了我们的需求。

黑色代表强引用,绿色代表弱引用
1609d0339502c23e.png
解决办法二

  • (void)viewDidLoad {

    [super viewDidLoad];
    MitPerson*person = [[MitPerson alloc]init];
    @weakify(self)
    person.mitBlock = ^{
        @strongify(self)
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            [self test];
        });
    };
    person.mitBlock();

    }

可以看出,这样就完美解决了weak的缺陷,我们可以在block中随意使用self。

3.参考

iOS block为什么要用copy
Block作为属性为什么要用copy修饰
【iOS 开发】为什么要用copy修饰Block
iOS小记--block的循环引用问题以及block何时需要/不需要使用[weak self]
实际开发中-Block导致循环引用的问题
循环引用,看我就对了
iOS - 深入探究Block循环引用