100 行实现的 iOS 辅助类

知乎上看到一个问题《一百行以下有那些给力代码》,几乎所有回答都是用 100 行实现的某个小功能,有趣归有趣,但对实际工作并没有什么帮助。翻了翻自己的 M80Kit ,里面也有些比较有趣且实用的辅助类都是一百行左右,可以拿出来秀下。

M80MulticastDelegate

一个从 XMPP 框架中抽离出来的类,提供同步的一对多 delegate 机制。

在很多场景下,往往多个对象会依赖于某个核心对象。使用 M80MulticastDelegate 就能够很好地进行解耦:不需要定义过多正式的 protocol 或者 notification 进行通信。巧妙地利用 OC 的消息转发机制,当发一个不认识的消息给当前 MulticaseDelegate 时,它就会自动进行转发:遍历已注册的 delegate node,找到能够响应当前 selector 的 node 并执行。

@interface M80DelegateNode : NSObject
@property (nonatomic,weak) id nodeDelegate;
+ (M80DelegateNode *)node:(id)delegate;
@end

@implementation M80DelegateNode
+ (M80DelegateNode *)node:(id)delegate
{
M80DelegateNode *instance = [[M80DelegateNode alloc] init];
instance.nodeDelegate = delegate;
return instance;
}
@end


@interface M80MulticastDelegate ()
{
NSMutableArray *_delegateNodes;
}

@end

@implementation M80MulticastDelegate

- (id)init
{
if (self = [super init])
{
_delegateNodes = [[NSMutableArray alloc] init];
}
return self;
}

- (void)dealloc {}

- (void)addDelegate:(id)delegate
{
[self removeDelegate:delegate];
M80DelegateNode *node = [M80DelegateNode node:delegate];
[_delegateNodes addObject:node];
}

- (void)removeDelegate:(id)delegate
{
NSMutableIndexSet *indexs = [NSMutableIndexSet indexSet];
for (NSUInteger i = 0; i < [_delegateNodes count]; i ++)
{
M80DelegateNode *node = [_delegateNodes objectAtIndex:i];
if (node.nodeDelegate == delegate)
{
[indexs addIndex:i];
}
}

if ([indexs count])
{
[_delegateNodes removeObjectsAtIndexes:indexs];
}
}

- (void)removeAllDelegates
{
[_delegateNodes removeAllObjects];
}

- (NSUInteger)count
{
return [_delegateNodes count];
}

- (NSUInteger)countForSelector:(SEL)aSelector
{
NSUInteger count = 0;
for (M80DelegateNode *node in _delegateNodes)
{
if ([node.nodeDelegate respondsToSelector:aSelector])
{
count++;
}
}
return count;
}

- (BOOL)hasDelegateThatRespondsToSelector:(SEL)aSelector
{
BOOL hasSelector = NO;
for (M80DelegateNode *node in _delegateNodes)
{
if ([node.nodeDelegate respondsToSelector:aSelector])
{
hasSelector = YES;
break;
}
}
return hasSelector;
}


- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
for (M80DelegateNode *node in _delegateNodes)
{
NSMethodSignature *method = [node.nodeDelegate methodSignatureForSelector:aSelector];
if (method)
{
return method;
}
}
// 如果发现没有可以响应当前方法的 Node, 就返回一个空方法
// 否则会引起崩溃
return [[self class] instanceMethodSignatureForSelector:@selector(doNothing)];
}

- (void)forwardInvocation:(NSInvocation *)invocation
{
SEL selector = [invocation selector];
BOOL hasNilDelegate = NO;

for (M80DelegateNode *node in _delegateNodes)
{
id nodeDelegate = node.nodeDelegate;

if (nodeDelegate == nil)
{
hasNilDelegate = YES;
}
else if ([nodeDelegate respondsToSelector:selector])
{
[invocation invokeWithTarget:nodeDelegate];
}
}

if (hasNilDelegate)
{
[self removeDelegate:nil];
}
}

- (void)doesNotRecognizeSelector:(SEL)aSelector {}

- (void)doNothing {}

M80TimerHolder

一个 NSTimer 的 Wrapper,用于 NSTimer 的管理。iOS 上 NSTimer 的最大坑是它会 retain 当前 target,这样就导致了 target 的延迟释放甚至无法释放 (repeat 为 YES 的 NSTimer 和 target 形成 retain-cycle 又没有合理时机进行 invalidate)。

一种通用的解决方式是用 Block 来解除 retain-cycle,但需要在 block 中将 target 设为 weak,这是这种方式比较容易出错的地方。

而 M80TimerHolder 则采用稍微有点耦合但更安全的方式: 真正的 Target (通常是 VC) 和 NSTimer 持有 M80TimerHolder, 后者通过 weak delegate 和 VC 进行通信。对于 repeat 的 Timer, 没有即使不做任何处理都不会有延迟处理和 retain-cycle 的问题。而对于 repeat 的 Timer 只需要在 VC 的 dealloc 方法调用 M80TimerHolder 的 stopTimer 方法即可。就算不调用,形成 retain-cycle 的也仅仅是 Timer 和 M80TimerHolder,并不会对 APP 后续执行造成任何影响,只是多了个空跑的 Timer 和泄露的 M80TimerHolder 而已。

具体实现如下:

@interface M80TimerHolder ()
{
NSTimer *_timer;
BOOL _repeats;
}
- (void)onTimer: (NSTimer *)timer;
@end

@implementation M80TimerHolder

- (void)dealloc
{
[self stopTimer];
}

- (void)startTimer: (NSTimeInterval)seconds
delegate: (id<M80TimerHolderDelegate>)delegate
repeats: (BOOL)repeats
{
_timerDelegate = delegate;
_repeats = repeats;
if (_timer)
{
[_timer invalidate];
_timer = nil;
}
_timer = [NSTimer scheduledTimerWithTimeInterval:seconds
target:self
selector:@selector(onTimer:)
userInfo:nil
repeats:repeats];
}

- (void)stopTimer
{
[_timer invalidate];
_timer = nil;
_timerDelegate = nil;
}

- (void)onTimer: (NSTimer *)timer
{
if (!_repeats)
{
_timer = nil;
}
if (_timerDelegate && [_timerDelegate respondsToSelector:@selector(onM80TimerFired:)])
{
[_timerDelegate onM80TimerFired:self];
}
}

@end

M80WeakProxy

顾名思义,这是个代理,解决的问题和 M80TimeHolder 一样:使得 NSTimer 不持有 target 以免形成 retain-cycle。原理很简单: NSTimer 持有 M80WeakProxy,而 M80WeakProxy 只持有真正 target 的虚引用,并通过 OC 的消息转发机制调用 target 的 onTimer 方法。(Fork from FLAnimatedImage)

具体实现如下

@interface M80WeakProxy : NSObject
+ (instancetype)weakProxyForObject:(id)object;
@end


@interface M80WeakProxy ()
@property (nonatomic,weak) id target;
@end

@implementation M80WeakProxy

+ (instancetype)weakProxyForObject:(id)object
{
M80WeakProxy *instance = [[M80WeakProxy alloc] init];
instance.target = object;
return instance;
}

- (id)forwardingTargetForSelector:(SEL)aSelector
{
return _target;
}

- (void)forwardInvocation:(NSInvocation *)invocation
{
void *nullPointer = NULL;
[invocation setReturnValue:&nullPointer];
}


- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector
{
return [NSObject instanceMethodSignatureForSelector:@selector(init)];
}
@end

而真正调用时候只需要这样:

M80WeakProxy *weakProxy = [M80WeakProxy weakProxyForObject:self];

self.displayLink = [CADisplayLink displayLinkWithTarget:weakProxy selector:@selector(displayDidRefresh:)];

M80Observer

用 Block 实现 KVO,规避 KVO register 和 unregister 没有配对调用的问题。同样不超过 150 行,采用的原理和 C++ 中实现自动锁之类的方式相近,在类初始化时 register KVO, 析构时 unregister KVO 以形成配对的调用。

typedef NS_ENUM(NSUInteger, M80ObserveType) {
M80ObserveTypeNone,
M80ObserveTypeOldAndNew,
M80ObserveTypeChange,
};
@interface M80Observer ()
@property (nonatomic,weak) id observeObject;
@property (nonatomic,strong) NSString *keyPath;
@property (nonatomic,copy) id block;
@property (nonatomic,assign) M80ObserveType type;

@end

@implementation M80Observer

- (void)dealloc
{
_block = nil;
[_observeObject removeObserver:self
forKeyPath:_keyPath];
_observeObject = nil;
}


- (instancetype)initWithObject:(id)object
keyPath:(NSString *)keyPath
block:(id)block
options:(NSKeyValueObservingOptions)options
type:(M80ObserveType)type
{
if (self = [super init])
{
_observeObject = object;
_keyPath = keyPath;
_block = [block copy];
_type = type;
[object addObserver:self
forKeyPath:keyPath
options:options
context:NULL];
}
return self;
}

+ (instancetype)observer:(id)object
keyPath:(NSString *)keyPath
block:(M80ObserverBlock)block
{
return [[M80Observer alloc]initWithObject:object
keyPath:keyPath
block:block
options:0
type:M80ObserveTypeNone];
}

+ (instancetype)observer:(id)object
keyPath:(NSString *)keyPath
oldAndNewBlock:(M80ObserverBlockWithOldAndNew)block
{
return [[M80Observer alloc]initWithObject:object
keyPath:keyPath
block:block
options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
type:M80ObserveTypeOldAndNew];
}

+ (instancetype)observer:(id)object keyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
changeBlock:(M80ObserverBlockWithChangeDictionary)block
{
return [[M80Observer alloc]initWithObject:object
keyPath:keyPath
block:block
options:options
type:M80ObserveTypeChange];

}

- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
switch (_type)
{
case M80ObserveTypeNone:
if (_block)
{
((M80ObserverBlock)_block)();
}
break;
case M80ObserveTypeOldAndNew:
if (_block)
{
((M80ObserverBlockWithOldAndNew)_block)(change[NSKeyValueChangeOldKey],change[NSKeyValueChangeNewKey]);
}
break;
case M80ObserveTypeChange:
if (_block)
{
((M80ObserverBlockWithChangeDictionary)_block)(change);
}
break;
default:
break;
}
}
@end