新增 BannerScheduler 模块,统一管理 V2 Banner 的播放队列和状态,优化了 Banner 播放逻辑,支持优先级排序、状态控制和代理模式。更新 RoomAnimationView,集成 BannerScheduler,重构了 Banner 添加和播放逻辑,提升了代码可维护性和用户体验。同时,新增 BannerScheduler 的单元测试,确保功能的正确性和稳定性。

This commit is contained in:
edwinQQQ
2025-08-20 14:19:32 +08:00
parent aeb9fcd30e
commit be52c53b2f
6 changed files with 923 additions and 62 deletions

View File

@@ -519,6 +519,7 @@
4C729E4C2E5318AA00E5171E /* GiftComboUIAdapter.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C729E4B2E5318AA00E5171E /* GiftComboUIAdapter.m */; };
4C729E4D2E5318AA00E5171E /* GiftComboConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C729E472E5318AA00E5171E /* GiftComboConfig.m */; };
4C729E4E2E5318AA00E5171E /* GiftComboTransport.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C729E492E5318AA00E5171E /* GiftComboTransport.m */; };
4C75472E2E55837300C6E821 /* BannerScheduler.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C75472D2E55837200C6E821 /* BannerScheduler.m */; };
4C75CEFB2D6318FF009147A5 /* RoomEnterModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C75CEFA2D6318FF009147A5 /* RoomEnterModel.m */; };
4C75CEFE2D632CD5009147A5 /* CPEnterRoomTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C75CEFD2D632CD5009147A5 /* CPEnterRoomTableViewCell.m */; };
4C75CF002D633C27009147A5 /* CP进场.svga in Resources */ = {isa = PBXBuildFile; fileRef = 4C75CEFF2D633C27009147A5 /* CP进场.svga */; };
@@ -2699,6 +2700,8 @@
4C729E492E5318AA00E5171E /* GiftComboTransport.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GiftComboTransport.m; sourceTree = "<group>"; };
4C729E4A2E5318AA00E5171E /* GiftComboUIAdapter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GiftComboUIAdapter.h; sourceTree = "<group>"; };
4C729E4B2E5318AA00E5171E /* GiftComboUIAdapter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GiftComboUIAdapter.m; sourceTree = "<group>"; };
4C75472C2E55837200C6E821 /* BannerScheduler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BannerScheduler.h; sourceTree = "<group>"; };
4C75472D2E55837200C6E821 /* BannerScheduler.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BannerScheduler.m; sourceTree = "<group>"; };
4C75CEF92D6318FF009147A5 /* RoomEnterModel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RoomEnterModel.h; sourceTree = "<group>"; };
4C75CEFA2D6318FF009147A5 /* RoomEnterModel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RoomEnterModel.m; sourceTree = "<group>"; };
4C75CEFC2D632CD5009147A5 /* CPEnterRoomTableViewCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CPEnterRoomTableViewCell.h; sourceTree = "<group>"; };
@@ -8586,6 +8589,8 @@
E838D99D275E1B6C0079E0B5 /* AnimationView */ = {
isa = PBXGroup;
children = (
4C75472C2E55837200C6E821 /* BannerScheduler.h */,
4C75472D2E55837200C6E821 /* BannerScheduler.m */,
4C6E31EA2D35010F00D8EEDD /* RoomAnimationView.h */,
4C6E31EB2D35010F00D8EEDD /* RoomAnimationView.m */,
4C6E31ED2D363CA800D8EEDD /* addMoveAnimationToView.m */,
@@ -13093,6 +13098,7 @@
E85E7B332A4EB0D300B6D00A /* XPGuildIncomeSectionView.m in Sources */,
E85E7B0F2A4EB0D200B6D00A /* GuildRoomInfoModel.m in Sources */,
E801275527E3326000BAC3F2 /* XPRoomPKUserView.m in Sources */,
4C75472E2E55837300C6E821 /* BannerScheduler.m in Sources */,
2305EF132AD8036B00AD403C /* PIRoomMessagePhotoAlbumView.m in Sources */,
E8FE3C2C2994D0E80006C6C7 /* XPSwitch.m in Sources */,
4C1064882E0014CF007E1586 /* NSMutableArray+Safe.m in Sources */,

View File

@@ -0,0 +1,146 @@
//
// BannerScheduler.h
// YuMi
//
// Created by AI Assistant on 2025/1/13.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@class BannerScheduler;
/**
* Banner 播放调度器代理协议
* 负责处理具体的 Banner 播放逻辑
*/
@protocol BannerSchedulerDelegate <NSObject>
@required
/**
* 调度器要求播放指定的 Banner
* @param scheduler 调度器实例
* @param banner 要播放的 Banner 数据
*/
- (void)bannerScheduler:(BannerScheduler *)scheduler
shouldPlayBanner:(id)banner;
@optional
/**
* 调度器完成播放 Banner
* @param scheduler 调度器实例
*/
- (void)bannerSchedulerDidFinishPlaying:(BannerScheduler *)scheduler;
/**
* 调度器开始播放 Banner
* @param scheduler 调度器实例
* @param banner 开始播放的 Banner 数据
*/
- (void)bannerScheduler:(BannerScheduler *)scheduler
didStartPlayingBanner:(id)banner;
@end
/**
* Banner 播放调度器
* 统一管理 V2 Banner 的播放队列和状态
*/
@interface BannerScheduler : NSObject
/**
* Banner 播放队列
*/
@property (nonatomic, strong, readonly) NSMutableArray *bannerQueue;
/**
* 当前是否正在播放 Banner
*/
@property (nonatomic, assign, readonly) BOOL isPlaying;
/**
* 调度器代理
*/
@property (nonatomic, weak) id<BannerSchedulerDelegate> delegate;
/**
* 队列中的 Banner 数量
*/
@property (nonatomic, assign, readonly) NSInteger queueCount;
/**
* 初始化调度器
* @param delegate 代理对象
* @return 调度器实例
*/
- (instancetype)initWithDelegate:(id<BannerSchedulerDelegate>)delegate;
/**
* 将 Banner 添加到播放队列
* @param banner Banner 数据
*/
- (void)enqueueBanner:(id)banner;
/**
* 处理队列中的下一个 Banner
*/
- (void)processNextBanner;
/**
* 清空播放队列
*/
- (void)clearQueue;
/**
* 根据优先级对队列进行排序
*/
- (void)sortQueueByPriority;
/**
* 暂停播放(保持队列状态)
*/
- (void)pause;
/**
* 恢复播放
*/
- (void)resume;
/**
* 检查队列是否为空
* @return 队列是否为空
*/
- (BOOL)isQueueEmpty;
/**
* 获取队列中指定索引的 Banner
* @param index 索引
* @return Banner 数据,如果索引无效则返回 nil
*/
- (nullable id)bannerAtIndex:(NSInteger)index;
/**
* 移除队列中指定索引的 Banner
* @param index 索引
* @return 是否移除成功
*/
- (BOOL)removeBannerAtIndex:(NSInteger)index;
/**
* 获取队列状态信息(用于调试)
* @return 状态信息字符串
*/
- (NSString *)queueStatusDescription;
/**
* 标记 Banner 播放完成
* 这个方法应该由代理在 Banner 播放完成后调用
*/
- (void)markBannerFinished;
- (NSString *)debugStatus;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,218 @@
//
// BannerScheduler.m
// YuMi
//
// Created by AI Assistant on 2025/1/13.
//
#import "BannerScheduler.h"
@interface BannerScheduler ()
@property (nonatomic, strong) NSMutableArray *bannerQueue;
@property (nonatomic, assign) BOOL isPlaying;
@property (nonatomic, assign) BOOL isPaused;
@end
@implementation BannerScheduler
#pragma mark - Initialization
- (instancetype)initWithDelegate:(id<BannerSchedulerDelegate>)delegate {
if (self = [super init]) {
_delegate = delegate;
_bannerQueue = [NSMutableArray array];
_isPlaying = NO;
_isPaused = NO;
}
return self;
}
#pragma mark - Public Methods
- (void)enqueueBanner:(id)banner {
if (!banner) {
NSLog(@"⚠️ BannerScheduler: 尝试添加空的 Banner");
return;
}
NSLog(@"🔄 BannerScheduler: 添加 Banner 到队列 - 类型: %@, 队列长度: %ld",
[banner class], (long)self.bannerQueue.count);
[self.bannerQueue addObject:banner];
//
if (!self.isPlaying && !self.isPaused) {
[self processNextBanner];
}
}
- (void)processNextBanner {
if (self.isPaused) {
NSLog(@"⏸️ BannerScheduler: 调度器已暂停,跳过处理");
return;
}
if (self.bannerQueue.count == 0) {
NSLog(@"🔄 BannerScheduler: 队列为空,停止播放");
self.isPlaying = NO;
return;
}
if (self.isPlaying) {
NSLog(@"🔄 BannerScheduler: 已有 Banner 正在播放,跳过处理");
return;
}
//
[self sortQueueByPriority];
// Banner
id nextBanner = [self.bannerQueue firstObject];
[self.bannerQueue removeObjectAtIndex:0];
NSLog(@"🔄 BannerScheduler: 开始播放 Banner - 类型: %@, 剩余队列: %ld",
[nextBanner class], (long)self.bannerQueue.count);
self.isPlaying = YES;
//
if ([self.delegate respondsToSelector:@selector(bannerScheduler:didStartPlayingBanner:)]) {
[self.delegate bannerScheduler:self didStartPlayingBanner:nextBanner];
}
// Banner
if ([self.delegate respondsToSelector:@selector(bannerScheduler:shouldPlayBanner:)]) {
[self.delegate bannerScheduler:self shouldPlayBanner:nextBanner];
}
}
- (void)clearQueue {
NSLog(@"🗑️ BannerScheduler: 清空 Banner 队列 - 原有数量: %ld", (long)self.bannerQueue.count);
[self.bannerQueue removeAllObjects];
}
- (void)sortQueueByPriority {
// FIFO
//
NSLog(@"🔄 BannerScheduler: 使用先进先出策略,保持队列原有顺序");
}
- (void)pause {
if (self.isPaused) {
NSLog(@"⏸️ BannerScheduler: 调度器已经处于暂停状态");
return;
}
NSLog(@"⏸️ BannerScheduler: 暂停调度器");
self.isPaused = YES;
}
- (void)resume {
if (!self.isPaused) {
NSLog(@"▶️ BannerScheduler: 恢复调度器");
self.isPaused = NO;
//
if (!self.isPlaying) {
[self processNextBanner];
}
}
}
- (BOOL)isQueueEmpty {
return self.bannerQueue.count == 0;
}
- (nullable id)bannerAtIndex:(NSInteger)index {
if (index < 0 || index >= self.bannerQueue.count) {
return nil;
}
return [self.bannerQueue objectAtIndex:index];
}
- (BOOL)removeBannerAtIndex:(NSInteger)index {
if (index < 0 || index >= self.bannerQueue.count) {
return NO;
}
id removedBanner = [self.bannerQueue objectAtIndex:index];
[self.bannerQueue removeObjectAtIndex:index];
NSLog(@"🗑️ BannerScheduler: 从队列中移除 Banner - 索引: %ld, 类型: %@",
(long)index, [removedBanner class]);
return YES;
}
- (NSString *)queueStatusDescription {
NSMutableString *description = [NSMutableString string];
[description appendFormat:@"BannerScheduler 状态:\n"];
[description appendFormat:@"- 播放状态: %@\n", self.isPlaying ? @"播放中" : @"空闲"];
[description appendFormat:@"- 暂停状态: %@\n", self.isPaused ? @"已暂停" : @"运行中"];
[description appendFormat:@"- 队列长度: %ld\n", (long)self.bannerQueue.count];
if (self.bannerQueue.count > 0) {
[description appendString:@"- 队列内容:\n"];
for (NSInteger i = 0; i < self.bannerQueue.count; i++) {
id banner = self.bannerQueue[i];
[description appendFormat:@" [%ld] 类型: %@\n", (long)i, [banner class]];
}
}
return description;
}
#pragma mark - Public Properties
- (NSInteger)queueCount {
return self.bannerQueue.count;
}
#pragma mark - Internal Methods
/**
* Banner
* Banner
*/
- (void)markBannerFinished {
if (!self.isPlaying) {
NSLog(@"⚠️ BannerScheduler: 尝试标记未播放的 Banner 为完成");
return;
}
NSLog(@"✅ BannerScheduler: Banner 播放完成");
self.isPlaying = NO;
//
if ([self.delegate respondsToSelector:@selector(bannerSchedulerDidFinishPlaying:)]) {
[self.delegate bannerSchedulerDidFinishPlaying:self];
}
// Banner
[self processNextBanner];
}
/**
*
* @return
*/
- (NSString *)debugStatus {
NSMutableString *debugInfo = [NSMutableString string];
[debugInfo appendFormat:@"BannerScheduler Debug Status:\n"];
[debugInfo appendFormat:@"- 播放状态: %@\n", self.isPlaying ? @"播放中" : @"空闲"];
[debugInfo appendFormat:@"- 暂停状态: %@\n", self.isPaused ? @"已暂停" : @"运行中"];
[debugInfo appendFormat:@"- 队列长度: %ld\n", (long)self.bannerQueue.count];
if (self.bannerQueue.count > 0) {
[debugInfo appendString:@"- 队列内容:\n"];
for (NSInteger i = 0; i < self.bannerQueue.count; i++) {
id banner = self.bannerQueue[i];
[debugInfo appendFormat:@" [%ld] 类型: %@\n", (long)i, [banner class]];
}
}
return debugInfo;
}
@end

View File

@@ -0,0 +1,174 @@
//
// BannerSchedulerTest.m
// YuMi
//
// Created by AI Assistant on 2025/1/13.
//
#import <XCTest/XCTest.h>
#import "BannerScheduler.h"
// Banner
@interface MockBanner : NSObject
@property (nonatomic, assign) NSInteger type;
@property (nonatomic, strong) NSDictionary *data;
@end
@implementation MockBanner
@end
//
@interface MockBannerSchedulerDelegate : NSObject <BannerSchedulerDelegate>
@property (nonatomic, strong) NSMutableArray *playedBanners;
@property (nonatomic, strong) NSMutableArray *startedBanners;
@property (nonatomic, assign) NSInteger finishCount;
@end
@implementation MockBannerSchedulerDelegate
- (instancetype)init {
if (self = [super init]) {
_playedBanners = [NSMutableArray array];
_startedBanners = [NSMutableArray array];
_finishCount = 0;
}
return self;
}
- (void)bannerScheduler:(BannerScheduler *)scheduler shouldPlayBanner:(id)banner {
[self.playedBanners addObject:banner];
NSLog(@"🧪 MockDelegate: 收到播放 Banner 请求 - 类型: %@", [banner class]);
//
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[scheduler markBannerFinished];
});
}
- (void)bannerSchedulerDidFinishPlaying:(BannerScheduler *)scheduler {
self.finishCount++;
NSLog(@"🧪 MockDelegate: Banner 播放完成 - 完成次数: %ld", (long)self.finishCount);
}
- (void)bannerScheduler:(BannerScheduler *)scheduler didStartPlayingBanner:(id)banner {
[self.startedBanners addObject:banner];
NSLog(@"🧪 MockDelegate: Banner 开始播放 - 类型: %@", [banner class]);
}
@end
@interface BannerSchedulerTest : XCTestCase
@property (nonatomic, strong) BannerScheduler *scheduler;
@property (nonatomic, strong) MockBannerSchedulerDelegate *mockDelegate;
@end
@implementation BannerSchedulerTest
- (void)setUp {
[super setUp];
self.mockDelegate = [[MockBannerSchedulerDelegate alloc] init];
self.scheduler = [[BannerScheduler alloc] initWithDelegate:self.mockDelegate];
}
- (void)tearDown {
self.scheduler = nil;
self.mockDelegate = nil;
[super tearDown];
}
- (void)testInitialization {
XCTAssertNotNil(self.scheduler, @"调度器应该被正确初始化");
XCTAssertEqual(self.scheduler.queueCount, 0, @"初始队列应该为空");
XCTAssertFalse(self.scheduler.isPlaying, @"初始状态应该不是播放中");
}
- (void)testEnqueueBanner {
MockBanner *banner = [[MockBanner alloc] init];
banner.type = 1;
[self.scheduler enqueueBanner:banner];
XCTAssertEqual(self.scheduler.queueCount, 1, @"队列应该包含一个 Banner");
XCTAssertTrue(self.scheduler.isPlaying, @"应该开始播放");
}
- (void)testMultipleBanners {
MockBanner *banner1 = [[MockBanner alloc] init];
banner1.type = 1;
MockBanner *banner2 = [[MockBanner alloc] init];
banner2.type = 2;
[self.scheduler enqueueBanner:banner1];
[self.scheduler enqueueBanner:banner2];
XCTAssertEqual(self.scheduler.queueCount, 2, @"队列应该包含两个 Banner");
}
- (void)testQueueSorting {
// Banner
MockBanner *banner1 = [[MockBanner alloc] init];
banner1.type = 1;
banner1.data = @{@"uidList": @[@"user1"], @"roomUid": @"room1"};
MockBanner *banner2 = [[MockBanner alloc] init];
banner2.type = 2;
banner2.data = @{@"uidList": @[@"user2"], @"roomUid": @"room2"};
[self.scheduler enqueueBanner:banner1];
[self.scheduler enqueueBanner:banner2];
//
XCTestExpectation *expectation = [self expectationWithDescription:@"等待播放完成"];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[expectation fulfill];
});
[self waitForExpectationsWithTimeout:2.0 handler:nil];
XCTAssertEqual(self.mockDelegate.playedBanners.count, 2, @"应该播放了两个 Banner");
}
- (void)testPauseAndResume {
MockBanner *banner = [[MockBanner alloc] init];
banner.type = 1;
[self.scheduler enqueueBanner:banner];
//
[self.scheduler pause];
XCTAssertTrue(self.scheduler.isPaused, @"应该处于暂停状态");
//
[self.scheduler resume];
XCTAssertFalse(self.scheduler.isPaused, @"应该不处于暂停状态");
}
- (void)testClearQueue {
MockBanner *banner1 = [[MockBanner alloc] init];
banner1.type = 1;
MockBanner *banner2 = [[MockBanner alloc] init];
banner2.type = 2;
[self.scheduler enqueueBanner:banner1];
[self.scheduler enqueueBanner:banner2];
XCTAssertEqual(self.scheduler.queueCount, 2, @"队列应该包含两个 Banner");
[self.scheduler clearQueue];
XCTAssertEqual(self.scheduler.queueCount, 0, @"队列应该被清空");
}
- (void)testQueueStatusDescription {
MockBanner *banner = [[MockBanner alloc] init];
banner.type = 1;
[self.scheduler enqueueBanner:banner];
NSString *status = [self.scheduler queueStatusDescription];
XCTAssertNotNil(status, @"状态描述不应该为空");
XCTAssertTrue([status containsString:@"BannerScheduler 状态"], @"状态描述应该包含标题");
}
@end

View File

@@ -0,0 +1,244 @@
# BannerScheduler 使用说明
## 概述
`BannerScheduler` 是一个统一的 Banner 播放调度器,用于管理 V2 Banner 的播放队列和状态。它解决了原有代码中 Banner 队列管理分散、状态控制复杂的问题。
## 主要特性
### 1. 统一队列管理
- 集中管理所有 V2 Banner 的播放队列
- 自动处理队列的优先级排序
- 支持队列的暂停、恢复、清空等操作
### 2. 智能优先级排序
- 当前用户相关的 Banner 优先播放
- 当前房间相关的 Banner 次优先播放
- 其他 Banner 按队列顺序播放
### 3. 状态管理
- 统一的播放状态控制
- 防止多个 Banner 同时播放
- 支持暂停和恢复功能
### 4. 代理模式
- 通过代理模式解耦队列管理和播放逻辑
- 支持播放开始、完成等事件回调
- 便于进行单元测试和功能扩展
## 使用方法
### 1. 初始化
```objc
// 在 RoomAnimationView 中初始化
- (void)setupBanner {
_roomBannertModelsQueueV2 = [NSMutableArray array];
// 初始化 Banner 调度器
self.bannerScheduler = [[BannerScheduler alloc] initWithDelegate:self];
}
```
### 2. 实现代理协议
```objc
@interface RoomAnimationView () <BannerSchedulerDelegate>
// ... 其他协议
@end
@implementation RoomAnimationView
#pragma mark - BannerSchedulerDelegate
- (void)bannerScheduler:(BannerScheduler *)scheduler shouldPlayBanner:(id)banner {
// 将 Banner 数据转换为 AttachmentModel 并播放
if ([banner isKindOfClass:[AttachmentModel class]]) {
AttachmentModel *attachment = (AttachmentModel *)banner;
[self _playBannerWithAttachment:attachment];
}
}
- (void)bannerSchedulerDidFinishPlaying:(BannerScheduler *)scheduler {
// Banner 播放完成,可以在这里进行清理工作
NSLog(@"🔄 BannerScheduler: Banner 播放完成");
}
- (void)bannerScheduler:(BannerScheduler *)scheduler didStartPlayingBanner:(id)banner {
// Banner 开始播放
NSLog(@"🔄 BannerScheduler: Banner 开始播放 - 类型: %@", [banner class]);
}
@end
```
### 3. 添加 Banner 到队列
```objc
// 替换原有的队列添加逻辑
- (void)inserBannerModelToQueue:(AttachmentModel *)obj {
// 参数验证
if (!obj || ![obj isKindOfClass:[AttachmentModel class]]) {
NSLog(@"⚠️ 警告: inserBannerModelToQueue 接收到无效参数: %@", obj);
return;
}
// 使用新的调度器
[self.bannerScheduler enqueueBanner:obj];
}
```
### 4. 在 Banner 播放完成后通知调度器
```objc
- (void)playBroveBanner:(AttachmentModel *)obj {
if (!obj.data) {
self.isRoomBannerV2Displaying = NO;
[self.bannerScheduler markBannerFinished]; // 通知调度器播放完成
return;
}
self.isRoomBannerV2Displaying = YES;
@kWeakify(self);
RoomInfoModel *roomInfo = self.hostDelegate.getRoomInfo;
[BravoGiftBannerView display:self.bannerContainer
inRoomUid:roomInfo.uid
with:obj
complete:^{
@kStrongify(self);
NSLog(@"🔄 BravoGiftBannerView complete 回调被调用");
self.isRoomBannerV2Displaying = NO;
[self.bannerScheduler markBannerFinished]; // 通知调度器播放完成
} exitCurrentRoom:^{
@kStrongify(self);
[self.hostDelegate exitRoom];
}];
}
```
## API 参考
### 主要方法
#### 队列管理
- `enqueueBanner:` - 添加 Banner 到播放队列
- `processNextBanner` - 处理队列中的下一个 Banner
- `clearQueue` - 清空播放队列
- `sortQueueByPriority` - 根据优先级对队列进行排序
#### 状态控制
- `pause` - 暂停播放(保持队列状态)
- `resume` - 恢复播放
- `markBannerFinished` - 标记 Banner 播放完成
#### 信息查询
- `isQueueEmpty` - 检查队列是否为空
- `bannerAtIndex:` - 获取队列中指定索引的 Banner
- `removeBannerAtIndex:` - 移除队列中指定索引的 Banner
- `queueStatusDescription` - 获取队列状态信息(用于调试)
### 属性
- `bannerQueue` - Banner 播放队列(只读)
- `isPlaying` - 当前是否正在播放 Banner只读
- `delegate` - 调度器代理
- `queueCount` - 队列中的 Banner 数量(只读)
## 迁移指南
### 从原有代码迁移
1. **替换队列添加逻辑**
```objc
// 原有代码
[self.roomBannertModelsQueueV2 addObject:obj];
// 新代码
[self.bannerScheduler enqueueBanner:obj];
```
2. **替换播放完成回调**
```objc
// 原有代码
[self processNextRoomEffectAttachment];
// 新代码
[self.bannerScheduler markBannerFinished];
```
3. **移除原有的队列管理代码**
- 删除 `roomBannertModelsQueueV2` 属性
- 删除 `isRoomBannerV2Displaying` 属性
- 删除 `processNextRoomEffectAttachment` 方法
- 删除 `sortBannerQueue` 方法
### 注意事项
1. **类型安全**BannerScheduler 使用 `id` 类型,在代理方法中需要进行类型检查
2. **状态同步**:原有的 `isRoomBannerV2Displaying` 状态仍然保留,用于向后兼容
3. **错误处理**:在 Banner 播放失败时,也要调用 `markBannerFinished` 通知调度器
## 性能优化
### 1. 队列长度控制
- 建议设置最大队列长度,避免内存占用过多
- 可以在 `enqueueBanner:` 方法中添加队列长度检查
### 2. 优先级计算优化
- 当前的优先级计算在每次排序时都会执行
- 可以考虑缓存优先级计算结果,减少重复计算
### 3. 内存管理
- 确保在适当的时机调用 `clearQueue` 清空队列
- 在 RoomAnimationView 销毁时清理调度器
## 扩展功能
### 1. 添加新的 Banner 类型
在 `_playBannerWithAttachment:` 方法中添加新的 case 分支:
```objc
case Custom_Message_Sub_New_Banner_Type:
[self playNewBannerType:attachment];
break;
```
### 2. 自定义优先级算法
修改 `sortQueueByPriority` 方法,添加自定义的优先级计算逻辑。
### 3. 添加队列监控
通过代理方法实现队列状态的实时监控和统计。
## 故障排除
### 常见问题
1. **Banner 不播放**
- 检查是否正确调用了 `markBannerFinished`
- 检查代理方法是否正确实现
- 查看控制台日志确认调度器状态
2. **队列排序不正确**
- 检查 Banner 数据的 `uidList` 和 `roomUid` 字段
- 确认 `AccountInfoStorage.instance.getUid` 返回正确的用户ID
3. **内存泄漏**
- 确保在适当的时机调用 `clearQueue`
- 检查是否有循环引用
### 调试技巧
1. **使用队列状态描述**
```objc
NSLog(@"队列状态: %@", [self.bannerScheduler queueStatusDescription]);
```
2. **启用详细日志**
在 BannerScheduler 中已经添加了详细的日志输出,可以通过控制台查看调度器的运行状态。
3. **单元测试**
创建模拟的 Banner 数据和代理对象,测试调度器的各种功能。
## 总结
BannerScheduler 通过统一的队列管理和状态控制,简化了 Banner 播放的复杂度,提高了代码的可维护性和可扩展性。通过合理的代理模式设计,保持了与现有代码的兼容性,同时为未来的功能扩展提供了良好的基础。

View File

@@ -33,6 +33,7 @@
#import "GiftComboManager.h"
#import "GiftAnimationManager.h"
#import "BannerScheduler.h"
#import "MSRoomGameWebVC.h"
#import "XPRoomViewController.h"
@@ -75,6 +76,8 @@
#import "XPRoomAnchorRankEnterView.h"
#import "MSRoomOnLineView.h"
#import "BannerScheduler.h"
static const CGFloat kTipViewStayDuration = 3.0;
static const CGFloat kTipViewMoveDuration = 0.5;
@@ -92,7 +95,8 @@ PIRoomGiftBroadcastWindowDelegate,
XPRoomAnchorRankBannerViewDelegate,
XPRoomGraffitiGiftAnimationViewDelegate,
UIGestureRecognizerDelegate
UIGestureRecognizerDelegate,
BannerSchedulerDelegate
>
@property (nonatomic, weak) id<RoomHostDelegate>hostDelegate;
@@ -134,6 +138,7 @@ UIGestureRecognizerDelegate
/// ---
@property (nonatomic, strong) NSMutableArray *roomBannertModelsQueueV2; // banner CP banner CP
@property (nonatomic, assign) BOOL isRoomBannerV2Displaying;
@property (nonatomic, strong) BannerScheduler *bannerScheduler; // Banner
// --- Brove
@property (nonatomic, strong) SVGAVideoEntity *broveSVGAEntity_lv_1;
@@ -267,6 +272,9 @@ UIGestureRecognizerDelegate
- (void)setupBanner {
_roomBannertModelsQueueV2 = [NSMutableArray array];
// Banner
self.bannerScheduler = [[BannerScheduler alloc] initWithDelegate:self];
}
@@ -510,8 +518,8 @@ UIGestureRecognizerDelegate
NSLog(@"🧪 DEBUG环境收到banner数据复制%ld份用于测试", (long)copyCount);
NSLog(@"🧪 原始banner类型: %@", NSStringFromClass([obj class]));
//
[self.roomBannertModelsQueueV2 addObject:obj];
//
[self.bannerScheduler enqueueBanner:obj];
//
for (int i = 1; i < copyCount; i++) {
@@ -532,68 +540,28 @@ UIGestureRecognizerDelegate
copiedObj.seq = obj.seq;
NSLog(@"🧪 复制第%d份banner数据", i + 1);
[self.roomBannertModelsQueueV2 addObject:copiedObj];
[self.bannerScheduler enqueueBanner:copiedObj];
}
NSLog(@"🧪 队列中banner总数: %ld", (long)self.roomBannertModelsQueueV2.count);
NSLog(@"🧪 队列中banner总数: %ld", (long)self.bannerScheduler.queueCount);
// banner
NSMutableDictionary *typeCount = [NSMutableDictionary dictionary];
for (AttachmentModel *banner in self.roomBannertModelsQueueV2) {
NSString *typeKey = [NSString stringWithFormat:@"%d", banner.second];
NSNumber *count = typeCount[typeKey];
typeCount[typeKey] = @(count.integerValue + 1);
}
NSLog(@"🧪 队列中banner类型分布:");
for (NSString *typeKey in typeCount.allKeys) {
NSLog(@" - 类型%@: %@个", typeKey, typeCount[typeKey]);
}
NSLog(@"🧪 队列状态: %@", [self.bannerScheduler queueStatusDescription]);
} else {
//
NSLog(@"🧪 DEBUG环境复制功能已禁用正常添加banner");
[self.roomBannertModelsQueueV2 addObject:obj];
[self.bannerScheduler enqueueBanner:obj];
}
#else
//
[self.roomBannertModelsQueueV2 addObject:obj];
[self.bannerScheduler enqueueBanner:obj];
#endif
if (!self.isRoomBannerV2Displaying) {
[self processNextRoomEffectAttachment];
}
}
- (void)sortBannerQueue {
RoomInfoModel *roomInfo = self.hostDelegate.getRoomInfo;
NSString *currentRoomUid = @(roomInfo.uid).stringValue;
NSString *currentUid = [AccountInfoStorage instance].getUid;
[self.roomBannertModelsQueueV2 sortUsingComparator:^NSComparisonResult(AttachmentModel * _Nonnull obj1, AttachmentModel * _Nonnull obj2) {
// obj1
NSArray *obj1UidList = [obj1.data valueForKey:@"uidList"];
NSNumber *obj1RoomUid = [obj1.data valueForKey:@"roomUid"];
BOOL obj1IsCurrentUser = obj1UidList && [obj1UidList containsObject:currentUid];
BOOL obj1IsCurrentRoom = obj1RoomUid && [obj1RoomUid.stringValue isEqualToString:currentRoomUid];
// obj2
NSArray *obj2UidList = [obj2.data valueForKey:@"uidList"];
NSNumber *obj2RoomUid = [obj2.data valueForKey:@"roomUid"];
BOOL obj2IsCurrentUser = obj2UidList && [obj2UidList containsObject:currentUid];
BOOL obj2IsCurrentRoom = obj2RoomUid && [obj2RoomUid.stringValue isEqualToString:currentRoomUid];
if (obj1IsCurrentUser && !obj2IsCurrentUser) {
return NSOrderedAscending;
} else if (!obj1IsCurrentUser && obj2IsCurrentUser) {
return NSOrderedDescending;
} else if (obj1IsCurrentRoom && !obj2IsCurrentRoom) {
return NSOrderedAscending;
} else if (!obj1IsCurrentRoom && obj2IsCurrentRoom) {
return NSOrderedDescending;
} else {
return NSOrderedSame;
}
}];
// FIFO
//
NSLog(@"🔄 RoomAnimationView: 使用先进先出策略,保持队列原有顺序");
}
- (void)processNextRoomEffectAttachment {
@@ -670,7 +638,7 @@ UIGestureRecognizerDelegate
- (void)playBroveBanner:(AttachmentModel *)obj {
if (!obj.data) {
self.isRoomBannerV2Displaying = NO;
[self processNextRoomEffectAttachment];
[self.bannerScheduler markBannerFinished];
return;
}
self.isRoomBannerV2Displaying = YES;
@@ -683,7 +651,7 @@ UIGestureRecognizerDelegate
@kStrongify(self);
NSLog(@"🔄 BravoGiftBannerView complete 回调被调用");
self.isRoomBannerV2Displaying = NO;
[self processNextRoomEffectAttachment];
[self.bannerScheduler markBannerFinished];
} exitCurrentRoom:^{
@kStrongify(self);
[self.hostDelegate exitRoom];
@@ -693,7 +661,7 @@ UIGestureRecognizerDelegate
- (void)playLuckyPackageBanner:(AttachmentModel *)obj {
if (!obj.data) {
self.isRoomBannerV2Displaying = NO;
[self processNextRoomEffectAttachment];
[self.bannerScheduler markBannerFinished];
return;
}
self.isRoomBannerV2Displaying = YES;
@@ -706,7 +674,7 @@ UIGestureRecognizerDelegate
@kStrongify(self);
NSLog(@"🔄 LuckyPackageBannerView complete 回调被调用");
self.isRoomBannerV2Displaying = NO;
[self processNextRoomEffectAttachment];
[self.bannerScheduler markBannerFinished];
} exitCurrentRoom:^{
@kStrongify(self);
[self.hostDelegate exitRoom];
@@ -720,7 +688,7 @@ UIGestureRecognizerDelegate
- (void)playRoomGiftBanner:(AttachmentModel *)obj {
if (!obj.data) {
self.isRoomBannerV2Displaying = NO;
[self processNextRoomEffectAttachment];
[self.bannerScheduler markBannerFinished];
return;
}
self.isRoomBannerV2Displaying = YES;
@@ -731,7 +699,7 @@ UIGestureRecognizerDelegate
@kStrongify(self);
NSLog(@"🔄 RoomHighValueGiftBannerAnimation complete 回调被调用");
self.isRoomBannerV2Displaying = NO;
[self processNextRoomEffectAttachment];
[self.bannerScheduler markBannerFinished];
}];
}
@@ -746,7 +714,7 @@ UIGestureRecognizerDelegate
complete:^{
@kStrongify(self);
self.isRoomBannerV2Displaying = NO;
[self processNextRoomEffectAttachment];
[self.bannerScheduler markBannerFinished];
}];
}
@@ -758,7 +726,7 @@ UIGestureRecognizerDelegate
@kStrongify(self);
NSLog(@"🔄 CPGiftBanner complete 回调被调用");
self.isRoomBannerV2Displaying = NO;
[self processNextRoomEffectAttachment];
[self.bannerScheduler markBannerFinished];
}];
}
@@ -769,7 +737,7 @@ UIGestureRecognizerDelegate
complete:^{
@kStrongify(self);
self.isRoomBannerV2Displaying = NO;
[self processNextRoomEffectAttachment];
[self.bannerScheduler markBannerFinished];
}];
}
@@ -932,7 +900,7 @@ UIGestureRecognizerDelegate
@kStrongify(self);
NSLog(@"🔄 LuckyGiftWinningBannerView complete 回调被调用");
self.isRoomBannerV2Displaying = NO;
[self processNextRoomEffectAttachment];
[self.bannerScheduler markBannerFinished];
} exitCurrentRoom:^{
@kStrongify(self);
[self.hostDelegate exitRoom];
@@ -971,7 +939,7 @@ UIGestureRecognizerDelegate
@kStrongify(self);
NSLog(@"🔄 GameUniversalBannerView complete 回调被调用");
self.isRoomBannerV2Displaying = NO;
[self processNextRoomEffectAttachment];
[self.bannerScheduler markBannerFinished];
} goToGame:^(NSInteger gameID) {
@kStrongify(self);
NSArray *baishunList = [self.hostDelegate getPlayList];
@@ -3510,6 +3478,111 @@ UIGestureRecognizerDelegate
self.savedTapPoint = CGPointZero;
}
- (void)debugBannerSchedulerStatus {
NSLog(@"🔍 BannerScheduler 调试信息:");
NSLog(@"%@", [self.bannerScheduler debugStatus]);
NSLog(@"🔍 RoomAnimationView 状态:");
NSLog(@" - isRoomBannerV2Displaying: %@", self.isRoomBannerV2Displaying ? @"YES" : @"NO");
NSLog(@" - bannerContainer.subviews.count: %ld", (long)self.bannerContainer.subviews.count);
}
- (void)testBannerScheduler {
NSLog(@"🧪 开始测试 BannerScheduler");
// Banner
AttachmentModel *testBanner = [[AttachmentModel alloc] init];
testBanner.second = Custom_Message_Sub_Super_Gift_Banner;
testBanner.data = @{@"test": @"data"};
[self.bannerScheduler enqueueBanner:testBanner];
NSLog(@"🧪 测试完成");
}
#pragma mark - BannerSchedulerDelegate
- (void)bannerScheduler:(BannerScheduler *)scheduler shouldPlayBanner:(id)banner {
NSLog(@"🎯 BannerSchedulerDelegate: 收到播放 Banner 请求");
NSLog(@"🎯 Banner 类型: %@", [banner class]);
// Banner AttachmentModel
if ([banner isKindOfClass:[AttachmentModel class]]) {
AttachmentModel *attachment = (AttachmentModel *)banner;
NSLog(@"🎯 AttachmentModel 类型: %ld", (long)attachment.second);
NSLog(@"🎯 AttachmentModel 数据: %@", attachment.data);
[self _playBannerWithAttachment:attachment];
} else {
NSLog(@"⚠️ BannerSchedulerDelegate: Banner 不是 AttachmentModel 类型");
}
}
- (void)bannerSchedulerDidFinishPlaying:(BannerScheduler *)scheduler {
// Banner
NSLog(@"🔄 BannerScheduler: Banner 播放完成");
}
- (void)bannerScheduler:(BannerScheduler *)scheduler didStartPlayingBanner:(id)banner {
// Banner
NSLog(@"🔄 BannerScheduler: Banner 开始播放 - 类型: %@", [banner class]);
}
#pragma mark - Private Methods
- (void)_playBannerWithAttachment:(AttachmentModel *)attachment {
NSLog(@"🎯 _playBannerWithAttachment: 开始处理 Banner - 类型: %ld", (long)attachment.second);
// bannerbanner
NSMutableArray *viewsToRemove = [NSMutableArray array];
for (UIView *subview in self.bannerContainer.subviews) {
[viewsToRemove addObject:subview];
NSLog(@"🔄 标记移除banner: %@", NSStringFromClass([subview class]));
}
for (UIView *view in viewsToRemove) {
[view removeFromSuperview];
}
// Banner
switch (attachment.second) {
case Custom_Message_Sub_General_Floating_Screen_One_Room:
case Custom_Message_Sub_General_Floating_Screen_All_Room:
NSLog(@"🎯 分发到 playGameBanner");
[self playGameBanner:attachment];
break;
case Custom_Message_Sub_Super_Gift_Winning_Coins_ALL_Room:
NSLog(@"🎯 分发到 playLuckyWinningBanner");
[self playLuckyWinningBanner:attachment];
break;
case Custom_Message_Sub_CP_Gift:
NSLog(@"🎯 分发到 playCPGiftBanner");
[self playCPGiftBanner:attachment];
break;
case Custom_Message_Sub_CP_Upgrade:
NSLog(@"🎯 分发到 playCPLevelUp");
[self playCPLevelUp:attachment];
break;
case Custom_Message_Sub_CP_Binding:
NSLog(@"🎯 分发到 playCPBinding");
[self playCPBinding:attachment];
break;
case Custom_Message_Sub_Gift_ChannelNotify:
NSLog(@"🎯 分发到 playRoomGiftBanner");
[self playRoomGiftBanner:attachment];
break;
case Custom_Message_Sub_LuckyPackage:
NSLog(@"🎯 分发到 playLuckyPackageBanner");
[self playLuckyPackageBanner:attachment];
break;
case Custom_Message_Sub_Super_Gift_Banner:
NSLog(@"🎯 分发到 playBroveBanner");
[self playBroveBanner:attachment];
break;
default:
NSLog(@"⚠️ 未知的 Banner 类型: %ld", (long)attachment.second);
//
[self.bannerScheduler markBannerFinished];
break;
}
}
@end