背景
在平时的开发中,会有很多弹窗的需求,尤其是在首页(版本升级,运营活动)等等
但是很多时候都是由不同的童鞋开发,而且基本都是依赖于网络接口,所以先后顺序都是无法保证
这就导致一个后果,弹窗泛滥成灾
另外我们可以发现,系统自带的AlertView是有队列管理的,总是消失了一个弹窗以后,再显示下一个弹窗
于是便有了这个Popup队列管理中心
雏形
在开发前,我们可以大概的设想一下这个的设计雏形
有一个Popup管理类,由这个类维护一个数组,外部调用该类的API将弹窗添加到这个数组中
外部在消失弹窗的时候,调用该类的API将该弹窗从这个数组中移除
最后从队列中取出下一次弹窗进行展示
以上方式有几个弊端
-
接入方式过于复杂
-
使用者需要关心的太多了,如果哪个童鞋在消失自己的弹窗的时候,忘记了从队列中移除,并且没有从队列中取出下一个弹窗进行展示,那队列中的其他弹窗都将永不见天日
原理
为了解决以上问题,该项目运用了几个Objective-C底层的原理,下面将一一剖析
为了不让使用者在消失自己的弹窗后,需要手动从队列中移除,并且唤醒下一个弹窗
最开始我想到的是定时任务,但是后面觉得定时任务不是很优雅,后面选择了监听Runloop运行状态变化
Runloop
- (instancetype)init {
self = [super init];
if (self) {
//创建一个以和屏幕刷新率相同的频率的定时器
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(wakeup)];
[_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
dispatch_async(dispatch_get_main_queue(), ^{
//添加Runloop监听者
[self addRunloopObserver];
});
}
return self;
}
- (void)addRunloopObserver {
//获取当前runloop
CFRunLoopRef runloop = CFRunLoopGetCurrent();
//创建context
CFRunLoopObserverContext context = {0, (__bridge void *)self, &CFRetain, &CFRelease, NULL};
//创建runloop监听者
static CFRunLoopObserverRef defaultModeObserver;
/*其中runloop的状态有如下几种,这里监听的是kCFRunLoopBeforeWaiting
kCFRunLoopEntry = (1UL << 0),
kCFRunLoopBeforeTimers = (1UL << 1),
kCFRunLoopBeforeSources = (1UL << 2),
kCFRunLoopBeforeWaiting = (1UL << 5),
kCFRunLoopAfterWaiting = (1UL << 6),
kCFRunLoopExit = (1UL << 7),
kCFRunLoopAllActivities = 0x0FFFFFFFU
*/
defaultModeObserver = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopBeforeWaiting, YES, 0, &callback, &context);
//为当前runloop添加runloop监听者
CFRunLoopAddObserver(runloop, defaultModeObserver, kCFRunLoopCommonModes);
CFRelease(defaultModeObserver);
}
void callback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
//Runloop 回调,在这里面可以管理Popup队列
}
截止,就完成了利用Runloop取代定时任务,对Popup队列进行定时管理
KVO
为了简化使用者调用,不需要使用者自己将类添加到队列中
一开始想着定义一个show的接口方法,然后添加NSObject的分类,在load里面通过hook的方式,动态替换show成swizzle方法,在swizzle方法里面将popup添加进队列
想法是挺美好的,但是现实是残酷的,因为hook是不能影响到子类的
后面苦思冥想,想有什么办法通过监听方法是否有被调用
最终醍醐灌顶,参照了系统的KVO原理,通过创建派生类方式,再利用runtime,hook原有的show方法
- (void)ssp_registerToPopupCenter {
BOOL flag = [self conformsToProtocol:@protocol(SSPPopupProtocol)];
NSString *msg = [NSString stringWithFormat:@"%@ should conform SSPPopupProtocol", NSStringFromClass([self class])];
NSAssert(flag, msg);
//创建派生类
NSString * className = [NSString stringWithFormat:@"SSPPopupCenter_%@",NSStringFromClass(self.class)];
const char *cla = className.UTF8String;
Class subP = objc_allocateClassPair([self class], cla, 0);
if (nil != subP && ![subP isKindOfClass:[self class]]) {
//替换原有的show的方法,该方法是通过协议约束的
[self swizzleClass:subP method:@selector(ssp_show) swizzledSelector:@selector(ssp_swizzle_show)];
//如果实现了hide的方法,同样也替换
if (nil != class_getInstanceMethod([self class], @selector(ssp_hide))) {
[self swizzleClass:subP method:@selector(ssp_hide) swizzledSelector:@selector(ssp_swizzle_hide)];
}
[self addClass:[self class] method:@selector(ssp_level)];
[self addClass:[self class] method:@selector(setSsp_level:)];
//注册派生类
objc_registerClassPair(subP);
} else {
//获取派生类
subP = objc_getClass(cla);
}
//修改对象的isa指针为派生类
object_setClass(self, subP);
}
Runtime
以下是hook的普遍方式,但大多数人都是直接拷贝,拷贝的版本也是多样化,因为他们没有真正的看过里面的实现
下面将大概讲解一下
- (void)swizzleClass:(Class)clazz method:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector {
//通过selector的名字,搜索该类的方法列表中是否有该方法
Method originalMethod = class_getInstanceMethod(clazz, originalSelector);
Method swizzledMethod = class_getInstanceMethod(clazz, swizzledSelector);
//如果两个方法都有,则直接替换两个方法的实现
if (originalMethod && swizzledMethod) {
method_exchangeImplementations(originalMethod, swizzledMethod);
return;
}
//如果swizzle方法没有,则动态添加,一般如果是利用分类hook,那么一般swizzle方法也是有的
if (nil == swizzledMethod) {
BOOL didAddMethod = class_addMethod(clazz,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
if (didAddMethod) {
swizzledMethod = class_getInstanceMethod(clazz, swizzledSelector);
}
}
//最终将原来的方法具体实现替换成swizzle方法的实现
if (swizzledMethod) {
class_replaceMethod(clazz,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
}
}
Category
弹窗有时候是有优先级的,该项目里在协议中定义了一个属性ssp_level, 定义如下:
typedef enum : NSUInteger {
SSPPopupActivityLevelNormal, //默认优先级
SSPPopupActivityLevelHigh, //高优先级,会优先插到前面
} SSPPopupActivityLevel;
大家都知道Category正常是不能添加属性的,要添加属性需要用到Runtime,下面就是具体实现
- (void)ssp_registerToPopupCenter {
...
//添加ssp_level对应的getter,setter方法
[self addClass:[self class] method:@selector(ssp_level)];
[self addClass:[self class] method:@selector(setSsp_level:)];
...
}
- (void)addClass:(Class)clazz method:(SEL)selector {
Method method = class_getInstanceMethod(clazz, selector);
BOOL didAddMethod = class_addMethod(clazz,
selector,
method_getImplementation(method),
method_getTypeEncoding(method));
NSAssert(didAddMethod, @"add method failure");
}
- (SSPPopupActivityLevel)ssp_level {
return [objc_getAssociatedObject(self, &kSSPopupCenterLevel) integerValue];
}
- (void)setSsp_level:(SSPPopupActivityLevel)level {
objc_setAssociatedObject(self, &kSSPopupCenterLevel, @(level), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
至此,就完成了所有的核心功能,剩下的就是对队列的管理,这个无非就是操作一个数组,就不在此展开了
后记
后面可以将系统的AlertView也管理起来,不过现在还没有想好一个比较优雅的方案
源码
具体源码请前往:SSPPopupCenter