资讯动态

Objective-C 短视频信息流 Demo:双端适配、列表复用与播放器池

发布时间:2026/10/26 2:10:10 来源:尧图企业网站定制
简介面向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会报找不到 SDKxcodebuild -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 platformiOS Simulator,nameSIMULATOR_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) NSMutableDictionaryNSString *, 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) NSMutableArrayAVPlayer * *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.xcworkspacePROJECT_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:(idUIViewControllerTransitionCoordinator)coordinator { [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]; [coordinator animateAlongsideTransition:nil completion:^(idUIViewControllerTransitionCoordinatorContext 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 秒滚动曲线只要这三步有一处不对就先不进业务联调。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑