Android 11下加载OSM地图瓦片时App随机崩溃(提示文件不存在)问题求助
OSM地图瓦片加载崩溃(Android 11):原因分析与解决方案
我帮你拆解这个osmdroid在Android 11上加载本地瓦片时的随机崩溃问题,这是高版本Android适配中典型的存储访问与库兼容性问题,先看核心原因:
问题原因分析
- Android 11存储权限与Asset访问限制:Android 11(API 30)对应用私有目录和AssetManager的访问机制做了严格限制,旧版osmdroid的
MapTileAssetsProvider在处理瓦片路径时,错误地将Asset路径映射到了/data/data/packageName/code_cache/下的临时文件,而系统对这个目录的访问有额外的权限校验,导致部分瓦片文件无法被正确读取。 - 多线程加载的竞态条件:从崩溃堆栈可以看到,瓦片加载是在ThreadPoolExecutor的Worker线程中执行的,多线程并发访问
AssetManager时,会出现竞态条件——Android 11对AssetManager的线程安全控制更严格,部分线程可能无法获取到有效的Asset引用,从而抛出文件不存在的异常。 - osmdroid版本兼容性:如果你的osmdroid版本低于6.1.10,大概率没有适配Android 10+的存储变更,库内部的Asset路径解析逻辑在高版本系统上会失效。
解决方案
按优先级从高到低推荐以下解决办法:
1. 升级osmdroid到最新稳定版
这是最直接有效的方案,新版本已经修复了Android 11+的Asset访问问题。在你的build.gradle中更新依赖:
implementation 'org.osmdroid:osmdroid-android:6.1.16' // 或最新稳定版本
升级后,MapTileAssetsProvider会正确通过AssetManager访问资源,不再错误映射到code_cache目录,同时优化了多线程加载的线程安全。
2. 改用私有存储加载瓦片(替代AssetProvider)
如果暂时无法升级库,可以将assets中的瓦片文件提前复制到应用私有存储目录(比如getFilesDir()),然后使用MapTileFileProvider加载,绕开AssetManager的访问限制:
// 第一步:将assets中的瓦片复制到私有目录 private void copyTileAssetsToPrivateDir(Context context) { AssetManager assetManager = context.getAssets(); String tileRoot = "Mapnik"; try { // 遍历瓦片目录层级(这里以zoom/x/y结构为例) String[] zoomDirs = assetManager.list(tileRoot); for (String zoom : zoomDirs) { String[] xDirs = assetManager.list(tileRoot + "/" + zoom); for (String x : xDirs) { String[] yFiles = assetManager.list(tileRoot + "/" + zoom + "/" + x); for (String y : yFiles) { String assetPath = tileRoot + "/" + zoom + "/" + x + "/" + y; File destFile = new File(context.getFilesDir(), assetPath); // 创建目录 destFile.getParentFile().mkdirs(); // 复制文件 InputStream in = assetManager.open(assetPath); OutputStream out = new FileOutputStream(destFile); byte[] buffer = new byte[1024]; int length; while ((length = in.read(buffer)) > 0) { out.write(buffer, 0, length); } in.close(); out.close(); } } } } catch (IOException e) { e.printStackTrace(); } } // 第二步:使用FileProvider加载瓦片 ITileProvider tileProvider = new MapTileFileProvider( new SimpleRegisterReceiver(context), new File(context.getFilesDir(), "Mapnik") ); mapView.setTileProvider(tileProvider);
3. 优化AssetManager的线程访问
如果必须使用MapTileAssetsProvider,可以自定义TileLoader,对AssetManager的访问加锁,避免多线程竞态:
public class SafeTileLoader extends MapTileAssetsProvider.TileLoader { private static final Object ASSET_LOCK = new Object(); public SafeTileLoader(MapTileAssetsProvider pProvider) { super(pProvider); } @Override protected Drawable loadTile(final MapTileRequestState pState) { synchronized (ASSET_LOCK) { // 调用父类的加载逻辑,确保单线程访问AssetManager return super.loadTile(pState); } } } // 使用自定义Loader替换默认Loader MapTileAssetsProvider assetsProvider = new MapTileAssetsProvider(new SimpleRegisterReceiver(context)); assetsProvider.setTileLoader(new SafeTileLoader(assetsProvider)); mapView.setTileProvider(assetsProvider);
4. 临时过渡:启用旧存储模型(不推荐长期使用)
如果以上方案都无法快速实施,可以在AndroidManifest.xml中添加旧存储兼容配置,让应用在Android 11上使用旧的存储访问逻辑:
<application ... android:requestLegacyExternalStorage="true"> </application>
注意:这个配置在Android 12及以上会失效,仅作为临时过渡方案,最终还是要升级库或改用私有存储。
验证步骤
- 确认assets目录下的瓦片文件结构完全正确(比如
Mapnik/14/4294/6311.png确实存在); - 测试时清理应用缓存,避免旧的code_cache文件干扰;
- 在Android 11真机上测试,确保崩溃不再出现。
内容的提问来源于stack exchange,提问作者Johnny Antony . P
相关产品推荐
相关产品推荐

