Android开源框架系列-OsmDroid(二)地图加载多级缓存逻辑分析

前言

上一篇我们分析了OsmDroid地图加载的核心流程,其中提到了地图加载的多级缓存实现,今天我们重点来看一下这一块的逻辑,从下面这一行代码开始入手分析:

ini 复制代码
final MapTileRequestState state = new MapTileRequestState(pMapTileIndex, mTileProviderList, MapTileProviderArray.this);
runAsyncNextProvider(state);

源码

1.MapTileProviderArray -> List<MapTileModuleProviderBase> mTileProviderList

通过断点我们可以看到mTileProviderList中包含了5个不同类型的缓存实现,这几个provider是在MapView创建的时候设置的,MapView创建时会创建MapTileProviderBasic,在MapTileProviderBasic的构造方法中为mTileProviderList添加了这5个provider的实现类。 那么这5个类的作用分别是什么?

MapTileAssetsProvider

从Android assets目录下加载瓦片地图。它负责从指定的资产目录中加载地图瓦片资源,并将其提供给MapView进行显示。

MapTileSqlCacheProvider

从android数据库文件中加载瓦片地图。MapTileSqlCacheProvider的作用是管理地图瓦片的缓存,它通过将地图瓦片存储在本地数据库中,实现对瓦片的快速访问和重用。这样可以减少网络请求次数,提高地图加载速度,尤其是在离线或网络不稳定的情况下,能够提供更好的用户体验。

MapTileFileArchiveProvider

从文件中加载瓦片地图。用于从文件系统中的归档文件(如.zip文件)提供地图瓦片的类。通过使用MapTileFileArchiveProvider,可以将地图瓦片存储在文件中,并在需要时从文件中加载瓦片。这对于离线地图或需要快速加载地图瓦片的情况非常有用。

MapTileApproximater

不独立存在的一层缓存,他的实现依赖于前边的三个provider,借助他们来找到最近似的缓存。MapTileApproximater的作用是在地图缩放级别较高时,提供地图瓦片的近似表示。它通过对低分辨率的瓦片进行放大和处理,来模拟高分辨率瓦片的显示效果,从而在不加载实际高分辨率瓦片的情况下,提供较为平滑的地图浏览体验。

MapTileDownloader

网络缓存,前几层都属于本地缓存,当本地缓存不存在时,才会通过MapTileDownloader进行网络数据的下载。

2.runAsyncNextProvider

接下来我们从runAsyncNextProvider入口开始分析地图的多级缓存加载逻辑。

scss 复制代码
private void runAsyncNextProvider(final MapTileRequestState pState) {
    //找到下一个合适的provider,找到后直接调用loadMapTileAsync进行异步加载。
    final MapTileModuleProviderBase nextProvider = findNextAppropriateProvider(pState);
    if (nextProvider != null) {
        nextProvider.loadMapTileAsync(pState);
        return;
    }
    //走到这里说明所有的provider都找完了也没有合适的,那么就将本次请求从mWorking列表中移除
    final Integer status; 
    synchronized (mWorking) {
        status = mWorking.get(pState.getMapTile());
    }
    //如果status == WORKING_STATUS_STARTED表示所有请求都失败了,那么就回调失败的方法
    if (status != null && status == WORKING_STATUS_STARTED) {
        super.mapTileRequestFailed(pState);
    }
    remove(pState.getMapTile());
}

3.findNextAppropriateProvider

查找下一个合适的provider,这里就是从前边提到的5个provider集合中一个一个找,找到一个就尝试用他去加载瓦片地图,如果加载失败就继续找到下一个provider,直到所有的provider都尝试完成。

ini 复制代码
protected MapTileModuleProviderBase findNextAppropriateProvider(final MapTileRequestState aState) {
    MapTileModuleProviderBase provider;
    //provider不存在
    boolean providerDoesntExist = false;
    //provider不支持网络加载
    boolean providerCantGetDataConnection = false; 
    //provider提供的瓦片缩放级别不满足条件
    boolean providerCantServiceZoomlevel = false;
    do {
        //从5个provider中逐个获取provider
        provider = aState.getNextProvider();
        if (provider != null) {
            //provider是否存在,判断列表中是否有,这个条件一般都是满足的
            providerDoesntExist = !this.getProviderExists(provider);
            //所以providerCantGetDataConnection变量的含义是provider不能支持数据连接
            //如何理解呢?比如本次地图是离线加载,那么!useDataConnection()就是true,
            //对于MapTileDownloader,他的getUsesDataConnection为true,那么这个provider就不能使用
            //因为我们只要离线加载。
            providerCantGetDataConnection = !useDataConnection()
                    && provider.getUsesDataConnection();
            int zoomLevel = MapTileIndex.getZoom(aState.getMapTile());
            //如果本次请求的瓦片的缩放比大于provider支持的最大缩放比或者小于provider支持的
            //最小缩放比,则这个provider不能使用
            providerCantServiceZoomlevel = zoomLevel > provider.getMaximumZoomLevel()
                    || zoomLevel < provider.getMinimumZoomLevel();
        }
    } while ((provider != null)
            && (providerDoesntExist || providerCantGetDataConnection || providerCantServiceZoomlevel));
    //当provider能支持本次加载后,返回这个provider进行加载
    return provider;
}

4.loadMapTileAsync

回到上一步拿到provider后的逻辑中,调用provider的loadMapTileAsync方法开始异步加载。可以看到这里是将任务放入线程池执行的。

java 复制代码
public void loadMapTileAsync(final MapTileRequestState pState) {
    //如果线程池已经被关闭,就退出
    if (mExecutor.isShutdown())
        return;
    synchronized (mQueueLockObject) {
        //用一个LinkedHashMap来记录本次请求的信息
        mPending.put(pState.getMapTile(), pState);
    }
    try {
        //线程池开始执行
        mExecutor.execute(getTileLoader());
    } catch (final RejectedExecutionException e) {
        Log.w(IMapView.LOGTAG, "RejectedExecutionException", e);
    }
}

5.getTileLoader

前边我们知道,一共有5个provider来提供地图的多级缓存,他们的顺序分别是MapTileAssetsProvider、MapTileSqlCacheProvider、MapTileFileArchiveProvider、MapTileApproximater、MapTileDownloader,这个顺序正好决定了缓存加载的顺序,于是getTileLoader第一个调用的就是MapTileAssetsProvider,返回一个MapTileAssetsProvider的内部类TileLoader,它继承自MapTileModuleProviderBase.TileLoader。

实际上,5个provider中各有一个内部类TileLoader,并且都继承自MapTileModuleProviderBase.TileLoader,它实现了Runnable接口,所以可以直接被线程池执行。

typescript 复制代码
@Override
public TileLoader getTileLoader() {
    return new TileLoader(mAssets);
}

6.run

既然TileLoader也是一个Runnable,那么我们就来看一下它的run方法,线程池执行之后,run方法就是他的第一入口。首先从mPending待加载列表中拿到一个待请求的对象信息,然后开始加载加载成功则回调tileLoaded方法,加载失败则回调tileLoadedFailed方法。

java 复制代码
public abstract class TileLoader implements Runnable {
    @Override
    final public void run() {
        MapTileRequestState state;
        Drawable result = null;
        //拿到本次请求的信息MapTileRequestState,然后屌用loadTileIfReachable开始加载
        while ((state = nextTile()) != null) {
            try {
                result = null;
                result = loadTileIfReachable(state.getMapTile());
            } catch (final CantContinueException e) {
                clearQueue();
            } catch (final Throwable e) {
            }
            //成功和失败都有对应的回调方法
            if (result == null) {
                tileLoadedFailed(state);
            } else if (ExpirableBitmapDrawable.getState(result) == ExpirableBitmapDrawable.EXPIRED) {
                tileLoadedExpired(state, result);
            } else if (ExpirableBitmapDrawable.getState(result) == ExpirableBitmapDrawable.SCALED) {
                tileLoadedScaled(state, result);
            } else {
                tileLoaded(state, result);
            }
        }

        onTileLoaderShutdown();
    }
}

7.nextTile

我们看待nextTile方法是怎么拿到待请求的对象信息的。首先遍历待请求列表,判断待请求列表中的任务如果不在请求中列表mWorking中,然后将这个任务加入请求中列表mWorking,并返回这个任务对象。这里需要提一下的是mPending是一个LinkedHashMap列表,并且在初始化的时候accessOrder被设置为true,表示它的存储顺序是按访问顺序来排序的,最近访问的元素将排在迭代顺序的末尾,而nextTile在遍历待请求列表的时候,也是找到最后一个mapTileIndex对应的请求对象返回,表示瓦片地图的请求也是按照最近加入的优先加载的顺序进行的。为什么这么做呢?其实也很简单,地图在滑动过程中,最后加入的才是当前屏幕中的,所以应该优先被加载。

kotlin 复制代码
protected MapTileRequestState nextTile() {
    synchronized (mQueueLockObject) {
        Long result = null;
        //遍历待请求列表,判断待请求列表中的任务如果不在请求中列表mWorking中,就将这个任务
        //加入请求中列表mWorking,并返回这个任务对象
        Iterator<Long> iterator = mPending.keySet().iterator();
        //TODO this iterates the whole list, make this faster...
        //官方注释,说这里每次要遍历整个列表,只为找到最后一个加入的请求,很慢,是优化空间的
        while (iterator.hasNext()) {
            final Long mapTileIndex = iterator.next();
            if (!mWorking.containsKey(mapTileIndex)) {
                result = mapTileIndex;
            }
        }
        if (result != null) {
            mWorking.put(result, mPending.get(result));
        }
        return (result != null ? mPending.get(result) : null);
    }
}

8.loadTileIfReachable

拿到请求对象信息后,开始尝试加载,这里又调用了loadTile这个方法,也就是从基类转移到子类中了,那么第一个loadTile的就是MapTileAssetsProvider。

java 复制代码
public Drawable loadTileIfReachable(final long pMapTileIndex)
        throws CantContinueException {
    if (!isTileReachable(pMapTileIndex)) {
        return null;
    }
    return loadTile(pMapTileIndex);
}

9.MapTileAssetsProvider -> loadTile

这里我们就认为它一定返回null吧,毕竟我们并没有在assets文件夹中放离线地图,也就是说第一层缓存assets缓存请求失败。

java 复制代码
@Override
public Drawable loadTile(final long pMapTileIndex) throws CantContinueException {
    ITileSource tileSource = mTileSource.get();
    if (tileSource == null) {
        return null;
    }
    try {
        InputStream is = mAssets.open(tileSource.getTileRelativeFilenameString(pMapTileIndex));
        final Drawable drawable = tileSource.getDrawable(is);
        return drawable;
    } catch (IOException e) {
    } catch (final LowMemoryException e) {
        throw new CantContinueException(e);
    }
    return null;
}

10.tileLoadedFailed

loadTile返回null,也就是说loadTileIfReachable也返回了null,我们再回到第6步中,loadTileIfReachable返回null,接下来就执行了tileLoadedFailed方法。

scss 复制代码
protected void tileLoadedFailed(final MapTileRequestState pState) {
    //从待加载列表和加载中列表都移除掉
    removeTileFromQueues(pState.getMapTile());
    //回调失败
    pState.getCallback().mapTileRequestFailed(pState);
}

11.MapTileProviderArray -> mapTileRequestFailed

这个provider请求失败,就拿下一个provider执行。

java 复制代码
@Override
public void mapTileRequestFailed(final MapTileRequestState aState) {
    runAsyncNextProvider(aState);
}

12.runAsyncNextProvider

这里就又回到了第2步中的runAsyncNextProvider方法了,本次请findNextAppropriateProvider方法会找到第2个provider - MapTileSqlCacheProvider,于是又进入它的loadTile方法。

13.MapTileSqlCacheProvider -> loadTile

尝试从数据库中读区瓦片地图,但是由于是第一次加载,还没有缓存,所有也返回了null。

java 复制代码
    @Override
    public Drawable loadTile(final long pMapTileIndex) throws CantContinueException {
        ITileSource tileSource = mTileSource.get();
        if (tileSource == null) {
            return null;
        }
        if (mWriter != null) {
            try {
                final Drawable result = mWriter.loadTile(tileSource, pMapTileIndex);
                return result;
            } catch (final BitmapTileSourceBase.LowMemoryException e) {
                throw new CantContinueException(e);
            } catch (final Throwable e) {
                return null;
            }
        } else {
        }
        return null;
    }
}

14.MapTileFileArchiveProvider -> loadTile

于是又回调一次tileLoadedFailed,然后继续执行runAsyncNextProvider,找到了下一个provider - MapTileFileArchiveProvider。MapTileFileArchiveProvider一般用于离线地图加载,我们认为它还是返回了null。

ini 复制代码
@Override
public Drawable loadTile(final long pMapTileIndex) {
    Drawable returnValue = null;
    ITileSource tileSource = mTileSource.get();
    if (tileSource == null) {
        return null;
    }
    InputStream inputStream = null;
    try {
        inputStream = getInputStream(pMapTileIndex, tileSource);
        if (inputStream != null) {
            final Drawable drawable = tileSource.getDrawable(inputStream);
            returnValue = drawable;
        }
    } catch (final Throwable e) {
    } finally {
        if (inputStream != null) {
            StreamUtils.closeStream(inputStream);
        }
    }
    return returnValue;
}

15.MapTileApproximater -> loadTile

于是又回调一次tileLoadedFailed,然后继续执行runAsyncNextProvider,找到了下一个provider - MapTileApproximater。MapTileApproximater依赖于前边三个provider,前边3个provider都不存在缓存,所以这里也是返回了null。

java 复制代码
@Override
public Drawable loadTile(final long pMapTileIndex) {
    final Bitmap bitmap = approximateTileFromLowerZoom(pMapTileIndex);
    if (bitmap != null) {
        final BitmapDrawable drawable = new BitmapDrawable(bitmap);
        ExpirableBitmapDrawable.setState(drawable, ExpirableBitmapDrawable.SCALED);
        return drawable;
    }
    return null;
}

16.MapTileApproximater -> loadTile

前边的几层缓存都没有拿到瓦片地图,那么只能去网络端请求了,所以进入了最后一个provider - MapTileDownloader。

java 复制代码
@Override
public Drawable loadTile(final long pMapTileIndex) throws CantContinueException {
    OnlineTileSourceBase tileSource = mTileSource.get();
    if (tileSource == null) {
        return null;
    }
    //无网直接返回null
    if (mNetworkAvailablityCheck != null
            && !mNetworkAvailablityCheck.getNetworkAvailable()) {
        return null;
    }
    //拼接好请求地址
    final String tileURLString = tileSource.getTileURLString(pMapTileIndex);
    if (TextUtils.isEmpty(tileURLString)) {
        return null;  
    }
    //管理失败请求的重试时间间隔,如果等待时间不足则返回空继续等待
    if (mUrlBackoff.shouldWait(tileURLString)) {
        return null;
    }
    final Drawable result = downloadTile(pMapTileIndex, 0, tileURLString);
    if (result == null) {
        mUrlBackoff.next(tileURLString);
    } else {
        mUrlBackoff.remove(tileURLString);
    }
    return result;

}

17.downloadTile

java 复制代码
protected Drawable downloadTile(final long pMapTileIndex, final int redirectCount, final String targetUrl) throws CantContinueException {
    final OnlineTileSourceBase tileSource = mTileSource.get();
    if (tileSource == null) {
        return null;
    }
    try {
        //控制并发请求的线程数量,通过一个Semaphore类控制,默认似乎是null,也就是没有限制
        tileSource.acquire();
    } catch (final InterruptedException e) {
        return null;
    }
    try {
        //使用下载器开始下载瓦片地图,这里是纯粹的网络请求
        return mTileDownloader.downloadTile(pMapTileIndex, redirectCount, targetUrl, mFilesystemCache, tileSource);
    } finally {
        tileSource.release();
    }
}

18.downloadTile

走到这里最终瓦片地图就从网络端下载下来了,封装程drawable对象,往上执行就回到了tileLoaded回调方法,此时再唤起主线程进行ui刷新,图片就展示在屏幕上了。

ini 复制代码
public Drawable downloadTile(final long pMapTileIndex, final int redirectCount, final String targetUrl,
                             final IFilesystemCache pFilesystemCache, final OnlineTileSourceBase pTileSource) throws CantContinueException {
    try {
        final String tileURLString = targetUrl;
        c.connect();
        ...
        String mime = c.getHeaderField("Content-Type");
        in = c.getInputStream();
        ...
        dataStream = new ByteArrayOutputStream();
        ...
        final byte[] data = dataStream.toByteArray();
        byteStream = new ByteArrayInputStream(data);
        //拿到网络数据后,将其写入缓存,这个缓存是可以指定的,由外界传入,若不指定,则默认是存在
        //数据库文件中,即SqlTileWriter
        if (pFilesystemCache != null) {
            pFilesystemCache.saveFile(pTileSource, pMapTileIndex, byteStream, expirationTime);
            byteStream.reset();
        }
        return pTileSource.getDrawable(byteStream);
    }
    return null;
}

总结

OsmDroid地图加载存在多级缓存,分别包括内存缓存(这里未涉及到)、assets文件缓存、sql数据库缓存、本地文件缓存和网络缓存。其中默认状态下真正有效的缓存也就三层,内存缓存、数据库缓存和网络缓存,assets缓存和文件缓存属于特殊场景下才会用到。

相关推荐
众拾达人3 小时前
Android自动化测试实战 Java篇 主流工具 框架 脚本
android·java·开发语言
吃着火锅x唱着歌4 小时前
PHP7内核剖析 学习笔记 第四章 内存管理(1)
android·笔记·学习
_Shirley5 小时前
鸿蒙设置app更新跳转华为市场
android·华为·kotlin·harmonyos·鸿蒙
hedalei7 小时前
RK3576 Android14编译OTA包提示java.lang.UnsupportedClassVersionError问题
android·android14·rk3576
锋风Fengfeng7 小时前
安卓多渠道apk配置不同签名
android
枫_feng7 小时前
AOSP开发环境配置
android·安卓
叶羽西8 小时前
Android Studio打开一个外部的Android app程序
android·ide·android studio
qq_171538859 小时前
利用Spring Cloud Gateway Predicate优化微服务路由策略
android·javascript·微服务
Vincent(朱志强)10 小时前
设计模式详解(十二):单例模式——Singleton
android·单例模式·设计模式
mmsx11 小时前
android 登录界面编写
android·登录界面