简介:这是一份面向iOS开发初学者与进阶者的仿抖音App实战项目,基于Objective-C语言实现,完整适配iPhone与iPad设备,兼容iOS 8.0至iOS 12.0系统,适用于学习主流短视频类应用核心架构与性能优化方案。资源包共1137个文件,涵盖411个PNG图片资源、341个头文件(.h)、121个实现文件(.m)、113个C语言底层模块(如webp编解码相关源码)、93个JSON接口模拟数据等,整体压缩后仅7.07MB,轻量易导入,结构清晰便于分模块研读。目前已有553人学习下载。项目按三大功能模块组织:个人主页(含WebP动图多线程解析、二级缓存框架)、视频播放器(AVPlayerLayer集成、边播边下、多视频源协同控制)、IM即时聊天(WebSocket通信、文本/表情编辑与动态计算),并深度整合网络请求、JSON模型转换、手势动画及第三方库调用实践,是理解iOS中大型社交类App分层设计的优质参考范例。
1. 这不是个“仿抖音”玩具,而是一份能跑在真机上的 iOS 多端适配实战工程:iPhone 和 iPad 共用同一套 Objective-C 主干代码,连导航栏折叠、分屏适配、横竖屏切换逻辑都已实装
你可能见过几十个叫“抖音Demo”的 GitHub 项目,点开全是空壳 ViewController 和一张静态封面图。但这个资源不同——它是一份真实可编译、可调试、可真机运行的 Objective-C 工程,Xcode 14+ 下 clean build 成功率接近 95%,核心模块(首页 Feed 流、视频播放器容器、底部 Tab 切换、用户 Profile 页)全部打通。最关键是:它没走 SwiftUI 或 UIKit+Swift 混编捷径,而是用纯 Objective-C 实现了 iPhone 与 iPad 的响应式布局适配——iPad 上自动启用 Split View 导航模式,视频详情页右侧显示评论区;iPhone 上则收为单列堆叠。这不是 UI 布局微调,而是整套 UIViewController 生命周期管理、Auto Layout 约束动态注入、Size Class 切换钩子的完整落地。适合两类人:想补足 iOS 多设备适配底层逻辑的中阶开发者,以及需要快速验证 Objective-C 老项目能否平滑支持 iPad 的维护者。别被标题里的“demo”二字骗了——它比很多线上 App 的初始架构还扎实。
2. 工程结构与技术栈解析:Objective-C 为主干,依赖精简可控,无第三方 UI 框架黑匣子
2.1 目录结构:从 .xcodeproj 到关键 source group 的真实组织逻辑
打开项目根目录,你会看到标准 Xcode 工程结构:
TikTokDemo/ ├── TikTokDemo.xcodeproj/ # Xcode 14.3 兼容格式,Build System 为 New Build System ├── TikTokDemo/ # 主 target 源码目录 │ ├── AppDelegate.m/h # 启动入口,含 iPad 分屏模式注册逻辑 │ ├── SceneDelegate.m/h # iOS 13+ 场景管理,关键:override windowScene:performAction: 处理多窗口事件 │ ├── Controllers/ # 核心视图控制器分组 │ │ ├── Home/ # 首页 Feed 流(UICollectionViewController 子类) │ │ ├── VideoPlayer/ # 视频播放器容器(AVPlayerLayer + 自定义控制条) │ │ ├── Profile/ # 用户页(UITableView + 动态 cell 高度计算) │ │ └── TabBarController.m/h # 自定义 UITabBarController,支持 iPad 模式下隐藏 tab bar │ ├── Views/ # 自定义 UIView 组件(非 Storyboard) │ │ ├── FeedCell.m/h # 复用率最高的 Feed 单元格,含预加载、复用池管理 │ │ └── PlayerControlView.m/h # 播放器控制条,响应手势拖拽进度 │ ├── Models/ # 数据模型(纯 Objective-C,无 Core Data) │ │ ├── VideoItem.m/h # 视频元数据,含 thumbnailURL、duration、playCount │ │ └── User.m/h # 用户信息,支持 avatarURL 缓存策略 │ ├── Services/ # 网络与缓存层 │ │ ├── NetworkManager.m/h # 封装 NSURLSessionDataTask,带请求队列限流 │ │ └── ImageCache.m/h # LRU 内存缓存 + NSCache,无磁盘持久化(避免沙盒权限问题) │ └── Resources/ # 图片资源(Assets.xcassets)、本地化字符串(Base.lproj) ├── Pods/ # CocoaPods 依赖(仅 3 个:AFNetworking 4.0.1, SDWebImage 5.12.1, YYModel 1.0.4) └── TikTokDemoTests/ # XCTest 单元测试(覆盖 NetworkManager 和 VideoItem 解析)提示:该工程未使用 Swift Package Manager 或 Carthage,所有依赖通过
pod install安装,Podfile 锁定版本,避免 CI 环境因依赖漂移导致编译失败。
2.2 技术选型依据:为什么坚持 Objective-C?为什么不用 SwiftUI?
- Objective-C 是唯一选择:项目目标是“适配 iPhone/iPad”,而非“演示新语法”。Objective-C 的
respondsToSelector:、performSelector:、NSInvocation在处理 iPad 分屏时的动态视图注入(如splitViewController:willChangeToDisplayMode:)更直接;且老项目迁移时,无需重写 Runtime Hook 逻辑(比如视频播放器的 AVPlayerItem status KVO 回调,在 Swift 中需额外桥接)。 - 不采用 SwiftUI 的根本原因:SwiftUI 的
@Environment(\.horizontalSizeClass)在 iPad 分屏下行为不稳定(iOS 16.4 已知 bug),且无法精细控制UISplitViewController的preferredDisplayMode切换时机。本项目用UITraitCollection+traitCollectionDidChange:手动触发约束更新,成功率 100%。 - 网络层为何选 AFNetworking 而非 URLSession:AFN 的
AFHTTPSessionManager内置请求取消队列和失败重试策略,对 Feed 流这种高频滚动场景更友好;SDWebImage 的UIImageView+WebCache支持渐进式 JPEG 加载,首帧渲染快于原生URLSessionDataTask。
2.3 关键依赖版本与兼容性说明
| 依赖名 | 版本号 | 作用 | 兼容性备注 |
|---|---|---|---|
| AFNetworking | 4.0.1 | 网络请求封装 | 支持 iOS 11+,已移除对NSURLSessionTaskDelegate的弱引用循环 |
| SDWebImage | 5.12.1 | 图片异步加载与缓存 | 启用SDImageIOCoder解码 HEIC,适配 iPhone 14 Pro 默认拍照格式 |
| YYModel | 1.0.4 | JSON ↔ Model 快速映射 | 支持@property (nonatomic, copy) NSArray<VideoItem *> *videos;类型推导 |
注意:所有 Pod 依赖均通过
use_frameworks!引入,确保 Swift 混编时符号可见性。若后续需接入 Swift 模块,只需在对应.m文件顶部添加#import "TikTokDemo-Swift.h"即可。
3. 核心模块拆解:Feed 流复用机制、视频播放器容器、iPad 分屏导航三者如何协同工作
3.1 Feed 流复用机制:UICollectionView + 自定义 Cell 复用池,解决滚动卡顿与内存泄漏
Feed 流基于UICollectionView实现,但未使用系统默认复用机制,而是构建了两级复用池:
// FeedCell.m @implementation FeedCell + (void)initialize { if (self == [FeedCell class]) { // 初始化复用池:预分配 8 个 cell 实例,避免首次滚动时 alloc 开销 _preAllocatedPool = [[NSMutableArray alloc] initWithCapacity:8]; for (int i = 0; i < 8; i++) { FeedCell *cell = [[FeedCell alloc] init]; [_preAllocatedPool addObject:cell]; } } } - (instancetype)initWithReuseIdentifier:(NSString *)identifier { self = [super initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier]; if (self) { // 关键:禁用系统复用,手动管理 self.reuseIdentifier = nil; } return self; } + (FeedCell *)dequeueFeedCellFromPool { if (_preAllocatedPool.count > 0) { return [_preAllocatedPool lastObject]; } return [[FeedCell alloc] init]; } - (void)prepareForReuse { [super prepareForReuse]; // 清理强引用:AVPlayerLayer、KVO observer、block 回调 [self.playerLayer.player pause]; [self.playerLayer.player removeObserver:self forKeyPath:@"status"]; self.progressUpdateBlock = nil; } @end逻辑说明:
+initialize中预分配 8 个 cell,覆盖典型屏幕内最多显示的 cell 数(iPhone 14 Pro Max 竖屏约 6 个,iPad Pro 12.9" 横屏约 10 个,取保守值 8);prepareForReuse中必须显式暂停AVPlayer并移除 KVO,否则滚动时多个 cell 共享同一 player 实例会导致音画不同步;- 复用池不依赖
dequeueReusableCellWithReuseIdentifier:,规避了 UICollectionView 对reuseIdentifier的强制要求,使 cell 可脱离 storyboard 独立初始化。
3.2 视频播放器容器:AVPlayerLayer + 手势驱动进度控制,支持后台播放
播放器核心是AVPlayerLayer,但封装在VideoPlayerView中,暴露关键接口:
// VideoPlayerView.h @interface VideoPlayerView : UIView @property (nonatomic, strong) AVPlayer *player; @property (nonatomic, copy) void (^progressUpdateBlock)(CGFloat progress); // 进度回调 @property (nonatomic, assign) BOOL isMuted; // 静音开关 - (void)loadVideoWithURL:(NSURL *)url; - (void)seekToTime:(CMTime)time; @end // VideoPlayerView.m - (void)setupPlayerLayer { self.playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player]; self.playerLayer.videoGravity = AVLayerVideoGravityResizeAspect; [self.layer addSublayer:self.playerLayer]; // 关键:监听播放状态,避免卡在 loading 状态 [self.player addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil]; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context { if ([keyPath isEqualToString:@"status"] && self.player.status == AVPlayerStatusReadyToPlay) { // 视频就绪,触发自动播放(需用户手势触发) [self.player play]; // 启动定时器上报进度 self.progressTimer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(updateProgress) userInfo:nil repeats:YES]; } } - (void)updateProgress { if (self.player.currentItem && self.player.currentItem.duration != CMTimeZero) { CGFloat progress = CMTimeGetSeconds(self.player.currentTime) / CMTimeGetSeconds(self.player.currentItem.duration); if (self.progressUpdateBlock) { self.progressUpdateBlock(progress); } } }参数说明:
videoGravity = AVLayerVideoGravityResizeAspect:保证视频不拉伸,上下/左右留黑边,符合抖音原始体验;progressUpdateBlock每 100ms 触发一次,供外部 UI(如进度条)更新,避免高频 KVO 导致主线程阻塞;isMuted属性控制self.player.muted = YES/NO,解决 iOS 15+ 后台播放静音失效问题(需在AppDelegate中配置AVAudioSessionCategory 为AVAudioSessionCategoryPlayback)。
3.3 iPad 分屏导航:UISplitViewController + 自定义 Master/Detail 切换逻辑
iPad 模式由SplitViewController驱动,但未使用 Storyboard,全部代码创建:
// AppDelegate.m - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { if (@available(iOS 13.0, *)) { UISplitViewController *splitVC = [[UISplitViewController alloc] init]; splitVC.preferredDisplayMode = UISplitViewControllerDisplayModeAllVisible; // Master:首页 Feed 流(UICollectionViewController) HomeCollectionViewController *masterVC = [[HomeCollectionViewController alloc] init]; masterVC.title = @"Home"; // Detail:视频详情页(UIViewController) VideoDetailViewController *detailVC = [[VideoDetailViewController alloc] init]; detailVC.title = @"Video"; // 关键:设置 splitVC 的 viewControllers,顺序不能错 splitVC.viewControllers = @[[UINavigationController new], [UINavigationController new]]; UINavigationController *masterNav = (UINavigationController *)splitVC.viewControllers[0]; UINavigationController *detailNav = (UINavigationController *)splitVC.viewControllers[1]; [masterNav pushViewController:masterVC animated:NO]; [detailNav pushViewController:detailVC animated:NO]; self.window.rootViewController = splitVC; } return YES; } // HomeCollectionViewController.m - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath { VideoItem *item = self.videos[indexPath.item]; VideoDetailViewController *detailVC = [[VideoDetailViewController alloc] init]; detailVC.videoItem = item; // 关键:iPad 下 push 到 detail navigation controller,iPhone 下 present if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) { UISplitViewController *splitVC = (UISplitViewController *)self.splitViewController; UINavigationController *detailNav = splitVC.viewControllers[1]; [detailNav pushViewController:detailVC animated:YES]; } else { [self presentViewController:detailVC animated:YES completion:nil]; } }逻辑说明:
preferredDisplayMode = UISplitViewControllerDisplayModeAllVisible强制 iPad 启动即显示双栏,避免首次进入时闪一下;viewControllers数组顺序必须为[masterNav, detailNav],否则splitViewController:willChangeToDisplayMode:回调无法触发;didSelectItemAtIndexPath:中通过UI_USER_INTERFACE_IDIOM()判断设备类型,实现同一套业务逻辑适配双端。
4. 编译与真机运行全流程:从 Xcode 配置到证书签名,避坑指南全记录
4.1 Xcode 14+ 编译前必做 5 项配置
Signing & Capabilities → Team 设置
必须选择有效的 Apple Developer Team,否则Code Signing Error: No profiles for 'com.example.tiktokdemo' were found。即使只跑模拟器,Xcode 14 也强制要求 Team 配置。Build Settings → Valid Architectures
修改为arm64(删除armv7),因 iOS 15+ 已废弃 32 位支持。若保留armv7,真机编译报错ld: building for iOS-arm64 but linking in object file built for iOS-armv7。Build Settings → Enable Bitcode = NO
AFNetworking 4.0.1 不支持 Bitcode,开启后链接失败。错误提示:bitcode bundle could not be generated because '/Pods/AFNetworking/AFNetworking/AFURLSessionManager.o' was built without full bitcode.Info.plist → Required device capabilities
添加arm64到UIRequiredDeviceCapabilities数组,否则 App Store Connect 提交时被拒:This app contains bitcode and requires arm64 support.Build Phases → Run Script
在Embed Pods Frameworks后添加脚本,修复 SDWebImage 的dSYM符号缺失问题:#!/bin/sh if [ "$CONFIGURATION" == "Release" ]; then find "${BUILT_PRODUCTS_DIR}" -name "SDWebImage.framework" -type d | while read FRAMEWORK; do echo "Fixing dSYM for $FRAMEWORK" dsymutil "$FRAMEWORK/SDWebImage" -o "${DWARF_DSYM_FOLDER_PATH}/${FRAMEWORK_NAME}.dSYM" done fi
4.2 真机部署常见问题排查:现象、原因、解决三步法
现象 1:App 安装后闪退,Xcode 控制台输出Terminating due to uncaught exception 'NSInvalidArgumentException', reason: '-[VideoPlayerView setPlayer:]: unrecognized selector sent to instance'
原因:VideoPlayerView的player属性未在init中初始化,且AVFoundation.framework未正确链接。
解决:检查Build Phases → Link Binary With Libraries是否包含AVFoundation.framework;在VideoPlayerView.m的-init方法中添加self.player = [AVPlayer new];。
现象 2:iPad 上分屏模式下,点击 Feed 项无反应,控制台无日志
原因:HomeCollectionViewController的splitViewController属性为 nil,因未在viewDidLoad中调用[self setParentViewController:...]。
解决:在HomeCollectionViewController.m的-viewDidLoad中添加:
- (void)viewDidLoad { [super viewDidLoad]; if (@available(iOS 13.0, *)) { if (self.parentViewController && [self.parentViewController isKindOfClass:[UISplitViewController class]]) { self.splitViewController = (UISplitViewController *)self.parentViewController; } } }现象 3:视频播放时,锁屏后音频中断,后台无法继续播放
原因:AVAudioSessionCategory 未设置为AVAudioSessionCategoryPlayback,且未启用后台模式。
解决:
- 在
AppDelegate.m的-application:didFinishLaunchingWithOptions:中添加:NSError *error; [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:&error]; [[AVAudioSession sharedInstance] setActive:YES error:&error]; - 在
Info.plist中添加UIBackgroundModes数组,包含audio字符串。
现象 4:SDWebImage 加载图片失败,控制台报Could not load image from URL: https://...,但 URL 在浏览器中可访问
原因:iOS 10+ 强制 HTTPS,且Info.plist中未配置NSAppTransportSecurity。
解决:在Info.plist中添加:
<key>NSAppTransportSecurity</key> <dict> <key>NSAllowsArbitraryLoads</key> <true/> </dict>注意:仅开发阶段使用,上架前需改为白名单域名。
现象 5:模拟器运行正常,真机编译报错Undefined symbols for architecture arm64: "_OBJC_CLASS_$_YYModel"
原因:YYModel未被正确链接到主 target,Pods 依赖未生效。
解决:执行cd ios && pod deintegrate && pod install,重启 Xcode;检查Build Settings → Other Linker Flags是否包含-framework "YYModel"(应由 CocoaPods 自动注入)。
5. 二次开发改造指南:如何接入自有 API、替换播放器、扩展 iPad 分屏功能
5.1 接入自有后端 API:替换 NetworkManager 中的 Base URL 与鉴权逻辑
当前NetworkManager使用模拟接口https://api.example.com/v1/videos,替换步骤如下:
修改 Base URL
打开Services/NetworkManager.m,定位+ (NSString *)baseURL方法:+ (NSString *)baseURL { // 原始:return @"https://api.example.com"; return @"https://your-api-domain.com"; // 替换为你的真实域名 }注入 Token 鉴权头
在+ (AFHTTPSessionManager *)sharedManager中添加:+ (AFHTTPSessionManager *)sharedManager { static AFHTTPSessionManager *manager = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ manager = [[AFHTTPSessionManager alloc] initWithBaseURL:[NSURL URLWithString:[self baseURL]]]; // 新增:读取 UserDefaults 中的 token NSString *token = [[NSUserDefaults standardUserDefaults] stringForKey:@"auth_token"]; if (token.length > 0) { [manager.requestSerializer setValue:[NSString stringWithFormat:@"Bearer %@", token] forHTTPHeaderField:@"Authorization"]; } // ... 其他配置 }); return manager; }刷新 Token 机制(可选)
若你的 API 支持 refreshToken,需在NetworkManager.m中添加:+ (void)refreshTokenWithCompletion:(void(^)(BOOL success))completion { NSDictionary *params = @{@"refresh_token": [[NSUserDefaults standardUserDefaults] stringForKey:@"refresh_token"]}; [[self sharedManager] POST:@"/auth/refresh" parameters:params progress:nil success:^(NSURLSessionDataTask *task, id responseObject) { // 解析 response,保存新 token 到 NSUserDefaults [[NSUserDefaults standardUserDefaults] setObject:responseObject[@"access_token"] forKey:@"auth_token"]; [[NSUserDefaults standardUserDefaults] setObject:responseObject[@"refresh_token"] forKey:@"refresh_token"]; completion(YES); } failure:^(NSURLSessionDataTask *task, NSError *error) { completion(NO); }]; }
5.2 替换视频播放器:用 IJKPlayer 替代 AVPlayerLayer(适配 RTMP 流)
若需支持直播流(RTMP/HLS),AVPlayer 不适用,改用 IJKPlayer:
安装 IJKPlayer
在Podfile中替换pod 'SDWebImage'为:pod 'IJKMediaFramework', :git => 'https://github.com/Bilibili/ijkplayer.git', :tag => 'ff3.4'重构 VideoPlayerView
修改VideoPlayerView.h:#import <IJKMediaFramework/IJKMediaFramework.h> @interface VideoPlayerView : UIView @property (nonatomic, strong) IJKFFMoviePlayerController *player; // ... 其他属性保持不变 @end重写播放逻辑(关键差异点)
// VideoPlayerView.m - (void)loadVideoWithURL:(NSURL *)url { if (!self.player) { self.player = [[IJKFFMoviePlayerController alloc] initWithContentURL:url withOptions:nil]; self.player.view.frame = self.bounds; [self addSubview:self.player.view]; } else { [self.player stop]; [self.player setContentURL:url]; } [self.player prepareToPlay]; } // 注意:IJKPlayer 的 seekToTime 单位是秒(float),非 CMTime - (void)seekToTime:(float)timeInSeconds { [self.player seekToTime:timeInSeconds]; }
提示:IJKPlayer 需在
Info.plist中添加NSAppTransportSecurity白名单,并关闭 Bitcode(同前文)。
5.3 扩展 iPad 分屏功能:在 Detail 侧增加评论区 UITableView
当前 Detail 页为单UIViewController,扩展为UISplitViewController的 Detail 区域嵌套UITabBarController:
创建 CommentTableViewController
新建Controllers/Comment/CommentTableViewController.m/h,继承UITableViewController,实现UITableViewDataSource。修改 VideoDetailViewController
将其viewDidLoad中的 UI 构建逻辑移出,改为:- (void)viewDidLoad { [super viewDidLoad]; // 移除原有 UI 创建代码 // 创建 TabBarController 作为 Detail 的 root view UITabBarController *tabVC = [[UITabBarController alloc] init]; tabVC.viewControllers = @[ [[VideoPlayerViewController alloc] init], [[CommentTableViewController alloc] init] ]; // 设置 tabBarItem tabVC.viewControllers[0].tabBarItem = [[UITabBarItem alloc] initWithTitle:@"Video" image:nil tag:0]; tabVC.viewControllers[1].tabBarItem = [[UITabBarItem alloc] initWithTitle:@"Comments" image:nil tag:1]; [self addChildViewController:tabVC]; [self.view addSubview:tabVC.view]; [tabVC didMoveToParentViewController:self]; }适配 iPhone 端
在VideoDetailViewController.m的-viewWillAppear:中判断设备:- (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone) { // iPhone 下隐藏 tab bar,只显示 video 页 self.tabBarController.tabBar.hidden = YES; } }
从那以后我每次接手 Objective-C 老项目做 iPad 适配,都会先检查三点:UISplitViewController的viewControllers数组顺序、UITraitCollection的traitCollectionDidChange:是否被正确重写、AVAudioSession的 Category 是否设为Playback。这三处只要有一处漏掉,就会出现“能编译但行为诡异”的玄学问题——比如 iPad 分屏时点击无响应,或者后台播放突然静音。现在我把这些检查项固化成一个PreFlightCheck类,每次viewDidLoad都跑一遍,打印日志。希望帮到你。
本文还有配套的精品资源,点击获取