热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

视频提取图片/图片合成视频ffmpeg(二十)

前言需求场景1(视频中提取照片):各大网站在线播放视频时,鼠标滑到某一时刻能够提前显示那一时刻的画面。短的视频编辑APP中,为了更好的对

前言


  • 需求场景1(视频中提取照片):
    各大网站在线播放视频时,鼠标滑到某一时刻能够提前显示那一时刻的画面。短的视频编辑APP中,为了更好的对视频进行编辑,会提取出视频各个时刻的画面进行预览,那么这些是如何实现的呢?本文将给出基于ffmpeg的实现代码以及实现思路。
  • 需求场景2(照片合成视频):
    摄影师经常不间断的拍摄一组连续的画面用于合成延时视频,剪印APP中也有时光相册这样通过照片生成视频的功能(不过剪印APP照片合成的视频采用了插值算法生成了额外的过度动画照片以及特效,功能更加复杂,但是不管怎样,最终还是会由照片合成视频)。本文基于ffmpeg实现简单的照片合成视频思路以及详细代码

实现思路分析

这里照片以JPG为例,视频以MP4为例,其它格式类似


  • 视频中提取照片:

1、fmpeg对将像素数据写入到JPG图片中也封装到了avformat_xxx系列接口中,它的使用流程和封装视频数据到mp4文件一模一样,只不过一个JPG文件中只包含了一帧视频数据而已;
2、ffmpeg对JPG文件的封装支持模式匹配,即如果想要将多张图片写入到多张jpg中只需要文件名包含百分号即可,例如 name%3d.jpg,那么在每一次调用av_write_frame()函数写入视频数据到jpg图片时都会生成一张jpg图片。这样做的好处是不需要每一张要写入的jpg文件都创建一个AVFormatContext与之对应。其它流程和写入一张jpg一样。


流程为:
1、先从MP4中提取指定时刻AVPacket解码成AVFrame
2、然后将步骤1得到的AVFrame进行从素格式YUV420P到JPG需要的YUVJ420P像素格式的转换
3、再重新编码,然后再封装到jpg中


  • 照片合成视频:
    因为JPG的编码方式为AV_CODEC_ID_MJPEG,MP4如果采用h264编码,那么两者的编码方式是不一致的,所以就需要先解码再编码,具体流程为:
    1、先将JPG解码成AVFrame
    2、将JPG解码后的源像素格式YUVJ420P转换成x264编码需要的YUV420P像素格式
    3、再重新编码,然后再封装到mp4中

流程图


  • 视频中提取照片流程图

     

    #include "cppcommon/CLog.h"
    #include
    #include
    #include
    #include
    #include
    #include
    }
    using namespace std;class VideoJPG
    {
    public:VideoJPG();~VideoJPG();/** 功能:实现提取任意时刻视频的某一帧并将其转化为JPG图片输出到当前目录*/void doJpgGet();/** 功能:将多张JPG照片合并成一段视频*/void doJpgToVideo();
    private:AVFrame *de_frame;AVFrame *en_frame;// 用于视频像素转换SwsContext *sws_ctx;// 用于读取视频AVFormatContext *in_fmt;// 用于解码AVCodecContext *de_ctx;// 用于编码AVCodecContext *en_ctx;// 用于封装jpgAVFormatContext *ou_fmt;int video_ou_index;void releaseSources();void doDecode(AVPacket *in_pkt);void doEncode(AVFrame *en_frame);};

    备注:
    视频提取图片的实现位于doJpgGet();函数,图片合成视频位于doJpgToVideo();函数

    实现文件

    #include "VideoJpg.hpp"VideoJPG::VideoJPG()
    {sws_ctx = NULL;de_frame = NULL;en_frame = NULL;in_fmt = NULL;ou_fmt = NULL;de_ctx = NULL;en_ctx = NULL;
    }VideoJPG::~VideoJPG()
    {}void VideoJPG::releaseSources()
    {if (in_fmt) {avformat_close_input(&in_fmt);in_fmt = NULL;}if (ou_fmt) {avformat_free_context(ou_fmt);ou_fmt = NULL;}if (en_frame) {av_frame_unref(en_frame);en_frame = NULL;}if (de_frame) {av_frame_unref(de_frame);de_frame = NULL;}if (en_ctx) {avcodec_free_context(&en_ctx);en_ctx = NULL;}if (de_ctx) {avcodec_free_context(&de_ctx);de_ctx = NULL;}
    }/** 写入jpg说明:* 1、ffmpeg对将像素数据写入到JPG图片中也封装到了avformat_xxx系列接口中,它的使用流程和封装视频数据到mp4文件一模一样* 只不过一个JPG文件中只包含了一帧视频数据而已;* 2、ffmpeg对JPG文件的封装支持模式匹配,即如果想要将多张图片写入到多张jpg中只需要文件名包含百分号即可,例如 name%3d.jpg,那么在每一次调用av_write_frame()* 函数写入视频数据到jpg图片时都会生成一张jpg图片。这样做的好处是不需要每一张要写入的jpg文件都创建一个AVFormatContext与之对应。其它流程和写入一张jpg一样,具体* 参考如下示例:* 3、jpg对应的封装器为ff_image2_muxer,对应的编码器为ff_mjpeg_encoder*/
    #define Get_More 1 // 1代表使用模式匹配,一次可以写入多张jpg图片。0代表一次写入1张图片
    void VideoJPG::doJpgGet()
    {string curFile(__FILE__);unsigned long pos = curFile.find("2-video_audio_advanced");if (pos == string::npos) {LOGD("not find file");return;}string srcDic = curFile.substr(0,pos) + "filesources/";string srcPath = srcDic + "test_1280x720_3.mp4";
    #if Get_Morestring dstPath = srcDic + "1-doJpg_get%3d.jpg";int num = 5;
    #elsestring dstPath = srcDic + "1-doJpg_get.jpg";
    #endifint video_index &#61; -1;// 要截取的时刻string start &#61; "00:00:05";int64_t start_pts &#61; stoi(start.substr(0,2));start_pts &#43;&#61; stoi(start.substr(3,2));start_pts &#43;&#61; stoi(start.substr(6,2));if (avformat_open_input(&in_fmt,srcPath.c_str(),NULL,NULL) <0) {LOGD("avformat_open_input fail");return;}if (avformat_find_stream_info(in_fmt, NULL) <0) {LOGD("avformat_find_stream_info fail");return;}// 遍历出视频索引for (int i &#61; 0; inb_streams; i&#43;&#43;) {AVStream *stream &#61; in_fmt->streams[i];if (stream->codecpar->codec_type &#61;&#61; AVMEDIA_TYPE_VIDEO) { // 说明是视频video_index &#61; i;// 初始化解码器用于解码AVCodec *codec &#61; avcodec_find_decoder(stream->codecpar->codec_id);de_ctx &#61; avcodec_alloc_context3(codec);if (!de_ctx) {LOGD("video avcodec_alloc_context3 fail");releaseSources();return;}// 设置解码参数&#xff0c;这里直接从源视频流中拷贝if (avcodec_parameters_to_context(de_ctx, stream->codecpar) <0) {LOGD("video avcodec_parameters_to_context");releaseSources();return;}// 初始化解码器上下文if (avcodec_open2(de_ctx, codec, NULL) <0) {LOGD("video avcodec_open2() fail");releaseSources();return;}break;}}// 初始化编码器;因为最终是要写入到JPEG&#xff0c;所以使用的编码器ID为AV_CODEC_ID_MJPEGAVCodec *codec &#61; avcodec_find_encoder(AV_CODEC_ID_MJPEG);en_ctx &#61; avcodec_alloc_context3(codec);if (!en_ctx) {LOGD("avcodec_alloc_context3 fail");releaseSources();return;}// 设置编码参数AVStream *in_stream &#61; in_fmt->streams[video_index];en_ctx->width &#61; in_stream->codecpar->width;en_ctx->height &#61; in_stream->codecpar->height;// 如果是编码后写入到图片中&#xff0c;那么比特率可以不用设置&#xff0c;不影响最终的结果(也不会影响图像清晰度)en_ctx->bit_rate &#61; in_stream->codecpar->bit_rate;// 如果是编码后写入到图片中&#xff0c;那么帧率可以不用设置&#xff0c;不影响最终的结果en_ctx->framerate &#61; in_stream->r_frame_rate;en_ctx->time_base &#61; in_stream->time_base;// 对于MJPEG编码器来说&#xff0c;它支持的是YUVJ420P/YUVJ422P/YUVJ444P格式的像素en_ctx->pix_fmt &#61; AV_PIX_FMT_YUVJ420P;// 初始化编码器上下文if (avcodec_open2(en_ctx, codec, NULL) <0) {LOGD("avcodec_open2() fail");releaseSources();return;}// 创建用于输出JPG的封装器if (avformat_alloc_output_context2(&ou_fmt, NULL, NULL, dstPath.c_str()) <0) {LOGD("avformat_alloc_output_context2");releaseSources();return;}/** 添加流* 对于图片封装器来说&#xff0c;也可以把它想象成只有一帧视频的视频封装器。所以它实际上也需要一路视频流&#xff0c;而事实上图片的流是视频流类型*/AVStream *stream &#61; avformat_new_stream(ou_fmt, NULL);// 设置流参数&#xff1b;直接从编码器拷贝参数即可if (avcodec_parameters_from_context(stream->codecpar, en_ctx) <0) {LOGD("avcodec_parameters_from_context");releaseSources();return;}/** 初始化上下文* 对于写入JPG来说&#xff0c;它是不需要建立输出上下文IO缓冲区的的&#xff0c;所以avio_open2()没有调用到&#xff0c;但是最终一样可以调用av_write_frame()写入数据*/if (!(ou_fmt->oformat->flags & AVFMT_NOFILE)) {if (avio_open2(&ou_fmt->pb, dstPath.c_str(), AVIO_FLAG_WRITE, NULL, NULL) <0) {LOGD("avio_open2 fail");releaseSources();return;}}/** 为输出文件写入头信息* 不管是封装音视频文件还是图片文件&#xff0c;都需要调用此方法进行相关的初始化&#xff0c;否则av_write_frame()函数会崩溃*/if (avformat_write_header(ou_fmt, NULL) <0) {LOGD("avformat_write_header() fail");releaseSources();return;}/** 创建视频像素转换上下文* 因为源视频的像素格式是yuv420p的&#xff0c;而jpg编码需要的像素格式是yuvj420p的&#xff0c;所以需要先进行像素格式转换*/sws_ctx &#61; sws_getContext(in_stream->codecpar->width, in_stream->codecpar->height, (enum AVPixelFormat)in_stream->codecpar->format,en_ctx->width, en_ctx->height, en_ctx->pix_fmt,0, NULL, NULL, NULL);if (!sws_ctx) {LOGD("sws_getContext fail");releaseSources();return;}// 创建编码解码用的AVFramede_frame &#61; av_frame_alloc();en_frame &#61; av_frame_alloc();en_frame->width &#61; en_ctx->width;en_frame->height &#61; en_ctx->height;en_frame->format &#61; en_ctx->pix_fmt;if (av_frame_get_buffer(en_frame, 0) <0) {LOGD("av_frame_get_buffer fail");releaseSources();return;}if (av_frame_make_writable(en_frame) <0) {LOGD("av_frame_make_writeable fail");releaseSources();return;}AVPacket *in_pkt &#61; av_packet_alloc();AVPacket *ou_pkt &#61; av_packet_alloc();AVRational time_base &#61; in_fmt->streams[video_index]->time_base;AVRational frame_rate &#61; in_fmt->streams[video_index]->r_frame_rate;// 一帧的时间戳int64_t delt &#61; time_base.den/frame_rate.num;start_pts *&#61; time_base.den;/** 因为想要截取的时间处的AVPacket并不一定是I帧&#xff0c;所以想要正确的解码&#xff0c;得先找到离想要截取的时间处往前的最近的I帧* 开始解码&#xff0c;直到拿到了想要获取的时间处的AVFrame* AVSEEK_FLAG_BACKWARD 代表如果start_pts指定的时间戳处的AVPacket非I帧&#xff0c;那么就往前移动指针&#xff0c;直到找到I帧&#xff0c;那么* 当首次调用av_frame_read()函数时返回的AVPacket将为此I帧的AVPacket*/if (av_seek_frame(in_fmt, video_index, start_pts, AVSEEK_FLAG_BACKWARD) <0) {LOGD("av_seek_frame fail");releaseSources();return;}bool found &#61; false;while (av_read_frame(in_fmt, in_pkt) &#61;&#61; 0) {if (in_pkt->stream_index !&#61; video_index) {continue;}// 先解码avcodec_send_packet(de_ctx, in_pkt);LOGD("video pts %d(%s)",in_pkt->pts,av_ts2timestr(in_pkt->pts,&in_stream->time_base));while (true) {int ret &#61; avcodec_receive_frame(de_ctx, de_frame);if (ret <0) {break;}/** 解码得到的AVFrame中的pts和解码前的AVPacket中的pts是一一对应的&#xff0c;所以可以利用AVFrame中的pts来判断此帧是否在想要截取的时间范围内*/LOGD("sucess pts %d",de_frame->pts);// 成功解码出来了
    #if Get_More// 取多帧视频并写入到文件static int i&#61;0;delt &#61; delt*num;if (abs(de_frame->pts - start_pts) #else// 去一帧帧视频并写入到文件if (abs(de_frame->pts - start_pts) #endifLOGD("找到了这一帧");// 因为源视频帧的格式和目标视频帧的格式可能不一致&#xff0c;所以这里需要转码ret &#61; sws_scale(sws_ctx, de_frame->data, de_frame->linesize, 0, de_frame->height, en_frame->data, en_frame->linesize);if (ret <0) {LOGD("sws_scale fail");releaseSources();return;}#if Get_More// 重新编码en_frame->pts &#61; i;avcodec_send_frame(en_ctx, en_frame);// 拿到指定数目的AVPacket后再清空缓冲区if (i > num) {avcodec_send_frame(en_ctx, NULL);}
    #else// 重新编码;因为只有一帧&#xff0c;所以这里直接写1 即可en_frame->pts &#61; 1;avcodec_send_frame(en_ctx, en_frame);// 因为只编码一帧&#xff0c;所以发送一帧视频后立马清空缓冲区avcodec_send_frame(en_ctx, NULL);
    #endifret &#61; avcodec_receive_packet(en_ctx, ou_pkt);if (ret <0) {LOGD("fail ");releaseSources();return;}// 写入文件if(av_write_frame(ou_fmt, ou_pkt) <0) {LOGD("av_write_frame fail");releaseSources();return;}
    #if Get_Moreif (i>num) {found &#61; true;}#elsefound &#61; true;
    #endifbreak;}}av_packet_unref(in_pkt);if (found) {LOGD("has get jpg");break;}}/** 写入文件尾* 对于写入视频文件来说&#xff0c;此函数必须调用&#xff0c;但是对于写入JPG文件来说&#xff0c;不调用此函数也没关系&#xff1b;*/
    // av_write_trailer(ou_fmt);// 释放资源releaseSources();
    }/** 多张图片合成为一段视频说明&#xff1a;* 1、ffmpeg对jpg的解封装和对视频的解封装一样&#xff0c;都封装到了avformat_xxxx系列接口里面。流程和解封装音视频的流程一模一样,对一张jpg图片的解封装可以理解为对只包含一帧* 视频的视频文件的解封装* 2、ffmpeg对jpeg的解封装支持模式匹配&#xff0c;例如对于name%3d.jpg进行解封装时(假如目录中包含的jpg列表为* name001.jpg,name002.jpg,name004.jpg,........)&#xff0c;每次调用av_frame_read()函数&#xff0c;它将按照name001.jpg,name002.jpg,name004.jpg,........的顺序依次进行读取* 3、jpg对应的解封装器为ff_image2_demuxer&#xff0c;对应的编码器为ff_mjpeg_decoder*//** 将前面视频生成的jpg合成mp4文件* 因为JPG的编码方式为AV_CODEC_ID_MJPEG&#xff0c;MP4如果采用h264编码&#xff0c;那么两者的编码方式是不一致的&#xff0c;所以就需要先解码再编码&#xff0c;具体流程为&#xff1a;* 1、先将JPG解码成AVFrame* 2、将JPG解码后的源像素格式YUVJ420P转换成x264编码需要的YUV420P像素格式* 3、再重新编码&#xff0c;然后再封装到mp4中*/
    void VideoJPG::doJpgToVideo()
    {string curFile(__FILE__);unsigned long pos &#61; curFile.find("2-video_audio_advanced");if (pos &#61;&#61; string::npos) {LOGD("not find file");return;}string srcDic &#61; curFile.substr(0,pos) &#43; "filesources/";string srcPath &#61; srcDic &#43; "1-doJpg_get%3d.jpg";string dstPath &#61; srcDic &#43; "1-dojpgToVideo.mp4";int video_index &#61; -1;// 创建jpg的解封装上下文if (avformat_open_input(&in_fmt, srcPath.c_str(), NULL, NULL) <0) {LOGD("avformat_open_input fail");return;}if (avformat_find_stream_info(in_fmt, NULL) <0) {LOGD("avformat_find_stream_info()");releaseSources();return;}// 创建解码器及初始化解码器上下文用于对jpg进行解码for (int i&#61;0; inb_streams; i&#43;&#43;) {AVStream *stream &#61; in_fmt->streams[i];/** 对于jpg图片来说&#xff0c;它里面就是一路视频流&#xff0c;所以媒体类型就是AVMEDIA_TYPE_VIDEO*/if (stream->codecpar->codec_type &#61;&#61; AVMEDIA_TYPE_VIDEO) {AVCodec *codec &#61; avcodec_find_decoder(stream->codecpar->codec_id);if (!codec) {LOGD("not find jpg codec");releaseSources();return;}de_ctx &#61; avcodec_alloc_context3(codec);if (!de_ctx) {LOGD("jpg codec_ctx fail");releaseSources();return;}// 设置解码参数;文件解封装的AVStream中就包括了解码参数&#xff0c;这里直接流中拷贝即可if (avcodec_parameters_to_context(de_ctx, stream->codecpar) <0) {LOGD("set jpg de_ctx parameters fail");releaseSources();return;}// 初始化解码器及解码器上下文if (avcodec_open2(de_ctx, codec, NULL) <0) {LOGD("avcodec_open2() fail");releaseSources();return;}video_index &#61; i;break;}}// 创建mp4文件封装器if (avformat_alloc_output_context2(&ou_fmt,NULL,NULL,dstPath.c_str()) <0) {LOGD("MP2 muxer fail");releaseSources();return;}// 添加视频流AVStream *stream &#61; avformat_new_stream(ou_fmt, NULL);video_ou_index &#61; stream->index;// 创建h264的编码器及编码器上下文AVCodec *en_codec &#61; avcodec_find_encoder(AV_CODEC_ID_H264);if (!en_codec) {LOGD("encodec fail");releaseSources();return;}en_ctx &#61; avcodec_alloc_context3(en_codec);if (!en_ctx) {LOGD("en_codec ctx fail");releaseSources();return;}// 设置编码参数AVStream *in_stream &#61; in_fmt->streams[video_index];en_ctx->width &#61; in_stream->codecpar->width;en_ctx->height &#61; in_stream->codecpar->height;en_ctx->pix_fmt &#61; (enum AVPixelFormat)in_stream->codecpar->format;en_ctx->bit_rate &#61; 0.96*1000000;en_ctx->framerate &#61; (AVRational){5,1};en_ctx->time_base &#61; (AVRational){1,5};// 某些封装格式必须要设置&#xff0c;否则会造成封装后文件中信息的缺失if (ou_fmt->oformat->flags & AVFMT_GLOBALHEADER) {en_ctx->flags |&#61; AV_CODEC_FLAG_GLOBAL_HEADER;}// x264编码特有if (en_codec->id &#61;&#61; AV_CODEC_ID_H264) {// 代表了编码的速度级别av_opt_set(en_ctx->priv_data,"preset","slow",0);en_ctx->flags2 &#61; AV_CODEC_FLAG2_LOCAL_HEADER;}// 初始化编码器及编码器上下文if (avcodec_open2(en_ctx,en_codec,NULL) <0) {LOGD("encodec ctx fail");releaseSources();return;}// 设置视频流参数;对于封装来说&#xff0c;直接从编码器上下文拷贝即可if (avcodec_parameters_from_context(stream->codecpar, en_ctx) <0) {LOGD("copy en_code parameters fail");releaseSources();return;}// 初始化封装器输出缓冲区if (!(ou_fmt->oformat->flags & AVFMT_NOFILE)) {if (avio_open2(&ou_fmt->pb, dstPath.c_str(), AVIO_FLAG_WRITE, NULL, NULL) <0) {LOGD("avio_open2 fail");releaseSources();return;}}// 创建像素格式转换器sws_ctx &#61; sws_getContext(de_ctx->width, de_ctx->height, de_ctx->pix_fmt,en_ctx->width, en_ctx->height, en_ctx->pix_fmt,0, NULL, NULL, NULL);if (!sws_ctx) {LOGD("sws_getContext fail");releaseSources();return;}// 写入封装器头文件信息&#xff1b;此函数内部会对封装器参数做进一步初始化if (avformat_write_header(ou_fmt, NULL) <0) {LOGD("avformat_write_header fail");releaseSources();return;}// 创建编解码用的AVFramede_frame &#61; av_frame_alloc();en_frame &#61; av_frame_alloc();en_frame->width &#61; en_ctx->width;en_frame->height &#61; en_ctx->height;en_frame->format &#61; en_ctx->pix_fmt;av_frame_get_buffer(en_frame, 0);av_frame_make_writable(en_frame);AVPacket *in_pkt &#61; av_packet_alloc();while (av_read_frame(in_fmt, in_pkt) &#61;&#61; 0) {if (in_pkt->stream_index !&#61; video_index) {continue;}// 先解码doDecode(in_pkt);av_packet_unref(in_pkt);}// 刷新解码缓冲区doDecode(NULL);av_write_trailer(ou_fmt);LOGD("结束。。。");// 释放资源releaseSources();
    }void VideoJPG::doDecode(AVPacket *in_pkt)
    {static int num_pts &#61; 0;// 先解码avcodec_send_packet(de_ctx, in_pkt);while (true) {int ret &#61; avcodec_receive_frame(de_ctx, de_frame);if (ret &#61;&#61; AVERROR_EOF) {doEncode(NULL);break;} else if(ret <0) {break;}// 成功解码了&#xff1b;先进行格式转换然后再编码if(sws_scale(sws_ctx, de_frame->data, de_frame->linesize, 0, de_frame->height, en_frame->data, en_frame->linesize) <0) {LOGD("sws_scale fail");releaseSources();return;}// 编码前要设置好pts的值&#xff0c;如果en_ctx->time_base为{1,fps}&#xff0c;那么这里pts的值即为帧的个数值en_frame->pts &#61; num_pts&#43;&#43;;doEncode(en_frame);}}void VideoJPG::doEncode(AVFrame *en_frame1)
    {avcodec_send_frame(en_ctx, en_frame1);while (true) {AVPacket *ou_pkt &#61; av_packet_alloc();if (avcodec_receive_packet(en_ctx, ou_pkt) <0) {av_packet_unref(ou_pkt);break;}// 成功编码了;写入之前要进行时间基的转换AVStream *stream &#61; ou_fmt->streams[video_ou_index];av_packet_rescale_ts(ou_pkt, en_ctx->time_base, stream->time_base);LOGD("video pts %d(%s)",ou_pkt->pts,av_ts2timestr(ou_pkt->pts, &stream->time_base));av_write_frame(ou_fmt, ou_pkt);}
    }

     


    项目地址

    https://github.com/nldzsz/ffmpeg-demo

    位于cppsrc目录下
    VideoJpg.hpp/VideoJpg.cpp文件

    项目下示例可运行于iOS/android/mac平台&#xff0c;工程分别位于demo-ios/demo-android/demo-mac三个目录下&#xff0c;可根据需要选择不同平台


推荐阅读
  • 优化ListView性能
    本文深入探讨了如何通过多种技术手段优化ListView的性能,包括视图复用、ViewHolder模式、分批加载数据、图片优化及内存管理等。这些方法能够显著提升应用的响应速度和用户体验。 ... [详细]
  • 本文探讨了Hive中内部表和外部表的区别及其在HDFS上的路径映射,详细解释了两者的创建、加载及删除操作,并提供了查看表详细信息的方法。通过对比这两种表类型,帮助读者理解如何更好地管理和保护数据。 ... [详细]
  • 本文详细介绍了如何使用 Yii2 的 GridView 组件在列表页面实现数据的直接编辑功能。通过具体的代码示例和步骤,帮助开发者快速掌握这一实用技巧。 ... [详细]
  • 如何高效创建和使用字体图标
    在Web和移动开发中,为什么选择字体图标?主要原因是其卓越的性能,可以显著减少HTTP请求并优化页面加载速度。本文详细介绍了从设计到应用的字体图标制作流程,并提供了专业建议。 ... [详细]
  • 深入解析Android自定义View面试题
    本文探讨了Android Launcher开发中自定义View的重要性,并通过一道经典的面试题,帮助开发者更好地理解自定义View的实现细节。文章不仅涵盖了基础知识,还提供了实际操作建议。 ... [详细]
  • 深入理解Tornado模板系统
    本文详细介绍了Tornado框架中模板系统的使用方法。Tornado自带的轻量级、高效且灵活的模板语言位于tornado.template模块,支持嵌入Python代码片段,帮助开发者快速构建动态网页。 ... [详细]
  • 本文深入探讨了Linux系统中网卡绑定(bonding)的七种工作模式。网卡绑定技术通过将多个物理网卡组合成一个逻辑网卡,实现网络冗余、带宽聚合和负载均衡,在生产环境中广泛应用。文章详细介绍了每种模式的特点、适用场景及配置方法。 ... [详细]
  • 2023年京东Android面试真题解析与经验分享
    本文由一位拥有6年Android开发经验的工程师撰写,详细解析了京东面试中常见的技术问题。涵盖引用传递、Handler机制、ListView优化、多线程控制及ANR处理等核心知识点。 ... [详细]
  • 本文介绍了如何通过 Maven 依赖引入 SQLiteJDBC 和 HikariCP 包,从而在 Java 应用中高效地连接和操作 SQLite 数据库。文章提供了详细的代码示例,并解释了每个步骤的实现细节。 ... [详细]
  • 本文介绍如何使用阿里云的fastjson库解析包含时间戳、IP地址和参数等信息的JSON格式文本,并进行数据处理和保存。 ... [详细]
  • 帝国CMS多图上传插件详解及使用指南
    本文介绍了一款用于帝国CMS的多图上传插件,该插件通过Flash技术实现批量图片上传功能,显著提升了多图上传效率。文章详细说明了插件的安装、配置和使用方法。 ... [详细]
  • 获取计算机硬盘序列号的方法与实现
    本文介绍了如何通过编程方法获取计算机硬盘的唯一标识符(序列号),并提供了详细的代码示例和解释。此外,还涵盖了如何使用这些信息进行身份验证或注册保护。 ... [详细]
  • ASP.NET MVC中Area机制的实现与优化
    本文探讨了在ASP.NET MVC框架中,如何通过Area机制有效地组织和管理大规模应用程序的不同功能模块。通过合理的文件夹结构和命名规则,开发人员可以更高效地管理和扩展项目。 ... [详细]
  • dotnet 通过 Elmish.WPF 使用 F# 编写 WPF 应用
    本文来安利大家一个有趣而且强大的库,通过F#和C#混合编程编写WPF应用,可以在WPF中使用到F#强大的数据处理能力在GitHub上完全开源Elmis ... [详细]
  • 获得头条Offer后,我感激的七个技术公众号
    是否感觉订阅的公众号过多,浏览时缺乏目标性,未能获取实质性的知识?本文将介绍如何精简公众号列表,提升信息吸收效率,并推荐几个高质量的技术公众号。 ... [详细]
author-avatar
elgin2010
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有