Mapbox源码分析之url解析

简介:

通过源码,我们来一步步分析Mapbox地图引擎如何进行指定字符串变量解析成url地址加载的,这里是基于5.3.0的版本.

在官方demo中,我们不仅可以加载本地样式文件,已定义样式文件和网络在线文件,它们的格式分别是

  • “asset://test.json”
  • https://www.mapbox.com/android-docs/files/mapbox-raster-v8.json
  • “mapbox://styles/mapbox/streets-v10”
    这些格式,那么Mapbox如果解析这些字符串去获取到需要的样式数据呢?我们从Mapbox源码分析之样式加载这篇的loadURL()方法开始看起
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    void Style::Impl::loadURL(const std::string& url_) {
    lastError = nullptr;
    observer->onStyleLoading();
    loaded = false;
    url = url_;
    styleRequest = fileSource.request(Resource::style(url), [this](Response res) {
    // Once we get a fresh style, or the style is mutated, stop revalidating.
    if (res.isFresh() || mutated) {
    styleRequest.reset();
    }
    // Don't allow a loaded, mutated style to be overwritten with a new version.
    if (mutated && loaded) {
    return;
    }
    if (res.error) {
    const std::string message = "loading style failed: " + res.error->message;
    Log::Error(Event::Setup, message.c_str());
    observer->onStyleError(std::make_exception_ptr(util::StyleLoadException(message)));
    observer->onResourceError(std::make_exception_ptr(std::runtime_error(res.error->message)));
    } else if (res.notModified || res.noContent) {
    return;
    } else {
    parse(*res.data);
    }
    });
    }

我们在这里看到,样式的数据是通过fileSource.request进行请求加载的,通过调试我们发现这个fileSource是FileSource的子类DefaultFileSource,那么我们先看看这个DefaultFileSource是什么时候传进来的

1
2
3
4
5
6
7
8
9
Style::Impl::Impl(Scheduler& scheduler_, FileSource& fileSource_, float pixelRatio)
: scheduler(scheduler_),
fileSource(fileSource_),
spriteLoader(std::make_unique<SpriteLoader>(pixelRatio)),
light(std::make_unique<Light>()),
observer(&nullObserver) {
spriteLoader->setObserver(this);
light->setObserver(this);
}

我们在这里看到,是在构造方法时对fileSource变量进行初始化的,那么我们只需要看到Style::Impl对象什么时候构造的,便知道了fileSource的来源,继续往回找

1
2
3
Style::Style(Scheduler& scheduler, FileSource& fileSource, float pixelRatio)
: impl(std::make_unique<Impl>(scheduler, fileSource, pixelRatio)) {
}

在这里我们发现Impl对象的fileSource是Style对象构造时传进来的,那么我们继续往回找

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
style::Style& Map::getStyle() {
return *impl->style;
}
Map::Impl::Impl(Map& map_,
RendererFrontend& frontend,
MapObserver& mapObserver,
float pixelRatio_,
FileSource& fileSource_,
Scheduler& scheduler_,
MapMode mode_,
ConstrainMode constrainMode_,
ViewportMode viewportMode_)
: map(map_),
observer(mapObserver),
rendererFrontend(frontend),
fileSource(fileSource_),
scheduler(scheduler_),
transform(observer,
constrainMode_,
viewportMode_),
mode(mode_),
pixelRatio(pixelRatio_),
style(std::make_unique<Style>(scheduler, fileSource, pixelRatio)),
annotationManager(*style) {
style->impl->setObserver(this);
rendererFrontend.setObserver(*this);
}

这里我们看到Style对象是通过map.cpp里的getStyle对象获取的,而style对象是在Map::Impl::Impl构造方法时初始化的,继续往回找

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Map::Map(RendererFrontend& rendererFrontend,
MapObserver& mapObserver,
const Size size,
const float pixelRatio,
FileSource& fileSource,
Scheduler& scheduler,
MapMode mapMode,
ConstrainMode constrainMode,
ViewportMode viewportMode)
: impl(std::make_unique<Impl>(*this,
rendererFrontend,
mapObserver,
pixelRatio,
fileSource,
scheduler,
mapMode,
constrainMode,
viewportMode)) {
impl->transform.resize(size);
}

这里我们其实也能大概猜出来Map::Impl对象是在Map构造方法时初始化的,那么map对象又是什么时候初始化的,是不是觉得很绕,马上就快到了,我们找到native_map_view.cpp文件,发现在NativeMapView构造方法中构造了map对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
NativeMapView::NativeMapView(jni::JNIEnv& _env,
jni::Object<NativeMapView> _obj,
jni::Object<FileSource> jFileSource,
jni::Object<MapRenderer> jMapRenderer,
jni::jfloat _pixelRatio)
: javaPeer(_obj.NewWeakGlobalRef(_env))
, mapRenderer(MapRenderer::getNativePeer(_env, jMapRenderer))
, pixelRatio(_pixelRatio)
, threadPool(sharedThreadPool()) {
// Get a reference to the JavaVM for callbacks
if (_env.GetJavaVM(&vm) < 0) {
_env.ExceptionDescribe();
return;
}
// Get native peer for file source
mbgl::FileSource& fileSource = mbgl::android::FileSource::getDefaultFileSource(_env, jFileSource);
// Create a renderer frontend
rendererFrontend = std::make_unique<AndroidRendererFrontend>(mapRenderer);
// Create the core map
map = std::make_unique<mbgl::Map>(*rendererFrontend, *this,
mbgl::Size{ static_cast<uint32_t>(width),
static_cast<uint32_t>(height) }, pixelRatio,
fileSource, *threadPool, MapMode::Continuous,
ConstrainMode::HeightOnly, ViewportMode::Default);
}

到这里我们已经基本清楚fileSource的来源了,是JAVA层NativeMapView对象初始化的时候传下来的,我们继续看到开头,既然我们已经知道fileSource对象是DefaultFileSource,那么它调用的request方法,也就是调用的DefaultFileSource的request方法,这里我们看到default_file_source.cpp文件

1
2
3
4
5
6
7
8
9
std::unique_ptr<AsyncRequest> DefaultFileSource::request(const Resource& resource, Callback callback) {
auto req = std::make_unique<FileSourceRequest>(std::move(callback));
req->onCancel([fs = impl->actor(), req = req.get()] () mutable { fs.invoke(&Impl::cancel, req); });
impl->actor().invoke(&Impl::request, req.get(), resource, req->actor());
return std::move(req);
}

这里我们看到它转到了它的实现类的request方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
void request(AsyncRequest* req, Resource resource, ActorRef<FileSourceRequest> ref) {
auto callback = [ref] (const Response& res) mutable {
ref.invoke(&FileSourceRequest::setResponse, res);
};
if (isAssetURL(resource.url)) {
//Asset request
tasks[req] = assetFileSource->request(resource, callback);
} else if (LocalFileSource::acceptsURL(resource.url)) {
//Local file request
tasks[req] = localFileSource->request(resource, callback);
} else {
// Try the offline database
if (resource.hasLoadingMethod(Resource::LoadingMethod::Cache)) {
auto offlineResponse = offlineDatabase->get(resource);
if (resource.loadingMethod == Resource::LoadingMethod::CacheOnly) {
if (!offlineResponse) {
// Ensure there's always a response that we can send, so the caller knows that
// there's no optional data available in the cache, when it's the only place
// we're supposed to load from.
offlineResponse.emplace();
offlineResponse->noContent = true;
offlineResponse->error = std::make_unique<Response::Error>(
Response::Error::Reason::NotFound, "Not found in offline database");
} else if (!offlineResponse->isUsable()) {
// Don't return resources the server requested not to show when they're stale.
// Even if we can't directly use the response, we may still use it to send a
// conditional HTTP request, which is why we're saving it above.
offlineResponse->error = std::make_unique<Response::Error>(
Response::Error::Reason::NotFound, "Cached resource is unusable");
}
callback(*offlineResponse);
} else if (offlineResponse) {
// Copy over the fields so that we can use them when making a refresh request.
resource.priorModified = offlineResponse->modified;
resource.priorExpires = offlineResponse->expires;
resource.priorEtag = offlineResponse->etag;
resource.priorData = offlineResponse->data;
if (offlineResponse->isUsable()) {
callback(*offlineResponse);
}
}
}
// Get from the online file source
if (resource.hasLoadingMethod(Resource::LoadingMethod::Network)) {
tasks[req] = onlineFileSource.request(resource, [=] (Response onlineResponse) mutable {
this->offlineDatabase->put(resource, onlineResponse);
callback(onlineResponse);
});
}
}
}

这里我们可以看到根据url的不同,和加载方法的不同,将请求分别转给了assetFileSource,localFileSource,onlineFileSource等的request方法,这里我们看onlineFileSource的request方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
std::unique_ptr<AsyncRequest> OnlineFileSource::request(const Resource& resource, Callback callback){
Resource res = resource;
switch (resource.kind) {
case Resource::Kind::Unknown:
case Resource::Kind::Image:
break;
case Resource::Kind::Style:
res.url = mbgl::util::mapbox::normalizeStyleURL(apiBaseURL, resource.url, accessToken);
break;
case Resource::Kind::Source:
res.url = util::mapbox::normalizeSourceURL(apiBaseURL, resource.url, accessToken);
break;
case Resource::Kind::Glyphs:
res.url = util::mapbox::normalizeGlyphsURL(apiBaseURL, resource.url, accessToken);
break;
case Resource::Kind::SpriteImage:
case Resource::Kind::SpriteJSON:
res.url = util::mapbox::normalizeSpriteURL(apiBaseURL, resource.url, accessToken);
break;
case Resource::Kind::Tile:
res.url = util::mapbox::normalizeTileURL(apiBaseURL, resource.url, accessToken);
break;
}
return std::make_unique<OnlineFileRequest>(std::move(res), std::move(callback), *impl);}

看到这里我们看到根据请求的类型不同,去处理不同的url,在这些参数里我们看下apiBaseURL这个变量,这是一个base url,指定了服务器地址,我们在constants.hpp文件中找到了它

1
constexpr const char* API_BASE_URL = "https://api.mapbox.com";

继续往下看,我们选normalizeStyleURL()方法往下看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
std::string normalizeStyleURL(const std::string& baseURL,
const std::string& str,
const std::string& accessToken) {
if (!isMapboxURL(str)) {
return str;
}
const URL url(str);
if (!equals(str, url.domain, "styles")) {
Log::Error(Event::ParseStyle, "Invalid style URL");
return str;
}
const auto tpl = baseURL + "/styles/v1{path}?access_token=" + accessToken;
return transformURL(tpl, str, url);
}

这里我们看到它先验证了一下url,然后将url字符串包装成URL对象,然后进行一个拼接成tpl变量,最后再通过transformURL函数进行一个转换,这里我们先看它如何包装这个URL对象的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
URL::URL(const std::string& str)
: query([&]() -> Segment {
const auto hashPos = str.find('#');
const auto queryPos = str.find('?');
if (queryPos == std::string::npos || hashPos < queryPos) {
return { hashPos != std::string::npos ? hashPos : str.size(), 0 };
}
return { queryPos, (hashPos != std::string::npos ? hashPos : str.size()) - queryPos };
}()),
scheme([&]() -> Segment {
if (str.empty() || !isAlphaCharacter(str.front())) return { 0, 0 };
size_t schemeEnd = 0;
while (schemeEnd < query.first && isSchemeCharacter(str[schemeEnd])) ++schemeEnd;
return { 0, str[schemeEnd] == ':' ? schemeEnd : 0 };
}()),
domain([&]() -> Segment {
auto domainPos = scheme.first + scheme.second;
while (domainPos < query.first && (str[domainPos] == ':' || str[domainPos] == '/')) {
++domainPos;
}
const bool isData = str.compare(scheme.first, scheme.second, "data") == 0;
const auto endPos = str.find(isData ? ',' : '/', domainPos);
return { domainPos, std::min(query.first, endPos) - domainPos };
}()),
path([&]() -> Segment {
auto pathPos = domain.first + domain.second;
const bool isData = str.compare(scheme.first, scheme.second, "data") == 0;
if (isData) {
// Skip comma
pathPos++;
}
return { pathPos, query.first - pathPos };
}()) {
}

这里我们看到它将字符串分解成query,scheme,domain,path四个变量进行存储,我们再看看transformURL()函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
std::string transformURL(const std::string& tpl, const std::string& str, const URL& url) {
auto result = util::replaceTokens(tpl, [&](const std::string& token) -> std::string {
if (token == "path") {
return str.substr(url.path.first, url.path.second);
} else if (token == "domain") {
return str.substr(url.domain.first, url.domain.second);
} else if (token == "scheme") {
return str.substr(url.scheme.first, url.scheme.second);
} else if (token == "directory") {
const Path path(str, url.path.first, url.path.second);
return str.substr(path.directory.first, path.directory.second);
} else if (token == "filename") {
const Path path(str, url.path.first, url.path.second);
return str.substr(path.filename.first, path.filename.second);
} else if (token == "extension") {
const Path path(str, url.path.first, url.path.second);
return str.substr(path.extension.first, path.extension.second);
}
return "";
});
// Append the query string if it exists.
if (url.query.second > 1) {
const auto amp = result.find('?') != std::string::npos ? result.size() : std::string::npos;
result.append(str, url.query.first, url.query.second);
// Transform the question mark to an ampersand if we had a query string previously.
if (amp < result.size()) {
result[amp] = '&';
}
}
return result;
}

这里我们看到根据url的不同变量值进行了再次字符串拼接,甚至根据路径的不同,继续拆分成Path对象,最后将拼接结果返回,到这里有关url解析拼接的过程就讲完了.