OC底层原理分析3

503 阅读3分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第16天,点击查看活动详情

多线程

多线程方案有如下几种

image.png

判断以下代码是否会产生线程死锁

// 🌰 1
    dispatch_sync(dispatch_get_main_queue(), ^{
       
        NSLog(@"打印");
    });

// 🌰 2
    dispatch_queue_t queue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
        dispatch_async(queue, ^{
       
        dispatch_sync(queue, ^{
           
            NSLog(@"打印");
        });
        
    });

结论为都会产生死锁

线程死锁总结:使用sync当前串行队列添加任务,会产生死锁

判断以下代码输出结果

- (void)test {
    
    NSLog(@"2");
}
dispatch_async(dispatch_get_global_queue(0, 0), ^{
       
    NSLog(@"1");
        
    [self performSelector:@selector(test) withObject:nil afterDelay:.0];
        
    NSLog(@"3");
});

// 1
// 3

因为performSelector:withObject:afterDelay:方法本质是添加了一个NSTimer定时器,并将定时器添加到当前runloop中,但是当前线程是没有保活的,也就是执行完NSLog(@"3"),线程就挂掉了,虽然afterDelay设置为.0,但也是个延迟操作,需要在下一个runloop生命周期才能生效,因此需要设置runloop的一个运行时长

- (void)test {
    
    NSLog(@"2");
}
dispatch_async(dispatch_get_global_queue(0, 0), ^{
       
    NSLog(@"1");
        
    [self performSelector:@selector(test) withObject:nil afterDelay:.0];
        
    NSLog(@"3");
  
    // 需要注意以下代码添加位置,因为runMode:beforeDate:是开启了一个一定时长的循环,也就是在这时间内都是循环在当前位置,runMode:beforeDate:方法后面的代码将不会执行到
    [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
});

多线程加锁

多线程一般需要考虑线程同步问题,以及防止死锁(一些递归调用,嵌套调用会导致死锁,还有同步主队列任务也会死锁)。还有某些依赖条件下,锁的问题,如增删操作,当删除操作的线程先加锁,但是此时无数据可删,此时需要条件等待(如pthread_cond_wait() 此操作会将当前线程休眠,放开锁,被唤醒后会再次加锁,与pthread_cond_signal() 配对使用,signal是激活一个等待该条件的线程)

  • OSSpinLock:

    • 自旋锁,加锁的地方会忙等,一直占用CPU资源,直到锁放开。此方法存在优先级反转问题,即如果优先级低的线程先加锁,然后优先级高的线程会忙等,但是由于优先级高的线程分配的资源更多,会存在优先级的线程资源不足而无法继续执行,此时互相等待,就造成死锁状态。
  • os_unfair_lock

    • 用于替代OSSpinLock,等待锁的线程处于休眠状态而不是忙等。
  • pthread_mutex

    • 互斥锁,等待锁的线程休眠,可以设置递归锁,以及条件等待
  • NSLock

    • 对pthread_mutex的封装
  • NSRecursiveLock

    • 对pthread_mutex的封装,递归锁
  • NSCondition

    • 对mutex和cond的封装
  • NSConditionLock

    • 对NSCondition的进一步封装
  • dispatch_semaphore

    • 控制线程并发访问的最大数量

iOS内存布局

image.png

Tagged Pointer

如何判断一个指针是否为Tagged Pointer?
iOS平台,最高有效位是1(第64bit)
Mac平台,最低有效位是1

// 判断下列两个方法是否能正常运行,结果是否一致
dispatch_async(dispatch_get_global_queue(0, 0), ^{
   
    for (NSInteger i = 0; i < 1000; i++) {
        self.name = [NSString stringWithFormat:@"abcdefghijklmn"];
    }
});

dispatch_async(dispatch_get_global_queue(0, 0), ^{
   
    for (NSInteger i = 0; i < 1000; i++) {
        self.name = [NSString stringWithFormat:@"abc"];
    }
});

// 结论
此处需要结合ARC下set方法的底层实现,以及Tagged Pointer技术
- (void)setName:(NSString *)name {
    
    if (_name != name) {
        
        [_name release];
        
        _name = [name copy];
    }
}

可以看到,在多线程情况下,如果同时调用[_name release];方法,则会导致过度释放问题,导致坏内存访问。
至于@“abc”字符串,因为其使用的是Tagged Pointer技术,也就是直接存储在地址中,赋值操作是直接地址赋值

dealloc实现

内部会调用rootDealloc(), 此方法内部判断当前对象是否是taggedPointer, 根据isa判断是否是nonpointer、weakly_referenced、has_assoc、has_cxx_dtor、has_sidetable_rc,如果是就走快速释放,否则调用object_dispose()方法,此方法内部调用objc_destructInstance()方法,然后调用C函数free()
objc_destructInstance()方法内部,清除成员变量,移除关联对象,调用当前对象的clearDeallocating()方法,将指向当前对象的弱指针置为nil,弱指针存在SideTables这个哈希表中

ARC

ARC是LLVM+Runtime相互协作的结果,利用LLVM编译器自动在代码合适的位置添加release或autorelease, 利用runtime在对象释放的时候将弱引用置空