ffmpeg播放器(一) 视频解码与播放

news/2024/5/5 23:17:21/文章来源:https://blog.csdn.net/weixin_58308529/article/details/127043470

1、环境搭建

首先需要导入所需要的包include、armeabi-v7a。

 然后跟项目建立连接,在CMakeList.txt,并做了相关的解释:

cmake_minimum_required(VERSION 3.4.1)file(GLOB source_file src/main/cpp/*.cpp) //cpp文件下所有的包
# Declares and names the project.add_library( # Sets the name of the library.native-lib# Sets the library as a shared library.SHARED# Provides a relative path to your source file(s).${source_file})include_directories(src/main/cpp/include)set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L${CMAKE_SOURCE_DIR}/src/main/cpp/libs/${ANDROID_ABI}") 导入libs下的所有的包target_link_libraries( # Specifies the target library.native-libavfilter   avformat avcodec   avutil swresample swscale# Links the target library to the log library# included in the NDK.log  z  android)  //armeabi下的包

然后在build.gradle里面进行配置:

 ndk {abiFilters 'armeabi-v7a'}

然后在native-lib下导入看看能否成功。

extern  "C" {
#include <libavformat/avformat.h>
}

下面正式进入视频解码与播放的阶段:

准备阶段:

首先在创建一个类,在里面先写好准备、开始、画布等功能。

package com.example.player08;import android.media.MediaPlayer;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;import androidx.annotation.NonNull;/*提供java 进行播放 停止等操作*/
public class DNPlayer implements SurfaceHolder.Callback {static {System.loadLibrary("native-lib");}private String dataSource;private SurfaceHolder holder;private OnPrepareListener listener;/*** 让使用 设置播放的文件 或者 直播地址*/public void setDataSource(String dataSource) {this.dataSource = dataSource;}/*** 设置播放显示的画布** @param surfaceView*/public void setSurfaceView(SurfaceView surfaceView) {holder = surfaceView.getHolder();holder.addCallback(this);}public void onError(int errorCode){System.out.println("Java接到回调:"+errorCode);}public void onPrepare(){if (null != listener){listener.onPrepare();}}public void setOnPrepareListener(OnPrepareListener listener){this.listener = listener;}public interface OnPrepareListener{void onPrepare();}/*** 准备好 要播放的视频*/public void prepare() {native_prepare(dataSource);}/*** 开始播放*/public void start() {native_start();}/*** 停止播放*/public void stop() {}public void release() {holder.removeCallback(this);}/*** 画布创建好了** @param holder*/@Overridepublic void surfaceCreated(SurfaceHolder holder) {}/*** 画布发生了变化(横竖屏切换、按了home都会回调这个函数)** @param holder* @param format* @param width* @param height*/@Overridepublic void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {native_setSurface(holder.getSurface());}/*** 销毁画布 (按了home/退出应用/)** @param holder*/@Overridepublic void surfaceDestroyed(SurfaceHolder holder) {}native void native_prepare(String dataSource);
}

在MainActivity里面进行地址获取等信息:

package com.example.player08;import androidx.appcompat.app.AppCompatActivity;import android.os.Bundle;
import android.view.SurfaceView;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;import com.example.player08.databinding.ActivityMainBinding;public class MainActivity extends AppCompatActivity {// Used to load the 'player08' library on application startup.private DNPlayer dnPlayer;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate( savedInstanceState );setContentView( R.layout.activity_main );SurfaceView surfaceView=findViewById( R.id.surfaceView );dnPlayer=new DNPlayer();dnPlayer.setSurfaceView(surfaceView);dnPlayer.setDataSource("rtmp://47.94.57.236/myapp/");
//        dnPlayer.setDataSource("rtmp://live.hkstv.hk.lxdns.com/live/hks");dnPlayer.setOnPrepareListener(new DNPlayer.OnPrepareListener() {@Overridepublic void onPrepare() {runOnUiThread(new Runnable() {@Overridepublic void run() {Toast.makeText(MainActivity.this, "可以开始播放了", Toast.LENGTH_LONG).show();}});dnPlayer.start();}});}public void start(View view) {dnPlayer.prepare();}
}

接下来开始进行c++的编写。

首先,native-lib只是一个桥梁,只是负责传输信息,然后和c++进行连接。

首先在native里面创建播放器:

extern "C"
JNIEXPORT void JNICALL
Java_com_example_player08_DNPlayer_native_1prepare(JNIEnv *env, jobject instance, jstring dataSource_) {const char *dataSource=env->GetStringUTFChars(dataSource_,0);//创建播放器ffmpeg = new DNFFMPEG( dataSource);env->ReleaseStringUTFChars(dataSource_, dataSource);
}

 接下来创建DNFFMPEH.h和.cpp,视频解码与播放和音频的解码与播放主要在里面进行。

首先native-lib传送的数据datasource需要拷贝到DNFFMPEG中,以防止信息被处理,传出一个空数据。

DNFFMPEG::DNFFMPEG(JavaCallHelper *callHelper,const char *dataSource) { //构造方法//this->dataSource=const_cast<char *>(dataSource);//不能这么实用,因为native-lib里面会释放dataSource,会造成指针悬空//防止 dataSourec参数 指向的内存被释放//strlen 获得字符串的长度 不包括\0this->dataSource=new char [strlen(dataSource)+1];//进行内存的拷贝strcpy(this->dataSource,dataSource); //拷贝
}DNFFMPEG::~DNFFMPEG() { //析构方法//释放DELETE(dataSource);
}

创建线程准备视频的解码:

void DNFFMPEG::prepare() {//创建一个线程pthread_create(&pid,0, task_prepare, this);
}
void* task_prepare(void *args){DNFFMPEG *ffmpeg=static_cast<DNFFMPEG *>(args);ffmpeg->_prepare(); //为了方便起见,防止每次调用都需要ffmpeg->  创建有个新的线程return 0;}

同时在DNFFMPEG.里面进行相应的注册:

public:DNFFMPEG(const char* dataSource); //接收播放的地址~DNFFMPEG();void prepare(); //解析datasource 地址void _prepare();private:char *dataSource;pthread_t pid;pthread_t pid_play;
};

在解码过程中,C++会出现报错现象,需要传递给java代码,所以需要进行java回调、签名来讲c++中的错误传递给java代码。

在java代码中加入onError()方法:

 public void onError(int errorCode){System.out.println("Java接到回调:"+errorCode);}public interface OnPrepareListener{void onPrepare();}

然后在cpp文件中创建JavaCallHelper.cpp/.h来实现java的反射。

在编写该代码时,需要注意两点。一个是传递什么参数,为什么传递该参数的问题,已经在代码中详细注释了。另一个问题是需要判断在子线程还是在主线程,在主线程可以直接使用env进行java回调,在子线程,需要借助vm进行java方法的回调,具体看代码:

JavaCallHelper.h代码:

//
// Created by 14452 on 2022/9/16.
//#ifndef PLAYER08_JAVACALLHELPER_H
#define PLAYER08_JAVACALLHELPER_H#include <jni.h>class JavaCallHelper { //用来将c++里面程序报错传给java
public://instance:表示反射的对象 dnplayer env:简单调用接口函数 vm是为了跨线程JavaCallHelper(JavaVM *vm,JNIEnv* env,jobject instance);~JavaCallHelper();//回调javavoid onError(int thread,int errorCode); //第一个参数判断是否在主线程还是子线程,第二个参数是错误信息void onPrepare(int thread);
private:JavaVM *vm;JNIEnv *env;jobject instance;jclass clazz;jmethodID onErrorID;jmethodID onPrepareID;
};#endif //PLAYER08_JAVACALLHELPER_H

JavaCallHelper.cpp:

//
// Created by 14452 on 2022/9/16.
//#include "JavaCallHelper.h"
#include "macro.h"JavaCallHelper::JavaCallHelper(JavaVM *vm, JNIEnv *env, jobject instance) {this->vm=vm;//如果在主线程 直接进行env回调,不需要使用java vmthis->env=env;//一旦涉及到jobject跨方法 跨线程 就需要创建全局引用this->instance=env->NewGlobalRef(instance);clazz=env->GetObjectClass(instance);onErrorID=env->GetMethodID(clazz,"onError","(I)V"); //获取java里面onerror方法onPrepareID=env->GetMethodID(clazz,"onPrepare","()V");
}JavaCallHelper::~JavaCallHelper() {env->DeleteGlobalRef(instance);
}void JavaCallHelper::onError(int thread, int errorCode) {//主线程if(thread==THREAD_MAIN){env->CallVoidMethod(instance,onErrorID,errorCode);} else{//子线程JNIEnv *env;//获得属于我这一个线程的jnienvvm->AttachCurrentThread(&env,0);env->CallVoidMethod(instance,onErrorID,errorCode);vm->DetachCurrentThread();}
}void JavaCallHelper::onPrepare(int thread) {//主线程 直接使用envif(thread==THREAD_MAIN){env->CallVoidMethod(instance,onPrepareID);} else{//子线程 需要使用 vmJNIEnv *env;//获得属于我这一个线程的jnienvvm->AttachCurrentThread(&env,0);env->CallVoidMethod(instance,onPrepareID);vm->DetachCurrentThread();}
}

在native-lib创建javaCallHelper将javaCallHelper传递给DNFFMEPG.cpp

JavaVM *javaVm=0;
int JNI_OnLoad(JavaVM *vm,void *r){javaVm=vm;return JNI_VERSION_1_6;
}extern "C"
JNIEXPORT void JNICALL
Java_com_example_player08_DNPlayer_native_1prepare(JNIEnv *env, jobject instance, jstring dataSource_) {const char *dataSource=env->GetStringUTFChars(dataSource_,0);//创建播放器JavaCallHelper *helper = new JavaCallHelper(javaVm, env, instance);ffmpeg = new DNFFMPEG(helper, dataSource);ffmpeg->prepare();env->ReleaseStringUTFChars(dataSource_, dataSource);
}

以上基本上实现java方法的回调。

接下来在音频解码个视频解码公用的一部分,如打开流媒体、打开编码器等操作。

void DNFFMPEG::_prepare() {// 初始化网络 让ffmpeg能够使用网络avformat_network_init();//1、打开媒体地址(文件地址、直播地址)// AVFormatContext  包含了 视频的 信息(宽、高等)formatContext = 0;//文件路径不对 手机没网int ret = avformat_open_input(&formatContext, dataSource, 0, 0);//ret不为0表示 打开媒体失败if (ret != 0) {LOGE("打开媒体失败:%s", av_err2str(ret));callHelper->onError(THREAD_CHILD, FFMPEG_CAN_NOT_OPEN_URL);return;}//2、查找媒体中的 音视频流 (给 contxt里的 streams等成员赋)ret = avformat_find_stream_info(formatContext, 0);// 小于0 则失败if (ret < 0) {LOGE("查找流失败:%s", av_err2str(ret));callHelper->onError(THREAD_CHILD, FFMPEG_CAN_NOT_FIND_STREAMS);return;}//nb_streams :几个流(几段视频/音频)for (int i = 0; i < formatContext->nb_streams; ++i) {//可能代表是一个视频 也可能代表是一个音频AVStream *stream = formatContext->streams[i];//包含了 解码 这段流 的各种参数信息(宽、高、码率、帧率)AVCodecParameters *codecpar = stream->codecpar;//无论视频还是音频都需要干的一些事情(获得解码器)// 1、通过 当前流 使用的 编码方式,查找解码器AVCodec *dec = avcodec_find_decoder(codecpar->codec_id);if (dec == NULL) {LOGE("查找解码器失败:%s", av_err2str(ret));callHelper->onError(THREAD_CHILD, FFMPEG_FIND_DECODER_FAIL);return;}//2、获得解码器上下文AVCodecContext *context = avcodec_alloc_context3(dec);if (context == NULL) {LOGE("创建解码上下文失败:%s", av_err2str(ret));callHelper->onError(THREAD_CHILD, FFMPEG_ALLOC_CODEC_CONTEXT_FAIL);return;}//3、设置上下文内的一些参数 (context->width)
//        context->width = codecpar->width;
//        context->height = codecpar->height;ret = avcodec_parameters_to_context(context, codecpar);//失败if (ret < 0) {LOGE("设置解码上下文参数失败:%s", av_err2str(ret));callHelper->onError(THREAD_CHILD, FFMPEG_CODEC_CONTEXT_PARAMETERS_FAIL);return;}// 4、打开解码器ret = avcodec_open2(context, dec, 0);if (ret != 0) {LOGE("打开解码器失败:%s", av_err2str(ret));callHelper->onError(THREAD_CHILD, FFMPEG_OPEN_DECODER_FAIL);return;}//单位AVRational time_base=stream->time_base;//音频if (codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {//0audioChannel = new AudioChannel(i,context,time_base);} else if (codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {//1//帧率:单位时间内 需要显示多少个图像AVRational  frame_rate=stream->avg_frame_rate;int fps= av_q2d(frame_rate);videoChannel = new VideoChannel(i,context,time_base,fps);videoChannel->setRenderFrameCallback(callback);}}//没有音视频  (很少见)if (!audioChannel && !videoChannel) {LOGE("没有音视频");callHelper->onError(THREAD_CHILD, FFMPEG_NOMEDIA);return;}// 准备完了 通知java 你随时可以开始播放callHelper->onPrepare(THREAD_CHILD);
};

以上信息完成后,需要将已准备好的信息传递给java层,所以需要创建prepare方法和上面error报错的方法差不多。

为了简化代码,将videoChannel和AudioChannel共有的参数放进一个新建的类BaseChannel。第一个参数id,0代表音频,1代表视频。

然后开始进行播放:

void DNFFMPEG::start() {// 正在播放isPlaying = 1;//    //启动声音的解码与播放if (audioChannel){audioChannel->play();}if (videoChannel){if (audioChannel){videoChannel->play();}}pthread_create(&pid_play, 0, play, this);
}void *play(void *args) {DNFFMPEG *ffmpeg = static_cast<DNFFMPEG *>(args);ffmpeg->_start();return 0;
}/*** 专门读取数据包*/
void DNFFMPEG::_start() {//1、读取媒体数据包(音视频数据包)int ret;while (isPlaying) {AVPacket *packet = av_packet_alloc();ret = av_read_frame(formatContext, packet);//=0成功 其他:失败if (ret == 0) {//stream_index 这一个流的一个序号if (audioChannel && packet->stream_index == audioChannel->id) {audioChannel->packets.push(packet);}if (videoChannel && packet->stream_index == videoChannel->id) {videoChannel->packets.push(packet);}} else if (ret == AVERROR_EOF) {//读取完成 但是可能还没播放完} else {//}}};

packet申请的内存在堆中,需要释放内存,且packet参数公用在音频和视频的解码中,所以在baseChannel里面进行内存释放。

/*** 释放 AVPacket* @param packet*/static void releaseAvPacket(AVPacket** packet) {if (packet) {av_packet_free(packet);//为什么用指针的指针?// 指针的指针能够修改传递进来的指针的指向*packet = 0;}}

解码:取出数据包->将包丢给解码器->从解码器中读取 解码后的数据包

播放(目标是先将数据包转换成RGBA,通过sws_scale进行转换,然后在ANativeWindow里面进行画画。(注意:要是用同步锁,防止在画画过程中被释放)

解码:

void VideoChannel::play() {isPlaying = 1;frames.setWork(1);packets.setWork(1);//1、解码pthread_create(&pid_decode, 0, decode_task, this);//2、播放pthread_create(&pid_render, 0, render_task, this);
}void *decode_task(void *args) {VideoChannel *channel = static_cast<VideoChannel *>(args);channel->decode();return 0;
}//解码
void VideoChannel::decode() {AVPacket *packet = 0;while (isPlaying) {//取出一个数据包int ret = packets.pop(packet);if (!isPlaying) {break;}//取出失败if (!ret) {continue;}//把包丢给解码器ret = avcodec_send_packet(avCodecContext, packet);releaseAvPacket(&packet);//重试if (ret != 0) {break;}//代表了一个图像 (将这个图像先输出来)AVFrame *frame = av_frame_alloc();//从解码器中读取 解码后的数据包 AVFrameret = avcodec_receive_frame(avCodecContext, frame);//需要更多的数据才能够进行解码if (ret == AVERROR(EAGAIN)) {continue;} else if(ret != 0){break;}//再开一个线程 来播放 (流畅度)frames.push(frame);}releaseAvPacket(&packet);
}

播放:

void VideoChannel::play() {isPlaying = 1;frames.setWork(1);packets.setWork(1);//1、解码pthread_create(&pid_decode, 0, decode_task, this);//2、播放pthread_create(&pid_render, 0, render_task, this);
}void *render_task(void *args) {VideoChannel *channel = static_cast<VideoChannel *>(args);channel->render();return 0;
}//播放
void VideoChannel::render() {//目标: RGBAswsContext = sws_getContext(avCodecContext->width, avCodecContext->height,avCodecContext->pix_fmt,avCodecContext->width, avCodecContext->height,AV_PIX_FMT_RGBA,SWS_BILINEAR,0,0,0);AVFrame* frame = 0;//指针数组uint8_t *dst_data[4];int dst_linesize[4];av_image_alloc(dst_data, dst_linesize,avCodecContext->width, avCodecContext->height,AV_PIX_FMT_RGBA, 1);while (isPlaying){int ret = frames.pop(frame);if (!isPlaying){break;}//src_linesize: 表示每一行存放的 字节长度sws_scale(swsContext, reinterpret_cast<const uint8_t *const *>(frame->data),frame->linesize, 0,avCodecContext->height,dst_data,dst_linesize);//回调出去进行播放callback(dst_data[0],dst_linesize[0],avCodecContext->width, avCodecContext->height);releaseAvFrame(&frame);}av_freep(&dst_data[0]);releaseAvFrame(&frame);
}

在native-lib中画画:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER ;//画画
void render(uint8_t *data, int lineszie, int w, int h) {pthread_mutex_lock(&mutex);if (!window) {pthread_mutex_unlock(&mutex);return;}//设置窗口属性ANativeWindow_setBuffersGeometry(window, w,h,WINDOW_FORMAT_RGBA_8888);ANativeWindow_Buffer window_buffer;if (ANativeWindow_lock(window, &window_buffer, 0)) {ANativeWindow_release(window);window = 0;pthread_mutex_unlock(&mutex);return;}//填充rgb数据给dst_datauint8_t *dst_data = static_cast<uint8_t *>(window_buffer.bits);// stride:一行多少个数据(RGBA) *4int dst_linesize = window_buffer.stride * 4;//一行一行的拷贝for (int i = 0; i < window_buffer.height; ++i) {//memcpy(dst_data , data, dst_linesize);memcpy(dst_data + i * dst_linesize, data + i * lineszie, dst_linesize);}ANativeWindow_unlockAndPost(window);pthread_mutex_unlock(&mutex);
}extern "C"
JNIEXPORT void JNICALL
Java_com_example_player08_DNPlayer_native_1setSurface(JNIEnv *env, jobject instance, jobject surface) {pthread_mutex_lock(&mutex);if (window){ //判断之前是否有surface//把老的释放ANativeWindow_release(window);window=0;}window=ANativeWindow_fromSurface(env,surface);pthread_mutex_unlock(&mutex);
}

然后采用EV录屏进行在线播放:

 

 链接:https://pan.baidu.com/s/1au6zAAa7-Fdh6uNggPTRig 
提取码:j7qn 
 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_203669.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

打印字符串、排序、引用调用

设计一个函数print打印字符串&#xff0c;如果只传string型参数s&#xff0c;则字符串长度跟10比较&#xff0c;大于10&#xff0c;打印前10个字符&#xff0c;小于10&#xff0c;全部输出s&#xff1b;如果传string型参数s和int型n&#xff0c;则字符串长度跟n比较&#xff0c…

教师在初中数学课堂该如何有效提问(内有示例)

数学是中学数学的一个重要组成部分&#xff0c;它在培养学生的思维和创造力方面具有很大的作用。 在我国当前的教育体制改革与发展中&#xff0c;中学数学教学应按照新课标的要求&#xff0c;改变教学方法&#xff0c;提高学生的数学综合素质。但是&#xff0c;在实际的教学过…

高性能数据访问中间件 OBProxy(五):一文讲透数据路由

上篇文章我们介绍了 OBProxy 的连接管理&#xff0c;通过连接管理功能&#xff0c;OBProxy 和 OBServer 联系起来&#xff0c;同时 OBProxy 屏蔽了连接的复杂性&#xff0c;让用户使用起来和单机数据库一样简单。完成接入后&#xff0c;接下来的一个重要功能就是数据路由&#…

[luogu3980]志愿者招募

记$x_{i}$为第$i$类志愿者数量$,y_{j}=\sum_{j\in [s_{i},t_{i}]}x_{i}-a_{j}$​,则问题即$$\forall i\in [1,m],x_{i}\ge 0\\\forall j\in [1,n],y_{j}\ge 0\\y_{1}-\sum_{s_{i}=1}x_{i}=-a_{1}\\\sum_{t_{i}=n}x_{i}-y_{n}=a_{n}\\\forall j\in [2,n],y_{j}+\sum_{t_{i}=j-1…

redis主从+哨兵+集群模式搭建详解

一、redis主从安装 1. 下载redis Download | Redis 我这里选择的是redis-6.2.7版本 这里三台机器&#xff0c;都需要安装redis node1 192.168.157.128 node2 192.168.157.129 node3 192.168.157.130 2. 安装redis # 解压redis tar -zxvf redis-6.2.7.tar.gz # 编译安装…

数据分析 面经(已拿到offer)

北航计算机专业&#xff08;计院太卷&#xff0c;现考虑转向信息安全方向&#xff09;本科二年级&#xff0c;闲来无事找份日常实习试试水 考虑数分岗也是因为楼主目前大二&#xff0c;专业课学习不够深入&#xff0c;开发技术尚不成熟&#xff0c;而sql、excel和数据可视化比…

四元数是什么

1、四元数的构成 四元数是简单的超复数&#xff0c;由实数加上三个虚数单位组成&#xff0c;主要用于在三维空间中表示旋转 四元数原理包含大量数学相关知识&#xff0c;较为复杂&#xff0c;比如&#xff1a;复数、四维空间等等 因此此文章只对其基本构成和基本公式进行学习…

多视图属性网络异常检测系列一

论文《Deep Anomaly Detection on Attributed Networks》近期会对多视图属性网络异常检测系列进行学习记录 这篇虽然不是多视图的,但可以说是属性网络上异常检测的典型,已是近年属性网络异常检测必参考的一篇文献。背景 由于属性网络中附加的节点属性补充了知识发现中的原始网…

.Net Redis的秒杀Dome和异步执行

1.先到官网下载Redis部署好 Redis 教程 | 菜鸟教程 2.创建一个上游业务项目&#xff08;这里用控制台项目了&#xff0c;Framwork4.7.2&#xff09; NuGet包下载SerivceStack.Redis 创建一个RedisMessgaeQueue(Redis连接帮助类) using ServiceStack.Redis; using System;name…

PCIe系列专题之三:3.0 数据链路层概述

一、故事前传 之前我们讲了对PCIe的一些基础概念作了一个宏观的介绍&#xff0c;了解了PCIe是一种封装分层协议&#xff08;packet-based layered protocol),主要包括事务层&#xff08;Transaction layer), 数据链路层&#xff08;Data link layer)和物理层&#xff08;Physi…

MySQL 常用数据类型说明

目录 MySQL中常用的数据类型 整型 整型声明 整型属性 整型的选择 浮点型 定点数类型 浮点数和定点数的区别 时间日期类型 DATE类型 TIME类型 DATETIME类型 YEAR类型 文本字符串 CHAR与VARCHAR类型 TEXT类型 ​编辑 枚举类型(ENUM) MySQL中常用的数据类型 数据类…

直播平台怎么搭建,实现js开光灯效果

直播平台怎么搭建,实现js开光灯效果<!DOCTYPE html><html><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>点击切换灯亮</t…

Spring容器与依赖注入(DI)

1 Spring框架简介 1.1 什么是Spring Spring框架是一个开源的轻量级的DI和AOP容器框架&#xff0c;致力于简化企业级应用开发&#xff0c;让开发者使用简单的Java Bean来实现从前只有EJB才能实现的功能。 1.2 为什么要使用Spring Spring堪称Java世界中最强大的框架&#xff0c;…

单调栈题目:柱状图中最大的矩形

文章目录题目标题和出处难度题目描述要求示例数据范围解法思路和算法代码复杂度分析题目 标题和出处 标题&#xff1a;柱状图中最大的矩形 出处&#xff1a;84. 柱状图中最大的矩形 难度 7 级 题目描述 要求 给定整数数组 heights\texttt{heights}heights 表示柱状图中…

正弦信号发生器的设计

目 录 1 引言 1 2 总体结构设计 2 2.1 单片机概述 2 2.1.1 单片机的发展 2 2.1.2 单片机的用途 3 2.2 系统设计的功能 3 2.3 波形发生和输出频率的方法 4 2.3.1 波形发生的方法 4 2.3.2 输出频率的方法 4 3 系统硬件设计 5 3.1 硬件电路芯片的选择 5 3.1.1 CPU芯片 AT89C51 5 3…

MyBatis中的复杂映射

上一章中实现的MyBatis对象映射较为简单&#xff0c;对象中的属性和数据库中的表字段是一一对应的&#xff08;无论数量和名称都完全一样&#xff09;&#xff0c;如果对象中的属性名和表中的字段名不一致怎么办&#xff1f;又或者Java对象中存在复杂类型属性&#xff08;即类似…

百分点数据科学产教融合计划继续扩大招募

从全球发展来看&#xff0c;数字经济已经成为重组全球要素资源、重塑全球经济结构、改变全球竞争格局的关键力量&#xff0c;是全球共同的发展战略。作为新经济中的数据科学&#xff0c;伴随社会各领域对数字人才需求的与日俱增&#xff0c;也成为了这一波科技革命中的人才竞争…

中国新出海故事:人、疫情与纽带

【潮汐商业评论/原创】 《枪炮、病菌与钢铁》中对人类社会发展的洞察与新千禧年发生了奇妙的呼应&#xff0c;战争、疫情成为了这十年的历史注脚&#xff0c;但时代的车轮总是滚滚向前的&#xff0c;新的世界版图里&#xff0c;全球化、数字化的浪潮构成了新世界跳动的脉搏&am…

第2章 ROS 通信机制 4 —— 常用命令

文章目录1 应用场景2 rosnode 功能包 plumbing_pub_sub编译执行3 rostopic 功能包 plumbing_pub_sub编译执行4 rosservice (服务通信) 功能包 plumbing_server_client编译执行5 rosmsg (话题通信) 功能包 plumbing_pub_sub编译执行6 rossrv (服务通信) 功能包 plumbing_server_…

常见网络知识面试题总结

&#x1f353;个人主页&#xff1a;个人主页 &#x1f352;系列专栏&#xff1a;C/C基础与进阶 &#x1f4ac;推荐一款模拟面试、刷题神器&#xff0c;从基础到大厂面试题&#x1f449;点击跳转刷题网站进行注册学习 目录 1、OSI七层模型与TCPIP四层模型是什么&#xff1f; 2…