0%

知识点

  • OpenGL:一个定义了函数布局和输出的图形 API 的正式规范。
  • GLAD:一个拓展加载库,用来加载并设定所有 OpenGL 函数指针,从而能够使用所有(现代)OpenGL 函数。
  • 视口(Viewport):需要渲染的窗口。
  • 图形管线(Graphics Pipeline):一个顶点在呈现为像素之前经过的全部过程。
  • 着色器(Shader):一个运行在显卡上的小型程序。很多阶段的图形管道都可以使用自定义的着色器来代替原有的功能。
  • 标准化设备坐标(Normalized Device Coordinates, NDC):顶点在通过在剪裁坐标系中剪裁与透视除法后最终呈现在的坐标系。所有位置在NDC下 -1.0到1.0 的顶点将不会被丢弃并且可见。
  • 顶点缓冲对象(Vertex Buffer Object):一个调用显存并存储所有顶点数据供显卡使用的缓冲对象。
  • 顶点数组对象(Vertex Array Object):存储缓冲区和顶点属性状态。
  • 索引缓冲对象(Element Buffer Object):一个存储索引供索引化绘制使用的缓冲对象。
  • Uniform:一个特殊类型的 GLSL 全局(在一个着色器程序中每一个着色器都能够访问uniform变量)的变量,并且只需要被设定一次。
  • 纹理(Texture):一种包裹着物体的特殊类型图像,给物体精细的视觉效果。
  • 纹理缠绕(Texture Wrapping):定义了一种当纹理顶点超出范围 (0, 1) 时指定 OpenGL 如何采样纹理的模式。
  • 纹理过滤(Texture Filtering):定义了一种当有多种纹素选择时指定 OpenGL 如何采样纹理的模式。(这通常在纹理被放大情况下发生。)
  • 多级渐远纹理(Mipmaps):被存储的材质的一些缩小版本,根据距观察者的距离会使用材质的合适大小。
  • stb_image.h:图像加载库。
  • 纹理单元(Texture Units):通过绑定纹理到不同纹理单元从而允许多个纹理在同一对象上渲染。
  • 向量(Vector):一个定义了在空间中 方向位置 的数学实体。
  • 矩阵(Matrix):一个矩形阵列的数学表达式。
  • GLM:一个为 OpenGL 打造的数学库。
  • 局部空间(Local Space):一个物体的初始空间。(所有的坐标都是相对于物体的原点的。)
  • 世界空间(World Space):所有的坐标都相对于全局原点。
  • 观察空间(View Space):所有的坐标都是从摄像机的视角观察的。
  • 裁剪空间(Clip Space):所有的坐标都是从摄像机视角观察的,但是该空间应用了 投影。(这个空间应该是一个顶点坐标最终的空间,作为顶点着色器的输出。OpenGL 负责处理剩下的事情(裁剪/透视除法)。)
  • 屏幕空间(Screen Space):所有的坐标都由屏幕视角来观察。(坐标的范围是从 0 到屏幕的宽/高。)
  • LookAt矩阵:一种特殊类型的 观察矩阵,它创建了一个坐标系,其中所有坐标都根据从一个位置正在观察目标的用户旋转或者平移。
  • 欧拉角(Euler Angles):被定义为 偏航角(Yaw)俯仰角(Pitch)滚转角(Roll) 从而通过这三个值构造任何 3D 方向。

参考资料

  1. learnopengl.com
阅读全文 »

摄像机

OpenGL 本身没有 摄像机(Camera) 的概念,但通过把场景中的所有物体往相反方向移动的方式来模拟出摄像机,产生一种观察者在移动的感觉,而不是场景在移动。

要定义一个摄像机,通常需要它在世界空间中的位置、观察的方向、一个指向它右测的向量以及一个指向它上方的向量

图片来源于:learnopengl.com

Look At 矩阵

Look At 矩阵:使用 3个 相互垂直(或非线性) 的轴定义了一个坐标空间,使用这 3 个轴外加一个平移向量来创建一个矩阵,则可以用这个矩阵乘以任何向量来将其变换到所定义的坐标空间。

Look At 矩阵

其中 R 是右向量,U 是上向量,D 是方向向量 P 是摄像机位置向量。(注意,位置向量是相反的,因为希望把世界平移到与观察者自身移动的相反方向。)

欧拉角

**欧拉角(Euler Angle)**是可以表示 3D 空间中任何旋转的 3 个值,有:俯仰角(Pitch)偏航角(Yaw)滚转角(Roll),如图:
图片来源于:learnopengl.com

阅读全文 »

坐标系统

OpenGL 每次在顶点着色器运行后,可见的所有顶点都为 标准化设备坐标(Normalized Device Coordinate, NDC)**;即每个顶点的 xyz 坐标都应该在 **[-1.0, 1.0] 区域之间,超出这个坐标范围的顶点都将不可见。然后将这些标准化设备坐标传入光栅器(Rasterizer),将其变换为屏幕上的二维坐标或像素。

在图形渲染管线(Pipe line)中,物体的顶点在最终转化为屏幕坐标之前还会被变换到多个坐标系统(Coordinate System),其中 5 个比较重要的坐标系统:

  • 局部空间(Local Space,或者称为物体空间(Object Space))
  • 世界空间(World Space)
  • 观察空间(View Space,或者称为视觉空间(Eye Space))
  • 裁剪空间(Clip Space)
  • 屏幕空间(Screen Space)

坐标变换流程

图片来源:learnopengl.com

  1. 局部坐标是对象相对于局部原点的坐标,也是物体起始的坐标。
  2. 将局部坐标变换为世界空间坐标,世界空间坐标是处于一个更大的空间范围的。这些坐标相对于世界的全局原点,它们会和其它物体一起相对于世界的原点进行摆放。
  3. 将世界坐标变换为观察空间坐标,使得每个坐标都是从摄像机或者说观察者的角度进行观察的。
  4. 坐标到达观察空间之后,需要将其投影到裁剪坐标。裁剪坐标会被处理至 -1.0 到 1.0 的范围内,并判断哪些顶点将会出现在屏幕上。
  5. 将裁剪坐标变换为屏幕坐标,进行**视口变换(Viewport Transform)**的过程。(视口变换将位于 -1.0到1.0 范围的坐标变换到由 glViewport 函数所定义的坐标范围内;最后变换出来的坐标将会送到光栅器,将其转化为片段。)

局部空间

局部空间:是指物体自身所在的坐标空间,即对象最开始所在的地方。

世界空间

阅读全文 »

向量

**向量(Vector)**:是指一个同时具有大小(Magnitude)和方向(Direction),且满足平行四边形法则的几何对象。在数学上通常是在字母上面加一横表示向量,如:
向量形式

长度

图片来源于:learnopengl.com

通常通过勾股定理就计算:

向量长度

单位向量

单位向量(Unit Vector):长度是 1 的向量;其计算是通过向量的每个分量除以向量的长度得到:
单位向量

向量与标量运算

把一个向量一个标量(一个数字或者说是仅有一个分量的向量),其结果是把向量的每个分量分别进行该运算。例如加法运算:
向量与标量运算

阅读全文 »

纹理

纹理:一张贴到物体上的二维图像。

映射

纹理映射规则:坐标起始于(0, 0),也就是纹理图片的左下角,终始于(1, 1),即纹理图片的右上角;如图:

图片来源于:learnopengl.com

使用纹理坐标获取纹理颜色叫做**采样(Sampling)**。

环绕方式

纹理坐标的范围通常是从 (0, 0)(1, 1),但是如果把纹理坐标设置在范围之外,该如何处理呢?OpenGL 提供了更多的选择:

环绕方式 描述
GL_REPEAT 重复纹理图像(默认)。
GL_MIRRORED_REPEAT 和 GL_REPEAT 一样,但每次重复图片是镜像放置的。
GL_CLAMP_TO_EDGE 纹理坐标会被约束在 01 之间,超出的部分会重复纹理坐标的边缘,产生一种边缘被拉伸的效果。
GL_CLAMP_TO_BORDER 超出的坐标为用户指定的边缘颜色。

效果如图:

阅读全文 »

GLSL

着色器是运行在 GPU 的小程序,用于渲染管线的特定阶段工作。着色器是使用 GLSL 语言编写的,其格式大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#version 版本号

in 数据类型 变量名;
in 数据类型 变量名;

out 数据类型 变量名;

uniform 数据类型 变量名;

void main()
{
// 处理过程
输出变量 = 处理结果;
}

其中:#version 用于指定使用的 OpenGL 对应的版本号;in 类型是用于输入的数据(也就是上一阶段的输出数据);out 类型是用于输出的数据(也就是下一阶段的输入数据);uniform 是全局数据类型,其作用是用于在 CPU 配置 GPU 的数据。

数据类型

GLSL 中包含默认基础数据类型:intfloatdoubleuintbool;以及两种容器类型: 向量(Vector)矩阵(Matrix)

向量

GLSL 中的向量是一个可以包含有 123 或者 4 个分量的容器;分量的类型是默认基础类型的任意一个;如下( n 代表分量的数量):

类型 含义
vecn 包含 nfloat 分量的默认向量
bvecn 包含 nbool 分量的向量
ivecn 包含 nint 分量的向量
uvecn 包含 nunsigned int 分量的向量
dvecn 包含 ndouble 分量的向量

Uniform

阅读全文 »

最近公司有个项目需要对锁屏进行监控以便进行一些操作,然后在更新新版本的时候,审核竟然被拒绝了。原因竟然是调用了 Apple 不允许使用的 锁屏API ,如下方法一;后来改成方法二,终于审核通过了。

锁屏监听

  1. 方法一:(审核会被拒)

    • 导入头文件和宏定义

      1
      2
      3
      4
      5
      6
      7
      8
      //  AppDelegate.m

      #import <notify.h>

      #define NotificationLock CFSTR("com.apple.springboard.lockcomplete")
      #define NotificationChange CFSTR("com.apple.springboard.lockstate")
      #define NotificationPwdUI CFSTR("com.apple.springboard.hasBlankedScreen")
      #define LOCK_SCREEN_NOTIFY @"LockScreenNotify"
    • 定义监听锁屏函数

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      //  AppDelegate.m

      static void screenLockStateChanged(CFNotificationCenterRef center,
      void *observer,
      CFStringRef name,
      const void *object,
      CFDictionaryRef userInfo)
      {
      NSString *lockstate = (__bridge NSString *)name;
      if ([lockstate isEqualToString:(__bridge NSString *)NotificationLock])
      {
      // 发送锁屏通知
      [[NSNotificationCenter defaultCenter] postNotificationName:LOCK_SCREEN_NOTIFY
      object:nil];
      NSLog(@"Lock screen.");
      }
      else
      {
      // 此处监听到屏幕解锁事件(锁屏也会掉用此处一次,所有锁屏事件要在上面实现)
      NSLog(@"Lock state changed.");
      }
      }
    • 添加监听函数

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 
      {
      CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(),
      NULL,
      screenLockStateChanged,
      NotificationLock,
      NULL,
      CFNotificationSuspensionBehaviorDeliverImmediately);
      CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(),
      NULL,
      screenLockStateChanged,
      NotificationChange,
      NULL,
      CFNotificationSuspensionBehaviorDeliverImmediately);

      }
    • 注意:该方法已被 Apple 禁止使用,上传的 App 审核会被拒绝!
      UnsunportLockScreen.png

  2. 方法二:(Apple 推荐使用的方法)

    • 实现 applicationProtectedDataWillBecomeUnavailable: 方法监听锁屏

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      //  AppDelegate.m

      #define LOCK_SCREEN_NOTIFY @"LockScreenNotify"

      - (void)applicationProtectedDataWillBecomeUnavailable:(UIApplication *)application
      {
      [[NSNotificationCenter defaultCenter] postNotificationName:LOCK_SCREEN_NOTIFY
      object:nil];
      NSLog(@"Lock screen.");
      }
    • 实现 applicationProtectedDataDidBecomeAvailable: 方法监听解锁

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      //  AppDelegate.m

      #define UN_LOCK_SCREEN_NOTIFY @"UnLockScreenNotify"

      - (void) applicationProtectedDataDidBecomeAvailable:(UIApplication *)application
      {
      [[NSNotificationCenter defaultCenter] postNotificationName:UN_LOCK_SCREEN_NOTIFY
      object:nil];
      NSLog(@"UnLock screen.");
      }
    • 官网 API 说明如下:

      When the user locks the device, the system calls the app delegate’s applicationProtectedDataWillBecomeUnavailable: method. Data protection prevents unauthorized access to files while the device is locked. If your app references a protected file, you must remove that file reference and release any objects associated with the file when this method is called. When the user subsequently unlocks the device, you can reestablish your references to the data in the app delegate’s applicationProtectedDataDidBecomeAvailable: method.


参考资料

阅读全文 »

图形渲染管线(Graphics-Pipeline )

我们知道,展示在屏幕上的图像都是由一个个单独的不同颜色的像素组合在一起形成的;那么,该如何确定像素以何种组合方式,以及何种颜色进行展示呢?这就需要图形渲染管线。

图形渲染管线:就是接受一组 3D 坐标,经过一系列的转换最终变成屏幕上展示的图像的处理过程。根据其不同的处理,可以细分为几个阶段:

图片来源于:learnopengl.com

  1. 顶点着色器(Vertex Shader):把输入的 3D 坐标转为另一空间的 3D 坐标,这里是产生图形变换(如:移动、放大。。。)的地方。
  2. 图元装配(Shape Assembly):顶点着色器 输出的一系列顶点作为输入并装配成指定图元的形状。
  3. 几何着色器(Geometry Shader):图元装配 的一系列顶点的作为输入,可以通过产生新顶点构造出新的(或是其它的)图元来生成其他形状。
  4. 光栅化(Rasterization):把图元映射为最终屏幕上相应的像素,生成供片 段着色器 使用的片段。
  5. 片段着色器(Fragment Shader):计算一个像素的最终颜色,这也是所有OpenGL高级效果(比如光照、阴影、光的颜色。。。)产生的地方。
  6. 测试与混合(Tests and Blending):检测片段的对应的深度和模板值,用它们来判断这个像素是其它物体的前面还是后面,决定是否应该丢弃;这个阶段也会检查 alpha 值并对物体进行混合;因此,即使在片段着色器中计算出来了一个像素输出的颜色,在渲染多个三角形的时候最后的像素颜色也可能完全不同。

在这些处理流程中,我们大多数的工作都是在 顶点着色器片段着色器 中。

NDC

在了解顶点处理之前,需要先了解 标准化设备坐标(Normalized Device Coordinates, NDC)
标准化设备坐标(Normalized Device Coordinates, NDC):标准化设备坐标是一个 xyz 值在 -1.01.0 的一小段空间;任何落在范围外的坐标都会被丢弃/裁剪,不会显示在你的屏幕上。如图:
图片来源于:learnopengl.com

顶点处理

有了顶点数据,接下来就是要告诉 OpenGL 该如何处理这些顶点数据:

阅读全文 »

使用 GLFW 要点

  1. 使用之前需要对其进行初始化;

    1
    glfwInit();
  2. 告诉 GLFW 所使用的 OpenGL 版本以及模式(如果是 Apple 则还需要向前兼容);

    1
    2
    3
    4
    5
    6
       glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    #ifdef __APPLE__
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GLFW_TRUE);
    #endif
  3. 指定 OpenGL 上下文;

    1
    glfwMakeContextCurrent(window);
  4. 注册相关回调函数;

    • 窗体大小改变回调;

      1
      glfwSetFramebufferSizeCallback(window, frameBufferSizeCB);
    • 错误回调;

      1
      glfwSetErrorCallback(errorCallback);
  5. 在使用 OpenGL 相关 API 之前,必需先初始化 GLAD

    1
    gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)
  6. 开始渲染:

    • 在渲染循环中处理相关输入:键盘、鼠标。。。

    • 设置帧缓存清除时窗体背景颜色,避免帧切换时还出现上一帧内容;

      1
      2
      glClearColor(0.2, 0.3, 0.3, 1.0f);
      glClear(GL_COLOR_BUFFER_BIT);

Demo


参考资料

  1. learnopengl.com
阅读全文 »

这里不是比较 GLADGLEW 优劣问题,而是简单地说一下其实现流程。

GLAD

  1. 因为 OpenGL 只是一个 标准/规范,具体的实现是由驱动开发商针对特定显卡实现的。由于 OpenGL 驱动版本众多,大多数函数的位置(内存地址)都无法在 编译 时确定下来,需要在 运行时 查询。所以开发者在开发使用 OpenGL时,需要在 运行时 获取函数的内存地址并将其保存在一个函数指针中供以后使用。取得地址的方法因平台而异,(Windows)大致流程 如下:

    1
    2
    3
    4
    5
    6
    7
    // 定义函数原型
    typedef void (*GL_GENBUFFERS) (GLsizei, GLuint*);
    // 找到正确的函数并赋值给函数指针
    GL_GENBUFFERS glGenBuffers = (GL_GENBUFFERS)wglGetProcAddress("glGenBuffers");
    // 现在函数可以被正常调用了
    GLuint buffer;
    glGenBuffers(1, &buffer);
  2. 然而写这些代码非常复杂,而且很繁琐,需要对每个可能使用的函数都要重复这个过程。不过 GLAD 的做过就是将上面的过程进行简化供开发者使用。

  3. 根据 Load OpenGL Functions WiKiwglGetProcAddress 在失败时返回 NULL,但一些实现将返回其他值。1,2和3,以及-1过程如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    void *GetAnyGLFuncAddress(const char *name)
    {
    void *p = (void *)wglGetProcAddress(name);
    if(p == 0 ||
    (p == (void*)0x1) || (p == (void*)0x2) || (p == (void*)0x3) ||
    (p == (void*)-1) )
    {
    HMODULE module = LoadLibraryA("opengl32.dll");
    p = (void *)GetProcAddress(module, name);
    }

    return p;
    }
  4. MacOSX 平台,在 OSX 10.2 后 GL Fuctionweak 链接,也就意味着可以直接调用它们,未实现的扩展将解析为 NULLApple 建议需要 getProcAddress 功能的程序使用 NSSymbol 直接查找函数指针。如下:

    • Listing C-1 :

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      #import <mach-o/dyld.h>
      #import <stdlib.h>
      #import <string.h>
      void * MyNSGLGetProcAddress (const char *name)
      {
      NSSymbol symbol;
      char *symbolName;
      symbolName = malloc (strlen (name) + 2);
      strcpy(symbolName + 1, name);
      symbolName[0] = '_';
      symbol = NULL;
      if (NSIsSymbolNameDefined (symbolName))
      symbol = NSLookupAndBindSymbol (symbolName);
      free (symbolName);
      return symbol ? NSAddressOfSymbol (symbol) : NULL;
      }
    • Listing C-2:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      #import "MyNSGLGetProcAddress.h" 
      static void InitEntryPoints (void);
      static void DeallocEntryPoints (void);

      // Function pointer type definitions
      typedef void (*glBlendColorProcPtr)(GLclampf red,GLclampf green,
      GLclampf blue,GLclampf alpha);
      typedef void (*glBlendEquationProcPtr)(GLenum mode);
      typedef void (*glDrawRangeElementsProcPtr)(GLenum mode, GLuint start,
      GLuint end,GLsizei count,GLenum type,const GLvoid *indices);

      glBlendColorProcPtr pfglBlendColor = NULL;
      glBlendEquationProcPtr pfglBlendEquation = NULL;
      glDrawRangeElementsProcPtr pfglDrawRangeElements = NULL;

      static void InitEntryPoints (void)
      {
      pfglBlendColor = (glBlendColorProcPtr) MyNSGLGetProcAddress
      ("glBlendColor");
      pfglBlendEquation = (glBlendEquationProcPtr)MyNSGLGetProcAddress
      ("glBlendEquation");
      pfglDrawRangeElements = (glDrawRangeElementsProcPtr)MyNSGLGetProcAddress
      ("glDrawRangeElements");
      }
      // -------------------------
      static void DeallocEntryPoints (void)
      {
      pfglBlendColor = NULL;
      pfglBlendEquation = NULL;
      pfglDrawRangeElements = NULL;;
      }
    • 关于以上代码的解释,可以到这里查看。

  5. 再回到 GLAD ,

    • glad.h 文件可以看到 glGenBuffers 的定义:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      #ifndef APIENTRYP
      #define APIENTRYP APIENTRY *
      #endif

      define GLAPI extern

      typedef void (APIENTRYP PFNGLDELETEBUFFERSPROC)(GLsizei n, const GLuint *buffers);
      GLAPI PFNGLDELETEBUFFERSPROC glad_glDeleteBuffers;
      #define glDeleteBuffers glad_glDeleteBuffers

    • glad.c 文件中可以看到 gladGetProcAddressPtr 的定义:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      #ifndef __APPLE__
      typedef void* (APIENTRYP PFNGLXGETPROCADDRESSPROC_PRIVATE)(const char*);
      static PFNGLXGETPROCADDRESSPROC_PRIVATE gladGetProcAddressPtr;
      #endif

      static
      int open_gl(void) {
      #ifdef __APPLE__
      static const char *NAMES[] = {
      "../Frameworks/OpenGL.framework/OpenGL",
      "/Library/Frameworks/OpenGL.framework/OpenGL",
      "/System/Library/Frameworks/OpenGL.framework/OpenGL",
      "/System/Library/Frameworks/OpenGL.framework/Versions/Current/OpenGL"
      };
      #else
      static const char *NAMES[] = {"libGL.so.1", "libGL.so"};
      #endif

      unsigned int index = 0;
      for(index = 0; index < (sizeof(NAMES) / sizeof(NAMES[0])); index++) {
      libGL = dlopen(NAMES[index], RTLD_NOW | RTLD_GLOBAL);

      if(libGL != NULL) {
      #ifdef __APPLE__
      return 1;
      #else
      gladGetProcAddressPtr = (PFNGLXGETPROCADDRESSPROC_PRIVATE)dlsym(libGL,
      "glXGetProcAddressARB");
      return gladGetProcAddressPtr != NULL;
      #endif
      }
      }

      return 0;
      }

GLEW

  1. GLEW 的出现也是为了在开发时,能够针对不能显卡制造商的显卡的 OpenGL 的不同版本,实现在 运行时 获取函数的内存地址并将其保存在一个函数指针中供以使用。

  2. glew.h 文件中可以看懂 glGenBuffers 的定义:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #ifndef GLEW_GET_FUN
    #define GLEW_GET_FUN(x) x
    #endif

    #ifndef GLAPIENTRY
    #define GLAPIENTRY
    #endif

    typedef void (GLAPIENTRY * PFNGLGENBUFFERSPROC) (GLsizei n, GLuint* buffers);

    GLEW_FUN_EXPORT PFNGLGENBUFFERSPROC __glewGenBuffers;

    #define glGenBuffers GLEW_GET_FUN(__glewGenBuffers)

    小结

    GLADGLEW 的作用都是帮助在开发 OpenGL 时简化其在 运行时 获取函数的内存地址并将其保存在一个函数指针中供以使用的流程。

参考资料

阅读全文 »