热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

iOS端矢量图解决方案汇总

Python实战社群Java实战社群长按识别下方二维码,按需求添加扫码关注添加客服进Python社群▲扫码关注添加客服进Java社群▲作者|小猪来源|小猪的博客htt

Python实战社群

Java实战社群

长按识别下方二维码,按需求添加

扫码关注添加客服

进Python社群▲

扫码关注添加客服

进Java社群


作者 | 小猪 
来源 | 小猪的博客

https://dreampiggy.com

简介

矢量图,指的是通过一系列数学描述,能够进行无损级别的变化和缩放的一种图像。相比于标量图(如JPEG等标量图压缩格式),能够在绘制时进行任意大小伸缩而不产生模糊,甚至能够实现动态着色,动画等等一系列交互。

在当今移动端设备尺寸越来越复杂,各种操作系统级别的夜间主题(或者Dark Mode)越来越提倡的场景下,如果依旧使用标量图,我们需要针对不同的屏幕大小(如2x,3x),和对应主题场景(Light/Dark),提供NxM数量级的标量图,对于App大小开销是很大的。因此,使用矢量图是一个非常有效的解决方案。这个系列文章,就是主要侧重讲解iOS端上的矢量图解决方案。

第一章是关于SVG及其相应衍生方案的解决方案,后续会有其他矢量图相关的PDF章节,Lottie等。他们各自有不同的细节场景区分和优缺点。

SVG作为目前在Web上最流行的矢量格式,在iOS端的支持可以说是一言难尽。在这里,我从各个方向上总结了截至目前已有的实现(公开的方案,企业内部实现无从得知),方便对比选择最适合自己场景的选择。

Symbol Image

Symbol Image,是Apple在WWDC 2019和iOS 13上提供的矢量图解析方案。

之所以名称叫做Symbol Image,源自于这个技术方案的实现细节,它最早诞生于SVG字体规范:OpenType-SVG。这个规范是Adobe提出的,并且得到了包括Microsoft在内的多家公司支持。Apple自己的CoreText字体框架,其实早早就在iOS 11时代内部支持了SVG类型的font table。

制作Symbol Image

Symbol Image的整体API设计,其实不像是图像,更像是一种字体(和Icon Font类似)。

对于同一个Symbol Image,它可以看作是一个SVG Path的集合。前面提到,Symbol Image基于OpenType-SVG字体,对于字体来说,我们都知道字重的概念,用来决定渲染时候的线条粗细程度。

因此Symbol Image也有9个字重:Ultralight,Thin,Light,Regular,Medium,Semibold,Bold,Heavy,Black。与此同时,Symbol Image对每一个字重,支持了3种大小,分别是Small,Medium和Large。这也就是说,一个Symbol Image最多可以有27种大小字重的样式选择。

一般来说,从头构建一个Symbol Image会非常复杂,Apple推荐的方式,是通过使用SF Symbols App,来导出一个SVG模版,再通过Sketch来进行图层编辑。

从原始的SVG数据来看,每一个Symbol Image包含的所有样式都是一个单独的Path节点,对应了图标的绘制。如果要新建一个Symbol Image,需要完全删除Path节点,重新绘制矢量路径。




导入Symbol Image

导入Symbol Image的方式非常简单,你只需要将制作好的Symbol Image,向Xcode的Asset Catalog窗口拖动,就可以集成。Xcode可以会展示对应的预览效果。

另外,实际上产生的文件夹后缀为.symbolset,这个不同于普通的Asset Image(后缀名.imageset),也就意味着你可以同时引入一个同名的Symbol Image和普通Image。

使用Symbol Image

对于iOS 13系统提供的自带Symbol Image,UIKit提供了init(systemName:)方法来获取,对于App自行提供的Symbol Image,我们使用init(named:)方法。

注意,你可以同时包含一个Symbol Image和普通的Asset Image,共享一个Name。这样设计的好处,在WWDC上有介绍,是为了兼容iOS 12等低系统版本,在iOS 13上,Symbol Image优先级永远高于普通Asset Image,在iOS 12会自动fallback。

let imageView = UIImageView()
let symbolImage = UIImage(named: "my.symbol.image")
// 默认配置下,这个symbol image是template的,意味着他不会含有颜色,颜色由UIView级别tintColor决定
imageView.image = symbolImage// 如果确定要获取系统Symbol Image
let systemSymbolImage = UIImage(systemName: "wifi.exclamationmark")// 如果要指定颜色
let redSymbolImage = symbolImage.withTintColor(.red, renderingMode: .alwaysOrigin)
imageView.image = redSymbolImage

对于Symbol Image来说,我们可以指定在运行时需要的字重

let regularSymbolImage = UIImage(named: "my.symbol.image")
// 指定你想要的字号,字重,这里是18号,Bold 字重,Large 大小
let symbolConfiguration = UImage.SymbolConfiguration(pointSize: 18, weight: .large, scale: .large)
let boldSymbolImage = regularSymbolImage.applyingSymbolConfiguration(symbolConfiguration)
imageView.image = boldSymbolImage

另外,我们还可以配合AttributedString使用,只要使用TextAttachment传入对应的Symbol Image即可。

let textView = UITextView()
// 可以微调Symbol Image与文字的对齐
let baselineSymbolImage = symbolImage.withBaselineOffset(fromBottom: 1.0)
let imageAttachment = NSTextAttachment(image: baselineSymbolImage)
let imageString = NSAttributedString(attachment: imageAttachment)
textView.attributedText = imageString

优缺点

优点:

• iOS原生支持,工具链完善

• SwiftUI原生支持,截止目前Image能唯一使用的矢量方案(排除UIViewRepresentable)

• 支持和AttributedString无缝混合,类似Icon Font

缺点:

• iOS 13+ Only

• 通过字体属性控制大小,取决于UI场景,做到Pixel级别的拉伸会是一个问题

• 需要单独制作Symbol Image,跨平台,Web使用痛点

CoreSVG

CoreSVG是iOS 13支持Symbol Image的背后的底层SVG渲染引擎,使用C++编写。

截至目前,CoreSVG依然属于Private Framework,社区也有很多人向Apple提了反馈并建议开放出来,可能在之后的WWDC 2020我们能够得知更多的消息。

注意!以下方法均为使用了CoreSVG的Private API,可能随着操作系统变动会有改变,并且有审核风险,如果需要线上使用,请自行进行代码混淆等方案。

通过Asset Catalog使用SVG

目前Xcode不支持直接拖动SVG文件来集成到Asset Catalog,因为拖动SVG默认会当作Symbol Image处理。

但是我们可以通过一个取巧的方式来实现,Xcode支持PDF矢量图(从iOS 11与Xcode 9开始支持,PDF章会讲解)。因此,我们可以将SVG后缀改成PDF,然后拖动到Xcode中,最后再修改回SVG后缀名,并且同步.imageset/Contents.json里面的文件名即可,如下:

当你添加好SVG图像后,可以通过Name,以和PDF矢量图一样的方式来引入和使用,如下

UIImageView *imageView = [UIImageView new];
UIImage *svgImage = [UIImage imageNamed:@"my_svg"];
imageView.image = svgImage;
// 然后我们可以自由缩放ImageView的大小,会自动触发矢量绘制
imageView.frame = CGRectMake(0, 0, 1000, 1000);

从运行时来看,加入Asset Catalog的SVG矢量图的UIImage,含有对应的CGSVGDocumentRef对象,并且也包含了一个标量图的缩略图,可以供缩略图或者其他系统API来调用。并且在Xcode的Interface Builder上也会有明显的SVG标识(类似PDF)

加载任意SVG数据(网络)

除了能够通过Asset Catalog添加SVG图像,通过CoreSVG,我们可以在运行时去解析网络数据下载得到的SVG数据,为此能提供更为广阔的应用场景。

UIImageView *imageView = [UIImageView new];
NSData *data;
CGSVGDocumentRef document = CGSVGDocumentCreateFromData((__bridge CFDataRef)data, NULL);
UIImage *svgImage = [UIImage _imageWithCGSVGDocument:document];
imageView.image = svgImage;

渲染SVG矢量图到标量图

一些UIKit的视图,或者一些图像处理,对矢量图支持并没有考虑,或者是我们在做性能优化时,需要将矢量图光栅化得到对应的标量图。CoreSVG提供了和CoreGraphics的PDF类似的接口,允许你去绘制得到对应的标量图。

CGSVGDocumentRef document; // 原始SVG Document
CGSize targetSize; // 指定标量图大小
BOOL preserveAspectRatio; // 是否保持宽高比// 获取SVG的canvas大小,本质上是按照SVG规范,将viewPort和viewBox计算得出的
CGSize size = CGSVGDocumentGetCanvasSize(document);
// 计算Transform
CGFloat xRatio = targetSize.width / size.width;
CGFloat yRatio = targetSize.height / size.height;
CGFloat xScale = preserveAspectRatio ? MIN(xRatio, yRatio) : xRatio;
CGFloat yScale = preserveAspectRatio ? MIN(xRatio, yRatio) : yRatio;CGAffineTransform scaleTransform = CGAffineTransformMakeScale(xScale, yScale);
CGSize scaledSize = CGSizeApplyAffineTransform(size, scaleTransform);
CGAffineTransform translationTransform = CGAffineTransformMakeTranslation(targetSize.width / 2 - scaledSize.width / 2, targetSize.height / 2 - scaledSize.height / 2);
// 开始CGContext绘制
UIGraphicsBeginImageContextWithOptions(targetSize, NO, 0);
CGContextRef context = UIGraphicsGetCurrentContext();
// UIKit坐标系和CG坐标系转换
CGContextTranslateCTM(context, 0, targetSize.height);
CGContextScaleCTM(context, 1, -1);
// 应用Transform
CGContextConcatCTM(context, translationTransform);
CGContextConcatCTM(context, scaleTransform);
// 绘制SVG Document
CGContextDrawSVGDocument(context, document);
// 获取标量图
image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

SVG导出

目前,CoreSVG没有提供类似于PDF的修改元素的接口,我们只能直接对SVGDocument进行导出。或许随着未来框架的开放,会有类似于目前CoreGraphics对PDF进行编辑的高级接口。

// 获取SVG Document
UIImage *svgImage;
CGSVGDocumentRef document = [svgImage _CGSVGDocument];
NSURL *url = [NSURL fileURLWithPath:@"/tmp/output.svg"];
NSMutableData *data = [NSMutableData data];
// 导出到Data
CGSVGDocumentWriteToData(document, (__bridge CFMutableDataRef)data, NULL);
// 或者文件
CGSVGDocumentWriteToURL(document, (__bridge CFURLRef)url, NULL);

优缺点

优点

• 能够支持目前已有的大量SVG,在Android和Web端复用

• Apple原生支持,稳定性有一定保证,并且随系统升级会持续优化

• 性能高,CoreSVG利用了CoreGraphics系统库和内部的SPI做矢量绘制,目前性能最好

缺点

• 目前是私有Framework,有审核和使用风险

• 可能存在一些SVG元素兼容问题,需要不断摸索

• SwiftUI不支持,需要使用UIViewRepresentable

三方SVG库

SVGKit

SVGKit是最早的iOS上开源SVG渲染方案,已经有8年之久。SVGKit内部支持两种渲染模式,一种是通过CPU渲染(CoreGraphics重绘制),一种是通过GPU渲染(CALayer树组合)。有着不同的兼容性和性能。

示例

// CPU渲染
SVGKImageView *imageView = [SVGKFastImageView new];
// GPU渲染
imageView = [SVGKLayeredImageView new];
SVGKImage *svgImage = [[SVGKImage alloc] initWithData:data];
imageView.image = svgImage;

优点

• 支持纯Objective-C

• 如果是支持的图像,性能相对较高(1000个级别的Path可在1秒内渲染)

缺点

• 社区不再维护,大量Issue无人跟进解决

• 不遵循语义版本号,用分支发布更新,下游无法依赖

• 部分SVG特性虽然声明支持,但存在问题,如Gradient等,缺少单测

• 不支持SVG动画

Macaw

Macaw是一个矢量绘制框架,提供了非常简单的DSL语法来描述矢量路径绘制的场景。它本身不是和SVG强绑定的,但是对SVG格式提供了兼容和支持

示例

let node = try! SVGParser.parse(path: "/path/to/svg")
let imageView = SVGView()
imageView.node = node

优点

• 目前最活跃和成熟的iOS端SVG开源框架(在GitHub上)

• 支持DSL去直接生成矢量图,修改节点等,非常强大

• 支持SVG动画(部分特性)

缺点

• 部分SVG特性特性声明不支持

• SVG性能渲染差(相对于SVGKit),依赖大量的的CPU绘制操作(非CALayer组合),可能需要结合异步绘制框架

SwiftSVG

SwiftSVG是一个专门针对SVG Path等常见特性的矢量图解析框架,他不侧重于完整的SVG/1.1规范支持,而是保证了基本的绘制实现的正确性,并且支持导出SVG的Path到UIBezierPath

示例

let svgURL = URL(string: "https://openclipart.org/download/181651/manhammock.svg")!
let hammock = UIView(SVGURL: svgURL) { (svgLayer) insvgLayer.fillColor = UIColor(red:0.52, green:0.16, blue:0.32, alpha:1.00).cgColorsvgLayer.resizeToFit(self.view.bounds)
}
self.view.addSubview(hammock)

优点

• 性能相对MacPaw较好

• 对Path,Circle等常见元素,有着良好的兼容性和完整单测,基本上只用这些特性的SVG不存在问题

• 支持导出UIBezierPath,可以用作一些描边的交互

• 提供了便携方法,能直接读取Xcode的Data Asset,URL等

缺点

• 基本上只针对Path,Circle等元素有良好的支持,其他的

• Gradient,Text等均不支持

• 不支持SVG动画

VectorDrawable

VectorDrawable是Android平台上官方提供的一套矢量图解决方案,他是以一个类似SVG的XML表达形式,来描述矢量图的绘制方式。

从整体设计上看,VectorDrawable基本上是对SVG的精简和二次改造,大部分的元素在SVG中都有对应的概念,并且样式属性也一一对应。甚至,Android Studio支持直接将SVG导出成VectorDrawable文件并直接集成。

在iOS上平台上,Uber内部开源了一套自己在用的VectorDrawable实现:Cyborg,通过利用CoreGraphics和CoreAnimation来渲染VectorDrawable文件。

使用VectorDrawable渲染

VectorDrawable提供了一个专门用于矢量图的View,并且能够制定对应的Theme(Theme是用来支持不同资源的Dark Mode切换的)。

// Bundle加载
let vectorView = VectorView(theme: myTheme)
vectorView.drawable = VectorDrawable.named("MyDrawable")// Data加载
vectorView.drawable = VectorDrawable.create(from: data)
如果这个不满足,你也可以通过CALayer来做渲染,做更为细致的调节。并且VectorDrawable也提供了一些定制项(如设置tintColor)

优缺点

优点

• 能够和Android端复用,并且由于可由SVG生成,意味着Web端也可复用设计资源

• 性能良好,无论官方还是Example测试,除去CoreSVG外都是最快的渲染速度

缺点

• 目前iOS实现不支持动画(AnimatedVectorDrawable)

• 部分SVG实现VectorDrawable不支持,需要设计资源修改

• Uber内部开源,可能存在未来持续社区建设和维护成本,需要评估

SVG-Native

SVG-Native是由Adobe主导提出的一个W3C规范,目前处于Draft Stage,不过由于Apple,Google的赞同,大概率会在2020年内通过,并且正式规范定稿。

SVG-Native基于目前的SVG/1.1版本,是SVG/1.1的真子集(即一个SVG-Native图一定可以被浏览器正确渲染)。

注:曾经W3C有一个SVG Tiny的规范,但是它是针对移动浏览器场景的,和SVG-Native解决的问题是不一样的。

它针对移动平台,桌面平台等非浏览器场景做了针对性定制,废弃了一些Native端非常困难实现的功能,包括:

• scripting: 不依赖Javascript环境

• animations: 不支持动画

• filters: 不支持滤镜,部分效果(如文字滤镜)依赖实现复杂

• masks: 不支持蒙层

• patterns: 不支持仿制图章,Color Pattern

• texts: 不内嵌文字,文字使用Path绘制

• events: 点击事件等,因为没有Script交互自然不需要

• CSS3:CSS3是一个完整布局系统,大量属性远远超过SVG的功能,如Flexbox,Media-Query,都是不必要的,只有基本的渲染属性

可以看出,这些剥离的功能都是和浏览器场景完全绑定的,不适用于通用的App内渲染矢量图的用途。SVG-Native更适合桌面/移动的App,渲染器实现也会精简很多,容易单元测试,并且可供操作系统内嵌集成。

使用

Adobe提供了一个目前Draft规范的渲染实现SVG Native Viewer,目前提供了多种渲染引擎的桥接,包括我们熟悉的CoreGraphics和Skia。

SVG-Native解码器,能够以标量图的方式,渲染SVG到一个指定大小的CGContext上,性能目前看足够快(和CoreSVG对比)。目前一般是通过重写drawRect来让View大小变化时进行重绘。

- (void)drawRect:(NSRect)dirtyRect {[super drawRect:dirtyRect];Document* d = [[[self window] windowController] document];SVGNative::SVGDocument* doc = [d getSVGDocument];if (!doc)return;NSGraphicsContext* nsGraphicsContext = [NSGraphicsContext currentContext];CGContextRef ctx = (CGContextRef) [nsGraphicsContext CGContext];SVGNative::CGSVGRenderer* renderer = static_cast(doc->Renderer());CGRect r(dirtyRect);CGAffineTransform m = {1.0, 0.0, 0.0, -1.0, 0.0, r.size.height};CGContextConcatCTM(ctx, m);renderer->SetGraphicsContext(ctx);doc->Render(r.size.width, r.size.height);renderer->ReleaseGraphicsContext();
}

优缺点

优点

• W3C规范,可以确保未来规范的准确性,并且操作系统提供商,如Apple更容易集成

• SVG-Native是SVG1.1的真子集,意味者可以复用到Web上

• SVG-Native会是未来的OpenType-SVG实现,意味着Adobe字体或者设计师群体更容易接受

缺点

• SVG-Native是SVG真子集,意味着目前的SVG设计资源,需要适配修改才可支持

• 截至目前,SVG-Native依然处于Draft阶段,稳定,推广普及需要较长时间

• SVG-Native目前只有Adobe的解析器实现,部分特性在CoreGraphics上工作并不良好

• 目前没有看到动画的支持

总结

总结一下关于SVG的相关解决方案,可以看出,没有一种Case能够涵盖所有场景,当然,这和Apple本身对矢量图支持的建设有一定关系,大部分建设依赖于开源社区。因此,通常情况下需要根据自己具体的实际需要来选择,比如:

• 只考虑Path,Circle等矢量路径:使用SwiftSVG、Macaw即可

• 考虑和Android复用:使用VectorDrawable

• 不考虑iOS 13以下兼容:优先用Symbol Image和CoreSVG

• 考虑SVG动画:Macaw

• 面向未来:SVG-Native

参考资料

[1]https://swiftcafe.io/post/sf-symbol 
[2]https://www.avanderlee.com/swift/sf-symbols-guide/ 
[3]https://developer.mozilla.org/en-US/docs/Web/SVG 
[4]https://github.com/SDWebImage/SDWebImageSVGCoder 
[5]https://developer.android.com/guide/topics/graphics/vector-drawable-resources 
[6]https://eng.uber.com/cyborg/ 
[7]https://helpx.adobe.com/fonts/using/ot-svg-color-fonts.html 
[8]https://medium.com/adobetech/svg-native-open-sourcing-svg-native-viewer-988125328a07

程序员专栏 扫码关注填加客服 长按识别下方二维码进群

近期精彩内容推荐:  

 再见!程序员!!!

 微信号 可以改了 !!!真事 !!

 2020 常用的 7 款 MySQL 客户端工具

 别再问我Redis内存满了该怎么办了

在看点这里好文分享给更多人↓↓



推荐阅读
  • android listview OnItemClickListener失效原因
    最近在做listview时发现OnItemClickListener失效的问题,经过查找发现是因为button的原因。不仅listitem中存在button会影响OnItemClickListener事件的失效,还会导致单击后listview每个item的背景改变,使得item中的所有有关焦点的事件都失效。本文给出了一个范例来说明这种情况,并提供了解决方法。 ... [详细]
  • 使用nodejs爬取b站番剧数据,计算最佳追番推荐
    本文介绍了如何使用nodejs爬取b站番剧数据,并通过计算得出最佳追番推荐。通过调用相关接口获取番剧数据和评分数据,以及使用相应的算法进行计算。该方法可以帮助用户找到适合自己的番剧进行观看。 ... [详细]
  • CF:3D City Model(小思维)问题解析和代码实现
    本文通过解析CF:3D City Model问题,介绍了问题的背景和要求,并给出了相应的代码实现。该问题涉及到在一个矩形的网格上建造城市的情景,每个网格单元可以作为建筑的基础,建筑由多个立方体叠加而成。文章详细讲解了问题的解决思路,并给出了相应的代码实现供读者参考。 ... [详细]
  • flowable工作流 流程变量_信也科技工作流平台的技术实践
    1背景随着公司业务发展及内部业务流程诉求的增长,目前信息化系统不能够很好满足期望,主要体现如下:目前OA流程引擎无法满足企业特定业务流程需求,且移动端体 ... [详细]
  • 本文介绍了如何使用PHP向系统日历中添加事件的方法,通过使用PHP技术可以实现自动添加事件的功能,从而实现全局通知系统和迅速记录工具的自动化。同时还提到了系统exchange自带的日历具有同步感的特点,以及使用web技术实现自动添加事件的优势。 ... [详细]
  • 微软头条实习生分享深度学习自学指南
    本文介绍了一位微软头条实习生自学深度学习的经验分享,包括学习资源推荐、重要基础知识的学习要点等。作者强调了学好Python和数学基础的重要性,并提供了一些建议。 ... [详细]
  • 本文详细介绍了GetModuleFileName函数的用法,该函数可以用于获取当前模块所在的路径,方便进行文件操作和读取配置信息。文章通过示例代码和详细的解释,帮助读者理解和使用该函数。同时,还提供了相关的API函数声明和说明。 ... [详细]
  • vue使用
    关键词: ... [详细]
  • 本文介绍了使用kotlin实现动画效果的方法,包括上下移动、放大缩小、旋转等功能。通过代码示例演示了如何使用ObjectAnimator和AnimatorSet来实现动画效果,并提供了实现抖动效果的代码。同时还介绍了如何使用translationY和translationX来实现上下和左右移动的效果。最后还提供了一个anim_small.xml文件的代码示例,可以用来实现放大缩小的效果。 ... [详细]
  • Nginx使用(server参数配置)
    本文介绍了Nginx的使用,重点讲解了server参数配置,包括端口号、主机名、根目录等内容。同时,还介绍了Nginx的反向代理功能。 ... [详细]
  • IhaveconfiguredanactionforaremotenotificationwhenitarrivestomyiOsapp.Iwanttwodiff ... [详细]
  • 《数据结构》学习笔记3——串匹配算法性能评估
    本文主要讨论串匹配算法的性能评估,包括模式匹配、字符种类数量、算法复杂度等内容。通过借助C++中的头文件和库,可以实现对串的匹配操作。其中蛮力算法的复杂度为O(m*n),通过随机取出长度为m的子串作为模式P,在文本T中进行匹配,统计平均复杂度。对于成功和失败的匹配分别进行测试,分析其平均复杂度。详情请参考相关学习资源。 ... [详细]
  • C++字符字符串处理及字符集编码方案
    本文介绍了C++中字符字符串处理的问题,并详细解释了字符集编码方案,包括UNICODE、Windows apps采用的UTF-16编码、ASCII、SBCS和DBCS编码方案。同时说明了ANSI C标准和Windows中的字符/字符串数据类型实现。文章还提到了在编译时需要定义UNICODE宏以支持unicode编码,否则将使用windows code page编译。最后,给出了相关的头文件和数据类型定义。 ... [详细]
  • Android开发实现的计时器功能示例
    本文分享了Android开发实现的计时器功能示例,包括效果图、布局和按钮的使用。通过使用Chronometer控件,可以实现计时器功能。该示例适用于Android平台,供开发者参考。 ... [详细]
  • 2022年的风口:你看不起的行业,真的很挣钱!
    本文介绍了2022年的风口,探讨了一份稳定的副业收入对于普通人增加收入的重要性,以及如何抓住风口来实现赚钱的目标。文章指出,拼命工作并不一定能让人有钱,而是需要顺应时代的方向。 ... [详细]
author-avatar
我是小崔来啦
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有