本文为作者原创:https://www.cnblogs.com/leisure_chn/p/10040202.html,转载请注明出处

基于 FFmpeg 和 SDL 实现的简易视频播放器,主要分为读取视频文件解码和调用 SDL 播放两大部分。

本实验仅实现最简单的视频播放流程,不考虑细节,不考虑音频。本实验主要参考如下两篇文章:

[1]. 最简单的基于FFMPEG+SDL的视频播放器ver2(采用SDL2.0)

[2]. An ffmpeg and SDL Tutorial

FFmpeg 简易播放器系列文章如下:

[1]. FFmpeg简易播放器的实现1-最简版

[2]. FFmpeg简易播放器的实现2-视频播放

[3]. FFmpeg简易播放器的实现3-音频播放

[4]. FFmpeg简易播放器的实现4-音视频播放

[5]. FFmpeg简易播放器的实现5-音视频同步

1. 视频播放器基本原理

下图引用自 “雷霄骅,视音频编解码技术零基础学习方法”,因原图太小,看不太清楚,故重新制作了一张图片。



如下内容引用自 “雷霄骅,视音频编解码技术零基础学习方法”:

解协议

将流媒体协议的数据,解析为标准的相应的封装格式数据。视音频在网络上传播的时候,常常采用各种流媒体协议,例如 HTTP,RTMP,或是 MMS 等等。这些协议在传输视音频数据的同时,也会传输一些信令数据。这些信令数据包括对播放的控制(播放,暂停,停止),或者对网络状态的描述等。解协议的过程中会去除掉信令数据而只保留视音频数据。例如,采用 RTMP 协议传输的数据,经过解协议操作后,输出 FLV 格式的数据。

解封装

将输入的封装格式的数据,分离成为音频流压缩编码数据和视频流压缩编码数据。封装格式种类很多,例如 MP4,MKV,RMVB,TS,FLV,AVI 等等,它的作用就是将已经压缩编码的视频数据和音频数据按照一定的格式放到一起。例如,FLV 格式的数据,经过解封装操作后,输出 H.264 编码的视频码流和 AAC 编码的音频码流。

解码

将视频/音频压缩编码数据,解码成为非压缩的视频/音频原始数据。音频的压缩编码标准包含 AAC,MP3,AC-3 等等,视频的压缩编码标准则包含 H.264,MPEG2,VC-1 等等。解码是整个系统中最重要也是最复杂的一个环节。通过解码,压缩编码的视频数据输出成为非压缩的颜色数据,例如 YUV420P,RGB 等等;压缩编码的音频数据输出成为非压缩的音频抽样数据,例如 PCM 数据。

音视频同步

根据解封装模块处理过程中获取到的参数信息,同步解码出来的视频和音频数据,并将视频音频数据送至系统的显卡和声卡播放出来。

2. 最简播放器的实现

2.1 实验平台

  1. 实验平台: openSUSE Leap 42.3
  2. FFmpeg 版本:4.1
  3. SDL 版本: 2.0.9

FFmpeg 开发环境搭建可参考 “FFmpeg开发环境构建

2.2 源码清单

  1. /*****************************************************************
  2. * ffplayer.c
  3. *
  4. * history:
  5. * 2018-11-27 - [lei] created file
  6. *
  7. * details:
  8. * A simple ffmpeg player.
  9. *
  10. * refrence:
  11. * 1. https://blog.csdn.net/leixiaohua1020/article/details/38868499
  12. * 2. http://dranger.com/ffmpeg/ffmpegtutorial_all.html#tutorial01.html
  13. * 3. http://dranger.com/ffmpeg/ffmpegtutorial_all.html#tutorial02.html
  14. ******************************************************************/
  15. #include <stdio.h>
  16. #include <libavcodec/avcodec.h>
  17. #include <libavformat/avformat.h>
  18. #include <libswscale/swscale.h>
  19. #include <SDL2/SDL.h>
  20. #include <SDL2/SDL_video.h>
  21. #include <SDL2/SDL_render.h>
  22. #include <SDL2/SDL_rect.h>
  23. int main(int argc, char *argv[])
  24. {
  25. // Initalizing these to NULL prevents segfaults!
  26. AVFormatContext* p_fmt_ctx = NULL;
  27. AVCodecContext* p_codec_ctx = NULL;
  28. AVCodecParameters* p_codec_par = NULL;
  29. AVCodec* p_codec = NULL;
  30. AVFrame* p_frm_raw = NULL; // 帧,由包解码得到原始帧
  31. AVFrame* p_frm_yuv = NULL; // 帧,由原始帧色彩转换得到
  32. AVPacket* p_packet = NULL; // 包,从流中读出的一段数据
  33. struct SwsContext* sws_ctx = NULL;
  34. int buf_size;
  35. uint8_t* buffer = NULL;
  36. int i;
  37. int v_idx;
  38. int ret;
  39. SDL_Window* screen;
  40. SDL_Renderer* sdl_renderer;
  41. SDL_Texture* sdl_texture;
  42. SDL_Rect sdl_rect;
  43. if (argc < 2)
  44. {
  45. printf("Please provide a movie file\n");
  46. return -1;
  47. }
  48. // 初始化libavformat(所有格式),注册所有复用器/解复用器
  49. // av_register_all(); // 已被申明为过时的,直接不再使用即可
  50. // A1. 打开视频文件:读取文件头,将文件格式信息存储在"fmt context"中
  51. ret = avformat_open_input(&p_fmt_ctx, argv[1], NULL, NULL);
  52. if (ret != 0)
  53. {
  54. printf("avformat_open_input() failed\n");
  55. return -1;
  56. }
  57. // A2. 搜索流信息:读取一段视频文件数据,尝试解码,将取到的流信息填入pFormatCtx->streams
  58. // p_fmt_ctx->streams是一个指针数组,数组大小是pFormatCtx->nb_streams
  59. ret = avformat_find_stream_info(p_fmt_ctx, NULL);
  60. if (ret < 0)
  61. {
  62. printf("avformat_find_stream_info() failed\n");
  63. return -1;
  64. }
  65. // 将文件相关信息打印在标准错误设备上
  66. av_dump_format(p_fmt_ctx, 0, argv[1], 0);
  67. // A3. 查找第一个视频流
  68. v_idx = -1;
  69. for (i=0; i<p_fmt_ctx->nb_streams; i++)
  70. {
  71. if (p_fmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
  72. {
  73. v_idx = i;
  74. printf("Find a video stream, index %d\n", v_idx);
  75. break;
  76. }
  77. }
  78. if (v_idx == -1)
  79. {
  80. printf("Cann't find a video stream\n");
  81. return -1;
  82. }
  83. // A5. 为视频流构建解码器AVCodecContext
  84. // A5.1 获取解码器参数AVCodecParameters
  85. p_codec_par = p_fmt_ctx->streams[v_idx]->codecpar;
  86. // A5.2 获取解码器
  87. p_codec = avcodec_find_decoder(p_codec_par->codec_id);
  88. if (p_codec == NULL)
  89. {
  90. printf("Cann't find codec!\n");
  91. return -1;
  92. }
  93. // A5.3 构建解码器AVCodecContext
  94. // A5.3.1 p_codec_ctx初始化:分配结构体,使用p_codec初始化相应成员为默认值
  95. p_codec_ctx = avcodec_alloc_context3(p_codec);
  96. // A5.3.2 p_codec_ctx初始化:p_codec_par ==> p_codec_ctx,初始化相应成员
  97. ret = avcodec_parameters_to_context(p_codec_ctx, p_codec_par);
  98. if (ret < 0)
  99. {
  100. printf("avcodec_parameters_to_context() failed %d\n", ret);
  101. return -1;
  102. }
  103. // A5.3.3 p_codec_ctx初始化:使用p_codec初始化p_codec_ctx,初始化完成
  104. ret = avcodec_open2(p_codec_ctx, p_codec, NULL);
  105. if (ret < 0)
  106. {
  107. printf("avcodec_open2() failed %d\n", ret);
  108. return -1;
  109. }
  110. // A6. 分配AVFrame
  111. // A6.1 分配AVFrame结构,注意并不分配data buffer(即AVFrame.*data[])
  112. p_frm_raw = av_frame_alloc();
  113. p_frm_yuv = av_frame_alloc();
  114. // A6.2 为AVFrame.*data[]手工分配缓冲区,用于存储sws_scale()中目的帧视频数据
  115. // p_frm_raw的data_buffer由av_read_frame()分配,因此不需手工分配
  116. // p_frm_yuv的data_buffer无处分配,因此在此处手工分配
  117. buf_size = av_image_get_buffer_size(AV_PIX_FMT_YUV420P,
  118. p_codec_ctx->width,
  119. p_codec_ctx->height,
  120. 1
  121. );
  122. // buffer将作为p_frm_yuv的视频数据缓冲区
  123. buffer = (uint8_t *)av_malloc(buf_size);
  124. // 使用给定参数设定p_frm_yuv->data和p_frm_yuv->linesize
  125. av_image_fill_arrays(p_frm_yuv->data, // dst data[]
  126. p_frm_yuv->linesize, // dst linesize[]
  127. buffer, // src buffer
  128. AV_PIX_FMT_YUV420P, // pixel format
  129. p_codec_ctx->width, // width
  130. p_codec_ctx->height, // height
  131. 1 // align
  132. );
  133. // A7. 初始化SWS context,用于后续图像转换
  134. // 此处第6个参数使用的是FFmpeg中的像素格式,对比参考注释B4
  135. // FFmpeg中的像素格式AV_PIX_FMT_YUV420P对应SDL中的像素格式SDL_PIXELFORMAT_IYUV
  136. // 如果解码后得到图像的不被SDL支持,不进行图像转换的话,SDL是无法正常显示图像的
  137. // 如果解码后得到图像的能被SDL支持,则不必进行图像转换
  138. // 这里为了编码简便,统一转换为SDL支持的格式AV_PIX_FMT_YUV420P==>SDL_PIXELFORMAT_IYUV
  139. sws_ctx = sws_getContext(p_codec_ctx->width, // src width
  140. p_codec_ctx->height, // src height
  141. p_codec_ctx->pix_fmt, // src format
  142. p_codec_ctx->width, // dst width
  143. p_codec_ctx->height, // dst height
  144. AV_PIX_FMT_YUV420P, // dst format
  145. SWS_BICUBIC, // flags
  146. NULL, // src filter
  147. NULL, // dst filter
  148. NULL // param
  149. );
  150. // B1. 初始化SDL子系统:缺省(事件处理、文件IO、线程)、视频、音频、定时器
  151. if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER))
  152. {
  153. printf("SDL_Init() failed: %s\n", SDL_GetError());
  154. return -1;
  155. }
  156. // B2. 创建SDL窗口,SDL 2.0支持多窗口
  157. // SDL_Window即运行程序后弹出的视频窗口,同SDL 1.x中的SDL_Surface
  158. screen = SDL_CreateWindow("Simplest ffmpeg player's Window",
  159. SDL_WINDOWPOS_UNDEFINED,// 不关心窗口X坐标
  160. SDL_WINDOWPOS_UNDEFINED,// 不关心窗口Y坐标
  161. p_codec_ctx->width,
  162. p_codec_ctx->height,
  163. SDL_WINDOW_OPENGL
  164. );
  165. if (screen == NULL)
  166. {
  167. printf("SDL_CreateWindow() failed: %s\n", SDL_GetError());
  168. return -1;
  169. }
  170. // B3. 创建SDL_Renderer
  171. // SDL_Renderer:渲染器
  172. sdl_renderer = SDL_CreateRenderer(screen, -1, 0);
  173. // B4. 创建SDL_Texture
  174. // 一个SDL_Texture对应一帧YUV数据,同SDL 1.x中的SDL_Overlay
  175. // 此处第2个参数使用的是SDL中的像素格式,对比参考注释A7
  176. // FFmpeg中的像素格式AV_PIX_FMT_YUV420P对应SDL中的像素格式SDL_PIXELFORMAT_IYUV
  177. sdl_texture = SDL_CreateTexture(sdl_renderer,
  178. SDL_PIXELFORMAT_IYUV,
  179. SDL_TEXTUREACCESS_STREAMING,
  180. p_codec_ctx->width,
  181. p_codec_ctx->height);
  182. sdl_rect.x = 0;
  183. sdl_rect.y = 0;
  184. sdl_rect.w = p_codec_ctx->width;
  185. sdl_rect.h = p_codec_ctx->height;
  186. p_packet = (AVPacket *)av_malloc(sizeof(AVPacket));
  187. // A8. 从视频文件中读取一个packet
  188. // packet可能是视频帧、音频帧或其他数据,解码器只会解码视频帧或音频帧,非音视频数据并不会被
  189. // 扔掉、从而能向解码器提供尽可能多的信息
  190. // 对于视频来说,一个packet只包含一个frame
  191. // 对于音频来说,若是帧长固定的格式则一个packet可包含整数个frame,
  192. // 若是帧长可变的格式则一个packet只包含一个frame
  193. while (av_read_frame(p_fmt_ctx, p_packet) == 0)
  194. {
  195. if (p_packet->stream_index == v_idx) // 仅处理视频帧
  196. {
  197. // A9. 视频解码:packet ==> frame
  198. // A9.1 向解码器喂数据,一个packet可能是一个视频帧或多个音频帧,此处音频帧已被上一句滤掉
  199. ret = avcodec_send_packet(p_codec_ctx, p_packet);
  200. if (ret != 0)
  201. {
  202. printf("avcodec_send_packet() failed %d\n", ret);
  203. return -1;
  204. }
  205. // A9.2 接收解码器输出的数据,此处只处理视频帧,每次接收一个packet,将之解码得到一个frame
  206. ret = avcodec_receive_frame(p_codec_ctx, p_frm_raw);
  207. if (ret != 0)
  208. {
  209. printf("avcodec_receive_frame() failed %d\n", ret);
  210. return -1;
  211. }
  212. // A10. 图像转换:p_frm_raw->data ==> p_frm_yuv->data
  213. // 将源图像中一片连续的区域经过处理后更新到目标图像对应区域,处理的图像区域必须逐行连续
  214. // plane: 如YUV有Y、U、V三个plane,RGB有R、G、B三个plane
  215. // slice: 图像中一片连续的行,必须是连续的,顺序由顶部到底部或由底部到顶部
  216. // stride/pitch: 一行图像所占的字节数,Stride=BytesPerPixel*Width+Padding,注意对齐
  217. // AVFrame.*data[]: 每个数组元素指向对应plane
  218. // AVFrame.linesize[]: 每个数组元素表示对应plane中一行图像所占的字节数
  219. sws_scale(sws_ctx, // sws context
  220. (const uint8_t *const *)p_frm_raw->data, // src slice
  221. p_frm_raw->linesize, // src stride
  222. 0, // src slice y
  223. p_codec_ctx->height, // src slice height
  224. p_frm_yuv->data, // dst planes
  225. p_frm_yuv->linesize // dst strides
  226. );
  227. // B5. 使用新的YUV像素数据更新SDL_Rect
  228. SDL_UpdateYUVTexture(sdl_texture, // sdl texture
  229. &sdl_rect, // sdl rect
  230. p_frm_yuv->data[0], // y plane
  231. p_frm_yuv->linesize[0], // y pitch
  232. p_frm_yuv->data[1], // u plane
  233. p_frm_yuv->linesize[1], // u pitch
  234. p_frm_yuv->data[2], // v plane
  235. p_frm_yuv->linesize[2] // v pitch
  236. );
  237. // B6. 使用特定颜色清空当前渲染目标
  238. SDL_RenderClear(sdl_renderer);
  239. // B7. 使用部分图像数据(texture)更新当前渲染目标
  240. SDL_RenderCopy(sdl_renderer, // sdl renderer
  241. sdl_texture, // sdl texture
  242. NULL, // src rect, if NULL copy texture
  243. &sdl_rect // dst rect
  244. );
  245. // B8. 执行渲染,更新屏幕显示
  246. SDL_RenderPresent(sdl_renderer);
  247. // B9. 控制帧率为25FPS,此处不够准确,未考虑解码消耗的时间
  248. SDL_Delay(40);
  249. }
  250. av_packet_unref(p_packet);
  251. }
  252. SDL_Quit();
  253. sws_freeContext(sws_ctx);
  254. av_free(buffer);
  255. av_frame_free(&p_frm_yuv);
  256. av_frame_free(&p_frm_raw);
  257. avcodec_close(p_codec_ctx);
  258. avformat_close_input(&p_fmt_ctx);
  259. return 0;
  260. }

源码清单中涉及的一些概念简述如下:

container:

容器,也称封装器,对应数据结构 AVFormatContext。封装是指将流数据组装为指定格式的文件。封装格式有 AVI、MP4 等。FFmpeg 可识别五种流类型:视频 video(v)、音频 audio(a)、attachment(t)、数据 data(d)、字幕 subtitle。

codec:

编解码器,对应数据结构 AVCodec。编码器将未压缩的原始图像或音频数据编码为压缩数据。解码器与之相反。

codec context:

编解码器上下文,对应数据结构 AVCodecContext。此为非常重要的一个数据结构,后文分析。各API大量使用 AVCodecContext 来引用编解码器。

codec par:

编解码器参数,对应数据结构 AVCodecParameters。新版本增加的字段。新版本建议使用 AVStream->codepar 替代 AVStream->codec。

packet:

经过编码的数据包,对应数据结构 AVPacket。通过 av_read_frame() 从媒体文件中获取得到的一个 packet 可能包含多个(整数个)音频帧或单个视频帧,或者其他类型的流数据。

frame:

未编码的原始数据帧,对应数据结构 AVFrame。解码器将 packet 解码后生成 frame。

plane:

如 YUV 有 Y、U、V 三个 plane,RGB 有 R、G、B 三个 plane。

slice:

图像中一片连续的行,必须是连续的,顺序由顶部到底部或由底部到顶部

stride/pitch:

一行图像所占的字节数,Stride = BytesPerPixel × Width,按 x 字节对齐[待确认]

sdl window:

播放视频时弹出的窗口,对应数据结构SDL_Window。在 SDL1.x 版本中,只可以创建一个窗口。在 SDL2.0 版本中,可以创建多个窗口。

sdl texture:

对应数据结构 SDL_Texture。一个SDL_Texture对应一帧解码后的图像数据。

sdl renderer:

渲染器,对应数据结构SDL_Renderer。将 SDL_Texture 渲染至 SDL_Window。

sdl rect:

对应数据结构 SDL_Rect,SDL_Rect 用于确定 SDL_Texture 显示的位置。一个 SDL_Window 上可以显示多个 SDL_Rect。这样可以实现同一窗口的分屏显示。

2.3 源码流程简述

流程比较简单,不画流程图了,简述如下:

  1. media file --[decode]--> raw frame --[scale]--> yuv frame --[SDL]--> display
  2. media file ------------> p_frm_raw -----------> p_frm_yuv ---------> sdl_renderer

加上相关关键函数后,流程如下:

  1. media_file ---[av_read_frame()]----------->
  2. p_packet ---[avcodec_send_packet()]----->
  3. decoder ---[avcodec_receive_frame()]--->
  4. p_frm_raw ---[sws_scale()]--------------->
  5. p_frm_yuv ---[SDL_UpdateYUVTexture()]---->
  6. display

2.3.1 初始化

初始化解码及显示环境。

2.3.2 读取视频数据

调用 av_read_frame() 从输入文件中读取视频数据包。

2.3.3 视频数据解码

调用 avcodec_send_packet() 和 avcodec_receive_frame() 对视频数据解码。

2.3.4 图像格式转换

图像格式转换的目的,是为了解码后的视频帧能被 SDL 正常显示。因为 FFmpeg 解码后得到的图像格式不一定就能被SDL支持,这种情况下不作图像转换是无法正常显示的。

2.3.5 显示

调用 SDL 相关函数将图像在屏幕上显示。

3. 编译与验证

3.1 编译

  1. gcc -o ffplayer ffplayer.c -lavutil -lavformat -lavcodec -lavutil -lswscale -lSDL2

3.2 验证

选用 bigbuckbunny_480x272.h265 测试文件,测试文件下载(右键另存为):bigbuckbunny_480x272.h265

运行测试命令:

  1. ./ffplayer bigbuckbunny_480x272.h265

4. 参考资料

[1] 雷霄骅,视音频编解码技术零基础学习方法

[2] 雷霄骅,FFmpeg源代码简单分析:常见结构体的初始化和销毁(AVFormatContext,AVFrame等)

[3] 雷霄骅,最简单的基于FFMPEG+SDL的视频播放器ver2(采用SDL2.0)

[4] Martin Bohme, An ffmpeg and SDL Tutorial, Tutorial 01: Making Screencaps

[5] Martin Bohme, An ffmpeg and SDL Tutorial, Tutorial 02: Outputting to the Screen

[6] YUV图像里的stride和plane的解释

[7] 图文详解YUV420数据格式

[8] YUVhttps://zh.wikipedia.org/wiki/YUV

5. 修改记录

2018-11-23 V1.0 初稿

2018-11-29 V1.1 增加定时刷新线程,使解码帧率更加准确

FFmpeg简易播放器的实现-最简版的更多相关文章

  1. FFmpeg简易播放器的实现-视频播放

    本文为作者原创:https://www.cnblogs.com/leisure_chn/p/10047035.html,转载请注明出处 基于FFmpeg和SDL实现的简易视频播放器,主要分为读取视频文 ...

  2. FFmpeg简易播放器的实现-音视频同步

    本文为作者原创,转载请注明出处:https://www.cnblogs.com/leisure_chn/p/10284653.html 基于FFmpeg和SDL实现的简易视频播放器,主要分为读取视频文 ...

  3. FFmpeg简易播放器的实现-音视频播放

    本文为作者原创,转载请注明出处:https://www.cnblogs.com/leisure_chn/p/10235926.html 基于FFmpeg和SDL实现的简易视频播放器,主要分为读取视频文 ...

  4. FFmpeg简易播放器的实现-音频播放

    本文为作者原创,转载请注明出处:https://www.cnblogs.com/leisure_chn/p/10068490.html 基于FFmpeg和SDL实现的简易视频播放器,主要分为读取视频文 ...

  5. 基于ffmpeg网络播放器的教程与总结

    基于ffmpeg网络播放器的教程与总结   一.         概述 为了解决在线无广告播放youku网上的视频.(youku把每个视频切换成若干个小视频). 视频资源解析可以从www.flvcd. ...

  6. <Win32_17>集音频和视频播放功能于一身的简易播放器

    前段时间,在学习中科院杨老师的教学视频时,他说了一句话: "我很反对百八十行的教学程序,要来就来一个完整的程序" 对此,我很是赞同.所谓真刀真枪的做了,你才会发现其中的奥秘——然而 ...

  7. PotPlayer播放器 莫尼卡汉化绿色版 V1.6.48089 32位

    软件名称: PotPlayer播放器 莫尼卡汉化绿色版 软件语言: 简体中文 授权方式: 免费软件 运行环境: Win7 / Vista / Win2003 / WinXP 软件大小: 10.5MB ...

  8. HTML5播放器FlowPlayer的极简风格效果

    在线演示 本地下载 使用Flowplayer生成的极简风格的播放器效果.

  9. ffmpeg+SDl+ 播放器 -01

    最近因公司项目需要,打算自己在LINUX平台整一个播放器,来学习和研究音频编解码. 项目需求: 支持下列格式文件播放. 1> WMA 硬件解码,但需要软件分析ASF格式,提取Payload数据 ...

随机推荐

  1. [代码优化]PHP代码优化

    // Code snippet 1 $u_id = Hnb_Session::getInstance()->getUserID(); $arr_joinedTribeInfo = array() ...

  2. cxGrid实现取消过滤和排序后定位到首行(单选和多选)

    cxGrid实现取消过滤和排序后定位到首行(单选和多选) 原创 2013年10月06日 18:42:24 2107 DataContoller中的函数FocusedRecordIndex没有反应,Fo ...

  3. 使用docker部署WordPress博客系统(win10企业版)

    docker介绍: docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的Linux机器上,也可以实现虚拟化,容器是完全使用沙箱机制,相 ...

  4. 轻量级Config文件AppSettings节点编辑帮助类

    using System.Configuration; using System.Windows.Forms; namespace Allyn.Common { public class XmlHep ...

  5. ionic 2.x 3.x input触发调用键盘搜索及事件

    html (1.input  type='search'   2.将input套在一个带action的form中 ) <form action=""> <ion- ...

  6. [leetcode.com]算法题目 - Decode Ways

    A message containing letters from A-Z is being encoded to numbers using the following mapping: 'A' - ...

  7. 分布式锁实现思路及开源项目集成到springmvc并使用

    分布式锁顾名思义就是在分布式系统下的锁,而使用锁的唯一目的就是为了防止多个请求同时对某一个资源进行竞争性读写 在使用多线程时,为了让某一资源某一时刻只能有一个操作者,经常使用synchronized, ...

  8. D16——C语言基础学PYTHON

    C语言基础学习PYTHON——基础学习D16 20180927内容纲要: 1.JavaScript介绍 2.JavaScript功能介绍 3.JavaScript变量 4.Dom操作 a.获取标签 b ...

  9. python学习笔记11-文件操作方法

    f=open("1.txt","r",encoding='utf-8') # a=f.readline() print(a) #光标会移动 下面两者结果不一样 ...

  10. iOS-项目创建多个target

    在开发中,有时需要两个或多个APP版本,每个版本的改动,不是很多,但是需要另外打包,那么我们就有两套方案: 1.重新开发,把代码复制一遍,然后在修改: 2.用一套代码,根据需求生成不同的包: 我们一般 ...