简介:这是一份面向Android开发调试人员与C#学习者的图形化ADB工具源码工程,用C#封装了Google官方ADB命令行能力,让安装应用、查看设备状态、传输文件等操作摆脱黑窗口,转为可视化界面。资源包共96个文件、约4.77MB,以cs源码、deploy与application部署文件、exe可执行程序、dll类库、manifest清单、resx资源及ico图标为主,另含sln解决方案与csproj工程文件,结构完整可直接编译运行。内容覆盖C#语法与面向对象、.NET运行时、Process进程通信、WinForms/WPF界面设计、async/await异步编程、异常处理、文件流传输、设备枚举与权限配置、批量命令自动化等知识点,并附帮助文档与APK示例。目前已有3382人学习下载,适合想通过实战掌握C#桌面开发与移动调试技能的开发者参考借鉴。
1. 用 C# 把 ADB 包成桌面工具:为什么一线测试和开发都在自己造轮子
如果你日常要在 Windows 上反复敲adb devices、adb install、adb logcat,大概率动过「自己写个带界面的 ADB 工具」的念头。C# 实现的 Android ADB 工具,本质就是用 .NET 起一个桌面壳,把 adb.exe 当子进程调用,把命令、参数、输出、退出码封装成可点、可查、可复用的操作。它解决的不是「ADB 能不能用」,而是「批量设备、重复操作、日志留存、非命令行同事也能用」这几件事。适合谁?做 Android 自动化测试的、做设备农场运维的、以及需要把调试能力交付给产品/运营的团队。我见过太多人卡在第一步:以为要重写 ADB 协议,其实 90% 的场景只是把命令行包稳。这篇就按「能跑起来、能装进项目、能避开坑」的路径讲透。
2. 先想清楚:C# 调 ADB 的三种姿势与选型理由
2.1 为什么不是「直接 P/Invoke adb 的 API」
ADB 本身没有稳定的 C 接口给你 P/Invoke,它对外暴露的是命令行和一套 host service 协议(走 TCP 5037 端口)。常见做法是启动adb.exe子进程,通过标准输入输出交互。这条路的好处是:adb 版本升级不用改你的代码,命令行为准;坏处是:输出解析要自己扛,尤其是logcat这种流式输出。另一种是直接连 5037 端口发host:devices这类请求,能拿到更结构化的数据,但协议细节多、易随版本漂移,除非你要做设备管理中间件,否则不划算。我一般会:普通操作走子进程,设备列表这种高频查询可以加一层 5037 短连接做补充。
2.2 子进程封装的骨架代码
using System.Diagnostics; public class AdbRunner { private readonly string _adbPath; public AdbRunner(string adbPath = "adb") { _adbPath = adbPath; } // 同步执行,适合 install / push / shell 短命令 public (int code, string stdout, string stderr) Run(string args, int timeoutMs = 30000) { var psi = new ProcessStartInfo { FileName = _adbPath, Arguments = args, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true, StandardOutputEncoding = System.Text.Encoding.UTF8, StandardErrorEncoding = System.Text.Encoding.UTF8 }; using var p = new Process { StartInfo = psi }; p.Start(); // 异步读,避免缓冲区满导致死锁 var outTask = p.StandardOutput.ReadToEndAsync(); var errTask = p.StandardError.ReadToEndAsync(); if (!p.WaitForExit(timeoutMs)) { p.Kill(true); return (-1, outTask.Result, "timeout"); } return (p.ExitCode, outTask.Result, errTask.Result); } }逻辑说明:RedirectStandardOutput/Error必须开,否则拿不到输出;CreateNoWindow防止弹黑框;两个ReadToEndAsync要在WaitForExit之前启动,这是子进程封装最经典的死锁点——如果先 Wait 再读,输出超过管道缓冲区就会卡死。参数上,timeoutMs对install这种大包操作要放宽到 120000 以上,对shell getprop这种可以压到 5000。StandardOutputEncoding设 UTF8 是为了中文设备名和日志不乱码,但注意 adb 在 Windows 上部分输出是 GBK,遇到乱码要按命令类型切换编码。
2.3 异步流式执行:logcat 必须单独处理
logcat不会自己结束,用上面的同步方法会一直等到超时。正确姿势是事件驱动:
public void StartLogcat(string serial, Action<string> onLine) { var psi = new ProcessStartInfo { FileName = _adbPath, Arguments = $"-s {serial} logcat -v time", RedirectStandardOutput = true, UseShellExecute = false, CreateNoWindow = true }; var p = new Process { StartInfo = psi }; p.OutputDataReceived += (s, e) => { if (e.Data != null) onLine(e.Data); // 逐行回调,交给 UI 线程 }; p.Start(); p.BeginOutputReadLine(); // 关键:开启异步行读取 }逻辑说明:BeginOutputReadLine配合OutputDataReceived是 .NET 处理流式输出的标准组合,比自己开线程读ReadLine更省心。参数-v time让每行带时间戳,方便后续做日志切分;如果要按 tag 过滤,加-s TAG。注意回调是在线程池线程上触发的,更新 UI 必须Dispatcher.Invoke或SynchronizationContext.Post,否则会抛跨线程异常——这个坑我踩过不止一次。
3. 把常用操作做成可复用的命令层
3.1 设备发现与序列号管理
多设备场景下,所有命令都要带-s serial,否则 adb 会报「more than one device」。设备列表解析是基础功:
public List<string> GetDevices() { var (code, stdout, _) = Run("devices"); var list = new List<string>(); foreach (var line in stdout.Split('\n')) { var t = line.Trim(); if (t.EndsWith("\tdevice")) // 只取在线设备,过滤 offline/unauthorized list.Add(t.Split('\t')[0]); } return list; }逻辑说明:devices输出格式是「序列号\t状态」,状态有device、offline、unauthorized三种。只取device能避免对未授权设备发命令导致卡住。参数上,如果设备是网络调试,序列号形如192.168.1.10:5555,解析逻辑一样,但连接前要先adb connect。这里有个细节:unauthorized状态需要用户在设备上点「允许调试」,工具里最好给个提示,而不是静默过滤掉。
3.2 安装、卸载与覆盖安装的参数差异
public bool Install(string serial, string apkPath, bool replace = true) { // -r 覆盖安装,-t 允许测试包,-d 允许降级 var flags = replace ? "-r -t -d" : ""; var (code, stdout, stderr) = Run($"-s {serial} install {flags} \"{apkPath}\"", 120000); return code == 0 && stdout.Contains("Success"); }逻辑说明:-r是覆盖安装,不加会报INSTALL_FAILED_ALREADY_EXISTS;-t允许安装android:testOnly的包,测试包不加会报INSTALL_FAILED_TEST_ONLY;-d允许版本降级,调试时经常需要。注意install的退出码不一定可靠,有些失败也返回 0,所以必须检查 stdout 里有没有Success。路径带空格要用引号包住,这是 Windows 下的高频翻车点。
3.3 文件推送与拉取:权限和路径的边界
public bool Push(string serial, string local, string remote) { // remote 建议用 /sdcard/ 或 /data/local/tmp/,后者不需要额外权限 var (code, _, stderr) = Run($"-s {serial} push \"{local}\" \"{remote}\"", 60000); return code == 0; }逻辑说明:push到/sdcard/在 Android 10 以上可能受分区存储限制,/data/local/tmp/是更稳的落点,且不需要 root。参数上,大文件要调大超时;如果目标路径是目录,adb 会把文件放进去,如果是文件路径则直接覆盖。拉取用pull,方向相反,注意pull目录时要加-a保留时间戳,否则增量对比会失效。
4. 避坑与排查:ADB 封装最容易翻车的 5 个点
4.1 现象:命令偶尔卡死,界面无响应
原因:子进程输出填满管道缓冲区,主线程在WaitForExit上死等。解决:先启动异步读取再 Wait,或直接用BeginOutputReadLine。同步方法里两个ReadToEndAsync必须在WaitForExit之前调用,这是铁律。
4.2 现象:中文设备名或日志乱码
原因:adb 在 Windows 上部分输出用系统默认编码(GBK),而代码里统一设了 UTF8。解决:对devices -l、shell getprop这类可能含中文的输出,先按 GBK 解一次,检测到乱码再切 UTF8;或者干脆用chcp 65001前置命令强制 UTF8,但会受系统环境影响。
4.3 现象:多设备时命令报「more than one device」
原因:没带-s serial,adb 不知道发给谁。解决:所有操作强制传序列号,工具层做一层「当前选中设备」的状态管理,禁止裸调。如果确实要广播到所有设备,用adb -s循环,别用adb裸命令。
4.4 现象:install 返回 0 但实际没装上
原因:adb 的退出码不可靠,部分失败仍返回 0。解决:解析 stdout,必须包含Success才算成功;失败时把 stderr 和 stdout 一起展示,常见错误码如INSTALL_FAILED_INSUFFICIENT_STORAGE、INSTALL_PARSE_FAILED_NO_CERTIFICATES要能识别并给中文提示。
4.5 现象:logcat 回调里更新 UI 抛异常
原因:OutputDataReceived在线程池线程触发,WPF/WinForms 不允许跨线程操作控件。解决:用Dispatcher.Invoke或Control.BeginInvoke切回 UI 线程;高频日志要做批量刷新(比如 100ms 合并一次),否则界面会被日志刷爆。
5. 进阶:把 ADB 工具做成可交付的调试面板
5.1 用命令队列串行化设备操作
多设备并行操作时,adb server 本身是单点,并发太高会互相干扰。我一般会做一个按序列号分组的队列,同一设备的命令串行,不同设备可并行。实现上用ConcurrentDictionary<string, SemaphoreSlim>,每个序列号一把锁,简单可靠。这样批量安装 10 台设备时,不会出现某台设备命令错乱。
5.2 日志留存与关键字高亮
logcat的原始输出价值有限,工具层要做两件事:按行写入滚动文件(比如每 10MB 切一个),以及按关键字(FATAL、ANR、自定义 tag)高亮。高亮不要在回调里直接操作 UI,而是先入内存队列,由定时器批量渲染。参数上,滚动文件保留最近 5 个,避免磁盘被日志吃满。
5.3 一个可验证的最小闭环
想确认你的封装是否稳,跑这个闭环:devices拿到序列号 →install一个测试 apk →shell am start拉起主 Activity →logcat抓 10 秒 →uninstall。五步全过且无卡死、无乱码、无跨线程异常,基本就能进项目用了。我自己的习惯是每次改完命令层,先跑这个闭环,再跑多设备版本,最后才接 UI。这套流程帮我省下了大量「以为好了结果现场翻车」的后悔药。希望帮到你。
本文还有配套的精品资源,点击获取