Android 应用安装背后的 "快递小哥":installd 的故事

75 阅读5分钟

一、公司架构:项目经理与快递小哥

想象 Android 系统是一家 "应用安装公司":

  • PKMS(PackageManagerService)  是穿着西装的项目经理,坐在办公室里(system 权限)负责接收用户安装请求、规划流程

  • installd 是穿着工作服的快递小哥,真正跑腿干活(拥有 root 权限),负责执行安装的实际操作

项目经理不会亲自搬箱子,而是通过一个 "内部电话系统"(socket 套接字)给快递小哥派任务。这个电话系统的号码是 "installd",专门用于两者沟通。

二、快递小哥的上班流程

2.1 入职报到:installd 的启动

清晨,公司老板(init 进程,pid=1)翻开员工花名册(init.rc 文件),看到这行记录:

plaintext

service installd /system/bin/installd
    class main
    socket installd stream 600 system system

老板下令:"installd,开工!" 于是快递小哥正式入职,并且老板给了他一部专用电话(创建 installd socket),号码是 "installd",权限设置为只能内部人员(system 用户)拨打。

快递小哥的工作入口在 installd.cpp 的 main 函数,他上班后的第一件事就是:

  1. 整理办公桌(初始化全局目录)
  2. 打扫仓库(初始化数据目录)
  3. 拿起电话听筒(监听 socket),开始等待任务

2.2 整理办公桌:初始化目录

快递小哥需要知道各种包裹该放哪里,所以上班第一件事就是记住这些地址:

  • 应用仓库:/data/app/

  • 私密应用仓库:/data/priv-app/

  • 应用配件仓库:/data/app-lib/

  • 临时仓库:/mnt/asec

  • 系统应用仓库:/system/app/、/vendor/app/ 等

这些地址记录在 initialize_globals 函数里,就像快递小哥的地址簿:

c

运行

int initialize_globals() {
    // 从环境变量获取数据根目录
    if (get_path_from_env(&android_data_dir, "ANDROID_DATA") < 0) return -1;
    
    // 拼接出各个子目录路径
    if (copy_and_append(&android_app_dir, &android_data_dir, "app") < 0) return -1;
    if (copy_and_append(&android_app_private_dir, &android_data_dir, "priv-app") < 0) return -1;
    // ...更多目录初始化
}

2.3 打扫仓库:初始化目录结构

接下来快递小哥需要确保仓库结构正确,比如:

  • 创建用户包裹区:/data/user/

  • 创建主用户包裹区:/data/user/0/

  • 将 /data/user/0 链接到 /data/data,方便旧系统兼容

这部分工作在 initialize_directories 函数中,就像快递小哥按区域划分仓库:

c

运行

int initialize_directories() {
    // 读取当前仓库版本号
    char version_path[PATH_MAX];
    snprintf(version_path, PATH_MAX, "%s.layout_version", android_data_dir.path);
    
    // 创建用户数据目录
    char *user_data_dir = build_string2(android_data_dir.path, "user/");
    char *primary_data_dir = build_string3(android_data_dir.path, "user/", "0");
    
    // 建立目录链接
    if (access(primary_data_dir, R_OK) < 0) {
        symlink("/data/data", primary_data_dir); // 将user/0链接到data
    }
    // ...更多目录处理
}

2.4 接听电话:处理任务

电话铃声响起(socket 收到消息),快递小哥开始处理任务。他的工作流程是:

  1. 接听电话(accept socket 连接)

  2. 听清楚任务长度(读取命令长度)

  3. 记录任务内容(读取命令内容)

  4. 查看任务清单(命令表),找到对应的处理方法

  5. 执行任务,返回结果

核心代码就像这样:

c

运行

int main() {
    // 初始化...
    
    int lsocket = android_get_control_socket("installd");
    listen(lsocket, 5); // 开始接听电话
    
    for (;;) {
        int s = accept(lsocket, ...); // 接听来电
        for (;;) {
            unsigned short count;
            readx(s, &count, sizeof(count)); // 听任务长度
            char buf[1024];
            readx(s, buf, count); // 听任务内容
            buf[count] = 0;
            
            execute(s, buf); // 执行任务
        }
        close(s); // 挂电话
    }
}

static int execute(int s, char cmd[1024]) {
    char arg[100+1];
    int n = 0;
    arg[0] = cmd;
    // 解析命令参数...
    
    // 查看任务清单
    for (int i=0; i<命令表长度; i++) {
        if (strcmp(cmds[i].name, arg[0]) == 0) {
            if (参数数量匹配) {
                cmds[i].func(参数, 回复); // 执行对应任务函数
            }
            break;
        }
    }
    // 返回结果...
}

2.5 任务清单:快递小哥的工作内容

快递小哥能处理 25 种任务,记录在命令表里:

c

运行

struct cmdinfo cmds[] = {
    {"ping", 0, do_ping},         // 测试连通性("你好,在吗?")
    {"install", 5, do_install},   // 安装应用(核心任务)
    {"dexopt", 9, do_dexopt},     // 优化应用代码
    {"remove", 3, do_remove},     // 卸载应用
    {"getsize", 8, do_get_size},  // 获取应用大小
    // ...更多任务
};

比如当收到 "install" 命令时,就会调用 do_install 函数,完成复制 APK、优化 DEX、设置权限等实际安装工作。

三、项目经理如何派任务

3.1 项目经理的秘书:Installer 类

项目经理(PKMS)不会直接打电话,而是让秘书(Installer 类)处理沟通:

java

// SystemServer.java中启动秘书服务
private void startBootstrapServices() {
    Installer installer = mSystemServiceManager.startService(Installer.class);
}

// Installer.java初始化
public Installer(Context context) {
    mInstaller = new InstallerConnection(); // 创建通信员
}

public void onStart() {
    mInstaller.waitForConnection(); // 等待快递小哥就绪
}

3.2 打电话的流程:socket 通信

秘书打电话的流程就像这样:

  1. 拨号连接(connect):

java

private boolean connect() {
    mSocket = new LocalSocket();
    LocalSocketAddress address = new LocalSocketAddress("installd", 
            LocalSocketAddress.Namespace.RESERVED);
    mSocket.connect(address); // 拨打"installd"号码
    mIn = mSocket.getInputStream(); // 电话听筒
    mOut = mSocket.getOutputStream(); // 电话话筒
}
  1. 发送任务(writeCommand):

java

private boolean writeCommand(String cmdString) {
    byte[] cmd = cmdString.getBytes();
    int len = cmd.length;
    // 先告诉对方任务长度(2字节)
    buf[0] = (byte)(len & 0xff);
    buf[1] = (byte)((len >> 8) & 0xff);
    mOut.write(buf, 0, 2); // 说"我有X字节的任务"
    mOut.write(cmd, 0, len); // 说具体任务内容
}
  1. 等待回复(readReply):

java

private int readReply() {
    readFully(buf, 2); // 先听回复长度
    int len = (buf[0] & 0xff) | ((buf[1] & 0xff) << 8);
    readFully(buf, len); // 再听具体回复内容
    return len;
}

3.3 第一次通话:确认快递小哥在岗

秘书上班后做的第一件事就是确认快递小哥是否在岗:

java

public void waitForConnection() {
    for (;;) {
        if (execute("ping") >= 0) { // 发送"ping"命令
            return; // 收到回复,确认在岗
        }
        SystemClock.sleep(1000); // 没回应就等1秒再打
    }
}

四、总结:应用安装的完整流程

  1. 用户点击安装 APK,PKMS(项目经理)收到请求

  2. PKMS 将安装任务打包成命令(如 "install"),交给 Installer 秘书

  3. 秘书通过 socket 电话("installd" 号码)联系 installd 快递小哥

  4. 快递小哥解析命令,从命令表中找到 do_install 函数

  5. 执行实际安装工作:复制 APK 到 /data/app、优化 DEX、设置文件权限等

  6. 完成后将结果通过 socket 返回给秘书

  7. 秘书将结果汇报给 PKMS,PKMS 通知用户安装完成

installd 就像默默工作的快递小哥,虽然不直接面对用户,却是 Android 应用安装流程中最核心的执行者,所有文件操作、权限设置等实际工作都由它完成。通过 socket 通信,它与上层服务解耦,保证了系统的稳定性和安全性。