Android OpenGL ES 学习(十二) - MediaCodec + OpenGL 解析H264视频+滤镜

news2024/11/18 9:48:30

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
Android OpenGL ES 学习(十一) –渲染YUV视频以及视频抖音特效

代码工程地址: https://github.com/LillteZheng/OpenGLDemo.git
更多音视频,参考:Android 音视频入门/进阶教程

这是OpenGL 最后一篇教程了,待我把C/Jni/Ndk 相关的知识,再深入一遍,再来学习光照等知识。

前面我们学习了OpenGL是如何渲染 YUV 视频的Android OpenGL ES 学习(十一) –渲染YUV视频以及视频抖音特效 ,这一章,我们让OpenGL 与 MediaCodec 结合,实现解析 H264 文件,并实现抖音效果。效果如下:
在这里插入图片描述

MediaCodec 为Android 的硬编,在一些快速解码设备,我们都是使用MediaCodec,如果你对 MediaCodec 如何解码不熟悉,可以先阅读
Android 音视频编解码(一) – MediaCodec 初探
Android 音视频编解码(二) – MediaCodec 解码(同步和异步)

实际工作中,我们也会使用 MediaCodec 把其他设备传输过来的码流,通过与 OpenGL 结合,实现解码和滤镜效果,比如投屏,投屏的基础上,加一些滤镜和特效。

OpenGL 与 MediaCodec 结合,需要 OpenGL 提供一个 Surface ,让MediaCodec 把解码出来的 YUV 渲染出来,而这个 Surface 就是 SurfaceTexure

一. 外部纹理 SurfaceTexture

SurfaceTexture 是 Surface 与 OpenGL ES 的结合 ,与传统的纹理(GL_TEXTURE_2D)不同,它有以下特点:

  • SurfaceTexture 可以直接 BufferQueue 拿到数据并渲染,在拿到 BufferQueue实例时,会将使用者标志设置成 GRALLOC_USAGE_HW_TEXTURE ,以确保 SurfaceTexture 可以识别缓冲区的数据。
  • 与 GL_TEXTURE_2D 不同,需要使用 samplerExternalOES 去识别外部纹理。
  • 不能执行与 GL_TEXTURE_2D 相同的操作。

1.1 时间戳和转换

SurfaceTexture实例包括检索时间戳的getTimeStamp()方法和检索变换矩阵的getTransformMatrix()方法。调用updateTexImage()设置时间戳和转换矩阵

  • 转换:比如某些情况下,接收端的数据是颠倒的,使用Matrix ,我们可以很容易把画面反转回来。
  • 时间戳:这个在相机会用的多,比如相机的每一帧,都需要带一个从捕获时拿到的演示时间戳,通过设置这个属性,我们能保证一致的时间戳。

1.2 数据回调

当你创建了SurfaceTexture ,也会创建一个待消耗的BufferQueue,当生产方(比如 MediaCodec )有新的缓冲数据加入队列,会回调 onFrameAvailable() 方法,表示已经消化了一帧。
当你调用了 updateTexImage() ,会释放当前的缓冲区,并从BufferQueue 拿到最新的缓冲区,这时会调用 EGL 的一些操作,使 GLES 可以将缓冲区作为外部纹理使用,即告知 OpenGL ,当前缓冲区可用,可进行一些操作。

二. 渲染视频

从上面的了解,我们可以得出MediaCodec , SurfaceTexture 和 OpenGL 结合的关系:
在这里插入图片描述
流程如下:

  1. 创建 SurfaceTexture,并把OpenGL的纹理 id 给到 SurfaceTexture
  2. 创建 MediaCodec,并拿到 SurfaceTexture 的 Surface
  3. 当第一次回调 onDrawFrame 时,会调用 updateTexture,刷新 BufferQueue,待 Mediacodec 生产数据时,更新 BufferQueue,会重触发 onDrawFrame ,循环至视频解码结束。

2.1 OpenGL 外部纹理

OpenGL 的外部纹理,使用的是GLES11Ext中的 samplerExternalOES:

uniform samplerExternalOES ourTexture;

因此,我们的片段着色器可以修改成:

/**
 * 片段着色器
 */
private var FRAGMENT_SHADER = """#version 300 es
    precision mediump float;
    out vec4 FragColor;
    in vec2 vTexture;
    uniform samplerExternalOES ourTexture;
    void main() {
        FragColor = texture(ourTexture,vTexture);
    }
"""

纹理的绑定,需要注意的是使用 GLES11Ext :

GLES30.glGenTextures(1, textures, 0)
GLES30.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textures[0])

 //纹理环绕
 GLES30.glTexParameteri(
     GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
     GLES30.GL_TEXTURE_WRAP_S,
     GLES30.GL_REPEAT
 )
 GLES30.glTexParameteri(
     GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
     GLES30.GL_TEXTURE_WRAP_T,
     GLES30.GL_REPEAT
 )

 //纹理过滤
 GLES30.glTexParameteri(
     GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
     GLES30.GL_TEXTURE_MIN_FILTER,
     GLES30.GL_LINEAR
 )
 GLES30.glTexParameteri(
     GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
     GLES30.GL_TEXTURE_MAG_FILTER,
     GLES30.GL_LINEAR
 )

 //解绑纹理对象
 GLES30.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 0)

然后创建 SurfaceTexture:

 surfaceTexture = SurfaceTexture(textures[0]).apply {
   setDefaultBufferSize(width, height)
   setOnFrameAvailableListener {
   }
}

2.2 与MediaCodec 绑定

Mediacodec 解码H264比较简单,配置解码的属性,使用异步解码即可,不熟悉Mediacodec可以参考:
Android 音视频编解码(一) – MediaCodec 初探
Android 音视频编解码(二) – MediaCodec 解码(同步和异步)

解码代码如下:

/**
 * @author by zhengshaorui 2022/12/26
 * describe:视频解码
 */
class VideoDncoder {
    companion object {
        internal val instance: VideoDncoder by lazy { VideoDncoder() }
        private const val MSG_INIT = 1;
        private const val MSG_QUERY = 2;
        private const val DECODE_NAME = "video/avc"
        private const val TAG = "VideoEncoder"
    }

    private var handleThread: HandlerThread? = null
    private var handler: Handler? = null
    private var surface: Surface? = null
    private var decoder: MediaCodec? = null
    private val indexQueue = LinkedBlockingDeque<Int>();
    private val handlerCallback = Handler.Callback { msg ->
        when (msg.what) {
            MSG_INIT -> {
                configAndStart()
            }
            MSG_QUERY -> {

                // handler?.sendEmptyMessageDelayed(MSG_QUERY, 10)
            }
        }
        false
    }
    private var listener: IDecoderListener? = null
    fun start(surface: Surface, iDecoderListener: IDecoderListener) {
        listener = iDecoderListener
        this.surface = surface
        if (handleThread == null) {
            handleThread = HandlerThread("VideoEncoder").apply {
                start()
                handler = Handler(this.looper, handlerCallback)
            }
        }
        handler?.let {
            it.removeMessages(MSG_INIT)
            it.sendEmptyMessage(MSG_INIT)
        }
    }

    /**
     * 喂数据
     */
    fun feedData(buffer: ByteArray, offset: Int, length: Int) {
        val index = indexQueue.take()
        if (index != -1) {
            decoder?.let {
                it.getInputBuffer(index)?.apply {
                    clear()
                    val time = System.nanoTime() / 1000000
                    put(buffer, offset, length)


                    it.queueInputBuffer(index, 0, length, time, 0)
                }

            }
        }
    }

    
    private fun configAndStart() {
        var width = getRealWidth(MainApplication.context)
        var height = getRealHeight(MainApplication.context)
        if (null == surface || !surface!!.isValid || width < 1 || height < 1) {
            throw  IllegalArgumentException("Some argument is invalid");
        }
        Log.d(TAG, "configAndStart() called: $width,$height")
        decoder = MediaCodec.createDecoderByType(DECODE_NAME)
        val format = MediaFormat()
        format.setString(MediaFormat.KEY_MIME, DECODE_NAME)
        format.setInteger(MediaFormat.KEY_WIDTH, width)
        format.setInteger(MediaFormat.KEY_HEIGHT, height)
        decoder?.let {
            it.reset()
            it.configure(format, surface, null, 0)
            it.setCallback(decodeCallback)
            it.start()
            Log.d(TAG, "解码器启动成功")
            listener?.onReady()
            handler?.sendEmptyMessage(MSG_QUERY)
        }
    }

    public interface IDecoderListener {
        fun onReady()
    }

    private val decodeCallback = object : MediaCodec.Callback() {
        override fun onInputBufferAvailable(codec: MediaCodec, index: Int) {
            indexQueue.add(index)
        }

        override fun onOutputBufferAvailable(
            codec: MediaCodec,
            index: Int,
            info: MediaCodec.BufferInfo
        ) {

            decoder?.releaseOutputBuffer(index, true)
        }

        override fun onError(codec: MediaCodec, e: MediaCodec.CodecException) {
            Log.e(TAG, "onError() called with: codec = $codec, e = $e")
        }

        override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) {
            var width = format.getInteger(MediaFormat.KEY_WIDTH)
            if (format.containsKey("crop-left") && format.containsKey("crop-right")) {
                width = format.getInteger("crop-right") + 1 - format.getInteger("crop-left")
            }
            var height = format.getInteger(MediaFormat.KEY_HEIGHT)
            if (format.containsKey("crop-top") && format.containsKey("crop-bottom")) {
                height = format.getInteger("crop-bottom") + 1 - format.getInteger("crop-top")
            }
            Log.d(TAG, "视频解码后的宽高:$width,$height")

        }

    }

    fun release() {
        handleThread?.quitSafely()
        handleThread = null
        handler = null
        surface?.release()
        try {
            decoder?.let {
                it.stop()
                it.release()
            }
        } catch (e: Exception) {
        }
    }


}

2.3 解析H264文件

接下来就是解析H264文件了,需要注意的是,喂给解码器的数据,要以一帧的结尾,不然会出现数据错乱,花屏的问题,如果你对H264不熟悉,可参考 Android 音视频编解码(三) – 视频编码和H264格式原理讲解

因此,我们读取H264每一帧的数据,然后一帧一帧喂给解码器,H264解析的简单代码如下:

/**
 * @author by zhengshaorui 2022/12/28
 * describe:H264 帧解析类
 */
class H264ParseThread(val inputStream: InputStream, val listener: IFrameListener) : Thread() {
    companion object {
        private const val TAG = "H264Parse"

        //一般H264帧大小不超过200k,如果解码失败可以尝试增大这个值
        private const val FRAME_MAX_LEN = 300 * 1024
        private const val P_FRAME = 0x01
        private const val I_FRAME = 0x05
        private const val SPS = 0x07
        private const val PPS = 0x08
    }

    private var isFinish = false

    public interface IFrameListener {
        fun onLog(msg: String)
        fun onFrame(byteArray: ByteArray, offset: Int, count: Int)
    }

    override fun run() {
        super.run()

        try {
            isFinish = false
            val header = ByteArray(4)
            val formatLength = getHeaderFormatLength(header, inputStream)
            if (formatLength < 0) {
                listener.onLog("不符合H264文件规范: $formatLength")
                return
            }
            //帧数组
            val frame = ByteArray(FRAME_MAX_LEN)
            //每次读取的数据
            val readData = ByteArray(2 * 1024)
            //把头部信息给到 frame
            System.arraycopy(header, 0, frame, 0, header.size)

            //开始肯定是 sps,所以,帧的起始位置为0,由于前面读取了头部,偏移量为4
            var frameLen = 4

            while (!isFinish) {
                val readLen = inputStream.read(readData)
                if (readLen < 0) {
                    //文件末尾
                    listener.onLog("文件末尾,退出")
                    isFinish = true
                    return
                }
                if (frameLen + readLen > FRAME_MAX_LEN) {
                    //文件末尾
                    listener.onLog("文件末尾,大于预留数组,退出")
                    isFinish = true
                    return
                }
                //先把数据拷贝到帧数组
                System.arraycopy(readData, 0, frame, frameLen, readLen)
                //修改当前帧的大小
                frameLen += readLen
                //寻找第一帧
                var firstHeadIndex = findHeader(frame, 0, frameLen)
                while (firstHeadIndex >= 0) {
                    //找第二帧,从第一帧之后的间隔开始找
                    val secondFrameIndex =
                        findHeader(frame, firstHeadIndex + 100, frameLen)
                    if (secondFrameIndex > 0) {
                        //找到第二帧
                        listener.onFrame(frame, firstHeadIndex, secondFrameIndex - firstHeadIndex)
                        //把第二帧的数组数据,拷贝到前面,方便继续寻找下一帧
                        val temp = frame.copyOfRange(secondFrameIndex, frameLen)
                        System.arraycopy(temp, 0, frame, 0, temp.size)
                        //帧下表指向第二帧的数据
                        frameLen = temp.size

                        //继续寻找下一帧
                        firstHeadIndex = findHeader(frame, 0, frameLen)
                    } else {
                        //没有找到,继续循环去找
                        firstHeadIndex = -1
                    }


                }


            }

        } catch (e: Exception) {
            listener.onLog("read file fail: $e")
        }
    }

    fun release() {
        isFinish = true
    }

    private fun findHeader(data: ByteArray, offset: Int, count: Int): Int {
        for (i in offset until count) {
            if (isFrameHeader(data, i)) {
                return i
            }
        }
        return -1

    }

    private fun isFrameHeader(data: ByteArray, index: Int): Boolean {
        if (data.size < 5) {
            return false
        }
        val d1 = data[index].toInt() == 0
        val d2 = data[index + 1].toInt() == 0
        val isNaluHeader = d1 && d2
        if (isNaluHeader && data[index + 2].toInt() == 1 && isFrameHeadType(data[index + 3])){
            return true
        }else if (isNaluHeader && data[index + 2].toInt() == 0 && data[index + 3].toInt() == 1 && isFrameHeadType(data[index + 4])){
            return true
        }

        return false
    }

    /**
     * 解析的时候,找到I和P去解析即可
     * 为啥使用and这个会导致播放卡顿?有大佬可以解释一下吗
     */
    private fun isSpecialFrame(byte: Byte): Boolean {
        val type = byte.toInt() and 0x11
        return  type == P_FRAME || type == I_FRAME || type == SPS || type == PPS
    }

    /**
     * 65    --   I帧/IDR帧
     * 41/61 --   p帧
     * 67    --   sps
     * 68    --   pps
     *
     */
    fun isFrameHeadType(head: Byte): Boolean {
        // val type = byte.toInt() and 0x11
        return head == 0x65.toByte() || head == 0x61.toByte()
                || head == 0x41.toByte() || head == 0x67.toByte()
                || head == 0x68.toByte() || head == 0x06.toByte()
    }

    private fun getHeaderFormatLength(header: ByteArray, inputStream: InputStream): Int {
        //先读取头部4个字节,判断h264 是哪种格式
        if (header.size < 4) {
            return -1
        }
        inputStream.read(header)
        val h1 = header[0].toInt()
        val h2 = header[1].toInt()
        val h3 = header[2].toInt()
        val h4 = header[3].toInt()
        //码流格式类型00 00 01 或者 00 00 00 01
        return if (h1 == 0 && h2 == 0 && h3 == 1) {
            3
        } else if (h1 == 0 && h2 == 0 && h3 == 0 && h4 == 1) {
            4
        } else {
            //不符合H264文件规范
            -1
        }

    }
}

2.3 渲染

综上,拿到H264数据后,直接丢给解码器:

 decoder = VideoDncoder().apply {
   surfaceTexture = SurfaceTexture(textures[0]).apply {
        setDefaultBufferSize(width, height)
        setOnFrameAvailableListener {
        }
    }
    start(Surface(surfaceTexture), object : VideoDncoder.IDecoderListener {
        override fun onReady() {
            readFile()
        }

    })
}

    /**
     * 读取文件
     */
private fun readFile() {
    val stream = context.resources.assets.open("video.h264")
    h264ParseThread = H264ParseThread(stream, object : H264ParseThread.IFrameListener {
        override fun onLog(msg: String) {
            Log.d(TAG, "H264ParseThread msg: $msg")
        }

        override fun onFrame(byteArray: ByteArray, offset: Int, count: Int) {
            decoder?.feedData(byteArray, offset, count)
            Thread.sleep(55)

        }
    })
    h264ParseThread?.start()

    }

onDrawFrame 那里,更新缓冲区,updateTexImage:

    override fun onDrawFrame(gl: GL10?) {
        //步骤1:使用glClearColor设置的颜色,刷新Surface
        GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT)
        surfaceTexture?.updateTexImage()
        GLES30.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textures[0])
        GLES30.glBindVertexArray(vao[0])
        GLES30.glDrawElements(GLES30.GL_TRIANGLE_STRIP, 6, GLES30.GL_UNSIGNED_INT, 0)
    }

记得释放:

    private fun release() {
        h264ParseThread?.release()
        h264ParseThread = null
        decoder?.release()
        decoder = null
        surfaceTexture?.release()
        surfaceTexture = null
    }

这样,我们就完成 Mediacodec 和 OpenGL 的结合,实现了H264的解析。

四. 滤镜

从Android OpenGL ES 学习(十一) –渲染YUV视频以及视频抖音特效 知道,滤镜是对 rgb 的基础上,添加一些效果。
所以,在片段着色器拿到 rgb 的数据后,我们也能实现一些滤镜效果,如灰色:

#version 300 es
precision mediump float;
 out vec4 FragColor;
 in vec2 vTexture;
 uniform samplerExternalOES ourTexture;
 void main() {
    vec4 temColor = texture(ourTexture,vTexture);
	float gray = temColor.r * 0.2126 + temColor.g * 0.7152 + temColor.b * 0.0722;
	FragColor = vec4(gray,gray,gray,1.0);
 }

其他效果,参考工程。

至此,OpenGL 教程,就暂时告一段了。

参考:
https://source.android.google.cn/docs/core/graphics/arch-st

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

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

相关文章

宝妈可以做什么副业比较好?盘点七种适合宝妈的在家工作

现在有很多全职宝妈在家带孩子的&#xff0c;除了带孩子以外呢&#xff0c;还有很多的空闲时间&#xff0c;所以找一份自由的兼职工作是如今很多宝妈的想法。不仅可以给家里增加收入&#xff0c;支付日常开支&#xff0c;减轻老公的压力&#xff0c;还可以让自己学点新的赚钱知…

Linux编辑器-vim

一、vim的基本概念 我们讲解vim的三种模式(其实有好多模式&#xff0c;目前掌握这3种即可),分别是命令模式&#xff08;command mode&#xff09;、插入模式&#xff08;Insert mode&#xff09;和底行模式&#xff08;last line mode&#xff09;&#xff0c;各模式的功能区分…

OpenCV4入门到进阶

OpenCV4入门到进阶 第1章 介绍与学习指南 第2章 OpenCV开发环境搭建 第3章 图像&视频的加载与展示 第4章 OpenCV必知必会基础 第5章 OpenCV实现图形的绘制 第6章 OpenCV的算术与位运算 第7章 图像基本变换 第8章 OpenCV中的滤波器 第9章 OpenCV中的形态学 第10章 目标识别…

Redis事务以及缓存雪崩,缓存穿透,缓存击穿简介及解决策略

事物的基本操作 *开启事务 multi *作用 设定事物的开启位置,此指令执行后,后续的所有指令均加入到事物中 *执行事物 exec *作用 设定事物的结束位置,同时执行事物,与multi成对出现,成对使用 注意:加入事物的命令暂时进入到任务队列中,并没有立即执行,只有执行exec命令才开始执行…

商业与新消费:从2022到2023

【潮汐商业评论/原创】 “暴风雨结束后&#xff0c;你不会记得自己是怎样活下来的&#xff0c;你甚至不确定暴风雨真的结束了。但有一件事是确定的&#xff1a;当你穿过暴风雨&#xff0c;你就不再是原来那个人。”——村上春树转眼间&#xff0c;时间的齿轮已经从2022滑向了20…

开发中常用、实用命令记录总结

开发中常用、实用命令记录总结前言Linux修改Linux主机名称设置Linux虚拟内存Linux的网络和IO测试Linux查看内存、CPU占用最多的程序防火墙Firewall命令系统管理systemctl命令Centos的yum源设置Windos进程、端口、网络连接快速调出Cmdtext文本文件妙用任务计划Docker青龙面板忘记…

tomcat组件-Server

目录 概述 tomcat 启动 大致流程 Server StandardServer 利用8005端口关闭tomcat 总结 概述 了解一个程序&#xff0c;一定要知道他是干什么的&#xff0c;以及内部架构如何支撑这么干的&#xff0c;以及牵扯的到的概念、模式等知识点的解析(这里只介绍组件)。 Tomcat是…

软件测试 | 简历中应该如何描述才能体现出软技能的实力 ?

很多同学写简历的时候&#xff0c;真的是非常纠结的。技术能力怎么写&#xff1f;工作经历怎么写&#xff1f;项目经验怎么写&#xff1f;其中以拥有多个项目经验的同学和没有项目经验的学生最为纠结。前者是项目经验太多了&#xff0c;工作多年&#xff0c;每个项目经验要是都…

2023年全国最新消防设施操作员模拟试题题库及答案

百分百题库提供消防设施操作员考试试题、消防设施操作员考试预测题、消防设施操作员考试真题、消防设施操作员证考试题库等,提供在线做题刷题&#xff0c;在线模拟考试&#xff0c;助你考试轻松过关。 15.&#xff08;&#xff09;是平时不能满足水灭火设施所需的工作压力和流量…

以太网交换基础

以太网协议 以太网是当今现有局域网&#xff08;Local Area Network&#xff0c;LAN&#xff09;采用的最通用的通信协议标准。 以太网是建立在CSDA/CD&#xff08;Carrier Sense Multiple Access/Collision Detection,载波监听多路访问&#xff09; 冲突域 解决机制&#xff1…

ChatGPT 都在用的身份认证产品,你了解多少?

近期&#xff0c;人工智能研究公司 OpenAI 正式推出 ChatGPT&#xff0c;这是一种基于对话的人工智能聊天机器人模型&#xff0c;它能够理解自然语言并以自然语言的方式做出回应。上线不到一周&#xff0c;用户数已经突破 100 万大关。 上线仅 5 天用户数就突破百万大关&#…

educoder头歌数据结构 查找 第1关:实现折半查找

本文已收录于专栏 &#x1f332;《educoder数据结构与算法_大耳朵宋宋的博客-CSDN博客》&#x1f332; 任务描述 本关要求通过补全函数BSL_FindKey来实现在已排序的顺序表中查找关键码值为key的结点并返回该结点的编号。 相关知识 折半查找通常是针对顺序存储的线性表&…

SWOT分析

什么是SWOT分析?从做法到具体事例、注意点进行解说 SWOT分析法是将本公司的内部环境和外部环境作为强项(Strength)、弱点(Weakness)、机会(Opportunity)、威胁(Threat)进行梳理和分析的手法。以及把握事业现状的框架。从SWOT分析的方法到具体事例、注意事项&#xff0c;以图解…

砥砺前行,匠心致远|袋鼠云联合创始人、易知微CEO宁海元荣获“数字工匠 ”

近期&#xff0c;在余杭区数管局召开的“学习贯彻二十大&#xff0c;聚力建设新中心”联合主题党日活动暨余杭数改企业联盟党建联建共建机制圆桌恳谈会上&#xff0c;袋鼠云联合创始人、易知微CEO宁海元荣获2022年度“余杭数字工匠”&#xff0c;区数管局党组书记、局长周汝琴为…

【谷粒商城基础篇】商品服务开发:品牌管理

谷粒商城笔记合集 分布式基础篇分布式高级篇高可用集群篇简介&环境搭建项目简介与分布式概念&#xff08;第一、二章&#xff09;基础环境搭建&#xff08;第三章&#xff09;整合SpringCloud整合SpringCloud、SpringCloud alibaba&#xff08;第四、五章&#xff09;前端知…

Linux进程间通信---->共享内存

文章目录什么是共享内存共享内存基本原理和共享内存有关的系统接口ftokshmgetipc相关命令查看相关共享内存信息删除相关共享内存信息shmat/shmdtshmctlipc系列设计思想总结什么是共享内存 前面我们学习了管进程间通信的一种方式—>管道。 而我们今天将要介绍的共享内存也是…

10天,几万字,源码深度解析之 Spring IOC

历时 10 天&#xff0c;终于把 Sping 源码系列写完了&#xff0c;该系列一共 5 篇&#xff0c;后续会整理成 PDF 教程&#xff0c;本文是最后一篇。 这篇文章主要讲解 IOC 容器的创建过程&#xff0c;让你对整体有一个全局的认识&#xff0c;文章没有复杂嵌套的 debug 流程&am…

SA实战 ·《SpringCloud Alibaba实战》第06章-快速搭建三大微服务并完成交互开发与测试

作者:冰河 星球:http://m6z.cn/6aeFbs 博客:https://binghe.gitcode.host 文章汇总:https://binghe.gitcode.host/md/all/all.html 大家好,我是冰河~~ 在《SpringCloud Alibaba实战》专栏中前面的文章,我们为开发用户微服务、商品微服务和订单微服务做了充分的准备。今天…

微服务 SpringBoot 整合 Redis GEO 实现附近商户功能

文章目录⛄引言♨️广播站一、Redis GEO 数据结构用法⛅GEO基本语法、指令⚡使用GEO存储经纬度、查询距离二、SpringBoot 整合Redis 导入 店铺数据 到GEO三、SpringBoot 整合 Redis 实现 附近商户功能☁️需求介绍⚡核心源码✅附近商户效果图⛵小结⛄引言 本文参考黑马 点评项…

Spring之xml方式整合第三方框架

目录 一&#xff1a;概述 二&#xff1a;代码演示 二&#xff1a;Spring整合MyBatis的原理剖析 三&#xff1a;案例演示 一&#xff1a;概述 xml整合第三方框架有两种整合方案&#xff1a; 不需要自定义名空间,不需要使用Spring的配置文件配置第三方框架本身内容,例如&…