四、iOS中图形图像渲染技术栈及流水线

四、iOS中图形图像渲染技术栈及流水线,第1张

下图为 iOS APP 图形渲染框架, APP 在显示可视化的图形时,使用到了 Core Animation 、 Core Graphics 、 Core Image 等框架,这些框架在渲染图形时,都需要通过 OpenGL ES / Metal 来驱动 GPU 进行渲染与绘制。

UIKit 是 iOS 开发者最常用的框架,里面提供了 UIView 。

UIView 供开发者用来:

Core Animation 源自于 Layer Kit, 是一个复合引擎,主要职责包含渲染( CALayer )、构建和实现动画。 CALayer 是用户所能在屏幕上看到一切的基础。

Core Graphics 是基于Quartz 的高级绘图引擎,主要用于运行时绘制图像。其功能有绘制路径、颜色管理、渐变、阴影、创建图像、图像遮罩、PDF文档创建显示及分析。

Core Image 拥有一系列现成的图像过滤器,可以对已存在的进行高效处理。大部分情况下,``Core Image ``` 是在GPU中完成工作,如果GPU忙,会使用CPU进行处理。

Core Animation 、 Core Graphics 、 Core Image 这个三个框架间也存在着依赖关系。

上面提到 CALayer 是用户所能在屏幕上看到一切的基础。所以 Core Graphics 、 Core Image 是需要依赖于 CALayer 来显示界面的。由于 CALayer 又是 Core Animation 框架提供的,所以说 Core Graphics 、 Core Image 是依赖于``Core Animation ```的。

上文还提到每一个 UIView 内部都关联一个 CALayer 图层,即 backing layer ,每一个 CALayer 都包含一个 content 属性指向一块缓存区,即 backing store , 里面存放位图(Bitmap)。 iOS 中将该缓存区保存的称为 寄宿图 。

这个寄宿图有两个设置方式:

CALayer 是如何调用 GPU 并显示可视化内容的呢?下面我们就需要介绍一下 Core Animation 流水线的工作原理。

事实上,app 本身并不负责渲染,渲染则是由一个独立的进程负责,即 Render Server 进程。

App 通过 IPC 将渲染任务及相关数据提交给 Render Server 。 Render Server 处理完数据后,再传递至 GPU。最后由 GPU 调用 iOS 的图像设备进行显示。

Core Animation 流水线的详细过程如下:

对上述步骤进行串联,它们执行所消耗的时间远远超过 1667 ms,因此为了满足对屏幕的 60 FPS 刷新率的支持,需要将这些步骤进行分解,通过流水线的方式进行并行执行,如下图所示。

在 Core Animation 流水线中,app 调用 Render Server 前的最后一步 Commit Transaction 其实可以细分为 4 个步骤:

参考文章: iOS 图像渲染原理

语言

前端和终端作为面向用户端的程序,有个共同特点:需要依赖用户机器的运行环境,所以开发语言基本上是没有选择的,不像后台想用什么就用什么,iOS只能用Objective-C,前端只能javascript,当然iOS还可以用RubyMotion,前端还能用GWT/CoffieScript,但不是主流,用的人很少,真正用了也会多出很多麻烦。

这两者有个有意思的对比:变量/方法命名的风格正好相反。苹果一直鼓吹用户体验,写代码也不例外,程序命名都是用英文全称并且要多详细有多详细,力求看变量和方法名就能知道是干嘛的,例如application:didFinishLaunchingWithOptions:。而js因为每次都要从网络下载,要力求减少代码体积,所以变量方法名是尽量用缩写,实际上有代码压缩工具,无论变量名写多长最终上线的效果是一样的,但大家也都习惯了用短的命名,例如上述objc的application:didFinishLaunchingWithOptions:方法在js里习惯的命名是:$()。

objc与js都是动态语言,使用起来还蛮像,但objc是编译型,速度快,很多错误也能在编译过程中被发现,js是解释型,性能依赖于解释引擎,即使在强劲的v8引擎下性能也赶不上编译型语言,语言太动态,变量完全没有类型,写起来爽,debug起来稍微费点劲。一直感觉js轻巧灵活放荡不羁充满各种奇技*巧,objc中规中矩没c++ java那么严肃也没有js那么灵活。

线程

前端开发几乎不需要线程这个概念,浏览器实现上页面HTML和CSS解析渲染可能与js不在同一个线程,但所有js代码只执行在一条线程上,不会并发执行,也就不需要考虑各种并发编程的问题。在新的JS特性中可以创建worker任务,这样的任务是可以另起一条线程并行执行的,但由于并不是所有浏览器都支持,不同线程传递数据各个标准定的还不一样,使用场景也少,似乎没有大规模用起来。对于数据库 *** 作/发送网络请求这样的任务是在不同于js代码执行线程的,不过这些都由浏览器管理,前端无需关心也无法影响这些线程,只需接收事件回调,不需要处理任何并发问题。

终端开发需要大量使用多线程,iOS有一条主线程,UI渲染都在这个线程,其他耗时长的逻辑或者数据库IO/网络请求都需要自己另开线程执行,否则会占用主线程的时间,导致界面无法响应用户交互事件,或者渲染慢导致滚动卡顿。程序逻辑分布在多个线程里跑,需要处理好各种代码并发执行可能带来的数据不一致/时序错乱之类的问题,并发也导致有些bug难以排查,一不留神就掉坑,需要适当用一些队列/锁保证程序的执行顺序。iOS提供了一套多线程管理的方法GCD,已经把线程和队列封装得非常简单易用功能强大,比其他端或后台是好很多了,但还是会花大量功夫在处理多线程问题上。

存储

终端开发需要大量的数据存储逻辑,手机APP不像浏览器,用户打开浏览器必定是连着网,但打开一个APP时很可能是离线,也很可能处于网络状况极差的移动GPRS,所以必须把之前请求回来的数据保存好。保存数据后又需要与服务端最新的数据同步,如果全量同步数据量太大,耗流量速度也慢,于是需要增量同步,需要与服务端一起制定实现增量数据返回的方案,需要处理好客户端与服务端数据一致性的问题。当数据存储量大结构复杂时,还需要利用好有限的内存做cache,优化各类存储查询性能。

前端在桌面端很少需要存储,除非是Single Page App,不存储自然就不需要数据更新的一系列工作,数据都是从后台取出拼接后直接显示到页面上,即使像微博有可以在页面内不断加载更多数据,数据也只存在于内存,不会持久化存储,因为桌面端网速稳定,不计流量,所有数据可以直接从后端拿取,客户端没必要再做一套存储。移动端那些做得很像原生APP的Web应用就跟终端开发一样了,数据同样保存到SQLite,存储逻辑以及要处理的问题都差不多。

框架

在第三方框架上Web前端和iOS开发完全相反,Web原生弱小又十分开放,让大量第三方框架和类库可以施展拳脚,而iOS原生强大又十分封闭,导致第三方框架没有多少生存空间。

浏览器一开始只为内容型的网页而设计,js也只是这个网页上能加点小特效的脚本语言,在Web应用时代跟不上发展,需要很多第三方库和框架辅助,再加上前端开发是完全开放的领域,导致库和框架百花齐放多如牛毛,在初期多数库的作用集中在封装dom *** 作,大家不断重复造dom *** 作基础库的轮子,在一段时间百家争鸣后独尊jQuery,在有使用库的网站中90%以上使用jq,几乎成了个标准基础库。后期大家已经不再重复造这个基础库的轮子了,多了一些代码组织和前端架构的框架,例如一些帮助项目模块化的框架requirejs,MVC框架backbone/angularjs等。

iOS开发苹果已提供了完整的开发框架cocoa,而这框架在每一代系统中都在升级优化和添砖加瓦,开发模式也已经定型,第三方框架没有多少生存空间,大量流行的开源项目是一些通用组件和库,像网络请求库AFNetworking,数据库 *** 作库FMDB。而一些大的框架像beeFramework/ReactiveCocoa较难流行起来。

兼容

前端开发需要兼容大——量的浏览器,桌面的chrome,safari,ie6-ie10,firefox,以及各种套壳猎豹360等浏览器,移动端iOS/Android各自的浏览器,以及无限的不同的屏幕尺寸。看起来挺可怕,实际上也没那么难搞,只是拿出来吓唬下人。桌面端chrome/safari以及各种套壳的极速模式用的都是Webkit,差异很小,firefox也大体遵从标准实现,与Webkit差别不大,旧的ie6/7就需要特别照顾,不过很多网站都不支持ie6了,移动端更是一家亲,全是Webkit,除了新特性上的支持程度不一,其他差异不大。对于不同的屏幕尺寸,高端点的会用响应式布局,针对不同屏幕尺寸自适应到不同布局,一般点的桌面端定死宽度,移动端拉伸自适应宽度就搞定。

终端开发也需要兼容各种不同的系统版本和手机尺寸,Android不用说,iOS也有35/4/47/55/97英寸这些尺寸,不过兼容起来跟Web一样挺容易,就是自适应宽度,iOS的UIKit把这些都处理好了,还有autolayout,sizeClass等高级特性可用,在尺寸上并不用花太多功夫。系统版本上iOS7为分水岭,iOS7前后版本UI上差异比较大,需要做一些功夫兼容,不过iOS用户更新换代很快,预计再过一两年iOS7以下用户就可以忽略了。

性能

终端和前端都是面向用户的,性能优化目的都是尽快呈现内容,以及让程序在用户 *** 作下流畅运行。终端主要关注的是存储/渲染性能。当一个APP存储数据量大,数据关系复杂时,数据查询很容易成为性能瓶颈,需要不断优化数据存取的效率,规划数据IO线程,设计内存cache,利用好终端设备有限的内存,渲染上避免重复渲染,尽可能复用视图,寻找最高效的渲染方案。

前端关注页面加载速度,由于Web页面的结构/样式/程序/资源都是实时请求的,要让页面更快呈现内容,就要优化这些请求,让这些资源以最快速度加载下来,包括合并/合并代码减少请求数,压缩代码,并行请求,根据版本号缓存代码请求,gzip压缩,模块/懒加载等。此外跟终端一样也关注渲染性能,遵从一些规则避免页面reflow,避免使用CSS阴影这样耗性能的特效,用CSS3动画代替js等。

编译

终端开发需要编译的过程,把程序编译成机器语言,再与各种库链接后生成平台对应的可执行文件,最后由 *** 作系统调度执行。在iOS终端开发中编译和链接的规则苹果已经在xcode这个开发工具上封装好,一般开发可以不用关心,但有深层需求时还是需要跟编译打很多交道,例如用编译前端Clang自定义静态代码检测规则,写编译脚本做自动化编译和持续集成,打包生成静态库,根据链接后的可执行文件的组成优化APP体积等。

前端开发的程序则不需要编译过程,只需要把代码扔给浏览器,浏览器边解析代码边执行。虽然js/css代码写完无需做任何事情浏览器就可以解析执行,但为了上面说的性能优化,前端代码上线前会对所有代码和资源文件进行处理,这些处理包括:压缩合并js/css,合并css sprite图,处理模块依赖,处理代码资源版本号,处理资源定位等。这个过程很像传统程序的编译,把给人看的代码优化处理成给机器看的,并解决一些依赖关系,可以算是前端的编译过程。像gruntjs/fis这些工具可以帮助完成这个编译过程,通常前端编译跟上线部署结合在一起,作为上线系统的一部分。

安全

前端和终端的安全性问题上虽然不需要像后端考虑得那么多,但还是有些需要注意。在请求的安全上,终端和前端都一样,用户向后端发送的请求都需要经过层层路由,不知道在哪里就被截获篡改或回放了,于是需要做一些措施防御这些情况,最常见的就是身份验证,多是采用会过期的token形式代替用户名密码,防止被抓包后黑客可以永远登陆这个账号。数据安全要求高的会用加密传输,或者使用>

GPU 全称是 GraphicProcessing Unit -- 图形处理器 ,其最大的作用就是进行各种绘制计算机图形所需的运算,包括顶点设置、光影、像素 *** 作等。 GPU 实际上是 一组图形函数的集合 ,而这些函数有硬件实现,只要用于3D游戏中物体移动时的坐标转换及光源处理。在很久以前,这些工作都是由CPU配合特定软件进行的,后来随着 图像的复杂程度越来越高 ,单纯由CPU进行这项工作对于 CPU的负荷远远超出了CPU的正常性能范围 ,这个时候就需要一个在图形处理过程中担当重任的角色,GPU也就是从那时起正式诞生了。

从GPU的结构示意图上来看,一块标准的GPU主要包括 通用计算单元、控制器和寄存器 ,从这些模块上来看,是不是跟和CPU的内部结构很像呢?

事实上两者的确在内部结构上有许多类似之处,但是由于GPU具有 高并行结构(highly parallel structure) ,所以GPU在处理图形数据和复杂算法方面拥有比CPU更高的效率。上图展示了GPU和CPU在结构上的差异,CPU大部分面积为 控制器和寄存器 ,与之相比,GPU拥有更多的 ALU (Arithmetic Logic Unit, 逻辑运算单元 )用于数据处理,而非数据高速缓存和流控制,这样的结构适合对密集型数据进行并行处理。CPU执行计算任务时,一个时刻只处理一个数据,不存在真正意义上的并行,而GPU具有多个处理器核,在一个时刻可以并行处理多个数据。

GPU采用 流式并行计算模式 ,可对每个数据进行独立的并行计算,所谓“对数据进行独立计算”,即, 流内任意元素的计算不依赖于其它同类型数据 ,例如,计算一个顶点的世界位置坐标,不依赖于其他顶点的位置。而所谓“并行计算”是指“多个数据可以同时被使用,多个数据并行运算的时间和1个数据单独执行的时间是一样的”。

第一部分 Application 阶段,后续主要都由 GPU 负责

这个阶段具体指的就是图像在应用中被处理的阶段,此时还处于 CPU 负责的时期。在这个阶段应用可能会对图像进行一系列的 *** 作或者改变,最终将新的图像信息传给下一阶段。这部分信息被叫做图元(primitives),通常是三角形、线段、顶点等。

GPU 图形渲染流程的主要工作可以被划分为两个部分:

GPU 图形渲染流程的具体实现可分为六个阶段,如下图所示。

第一阶段,顶点着色器 。 该阶段的输入是 顶点数据( Vertex Data )数据,比如以数组的形式传递 3 个 3D 坐标用来表示一个三角形。顶点数据是一系列顶点的集合。顶点着色器主要的目的是把 3D 坐标转为另一种 3D 坐标,同时顶点着色器可以对顶点属性进行一些基本处理。

第二阶段,形状(图元)装配 。 该阶段将 顶点着色器 输出的所有顶点作为输入,并将所有的点装配成指定图元的形状。图中则是一个三角形。 图元(Primitive) 用于表示如何渲染顶点数据,如:点、线、三角形。

第三阶段,几何着色器 。 该阶段把图元形式的一系列顶点的集合作为输入,它可以通过产生新顶点构造出新的(或是其它的)图元来生成其他形状。例子中,它生成了另一个三角形。

第四阶段,光栅化 。 该阶段会把图元映射为最终屏幕上相应的像素,生成片段。片段(Fragment) 是渲染一个像素所需要的所有数据。

第五阶段,片段着色器 。 该阶段首先会对输入的片段进行 裁切(Clipping)。裁切会丢弃超出视图以外的所有像素,用来提升执行效率。

第六阶段,测试与混合 。 该阶段会检测片段的对应的深度值(z坐标),判断这个像素位于其它物体的前面还是后面,决定是否应该丢弃。此外,该阶段还会检查 alpha 值( alpha 值定义了一个物体的透明度),从而对物体进行混合。因此,即使在片段着色器中计算出来了一个像素输出的颜色,在渲染多个三角形的时候最后的像素颜色也可能完全不同。

顶点处理 :这阶段GPU读取描述3D图形外观的顶点数据并根据顶点数据确定3D图形的形状及位置关系,建立起3D图形的骨架。在现有的GPU中,这些工作由硬件实现的Vertex Shader(定点着色器)完成。

光栅化计算 :显示器实际显示的图像是由像素组成的,我们需要将上面生成的图形上的点和线通过一定的算法转换到相应的像素点。把一个矢量图形转换为一系列像素点的过程就称为光栅化。例如, 一条数学表示的斜线段,最终被转化成阶梯状的连续像素点 。

纹理帖图 :顶点单元生成的多边形只构成了3D物体的轮廓,而纹理映射(texture mapping)工作完成对多变形表面的帖图,通俗的说,就是将多边形的表面贴上相应的,从而生成“真实”的图形。TMU(Texture mapping unit)即是用来完成此项工作。

像素处理 :这阶段(在对每个像素进行光栅化处理期间)GPU完成对像素的计算和处理,从而确定每个像素的最终属性。在支持DX8和DX9规格的GPU中,这些工作由硬件实现的Pixel Shader(像素着色器)完成。

屏幕显示 :由ROP(光栅化引擎)最终完成像素的输出,1帧渲染完毕后,被送到显存帧缓冲区。

GPU的工作通俗的来说就是完成3D图形的生成,将图形映射到相应的像素点上,对每个像素进行计算确定最终颜色并完成输出。

不过需要注意的是,无论多牛的游戏家用显卡,光影都是CPU计算的,GPU只有2个工作,1多边形生成。2为多边形上颜色。

一文详解GPU结构及工作原理

iOS 渲染原理总结

先绘制红色部分,再绘制⻩色部分,最后再绘制灰⾊部分,即可解决隐藏面消除的问题。即将场景按照物理距离和观察者的距离远近排序,由远及近的绘制即可。

当我们设置了cornerRadius以及masksToBounds进行圆角+裁剪时,masksToBounds裁剪属性会应用到所有的图层上。

本来我们从后往前绘制,绘制完一个图层就可以丢弃了。但现在需要依次在 Offscreen Buffer中保存,等待圆角+裁剪处理,即引发了 离屏渲染 。

参考好文章:

>

《iOS Core Animation: Advanced Techniques》- 性能调优篇

当我们想开发一个基于时间流逝运动的动画时,首先会想到使用NSTimer计时器,但是这里不推荐使用这个类,我们看下NSTimer是怎么工作的。

iOS上每个线程都管理一个Runloop。对于主线程的Runloop,每一次循环都会做以下 *** 作:

当设置了一个NSTimer计时器,这个任务会被插入任务队列中,但是它只会在上一个任务完成之后开始执行。这通常会导致有几毫秒的延迟,但是如果上一个任务过了很久才完成就会导致延迟很长一段时间。

我们可以通过一些途径来优化:

CADisplayLink和NSTimer的接口很相似,但是和NSTimer用秒作为及时单位不同,它使用属性 frameInterval 指定间隔多少帧后执行,用CADisplayLink而不是NSTimer,会保证帧率足够连续,使得动画看起来更加平滑。

但要知道即使CADisplayLink也不能保证每一帧都按计划执行,一些失去控制的离散任务或者事件(例如资源紧张的后台程序,GPU渲染进程)可能会导致动画偶尔地丢帧。

添加到Runloop的任务都有一个指定优先级的模式,为了保证用户界面保持平滑,iOS会提供和用户界面相关任务的优先级,而且当UI很活跃的时候的确会暂停一些别的任务。

一个典型的例子就是当是用UIScrollview滑动的时候,重绘滚动视图的内容会比别的任务优先级更高,所以标准的NSTimer和网络请求就不会启动。

我们可以同时加入NSDefaultRunLoopMode和UITrackingRunLoopMode来保证动画不会被滑动或者其他IO行为打断

动画和屏幕上组合的图层实际上被一个单独的进程管理,而不是你的应用程序。这个进程我们称它为渲染服务。在iOS5之前叫SpringBoard(同时管理着iOS的主屏),在iOS6之后叫做BackBoard。

Core Animation运行一段动画的过程:

CPU处理:

GPU处理:

所以我们真正能控制和优化的,只有在CPU处理布局和显示阶段,但是我们提交到IPC的渲染行为是可以被优化的,下面介绍CPU行为上的优化方法

视图布局计算会消耗掉部分时间,特别是使用AutoLayout。以60FPS作为一个iOS流畅度的黄金标准,那么将要求布局在00166667s内完成,而AutoLayout基于Cassowary算法会计算大量线性等式和不等式,下图(来自互联网)做了一个简单的布局对比,当视图数达到50个,AutoLayout将会出现性能瓶颈。Facebook的 yoga 框架允许你在iOS开发中使用FlexBox布局,同样来自Facebook的 AsyncDisplayKit 框架也引入了FlexBox优化布局的性能开销

懒加载只有在视图需要加载时才会去加载,这样的做法对内存占用和启动速度都要好处,但是在完成初始化 *** 作前,你的动画都会被延迟。所以可以对动画必要视图进行优先初始化,而非傻傻的懒加载

当实现了视图中的 -drawRect: 方法,或者CALayerDelegate的 -drawLayer:inContext: 方法,就会在绘制前产生一个可估算的性能开销,CoreAnimation需要在内存中开辟一个等大小的寄宿图用于绘制,CoreGraphics绘制会十分缓慢,绘制结束还需通过通过IPC将数据上传到BackBoard,这也是为什么非不得已都不建议使用软件绘图,并且不要实现 -drawRect: 方法,尽管可能它是空方法。CoreAniamtion为图形绘制提供了专有图层,并提供了硬件加速,总体上都比Core Graphics更快,同时他们也避免了创造一个寄宿图

PNG或者JPEG压缩之后的文件会比同质量的位图小得多,直接或间接使用UIImageView,或者将绘制到CoreGraphics都需要对解压缩,对于一个较大的,都会占用一定的时间。这一步虽然不可避免,但是我们可以把这个 *** 作放到后台线程,先把绘制到CGBitmapContext中,然后从Bitmap直接创建。

主流的网络库都用了这样的方式,我们来看下SDWebImage的网络Res解析类,在获取到网络资源后,直接在子线程对绘制解码:

之前我们说过,CoreGraphic绘图是有较大性能开销的,那么如果一定要使用软件绘图,那么我们在封装的时候,可以提供同步和异步的绘制方法,非常幸运,CoreGraphic提供的方法都是线程安全的,例如提供一个绘制色块的方法

有时候要用CAShapeLayer并不能完全代替CoreGraphics,比如创建一个绘图应用时。当我们绘制的轨迹越复杂,绘制的越多,就会越卡顿,帧数将会下降。这是由于每次移动手指绘制时,都会重绘之前的轨迹,即使场景大部分都没有改变

为了减少不必要的绘制,Mac OS和iOS设备将会把屏幕区分为需要重绘的区域和不需要重绘的区域。那些需要重绘的部分被称作「脏区域」。在实际应用中,鉴于非矩形区域边界裁剪和混合的复杂性,通常会区分出包含指定视图的矩形位置,而这个位置就是「脏矩形」,如果你可以高效确定指定系统需要重绘的脏矩形位置,那么可以调用 -setNeedsDisplayInRect: 来避免不必要的绘制而非调用 -setNeedsDisplay 。

这里有一个例子,例如当我们创建了一个画笔,触碰屏幕则会将画笔size的矩形绘制到图层上,由于我们明确知道画笔的尺寸,那么在用户绘制时每次拖拽所产生的「脏矩形」我们都是可以准确计算的,然后告诉GPU我们只需要重绘画笔矩形而非重绘整个画布

GPU会放弃绘制那些完全被其他图层遮挡的像素,但是要计算出一个图层是否被遮挡也是相当复杂并且会消耗处理器资源。同样,重叠多个透明视图(图层)消耗的资源也是相当客观的。所以为了加速处理进程,不到必须时刻不要使用透明图层。任何情况下,你应该这样做:

这样做可以使计算过程加速,在CPU处理阶段,Core Animation就可以处理好并抛弃那些完全被遮盖的图层

当图层被指定为在未预合成之前不能直接在屏幕中绘制时,离屏渲染会被唤醒,这意味着图层必须在被显示之前在一个屏幕外上下文中被渲染,而对于GPU来说,这样的 *** 作对性能是有较大损耗的。

会产生离屏渲染的 *** 作:

例如当一个列表视图中出现大量圆角视图快速滑动时,可以观察到GPU资源已经占满,而CPU资源消耗很少。这是由于CPU已经计算完所有图层信息提交IPC,而GPU负担了大量的离屏渲染任务

优化的方案首先是避免圆角和 maskToBounds 一起使用,非必须不适用图层蒙版,若无法避免那么就将性能开销转嫁给CPU

这里有几种处理方式,一种是对于需要切圆角的,不要使用 CALayercorner 在图层上裁剪,而是在获取到资源后在子线程提交前再进行一次对裁切的异步绘制;第二种使用图层栅格化 CALayershouldRasterize 转化为位图

以上就是关于四、iOS中图形图像渲染技术栈及流水线全部的内容,包括:四、iOS中图形图像渲染技术栈及流水线、Web前端开发与iOS终端开发的异同、iOS Rendering 一: 计算机渲染原理等相关内容解答,如果想了解更多相关内容,可以关注我们,你们的支持是我们更新的动力!

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/web/9789254.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-05-02
下一篇 2023-05-02

发表评论

登录后才能评论

评论列表(0条)

保存