
摘要: 在安防监控、无人机图传、工业巡检、远程运维、移动导播等场景中,iOS端播放器并不只是“把视频显示出来”这么简单。一个可实际落地的RTSP/RTMP直播播放模块,需要同时兼顾低延迟、首屏速度、软硬解兼容、横竖屏适配、录像快照、弱网状态监控、YUV/SEI回调以及播放器生命周期管理。本文结合大牛直播SDK(SmartMediaKit)iOS版SmartiOSPlayerV2 Demo,系统梳理RTSP/RTMP直播播放器在iPhone/iPad端的集成方式和关键设计点。
关键词:SmartMediaKit、iOS播放器、RTSP播放器、RTMP播放器、低延迟直播、VideoToolbox、Metal、AVSampleBufferDisplayLayer、音视频开发
在iOS端做点播播放器,开发者更多关注的是进度条、倍速、缓存、字幕和本地文件兼容;但直播播放器面对的是持续到达、不可预知、不可回退的实时码流。
尤其在RTSP/RTMP直播场景中,播放器需要处理的问题远比“拉流、解码、显示”复杂:
很多播放器Demo只演示“开始播放”和“停止播放”,但真正进入项目后,稳定性往往不是毁在主流程,而是毁在异常流程:比如停止播放失败后提前释放View、旧实例事件覆盖新实例状态、横竖屏切换后渲染层尺寸异常、录像和播放共用实例时释放时机不对等。
SmartMediaKit iOS版SmartiOSPlayerV2 Demo的价值,正是在于它不是一个极简播放样例,而是把播放器集成中常见的工程问题也一并考虑进去了。
SmartiOSPlayerV2基于Objective-C和UIKit实现。竖屏状态下,上方为直播画面,下方为可滚动控制区;横屏状态下自动进入全屏播放,并提供返回竖屏按钮。

Demo覆盖的核心能力如下:
能力 | 说明 |
|---|---|
RTSP/RTMP播放 | 支持输入RTSP或RTMP直播地址进行播放 |
RTSP传输模式 | 支持TCP/UDP选择,并可配置自动切换 |
低延迟播放 | 支持低延迟模式、快速启动和播放缓冲配置 |
三种解码模式 | 软解、普通硬解、硬解Layer直显 |
视频渲染 | 普通模式使用Metal渲染,适配iPhone和iPad |
横竖屏适配 | 竖屏控制面板、横屏全屏播放、主动返回竖屏 |
比例绘制 | 支持保持比例或拉伸铺满 |
播放中切换URL | 支持主备流、轮巡流或测试流动态切换 |
音频控制 | 支持音量调节、静音和取消静音 |
图像调节 | 支持亮度、对比度、饱和度和一键重置 |
画面变换 | 支持90度旋转、水平镜像、垂直镜像 |
本地录像 | 支持MP4录像、文件大小限制和音频转AAC |
录像回放 | 独立RecorderView页面支持本地录像文件播放 |
快照 | 支持保存当前画面为PNG并写入系统相册 |
YUV回调 | 可选I420 YUV回调,便于AI分析或自定义处理 |
SEI用户数据 | 支持用户数据回调,可用于业务信息透传 |
状态监控 | 支持连接状态、分辨率、下载速度、丢包率等事件 |
从功能覆盖看,这个Demo已经接近一个移动端RTSP/RTMP播放测试工具的完整形态,开发者可以在此基础上改造成监控预览、无人机图传预览、工业巡检终端、移动导播预览或内部流媒体测试工具。
在安防项目中,iPhone或iPad常用于查看IPC、NVR或边缘设备输出的RTSP视频。播放器需要支持横屏查看、低延迟预览、快照留证、本地录像和异常状态提示。
SmartMediaKit播放器模块可以直接用于移动监控客户端,也可以作为项目中的视频预览组件集成到现有App中。
无人机图传、机器人巡检和远程操控对延迟非常敏感。播放侧需要尽可能减少缓冲等待,同时保持解码和渲染链路稳定。
Demo默认开启低延迟模式、快速启动,并将播放缓冲设置为0,适合局域网或专网环境下追求实时性的预览场景。对于公网或弱网环境,也可以切换RTSP TCP模式,提高连续性。
工业现场的视频流可能来自摄像机、采集盒、边缘网关或自定义推流设备。移动端通常承担实时查看、异常留档和远程辅助判断的角色。
Demo提供的录像、快照、SEI用户数据回调和下载速度回调,适合和设备状态、AI检测结果、巡检事件等业务数据结合。
iPad可以作为移动预览终端,用于查看不同直播源、切换主备流、确认输入信号是否正常。播放中动态切换URL和下载速度回调,对导播预览、流媒体测试和直播前检查非常实用。

直播播放器生命周期是最容易被忽视的部分。
SmartiOSPlayerV2没有简单地用一个isPlaying控制全部状态,而是分别维护:
BOOL is_inited_player_; // SDK是否已初始化
BOOL is_playing_; // 是否正在播放
BOOL is_recording_; // 是否正在录像
这样设计的原因是,播放和录像并不是完全绑定的:
整体调用关系可以理解为:
InitPlayer
├── StartPlayer
└── StartRecorder
StopPlayer / StopRecorder
└── TryUnInitPlayer
└── 播放和录像均停止后,再执行UnInitPlayer
页面销毁时,Demo也遵循更安全的释放顺序:
停止录像 -> 停止播放 -> 释放播放View -> UnInit SDK
这类细节在Demo阶段看起来“多写了一点代码”,但在实际项目中非常关键。特别是播放View由SDK创建并由底层渲染线程使用,如果SmartPlayerStop失败,不能简单地马上移除和释放UIView,否则可能出现渲染线程仍访问已释放对象的风险。

播放器初始化主要在InitPlayer中完成:
_smart_player_sdk = [[SmartPlayerSDK alloc] init];
_smart_player_sdk.delegate = self;
NSInteger ret = [_smart_player_sdk SmartPlayerInitPlayer];
if (ret != DANIULIVE_RETURN_OK) {
return NO;
}
随后设置播放地址:
NSString *initialURL = [self currentPlaybackURL];
[_smart_player_sdk SmartPlayerSetPlayURL:initialURL];
playback_url_ = [initialURL copy];
self.playbackURLTextField.text = playback_url_;
Demo默认播放地址为RTSP地址:
playback_url_ = @"rtsp://192.168.0.103:18554/stream1";
也可以切换为RTMP地址:
// playback_url_ = @"rtmp://192.168.0.102:1935/hls/stream1";
低延迟相关配置如下:
is_fast_startup_ = YES; // 快速启动
is_low_latency_mode_ = YES; // 低延迟模式
buffer_time_ = 0; // 0ms播放缓冲
is_rtsp_tcp_mode_ = NO; // 默认RTSP UDP模式
对应SDK接口:
[_smart_player_sdk SmartPlayerSetLowLatencyMode:is_low_latency_mode_];
[_smart_player_sdk SmartPlayerSetBuffer:buffer_time_];
[_smart_player_sdk SmartPlayerSetFastStartup:is_fast_startup_];
[_smart_player_sdk SmartPlayerSetRTSPTcpMode:is_rtsp_tcp_mode_];
[_smart_player_sdk SmartPlayerSetRTSPTimeout:10];
[_smart_player_sdk SmartPlayerSetRTSPAutoSwitchTcpUdp:1];
这组配置偏向实时性,适合局域网、专网、图传和远程操控类场景。
但在实际项目中,低延迟配置不能孤立看待。端到端延迟还会受到采集端缓存、编码器GOP、B帧、服务端转发、网络排队、播放器缓冲和解码等待关键帧等因素影响。
因此,建议在产品中提供两类模式:
实时性优先:低延迟 + 快速启动 + 小缓冲 + UDP优先
连续性优先:TCP传输 + 适当缓冲 + 更关注观看稳定性
SmartMediaKit iOS播放器通过SDK创建播放View,再桥接为UIKit中的UIView使用:
_glView = (__bridge UIView *)([SmartPlayerSDK SmartPlayerCreatePlayView:
player_view_x_
y:player_view_y_
width:player_view_width_
height:player_view_height_
renderType:1]);
创建成功后加入当前页面:
[self.view addSubview:_glView];
[self.view sendSubviewToBack:_glView];
然后绑定给播放器实例:
NSInteger setViewRet =
[_smart_player_sdk SmartPlayerSetPlayView:(__bridge void *)_glView];
最后启动播放:
NSInteger ret = [_smart_player_sdk SmartPlayerStart];
if (ret == DANIULIVE_RETURN_OK) {
is_playing_ = YES;
}
停止播放时,必须先调用SDK停止接口:
NSInteger ret = [_smart_player_sdk SmartPlayerStop];
停止成功后,再释放播放View:
[_glView removeFromSuperview];
[SmartPlayerSDK SmartPlayeReleasePlayView:(__bridge void *)_glView];
_glView = nil;
这里需要特别强调:SDK创建的播放View应使用SDK对应的释放接口回收,不能只调用removeFromSuperview后直接丢弃对象引用。

Demo提供三种解码模式:
NSArray *decoderItems = @[@"软解", @"硬解", @"硬解(layer)"];
self.decoderModeSegment =
[[UISegmentedControl alloc] initWithItems:decoderItems];
self.decoderModeSegment.selectedSegmentIndex = 1;
默认使用普通硬解模式。
软件解码适合兼容性优先的场景。例如某些特殊码流、硬解不支持的编码参数、或者需要完整CPU图像处理链路时,可以选择软解。
软解的优势是可控性强,缺点是在高分辨率、高帧率场景下CPU占用和功耗会更高。
普通硬解使用VideoToolbox完成硬件解码,再交给普通渲染链路显示。
这个模式是Demo默认选择,也是多数项目中的推荐模式,因为它兼顾了性能和功能:
第三种模式是硬解Layer直显,对应:
NSInteger directRender = (selectedIndex == 2) ? 1 : 0;
[_smart_player_sdk SmartPlayerSetVideoDecoderMode:decoderMode];
[_smart_player_sdk SetDirectHardwareRender:directRender];
Layer直显模式使用AVSampleBufferDisplayLayer完成显示,减少普通渲染路径中的数据转换和回传,更偏向性能和低开销显示。
但它也有明确限制:由于视频图像不再回流到普通CPU图像处理链路,因此不支持:
Demo在选择Layer模式后,会自动禁用快照按钮和色彩调节滑块:
BOOL supportsImageProcessing = (modeIndex != 2);
self.brightnessSlider.enabled = supportsImageProcessing;
self.contrastSlider.enabled = supportsImageProcessing;
self.saturationSlider.enabled = supportsImageProcessing;
resetColorBtn.enabled = supportsImageProcessing;
saveImageButton.enabled = supportsImageProcessing;
同时,为了避免播放过程中重建解码链和渲染链,Demo在播放期间禁用解码模式切换:
self.decoderModeSegment.enabled = NO;
停止播放后再恢复选择。

iOS播放器如果要用于真实业务,横竖屏适配必须处理好。SmartiOSPlayerV2在viewDidLayoutSubviews中根据当前页面宽高判断方向:
CGFloat w = self.view.bounds.size.width;
CGFloat h = self.view.bounds.size.height;
BOOL isLandscape = w > h;
横屏模式下,视频全屏显示,隐藏控制面板:
videoFrame = CGRectMake(0, 0, w, h);
self.controlContainerView.hidden = YES;
self.backButton.hidden = NO;
竖屏模式下,上方40%用于显示视频,下方60%用于控制区:
CGFloat videoHeight = h * 0.4;
videoFrame = CGRectMake(0, 50, w, videoHeight);
CGRect controlFrame =
CGRectMake(0, 50 + videoHeight, w, h - (50 + videoHeight));
当播放View存在时,更新其Frame:
if (_glView) {
if (!CGRectEqualToRect(_glView.frame, videoFrame)) {
_glView.frame = videoFrame;
}
}
普通Metal渲染路径下,播放View内部会同步drawable尺寸,不需要业务层频繁主动刷新。
但如果使用Layer直显模式,横竖屏变化后需要主动通知SDK更新显示区域:
if (_smart_player_sdk && is_playing_ &&
self.decoderModeSegment.selectedSegmentIndex == 2) {
[_smart_player_sdk UpdateViewSize:player_view_width_
height:player_view_height_];
}
对于iOS 16及以上版本,Demo使用UIWindowSceneGeometryPreferencesIOS请求切回竖屏;旧版本则采用兼容方式处理方向切换。
在监控和工业视觉场景中,很多时候需要保持原始比例,避免画面中人物、车辆、仪表或设备结构变形。
Demo提供“按比例绘制”开关:
self.renderAspectSwitch.on = YES;
播放中可动态切换:
NSInteger scaleMode = sender.isOn ? 1 : 0;
[_smart_player_sdk SmartPlayerSetRenderScaleMode:scaleMode];
这里的逻辑很适合实际项目参考:如果接口设置失败,UI开关会回滚到原状态,避免出现“界面显示已经切换,但底层实际没有切换”的状态不一致问题。
摄像机轮巡、主备流切换、导播预览、测试不同协议源时,经常需要在播放中切换URL。
Demo中提供了备用地址:
static NSString * const kSwitchPlaybackURL =
@"rtmp://192.168.0.103:1935/hls/stream1";
播放中切换时调用:
NSInteger ret =
[_smart_player_sdk SmartPlayerSwitchPlaybackUrl:newUrl];
if (ret == DANIULIVE_RETURN_OK) {
is_switch_url_ = targetSwitchState;
self.playbackURLTextField.text = newUrl;
}
这里需要区分两类场景:
修改输入框后重新启动:
适合完整更换播放源、重新走初始化配置。
播放中SwitchPlaybackUrl:
适合主备流切换、轮巡预览、临时切换测试源。
对于监控平台或移动导播工具来说,播放中切换URL可以减少用户等待,也能让Demo更容易扩展为多源预览工具。
播放器常用控制能力包括音量、静音、旋转和镜像。
音量调节:
[_smart_player_sdk SmartPlayerSetAudioVolume:
(NSInteger)self.audioVolumeSlider.value];
静音切换:
BOOL targetMute = !is_mute_;
[_smart_player_sdk SmartPlayerSetMute:targetMute ? 1 : 0];
is_mute_ = targetMute;
画面旋转:
NSInteger targetRotation = (rotate_degrees_ + 90) % 360;
[_smart_player_sdk SmartPlayerSetRotation:targetRotation];
rotate_degrees_ = targetRotation;
水平镜像:
[_smart_player_sdk SmartPlayerSetFlipHorizontal:
targetFlip ? 1 : 0];
垂直镜像:
[_smart_player_sdk SmartPlayerSetFlipVertical:
targetFlip ? 1 : 0];
这些功能在实际项目中非常常见。例如:
Demo在初始化阶段启用图像调节能力:
[_smart_player_sdk EnableVideoBrightnessOption:1];
[_smart_player_sdk EnableVideoContrastOption:1];
[_smart_player_sdk EnableVideoSaturationOption:1];
然后设置默认值:
[_smart_player_sdk SetVideoBrightness:50];
[_smart_player_sdk SetVideoContrast:50];
[_smart_player_sdk SetVideoSaturation:50];
滑块变化时动态设置:
NSInteger val = (NSInteger)self.brightnessSlider.value;
[_smart_player_sdk SetVideoBrightness:val];
对比度和饱和度同理。
这类能力在安防、工业视觉和户外图传场景中很有价值。比如摄像头逆光、夜间画面偏暗、图像颜色偏淡时,上层可以提供简单的画面增强入口,改善预览体验。
不过需要注意,Layer直显模式下不支持这类图像调节,因此Demo会在UI层主动禁用相关控件。

SmartiOSPlayerV2支持播放侧录像。录像目录使用App的Documents目录:
NSString *docDir =
[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
NSUserDomainMask,
YES) firstObject];
[_smart_player_sdk SmartPlayerSetRecorderDirectory:docDir];
设置单个录像文件最大大小:
[_smart_player_sdk SmartPlayerSetRecorderFileMaxSize:500];
开启音频转AAC,并启用音视频录像:
[_smart_player_sdk SmartPlayerSetRecorderAudioTranscodeAAC:1];
[_smart_player_sdk SmartPlayerSetRecorderVideo:1];
[_smart_player_sdk SmartPlayerSetRecorderAudio:1];
启动录像:
NSInteger ret = [_smart_player_sdk SmartPlayerStartRecorder];
停止录像:
NSInteger ret = [_smart_player_sdk SmartPlayerStopRecorder];
Demo中的录像和播放共用SDK实例,但播放状态和录像状态相互独立。这样可以支持:
Demo还提供独立的RecorderView页面,用于本地录像文件回放。回放页可扫描Documents目录下的录像文件,并支持播放、暂停、进度拖动、倍速、循环播放、删除和横屏回放。
这让播放器Demo不仅能预览直播流,还能完成“实时查看 + 本地留档 + 本地回放”的完整闭环。

快照流程分为两步:
第一步,SDK将当前画面保存到App Documents目录中的临时PNG文件:
[_smart_player_sdk SmartPlayerSaveCurImage:fileName];
第二步,在快照事件回调中读取图片,并写入系统相册:
UIImage *snapshotImage =
[UIImage imageWithContentsOfFile:param3];
UIImageWriteToSavedPhotosAlbum(snapshotImage,
self,
@selector(image:didFinishSavingWithError:contextInfo:),
(void *)CFBridgingRetain(param3));
Demo这里有一个值得注意的细节:临时图片路径通过contextInfo跟随本次相册写入操作传递。这样即使用户连续点击快照,每次写入完成后也只删除自己对应的临时文件,避免多个快照共用成员变量导致路径覆盖或误删。
回调完成后删除临时文件:
if (tmpImagePath) {
[[NSFileManager defaultManager] removeItemAtPath:tmpImagePath error:nil];
}
保存到相册需要在Info.plist中配置照片写入权限说明:
<key>NSPhotoLibraryAddUsageDescription</key>
<string>用于保存直播画面快照</string>
正式项目中,权限文案建议写得更贴近业务,例如“用于保存监控画面截图”或“用于保存巡检过程中的视频快照”。
Demo中保留了I420 YUV回调示例,但默认关闭:
static const BOOL kEnableYuvCallbackDemo = NO;
原因很清楚:普通硬解路径下开启YUV回调,通常需要从NV12转换到I420,并增加一次数据拷贝。对于只做正常播放的场景,默认开启会带来额外开销。
需要图像分析、自定义渲染或AI处理时,可以打开:
_smart_player_sdk.yuvDataBlock =
^(int width, int height, unsigned long long timeStamp,
const unsigned char *yData,
const unsigned char *uData,
const unsigned char *vData,
int yStride, int uStride, int vStride) {
// 处理I420图像
};
[_smart_player_sdk SmartPlayerSetYuvBlock:YES];
使用YUV回调时要注意:
因此,比较合理的方式是:回调中快速复制或入队,再交给独立工作线程做AI检测、图像分析或业务处理。
除了视频帧,直播流中还可能携带业务数据。例如:
Demo通过用户数据回调接收这类信息:
_smart_player_sdk.spUserDataCallBack =
^(int type,
unsigned char *data,
unsigned int size,
unsigned long long ts,
unsigned long long r1,
long long r2,
unsigned char *r3) {
[strongSelf OnUserDataCallBack:type
data:data
size:(int)size];
};
[_smart_player_sdk SmartPlayerSetUserDataCallback:YES];
在OnUserDataCallBack中,Demo针对UTF-8字符串做了基本保护:
if (type == 2 && data != NULL && size > 0) {
NSUInteger length = (NSUInteger)size;
if (data[length - 1] == 0) {
--length;
}
NSString *msg =
[[NSString alloc] initWithBytes:data
length:length
encoding:NSUTF8StringEncoding];
}
随后切回主线程更新UI。
这个能力对于“视频 + 业务数据同步”的场景很实用。例如无人机图传中同步飞行状态,工业检测中同步AI识别结果,远程巡检中同步设备编号、时间戳和异常信息。

直播播放器不能只关心“播放按钮是否按下”。实际项目更需要知道当前播放器处于什么状态:
Demo在handleSmartPlayerEvent中统一处理这些事件,并切回主线程更新UI:
dispatch_async(dispatch_get_main_queue(), ^{
// 更新UILabel、UIButton等UIKit控件
});
下载速度事件中,param1表示Byte/s,Demo同时换算成kbps和KB/s:
NSInteger kbps = (NSInteger)param1 * 8 / 1000;
NSInteger kbs = (NSInteger)param1 / 1024;
丢包率通过param2解析:
const float Q8_SCALE = 256.0f;
const unsigned long long MASK_15_BIT = 0x7FFFULL;
if (((param2 >> 15) & 1ULL) == 1ULL) {
float lr = (param2 & MASK_15_BIT) / Q8_SCALE;
}
这组数据对排查问题很有价值。
例如:
有下载速度,但画面不动:
可能是解码等待关键帧、码流异常或渲染链路问题。
没有下载速度:
可能是网络连接、服务端、URL或权限问题。
UDP丢包率较高:
可能需要切换TCP,或降低码率、帧率、分辨率。
播放器Demo提供这些状态,不只是为了显示好看,而是为了让开发者在真实网络环境中快速定位问题。
iOS 14及以上访问局域网设备时,需要配置本地网络权限说明:
<key>NSLocalNetworkUsageDescription</key>
<string>用于连接局域网内的直播设备</string>
如果RTSP服务器、IPC、NVR或测试推流设备位于局域网,首次访问时系统会弹出授权提示。用户拒绝后,播放器可能表现为连接失败或无数据。
快照写入系统相册,需要配置:
<key>NSPhotoLibraryAddUsageDescription</key>
<string>用于保存直播画面快照</string>
Demo开启UIFileSharingEnabled后,调试阶段可以通过系统文件共享方式查看App的Documents目录,便于导出录像文件。
Demo支持iPhone和iPad横竖屏:
iPhone:竖屏、横屏左、横屏右
iPad:竖屏、倒置竖屏、横屏左、横屏右
将SDK对应的SmartPlayerSDKAll.xcframework加入工程,并根据SDK交付说明完成链接配置。
常见配置包括:
Embed:Do Not Embed
Other Linker Flags:-ObjC
Demo涉及的系统依赖通常包括:
AVFoundation.framework
AudioToolbox.framework
CFNetwork.framework
Security.framework
SystemConfiguration.framework
CoreFoundation.framework
CoreGraphics.framework
CoreMedia.framework
CoreVideo.framework
VideoToolbox.framework
Metal.framework
GLKit.framework
OpenGLES.framework
QuartzCore.framework
libz.tbd
libbz2.tbd
libiconv.tbd
libc++.tbd
具体依赖应以实际SDK版本和交付文档为准。
需要注意的是,如果Demo工程中保留了历史框架配置,正式项目中应结合当前代码和SDK依赖确认是否仍然需要,不建议无差别照搬旧工程配置。
以下流程属于关键路径,失败后不建议继续启动:
SDK初始化失败
播放URL设置失败
解码模式设置失败
播放View创建失败
播放View绑定失败
SmartPlayerStart失败
录像目录设置失败
而音量、亮度、镜像、下载速度上报等附加配置失败,可以记录日志并继续播放,避免非核心功能阻断主流程。
RTSP TCP和UDP没有绝对优劣。
UDP:
实时性更好,适合局域网、专网、低延迟图传;
但弱网丢包后可能影响视频参考帧恢复。
TCP:
连续性更好,适合公网、跨网和观看稳定性优先场景;
但网络抖动时可能出现重传等待,增加延迟。
工程上建议不要把TCP/UDP写死,而是作为业务配置项提供给上层。
模拟器适合验证UI布局和部分流程,但以下内容必须用真机验证:
YUV回调适合做AI分析、自定义处理或第三方渲染,但不要在回调线程直接做耗时计算。
推荐方式:
YUV回调线程:快速校验 + 拷贝/入队
业务工作线程:AI分析/图像处理
主线程:只负责UI更新
SDK创建的View不要随意释放。只有在停止播放成功,或SDK强制释放完成后,才能拆除播放View。
这对于避免偶发崩溃、黑屏和渲染线程异常非常重要。
当前Demo重点展示前台RTSP/RTMP直播播放,没有把后台持续拉流、后台音频或画中画作为默认能力。
如果业务需要后台运行,应额外结合:
大牛直播SDK(SmartMediaKit)iOS版RTSP/RTMP播放模块,覆盖了移动端实时播放中最核心的工程能力:
SmartiOSPlayerV2 Demo的意义,不只是演示SDK接口怎么调,而是给出了一个接近真实项目的播放器集成参考。
对于正在开发移动安防、工业巡检、无人机图传、机器人远程操控、教育直播预览或流媒体测试工具的团队,可以基于该Demo快速构建iOS端RTSP/RTMP直播播放模块,再结合自身业务UI、网络策略和状态机进行二次封装。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。