Android高德地图离线瓦片缓存实现:从卫星图接入到自定义TileOverlay
2026/9/14 14:20:30 网站建设 项目流程

简介:面向Android开发者的高德卫星地图离线缓存示例工程,针对谷歌中国卫星地图无法访问的现状,提供基于高德地图API的替代方案。资源内含AMapTest示例项目,覆盖地图初始化、卫星图层切换、瓦片监听下载与本地缓存机制,适合需要集成离线地图功能的中初级开发者参考。压缩包共124个文件,以75个xml配置、7个java源码、10个png图片为主,并包含gradle构建脚本、jar依赖库及bin缓存文件,整体仅803KB,结构轻量。已有492人学习下载,说明其实用性得到初步验证。通过该工程可快速理解卫星瓦片图的组织方式,学会在无网络环境下读取本地瓦片并拼接显示,同时掌握分区域、分等级的缓存清理与存储策略。对于地图类应用开发而言,这是一份可直接运行的落地参考。

1. 谷歌卫星图断供之后,高德瓦片离线缓存是怎么补位的

国内不少地图应用早期直接拉谷歌中国的卫星瓦片,但这两年谷歌中国卫星图在国内已经拿不到稳定数据,很多项目被迫换数据源。高德的卫星影像覆盖相对完整,API 也是公开的,问题在于高德官方 SDK 默认只提供在线瓦片,断网后地图直接空白。这个 zip 里的 Android 工程解决的就是这件事:它把高德卫星图切成 256x256 的瓦片,在相机移动时异步下载到本地,再通过自定义 TileOverlay 把磁盘缓存的瓦片回填给地图,离线重放时不依赖网络。适合做野外巡检、冷链车辆离线调度,以及需要在隔离网络环境里保留卫星底图的场景。

2. Android 高德地图接入与卫星图层切换:从依赖到 MapType

要谈离线缓存,先得把高德地图跑起来。这个工程是标准的 Android Gradle 项目,解压后能看到gradlew.batbuild/目录下的一堆fileHashes.binexecutionHistory.binlast-build.bin之类的构建缓存,说明它在本机构建过,项目结构完整。下面按通用接入步骤展开,具体 SDK 版本以工程内build.gradle为准,不要盲目升级。

2.1 Gradle 依赖与 AndroidManifest 初始化

app/build.gradle中添加高德地图 SDK 依赖,通常是这样:

dependencies { // 地图渲染 SDK,负责 MapView 和高德底层渲染 implementation 'com.amap.api:map3d:latest.integration' // 定位 SDK,用于后续以当前位置为中心预取瓦片 implementation 'com.amap.api:location:latest.integration' }

这里使用latest.integration只是为了快速验证,因为工程里可能已经锁定了具体版本。如果你自己维护项目,不建议用这种写法,它会在每次构建时检查远程仓库里的最新版本,导致昨天跑得好好的工程今天编译不过。把版本号固定下来,才能保证离线缓存逻辑的稳定性。

高德地图的 Key 必须配置在AndroidManifest.xml<application>节点里,同时声明必要权限:

<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> <application> <meta-data android:name="com.amap.api.v2.apikey" android:value="你的高德Key"/> </application>

说明:INTERNET是地图加载的底线,没有它瓦片请求直接失败;ACCESS_FINE_LOCATION用于定位,虽然离线瓦片本身不依赖定位,但高德地图在某些定制图层上会检查定位权限,缺少时地图视图可能出现网格。另外一个很常见的坑是签名不一致:你用 debug keystore 构建,却申请了 release 签名对应的 Key,地图会一直白屏,logcat 里没有任何错误信息。把签名配置调一致后再切卫星图层,问题才会消失。

2.2 MapView 生命周期与 MapType 切换

高德的MapView继承自FrameLayout,必须把 Activity 的生命周期事件同步转发给它。布局文件写:

<com.amap.api.maps.MapView android:id="@+id/map" android:layout_width="match_parent" android:layout_height="match_parent"/>

在 Activity 里初始化:

public class MainActivity extends AppCompatActivity { private MapView mapView; private AMap aMap; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mapView = findViewById(R.id.map); // 必须调用,否则地图渲染不出来 mapView.onCreate(savedInstanceState); aMap = mapView.getMap(); // 切换为卫星影像图层 aMap.setMapType(AMap.MAP_TYPE_SATELLITE); // 关掉默认缩放按钮,腾出更多屏幕给离线瓦片 aMap.getUiSettings().setZoomControlsEnabled(false); } @Override protected void onResume() { super.onResume(); mapView.onResume(); } @Override protected void onPause() { super.onPause(); mapView.onPause(); } @Override protected void onDestroy() { super.onDestroy(); mapView.onDestroy(); } }

高德的MapType不止一种,下面的表格是常用类型:

类型常量说明适用场景
MAP_TYPE_NORMAL1标准路网图日常导航显示
MAP_TYPE_SATELLITE2卫星影像离线缓存、巡检
MAP_TYPE_NIGHT3夜间模式夜间驾驶
MAP_TYPE_NAVI4导航动效模式导航界面

可以看到MAP_TYPE_SATELLITE正是我们要的。但要注意,MapView必须放在android:layout_heightwrap_content的容器里,否则地图渲染尺寸为 0。很多人把MapView放到ScrollView中,导致高德引擎在滑动时频繁重绘,瓦片缓存线程可能被触发数十次,这个在离线缓存场景里尤其致命。

3. 卫星瓦片离线缓存的实现:TileOverlay、文件存储与四叉树目录

在线模式只解决了「能看」,离线缓存要解决「断网也能看」。高德官方 SDK 不主动导出一张张瓦片,所以工程里通常自己实现一层TileProvider:它负责从本地文件系统返回瓦片字节流,同时在地图相机移动结束后,把当前可视区域的瓦片从网络下载到本地。这样既绕开了高德内置在线瓦片缓存,又能完全掌控缓存生命周期。

3.1 自定义 TileProvider 与瓦片坐标拼接

高德的TileOverlay接受一个TileProvider接口,核心方法是getTile(int x, int y, int zoom)。三个参数对应瓦片坐标系的列、行和缩放级别,高德要求返回一张 256x256 的 PNG 数据。下面是一个从本地文件读取瓦片的实现:

@Override public Tile getTile(int x, int y, int zoom) { // 本地路径:tilecache/{zoom}/{x}/{y}.png File file = new File(cacheDir, zoom + "/" + x + "/" + y + ".png"); if (file.exists()) { byte[] data = FileUtils.read(file); // 256x256 是瓦片的标准尺寸 return new Tile(256, 256, data); } // 返回 null 表示没有瓦片,高德会显示底图网格 return null; }

逻辑说明:每次高德渲染引擎请求瓦片时,会先查本地文件。这里有一个容易被忽略的问题:即使文件存在,每次getTile都会进行一次磁盘exists()判断。在地图快速拖动时,高德可能一秒内调几十次getTile,大量磁盘 IO 会掉帧。改进方式是在内存里维护一个已缓存瓦片路径的HashSet,在下载完成后加入集合,getTile先查内存集合,再决定要不要访问磁盘。

拼接网络瓦片 URL 时要注意,高德官方瓦片地址通常带 Key 和签名参数,不能长期依赖公共接口。示例工程为了演示离线逻辑,一般会写成:

String url = "https://webrd0s.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x=" + x + "&y=" + y + "&z=" + zoom;

这串 URL 里的style=8表示卫星影像,style=6是带路网标注的矢量。公共接口的稳定性没有 SLA,如果你要用于生产,申请高德 Key 后把key参数加进去。比较稳妥的做法是在getTile中拦截 zoom 越界:当 zoom 小于 3 或大于 18 时直接返回 null,避免向一个不存在的瓦片地址发请求。

3.2 本地文件缓存与 LRU 淘汰

瓦片按zoom/x/y存放,天然形成了一棵四叉树目录。zoom 每增加一级,瓦片数量变成原来的四倍,例如 zoom 15 下某个城市就有数千张瓦片。如果不对磁盘做上限控制,缓存就会无限膨胀。工程里通常用 LRU 策略来限制总量:

public class TileCache { private final File rootDir; private final long maxSize; // 磁盘上限,单位字节 private final LinkedHashMap<File, Long> lastAccess = new LinkedHashMap<>(); public synchronized void put(File tileFile, byte[] data) { if (tileFile == null || data == null) return; tileFile.getParentFile().mkdirs(); write(tileFile, data); // 实际落盘 lastAccess.put(tileFile, System.currentTimeMillis()); // 超过上限时,从链表头部删除最久未访问的瓦片 while (getDirSize() > maxSize) { File oldest = lastAccess.keySet().iterator().next(); if (oldest == null || !oldest.delete()) break; lastAccess.remove(oldest); } } private long getDirSize() { // 遍历 rootDir 下的所有 png 文件,累加长度 } }

参数说明:maxSize建议根据场景设置。只缓存一个城市 18 级别细节,500MB 差不多;如果缓存整个省份的 10-14 级概览,可能要到 1GB。LinkedHashMap作为一个简单 LRU 结构够用,但生产环境适合用DiskLruCache,它会在每条缓存记录前写入元数据,即使 app 被 kill 也能恢复 LRU 状态。这里同步方法是为了避免多线程同时写同一个瓦片文件造成部分写入。

3.3 监听地图移动动态下载

有了存储层,还需要一个触发下载的入口。最常见的做法是注册高德的相机移动监听器,在地图移动结束后计算当前可视区域覆盖了哪些瓦片:

aMap.setOnCameraChangeListener(new AMap.OnCameraChangeListener() { @Override public void onCameraChange(CameraPosition position) {} @Override public void onCameraChangeFinish(CameraPosition position) { int zoom = (int) position.zoom; // 获取当前视野的经纬度范围 VisibleRegion region = aMap.getProjection().getVisibleRegion(); LatLngBounds bounds = region.latLngBounds; int xmin = lonToTileX(bounds.southwest.longitude, zoom); int xmax = lonToTileX(bounds.northeast.longitude, zoom); int ymin = latToTileY(bounds.northeast.latitude, zoom); int ymax = latToTileY(bounds.southwest.latitude, zoom); // 提交下载任务到线程池 exec.submit(new DownloadTask(xmin, xmax, ymin, ymax, zoom)); } });

对应的经纬度转瓦片坐标公式:

private int lonToTileX(double lon, int zoom) { return (int) Math.floor((lon + 180.0) / 360.0 * (1 << zoom)); } private int latToTileY(double lat, int zoom) { double sinLat = Math.sin(Math.toRadians(lat)); return (int) Math.floor((0.5 - Math.log((1 + sinLat) / (1 - sinLat)) / (4 * Math.PI)) * (1 << zoom)); }

逻辑说明:经纬度换算成瓦片坐标后,bounds.southwestbounds.northeast围起来的长方形就是相机可视区域。注意 Y 轴方向和经纬度是反的:维度越高,瓦片行号越小,所以ymin要用northeast.latitude算,ymaxsouthwest.latitude算。这里写反的话,下载任务会把相邻区域瓦片全部拉进来,缓存里全是不需要的图块。

DownloadTask里遍历xminxmaxyminymax,对每张瓦片先检查本地文件是否存在,不存在再 HTTP 下载。下载线程池建议用Executors.newFixedThreadPool(4),流量环境下 8 个线程会让带宽打满,地图本身还有其他请求,会互相争抢。实际体验下来,2 到 4 个线程就够。

4. 缓存策略参数调优:分级、预取与磁盘边界

卫星瓦片不是下载得越多越好。zoom 15 的一张瓦片大概覆盖 4.9 公里见方,zoom 18 只覆盖 600 米见方,如果对缩放级别和范围不加约束,离线缓存会迅速写满磁盘,也会让onCameraChangeFinish带来的下载任务不断堆积。这一章把参数和边界理清。

4.1 分级缓存与范围白名单

通常把离线缓存划分为三个层级,每个层级有不同的目标和磁盘预算:

层级zoom 范围用途参考上限
概览层3-10全国或省际范围10MB
城市层11-14城市周边路网200MB
细节点层15-18道路、建筑细节800MB

实现时用一棵有序表记录每层上限:

public class CachePolicy { public static final TreeMap<Integer, Integer> LEVEL_LIMIT = new TreeMap<>(); static { LEVEL_LIMIT.put(10, 10 * 1024 * 1024); // zoom 10 以内 LEVEL_LIMIT.put(14, 200 * 1024 * 1024); // zoom 11-14 LEVEL_LIMIT.put(18, 800 * 1024 * 1024); // zoom 15-18 } public static boolean insideChina(int x, int y, int zoom) { // 粗略边界,实际可换成准确国界多边形 return x >= 66 && x <= 148 && y >= 0 && y <= 60; } }

参数说明:TreeMap的 key 是某一层的最高 zoom,value 是这一层的磁盘预算。清点时按目录名取 zoom,累加同层所有瓦片大小。insideChina的目的是防止用户在地图边缘快速拖动时,误缓存国外瓦片——高德国外瓦片往往没有真实影像,下载回来全是灰色块,占据宝贵空间。

4.2 预取任务优先级与队列限流

地图滑动场景会频繁触发onCameraChangeFinish,如果每次任务都直接丢给线程池,用户快速滑动时队列里可能积压上千个无关瓦片。一个常用的做法是给下载任务增加优先级队列:

PriorityBlockingQueue<TileRequest> queue = new PriorityBlockingQueue<>( 512, Comparator.comparingInt(req -> Math.abs(req.x - currentX) + Math.abs(req.y - currentY)) );

currentX/currentY是当前视野中心瓦片坐标。优先级按曼哈顿距离计算,离中心越近的瓦片下载越早。同时要限制队列长度:如果队列里的任务数超过当前视野内瓦片总数,就把不在视野范围内的请求丢弃。否则在低端机上滑动地图,内存会先被任务对象占满。

4.3 延迟清理与访问时间排序

磁盘空间达到上限后,单纯在put方法里做 LRU 删除不够充分,因为瓦片有很强的时间局部性:用户从北京到上海,北京旧瓦片可能几周都不会再访问。所以除容量外还要记录访问时间,定期做一次全量清理:

public void evictIfNeeded() { if (getTotalSize() <= maxSize) return; List<CacheEntry> entries = queryAllEntries(); // 先按最后访问时间升序,再按创建时间升序 entries.sort(Comparator .comparingLong(CacheEntry::getLastAccess) .thenComparingLong(CacheEntry::getCreateTime)); for (CacheEntry entry : entries) { if (getTotalSize() <= maxSize) break; deleteEntry(entry); } }

这段代码的要点是排序键的选择:lastAccess是每次地图渲染命中瓦片时更新的时间戳,createTime是瓦片首次下载时间。两者组合起来能避免一个场景:瓦片昨天刚被下载,但用户已经不在那个区域了,没有命中记录,不会被优先删除。清理动作必须在子线程执行,因为queryAllEntries()要遍历整个缓存目录,在上千个文件时耗时可到数百毫秒,放主线程直接导致掉帧。

清理的触发点建议放在onCameraChangeFinish后延迟 2000ms。一次滑动可能拉取几十张瓦片,如果每放一张都检查容量,会频繁触发while循环和文件删除。延迟触发能把这几十次写入合并成一次清理,对闪存寿命也更友好。

5. 验证离线瓦片正确命中的三个技巧与常见 zip 坑

离线缓存最怕「看起来在空转」:断网后地图能显示地层,但显示的是高德内置底图缓存,不是我们的瓦片池。验证必须有明确信号,不能只看地图上有没有图。

5.1 用日志确认瓦片来源

TileProvidergetTile方法里加一个开关日志:

@Override public Tile getTile(int x, int y, int zoom) { // BuildConfig.DEBUG 自动跟随 buildType if (BuildConfig.DEBUG) { File f = new File(cacheDir, zoom + "/" + x + "/" + y + ".png"); Log.d("TileCache", "hit=" + f.exists() + " zxy=" + zoom + "/" + x + "/" + y); } ... }

在 logcat 里过滤TileCache标签,如果断网状态下hit=true的日志持续出现,说明缓存已经正确供图;如果全是hit=false,检查线程池有没有执行、缓存目录路径是不是被系统清理了。注意日志本身也有 IO 开销,release 包里要关闭。

5.2 adb 拉取瓦片校验尺寸

用 adb 把缓存目录拉到本地,验证每张瓦片是否为 256x256:

adb pull /sdcard/Android/data/com.your.app/files/tilecache/ ./tilecache

再运行一个简单的 Python 脚本:

from PIL import Image from pathlib import Path # 遍历瓦片目录,检查尺寸 for p in Path("./tilecache").rglob("*.png"): w, h = Image.open(p).size if (w, h) != (256, 256): print("bad tile:", p)

这个脚本能发现两类问题:一类是高德返回的默认占位图,它们可能不是 PNG;另一类是部分机型上 Bitmap 解码异常生成的 512x512 图片。高德渲染引擎会自动缩放这类非标瓦片,但显示会很模糊,而这种模糊在在线模式下感觉不出来,只有离线时才会察觉。

5.3 解压 zip 后构建报错排查

很多人在解压这个资源后直接构建,遇到error read zip archive或 Gradle 构建报 zip 相关错误。常见原因有三个:第一,gradle/wrapper/gradle-wrapper.properties指定的 Gradle 发行版 zip 损坏,删除~/.gradle/wrapper/dists/下对应缓存后重新构建;第二,整个压缩包解压时把.gradle目录也解了出来,而其中的.gradle/8.10/buildOutputCleanup等文件是机器相关的缓存,会干扰新环境,直接删掉即可;第三,Android Studio 使用的 JDK 版本与该工程不匹配,报错信息里通常会出现Unsupported class file major version,把 JDK 切到 17 或 21 重试。如果想先验证 zip 文件本身完整性,运行unzip -t 下载的包名.zip比任何修复工具都直观。

另一个值得注意的验证点是:高德引擎自己也有网络缓存,它会掩盖本地瓦片缺失的问题。在高德 SDK 支持的情况下,初始化时调用aMap.setDiskCacheEnabled(false),把 SDK 内置磁盘缓存关掉。这样测试断网时,地图显示的内容只能来自你的TileProvider,命中率数据才是真实的。如果 SDK 版本没有这个方法,可以在断网前清空高德自带的缓存目录再测试,效果是一样的。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询