iOS图像处理----OpenGL ES之大长腿特效

目录

 一、代码部分概括

二、实现流程概括 

 1、第一次加载图片

 ①、GLKView初始化数据

这部分内容主要是初始化顶点数组、上下文以及顶点数组缓存区,需要在加载图片之前做好准备​编辑

②、加载图片 

③、绘制

 2、拉伸图片

①、滑块调整

②、图片拉伸过程 

3、图片保存

①、获取处理后的图片​编辑

②、 根据屏幕上的显示,重新计算顶点数据,为重绘提供数据支持

 ③、从帧缓存区中获取拉伸后的图片

④、存储到相册

 三、demo地址:


 

大长腿效果.gif

 一、代码部分概括

1、主控制器UI界面逻辑:主要是一些控件的操作

2、自定义的GLKView(LongLegView):用于显示 & 更新纹理图片

3、两个封装的工具类:
      ①、LongLegVertexAttribArrayBuffer:缓存区初始化&更新、准备绘制及绘制的封装

      ②、LongLegHelper:着色器编译及连接的封装

4、spring.vsh(顶点着色器)和spring.fsh(片元着色器)文件

二、实现流程概括 

实现的功能模块主要分为三部分:

1、第一次加载图片
2、拉伸图片
3、图片保存
下图为三部分的具体实现流程

实现效果流程图

 1、第一次加载图片

第一次图片的加载是使用GLKit加载,利用自定义的GLKView视图,通过计算图片的顶点数据,绘制图片并显示到屏幕上,整体的流程如图所示

主要分为两部分

1、GLKView初始化数据:为纹理的加载做准备工作

2、加载图片:view上加载未拉伸的原图

3、绘制

 ①、GLKView初始化数据
这部分内容主要是初始化顶点数组、上下文以及顶点数组缓存区,需要在加载图片之前做好准备
②、加载图片 

主要是使用GLKit加载原图,有以下几步 

其中,LongLegView加载图片updateImage的流程如图所示

在图片绘制之前,还需要通过原图的size以及设定的view的大小,计算纹理的顶点数据,其计算流程如下

  • 计算图片的宽高比:根据已知的图片大小和view的大小计算(图片的宽高比和view的宽高比之比)​​​​​​​
  • CGFloat ratio = (size.height / size.width) *

        (self.bounds.size.width / self.bounds.size.height);

  • 计算拉伸量 = (newHeight - (endY-startY)) * 纹理高度,即换算成纹理的拉伸量
  • 计算纹理坐标:根据传入的开始位置和结束位置的纹理坐标计算
  • 计算顶点坐标:需要先将传入的开始和结束的纹理坐标换换算为顶点坐标在计算顶点坐标之前,需要计算出开始位置和结束位置的坐标
  • 根据开始坐标和结束位置坐标计算8个点的顶点
③、绘制

调用GLKView的display方法,系统会自动回调GLKViewDelagate的代码方法glkView: drawInRect,具体的绘制流程如下

 2、拉伸图片

大长腿的拉伸,主要是通过两部分控制的,首先需要通过上下滑块选定需要拉伸的范围,其次需要滑动slider来决定拉伸范围是拉长还是缩短

①、滑块调整

这部分就是通过两个下上滑杆,确定拉伸范围,主要流程如下

②、图片拉伸过程 

确定拉伸范围后,需要通过操作slider来进行图片的拉伸,滑动slider会改变slder的值,继而回调slider的值改变方法,其实现流程吐下

在拉伸时,需要根据拉伸区域以及slder的值重新计算纹理的顶点数据,并重新绘制显示到屏幕上,这部分的计算与前面提及的计算原理是一致的,请参考前文计算原理

3、图片保存

前两两部分将的都是纹理图片的显示,且都是通过GLKit框架显示的,接下来我们需要说明的是如何存储拉伸后的纹理图片,这里就需要用到GLSL自定义的着色器来帮助我们完成图片的存储,这里的保存实际是利用context重绘来实现的,实现流程如下

由上图可知,图片的保存主要分为四部分

①、获取处理后的图片
//从帧缓存区中获取纹理图片文件; 获取当前的渲染结果
- (UIImage *)createResult {
    
//    1、根据屏幕显示的图片,重新获取顶点 & 纹理坐标
//    拉伸--显示:baseEffect、图片获取--存储:GLSL
//    :顶点&纹理坐标--GLSL绘制图片--帧缓存区--纹理(即新的图片),当次处理的结果作为下一次处理的初始图片
    [self resetTextureWithOriginWidth:self.currentImageSize.width originHeight:self.currentImageSize.height topY:self.currentTextureStartY bottomY:self.currentTextureEndY newHeight:self.currentNewHeight];
    
//    2、绑定帧缓存区
    glBindBuffer(GL_FRAMEBUFFER, self.tmpFrameBuffer);
    
//    3、获取新的图片size
    CGSize imageSize = [self newImageSize];
    
//    4、从帧缓存区中获取拉伸后的图片
    UIImage *image = [self imageFromTextureWithWidth:imageSize.width height:imageSize.height];
    
//    5、将帧缓存区绑定0,清空
    glBindBuffer(GL_FRAMEBUFFER, 0);
    
//    6、返回拉伸后的图片
    return image;
}
②、 根据屏幕上的显示,重新计算顶点数据,为重绘提供数据支持

主要分为两部分

  • 计算顶点数据:这部分的计算逻辑与calculateOriginTextureCoordWithTextureSize函数中逻辑是一致的
  • 通过GLSL将顶点数据渲染成一张新的纹理图片,并存储到帧缓存区

其实渲染到纹理的过程就是所谓的滤镜链,即当次处理的结果作为下一次处理的初始图片:
1、获取顶点&纹理坐标
2、通过GLSL利用1中的数据绘制图片
3、将图片存储到帧缓存区
4、从帧缓存区获取的新图片作为纹理,即为下一次处理的初始图片 

/**
 根据当前屏幕上的显示,来重新创建纹理
 
 @param originWidth 纹理的原始实际宽度
 @param originHeight 纹理的原始实际高度
 @param topY 0 ~ 1,拉伸区域的顶边的纵坐标
 @param bottomY 0 ~ 1,拉伸区域的底边的纵坐标
 @param newHeight 0 ~ 1,拉伸区域的新高度
 */
- (void)resetTextureWithOriginWidth:(CGFloat)originWidth
                       originHeight:(CGFloat)originHeight
                               topY:(CGFloat)topY
                            bottomY:(CGFloat)bottomY
                          newHeight:(CGFloat)newHeight {
   //1.新的纹理尺寸(新纹理图片的宽高)
   GLsizei newTextureWidth = originWidth;
   GLsizei newTextureHeight = originHeight * (newHeight - (bottomY - topY)) + originHeight;
   
   //2.高度变化百分比
   CGFloat heightScale = newTextureHeight / originHeight;
   
   //3.在新的纹理坐标下,重新计算topY、bottomY
   CGFloat newTopY = topY / heightScale;
   CGFloat newBottomY = (topY + newHeight) / heightScale;
   
   //4.创建顶点数组与纹理数组(逻辑与calculateOriginTextureCoordWithTextureSize 中关于纹理坐标以及顶点坐标逻辑是一模一样的)
   SenceVertex *tmpVertices = malloc(sizeof(SenceVertex) * kVerticesCount);
   tmpVertices[0] = (SenceVertex){{-1, 1, 0}, {0, 1}};
   tmpVertices[1] = (SenceVertex){{1, 1, 0}, {1, 1}};
   tmpVertices[2] = (SenceVertex){{-1, -2 * newTopY + 1, 0}, {0, 1 - topY}};
   tmpVertices[3] = (SenceVertex){{1, -2 * newTopY + 1, 0}, {1, 1 - topY}};
   tmpVertices[4] = (SenceVertex){{-1, -2 * newBottomY + 1, 0}, {0, 1 - bottomY}};
   tmpVertices[5] = (SenceVertex){{1, -2 * newBottomY + 1, 0}, {1, 1 - bottomY}};
   tmpVertices[6] = (SenceVertex){{-1, -1, 0}, {0, 0}};
   tmpVertices[7] = (SenceVertex){{1, -1, 0}, {1, 0}};
   
   
   ///下面开始渲染到纹理的流程(将结果渲染成一张新的纹理图片)
   
   //1. 生成帧缓存区;
   GLuint frameBuffer;
   GLuint texture;
   //glGenFramebuffers 生成帧缓存区对象名称;
   glGenFramebuffers(1, &frameBuffer);
   //glBindFramebuffer 绑定一个帧缓存区对象;
   glBindFramebuffer(GL_FRAMEBUFFER, frameBuffer);
   
   //2. 生成纹理ID,绑定纹理;
   //glGenTextures 生成纹理ID
   glGenTextures(1, &texture);
   //glBindTexture 将一个纹理绑定到纹理目标上;
   glBindTexture(GL_TEXTURE_2D, texture);
   //glTexImage2D 指定一个二维纹理图像;
   glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, newTextureWidth, newTextureHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
   
   //3. 设置纹理相关参数
   glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
   glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
   glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
   glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
   
   //4. 将纹理图像加载到帧缓存区对象上;
//    帧缓存区 可以附着 渲染缓存区,还可以加载纹理对象
   /*
    glFramebufferTexture2D (GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level)
    target: 指定帧缓冲目标,符合常量必须是GL_FRAMEBUFFER;
    attachment: 指定附着纹理对象的附着点GL_COLOR_ATTACHMENT0
    textarget: 指定纹理目标, 符合常量:GL_TEXTURE_2D
    teture: 指定要附加图像的纹理对象;
    level: 指定要附加的纹理图像的mipmap级别,该级别必须为0。
    */
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
   
   //5. 设置视口尺寸
   glViewport(0, 0, newTextureWidth, newTextureHeight);
   
   //6. 获取着色器程序
   GLuint program = [LongLegHelper programWithShaderName:@"spring"];
   glUseProgram(program);
   
   //7. 获取传递数据的入口
   GLuint positionSlot = glGetAttribLocation(program, "Position");
   GLuint textureSlot = glGetUniformLocation(program, "Texture");
   GLuint textureCoordsSlot = glGetAttribLocation(program, "TextureCoords");
   
   //8. 传值,即传递纹理ID
   glActiveTexture(GL_TEXTURE0);
   glBindTexture(GL_TEXTURE_2D, self.baseEffect.texture2d0.name);
   glUniform1i(textureSlot, 0);
   
   //9.初始化缓存区,即创建VBO
   LongLegVertexAttribArrayBuffer *vbo = [[LongLegVertexAttribArrayBuffer alloc] initWithAttribStride:sizeof(SenceVertex) numberOfVertices:kVerticesCount data:tmpVertices usage:GL_STATIC_DRAW];
   
   //10.准备绘制,将纹理/顶点坐标传递进去;
//    顶点 & 纹理坐标 -- 准备绘制
   [vbo prepareToDrawWithAttrib:positionSlot numberOfCoordinates:3 attribOffset:offsetof(SenceVertex, positionCoord) shouldEnable:YES];
   [vbo prepareToDrawWithAttrib:textureCoordsSlot numberOfCoordinates:2 attribOffset:offsetof(SenceVertex, textureCoord) shouldEnable:YES];
   
   //11. 绘制
   [vbo drawArrayWithMode:GL_TRIANGLE_STRIP startVertexIndex:0 numberOfVertices:kVerticesCount];
   
   //12.解绑缓存
   glBindFramebuffer(GL_FRAMEBUFFER, 0);
   //13.释放顶点数组
   free(tmpVertices);
   
   //14.保存临时的纹理对象/帧缓存区对象;
   self.tmpTexture = texture;
   self.tmpFrameBuffer = frameBuffer;
}
 ③、从帧缓存区中获取拉伸后的图片

图片存储的本质其实是利用上下文将帧缓存区的图片像素点进行重绘,得到一张新的图片

// 返回某个纹理对应的 UIImage,调用前先绑定对应的帧缓存
- (UIImage *)imageFromTextureWithWidth:(int)width height:(int)height {
    
//    1、绑定帧缓存区
    glBindFramebuffer(GL_FRAMEBUFFER, self.tmpFrameBuffer);
    
//    2、将帧缓存区内的图片纹理绘制到图片上
    //计算图片的字节数
    int size = width * height * 4;
    GLubyte *buffer = malloc(size);
    /*
    
    glReadPixels (GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, GLvoid* pixels);
    @功能: 读取像素(理解为将已经绘制好的像素,从显存中读取到内存中;)
    @参数解读:
    参数x,y,width,height: xy坐标以及读取的宽高;
    参数format: 颜色格式; GL_RGBA;
    参数type: 读取到的内容保存到内存所用的格式;GL_UNSIGNED_BYTE 会把数据保存为GLubyte类型;
    参数pixels: 指针,像素数据读取后, 将会保存到该指针指向的地址内存中;
    
    注意: pixels指针,必须保证该地址有足够的可以使用的空间, 以容纳读取的像素数据; 例如一副256 * 256的图像,如果读取RGBA 数据, 且每个数据保存在GLUbyte. 总大小就是 256 * 256 * 4 = 262144字节, 即256M;
    int size = width * height * 4;
    GLubyte *buffer = malloc(size);
    */
    glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, buffer);
    
    //使用data和size 数组来访问buffer数据;
    /*
     CGDataProviderRef CGDataProviderCreateWithData(void *info, const void *data, size_t size, CGDataProviderReleaseDataCallback releaseData);
     @功能: 新的数据类型, 方便访问二进制数据;
     @参数:
     参数info: 指向任何类型数据的指针, 或者为Null;
     参数data: 数据存储的地址,buffer
     参数size: buffer的数据大小;
     参数releaseData: 释放的回调,默认为空;
     
     */
    CGDataProviderRef provider = CGDataProviderCreateWithData(NULL, buffer, size, NULL);
    //每个组件的位数;
    int bitsPerComponent = 8;
    //像素占用的比特数4 * 8 = 32;
    int bitsPerPixel = 32;
    //每一行的字节数
    int bytesPerRow = 4 * width;
    //颜色空间格式;
    CGColorSpaceRef colorSpaceRef = CGColorSpaceCreateDeviceRGB();
    //位图图形的组件信息 - 默认的
    CGBitmapInfo bitmapInfo = kCGBitmapByteOrderDefault;
    //颜色映射
    CGColorRenderingIntent renderingIntent = kCGRenderingIntentDefault;

    
//    3、将帧缓存区里的像素点绘制到一张图片上:读取的数据 -- 图片
    /*
    CGImageCreate(size_t width, size_t height,size_t bitsPerComponent, size_t bitsPerPixel, size_t bytesPerRow,CGColorSpaceRef space, CGBitmapInfo bitmapInfo, CGDataProviderRef provider,const CGFloat decode[], bool shouldInterpolate,CGColorRenderingIntent intent);
    @功能:根据你提供的数据创建一张位图;
    注意:size_t 定义的是一个可移植的单位,在64位机器上为8字节,在32位机器上是4字节;
    参数width: 图片的宽度像素;
    参数height: 图片的高度像素;
    参数bitsPerComponent: 每个颜色组件所占用的位数, 比如R占用8位;
    参数bitsPerPixel: 每个颜色的比特数, 如果是RGBA则是32位, 4 * 8 = 32位;
    参数bytesPerRow :每一行占用的字节数;
    参数space:颜色空间模式,CGColorSpaceCreateDeviceRGB
    参数bitmapInfo:kCGBitmapByteOrderDefault 位图像素布局;
    参数provider: 图片数据源提供者, 在CGDataProviderCreateWithData ,将buffer 转为 provider 对象;
    参数decode: 解码渲染数组, 默认NULL
    参数shouldInterpolate: 是否抗锯齿;
    参数intent: 图片相关参数;kCGRenderingIntentDefault
    
    */
    CGImageRef imageRef = CGImageCreate(width, height, bitsPerComponent, bitsPerPixel, bytesPerRow, colorSpaceRef, bitmapInfo, provider, NULL, NO, renderingIntent);
    
//    4、此时的 imageRef 是上下颠倒的,调用 CG 的方法重新绘制一遍,刚好翻转过来
    //创建一个图片context
    UIGraphicsBeginImageContext(CGSizeMake(width, height));
    CGContextRef context = UIGraphicsGetCurrentContext();
    //将图片绘制上去
    CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
    //从context中获取图片
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    //结束图片context处理
    UIGraphicsEndImageContext();
    
    //释放buffer
    free(buffer);
    //返回图片
    return image;
}
④、存储到相册

将获取的拉伸后的图片通过苹果自带的Photos框架,将其存储到系统相册

// 保存图片到相册
- (void)saveImage:(UIImage *)image {
    //将图片通过PHPhotoLibrary保存到系统相册
    [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
        [PHAssetChangeRequest creationRequestForAssetFromImage:image];
    } completionHandler:^(BOOL success, NSError * _Nullable error) {
        NSLog(@"success = %d, error = %@ 图片已保存到相册", success, error);
    }];
}

 三、demo地址:

PictureShowsDemo: OpenGL ES实现大长腿效果

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

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

相关文章

【React】react组件传参

【React】react组件传参 一、props:父组件向子组件传参1、将普通的参数作为props传递2、将jsx作为props传递(组件插槽) 二、自定义事件:子父组件向父组件传参三、context进行多级组件传参四、redux全局状态管理 一、props&#xf…

Redis客户端有哪些:你了解吗?

一、分类 Redis客户端工具是用来连接和管理redis服务器的软件,它们可以有不同的类型,如桌面客户端、web客户端和IDE插件。不同的客户端工具有各自的优缺点和特色,你可以根据你的需求和喜好选择合适的工具。 1、Redis 命令行工具 redis-cli官…

智慧商城(continue)

文章目录 1.静态页面结构准备和动态渲染2.搜索 - 历史记录管理1. 写好基础静态页面,可以先往里面加一点假数据2. 上面基本的渲染直接利用history渲染就可以了3. 搜索历史基本渲染结束了,开始点击搜索添加历史4. vant内用v-model" ",可以快速拿到搜索框的值5. 往历史记…

【DevOps】产品需求文档(PRD)与常见原型软件

文章目录 1、PRD介绍1.1、概述1.2、前提条件1.3、主要目的1.4、关键内容1.5、表述方式1.6、需求评审人员1.7、一般内容结构 2、需求流程3、常见原型软件3.1、Word3.2、Axure3.2.1、详细介绍3.2.2、应用分类3.2.3、优缺点 3.3、摹客RP3.4、蓝湖3.5、GUI Design Studio 1、PRD介绍…

TQ15EG开发板教程:在VIVADO2023.1 以及VITIS环境下 检测DDR4

打开VIVADO2023.1 创建一个新的工程,设置工程名称和地址 选择RTL工程,勾选不添加文件 搜索15eg,选择xqzu15eg-ffrb1156-2-i 完成创建工程 添加设计模块 设置模块名称 在模块中添加mpsoc器件 双击器件进行配置 若有配置文件预设可以直接导入配…

分布式任务调度框架XXL-JOB详解

分布式任务调度 概述 场景: 如12306网站根据不同车次设置放票时间点,商品成功发货后向客户发送短信提醒等任务,某财务系统需要在每天上午10天前统计前一天的账单数据 任务的调度是指系统为了完成特定业务,基于给定的时间点,时间间隔&#…

C# SSH.NET 长命令及时返回

在SSH中执行长时间的命令,SSH.NET及时在文本框中返回连续显示结果。 c# - Execute long time command in SSH.NET and display the results continuously in TextBox - Stack Overflow 博主管理了一个服务器集群,准备上自动巡检工具,测试在…

计算机网络实验二

目录 实验二 交换机的基本配置 1、实验目的 2、实验设备 (1)实验内容: (2)练习: 1.实验内容一:(交换机的配置方式) 2.实验内容二:(交换机…

LabVIEW汽车自燃监测预警系统

LabVIEW汽车自燃监测预警系统 随着汽车行业的飞速发展,汽车安全问题日益受到公众的关注。其中,汽车自燃现象因其突发性和破坏性,成为一个不可忽视的安全隐患。为了有效预防和减少自燃事故的发生,提出了LabVIEW的汽车自燃监测预警…

算法学习——华为机考题库4(HJ26 - HJ30)

算法学习——华为机考题库4(HJ26 - HJ30) HJ26 字符串排序 描述 编写一个程序,将输入字符串中的字符按如下规则排序。 规则 1 :英文字母从 A 到 Z 排列,不区分大小写。 如,输入: Type 输出…

2024年 复习 HTML5+CSS3+移动web 笔记 之CSS遍 第5天

第 五 天 整个网站例 5.1 准备工作 项目目录与版心 base.css 5.2 网页制作思路 5.3 header 区域-整体布局 5.4 header区域-logo 5.5 header区域-导航 index.html <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8">&l…

qt -chart控件设计器可拖拉

qt -chart控件设计器可拖拉 一、演示效果二、安装过程三、核心程序四、程序链接 一、演示效果 二、安装过程 三、核心程序 #include <QtGui> #include <QColor>#include <cstdlib> #include <cassert> #include <numeric>#include <chartwor…

Python 数据分析(PYDA)第三版(六)

原文&#xff1a;wesmckinney.com/book/ 译者&#xff1a;飞龙 协议&#xff1a;CC BY-NC-SA 4.0 十二、Python 建模库介绍 原文&#xff1a;wesmckinney.com/book/modeling 译者&#xff1a;飞龙 协议&#xff1a;CC BY-NC-SA 4.0 此开放访问网络版本的《Python 数据分析第三版…

如何在win系统部署开源云图床Qchan并无公网ip访问本地存储图片

文章目录 前言1. Qchan网站搭建1.1 Qchan下载和安装1.2 Qchan网页测试1.3 cpolar的安装和注册 2. 本地网页发布2.1 Cpolar云端设置2.2 Cpolar本地设置 3. 公网访问测试总结 前言 图床作为云存储的一项重要应用场景&#xff0c;在大量开发人员的努力下&#xff0c;已经开发出大…

01-Java工厂模式 ( Factory Pattern )

工厂模式 Factory Pattern 摘要实现范例 工厂模式&#xff08;Factory Pattern&#xff09;提供了一种创建对象的最佳方式 工厂模式在创建对象时不会对客户端暴露创建逻辑&#xff0c;并且是通过使用一个共同的接口来指向新创建的对象 工厂模式属于创建型模式 摘要 1. 意图 …

Mac M1使用PD虚拟机运行win10弹出“内部版本已过期立即安装新的windows内部版本”

一、问题 内部版本已过期立即安装新的windows内部版本 二、解决 1、如图所示打开zh-CN目录 C:\windows\system32\zh-CN找到licensingui.exe文件 将该文件重命名为licensingui_bak.exe 2、修改完成效果如下 &#xff08;1&#xff09;但操作中发现&#xff0c;需要TrustedIns…

店群如何防关联?抖音小店被限流怎么办?——站斧浏览器云桌面

无论是抖音小店店铺&#xff0c;还是其他店铺&#xff1b;使用相同法人、相同类目&#xff0c;多开都会被限流&#xff0c;甚至严重到全部店铺迟迟不出单。 下面小编根据不同情况给出解决方案&#xff1a; 1.不同法人、相同类目的情况 使用云服务器&#xff08;站斧云桌面&am…

webpack配置

一、很多基础方面的配置被vuecli所集成一般项目都是使用vuecli,不会真正的去从0-1进行webpack配置: 1、vuecli中的webpack基础配置: (1)入口文件默认在src/main;输出在dist; (2)集成了大量的插件和加载器:babel-loader 处理 JavaScript 文件、使用 css-loader 和 style-load…

简单说说redis分布式锁

什么是分布式锁 分布式锁&#xff08;多服务共享锁&#xff09;在分布式的部署环境下&#xff0c;通过锁机制来让多客户端互斥的对共享资源进行访问/操作。 为什么需要分布式锁 在单体应用服务里&#xff0c;不同的客户端操作同一个资源&#xff0c;我们可以通过操作系统提供…

Jupyter Notebook中的%matplotlib inline详解

Jupyter Notebook中的%matplotlib inline详解 &#x1f335;文章目录&#x1f335; &#x1f333;引言&#x1f333;&#x1f333;什么是魔术命令&#x1f333;&#x1f333;%matplotlib inline详解&#x1f333;(&#x1f448;直入主题请点击)&#x1f333;小结&#x1f333;&…