1. 移动端地图开发的现实选择
做过移动GIS开发的人都有一个共识:底图这件事,看起来最简单,实际上最容易翻车。ArcGIS for Android 本身提供了强大的地图渲染和空间分析能力,但它的默认底图服务在国内的网络环境下加载速度往往不尽如人意,尤其是在户外作业、车载导航、巡检打卡这类对底图响应速度要求较高的场景里,底图加载慢半拍,用户体验就会大打折扣。
天地图作为国内权威的地理信息公共服务平台,提供了矢量底图、影像底图、地形晕渲等多种瓦片服务,覆盖范围广、更新频率稳定,而且对国内网络环境非常友好。把天地图作为 ArcGIS for Android 的底图图层加载进去,既能利用 ArcGIS 强大的空间分析能力,又能保证底图的加载速度和显示效果,这是目前很多国内移动GIS项目的常规做法。
这篇文章面向的是有一定 Android 开发基础、正在使用或准备使用 ArcGIS for Android SDK 做地图应用的开发者。不管你是刚接触 ArcGIS 移动端开发的新手,还是已经做过几个项目但一直用默认底图的老手,下面这些内容应该都能帮你少走一些弯路。我会从坐标系选择、瓦片服务原理、图层类选型、具体编码实现、常见报错排查几个维度,把"加载天地图"这件事彻底讲透。
2. 坐标系与瓦片服务的前置知识
2.1 为什么坐标系选错了一切都白搭
ArcGIS for Android 默认使用的空间参考是 WGS84 地理坐标系(WKID: 4326)或者 Web 墨卡托投影坐标系(WKID: 3857)。而天地图提供的瓦片服务,绝大多数情况下使用的是EPSG:3857(Web 墨卡托)投影,也就是我们常说的"球面墨卡托"。如果你在初始化 MapView 的时候用了默认的 WGS84 地理坐标系,然后直接叠加天地图的瓦片图层,结果就是瓦片位置偏移、缩放级别对不上、地图看起来"飘"了。
这个问题的本质在于:瓦片服务是按照特定的投影方式和瓦片切分规则预先切好的图片,每一张瓦片对应一个固定的地理范围。如果你的地图控件使用的投影方式和瓦片服务的投影方式不一致,ArcGIS 在渲染时就需要做投影转换,而瓦片图层(WebTiledLayer)本身并不支持动态投影转换,所以就会出现错位。
实操心得:在加载天地图之前,第一件事就是确认你的 MapView 初始化时使用的 SpatialReference 是否为 Web 墨卡托(3857)。如果不是,要么改初始化参数,要么换用支持动态投影的图层类型。
2.2 天地图瓦片服务的 URL 结构
天地图的瓦片服务遵循标准的 XYZ 瓦片地图服务规范,URL 模板大致是这样的:
http://t{0-7}.tianditu.gov.cn/vec_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={level}&TILEROW={row}&TILECOL={col}&tk=你的密钥这里面有几个关键参数需要理解:
- t{0-7}:天地图提供了8个子域名用于负载均衡,ArcGIS 的 WebTiledLayer 支持用占位符的方式自动轮询子域名,这个后面会详细讲。
- vec_w:表示矢量底图的 Web 墨卡托版本。
vec是矢量底图,img是影像底图,ter是地形晕渲。加_w后缀表示 Web 墨卡托投影版本,不加后缀的是经纬度投影版本(EPSG:4326)。 - TILEMATRIX={level}:缩放级别,从1到18(部分服务到20)。
- TILEROW={row}和TILECOL={col}:瓦片的行号和列号。
- tk:天地图的访问密钥,需要去天地图官网注册申请。
ArcGIS for Android 的WebTiledLayer类支持用{level}、{row}、{col}这样的占位符来构建 URL 模板,SDK 会在请求瓦片时自动替换这些占位符。同时它还支持{subDomain}占位符来实现子域名轮询,不过天地图的子域名格式是t0到t7,需要稍微处理一下。
2.3 缩放级别与瓦片矩阵的对应关系
天地图的瓦片矩阵集(TileMatrixSet)标识为w,对应的缩放级别从1开始。这里有一个容易踩的坑:ArcGIS 的WebTiledLayer默认的缩放级别起始值是0,而天地图是从1开始的。如果你不做处理,直接加载,可能会发现地图在最小缩放级别时显示空白或者瓦片错位。
解决办法有两种:一种是在构建 URL 模板时对级别做偏移处理,另一种是通过setLevelOffset方法来设置偏移量。我一般推荐用后者,因为更直观,代码也更干净。
3. ArcGIS for Android 加载天地图的完整方案
3.1 工具选型:WebTiledLayer 还是 WMTSLayer
ArcGIS for Android SDK 提供了两种加载瓦片服务的方式:
| 对比项 | WebTiledLayer | WMTSLayer |
|---|---|---|
| 适用场景 | 标准 XYZ 瓦片服务 | 符合 OGC WMTS 规范的服务 |
| URL 模板 | 支持占位符替换 | 需要 GetCapabilities 文档 |
| 子域名支持 | 内置{subDomain}占位符 | 需要手动配置 |
| 坐标系要求 | 必须与地图一致 | 支持动态投影 |
| 配置复杂度 | 低 | 中等 |
| 灵活性 | 高,可自定义 URL | 受限于服务元数据 |
对于天地图这种标准的 XYZ 瓦片服务,WebTiledLayer 是更轻量、更直接的选择。你不需要去解析 GetCapabilities 文档,只需要拼好 URL 模板,设置好子域名和级别偏移,就能直接加载。WMTSLayer 虽然更"规范",但在实际项目里,配置起来反而更繁琐,而且对天地图这种已经非常明确的瓦片服务来说,有点杀鸡用牛刀的意思。
当然,如果你需要同时加载多个图层并做图层叠加控制,或者需要动态投影支持,那 WMTSLayer 可能更合适。但对于"加载天地图底图"这个单一需求,WebTiledLayer 足够了。
3.2 密钥申请与环境准备
在写代码之前,你需要先去天地图官网注册一个开发者账号,然后创建一个应用,获取一个访问密钥(tk)。这个密钥是必须的,没有密钥的话,瓦片请求会返回 403 错误。
申请流程大致是:注册账号 -> 实名认证 -> 创建应用 -> 获取密钥。整个过程一般一两个工作日就能搞定。密钥拿到之后,建议不要硬编码在代码里,而是放在local.properties或者BuildConfig里,避免泄露。
另外,确保你的 Android 项目已经正确集成了 ArcGIS for Android SDK。在build.gradle中添加依赖:
dependencies { implementation 'com.esri.arcgisruntime:arcgis-android:100.15.0' }版本号根据你的项目实际情况选择,建议用 100.x 以上的版本,API 更稳定,文档也更完善。
3.3 核心代码实现:一步步构建天地图图层
下面是一个完整的实现方案,我会把每一步拆开讲清楚。
第一步:初始化 MapView 并设置空间参考
// 在 Activity 或 Fragment 中 MapView mapView = findViewById(R.id.mapView); // 创建地图,使用 Web 墨卡托投影 ArcGISMap map = new ArcGISMap(SpatialReference.create(3857)); mapView.setMap(map); // 设置初始视点,比如定位到某个城市 mapView.setViewpoint(new Viewpoint(39.9042, 116.4074, 10000));这里的关键是SpatialReference.create(3857),确保地图使用 Web 墨卡托投影。如果你用的是ArcGISMap()无参构造函数,默认可能是 WGS84,那就需要额外处理。
第二步:构建天地图矢量底图的 URL 模板
// 天地图矢量底图 URL 模板 String vecUrl = "http://t{subDomain}.tianditu.gov.cn/vec_w/wmts?" + "SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0" + "&LAYER=vec&STYLE=default&TILEMATRIXSET=w" + "&FORMAT=tiles" + "&TILEMATRIX={level}&TILEROW={row}&TILECOL={col}" + "&tk=你的密钥";注意这里的{subDomain}占位符,ArcGIS 的 WebTiledLayer 会自动用你设置的子域名列表来替换它。
第三步:创建 WebTiledLayer 并设置子域名和级别偏移
// 子域名列表,对应天地图的 t0 到 t7 List<String> subDomains = Arrays.asList("0", "1", "2", "3", "4", "5", "6", "7"); // 创建 WebTiledLayer WebTiledLayer vecLayer = new WebTiledLayer(vecUrl, subDomains); // 设置级别偏移,天地图从1开始,ArcGIS 从0开始 vecLayer.setLevelOffset(1); // 设置瓦片信息,包括瓦片尺寸和DPI vecLayer.setTileInfo(new TileInfo(3857, Arrays.asList( new LevelOfDetail(1, 295829355.455, 295829355.455), new LevelOfDetail(2, 147914677.727, 147914677.727), // ... 更多级别 )));这里有一个细节:setLevelOffset(1)是关键,它告诉 SDK 在请求瓦片时,把 ArcGIS 的级别加1再传给天地图。比如 ArcGIS 的级别0对应天地图的级别1,级别1对应级别2,以此类推。
关于TileInfo的设置,如果你不想手动列出所有级别,也可以不设置,WebTiledLayer 会根据 URL 模板中的{level}自动推断。但手动设置可以更精确地控制瓦片的分辨率和比例尺,避免在某些缩放级别下出现瓦片模糊或错位。
第四步:添加注记图层
天地图的矢量底图只包含道路、边界等基础要素,不含文字注记。如果你需要显示地名、道路名称等注记信息,需要额外加载注记图层:
// 天地图矢量注记 URL 模板 String cvaUrl = "http://t{subDomain}.tianditu.gov.cn/cva_w/wmts?" + "SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0" + "&LAYER=cva&STYLE=default&TILEMATRIXSET=w" + "&FORMAT=tiles" + "&TILEMATRIX={level}&TILEROW={row}&TILECOL={col}" + "&tk=你的密钥"; WebTiledLayer cvaLayer = new WebTiledLayer(cvaUrl, subDomains); cvaLayer.setLevelOffset(1); // 将注记图层叠加在底图之上 map.getOperationalLayers().add(vecLayer); map.getOperationalLayers().add(cvaLayer);注意图层的添加顺序:先添加底图,再添加注记图层,这样注记才会显示在底图上面。如果你还需要影像底图,把vec_w换成img_w,注记图层换成cia_w即可。
第五步:加载影像底图(可选)
// 影像底图 String imgUrl = "http://t{subDomain}.tianditu.gov.cn/img_w/wmts?" + "SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0" + "&LAYER=img&STYLE=default&TILEMATRIXSET=w" + "&FORMAT=tiles" + "&TILEMATRIX={level}&TILEROW={row}&TILECOL={col}" + "&tk=你的密钥"; WebTiledLayer imgLayer = new WebTiledLayer(imgUrl, subDomains); imgLayer.setLevelOffset(1); // 影像注记 String ciaUrl = "http://t{subDomain}.tianditu.gov.cn/cia_w/wmts?" + "SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0" + "&LAYER=cia&STYLE=default&TILEMATRIXSET=w" + "&FORMAT=tiles" + "&TILEMATRIX={level}&TILEROW={row}&TILECOL={col}" + "&tk=你的密钥"; WebTiledLayer ciaLayer = new WebTiledLayer(ciaUrl, subDomains); ciaLayer.setLevelOffset(1);影像底图适合展示卫星照片、航拍影像等场景,注记图层则负责标注地名和重要地物。
3.4 图层切换与性能优化
在实际项目中,用户往往需要在矢量底图和影像底图之间切换。最简单的做法是维护两个图层列表,切换时清空OperationalLayers再重新添加:
public void switchBaseMap(boolean isVector) { map.getOperationalLayers().clear(); if (isVector) { map.getOperationalLayers().add(vecLayer); map.getOperationalLayers().add(cvaLayer); } else { map.getOperationalLayers().add(imgLayer); map.getOperationalLayers().add(ciaLayer); } }性能方面,有几个点值得注意:
- 子域名轮询:天地图提供了8个子域名,合理利用可以显著提升瓦片加载速度。WebTiledLayer 会自动轮询你传入的子域名列表,所以一定要把8个都用上。
- 瓦片缓存:ArcGIS for Android 默认会缓存已加载的瓦片,但你可以通过
mapView.setMapCacheSize()来调整缓存大小,默认是100MB,对于长时间使用的应用可以适当调大。 - 按需加载:如果应用只需要在特定区域使用,可以考虑只加载该区域的瓦片,减少不必要的网络请求。
4. 常见问题与排查技巧实录
4.1 瓦片显示空白或 403 错误
这是最常见的问题,通常有以下几个原因:
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| 瓦片全白 | 密钥无效或过期 | 检查 tk 参数是否正确,去天地图官网确认密钥状态 |
| 部分瓦片空白 | 子域名配置错误 | 确认子域名列表是 "0" 到 "7",不要带 "t" 前缀 |
| 403 Forbidden | 密钥未实名认证或配额用完 | 完成实名认证,检查每日请求量是否超限 |
| 瓦片错位 | 坐标系不匹配 | 确认 MapView 使用 3857 投影 |
| 缩放级别不对 | 级别偏移未设置 | 设置setLevelOffset(1) |
避坑技巧:天地图的密钥有每日请求配额限制,免费版一般是每天10万次瓦片请求。在开发阶段,建议在代码里加一个简单的请求计数器,方便监控用量。另外,密钥不要提交到公开的代码仓库,用
local.properties管理最稳妥。
4.2 地图缩放时瓦片闪烁或加载慢
这个问题通常和瓦片缓存策略有关。ArcGIS for Android 在缩放时会先显示低级别的瓦片,然后逐步加载高级别的瓦片。如果网络状况不好,就会出现闪烁。
优化方案:
// 增大瓦片缓存 mapView.setMapCacheSize(200 * 1024 * 1024); // 200MB // 设置瓦片请求的超时时间 // 在 WebTiledLayer 创建后 vecLayer.setRequestTimeout(30); // 单位秒另外,确保你的 URL 模板中使用了http而不是https。虽然天地图也支持 https,但在某些 Android 版本上,https 的瓦片请求可能会因为证书问题导致加载失败。如果必须用 https,记得在AndroidManifest.xml中配置网络安全策略。
4.3 注记图层不显示或显示不全
注记图层不显示,最常见的原因是图层顺序问题。ArcGIS 的OperationalLayers是按添加顺序渲染的,后添加的图层会覆盖在先添加的图层上面。所以注记图层一定要在底图图层之后添加。
另一个可能的原因是注记图层的 URL 模板写错了。天地图的矢量注记服务是cva_w,影像注记是cia_w,不要搞混。另外,注记图层的LAYER参数也要对应修改,cva对应矢量注记,cia对应影像注记。
4.4 在低版本 Android 上的兼容性问题
ArcGIS for Android SDK 100.x 版本要求最低 Android API 级别是 21(Android 5.0)。如果你的项目需要支持更低的版本,那就只能用 10.x 版本的 SDK,但那个版本的 API 和 100.x 差异很大,很多类名和方法都不一样。
另外,在 Android 9.0(API 28)及以上版本,默认禁止明文 HTTP 请求。如果你的瓦片 URL 用的是http,需要在AndroidManifest.xml的<application>标签中添加:
<application android:usesCleartextTraffic="true" ...>或者在res/xml/network_security_config.xml中配置白名单。不过从安全角度考虑,建议尽量使用 https。
4.5 内存泄漏与生命周期管理
MapView 持有大量的图形资源和网络连接,如果在 Activity 销毁时没有正确释放,很容易造成内存泄漏。正确的做法是在onDestroy中调用mapView.dispose():
@Override protected void onDestroy() { if (mapView != null) { mapView.dispose(); } super.onDestroy(); }另外,如果你在 Fragment 中使用 MapView,还需要处理onPause、onResume、onSaveInstanceState等生命周期方法,确保地图状态正确保存和恢复。
5. 进阶技巧与实战经验
5.1 自定义瓦片请求头与鉴权
有些项目可能需要对瓦片请求做额外的鉴权,比如在请求头中携带 token。ArcGIS for Android 的 WebTiledLayer 本身不直接支持自定义请求头,但你可以通过自定义TileCache或者拦截网络请求来实现。
一个比较实用的做法是继承WebTiledLayer并重写getTileUrl方法,在 URL 中拼接额外的鉴权参数:
public class CustomTiledLayer extends WebTiledLayer { private String token; public CustomTiledLayer(String urlTemplate, List<String> subDomains, String token) { super(urlTemplate, subDomains); this.token = token; } @Override public String getTileUrl(int level, int row, int col) { String url = super.getTileUrl(level, row, col); return url + "&token=" + token; } }这种方式适合在 URL 中传递鉴权信息的场景。如果需要在请求头中传递,那就需要更底层的网络拦截,实现起来会复杂一些。
5.2 离线瓦片与混合加载策略
在一些网络条件不稳定的场景(比如野外作业、地下停车场),可以考虑离线瓦片方案。基本思路是:提前把目标区域的瓦片下载到本地,然后用一个本地的瓦片服务或者自定义的 TileCache 来提供瓦片。
ArcGIS for Android 提供了TileCache类,可以用于管理本地瓦片。你可以把下载好的天地图瓦片按照level/row/col的目录结构存储,然后创建一个自定义的TiledLayer来读取本地瓦片。当本地没有瓦片时,再回退到在线请求。
这种混合加载策略在实地测绘、巡检等场景中非常实用,可以显著提升地图的响应速度和可用性。
5.3 多图层叠加与透明度控制
在实际项目中,往往需要在地图底图之上叠加多个业务图层,比如 GPS 轨迹、POI 标记、区域边界等。这些图层可以通过GraphicsOverlay来添加,和天地图底图互不干扰。
如果需要调整天地图底图的透明度(比如在影像底图上叠加矢量底图做对比),可以通过setOpacity方法:
vecLayer.setOpacity(0.7f); // 设置70%不透明度这个技巧在需要同时展示影像和矢量数据的场景中很有用,比如对比不同时期的地貌变化。
5.4 天地图服务类型速查
天地图提供了多种瓦片服务,常用的有以下几种:
| 服务类型 | URL 标识 | 说明 |
|---|---|---|
| 矢量底图 | vec_w | 道路、边界等基础地理要素 |
| 矢量注记 | cva_w | 地名、道路名等文字标注 |
| 影像底图 | img_w | 卫星影像、航拍照片 |
| 影像注记 | cia_w | 影像上的地名标注 |
| 地形晕渲 | ter_w | 地形起伏的晕渲效果 |
| 地形注记 | cta_w | 地形相关的地名标注 |
这些服务可以自由组合,比如"矢量底图+矢量注记"适合日常导航,"影像底图+影像注记"适合查看真实地貌,"地形晕渲+地形注记"适合户外登山场景。
5.5 调试技巧:如何快速定位瓦片问题
当瓦片加载出现问题时,最快的排查方法是直接在浏览器中打开瓦片 URL,看看能不能正常返回图片。比如:
http://t0.tianditu.gov.cn/vec_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX=1&TILEROW=0&TILECOL=0&tk=你的密钥如果浏览器能正常显示瓦片图片,说明密钥和服务都没问题,问题出在代码配置上。如果浏览器也返回错误,那就检查密钥和网络环境。
另外,ArcGIS for Android 提供了日志输出功能,可以在 Logcat 中查看瓦片请求的详细日志。把日志级别调到 DEBUG,就能看到每个瓦片请求的 URL 和返回状态,非常方便定位问题。
// 开启调试日志 ArcGISRuntimeEnvironment.setLogLevel(LogLevel.DEBUG);6. 一些踩过的坑和最终建议
我在多个移动GIS项目中使用天地图作为底图,踩过的坑不算少,这里挑几个最有代表性的说一下。
第一个坑是坐标系不一致导致的瓦片错位。早期做项目时,我直接用默认的ArcGISMap()初始化地图,然后叠加天地图图层,结果地图上的标记点全部偏移了几百米。排查了半天才发现是投影问题。后来养成了习惯,初始化地图时第一件事就是明确指定SpatialReference.create(3857)。
第二个坑是级别偏移忘记设置。天地图的瓦片级别从1开始,ArcGIS 从0开始,如果不设置setLevelOffset(1),地图在最小级别时会显示空白,用户以为地图加载失败了。这个问题的隐蔽性在于,它只在特定缩放级别下出现,很容易被忽略。
第三个坑是密钥泄露。有一次把密钥硬编码在代码里,提交到了公开仓库,结果密钥被滥用,配额很快就用完了。后来改用local.properties管理密钥,并且在.gitignore中排除该文件,问题才解决。
第四个坑是注记图层顺序错误。注记图层一定要在底图图层之后添加,否则会被底图覆盖。这个问题的表现是地图上只有底图没有文字,看起来像是注记服务挂了,实际上只是图层顺序问题。
最后分享一个实用建议:在正式项目中使用天地图之前,建议先写一个最小化的 Demo,把矢量底图、影像底图、注记图层的加载都跑通,确认密钥、坐标系、级别偏移、图层顺序都没问题,再集成到主项目中。这样可以避免在复杂的业务代码中排查底图问题,效率会高很多。
另外,天地图的服务偶尔会有维护或调整,建议关注其官方公告,及时了解服务变更。如果项目对底图的稳定性要求极高,可以考虑同时配置多个底图源作为备份,当天地图不可用时自动切换到备用底图。