0%

容器里的 Android:小包调试验证方法

背景:云手机项目,Android 以容器方式运行在 Docker / K8S 之上,底座为 ARM64 服务器。本文从“容器里为什么不能靠 reboot 验证”出发,先给出 Android 用户空间的重启分层,再拆解 toolbox、init、ActivityManager 三套控制面,最后用一个 HAL 二进制的小包替换案例(以 sensors HAL 为例)走完整个验证流程。命令速查、代码路径和坑清单见文末附录。

说明:除非特别注明,本文的设备端命令默认在 root shell 下执行(adb root 后 adb shell);am 系列命令普通 shell 即可。


1. 问题:容器里的 reboot 不是重启,是回滚

在普通 Android 终端上,改一个 HAL 二进制、一个系统 so 或者一个 APK,最省事的验证流程是:

adb push <产物> <目标路径>
adb reboot

reboot 会把所有进程、服务、缓存清干净,起来之后一定是新代码。代价只是几十秒。

容器化 Android 打破了这个前提。云机实例的根文件系统来自镜像 + 可写层(Docker / K8S 底座的具体实现不同,但表现一致):实例重启后回到镜像基线,push 到 /system、/vendor 的文件不会保留。于是“重启”在容器里变成了两件事:

  • 重启整机 / 重启容器:可能回到基线,等于把刚 push 的文件回滚掉,不能用来验证;
  • 重启用户空间的某一层:进程重新加载磁盘上的新文件,push 的内容还在,这才是正确手段。

本文说的“小包调试”,就是:

只编译、只推送改动的那一个产物(HAL 二进制、native daemon、framework jar、APK),然后精确重启持有它的那一层,而不是刷整包、更不是重启整机。

要把这件事做对,先得建立一张映射:改了哪个文件 → 谁加载它 → 最小重启动作是什么。

1.1 五种重启粒度

粒度 影响范围 容器里是否可行 是否保留 push 的文件
整机 reboot kernel + rootfs + 所有进程 可行,但会回滚 否
容器 / 实例重启 Android rootfs + 所有进程 取决于可写层持久化策略 通常否
Framework 软重启 zygote、system_server、部分 native 服务 可行 是
单个 init service 一个 HAL / daemon 可行 是
单个 App 一个应用进程 可行 是

真正需要掌握的是后三行:Framework、单个 service、单个 App。它们分别对应三套不同的控制面(见第 3 节)。


2. 方案总览:Android 用户空间的重启分层

先看一张分层图。容器里的 Android 用户空间,大致是下面这个依赖关系:

flowchart TB
    K["kernel / kernel modules"] --> I["init (PID 1)"]
    I --> N["native services<br/>netd / surfaceflinger / audioserver"]
    I --> H["vendor HAL services<br/>sensors / camera / audio ..."]
    I --> Z["zygote"]
    Z --> S["system_server<br/>AMS / WMS / SensorService ..."]
    S --> A["App processes"]

    style K fill:#f5f5f5
    style I fill:#e8f4ff
    style H fill:#fff2cc
    style N fill:#fff2cc
    style Z fill:#d9ead3
    style S fill:#d9ead3
    style A fill:#fce5cd

这张图里,不同重启动作的边界非常清楚:

动作 实际影响 不影响
整机 reboot kernel、init、native services、vendor HAL、zygote、system_server、App —
stop / start native services(netd / surfaceflinger / audioserver)、zygote、system_server、App(固定列表) vendor HAL、init、kernel
am restart zygote、system_server、App native services、vendor HAL、init、kernel
stop/start <service> 指定的那个 native service 或 vendor HAL 其他所有层
am force-stop <pkg> 指定的那个 App 其他所有层

最关键的一条结论:HAL 不在 Framework 的重启范围内。 这是后面所有坑的根源(见 3.2 节和 3.4 节)。

把“改了什么”映射到“重启哪一层”,就是下面这张表:

改动产物 例子 谁加载它 最小重启动作
APK 普通应用、系统 App zygote / App 进程 am force-stop + am start
framework jar services.jar、framework.jar system_server am restart 或 stop / start
native daemon netd、surfaceflinger init service stop/start <service>
vendor HAL sensors HAL、camera HAL init service stop/start <service> 或 ctl.restart;接口清单变化时先停 framework
kernel module .ko kernel 只能 reboot;容器里通常不可行

下面的章节,就是把这张表背后的三套控制面拆开。


3. 三套控制面:toolbox、init、ActivityManager

3.1 toolbox:一个二进制,六个身份

我们每天都在敲的 getprop、setprop、start、stop,在 AOSP 11 里其实是同一个 115 KB 的二进制——toolbox。

先看设备上的实拍(AOSP 11 云机实例实测):

generic:/ # which getevent
/system/bin/getevent
generic:/ # which getprop setprop
/system/bin/getprop
/system/bin/setprop
generic:/ # which start stop
/system/bin/start
/system/bin/stop

generic:/ # ls -lh /system/bin/* | grep toolbox
lrwxrwxrwx 1 root shell    7 2026-11-10 11:46 /system/bin/getevent -> toolbox
lrwxrwxrwx 1 root shell    7 2026-11-10 11:46 /system/bin/getprop -> toolbox
lrwxrwxrwx 1 root shell    7 2026-11-10 11:46 /system/bin/modprobe -> toolbox
lrwxrwxrwx 1 root shell    7 2026-11-10 11:46 /system/bin/setprop -> toolbox
lrwxrwxrwx 1 root shell    7 2026-11-10 11:46 /system/bin/start -> toolbox
lrwxrwxrwx 1 root shell    7 2026-11-10 11:46 /system/bin/stop -> toolbox
-rwxr-xr-x 1 root shell 115K 2026-10-29 14:42 /system/bin/toolbox

这是一个典型的 multicall 二进制,思路和 busybox 一样:所有工具编进同一个可执行文件,启动时看 argv[0] 决定跑哪个入口。

// system/core/toolbox/toolbox.c(节选)
int main(int argc, char** argv) {
    char* cmd = strrchr(argv[0], '/');
    char* name = cmd ? (cmd + 1) : argv[0];

    for (size_t i = 0; tools[i].name; i++) {
        if (!strcmp(tools[i].name, name)) {
            return tools[i].func(argc, argv);
        }
    }
    printf("%s: no such tool\n", argv[0]);
    return 127;
}

这些软链接不是手工建的,而是构建规则生成的:

// system/core/toolbox/Android.bp
symlinks: [
    "getevent",
    "getprop",
    "modprobe",
    "setprop",
    "start",
    "stop",
],

在这棵 AOSP 11 里,toolbox 只包含 6 个工具:

工具 实现 作用
getprop getprop.cpp 读系统属性
setprop setprop.cpp 写系统属性,带名称、长度、UTF-8 校验
start / stop start.cpp 通过 ctl.start / ctl.stop 控制 init 服务
getevent getevent.c 读 /dev/input 事件,调试触摸、按键
modprobe modprobe.cpp 加载内核模块

其他常见命令(ls、cat、ps、cp 等)由 toybox 提供;toolbox 只保留这几个系统控制/调试工具。不同 Android 版本 toolbox 的成员可能变化,本文以这棵 AOSP 11 为准。

两个直接推论:

  1. 这 6 个命令共享同一份代码和同一个二进制。真要替换 toolbox,它们会一起变。
  2. start / stop 不是独立程序,只是 toolbox 里的一层薄封装。它们的全部逻辑,就是往属性服务写 ctl.start / ctl.stop。

3.2 start / stop:只重启“框架最小集合”

start.cpp 的核心只有两段:

// system/core/toolbox/start.cpp
static void ControlService(bool start, const std::string& service) {
    if (!android::base::SetProperty(start ? "ctl.start" : "ctl.stop", service)) {
        std::cerr << "Unable to " << (start ? "start" : "stop") << " service '" << service
                  << "'\nSee dmesg for error reason." << std::endl;
        exit(EXIT_FAILURE);
    }
}
// system/core/toolbox/start.cpp(节选,省略了 zygote_secondary 的判断)
static void ControlDefaultServices(bool start) {
    std::vector<std::string> services = {
        "iorapd",
        "netd",
        "surfaceflinger",
        "audioserver",
        "zygote",
    };

    if (start) {
        for (auto& service : services) {
            ControlService(true, service);
        }
    } else {
        for (auto it = services.crbegin(); it != services.crend(); ++it) {
            ControlService(false, *it);
        }
    }
}

不带参数时,stop / start 只操作这个固定列表:

iorapd → netd → surfaceflinger → audioserver → zygote

顺序也是有讲究的:

  • start 正序:先起底层依赖,netd、surfaceflinger、audioserver 就绪,最后起 zygote,框架才有一致的运行环境;
  • stop 逆序:先停 zygote(system_server 和 App 一起下),再停 audioserver、surfaceflinger、netd、iorapd。

这段代码解释了两个常见误解:

误解一:stop 会停掉所有服务。

不会。它只停这 5 个(多架构再加 zygote_secondary)。vendor HAL、hwservicemanager、servicemanager、vold、logd 都不会被停。

误解二:stop / start 能把新 push 的 HAL 加载进来。

不会。sensors HAL 是独立的 init service,不在默认列表里,stop / start 之后它还是原来那个进程,跑的仍然是旧二进制。

不过 start / stop 支持带参数,这就是“只重启一个服务”的入口:

stop vendor.sensors-hal-2-1-mock
start vendor.sensors-hal-2-1-mock

它的内部就是循环对每个参数写一次 ctl.stop / ctl.start。另外,start / stop 在写属性之前会检查 uid:

// system/core/toolbox/start.cpp
if (getuid()) {
    std::cerr << "Must be root" << std::endl;
    return EXIT_FAILURE;
}

所以设备上通常要先 adb root,或者直接在一个 root shell 里执行。

3.3 init:ctl.* 才是服务控制的真正入口

toolbox 只是“写属性的手”,真正干活的是 init。

init 在属性服务里注册了一组控制消息:

// system/core/init/init.cpp(节选,省略了 sigstop_on / oneshot_on 等其他控制消息)
static const std::map<std::string, ControlMessageFunction, std::less<>>& GetControlMessageMap() {
    static const std::map<std::string, ControlMessageFunction, std::less<>> control_message_functions = {
        {"start",             DoControlStart},
        {"stop",              DoControlStop},
        {"restart",           DoControlRestart},
    };
    return control_message_functions;
}

因此下面三组命令是等价的:

stop vendor.sensors-hal-2-1-mock
setprop ctl.stop vendor.sensors-hal-2-1-mock
start vendor.sensors-hal-2-1-mock
setprop ctl.start vendor.sensors-hal-2-1-mock
setprop ctl.restart vendor.sensors-hal-2-1-mock

ctl.restart 没有直接对应的 toolbox 命令,但它就是 init 的 DoControlRestart:先停再起。

怎么知道一个服务的名字? 看它的 init rc:

# hardware/interfaces/sensors/2.1/default/android.hardware.sensors@2.1-service-mock.rc
service vendor.sensors-hal-2-1-mock /vendor/bin/hw/android.hardware.sensors@2.1-service.mock
    interface android.hardware.sensors@2.0::ISensors default
    interface android.hardware.sensors@2.1::ISensors default
    class hal
    user system
    group system
    rlimit rtprio 10 10

第一行的第一个字段 vendor.sensors-hal-2-1-mock 就是服务名,第二个字段才是二进制路径。服务名和二进制名经常不一样,stop/start 用的是服务名。

服务的运行状态可以直接读属性:

getprop init.svc.vendor.sensors-hal-2-1-mock
# running / stopped / restarting

3.4 ActivityManager:am restart 重启的是什么

am restart 是 framework 层的命令,帮助文本写得很直白:

restart
    Restart the user-space system.

实现不在 init,而在 ActivityManagerService:

// frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java
@Override
public void restart() {
    if (checkCallingPermission(android.Manifest.permission.SET_ACTIVITY_WATCHER)
            != PackageManager.PERMISSION_GRANTED) {
        throw new SecurityException("Requires permission "
                + "android.Manifest.permission.SET_ACTIVITY_WATCHER");
    }

    Log.i(TAG, "Sending shutdown broadcast...");

    BroadcastReceiver br = new BroadcastReceiver() {
        @Override public void onReceive(Context context, Intent intent) {
            Log.i(TAG, "Shutting down activity manager...");
            shutdown(10000);
            Log.i(TAG, "Shutdown complete, restarting!");
            killProcess(myPid());
            System.exit(10);
        }
    };

    Intent intent = new Intent(Intent.ACTION_SHUTDOWN);
    intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
    intent.putExtra(Intent.EXTRA_SHUTDOWN_USERSPACE_ONLY, true);
    /* For now we are not doing a clean shutdown, because things seem to get unhappy.
    mContext.sendOrderedBroadcastAsUser(intent, UserHandle.ALL, null, br, mHandler, 0, null);
    */
    br.onReceive(mContext, intent);
}

注意 AOSP 11 里这段的细节:ACTION_SHUTDOWN 的有序广播被注释掉了,代码直接调用 br.onReceive(),然后 shutdown(10000)、杀掉 system_server、System.exit(10)。所以它是:

  1. 由 system_server 自己发起的 userspace-only shutdown;
  2. 杀掉 system_server 后,zygote / init 把 framework 重新拉起来;
  3. 不碰 kernel,不碰 vendor HAL,也不碰 netd / surfaceflinger / audioserver。

这也解释了为什么 am restart 比 stop / start “轻”:它只重启 zygote + system_server + App,native 服务保持原样。

3.5 三种方式对比

stop / start am restart ctl.restart <service>
控制面 init(toolbox 写 ctl.*) ActivityManager init
影响范围 iorapd、netd、surfaceflinger、audioserver、zygote zygote、system_server、App 指定的一个 init service
是否碰 vendor HAL 否 否 是(只碰指定的那个)
App 是否优雅退出 否 是(先走 AM shutdown) 不涉及
需要 root 是 否(shell 有 SET_ACTIVITY_WATCHER) 是(写 ctl.*)
依赖 system_server 存活 否 是 否
系统卡死时能否恢复 能 不能 能
典型用途 换 framework jar、整体重置框架状态 换 framework jar、干净重启 App 层 换单个 HAL / native daemon 二进制

4. 怎么选:从“改了哪个文件”到“重启哪一层”

把前面的内容串起来,可以得到一张决策图:

flowchart TD
    A["改了某个产物"] --> B{"是 APK 吗?"}
    B -->|是| B1["am force-stop &lt;pkg&gt;<br/>am start ..."]
    B -->|否| C{"是 framework jar 吗?"}
    C -->|是| C1{"需要 App 优雅退出吗?"}
    C1 -->|是| C2["am restart"]
    C1 -->|否,或 system_server 已卡死| C3["stop / start"]
    C -->|否| D{"是 native service / HAL 吗?"}
    D -->|是| D1{"该服务的 SensorInfo<br/>或接口清单变了吗?"}
    D1 -->|否| D2["ctl.restart &lt;service&gt;"]
    D1 -->|是| D3["先停 framework<br/>再 stop/start 该服务"]
    D -->|否| E["kernel / module<br/>只能 reboot,容器里通常不可行"]

其中“接口清单有没有变”是 HAL 场景最容易踩的坑。对 sensors HAL 来说,判断依据不是“代码改了多少”,而是 SensorInfo 有没有变:

// frameworks/native/services/sensorservice/SensorDevice.cpp
bool SensorDevice::sensorIsEquivalent(const sensor_t& prevSensor, const sensor_t& newSensor) {
    bool equivalent = true;
    if (prevSensor.handle != newSensor.handle ||
            (strcmp(prevSensor.vendor, newSensor.vendor) != 0) ||
            (strcmp(prevSensor.stringType, newSensor.stringType) != 0) ||
            (strcmp(prevSensor.requiredPermission, newSensor.requiredPermission) != 0) ||
            (prevSensor.version != newSensor.version) ||
            (prevSensor.type != newSensor.type) ||
            (std::abs(prevSensor.maxRange - newSensor.maxRange) > 0.001f) ||
            (std::abs(prevSensor.resolution - newSensor.resolution) > 0.001f) ||
            (std::abs(prevSensor.power - newSensor.power) > 0.001f) ||
            (prevSensor.minDelay != newSensor.minDelay) ||
            (prevSensor.fifoReservedEventCount != newSensor.fifoReservedEventCount) ||
            (prevSensor.fifoMaxEventCount != newSensor.fifoMaxEventCount) ||
            (prevSensor.maxDelay != newSensor.maxDelay) ||
            (prevSensor.flags != newSensor.flags)) {
        equivalent = false;
    }
    return equivalent;
}

只要 handle、type、vendor、typeAsString(stringType)、maxRange、resolution、power、minDelay、maxDelay、flags 里任何一项变了,sensorservice 就认为这是“另一套传感器”,不是“同一个服务换了个二进制”。sensors HAL 的具体 fatal 路径见 5.3 节。


5. 实战:换一个 bin(以 sensors HAL 为例)

小包调试的对象通常有三类:framework jar、native so、可执行 bin。它们的替换路径和重启边界不同。

5.1 jar / so / bin:先找加载者

产物 典型目标路径 谁加载它 替换前 替换后验证
jar /system/framework/*.jar system_server stop / am restart pidof system_server、dumpsys、logcat、原功能
so /system/lib64、/vendor/lib64 某个进程 dlopen 停掉加载它的进程 /proc/<pid>/maps、原功能
bin /system/bin、/vendor/bin/hw init service(或 shell) stop <service> init.svc、pidof、logcat、原功能

三点经验:

  • jar:替换 /system/framework 下的 jar 后,必须让 system_server 重新启动;services.jar 这类核心 jar 建议 stop / start,普通 system service 可以先试 am restart。
  • so:先确认谁加载了它。进程一旦 mmap 了旧 so,直接覆盖文件不会让它重新加载,必须先停掉加载者(App、native daemon 或 HAL)。
  • bin:先看它是不是 init service。是的话用 stop/start <service>;不是的话(例如从 shell 临时启动的调试程序),直接杀进程再启动。

找某个 so 被哪些进程加载:

grep -l "libfoo.so" /proc/[0-9]*/maps
# 输出 /proc/<pid>/maps,再用 ps -A 查 pid 对应的进程

5.2 案例现场:一个 HAL 二进制

下面用 AOSP 自带的 sensors mock 服务作为 bin 的例子。jar / so 的替换思路相同,只是“谁加载它”和“重启哪一层”不同:

项 值
模块名 android.hardware.sensors@2.1-service.mock
设备路径 /vendor/bin/hw/android.hardware.sensors@2.1-service.mock
init 服务名 vendor.sensors-hal-2-1-mock
rc 文件 /vendor/etc/init/android.hardware.sensors@2.1-service-mock.rc
假设的变更 给 HAL 增加或删除一个对外条目,接口清单发生变化

先只编译这个模块:

# 主机端
m android.hardware.sensors@2.1-service.mock

产物在:

out/target/product/<product>/vendor/bin/hw/android.hardware.sensors@2.1-service.mock

5.3 为什么不能只重启 HAL

如果 Framework 还在运行,直接:

setprop ctl.restart vendor.sensors-hal-2-1-mock

会发生什么?

sensorservice 在初始化时给 HAL 注册了死亡回调:

// frameworks/native/services/sensorservice/SensorDevice.cpp
void SensorsHalDeathReceivier::serviceDied(
        uint64_t /* cookie */,
        const wp<::android::hidl::base::V1_0::IBase>& /* service */) {
    ALOGW("Sensors HAL died, attempting to reconnect.");
    SensorDevice::getInstance().prepareForReconnect();
}

HAL 重启后,sensorservice 会走 reconnect():重新连接 HIDL 服务、重新拉传感器列表,然后对比新旧列表:

// frameworks/native/services/sensorservice/SensorDevice.cpp
void SensorDevice::reconnect() {
    Mutex::Autolock _l(mLock);
    mSensors = nullptr;

    auto previousActivations = mActivationCount;
    auto previousSensorList = mSensorList;

    mActivationCount.clear();
    mSensorList.clear();

    if (connectHidlServiceV2_0() == HalConnectionStatus::CONNECTED) {
        initializeSensorList();

        if (sensorHandlesChanged(previousSensorList, mSensorList)) {
            LOG_ALWAYS_FATAL("Sensor handles changed, cannot re-enable sensors.");
        } else {
            reactivateSensors(previousActivations);
        }
    }
    mReconnecting = false;
}

如果这次替换让新旧清单不一致,sensorHandlesChanged() 就会返回 true,于是:

LOG_ALWAYS_FATAL("Sensor handles changed, cannot re-enable sensors.");

LOG_ALWAYS_FATAL 会直接把 system_server 带崩。也就是说,在 Framework 运行时单独重启一个“接口清单变了”的 HAL,不是重启,是自杀。

sequenceDiagram
    participant U as 操作者
    participant I as init
    participant H as sensors-HAL
    participant S as sensorservice

    U->>I: setprop ctl.restart vendor.sensors-hal-2-1-mock
    I->>H: stop
    H-->>S: serviceDied()
    S->>S: prepareForReconnect()
    I->>H: start (新二进制)
    S->>H: reconnect / getSensorsList
    H-->>S: 新的接口清单
    S->>S: sensorHandlesChanged() == true
    S-->>S: LOG_ALWAYS_FATAL<br/>system_server 崩溃

5.4 正确顺序:先下框架,再换 HAL,最后一起起来

安全的顺序是:

  1. 停 Framework(zygote / system_server / native services);
  2. 确认 system_server 已经退出;
  3. 停 sensors HAL;
  4. remount、push 新二进制、chmod、sync;
  5. 先启动 sensors HAL;
  6. 再启动 Framework。

这样 sensorservice 是在“全新启动”时第一次看到新的接口清单,不会走 reconnect() 的等价性检查。

主机端完整流程:

# 主机端
adb root

# 1. 停 Framework
adb shell stop

# 2. 等 system_server 真正退出
adb shell 'sleep 2; pidof system_server'      # 预期输出为空

# 3. 停 HAL
adb shell stop vendor.sensors-hal-2-1-mock
adb shell getprop init.svc.vendor.sensors-hal-2-1-mock   # 预期 stopped

# 4. 替换二进制
adb remount
adb push <out目录>/vendor/bin/hw/android.hardware.sensors@2.1-service.mock /vendor/bin/hw/
adb shell chmod 755 /vendor/bin/hw/android.hardware.sensors@2.1-service.mock
adb shell sync

# 5. 先起 HAL
adb shell start vendor.sensors-hal-2-1-mock

# 6. 再起 Framework
adb shell start

设备端等价命令(adb shell 内):

stop
sleep 2
pidof system_server

stop vendor.sensors-hal-2-1-mock
start vendor.sensors-hal-2-1-mock
start

两个细节:

  • 先停 HAL 再 push。正在运行的二进制被覆盖时,adb push 可能报 Text file busy(ETXTBSY)。先停进程,文件锁才会释放。
  • stop 之后等 system_server 真的退出。stop 返回不代表进程已经死透;如果 sensorservice 还活着时新 HAL 就起来了,仍然可能撞上 reconnect()。
sequenceDiagram
    participant U as 操作者
    participant I as init
    participant H as sensors-HAL
    participant S as sensorservice

    U->>I: stop (framework)
    I->>S: 停 zygote → system_server 退出
    U->>I: stop vendor.sensors-hal-2-1-mock
    I->>H: stop
    U->>H: 覆盖二进制 + chmod + sync
    U->>I: start vendor.sensors-hal-2-1-mock
    I->>H: start (新二进制)
    U->>I: start (framework)
    I->>S: 新建 sensorservice
    S->>H: getSensorsList
    H-->>S: 新的接口清单(首次连接,无旧列表可对比)
    S->>S: 正常注册

5.5 验证

替换完成后,按“进程 → 服务 → 框架 → 业务”的顺序验证。

① HAL 进程是不是新起来的

getprop init.svc.vendor.sensors-hal-2-1-mock
# running

pidof android.hardware.sensors@2.1-service.mock

pidof 的值应该和替换前不同;如果没变,说明旧进程还在,新二进制没被加载。

② sensorservice 有没有看到新列表

dumpsys sensorservice

关注两点:

  • 接口清单是否与预期一致(例如新增或删除了一个条目);
  • 日志里是否出现 Sensor handles changed, cannot re-enable sensors.(期望:不出现)。
logcat -d | grep -i "Sensor handles changed"

预期不会出现上面那条 LOG_ALWAYS_FATAL。

③ 原有功能有没有回归

如果这次替换只改实现、不改对外行为,原有功能应保持不变;用原来的验证 App / 脚本跑一遍回归即可。

④ 故障态 vs 正确态

检查点 只重启 HAL(错误) 先下框架再换 HAL(正确)
sensorservice 走 reconnect() 全新初始化
接口清单 与旧清单不同,判定为 changed 首次读到新清单
HAL 启动时机 system_server 仍存活 framework 已停止
结果 LOG_ALWAYS_FATAL,system_server 崩溃 正常注册
push 的文件 可能 ETXTBSY 正常覆盖

6. 方法论:小包调试的五步判断

把上面的案例抽象一下,任何“只换一个小产物”的验证都可以按这五步走:

flowchart LR
    A["1. 定位产物<br/>out/..."] --> B["2. 找加载者<br/>谁 mmap / exec 它"]
    B --> C["3. 查重启边界<br/>进程 / service / framework"]
    C --> D["4. 选最小动作"]
    D --> E["5. 验证<br/>pid / init.svc / 日志 / 业务"]

第 1 步:定位产物。 先确认 m <module> 生成的到底是哪个文件,不要拿错 out 目录。

第 2 步:找加载者。 是 App 加载的 APK?是 system_server 加载的 jar?是 init exec 的 HAL?还是某个 native daemon dlopen 的 so?加载者决定了重启粒度。

第 3 步:查重启边界。 特别要确认:这个服务的“接口清单”有没有变。HAL 的 SensorInfo、Camera 的 device list、Audio 的 port config,都属于这一类。

第 4 步:选最小动作。 能只重启一个 service,就不要重启 framework;能 am restart,就不要 stop/start;能软重启,就不要 reboot。

第 5 步:验证。 至少看三样:

  • 进程:pidof / ps -A,确认新进程;
  • 服务:getprop init.svc.<service>,确认 running;
  • 业务:dumpsys / logcat / 原验证脚本,确认状态正确且没有 fatal。

6.1 坑清单

坑 现象 原因 规避
用 reboot 验证 push 的文件消失 容器 rootfs 回到镜像基线 改用用户空间软重启
只重启 HAL system_server 崩,日志 Sensor handles changed reconnect() 检测到接口清单变化 先停 framework,再换 HAL
直接 push 运行中的二进制 Text file busy 进程还 mmap 着旧文件 先 stop 对应 service
以为 stop 会停所有服务 HAL 还是旧进程 stop 只停固定 5 个服务 单独 stop/start <service>
以为 am restart 会重启 HAL 新二进制没生效 am restart 只重启 system_server / zygote 单独重启 HAL
stop 报 Must be root 命令失败 toolbox 检查 getuid() adb root 或用 root shell
setprop ctl.* 失败 属性写不进去 SELinux / property contexts 限制 看 dmesg,确认域权限
改完 HAL 直接 start framework 旧 HAL 进程被复用 HAL 没停,framework 只是重连 先停 HAL 再起 framework

6.2 一条经验

“重启”要按“谁加载这个文件”来选,而不是按“哪个命令听起来更彻底”来选。

stop/start 听起来最彻底,但它只覆盖框架最小集合;am restart 听起来很重,但它不碰 native 服务和 HAL;ctl.restart 听起来最轻,但如果接口清单变了,它会直接把 sensorservice 送走。


附录

附录 A:命令速查

主机端:

# 只编译单个模块
m android.hardware.sensors@2.1-service.mock

# 推送前先停 Framework / HAL
adb root
adb shell stop
adb shell stop vendor.sensors-hal-2-1-mock

# 替换
adb remount
adb push <out目录>/vendor/bin/hw/android.hardware.sensors@2.1-service.mock /vendor/bin/hw/
adb shell chmod 755 /vendor/bin/hw/android.hardware.sensors@2.1-service.mock
adb shell sync

# 启动
adb shell start vendor.sensors-hal-2-1-mock
adb shell start

设备端:

# Framework 软重启
stop
start

# 只重启一个 init service
stop <service>
start <service>
setprop ctl.restart <service>

# 状态
getprop init.svc.<service>
pidof <binary-name>
dumpsys sensorservice
logcat -d | grep -i "Sensor handles changed"

附录 B:代码路径

内容 路径
toolbox 多入口分发 system/core/toolbox/toolbox.c
toolbox 构建与软链接 system/core/toolbox/Android.bp
start / stop 实现 system/core/toolbox/start.cpp
setprop 实现 system/core/toolbox/setprop.cpp
init 控制消息 system/core/init/init.cpp
am restart 命令入口 frameworks/base/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
am restart 实现 frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java
sensors HAL 死亡回调 frameworks/native/services/sensorservice/SensorDevice.cpp
sensors 重连与列表对比 frameworks/native/services/sensorservice/SensorDevice.cpp
sensors HAL 服务定义 hardware/interfaces/sensors/2.1/default/android.hardware.sensors@2.1-service-mock.rc

附录 C:经验清单

  1. 容器里 reboot 是回滚,不是重启;验证优先用用户空间软重启。
  2. stop / start 只覆盖固定列表,不等于“重启所有服务”。
  3. am restart 只重启 system_server / zygote,不碰 native 服务和 HAL。
  4. 换 HAL 前先确认接口清单有没有变;变了就必须先停 framework。
  5. push 运行中的二进制前先停进程,避免 ETXTBSY。
  6. 用 pidof 的前后变化确认新二进制真的被加载,而不是只看 init.svc 显示 running。
  7. getprop init.svc.<service> 是判断 init 服务状态最快的方式。
  8. ctl.restart 是“停 + 起”,不是“平滑 reload”;有状态的服务要评估副作用。

附录 D:相关资源

  • AOSP system/core/toolbox/:toolbox、start、stop、getprop、setprop 的实现
  • AOSP system/core/init/:init 控制消息与服务生命周期
  • AOSP frameworks/native/services/sensorservice/:sensorservice 与 HAL 的连接、重连
  • AOSP hardware/interfaces/sensors/:sensors HAL 接口与 mock 服务
  • 本文作者: 6x
  • 本文链接: https://6xyun.cn/article/245
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-ND 许可协议。转载请注明出处!