经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 移动开发 » iOS » 查看文章
iOS底层原理(七)多线程(下)
来源:cnblogs  作者:FunkyRay  时间:2021/4/12 9:49:36  对本文有异议

iOS中的读写安全

atomic

atomic用于保证属性setter、getter的原子性操作,相当于在getter和setter内部加了线程同步的锁原子性:原子即为最小的物理单位,意味不可再分割;即代码都为一个整体在同一线程进行操作

atomic只是保证setter、getter是线程安全的,并不能保证使用属性的过程是线程安全的

从源码分析getter和setter对于atomic的使用

我们在objc4中的objc-accessors.mm中找到对应的getter和setter的实现

getter的实现

  1. // getter
  2. id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
  3. if (offset == 0) {
  4. return object_getClass(self);
  5. }
  6. // Retain release world
  7. id *slot = (id*) ((char*)self + offset);
  8. if (!atomic) return *slot;
  9. // Atomic retain release world
  10. spinlock_t& slotlock = PropertyLocks[slot];
  11. slotlock.lock();
  12. id value = objc_retain(*slot);
  13. slotlock.unlock();
  14. // for performance, we (safely) issue the autorelease OUTSIDE of the spinlock.
  15. return objc_autoreleaseReturnValue(value);
  16. }

setter的实现

  1. static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
  2. {
  3. if (offset == 0) {
  4. object_setClass(self, newValue);
  5. return;
  6. }
  7. id oldValue;
  8. id *slot = (id*) ((char*)self + offset);
  9. if (copy) {
  10. newValue = [newValue copyWithZone:nil];
  11. } else if (mutableCopy) {
  12. newValue = [newValue mutableCopyWithZone:nil];
  13. } else {
  14. if (*slot == newValue) return;
  15. newValue = objc_retain(newValue);
  16. }
  17. if (!atomic) {
  18. oldValue = *slot;
  19. *slot = newValue;
  20. } else {
  21. spinlock_t& slotlock = PropertyLocks[slot];
  22. slotlock.lock();
  23. oldValue = *slot;
  24. *slot = newValue;
  25. slotlock.unlock();
  26. }
  27. objc_release(oldValue);
  28. }

从源码可以看出只有automic的属性才会进行加锁操作

iOS中的读写安全方案

思考以下场景,怎么做最合适

  • 同一时间,只能有1个线程进行写的操作
  • 同一时间,允许有多个线程进行读的操作
  • 同一时间,不允许既有写的操作,又有读的操作

上面的场景就是典型的“多读单写”,经常用于文件等数据的读写操作,iOS中的实现方案有以下两个

  • pthread_rwlock:读写锁
  • dispatch_barrier_async:异步栅栏调用

pthread_rwlock

pthread_rwlock是专用于读写文件的锁,其本质也是互斥锁,等待锁的线程会进入休眠

使用代码如下

  1. @interface ViewController ()
  2. @property (assign, nonatomic) pthread_rwlock_t lock;
  3. @end
  4. @implementation ViewController
  5. - (void)viewDidLoad {
  6. [super viewDidLoad];
  7. // 初始化锁
  8. pthread_rwlock_init(&_lock, NULL);
  9. dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
  10. for (int i = 0; i < 10; i++) {
  11. dispatch_async(queue, ^{
  12. [self read];
  13. });
  14. dispatch_async(queue, ^{
  15. [self write];
  16. });
  17. }
  18. }
  19. - (void)read {
  20. // 加上读取数据的锁
  21. pthread_rwlock_rdlock(&_lock);
  22. sleep(1);
  23. NSLog(@"%s", __func__);
  24. pthread_rwlock_unlock(&_lock);
  25. }
  26. - (void)write
  27. {
  28. // 加上写入数据的锁
  29. pthread_rwlock_wrlock(&_lock);
  30. sleep(1);
  31. NSLog(@"%s", __func__);
  32. pthread_rwlock_unlock(&_lock);
  33. }
  34. - (void)dealloc
  35. {
  36. // 释放时要销毁锁
  37. pthread_rwlock_destroy(&_lock);
  38. }
  39. @end

dispatch_barrier_async

dispatch_barrier_async也叫栅栏函数,意在用于拦截多线程异步并发操作,只保证同时有一条线程在操作

用栅栏函数也可以保证多读单写的操作

使用代码如下

  1. @interface ViewController ()
  2. @property (strong, nonatomic) dispatch_queue_t queue;
  3. @end
  4. @implementation ViewController
  5. - (void)viewDidLoad {
  6. [super viewDidLoad];
  7. self.queue = dispatch_queue_create("rw_queue", DISPATCH_QUEUE_CONCURRENT);
  8. for (int i = 0; i < 10; i++) {
  9. dispatch_async(self.queue, ^{
  10. [self read];
  11. });
  12. dispatch_async(self.queue, ^{
  13. [self read];
  14. });
  15. dispatch_async(self.queue, ^{
  16. [self read];
  17. });
  18. // 这个函数传入的并发队列必须是自己通过dispatch_queue_cretate创建的
  19. dispatch_barrier_async(self.queue, ^{
  20. [self write];
  21. });
  22. }
  23. }
  24. - (void)read {
  25. sleep(1);
  26. NSLog(@"read");
  27. }
  28. - (void)write
  29. {
  30. sleep(1);
  31. NSLog(@"write");
  32. }
  33. @end

定时器

我们日常使用的定时器有以下几个

  • CADisplayLink
  • NSTimer
  • GCD定时器

CADisplayLink是用于同步屏幕刷新频率的定时器

CADisplayLink和NSTimer的区别

  • iOS设备的屏幕刷新频率是固定的,CADisplayLink在正常情况下会在每次刷新结束都被调用,精确度相当高
  • NSTimer的精确度就显得低了点,比如NSTimer的触发时间到的时候,runloop如果在阻塞状态,触发时间就会推迟到下一个runloop周期。并且NSTimer新增了tolerance属性,让用户可以设置可以容忍的触发的时间的延迟范围
  • CADisplayLink使用场合相对专一,适合做UI的不停重绘,比如自定义动画引擎或者视频播放的渲染。NSTimer的使用范围要广泛的多,各种需要单次或者循环定时处理的任务都可以使用。在UI相关的动画或者显示内容使用CADisplayLink比起用NSTimer的好处就是我们不需要在格外关心屏幕的刷新频率了,因为它本身就是跟屏幕刷新同步的。

CADisplayLink在使用中会出现的循环引用问题

CADisplayLink在日常使用中,可能会出现循环引用问题,见示例代码

  1. @interface ViewController ()
  2. @property (strong, nonatomic) CADisplayLink *link;
  3. @end
  4. @implementation ViewController
  5. - (void)viewDidLoad {
  6. [super viewDidLoad];
  7. self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(linkTest)];
  8. [self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
  9. }
  10. - (void)linkTest
  11. {
  12. NSLog(@"%s", __func__);
  13. }
  14. - (void)dealloc
  15. {
  16. NSLog(@"%s", __func__);
  17. [self.link invalidate];
  18. }
  19. @end

由于ViewController里有个link属性指向这CADisplayLink对象CADisplayLink对象里的target又指向着ViewController里的linkTest,都是强引用,所以会造成循环引用,无法释放

解决方案

增加第三个对象,通过第三个对象将target调用的方法转发出去,具体如下图所示

实现代码如下

  1. @interface LLProxy : NSObject
  2. + (instancetype)proxyWithTarget:(id)target;
  3. @property (weak, nonatomic) id target;
  4. @end
  5. @implementation LLProxy
  6. + (instancetype)proxyWithTarget:(id)target
  7. {
  8. LLProxy *proxy = [[LLProxy alloc] init];
  9. proxy.target = target;
  10. return proxy;
  11. }
  12. - (id)forwardingTargetForSelector:(SEL)aSelector
  13. {
  14. return self.target;
  15. }
  16. @end
  17. // ViewController.m文件中
  18. #import "ViewController.h"
  19. #import "LLProxy.h"
  20. @interface ViewController ()
  21. @property (strong, nonatomic) CADisplayLink *link;
  22. @end
  23. @implementation ViewController
  24. - (void)viewDidLoad {
  25. [super viewDidLoad];
  26. self.link = [CADisplayLink displayLinkWithTarget:[LLProxy proxyWithTarget:self] selector:@selector(linkTest)];
  27. [self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
  28. }
  29. - (void)linkTest
  30. {
  31. NSLog(@"%s", __func__);
  32. }
  33. - (void)dealloc
  34. {
  35. NSLog(@"%s", __func__);
  36. [self.link invalidate];
  37. }
  38. @end

NSTimer

NSTimer也是定时器,相比CADisplayLink使用范围更广,更灵活,但精确度会低一些

NSTimer在使用中会出现的循环引用问题

NSTimer在使用时也会存在循环引用问题,同CADisplayLink

  1. @interface ViewController ()
  2. @property (strong, nonatomic) NSTimer *timer;
  3. @end
  4. @implementation ViewController
  5. - (void)viewDidLoad {
  6. [super viewDidLoad];
  7. self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
  8. }
  9. - (void)timerTest
  10. {
  11. NSLog(@"%s", __func__);
  12. }
  13. - (void)dealloc
  14. {
  15. NSLog(@"%s", __func__);
  16. [self.timer invalidate];
  17. }
  18. @end

解决方案

【第一种】借助第三对象并将方法转发,同CADisplayLink

  1. @interface ViewController ()
  2. @property (strong, nonatomic) NSTimer *timer;
  3. @end
  4. @implementation ViewController
  5. - (void)viewDidLoad {
  6. [super viewDidLoad];
  7. self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[LLProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
  8. }
  9. - (void)timerTest
  10. {
  11. NSLog(@"%s", __func__);
  12. }
  13. - (void)dealloc
  14. {
  15. NSLog(@"%s", __func__);
  16. [self.timer invalidate];
  17. }
  18. @end

【第二种】使用NSTimerblock回调来调用方法,并将self改为弱指针

  1. __weak typeof(self) weakSelf = self;
  2. self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
  3. [weakSelf timerTest];
  4. }];

统一优化方案

NSProxy

NSProxy是唯一一个没有继承自NSObject的类,它是专门用来做消息转发的

特点
  • 不继承NSObject,也是基类类型
  • 没有init方法,直接用alloc方法来初始化
  • 没有forwardingTargetForSelector方法,只支持消息转发
优化方案

LLProxy继承自NSProxy,然后在消息转发里替换target

这么做的好处在于NSProxy相比NSObject少了消息发送先从父类查找的过程,以及不经过forwardingTargetForSelector,相比之下性能会高

替换代码如下

  1. @interface LLProxy : NSProxy
  2. + (instancetype)proxyWithTarget:(id)target;
  3. @property (weak, nonatomic) id target;
  4. @end
  5. @implementation LLProxy
  6. + (instancetype)proxyWithTarget:(id)target
  7. {
  8. // NSProxy对象不需要调用init,因为它本来就没有init方法
  9. LLProxy *proxy = [LLProxy alloc];
  10. proxy.target = target;
  11. return proxy;
  12. }
  13. - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
  14. {
  15. return [self.target methodSignatureForSelector:sel];
  16. }
  17. - (void)forwardInvocation:(NSInvocation *)invocation
  18. {
  19. [invocation invokeWithTarget:self.target];
  20. }
  21. @end
从源码实现来分析

我们先查看下面这句代码打印什么

  1. ViewController *vc = [[ViewController alloc] init];
  2. LLProxy *proxy = [LLProxy proxyWithTarget:vc];
  3. NSLog(@"%d, [proxy isKindOfClass:[ViewController class]]);

打印结果为1,可以看出NSProxyisKindOfClassNSObjectisKindOfClass有所差别

我们可以通过GNUstep来查看NSProxy的源码实现,发现其内部会直接调用消息转发的方法,才会有我们将target替换成了ViewController对象,所以最后调用isKindOfClass的是ViewController对象,那么结果也就知晓了

从该方法可以反观NSProxy的其他方法内部实现,都会主动触发消息转发的实现

GCD定时器

GCD定时器相比其他两个定时器是最准时的,因为和系统内核直接挂钩

使用代码如下

我们将GCD定时器封装到自定义的LLTimer文件来使用

  1. // LLTimer.h文件
  2. @interface LLTimer : NSObject
  3. + (NSString *)execTask:(void(^)(void))task
  4. start:(NSTimeInterval)start
  5. interval:(NSTimeInterval)interval
  6. repeats:(BOOL)repeats
  7. async:(BOOL)async;
  8. + (NSString *)execTask:(id)target
  9. selector:(SEL)selector
  10. start:(NSTimeInterval)start
  11. interval:(NSTimeInterval)interval
  12. repeats:(BOOL)repeats
  13. async:(BOOL)async;
  14. + (void)cancelTask:(NSString *)name;
  15. // LLTimer.m文件
  16. #import "LLTimer.h"
  17. @implementation LLTimer
  18. static NSMutableDictionary *timers_;
  19. static dispatch_semaphore_t semaphore_;
  20. + (void)initialize
  21. {
  22. static dispatch_once_t onceToken;
  23. dispatch_once(&onceToken, ^{
  24. timers_ = [NSMutableDictionary dictionary];
  25. // 加锁来保证多线程创建定时器和取消定时器同时只能有一个操作
  26. semaphore_ = dispatch_semaphore_create(1);
  27. });
  28. }
  29. + (NSString *)execTask:(void (^)(void))task start:(NSTimeInterval)start interval:(NSTimeInterval)interval repeats:(BOOL)repeats async:(BOOL)async
  30. {
  31. if (!task || start < 0 || (interval <= 0 && repeats)) return nil;
  32. // 队列
  33. dispatch_queue_t queue = async ? dispatch_get_global_queue(0, 0) : dispatch_get_main_queue();
  34. // 创建定时器
  35. dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
  36. // 设置时间
  37. // dispatch_time_t start:几秒后开始执行
  38. // uint64_t interval:执行间隔
  39. dispatch_source_set_timer(timer,
  40. dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC),
  41. interval * NSEC_PER_SEC, 0);
  42. dispatch_semaphore_wait(semaphore_, DISPATCH_TIME_FOREVER);
  43. // 定时器的唯一标识
  44. NSString *name = [NSString stringWithFormat:@"%zd", timers_.count];
  45. // 存放到字典中
  46. timers_[name] = timer;
  47. dispatch_semaphore_signal(semaphore_);
  48. // 设置回调
  49. dispatch_source_set_event_handler(timer, ^{
  50. task();
  51. if (!repeats) { // 不重复的任务
  52. [self cancelTask:name];
  53. }
  54. });
  55. // 启动定时器
  56. dispatch_resume(timer);
  57. return name;
  58. }
  59. + (NSString *)execTask:(id)target selector:(SEL)selector start:(NSTimeInterval)start interval:(NSTimeInterval)interval repeats:(BOOL)repeats async:(BOOL)async
  60. {
  61. if (!target || !selector) return nil;
  62. return [self execTask:^{
  63. if ([target respondsToSelector:selector]) {
  64. // 去掉警告
  65. #pragma clang diagnostic push
  66. #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
  67. [target performSelector:selector];
  68. #pragma clang diagnostic pop
  69. }
  70. } start:start interval:interval repeats:repeats async:async];
  71. }
  72. + (void)cancelTask:(NSString *)name
  73. {
  74. if (name.length == 0) return;
  75. dispatch_semaphore_wait(semaphore_, DISPATCH_TIME_FOREVER);
  76. dispatch_source_t timer = timers_[name];
  77. if (timer) {
  78. dispatch_source_cancel(timer);
  79. [timers_ removeObjectForKey:name];
  80. }
  81. dispatch_semaphore_signal(semaphore_);
  82. }
  83. @end

然后在控制器里调用

  1. #import "ViewController.h"
  2. #import "LLTimer.h"
  3. @interface ViewController ()
  4. @property (copy, nonatomic) NSString *task;
  5. @end
  6. @implementation ViewController
  7. - (void)viewDidLoad {
  8. [super viewDidLoad];
  9. NSLog(@"begin");
  10. // selector方式
  11. self.task = [LLTimer execTask:self
  12. selector:@selector(doTask)
  13. start:2.0
  14. interval:1.0
  15. repeats:YES
  16. async:YES];
  17. // block方式
  18. // self.task = [LLTimer execTask:^{
  19. // NSLog(@"111111 - %@", [NSThread currentThread]);
  20. // } start:2.0 interval:-10 repeats:NO async:NO];
  21. }
  22. - (void)doTask
  23. {
  24. NSLog(@"doTask - %@", [NSThread currentThread]);
  25. }
  26. - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
  27. {
  28. [LLTimer cancelTask:self.task];
  29. }

LLTimer封装细节

  • initialize方法里只执行一次字典的创建和锁的创建(只有用到该类时才创建,并且避免多次调用)
  • 内部创建一个全局的字典用来保存多个定时器的创建(定时器的个数递增作为key,timer为value)
  • 外部支持多个参数来控制定时器在哪个线程创建,以及是否只调用一次
  • 注意细节的优化,对于传入的时间、是否有任务,以及定时器的标识都对应做校验
  • 在多线程环境下,保证创建定时器和取消删除定时器同一时间只能有一个线程在执行

面试题

1.看下面两段代码,会不会造成死锁

  1. // 段1
  2. - (void)viewDidLoad {
  3. [super viewDidLoad];
  4. NSLog(@"执行任务1");
  5. dispatch_queue_t queue = dispatch_get_main_queue();
  6. dispatch_sync(queue, ^{
  7. NSLog(@"执行任务2");
  8. });
  9. NSLog(@"执行任务3");
  10. }
  11. // 段2
  12. - (void)viewDidLoad {
  13. [super viewDidLoad];
  14. NSLog(@"执行任务1");
  15. dispatch_queue_t queue = dispatch_get_main_queue();
  16. dispatch_async(queue, ^{
  17. NSLog(@"执行任务2");
  18. });
  19. NSLog(@"执行任务3");
  20. }

第一段会死锁,第二段不会

因为整个函数viewDidLoad的执行都是在主队列中串行执行的,所以要等函数执行完才会执行任务2,但是dispatch_sync又是同步的,在主线程中是要执行dispatch_sync之后才会执行任务3的代码,所以互相之前都要等待,就造成了死锁

dispatch_async不会,因为需要等待一会才会执行任务2的代码,所以会先执行任务再执行任务2,不需要马上执行;但是不会开启新的线程

2.看下面这段代码,会不会造成死锁?将队列改为并发队列,会不会死锁

  1. NSLog(@"执行任务1");
  2. dispatch_queue_t queue = dispatch_queue_create("myqueu", DISPATCH_QUEUE_SERIAL);
  3. dispatch_async(queue, ^{ // block0
  4. NSLog(@"执行任务2");
  5. dispatch_sync(queue, ^{ // block1
  6. NSLog(@"执行任务3");
  7. });
  8. NSLog(@"执行任务4");
  9. });
  10. NSLog(@"执行任务5");

会的。
原因是由于是dispatch_async,所以会先执行任务1和任务5,然后由于是串行队列,那么先会执行block0,再执行block1;但是任务2执行完,后面的是dispatch_sync,就表示要马上执行任务3,可任务3的执行又是要等block0执行完才可以,于是就会造成死锁

改为并发队列后不会死锁,虽然都是同一个并发队列,但是可以同时执行多个任务,不需要等待

3.看下面这段代码,会不会造成死锁?将队列2改为并发队列,会不会死锁

  1. NSLog(@"执行任务1");
  2. dispatch_queue_t queue = dispatch_queue_create("myqueu", DISPATCH_QUEUE_SERIAL);
  3. dispatch_queue_t queue2 = dispatch_queue_create("myqueu2", DISPATCH_QUEUE_SERIAL);
  4. dispatch_async(queue, ^{ // block0
  5. NSLog(@"执行任务2");
  6. dispatch_sync(queue2, ^{ // block1
  7. NSLog(@"执行任务3");
  8. });
  9. NSLog(@"执行任务4");
  10. });
  11. NSLog(@"执行任务5");

都不会。因为两个任务都是在两个队列里,所以不会有等待情况

4.看下面代码打印结果是什么,为什么,怎么改

  1. dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
  2. dispatch_async(queue, ^{
  3. NSLog(@"1");
  4. [self performSelector:@selector(test) withObject:nil afterDelay:.0];
  5. NSLog(@"3");
  6. });
  7. - (void)test
  8. {
  9. NSLog(@"2");
  10. }

打印1、3。

因为performSelector: withObject: afterDelay:这个方法是属于RunLoop的库的,有afterDelay: 参数的本质都是往RunLoop中添加定时器的,由于当前是在子线程中,不会创建RunLoop,所以创建RunLoop后就可以执行该调用,并打印1、3、2

由于该方法的实现RunLoop是没有开源的,我们要想了解方法实现的本质,可以通过GNUstep开源项目来查看,这个项目将Cocoa的OC库重新开源实现了一遍,虽然不是官网源码,但也有一定的参考价值

源码地址:http://www.gnustep.org/resources/downloads.php

我们在官网上找到GNUstep Base进行下载

然后找到RunLoop.mperformSelector: withObject: afterDelay:的实现

  1. - (void) performSelector: (SEL)aSelector
  2. withObject: (id)argument
  3. afterDelay: (NSTimeInterval)seconds {
  4. NSRunLoop *loop = [NSRunLoop currentRunLoop];
  5. GSTimedPerformer *item;
  6. item = [[GSTimedPerformer alloc] initWithSelector: aSelector
  7. target: self
  8. argument: argument
  9. delay: seconds];
  10. [[loop _timedPerformers] addObject: item];
  11. RELEASE(item);
  12. [loop addTimer: item->timer forMode: NSDefaultRunLoopMode];
  13. }

找到GSTimedPerformer的构造方法里面可以看到,会创建一个timer的定时器,然后将它加到RunLoop

  1. - (id) initWithSelector: (SEL)aSelector
  2. target: (id)aTarget
  3. argument: (id)anArgument
  4. delay: (NSTimeInterval)delay
  5. {
  6. self = [super init];
  7. if (self != nil)
  8. {
  9. selector = aSelector;
  10. target = RETAIN(aTarget);
  11. argument = RETAIN(anArgument);
  12. timer = [[NSTimer allocWithZone: NSDefaultMallocZone()]
  13. initWithFireDate: nil
  14. interval: delay
  15. target: self
  16. selector: @selector(fire)
  17. userInfo: nil
  18. repeats: NO];
  19. }
  20. return self;
  21. }

如此一来就印证了我们的分析,下面我们就手动在子线程创建RunLoop来查看

  1. dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
  2. dispatch_async(queue, ^{
  3. NSLog(@"1");
  4. // 这句代码的本质是往Runloop中添加定时器
  5. [self performSelector:@selector(test) withObject:nil afterDelay:.0];
  6. NSLog(@"3");
  7. [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
  8. });
  9. - (void)test
  10. {
  11. NSLog(@"2");
  12. }

创建空的RunLoop之前因为已经通过performSelector: withObject: afterDelay:创建了一个定时器加了进去,所以RunLoop就不为空了,不需要我们再添加一个Source1了,这样也保证RunLoop不会退出

运行程序,打印结果为1、3、2

最后打印2是因为RunLoop被唤醒处理事件有时间延迟,所以会晚一些打印

5.看下面代码打印结果是什么,为什么,怎么改

  1. - (void)test
  2. {
  3. NSLog(@"2");
  4. }
  5. - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
  6. NSThread *thread = [[NSThread alloc] initWithBlock:^{
  7. NSLog(@"1");
  8. }];
  9. [thread start];
  10. [self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:YES];
  11. }

打印结果为1,并且崩溃了。

因为执行线程的blockperformSelector几乎是同时的,所以先执行了block后的线程就被销毁了,这时再在该线程上发消息就是会报错

解决办法也是创建RunLoop并让子线程不被销毁

  1. - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
  2. NSThread *thread = [[NSThread alloc] initWithBlock:^{
  3. NSLog(@"1");
  4. [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
  5. [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
  6. }];
  7. [thread start];
  8. [self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:YES];
  9. }

6.dispatch_once是怎么做到只创建一次的,内部是怎么实现的

我们知道GCD中的dispatch_once的使用如下代码,可以做的只执行一次,一般用来创建单例

  1. static dispatch_once_t onceToken;
  2. dispatch_once(&onceToken, ^{
  3. NSLog(@"单例应用");
  4. });

我们可以通过源码来分析内部实现,在once.c中找到dispatch_once的实现

  1. void
  2. dispatch_once(dispatch_once_t *val, dispatch_block_t block)
  3. {
  4. // val是onceToken静态变量
  5. dispatch_once_f(val, block, _dispatch_Block_invoke(block));
  6. }

其中的_dispatch_Block_invoke是一个宏定义,用来包装block

  1. #define _dispatch_Block_invoke(bb) ((dispatch_function_t)((struct Block_layout *)bb)->invoke)

找到其底层是通过dispatch_once_f实现的

  1. void
  2. dispatch_once_f(dispatch_once_t *val, void *ctxt, dispatch_function_t func)
  3. {
  4. // 将外界传入的静态变量val转变为dispatch_once_gate_t类型的变量l
  5. dispatch_once_gate_t l = (dispatch_once_gate_t)val;
  6. #if !DISPATCH_ONCE_INLINE_FASTPATH || DISPATCH_ONCE_USE_QUIESCENT_COUNTER
  7. // 获取任务标识符v
  8. uintptr_t v = os_atomic_load(&l->dgo_once, acquire);
  9. // 如果v == DLOCK_ONCE_DONE,表示任务已经执行过了,直接return
  10. if (likely(v == DLOCK_ONCE_DONE)) {
  11. return;
  12. }
  13. #if DISPATCH_ONCE_USE_QUIESCENT_COUNTER
  14. // 如果加锁失败走到这里,再次进行存储,并标记为DLOCK_ONCE_DONE
  15. if (likely(DISPATCH_ONCE_IS_GEN(v))) {
  16. return _dispatch_once_mark_done_if_quiesced(l, v);
  17. }
  18. #endif
  19. #endif
  20. if (_dispatch_once_gate_tryenter(l)) { // 尝试进入任务
  21. return _dispatch_once_callout(l, ctxt, func);
  22. }
  23. // 此时已有任务,则进入无限等待
  24. return _dispatch_once_wait(l);
  25. }

dispatch_once_f函数的详细调用分析

1.os_atomic_load这个宏用来获取任务标识

  1. #define os_atomic_load(p, m) atomic_load_explicit(_os_atomic_c11_atomic(p), memory_order_##m)

2.通过_dispatch_once_mark_done_if_quiesced进行再次存储和标记

  1. DISPATCH_ALWAYS_INLINE
  2. static inline void
  3. _dispatch_once_mark_done_if_quiesced(dispatch_once_gate_t dgo, uintptr_t gen)
  4. {
  5. if (_dispatch_once_generation() - gen >= DISPATCH_ONCE_GEN_SAFE_DELTA) {
  6. /*
  7. * See explanation above, when the quiescing counter approach is taken
  8. * then this store needs only to be relaxed as it is used as a witness
  9. * that the required barriers have happened.
  10. */
  11. // 再次存储,并标记为DLOCK_ONCE_DONE
  12. os_atomic_store(&dgo->dgo_once, DLOCK_ONCE_DONE, relaxed);
  13. }
  14. }

3.通过_dispatch_once_gate_tryenter内部进行比较并加锁

  1. DISPATCH_ALWAYS_INLINE
  2. static inline bool
  3. _dispatch_once_gate_tryenter(dispatch_once_gate_t l)
  4. {
  5. // 进行比较,如果没问题,则进行加锁,并标记为DLOCK_ONCE_UNLOCKED
  6. return os_atomic_cmpxchg(&l->dgo_once, DLOCK_ONCE_UNLOCKED,
  7. (uintptr_t)_dispatch_lock_value_for_self(), relaxed);
  8. }

4.通过_dispatch_once_callout来执行回调

  1. DISPATCH_NOINLINE
  2. static void
  3. _dispatch_once_callout(dispatch_once_gate_t l, void *ctxt,
  4. dispatch_function_t func)
  5. {
  6. // 回调执行
  7. _dispatch_client_callout(ctxt, func);
  8. // 进行广播
  9. _dispatch_once_gate_broadcast(l);
  10. }

_dispatch_client_callout内部就是执行block回调

  1. DISPATCH_ALWAYS_INLINE
  2. static inline void
  3. _dispatch_client_callout(void *ctxt, dispatch_function_t f)
  4. {
  5. return f(ctxt);
  6. }

_dispatch_once_gate_broadcast内部会调用_dispatch_once_mark_done

  1. DISPATCH_ALWAYS_INLINE
  2. static inline void
  3. _dispatch_once_gate_broadcast(dispatch_once_gate_t l)
  4. {
  5. dispatch_lock value_self = _dispatch_lock_value_for_self();
  6. uintptr_t v;
  7. #if DISPATCH_ONCE_USE_QUIESCENT_COUNTER
  8. v = _dispatch_once_mark_quiescing(l);
  9. #else
  10. v = _dispatch_once_mark_done(l);
  11. #endif
  12. if (likely((dispatch_lock)v == value_self)) return;
  13. _dispatch_gate_broadcast_slow(&l->dgo_gate, (dispatch_lock)v);
  14. }

_dispatch_once_mark_done内部就是赋值并标记,即解锁

  1. DISPATCH_ALWAYS_INLINE
  2. static inline uintptr_t
  3. _dispatch_once_mark_done(dispatch_once_gate_t dgo)
  4. {
  5. // 如果不相同,则改成相同,并标记为DLOCK_ONCE_DONE
  6. return os_atomic_xchg(&dgo->dgo_once, DLOCK_ONCE_DONE, release);
  7. }
总结:

GCD单例中,有两个重要参数,onceTokenblock,其中onceToken是静态变量,具有唯一性,在底层被封装成了dispatch_once_gate_t类型的变量ll主要是用来获取底层原子封装性的关联,即变量v,通过v来查询任务的状态,如果此时v等于DLOCK_ONCE_DONE,说明任务已经处理过一次了,直接return

如果此时任务没有执行过,则会在底层通过C++函数的比较,将任务进行加锁,即任务状态置为DLOCK_ONCE_UNLOCK,目的是为了保证当前任务执行的唯一性,防止在其他地方有多次定义。加锁之后进行block回调函数的执行,执行完成后,将当前任务解锁,将当前的任务状态置为DLOCK_ONCE_DONE,在下次进来时,就不会在执行,会直接返回

原文链接:http://www.cnblogs.com/funkyRay/p/ios-di-ceng-yuan-li-qi-duo-xian-cheng-xia.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号