尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Unity3D集成Android原生播放器SDK实现RTSP/RTMP低延迟播放

Unity3D集成Android原生播放器SDK实现RTSP/RTMP低延迟播放 1. 项目概述为什么要在Unity3D里集成一个原生播放器如果你正在用Unity3D开发一款面向Android平台的App并且这个App的核心功能之一就是播放网络视频流——无论是安防监控的实时画面、工业设备的远程可视化、还是无人机回传的航拍影像你大概率会遇到一个头疼的问题Unity自带的VideoPlayer组件在播放RTSP、RTMP这类实时流媒体协议时表现往往不尽如人意。卡顿、延迟高、格式支持有限、CPU占用率飙升这些问题在追求稳定和低延迟的工业级应用中几乎是致命的。这时候转向一个成熟、专业的原生播放器SDK就成了一个非常自然的选择。大牛直播SDKSmartMediaKit就是这样一个在音视频领域深耕多年的解决方案。它不是一个简单的播放器控件而是一套完整的、针对移动端尤其是Android优化过的音视频处理引擎。把这样一套引擎集成到Unity3D里意味着你的Unity应用可以直接调用底层的高性能解码、渲染和网络处理能力从而获得与原生Android应用媲美甚至更优的流媒体播放体验。我接手过好几个需要将监控视频流嵌入到AR/VR培训场景中的项目Unity负责构建炫酷的3D交互界面而稳定、低延迟的视频流则必须交给像SmartMediaKit这样的专业SDK来处理。这个过程听起来像是“跨界合作”实际上也确实需要打通Unity的C#脚本层和Android的Java原生层。这份文档就是基于我多次“踩坑”和成功集成的经验为你梳理的一条从零开始、清晰可循的集成路径。无论你是Unity开发者想增强视频能力还是Android音视频工程师需要将成果赋能给Unity生态这篇文章都能帮你理清思路。2. 核心思路与架构设计Unity如何与Android原生SDK对话在开始敲代码之前我们必须理解Unity for Android应用的运行架构。一个发布成APK的Unity应用本质上是一个特殊的Android应用。它的入口是UnityPlayerActivity渲染由Unity的图形引擎负责但所有Android系统的能力如摄像头、传感器、网络、以及我们要用的原生SDK都存在于Java/Kotlin层。因此集成第三方Android SDK的核心思路就是建立一座桥梁让Unity的C#脚本能够跨越边界调用到Android原生代码中的方法并接收来自原生层的事件回调。这座桥梁通常通过两种技术实现AndroidJavaClass / AndroidJavaObject这是Unity官方提供的、在C#中直接调用Java静态方法和实例方法的方式。它简单直接适合调用量不大、接口不复杂的场景。Android Native Plugin这是一种更强大、更标准的集成方式。你需要编写一个JAR或AAR格式的插件其中包含Java封装层并在C#端通过DllImport调用C/C或AndroidJavaClass调用Java与之交互。这种方式性能更好封装更彻底适合功能复杂的SDK。对于大牛直播SDK这种功能丰富的商业SDK强烈推荐使用第二种方式即构建一个Android Native Plugin。我们的架构设计可以分层来看原生层Java/CPP这是核心。你需要创建一个Android Library模块在其中引入大牛直播SDK的AAR或JAR依赖包。然后编写一个或多个Java类作为“包装器”或“适配器”。这些类负责初始化SDK。创建并配置播放器实例。提供开始播放、停止播放、截图、调整音量等公共方法。监听SDK内部的事件如播放状态、错误信息、视频分辨率变化并将其转换为Unity可以接收的形式。桥接层C#在Unity的Assets目录下你需要创建对应的C#脚本。这些脚本通过AndroidJavaClass和AndroidJavaObject与上一步创建的Java包装器类进行通信。它会封装原生方法提供一套更符合Unity使用习惯的API例如将播放状态回调转换为C#的event或Action。表现层Unity GameObject这是最终用户看到的。你需要创建一个Unity的Texture2D或RenderTexture。桥接层的C#脚本会从原生SDK不断获取最新的视频帧数据通常是RGB或YUV格式然后通过Texture2D.LoadRawTextureData或类似方法更新到这个Texture上。最后将这个Texture赋给一个RawImageUI组件或某个3D物体的材质视频画面就显示出来了。为什么选择Native Plugin而不是纯C#调用性能是关键。视频解码和渲染是计算密集型操作频繁地通过AndroidJavaObject在C#和Java之间传递图像数据每一帧会带来巨大的性能开销和延迟。而Native Plugin允许在原生层直接操作纹理内存甚至可以利用GPU进行零拷贝渲染效率天壤之别。此外好的封装能隐藏复杂性让Unity开发者像使用普通组件一样使用播放器。2.1 工具选型与环境准备在动手之前请确保你的“武器库”已经齐备Unity版本建议使用较新的LTS版本如2021.3 LTS或2022.3 LTS。这些版本对Android构建的支持更稳定。确保在Unity Hub中安装了对应的Android Build Support模块。Android开发环境Android Studio这是管理和编译Android原生插件必不可少的IDE。从官网下载并安装。Android SDK NDK在Android Studio的SDK Manager中安装你目标Android版本对应的SDK Platform例如API Level 31。NDKNative Development Kit是必须的因为许多高性能SDK包括视频编解码底层是C/C库。安装一个版本如NDK 25.x。JDKUnity通常内置了OpenJDK但为了兼容性建议单独安装Oracle JDK 11或OpenJDK 11并在Unity的Preferences - External Tools中指定路径。大牛直播SDK从官方渠道获取最新的Android版SDK开发包。它通常包含SmartMediaKit.aar核心SDK库文件。docs/API文档。demo/示例工程这是最重要的参考资料里面包含了SDK的所有用法。Unity工程设置打开你的Unity项目前往File - Build Settings切换平台到Android。点击Player Settings进行关键配置Other SettingsMinimum API Level根据你的SDK要求设置例如Android 5.1API 22或更高。Target API Level建议设置为最新的稳定版如API 33以获得更好的兼容性和性能。Scripting Backend选择IL2CPP。IL2CPP比Mono有更好的性能和兼容性尤其是与原生代码交互时。Target Architectures勾选ARMv7和ARM64。现在市面上绝大多数Android设备都是64位同时兼容32位可以覆盖更老的设备。3. 构建Android原生插件AAR这是集成过程中最具技术含量的一步。我们将在Android Studio中创建一个独立的模块专门用于封装大牛直播SDK。3.1 创建Android Library模块打开Android Studio选择New Project模板选择Empty Views Activity给项目起个名字例如UnityMediaPlayerPlugin。确保Package name和Save location是你容易找到的路径。项目创建后我们需要的是一个供Unity调用的库而不是一个可运行的App。因此在项目根目录上右键选择New - Module。在弹出的窗口中选择Android Library。输入模块名称例如smartplayerlib。确保Package name与主项目不同但相关例如com.yourcompany.smartplayer。Minimum SDK选择与Unity中设置一致的版本。点击Finish。现在你的项目应该包含一个app模块和一个smartplayerlib模块。app模块我们可以暂时不管甚至删除我们只关心smartplayerlib。3.2 导入SDK并编写封装类导入AAR将获取到的SmartMediaKit.aar文件复制到smartplayerlib模块的libs目录下如果没有就新建一个。配置依赖打开smartplayerlib模块下的build.gradle文件Module级别在dependencies块中添加对本地AAR的依赖。dependencies { implementation fileTree(dir: libs, include: [*.jar, *.aar]) // 这行可能已存在确保它能包含aar // 如果上一条不行可以显式添加 implementation files(libs/SmartMediaKit.aar) implementation androidx.appcompat:appcompat:1.3.1 // 添加基础依赖根据SDK要求 // 可能还需要其他依赖请参考SDK官方文档 }点击Sync Now同步Gradle。编写Java封装类在smartplayerlib的Java源目录下通常是src/main/java/com/yourcompany/smartplayer/创建我们的核心封装类例如UnitySmartPlayer.java。这个类的目标是提供一个简洁、稳定的接口给Unity调用。下面是一个高度简化的示例展示了核心结构package com.yourcompany.smartplayer; import android.content.Context; import android.view.SurfaceView; import com.dartmedia.smartplayer.SmartPlayer; // 假设SDK的主类是SmartPlayer public class UnitySmartPlayer { private SmartPlayer mPlayer; private SurfaceView mSurfaceView; // 用于渲染视频的画面 private Context mUnityContext; // Unity将通过这个静态方法获取实例 public static UnitySmartPlayer getInstance() { return new UnitySmartPlayer(); } // 初始化播放器需要Unity传递当前的Activity上下文 public void initialize(Context context) { this.mUnityContext context; mPlayer new SmartPlayer(context); mSurfaceView new SurfaceView(context); // 配置播放器参数缓存大小、硬解码、渲染模式等 mPlayer.setBufferTime(300); // 设置300ms缓存平衡延迟和流畅性 mPlayer.setHardwareDecoder(true); // 开启硬解码节省CPU mPlayer.setRenderView(mSurfaceView); // 将播放器绑定到SurfaceView // 设置事件监听器 mPlayer.setOnEventListener(new SmartPlayer.OnEventListener() { Override public void onEvent(int event, String info) { // 这里处理SDK回调的事件例如 // EVENT_PLAY_STARTED, EVENT_PLAY_ERROR, EVENT_VIDEO_RESOLUTION等 // 我们需要将这些事件通知给Unity通常通过UnitySendMessage或本地方法回调。 // 为了简化可以先打印日志。 android.util.Log.d(UnitySmartPlayer, Event: event , Info: info); } }); } // 开始播放RTSP/RTMP流 public void play(String url) { if (mPlayer ! null) { mPlayer.play(url); // SDK的核心播放方法 } } // 停止播放 public void stop() { if (mPlayer ! null) { mPlayer.stop(); } } // 获取SurfaceViewUnity需要获取这个View并添加到其视图层级中 public SurfaceView getRenderSurfaceView() { return mSurfaceView; } // 释放资源非常重要 public void release() { if (mPlayer ! null) { mPlayer.stop(); mPlayer.release(); mPlayer null; } } }关键点解析Context在Android中Context是执行很多操作的“上下文”比如创建View、访问资源。Unity在调用时需要将当前的UnityPlayer.currentActivity作为Context传递过来。SurfaceView这是Android系统用于高性能图形渲染的组件。我们将SDK的视频输出绑定到它上面。之后Unity需要获取这个SurfaceView并将其“嵌入”到自己的视图系统中。事件回调SDK内部的各种状态如播放开始、出错、分辨率变更需要通过监听器回调。如何将这些回调“跨域”传递到Unity的C#层是集成的难点之一。上面示例用了日志实际项目中你需要使用UnityPlayer.UnitySendMessage或通过JNI调用一个C#静态方法。更高效的方式是在C层编写JNI代码但这更复杂。初级集成可以先用UnitySendMessage它允许你向指定的GameObject发送消息。3.3 生成AAR插件包编写完封装类后我们需要将这个smartplayerlib模块编译成Unity可以识别的AAR文件。在Android Studio右侧的Gradle面板中展开UnityMediaPlayerPlugin - smartplayerlib - Tasks - build。双击assemble或assembleRelease任务。Gradle会开始编译。编译成功后在smartplayerlib/build/outputs/aar/目录下你会找到smartplayerlib-release.aar也可能是debug版本。将这个AAR文件复制到你的Unity项目的Assets/Plugins/Android/目录下。如果Plugins或Android文件夹不存在请手动创建。注意目录结构必须准确Assets/Plugins/Android/。实操心得AAR与JAR的选择AARAndroid Archive比JARJava Archive更强大因为它不仅可以包含编译后的Java代码.class文件还能包含资源文件res、清单文件AndroidManifest.xml和原生库.so文件。大牛直播SDK很可能依赖一些C/C的.so库用于硬件解码加速所以提供AAR格式是最完整的。如果你只拿到JAR可能需要手动将对应的.so文件放到Unity项目的Assets/Plugins/Android/libs/[abi]目录下过程会更繁琐。4. Unity C#桥接层实现现在我们回到Unity创建C#脚本来调用刚刚打包好的Android原生插件。4.1 创建播放器管理器脚本在Unity中创建一个C#脚本例如AndroidStreamPlayer.cs。这个脚本将挂载到一个空的GameObject上负责整个播放器的生命周期管理。首先我们需要在脚本中定义与Java类交互的接口using UnityEngine; using System; using System.Runtime.InteropServices; public class AndroidStreamPlayer : MonoBehaviour { // 定义原生插件回调的委托函数指针 public delegate void PlayerEventDelegate(int eventId, string eventInfo); public static event PlayerEventDelegate OnPlayerEvent; // 通过AndroidJavaClass调用静态方法获取Java对象实例 private AndroidJavaObject _nativePlayer null; private AndroidJavaObject _surfaceView null; private IntPtr _texturePtr IntPtr.Zero; // 用于高级渲染暂不展开 // Unity启动时调用 void Start() { // 检查是否在Android平台 if (Application.platform ! RuntimePlatform.Android) { Debug.LogWarning(AndroidStreamPlayer only works on Android platform.); return; } InitializeNativePlayer(); } void InitializeNativePlayer() { try { // 1. 获取当前Unity的Activity上下文 AndroidJavaClass unityPlayerClass new AndroidJavaClass(com.unity3d.player.UnityPlayer); AndroidJavaObject currentActivity unityPlayerClass.GetStaticAndroidJavaObject(currentActivity); // 2. 调用我们Java封装类的静态方法获取实例 AndroidJavaClass pluginClass new AndroidJavaClass(com.yourcompany.smartplayer.UnitySmartPlayer); _nativePlayer pluginClass.CallStaticAndroidJavaObject(getInstance); // 3. 调用initialize方法传入Activity上下文 _nativePlayer.Call(initialize, currentActivity); // 4. 获取SurfaceView对象 _surfaceView _nativePlayer.CallAndroidJavaObject(getRenderSurfaceView); // 5. 将SurfaceView添加到Unity的视图层级中这是关键且复杂的一步 AttachSurfaceViewToUnity(currentActivity); Debug.Log(Native player initialized successfully.); } catch (System.Exception e) { Debug.LogError(Failed to initialize native player: e.Message); } } // 开始播放 public void PlayStream(string url) { if (_nativePlayer ! null) { _nativePlayer.Call(play, url); } else { Debug.LogError(Native player is not initialized.); } } // 停止播放 public void StopPlay() { if (_nativePlayer ! null) { _nativePlayer.Call(stop); } } // Unity销毁时必须释放原生资源 void OnDestroy() { if (_nativePlayer ! null) { _nativePlayer.Call(release); _nativePlayer null; } DetachSurfaceViewFromUnity(); } // --- 以下是将Android SurfaceView嵌入Unity的关键方法 --- // 注意这里涉及Android的View系统和Unity的Native交互是高级主题。 // 一种常见做法是获取SurfaceView的SurfaceHolder然后将其与一个Unity的Texture2D或直接与渲染管线关联。 // 由于实现复杂且依赖于具体SDK的渲染输出方式此处仅提供概念性伪代码。 private void AttachSurfaceViewToUnity(AndroidJavaObject activity) { // 伪代码步骤 // 1. 获取Android的WindowManager和LayoutParams // 2. 创建一个FrameLayout作为容器将_surfaceView添加进去 // 3. 使用WindowManager将FrameLayout添加到窗口上并设置其位置、大小例如全屏或指定Rect // 4. 关键可能需要获取SurfaceView的SurfaceTexture或Surface并将其绑定到Unity的一个Material的纹理上。 // 大牛直播SDK可能提供了更直接的接口例如将解码后的数据帧以ByteBuffer形式回调允许你在Unity端直接生成Texture2D。 // **务必参考SDK的Unity集成示例或文档这是整个流程中最易出错的部分。** Debug.Log(AttachSurfaceViewToUnity needs to be implemented based on SDKs rendering method.); } private void DetachSurfaceViewFromUnity() { // 移除添加的View避免内存泄漏和窗口错误。 } }4.2 处理视频渲染与画面显示上面代码中留白的AttachSurfaceViewToUnity方法是集成的核心难点。大牛直播SDK通常提供几种渲染方式SurfaceView直接渲染到Android窗口这是最传统的方式。我们需要将SurfaceView作为一个Android控件添加到Unity的Activity窗口上。这需要操作Android的WindowManager并精确定位View的位置和大小以匹配Unity中某个UI区域如一个RawImage。这种方法画面流畅但交互如Unity的UI覆盖在视频上可能需要进行额外的层级管理。输出RGB/YUV数据到Unity更灵活的方式。在Java封装层设置SDK的回调每当有新视频帧解码完成就将图像数据字节数组通过JNI直接传递到C#端。C#端收到数据后使用Texture2D.LoadRawTextureData来更新一个纹理然后将这个纹理赋给RawImage或3D物体的材质。这种方式Unity对画面有完全的控制权但CPU到GPU的数据拷贝会有一定性能开销。使用OpenGL ES纹理共享这是最高性能的方式但实现最复杂。在原生层CSDK将解码后的视频帧渲染到一个OpenGL ES纹理上。然后通过JNI将这个纹理的ID一个int值传递到Unity。Unity端可以使用Texture2D.CreateExternalTexture来创建一个“外部纹理”并关联到这个纹理ID上从而实现零内存拷贝的纹理共享。这需要你熟悉Android NDK、OpenGL ES和Unity Native Plugin开发。对于大多数项目我建议先尝试与SDK供应商确认他们推荐的Unity集成方案。如果SDK提供了Unity插件包那通常已经封装好了最合适的渲染方式。如果只能拿到Android SDK那么方式2回调数据是一个在可控复杂度内能获得较好效果的选择。你需要仔细阅读SDK文档找到设置视频数据回调的方法。假设SDK支持设置onVideoFrameCallback你的Java封装类需要添加类似代码// 在initialize方法中 mPlayer.setVideoFrameCallback(new SmartPlayer.VideoFrameCallback() { Override public void onVideoFrame(byte[] data, int width, int height, int format) { // data: 视频帧数据 (可能是RGB24, NV21等格式) // width/height: 视频分辨率 // format: 数据格式标识 // 调用一个本地方法将数据传递给Unity nativeSendFrameToUnity(data, width, height, format); } }); // 声明一个本地Native方法通过JNI与C/C#通信 private native void nativeSendFrameToUnity(byte[] data, int width, int height, int format);然后在CJNI层实现这个nativeSendFrameToUnity函数将数据最终传递给一个C#的静态方法。C#端接收到数据后进行格式转换如NV21转RGB并更新Texture2D。5. 在Unity场景中配置与使用创建UI在Unity的Canvas下创建一个RawImage组件它将用于显示视频纹理。调整其大小和位置。挂载脚本创建一个空的GameObject命名为“StreamPlayerManager”。将AndroidStreamPlayer.cs脚本挂载上去。关联纹理如果采用上述“方式2”你需要在AndroidStreamPlayer脚本中声明一个public RawImage targetDisplay;并在Inspector面板中将上一步创建的RawImage对象拖拽赋值。调用播放你可以在另一个脚本中如UI按钮事件获取AndroidStreamPlayer组件并调用其PlayStream(“rtsp://your-stream-url”)方法。一个简单的UI控制器脚本示例using UnityEngine; using UnityEngine.UI; public class StreamController : MonoBehaviour { public AndroidStreamPlayer player; // 拖拽赋值 public InputField urlInputField; public Button playButton; public Button stopButton; void Start() { playButton.onClick.AddListener(OnPlayClicked); stopButton.onClick.AddListener(OnStopClicked); } void OnPlayClicked() { if (player ! null !string.IsNullOrEmpty(urlInputField.text)) { player.PlayStream(urlInputField.text); } } void OnStopClicked() { if (player ! null) { player.StopPlay(); } } }6. 构建、部署与真机调试构建APK在Unity中确保所有设置无误后点击File - Build Settings - Build生成APK文件。安装到设备使用USB数据线连接Android手机并开启USB调试模式。将APK安装到设备上。调试Logcat这是你最好的朋友。在Android Studio中打开Logcat工具选择你的设备过滤标签Tag为“Unity”或你自定义的标签如“UnitySmartPlayer”可以查看Unity和原生插件打印的所有日志这对于排查初始化失败、播放错误至关重要。Android Profiler如果遇到性能问题卡顿、发热使用Android Studio的Profiler工具监控CPU、内存和网络使用情况判断瓶颈是在解码、渲染还是数据传递环节。Unity Remote对于快速迭代UI布局可以使用Unity Remote App但它对原生插件调试帮助有限。7. 常见问题与排查技巧实录集成过程中你几乎一定会遇到下面这些问题。这里记录了我的排查实录问题1Unity打包后App启动立即崩溃Logcat显示java.lang.ClassNotFoundException。排查这通常意味着Unity没有正确打包你的AAR插件或其依赖。解决确认AAR文件放在了Assets/Plugins/Android/目录下且目录名大小写正确。检查AAR文件是否包含必要的AndroidManifest.xml如果缺少可能需要手动在Assets/Plugins/Android/下创建一个并合并必要的权限和组件声明。在Unity的Player Settings - Publishing Settings下勾选Custom Main Gradle Template和Custom Gradle Properties Template。这允许你编辑底层的Gradle构建脚本。在生成的mainTemplate.gradle文件中确保dependencies块里包含了你的AAR依赖。有时需要手动添加implementation files(libs/YourPlugin.aar)。确保你的Java封装类包名和路径完全正确没有拼写错误。问题2能初始化但调用play方法后黑屏没有画面也没有错误日志。排查这是最典型的问题根源几乎都在“渲染”环节。解决步骤确认流地址可播先用VLC播放器或SDK提供的原生Demo测试你的RTSP/RTMP流地址确保源本身是正常的。检查SurfaceView在Java代码中初始化后打印SurfaceView的宽高看是否为0。SurfaceView需要被添加到视图树并完成布局后才会有有效的Surface。确保AttachSurfaceViewToUnity方法被正确调用并且SurfaceView被添加到了Activity的ContentView中。检查数据回调如果采用数据回调方式在onVideoFrame回调里打印日志看是否被触发以及data的长度和width/height是否合理。如果没触发说明SDK没有成功解码或未设置回调。检查Unity端纹理更新在C#端确认接收数据的回调函数被执行并且Texture2D.Apply()被调用。检查Texture2D的格式是否与传入的数据格式匹配如TextureFormat.RGB24对应RGB数据。权限确保AndroidManifest.xml中声明了网络权限uses-permission android:nameandroid.permission.INTERNET /。问题3播放延迟很高超过3秒。排查延迟是流媒体播放的核心指标。解决调整SDK缓存像示例中setBufferTime(300)这个值单位毫秒决定了播放器为了抗网络抖动而缓冲的数据量。值越小延迟越低但越容易卡顿值越大越流畅但延迟越高。对于实时监控可以尝试设置为100-200ms。启用硬解码确保setHardwareDecoder(true)被调用。软解码CPU解码的延迟和功耗通常远高于硬解码GPU/专用芯片解码。检查网络路径Wi-Fi还是4G/5G网络本身的RTT往返延迟会直接影响首屏时间和实时性。在同一个局域网内测试以排除公网问题。流服务器配置有些RTSP服务器如某些安防摄像头默认配置了较大的GOP关键帧间隔导致首屏打开慢。尝试调整服务器配置减少关键帧间隔。问题4播放一段时间后内存持续增长最终OOMOut Of Memory崩溃。排查这是典型的内存泄漏。解决严格管理生命周期确保每次退出播放界面或销毁GameObject时都调用了原生播放器的release()方法。在Unity的OnDestroy、OnApplicationPause等生命周期函数中妥善处理。检查回调引用在C#端如果你注册了来自Java层的事件回调通过UnitySendMessage或JNI确保在销毁时解除注册否则Java层会持有对C#对象的引用导致无法被垃圾回收。纹理管理如果你在每一帧都创建新的Texture2D来更新画面务必在下一帧开始时销毁旧的纹理Destroy(oldTexture)或者复用同一个纹理对象。问题5在Unity编辑器中运行正常打APK到真机后功能失效。排查编辑器环境是x86/x64架构而真机是ARM架构。问题通常出在原生库.so文件上。解决检查AAR文件中是否包含了ARM架构armeabi-v7a, arm64-v8a的.so文件。可以用解压软件打开AAR查看jni/目录。在Unity的Player Settings - Android - Other Settings - Configuration中检查Scripting Backend是否为IL2CPP以及Target Architectures是否勾选了ARMv7和ARM64。如果SDK提供了单独的.so文件确保它们被正确放置在了Assets/Plugins/Android/libs/[abi]/对应的目录下。集成第三方原生SDK到Unity是一个系统工程考验的是你对两个平台的理解和解决问题的能力。从理清架构开始步步为营善用日志工具大部分问题都能被定位和解决。当你看到来自网络摄像头的实时画面稳定地出现在自己Unity制作的3D场景中时那种成就感会让你觉得这一切的折腾都是值得的。
返回列表