干货技巧:单模块编译再见adb push迎接sync命令

ADB Sync 深度解析:从 lunch 到 system 分区同步的完整链路

背景

我们平时如果是在公司做安卓设备类开发的话,一般我们都是修改某一个模块,比如framework.jar,services.jar,Launcher,SystemUI等,修改这些模块后我们不太可能像模拟器一样方便直接使用一个make方式,因为make方式在实体手机等设备上太慢了,一般采用是单编某个模块如make services等,然后再adb push services xxxx。

这种方式有一个比较大的痛点,就是你得非常清楚你make模块是啥,而且make模块后在ubuntu电脑上的out目录具体哪个目录下,目标手机上是哪个目录,一个字符都不可以写错。

案例:

adb push out/target/product/gemini/system/framework/services.jar /system/framework/services.jar

一旦错了字符肯定就无法成功,要么报错,要么push地方不对半天找不到原因,或者说make xxx模块会导致一堆so进行更新,自己又不知道哪些so。

所以这种make 单模块,push单模块方式有如下几个痛点:

1、不清楚目标的相关依赖,不知道push具体有哪些,需要程序员自身非常清楚模块的make后输出目标是啥,一旦make 目标会有相关的波及so依赖等情况,就很可能验证不起作用情况,浪费时间。

2、push目标路径因为可能比较长,有可能出错,一旦错误就无法生效

3、也有的版本push framework.jar时候需要push一堆相关的优化文件等

所以基于上面痛点,有vip学员朋友给马哥分享了一个很好的简单命令adb sync来解决。

在这里插入图片描述
在这里再次对这个学员表示感谢。

1、实战adb sync使用

编译单目标:

. build/envsetup.sh 
breakfast gemini
make framework-minus-apex

这样就编译出来了framework.jar,然后不采用常用的push方式,使用sync方式

看看核心的 adb sync 命令使用格式:

adb: usage: adb sync [-l] [-n]  [-z ALGORITHM] [-Z] [-q] [PARTITION]

然后正式使用


$ adb root;adb remount
$ adb sync system
/system/: 6807 files pushed, 19 skipped. 24.9 MB/s (1617253547 bytes in 61.879s)
$ adb reboot
$ adb logcat -s Activity
--------- beginning of system
06-05 23:20:28.842  2632  2632 V Activity: onCreate 1111 com.android.settings.FallbackHome@8ef16b: null

2. adb sync 命令源码解析与分区映射

2.1 入口:CLI 命令分发(电脑adb是客户端)

当你敲下 adb sync system,代码入口在 packages/modules/adb/client/commandline.cpp:2007

// commandline.cpp:2007-2060
} else if (!strcmp(argv[0], "sync")) {
    std::string src;
    bool list_only = false;
    bool dry_run = false;
    CompressionType compression = CompressionType::Any;
    // 解析选项: -l(list), -n(dry-run), -z(压缩), -Z(不压缩)

    if (optind == argc) {
        src = "all";              // 不传参数 = 同步所有分区
    } else if (optind + 1 == argc) {
        src = argv[optind];       // 如 "system"
    } else {
        error_exit("...");
    }

    // 硬编码的8个支持分区
    std::vector<std::string> partitions{"data",   "odm",        "oem",   "product",
                                        "system", "system_ext", "vendor"};

    bool found = false;
    for (const auto& partition : partitions) {
        if (src == "all" || src == partition) {
            std::string src_dir{product_file(partition)};   // 关键!
            if (!directory_exists(src_dir)) continue;
            found = true;
            if (!do_sync_sync(src_dir, "/" + partition, list_only, compression, dry_run)) {
                return 1;
            }
        }
    }
}

2.2 product_file():主机路径 → 设备路径的桥接

// commandline.cpp:78-84
static std::string product_file(const std::string& file) {
    const char* ANDROID_PRODUCT_OUT = getenv("ANDROID_PRODUCT_OUT");
    if (ANDROID_PRODUCT_OUT == nullptr) {
        error_exit("product directory not specified; set $ANDROID_PRODUCT_OUT");
    }
    return std::string{ANDROID_PRODUCT_OUT} + OS_PATH_SEPARATOR_STR + file;
}

这就是 lunch 设置的环境变量被用上的地方。

所以 adb sync system 最终会调用:

do_sync_sync(
    "$ANDROID_PRODUCT_OUT/system",   // 主机源目录
    "/system",                        // 设备目标目录
    ...
)

注意:不是只同步某个单独文件,而是递归遍历主机上的整个 system/ 目录树,把它和设备的 /system/ 做差异对比。


3. 核心算法:copy_local_dir_remote 的增量同步逻辑

这是 adb sync 最核心的函数,位于 packages/modules/adb/client/file_sync_client.cpp:1339

3.1 整体流程

// file_sync_client.cpp:1339-1437
static bool copy_local_dir_remote(SyncConnection& sc, std::string lpath, std::string rpath,
                                  bool check_timestamps, bool list_only,
                                  CompressionType compression, bool dry_run) {
    sc.NewTransfer();

    ensure_trailing_separators(lpath, rpath);

    // 第一步:递归构建文件列表
    std::vector<copyinfo> file_list;
    std::vector<std::string> directory_list;
    // ...
    if (!local_build_list(sc, &file_list, &directory_list, lpath, rpath)) {
        return false;
    }

函数先是调用 local_build_list 把主机上 $ANDROID_PRODUCT_OUT/system/ 下面的所有文件(递归遍历子目录)收集成一个列表,后续的同步决策全部基于这个列表。

3.2 local_build_list:递归遍历主机目录

// file_sync_client.cpp:1279-1327
static bool local_build_list(SyncConnection& sc, std::vector<copyinfo>* file_list,
                             std::vector<std::string>* directory_list, const std::string& lpath,
                             const std::string& rpath) {
    std::vector<copyinfo> dirlist;
    std::unique_ptr<DIR, int (*)(DIR*)> dir(opendir(lpath.c_str()), closedir);

    dirent* de;
    while ((de = readdir(dir.get()))) {
        if (IsDotOrDotDot(de->d_name)) continue;

        std::string stat_path = lpath + de->d_name;
        struct stat st;
        if (lstat(stat_path.c_str(), &st) == -1) { continue; }

        copyinfo ci(lpath, rpath, de->d_name, st.st_mode);
        if (S_ISDIR(st.st_mode)) {
            dirlist.push_back(ci);           // 目录暂存,稍后递归
        } else {
            if (!should_push_file(st.st_mode)) {
                ci.skip = true;              // 跳过特殊文件(字符设备等)
            }
            ci.time = st.st_mtime;           // 记录本地 mtime
            ci.size = st.st_size;            // 记录本地 size
            file_list->push_back(ci);        // 普通文件加入列表
        }
    }
    dir.reset();

    // 递归处理子目录
    for (const copyinfo& ci : dirlist) {
        directory_list->push_back(ci.rpath);
        local_build_list(sc, file_list, directory_list, ci.lpath, ci.rpath);
    }
    return true;
}

这一步结束后,file_list 包含了整个 system/ 目录下所有文件的路径、大小、修改时间。每个文件都会生成一个 copyinfo 结构体。

3.3 copyinfo 结构体

// file_sync_client.cpp:85-105
struct copyinfo {
    std::string lpath;   // 主机上的完整路径
    std::string rpath;   // 设备上的完整路径
    time_t time;         // 本地文件 mtime
    mode_t mode;         // 文件权限
    size_t size;         // 文件大小
    bool skip;           // true = 跳过, 不推送
};

3.4 增量对比:谁来决定哪些文件要推送?(最关键的部分)

回到 copy_local_dir_remote,文件列表构建完成后,紧接着就是核心差异对比逻辑

// file_sync_client.cpp:1399-1414
if (check_timestamps) {
    // 向设备发送所有文件的 lstat 请求
    for (const copyinfo& ci : file_list) {
        if (!sc.SendLstat(ci.rpath)) {
            sc.Error("failed to send lstat");
            return false;
        }
    }
    // 读取设备返回的 stat 信息,逐个对比
    for (copyinfo& ci : file_list) {
        struct stat st;
        if (sc.FinishStat(&st)) {
            if (st.st_size == static_cast<off_t>(ci.size) && st.st_mtime == ci.time) {
                ci.skip = true;  // <-- 大小和时间都匹配,标记为跳过!
            }
        }
    }
}

这就是增量同步的决策核心:

  • 向设备批量发送 lstat 请求,获取设备上每个文件的元数据
  • 对比文件大小 + 修改时间戳
  • 两者都匹配skip = true,不传输
  • 任一不匹配 → 需要传输,进入下面的推送阶段

不需要校验和(checksum),没有 MD5/SHA 计算。纯粹的 size + mtime 判等。

3.5 执行推送

// file_sync_client.cpp:1418-1431
for (const copyinfo& ci : file_list) {
    if (!ci.skip) {
        if (list_only) {
            sc.Println("would push: %s -> %s", ci.lpath.c_str(), ci.rpath.c_str());
        } else {
            if (!sync_send(sc, ci.lpath, ci.rpath, ci.time, ci.mode, false, compression, dry_run)) {
                return false;
            }
        }
    } else {
        skipped++;
    }
}

未跳过的文件逐个调用 sync_send 发送,已跳过的计入 skipped 统计。

3.6 sync_send:单个文件发送的细节

// file_sync_client.cpp:1040-1091
static bool sync_send(SyncConnection& sc, const std::string& lpath, const std::string& rpath,
                      unsigned mtime, mode_t mode, bool sync, CompressionType compression,
                      bool dry_run) {
    // 二次确认:sync 模式下再次 lstat 对比
    if (sync) {
        struct stat st;
        if (sync_lstat(sc, rpath, &st)) {
            if (st.st_mtime == static_cast<time_t>(mtime)) {
                sc.RecordFilesSkipped(1);
                return true;  // 又被跳过了
            }
        }
    }

    // 小文件 (< 64KB): 一次性读取并发送
    if (st.st_size < SYNC_DATA_MAX) {
        std::string data;
        android::base::ReadFileToString(lpath, &data, true);
        sc.SendSmallFile(rpath, mode, lpath, rpath, mtime, data.data(), data.size(), dry_run);
    } else {
        // 大文件 (>= 64KB): 分块流式传输,支持压缩
        sc.SendLargeFile(rpath, mode, lpath, rpath, mtime, compression, dry_run);
    }
    return sc.ReadAcknowledgements(sync);
}

传输策略:

  • 小文件(<64KB):一次读出、一次发送
  • 大文件(>=64KB):64KB 分块流式传输,可选用 Brotli / LZ4 / Zstd 压缩

4. daemon 端:设备上如何接收文件

4.1 服务入口

设备端 ADB daemon 通过下面这条路由开始处理 sync 请求(daemon/services.cpp:322):

} else if (name.starts_with("sync:")) {
    return create_service_thread("sync", file_sync_service);
}

4.2 主循环

// daemon/file_sync_service.cpp:861
void file_sync_service(unique_fd fd) {
    std::vector<char> buffer(SYNC_DATA_MAX);
    while (handle_sync_command(fd.get(), buffer)) {
    }
}

handle_sync_command() 根据协议 ID 分发到不同的处理函数。

5.3 文件接收 (handle_send_file)

// daemon/file_sync_service.cpp:347-414
static bool handle_send_file(borrowed_fd s, const char* path, uint32_t* timestamp,
                             uid_t uid, gid_t gid, uint64_t capabilities, mode_t mode,
                             CompressionType compression, bool dry_run,
                             std::vector<char>& buffer, bool do_unlink) {
    syncmsg msg;
    unique_fd fd;

    if (!dry_run) {
        // 安全审计日志
        __android_log_security_bswrite(SEC_TAG_ADB_SEND_FILE, path);

        // 创建目标文件 (O_EXCL 是原子性的)
        fd.reset(adb_open_mode(path, O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, mode));

        // 如果父目录不存在,自动创建目录 (同时应用 fs_config 权限)
        if (fd < 0 && errno == ENOENT) {
            if (!secure_mkdirs(Dirname(path))) {
                SendSyncFailErrno(s, "secure_mkdirs failed");
                goto fail;
            }
            fd.reset(adb_open_mode(path, O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, mode));
        }

        // 设置文件所有者
        if (fchown(fd.get(), uid, gid) == -1) { /* ... */ }

        // 恢复 SELinux 上下文
        selinux_android_restorecon(path, 0);

        // 设置文件权限 (包含 setuid 位恢复)
        fchmod(fd.get(), mode);
    }

    // 接收并写入文件数据
    if (!handle_send_file_data(s, std::move(fd), timestamp, compression)) {
        goto fail;
    }

    // 更新文件 capabilities
    if (!update_capabilities(path, capabilities)) {
        SendSyncFailErrno(s, "update_capabilities failed");
        goto fail;
    }

    // 返回 ID_OKAY 给客户端
    msg.status.id = ID_OKAY;
    msg.status.msglen = 0;
    return WriteFdExactly(s, &msg.status, sizeof(msg.status));
    // ...
}

daemon 端处理 push 时会自动:

  1. secure_mkdirs:如果目标目录不存在,自动创建目录并按 fs_config 设置正确的权限和 SELinux 标签
  2. fchown:设置文件的 uid/gid
  3. selinux_android_restorecon:恢复文件的 SELinux 上下文
  4. fchmod:设置文件权限模式
  5. update_capabilities:更新文件的 Linux capabilities

这些操作确保 push 过去的文件具有正确的权限和安全标签,和 ROM 构建时完全一致。


5. 总结:完整链路回顾

在这里插入图片描述原文地址及更多安卓系统开发技巧:
https://mp.weixin.qq.com/s/oD7Kvxc3USqLEhK3odEnGA

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

千里马学框架

帮助你了,就请我喝杯咖啡

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值