NDK OpenGL仿抖音极快极慢录制特效视频

news2024/11/25 6:34:13

NDK​系列之OpenGL仿抖音极快极慢录制特效视频,本节主要是在上一节OpenGL代码架构上增加极快极慢等特效的视频录制功能。

实现效果:

实现逻辑:

在上一节的特效效果的基础上,使用MediaCodec和自定义EGL,将效果视频录制保存到本地.mp4文件。

本节主要内容:

1.MediaCode介绍;

2.EGL的理解;

3.自定义MyMediaRecord;

4.自定义MyEGL;

源码:

NdkOpenGLPlay: NDK OpenGL渲染画面效果

一、MediaCode

MediaCodec是Android 4.1.2(API 16)提供的一套编解码API。

1)它的使用非常简单,它存在一个输入缓冲区与一个输出缓冲区,在编码时我们将数据塞入输入缓冲区,然后从输出缓冲区取出编码完成后的数据就可以了。

2)除了直接操作输入缓冲区之外,还有另一种方式来告知MediaCodec需要编码的输入数据,那就是:

public native final Surface createInputSurface();

以上接口创建一个Surface,然后我们在这个Surface中作画(输入源的直接绑定),MediaCodec就能够自动的编码Surface中的画作,我们只需要从输出缓冲区取出编码完成之后的数据。

3)录制我们在另外一个线程中进行(录制线程),所以录制的EGL环境和显示的EGL环境(GLSurfaceView,显示线程)是两个独立的工作环境,他们又能够共享上下文资源:显示线程中使用的texture等,需要能够在录制线程中操作 (通过录制线程中使用OpenGL绘制到MediaCodec的Surface)。

在这个线程中我们需要自己来:
1、配置录制使用的EGL环境(参照GLSurfaceView是怎么配置的)
2、将显示的图像绘制到MediaCodec的Surface中
3、编码(H.264)与复用(封装mp4)的工作

二、EGL

通俗上讲,OpenGL是一个操作GPU的API,它通过驱动向GPU发送相关指令,控制图形渲染管线状态机的运行状态。但OpenGL需要本地视窗系统进行交互,这就需要一个中间控制层,最好与平台无关。

EGL 因此被独立的设计出来,它作为OpenGL ES和本地窗口的桥梁。EGL 是 OpenGL ES(嵌入式)和底层 Native 平台视窗系统之间的接口。EGL API 是独立于OpenGL ES各版本标准的独立API ,其主要作用是为OpenGL指令创建 Context 、绘制目标Surface 、
配置Framebuffer属性、Swap提交绘制结果等。此外,EGL为GPU厂商和OS窗口系统之间提供了一个标准配置接口。

一般来说,OpenGL ES 图形管线的状态被存储于 EGL 管理的一个Context中。而Frame Buffers 和其他绘制 Surfaces 通过 EGL API进行创建、管理和销毁。EGL 同时也控制和提供了对设备显示和可能的设备渲染配置的访问。EGL标准是C的,在Android系统Java层封装了相关API。

三、MyMediaRecord

录制视频的工具类,其内部封装了MediaCodec;

1)当自定义渲染器MyGlRenderer,回调onSurfaceCreated()函数时,初始化录制工具类;

mMediaRecorder = new MyMediaRecorder(480, 800, FileUtil.getFilePath(), eglContext,
                myGLSurfaceView.getContext());

MyMediaRecord.java

public MyMediaRecorder(int width, int height, String outputPath, EGLContext eglContext, Context context) {
	mWidth = width;
	mHeight = height;
	mOutputPath = outputPath;
	mEglContext = eglContext;
	mContext = context;
}

2)当自定义渲染器MyGlRenderer,回调onDrawFrame()函数时,启动录制,真正执行编码的函数;

mMediaRecorder.encodeFrame(textureId, mSurfaceTexture.getTimestamp());

MyMediaRecord.java

public void encodeFrame(final int textureId, final long timestamp) {
	// 当用户点击开始录制时才需要编码,预览时不需要
	if (!isStart) {
		return;
	}
	if (mHandler != null) {
		mHandler.post(new Runnable() {
			@Override
			public void run() {
				// 画到 虚拟屏幕上
				if (null != mEGL) {
					mEGL.draw(textureId, timestamp);
				}

				// 从编码器中去除数据 编码 封装成derry_xxxx.mp4文件成果
				getEncodedData(false);
			}
		});
	}
}

/**【MediaCodec输出缓冲区】
 * 获取编码后的数据,写入封装成 derry_xxxx.mp4
 * @param endOfStream 流结束的标记  true代表结束,false才继续工作
 */
private void getEncodedData(boolean endOfStream) {
	if (endOfStream) {
		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
			mMediaCodec.signalEndOfInputStream(); // 让MediaCodec的输入流结束
		}
	}

	// MediaCodec的输出缓冲区
	if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
		MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();

		while (true) {
			// 使输出缓冲区出列,最多阻止“timeoutUs”微秒。 // 10ms
			int status = mMediaCodec.dequeueOutputBuffer(bufferInfo, 10_000);

			if(status == MediaCodec.INFO_TRY_AGAIN_LATER) { // 稍后再试的意思
				// endOfStream = true, 要录制,继续循环,继续取新的编码数据
				if(!endOfStream){
					break;
				}
			} else if(status == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { // 输出格式已更改
				MediaFormat outputFormat = mMediaCodec.getOutputFormat();
				if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
					index = mMediaMuxer.addTrack(outputFormat);
					mMediaMuxer.start();// 启动封装器
				}
			} else if(status == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED){ // 输出缓冲区已更改

			} else {
				// 成功取到一个有效数据
				ByteBuffer outputBuffer = null;
				if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
					outputBuffer = mMediaCodec.getOutputBuffer(status);
				}
				if (null == outputBuffer){
					throw new RuntimeException("getOutputBuffer fail");
				}

				if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0){
					bufferInfo.size = 0; // 如果是配置信息
				}

				// TODO 下面开始写入操作
				if(bufferInfo.size != 0){
					// 除以大于1的 : 加速
					// 小于1 的: 减速
					bufferInfo.presentationTimeUs = (long)(bufferInfo.presentationTimeUs / mSpeed);
					// 可能会出现类似:TimeUs < lastTimeUs xxxxxx for video Track
					if(bufferInfo.presentationTimeUs <= lastTimeUs){
						bufferInfo.presentationTimeUs = (long)(lastTimeUs + 1_000_000 /25/mSpeed);
					}
					lastTimeUs = bufferInfo.presentationTimeUs;

					// 偏移位置
					outputBuffer.position(bufferInfo.offset);
					// 可读写的总长度
					outputBuffer.limit(bufferInfo.offset + bufferInfo.size);
					try{
						// 写数据
						if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
							mMediaMuxer.writeSampleData(index, outputBuffer, bufferInfo);
						}
					}catch (Exception e){
						e.printStackTrace();
					}
				}
				// 释放输出缓冲区
				mMediaCodec.releaseOutputBuffer(status, false);
				// 编码结束
				if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0){
					break;
				}
			}
		} // while end
	}
}

3)用户按住拍Button时,事件分发到MyGlRenderer,调用MyMediaRecord开始录制

mMediaRecorder.start(speed);

MyMediaRecord.java

public void start(float speed) throws IOException {
	mSpeed = speed;
	/**
	 * 1.创建 MediaCodec 编码器
	 * type: 哪种类型的视频编码器
	 * MIMETYPE_VIDEO_AVC: H.264
	 */
	mMediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);

	/**
	 * 2.配置编码器参数
	 */
	// 视频格式
	MediaFormat videoFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC,  mWidth, mHeight);
	// 设置码率
	videoFormat.setInteger(MediaFormat.KEY_BIT_RATE, 1500_000);
	// 帧率
	videoFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 25);
	// 颜色格式 (从Surface中自适应)
	videoFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
	// 关键帧间隔
	videoFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 20);
	// 配置编码器
	mMediaCodec.configure(videoFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);

	/**
	 * 3.创建输入 Surface(虚拟屏幕)
	 */
	if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
		mInputSurface = mMediaCodec.createInputSurface();
	}

	/**
	 * 4, 创建封装器
	 */
	if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
		mMediaMuxer = new MediaMuxer(mOutputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
	}

	/**
	 * 5, 配置 EGL 环境
	 */
	HandlerThread handlerThread = new HandlerThread("MyMediaRecorder");
	handlerThread.start();
	Looper looper = handlerThread.getLooper();
	mHandler = new Handler(looper);
	mHandler.post(new Runnable() {
		@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR1)
		@Override
		public void run() {
			mEGL = new MyEGL(mEglContext, mInputSurface, mContext, mWidth, mHeight); // 在一个新的Thread中,初始化EGL环境
			mMediaCodec.start(); // 启动编码器
			isStart = true;
		}
	});
}

4)用户松开拍Button时,事件分发到MyGlRenderer,调用MyMediaRecord录制完成,获取编码后的数据,写入封装成.mp4文件

mMediaRecorder.stop();

MyMediaRecord.java

public void stop() {
	isStart = false;
	if (mHandler != null) {
		mHandler.post(new Runnable() {
			@Override
			public void run() {
				getEncodedData(true); // true代表:结束工作

				if (mMediaCodec != null){ // MediaCodec 释放掉
					if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
						mMediaCodec.stop();
						mMediaCodec.release();
					}
					mMediaCodec = null;
				}

				if (mMediaMuxer != null) { // 封装器 释放掉
					try{
						if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
							mMediaMuxer.stop();
							mMediaMuxer.release();
						}
					}catch (Exception e){
						e.printStackTrace();
					}
					mMediaMuxer = null;
				}

				if (mInputSurface != null) { // MediaCodec的输入画布/虚拟屏幕 释放掉
					mInputSurface.release();
					mInputSurface = null;
				}

				mEGL.release(); // EGL中间件 释放掉
				mEGL = null;
				if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
					mHandler.getLooper().quitSafely();
					mHandler = null;
				}
			}
		});
	}
}

四、MyEGL

管理EGL环境的工具类

1)用户按住拍Button时,事件分发到MyMediaRecord,初始化EGL环境

mEGL = new MyEGL(mEglContext, mInputSurface, mContext, mWidth, mHeight);

MyEGL.java

public MyEGL(EGLContext eglContext, Surface surface, Context context, int width, int height) {
	// 第一大步:创建EGL环境
	if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
		createEGL(eglContext);
	}

	// 第二大步:创建窗口(画布),绘制线程中的图像,直接往这里创建的mEGLSurface上面画
	int[] attrib_list = {EGL_NONE }; // 一定要有结尾符
	if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
		mEGLSurface = eglCreateWindowSurface(mEGLDisplay, // EGL显示链接
				mEGLConfig,  // EGL最终选择配置的成果
				surface,     // MediaCodec的输入Surface画布
				attrib_list, // 无任何配置,但也必须要传递 结尾符,否则人家没法玩
				0           // attrib_list的零下标开始读取
		); // 【关联的关键操作,关联(EGL显示链接)(EGL配置)(MediaCodec的输入Surface画布)】
	}

	// 第三大步:让 画布 盖住屏幕( 让 mEGLDisplay(EGL显示链接) 和 mEGLSurface(EGL的独有画布) 发生绑定关系)
	if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
		if(!eglMakeCurrent(mEGLDisplay, // EGL显示链接
				mEGLSurface, // EGL的独有画布 用来画
				mEGLSurface, // EGL的独有画布 用来读
				mEGLContext  // EGL的上下文
		)){
			throw new RuntimeException("eglMakeCurrent fail");
		}
	}

	// 4,往虚拟屏幕上画画
	mScreenFilter = new ScreenFilter(context);
	mScreenFilter.onReady(width, height);
}

private void createEGL(EGLContext share_eglContext) {
	// 1.获取EGL显示设备: EGL_DEFAULT_DISPLAY(代表 默认的设备 手机屏幕)
	mEGLDisplay = eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);

	// 2.初始化设备
	int[] version = new int[2]; // 主版本号,主版本号的位置下标,副版本号,副版本号的位置下标
	if(!eglInitialize(mEGLDisplay, version, 0, version, 1)){
		throw new RuntimeException("eglInitialize fail");
	}

	// 3.选择配置
	int[] attrib_list = {
			// key 像素格式 rgba
			EGL_RED_SIZE, 8,   // value 颜色深度都设置为八位
			EGL_GREEN_SIZE, 8, // value 颜色深度都设置为八位
			EGL_BLUE_SIZE, 8,  // value 颜色深度都设置为八位
			EGL_ALPHA_SIZE, 8, // value 颜色深度都设置为八位
			// key 指定渲染api类型
			EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, // value EGL 2.0版本号
			EGLExt.EGL_RECORDABLE_ANDROID, 1, // 告诉egl 以android兼容方式创建 surface
			EGL_NONE // 一定要有结尾符
	};
	EGLConfig[] configs = new EGLConfig[1];
	int[] num_config = new int[1];
	if(!eglChooseConfig(
			mEGLDisplay,      // EGL显示链接
			attrib_list,      // 属性列表
			0 ,  // attrib_list 从数组第零个下标开始找
			configs,          // 输出的配置选项成果
			0,     // configs 从数组第零个下标开始找
			configs.length,   // 配置的数量,只有一个
			num_config,       // 需要的配置int数组,他需要什么就给他什么
			0  // num_config 从数组第零个下标开始找
	)){
		throw new RuntimeException("eglChooseConfig fail");
	}
	mEGLConfig = configs[0]; // 最终EGL选择配置的成果 保存起来

	// 4.创建上下文
	int[] ctx_attrib_list = {
			EGL_CONTEXT_CLIENT_VERSION, 2, // EGL 上下文客户端版本 2.0
			EGL_NONE // 一定要有结尾符
	};
	mEGLContext = eglCreateContext(
			mEGLDisplay, // EGL显示链接
			mEGLConfig,  // EGL最终选择配置的成果
			share_eglContext, // 共享上下文, 绘制线程 GLThread 中 EGL上下文,达到资源共享
			ctx_attrib_list, // 传入上面的属性配置项
			0);
	if(null == mEGLContext || mEGLContext == EGL_NO_CONTEXT){
		mEGLContext = null;
		throw new RuntimeException("eglCreateContext fail");
	}
}

2)当自定义渲染器MyGlRenderer,回调onDrawFrame()函数时,启动录制,调用EGL在虚拟屏幕上渲染,交换缓冲区数据

mEGL.draw(textureId, timestamp);

MyEGL.java

public void draw(int textureId, long timestamp){
	// 在虚拟屏幕上渲染(为什么还要写一次同样的代码?答:这个是在EGL的专属线程中的)
	mScreenFilter.onDrawFrame(textureId);

	// 刷新时间戳(如果设置不合理,编码时会采取丢帧或降低视频质量方式进行编码)
	if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
		EGLExt.eglPresentationTimeANDROID(mEGLDisplay, mEGLSurface, timestamp);
	}

	// 交换缓冲区数据
	if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
		eglSwapBuffers(mEGLDisplay, mEGLSurface); // 绘制操作
	}
}

至此,OpenGL仿抖音极快极慢录制特效视频保存本地.mp4文件功能已完成。

源码:

NdkOpenGLPlay: NDK OpenGL渲染画面效果

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

CountDownLatch与Binder连接池

CountDownLatch与Binder连接池 CountDownLatch 如果现在有一个题,有5个数,这时候我想让这5个数同时都乘2,然后算出结果后再算它们的平均数 这时候就可以用CountDownLatch import java.util.concurrent.CountDownLatch; public class Example {public static void main(Stri…

总结853

学习目标&#xff1a; 月目标&#xff1a;5月&#xff08;张宇强化前10讲&#xff0c;背诵15篇短文&#xff0c;熟词僻义300词基础词&#xff09; 周目标&#xff1a;张宇强化前3讲并完成相应的习题并记录&#xff0c;英语背3篇文章并回诵 每日必复习&#xff08;5分钟&#…

leetcode(力扣)刷题笔记(c++)【下】

文章预览&#xff1a; 单调栈739. 每日温度496.下一个更大元素 I503. 下一个更大元素 II42. 接雨水84.柱状图中最大的矩形 额外题目1365.有多少小于当前数字的数字941. 有效的山脉数组1207. 独一无二的出现次数189. 轮转数组724. 寻找数组的中心下标922. 按奇偶排序数组 II 后续…

软考高级架构师笔记-3数据库

目录 1. 前言 & 更新2. 数据库基本概念3. E-R图与二维表4. 约束、范式5. 数据库新技术1. 前言 & 更新 前文回顾: 软考高级架构师笔记-1计算机硬件软考高级架构师笔记-2计算机软件(操作系统)本章考情: 数据库章节都会考3-5分左右,第二版教材上对应2.3.3和6,主要考…

软考A计划-真题-分类精讲汇总-第十四章(数据流图)

点击跳转专栏>Unity3D特效百例点击跳转专栏>案例项目实战源码点击跳转专栏>游戏脚本-辅助自动化点击跳转专栏>Android控件全解手册点击跳转专栏>Scratch编程案例 &#x1f449;关于作者 专注于Android/Unity和各种游戏开发技巧&#xff0c;以及各种资源分享&am…

Python每日一练(20230517) 最大连续1的个数 I\II\III

目录 1. 最大连续1的个数 I Max Consecutive Ones &#x1f31f; 2. 最大连续1的个数 II Max Consecutive Ones &#x1f31f;&#x1f31f; 3. 最大连续1的个数 III Max Consecutive Ones &#x1f31f;&#x1f31f; &#x1f31f; 每日一练刷题专栏 &#x1f31f; G…

RabbitMQ养成记 (5. MQ的topics模式)

主题模式 Topic类型的Exchange与Direct相比&#xff0c;都是可以根据RoutingKey把消息路由到不同的队列。只不过Topic类型Exchange可以让队列在绑定Routing key 的时候使用通配符&#xff01; Routingkey 一般都是有一个或多个单词组成&#xff0c;多个单词之间以”.”分割&a…

【夜莺(Flashcat)V6监控】2.夜莺告警相关: 多服务器多业务配置

介绍 本章侧重点是应用&#xff0c;根据大家不同业务、服务器部署众多等等&#xff1b;根据不同团队&#xff0c;不同业务进行划分&#xff1b;方便不同的团队负责自己职责内的工作&#xff1b; 比如我们场景如下&#xff1a; 三块业务&#xff1a;人工智能、医药、团购&…

分层强化学习 学习笔记

分层强化学习代码汇总 0.综述 《The Promise of Hierarchical Reinforcement Learning》分层强化学习的前景 强化学习 强化学习问题的设置&#xff1a; 有两个主角&#xff1a;一个代理和一个环境。环境是代理所生活和交互的地方。在每一次交互中&#xff0c;代理都能看到世…

深度解析:5G与未来天线技术 5G通信到底需要什么样的天线?

过去二十年&#xff0c;我们见证了移动通信从1G到4G LTE的转变。在这期间&#xff0c;通信的关键技术在发生变化&#xff0c;处理的信息量成倍增长。而天线&#xff0c;是实现这一跨越式提升不可或缺的组件。 按照业界的定义&#xff0c;天线是一种变换器&#xff0c;它把传输…

一图看懂 et_xmlfile 模块:一个用于创建大型XML文件的低内存库,资料整理+笔记(大全)

本文由 大侠(AhcaoZhu)原创&#xff0c;转载请声明。 链接: https://blog.csdn.net/Ahcao2008 一图看懂 et_xmlfile 模块&#xff1a;一个用于创建大型XML文件的低内存库&#xff0c;资料整理笔记&#xff08;大全&#xff09; &#x1f9ca;摘要&#x1f9ca;解释&#x1f9ca…

网络进阶学习:子网掩码及VLAN划分

网络进阶学习&#xff1a;子网掩码及VLAN划分 什么是子网&#xff1f;什么是子网掩码&#xff1f;什么是VLAN子网掩码和VLAN的关系小结 什么是子网&#xff1f; ⭐子网是将一个大的IP地址段划分成若干个小的IP地址段的网络。子网可以帮助网络管理员更好地管理网络&#xff0c;…

reids学习--redis常用命令

字符串string操作命令 Redis中字符串类型常用命令&#xff1a; 操作描述set key value设置指定key的值get key获取指定key的值setex key seconds value设置指定key的值&#xff0c;并将key的过期时间设为seconds秒(可用于验证码登录)setnx key value只有在key不存在时设置key…

玩转SAM语义分割之(2)显示特定的图片

文章目录 1. 使用matplotlib显示出完整彩色的掩码&#xff0c;并将其保存下来 2. 使用matplotlib显示出单张掩码&#xff0c;只保存面积大于一个阈值的掩码图片3. 对一整个文件夹中的图片进行处理&#xff0c;只保存面积大于一定阈值的掩码图片4. 查看特定坐标点处是否有mask掩…

Go语言的并发:goroutine和channel

目录 【Go 的并发方案&#xff1a;goroutine】 goroutine 的基本用法 【通道channel】 创建channel&#xff1a; 发送与接收变量&#xff1a; 关闭channel&#xff1a; 【channel的类型】 无缓冲channel和带缓冲channel 无缓冲channel 带缓冲channel nil channel 单…

随便聊聊 顺便晒一下我的听歌设备

平时最大的爱好就是听歌了&#xff0c;我平时的听歌类型挺多元化的&#xff0c;硬要说的话更偏向 Jpop、ACG、女声、轻音乐、大编制、交响乐&#xff0c;当然好听的都听不局限于类型。 又是30天一天不落O(∩_∩)O&#x1f604; 作为一个音乐爱好者&#xff0c;在听歌设备上面花…

Liunx压缩命令 - zip

zip命令 – 压缩文件 zip命令的功能是用于压缩文件&#xff0c;解压命令为unzip。通过zip命令可以将文件打包成.zip格式的压缩包&#xff0c;里面会包含文件的名称、路径、创建时间、上次修改时间等等信息&#xff0c;与tar命令相似。 语法格式&#xff1a;zip 参数 目标文件…

MySQL高级_第04章_逻辑架构

MySQL高级_第04章_逻辑架构 1. 逻辑架构剖析 1.1 服务器处理客户端请求 那服务器进程对客户端进程发送的请求做了什么处理&#xff0c;才能产生最后的处理结果呢&#xff1f;这里以查询请求为例展示&#xff1a; 下面具体展开看一下&#xff1a; 1.2 Connectors 1.3 第1…

Go 本地缓存 bigcache

​本地缓存已经算是数据库的常规设计了&#xff0c;因为数据直接缓存在机器内存中&#xff0c;避免昂贵的IO开销&#xff0c;数据的读取和写入快到不可思议。本地缓存常驻在内存中&#xff0c;就好像业务逻辑中声明的全局变量一样&#xff0c;不会被垃圾回收。但本地内存也会导…

JavaScript:new操作符

一、new操作符的作用 用于创建一个给定构造函数的实例对象 new操作符创建一个用户定义的对象类型的实例 或 具有构造函数的内置对象的实例。二、new一个构造函数的执行过程 2.1、创建一个空对象obj 2.2、将空对象的原型与构造函数的原型连接起来 2.3、将构造函数中的this绑定…