前言
上一篇我们分析了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缓存和文件缓存属于特殊场景下才会用到。