简介面向iOS开发者的视频与图片混合轮播实现资源基于Objective-C编程语言和Xcode工具链适配社交媒体、电商、媒体播放等常见App场景。资源重点梳理两条技术路线第一条基于UICollectionView自定义图片与视频两种Cell通过AVPlayerLayer接入AVFoundation框架播放视频帧并细致处理Cell重用、内存管理以及UIPanGestureRecognizer手势带来的无缝滑动第二条基于SDCycleScrollView扩展自定义视图让轮播数据源同时携带图片地址与视频地址并完善视频暂停、继续播放和切换过渡等逻辑。两种方案均给出了实现思路、关键代码组织方式和组件选型比较便于开发者结合项目现状取舍。资源包大小5.64MB文件总数与明细暂未单独提供已有500人学习下载适合具备Objective-C基础并希望深入理解轮播机制的开发者。内容还涵盖性能优化建议如图片异步缓存、相邻视频预加载等可帮助读者直接借鉴到实际开发中。1. 轮播图从图片进化到视频这个压缩包解决的核心矛盾电商首页、内容App、视频平台的 banner 位早就不满足于只放静态图了。用户刷首页时头条位置放着三段产品视频自动播放点击率比纯图片高一大截。但“iOS视频和图片混合轮播.zip”这个资源解决的不是“能放视频”这么简单而是在同一个轮播容器里让图片和视频按同一套时间轴滚动、暂停、点击跳转视频不卡顿、图片不闪烁、内存不被撑爆。它适配的是 Objective-C 老工程依赖 Xcode 开发环境用 UIKit 原生控件搭建。适合两类人一类是还在维护 OC 老项目的开发者想给首页 banner 加视频能力又不想重写架构另一类是刚接触 iOS 轮播组件想找个能直接跑的完整案例而不是网上那种只演示单图片滚动的半成品。这类组件市面上并不缺但绝大多数轮播库只处理 UIImageView视频轮播要么单独写一套要么用第三方播放器强行嵌进去结果就是图片轮播和视频轮播各转各的切到视频页时定时器失效、滑动卡顿、视频黑屏。这套资源的思路是把“图片”和“视频”统一成“媒体条目”底层用 UICollectionView 驱动每个 cell 内部同时具备图片渲染层和视频渲染层根据数据模型自动切换。2. 轮播架构拆解UICollectionView 驱动、双渲染层和媒体模型设计2.1 为什么用 UICollectionView 而不是 UIScrollView 手动拼老一代轮播组件大多基于 UIScrollView把每个页面当作一个 subview手动设置 contentOffset 实现滚动。这样做有个硬伤视频页面多了之后所有页面都常驻内存每页一个 AVPlayer 实例三五个页面就已经让内存飙升而且手势冲突、复用、预加载全靠手写维护成本高。这套资源选择 UICollectionView本质上是把“页面”换成“cell”让 UICollectionView 的复用机制接管内存。需要注意的是这不是一个“自定义控件”而是一个“工程级示例”里面已经包含完整的 Xcode 工程结构和 Podfile。导入后用pod install拉取依赖SDWebImage 负责图片异步加载和缓存SDCycleScrollView 或者类似的轮播调度逻辑负责定时器驱动。核心控制器文件里UICollectionView 的注册 cell 代码通常是这样的[self.collectionView registerClass:[MediaCarouselCell class] forCellWithReuseIdentifier:MediaCarouselCell]; - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { MediaCarouselCell *cell [collectionView dequeueReusableCellWithReuseIdentifier:MediaCarouselCell forIndexPath:indexPath]; MediaItem *item [self mediaItemAtIndexPath:indexPath]; [cell configureWithMediaItem:item]; return cell; }dequeueReusableCellWithReuseIdentifier返回的 cell 可能刚被回收也可能刚从复用池拿出来它的内部状态播放中的视频、已经加载的图片必须彻底重置。在这个库里configureWithMediaItem:内部会先判断媒体类型图片走setImage:视频走setPlayer:然后强制清掉上一次遗留的 AVPlayerLayer 和定时器回调。2.2 媒体模型用一个对象统一图片和视频整套资源的核心设计是这个MediaItem模型。它不是急着区分图片和视频而是把它们抽象成“一个有展示类型、有资源地址、可选封面”的数据单元。模型文件不长但字段的取舍很关键typedef NS_ENUM(NSInteger, MediaItemType) { MediaItemTypeImage 0, MediaItemTypeVideo 1 }; interface MediaItem : NSObject property (nonatomic, assign) MediaItemType type; property (nonatomic, copy) NSString *imageURL; // 图片地址或视频封面图地址 property (nonatomic, copy) NSString *videoURL; // 视频地址type Video 时才有值 property (nonatomic, copy) NSString *linkURL; // 点击跳转地址 property (nonatomic, strong) UIImage *placeholderImage; // 本地占位图 (instancetype)imageItemWithURL:(NSString *)imageURL linkURL:(NSString *)linkURL; (instancetype)videoItemWithVideoURL:(NSString *)videoURL coverImageURL:(NSString *)coverImageURL linkURL:(NSString *)linkURL; endimageURL字段复用为“视频封面图地址”是这套设计里很聪明的一步。因为视频在等待播放或预加载时cell 上必须有一张图顶着否则就是黑底的 cell 在轮播里滑过去视觉上像破洞。这套实现的渲染时序是先显示封面图再准备播放器播放器 ready 后再把 playerLayer 盖上去这样用户不会看到加载白屏。2.3 播放器渲染层AVPlayer 和 AVPlayerLayer 的绑定与剥离图片轮播的 cell 里是一个 UIImageView视频轮播的 cell 要换成 AVPlayerLayer。但因为用的是同一个 cell 复用图层必须动态管理。常见做法是这样cell 上放一个UIImageView *imageView作为底再加一个UIView *videoContainerView只有视频条目才把 AVPlayerLayer 挂进去。- (void)layoutSubviews { [super layoutSubviews]; self.imageView.frame self.bounds; self.videoContainerView.frame self.bounds; } - (void)configureWithMediaItem:(MediaItem *)item { _item item; // 清掉上一个条目的视频层 if (self.playerLayer.superlayer) { [self.playerLayer removeFromSuperlayer]; self.playerLayer nil; } if (item.type MediaItemTypeImage) { // 图片模式隐藏视频容器显示图片 self.videoContainerView.hidden YES; [self.imageView sd_setImageWithURL:[NSURL URLWithString:item.imageURL] placeholderImage:item.placeholderImage]; } else { // 视频模式先显示封面再准备播放器 self.videoContainerView.hidden NO; [self.imageView sd_setImageWithURL:[NSURL URLWithString:item.imageURL] placeholderImage:item.placeholderImage]; AVPlayerItem *playerItem [AVPlayerItem playerItemWithURL:[NSURL URLWithString:item.videoURL]]; self.player [AVPlayer playerWithPlayerItem:playerItem]; self.playerLayer [AVPlayerLayer playerLayerWithPlayer:self.player]; self.playerLayer.videoGravity AVLayerVideoGravityResizeAspectFill; self.playerLayer.frame self.videoContainerView.bounds; [self.videoContainerView.layer addSublayer:self.playerLayer]; [self.player play]; } }这里有两个细节值得留意。videoGravity设成ResizeAspectFill和图片模式下imageView.contentMode UIViewContentModeScaleAspectFill保持视觉一致否则图片是填满铺开的、视频是黑边居中的两种条目轮播时像两套皮肤。removeFromSuperlayer这步不能省因为复用池里的 cell 如果上次挂过 playerLayer下次复用不摘除图层会越叠越多内存逐步泄漏播放声音也可能串。3. 播放控制与滚动协同定时器、预加载和回调优先级的取舍3.1 定时器与滚动的冲突处理视频轮播和图片轮播最大的区别在于“时间感知”。图片轮播可以靠一个 NSTimer 每 4 秒切一页混合轮播里切到视频条目时timer 必须停下来让视频自己播。如果视频播放时长 15 秒而轮播间隔只有 4 秒意味着视频本来只该播 4 秒就要滚走。该怎么做这个库给出的方案是“分级优先级”滚动优先、视频播放次之、定时器最后。也就是说首页自动轮播时遇到视频条目就“停表”切换为“跟随播放完成再走”用户手动滑动时不管是图片还是视频timer 一律失效等滑动结束后重新计时。这需要把定时器拆成“自动轮播”和“手动轮播”两套状态机- (void)startAutoPlay { if (self.isDragging || self.isDecelerating) return; [self autoPlayTimer invalidate]; self.autoPlayTimer [NSTimer scheduledTimerWithTimeInterval:self.autoPlayInterval target:self selector:selector(autoScrollToNextPage) userInfo:nil repeats:YES]; }autoPlayInterval建议设成视频平均时长的三分之一到二分之一而不是图片轮播的固定 3 秒。原因很直接如果视频普遍 15 秒固定 3 秒切走用户根本看不清视频内容如果固定 15 秒图片页会显得很拖沓。折中方案是 5 到 8 秒并且在即将切换到视频页的前 1 秒开始预加载。3.2 视频预加载提前准备好下一个播放器滑动轮播时常见问题是切到视频页的瞬间要现场创建 AVPlayer网络差时黑屏两三秒体验非常割裂。这个库的优化思路和 UICollectionView 的 prefetching 类似在 item 即将显示前提前把 AVPlayerItem 加载到内存甚至提前播放。实现方式是在滚动回调里计算 targetIndex- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(CGPoint *)targetContentOffset { CGFloat pageWidth CGRectGetWidth(scrollView.bounds); CGFloat targetIndex targetContentOffset-x / pageWidth; [self prepareMediaItemAtIndex:(NSInteger)targetIndex]; } - (void)prepareMediaItemAtIndex:(NSInteger)index { MediaItem *item [self mediaItemAtIndex:index]; if (item.type MediaItemTypeVideo) { // 提前创建 AVURLAsset触发预加载 AVURLAsset *asset [AVURLAsset assetWithURL:[NSURL URLWithString:item.videoURL]]; AVPlayerItem *playerItem [AVPlayerItem playerItemWithAsset:asset]; AVPlayer *preloadPlayer [AVPlayer playerWithPlayerItem:playerItem]; [self.preloadedPlayers setObject:preloadPlayer forKey:(index)]; } }preloadedPlayers是一个 NSMutableDictionary缓存即将展示的 AVPlayer 实例。真实环境里网络波动大AVURLAsset初始化后不一定立刻拉数据所以还需要监听 asset 的加载状态。常见的做法是用loadValuesAsynchronouslyForKeys:异步加载时长属性等真正需要展示时直接复用预加载好的 player。这里有个反直觉点预加载并不总是“越早越好”。如果提前 3 秒以上创建 AVPlayer用户可能根本没滑过去白跑了流量和内存如果提前太晚又来不及缓冲。我实测下来提前一页预加载、最多缓存两个 AVPlayer 是比较稳妥的配置内存占用大约多出 15MB 到 30MB视视频分辨率而定。3.3 播放完成与循环通知回调的正确写法视频播完不切图是混合轮播最常见的问题。原因通常是回调监听写错了地方。正确写法是监听AVPlayerItemDidPlayToEndTimeNotification而不是定时轮询播放进度。这个通知需要把 playerItem 作为 object 传进去并且要在 cell 销毁或复用时主动移除观察者否则就会“黑匣子式”地收不到回调。[[NSNotificationCenter defaultCenter] addObserver:self selector:selector(videoDidPlayToEnd:) name:AVPlayerItemDidPlayToEndTimeNotification object:self.player.currentItem]; - (void)videoDidPlayToEnd:(NSNotification *)notification { AVPlayerItem *finishedItem notification.object; if (finishedItem ! self.player.currentItem) return; if (self.item.type MediaItemTypeVideo) { // 视频播完直接滚到下一页 [self scrollToNextPage]; } }注意末尾的防护判断。复用池里的 cell 可能先后绑定过不同的 AVPlayerItem通知回调时如果只认 object 不认当前 item会出现这个 cell 在播视频 A收到的却是视频 B 播完的通知。处理方式就是比较finishedItem和self.player.currentItem是否同一个对象不同就丢弃。4. 数据源与缓存策略图片走 SDWebImage视频走本地临时文件4.1 图片缓存复用工程已有的 SDWebImage 链路这个工程既然用 CocoaPods 管理图片加载自然走 SDWebImage。但要注意缓存键的设计——视频封面图的 URL 如果和视频 URL 不同没问题如果封面图和视频是同一个 URLSDWebImage 会把视频地址也当作图片缓存查查不到再请求实际上永远命中不了还浪费一次网络请求。也就是说代码里的imageURL和videoURL务必使用不同地址。另外封面图的缓存策略要容忍失败。离线的弱网场景下轮播条目如果全是视频SDWebImage 加载不出封面cell 会露出 placeholderImage。建议把 placeholderImage 设计成深色底加一张本地默认图至少保证视觉上没有破洞。- (void)loadCoverImageForMediaItem:(MediaItem *)item intoImageView:(UIImageView *)imageView { [imageView sd_setImageWithURL:[NSURL URLWithString:item.imageURL] placeholderImage:item.placeholderImage options:SDWebImageRetryFailed | SDWebImageLowPriority completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) { if (image) { // 封面加载成功后可以交给视频渲染层做首帧替换 } }]; }SDWebImageRetryFailed这个选项值得打开默认失败会丢弃缓存记录下一次滚动到这个条目会重新请求SDWebImageLowPriority是为了不阻塞滚动事件。封面是“可以晚一点出现”的资源优先级应该低于视频首帧。4.2 视频缓存不要塞进 SDWebImage视频文件必须避开图片缓存体系。SDWebImage 的磁盘缓存是按图片解码优化的视频如果塞进去会占用大量磁盘空间而且缓存清理策略完全失控。视频缓存一般两条路要么交给 AVPlayer 自身的 HTTP 缓存机制要么下载到本地临时目录。在这个工程里视频资源基本是远程播放不做时长太长的完整下载。这是合理的——banner 位的视频本来就该控制在 10 到 30 秒网络加载很快直接播放即可。真正需要关注的是防止 AVPlayer 反复重新加载同一段资源比如用户滑走又滑回来同一个视频 URL 被重新请求。做法是维护一个内存级 URL 到 AVPlayerItem 的映射property (nonatomic, strong) NSMutableDictionaryNSString *, AVPlayerItem * *videoItemCache; - (AVPlayerItem *)cachedPlayerItemForURL:(NSString *)urlString { AVPlayerItem *item self.videoItemCache[urlString]; if (!item) { item [AVPlayerItem playerItemWithURL:[NSURL URLWithString:urlString]]; self.videoItemCache[urlString] item; } return item; }注意这个 cache 必须设容量上限比如 3 个条目超过就清掉最久没用的。AVPlayerItem 持有的是 AVURLAsset一个 30 秒的视频可能占几十 MB 内存。缓存不限制的话轮播十条视频可以吃掉近 300MB真机直接闪退。4.3 滚动性能保障图片预解码与视频层的离屏处理混合轮播最容易让主线程卡顿的场景是三张视频封面同时加载、同时开始解码。SDWebImage 默认在后台线程解码但在cellForItemAtIndexPath里同步sd_setImageWithURL:时如果命中磁盘缓存解码操作依然可能占用主线程时间片。规避手段是开启SDWebImageQueryDiskSync之外的异步选项并且对占位图做预处理// 提前把占位图渲染成指定尺寸避免每次绘制都走一遍 Tiling UIImage *placeholder [UIImage imageNamed:banner_placeholder]; UIGraphicsBeginImageContextWithOptions(targetSize, NO, 0); [placeholder drawInRect:CGRectMake(0, 0, targetSize.width, targetSize.height)]; UIImage *resizedPlaceholder UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext();这种做法其实是在预防“首帧卡顿”。预先把占位图缩放到 banner 实际渲染尺寸之后每个 cell 绘制时 CPU 不用重新缩放源图。对视频条目而言AVPlayerLayer 不是普通的 UIView它直接绘制在 layer 树上内容不走 UIKit 的 drawRect所以滚动时不会像大图那样掉帧。但这也意味着无法对播放内容做圆角遮罩如果 banner 设计有圆角需要在外层再套一个 mask否则视频画面是方角配图反而是圆角两者不一致。5. 常见坑与排查六个绕不开的适配与性能问题5.1 视频点击事件与手势冲突现象点击视频区域没反应或者把视频的播放/暂停手势和轮播的手势混在一起。原因AVPlayerLayer 本身不接收 UIEvent点击事件走的是 cell 上的 UITapGestureRecognizer。如果 cell 内部加了播放/暂停的手势识别器会和轮播的滚动手势抢占。解决视频条目统一在 cell 根部挂 UITapGestureRecognizer播放/暂停不要用手势改成在视频容器上加一个透明的 UIButton点击按钮切换播放状态。5.2 复用后视频声音残留现象滑动轮播后视频滚出屏幕但声音还在播放或者在静音模式下下一个视频没有声音。原因cell 复用后对应的 player 没有被暂停和重置。解决在prepareForReuse里强制暂停 player 并 seek 到零- (void)prepareForReuse { [super prepareForReuse]; [self.player pause]; [self.player seekToTime:CMTimeMake(0, 1)]; self.player nil; }5.3 MPMoviePlayerController 的遗留代码现象编译报错找不到MPMoviePlayerController。原因这套资源基于旧 Xcode 模板改动最初的代码可能用了 iOS 8 时代的 MPMoviePlayerController这个类在 iOS 9 以后被废弃在 iOS 12 SDK 里已经不存在。解决换成 AVPlayer代码量不大MPMoviePlayerController 的prepareToPlay对应 AVPlayer 的play视图层换成 AVPlayerLayer。5.4 视频首帧黑屏与封面闪烁现象视频 cell 出现时先黑一下然后才显示封面再切入视频。原因cell 布局时先 addSublayer后加载封面封面图 URL 加载是异步的视频层已经在图下层撑开了黑底。解决把封面加载完成回调里再挂 AVPlayerLayer并且在layoutSubviews里对 AVPlayerLayer 做尺寸同步不是每次 layout 都重挂。5.5 声音模式的切换不及时现象系统的静音键拨到静音视频轮播依然发出声音。原因AVPlayer 不走AVAudioSession的默认静音策略适配需要在播放前手动配置 session。解决AVAudioSession *audioSession [AVAudioSession sharedInstance]; [audioSession setCategory:AVAudioSessionCategoryAmbient error:nil]; [audioSession setActive:YES error:nil];AVAudioSessionCategoryAmbient会让视频声音跟随系统静音键这一点在标题推荐场景非常重要否则用户在公共场合刷推荐页突然出声很尴尬。5.6 无限轮播页码计算的边界现象IndexPath 越界、第一页向前滑直接崩。原因无限轮播通常用数据源拼接虚拟页数比如 100 倍页面数量。工程里的页码换算在视频播放完成的回调里如果没做模除就会越界。解决所有 IndexPath 到数据源下标的方法统一收口不要散落在多个文件里各自换算。推荐写一个realIndexByIndexPath:的方法- (NSInteger)realItemIndexForIndexPath:(NSIndexPath *)indexPath { if (self.itemCount 0) return 0; return indexPath.item % self.itemCount; }6. 进阶技巧用封面首帧做无缝衔接与弱网降级视频轮播的玄学细节主要集中在上图和下一条切换的瞬间。工程源码里其实没有完整展示“用视频首帧预渲染”的手法但这是我在类似项目里反复实验后确定最有效的一招值得在已有资源基础上加装。原理很简单AVAssetImageGenerator 可以从视频文件里截取指定时间的帧图。与其等 AVPlayer 加载完成后首帧自然显示不如在数据准备阶段就用这个生成器异步抓一帧图然后作为 UIImage 显示在 imageView 上。这招有两个收益第一加载视频期间用户看到的是同步的“视频截图”而不是封面设计图视觉一致性更高——很多视频封面是特效字跟视频内帧差距很大第二弱网环境下就算视频根本没缓冲成功页面上也有一张真实画面撑着不至于黑屏。- (void)generateVideoFrameWithURL:(NSString *)videoURL atTime:(CMTime)time completion:(void(^)(UIImage *frameImage))completion { AVURLAsset *asset [AVURLAsset assetWithURL:[NSURL URLWithString:videoURL]]; AVAssetImageGenerator *generator [AVAssetImageGenerator assetImageGeneratorWithAsset:asset]; generator.appliesPreferredTrackTransform YES; generator.maximumSize CGSizeMake(320, 320); [generator generateCGImagesAsynchronouslyForTimes:[[NSValue valueWithCMTime:time]] completionHandler:^(CMTime requestedTime, CGImageRef image, CMTime actualTime, AVAssetImageGenerationResult result, NSError *error) { if (result AVAssetImageGenerationSucceeded image) { UIImage *frame [UIImage imageWithCGImage:image]; dispatch_async(dispatch_get_main_queue(), ^{ if (completion) completion(frame); }); } }]; }generateCGImagesAsynchronouslyForTimes:是异步的所以这个调用必须放在视频条目即将显示前而不是 cell 显示后。触发时机和“预加载播放器”保持一致在prepareMediaItemAtIndex:里一起调用。这样用户滚动到视频条目时封面已经替换成了一帧真实画面播放器加载好后无缝盖上视觉上看不出“图变视频”的断层。另一个常用技巧是“视频循环播放不滚走”。某些设计下 banner 视频是作为背景存在的根本不轮播播完自动从头再播。处理方式是把videoDidPlayToEnd回调里的scrollToNextPage换成seekToTime: kCMTimeZero再play。但要注意这个循环逻辑不要和轮播混用否则会出现“视频播完重新播永远不切页”的循环卡死。从工程实践角度说这套资源的最大价值不是代码本身有多精妙而是把图片轮播和视频轮播统一到了同一个架构模型里。对照它落地时最容易出问题的不是播放器而是持久化和复用的边界管理。我自己的习惯是从那次以后每次写轮播组件都在 cell 复用和缓存清理上加日志把所有prepareForReuse和 AVPlayerItem 销毁路径打印出来排查内存和残留问题时能省一半时间。希望这套拆解能帮你少踩几个坑拿到项目后直接把它用起来。本文还有配套的精品资源点击获取