ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

WPF大图加载优化:基于瓦片金字塔的TiledViewer实践

WPF大图加载优化:基于瓦片金字塔的TiledViewer实践 我最初做TiledViewer纯粹是被一张卫星图逼的。甲方给的遥感影像单张60000乘40000像素直接用WPF的Image控件丢进去程序当场吃满4G内存然后卡死、转圈、白屏最后直接崩溃。那时候我深刻意识到在大图面前WPF原生控件就是个摆设。查了一堆资料折中的方案有切片、压缩、降采样但都不够彻底。最终我决定自己写一套基于瓦片金字塔的按需加载浏览器也就是TiledViewer。TiledViewer这套思路并不新鲜地图厂商用了十几年了但把它搬进WPF、和桌面端交互深度绑定还是有很多细节值得拿来拆一拆。这篇文章不做空洞的原理汇报我把架构设计、核心代码、关键算法、以及我踩过的坑完整写出来希望能给同样被大图折磨过的人一个可靠的参考。无论你是在做GIS应用、医学影像查看器还是CAD图纸预览、设计稿审查工具这套方案都能直接落地。1. 为什么在WPF里直接加载大图是一场灾难1.1 实测一张亿级像素图片的暴力加载先说一个我自己的实测数据。一张分辨率为60000乘40000的24位位图未压缩的原始数据量大约是60000乘以40000乘以3字节算下来是60GB当然实际文件以压缩格式存储比如JPEG可能只有200到500MB。但是文件尺寸小不代表解码后内存小WPF调用BitmapImage加载时如果没做任何特殊处理最终会把这个60GB的原始位图数据完整解压到内存里只不过它不会一次性全部摊开会走托管内存和本机内存混合的通道但量级是逃不掉的。在我的测试机器上16G内存.NET Framework 4.8x64进程这样一张图加载后进程工作集稳定在6GB左右UI线程直接冻住十几秒期间窗口无法拖动、无法响应任何鼠标事件。更糟的是缩放和平移操作会触发重新渲染每次都掉进卡顿的泥潭里。这还只是一张图如果应用逻辑里存在多图切换或缩略图并排展示内存会直接爆掉进程被系统判定为无响应连优雅退出的机会都没有。有人会问WPF不是自带解码时的尺寸缩放吗确实有DecodePixelWidth和DecodePixelHeight这两个属性可以让我们在解码阶段就按需缩小比如把60000宽的图按1000宽来解码内存占用可以降到几MB。但问题在于这种方案牺牲了细节当用户想放大看图上某个区域的清晰内容时看到的是一团马赛克根本没法用。我们要的是既能缩略又能看细节还想内存可控、操作流畅这种既要又要的需求原生方案给不了。1.2 三个致命瓶颈内存、解码线程与UI渲染深入分析后大图在WPF里卡死其实源自三个互相叠加的瓶颈。第一个瓶颈是内存。大图解码后是一块巨大的连续本机内存加上WPF为了显示图像还会针对渲染场景生成对应的中间层缓存比如软件渲染模式的系统内存位图、硬件渲染模式的显存纹理。无论是哪一种数据量都是像素宽乘像素高乘字节数动辄上GB。64位进程虽然能用大内存但用户机器不一定大而且内存分配和GC压力会带来显著的耗时。第二个瓶颈是解码。BitmapImage默认在UI线程进行解码加载或者更准确地说它触发的很多操作会和UI线程同步等待。一张大图的解码时间用秒甚至十几秒计这段时间里消息泵被阻塞整个窗口表现为无响应。虽然WPF有BitmapCacheOption.OnLoad、OnDemand几种缓存方式但OnLoad会在加载完成后立即释放文件流而解码大图的耗时并没有因此减少多少关键线程还是卡。第三个瓶颈是渲染。即使我们把图片成功加载了WPF在渲染时会把整张位图作为Texture上传到GPU。如果分辨率超过GPU的单纹理上限比如很多显卡只支持16384乘16384那么图片会被切分上传同时缩放变换会引起GPU重新采样每一帧都要处理上百兆甚至上GB的纹理数据帧率自然暴跌。此外WPF的渲染线程如果发现纹理太大还可能回退到软件渲染画面一卡一卡就是这么来的。1.3 WPF自带的虚拟化机制为什么救不了大图有经验的朋友会说ListView有VirtualizingStackPanel可以用UI虚拟化来解决大数据量显示。我们把大图切成很多小块再用ItemsControl虚拟化显示不就行了这个思路方向是对的但落到实处就会发现几个麻烦。VirtualizingStackPanel的虚拟化原理是只让可视区域内的控件实例化可视区域外的控件连DataTemplate都不会创建。但是在大图场景里我们的可视区域是动态的、连续变化的随着缩放比例不同可见瓦片数量、层级都不同VirtualizingStackPanel并不知道这些瓦片的空间坐标应该怎样根据缩放系数变换。你确实可以用ItemsControl作为宿主配合自定义Panel来布局但虚拟化逻辑就必须自己接管这等于绕了半个圈子最后还是回到自定义画布的老路上。还有一个深层问题即使使用UI虚拟化每个瓦片仍然是一个Image控件而Image控件本身的开销不低。当瓦片数量达到几百甚至上千个时即使它们都可见布局、测量、渲染的开销已经吃掉了一部分性能而瓦片图像的解码工作如果不同步控制依然会给UI线程带来压力。所以纯粹的控件级虚拟化解决不了根本问题我们需要的是一套从数据准备到加载调度再到渲染展示的完整方案。2. TiledViewer的设计核心向地图App偷师2.1 瓦片金字塔模型一张图拆出一棵树既然原生方案走不通我不打算为了显示一张图而把计算机资源烧光我要做的是只看当下需要看的。想象一下我们不是把一整张地图塞进屏幕而是像地图应用那样把地图切成小方块根据当前视野动态请求对应的小方块。这就是瓦片金字塔模型它的核心思想是同一张图按照不同缩放比例拆成多组瓦片每组瓦片属于一个级别级别越高瓦片数量越多、分辨率越高、展示的细节越丰富。具体到TiledViewer里我把原始大图作为最高级Level N然后按照2倍步长做降采样生成Level N-1、Level N-2……一直到Level 0。Level 0是整张图的完整缩略图通常是一个瓦片甚至更小Level 1是2乘2个瓦片Level 2是4乘4个瓦片依此类推。由于每一层只比上一层多两倍的线性分辨率整体数据量是收敛的不会爆炸式增长。举个例子原图尺寸如果是四百万乘四百万像素理论上Level 0就长这样但因为我们做了降采样Level 0甚至可以缩到几百乘几百用户第一眼看到的是缩略图当放大操作发生时再去加载更高级别的瓦片。这套模型的一个关键好处是无论原图多大任何时刻真正需要显示的数据量只跟视口大小和缩放级别相关而不跟原图总量挂钩。显示一屏最多只需要几十张瓦片每张瓦片足够小我习惯用256乘256解码时只用几十毫秒内存也就几十KB做起来非常轻松。2.2 瓦片尺寸与金字塔层级的选取逻辑瓦片尺寸没有绝对标准但实践经验里256乘256和512乘512是最常用的。我最终选择了256乘256原因是这个尺寸在解码性能、内存占用、渲染开销之间最均衡。256太小瓦片数量会变多加载调度频繁CPU和磁盘IO压力增加256太大单瓦片解码时间变长加载等待更明显同时超出视口范围的冗余加载量增加。512乘512虽然能减少瓦片总数但在频繁缩放和平移时单块加载的延迟会明显放大用户体验反而不如小瓦片顺滑。金字塔层级的选取关键在于最低层和最高层。最低层我一般设计为整图刚好能塞进一个瓦片或者最多2乘2个瓦片。如果整图宽高比不协调可以单独计算核心原则是保证第一眼看到全貌时的清晰度能接受。最高层就是原图本身但需要注意原图可能不是恰好能整除256切分时边缘瓦片会缺一块处理方式是填充透明像素或复制边缘像素保证每个瓦片都是完整的256乘256渲染时便于处理。从Level 0到Level N的降采样倍率我建议每层降低2倍这样用户从缩略图到全尺寸之间最多经历十来级缩放过渡自然而且每一层瓦片的分辨率差异一致做重采样时算法简单。如果原图特别大也可以每层降低4倍但不推荐因为层级跨越太大会导致缩放过程中清晰度跳跃感明显看起来像在切换图片而不是放大地图。2.3 磁盘目录与文件命名约定比配置更重要瓦片切好之后需要持久化存储否则每次启动都要重新切一遍。磁盘上的目录结构我采用根目录/级别/行号/列号的方式比如tiles/3/2/5.png表示Level 3、第2行、第5列的瓦片。文件名可以直接用列号扩展名根据格式而定我用PNG因为无损、带透明通道、解码速度可以接受。JPEG虽然体积更小但有损且透明通道支持不好边缘可能出现压缩伪影。目录层级的设计有讲究。如果把所有瓦片塞进同一个目录文件数量会非常恐怖比如Level 10一次就是1024乘1024个文件操作系统在单个目录下管理百万级文件时会严重降速。所以必须用级联目录每层目录控制几百个文件的上限。另外命名中不要加入多余的语义信息级别、行、列三个数字足够了这样路径拼接只需简单的字符串插值解析时也只用整数运算。注意如果瓦片数量很大NTFS文件系统本身也可能成为瓶颈。大规模的桌面应用可以考虑把瓦片打包成二进制合并文件或使用SQLite存储瓦片块。TiledViewer目前的方案是文件目录适合瓦片规模在十万级以下如果原图是那种几十GB的卫星影像建议升级存储方案。3. 按需加载与三级缓存让千万像素瞬间渲染的秘密3.1 视口驱动的瓦片坐标计算现在来到最关键的部分如何根据当前可视区域计算出应该加载哪些瓦片。这一步是TiledViewer的性能核心做得不好就会导致加载过多、加载过少或加载错层级。我定义了一个视口对象它包含归一化坐标下的位置信息当前缩放级别Level、视口左上角在全景坐标中的位置单位是瓦片以及视口的宽高单位也是瓦片。全景坐标用瓦片作为单位好处是各层级之间换算非常容易因为每一层瓦片坐标的分辨率按2倍递增。假设当前视口宽度为viewportWidth以像素为单位、高度为viewportHeight瓦片尺寸为tileSize缩放级别为level。那么可见瓦片的横向范围是startCol等于floor(offsetX除以tileSize)endCol等于ceil((offsetX加viewportWidth)除以tileSize)纵向同理startRow和endRow分别根据offsetY计算。这里的offsetX、offsetY是视口左上角相对于当前级别下全景左上角的偏移。有了范围之后双重循环即可得到瓦片索引集合。值得一提的是这个计算必须放在UI缩放变化时同步执行但在滚动和缩放过程中计算频率会非常高所以要确保整个过程是轻量级的不要在这里做任何IO或解码操作。我习惯把这个计算写成纯函数输入视口参数输出瓦片索引列表方便单元测试和后续优化。3.2 三级缓存架构内存、磁盘、解码队列瓦片索引计算出来后下一步是获取对应的图像数据。我设计了一个三级缓存架构分别是内存缓存、磁盘缓存、解码队列。内存缓存是最高优先级它保存已经解码好的BitmapSource对象。当需要某个瓦片时先查内存缓存命中就直接加入渲染队列不再走后续流程。内存缓存使用Dictionary作为底层结构键是瓦片索引的字符串表示值是弱引用或者强引用这取决于你的内存策略我后面会细说。磁盘缓存针对的是那些已经生成过、但当前内存中不存在的瓦片文件。查询磁盘时如果文件存在就发起异步解码请求解码完成后写入内存缓存并通知UI刷新如果文件不存在说明该瓦片要么还没生成要么是边缘越界的填充瓦片需要特殊处理。解码队列是整个架构中最容易被忽视的环节。WPF的BitmapSource解码默认是同步的如果你在UI线程直接调用new BitmapImage(new Uri(path))UI就会卡住。解决的方案是使用后台线程或Task来执行解码然后把解码完成的BitmapSourceDispatcher到UI线程。但这里有个问题如果用户以每秒数次的频率缩放和平移每秒产生的瓦片请求可能多达几十上百个如果全部无脑丢进后台线程池解码秩序会乱掉出现大量请求排队、高优先级瓦片被低优先级瓦片堵住的情况。所以必须引入优先级的控制。3.3 缓存淘汰与回收如何让内存不爆炸内存缓存的容量不能无限膨胀。我最初实现时用的是强引用缓存所有加载过的瓦片都留在内存里结果程序运行十分钟后内存就到了几个GB因为用户来回缩放平移时大部分历史瓦片已经不再需要但它们仍然占着内存。后来我引入了容量限制和LRU淘汰策略。内存缓存最多保存N个瓦片对象N可以根据用户机器内存动态调整我一般取200到500之间每次插入新瓦片时如果容量已满就淘汰最久未使用的那个瓦片。淘汰时要注意不仅要从Dictionary移除引用还要主动调用Freeze方法释放渲染资源或者让GC去回收但Freeze之后可写性会丢失所以我的做法是只在瓦片成为缓存对象时冻结它后续只读不写。弱引用是另一个可选方案。WeakReference不会阻止GC回收意味着内存紧张时瓦片会被自动回收不需要显式淘汰但这种方案的问题在于命中率不稳定瓦片可能在你还想用它的时候被GC清掉导致再次解码。实际组合策略是热瓦片用强引用LRU冷瓦片用弱引用这样既保证高频瓦片快速命中又让低频瓦片的内存开销可控。磁盘缓存不需要太复杂的淘汰策略因为瓦片文件一旦生成就是固定的后续访问只是一次磁盘IO。但如果瓦片总数据量过大比如几十GB可以考虑只保留用户最常用的几个层级或者提供配置项瘦身。3.4 缩放性能的关键多级分辨率重采样直接从一个层级跳到另一个层级如果跨度太大用户会明显感受到清晰度变化。TiledViewer在缩放时采用渐进式加载先显示当前层级分辨率较低的瓦片然后在高层级瓦片解码完成后替换掉旧瓦片。这样用户放大时画面先模糊一下再立刻变清晰体验上比卡顿好得多。为了让这个过程更平滑我还会在显示每个瓦片时指定合适的BitmapScalingMode。比如在缩小过程中目标渲染尺寸小于瓦片原始尺寸用Linear模式就能得到不错的抗锯齿效果在放大过程中目标渲染尺寸大于瓦片原始尺寸再用Linear可能发虚而使用Fant模式效果更好。但是Fant模式GPU开销更高所以我的策略是允许变化较慢的情况下用Fant缩放过程中优先保证帧率采用Linear或NearestNeighbor在放大倍数很大时可以接受马赛克。多级分辨率重采样还涉及层级切换时坐标对齐的问题。相邻层级的瓦片坐标其实是2倍关系比如Level 3的(2,3)瓦片对应Level 4的(4,6)瓦片的大致区域但因为是整除切换瞬间可能出现半瓦片的偏差。我的解决方法是把所有瓦片坐标统一转换为全景归一化坐标0到1范围渲染时再根据当前层级换算成像素坐标这样坐标变换始终是一致且精确的。4. 从零搭建一个简版TiledViewer4.1 项目结构与工具选型搭建TiledViewer我建议用.NET Framework 4.8或.NET 6以上的WPF项目。如果追求跨平台可以考虑Avalonia但这里以WPF为例因为题目和场景都锁定在WPF。项目结构分为三块瓦片生成器、瓦片浏览器核心库、演示UI。瓦片生成器负责把原始图片切分成金字塔瓦片文件我用一个独立的控制台项目不掺入UI逻辑瓦片浏览器核心库包含视口计算、缓存管理、异步解码、渲染画布等纯逻辑类不依赖具体页面演示UI是一个简单的WPF窗口负责承载画布控件和交互事件。工具方面瓦片生成用WPF自带的BitmapSource和TransformedBitmap就够用了不需要额外的图像处理库。如果原图是超大TIF或其他特殊格式可能需要引入第三方解码库比如ImageMagick或libtiff的.NET封装但常规的PNG、JPEG、BMP场景原生WPF图像栈完全够用。为了演示方便我这里采用一个自动生成的渐变测试图作为输入避免依赖特定素材。4.2 第一步写一个瓦片生成器瓦片生成器要做的任务很清晰读入原图从最低层开始向上生成每一层缩略图再把每一层切成256乘256的小块保存为PNG文件。我写核心代码时先把原图加载为BitmapSource然后循环level从0到maxLevel每一层计算该层的总尺寸用TransformedBitmap按比例缩放再用CroppedBitmap切割。注意TransformedBitmap生成的是一个延迟加载的位图真正渲染到像素数据前不会立即解码所以如果直接切可能会保留一份超大尺寸的中间对象反而浪费内存。我的做法是每层先转换成WriteableBitmap或CopyPixels到像素数组确保该层已经真正完成解码缩放再执行切割。切瓦片时每一层先计算整层宽高对应的瓦片行列数然后对每个区块执行CroppedBitmap图片一致后使用PngBitmapEncoder编码保存。边缘瓦片如果超出原图边界可以裁剪也可以填充。我选择保存为透明像素的完整256乘256瓦片因为这样渲染时边缘不会有黑色或白底闪烁尤其在叠加多个图层场景时透明更友好。为了提高生成速度可以用Parallel.For并行切瓦片。但有个细节同一层不同瓦片之间没有依赖可以安全并行不同层之间虽然有缩放依赖我们已经在内存里做好了像素数组所以也可以并行。唯一要注意的是PNG编码器线程安全所以每个线程必须创建自己的编码器实例。在我的测试机上约1亿像素的JPEG原图切成7级瓦片耗时大约15秒在可接受范围内。4.3 第二步自定义Canvas布局与视口窗口UI层我直接继承FrameworkElement实现了一个自定义画布把缩放、平移逻辑全部集中在这个控件里方便后续复用。渲染方式是在OnRender方法里遍历当前可见瓦片用DrawingContext.DrawImage绘制每个BitmapSource到对应位置。这里不推荐使用Image控件列表原因前面提过控件开销大且需要额外布局。FrameworkElement的OnRender是轻量级的它不会创建UIElement对象只是记录绘制指令性能上要好得多。画布的核心状态有三个当前Level、中心点坐标以全景归一化坐标表示、缩放系数。每次滚轮事件、拖动事件发生后更新这三个状态然后调用InvalidateVisual触发下一次OnRender。OnRender里根据当前状态计算视口对应的瓦片索引查找缓存绘制可见瓦片。一个重要的细节是OnRender中不应执行任何异步解码操作它必须只读取已经就绪的缓存内容。如果瓦片未就绪可以先绘制一个占位背景色或低分辨率占位图等解码完成后再触发InvalidateVisual刷新。这样可以避免渲染管线被阻塞保证交互始终流畅。4.4 第三步异步加载管线与优先级队列瓦片加载的核心是一个支持优先级的异步任务队列。我在后台维护一个ConcurrentQueue或更专业的优先队列队列元素包含瓦片索引、优先级分数、解码参数。视口变化后计算出的瓦片集合按离视口中心越近优先级越高的规则打分然后提交到队列。后台有一个长期运行的任务循环不断从队列取出最高优先级的瓦片执行磁盘读取和解码完成后将结果写入内存缓存并通过Dispatcher.BeginInvoke发出一个瓦片已就绪的信号。UI线程收到信号后如果是当前视口需要的瓦片就调用InvalidateVisual让画面更新。这里有个坑如果每个瓦片加载完成都立刻往UI线程发一次Invoke高频操作时消息队列会被刷爆。我的优化方案是引入批量通知机制后台线程每完成一批瓦片比如5个或所有当前批次才发一次刷新请求同时设置200毫秒的刷新合并窗口避免重复刷新。这样UI刷新次数被控制在一个合理范围视觉上并不会感知到延迟。优先级队列需要支持动态调整因为用户在缩放过程中某一刻视口覆盖的瓦片集合是动态变化的。我之前用固定优先级结果用户快速平移时画面永远在加载旧的瓦片新的瓦片排在队尾一直等。后来改成每次视口变化后旧的低优先级任务可以被取消或降级这样用户操作时始终优先加载当前视野内的内容而不是历史遗留下的请求。4.5 第四步滚轮缩放和平移交互滚轮缩放是用户最直观的操作实现时要注意缩放锚点。锚点应该是鼠标位置对应的全景坐标点缩放过程中这个点要保持不动否则用户会觉得画面在漂移。公式是先记录缩放前鼠标位置对应的全景坐标应用缩放系数后再调整中心点坐标使得缩放后鼠标位置仍然指向同一个全景坐标。缩放系数我采用2的幂次倍率最小0.1最大32超过范围就限制住。滚轮每转一格缩放系数乘以1.1或除以1.1对应的Level可以通过对数计算得出。Level可以是整数但为了平滑过渡我允许Level为小数只在瓦片选择时取floor和ceil两个层级分别绘制再按比例交叉淡入淡出。这样缩放动画非常平稳不会出现突变的锯齿感。平移操作我习惯用鼠标拖拽或触摸板滚动。按下鼠标左键记录起点拖动时根据像素距离除以当前缩放系数得到全景坐标偏移然后更新视口中心。为了避免拖动过程中画面跳动所有计算必须使用同一套坐标变换函数确保像素坐标和全景坐标的互转是可逆的。5. 实战踩坑常见问题与排查实录5.1 瓦片闪烁和黑块OnRender时机与缓存我最初写完第一版运行时发现一个诡异的问题快速缩放时画面频繁闪现黑块和白色闪烁。排查半天发现是OnRender里访问缓存时某些瓦片还没有解码完成我返回了一个NULL导致DrawingContext直接略过该瓦片画面就出现缺失区域。更麻烦的是解码完成后我虽然调用了InvalidateVisual但由于没有妥善处理缓存命中和未命中分支有时刷新请求还没执行旧帧已经被擦掉于是闪烁。解决方案是双管齐下一是为缺失瓦片绘制占位内容通常是从低层级瓦片拉伸的模糊缩略图视觉上不会出现黑缝二是引入版本号机制视口每次变化时递增版本号OnRender里只绘制与当前版本匹配的瓦片避免旧瓦片覆盖新画面导致闪跳。5.2 内存只涨不降Image控件与BitmapSource的生命周期之前用Image控件列表方案时内存疯涨不止。原因是每个Image控件的Source都会持有BitmapSource引用而UIElement滚出可视区后如果布局容器没有及时释放引用链会一直存在。后来改用OnRender之后这个坑自动消失了因为在OnRender里不会产生持续的对象引用。不过内存还是可能缓慢上涨排查后发现是高并发解码时每次new BitmapImage都会创建解码器对象这些对象如果没有及时释放会积压在托管堆里。解决办法是解码完的BitmapSource立即调用Freeze()把它变为不可变对象WPF内部可以更高效地管理它的本机资源同时在缓存淘汰时主动清除引用让GC能回收。另外解码请求失败或取消时也要确保不留下未释放的Stream对象。5.3 首屏太久加载优先级排序的教训如果用户打开应用首先看到的是密密麻麻的瓦片逐个弹出体验很糟糕。我一开始没有做优先级排序瓦片按索引顺序加载靠近视口中央的内容反而被边缘瓦片抢了先机。后来加了优先级打分逻辑瓦片到视口中心的距离越小优先级越高当前层级的瓦片优先级高于其他层级。这样首屏加载时间从原来的几秒缩短到不足一秒视觉感知上几乎是瞬间出现。优先级还有另一个用途用户连续缩放时系统应该优先加载目标层级的瓦片而不是当前层级的。我实现时是预计算下一层的瓦片索引在用户停止滚轮缩放300毫秒后才提交这些请求避免快速操作过程中高优先级任务反复更换导致队列频繁重排。5.4 高分屏下模糊DPI缩放坐标系修正高DPI显示器是WPF开发绕不开的问题。在150%缩放的屏幕上WPF的坐标系统会自动变换如果你的瓦片尺寸和画布坐标直接按物理像素对应画面会显得模糊或尺寸不对。我的处理办法是在计算瓦片绘制位置和视口大小时显式地除以DPI缩放系数把逻辑像素换算成物理像素。比如在150%缩放下256像素的瓦片在屏幕上应该占用384物理像素但WPF的布局系统会自动放大如果我不做转换瓦片会被放大1.5倍分辨率不足导致模糊。TiledViewer的应对策略是保持内部坐标系统一为逻辑像素但对于高DPI屏幕把瓦片尺寸定义为256逻辑像素除以DPI系数后的值这样在150%缩放下实际使用约170物理像素的瓦片分辨率屏幕上显示时正好占256逻辑像素清晰度匹配。6. 后续还能怎么玩TiledViewer当前已经稳定支撑了项目中多个大图浏览需求但我也在持续扩展它。一个方向是支持动态瓦片生成即不做预切割而是利用后台线程实时从原图裁剪出需要的块。这个方案适合原图经常变化或无法提前切片的场景缺点是首次浏览某个区域时会有加载延迟但随着本地缓存的热度上升体验会逐渐接近预切割方案。另一个方向是更大的生态整合。把TiledViewer封装成一个独立控件暴露瓦片源接口这样不仅可以用来浏览本地文件还可以对接远程瓦片服务比如加载在线地图数据或云端的影像服务。对于团队内多个项目复用这是个非常值得投入的方向。如果你也在做类似的功能我建议千万不要绕开瓦片金字塔这个核心思路不管是预切割还是动态生成金字塔模型都是大图浏览器的根本。最后再分享一个小技巧在调试瓦片画布时用Color或文字把瓦片坐标绘制到瓦片本身瞬间就能看出坐标计算和渲染位置是否正确比盯着纯图像调试高效得多。
返回列表