You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.30 06:17:47