ART深入浅出2 -- 认识和了解Runtime Options
2017-07-14 20:51
423 查看
本文基于Android 7.1,不过因为从BSP拿到的版本略有区别,所以本文提到的源码未必与读者找到的源码完全一致。本文在提供源码片断时,将按照
<源码相对android工程的路径>:<行号> <类名> <函数名> 的方式,如果行号对不上,请参考类名和函数名来找到对应的源码。
RuntimeOptions实际上只是一个vector定义:
art/runtime/parsed_options.h:38
内部的parie对象,第一个参数保存的是放进去的字符串数据,第二个是附加参数。一般附加参数不用。
art/runtime/java_vm_ext.cc:947 JNI_CreateJavaVM
art/runtime/runtime.cc:486 Runtime::Create
本章要重点分析的就是这个ParseOptions函数。
首先看看RuntimeArugmentMap是什么结构。
DoParse函数,主体有两个函数:MakeParse和RuntimeParser::Parse函数。
首先,看看MakeParse函数。
请先看下面的代码
runtime/parsed_options.cc:61 ParsedOptions::MakeParser
该函数用了一些模板函数,创建了一个解析器。Define函数创建一个特定解析器,特殊符号'_'表示匹配剩余部分字符串。 WithType函数指定'_'匹配部分需要转换为的对象;WithValueMap函数定义了字符串与最终值之间的映射关系。最后IntoKey函数建立和一个key的关联。
这样,解析完成后,结果就存储在 RuntimeArgumentMap对象中,而key正是IntoKey指定的。
在理解之前,先了解几个类:
CmdlineParser::Builder 创建解析器的builder
UntypedArgumentBuilder 创建一个没有指定类型的参数Builder
template <typename TArg> ArgumentBuilder 创建一个指定类型的参数builder
首先,Builder.Define函数创建一个UntypedArgumentBuilder实例
其次,UntypedArgumentBuilder.WithType函数创建一个ArgumentBuilder实例
第三,为ArgumentBuilder添加一系列的解析信息
最后,调用ArugmentBuilder.IntoKey,返回之前的Builder。
这里需要注意的是,UntypedArgumentBuilder和ArugmentBuilder其实都是临时对象,而Builder的实例则是一直存在的。
这样完成一个链式调用,可以方便的增加若干各模块。
ArgumentBuilder类支持若干种解析方法:
WithRange, 针对数值类型,指出数值的值域
WithValue: 如果一个参数被设置,就设置一个对应的值,否则就不设置。比如Define("-Xint").WithValue(true) 。 这里因为已经给出了具体value值,所以它的type可以确定,也就无需先用WithType产生一个ArgumentBuilder了,UntypedArgumentBuilder就能做到;
AppendValues 将多个参数的值收集到一个key下面。比如.Define("-D_").WithType<std::vector<std::string>>().AppendValues() 把多个-D定义的信息都放在一个模块内部。可以看到,这个属性是用vector数组保存的对象。
WithValues: 同WithValue,只是接受一个数组,建立一个映射值,例如.Define({"-XX:EnableHSpaceCompactForOOM", "-XX:DisableHSpaceCompactForOOM"}) .WithValues({true, false}) 如何设置第一个参数-XX:EnableHSpaceCompactForOOM,则取true,否则取false
WithValueMap: 将取得的值与最终建立一个映射,如
到目前为止,就可以了解参数解析的基本用法,并且能够修改参数了。如果您不想了解它的工作原理,您可以结束本章阅读了。如果本着死磕精神,我们就深入了解下,这种神奇的用法是如何用模板实现的。
根据MakeParser函数的写法,我们可以推测出开发者的意图是这样的:
建立一个解析器数组,数组内的每个解析器,都解析一个或者多个关联的参数
解析器有一个匹配器,通过字符串比较,找出要解析的参数,很多情况下,要解析参数前面的名称部分
解析器要获取参数值部分,并把参数值转换为各种内部数据结构
参数解析器还带有一个Key,将此Key和最终结果,关联存储到一个Map表中
内部的数据结构包括:
true/false
数值,可能带有值域
枚举值
字符串列表
带单位的数值,比如最大/最小堆大小,以MB为单位
一个Map表存储Key-Value值
OK,现在,我们试着用伪代码来表示这个想法,现在定义各个结构的名称:
CmdlineParser: 管理解析器数组的类,里面有一个参数解析器数组 argument_parsers_
ArgumentParser: 参数解析器类, 有函数:
match: 进行参数匹配;
fetchValue:从参数中提取值
covertValue:将参数值转为内部程序可以直接读取的值
key:得到关联的访问key
OptionsMap 保存结果的表,将key和value保存起来
参数列表是一个std::vector<string> arguments对象,那么伪代码可以这么写
下面,我们就一一的对应ART中的源码,看看这些想法究竟是怎么实现的?
art/runtime/parsed_options.cc:53
查看CmdlineParser类的定义,发现它的成员有
art/cmdline/cmdline_parser.h:41
其中ignore_list_就是忽略的参数列表,这个没什么。
save_destination_,这是用来想OptionsMap存储数据的,我们也不管它。
completed_aruments_ 就是最重要的参数解析器的列表。
看到,这个列表是CmdlineParseArgumentAny对象的指针数组,为什么是指针?因为需要处理很多种不同的数据类型,只能通过虚函数来实现,所以要做成指针数组,而不能是对象数组。
TokenRange类,先看看它的核心函数和成员
art/cmdline/token_range.h:35
因为参数解析器可能会同时解析几种参数,有几种情况:
参数缩写,比如-classpath和-cp cp是classpath的缩写,两者都要识别
互斥参数,比如-XX:EnableHSpaceCompactForOOM和-XX:DisableHSpaceCompactForOOM这种带有enable/dispable的参数
MaybeMatches函数返回的是有多少个匹配的token。因为包含了匹配字符"_",所以,有时候会出现匹配不精确的问题,比如 "-XAB_"和"-XABC_"这两种token都能匹配参数” -XABCDEF“,但是按照精确的原则,应该是匹配 "-XABC_"而不是"-XAB_"。
注:上面的说法是我按照注释,加上自己的猜想。但是根据MaybeMatches函数的实现,我感觉与注释的行为并不一致。开发者可能自己也糊涂了。实际上,java vm参数设计上不会出现上面举例的情况,反倒是开发者自己想多了。有兴趣的读者可以自行看代码了解。
关键的接口是:
MaybeMatches : 比较match的接口
ParseArgument : 提取参数并转换的函数
它的实现类就是 CmdlineParseArgument,这个类是一个模板类,必须指定具体类型才能实例化。
现在看看CmdlineParseArgument的实现。
代码在art/cmdline/detail/cmdline_parse_argument_detail.h:88,不在列代码了,只列出关键的成员
std::vector<const char*> names_ : 这是Builder.Define函数给出的列表,用于做参数匹配等操作
bool using_blanks_: names_里面是否至少有一个name包含了 "_", 有则true,否则为false
std::vector<TokenRange> tokenized_names_ 将names_ tokenized化后的结果,方便match
std::vector<TokenRange> simple_names_ 除去掉"_"字符所有名字
appending_values_: 如果调用了AppendValues就会为true。这样会将所有的值放在同一key下
has_range_, TArg min_, max_; 调用了WithRange后,就会设置为true,并填充min,max的值
has_value_map_, std::vector<std::pair<const char*, TArg>> value_map_ 调用WithValueMap就会使用它们
has_value_list_, std::vector<TArg> value_list_,调用WithValues就会使用它们。主要value_list_和name_它们是用索引一一对应的,比如names_[0]就对应value_list_[0]
成员函数MaybeMatches用于实现对参数的匹配。
这个函数有两部分组成:1. 解析参数, 2. 转换值,由函数ParseArgumentSingle来实现。
参数解析的过程是这样的:
如果using_blanking为false,那么,直接调用ParseArgumentSingle,走value_list_的映射处理
否则,提取出"_"映射的部分,比如"foo:bar",提取出blank_value == "bar",然后调用ParseArgumentSingle
art/cmdline/detail/cmdline_parse_argument_detail.h:386 CmdlineParseArgument.ParseeArgumentSingle
art/cmdline/detail/cmdline_parse_argument_detail.h:409 CmdlineParseArgument.ParseeArgumentSingle
1. 调用load_argument_方法(这是一个重载了operator()的类对象),获取已经加入的值,或者是新建一个值来加入
2. 调用type_parser.ParseAndAppend方法把值解析并加入进去
art/cmdline/detail/cmdline_parse_argument_detail.h:438 CmdlineParseArgument.ParseeArgumentSingle
art/cmdline/detail/cmdline_parse_argument_detail.h:447 CmdlineParseArgument.ParseeArgumentSingle
art/cmdline/detail/cmdline_parse_argument_detail.h:311 CmdlineParseArgument
art/cmdline/cmdline_types.h:42
当在WithType的时候给出什么参数, C++就会在这里挑选一个备用的半实例化CmdlineType类,而这个CmdlineType就知道如何解析该类型的参数了。
我没有列出具体的解析代码,是因为这些不重要,重要的是了解到它们的实现机理。
注意到,半实例化的CmdlineType都继承了CmdlineTypeParser类。这是因为,CmdlineType必须声明两个函数Parser和ParseAndAppend。但是,很多情况下,是不需要ParseAndAppend函数的。ART为了方便,就声明了一个CmdlineTypeParser类,大家都继承它。这样,如果需要ParseAndAppend函数时,就定义它,不需要时,就用基础类CmdlineTypeParser的,避免了重复声明。
当我们在using UserTypeInfo = CmdlineType<TArg>; 时候,C++编译器就根据TArg就选好了具体实现的CmdlineType了。因此,直接调用它的对象的Parser或者ParseAndAppend函数即可。
至于UntypedArgumentBuilder.WithType函数,只是创建了一个ArgumentBuilder类实例,只要把TArg参数传递出去,解析器就可以自动选定了。
art/runtime/runtime_options.h:50
要读懂这个代码,需要一些技巧。
首先,我们看到有Variant这个关键字:VariantMap, VariantMapKey,这说明这个Map是可以存储任意类型的数据的。
其次,按照我们一般的观念,要存储任意类型,只有两种方法:1. 用union,2: 用void*只保存其指针,那么,可以推定VariantMap的value只能是二者之一
第三,不管是union还是void*,都必须知道数据的类型,否则数据无法访问,那么VariantMapKey<TValue>的模板参数TValue肯定是用来指出参数类型的;
第四,如果是作为key,那么只能是一个变量值,不能是一个类型。那么RUNTIME_OPTION_KEY宏显然是定义了一个静态key变量。显然 runtime_options.def文件里面,就是用宏RUNTIME_OPTION_KEY定义了一堆key。之所以用宏+runtime_options.def的方式定义,只是因为,在头文件中声明后,还必须在源文件中定义。可以参阅
art/runtime/runtime_options.cc:32
VariantMapKey类的定义,大家可以自行看源码(art/runtime/base/variant_map.h), 它的作用,就是将所有对TValue类型的数据进行操作:创建、拷贝、删除等。不再赘述。
然后我们看看VariantMap类的实现。这里,关键是找它存储成员 StorageMap storage_map_, 而这个StoreageMap的定义就是:
art/runtime/base/variant_map.h:402
实际上,这是我读到的ART源码中最复杂的一块了,激起我下决心把它搞清楚。我觉得,相对于这部分代码的本身意义,解读的方法,更值得向大家推荐:
先搞明白它的意图,要实现什么功能,达到什么效果
想一个一般的实现思路
按照这个思路,去源码中寻找实现这个思路的关键点,在这个过程中,成员变量比成员函数更重要,数据比过程更重要
迭代这个过程,从高层函数到底层函数,层层推进,就能摸清出它的实现方法。
最后,我给ART用模板的方法实现这个功能,给一个中肯的评价:反人类!我曾经有段时间很迷恋模板,甚至研究了boost的实现,用模板实现了简单的lambda表达式。最顶峰时,用模板写了一个ARM机器码反汇编器。但是后来发现,几乎没有人能够再看懂这些代码,即便我自己也需要花些时间来温习。既然如此,用模板还有什么意义?一个设计良好的虚拟类+简单的模板运用,是不是比这样做更清晰、更容易理解、更容易维护呢?
<源码相对android工程的路径>:<行号> <类名> <函数名> 的方式,如果行号对不上,请参考类名和函数名来找到对应的源码。
RuntimeOptions到RuntimeArgumentMap
在JNI_CreateJavaVM函数中,JavaVMOption对象被转换为RuntimeOptions对象。RuntimeOptions实际上只是一个vector定义:
art/runtime/parsed_options.h:38
typedef std::vector<std::pair<std::string, const void*>> RuntimeOptions;
内部的parie对象,第一个参数保存的是放进去的字符串数据,第二个是附加参数。一般附加参数不用。
art/runtime/java_vm_ext.cc:947 JNI_CreateJavaVM
for (int i = 0; i < args->nOptions; ++i) { JavaVMOption* option = &args->options[i]; options.push_back(std::make_pair(std::string(option->optionString), option->extraInfo)); }当数据填充完成后,调用Runtime::Create方法,传递给Runtime,用于创建Runtime。
art/runtime/runtime.cc:486 Runtime::Create
bool Runtime::Create(const RuntimeOptions& raw_options, bool ignore_unrecognized) { RuntimeArgumentMap runtime_options; return ParseOptions(raw_options, ignore_unrecognized, &runtime_options) && Create(std::move(runtime_options)); }注意到return语句,先调用ParseOptions, 将RuntimeOptions转换为RuntimeArugmentMap。
本章要重点分析的就是这个ParseOptions函数。
首先看看RuntimeArugmentMap是什么结构。
Runtime::ParseOptions
该函数解析参数的入口。Runtime::ParseOptions调用 ParsedOptions:Parse函数,再调用 ParsedOptions::DoParse函数。DoParse函数,主体有两个函数:MakeParse和RuntimeParser::Parse函数。
首先,看看MakeParse函数。
MakeParse函数
这个函数用了非常复杂的C++模板,生成一个解析数据结构。因为内容太多,我摘录了关键部分,进行解说。请先看下面的代码
runtime/parsed_options.cc:61 ParsedOptions::MakeParser
std::unique_ptr<RuntimeParser> ParsedOptions::MakeParser(bool ignore_unrecognized) { using M = RuntimeArgumentMap; std::unique_ptr<RuntimeParser::Builder> parser_builder = std::unique_ptr<RuntimeParser::Builder>(new RuntimeParser::Builder()); parser_builder-> Define("-Xzygote") .IntoKey(M::Zygote) .Define("-help") .IntoKey(M::Help) ..... .WithType<ParseStringList<':'>>() // std::vector<std::string>, split by : .IntoKey(M::BootClassPathLocations) .Define({"-classpath _", "-cp _"}) .WithType<std::string>() .IntoKey(M::ClassPath) .Define("-Ximage:_") .WithType<std::string>() .IntoKey(M::Image) .... .Define("-D_") .WithType<std::vector<std::string>>().AppendValues() .IntoKey(M::PropertiesList) .... .Define({"-XX:EnableHSpaceCompactForOOM", "-XX:DisableHSpaceCompactForOOM"}) .WithValues({true, false}) .IntoKey(M::EnableHSpaceCompactForOOM) .... .Define("-XX:HeapTargetUtilization=_") .WithType<double>().WithRange(0.1, 0.9) .... .Define("-Xint") .WithValue(true) .IntoKey(M::Interpret) .... .Define("-Xmx_") .WithType<MemoryKiB>() .IntoKey(M::MemoryMaximumSize) ..... .Define("-XX:DumpNativeStackOnSigQuit:_") .WithType<bool>() .WithValueMap({{"false", false}, {"true", true}}) .IntoKey(M::DumpNativeStackOnSigQuit) ..... .Define("-XX:LargeObjectSpace=_") .WithType<gc::space::LargeObjectSpaceType>() .WithValueMap({{"disabled", gc::space::LargeObjectSpaceType::kDisabled}, {"freelist", gc::space::LargeObjectSpaceType::kFreeList}, {"map", gc::space::LargeObjectSpaceType::kMap}}) .IntoKey(M::LargeObjectSpace) ..... .Ignore({ "-ea", "-da", "-enableassertions", "-disableassertions", "--runtime-arg", "-esa", ..... "-Xjitdisableopt", "-Xjitsuspendpoll", "-XX:mainThreadStackSize=_"}) .IgnoreUnrecognized(ignore_unrecognized); // TODO: Move Usage information into this DSL. return std::unique_ptr<RuntimeParser>(new RuntimeParser(parser_builder->Build())); }
该函数用了一些模板函数,创建了一个解析器。Define函数创建一个特定解析器,特殊符号'_'表示匹配剩余部分字符串。 WithType函数指定'_'匹配部分需要转换为的对象;WithValueMap函数定义了字符串与最终值之间的映射关系。最后IntoKey函数建立和一个key的关联。
这样,解析完成后,结果就存储在 RuntimeArgumentMap对象中,而key正是IntoKey指定的。
在理解之前,先了解几个类:
CmdlineParser::Builder 创建解析器的builder
UntypedArgumentBuilder 创建一个没有指定类型的参数Builder
template <typename TArg> ArgumentBuilder 创建一个指定类型的参数builder
首先,Builder.Define函数创建一个UntypedArgumentBuilder实例
其次,UntypedArgumentBuilder.WithType函数创建一个ArgumentBuilder实例
第三,为ArgumentBuilder添加一系列的解析信息
最后,调用ArugmentBuilder.IntoKey,返回之前的Builder。
这里需要注意的是,UntypedArgumentBuilder和ArugmentBuilder其实都是临时对象,而Builder的实例则是一直存在的。
这样完成一个链式调用,可以方便的增加若干各模块。
ArgumentBuilder类支持若干种解析方法:
WithRange, 针对数值类型,指出数值的值域
WithValue: 如果一个参数被设置,就设置一个对应的值,否则就不设置。比如Define("-Xint").WithValue(true) 。 这里因为已经给出了具体value值,所以它的type可以确定,也就无需先用WithType产生一个ArgumentBuilder了,UntypedArgumentBuilder就能做到;
AppendValues 将多个参数的值收集到一个key下面。比如.Define("-D_").WithType<std::vector<std::string>>().AppendValues() 把多个-D定义的信息都放在一个模块内部。可以看到,这个属性是用vector数组保存的对象。
WithValues: 同WithValue,只是接受一个数组,建立一个映射值,例如.Define({"-XX:EnableHSpaceCompactForOOM", "-XX:DisableHSpaceCompactForOOM"}) .WithValues({true, false}) 如何设置第一个参数-XX:EnableHSpaceCompactForOOM,则取true,否则取false
WithValueMap: 将取得的值与最终建立一个映射,如
Define("-Xprofile:_") .WithType<TraceClockSource>() .WithValueMap({{"threadcpuclock", TraceClockSource::kThreadCpu}, {"wallclock", TraceClockSource::kWall}, {"dualclock", TraceClockSource::kDual}})"_"所匹配的值,将和map列出的值一一对应。
到目前为止,就可以了解参数解析的基本用法,并且能够修改参数了。如果您不想了解它的工作原理,您可以结束本章阅读了。如果本着死磕精神,我们就深入了解下,这种神奇的用法是如何用模板实现的。
Parse的基本原理讲解
在正式了解Parse的实现过程之前,我们先想想应该怎么实现。根据MakeParser函数的写法,我们可以推测出开发者的意图是这样的:
建立一个解析器数组,数组内的每个解析器,都解析一个或者多个关联的参数
解析器有一个匹配器,通过字符串比较,找出要解析的参数,很多情况下,要解析参数前面的名称部分
解析器要获取参数值部分,并把参数值转换为各种内部数据结构
参数解析器还带有一个Key,将此Key和最终结果,关联存储到一个Map表中
内部的数据结构包括:
true/false
数值,可能带有值域
枚举值
字符串列表
带单位的数值,比如最大/最小堆大小,以MB为单位
一个Map表存储Key-Value值
OK,现在,我们试着用伪代码来表示这个想法,现在定义各个结构的名称:
CmdlineParser: 管理解析器数组的类,里面有一个参数解析器数组 argument_parsers_
ArgumentParser: 参数解析器类, 有函数:
match: 进行参数匹配;
fetchValue:从参数中提取值
covertValue:将参数值转为内部程序可以直接读取的值
key:得到关联的访问key
OptionsMap 保存结果的表,将key和value保存起来
参数列表是一个std::vector<string> arguments对象,那么伪代码可以这么写
CmdlineParser parser; //解析对象 OptionsMap optionsMap; //保存结果 for (auto it = arguments.begin(); it != argments.end(); ++it) { stirng arg = *it; for (auto it_parser = parser.argument_parsers_.begin(); it_parser != parser.argument_parsers_.end(); ++it_parser) { if (it_parser->match(arg)) { string str_value = it_parser->fetchValue(); auto value = it_parser->covertValue(str_value); optionsMap.put(it_parser->key(), value); } } }
下面,我们就一一的对应ART中的源码,看看这些想法究竟是怎么实现的?
CmdlineParser
MakeParser函数最后返回的是一个RuntimeParser的指针对象,RuntimeParser就是CmdlineParser对象:art/runtime/parsed_options.cc:53
using RuntimeParser = CmdlineParser<RuntimeArgumentMap, RuntimeArgumentMap::Key>;其中,RuntimeArgumentMap就是上面说的OptionsMap对象,RuntimeArgumentMap::Key是和它对应的key对象。
查看CmdlineParser类的定义,发现它的成员有
art/cmdline/cmdline_parser.h:41
template <typename TVariantMap, template <typename TKeyValue> class TVariantMapKey> struct CmdlineParser { .... //604 private: bool ignore_unrecognized_ = false; std::vector<const char*> ignore_list_; std::shared_ptr<SaveDestination> save_destination_; std::vector<std::unique_ptr<detail::CmdlineParseArgumentAny>> completed_arguments_; };这个类的定义很长,有600多行,但是,大部分是内部类的定义和实现函数,它的核心数据就是上面这些。
其中ignore_list_就是忽略的参数列表,这个没什么。
save_destination_,这是用来想OptionsMap存储数据的,我们也不管它。
completed_aruments_ 就是最重要的参数解析器的列表。
看到,这个列表是CmdlineParseArgumentAny对象的指针数组,为什么是指针?因为需要处理很多种不同的数据类型,只能通过虚函数来实现,所以要做成指针数组,而不能是对象数组。
TokeRange
首先介绍下TokeRange这个类。这个类辅助比较用的。因为下面进行比较时,会大量用到这个类,所以有必要先介绍它。TokenRange类,先看看它的核心函数和成员
art/cmdline/token_range.h:35
struct TokenRange { // Short-hand for a vector of strings. A single string and a token is synonymous. using TokenList = std::vector<std::string>; .... //329 // Do a quick match token-by-token, and see if they match. // Any tokens with a wildcard in them are only matched up until the wildcard. // If this is true, then the wildcard matching later on can still fail, so this is not // a guarantee that the argument is correct, it's more of a strong hint that the // user-provided input *probably* was trying to match this argument. // // Returns how many tokens were either matched (or ignored because there was a // wildcard present). 0 means no match. If the size() tokens are returned. size_t MaybeMatches(const TokenRange& token_list, const std::string& wildcard) const { .... private: ....//418 const std::shared_ptr<std::vector<std::string>> token_list_; const iterator begin_; const iterator end_; };token_list_其实就是一个 字符串数组,也就是arguments数组。begin_和end_保存是这个数组的开头和结尾,也就是说,TokenRange保存的是整个参数数组的一部分。
因为参数解析器可能会同时解析几种参数,有几种情况:
参数缩写,比如-classpath和-cp cp是classpath的缩写,两者都要识别
互斥参数,比如-XX:EnableHSpaceCompactForOOM和-XX:DisableHSpaceCompactForOOM这种带有enable/dispable的参数
MaybeMatches函数返回的是有多少个匹配的token。因为包含了匹配字符"_",所以,有时候会出现匹配不精确的问题,比如 "-XAB_"和"-XABC_"这两种token都能匹配参数” -XABCDEF“,但是按照精确的原则,应该是匹配 "-XABC_"而不是"-XAB_"。
注:上面的说法是我按照注释,加上自己的猜想。但是根据MaybeMatches函数的实现,我感觉与注释的行为并不一致。开发者可能自己也糊涂了。实际上,java vm参数设计上不会出现上面举例的情况,反倒是开发者自己想多了。有兴趣的读者可以自行看代码了解。
CmdlineParseArgumentAny,template <typename TArg> CmdlineParseArgument
CmdlineParseArgumentAny是一个纯虚类,只是定义了解析器的接口。关键的接口是:
MaybeMatches : 比较match的接口
ParseArgument : 提取参数并转换的函数
它的实现类就是 CmdlineParseArgument,这个类是一个模板类,必须指定具体类型才能实例化。
现在看看CmdlineParseArgument的实现。
CmdlineParseArgumentInfo类
CmdlineParseArgument的很多信息都是放在这个类中的,所有有必要先了解这个类的结构。代码在art/cmdline/detail/cmdline_parse_argument_detail.h:88,不在列代码了,只列出关键的成员
std::vector<const char*> names_ : 这是Builder.Define函数给出的列表,用于做参数匹配等操作
bool using_blanks_: names_里面是否至少有一个name包含了 "_", 有则true,否则为false
std::vector<TokenRange> tokenized_names_ 将names_ tokenized化后的结果,方便match
std::vector<TokenRange> simple_names_ 除去掉"_"字符所有名字
appending_values_: 如果调用了AppendValues就会为true。这样会将所有的值放在同一key下
has_range_, TArg min_, max_; 调用了WithRange后,就会设置为true,并填充min,max的值
has_value_map_, std::vector<std::pair<const char*, TArg>> value_map_ 调用WithValueMap就会使用它们
has_value_list_, std::vector<TArg> value_list_,调用WithValues就会使用它们。主要value_list_和name_它们是用索引一一对应的,比如names_[0]就对应value_list_[0]
成员函数MaybeMatches用于实现对参数的匹配。
CmdlineParseArgument.MaybeMatches
见CmdlineParseArgumentInfo.MaybeMatches的实现。CmdlineParseArgument.ParseArgument
该函数返回CmdlineResult对象,表示成功或者失败。如果失败了,这个对象内包含有失败的描述信息。这个函数有两部分组成:1. 解析参数, 2. 转换值,由函数ParseArgumentSingle来实现。
参数解析的过程是这样的:
如果using_blanking为false,那么,直接调用ParseArgumentSingle,走value_list_的映射处理
否则,提取出"_"映射的部分,比如"foo:bar",提取出blank_value == "bar",然后调用ParseArgumentSingle
CmdlineParseArgument.ParseArgumentSingle
这个函数的实现看似很长,实际上,可以分为好几种情况进行处理。最终是调用SaveArgument函数,将值存入到OptionsMap中,这个函数就完成任务了。hash_value_map_ == true
从value_map_中映射取得值art/cmdline/detail/cmdline_parse_argument_detail.h:386 CmdlineParseArgument.ParseeArgumentSingle
if (argument_info_.has_value_map_) { for (auto&& value_pair : argument_info_.value_map_) { const char* name = value_pair.first; if (argument == name) { return SaveArgument(value_pair.second); } } ....... }
has_value_list_ == true
从names_和value_list_中取得值art/cmdline/detail/cmdline_parse_argument_detail.h:409 CmdlineParseArgument.ParseeArgumentSingle
if (argument_info_.has_value_list_) { size_t arg_def_idx = 0; for (auto&& value : argument_info_.value_list_) { auto&& arg_def_token = argument_info_.names_[arg_def_idx]; if (arg_def_token == argument) { return SaveArgument(value); } ++arg_def_idx; } .... }
appending_values_ == true
这有两个步骤:1. 调用load_argument_方法(这是一个重载了operator()的类对象),获取已经加入的值,或者是新建一个值来加入
2. 调用type_parser.ParseAndAppend方法把值解析并加入进去
art/cmdline/detail/cmdline_parse_argument_detail.h:438 CmdlineParseArgument.ParseeArgumentSingle
if (argument_info_.appending_values_) { TArg& existing = load_argument_(); CmdlineParseResult<TArg> result = type_parser.ParseAndAppend(argument, existing); assert(!argument_info_.has_range_); return result; }
最后
调用type_parser.Parse方法直接进行解析art/cmdline/detail/cmdline_parse_argument_detail.h:447 CmdlineParseArgument.ParseeArgumentSingle
CmdlineParseResult<TArg> result = type_parser.Parse(argument);
type_parser
type_parser这变量的类型,是UserTypeInfo,即art/cmdline/detail/cmdline_parse_argument_detail.h:311 CmdlineParseArgument
using UserTypeInfo = CmdlineType<TArg>;CmdLineType的定义是一个空的,但是,它有若干个半实例化的类型定义:
art/cmdline/cmdline_types.h:42
template <typename T> struct CmdlineType : CmdlineTypeParser<T> { }; // Specializations for CmdlineType<T> follow: // Parse argument definitions for Unit-typed arguments. template <> struct CmdlineType<Unit> : CmdlineTypeParser<Unit> { Result Parse(const std::string& args) { if (args == "") { return Result::Success(Unit{}); // NOLINT [whitespace/braces] [5] } return Result::Failure("Unexpected extra characters " + args); } }; template <> struct CmdlineType<JDWP::JdwpOptions> : CmdlineTypeParser<JDWP::JdwpOptions> { ... Result Parse(const std::string& options) { .... } Result ParseJdwpOption(const std::string& name, const std::string& value, JDWP::JdwpOptions* jdwp_options) { .... } static const char* Name() { return "JdwpOptions"; } }; template <size_t Divisor> struct CmdlineType<Memory<Divisor>> : CmdlineTypeParser<Memory<Divisor>> { using typename CmdlineTypeParser<Memory<Divisor>>::Result; Result Parse(const std::string arg) { .... } .... }; template <> struct CmdlineType<double> : CmdlineTypeParser<double> { Result Parse(const std::string& str) { .... } static const char* Name() { return "double"; } }; template <> struct CmdlineType<unsigned int> : CmdlineTypeParser<unsigned int> { Result Parse(const std::string& str) { .... } static const char* Name() { return "unsigned integer"; } }; template <> struct CmdlineType<std::vector<std::string>> : CmdlineTypeParser<std::vector<std::string>> { Result Parse(const std::string& args) { ... } Result ParseAndAppend(const std::string& args, std::vector<std::string>& existing_value) { ... } static const char* Name() { return "std::vector<std::string>"; } }; ..... template <> struct CmdlineType<XGcOption> : CmdlineTypeParser<XGcOption> { Result Parse(const std::string& option) { // -Xgc: already stripped ..... } static const char* Name() { return "XgcOption"; } }; .... template<> struct CmdlineType<BackgroundGcOption> : CmdlineTypeParser<BackgroundGcOption>, private BackgroundGcOption { Result Parse(const std::string& substring) { .... } static const char* Name() { return "BackgroundGcOption"; } }; template <> struct CmdlineType<LogVerbosity> : CmdlineTypeParser<LogVerbosity> { Result Parse(const std::string& options) { ..... } static const char* Name() { return "LogVerbosity"; } }; template <> struct CmdlineType<TestProfilerOptions> : CmdlineTypeParser<TestProfilerOptions> { ..... public: Result ParseAndAppend(const std::string& option, TestProfilerOptions& existing) { .... } static const char* Name() { return "TestProfilerOptions"; } static constexpr bool kCanParseBlankless = true; }; template<> struct CmdlineType<ExperimentalFlags> : CmdlineTypeParser<ExperimentalFlags> { Result ParseAndAppend(const std::string& option, ExperimentalFlags& existing) { .... } static const char* Name() { return "ExperimentalFlags"; } };
当在WithType的时候给出什么参数, C++就会在这里挑选一个备用的半实例化CmdlineType类,而这个CmdlineType就知道如何解析该类型的参数了。
我没有列出具体的解析代码,是因为这些不重要,重要的是了解到它们的实现机理。
注意到,半实例化的CmdlineType都继承了CmdlineTypeParser类。这是因为,CmdlineType必须声明两个函数Parser和ParseAndAppend。但是,很多情况下,是不需要ParseAndAppend函数的。ART为了方便,就声明了一个CmdlineTypeParser类,大家都继承它。这样,如果需要ParseAndAppend函数时,就定义它,不需要时,就用基础类CmdlineTypeParser的,避免了重复声明。
当我们在using UserTypeInfo = CmdlineType<TArg>; 时候,C++编译器就根据TArg就选好了具体实现的CmdlineType了。因此,直接调用它的对象的Parser或者ParseAndAppend函数即可。
至于UntypedArgumentBuilder.WithType函数,只是创建了一个ArgumentBuilder类实例,只要把TArg参数传递出去,解析器就可以自动选定了。
RuntimeArgumentMap
最后来说说这个Map对象。RuntimeArgumentMap定义来自art/runtime/runtime_options.h:50
template <typename TValue> struct RuntimeArgumentMapKey : VariantMapKey<TValue> { RuntimeArgumentMapKey() {} explicit RuntimeArgumentMapKey(TValue default_value) : VariantMapKey<TValue>(std::move(default_value)) {} // Don't ODR-use constexpr default values, which means that Struct::Fields // that are declared 'static constexpr T Name = Value' don't need to have a matching definition. }; ...... struct RuntimeArgumentMap : VariantMap<RuntimeArgumentMap, RuntimeArgumentMapKey> { // This 'using' line is necessary to inherit the variadic constructor. using VariantMap<RuntimeArgumentMap, RuntimeArgumentMapKey>::VariantMap; // Make the next many usages of Key slightly shorter to type. template <typename TValue> using Key = RuntimeArgumentMapKey<TValue>; // List of key declarations, shorthand for 'static const Key<T> Name' #define RUNTIME_OPTIONS_KEY(Type, Name, ...) static const Key<Type> Name; #include "runtime_options.def" };
要读懂这个代码,需要一些技巧。
首先,我们看到有Variant这个关键字:VariantMap, VariantMapKey,这说明这个Map是可以存储任意类型的数据的。
其次,按照我们一般的观念,要存储任意类型,只有两种方法:1. 用union,2: 用void*只保存其指针,那么,可以推定VariantMap的value只能是二者之一
第三,不管是union还是void*,都必须知道数据的类型,否则数据无法访问,那么VariantMapKey<TValue>的模板参数TValue肯定是用来指出参数类型的;
第四,如果是作为key,那么只能是一个变量值,不能是一个类型。那么RUNTIME_OPTION_KEY宏显然是定义了一个静态key变量。显然 runtime_options.def文件里面,就是用宏RUNTIME_OPTION_KEY定义了一堆key。之所以用宏+runtime_options.def的方式定义,只是因为,在头文件中声明后,还必须在源文件中定义。可以参阅
art/runtime/runtime_options.cc:32
#define RUNTIME_OPTIONS_KEY(Type, Name, ...) const RuntimeArgumentMap::Key<Type> RuntimeArgumentMap::Name {__VA_ARGS__}; // NOLINT [readability/braces] [4] #include "runtime_options.def"展开之后,就是key的定义了。
VariantMapKey类的定义,大家可以自行看源码(art/runtime/base/variant_map.h), 它的作用,就是将所有对TValue类型的数据进行操作:创建、拷贝、删除等。不再赘述。
然后我们看看VariantMap类的实现。这里,关键是找它存储成员 StorageMap storage_map_, 而这个StoreageMap的定义就是:
art/runtime/base/variant_map.h:402
using StorageMap = std::map<const detail::VariantMapKeyRaw*, void*, KeyComparator>;现在清楚了,它正是用void*存储数据的。
写在最后的话
本来,参数解析即算不上什么重要的模块,也算不上什么高深技术,即便一个菜鸟,用if-else结构,也完全能够写出解析器,无非看起来很low而已。稍微有点经验的,也可以利用虚函数,写出一个结构不错、也容易维护解析器来。ART没有这样做,而是利用模板,写了一个很漂亮、很高深的解析器。这个解析器无疑要比其他类型的解析器运行速度要快,至于快多少,除非写一个“常规”的解析器,才能比较出来。实际上,这是我读到的ART源码中最复杂的一块了,激起我下决心把它搞清楚。我觉得,相对于这部分代码的本身意义,解读的方法,更值得向大家推荐:
先搞明白它的意图,要实现什么功能,达到什么效果
想一个一般的实现思路
按照这个思路,去源码中寻找实现这个思路的关键点,在这个过程中,成员变量比成员函数更重要,数据比过程更重要
迭代这个过程,从高层函数到底层函数,层层推进,就能摸清出它的实现方法。
最后,我给ART用模板的方法实现这个功能,给一个中肯的评价:反人类!我曾经有段时间很迷恋模板,甚至研究了boost的实现,用模板实现了简单的lambda表达式。最顶峰时,用模板写了一个ARM机器码反汇编器。但是后来发现,几乎没有人能够再看懂这些代码,即便我自己也需要花些时间来温习。既然如此,用模板还有什么意义?一个设计良好的虚拟类+简单的模板运用,是不是比这样做更清晰、更容易理解、更容易维护呢?
相关文章推荐
- ART深入浅出4--了解Dex文件格式(1)
- ART深入浅出3--了解Boot.art和boot-*.art
- ART深入浅出5--了解Dex文件格式(2)
- ART深入浅出6--了解Dex文件格式(3)
- 深入浅出 Java Concurrency (1) : J.U.C的整体认识
- 深入浅出了解阿尔法宽带路由器
- TODO 了解java虚拟机、dalvik、art
- 从头认识java-14.2 进一步了解数组
- 对CMM的了解认识
- 通过了解MySpace的六次重构经历,来认识分布式系统到底该如何创建.
- 深入浅出了解Struts的处理流程
- 认识一个人靠缘分;了解一个人靠耐心;征服一个人靠智慧;处好一个人靠包容。人,相互帮扶才感到温暖;事,共同努力才感到简单;路,有伴同行才感到平坦;友,相互牵挂才感到情深。坚持与人为善,不遗余力地成就他人,也将在不知不觉中成就自己。一人亦人,众人为天;谍事在人,成事在天。
- C#接口的深入了解【深入浅出】
- 深入浅出了解 JavaScript 中的 this
- [原]Unity3D深入浅出 - 认识开发环境中的GameObject菜单栏
- C#视频---对.net的简单认识及对vs的简单了解
- 通过了解MySpace的六次重构经历,来认识分布式系统到底该如何创建
- struts2初学心得(对初步认识了解有帮助)
- 认识solr结构,了解核心的文件目录