本文使用 运行时和信号量加锁 的方式,低成本地实现了 线程安全的可变集合 ,并使用 CocoaPods 封装了 YCThreadSafeMutableCollection 库供大家把玩和吐槽。
1. atomic
属性声明为atomic时,在该属性在调用getter和setter方法时,会加上同步锁。即在属性在调用getter和setter方法时,保证同一时刻只能有一个线程调用属性的读/写方法。保证了读和写的过程是可靠的。
但是对于可变集合,我们操作的是atomic指针指向的对象,并不是直接调用其getter/setter,所以还是不可靠的。
2. 加锁
对于可变集合,我们应该在操作集合对象时加锁,以保证其线程安全。
在 「不再安全的 OSSpinLock」 一文中,我们看到了几种锁的性能对比。由于 OSSpinLock 已不再安全,所以我们采用信号量 dispatch_semaphore 来进行加锁操作,以获得较好的性能。
3. 低成本实现线程安全的可变集合
3.1 方案1
将可变集合封装到另一个对象中,提供操作接口,并在操作接口中加锁,保证其线程安全。
@interface YCThreadSafeMutableArray : NSObject
//NSArray、NSMutableArray APIs
@end
@interface YCThreadSafeMutableArray ()
@property (nonatomic, strong) dispatch_semaphore_t lock;
@property (nonatomic, strong) NSMutableArray *mutableArray;
@end
@implementation YCThreadSafeMutableArray
- (void)addObject:(id)anObject
{
dispatch_semaphore_wait(self.lock, DISPATCH_TIME_FOREVER);
[self.mutableArray addObject:anObject];
dispatch_semaphore_signal(self.lock);
}
... // 其他API
@end
- 缺点
- 需要复写大量API,工作量很大。
- 后期系统版本升级,需要额外的维护成本。
3.2 方案2
采用 继承 + Runtime 方案。
- 首先,继承可变集合类型。
- 通过 **Runtime **,将父类实现的API在子类中动态实现,绑定到 _objc_msgForward 或 _objc_msgForward_stret 上,直接进行消息转发。
- 子类实现必要的,或者不需要消息转发的方法。在遍历父类API时,如果发现子类已实现,可将其排除。
- 这里为了使用安全,除了必要的 init 方法,我还复写了一些API,以判断传入对象是否为空,索引值是否越界等。
- 消息转发过程最后 forwardInvocation: 时加锁来保证线程安全。
下面以 Set 为例:
@interface YCThreadSafeMutableSet ()
@property (nonatomic, strong) dispatch_semaphore_t lock;
@property (nonatomic, strong) NSMutableSet *mutableSet;
@end
@implementation YCThreadSafeMutableSet
+ (void)initialize
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[NSObject tsm_enumrateInstanceMethodsOfClasses:@[NSMutableSet.class, NSSet.class] addToClass:YCThreadSafeMutableSet.class];
});
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
NSMethodSignature *result = [super methodSignatureForSelector:aSelector];
if (result) {
return result;
}
result = [self.mutableSet methodSignatureForSelector:aSelector];
if (result && [self.mutableSet respondsToSelector:aSelector]) {
return result;
}
return [NSMethodSignature threadSafeMutableAvoidExceptionSignature];
}
- (void)forwardInvocation:(NSInvocation *)invocation
{
SEL selector = invocation.selector;
if ([self.mutableSet respondsToSelector:selector]) {
YC_SEMAPHORE_LOCKPAIR(self.lock, [invocation invokeWithTarget:self.mutableSet]);
}
}
#pragma mark - must override
- (void)dealloc
{
// NSLog(@"%s", __func__);
}
- (instancetype)init
{
self = [super init];
if (self) {
self.lock = dispatch_semaphore_create(1);
self.mutableSet = [[NSMutableSet alloc] init];
}
return self;
}
- (instancetype)initWithCapacity:(NSUInteger)numItems
{
self = [super init];
if (self) {
self.lock = dispatch_semaphore_create(1);
self.mutableSet = [[NSMutableSet alloc] initWithCapacity:numItems];
}
return self;
}
- (instancetype)initWithObjects:(id _Nonnull const [])objects count:(NSUInteger)cnt
{
self = [super init];
if (self) {
self.lock = dispatch_semaphore_create(1);
self.mutableSet = [[NSMutableSet alloc] initWithObjects:objects count:cnt];
}
return self;
}
- (void)addObject:(id)object
{
if (object) {
YC_SEMAPHORE_LOCKPAIR(self.lock, [self.mutableSet addObject:object]);
}
}
- (void)removeObject:(id)object
{
if (object) {
YC_SEMAPHORE_LOCKPAIR(self.lock, [self.mutableSet removeObject:object]);
}
}
@end
- 宏定义
#ifndef YC_SEMAPHORE_LOCK
#define YC_SEMAPHORE_LOCK(lock) dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER)
#endif
#ifndef YC_SEMAPHORE_UNLOCK
#define YC_SEMAPHORE_UNLOCK(lock) dispatch_semaphore_signal(lock)
#endif
#define YC_SEMAPHORE_LOCKPAIR(lock, ...) YC_SEMAPHORE_LOCK(lock); \
__VA_ARGS__; \
YC_SEMAPHORE_UNLOCK(lock)
-
初始化方法没什么好说的,按接口初始化实例对象即可。
-
消息转发 forwardInvocation: 时,通过 invokeWithTarget: 将响应对象换为内部的可变对象,并加锁保证其线程安全。
- (void)forwardInvocation:(NSInvocation *)invocation
{
SEL selector = invocation.selector;
if ([self.mutableSet respondsToSelector:selector]) {
YC_SEMAPHORE_LOCKPAIR(self.lock, [invocation invokeWithTarget:self.mutableSet]);
}
}
- 在 initialize ,即对象第一次接收到消息时,子类动态实现父类的主要API。
+ (void)initialize
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[NSObject tsm_enumrateInstanceMethodsOfClasses:@[NSMutableSet.class, NSSet.class] addToClass:YCThreadSafeMutableSet.class];
});
}
- tsm_enumrateInstanceMethodsOfClasses: 的实现。
- 子类动态实现父类的主要API,并将实现 IMP 绑定到 _objc_msgForward 或 _objc_msgForward_stret 上。
@implementation NSObject(ThreadSafeMutable)
CG_INLINE IMP tsm_getMsgForwardIMP(Class cls, SEL selector) {
IMP msgForwardIMP = _objc_msgForward;
#if !defined(__arm64__)
Method method = class_getInstanceMethod(cls, selector);
const char *typeDescription = method_getTypeEncoding(method);
if (typeDescription[0] == '{') {
// 以下代码参考 JSPatch 的实现:
//In some cases that returns struct, we should use the '_stret' API:
//http://sealiesoftware.com/blog/archive/2008/10/30/objc_explain_objc_msgSend_stret.html
//NSMethodSignature knows the detail but has no API to return, we can only get the info from debugDescription.
NSMethodSignature *methodSignature = [NSMethodSignature signatureWithObjCTypes:typeDescription];
if ([methodSignature.debugDescription rangeOfString:@"is special struct return? YES"].location != NSNotFound) {
msgForwardIMP = (IMP)_objc_msgForward_stret;
}
}
#endif
return msgForwardIMP;
}
+ (void)tsm_enumrateInstanceMethodsOfClass:(Class)aClass usingBlock:(void (^)(Method, SEL))block {
if (!block) return;
unsigned int methodCount = 0;
Method *methods = class_copyMethodList(aClass, &methodCount);
for (unsigned int i = 0; i < methodCount; i++) {
Method method = methods[i];
SEL selector = method_getName(method);
if (block) block(method, selector);
}
free(methods);
}
+ (void)tsm_enumrateInstanceMethodsOfClasses:(NSArray<Class> *)classes addToClass:(Class)toClass {
if (toClass) {
[classes enumerateObjectsUsingBlock:^(Class _Nonnull hookClass, NSUInteger idx, BOOL * _Nonnull stop) {
[self tsm_enumrateInstanceMethodsOfClass:hookClass usingBlock:^(Method _Nonnull method, SEL _Nonnull selector) {
// 如果已经实现了该方法,则不需要消息转发
if (class_getInstanceMethod(toClass, selector) != method) return;
const char * typeDescription = (char *)method_getTypeEncoding(method);
if (typeDescription) {
class_addMethod(toClass, selector, tsm_getMsgForwardIMP(hookClass, selector), typeDescription);
}
}];
}];
}
}
@end
方案2相比方案1:
- 优点
- 需要复习的API较少。
- 使用 Runtime 获取父类方法,父类API变更时动态获取,后期基本不需要维护。
- 缺点
- 采用 消息转发 ,会有一定性能损失,但大部分场景下损失较小,可以不用考虑。
- 采用 NSKeyedArchiver 和 NSKeyedUnarchiver 后,还原的对象为 NSMutableArray 。
- 这个问题暂时没有找到好的办法,使用 NSKeyedUnarchiver 根本不走 initWithCoder: 。
- (void)archiveLimitation
{
YCThreadSafeMutableArray *array = [[YCThreadSafeMutableArray alloc] init];
[array addObject:@0];
[array addObject:@1];
[array addObject:@2];
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:array];
YCThreadSafeMutableArray *unarchiveArray = [NSKeyedUnarchiver unarchiveObjectWithData:data];
NSLog(@"%@", NSStringFromClass(unarchiveArray.class));
NSLog(@"%@", unarchiveArray);
}
// 输出
__NSArrayM
(
0,
1,
2
)
- 局限性
- 对于对象直接调用的API可以实现加锁保护,但对
for in这种遍历不能实现保护。for in底层会调用 countByEnumeratingWithState:objects:count: ,但这个方法只有调用时被锁保护,遍历过程中其实是没有保护的。
- 对于 enumerateObjectsUsingBlock: 和 objectAtIndex: ,API调用都是被锁保护的,所以是线程安全的。
- 对于对象直接调用的API可以实现加锁保护,但对
- (void)badCase
{
YCThreadSafeMutableArray *array = [[YCThreadSafeMutableArray alloc] init];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
for (int i = 0; i < 1000; ++i) {
[array addObject:@(i)];
}
});
// crash
dispatch_async(queue, ^{
for (NSNumber *number in array) {
NSLog(@"%@", number);
}
NSLog(@"finished");
});
// safe
// dispatch_async(queue, ^{
// [array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
// NSLog(@"%@", obj);
// }];
// NSLog(@"finished");
// });
// safe
// dispatch_async(queue, ^{
// for (int i = 0; i < 1000; ++i) {
// NSLog(@"%@", [array objectAtIndex:i]);
// }
// NSLog(@"finished");
// });
}
4. 总结
通过采用 继承 + Runtime ,我们可以低成本实现线程安全的可变集合。
但这种实现还是 有一定缺点和局限性 ,另外这个方案只在 demo 场景下经受了一些考验,暂时没有应用在线上。
如果觉得本文对你有所帮助,给我点个赞吧~ 👍🏻
字节内推二维码:
社招
校招