iOS Safari下OpenLayers地图Canvas内存超限问题求助
iOS Safari中OpenLayers地图Canvas内存超限问题解决方案
问题描述
我在网站中使用OpenLayers展示地图,并基于经纬度添加不同类型的标记点,当前代码如下:
const urlArgs = new URLSearchParams(document.location.search) const layersToUse = 5; const disposeInternal = VectorTileLayer.prototype; let customFeatures = {} let toiletFeatures = [] let gastroFeatures = [] let allFeatures = {} function addToLayer(mt, pt) { let ft = new Feature({ geometry: new Point( fromLonLat([ parseFloat(pt.longitude), parseFloat(pt.latitude) ]) ), name: mt.name, iconUrl: mt.image, popupText: mt.popup_text, popupLinkText: mt.popup_link_text, popupLink: mt.popup_link, }); if (mt.imported_type === "toilet") { toiletFeatures.push(ft) } else if (mt.imported_type === "gastro") { gastroFeatures.push(ft) } else { if (customFeatures[mt.marker_index % layersToUse] === undefined) { customFeatures[mt.marker_index % layersToUse] = []; } customFeatures[mt.marker_index % layersToUse].push(ft) } key = pt.longitude+"/"+pt.latitude; allFeatures[key] = ft; } let htmlMarkers = Array.from(document.querySelectorAll('marker')) markers.forEach( (mt) => { Array.from(mt.points).forEach( (pt) => { addToLayer(mt, pt) } ) } ) let customSources = []; for (let i = 0; i < layersToUse; i++) { customSources.push( new VectorSource({ features: customFeatures[i] }) ) } let toiletSource = new VectorSource({ features: toiletFeatures, cacheSize: 10, }) let gastroSource = new VectorSource({ features: gastroFeatures, cacheSize: 10, }) let toiletLayer = new VectorLayer({ source: toiletSource, style: (feat) => { return new Style({ image: new Icon({ anchorOrigin: "bottom-left", anchor: [0.5, 0.5], width: 35, height: 35, anchorXUnits: 'fraction', anchorYUnits: 'fraction', src: feat.get("iconUrl"), }) }) }, declutter: true }) let gastroLayer = new VectorLayer({ source: gastroSource, style: (feat) => { return new Style({ image: new Icon({ anchorOrigin: "bottom-left", anchor: [0.5, 0.5], width: 35, height: 35, anchorXUnits: 'fraction', anchorYUnits: 'fraction', src: feat.get("iconUrl"), }) }) }, declutter: true }) let customLayers = []; for (let i = 0; i < layersToUse; i++) { customLayers.push( new VectorLayer({ source: customSources[i], style: (feat) => { return new Style({ image: new Icon({ anchorOrigin: "bottom-left", anchor: [0.5, 0.5], width: 35, height: 35, anchorXUnits: 'fraction', anchorYUnits: 'fraction', src: feat.get("iconUrl"), }) }) } }) ); } let zoneSource = new VectorSource({ url: '/static/map_assets/map_zones.kml', cacheSize: 10, format: new KML({ extractStyles: false }), }) let zoneLayer = new VectorLayer({ source: zoneSource, style: (feat) => { const zoneColors = { "zone_1": 'rgba(244, 174, 108, 0.6)', "zone_2": 'rgba(227, 148, 193, 0.6)', "zone_3": 'rgba(161, 140, 217, 0.6)', "zone_gastro": 'rgba(115, 211, 121, 0.6)', "zone_arena": 'rgba(198, 26, 12, 0.8)', "zone_industrie": 'rgba(253, 215, 64, 1)', } return new Style({ fill: new Fill({ color: zoneColors[feat.id_], }) }) }, }) const klotenBaseView = new View({ center: fromLonLat([8.57303, 47.46117]), rotation: -0.7, constrainRotation: true, zoom: 16, minZoom: 16, maxZoom: 18, // Matches full map size at minZoom extent: [952802.0356325043, 6016519.755591137, 955663.7673070097, 6018872.403698619] }) let zoomCtrl = new Zoom() let attribution = new Attribution() const map = new olMap({ controls: [zoomCtrl, attribution], target: 'map-container', layers: [ new VectorTileLayer({ declutter: true, preload: 0, source: new VectorTileSource({ projection: 'EPSG:3857', format: new MVT(), url: 'pbf_url', maxZoom: 14, cacheSize: 10, }) }) ], view: klotenBaseView, }); apply( map, "style.json_url" ).then((t) => { map.getLayers().extend([ zoneLayer, toiletLayer, gastroLayer ]); customLayers.forEach((l) => { map.addLayer(l); }); }) if (urlArgs.get("lat") && urlArgs.get("lon")) { let urlPos = fromLonLat([ parseFloat(urlArgs.get("lon")), parseFloat(urlArgs.get("lat"))] ); for(var key in allFeatures) { var value = allFeatures[key]; var lat_long = key.split("/"); var lat = parseFloat(lat_long[1]); var lon = parseFloat(lat_long[0]); var featLatLong = fromLonLat([lon, lat]); if (toStringXY(featLatLong) === toStringXY(urlPos)) { if (value && value.getGeometry().getType() === "Point") { showModal( value.get("name"), value.get("popupText"), value.get("popupLinkText"), value.get("popupLink") ) break; } } } map.once('postrender', function() { var centerPx = map.getPixelFromCoordinate(urlPos); var newCenterPx = [centerPx[0] , centerPx[1]+ 80]; var newCenter = map.getCoordinateFromPixel(newCenterPx); klotenBaseView.setCenter(newCenter); // klotenBaseView.setCenter(fromLonLat(urlLongLat)); klotenBaseView.setZoom(18) }); } let lastZoom = map.getView().getZoom() map.on('moveend', (_event) => { let newZoom = map.getView().getZoom(); if (newZoom < 17.3) { gastroLayer.setVisible(false) customLayers.forEach((l) => {l.setVisible(false)}) } else if (newZoom > 17.3) { gastroLayer.setVisible(true) customLayers.forEach((l) => {l.setVisible(true)}) } lastZoom = newZoom }) map.on('click', (event) => { if (event.pixel) { const clickedFeat = map.forEachFeatureAtPixel(event.pixel, (feat) => { return feat }) if (clickedFeat && clickedFeat.getGeometry().getType() === "Point") { showModal( clickedFeat.get("name"), clickedFeat.get("popupText"), clickedFeat.get("popupLinkText"), clickedFeat.get("popupLink") ) } event.preventDefault() } }) map.once('rendercomplete', (event => { const areaSelectors = document.querySelectorAll('.area_selector') areaSelectors.forEach((el) => { el.addEventListener('click', (ev) => { const zoneFeature = zoneSource.getFeatureById(el.dataset.zone) const zonePoly = zoneFeature.getGeometry() klotenBaseView.fit(zonePoly) }) }) if (urlArgs.get("zone")) { const zoneFeature = zoneSource.getFeatureById(urlArgs.get("zone")) const zonePoly = zoneFeature.getGeometry() klotenBaseView.fit(zonePoly) } }))
该地图在多数浏览器中运行正常,但在iOS Safari中触发如下错误:
Total canvas memory use exceeds the maximum limit (384 MB).
已知Safari存在Canvas缓存相关问题,但不清楚如何在现有代码中修改以解决,求技术方案。
解决方案
iOS Safari对单页面Canvas内存有严格限制,你的代码存在图标资源重复创建、图层拆分过多、无效绘制逻辑等问题,导致内存占用超标。以下是针对性修改:
1. 复用图标资源,避免重复加载
原代码中每个Feature的Style都新建Icon实例,即使图标URL相同也会重复缓存。新增图标缓存逻辑:
// 全局图标缓存对象 const iconCache = {}; // 复用Icon样式的工具函数 function getIconStyle(iconUrl) { if (!iconCache[iconUrl]) { iconCache[iconUrl] = new Style({ image: new Icon({ anchorOrigin: "bottom-left", anchor: [0.5, 0.5], width: 35, height: 35, anchorXUnits: 'fraction', anchorYUnits: 'fraction', src: iconUrl, }) }); } return iconCache[iconUrl]; }
将所有标记图层的style替换为调用该函数,比如:
// toiletLayer的style修改 style: (feat) => getIconStyle(feat.get("iconUrl"))
gastroLayer、customLayers的style同理替换。
2. 合并冗余图层,减少Canvas上下文数量
原代码将自定义标记拆分为5个图层,加上toilet、gastro图层共7个标记图层,每个图层对应独立的Canvas上下文,大幅增加内存。合并为单个自定义图层:
- 修改
addToLayer函数中的自定义标记逻辑:
let customFeatures = []; // 改为数组而非对象 // ... else { customFeatures.push(ft); // 直接加入统一数组 }
- 创建单个自定义图层:
let customSource = new VectorSource({ features: customFeatures }); let customLayer = new VectorLayer({ source: customSource, style: (feat) => getIconStyle(feat.get("iconUrl")), declutter: true });
- 添加图层时,替换原
customLayers.forEach逻辑,仅添加这个customLayer。
3. 优化图层可见性与资源清理
在图层隐藏时,触发重绘以释放无用Canvas资源:
map.on('moveend', (_event) => { let newZoom = map.getView().getZoom(); const shouldShow = newZoom > 17.3; gastroLayer.setVisible(shouldShow); customLayer.setVisible(shouldShow); // 隐藏时触发图层变更,清理缓存 if (!shouldShow) { gastroLayer.changed(); customLayer.changed(); } lastZoom = newZoom; });
同时,给标记图层的VectorSource设置匹配视图的maxZoom,避免加载超出范围的资源:
let toiletSource = new VectorSource({ features: toiletFeatures, cacheSize: 5, maxZoom: 18 // 与视图maxZoom一致 });
4. 修正KML图层样式的无效绘制
原代码中feat.id_是错误写法,改为OpenLayers标准的getId()方法,避免无效绘制:
style: (feat) => { const zoneColors = { "zone_1": 'rgba(244, 174, 108, 0.6)', "zone_2": 'rgba(227, 148, 193, 0.6)', "zone_3": 'rgba(161, 140, 217, 0.6)', "zone_gastro": 'rgba(115, 211, 121, 0.6)', "zone_arena": 'rgba(198, 26, 12, 0.8)', "zone_industrie": 'rgba(253, 215, 64, 1)', } return new Style({ fill: new Fill({ color: zoneColors[feat.getId()] || 'rgba(0,0,0,0)', // 默认透明避免无效绘制 }) }) },
5. 优化URL参数坐标匹配逻辑
避免重复执行坐标转换,直接用Feature的Geometry进行比较:
if (urlArgs.get("lat") && urlArgs.get("lon")) { let urlPos = fromLonLat([ parseFloat(urlArgs.get("lon")), parseFloat(urlArgs.get("lat"))] ); // 直接遍历Feature对象,无需重复转换坐标 for(const feat of Object.values(allFeatures)) { if (feat.getGeometry().getType() === "Point" && feat.getGeometry().equals(new Point(urlPos))) { showModal( feat.get("name"), feat.get("popupText"), feat.get("popupLinkText"), feat.get("popupLink") ); break; } } // ... 其余代码不变 }
内容的提问来源于stack exchange,提问作者Ilian Gion Häsler
相关产品推荐
相关产品推荐

