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缓存和文件缓存属于特殊场景下才会用到。

相关推荐
太空漫步112 小时前
android社畜模拟器
android
海绵宝宝_5 小时前
【HarmonyOS NEXT】获取正式应用签名证书的签名信息
android·前端·华为·harmonyos·鸿蒙·鸿蒙应用开发
凯文的内存6 小时前
android 定制mtp连接外设的设备名称
android·media·mtp·mtpserver
天若子7 小时前
Android今日头条的屏幕适配方案
android
林的快手8 小时前
伪类选择器
android·前端·css·chrome·ajax·html·json
望佑8 小时前
Tmp detached view should be removed from RecyclerView before it can be recycled
android
xvch11 小时前
Kotlin 2.1.0 入门教程(二十四)泛型、泛型约束、绝对非空类型、下划线运算符
android·kotlin
人民的石头14 小时前
Android系统开发 给system/app传包报错
android
yujunlong391915 小时前
android,flutter 混合开发,通信,传参
android·flutter·混合开发·enginegroup
rkmhr_sef15 小时前
万字详解 MySQL MGR 高可用集群搭建
android·mysql·adb