Skip to content

Import解析

追踪 import 的属性展开、文件与目录加载、EndFile 延迟提交及顺序和错误边界。

基于android-17.0.0_r1
AndroidinitImportParserinit.rc

Import解析 ​

本文面向已经读过 rc语法 的读者。本文只回答一个问题:init 读到 import path 后,路径何时展开、文件何时真正解析、目录如何排序、嵌套 import 如何排列,以及失败是否阻断后续配置。它不重复 tokenizer,也不展开 Action 或 Service 的 option 细节。

Android 17 的关键事实是:ImportParser::ParseSection() 只记录已经展开的路径,当前文件结束时 EndFile() 才调用父 Parser::ParseConfig()。因此 import 不是 shell 的“读到即 source”,也没有旧稿所称的全局去重或循环检测集合。

1. 所有权 ​

1.1 两个列表 ​

ImportParser 临时拥有 imports_,每项是路径和源行号;父 Parser 拥有 section parser 和全局 Action/Service parser。路径进入 imports_ 后,当前文件的其余 section 仍会继续解析。

相关源码:

  • system/core/init/import_parser.cpp
  • system/core/init/import_parser.h
  • system/core/init/parser.cpp
cpp
class ImportParser : public SectionParser {
  private:
    Parser* parser_;
    std::string filename_;
    std::vector<std::pair<std::string, int>> imports_;
};

1.2 生效时机 ​

时刻发生的事尚未发生
ParseSection参数校验、属性展开、路径入列表读取目标文件
当前文件继续其他 section 提交import 目标内容解析
EndFile依次调用 ParseConfig目标文件的后续嵌套 import

2. 路径解析 ​

2.1 参数约束 ​

import 是无 body 的独立 section,只能有一个参数。下一行若仍被 ImportParser::ParseLineSection() 收到,会返回 Unexpected line found after import statement。

源码文件:system/core/init/import_parser.cpp

cpp
Result<void> ImportParser::ParseSection(
        std::vector<std::string>&& args,
        const std::string& filename, int line) {
    if (args.size() != 2) {
        return Error() << "single argument needed for import\n";
    }
    // 后续展开并缓存
}

Result<void> ImportParser::ParseLineSection(
        std::vector<std::string>&&, int) {
    return Error() << "Unexpected line found after import statement";
}

2.2 属性展开 ​

路径在解析 import 行时调用 ExpandProps()。展开结果被缓存,而不是把 ${ro.hardware} 留给后续文件系统。属性不存在时,ExpandProps 的具体结果和错误由 property 工具实现决定;Importer 只接受成功的 Result。

源码文件:system/core/init/import_parser.cpp

cpp
auto conf_file = ExpandProps(args[1]);
if (!conf_file.ok()) {
    return Error() << "Could not expand import: " << conf_file.error();
}
LOG(INFO) << "Added '" << *conf_file << "' to import list";
if (filename_.empty()) filename_ = filename;
imports_.emplace_back(std::move(*conf_file), line);

2.3 源行保存 ​

源码同时保存 filename 和 line,供后续错误定位使用;EndFile() 当前实现只按路径解析,但保存源位置说明设计上需要把导入错误关联回原始 import 行。不要把 line number 当作排序键。

3. 文件加载 ​

3.1 ParseConfig ​

父 Parser 先判断路径类型:目录走 ParseConfigDir(),其他路径走 ParseConfigFile()。这一步发生在 EndFile(),所以 import 路径错误与当前文件语法错误属于两个不同阶段。

源码文件:system/core/init/parser.cpp

cpp
bool Parser::ParseConfig(const std::string& path) {
    if (is_dir(path.c_str())) {
        return ParseConfigDir(path);
    }
    auto result = ParseConfigFile(path);
    if (!result.ok()) {
        LOG(ERROR) << "could not parse file '" << path << "': " << result.error();
        return false;
    }
    return true;
}

3.2 单文件 ​

ParseConfigFile() 读取完整文件,调用 ParseData(),EOF 时触发当前 Parser 注册的所有 EndFile()。因此一个被导入文件自己的 imports,会在该文件解析结束时再展开。

3.3 目录排序 ​

目录只收集 DT_REG 普通文件,并对完整路径排序。目录中的子目录和非普通文件不因 import 自动递归;若需要子目录,必须显式再 import。

源码文件:system/core/init/parser.cpp

cpp
std::vector<std::string> files;
while ((current_file = readdir(config_dir.get()))) {
    if (current_file->d_type == DT_REG) {
        files.emplace_back(StringPrintf(
                "%s/%s", path.c_str(), current_file->d_name));
    }
}
std::sort(files.begin(), files.end());
for (const auto& file : files) {
    if (auto result = ParseConfigFile(file); !result.ok()) {
        LOG(ERROR) << "could not import file '" << file << "': " << result.error();
    }
}

4. 提交顺序 ​

4.1 EndFile ​

EndFile() 先 move 出 imports_,清空成员,再按记录顺序调用 ParseConfig()。move 的目的不是去重,而是允许递归解析时使用同一个 ImportParser 继续收集新文件的 import。

源码文件:system/core/init/import_parser.cpp

cpp
void ImportParser::EndFile() {
    auto current_imports = std::move(imports_);
    imports_.clear();
    for (const auto& [import, line_num] : current_imports) {
        parser_->ParseConfig(import);
    }
}

4.2 嵌套关系 ​

主文件有 import first、import dir、import last 时,主文件 EOF 先解析 first;随后解析目录中的排序文件;目录文件结束时解析它自己的 import;最后才解析 last。这是“当前文件的 import 列表按顺序展开,递归发生在被调用 ParseConfig 内部”,而不是简单的文本行位置。

4.3 顺序消费者 ​

被导入文件里的 Action 和 Service 进入与主文件相同的 ActionManager/ServiceList;后续同名 Action 的执行顺序和 Service duplicate/override 结果都会看到这一解析顺序。因此目录排序不是纯日志行为,而是会影响配置对象的相对位置。

5. 启动装载 ​

5.1 LoadBootScripts ​

Android 17 没有 ro.boot.init_rc 时,LoadBootScripts() 明确解析主 init、system、system_ext、vendor、odm、product 路径;属性非空时改为只解析指定 bootscript。

源码文件:system/core/init/init.cpp

相关函数/类型:LoadBootScripts

cpp
std::string bootscript = GetProperty("ro.boot.init_rc", "");
if (bootscript.empty()) {
    parser.ParseConfig("/system/etc/init/hw/init.rc");
    if (!parser.ParseConfig("/system/etc/init")) {
        late_import_paths.emplace_back("/system/etc/init");
    }
    parser.ParseConfig("/system_ext/etc/init");
    if (!parser.ParseConfig("/vendor/etc/init")) {
        late_import_paths.emplace_back("/vendor/etc/init");
    }
    parser.ParseConfig("/odm/etc/init");
    parser.ParseConfig("/product/etc/init");
} else {
    parser.ParseConfig(bootscript);
}

5.2 可选目录 ​

部分目录失败会被记录到 late_import_paths,不等于所有已经读取的 rc 回滚。该列表由后续 init 流程使用;文章只说明 LoadBootScripts() 的返回处理,不把缺失目录解释成必然启动失败。

5.3 APEX边界 ​

APEX 配置使用专门的 parser 创建路径和 subcontext 选择,不能用通用 /system/etc/init 目录顺序推导 APEX 的全部生命周期。ImportParser 的通用语义仍是文件结束时展开,但 APEX 何时调用 parser 属于 APEX 初始化入口。

6. 错误路径 ​

6.1 展开失败 ​

ExpandProps() 返回错误时,import 不会进入 imports_;当前文件的其他已提交 Action/Service 不回滚。错误位置由 Parser 的文件和行号日志补充。

6.2 读取失败 ​

单文件读取错误返回失败;目录打不开返回 false。目录中的单个文件解析失败只记录错误并继续下一个文件,因而目录不是事务。

6.3 重复与循环 ​

Android 17 的 ImportParser 没有全局 parsed_files_ 集合,也没有循环检测。重复 import 可能重复解析同一文件;循环 import 可能持续递归。该结论来自源码缺少去重状态,不能改写成系统会自动阻止循环。

7. 导入测试 ​

7.1 顺序测试 ​

EventTriggerOrderMultipleFiles 构造六个临时文件和目录,给每个 on boot 命令编号,断言顺序为 1,2,3,4,5,6。它证明主文件 import、目录排序和嵌套 import 的组合顺序,未证明所有设备分区内容相同。

7.2 最小实验 ​

可构造主文件:import first、import dir、import last,让每个子文件写入一个不同编号的 builtin,再触发 boot。同时加入不存在路径和错误 section,分别观察读取错误、解析错误和后续文件是否继续处理。

7.3 顺序实验 ​

对一个真实 import,写出:

text
import token
  -> ExpandProps
  -> imports_ 缓存
  -> 当前文件 EndFile
  -> ParseConfig(file/dir)
  -> Action / Service 提交顺序

若服务没有出现在 ServiceList,先查它所在文件是否真的被 ParseConfig() 读取、段结束是否报错,再查 ServiceParser;不要只检查 import 行是否存在。

7.4 边界 ​

本文不证明 APEX 专用加载时机、recovery 脚本集合、vendor subcontext 的完整安全策略,也不覆盖 Action 执行和 Service 启动。结论限定于 Android 17 Parser/ImportParser 的真实实现。