简介:面向iOS开发者的仿抖音短视频App完整工程源码,采用Object-C编写,适配iPhone与iPad,兼容iOS 8.0至iOS 12.0,适合短视频项目练手与二次开发。压缩包共1137个文件、约7.07MB,以png界面素材、h与m源码、c底层实现及json数据为主,辅以Xcode工程配置与少量md说明。内容分三部分:个人主页模块涉及NSOperationQueue多队列解析webp动图与网络资源二级缓存框架;视频模块围绕AVPlayerLayer实现边播边下与UITableView多视频源播放控制;聊天模块基于WebSocket完成IM即时通信,并穿插文本计算、表情编辑、手势与动画处理。已有553人学习,适合希望理解短视频App架构与音视频、IM实现的开发者参考。
1. 从一份 Objective-C 短视频 Demo 说起:它到底能替你把哪段路跑通
手里接到的需求是给一个 iOS 软件补短视频信息流,最省时间的路径不是从零写播放器,而是先找一份能跑的 iOS Objective-C 版 Demo,把手机端和平板端的壳先立起来。这份资源就是围绕这个场景整理的:一套 Objective-C 短视频信息流工程,包含列表、播放器、基础交互和双端布局,适配手机与平板尺寸。它适合三类人:刚接手老 Objective-C 项目、需要快速验证信息流方案的人;要拆播放器和列表复用逻辑的人;以及准备给现有 iOS 软件加短视频插件式模块的人。它不替你解决业务接口和审核规则,但能把“首屏能滚动、视频能播、双端不散架”这段路先跑通。
2. 拆开工程看骨架:Objective-C 目录、依赖与双端入口
2.1 先看目录:哪些是壳,哪些是短视频业务
拿到这类源码包,我一般不会先改 UI,而是先把目录按职责过一遍。短视频信息流 Demo 最容易出现的问题是“壳和业务混在一起”:列表控制器里直接写播放器,播放器里又去拿网络请求,最后改一个 iPad 分栏就牵出一串耦合。先认清模块边界,后面适配手机端和平板端时才知道该动哪一层。
常见目录大致可以按下面这张表理解,实际工程名可能不同,但职责划分基本跑不出这个范围。
| 目录 | 主要职责 | 优先改哪里 |
|---|---|---|
| App | 启动入口、根控制器选择、全局配置 | 双端入口、日志开关 |
| Feed | 信息流列表、Cell、布局计算 | 复用逻辑、行高缓存 |
| Player | 播放器封装、播放器池、播放状态 | 预加载、静音、释放 |
| Network | 请求封装、Mock 数据 | 接口替换、超时设置 |
| Models | 视频、作者、封面等数据模型 | 字段映射 |
| Resources | 占位图、示例视频、图标 | 替换版权素材 |
| Utils | 屏幕适配、日志、缓存工具 | 双端尺寸判断 |
| Pad | 平板容器、分栏或浮层布局 | 分屏与横竖屏 |
这张表的作用不是让你背目录,而是让你在改首屏之前先回答三个问题:列表数据从哪里来,播放器由谁创建和回收,平板端是复用手机端控制器还是另起容器。我的习惯是先找App里的根控制器入口,再顺着Feed找数据源,最后看Player的创建位置。只要这三条线清楚,后面 pod 装不上、视频黑屏、iPad 分屏错位时,排查范围会小很多。
提示:不要一上来全局替换类名前缀。Objective-C 老工程里经常有 Category、通知名和字符串绑定,批量改名很容易把运行时调用改断。
2.2 Podfile 与依赖:先把最小依赖跑通
这类 Demo 的依赖一般不会太夸张,常见是网络、图片缓存、下拉刷新和播放器封装。我的做法是先只保留工程真正引用到的库,不要看到 Podfile 里有就全部保留。短视频信息流最怕依赖链里混进一个很久没更新的库,真机编译时突然报架构或头文件问题,排查成本比写列表还高。
下面是一份最小化 Podfile 写法,工程名、目标名和部署版本要按你手里的源码包替换,不要照抄。
platform :ios, '<PROJECT_DEPLOYMENT_TARGET>' # 与工程现有 Deployment Target 保持一致 source 'https://cdn.cocoapods.org/' # 公共源;若工程配置了内网源,换成自己的源 use_frameworks! # 有动态库或混编依赖时开启;纯静态 ObjC 库可先关闭 inhibit_all_warnings! # 先压住第三方警告,避免首轮编译被噪音淹没 target 'ShortVideoDemo' do pod 'AFNetworking' # 网络层常见选择;工程已有网络层就删掉 pod 'SDWebImage' # 封面图缓存;注意和工程内图片库不要重复 pod 'MJRefresh' # 下拉刷新;只做首屏验证时可暂时移除 end这段配置的逻辑是先把“能编译、能拉图、能刷新”这条最小链路跑通,再谈播放器预加载和双端布局。参数上重点看三处:platform必须和 Xcode 工程里的 Deployment Target 一致,否则 Pods 目标可能编译不过;use_frameworks!不是必选项,纯 Objective-C 静态库工程乱开它会改变链接方式;source如果被替换成内网源,要确认源里确实有对应 pod 版本,不要一边报找不到 spec 一边怀疑代码。
如果你准备把播放器、日志、缓存做成插件式模块,Podfile 里反而要克制。插件式模块最怕“每个插件都带一份公共依赖”,最后 AFNetworking 或图片库出现重复符号。常见做法是公共依赖放主工程,插件只暴露协议和入口,播放器插件单独管自己的 AVFoundation 封装。这样后面替换播放器实现时,信息流层不用跟着重编译。
2.3 双端入口:AppDelegate、Scene 与 Size Class 的分工
手机端和平板端适配,第一步不是写约束,而是看入口怎么分流。老 Objective-C 工程常见两种写法:一种在AppDelegate里根据userInterfaceIdiom选根控制器;另一种已经切到 Scene 生命周期,在SceneDelegate里建窗口。源码包里用的是哪种,直接决定你把平板容器挂在哪里。
下面这段是常见的入口分流写法,核心是“先粗分手机与平板,再用 Size Class 做细调”。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; BOOL isPad = (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad); UIViewController *root = nil; if (isPad) { // 平板端先用容器包一层,后续可替换成 UISplitViewController root = [[PadFeedContainerViewController alloc] init]; } else { root = [[PhoneFeedViewController alloc] init]; } self.window.rootViewController = root; [self.window makeKeyAndVisible]; // 记录入口选择,排查双端问题时非常省事 NSLog(@"[App] idiom=%@ root=%@", isPad ? @"pad" : @"phone", NSStringFromClass([root class])); return YES; }这段代码只做入口选择,不承担布局职责。UI_USER_INTERFACE_IDIOM()只能区分当前设备大类,不能代表当前窗口宽度;平板分屏、横竖屏切换、Slide Over 场景下,真正应该看的是traitCollection.horizontalSizeClass和verticalSizeClass。所以我的习惯是:入口用 idiom 做第一层兜底,布局层再用 Size Class 做第二层判断。这样即使平板进入窄分屏,信息流也不会硬套宽屏两栏布局。
参数上要注意window的创建方式。如果工程已经使用 Scene,不要在AppDelegate里重复创建窗口,否则会出现黑屏或旧窗口残留。判断方法很简单:看工程里有没有SceneDelegate和UIApplicationSceneManifest配置。有 Scene 就把根控制器逻辑放到scene:willConnectToSession:options:里,没有 Scene 再留在AppDelegate。这一步不搞清楚,后面真机首屏空白时很容易把锅甩给播放器。
3. 把 Demo 跑起来:CocoaPods 安装、签名配置与首屏渲染验证
3.1 环境检查与 pod install 的实际命令
源码包能不能跑,第一道门槛通常不是代码,而是本机环境和 Pods 状态。我一般先看 Xcode 命令行工具是否指向当前安装版本,再看 CocoaPods 是否能正常解析,最后才执行安装。顺序反了,报错信息会混在一起,像黑匣子一样难拆。
xcode-select -p xcodebuild -version pod --version # 进入工程根目录后执行,不要在 Pods 目录里执行 pod install --verbosexcode-select -p用来确认命令行工具路径,路径不对时xcodebuild会报找不到 SDK;xcodebuild -version和pod --version只是记录环境,不要求某个固定版本。pod install --verbose的价值是能看到 spec 解析和下载过程,卡在哪个源、哪个依赖一目了然。首次安装可以加--repo-update,但内网源或网络条件一般时它会把时间拉得很长,我的习惯是先用普通pod install,确认是 spec 缺失后再单独更新源。
安装完成后,优先打开.xcworkspace,不要双击.xcodeproj。这是 Objective-C 老工程最常见的翻车点之一:Pods 已经装好,但 Xcode 打开的是原工程,编译时找不到头文件,开发者就开始怀疑 Podfile 写错了。实际上工程引用关系在 workspace 里,打开错文件,后面所有排查都是白费。
注意:如果工程里同时存在
Podfile.lock和Pods/Manifest.lock,先对比两者是否一致。不一致时pod install会重装,不要手工改 Manifest。
3.2 签名、Bundle ID 与真机安装
模拟器能跑不代表真机能装。短视频 Demo 经常带示例视频和图片资源,包体不小,真机安装时签名配置、Bundle ID、描述文件任何一处不对都会失败。我一般先用命令行把当前 scheme 的构建配置打印出来,确认 Bundle ID 和签名团队,再去 Xcode 里改,避免在 GUI 里来回点。
# 查看当前 scheme 的构建配置,重点看 PRODUCT_BUNDLE_IDENTIFIER 和 DEVELOPMENT_TEAM xcodebuild -workspace ShortVideoDemo.xcworkspace \ -scheme ShortVideoDemo \ -configuration Debug \ -showBuildSettings | grep -E 'PRODUCT_BUNDLE_IDENTIFIER|DEVELOPMENT_TEAM|CODE_SIGN_STYLE' # 模拟器构建,先确认代码本身能过编译 xcodebuild -workspace ShortVideoDemo.xcworkspace \ -scheme ShortVideoDemo \ -configuration Debug \ -sdk iphonesimulator \ -destination 'platform=iOS Simulator,name=<SIMULATOR_NAME>' \ build第一段命令只读配置,不改工程。PRODUCT_BUNDLE_IDENTIFIER如果还是模板里的默认值,真机安装前必须换成你自己的唯一标识;DEVELOPMENT_TEAM为空时,Xcode 自动签名可能选不中团队;CODE_SIGN_STYLE是 Automatic 还是 Manual,决定你要不要手动指定描述文件。第二段命令里的<SIMULATOR_NAME>要用xcrun simctl list devices available查到的实际名称替换,不要凭记忆写。
真机安装还有两个细节:一是工程里如果带了 Notification Extension、Widget 或分享扩展,每个 target 的 Bundle ID 都要有正确前缀,否则主应用能装、扩展装不上;二是示例视频如果走本地 Bundle,确认资源被加入 Copy Bundle Resources,而不是只拖进目录。判断方法是在真机日志里搜资源名,找不到就是没进包,不用先怀疑播放器。
3.3 首屏渲染验证:断点、日志和可见 Cell
首屏渲染验证不要只看“有没有画面”。短视频信息流至少要看四件事:数据源有没有值、列表有没有可见 Cell、封面图有没有回来、播放器有没有创建。最省事的做法是在viewDidAppear打一条聚合日志,把关键数量一次打出来。
- (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; // 首屏验证:数据、可见 Cell、当前播放器数量 NSLog(@"[Feed] appear models=%lu visibleCells=%lu players=%lu", (unsigned long)self.models.count, (unsigned long)self.tableView.visibleCells.count, (unsigned long)self.playerPool.activeCount); // 如果首屏为空,先看数据源而不是先改 UI if (self.models.count == 0) { NSLog(@"[Feed] empty data, check network mock or local json"); } }这条日志的逻辑是把“数据层”和“视图层”分开看。models为 0,问题在请求、Mock 或本地 JSON 解析;models有值但visibleCells为 0,问题在列表约束、数据源回调或 Cell 高度;两者都有但players为 0,才去看播放器创建条件。参数上不要长期保留太多NSLog,首屏验证完就收进调试开关,否则滚动时日志本身会影响性能。
验证顺序我一般按“冷启动首屏 → 上下滚动三屏 → 切后台再回来 → 平板横竖屏各一次”走。冷启动看首屏数据,滚动看复用,切后台看播放器暂停和恢复,平板看布局是否重算。每一步都留一条日志或截图,后面改布局时才有对照。
4. 信息流与播放器:Objective-C 下列表复用、预加载和内存水位
4.1 列表复用与行高缓存:别让高度计算拖垮滚动
短视频信息流的滚动性能,一半看图片解码,一半看行高计算。Objective-C 工程里常见写法是在heightForRowAtIndexPath里实时算文本高度,如果没有缓存,快速滚动时同一行会被反复计算,主线程很容易掉帧。我的做法是给行高加一层缓存,但缓存键不能只用 row,数据源插入删除后必须失效。
@interface FeedViewController () <UITableViewDataSource, UITableViewDelegate> @property (nonatomic, strong) UITableView *tableView; @property (nonatomic, strong) NSMutableDictionary<NSString *, NSNumber *> *heightCache; @end - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { FeedModel *model = self.models[indexPath.row]; NSString *key = [NSString stringWithFormat:@"%@-%.0f", model.itemId, CGRectGetWidth(tableView.bounds)]; NSNumber *cached = self.heightCache[key]; if (cached) { return cached.doubleValue; } CGFloat height = [self layoutHeightForModel:model width:CGRectGetWidth(tableView.bounds)]; self.heightCache[key] = @(height); return height; } - (void)reloadFeed { [self.heightCache removeAllObjects]; // 数据源变化后必须清缓存 [self.tableView reloadData]; }这段代码的关键不是字典,而是缓存键。用itemId + 当前列表宽度作为键,可以同时解决两个问题:数据源刷新后旧行高不会串到新数据上;平板横竖屏或分屏宽度变化后,旧宽度下的行高不会继续复用。参数上CGRectGetWidth(tableView.bounds)要在布局稳定后取,如果放在viewDidLoad里取,可能拿到 0 或旧宽度。我的习惯是在viewDidLayoutSubviews里比较宽度是否变化,变化了再清缓存并reloadData。
Cell 复用还有一个容易忽略的点:异步图片回调。Cell 被复用时,上一张封面图的回调可能晚到,直接覆盖新 Cell。常见做法是在 Cell 里持有当前图片 URL,回调时比对 URL 是否一致,不一致就丢弃。这个判断不复杂,但少了它,快速滚动时会出现“封面串图”,看起来像数据错乱,实际是复用竞态。
4.2 播放器池:预加载、复用和静音策略
短视频信息流不会给每个 Cell 都常驻一个播放器。常见做法是维护一个小型播放器池,当前可见行用一个,前后各预加载一个,滚动时回收离屏播放器。池子太小会频繁创建销毁,池子太大内存上涨,尤其平板分屏时同时可见行数变多,水位要单独看。
@interface PlayerPool : NSObject @property (nonatomic, strong) NSMutableArray<AVPlayer *> *idlePlayers; @property (nonatomic, assign) NSInteger createdCount; @property (nonatomic, assign) NSInteger maxCount; @end @implementation PlayerPool - (instancetype)init { self = [super init]; if (self) { _idlePlayers = [NSMutableArray array]; _maxCount = 3; // 手机端常见 2~3 个;平板端按可见行数调大 } return self; } - (AVPlayer *)dequeuePlayer { AVPlayer *player = self.idlePlayers.firstObject; if (player) { [self.idlePlayers removeObjectAtIndex:0]; return player; } if (self.createdCount < self.maxCount) { self.createdCount += 1; return [AVPlayer new]; } return nil; // 池满且无空闲,调用方应复用当前播放器或延迟创建 } - (void)enqueuePlayer:(AVPlayer *)player { [player pause]; [player replaceCurrentItemWithPlayerItem:nil]; if (self.idlePlayers.count < self.maxCount) { [self.idlePlayers addObject:player]; } } @end这段池子代码的重点是createdCount和idlePlayers.count分开记。只按空闲数组判断,会不断创建新播放器;只按最大数判断,又会在回收时把已经创建的播放器丢掉。maxCount不是固定答案,手机端先用 3 个跑首屏,平板端如果分栏后同时可见 4 到 6 行,就要按 Instruments 的内存曲线调,不要直接照抄。enqueuePlayer里先暂停再清playerItem,是为了让离屏播放器尽快释放解码资源。
静音策略也要在池子里统一。常见做法是信息流默认静音,用户点开某个视频后再解除静音,并且全局只允许一个播放器出声。这个状态不要散落在每个 Cell 里,否则快速滚动时会出现两个视频同时有声音。我的习惯是在池子外部加一个activePlayer引用,切换时先暂停旧播放器,再激活新播放器。
4.3 内存水位与滚动采样:用 Instruments 看,不靠感觉
播放器池和图片缓存有没有问题,不能靠“感觉不卡”。我一般用 Instruments 的 Allocations 和 Leaks 看两段:首屏加载后稳定占用,连续滚动 30 秒后的增长曲线。命令行可以先列出可用模板,再决定用哪个录制。
# 查看本机可用模板,不同 Xcode 版本模板名可能不同 xcrun xctrace list templates # 录制真机或模拟器上的内存与泄漏,<DEVICE_UDID> 用 xcrun simctl list 查 xcrun xctrace record \ --template 'Leaks' \ --device <DEVICE_UDID> \ --time-limit 30s \ --output feed-30s.trace--time-limit 30s是为了固定观察窗口,避免“滚一会儿看看”导致每次结果不可比;--output把 trace 落到文件,方便前后对比。模板名在不同 Xcode 版本里可能变化,所以先用list templates查,不要硬记。看结果时重点不是总内存绝对值,而是滚动结束后是否持续上涨、播放器对象是否一批批残留、AVPlayerItem是否在离屏后还被通知中心持有。
滚动采样还要结合日志。我会在scrollViewDidEndDecelerating里打一条当前活跃播放器数量和图片缓存数量,和 Instruments 曲线对得上,才能判断是播放器池泄漏还是图片缓存没上限。只盯一个指标,很容易把图片缓存当成播放器问题修。
5. 避坑与排查:短视频 Demo 在双端上最容易翻车的五处
5.1 启动即崩溃:打开的是工程文件,不是 workspace
现象很典型:pod install显示成功,Xcode 一编译就报找不到AFNetworking或SDWebImage头文件,偶尔还伴随重复符号。原因通常不是 Podfile 写错,而是打开错了工程文件。Pods 的引用关系在.xcworkspace里,双击.xcodeproj会绕过 workspace,原工程自然找不到 Pods 目标。
解决顺序是固定的:先关掉 Xcode,再进工程根目录执行pod install,最后打开.xcworkspace。如果已经打开错文件,不要在原窗口里“补配置”,直接关掉重开。
cd <PROJECT_ROOT> pod install open ShortVideoDemo.xcworkspace<PROJECT_ROOT>换成源码包根目录,ShortVideoDemo.xcworkspace换成实际 workspace 名。代码块里的open只适用于 macOS 图形界面;命令行构建仍然用xcodebuild -workspace。这一步没有技术含量,但血泪经验是:十次“Pods 装不上”里至少三次是打开错文件。
5.2 视频黑屏但有声音:先看播放器状态,不要先改 UI
现象是列表能滚,封面图正常,点进视频后有声音没画面,或者画面停在第一帧。原因常见有三类:播放器 layer 没有加到正确视图上;视频资源格式或编码当前设备不支持;AVPlayerItem状态失败但代码没有处理。排查时先看状态,不要直接改约束。
AVPlayerItem *item = player.currentItem; if (item.status == AVPlayerItemStatusFailed) { NSLog(@"[Player] failed error=%@", item.error); } else if (item.status == AVPlayerItemStatusReadyToPlay) { NSLog(@"[Player] ready size=%@", NSStringFromCGSize(item.presentationSize)); } // 播放器 layer 必须挂到可见视图上,且 frame 要跟随布局 AVPlayerLayer *layer = (AVPlayerLayer *)self.playerView.layer; layer.player = player; layer.videoGravity = AVLayerVideoGravityResizeAspectFill;AVPlayerItemStatusFailed的error会直接告诉你资源加载失败还是解码失败;presentationSize为 0 时,多半是资源还没准备好或地址不可达。AVPlayerLayer的videoGravity要按 UI 设计选,ResizeAspectFill会裁剪,ResizeAspect会留黑边。黑屏问题先确认状态,再确认 layer 是否挂上,最后才怀疑视图层级。
5.3 平板分屏后布局错乱:行高缓存没有跟着宽度失效
现象是手机端正常,平板横屏也正常,一分屏文本就被截断或出现大片空白。原因通常是我前面说的行高缓存只按 row 存,宽度变化后仍返回旧行高。短视频列表在平板分屏时宽度变化很频繁,缓存键不带宽度就会翻车。
解决方式是在尺寸变化时清缓存,并让布局重新计算。不要在每个 Cell 里单独判断屏幕宽度,统一在控制器层做。
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator { [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]; [coordinator animateAlongsideTransition:nil completion:^(id<UIViewControllerTransitionCoordinatorContext> context) { [self.heightCache removeAllObjects]; [self.tableView reloadData]; }]; }viewWillTransitionToSize覆盖旋转和分屏尺寸变化,completion里清缓存可以避免过渡动画中反复刷新。reloadData不是唯一选择,如果列表很长,可以只刷新可见区域,但首轮排查先用全量刷新确认问题来源,再优化。
5.4 滚动内存持续上涨:通知和播放器池没有释放
现象是滚动越久内存越高,退出页面也不降,Instruments 里能看到AVPlayerItem一批批残留。原因常见是播放器注册了通知没有移除,或者 Cell 持有播放器、播放器又通过 block 持有 Cell,形成循环引用。Objective-C 里通知中心对观察者是弱引用还是强引用,取决于 API 和系统版本,不能凭记忆赌。
- (void)startObservePlayerItem:(AVPlayerItem *)item { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playerItemDidPlayToEnd:) name:AVPlayerItemDidPlayToEndTimeNotification object:item]; } - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; NSLog(@"[Player] dealloc %@", NSStringFromClass([self class])); }排查时先看dealloc有没有打印。没有打印,说明对象还被持有;有打印但内存不降,去看图片缓存和视频解码缓存。通知移除要放在dealloc或明确的销毁方法里,不要只在下拉刷新时移除。播放器池回收时也要把playerItem置空,否则离屏播放器仍可能持有解码资源。
5.5 真机能装但首屏空白:网络策略与占位图分开查
现象是模拟器有数据,真机首屏空白,或者只有占位图没有封面。原因常见是资源地址协议不符合当前网络策略、真机网络环境与模拟器不同、占位图正常但请求失败没有回调。先打 URL 的 scheme 和 host,再决定是改资源地址还是补错误处理。
NSURL *url = [NSURL URLWithString:model.videoURLString]; NSLog(@"[Feed] video scheme=%@ host=%@", url.scheme, url.host); if (![url.scheme isEqualToString:@"https"]) { NSLog(@"[Feed] non-https resource, replace with https or add business exception"); }这段日志只做判断,不自动放行。短视频资源优先走 HTTPS,真机环境对明文资源更敏感;如果业务确实有历史资源,按域名配置例外,不要全局放开。首屏空白还要区分“没有数据”和“有数据但封面没回来”:前者看请求和 JSON 解析,后者看图片缓存和占位图状态。把这两类混在一起查,容易在播放器代码里绕很久。
6. 进阶验收:给信息流和播放器加一套能落地的观测点
6.1 用 os_signpost 标记滚动和首帧
真正要判断这份 Demo 能不能接进现有 iOS 软件,不能只看“能播”。我一般加两个观测点:列表开始滚动、播放器首帧渲染。用os_signpost比纯日志更适合看时间线,也不会在 Release 里产生太多噪音。
#import <os/signpost.h> static os_log_t gFeedLog; static os_signpost_id_t gScrollId; - (void)startScrollTrace { gFeedLog = os_log_create("com.demo.feed", "performance"); gScrollId = os_signpost_id_generate(gFeedLog); os_signpost_interval_begin(gFeedLog, gScrollId, "feed_scroll"); } - (void)stopScrollTrace { os_signpost_interval_end(gFeedLog, gScrollId, "feed_scroll"); }os_log_create的 subsystem 换成你自己的标识,category 用performance方便过滤;os_signpost_interval_begin/end要成对出现,滚动结束、页面消失时都要收口。首帧也可以用同样方式包住播放器readyToPlay到 layer 真正显示的时间。这样平板上分屏、横竖屏切换、播放器池换绑时,时间线能直接看出哪一段变长。
6.2 一份双端验收表
| 场景 | 观察点 | 通过标准 |
|---|---|---|
| 手机冷启动 | 首屏数据、可见 Cell、播放器数量 | 首屏有数据,滚动不白屏 |
| 连续滚动 30 秒 | 内存曲线、播放器残留 | 停止滚动后内存不持续上涨 |
| 切后台再回来 | 播放器暂停/恢复 | 不同时出声,画面不黑 |
| 平板横屏 | 列表宽度、行高 | 文本不截断,封面不变形 |
| 平板分屏 | Size Class、布局重算 | 窄分屏不硬套两栏 |
| 弱网 | 占位图、失败重试 | 首屏不空转,错误可恢复 |
这张表我一般贴在工程 README 旁边,每改一次布局或播放器池就跑一遍。表格里的“通过标准”不是绝对性能数字,而是可观察的行为边界。比如“内存不持续上涨”比“内存小于多少 MB”更适合不同设备。
从那以后我每次拿到这类 Objective-C 短视频 Demo,都会先跑一遍手机端首屏,再跑一遍平板分屏,最后用 Instruments 看 30 秒滚动曲线;只要这三步有一处不对,就先不进业务联调。希望帮到你。
本文还有配套的精品资源,点击获取