美文网首页
字符串与多线程的爱恨情仇

字符串与多线程的爱恨情仇

作者: klike | 来源:发表于2020-01-19 20:44 被阅读0次

    字符串与多线程的爱恨情仇

    问题的引出

    做一次埋点需求的时候,需要在最后埋点 SDK 上报的时候,进行一些拦截和计算,来插入一些新的埋点属性,顺便还要记录所有埋点的数据展示给测试看,所以就存在类似的代码:

    // 所有埋点上报最终调用的方法
    - (void)sendEventWithParams:(Event *)event {
        if (…) { // 一些计算判断
            [self update:event]; // 加入一些参数
        }
        [self.eventString appendString:[self stringFrom:event]]; // 记录一些埋点信息
        [EventSDK sendEvent:event]; // 上报埋点
    }
    

    后面考虑到性能问题,决定把一些计算操作放在子线程去处理,因为本身埋点上报 [EventSDK sendEvent:event]; 也是一个异步处理,所以觉得这样做应该没什么问题。修改之后的代码大概就是这样:

    // 所有埋点上报最终调用的方法
    - (void)sendEventWithParams:(Event *)event {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            if (…) { // 一些计算判断
                [self update:event]; // 加入一些参数
            }
            [self.eventString appendString:[self stringFrom:event]]; // 记录一些埋点信息
            [EventSDK sendEvent:event]; // 上报埋点
        })
    }
    

    一切都按照想象中的执行,没有因为埋点数量过多而存在浪费性能的问题。但是,当多次调试 app 的时候,会偶现崩溃!而且最终问题也定位到了我们修改的埋点方法,一定是存在隐藏的风险,在特殊的情况下会导致崩溃。

    一番调试,最终定位问题指向:

    [self.eventString appendString:[self stringFrom:event]]; // 记录一些埋点信息
    
    // 控制台输出
    malloc: Double free of object 0x101080c40
    

    多线程对字符串操作会存在风险?

    属性的赋值

    为什么会释放两次呢?第一反应就是去查询方法的内部实现,appendString 的源码我们是无从得知的,但是我们可以推断出:它的内部应该也存在一个赋值过程。当我们把记录埋点信息修改成赋值时:

    self.eventString = [self stringFrom:event]; // 记录一些埋点信息
    

    同样会偶现类似的崩溃:

    -[CFString release]: message sent to deallocated instance 0x100fe15b0
    

    两次崩溃错误都是向已经释放的对象发送 release 消息。

    对于属性的赋值,我们可以在 Apple 开源的 runtime 中略知一二,通过 编译的Runtime源码,属性相关赋值的方式分为下述两种:

    属性赋值方式一:

    void objc_storeStrong(id *location, id obj)
    {
        id prev = *location;
        if (obj == prev) {
            return;
        }
        objc_retain(obj);
        *location = obj;
        objc_release(prev);
    }
    

    属性赋值方式二:

    static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
    {
        if (offset == 0) {
            object_setClass(self, newValue);
            return;
        }
    
        id oldValue;
        id *slot = (id*) ((char*)self + offset);
    
        if (copy) {
            newValue = [newValue copyWithZone:nil];
        } else if (mutableCopy) {
            newValue = [newValue mutableCopyWithZone:nil];
        } else {
            if (*slot == newValue) return;
            newValue = objc_retain(newValue);
        }
    
        if (!atomic) {
            oldValue = *slot;
            *slot = newValue;
        } else {
            spinlock_t& slotlock = PropertyLocks[slot];
            slotlock.lock();
            oldValue = *slot;
            *slot = newValue;        
            slotlock.unlock();
        }
    
        objc_release(oldValue);
    }
    

    ==ARC 环境下==

    strong

    @property (nonatomic, strong) NSString *string;
    

    当我们在代码中调用到 self.string = @"like" 的时候,实际上会调用到

    void objc_storeStrong(id *location, id obj)
    

    atomic & copy

    @property (atomic) NSString *string;
    @property (copy) NSString *string;
    

    当我们在代码中调用到 self.string = @"like" 的时候,实际上会调用到

    static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
    

    对于直接调用 _string = @“like” 的时候,都会调用 void objc_storeStrong(id *location, id obj) ,与属性修饰符没有任何关系。

    ==MRC 环境下==

    所有属性赋值 self.string = @"like" 都会调用

    static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
    

    解决问题

    通过分析源码,崩溃的原因就无处可逃了。其实原因很简单,我们可以分析赋值的具体实现:

    void objc_storeStrong(id *location, id obj)
    {
        id prev = *location; // 1
        if (obj == prev) {
            return;
        }
        objc_retain(obj); // 2
        *location = obj;
        objc_release(prev); // 3
    }
    

    异步并发执行大量的赋值操作的时候,多个线程会对同一个对象进行赋值操作,很有可能线程 A 进行到 2 的同时 线程 B 进行到 1,这个时候,当线程 A 继续执行到 3,释放了 prev,随后线程 B 也执行到 3 ,也对 prev 进行释放操作,所以就会出现对已经释放的对象又进行释放操作,当然会崩溃。

    对于 copy 修饰对属性亦是如此,所以对于多线程的数据操作,我们都必须考虑安全问题

    当然解决方法其实也很简单,主要是我们要有多线程安全的意识,上面 属性赋值方式二 的相关源码已经为我们提供了一些思路:

    方案一 采用 atomic 进行属性修饰

    @property (atomic, strong) NSMutableString *string;
    

    方案二 采用串行队列

    dispatch_queue_t queue = dispatch_queue_create("lable", DISPATCH_QUEUE_SERIAL);
    

    上面我们讨论的属性的赋值安全问题,但实际上对于一些其他操作,例如: [self.stringArray addObject:@"hello"]; 也会存在多线程的安全问题。对于一些我们常用的对象:NSStringNSMutableStringNSArrayNSMutableArrayNSDictionaryNSMutableDictionary Apple 都没为其保证多线程的安全性,毕竟加锁消耗性能,特别对这些基础数据结构对象,高性能显得格外重要,所以多线程操作这种少数情况就交给我们去管理。

    NSCache 是线程安全的。

    番外

    尽量使用 copy 修饰不可变对象(NSString, NSArray, NSDictionary 等)

    观察上述两种属性赋值情况,strong 修饰的属性可能会调用到两种不同的方法,但是不论哪一种方法,都是相同的赋值逻辑:

    1. objc_retain(obj); 保留新值
    2. *location = obj; 赋值
    3. objc_release(prev); 释放旧值

    而对于 copy 来说,赋值逻辑只有一种:

    1. newValue = [newValue copyWithZone:nil]; 复制新值
    2. oldValue = *slot; *slot = newValue; 赋值
    3. objc_release(oldValue); 释放旧值

    所以对于一些不可变对象来说,作为属性的时候最好使用 copy 来修饰,这样每次赋值的时候都会 copy 一次新值,避免错误把可变对象赋值给不可变对象,这样就存在不可变对象可能会被改变的风险,举个 🌰 :

    strong 修饰

    @property (nonatomic, strong) NSString *string;
    

    执行

    - (void)someFunc {
        self.string = @"hi";
        NSMutableString *mString = @"hello".mutableCopy;
        self.string = mString;
        NSLog(@"time1: %@", self.string);
        //... do something
        [mString appendString:@" world"];
        NSLog(@“time2: %@“, self.string);
    }
    

    打印

    time1: hello
    time2: hello world
    

    可以看到不可变字符串被偷偷摸摸改变了!再试试 copy 修饰:

    @property (nonatomic, copy) NSString *string;
    

    打印

    time1: hello
    time2: hello
    

    还是 copy 靠得住呀!

    尽量使用点语法操作属性

    调试源码,我们可以知道成员变量的赋值的时候会调用:

    void objc_storeStrong(id *location, id obj)
    {
        id prev = *location;
        if (obj == prev) {
            return;
        }
        objc_retain(obj);
        *location = obj;
        objc_release(prev);
    }
    

    可以看到,根本没走属性修饰符语义! 所以我们应该尽可能的必须要使用点语法,这样不仅能执行属性修饰语义,而且还能避免重写 setter 或者 getter 后无效的风险!

    相关文章

      网友评论

          本文标题:字符串与多线程的爱恨情仇

          本文链接:https://www.haomeiwen.com/subject/jxkdzctx.html