Skip to content

属性触发时机

追踪 property trigger 从属性写入、事件入队、条件匹配到 Action 执行的完整时机,并区分启动补触发、事件触发和动态加载边界。

基于android-17.0.0_r1
AndroidinitpropertyActionManagertrigger

属性触发时机 ​

本文面向已经读过 rc语法、Action解析 和 Init主循环 的读者。前两篇已经说明 on 段如何变成 Action;本文继续追踪运行阶段一个更容易误判的问题:setprop 成功后,什么条件下 Action 会被执行,什么时候只入队还没有生效,为什么启动时已有属性仍可能触发一次,以及属性触发失败后对象是否会被删除。

本文不讲 property service 的完整 SELinux 授权模型,也不把所有 init.rc 属性用例逐条翻译。读完后,读者应能从一个 on property:name=value 反查到属性写入者、队列消费者、匹配条件和执行时机;也能解释“属性已经是目标值但 Action 没有执行”和“同一属性变化触发多条 Action”这两类现场现象。

1. 条件模型 ​

1.1 两类触发 ​

一个 on 首行可以包含一个事件触发器、一个或多个属性触发器,连接符只能是 &&。事件触发器最终保存在 event_trigger_,属性条件保存在 property_triggers_;两者不是同一个字符串字段。

相关源码:

  • system/core/init/action_parser.cpp
  • ParseTriggers
  • ParsePropertyTrigger
cpp
Result<void> ParseTriggers(const std::vector<std::string>& args, Subcontext* subcontext,
                           std::string* event_trigger,
                           std::map<std::string, std::string>* property_triggers) {
    static constexpr std::string_view prop_str("property:");
    for (std::size_t i = 0; i < args.size(); ++i) {
        if (args[i].empty()) {
            return Error() << "empty trigger is not valid";
        }

        if (i % 2) {
            if (args[i] != "&&") {
                return Error() << "&& is the only symbol allowed to concatenate actions";
            }
            continue;
        }

        if (!args[i].compare(0, prop_str.length(), prop_str)) {
            if (auto result = ParsePropertyTrigger(args[i], subcontext, property_triggers);
                !result.ok()) {
                return result;
            }
        } else {
            if (!event_trigger->empty()) {
                return Error() << "multiple event triggers are not allowed";
            }
            if (auto result = ValidateEventTrigger(args[i]); !result.ok()) {
                return result;
            }
            *event_trigger = args[i];
        }
    }
    return {};
}

解析器按奇偶位置检查连接符:偶数位置必须是条件,奇数位置必须是 &&。遇到第二个非 property 条件时直接报错,因此 on boot && late-init 不是两个事件的顺序表达式,而是非法的多个事件组合。多个 property 条件则会被分别写入 map。

1.2 去重规则 ​

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

相关函数/类型:ParsePropertyTrigger

cpp
Result<void> ParsePropertyTrigger(const std::string& trigger, Subcontext* subcontext,
                                  std::map<std::string, std::string>* property_triggers) {
    static constexpr std::string_view prop_str("property:");
    std::string prop_name(trigger.substr(prop_str.length()));
    size_t equal_pos = prop_name.find('=');
    if (equal_pos == std::string::npos) {
        return Error() << "property trigger found without matching '='";
    }

    std::string prop_value(prop_name.substr(equal_pos + 1));
    prop_name.erase(equal_pos);

    if (!IsActionableProperty(subcontext, prop_name)) {
        return Error() << "unexported property trigger found: " << prop_name;
    }

    if (auto [it, inserted] = property_triggers->emplace(prop_name, prop_value); !inserted) {
        return Error() << "multiple property triggers found for same property";
    }
    return {};
}

同一 Action 不能写两次同名 property trigger。这个限制不是运行时匹配器的“最后一次覆盖”,而是解析阶段 map::emplace 失败后拒绝整个 Action。property:foo= 在语法上可以通过等号检查,但它要求属性当前值为空;它不是“属性不存在”的通配写法。

1.3 可见边界 ​

当 Action 来自 vendor/odm subcontext 且兼容性开关启用时,IsActionableProperty() 只允许 partner 前缀,或通过 CanReadProperty() 证明 vendor_init context 可读。该检查发生在 rc 解析阶段,因而“Action 没有触发”可能其实是“Action 从未进入 ActionManager”。

2. 匹配规则 ​

2.1 当前值检查 ​

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

相关函数/类型:Action::CheckPropertyTriggers

cpp
bool Action::CheckPropertyTriggers(const std::string& name, const std::string& value) const {
    if (property_triggers_.empty()) {
        return true;
    }

    if (!name.empty()) {
        auto it = property_triggers_.find(name);
        if (it == property_triggers_.end()) {
            return false;
        }
        const auto& trigger_value = it->second;
        if (trigger_value != "*" && trigger_value != value) {
            return false;
        }
    }

    for (const auto& [trigger_name, trigger_value] : property_triggers_) {
        if (trigger_name != name) {
            std::string prop_value = android::base::GetProperty(trigger_name, "");
            if (trigger_value == "*" && !prop_value.empty()) {
                continue;
            }
            if (trigger_value != prop_value) return false;
        }
    }
    return true;
}

属性事件携带的 (name, value) 只用于快速检查“这次变化的属性”;其他条件会重新从 property area 读取当前值。因此多条件 Action 的语义是“变化属性命中,同时其余条件此刻也满足”,不是把每次历史属性变化拼成一条事务。

* 的语义要按检查位置区分:当 foo 是其他条件、需要从 property area 重新读取时,代码要求当前值非空;但当 foo 正是本次 PropertyChange 携带的属性时,前面的快速分支只检查 trigger_value != "*",因此会接受这次变化携带的空值。启动补触发走的是“重新读取当前值”路径,空属性不会被 * 命中;运行时属性变化则不能只凭“* 等于非空”这句经验判断。

2.2 事件分支 ​

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

相关函数/类型:Action::CheckEvent

cpp
bool Action::CheckEvent(const EventTrigger& event_trigger) const {
    return event_trigger == event_trigger_ && CheckPropertyTriggers();
}

bool Action::CheckEvent(const PropertyChange& property_change) const {
    const auto& [name, value] = property_change;
    return event_trigger_.empty() && CheckPropertyTriggers(name, value);
}

事件队列中的 event 只匹配有同名 event_trigger_ 的 Action;属性变化只匹配没有事件触发器的 Action。于是 on boot && property:ro.hardware=* 必须等待 boot 事件,而单独的 property 变化不会执行它;反过来,on property:foo=1 不会被 boot 事件直接执行。

3. 入队路径 ​

3.1 属性服务 ​

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

相关函数/类型:NotifyPropertyChange

cpp
void NotifyPropertyChange(const std::string& name, const std::string& value) {
    // If init hasn't started its main loop, then it won't be handling property changed messages
    // anyway, so there's no need to try to send them.
    auto lock = std::lock_guard{accept_messages_lock};
    if (accept_messages) {
        PropertyChanged(name, value);
    }
}

属性值写入 property area 后,property service 通过 NotifyPropertyChange 把变化交给 init。accept_messages 仍未开启时,变化不会被送入 init 的 ActionManager;这解释了为什么启动早期属性设置与后续 property trigger 不是无条件一一对应。

3.2 init分发 ​

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

相关函数/类型:PropertyChanged

cpp
void PropertyChanged(const std::string& name, const std::string& value) {
    if (name == "sys.powerctl") {
        trigger_shutdown(value);
    } else if (name == "sys.shutdown.requested") {
        HandleShutdownRequestedMessage(value);
    }

    if (property_triggers_enabled) {
        ActionManager::GetInstance().QueuePropertyChange(name, value);
        WakeMainInitThread();
    }

    prop_waiter_state.CheckAndResetWait(name, value);
}

属性变化有三个消费者:关机控制、property trigger 队列和 wait_for_prop 等待器。它们不是同一套机制。尤其 sys.powerctl 会先走立即关机路径;即使还有其他 Action 排队,也不能把它当作普通 property trigger 等待。

3.3 队列所有权 ​

相关源码:

  • system/core/init/action_manager.cpp
  • QueuePropertyChange
  • ExecuteOneCommand
cpp
void ActionManager::QueuePropertyChange(const std::string& name, const std::string& value) {
    auto lock = std::lock_guard{event_queue_lock_};
    event_queue_.emplace(std::make_pair(name, value));
}

void ActionManager::QueueEventTrigger(const std::string& trigger) {
    auto lock = std::lock_guard{event_queue_lock_};
    event_queue_.emplace(trigger);
}

两个入队函数共享同一把 event_queue_lock_,但保存的 variant 类型不同;下面的消费代码只展示“从队头找到匹配 Action”的核心阶段,完成清理逻辑见本节后的源码片段。

cpp
void ActionManager::ExecuteOneCommand() {
    std::vector<EventTrigger> triggered_events;
    {
        auto lock = std::lock_guard{event_queue_lock_};
        while (current_executing_actions_.empty() && !event_queue_.empty()) {
            for (const auto& action : actions_) {
                if (std::visit([&action](const auto& event) {
                                   return action->CheckEvent(event);
                               }, event_queue_.front())) {
                    current_executing_actions_.emplace(action.get());
                }
            }
            if (EventTrigger* trigger = std::get_if<EventTrigger>(&event_queue_.front())) {
                triggered_events.emplace_back(std::move(*trigger));
            }
            event_queue_.pop();
        }
    }

    if (current_executing_actions_.empty()) return;
    auto action = current_executing_actions_.front();
    action->ExecuteOneCommand(current_command_);
    ++current_command_;
    // ... event timestamp bookkeeping and oneshot cleanup continue here ...
}

这里的锁只保护队列和“找出待执行 Action”的阶段;真正命令执行在解锁后进行,避免 builtin 反过来设置属性造成锁重入。一个队列项会扫描当前 actions_,命中的 Action 按注册顺序进入执行队列;属性变化本身只是唤醒和入队,不等于命令已经执行。

4. 启动补触发 ​

4.1 开关时机 ​

相关源码:

  • system/core/init/init.cpp
  • property_enable_triggers_action
  • queue_property_triggers_action
cpp
static Result<void> property_enable_triggers_action(const BuiltinArguments&) {
    /* Enable property triggers. */
    property_triggers_enabled = 1;
    return {};
}

static Result<void> queue_property_triggers_action(const BuiltinArguments&) {
    ActionManager::GetInstance().QueueBuiltinAction(property_enable_triggers_action,
                                                     "enable_property_trigger");
    ActionManager::GetInstance().QueueAllPropertyActions();
    return {};
}

启动末尾不是简单地把开关设为 1,而是先把一个 builtin Action 放入队列,再追加 QueueAllPropertyActions()。这一设计让当前 property area 的值在启动阶段统一重新检查,而不是要求每个 ro.* 属性都必须在 trigger 开关之后再次变化。

4.2 主循环消费 ​

相关源码:

  • system/core/init/init.cpp
  • SecondStageMain
cpp
if (bootmode == BootMode::CHARGER_MODE) {
    am.QueueEventTrigger("charger");
} else {
    am.QueueEventTrigger("late-init");
}

// Run all property triggers based on current state of the properties.
am.QueueBuiltinAction(queue_property_triggers_action, "queue_property_triggers");

late-init 和“当前属性补触发”都是队列项,执行先后由 ActionManager 的队列顺序决定;后续属性变化则由 property service 直接入队。读日志时,看到属性已经存在,只能证明补触发具备输入,不能证明对应 Action 已经轮到执行。

5. 执行时机 ​

5.1 单条命令 ​

相关源码:

  • system/core/init/action_manager.cpp
  • ExecuteOneCommand
  • system/core/init/action.cpp
  • ExecuteCommand
cpp
void Action::ExecuteCommand(const Command& command) const {
    android::base::Timer t;
    auto result = command.InvokeFunc(subcontext_);
    auto duration = t.duration();

    if (!result.has_value() || duration > 50ms ||
        android::base::GetMinimumLogSeverity() <= android::base::DEBUG) {
        LOG(INFO) << "Command '" << command.BuildCommandString() << "' action="
                  << BuildTriggersString() << " (" << filename_ << ":" << command.line()
                  << ") took " << duration.count() << "ms and "
                  << (result.ok() ? "succeeded" : "failed: " + result.error().message());
    }
}

命令执行器记录耗时和结果,但失败不会自动从 ActionManager 删除 Action。普通 Action 仍可被后续事件或属性变化再次匹配;这与 service 的重启策略不是一回事。只有通过 QueueBuiltinAction 创建的 oneshot Action 才会在命令序列完成后从 actions_ 移除。

5.2 顺序与并发 ​

ExecuteOneCommand() 每次只推进当前 Action 的一条命令;命令执行期间可能产生新的 event 或 property queue 项,但它们要等当前执行队列处理到可切换点。trigger builtin 的作用是入队新事件,不是递归执行该事件的全部 Action。

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

相关函数/类型:ExecuteOneCommand

cpp
if (current_command_ == action->NumCommands()) {
    current_executing_actions_.pop();
    current_command_ = 0;
    if (action->oneshot()) {
        auto eraser = [&action](std::unique_ptr<Action>& a) { return a.get() == action; };
        actions_.erase(std::remove_if(actions_.begin(), actions_.end(), eraser), actions_.end());
    }
}

完成最后一条命令时,当前 Action 才从执行队列移除;oneshot 还会从所有者 actions_ 删除。命令失败不走这段“成功清理”之外的回滚流程,因此文件、属性和已启动 Service 等副作用不会由 ActionManager 自动撤销。

6. 动态加载 ​

6.1 当前事件边界 ​

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

相关函数/类型:LazilyLoadedActionsCantBeTriggeredByTheSameTrigger

cpp
// "start" script loads "lazy" script. Even though "lazy" scripts
// defines "on boot" action, it's not executed by the current "boot"
// event because it's already processed.
std::string start_script = "on boot\n"
                           "load " + std::string(lazy.path) + "\n"
                           "execute 1";

测试把 lazy 文件在 boot Action 执行过程中才加入 actions_,断言只执行一次 execute 1。这证明 ActionManager 不会回放已经扫描过的同一个 event;动态加载的 Action 需要后续新的 event 或 property change 才有机会匹配。

6.2 后续事件 ​

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

相关函数/类型:LazilyLoadedActionsCanBeTriggeredByTheNextTrigger

cpp
std::string start_script = "on boot\n"
                           "load " + std::string(lazy.path) + "\n"
                           "execute 1\n"
                           "trigger next";

对应测试把 lazy 文件定义为 on next,在同一个 boot Action 的最后入队 next,断言 execute 2 最终执行。关键不是“load 会立即运行新 Action”,而是新 Action 在下一个队列事件到来时才成为消费者。

7. 失败排查 ​

7.1 解析失败 ​

以下现象说明问题发生在 ActionManager 之前:

现象应先检查结论边界
property trigger found without matching '='on property: 首行Action 没有建立
multiple property triggers found for same property同名条件是否重复Action 没有加入 actions_
unexported property trigger foundvendor/odm 可见属性和 SELinux context不是运行时未触发
multiple event triggers are not allowed是否写了两个 event不是事件队列丢失

7.2 入队但未执行 ​

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

相关函数/类型:HasMoreCommands

cpp
bool ActionManager::HasMoreCommands() const {
    auto lock = std::lock_guard{event_queue_lock_};
    return !current_executing_actions_.empty() || !event_queue_.empty();
}

若日志能证明 property change 已进入队列,仍需区分三种情况:当前 Action 仍在执行、条件检查失败、或 init 主循环被 wait_for_prop / exec service 等路径暂时阻塞。队列存在不是命令成功的证明。

7.3 命令失败 ​

命令失败会在 ExecuteCommand() 中记录源文件、行号、trigger 和错误文本;Action 本身仍保留。排查应继续确认后续属性变化是否会再次命中,而不是假设 init 会自动重试一次失败命令。

8. 触发实验 ​

用一个最小 property trigger 实验分别观察当前值、变化事件和执行结果:

  1. 写入 on property:init.test.trigger=1,命令只设置一个独立结果属性;
  2. 先确认 rc 解析日志,再读取结果属性,区分“已注册”和“已执行”;
  3. 在 trigger 前后分别记录 getprop init.test.trigger,证明变化输入是否存在;
  4. 查看 init 日志中的 processing action 和 Command ... succeeded/failed,确认队列消费者与执行结果;
  5. 将 trigger 改成 on boot && property:init.test.trigger=*,比较 boot 事件和 property-only 事件的差异;
  6. 在 Action 内先 setprop 再 trigger next,验证属性变化和事件入队的先后关系;
  7. 删除结果属性或让命令返回错误,确认失败不会自动回滚已产生的副作用。

本文证明了 Android 17 中 property trigger 的解析约束、分支化的 * 匹配、事件/属性分支、属性入队、启动补触发、逐命令执行和动态加载边界;没有证明特定设备的 property_contexts 授权、厂商 Action 是否被实际导入、SELinux allow 规则、命令副作用的业务正确性或完整启动成功。