Android OpenGL ES 学习(十) – GLSurfaceView 源码解析GL线程以及自定义 EGL

news2024/11/17 15:40:25

OpenGL 学习教程
Android OpenGL ES 学习(一) – 基本概念
Android OpenGL ES 学习(二) – 图形渲染管线和GLSL
Android OpenGL ES 学习(三) – 绘制平面图形
Android OpenGL ES 学习(四) – 正交投影
Android OpenGL ES 学习(五) – 渐变色
Android OpenGL ES 学习(六) – 使用 VBO、VAO 和 EBO/IBO 优化程序
Android OpenGL ES 学习(七) – 纹理
Android OpenGL ES 学习(八) –矩阵变换
Android OpenGL ES 学习(九) – 坐标系统和。实现3D效果
Android OpenGL ES 学习(十) – GLSurfaceView 源码解析GL线程以及自定义 EGL
代码工程地址: https://github.com/LillteZheng/OpenGLDemo.git

前面我们已经学习了 GL 的基本实现效果,我们一般的操作顺序就是

  • onSurfaceCreated: 进行着色器的加载编译
  • onSurfaceChanged:设置 gl 的大小,或者设置正交投影
  • onDrawFrame:绘制

那你会不会好奇,为啥我们按照这样的顺序,弄完就可以正常显示了呢?你是否对OpenGL 绘制的 “完整流程” 感兴趣呢?你是否也遇到下面这些疑问呢?

  1. GL线程和普通线程有什么区别?
  2. texture所占用的空间是跟GL线程绑定的吗?
  3. 为什么通常一个GL线程的texture等数据,在另一个GL线程没法用?
  4. 为什么通常GL线程销毁后,为什么texture也跟着销毁了?
  5. 不同线程如何共享OpenGL数据

这一章,我们带着这些疑问,来探究 OpenGL 渲染的完整流程,今天要完成的效果,不使用GLSurfaceView,编写 EGL + SurfaceVIew 实现 3D 效果:
在这里插入图片描述

一. 渲染完整流程

为什么叫完整流程?前面也说道,Android 系统把复杂的过程都封装好了,我们只需要调用 GlSurfaceView 就可以很轻松的拿到OpenGL 的渲染环境,然后编写着色器代码和逻辑即可。
但实际上,OpenGL 的核心流程应该是这样的:

在这里插入图片描述
为了得到真相,我们通过查看GLSurfaceView 的源码去看。

二. GLSurfaceView 源码

从入口出发,我们都是调用 glSurface.setRenderer() 去设置的,进到源码后可以发现,它主要做了以下几件事情:

    // EGLConfig 对象.用于指定OpenGL颜色、深度、模版等设置
    if (mEGLConfigChooser == null) {
        mEGLConfigChooser = new SimpleEGLConfigChooser(true);
    }

    // EGLContext 对象.用于提供EGLContext创建和销毁的处理
    if (mEGLContextFactory == null) {
        mEGLContextFactory = new DefaultContextFactory();
    }
    
    // EGLSurface窗口对象. 用于提供EGLSurface创建和销毁的处理
    if (mEGLWindowSurfaceFactory == null) {
        mEGLWindowSurfaceFactory = new DefaultWindowSurfaceFactory();
    }
     
    //开启 GLThread线程 
    mRenderer = renderer;
    mGLThread = new GLThread(mThisWeakRef);
    mGLThread.start();

额,看完这个注释,你可能会一头雾水,EGL 是啥,EGLSurface 又是啥,看来我们需要先了解 EGL .

2.1 EGL

EGL 是 OpenGL 与本地窗口系统 Native Window System)之间的通信接口,它的主要作用是:

  • 与设备的原生窗口系统通信
  • 查询绘图表面的可用类型和配置
  • OpenGL 和其他图形渲染 API 渲染

不同平台上EGL配置是不一样的,而OpenGL的调用方式是一致的,也就是说OpenGL的跨平台特性依赖于EGL接口。

EGL 通过创建 eglSurface,和上下文 eglContext,就可以通过相关 api 访问本地窗口系统,实现绘制。如下图,体现了 EGL ,display 和 context 三者之间的关系。

在这里插入图片描述

  • Display(EglDisplay):实际显示设备的抽象,你可以理解成显示模块
  • Surface(EglSurface):用来存储图像的内存区域 FrameBuffer 的抽象,包括 Color Buffer (颜色缓冲区),Stencil Buffer(模板缓冲区),Depth Buffer(深度缓冲区)
  • Context(EglContext):存储 OpenGL ES 绘图的一些状态信息,比如存储 texture

在开发时,GLSurfaceView 已经帮我们对 Display,Surface,Context 进行封装管理,我们可以很方便的调用 GLSurfaceView.Render ,实现渲染绘制。
使用 EGL 渲染的一般步骤:

  1. 获取 EGLDisplay 对象,建立与本地窗口系统的连接:调用 eglGetDisplay 方法得到 EGLDisplay。
  2. 初始化 EGL 方法:打开连接之后,调用 eglInitialize 方法初始化。
  3. 获取 EGLConfig 对象,确定渲染表面的配置信息:调用 eglChooseConfig 方法得到 EGLConfig。
  4. 创建渲染表面 EGLSurface:通过 EGLDisplay 和 EGLConfig ,调用 eglCreateWindowSurface 或 eglCreatePbufferSurface 方法创建渲染表面,得到 EGLSurface,其中 eglCreateWindowSurface 用于创建屏幕上渲染区域,eglCreatePbufferSurface 用于创建屏幕外渲染区域。
  5. 创建渲染上下文 EGLContext :通过 EGLDisplay 和 EGLConfig ,调用 eglCreateContext 方法创建渲染上下文,得到 EGLContext。
  6. 绑定上下文:通过 eglMakeCurrent 方法将 EGLSurface、EGLContext、EGLDisplay 三者绑定,绑定成功之后 OpenGLES 环境就创建好了,接下来便可以进行渲染。
  7. 交换缓冲:OpenGLES 绘制结束后,使用 eglSwapBuffers 方法交换前后缓冲,将绘制内容显示到屏幕上,而屏幕外的渲染不需要调用此方法。
  8. 释放 EGL 环境:绘制结束后,不再需要使用 EGL 时,需要取消 eglMakeCurrent 的绑定,销毁 EGLDisplay、EGLSurface、EGLContext 三个对象。

原来如此,原来 GLSurfaceView 里面,已经帮我们把 EGL 初始化好了,回到 glSurface.setRenderer() 的方法:

    // EGLConfig 对象.用于指定OpenGL颜色、深度、模版等设置
    if (mEGLConfigChooser == null) {
        mEGLConfigChooser = new SimpleEGLConfigChooser(true);
    }

    // EGLContext 对象.用于提供EGLContext创建和销毁的处理
    if (mEGLContextFactory == null) {
        mEGLContextFactory = new DefaultContextFactory();
    }
    
    // EGLSurface窗口对象. 用于提供EGLSurface创建和销毁的处理
    if (mEGLWindowSurfaceFactory == null) {
        mEGLWindowSurfaceFactory = new DefaultWindowSurfaceFactory();
    }
     
    //开启 GLThread线程 
    mRenderer = renderer;
    mGLThread = new GLThread(mThisWeakRef);
    mGLThread.start();

我们就知道,原来注释是这个意思,管理关键还得看一下 GL 线程做了什么。

2.2 GL 线程

我们去看看它和其他线程有什么区别:
在这里插入图片描述
实际上,它跟普通线程没有什么区别,也是继承Thread,关键就是 guradedRun() 方法,它实际上把 EGL 给初始化好,然后回调相关方法,让你去绘制,在一个 while 循环中,它做了哪些操作呢?

while(true){
	//初始化 egl,配置等信息
	mEglHelper.start()
	//回去 Surface,并配置到当前线程
	mElgHeper.createSurface()
	回调 onSurfaceCreated()
	回调 onSurfaceChanged()
	回调 onDrawFrame()
	//egl交换缓冲区,呈现画面
	mEglHelper.swap()
}

从这里看,guradedRun() 跟我们之前说的流程基本是一样的,还有非常熟悉的 onSurfaceCreated(), onSurfaceChanged() 和 onDrawFrame() 也是在 EGL 配置之后,再回调的。
这里我们也可以得出一个结论,就是 GL线程跟普通线程没什么区别,就是一个普通线程,只是按照 OpenGL 完整的绘图方式走了一遍,因此我们也可以自己自定义,也可以实现OpenGL 的绘制,从 glGenTextures 等绘图api 都是 native 方法也可以证明这一点。

2.3 为什么通常一个GL线程的texture等数据,在另一个GL线程没法用

回答这个问题之前,先看看纹理对象是怎么生成的,查看 GLES30.glGenTextures 方法:

// C function void glGenTextures ( GLsizei n, GLuint *textures )

public static native void glGenTextures(
    int n,
    int[] textures,
    int offset
);

看来跟 EGL 没关系,那它是怎么知道是 GL 线程去调,还是普通线程去调呢?它又是怎么把 glGenTextures 和 glDeleteTextures 对应到正确的线程上的呢?看源码:

在这里插入图片描述

在这里插入图片描述

实际上,它会先通过 getGlThreadSpecific() 这个方法去拿到一个 context,这个 context 实际上就是 EGL Context ,它是比较特殊的。什么意思呢,就是不同线程去拿,得到的 EGL Context 可能都不一样,这取决于给这个 EGL Context 是什么,我的理解是相当于线程类自己的局部变量,每个线程存储的 context 都不一样。

那EGL Context 是什么时候被设置进去的呢?
还记得 前面说到的 eglMakeCurrent() 这个方法吗?

egl.eglMakeCurrent(display, eglSurface, eglSurface, eglContext)

实际上,第四个参数eglContext 最终会设置给 setGlThreadSpecific() 中的变量存储起来,给其 API 使用。
然后我们再看一下 eglMakeCurrent 做了什么:
在这里插入图片描述

可以得到以下总结:

  1. 获取当前线程 EGL context ,给底层使用
  2. 判断当前的 context 是否为 IS_CURRENT 状态,否则返回-1
  3. 如果 gl 是 IS_CURRENT 状态,但是不是当前线程,也return
  4. 如果 gl 不是 IS_CURRENT 状态,将 current 设置成非 IS_CURRENT 状态
  5. 将gl置为IS_CURRENT状态并将gl设置为当前线程的Thread Local的EGL Context

结论:

  1. 如果一个 EGL context 被一个线程使用 glMakeCurrent ,则它不能被另外一个线程 glMakeCurrent 了
  2. glMakeCurrent 之后, EGL Context 会跟当前的 EGL context 脱离关系

关于 texture 与 egl context 的关系,可以参考这篇文章 https://cloud.tencent.com/developer/article/1035505

因此,我们可以回答一下上面的问题:

  1. GL线程和普通线程有什么区别?: 没有区别,只是 GL线程 按照 OpenGL 的流程跑了一遍。
  2. texture所占用的空间是跟GL线程绑定的吗?:不是,跟 EGL context 绑定,跟 线程没关系。
  3. 为什么通常一个GL线程的texture等数据,在另一个GL线程没法用?:因为调用 OpenGL 接口,会先去获取 EGL context,而不同线程获取的状态是不一样的,而 texture 又放在 EGL context 中,因此无法在另一个 GL 线程使用。
  4. 为什么通常GL线程销毁后,为什么texture也跟着销毁了?:因为 GLSurfaceView 在销毁时,调用了 eglDestroyContext() 方法,销毁了 EGL context,从而 texture 也销毁了,跟 GL线程没关系
  5. 不同线程如何共享OpenGL数据:共享 EGL context,线程调用 eglCreateContext 时,传入另一个线程的 EGL Context。

三. 自定义 EGL 现成,使用SurfaceView+EGL 实现渲染

前面说到,GLSurfaceView 帮我们把 egl 的配置都弄好了,优点是使用简单,缺点是当我们想共享同个 EGL context,实现同个场景,不同 surface 的渲染时,GLSurfaceView 就使用了。
所以,我们自定义自己的 GLSurfaceView。

首先,创建一个类,让它也继承 SurfaceView,并创建一个线程,用来加载 EGL :

inner class EglSurfaceView(context: Context) : SurfaceView(context), SurfaceHolder.Callback {

        init {
            holder.addCallback(this)
        }l
        override fun surfaceCreated(holder: SurfaceHolder) {
      
        }

        override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {

        }

        override fun surfaceDestroyed(holder: SurfaceHolder) {

        }
        inner class EglThread(private val surface: Surface) : Thread() {
				 override fun run() {
				 	....
				 }
		}
}

接着,就需要创建 EGL 了,这里我们通过模仿 GLSurfaceView 的 EGLHelper 方法,得到以下类:

inner class EglHelper {
        private var egl: EGL10? = null
        private var eglDisplay: EGLDisplay? = null
        private var eglSurface: EGLSurface? = null
        private var eglContext: EGLContext? = null
        fun initEgl(surface: Surface) {
            //1、得到Egl实例:
            val egl = EGLContext.getEGL() as EGL10
            //2、得到默认的显示设备(就是窗口)
            val display = egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY)
            if (display === EGL10.EGL_NO_DISPLAY) {
                throw RuntimeException("eglGetDisplay failed")
            }

            //3、初始化默认显示设备
            val displayVersions = IntArray(2)
            if (!egl.eglInitialize(display, displayVersions)) {
                throw RuntimeException("eglInitialize failed")
            }
            //4、设置显示设备的属性
            val attr = intArrayOf(
                EGL14.EGL_RED_SIZE, 8,
                EGL10.EGL_GREEN_SIZE, 8,
                EGL10.EGL_BLUE_SIZE, 8,
                EGL10.EGL_ALPHA_SIZE, 8,
                EGL10.EGL_DEPTH_SIZE, 8,
                EGL10.EGL_STENCIL_SIZE, 8,
                EGL10.EGL_RENDERABLE_TYPE, 4,
                EGL10.EGL_NONE
            )
            //5、从系统中获取对应属性的配置
            val num_config = IntArray(1)
            if(!egl.eglChooseConfig(display, attr, null, 1, num_config)) {
                throw RuntimeException("eglChooseConfig failed")
            }

            val numConfigs = num_config[0]

            if (numConfigs <= 0) {
                throw RuntimeException("No configs match configSpec")
            }

            val configs = arrayOfNulls<EGLConfig>(numConfigs)
            if (!egl.eglChooseConfig(display, attr, configs, numConfigs, num_config)) {
                throw RuntimeException("eglChooseConfig#2 failed")
            }

            //6、创建EglContext
            val  attrib_list = intArrayOf(
                EGL14.EGL_CONTEXT_CLIENT_VERSION, 3,
                EGL10.EGL_NONE
            )
            val eglContext = egl.eglCreateContext(display,
                configs[0],
                EGL10.EGL_NO_CONTEXT, attrib_list)

            //7、创建渲染的Surface
            val eglSurface = egl.eglCreateWindowSurface(display, configs[0], surface, null)

            //8、绑定EglContext和Surface到显示设备中
            if (!egl.eglMakeCurrent(display, eglSurface, eglSurface, eglContext)) {
                throw RuntimeException("eglMakeCurrent fail")
            }
            this.egl = egl
            this.eglDisplay = display
            this.eglSurface = eglSurface
            this.eglContext = eglContext

        }

        fun swapBuffers() {
            egl?.eglSwapBuffers(eglDisplay, eglSurface)
        }

        fun destroy() {
            egl?.apply {
                eglMakeCurrent(
                    eglDisplay,
                    EGL10.EGL_NO_SURFACE,
                    EGL10.EGL_NO_SURFACE,
                    EGL10.EGL_NO_CONTEXT
                )
                eglDestroySurface(eglDisplay, eglSurface)
                eglSurface = null
                eglDestroyContext(eglDisplay, eglContext)
                eglContext = null
                eglTerminate(eglDisplay)
                eglDisplay = null
                egl = null
            }
        }
    }

步骤比较简单,就是:

  1. 拿到Egl实例 :EGLContext.getEGL()
  2. 得到默认的显示设备(就是窗口):egl.eglGetDisplay
  3. 初始化默认显示设备:egl.eglInitialize
  4. 设置显示设备的属性和配置到 egl 中:egl.eglChooseConfig
  5. 创建EglContext:egl.eglCreateContext
  6. 创建渲染的Surface:egl.eglCreateWindowSurface
  7. 绑定EglContext和Surface到显示设备中:egl.eglMakeCurrent
  8. 交换缓冲区,渲染:egl.eglSwapBuffers

然后我们把 EhlHeper 跟线程结合起来,完整的代码如下:

/**
* 自定义一个 SurfaceView,里面的线程模拟 EGL 环境
*/
inner class EglSurfaceView(context: Context) : SurfaceView(context), SurfaceHolder.Callback {

   init {
       holder.addCallback(this)
   }


   private var eglThread: EglThread? = null

   override fun surfaceCreated(holder: SurfaceHolder) {
       eglThread = EglThread(holder.surface)
       eglThread?.start()
   }

   override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
       eglThread?.changeSize(width, height)
   }

   override fun surfaceDestroyed(holder: SurfaceHolder) {
       eglThread?.release()
   }

   /**
    * gl线程,里面包含 EGL 创建流程
    */
   inner class EglThread(private val surface: Surface) : Thread() {
       private var isExit = false
       private var isFirst = true
       private var isSizeChange = false
       private var eglHelper: EglHelper? = null
       private var width = 0
       private var height = 0
       override fun run() {
           super.run()
           eglHelper = EglHelper().apply {
               initEgl(surface)
           }
           while (true) {
               if (isExit) {
                   release()
                   break
               }

               if (isFirst) {
                   isFirst = false
                   //回调给主类
                   this@L8_ShapeRender.onSurfaceCreated(null, null)
               }
               if (isSizeChange) {
                   isSizeChange = false
                   this@L8_ShapeRender.onSurfaceChanged(null, width, height)
               }
               eglHelper?.let {
                   this@L8_ShapeRender.onDrawFrame(null)
                   it.swapBuffers()
               }
               try {
                   sleep(16)
               } catch (e: Exception) {
               }
           }
       }

       fun release() {
           isExit = true
           eglHelper?.destroy()
           surface.release()
       }

       fun changeSize(width: Int, height: Int) {
           this.width = width
           this.height = height
           isSizeChange = true
       }
   }


}

都比较好理解,可以看到,我们也通过 onSurfaceCreated,onSurfaceChanged 和 onDrawFrame 回调给外部,这样,着色器相关的,就不用改变啦。
看一下效果,可以正常运行:
在这里插入图片描述

参考:
https://cloud.tencent.com/developer/article/1035505
https://cloud.tencent.com/developer/article/1899820

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

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

相关文章

python自定义包实例

了解python中的异常捕获与传递请点击“python中的异常捕获与传递” 了解python中的模块与包详解请点击“python中的模块与包详解” 目录 一.实例&#xff1a;自定义包 二.详解 1.新建my_utils包 2.新建str_util.py和file_util.py两个python file 3.str_util.py中的代码 演…

移动设备软件开发-AlertDialog6种使用方法

AlertDialog 1.AlertDialog的6种创建模式 1.1setMessage 1&#xff09;Java代码 //1.创建构造器AlertDialog.Builder buildernew AlertDialog.Builder(this);//2.设置参数builder.setTitle("弹窗提示").setIcon(R.mipmap.boy).setMessage("选择你的性别&#xf…

什么是WMS系统?WMS系统有什么功能

科技进步促使的数字化转型正在为大多数行业铺平道路&#xff0c;并重新定义它们在各个方面的功能&#xff0c;物流行业也不例外&#xff0c;因为它见证了日常运营的重大转变。改变物流行业的关键之一就是WMS系统的引入。仓储一直是运输和物流部门的核心支柱&#xff0c;随着新工…

基于Vue+Express+Mysql开发的手机端电影购票系统(附源码)

基于VueExpressMysql开发的手机端电影购票系统 基于手机的电影购票系统-VueNode 一个VueExpressMysql的电影售票项目 项目完整源码下载 https://download.csdn.net/download/DeepLearning_/87327200 前端展示 后台展示 项目说明 项目目录 ├── film 前端页面项目文件 …

HTML CSS JS游戏网页设计作业「响应式高端游戏资讯bootstrap网站」

&#x1f389;精彩专栏推荐&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业…

windows上datax的使用记录

datax使记录 简介 https://github.com/alibaba/DataX DataX 是阿里云 DataWorks数据集成 的开源版本&#xff0c;在阿里巴巴集团内被广泛使用的离线数据同步工具/平台。DataX 实现了包括 MySQL、Oracle、OceanBase、SqlServer、Postgre、HDFS、Hive、ADS、HBase、TableStore(O…

[洛谷]P1449 后缀表达式

[洛谷]P1449 后缀表达式一、问题描述&#xff1a;题目描述输入格式输出格式样例 #1样例输入 #1样例输出 #1提示二、思路分析1、算法标签2、思路三、代码实现一、问题描述&#xff1a; 传送门&#xff1a;[洛谷]P1449 后缀表达式 题目描述 所谓后缀表达式是指这样的一个表达式…

DJ14 简单接口电路及应用

目录 一、I/O 接口 1. 接口和端口的关系 2. 接口的基本结构 3. 8086/8088 端口编址 二、简单接口芯片 1. 74LS244 三态门 2. 74LS273 锁存器 3. 74LS374锁存器 4. 综合应用 三、基本输入输出方式 1. 无条件传送方式 2. 查询工作方式 一、I/O 接口 1. 接口和端口的…

校招面试真题 | 你的期望薪资是多少?为什么?

很多人去面试的时候&#xff0c;就像打游戏&#xff0c;过五关斩六将&#xff0c;终于到最后一关了&#xff0c;但是谈薪资的难度堪比打游戏中搞定终级 boss 的难度&#xff0c;真的是太「南」了&#xff0c;好多人都是因为这个问题让自己五味杂陈呀。报高了怕好 offer 失之交臂…

基因编辑相关最新研究进展(2022年12月)

【1】西湖大学马丽佳团队开发新型CRISPR脱靶和DNA易位检测工具 2022-12-15报道&#xff0c;2022年12月12日&#xff0c;西湖大学生命科学学院马丽佳团队在 Nature Communications 期刊发表了题为&#xff1a;PEAC-seq adopts Prime Editor to detect CRISPR off-target and DN…

C++11标准模板(STL)- 算法(std::next_permutation)

定义于头文件 <algorithm> 算法库提供大量用途的函数&#xff08;例如查找、排序、计数、操作&#xff09;&#xff0c;它们在元素范围上操作。注意范围定义为 [first, last) &#xff0c;其中 last 指代要查询或修改的最后元素的后一个元素。 产生某个元素范围的按字典…

Spring Cloud 2022.0.0 正式发布,代号 “Kilburn“

Spring Cloud 2022.0.0 已正式发布。 获取地址&#xff1a;https://repo1.maven.org/maven2/org/springframework/cloud/spring-cloud-dependencies/2022.0.0/ Spring Cloud 为开发人员提供了工具&#xff0c;以快速构建分布式系统中的某些常见模式&#xff08;例如&#xff1a…

java中的垃圾回收算法

java中有四种垃圾回收算法&#xff0c;分别是&#xff1a; 标记清除法、标记整理法、复制算法、分代收集算法 1、标记清除法: 第一步:利用可达性去遍历内存&#xff0c;把存活对象和垃圾对象进行标记; 第二步:在遍历一遍&#xff0c;将所有标记的对象回收掉; 特点:效率不行…

Java+MySQL基于ssm的超市进销存会员管理系统

随着我国经济的高速增长,各类超市和便利店也是越来越多,超市和便利店的出现,方便了人们对于日常生活消费的需要,为了能够更好的对超市的顾客进行服务,大多数超市提出了会员的机制,通过这种机制来增加用户的黏度,在给用户提供更好的服务的同时也提高了营业额。 超市会员管理系统…

转行,你考虑清楚了吗?

“我为什么离开中石油”写完后&#xff0c;引发了不少人的共鸣&#xff0c;一些在工作中苦苦挣扎、渴望转行的朋友&#xff0c;在微信上询问我转行情况和转行建议。 非常感谢朋友们的关心和信任&#xff0c;然而我并非什么职业规划大师&#xff0c;只是一个在石油圈混了五年的…

ChatGPT进化的秘密

本文作者&#xff0c;符尧 yao.fued.ac.uk&#xff0c;爱丁堡大学 (University of Edinburgh) 博士生&#xff0c;本科毕业于北京大学&#xff0c;与彭昊&#xff0c;Tushar Khot 在艾伦人工智能研究院 (Allen Institute for AI) 共同完成英文原稿&#xff0c;与剑桥大学郭志江…

搞懂Redis 数据存储原理,别只会 set、get 了

我的核心模块如图 1-10。 图 1-10 Client 客户端&#xff0c;官方提供了 C 语言开发的客户端&#xff0c;可以发送命令&#xff0c;性能分析和测试等。 网络层事件驱动模型&#xff0c;基于 I/O 多路复用&#xff0c;封装了一个短小精悍的高性能 ae 库&#xff0c;全称是 a si…

【C语言】函数的声明_函数定义_函数调用_函数递归 [函数的基本使用]

文章目录前言1.函数是什么?2.C语言中函数的分类2.1 库函数2.2 自定义函数3.函数的参数3.1 实际参数&#xff08;实参&#xff09;&#xff1a;3.2 形式参数&#xff08;形参&#xff09;&#xff1a;4.函数的调用4.1 传值调用4.2 传址调用4.3 练习5.函数的嵌套调用和链式访问5…

羊没羊,好像也没那么重要了!

疫情管控刚一放开&#xff0c;我就一直在想&#xff0c;如何降低羊&#x1f411;的概率和影响。​由于家里老人身体不太好&#xff0c;孩子年龄又太小&#xff0c;加上只有我一个人整天在外面跑&#xff0c;感染的几率最大。所以最后想了一下&#xff0c;决定先在外面租个房子&…

零基础学编程,怎么开始学习?

编程零基础的话&#xff0c;我先建议你看一些经典的书籍&#xff0c;抑或是通俗易懂的计算机常识书。 这几本书各有千秋&#xff0c;我参考了我自己尝试过的几种方法&#xff0c;可以为你选择最适合你的学习方法提供一种参考。首先要判断你的决心有多大&#xff0c;一则花费金…