Android APP 音视频(02)MediaProjection录屏与MediaCodec编码
说明: 此MediaProjection 录屏和编码实操主要针对Android12.0系统。通过MediaProjection获取屏幕数据,将数据通过mediacodec编码输出H264码流(使用ffmpeg播放),存储到sd卡上。
1 MediaProjection录屏与编码简介
这里主要是使用MediaProjection获取屏幕数据,将数据通过mediacodec编码输出到存储卡上。这里主要介绍 MediaProjection的基本原理和流程、 MediaCodec编码的简单说明,便于对代码有所理解。
1.1 MediaProjection录屏原理和流程
MediaProjection 是 Android 提供的一个用于屏幕捕捉和屏幕录制的功能,它允许应用程序在获得用户授权的情况下捕获设备屏幕的内容。这项技术自 Android 5.0(Lollipop)起引入,并在之后的版本中得到广泛应用和发展。
MediaProjection 的主要组件包括:
- MediaProjectionManager:系统服务,用于创建和管理 MediaProjection 会话。
- MediaProjection:表示屏幕捕获会话的令牌,通过用户的授权获得。
- VirtualDisplay:一个虚拟的显示设备,它可以捕获屏幕内容并将其渲染到指定的 Surface 上。
录屏功能的实现流程如下:
- 权限申请:APP需要请求用户授权使用屏幕录制功能。这会涉及
AndroidManifest.xml
文件的修改以及添加必要的权限,如WRITE_EXTERNAL_STORAGE
和RECORD_AUDIO
。 - 触发用户授权:通过
MediaProjectionManager
创建一个 Intent 来触发系统的屏幕录制授权界面。用户同意授权后,应用程序可以在onActivityResult
中接收到结果。 - 获取 MediaProjection 实例:如果用户授权成功,则可以通过
MediaProjectionManager
的getMediaProjection()
方法获取一个MediaProjection
实例 。 - 创建 VirtualDisplay:使用
MediaProjection
实例创建VirtualDisplay
,它将捕获屏幕内容并将其显示在 Surface 上。 - 开始录制:调用
MediaRecorder
的start()
方法开始录制屏幕内容。 - 结束录制:录制完成后,调用
MediaRecorder
的stop()
和reset()
方法停止录制并重置MediaRecorder
状态,然后释放VirtualDisplay
资源。
MediaProjection 录屏的原理主要是通过系统授权,捕获屏幕内容并利用虚拟显示设备将内容渲染到录制器上,实现屏幕录制的功能。开发者在使用时需要考虑到用户授权、资源管理和异常处理等关键步骤 。
1.2 MediaCodec编码说明
MediaCodec 是 Android 提供的一个音视频编解码器类,允许应用程序对音频和视频数据进行编码(压缩)和解码(解压缩)。它在 Android 4.1(API 级别 16)版本中引入,广泛应用于处理音视频数据,如播放视频、录制音频等。
以下是 MediaCodec 编码的基本步骤:
-
创建 MediaCodec 实例:通过调用
MediaCodec.createEncoderByType
方法并传入编码类型(如 "video/avc" 或 "audio/mp4a-latm")来创建编码器。 -
配置编码参数:通过调用
configure
方法配置编码器,传入编码参数如比特率、帧率、编码格式等。 -
准备输入和输出 Surface:为编码器准备输入和输出 Surface。输入 Surface 用于传递待编码的数据,输出 Surface 用于接收编码后的数据。
-
开始编码:调用
start
方法启动编码器。 -
发送输入数据:将待编码的数据通过
write
方法发送到编码器的输入队列。 -
处理输出数据:监听输出队列,通过
dequeueOutputBuffer
方法获取编码后的数据,并进行处理或存储。 -
停止编码:编码完成后,调用
stop
方法停止编码器。 -
释放资源:调用
release
方法释放编码器资源。
MediaCodec 支持处理三种数据类型:压缩数据、原始音频数据和原始视频数据。这些数据可以通过 ByteBuffer 传输给 MediaCodec 进行处理。对于原始视频数据,使用 Surface 作为输入源可以提高编解码器的性能。针对本工程,主要通过获得录屏的原始数据,通过mediacodec压缩成H264码流。
2 MediaProjection录屏与编码代码完整解读(android Q)
2.1 关于权限部分的处理
关于权限,需要在AndroidManifest.xml中添加权限,具体如下所示:
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"tools:ignore="ScopedStorage" /><uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"tools:ignore="ScopedStorage" /><uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"tools:ignore="ScopedStorage" /><uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
这里尤其要注意android.permission.FOREGROUND_SERVICE的添加。关于运行时权限的请求等,这里给出一个工具类参考代码,具体如下所示:
public class Permission {public static final int REQUEST_MANAGE_EXTERNAL_STORAGE = 1;//需要申请权限的数组private static final String[] permissions = {Manifest.permission.WRITE_EXTERNAL_STORAGE,Manifest.permission.READ_EXTERNAL_STORAGE,Manifest.permission.CAMERA};//保存真正需要去申请的权限private static final List<String> permissionList = new ArrayList<>();public static int RequestCode = 100;public static void requestManageExternalStoragePermission(Context context, Activity activity) {if (!Environment.isExternalStorageManager()) {showManageExternalStorageDialog(activity);}}private static void showManageExternalStorageDialog(Activity activity) {AlertDialog dialog = new AlertDialog.Builder(activity).setTitle("权限请求").setMessage("请开启文件访问权限,否则应用将无法正常使用。").setNegativeButton("取消", null).setPositiveButton("确定", (dialogInterface, i) -> {Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);activity.startActivityForResult(intent, REQUEST_MANAGE_EXTERNAL_STORAGE);}).create();dialog.show();}public static void checkPermissions(Activity activity) {for (String permission : permissions) {if (ContextCompat.checkSelfPermission(activity, permission) != PackageManager.PERMISSION_GRANTED) {permissionList.add(permission);}}if (!permissionList.isEmpty()) {requestPermission(activity);}}public static void requestPermission(Activity activity) {ActivityCompat.requestPermissions(activity,permissionList.toArray(new String[0]),RequestCode);}
}
2.2 MediaProjection服务的添加
从 Android 12 开始,如果应用需要使用 MediaProjection
进行屏幕录制,必须将相关的服务声明为前台服务。这是因为屏幕录制涉及到用户隐私,因此系统需要确保用户明确知道该服务正在运行。需要在应用的 AndroidManifest.xml
文件中声明服务,并添加相应的权限(2.1中已经添加)和特性,具体编写参考如下:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"...><uses-permission android:name="android.permission.FOREGROUND_SERVICE" /><application ...><serviceandroid:name=".serviceset.MediaProjectionService"android:exported="true"android:foregroundServiceType="mediaProjection" /><!-- 其他组件声明 --></application>
</manifest>
添加这些后,接下来需要实现.serviceset.MediaProjectionService 的代码,具体如下所示:
public class MediaProjectionService extends Service {private MediaProjection mMediaProjection;public static int resultCode;public static Intent resultData;public static Notification notification;public static Context context;@Overridepublic void onCreate() {super.onCreate();startMediaProjectionForeground();}@Overridepublic int onStartCommand(Intent intent, int flags, int startId) {MediaProjectionManager mMediaProjectionManager = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);MediaProjection mediaProjection = mMediaProjectionManager.getMediaProjection(resultCode, resultData);H264EncoderThread h264EncoderThread = new H264EncoderThread(mediaProjection, 640, 1920);h264EncoderThread.start();return START_NOT_STICKY;}@Nullable@Overridepublic IBinder onBind(Intent intent) {return null;}private void startMediaProjectionForeground() {String channelId = "CHANNEL_ID_MEDIA_PROJECTION";NotificationManager NOTIFICATION_MANAGER = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this,channelId).setSmallIcon(R.mipmap.ic_launcher).setContentTitle("服务已启动");NotificationChannel channel = new NotificationChannel(channelId, "屏幕录制", NotificationManager.IMPORTANCE_HIGH);NOTIFICATION_MANAGER.createNotificationChannel(channel);notificationBuilder.setChannelId(channelId);Notification notification = notificationBuilder.build();startForeground(1, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION);}
}
2.3 编码的处理
关于编码部分,主要是MediaCodec的初始化、编码处理部分和文件写入操作,代码如下所示:
public class H264EncoderThread extends Thread{private MediaProjection mMediaProjection;MediaCodec mediaCodec;private final String TAG = "H264EncoderThread";public H264EncoderThread(MediaProjection mMediaProjection, int width, int height) {this.mMediaProjection = mMediaProjection;MediaFormat format = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, width, height);try {mediaCodec = MediaCodec.createEncoderByType("video/avc");format.setInteger(MediaFormat.KEY_FRAME_RATE, 20);format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 30);format.setInteger(MediaFormat.KEY_BIT_RATE, width * height);format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);mediaCodec.configure(format,null,null,CONFIGURE_FLAG_ENCODE);Surface surface= mediaCodec.createInputSurface();mMediaProjection.createVirtualDisplay("wangdsh-test", width, height, 2,DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC, surface, null, null);} catch (IOException e) {Log.e("TAG",e.toString());//e.printStackTrace();}}@Overridepublic void run() {super.run();mediaCodec.start();MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();while (true) {int outIndex = mediaCodec.dequeueOutputBuffer(info, 11000);if (outIndex >= 0) {ByteBuffer byteBuffer = mediaCodec.getOutputBuffer(outIndex);byte[] ba = new byte[byteBuffer.remaining()];byteBuffer.get(ba);FileUtils.writeBytes(ba);FileUtils.writeContent(ba);mediaCodec.releaseOutputBuffer(outIndex, false);}}}
}
其中涉及的FileUtils参考实现如下:
public class FileUtils {private static final String TAG = "FileUtils";public static void writeBytes(byte[] array) {FileOutputStream writer = null;try {writer = new FileOutputStream(Environment.getExternalStorageDirectory() + "/codecoutput.h264", true);writer.write(array);writer.write('\n');writer.close();} catch (IOException e) {e.printStackTrace();}}public static String writeContent(byte[] array) {char[] HEX_CHAR_TABLE = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};StringBuilder sb = new StringBuilder();for (byte b : array) {sb.append(HEX_CHAR_TABLE[(b & 0xf0) >> 4]);sb.append(HEX_CHAR_TABLE[b & 0x0f]);}Log.d(TAG, "writeContent-: " + sb.toString());try {FileWriter writer = new FileWriter(Environment.getExternalStorageDirectory() + "/codecH264.txt", true);writer.write(sb.toString());writer.write("\n");writer.close();} catch (IOException e) {e.printStackTrace();}return sb.toString();}
}
2.4 主流程代码参考实现
这里以 H264encoderMediaProjActivity为例,给出一个MediaProjection录屏与编码功能代码的参考实现。具体实现如下:
public class H264encoderMediaProjActivity extends AppCompatActivity {private MediaProjectionManager mMediaProjectionManager;Context mContext;private ActivityResultLauncher<Intent> screenCaptureLauncher;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);EdgeToEdge.enable(this);mContext = this;setContentView(R.layout.h264_encode_media_projection);ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);return insets;});Permission.checkPermissions(this);Permission.requestManageExternalStoragePermission(getApplicationContext(), this);mMediaProjectionManager = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);screenCaptureLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),result -> {if (result.getResultCode() == Activity.RESULT_OK) {Intent resultData = result.getData();MediaProjectionService.resultCode = result.getResultCode();MediaProjectionService.resultData = resultData;MediaProjectionService.context = mContext;Intent SERVICE_INTENT = new Intent(this, MediaProjectionService.class);startForegroundService(SERVICE_INTENT);}});Button mButton = findViewById(R.id.button);mButton.setOnClickListener(view -> {// 创建屏幕录制的 IntentIntent captureIntent = mMediaProjectionManager.createScreenCaptureIntent();// 启动屏幕录制请求screenCaptureLauncher.launch(captureIntent);});}
}
这里涉及的layout布局文件内容如下:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"android:id="@+id/main"tools:context=".MainActivity"><Buttonandroid:layout_width="match_parent"android:layout_height="50dp"android:text="@string/startLive"android:gravity="center"android:id="@+id/button"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintLeft_toLeftOf="parent"app:layout_constraintRight_toRightOf="parent"app:layout_constraintTop_toTopOf="parent" /></androidx.constraintlayout.widget.ConstraintLayout>
2.5 MediaProjection录屏与编码 demo实现效果
实际运行效果展示如下:
使用ffmpeg对码流进行播放,说明编码生成的码流是有效的,截图如下所示:
相关文章:

Android APP 音视频(02)MediaProjection录屏与MediaCodec编码
说明: 此MediaProjection 录屏和编码实操主要针对Android12.0系统。通过MediaProjection获取屏幕数据,将数据通过mediacodec编码输出H264码流(使用ffmpeg播放),存储到sd卡上。 1 MediaProjection录屏与编码简介 这里…...
java中log4j.properties配置文件浅析
Log4J的配置文件(Configuration File)就是用来设置记录器的级别、存放器和布局的,它可按keyvalue格式的设置或xml格式的设置信息。通过配置,可以创建出Log4J的运行环境。 1、配置文件 Log4J配置文件的基本格式如下: #配置根Logger log4j.roo…...

RV1126 Linux 系统,接外设,时好时坏(二)排查问题的常用命令
在 RV1126 Linux 系统中,排查外设连接问题时,可以使用多种命令来诊断和调试。以下是一些常用的命令和工具: 1. 查看系统日志 dmesg: 显示内核环形缓冲区的消息,通常包含设备初始化、驱动加载和错误等信息。 dmesg | grep <设备名或相关关键字>journalctl: 查看系统…...

鸿蒙北向开发 DevEco Studio 4.1 下载安装傻瓜式教程
开篇 由于鸿蒙处于快速发展中,鸿蒙的api快速迭代更新,老版本的DevEco studio无法支持更新版本的api,因此华为官网放弃了老版本的维护.直接从华为开发者官网无法下载老版本,当前华为开发者官网已经推出next版本了 DevEco studio3.1安装教程 上述教程提供的华为开发者官网地址已经…...
pglogical扩展的基本用法介绍
瀚高数据库 目录 环境 文档用途 详细信息 环境 系统平台:Linux x86-64 Red Hat Enterprise Linux 7 版本:14 文档用途 本文翻译了pglogical扩展的官方文档,介绍了pglogical扩展的各类管理函数及使用限制,详情请看下文. 一、节点管理 节点可以使用以下…...

2024年虚拟主机转移教程
转移网站并不困难,但选择正确的选项和最佳程序才是关键。网站托管服务被视为当今数字世界的基石,全球有18 亿个网站。网站所有者可以通过下载备份、将其上传到新服务器并指向域名来手动转移网站。他们还可以通过新网站托管商的助手请求来移动网站。对于初…...
Python 函数对象和函数调用
Python 函数对象和函数调用 在 Python 中,函数是第一类对象(first-class objects)。这意味着函数可以像其他对象(如整数、字符串、列表等)一样被传递、赋值和操作。理解函数对象和函数调用的区别是学习 Python 的关键…...

sql注入的专项练习 sqlilabs(含代码审计)
在做题之前先复习了数据库的增删改查,然后自己用本地的环境,在自己建的库里面进行了sql语句的测试,主要是回顾了一下sql注入联合注入查询的语句和sql注入的一般做题步骤。 1.获取当前数据库 2.获取数据库中的表 3.获取表中的字段名 一、sql…...

淄博网站建设贵不贵
淄博网站建设的价格因各种因素而异,它可能会根据您对网站的需求、功能和设计复杂性等方面的要求而有所不同。虽然淄博网站建设的费用可能因需求的不同而有所变化,但是无论如何,它通常是值得的投资。 首先,对于个人和小型企业来说&…...

【学习笔记】无人机系统(UAS)的连接、识别和跟踪(十)-无人机A2X服务
引言 3GPP TS 23.256 技术规范,主要定义了3GPP系统对无人机(UAV)的连接性、身份识别、跟踪及A2X(Aircraft-to-Everything)服务的支持。 3GPP TS 23.256 技术规范: 【免费】3GPPTS23.256技术报告-无人机系…...

基于迁移学习的手势分类模型训练
1、基本原理介绍 这里介绍的单指模型迁移。一般我们训练模型时,往往会自定义一个模型类,这个类中定义了神经网络的结构,训练时将数据集输入,从0开始训练;而迁移学习中(单指模型迁移策略)&#x…...

个性化音频生成GPT-SoVits部署使用和API调用
一、训练自己的音色模型步骤 1、准备好要训练的数据,放在Data文件夹中,按照文件模板中的结构进行存放数据 2、双击打开go-webui.bat文件,等待页面跳转 3、页面打开后,开始训练自己的模型 (1)、人声伴奏分…...

MFC列表框示例
本文仅供学习交流,严禁用于商业用途,如本文涉及侵权请及时联系本人将于及时删除 目录 1.示例内容 2.程序步骤 3.运行结果 4.代码全文 1.示例内容 编写一个对话框应用程序CMFC_Li6_4_学生信息Dlg,对话框中有一个列表框,当用户…...

Android TabLayout的简单用法
TabLayout 注意这里添加tab,使用binding.tabLayout.newTab()进行创建 private fun initTabs() {val tab binding.tabLayout.newTab()tab.text "模板库"binding.tabLayout.addTab(tab)binding.tabLayout.addOnTabSelectedListener(object : TabLayout.On…...
基于vite + pnpm monorepo 实现一个UI组件库
基于vite pnpm monorepo的vue组件库 仓库地址 思路 好多文章都是直接咔咔咔的上代码。跟着做也没问题,但总觉得少了些什么。下次做的时候还要找文章参考。。 需求有三个模块,那么就需要三个包。使用monorepo进行分包管理。 a. 组件库 b. 组件库文档…...

FDM3D打印系列——Luck13关节可动模型打印和各种材料的尝试
luck13可动关节模型FDM3D打印制作过程 大家好,我是阿赵。 最近我沉迷于打印一个叫做Luck13的关节超可动人偶。 首先说明一下,这个模型是分为了外甲和骨骼两个部分的。 为什么我会打印了这么多个呢? 一、第一次尝试——PLATPU 刚开始…...
windows10 获取磁盘类型
powershell Get-PhysicalDisk | Select FriendlyName, MediaType FriendlyName MediaType ------------ --------- NVMe PC SN740 NVMe WD 256GB SSD WDC WD10EZEX-75WN4A1 HDD 适用场景 SSD: 适合需要快速访问速度和较高响…...
数据库之运算符
目录 一、算数运算符 二、比较运算符 1.常用比较运算符 2.实现特殊功能的比较运算符 三、逻辑运算符 1.逻辑与运算符(&&或者AND) 2.逻辑或运算符(||或者OR) 3.逻辑非运算符(!或者NOT&#…...
【自动化机器学习AutoML】AutoML工具和平台的使用
自动化机器学习AutoML:AutoML工具和平台的使用 目录 引言什么是AutoMLAutoML的优势常见的AutoML工具和平台 Google Cloud AutoMLH2O.aiAuto-sklearnTPOTMLBox AutoML的基本使用 Google Cloud AutoML使用示例Auto-sklearn使用示例 AutoML的应用场景结论 引言 自动…...

【每日一练】python求最后一个单词的长度
""" 求某变量中最后一个单词的长度 例如s"Good morning, champ! Youre going to rock this day" 分析思路: 遇到字符串问题,经常和列表结合使用来解决, 可以先用列表的.split()分割方法进行单词分割, 再…...

Docker 离线安装指南
参考文章 1、确认操作系统类型及内核版本 Docker依赖于Linux内核的一些特性,不同版本的Docker对内核版本有不同要求。例如,Docker 17.06及之后的版本通常需要Linux内核3.10及以上版本,Docker17.09及更高版本对应Linux内核4.9.x及更高版本。…...
解锁数据库简洁之道:FastAPI与SQLModel实战指南
在构建现代Web应用程序时,与数据库的交互无疑是核心环节。虽然传统的数据库操作方式(如直接编写SQL语句与psycopg2交互)赋予了我们精细的控制权,但在面对日益复杂的业务逻辑和快速迭代的需求时,这种方式的开发效率和可…...
C++ 基础特性深度解析
目录 引言 一、命名空间(namespace) C 中的命名空间 与 C 语言的对比 二、缺省参数 C 中的缺省参数 与 C 语言的对比 三、引用(reference) C 中的引用 与 C 语言的对比 四、inline(内联函数…...
TRS收益互换:跨境资本流动的金融创新工具与系统化解决方案
一、TRS收益互换的本质与业务逻辑 (一)概念解析 TRS(Total Return Swap)收益互换是一种金融衍生工具,指交易双方约定在未来一定期限内,基于特定资产或指数的表现进行现金流交换的协议。其核心特征包括&am…...
LLM基础1_语言模型如何处理文本
基于GitHub项目:https://github.com/datawhalechina/llms-from-scratch-cn 工具介绍 tiktoken:OpenAI开发的专业"分词器" torch:Facebook开发的强力计算引擎,相当于超级计算器 理解词嵌入:给词语画"…...

成都鼎讯硬核科技!雷达目标与干扰模拟器,以卓越性能制胜电磁频谱战
在现代战争中,电磁频谱已成为继陆、海、空、天之后的 “第五维战场”,雷达作为电磁频谱领域的关键装备,其干扰与抗干扰能力的较量,直接影响着战争的胜负走向。由成都鼎讯科技匠心打造的雷达目标与干扰模拟器,凭借数字射…...
大学生职业发展与就业创业指导教学评价
这里是引用 作为软工2203/2204班的学生,我们非常感谢您在《大学生职业发展与就业创业指导》课程中的悉心教导。这门课程对我们即将面临实习和就业的工科学生来说至关重要,而您认真负责的教学态度,让课程的每一部分都充满了实用价值。 尤其让我…...
全面解析各类VPN技术:GRE、IPsec、L2TP、SSL与MPLS VPN对比
目录 引言 VPN技术概述 GRE VPN 3.1 GRE封装结构 3.2 GRE的应用场景 GRE over IPsec 4.1 GRE over IPsec封装结构 4.2 为什么使用GRE over IPsec? IPsec VPN 5.1 IPsec传输模式(Transport Mode) 5.2 IPsec隧道模式(Tunne…...

什么是Ansible Jinja2
理解 Ansible Jinja2 模板 Ansible 是一款功能强大的开源自动化工具,可让您无缝地管理和配置系统。Ansible 的一大亮点是它使用 Jinja2 模板,允许您根据变量数据动态生成文件、配置设置和脚本。本文将向您介绍 Ansible 中的 Jinja2 模板,并通…...

GitFlow 工作模式(详解)
今天再学项目的过程中遇到使用gitflow模式管理代码,因此进行学习并且发布关于gitflow的一些思考 Git与GitFlow模式 我们在写代码的时候通常会进行网上保存,无论是github还是gittee,都是一种基于git去保存代码的形式,这样保存代码…...