通过阅读本文你将获得以下收获1.如何将Bitmap传到Native层处理2.如何使用代码实现纹理映射3.通过纹理映射实现一些有趣的效果上篇回顾上一篇一看就懂的OpenGL ES教程——临摹画手的浪漫之纹理映射(理论篇)已经详细叙述了纹理的概念以及纹理映射到图元上的原理都是纯理论略显枯燥今天就将理论付诸实践一起来看看具体代码如何实现纹理映射。最后再利用纹理映射来实现一些有意思的效果绝对不能错过~代码实战如何将图片传入Native层上一篇一看就懂的OpenGL ES教程——临摹画手的浪漫之纹理映射(理论篇)已经说过纹理就是携带图片信息的容器所以这里首先要获取到图片的信息不然还纹理映射个毛线在android的Java层获取位图的方式可谓妇孺皆知Bitmap bitmap ((BitmapDrawable) getResources().getDrawable(R.drawable.liyingai)).getBitmap();因为我们OpenGL代码是在Native层的那么怎么将Bitmap传给Native层处理呢其实直接传入即可在C层用jobject接收如果对于ndk还不太熟悉可以看下我之前写的入门文章初探ndk的世界一 然后ndk已经提供了对应的jnigraphics库来处理Bitmap相关操作它可以直接操作Bitmap的像素。使用之前先要在CmakeList中链接jnigraphics库target_link_libraries( # Specifies the target library. native-lib GLESv3 EGL android jnigraphics # 操作Bitmap的库 # Links the target library to the log library # included in the NDK. ${log-lib} )Java层创建绘制纹理的Native方法public native void drawTexture(Bitmap bitmap, Object surface);在Native层对应的方法如下Java_com_example_openglstudydemo_YuvPlayer_drawTexture(JNIEnv *env, jobject thiz, jobject bitmap, jobject surface)注意到在这里Bitmap对象已经是jobject类型。首先用jnigraphics库的AndroidBitmap_getInfo方法获取Bitmap对象的相关信息/** * Given a java bitmap object, fill out the {link AndroidBitmapInfo} struct for it. * If the call fails, the info parameter will be ignored. */ int AndroidBitmap_getInfo(JNIEnv* env, jobject jbitmap, AndroidBitmapInfo* info);第一个参数就是JNIEnv指针第二个参数为Bimtap对象第三个为结构体AndroidBitmapInfo的指针。AndroidBitmapInfo为何物呢其实它就类似一个水桶在函数执行完就将数据舀出来也就是获取到的信息会存放在AndroidBitmapInfo的结构体中对于图片来说最常见的信息莫过于宽高、像素格式等/** Bitmap info, see AndroidBitmap_getInfo(). */ typedef struct { /** The bitmap width in pixels. */ uint32_t width; /** The bitmap height in pixels. */ uint32_t height; /** The number of byte per row. */ uint32_t stride; /** The bitmap pixel format. See {link AndroidBitmapFormat} */ int32_t format; /** Bitfield containing information about the bitmap. * * pTwo bits are used to encode alpha. Use {link ANDROID_BITMAP_FLAGS_ALPHA_MASK} * and {link ANDROID_BITMAP_FLAGS_ALPHA_SHIFT} to retrieve them./p * * pOne bit is used to encode whether the Bitmap uses the HARDWARE Config. Use * {link ANDROID_BITMAP_FLAGS_IS_HARDWARE} to know./p * * pThese flags were introduced in API level 30./p */ uint32_t flags; } AndroidBitmapInfo;执行AndroidBitmap_getInfo方法的返回值会是以下几种情况成功返回为0。/** AndroidBitmap functions result code. */ enum { /** Operation was successful. */ ANDROID_BITMAP_RESULT_SUCCESS 0, /** Bad parameter. */ ANDROID_BITMAP_RESULT_BAD_PARAMETER -1, /** JNI exception occured. */ ANDROID_BITMAP_RESULT_JNI_EXCEPTION -2, /** Allocation failed. */ ANDROID_BITMAP_RESULT_ALLOCATION_FAILED -3, };一旦返回为0那么恭喜你已经成功拿到了Bitmap基本信息。但是光拿到Bitmap的基本信息还是不够的还记得上一篇一看就懂的OpenGL ES教程——临摹画手的浪漫之纹理映射(理论篇)提到纹理映射原理的时候说过遍历图形中所有的片段依次通过片段所在的位置坐标定位到其对应在纹理中的纹素再获取到对应的颜色。我们知道一张2D图片其实就是一个二维数组按照一定的格式每若干个数组元素其实就是代表一个纹素所以要拿到对应的纹素首先要拿到图片的像素二维数组。所幸的事jnigraphics库的AndroidBitmap_lockPixels已经帮我们做好这件事了/** * Given a java bitmap object, attempt to lock the pixel address. * Locking will ensure that the memory for the pixels will not move * until the unlockPixels call, and ensure that, if the pixels had been * previously purged, they will have been restored. * * If this call succeeds, it must be balanced by a call to * AndroidBitmap_unlockPixels, after which time the address of the pixels should * no longer be used. * * If this succeeds, *addrPtr will be set to the pixel address. If the call * fails, addrPtr will be ignored. */ int AndroidBitmap_lockPixels(JNIEnv* env, jobject jbitmap, void** addrPtr);前两个参数不言而喻最后一个参数就是指向Bitmap像素二维数组的二级指针如果对于二级指针不太理解可以看下我之前的博文 漫谈C语言指针三 简单来说该方法的作用就是通过一个二级指针指向传过来的Bitmap的像素数组。注意这个方法的名字带有lock即它会锁一些东西。锁什么呢通过方法的注释可知会锁住像素数据的内存直到AndroidBitmap_unlockPixels方法调用才解锁。关于如何处理Bitmap纹素就先看到这至于拿到的像素数据二级指针要怎么用等会再解答我们再看看其他的纹理映射逻辑先。[图片上传失败…(image-3ed02d-1668059205063)]添加纹理坐标上一篇一看就懂的OpenGL ES教程——临摹画手的浪漫之纹理映射(理论篇)已经提及过纹理坐标的概念所以这里我们需要指定纹理坐标这里指定坐标的意义是指定需要进行纹理映射的那一部分纹理的顶点的坐标点比如还是下面这张图就是指定了左边需要映射的三角形的三个顶点在整个纹理中的坐标为了简单我们这里先映射整张图吧float vertices[] { // 图元顶点坐标 // 纹理坐标 0.5f, 0.5f, 0.0f, 1.0f, 1.0f, // top right 0.5f, -0.5f,0.0f, 1.0f, 0.0f, // bottom right -0.5f, -0.5f,0.0f, 0.0f, 0.0f, // bottom left -0.5f, 0.5f, 0.0f, 0.0f, 1.0f // top left };这里的纹理坐标就是指定了整张图片四个顶点。当然也可以指定只采样图片的一部分后面会演示然后依然像一看就懂的OpenGL ES教程——缓冲对象优化程序二 一样使用VBO, VAO, EBO优化程序unsigned int indices[] { 0, 1, 3, // first triangle 1, 2, 3 // second triangle }; unsigned int VBO, VAO, EBO; glGenVertexArrays(1, VAO); glGenBuffers(1, VBO); glGenBuffers(1, EBO); glBindVertexArray(VAO); glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);着色器逻辑既然添加了纹理坐标了根据经验和直觉着色器是不是就要添加一个变量去接收纹理坐标了呢没错这是毋庸置疑的~顶点着色器#version 300 es layout (location 0) in vec4 aPosition; //新增的接收纹理坐标的变量 layout (location 1) in vec2 aTexCoord; //纹理坐标输出给片段着色器使用 out vec2 TexCoord; void main() { //直接把传入的坐标值作为传入渲染管线。gl_Position是OpenGL内置的 gl_Position aPosition; //纹理坐标传给片段着色器 TexCoord aTexCoord; };这里要新增一个接收纹理坐标的变量aTexCoord不过因为采样这个任务还是交给了片段着色器来完成毕竟着色还是片段着色器要干的活所以最终还是提供给片段着色器使用所以又用输出变量TexCoord“送”了出去。片段着色器#version 300 es precision mediump float; //新增的接收纹理坐标的变量 in vec2 TexCoord; out vec4 FragColor; //传入的纹理 uniform sampler2D ourTexture; void main() { //texture方法执行具体的采样 FragColor texture(ourTexture, TexCoord); };这里用同名的TexCoord去接收顶点着色器传过来的纹理坐标。这里开始出现了一个陌生的新变量类型sampler2D看下官网的定义Asampleris a set of GLSL variable types. Variables of one of the sampler types must be uniforms or as function parameters. Each sampler in a program represents a single texture of a particular texture type. The type of the sampler corresponds to the type of the texture that can be used by that sampler.可见它就是代表一个纹理对象这里sampler2D中的“2D”代表的就是2D纹理。但是说它代表一个纹理对象其实是不准确的更准确的是代表一个纹理单元通过纹理单元去绑定一个纹理对象从而间接绑定纹理对象。它只能被uniform修饰或者作为方法参数这里被uniform修饰也就代表了一帧图像内这个纹理单元是不会变的即对应的纹理的图片是不变的。再看看main函数里面唯一的宠儿FragColor texture(ourTexture, TexCoord);它就是传说中重中之重的采样函数了具体来说就是获取到传入的具体纹理坐标值TexCoord在ourTexture对应的纹理上的纹素的颜色当然由于不同的过滤模式会导致具体采样颜色的细节不同。你可能会问这里的TexCoord具体的坐标值是多少呢如果这样问那你可能8成没看过我之前讲过光栅化插值这个“骚操作”的博文一看就懂的OpenGL ES教程——这或许是你遇过最难画的三角形五 如果看过就知道在这里你传入顶点着色器的纹理坐标的那几个值已经经过光栅化等的处理把它通过几何关系转化为对应的一个坐标值了。这么一想是不是整个流程都非常通畅了呢纹理对象配置所以这里我们先通过AndroidBitmap_getInfo方法获取Bitmap基本信息//存储Bitmap基本信息的结构体 AndroidBitmapInfo bmpInfo; if (AndroidBitmap_getInfo(env, bitmap, bmpInfo) 0) { LOGD(AndroidBitmap_getInfo() failed ! ); return; }然后获取Bitmap像素数组的指针void *bmpPixels; AndroidBitmap_lockPixels(env, bitmap, bmpPixels);此时Bitmap像素数组的指针bmpPixels枪在手跟我走~接下来就是配置纹理对象了。配置什么呢还记得上一篇博文 一看就懂的OpenGL ES教程——临摹画手的浪漫之纹理映射(理论篇) 讲的纹理环绕和纹理过滤么不记得的话直接回去看看这篇博文先吧。前面讲过纹理对象就是一个OpenGL Object所以它的用法和其他的OpenGL Object是非常相似的以下是纹理对象的结构图:Diagram of the contents of a texture object可以看出纹理对象由纹理数据存储区采样参数纹理参数构成。根据之前博文 一看就懂的OpenGL ES教程——缓冲对象优化程序一 写的使用一个OpenGL Object的几部曲创建对象——绑定对象——处理相关操作逻辑——解绑对象——销毁对象纹理对象也是如此。//纹理id unsigned int texture1; //创建纹理 glGenTextures(1, texture1); //绑定纹理 glBindTexture(GL_TEXTURE_2D, texture1);绑定纹理开始具体的采样参数配置当然不配置也有默认配置一般最好配置一下为好//纹理环绕配置 //横坐标环绕配置 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); // set texture wrapping to GL_REPEAT (default wrapping method) //纵坐标环绕配置 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); //纹理过滤配置 // set texture filtering parameters配置纹理过滤 //纹理分辨率大于图元分辨率即纹理需要被缩小的过滤配置 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); //纹理分辨率小于图元分辨率即纹理需要被放大的过滤配置 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);纹理对象的配置是通过glTexParameteri函数实现的void glTexParameteri(GLenumtarget,GLenumpname,GLintparam);target是指定绑定的纹理目标必须为GL_TEXTURE_1D,GL_TEXTURE_1D_ARRAY,GL_TEXTURE_2D,GL_TEXTURE_2D_ARRAY,GL_TEXTURE_2D_MULTISAMPLE,GL_TEXTURE_2D_MULTISAMPLE_ARRAY,GL_TEXTURE_3D,GL_TEXTURE_CUBE_MAP,GL_TEXTURE_CUBE_MAP_ARRAY, o,GL_TEXTURE_RECTANGLE中的一种。我们映射的是普通的2D纹理所以使用GL_TEXTURE_2D。pname为需要配置具体配置种类。param为具体的配置的值。首先是纹理环绕配置这里通过 首先是纹理环绕配置这里指定的配置种类为GL_TEXTURE_WRAP_S和GL_TEXTURE_WRAP_T分别表示在s和t轴方向的采样环绕配置。GL_REPEAT表示超过范围重复出现。s和t轴是什么看下上篇文章这个熟悉的表示纹理坐标图估计你就懂了~然后是纹理过滤配置://纹理分辨率大于图元分辨率即纹理需要被缩小的过滤配置 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); //纹理分辨率小于图元分辨率即纹理需要被放大的过滤配置 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);GL_TEXTURE_MIN_FILTER和GL_TEXTURE_MAG_FILTER分别代表纹理被缩小和放大的场景。上一篇文章一看就懂的OpenGL ES教程——临摹画手的浪漫之纹理映射(理论篇)已经提到过当进行采样的时候纹理的纹素和图元的片段往往不是一样多的简单理解就是图元面积和纹理图片的面积不一样大这也就导致了当纹理映射的时候我们要做类似将纹理的几个顶点“拉伸”或者“收缩”到和图元顶点贴合在一起的时候纹理会被放大或者缩小于是就需要在纹理被放大和缩小2种情况下分别进行采样过滤的配置。接下来也就是最重要的一步那就是将前一步获取到的图片数据传给纹理对象glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, bmpInfo.width, bmpInfo.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, bmpPixels);看起来有点眼熟的bmpPixels正是前一步获取到的图片像素数组的指针。glTexImage2D方法的声明为void glTexImage2D(GLenumtarget, GLintlevel, GLintinternalformat, GLsizeiwidth, GLsizeiheight, GLintborder, GLenumformat, GLenumtype, const void *data);target依然代表纹理目标。level这里指的是mipmap的层级mipmap还没讲到这里我们暂时只传0。internalformat表示纹理存储在GPU中的颜色格式。包括Base Internal Formats、Sized Internal Formats、Compressed Internal Formats。最常见的Base Internal Formats有以下格式Base Internal FormatRGBA, Depth and Stencil ValuesInternal ComponentsGL_DEPTH_COMPONENTDepthDGL_DEPTH_STENCILDepth, StencilD, SGL_REDRedRGL_RGRed, GreenR, GGL_RGBRed, Green, BlueR, G, BGL_RGBARed, Green, Blue, AlphaR, G, B, Awidth和height分别表示纹理图片的宽度和高度一般要求至少有1024个纹素。border这个据说是历史遗留的一个参数现在固定传0就好。format:表示传入的纹理像素数据的颜色格式注意和internalformat的区别。比如GL_RED、GL_RG、GL_RGB,GL_BGR、GL_RGBA,GL_BGRA。type表示传入的纹理像素数据数组的元素的数据类型比如GL_UNSIGNED_BYTE,GL_BYTE,GL_UNSIGNED_SHORT,GL_SHORT,GL_UNSIGNED_INT,GL_INT,GL_HALF_FLOAT,GL_FLOAT等等。data这就是传入的纹理像素数据的指针了。还是那句话OpenGL为了强大的功能性牺牲了使用的方便性导致它就像一个憨憨需要把传入的数据的细枝末节非常唠叨地告诉它它才知道怎么去解析传入的数据。这里我们按如下参数来传glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, bmpInfo.width, bmpInfo.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, bmpPixels);首先映射的是2D纹理所以target传GL_TEXTURE_2D。然后通过AndroidBitmap_lockPixels方法得到的像素数据格式为RGBA所以internalformat和format都传GL_RGBA。接下来尺寸数据传从bmpInfo获取的宽高数据这里像素数据的每个通道由8位组成即范围为0-255所以对应的格式为GL_UNSIGNED_BYTE。然后又是熟悉的解析顶点属性数组数据分别传入顶点和纹理坐标数据如果还不清楚具体是怎么解析的请看系列的前面几篇博文//顶点坐标 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void *) 0); glEnableVertexAttribArray(0); //纹理坐标 glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void *) (3 * sizeof(float))); glEnableVertexAttribArray(1);为了增强大家的学习效果这次纹理映射的图片就依旧用经典的女神图。运行看下效果额图片怎么上下颠倒了。。这是Android平台的OpenGL es一个扎根多年的历史大坑不要问我出现的原因我只知道在Android平台的OpenGL es纹理坐标的原点是在左上角点即一般一般情况下0.0,1.0点而不是常见的左下角点导致我们直接使用传入的纹理坐标会发生上下沿y轴0.5的直线发生镜面翻转。在Android平台的OpenGL es真正的纹理坐标如下图红色文字所示所以这里顶点着色器传给片段着色器的纹理坐标我们需要做一点调整#version 300 es layout (location 0) in vec4 aPosition; layout (location 1) in vec2 aTexCoord; out vec2 TexCoord; void main() { gl_Position aPosition; //纹理坐标要经过上下翻转再传给片段着色器 TexCoord vec2(aTexCoord.x, 1.0 - aTexCoord.y);; };再运行一下完美聪明的你可能又觉察到一丝不对劲了……片段着色器中表示纹理纹理单元的变量ourTexture我们都没传咋就能够采样了呢原因很简单OpenGL内部帮我们传了。如果当前的渲染只需要一个纹理单元的情况下OpenGL会默认我们使用的是第一个纹理单元即GL_TEXTURE0。所以片段着色器声明的sampler2D对象就会默认赋值为0,0则代表和GL_TEXTURE0的纹理关联。而在客户端程序中我们也并没有制定创建的纹理是属于哪个纹理单元的所以默认也为第一个纹理单元即GL_TEXTURE0所以对该纹理对象的所有操作都默认为针对即GL_TEXTURE0对应的纹理单元所以我们的数据其实是默认和片段着色器的ourTexture变量关联上的。实现多图层混叠刚才实现的是单个纹理单元的渲染接下来我要做一件有趣的事情就是将石原美里和李英爱的图片混合在一起惊不惊喜~~首先新增一个纹理对象并配置参数和纹理数据glGenTextures(1, texture2); glBindTexture(GL_TEXTURE_2D, texture2); // set the texture wrapping parameters glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); // set texture wrapping to GL_REPEAT (default wrapping method) glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); // set texture filtering parameters glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, bmpInfo1.width, bmpInfo1.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, bmpPixels1); AndroidBitmap_unlockPixels(env, bitmap1);上面刚说的我想就不用在这里重复了吧。这里要增加的步骤是因为现在是需要2个纹理单元了所以我们需要手动对纹理单元进行赋值//对着色器中的纹理单元变量进行赋值 glUniform1i(glGetUniformLocation(program, ourTexture), 0); glUniform1i(glGetUniformLocation(program, ourTexture1), 1);关于Uniform变量的设置在一看就懂的OpenGL ES教程——这或许是你遇过最难画的三角形五已经提及这里就不赘述了。分别对片段着色器中的ourTexture和ourTexture1变量赋值0和1分别表示GL_TEXTURE0和GL_TEXTURE1纹理单元一一对应。然后将纹理单元和纹理对象进行绑定//将纹理单元和纹理对象进行绑定 //激活纹理单元下面的绑定就会和当前激活的纹理单元关联上 glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, texture1); glActiveTexture(GL_TEXTURE1); glBindTexture(GL_TEXTURE_2D, texture2);先使用glActiveTexture方法激活纹理单元然后根据OpenGL的规则接下来执行的glBindTexture对应的纹理对象就会和这个激活的纹理单元关联上。这样子便完成了纹理对象texture1和着色器中的变量ourTexture、纹理对象texture2和着色器中的变量ourTexture1的绑定。不得不说这个绑定真绕 片段着色器变成#version 300 es precision mediump float; in vec2 TexCoord; out vec4 FragColor; //传入的纹理 uniform sampler2D ourTexture; //新增纹理单元 uniform sampler2D ourTexture1; void main() { //对2个纹理进行混合 FragColor mix(texture(ourTexture, TexCoord), texture(ourTexture1, TexCoord), 0.5); };mix为OpenGL内置的函数表示对2个数进行按比例混合叠加这里就是对当前片段从2纹理采样得到的颜色值进行按照0.5的比例混合。运行看下是不是有点电影转场内味了是不是妙不可言~总结本文主要从代码实践角度详细盲猜可能网上没有比这个更详细的嘻嘻讲解为如何进行纹理映射最后通过将2个纹理进行混合实现了一个挺有意思的效果。当然不仅仅是混合这里就可以充分发挥想象力去干一些灰常有趣的事情这就是下一篇文章的内容即开始玩一些奇技淫巧了。项目代码opengl-es-study-demo 不断更新中参考纹理TextureSampler (GLSL)原创不易如果觉得本文对自己有帮助别忘了随手点赞和关注这也是我创作的最大动力~系列文章目录体系化学习系列博文请看音视频系统学习的浪漫马车之总目录实践项目介绍一个自己刚出炉的安卓音视频播放录制开源项目C/C基础与进阶之路音视频理论基础系列专栏音视频开发实战系列专栏轻松入门OpenGL系列一看就懂的OpenGL ES教程——图形渲染管线的那些事一看就懂的OpenGL ES教程——再谈OpenGL工作机制一看就懂的OpenGL ES教程——这或许是你遇过最难画的三角形一一看就懂的OpenGL ES教程——这或许是你遇过最难画的三角形二一看就懂的OpenGL ES教程——这或许是你遇过最难画的三角形三一看就懂的OpenGL ES教程——这或许是你遇过最难画的三角形四一看就懂的OpenGL ES教程——这或许是你遇过最难画的三角形五一看就懂的OpenGL ES教程——缓冲对象优化程序一一看就懂的OpenGL ES教程——缓冲对象优化程序二一看就懂的OpenGL ES教程——临摹画手的浪漫之纹理映射(理论篇)作者半岛铁盒里的猫链接https://juejin.cn/post/7155040552353234951最后如果想要成为架构师或想突破20~30K薪资范畴那就不要局限在编码业务要会选型、扩展提升编程思维。此外良好的职业规划也很重要学习的习惯很重要但是最重要的还是要能持之以恒任何不能坚持落实的计划都是空谈。如果你没有方向这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》帮大家将杂乱、零散、碎片化的知识进行体系化的整理让大家系统而高效地掌握Android开发的各个知识点。相对于我们平时看的碎片化内容这份笔记的知识点更系统化更容易理解和记忆是严格按照知识体系编排的。全套视频资料一、面试合集二、源码解析合集三、开源框架合集欢迎大家一键三连支持若需要文中资料直接点击文末CSDN官方认证微信卡片免费领取↓↓↓