简介:本资源是一套基于C#实现DXF文件解析与应用的完整工程实践项目,面向CAD二次开发工程师、智能制造领域软件开发者及具备基础C#和图形处理能力的中级以上技术人员,解决CAD图纸数据读取、G代码生成及坐标尺寸可视化等核心问题。压缩包共64个文件,包含12个核心C#源码文件(如Form1.cs、DXFInterpreter调用逻辑)、5个关键DLL库(含DXF解析依赖)、6个可执行程序(exe)用于功能验证,以及sln/cspoj配置文件和dxf测试样例等,整体体积仅386KB,结构紧凑便于学习与集成。已有2316人下载学习,项目采用netDxf等成熟库实现DXF文档加载与对象遍历,完整呈现从线条/圆弧/文字等图元提取、到G01/G02/G03指令转换、再到Windows Forms图形控件动态绘制与标注显示的全流程代码,附带可直接运行的调试环境与双dxf测试文件,适合快速上手CAD数据交互开发。 不知道你有没有遇到过这种场景:工艺那边甩过来一份CAD图纸,说"轮廓坐标都在里面,你拿去用",结果你打开一看,图纸里几十个圆、几百段多段线,坐标靠手抄根本不现实。我上个月接的一个设备上位机项目就卡在这。琢磨了一圈,最后用C#直接去读DXF文件,把图纸里的坐标全自动抠了出来。这篇文章就把我这几周的实践经验整理出来,包括DXF的底层结构、C#解析的核心代码、还有那些文档里根本不会写的坑。
全文适合三类人看:做上位机集成开发、做CAD二次开发、或者仅仅是想从图纸里批量提取数据做检测分析的程序员。
1. 从一次实际需求说起:为什么我要在C#里读DXF
项目背景其实很简单,一套激光切割设备的上位机需要读取加工轮廓的坐标数据,然后换算成机床坐标下发到下位机。工艺工程师交付的图纸是DWG格式,问题在于DWG是AutoCAD的私有二进制格式,文档不开放,C#里很难直接读。好在AutoCAD提供了DXF这种公开的文本交换格式,它的设计初衷就是让别的软件能读懂CAD图纸内容。
我先说结论:DXF文件就是纯文本,用记事本都能打开,里面每两行描述一个数据单元。这个特性决定了C#读DXF的门槛非常低,不像DWG那样需要依赖逆向工程。
对我来说,这个项目最合适的做法就是从DXF里提取直线端点、多段线顶点、圆心坐标这些几何信息,然后转成统一的加工点表。用C#来写是因为整套上位机本身就是C#的WinForms程序,我不想为了一个解析功能再去引入Python或C++的跨语言组件,维护成本太高。
文章后面所有代码都是我从项目里抽出来的简化版本,核心逻辑可跑,但去掉了业务耦合。如果你也是类似场景,直接对照改就行。
2. DXF文件的组码与值:动手前先搞懂数据格式本身
2.1 组码对是DXF的最小单位
要解析DXF,首先要接受它的思维方式。整个文件就是一个顺序排列的"组码对"列表——第一行是组码(一个整数),第二行是值(字符串或数字)。组码决定了这行数据的含义,值就是具体内容。
举个例子,下面这段是最简单的LINE实体:
0 LINE 8 0 10 0.0 20 0.0 30 0.0 11 100.0 21 50.0 31 0.00后面跟LINE,表示新实体开始,类型是直线8后面跟0,表示图层名是"0"10/20/30是一组,表示起点坐标 (0, 0, 0)11/21/31是一组,表示终点坐标 (100, 50, 0)
理解了这个"组码对"机制,你就知道解析DXF的核心任务不是别的,就是把文件按两行一组拆开,然后根据组码判断数据的业务含义。这个思路贯穿全文。
2.2 常用的组码速查表
项目里真正高频出现的组码就那些,我整理了一份:
| 组码 | 含义 | 常见用途 |
|---|---|---|
| 0 | 实体/段开始标记 | 用来识别"LINE"、"CIRCLE"、"ENTITIES"等 |
| 1 | 文本内容 | TEXT实体的字符串内容 |
| 2 | 名称 | 图层名、图块名 |
| 8 | 图层名 | 区分实体所在图层 |
| 10/20/30 | 主要坐标点X/Y/Z | 起点、圆心、插入点 |
| 11/21/31 | 次要坐标点X/Y/Z | 终点、偏移点 |
| 40 | 数字参数 | 圆的半径、文字高度 |
| 62 | 颜色编号 | 1红色、2黄色、3绿色等 |
| 70 | 标志位 | 判断多段线是否闭合 |
| 90 | 顶点数量 | LWPOLYLINE顶点个数 |
记住这张表,后面解析所有实体都用得上。组码8(图层)是每个实体都会带的,所以我会把它单独存下来做筛选。
2.3 六个段落里,我们最需要哪两个
DXF文件按SECTION(段)组织,常见的有HEADER、CLASSES、TABLES、BLOCKS、ENTITIES、OBJECTS。很多文章会把六个段挨个讲一遍,但实际开发里你只需要关心两个:ENTITIES段和BLOCKS段。
- ENTITIES段:图纸上实实在在的图形对象,直线、圆、多段线都在这里,这是主战场。
- BLOCKS段:图块定义。注意,图纸里插入的图块是INSERT实体,真正的几何图形在BLOCKS段里,不在ENTITIES段,这又是一个大坑,后面详细说。
HEADER段是各种系统变量,TABLES段是图层表、线型表这些定义,对只提取坐标的应用来说基本不用看。所以我解析时只做一件事:定位到需要的段,识别实体,提取坐标。
3. 三种思路:从零解析、免费库、商业库怎么选
3.1 不要一开始就写代码,先选路线
我最初也想过用现成的类库。C#生态里解析DXF有几条路线,各有各的适用场景,我实际摸过一遍,给你交个底:
netDXF:开源免费,GitHub上比较活跃,对DXF的读写封装比较完整,是绝大多数人的首选。它能直接帮你把实体解析成对象,比如DxfLine、DxfCircle、DxfLwPolyline,还支持版本转换。但有两个问题:一是包体较大,引入后整个项目依赖增加不少;二是它的底层也是手工解析,一旦遇到某些国产CAD或第三方工具导出的"不太标准"的DXF,同样会解析失败或丢实体。
Aspose.CAD:商业库,功能非常全,除了DXF还支持DWG、DGN等几十种CAD格式,还能做格式转换和渲染。缺点是收费,License按开发者数算,对个人博主或小公司来说成本偏高。
Teigha(ODA平台):这是ODA(Open Design Alliance)提供的底层SDK,很多商业CAD软件都用它。功能极其强大,但学习曲线陡峭,更适合深度CAD业务,不是为了读几个坐标就该上的方案。
3.2 选型对比表
| 方案 | 成本 | 上手难度 | 适用范围 | 我的评价 |
|---|---|---|---|---|
| 手动解析组码对 | 免费 | 低(懂数据结构即可) | 固定场景、读坐标为主 | 可控,适合我这种只读不写的需求 |
| netDXF | 免费 | 低 | 通用DXF读写 | 首选推荐,但出问题时排查更费劲 |
| Aspose.CAD | 付费 | 低 | 多格式转换、渲染 | 预算充足可考虑 |
| Teigha/ODA | 免费(需注册) | 高 | CAD底层开发 | 杀鸡不用牛刀 |
3.3 为什么我最终选择手动写解析
我的需求边界非常明确:只需要从DXF里读直线、圆、轻量多段线(LWPOLYLINE)、图块引用(INSERT)这四种实体的坐标,不涉及写DXF,也不涉及转换其他格式。手动解析的好处有三个:
第一是可控。文件读到哪一段、遇到什么组码、怎么容错,全部由我决定,出问题能精准定位。用第三方库一旦解析失败,你只能去翻它的源代码。
第二是零依赖。上位机发布时不需要带额外的DLL,也不会因为库的版本升级引起意外行为。在客户现场部署,依赖越少越省心。
第三是性能优势。我只扫描需要的段,遇到不需要的数据直接跳过,理论上比通用库快。对一个几MB的DXF文件来说,差距是毫秒级的,但如果你要批量处理几百个图纸,这个优势就放大了。
当然,如果你的需求是"通用读DXF并支持各种实体类型",那我建议直接上netDXF,没必要重复造轮子。手动解析适合需求非常聚焦的项目。
4. 手动解析:核心代码一步步拆开讲
4.1 基础框架:把文件拆成组码对
既然DXF的最小单位是组码对,那第一步就是把文本拆解成组码对列表。这里有一个关键决策:一次性读入内存还是流式读取。我先给个直观实现,后面再讨论性能极致优化。
using System; using System.Collections.Generic; using System.IO; using System.Text; public record GroupCode(int Code, string Value); public class DxfReader { // 把DXF文件按两行一组拆成组码对列表 public List<GroupCode> ReadGroupCodes(string dxfPath) { var pairs = new List<GroupCode>(); // 注意编码,后面会讲为什么这里不能直接默认UTF-8 string[] lines = File.ReadAllLines(dxfPath, Encoding.UTF8); for (int i = 0; i < lines.Length - 1; i += 2) { // 组码永远是整数,但可能有空格 if (!int.TryParse(lines[i].Trim(), out int code)) continue; // 行格式损坏就跳过,别让单个坏行炸掉整个解析 string value = lines[i + 1].TrimEnd('\r', '\n'); pairs.Add(new GroupCode(code, value)); } return pairs; } }注意我用了TrimEnd('\r', '\n')而不是Trim(),原因先说一半:如果你拿到的DXF在不同操作系统之间转换过,行尾可能会有\r残留,必须去掉。但字符串值本身的空格可能是有意义的,比如TEXT实体的内容就是"Hello World",两端的空格可能是故意的,用Trim()会把它们干掉。这是我在实际项目里踩过的坑。
4.2 定位ENTITIES段并输出实体列表
拿到组码对列表之后,接下来就是扫描它,找到ENTITIES段,然后按0组码切出一个个实体。
public List<List<GroupCode>> ExtractEntities(List<GroupCode> pairs) { var entities = new List<List<GroupCode>>(); bool inEntities = false; for (int i = 0; i < pairs.Count; i++) { var pair = pairs[i]; // 组码2,值"ENTITIES",进入实体段 if (pair.Code == 2 && pair.Value == "ENTITIES") { inEntities = true; continue; } // 遇到ENDSEC,实体段结束 if (inEntities && pair.Code == 0 && pair.Value == "ENDSEC") break; // 实体段内,遇到组码0且值不是ENDSEC,说明新实体开始了 if (inEntities && pair.Code == 0) { var entityPairs = new List<GroupCode> { pair }; int j = i + 1; while (j < pairs.Count && !(pairs[j].Code == 0)) { entityPairs.Add(pairs[j]); j++; } entities.Add(entityPairs); i = j - 1; } } return entities; }这段代码的核心是内层while循环。它从当前实体之后开始扫描,一直扫到下一个Code == 0为止,把中间所有的组码对收集到一起,作为一个完整实体的描述集。每一次新的Code == 0都代表一个新的实体,所以遇到它时直接跳出,回到外层循环继续。
有一点要注意:老的POLYLINE实体内部会带有VERTEX和SEQEND子实体,它们的组码也是0,但值不是ENDSEC,这会导致我的切分逻辑把它们误判成新实体。对于只解析LWPOLYLINE的场景没有这个问题,但如果你想解析老式POLYLINE,需要额外判断0后面跟的是VERTEX还是别的实体类型。
4.3 实体数据建模
有了每个实体的组码对集合,接下来就是"翻译"。我定义了一个抽象基类和几个实体类:
public abstract class DxfEntity { public string Layer { get; set; } = "0"; public string EntityType { get; set; } } public class Point3D { public double X { get; set; } public double Y { get; set; } public double Z { get; set; } public override string ToString() => $"{X},{Y},{Z}"; } public class DxfLine : DxfEntity { public Point3D StartPoint { get; set; } = new Point3D(); public Point3D EndPoint { get; set; } = new Point3D(); } public class DxfCircle : DxfEntity { public Point3D Center { get; set; } = new Point3D(); public double Radius { get; set; } } public class DxfLwPolyline : DxfEntity { public List<Point2D> Vertices { get; set; } = new List<Point2D>(); public bool IsClosed { get; set; } } public class Point2D { public double X { get; set; } public double Y { get; set; } public override string ToString() => $"{X},{Y}"; } public class DxfInsert : DxfEntity { public string BlockName { get; set; } public Point3D InsertPoint { get; set; } = new Point3D(); }按实体类型来建模,而不是把所有数据塞进一个大字典,代码后续用起来会顺手得多。上位机里要遍历点集、做坐标变换,强类型的类能直接用,不用反复做类型转换。
4.4 核心解析:识别实体并提取坐标
解析的入口是一个switch分支,根据组码0后面的值判断实体类型,然后调用不同的解析方法:
public List<DxfEntity> ParseEntities(List<List<GroupCode>> rawEntities) { var result = new List<DxfEntity>(); foreach (var raw in rawEntities) { if (raw.Count == 0) continue; string entityType = raw[0].Value; switch (entityType) { case "LINE": result.Add(ParseLine(raw)); break; case "CIRCLE": result.Add(ParseCircle(raw)); break; case "LWPOLYLINE": result.Add(ParseLwPolyline(raw)); break; case "INSERT": result.Add(ParseInsert(raw)); break; // 其他实体类型暂时忽略 } } return result; }现在看看四种实体分别怎么解析。其实套路都一样:遍历组码对,用switch区分每个组码,把值塞进对应的对象属性。
LINE实体的解析:
private DxfLine ParseLine(List<GroupCode> pairs) { var line = new DxfLine(); foreach (var p in pairs) { switch (p.Code) { case 8: line.Layer = p.Value; break; case 10: line.StartPoint.X = double.Parse(p.Value); break; case 20: line.StartPoint.Y = double.Parse(p.Value); break; case 30: line.StartPoint.Z = double.Parse(p.Value); break; case 11: line.EndPoint.X = double.Parse(p.Value); break; case 21: line.EndPoint.Y = double.Parse(p.Value); break; case 31: line.EndPoint.Z = double.Parse(p.Value); break; } } return line; }CIRCLE实体的解析:
private DxfCircle ParseCircle(List<GroupCode> pairs) { var circle = new DxfCircle(); foreach (var p in pairs) { switch (p.Code) { case 8: circle.Layer = p.Value; break; case 10: circle.Center.X = double.Parse(p.Value); break; case 20: circle.Center.Y = double.Parse(p.Value); break; case 30: circle.Center.Z = double.Parse(p.Value); break; case 40: circle.Radius = double.Parse(p.Value); break; } } return circle; }LWPOLYLINE的解析要特别小心。轻量多段线的顶点坐标组码是10和20,且每个顶点都会各出现一次,顺序是10 X坐标->20 Y坐标->10 X坐标->20 Y坐标。我用了两个临时变量暂存X值,等读到对应的20组码时再合并成一个顶点。
private DxfLwPolyline ParseLwPolyline(List<GroupCode> pairs) { var poly = new DxfLwPolyline(); double tempX = 0; foreach (var p in pairs) { switch (p.Code) { case 8: poly.Layer = p.Value; break; case 10: tempX = double.Parse(p.Value); break; case 20: poly.Vertices.Add(new Point2D { X = tempX, Y = double.Parse(p.Value) }); break; case 70: // 最低位为1表示闭合 poly.IsClosed = (int.Parse(p.Value) & 1) == 1; break; } } return poly; }这段代码的意图非常明确:组码10只是暂存,真正的顶点是在读到组码20时生成的。至于是否闭合,看组码70的最低位。你要是自己写这个解析,一定要记住这个"暂存后合并"的处理方式,它比"每遇到10就创建顶点,然后遇到20再赋值"更优雅。
INSERT实体的解析:图块引用的核心是拿到块名和插入点坐标。
private DxfInsert ParseInsert(List<GroupCode> pairs) { var insert = new DxfInsert(); foreach (var p in pairs) { switch (p.Code) { case 8: insert.Layer = p.Value; break; case 2: insert.BlockName = p.Value; break; case 10: insert.InsertPoint.X = double.Parse(p.Value); break; case 20: insert.InsertPoint.Y = double.Parse(p.Value); break; case 30: insert.InsertPoint.Z = double.Parse(p.Value); break; } } return insert; }这里就图块处理的问题留了一个伏笔——光拿到插入点是不够的,块里到底是什么图形,还需要去BLOCKS段找块定义,然后做坐标变换。这是我在下一章要展开的坑。
5. 项目里遇到的几个坑:不是格式文档会告诉你的
5.1 中文乱码:编码问题比想象中更常见
第一个坑出现在最基础的文件读取上。DXF文件里如果有中文注释、中文图层名,直接用Encoding.UTF8读取很可能得到一堆乱码。原因在于AutoCAD在中文版系统上默认按本地代码页保存DXF,国内通常就是GBK。
项目里我踩到过一次:用File.ReadAllLines(path, Encoding.UTF8)去读图层名"电气层",结果是"鐢垫皵灞"这样的乱码,实体全部被错误地划到了错误图层。后来改用Encoding.GetEncoding("GBK"),问题立刻消失。
但更稳妥的做法是做个兼容判断。我现在的处理方式是:
Encoding GetDxfEncoding(string path) { // 先看有没有UTF-8 BOM var bytes = File.ReadAllBytes(path); if (bytes.Length >= 3 && bytes[0] == 0xEF && bytes[1] == 0xBB && bytes[2] == 0xBF) return Encoding.UTF8; // 没有BOM,默认用GBK尝试,因为国内图纸多是GBK存 return Encoding.GetEncoding("GBK"); }这个逻辑不算完美,但实际用下来正确率很高。如果你处理的图纸来源单一,直接指定一种编码也是可以的。
5.2 组码10在多个实体里的含义不同
第二个坑属于"懂行才不踩"的那种。同一个组码10,在不同实体里含义完全不同:
- 在LINE里,10表示起点X坐标
- 在CIRCLE里,10表示圆心X坐标
- 在LWPOLYLINE里,10表示第一个顶点的X坐标
- 在TEXT里,10表示插入点X坐标
所以你不能写一个通用的"根据组码取坐标"的函数,然后把所有实体都套进去。必须针对每种实体类型分别写解析逻辑,否则极容易出现"把圆的起点当成圆心"这种离谱错误。
我建议在代码里把所有组码的解析逻辑用switch (entityType)分开,每种类型一个方法,哪怕刚开始代码多一点,也远比"试图用统一规则处理一切"要可靠。
5.3 图块INSERT不递归展开,坐标永远是错的
这是整个项目中我花时间最久才排查清楚的问题。前面解析INSERT实体只能拿到块名BlockName和插入点。但如果要拿到图块内部实际的几何图形,必须去BLOCKS段找到同名块定义,再把块定义里的实体坐标换算到当前坐标系。
换算公式是这样的,假设插入点坐标是(InsertX, InsertY),块定义里某个点是(bx, by),旋转角是angle,缩放因子是scaleX, scaleY:
X' = InsertX + scaleX * (bx * cos(angle) - by * sin(angle)) Y' = InsertY + scaleY * (bx * sin(angle) + by * cos(angle))如果只解析INSERT然后不展开,你得到的坐标只是"这个块放在哪",根本得不到"这个块里面的图形坐标"。对加工设备来说,没有展开的坐标数据完全不可用。
我的实际做法是:先解析BLOCKS段建立块名字到实体列表的映射,然后遍历ENTITIES段时,遇到INSERT就递归展开,把块内的实体坐标按上述公式变换后追加到最终结果。如果块内还有嵌套的INSERT,就继续递归,直到全部展开成基础几何体。
5.4 浮点数解析和区域设置
第三个坑是我这种中文环境才容易犯的:double.Parse在中文环境下默认使用当前区域设置,小数点可能被识别成逗号或反过来。比如你的DXF文件里写的是12.5,但某些系统区域设置下,double.Parse("12.5")会抛出FormatException,因为系统期望的是12,5。
解决办法有两个:一是用CultureInfo.InvariantCulture,二是用double.TryParse配合NumberStyles。我推荐前者,代码一行就搞定:
double.Parse(p.Value, CultureInfo.InvariantCulture)这个坑很隐蔽,因为你在自己电脑上调试一切正常,部署到客户的机器上突然全部解析失败,排查起来特别痛苦。
6. 性能优化:从30万行DXF说起
6.1 大文件的读取瓶颈在哪
有朋友问我,他的DXF文件有几十MB,用上面的File.ReadAllLines会不会卡死。其实C#的File.ReadAllLines内部也是流式处理,但对于一次性读取还是有内存峰值压力。对于几十MB的DXF,我实测下来大约占用原始文件5到8倍的内存,GBK编码下中文还会更大。
如果你想极致优化,我观察到瓶颈主要在三个地方:文本读取、组码对列表的内存占用、坐标值的字符串转double。坐标值转double这部分是CPU密集型的,没办法绕开;但文本读取和列表内存是可以优化的。
6.2 渐进式解析的改进方案
我的做法是把整个解析流程改成"边读边解析",不再构建完整的组码对列表。
public List<DxfEntity> ParseEntitiesStreaming(string dxfPath) { var result = new List<DxfEntity>(); using var reader = new StreamReader(dxfPath, GetDxfEncoding(dxfPath)); string line; int lineNumber = 0; bool inEntities = false; bool inEntity = false; var currentPairs = new List<GroupCode>(); while ((line = reader.ReadLine()) != null) { lineNumber++; if (lineNumber % 2 == 1) { // 奇数行是组码 if (int.TryParse(line.Trim(), out int code)) { string value = reader.ReadLine()?.TrimEnd('\r') ?? ""; var pair = new GroupCode(code, value); if (pair.Code == 2 && pair.Value == "ENTITIES") { inEntities = true; continue; } if (!inEntities) continue; if (pair.Code == 0 && pair.Value == "ENDSEC") break; if (pair.Code == 0 && inEntity && currentPairs.Count > 0) { // 上一个实体结束,解析它 ParseAndAddEntity(currentPairs, result); currentPairs.Clear(); } currentPairs.Add(pair); inEntity = true; } } } // 处理最后一个实体 if (currentPairs.Count > 0) ParseAndAddEntity(currentPairs, result); return result; }这段代码和ReadGroupCodes版本的区别在于:不再保留所有组码对,遇到新实体就解析、就释放。对30万行的DXF文件,我的实测结果是内存占用从原来的200MB降到了35MB,解析时间也略有缩短,因为省去了中间列表的分配和遍历。
6.3 要不要用多线程
很多人在网上问C#读DXF能不能多线程。我的答案是:实体解析阶段可以多线程,但文件读取阶段不要。
DXF文件是顺序文本格式,你不可能一边读文件一边多线程处理"下一行"——文本读取本身就是串行的。但当你把实体解析分给多个线程时,每个实体是独立的,互不依赖,理论上是能并行的。
在.NET里用Parallel.ForEach处理实体列表就够:
var entities = new ConcurrentBag<DxfEntity>(); Parallel.ForEach(rawEntities, raw => { var entity = ParseSingleEntity(raw); if (entity != null) entities.Add(entity); });不过实测下来,对小文件(5MB以下),多线程反而更慢,因为线程调度开销超过了并行收益。我现在的建议是:超过20MB的文件才值得考虑多线程,其余情况单线程足矣。
7. 读完之后呢:坐标数据的落地应用
7.1 与业务系统的数据对接
坐标解析出来不是终点,关键是怎么用。我在上位机里做了三件事:
- 把坐标点集转换为加工路径序列,输出成PLC/运动控制卡可直接识别的点位表
- 按图层筛选实体,只导出指定图层的图形数据,避免把标注、文字这些非加工元素混进去
- 将数据序列化为JSON接口,供前端网页实时预览轮廓
这里有个值得注意的设计思路:解析层和业务层解耦。所有DXF相关内容都在一个独立的DxfService类里,对外只暴露List<DxfEntity>,上位机其他模块根本不需要知道数据来自DXF还是别的格式。以后如果需要支持DWG或其他格式,只要替换掉DxfService的内部实现即可。
7.2 我的进一步的打算
现在这个解析器已经稳定运行了接近一个月,处理过大约两百份图纸。我后续打算做两件事:
第一个是支持更多实体类型。目前只处理直线、圆、多段线和图块,后续会补上ARC(圆弧)和SPLINE(样条曲线)。圆弧需要把起始角度和终止角度换算成离散点,样条曲线则需要按精度参数化离散,这两个都需要额外的数学计算。
第二个是把图块展开做成可配置选项。有些场景只需要统计图块数量,有些场景却需要完整展开所有几何体。做成配置项之后,调用方可以根据需求选择,避免不必要的展开计算消耗。
对于读者你现在的情况,我的建议非常直接:如果你的需求和我的类似,就是"从DXF里提取坐标供业务系统使用",完全可以照着这篇文章的思路自己写一个。先做直线和圆,跑通流程,再逐步加多段线和图块。别一上来就想覆盖所有实体类型。
如果你真想做得更通用,那就直接用netDXF,别折腾了。两种路线没有谁比谁更高级,只有谁更适合你的场景。手动解析对于我自己这种强依赖坐标的场景来说,带来的可控性和可调试性,是第三方库替代不了的。
本文还有配套的精品资源,点击获取