Android开源框架系列-OsmDroid(一)地图加载核心流程分析

833 阅读6分钟

前言

Osmdroid是Android的MapView (v1 API)类的一个(几乎)完全/免费的替代品。它还包括一个模块化的瓦片提供程序系统,支持许多在线和离线的瓷砖源,并支持内置的覆盖,用于绘制图标、跟踪位置和绘制形状。

本篇主要用来分析Osmdroid加载地图的核心流程,因为只分析核心流程,所以很多细节代码会被省略掉,请注意甄别。

使用

1.build.gradle

implementation 'org.osmdroid:osmdroid-android:6.1.18'

2.布局文件activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <org.osmdroid.views.MapView
        android:id="@+id/map"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>

3.activity

MapView mMapView = findViewById(R.id.map);
mMapView.setTileSource(GoogleTileSource.AutoNaviVector);
IMapController mapController = mMapView.getController();
mapController.setZoom(18.0);
GeoPoint startPoint = new GeoPoint(31.1153958, 121.2994426);
mapController.setCenter(startPoint);
mMapView.getZoomController().setVisibility(CustomZoomButtonsController.Visibility.NEVER);
mMapView.setMultiTouchControls(true);

4.地图source

    //高德地图
    public static final OnlineTileSourceBase AutoNaviVector = new XYTileSource("AutoNavi-Vector",
            0, 20, 256, ".png", new String[]{
            "https://wprd01.is.autonavi.com/appmaptile?",
            "https://wprd02.is.autonavi.com/appmaptile?",
            "https://wprd03.is.autonavi.com/appmaptile?",
            "https://wprd04.is.autonavi.com/appmaptile?",

    }) {
        @Override
        public String getTileURLString(long pMapTileIndex) {
            return getBaseUrl() + "x=" + MapTileIndex.getX(pMapTileIndex) + "&y=" + MapTileIndex.getY(pMapTileIndex) + "&z="
                    + MapTileIndex.getZoom(pMapTileIndex) + "&lang=zh_cn&size=1&scl=1&style=7&ltype=7";
        }
    };

5.效果图

Screenshot_20240629-144642.png

源码分析

1.MapView -> dispatchDraw

绘制所有的overlay,其中getOverlayManager()得到的是DefaultOverlayManager对象。

@Override
protected void dispatchDraw(final Canvas c) {
     /* Draw all Overlays. */ 绘制所有overlay
     this.getOverlayManager().onDraw(c, this);
}

2.DefaultOverlayManager -> onDraw -> onDrawHelper

@Override
public void onDraw(final Canvas c, final MapView pMapView) {
    onDrawHelper(c, pMapView, pMapView.getProjection());
}

mTilesOverlay是TilesOverlay的一个对象

private void onDrawHelper(final Canvas c, final MapView pMapView, final Projection pProjection) {
    //always pass false, the shadow parameter will be removed in a later version of osmdroid, this change should result in the on draw being called twice
    if (mTilesOverlay != null && mTilesOverlay.isEnabled()) {
        if (pMapView != null) {
            mTilesOverlay.draw(c, pMapView, false);
        } else {
            mTilesOverlay.draw(c, pProjection);
        }
    }
}

3.TilesOverlay -> draw

先设置窗口信息,然后开始绘制。

@Override
public void draw(Canvas c, Projection pProjection) {
    //这里是在设置可视窗口  
    if (!setViewPort(c, pProjection)) {
        return;
    }
    // Draw the tiles! 开始绘制
    drawTiles(c, getProjection(), getProjection().getZoomLevel(), mViewPort);
}

TilesOverlay -> setViewPort

getMercatorViewPort 方法用于获取墨卡托投影下的视口信息,视口可以理解为地图在屏幕上显示的可见区域。

protected boolean setViewPort(final Canvas pCanvas, final Projection pProjection) {
    setProjection(pProjection);
    getProjection().getMercatorViewPort(mViewPort);
    return true;
}

4.TilesOverlay -> drawTiles

其中mTileLooper指的是OverlayTileLooper的对象。方法命名为loop,可见其实循环绘制的。

public void drawTiles(final Canvas c, final Projection projection, final double zoomLevel, final RectL viewPort) {
    mProjection = projection;
    mTileLooper.loop(zoomLevel, viewPort, c);
}

5.OverlayTileLooper -> loop

public void loop(final double pZoomLevel, final RectL pViewPort, final Canvas pCanvas) {
    mCanvas = pCanvas;
    loop(pZoomLevel, pViewPort);
}

6.TileLooper -> loop(xxx,xxx)

mTiles是一个rect,表示屏幕上的范围,这里通过双层for循环进行加载,可以认为是将屏幕进行切割,屏幕被划分为多个小块,如3x3,4x4这样的结构,然后通过分块请求将每个独立区域的地图图片加载到,再拼接到屏幕上。

protected void loop(final double pZoomLevel, final RectL pMercatorViewPort) {
    /* Draw all the MapTiles (from the upper left to the lower right). */
    for (int i = mTiles.left; i <= mTiles.right; i++) {
        for (int j = mTiles.top; j <= mTiles.bottom; j++) {
            if ((horizontalWrapEnabled || (i >= 0 && i < mapTileUpperBound)) && (verticalWrapEnabled
                    || (j >= 0 && j < mapTileUpperBound))) {
                final int tileX = MyMath.mod(i, mapTileUpperBound);
                final int tileY = MyMath.mod(j, mapTileUpperBound);
                final long tile = MapTileIndex.getTileIndex(mTileZoomLevel, tileX, tileY);
                handleTile(tile, i, j);
            }
        }
    }
}

7.OverlayTileLooper -> handleTile

处理图片的加载和展示。mTileProvider指的就是tile的提供者MapTileProviderBasic,tile提供者存在多级缓存,先从内存中读取,如果读不到再从其他缓存加载,最终是从网络服务器加载。

@Override
public void handleTile(final long pMapTileIndex, int pX, int pY) {
    //根据index拿到图片得到图片的drawable
    Drawable currentMapTile = mTileProvider.getMapTile(pMapTileIndex);
    boolean isReusable = currentMapTile instanceof ReusableBitmapDrawable;
    final ReusableBitmapDrawable reusableBitmapDrawable =
           isReusable ? (ReusableBitmapDrawable) currentMapTile : null;
    //如果drawable为空表示缓存不存在,需要从云端下载,下载过程中页面显示加载中样式
    //这个样式通过getLoadingTile来决定。
    if (currentMapTile == null) {
        currentMapTile = getLoadingTile();
    }
    //如果drawable不为空,说明存在缓存,调用onTileReadyToDraw绘制到屏幕上
    if (currentMapTile != null) {
        onTileReadyToDraw(mCanvas, currentMapTile, mTileRect);
    }
}

MapTileProviderArray -> getMapTile

先从缓存中读取,如果存在,并判断不过期(代码省略了)就直接返回;若不存在,则进入异步加载流程,返回空。

@Override
public Drawable getMapTile(final long pMapTileIndex) {
    final Drawable tile = mTileCache.getMapTile(pMapTileIndex);
    if (tile != null) {
        return tile;
    }
    final MapTileRequestState state = new MapTileRequestState(pMapTileIndex, mTileProviderList, MapTileProviderArray.this);
    runAsyncNextProvider(state);
    return tile;
}

TilesOverlay -> getLoadingTile

加载中的样式什么样的?可以看到下面代码中默认构造了一个和瓦片地图图片大小一样的bitmap,然后给这个bitmap上绘制网格。

private Drawable getLoadingTile() {
    final int tileSize = mTileProvider.getTileSource() != null ? mTileProvider
         .getTileSource().getTileSizePixels() : 256;
    final Bitmap bitmap = Bitmap.createBitmap(tileSize, tileSize,
         Bitmap.Config.ARGB_8888);
    final Canvas canvas = new Canvas(bitmap);
    final Paint paint = new Paint();
    canvas.drawColor(mLoadingBackgroundColor);
    paint.setColor(mLoadingLineColor);
    paint.setStrokeWidth(0);
    final int lineSize = tileSize / 16;
    for (int a = 0; a < tileSize; a += lineSize) {
        canvas.drawLine(0, a, tileSize, a, paint);
        canvas.drawLine(a, 0, a, tileSize, paint);
    }
    mLoadingTile = new BitmapDrawable(bitmap);
    return mLoadingTile;
}

TilesOverlay -> onTileReadyToDraw

拿到图片的drawable后是怎么将它渲染到屏幕上的,又是怎么指定渲染的位置的?Drawable方法本身有一个接口setBounds,setBounds方法有四个参数,setBounds(int left, int top, int right, int bottom),这个四参数指的是drawable将在被绘制在canvas的哪个矩形区域内。所以设置好位置之后,调用drawable就会将bitmap绘制到屏幕上了。

protected void onTileReadyToDraw(final Canvas c, final Drawable currentMapTile, final Rect tileRect) {
    currentMapTile.setColorFilter(currentColorFilter);
    currentMapTile.setBounds(tileRect.left, tileRect.top, tileRect.right, tileRect.bottom);
    currentMapTile.draw(c);
}

8.接下来看看Tile的异步加载

final MapTileRequestState state = new MapTileRequestState(pMapTileIndex, mTileProviderList, MapTileProviderArray.this);
runAsyncNextProvider(state);

mTileProviderList中存储的就是加载过程中涉及到的多级缓存,看一张截图:当runAsyncNextProvider方法开始执行后就开始逐个从这些缓存中开始加载。

企业微信截图_17196477975417.png

9.MapTileProviderArray -> runAsyncNextProvider

findNextAppropriateProvider就是遍历mTileProviderList的过程,逐个取出每个provider,并检查有效性后返回,调用loadMapTileAsync方法开始加载。

private void runAsyncNextProvider(final MapTileRequestState pState) {
    final MapTileModuleProviderBase nextProvider = findNextAppropriateProvider(pState);
    nextProvider.loadMapTileAsync(pState);
}

10.MapTileModuleProviderBase -> loadMapTileAsync

异步线程开始加载。

public void loadMapTileAsync(final MapTileRequestState pState) {
    if (mExecutor.isShutdown())
        return;
    synchronized (mQueueLockObject) {
        mPending.put(pState.getMapTile(), pState);
    }
    mExecutor.execute(getTileLoader());
}

getTileLoader()得到的是TileLoader的子类,它实现了Runnable接口,所以可以通过线程池执行。

@Override
final public void run() {
    MapTileRequestState state;
    Drawable result = null;
    while ((state = nextTile()) != null) {
        result = null;
        result = loadTileIfReachable(state.getMapTile());
        tileLoaded(state, result);
    }
}

11.TileLoader -> loadTile

loadTileIfReachable最终调用了loadTile方法,我们直接进入云端数据加载的类MapTileDownloader中的loadTile方法。拿到source,拼接好请求的url,然后调用downloadTile开始请求网络数据。

@Override
public Drawable loadTile(final long pMapTileIndex) throws CantContinueException {
    OnlineTileSourceBase tileSource = mTileSource.get();
    final String tileURLString = tileSource.getTileURLString(pMapTileIndex);
    final Drawable result = downloadTile(pMapTileIndex, 0, tileURLString);
    return result;
}

12.TileLoader -> downloadTile

纯粹的网络请求,下载到图片后封装为drawable返回

protected Drawable downloadTile(final long pMapTileIndex, final int redirectCount, final String targetUrl) throws CantContinueException {
    //纯粹的网络请求,下载到图片后封装为drawable返回
}

13.TileLoader -> tileLoaded

图片拿到之后,我们重新回到前边的tileLoaded方法,loadTileIfReachable方法执行后拿到drawable,然后调用tileLoaded。

protected void tileLoaded(final MapTileRequestState pState, final Drawable pDrawable) {
    pState.getCallback().mapTileRequestCompleted(pState, pDrawable);
}

14.MapTileProviderBase -> mapTileRequestCompleted

拿到图片后第一件事,存入内存缓存,然后通知主线程进行ui刷新。

@Override
public void mapTileRequestCompleted(final MapTileRequestState pState, final Drawable pDrawable) {
    //内存缓存
    putTileIntoCache(pState.getMapTile(), pDrawable, ExpirableBitmapDrawable.UP_TO_DATE);
    // tell our caller we've finished and it should update its view
    sendMessage(MAPTILE_SUCCESS_ID);
}

15.SimpleInvalidationHandler

刷新页面,一次请求结束。

@Override
public void handleMessage(final Message msg) {
    switch (msg.what) {
        case MapTileProviderBase.MAPTILE_SUCCESS_ID:
            if (mView != null)
                mView.invalidate();
            break;
    }
}

总结

OsmDroid将地图分为很多层,根据不同的层级进行地图图片的加载,加载地图可以简单的认为是加载多张图片,并最终将图片拼接成完整地图的过程。在OsmDroid地图加载过程中,涉及到多级缓存,通过缓存已加载过的特定层级下的指定区域中的地图图片,从而加速地图图片的显示。