ADB批量安装APK到多台安卓设备:Python与Shell自动化脚本实战

📅 2026/8/6 4:01:48
ADB批量安装APK到多台安卓设备:Python与Shell自动化脚本实战
1. 项目概述与核心价值如果你是一名移动应用开发工程师、测试工程师或者是一名需要频繁在大量安卓设备上部署应用的运维人员那么“如何实现adb同时安装多个apk到多个设备”这个问题你一定感同身受。想象一下你手头有几十台测试机每次版本迭代都需要手动连接每一台设备执行adb install命令。这个过程不仅枯燥、耗时还极易出错比如装错了版本、漏装了某台设备或者因为设备连接不稳定而中断。这种重复性劳动严重拖慢了开发测试的节奏。这个项目的核心价值就是通过自动化脚本将我们从这种低效的“体力活”中解放出来。它要解决的不是一个高深的算法问题而是一个典型的“工程效率”问题。其目标非常明确一键操作批量执行。具体来说就是编写一个脚本或工具能够自动识别当前连接的所有安卓设备并针对每一台设备依次安装一个或多个指定的APK安装包。这听起来简单但其中涉及到设备状态管理、命令并行/串行执行、错误处理、日志记录等多个实操细节把这些细节处理好才能真正做到稳定、可靠、省心。在实际工作中这个需求场景非常普遍。比如在兼容性测试中我们需要将同一个APK安装到数十款不同型号、不同系统版本的手机上在功能测试中我们可能需要将一个主APK和若干个插件APK同时部署到测试机在演示环境搭建时也需要快速在多台设备上安装好演示应用。手动操作在这些场景下是完全不可行的。因此掌握这项自动化技能是提升个人和团队工作效率的一个非常实在的切入点。接下来我将从一个多年移动端开发者的角度详细拆解实现这一目标的完整思路、技术细节、避坑指南并提供可直接复用的脚本方案。2. 核心思路与方案设计实现“多设备多APK”安装核心在于两个循环的嵌套设备循环和APK文件循环。最直接的思路是先获取设备列表然后遍历每一台设备在每台设备上再遍历每一个APK文件进行安装。这个基础模型是所有方案的起点。2.1 基础模型串行执行最简单的实现是串行模型使用adb devices命令获取在线设备列表。过滤掉列表头如List of devices attached和非法行如offline状态的设备。对于列表中的每一台设备记为 Device_A对于指定的每一个APK文件记为 APK_1 2. 执行adb -s Device_A install APK_1。这个模型的优点是逻辑清晰易于理解和调试。但缺点也很明显效率低。如果设备多、APK多总耗时是单个设备安装单个APK耗时的乘积等待时间会很长。2.2 进阶模型并行执行为了提升效率我们很自然地会想到并行。这里的“并行”通常指在设备维度上进行并行。也就是说同时对所有设备发起安装任务而不是等一台设备装完所有APK再装下一台。为什么选择在设备维度并行因为每台设备都是独立的硬件它们之间的安装操作互不干扰。而在一台设备上连续安装多个APK有时存在依赖关系比如先装基础包再装插件或者安装本身是串行的虽然可以一个接一个发命令但设备处理时需要时间。因此更高效的策略是为每一台设备启动一个独立的安装进程这个进程负责将该设备需要安装的所有APK串行安装完毕。这样假设有N台设备安装时间就从“所有设备串行时间之和”缩短到了“单台设备安装最慢的那台所花费的时间”效率提升立竿见影。2.3 技术方案选型基于上述思路我们有几种常见的技术实现方案Shell脚本 (Bash/Batch)优点无需额外环境依赖直接利用操作系统和ADB的能力。利用操作符在Unix-like系统或start命令在Windows批处理可以方便地实现后台进程从而达到并行效果。缺点跨平台兼容性差需分别写Bash和Batch脚本错误处理和进程管理相对简陋。适用场景快速实现、轻量级任务、运行环境固定的情况。Python脚本优点跨平台语法简洁拥有强大的标准库如subprocess用于执行命令threading/multiprocessing或concurrent.futures用于并发控制argparse用于参数解析。可以非常精细地控制并发度、处理超时、收集和整理日志。缺点需要目标机器安装Python环境。适用场景需要复杂逻辑、良好错误处理、日志记录和跨平台部署的中大型项目。Gradle/构建脚本插件优点与Android项目原生集成可以在构建流程中直接调用方便CI/CD集成。缺点灵活性相对脚本较差更依赖于特定的项目结构。适用场景在Android Studio项目内作为自定义构建任务使用。对于大多数追求灵活性和控制力的开发者Python脚本是首选方案。它平衡了功能、易用性和可维护性。因此下文将主要围绕Python方案展开并会对比给出Shell脚本的核心思路。3. 核心细节解析与实操要点在动手编码之前有几个关键细节必须提前厘清它们直接决定了脚本的健壮性和实用性。3.1 设备列表的获取与过滤adb devices命令的输出不是纯净的设备ID列表。它的典型输出如下List of devices attached emulator-5554 device 192.168.1.100:5555 device ABCDEF0123456789 unauthorized ZXCVBNM987654321 offline我们需要解析这个输出提取出状态为device的行中的设备标识符如emulator-5554,192.168.1.100:5555。状态为unauthorized未授权或offline离线的设备无法执行安装命令必须被过滤掉。注意unauthorized状态常见于真机首次连接时需要在设备上点击“允许USB调试”。脚本应能识别并提示这类设备而不是简单地忽略或报错退出。3.2 安装命令的参数与选项adb install命令有几个常用参数直接影响安装行为-r: 替换已存在的应用相当于升级安装。在批量安装中强烈建议加上此参数因为它可以覆盖安装避免因已存在旧版本而失败。-t: 允许安装测试APK针对android:testOnly的应用。如果你的APK是测试包需要此参数。-d: 允许版本降级安装。在需要回退版本测试时有用。-g: 授予APK清单文件中声明的所有运行时权限。在Android 6.0上可以避免安装后首次运行还需要手动点授权。在批量脚本中一个稳妥的安装命令基础模板是adb -s device_id install -r apk_path。根据需求再添加-t或-g。3.3 并发控制与资源管理虽然我们希望在设备维度并行但“无限制的并行”可能带来问题。如果你同时连接了50台设备并瞬间发起50个并行的adb install进程可能会耗尽本机的CPU或I/O资源导致脚本本身响应缓慢。ADB服务端可能因瞬间请求过多而出现不稳定。不利于观察日志50个进程的输出混在一起。因此引入并发度控制是必要的。例如可以设置一个最大并发数如5或10让脚本最多同时处理这个数量的设备其他设备排队等待。这可以通过Python的ThreadPoolExecutor或Semaphore轻松实现。3.4 错误处理与日志记录批量操作中部分失败是常态。脚本必须具备良好的错误处理能力命令执行失败adb install可能因各种原因失败签名冲突、空间不足、不兼容等。脚本需要捕获这些错误通过检查命令的返回码并记录下是哪个设备、哪个APK失败了以及失败原因尽可能从输出中提取。设备断开连接在安装过程中设备可能意外断开。脚本需要能感知到这种变化并将对应设备的任务标记为失败而不是无限期挂起。日志分离理想的日志系统应该能同时提供两种视图全局进度日志记录脚本开始、结束、总体进度。每设备详细日志每个设备的安装过程输出stdout和stderr最好能分别重定向到独立的日志文件中方便事后排查特定设备的问题。4. 基于Python的完整实现方案下面我将展示一个功能相对完整的Python脚本它包含了上述所有考量并添加了参数解析、进度显示等实用功能。4.1 环境准备与依赖确保你的开发机上已经具备Python 3.6现代Python版本即可。ADB命令行工具已安装并添加到系统PATH环境变量中。在终端输入adb version能正常显示版本号即表示配置成功。目标APK文件准备一个或多个需要安装的APK文件。无需安装额外的Python第三方库我们将完全使用标准库subprocess,concurrent.futures,argparse,os,re,time。4.2 脚本代码详解我们将脚本保存为batch_install.py。#!/usr/bin/env python3 批量安装APK到多个Android设备 用法: python batch_install.py [APK文件路径1] [APK文件路径2] ... [选项] import subprocess import re import argparse import os import sys from concurrent.futures import ThreadPoolExecutor, as_completed import time def get_connected_devices(): 获取当前所有已连接且状态为‘device’的ADB设备ID列表。 返回: 设备ID列表例如 [emulator-5554, 192.168.1.100:5555] try: # 执行 adb devices 命令 result subprocess.run([adb, devices], capture_outputTrue, textTrue, timeout10) output result.stdout except subprocess.TimeoutExpired: print([错误] 执行 adb devices 命令超时请检查ADB服务。) return [] except FileNotFoundError: print([错误] 未找到adb命令请确保ADB已安装并加入PATH环境变量。) return [] devices [] lines output.strip().splitlines() # 跳过第一行 “List of devices attached” for line in lines[1:]: if line.strip(): # 跳过空行 parts line.split() if len(parts) 2 and parts[1] device: devices.append(parts[0]) elif len(parts) 2: print(f[信息] 设备 {parts[0]} 状态为 {parts[1]}将被跳过。) return devices def install_apk_to_device(device_id, apk_paths, install_args, device_log_dir): 在单个设备上安装一系列APK文件。 参数: device_id: 设备标识符 apk_paths: APK文件路径列表 install_args: 传递给 adb install 的额外参数列表如 [-r, -g] device_log_dir: 存放该设备详细日志的目录 返回: (device_id, success_count, total_count, error_messages) os.makedirs(device_log_dir, exist_okTrue) log_file_path os.path.join(device_log_dir, finstall_log_{device_id.replace(:, _).replace(., _)}.txt) success 0 total len(apk_paths) errors [] with open(log_file_path, w, encodingutf-8) as log_file: log_file.write(f 开始为设备 {device_id} 安装 {total} 个APK \n) log_file.write(f时间: {time.strftime(%Y-%m-%d %H:%M:%S)}\n) log_file.write(f安装参数: {install_args}\n) log_file.write(- * 50 \n) for i, apk_path in enumerate(apk_paths, 1): apk_name os.path.basename(apk_path) if not os.path.exists(apk_path): error_msg fAPK文件不存在: {apk_path} print(f[设备 {device_id}] 错误: {error_msg}) log_file.write(f[{i}/{total}] {apk_name}: 文件不存在\n) errors.append(error_msg) continue # 构建安装命令 cmd [adb, -s, device_id, install] install_args [apk_path] log_file.write(f[{i}/{total}] 执行: { .join(cmd)}\n) try: # 执行安装命令同时捕获输出 process subprocess.run(cmd, capture_outputTrue, textTrue, timeout120) # 设置2分钟超时 stdout process.stdout stderr process.stderr returncode process.returncode log_file.write(f返回码: {returncode}\n) log_file.write(f标准输出:\n{stdout}\n) if stderr: log_file.write(f标准错误:\n{stderr}\n) log_file.write(- * 30 \n) if returncode 0: if Success in stdout: print(f[设备 {device_id}] ({i}/{total}) {apk_name}: 安装成功) success 1 else: # 有时adb返回0但输出里没有Success可能是其他信息 error_msg f安装命令返回0但未检测到成功信息。输出: {stdout[:200]} print(f[设备 {device_id}] ({i}/{total}) {apk_name}: 疑似失败 - {error_msg}) errors.append(f{apk_name}: {error_msg}) else: error_msg f安装失败。返回码: {returncode}, 错误: {stderr[:200] if stderr else stdout[:200]} print(f[设备 {device_id}] ({i}/{total}) {apk_name}: 失败 - {error_msg}) errors.append(f{apk_name}: {error_msg}) except subprocess.TimeoutExpired: error_msg 安装命令执行超时超过120秒 print(f[设备 {device_id}] ({i}/{total}) {apk_name}: {error_msg}) log_file.write(f[{i}/{total}] {apk_name}: {error_msg}\n) errors.append(f{apk_name}: {error_msg}) except Exception as e: error_msg f执行命令时发生未知异常: {e} print(f[设备 {device_id}] ({i}/{total}) {apk_name}: {error_msg}) log_file.write(f[{i}/{total}] {apk_name}: {error_msg}\n) errors.append(f{apk_name}: {error_msg}) log_file.write(f\n 安装结束 \n成功: {success}/{total}\n) if errors: log_file.write(错误列表:\n) for err in errors: log_file.write(f - {err}\n) return device_id, success, total, errors def main(): parser argparse.ArgumentParser(description批量安装APK到多个已连接的Android设备) parser.add_argument(apk_files, nargs, help一个或多个APK文件的路径) parser.add_argument(-r, --replace, actionstore_true, help替换已存在的应用相当于adb install -r) parser.add_argument(-g, --grant-permissions, actionstore_true, help授予所有运行时权限相当于adb install -g) parser.add_argument(-t, --test-only, actionstore_true, help允许安装测试APK相当于adb install -t) parser.add_argument(-d, --downgrade, actionstore_true, help允许版本降级安装相当于adb install -d) parser.add_argument(-j, --jobs, typeint, default5, help最大并行设备数默认: 5) parser.add_argument(-l, --log-dir, default./install_logs, help日志文件保存目录默认: ./install_logs) args parser.parse_args() # 检查APK文件是否存在 valid_apks [] for apk in args.apk_files: if os.path.exists(apk): valid_apks.append(apk) else: print(f[警告] 跳过不存在的文件: {apk}) if not valid_apks: print([错误] 未提供任何有效的APK文件路径。) sys.exit(1) # 构建adb install参数列表 install_args [] if args.replace: install_args.append(-r) if args.grant_permissions: install_args.append(-g) if args.test_only: install_args.append(-t) if args.downgrade: install_args.append(-d) # 可以添加更多参数映射... # 获取设备列表 print(正在扫描已连接的ADB设备...) devices get_connected_devices() if not devices: print(未找到任何状态为‘device’的已连接ADB设备。请检查) print( 1. 设备是否通过USB连接或网络连接) print( 2. 设备上是否已开启‘USB调试’) print( 3. 对于真机连接后是否在设备上点击了‘允许USB调试’) sys.exit(1) print(f找到 {len(devices)} 台可用设备: {devices}) # 准备日志目录 os.makedirs(args.log_dir, exist_okTrue) timestamp time.strftime(%Y%m%d_%H%M%S) session_log_dir os.path.join(args.log_dir, fsession_{timestamp}) os.makedirs(session_log_dir, exist_okTrue) print(f详细日志将保存至: {session_log_dir}\n) # 使用线程池并发执行设备安装任务 results [] start_time time.time() with ThreadPoolExecutor(max_workersargs.jobs) as executor: # 提交所有设备任务到线程池 future_to_device { executor.submit(install_apk_to_device, device, valid_apks, install_args, os.path.join(session_log_dir, device.replace(:, _).replace(., _))): device for device in devices } # 处理完成的任务 for future in as_completed(future_to_device): device_id future_to_device[future] try: result future.result(timeout5) # 获取结果设置短超时防止阻塞 results.append(result) dev_id, success, total, errors result status 成功 if success total else 部分失败 if success 0 else 完全失败 print(f[汇总] 设备 {dev_id}: {status} ({success}/{total})) except Exception as exc: print(f[汇总] 设备 {device_id} 的任务生成异常: {exc}) results.append((device_id, 0, len(valid_apks), [f任务执行异常: {exc}])) total_time time.time() - start_time # 打印最终汇总报告 print(\n *60) print(批量安装任务完成) print(f总耗时: {total_time:.2f} 秒) print(f设备总数: {len(devices)}) print(fAPK总数: {len(valid_apks)}) print(-*60) total_apk_attempts len(devices) * len(valid_apks) total_success sum(r[1] for r in results) total_failed total_apk_attempts - total_success print(f总安装尝试次数: {total_apk_attempts}) print(f成功次数: {total_success}) print(f失败次数: {total_failed}) print(f成功率: {total_success/total_apk_attempts*100:.1f}% if total_apk_attempts 0 else N/A) print(*60) print(f每台设备的详细安装日志请查看: {session_log_dir}) if __name__ __main__: main()4.3 脚本使用示例假设你有两个APK文件app-release.apk和plugin-release.apk并且连接了3台设备。基本用法替换安装python batch_install.py app-release.apk plugin-release.apk -r这会将两个APK以替换模式-r安装到所有已连接的设备上。授予权限并限制并发数为3python batch_install.py app-release.apk -r -g -j 3这会将app-release.apk以替换模式安装并授予运行时权限同时最多只允许3台设备并行安装。指定日志目录python batch_install.py app-release.apk -r -l ./my_logs日志将保存在当前目录下的my_logs文件夹中并按会话时间创建子目录。5. Shell脚本快速实现方案如果你需要一个更轻量、无需Python环境的解决方案可以使用Shell脚本。以下是一个Bash脚本示例实现了基础的多设备并行安装。#!/bin/bash # batch_install.sh - 使用Shell并行安装APK到多个设备 # 配置部分 APK_FILES($) # 通过命令行参数传入APK路径 ADB_INSTALL_FLAGS-r # 默认使用替换安装 MAX_PARALLEL5 # 最大并行进程数 # 检查参数 if [ ${#APK_FILES[]} -eq 0 ]; then echo 用法: $0 apk文件1 [apk文件2 ...] exit 1 fi for apk in ${APK_FILES[]}; do if [ ! -f $apk ]; then echo 错误: 文件 $apk 不存在。 exit 1 fi done # 获取设备列表 DEVICES$(adb devices | grep -v List of devices attached | grep device$ | awk {print $1}) if [ -z $DEVICES ]; then echo 未找到已连接的设备。 exit 1 fi echo 找到设备: $DEVICES echo 将安装APK: ${APK_FILES[*]} echo 开始并行安装 (最大并行数: $MAX_PARALLEL)... # 创建一个临时目录存放设备日志 LOG_DIR./install_logs_$(date %Y%m%d_%H%M%S) mkdir -p $LOG_DIR # 用于控制并发的计数器 CURRENT_JOBS0 # 为每个设备启动安装进程 for DEVICE in $DEVICES; do ( # 为每个设备创建独立日志文件 LOG_FILE$LOG_DIR/device_${DEVICE//:/_}.log echo 开始为设备 $DEVICE 安装 $LOG_FILE date $LOG_FILE SUCCESS_COUNT0 for APK in ${APK_FILES[]}; do APK_NAME$(basename $APK) echo [设备 $DEVICE] 正在安装: $APK_NAME | tee -a $LOG_FILE # 执行安装命令并将输出重定向到日志文件 if adb -s $DEVICE install $ADB_INSTALL_FLAGS $APK $LOG_FILE 21; then echo [设备 $DEVICE] $APK_NAME: 成功 | tee -a $LOG_FILE ((SUCCESS_COUNT)) else echo [设备 $DEVICE] $APK_NAME: 失败 | tee -a $LOG_FILE fi echo --- $LOG_FILE done echo 设备 $DEVICE 安装结束 $LOG_FILE echo 成功: $SUCCESS_COUNT/${#APK_FILES[]} $LOG_FILE echo [设备 $DEVICE] 任务完成。日志: $LOG_FILE ) # 将整个子shell放入后台执行实现并行 # 并发控制 ((CURRENT_JOBS)) if [ $CURRENT_JOBS -ge $MAX_PARALLEL ]; then wait -n # 等待任意一个后台任务完成 ((CURRENT_JOBS--)) fi done # 等待所有剩余的后台任务完成 wait echo 所有设备安装任务已提交完成。 echo 详细日志请查看目录: $LOG_DIR使用方式# 赋予执行权限 chmod x batch_install.sh # 运行脚本 ./batch_install.sh path/to/app1.apk path/to/app2.apkShell脚本的局限性这个脚本的错误处理比Python版本简单它主要依赖命令的返回码。并发控制使用了基础的作业控制(和wait -n)在大多数现代Bash中可用。日志是每个设备一个文件但所有输出包括你屏幕上看到的也会混在一起。对于更复杂的需求还是推荐Python方案。6. 常见问题与排查技巧实录在实际使用中你肯定会遇到各种问题。下面是我在多年实践中总结的一些典型问题及其解决方法。6.1 设备识别与连接问题问题1脚本检测不到设备但adb devices手动执行能看到。可能原因1ADB服务未启动或异常。排查执行adb kill-server adb start-server重启ADB服务。技巧可以在脚本开头主动执行一次adb start-server来确保服务已启动。可能原因2设备状态为unauthorized。排查脚本已过滤此状态。你需要检查设备屏幕是否弹出了“允许USB调试”的提示框并点击“允许”。技巧对于测试机群可以在首次连接时统一点击授权或者使用一些自动化工具预先完成授权。可能原因3设备通过网络连接但端口被占用或IP变化。排查使用adb connect device_ip:port重新连接。如果IP经常变可以考虑编写脚本动态发现同一网段内的设备。问题2安装过程中设备突然离线(offline)。可能原因USB线接触不良、设备休眠、ADB连接不稳定。解决脚本中已经通过命令超时机制处理了这种情况。物理上尽量使用质量好的USB线和Hub并在开发者选项中关闭“USB调试安全设置”如果允许的话并设置“保持唤醒状态”。6.2 安装命令执行失败问题3安装失败提示INSTALL_FAILED_UPDATE_INCOMPATIBLE或类似签名冲突错误。可能原因尝试安装的APK与设备上已安装的应用签名不一致且未使用-r参数或者即使用了-r也无法覆盖如系统应用。解决首先确保脚本中使用了-r参数。如果仍失败需要先卸载旧应用adb -s device_id uninstall your.package.name。可以将卸载逻辑集成到脚本中作为安装前的一个可选步骤。对于系统应用可能需要adb root权限这在普通设备上通常无法获取。问题4安装失败提示INSTALL_FAILED_INSUFFICIENT_STORAGE。可能原因设备存储空间不足。解决脚本应捕获此错误并明确提示。手动处理需要清理设备空间。可以在脚本中增加一个安装前的存储空间检查步骤但这会增加复杂度。更实用的做法是让脚本清晰报错由人工介入清理。问题5安装失败提示adb: error: cannot stat apk_path: No such file or directory。可能原因提供给adb install的APK文件路径不正确或者路径中包含特殊字符如空格未正确处理。解决确保脚本中文件路径的传递是正确的。在Python中使用列表传递参数subprocess.run([..., apk_path])可以避免Shell转义问题。在Shell脚本中确保变量用双引号包裹adb install $APK_PATH。6.3 性能与稳定性问题问题6并行安装时大量日志输出到控制台造成混乱。解决这正是我们将每个设备的日志重定向到独立文件的原因。在Python脚本中每个设备的安装进程将其所有输出写入专属日志文件。控制台只显示进度摘要和关键错误保持整洁。问题7并行数设置过高导致脚本卡顿或ADB无响应。现象设置-j 50后脚本运行缓慢甚至有些设备任务超时。解决根据你的电脑性能和设备数量合理设置并发数。对于普通开发机建议并发数在5-10之间。我的脚本默认是5这是一个比较保守稳定的值。你可以通过-j参数调整。问题8如何知道每个APK分别安装成功了没有解决查看每台设备的独立日志文件。脚本会为每台设备在日志目录下生成一个以设备ID命名的文件如install_log_emulator_5554.txt里面详细记录了该设备上每个APK的安装命令和完整输出。6.4 脚本扩展与优化建议安装前卸载增加一个-u, --uninstall-first参数在安装前先执行adb uninstall package_name。这需要你能够从APK中解析出包名可以使用aapt工具或Python的androguard库。安装后启动增加一个-s, --start-after-install参数安装成功后自动启动应用的主Activity。同样需要解析APK获取主Activity信息。设备分组有时你可能只想将APK安装到特定型号或特定系统版本的设备上。可以扩展脚本通过adb -s device_id shell getprop获取设备属性如ro.product.model,ro.build.version.release然后根据属性过滤设备。集成到CI/CD将Python脚本作为Jenkins Pipeline、GitLab CI或GitHub Actions的一个步骤。关键是要确保CI服务器上安装了正确的ADB驱动并能连接到设备可能是通过USB Hub或网络。图形界面GUI对于非技术人员可以使用tkinter或PyQt为脚本包装一个简单的图形界面方便选择APK文件和设备。这个脚本提供了一个强大而灵活的基础。你可以根据自己的具体工作流对它进行裁剪和增强。核心思想始终是将重复、易错的手动操作转化为可重复、可审计的自动化流程。