【投屏】Scrcpy源码分析三(Client篇-投屏阶段)
Scrcpy源码分析系列
【投屏】Scrcpy源码分析一(编译篇)
【投屏】Scrcpy源码分析二(Client篇-连接阶段)
【投屏】Scrcpy源码分析三(Client篇-投屏阶段)
【投屏】Scrcpy源码分析四(最终章 - Server篇)
前一篇我们探究了Scrcpy Client端连接阶段的逻辑,这一篇我们继续探究Client端的投屏阶段。
Client篇-投屏阶段
- 1. 音视频和FFmpeg
- 1.1 音视频基础
- 1.1.1 编码/解码
- 1.1.2 容器
- 1.1.3 音视频播放流程
- 1.2 FFmpeg
- 2. 投屏阶段
- 2.1 ``sc_screen_init`` - 对窗口进行初始化
- 2.2 `sc_demuxer_start` - 分流和解码
- 2.3 ``event_loop`` - 事件循环
- 2.4 ``sc_keyboard_inject_init`` & ``sc_mouse_inject_init`` - 键鼠事件
- 2.5 ``sc_controller_start`` - 事件的收发
- 2.6 时序图
- 3. 小结
1. 音视频和FFmpeg
因为投屏阶段用到了很多音视频编解码知识和FFmpeg相关的API,所以在继续分析代码之前,我们先简单快速地回顾一下这些内容。因FFmpeg功能很广,我们只介绍Scrcpy中用到的一部分。
1.1 音视频基础
1.1.1 编码/解码
编码(Encode)- 将一种音视频格式文件(通常是原始、未经压缩的)通过压缩技术转换成另一种格式文件。
解码(Decode)- 将压缩后的音视频格式文件还原成原始的音视频格式文件。
通常我们所说的编解码器(Codec),就是同时包含了编码和解码的能力。
编码的意义在于,未经压缩的原始类型,数据流是非常大的,不利于存储和网络传输,所以需要对其进行编码。常见的视频原始类型有YUV
、RAW
等,音频原始类型有PCM
。常见的视频编码类型有H264
、H265
等,音频编码类型有AAC
、MP3
。
1.1.2 容器
容器通常指包含了多路流的封装格式。比如一个容器内可以包含音频流、视频流、字幕流等,而对应音频流和视频流的数据格式就是音视频的编码类型。
混流/复用(mux)- 将多个流混合到一个容器中。
分流/解复用(demux)- 从一个容器中分解成多个流。
常见的容器有MP4、FLV、MKV、AVI。
1.1.3 音视频播放流程
音视频播放的流程通常是:
如果只需要音频或视频则,则混流/分流的过程可以省略。
上一篇有提到Scrcpy的原理的Android设备侧不断录屏、编码,将视频流传输给PC,PC进行解码和渲染,就是类似上述的过程。
Android设备的编码用的是MediaCodec硬编码,这个暂且不用太关注,我们只需要只要Android是将YUV原始数据,编码生成H264,通过video_socket传给PC。PC侧收到视频流后,通过FFmpeg进行解码并通过SDL渲染出来。
1.2 FFmpeg
FFmpeg是一套音视频开源软件,提供强大的音视频处理能力,应用广泛。其中最基础就是编解码的能力。
Scrcpy主要用到FFmpeg的解码能力,并且此文我们的重点还是Scrcpy,所以只是简单描述一下FFmpeg的解码需要用到的API,方便后续分析。
FFmpeg解码的关键流程如下(代码不完整,只需关注关键API):
int ffmpeg_decode() {// 注册所有编解码器avcodec_register_all();// 创建解码器,传入对应的解码器ID,比如这里是H264解码器AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264);// 分配AVCodecContext空间并初始化AVCodecContext *codecContext = avcodec_alloc_context3(codec);// 通过AVCodec对AVCodecContext进行初始化avcodec_open2(codecContext, codec, NULL);// 初始化AVCodecParserContextAVCodecParserContext *parserContext = av_parse_init(AV_CODEC_ID_H264);// 分配AVPacket空间AVPacket *avPacket = av_packet_alloc();// 分配AVFramen空间AVFrame *frame = av_frame_alloc();while(!eof(input)) {// 解析一个packetav_parser_parse2(parserContext, codecContext, &pkt->data, &pkt->size, data, (int)data_size, AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0);// 解码decode();}// 资源释放avcodec_free_context(&codecContext);av_parse_close(parseContext);av_frame_free(&frame);av_packet_free(&avPacket);
}int decode(AVCodecContext *codec_ctx, AVPacket *pkt, AVFrame *frame) {// 将packet送入解码器int ret = avcodec_send_packet(codec_ctx, pkt);while(ret >= 0) {// 从解码器中拿到解码后的帧数据ret = avcodec_receive_frame(codec_ctx, frame);// [TODO] 已经拿到帧数据frame->data}
}
上面是使用FFmpeg对H264进行视频解码的模板代码,主要有几个阶段:
- 初始化相关,此阶段需要创建
AVCodec
、AVCodecContext
、AVCodecParserContext
变量,并进行相关初始化。 - 对
AVPacket
和AVFrame
结构分配空间。AVPacket
是指经过编码之后的一个数据包,AVFrame
是解码后的一帧数据,视频中一帧代表一帧图片数据。 - 解码阶段,此阶段需要从输入源(文件或网络)解析一个packet,然后送入解码器解码,到到frame帧数据。
- 数据处理阶段,在拿到帧数据
AVFrame->data
后,可以根据业务需要对数据进行处理。
Scrcpy中使用FFmpeg进行解码流程也大致如上。
2. 投屏阶段
上回说到scrcpy()
里的await_for_server()
函数,这个函数内部在等待SDL事件,在收到连接成功的事件之后,则跳出等待,继续执行后续的逻辑。
// scrcpy.c
enum scrcpy_exit_code
scrcpy(struct scrcpy_options *options) {// 连接阶段...await_for_server();// 【投屏阶段】// 初始化文件上传相关数据结构sc_file_pusher_init(&s->file_pusher, serial, options->push_target)// 初始化解码相关数据结构sc_decoder_init(&s->decoder);// 初始化录制相关数据结构sc_recorder_init(&s->recorder,options->record_filename,options->record_format,info->frame_size);// 初始化分流相关数据结构sc_demuxer_init(&s->demuxer, s->server.video_socket, &demuxer_cbs, NULL);// 将解码器加到分流器的一路流中sc_demuxer_add_sink(&s->demuxer, &dec->packet_sink);// 将录制器加到分流器的一路流中sc_demuxer_add_sink(&s->demuxer, &rec->packet_sink);// 初始化键盘拦截相关数据结构sc_keyboard_inject_init(&s->keyboard_inject, &s->controller,options->key_inject_mode,options->forward_key_repeat);// 初始化鼠标拦截相关数据结构sc_mouse_inject_init(&s->mouse_inject, &s->controller);// 初始化控制socketsc_controller_init(&s->controller, s->server.control_socket,acksync);// 开启两个控制相关的新线程,一个发,一个收sc_controller_start(&s->controller);// 初始化屏幕渲染相关数据结构sc_screen_init(&s->screen, &screen_params);// 将屏幕加到解码器的一路流中sc_decoder_add_sink(&s->decoder, &s->screen.frame_sink);// 将v4l2加到解码器的一路流中sc_decoder_add_sink(&s->decoder, &s->v4l2_sink.frame_sink);// 开启新线程执行分流和解码sc_demuxer_start(&s->demuxer);// SDL事件循环等待事件event_loop(s);// 关闭窗口sc_screen_hide_window(&s->screen);// 关闭和释放服务相关资源sc_server_destroy(&s->server);
}
投屏阶段我们需要关注几个部分:
sc_file_pusher_init
- 初始化文件上传相关的数据结构。文件上传是指将文件从PC拖入镜像窗口中自动同步至/sdcard/Download
目录中。sc_decoder_init
&sc_recorder_init
- 解码器和录制相关数据结构的初始化。主要设置struct sc_packet_sink_ops
的回调函数,在open
、close
、push
三个时机触发相应的动作。(注意:这里的回调是针对Packet的,如前面提到Packet指的是经过压缩编码后的一个数据包)。sc_demuxer_init
- 对分流相关的数据结构进行初始化。sc_demuxer_add_sink
- 将解码器和录制器加到分流中,Scrcpy的分流(Demuxer)和前面提到的容器分流不太一样。容器的分流是分离出多个流,而Scrcpy中的分流指的是把同一份数据送给不同的地方去处理。比如这里会送到解码器进行解码,如果在程序启动时指定了需要进行录制,那么也会送一份数据到录制器中进行数据保存。sc_keyboard_inject_init
&sc_mouse_inject_init
- 初始化键盘和鼠标拦截的数据结构。sc_controller_init
- 对control_socket链路进行初始化。sc_controller_start
- 开启两个控制相关的新线程,一个发,一个收。sc_screen_init
- 对窗口进行初始化,并用SDL创建窗口。设置struct sc_frame_sink_ops
的回调函数, 在open
、close
、push
三个时机触发相应的动作。(注意:和前面不同,这里的回调是针对frame的,即packet解码后帧数据)。sc_decoder_add_sink
- 将窗口和V4L2加到解码器的一路流中,同分流器一样,解码器解码后的帧数据也会送到窗口上和V4L2设备中(V4L2设备需在启动程序是指定,如不指定,则此处就不会触发V4L2逻辑)。sc_demuxer_start
- 开启新线程执行分流和解码。event_loop
- 事件循环,监听SDL事件。sc_server_destroy
- 关闭和释放服务相关资源。因为上一步是死循环,只有在触发退出事件才会退出循环,走到这里的释放逻辑。
其中需要重点关注的 5、7 、8、10、11,我们按照重要性顺序着重来看 - 8、10 、11、5、7。
2.1 sc_screen_init
- 对窗口进行初始化
我们列出sc_screen_init
函数的关键代码:
// screen.c
bool
sc_screen_init(struct sc_screen *screen,const struct sc_screen_params *params) {// 设置on_new_frame回调static const struct sc_video_buffer_callbacks cbs = {.on_new_frame = sc_video_buffer_on_new_frame,};// 对video buffer进行初始化sc_video_buffer_init(&screen->vb, params->buffering_time, &cbs, screen);// 开启新线程执行帧数据处理sc_video_buffer_start(&screen->vb);// 创建SDL窗口SDL_CreateWindow(params->window_title, 0, 0, 0, 0, window_flags);// 创建渲染器SDL_CreateRenderer(screen->window, -1, SDL_RENDERER_ACCELERATED);// 设置解码后frame数据回调static const struct sc_frame_sink_ops ops = {.open = sc_screen_frame_sink_open,.close = sc_screen_frame_sink_close,.push = sc_screen_frame_sink_push,};screen->frame_sink.ops = &ops;
}
我们看到sc_screen_init
的主要作用有四个:
设置
on_new_frame
,并将回调传入screen(也就是桌面窗口)的video_buffer的初始化方法中,可以简单理解为将这个回调和客户端做一个绑定,后面会用到。开启新线程执行帧处理,这里的功能最终是从一个帧队列里取帧数据,然后送给
on_new_frame
函数。// video_buffer.c bool sc_video_buffer_start(struct sc_video_buffer *vb) {// 开启新线程执行run_buffering函数sc_thread_create(&vb->b.thread, run_buffering, "scrcpy-vbuf", vb); }static int run_buffering(void *data) {for (;;) {// 从&vb->b.queue队列中取帧sc_queue_take(&vb->b.queue, next, &vb_frame);// 调用此函数,将帧传入sc_video_buffer_offer(vb, vb_frame->frame);} }static bool sc_video_buffer_offer(struct sc_video_buffer *vb, const AVFrame *frame) {// 帧数据处理后,将数据通过on_new_frame回调传出vb->cbs->on_new_frame(vb, previous_skipped, vb->cbs_userdata); }
通过SDL创建窗口和渲染器。
设置解码后frame数据的回调,正如前面提到,packet会送给解码器和录制器两路packet流,解码器里又可以分屏幕窗口和V4L2设备两路frame流。这里的回调就是这是解码器将数据解码后给到屏幕窗口的回调。
// screen.c
static const struct sc_frame_sink_ops ops = {.open = sc_screen_frame_sink_open,.close = sc_screen_frame_sink_close,.push = sc_screen_frame_sink_push,};static bool
sc_screen_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) {return sc_video_buffer_push(&screen->vb, frame);
}// video_buffer.c
bool
sc_video_buffer_push(struct sc_video_buffer *vb, const AVFrame *frame) {// 往&vb->b.queue队列中插帧数据sc_queue_push(&vb->b.queue, next, vb_frame);
}
分析完sc_screen_init
函数后,我们知道这部分的流程基本如下图所示,那么现在遗留的问题就是外部怎么发起解码器的push回调,以及on_new_frame
里到底做了什么。这里先埋个坑,我们后面填充。
2.2 sc_demuxer_start
- 分流和解码
我们列出sc_demuxer_start
函数的关键代码:
// demuxer.c
bool
sc_demuxer_start(struct sc_demuxer *demuxer) {sc_thread_create(&demuxer->thread, run_demuxer, "scrcpy-demuxer", demuxer);
}static int
run_demuxer(void *data) {// FFmpeg API: 初始化AVCodec和AVCodecContextAVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264);demuxer->codec_ctx = avcodec_alloc_context3(codec);// open sinks,回调到struct sc_packet_sink_ops的.open回调sc_demuxer_open_sinks(demuxer, codec);// FFmpeg API: 初始化AVCodecParserContext和AVPacketdemuxer->parser = av_parser_init(AV_CODEC_ID_H264);AVPacket *packet = av_packet_alloc();// 不断地读packet,并将packet窗到sink中for(;;) {sc_demuxer_recv_packet(demuxer, packet);sc_demuxer_push_packet(demuxer, packet);}// FFmpeg API: 释放av_packet_free(&packet);av_parser_close(demuxer->parser);avcodec_free_context(&demuxer->codec_ctx);
}
我们看到,sc_demuxer_start
主要就是在子线程中执行FFmpeg相关的数据结构初始化,然后在死循环中不断地读packet数据和push,具体是怎么做了,我们来看下sc_demuxer_recv_packet
和 sc_demuxer_push_packet
函数:
// demuxer.c
// sc_demuxer_recv_packet的作用就是接收packet
static bool
sc_demuxer_recv_packet(struct sc_demuxer *demuxer, AVPacket *packet) {// 通过video_socket从网络读packet headernet_recv_all(demuxer->socket, header, SC_PACKET_HEADER_SIZE);// 通过video_socket从网络读packet数据net_recv_all(demuxer->socket, packet->data, len);
}// sc_demuxer_push_packet的作用就是调用struct sc_packet_sink_ops的.push回调
static bool
sc_demuxer_push_packet(struct sc_demuxer *demuxer, AVPacket *packet) {push_packet_to_sinks(demuxer, packet);
}static bool
push_packet_to_sinks(struct sc_demuxer *demuxer, const AVPacket *packet) {for (unsigned i = 0; i < demuxer->sink_count; ++i) {struct sc_packet_sink *sink = demuxer->sinks[i];if (!sink->ops->push(sink, packet)) {return false;}}return true;
}
注意,这里是才从网络获取到packet,还没有解码成frame,所以调用是struct sc_packet_sink_ops
的.push
回调,并不是2.1节的解码后的push回调。这里packet的回调是在前文提到的sc_decoder_init
函数中注册的:
void
sc_decoder_init(struct sc_decoder *decoder) {decoder->sink_count = 0;static const struct sc_packet_sink_ops ops = {.open = sc_decoder_packet_sink_open,.close = sc_decoder_packet_sink_close,.push = sc_decoder_packet_sink_push,};decoder->packet_sink.ops = &ops;
}// packet的push回调方法
static bool
sc_decoder_packet_sink_push(struct sc_packet_sink *sink,const AVPacket *packet) {struct sc_decoder *decoder = DOWNCAST(sink);return sc_decoder_push(decoder, packet);
}static bool
sc_decoder_push(struct sc_decoder *decoder, const AVPacket *packet) {// FFmpeg API: 将packet送到packet送到解码器中avcodec_send_packet(decoder->codec_ctx, packet);// FFmpeg API: 从解码器中拿到解码后的帧数据avcodec_receive_frame(decoder->codec_ctx, decoder->frame);// 将解码后的帧数据传给sinkspush_frame_to_sinks(decoder, decoder->frame);
}
所以packet的push回调的主要功能就是通过解码器把packet解码成frame,这一点和前面说的FFmpeg解码流程是一致的。拿到frame之后,就该调用push_frame_to_sinks
把frame发给了解码器的push回调了:
static bool
push_frame_to_sinks(struct sc_decoder *decoder, const AVFrame *frame) {for (unsigned i = 0; i < decoder->sink_count; ++i) {struct sc_frame_sink *sink = decoder->sinks[i];if (!sink->ops->push(sink, frame)) {return false;}}return true;
}
对的,就是在这里触发了前面一节流程图的第一个问号,所以流程图可以填充一下:
2.3 event_loop
- 事件循环
// scrcpy.c
static enum scrcpy_exit_code
event_loop(struct scrcpy *s) {SDL_Event event;while (SDL_WaitEvent(&event)) {switch (event.type) {case EVENT_STREAM_STOPPED:LOGW("Device disconnected");return SCRCPY_EXIT_DISCONNECTED;case SDL_QUIT:LOGD("User requested to quit");return SCRCPY_EXIT_SUCCESS;default:sc_screen_handle_event(&s->screen, &event);break;}}return SCRCPY_EXIT_FAILURE;
}
event_loop
函数的结构比较清晰,就是一直在等待SDL事件,除了EVENT_STREAM_STOPPED
和SDL_QUIT
事件,其他的事件都是交给sc_screen_handle_event
函数处理:
// screen.c
void
sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) {switch (event->type) {// new frame事件case EVENT_NEW_FRAME:sc_screen_update_frame(screen);return;// SDL窗口事件,包括窗口最大化、恢复、窗口失去焦点等case SDL_WINDOWEVENT:return;// 键盘事件case SDL_KEYDOWN:case SDL_KEYUP:// 鼠标事件case SDL_MOUSEWHEEL:case SDL_MOUSEMOTION:case SDL_MOUSEBUTTONDOWN:// 触摸事件case SDL_FINGERMOTION:case SDL_FINGERDOWN:case SDL_FINGERUP:case SDL_MOUSEBUTTONUP:// 省略了部分代码}sc_input_manager_handle_event(&screen->im, event);
}
在sc_screen_handle_event
函数中,我们会处理EVENT_NEW_FRAME
事件和其他鼠标和键盘事件。我们先着重关注EVENT_NEW_FRAME
事件的。收到这个事件之后会执行sc_screen_update_frame
函数,关键代码如下:
// screen.c
static bool
sc_screen_update_frame(struct sc_screen *screen) {// 更新数据update_texture(screen, frame);// 第一次执行则打开窗口if (!screen->has_frame) {sc_screen_show_initial_window(screen);}// 数据渲染sc_screen_render(screen, false);
}static void
update_texture(struct sc_screen *screen, const AVFrame *frame) {// 将YUV数据写到SDL上下文中SDL_UpdateYUVTexture(screen->texture, NULL,frame->data[0], frame->linesize[0],frame->data[1], frame->linesize[1],frame->data[2], frame->linesize[2]);
}static void
sc_screen_show_initial_window(struct sc_screen *screen) {// 展示窗口SDL_ShowWindow(screen->window);
}static void
sc_screen_render(struct sc_screen *screen, bool update_content_rect) {// SDL模板代码,将上下文中的数据渲染到窗口上SDL_RenderClear(screen->renderer);SDL_RenderCopy(screen->renderer, screen->texture, NULL, &screen->rect);SDL_RenderPresent(screen->renderer);
}
所以我们知道这个函数的作用就是打开窗口并把frame数据(即解码后的YUV)渲染到窗口中。源头就是EVENT_NEW_FRAME
这个事件。那么这个事件是哪里发来的呢。就是前面的on_new_frame
回调,对应的是sc_video_buffer_on_new_frame
函数:
// screen.c
static void
sc_video_buffer_on_new_frame(struct sc_video_buffer *vb, bool previous_skipped,void *userdata) {// 这里将EVENT_NEW_FRAME通过SDL的事件机制发出static SDL_Event new_frame_event = {.type = EVENT_NEW_FRAME,};SDL_PushEvent(&new_frame_event);
}
看到这里,我们的流程图就可以填充完整了。
到目前为止,视频流这一块基本上已经分析完毕,这部分的数据走的是video_socket。上篇提到,还有一个control_socket,主要用于控制事件传输,比如鼠标键盘控制,也就是投屏中的反控功能,这也是投屏业务非常重要的一个环节,下面我们来看这部分。
2.4 sc_keyboard_inject_init
& sc_mouse_inject_init
- 键鼠事件
因为键盘和鼠标整体的逻辑差不多,所以这里我们追下键盘的流程,鼠标就不赘述。sc_keyboard_inject_init
的主要功能就是注册键盘回调:
// mouse_inject.c
void
sc_keyboard_inject_init(struct sc_keyboard_inject *ki,struct sc_controller *controller,enum sc_key_inject_mode key_inject_mode,bool forward_key_repeat) {static const struct sc_key_processor_ops ops = {.process_key = sc_key_processor_process_key,.process_text = sc_key_processor_process_text,};ki->key_processor.ops = &ops;
}static void
sc_key_processor_process_key(struct sc_key_processor *kp,const struct sc_key_event *event,uint64_t ack_to_wait) {sc_controller_push_msg(ki->controller, &msg)
}bool
sc_controller_push_msg(struct sc_controller *controller,const struct sc_control_msg *msg) {// 键盘事件入队列cbuf_push(&controller->queue, *msg);
}
可以看到键盘事件最终会放到队列中。那么键盘事件是哪里来的呢?就是前一节的event_loop
。SDL会自动检测窗口收到的键盘和鼠标事件,只需要在event_loop
中监听对应事件即可,最终会触发事件回调:
// input_manager.c
void
sc_input_manager_handle_event(struct sc_input_manager *im, SDL_Event *event) {switch (event->type) {// ...case SDL_KEYDOWN:case SDL_KEYUP:sc_input_manager_process_key(im, &event->key);break;// ...}
}static void
sc_input_manager_process_key(struct sc_input_manager *im,const SDL_KeyboardEvent *event) {// 调用process_key回调im->kp->ops->process_key(im->kp, &evt, ack_to_wait);
}
所以目前为止,键盘鼠标事件的流程是:
2.5 sc_controller_start
- 事件的收发
这里说的事件手法主要是和手机侧的事件交互,我们来看下是怎么做的:
bool
sc_controller_start(struct sc_controller *controller) {sc_thread_create(&controller->thread, run_controller,"scrcpy-ctl", controller);receiver_start(&controller->receiver);
}bool
receiver_start(struct receiver *receiver) {sc_thread_create(&receiver->thread, run_receiver,"scrcpy-receiver", receiver);
}
sc_controller_start
函数会开两个线程,一个负责收,一个负责发:
收线程 - 主要从手机侧收粘贴板事件,手机侧触发的复制操作,会将数据传至PC侧,PC会放到粘贴板中。这里不细说,感兴趣的同学可以自行追下源码。
发线程 - 将PC侧的事件发给手机。这是我们关注的重点,我们看下
run_controller
函数的核心逻辑:
// controller.c
static int
run_controller(void *data) {for(;;) {// 从队列里取事件cbuf_take(&controller->queue, &msg);// 处理事件process_msg(controller, &msg);}
}static bool
process_msg(struct sc_controller *controller,const struct sc_control_msg *msg) {// 通过control_socket将事件发出去net_send_all(controller->control_socket, serialized_msg, length);
}
发线程的主要逻辑就是一个死循环,不断地从队列中取事件,然后通过control_socket发出去。
所以键鼠事件的流程可以完善一下了:
2.6 时序图
老规矩,抛出一张投屏阶段的时序图。不同的颜色代表不同的线程。
3. 小结
这一篇我们探究了Scrcpy Client端投屏阶段的逻辑。涉及的点有FFmpeg解码、SDL的窗口绘制和键盘鼠标反控。
至此Client端的逻辑已经介绍完了,分为连接阶段和投屏阶段。下一篇我们就要探究Server端,也就是手机侧的功能逻辑了,下篇见。
【投屏】Scrcpy源码分析三(Client篇-投屏阶段)相关推荐
- 【投屏】Scrcpy源码分析二(Client篇-连接阶段)
Scrcpy源码分析系列 [投屏]Scrcpy源码分析一(编译篇) [投屏]Scrcpy源码分析二(Client篇-连接阶段) [投屏]Scrcpy源码分析三(Client篇-投屏阶段) [投屏]Sc ...
- 【投屏】Scrcpy源码分析四(最终章 - Server篇)
Scrcpy源码分析系列 [投屏]Scrcpy源码分析一(编译篇) [投屏]Scrcpy源码分析二(Client篇-连接阶段) [投屏]Scrcpy源码分析三(Client篇-投屏阶段) [投屏]Sc ...
- 【投屏】Scrcpy源码分析一(编译篇)
Scrcpy源码分析系列 [投屏]Scrcpy源码分析一(编译篇) [投屏]Scrcpy源码分析二(Client篇-连接阶段) [投屏]Scrcpy源码分析三(Client篇-投屏阶段) [投屏]Sc ...
- Nouveau源码分析(三):NVIDIA设备初始化之nouveau_drm_probe
Nouveau源码分析(三) 向DRM注册了Nouveau驱动之后,内核中的PCI模块就会扫描所有没有对应驱动的设备,然后和nouveau_drm_pci_table对照. 对于匹配的设备,PCI模块 ...
- Spring源码分析(三)
Spring源码分析 第三章 手写Ioc和Aop 文章目录 Spring源码分析 前言 一.模拟业务场景 (一) 功能介绍 (二) 关键功能代码 (三) 问题分析 二.使用ioc和aop重构 (一) ...
- JUC源码分析-线程池篇(五):ForkJoinPool - 2
通过上一篇(JUC源码分析-线程池篇(四):ForkJoinPool - 1)的讲解,相信同学们对 ForkJoinPool 已经有了一个大概的认识,本篇我们将通过分析源码的方式来深入了解 ForkJ ...
- Kubernetes Node Controller源码分析之配置篇
2019独角兽企业重金招聘Python工程师标准>>> Author: xidianwangtao@gmail.com Kubernetes Node Controller源码分析之 ...
- paho架构_MQTT系列最终章-Paho源码分析(三)-心跳与重连机制
写在前面 通过之前MQTT系列-Eclipse.Paho源码分析(二)-消息的发送与接收的介绍,相信仔细阅读过的小伙伴已经对Eclipse.Paho内部发送和订阅消息的流程有了一个较为清晰的认识,今天 ...
- ABP源码分析三十四:ABP.Web.Mvc
ABP.Web.Mvc模块主要完成两个任务: 第一,通过自定义的AbpController抽象基类封装ABP核心模块中的功能,以便利的方式提供给我们创建controller使用. 第二,一些常见的基础 ...
最新文章
- 【bzoj 3495】PA2010 Riddle
- java mplayer 源码_师兄写的一个JAVA播放器的源代码
- PAT甲级 -- 1005 Spell It Right (20 分)
- WindowsPhone-GameBoy模拟器开发四--Gameboy显示系统分析
- 怎样在DOS下查看屏蔽和开启端口了
- C# winform 按钮 响应鼠标经过变换图片,如何处理?
- JAVA简历1到三年
- 乐高机器人编程自学入门
- 鬼畜视频制作必备——vegas pro特别版歌声合成工具UTAU
- hdu 5755 Gambler Bo 三进制高斯消元(开关问题变形)
- 移动端手指事件和手机事件:
- 在win10基础上安装Ubuntu16.04双系统(双硬盘)
- mysql sql计算经纬度
- Unix Shell 介绍
- php玩偶,玩偶娃衣 织法教程|毛衣花样图解|视频教程-编织人生
- 3DMax_界面介绍及基本工具使用
- python中输出语句的怎么写_python 中简单的输出语句
- matlab模拟短波天波,短波的天波传播衰减预测模型
- 最难找工作的10种大学生
- 商品下单未支付,如何取消订单?
热门文章
- 路由器dns服务器怎么才能自动改变,更改路由器DNS 提高网速又一方法技巧
- 清北学堂2019.8.8
- Apple iPad
- 用Woocommerce建立一个网上商店 [03] 增加产品类别
- 清晰的理解大端和小端
- mysql ddl过程,MySQL基础教程3-DDL(创建表)
- t分布, 卡方x分布,F分布
- block标签、inline标签、inline-block标签的特点
- APP 合规讲堂 - 收集使用个人信息的目的、方式、范围发生变化时,是否以适当方式通知用户(五)
- 【408数据结构】备考常见必会算法图鉴