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