资讯动态

基于Python与ADB的荣耀设备应用批量自动化更新实践

发布时间:2026/9/15 3:13:32 来源:尧图企业网站定制
1. 需求分析与整体方案设计1.1 为什么用API而不是UI自动化我做自动化测试这几年几乎每个团队都会遇到一个烦人问题测试机上的App版本老是跟不上最新包。尤其是手头同时管着十几台荣耀手机的时候版本参差不齐跑用例前必须先手动打开应用商店一台一台点“更新”。运气好点的还能赶上商店的自动更新但在测试环境里我们往往安装的是内测包或者非公开渠道包商店压根不会主动推。最初我也考虑过用Appium或Playwright做UI自动化来点更新按钮后来实际一跑就放弃了。原因很简单UI自动化依赖界面元素定位商店版本一改版脚本就要跟着改维护成本极高。多台设备并行操作时弹窗、网络延迟、商店地区差异都会导致用例不稳定。点更新这种操作本身就是“过程导向”与其模拟点击不如直接调用商店的更新API拿到下载地址后静默安装稳定得多。而直接走API更新核心逻辑就三件事获取本地版本、向商店服务端请求最新版本、下载并安装差异包。这三步全部可以用Python脚本在几秒内完成批量跑几十台设备也不费劲。1.2 整体技术架构整套自动化更新系统的结构并不复杂我拆分成了下面几个模块模块职责关键工具/技术设备信息采集获取每台设备的型号、系统版本、已安装App版本号adb shell、pure-python-adb版本比对服务调用商店API解析返回数据判断是否有更新Pythonrequests JSON解析下载模块按版本差异下载全量包或增量包requests、hashlib安装执行把APK安装到指定设备adb install、pm install结果通知输出日志并推送结果到工作群logging、Webhook机器人可选调度定时/触发式执行更新巡检cron、Jenkins、GitLab CI实际运行时候脚本先读一个设备列表文件每台设备依次执行“采集版本 - 查增量 - 下载 - 安装”最后把汇总结果打印出来。这个流程看起来简单但每个环节都有细节坑后面我会逐步拆开讲。2. 荣耀应用商店更新机制与API关键点解析2.1 更新流程背后的逻辑要自动化更新得先搞清楚手机上的应用商店到底是怎么判断“有新版本”的。其实所有安卓应用商店的更新逻辑都大同小异客户端上报已安装应用的包名列表以及对应的versionCode。服务端拿着包名去库里去比对如果服务器上存在更高版本就返回新版本信息和更新包地址。客户端根据返回结果决定是提示用户、静默下载还是直接安装。所以自动化要做的就是把这套“客户端上报信息”的过程用Python复现出来。但这里有个关键点应用商店的接口通常会带签名参数用来防止第三方伪造请求。这也是为什么我建议优先申请官方开放接口或者至少是在自己的设备上通过合法调试手段获取请求格式。如果你只是想在测试环境里自动更新自家App更好的做法是走荣耀开发者平台提供的“应用升级”相关能力或者在应用内自行实现版本的OTA检查而不是曲解商店接口。2.2 接口参数与返回格式假设我们已经获取到了一个标准的商店更新接口它的请求参数一般会包含以下字段参数名含义示例packageName应用包名com.example.appversionCode当前安装版本号整数2025001deviceModel设备型号HONOR 90magicVersion手机系统版本MagicOS 8.0osVersionAndroid版本14language语言区域zh-CN返回的JSON一般长这样{ code: 0, message: ok, data: { needUpdate: true, latestVersionCode: 2025003, latestVersionName: 1.2.0, apkUrl: https://store.example.com/xx/xx.apk, diffUrl: https://store.example.com/xx/xx.patch, fileMd5: d41d8cd98f00b204e9800998ecf8427e, releaseNote: 修复若干崩溃问题 } }这里我特别想强调一下versionCode和versionName的区别。versionName是给用户看的比如1.2.0versionCode是给系统判断用的递增整数比如2025003。在自动化比对版本时永远用versionCode因为有些厂商发布版本时versionName并不严格递增但versionCode一定是单调递增的。曾经我见过一个团队用字符串比较versionName结果“1.10.0”被判断成比“1.9.0”旧导致更新永远不触发这个问题用versionCode就能完全避免。2.3 合法使用与边界控制在做任何自动化之前一定先确认你的操作边界。荣耀应用商店的接口是商业产品的一部分如果你用抓包工具去窃取并仿冒接口很可能违反用户协议严重的会被限流甚至封号。我的经验是如果做的是自己公司App的版本升级建议使用荣耀开发者平台上开放的“应用内升级”能力或者自建一个简单的更新服务用同样的对比逻辑下发安装包。如果只是给自己手头几台设备做批量更新可以基于本地安装包和ADB安装来完成不一定非要去碰商店接口。如果一定要对接商店更新能力优先查阅官方文档咨询商务或技术支持拿到正式授权。这也是我在文章里尽量不贴某个具体抓包URL的原因。下面演示的代码我会把请求地址替换成示例占位符重点在于逻辑实现而不是鼓励大家去逆向前端接口。3. Python实现自动化更新的详细过程3.1 环境准备开发环境的搭建不复杂但有几个容易漏掉的地方Python版本建议3.9及以上主要用requests、hashlib、subprocess、concurrent.futures这些标准库和常用库。ADB环境下载platform-tools配置到系统PATH中。荣耀手机连接电脑后在开发者选项中开启“USB调试”并且把“USB配置”切换到“传输文件”否则ADB经常识别不到。HONOR USB驱动Windows电脑上最好单独装一下Honor USB Driver不然adb devices有概率只显示unauthorized或offline。安装依赖pip install requests如果是大量设备并行操作可能还需要paramiko做远程设备控制但我一般直接用USB连接多台手机配合HUB效果更直接。3.2 获取设备已安装应用的版本信息第一步是获取设备上目标App的版本信息。我一般先用adb devices确认设备在线再通过adb shell dumpsys package解析版本号。下面是获取单个包名版本信息的函数import re import subprocess def get_local_version(device_id: str, package_name: str) - int | None: 通过 adb shell dumpsys 获取指定应用当前安装的 versionCode cmd [adb, -s, device_id, shell, dumpsys, package, package_name] result subprocess.run(cmd, capture_outputTrue, textTrue, timeout30) if result.returncode ! 0: raise RuntimeError(fdumpsys 失败: {result.stderr}) # 注意可能同时存在 device/emulator/vendor 多个版本取第一个即可 match re.search(rversionCode(\d), result.stdout) if match: return int(match.group(1)) return None很多人习惯用adb shell pm list packages来拿包列表但那个命令查不出版本号。还有命令adb shell dumpsys package 包名 | grep versionCode在Windows下是搞不定的因为grep不是Windows自带的最好就是用Python的re去解析跨平台也稳。执行这个函数时有个细节dumpsys输出里会出现不止一个versionCode比如versionCode1001 targetSdk34 minSdk23 versionCode1001其实它们代表同一版本但如果你解析时用了findall可能会有干扰。我这里直接取search的第一个匹配值实测下来是可靠的。3.3 调用商店更新API进行版本比对拿到本地版本号后就可以构造请求去商店API了。这里假设你已有合法的API入口下面代码重点是状态处理逻辑import requests import json UPDATE_API_URL https://api.example.com/v1/app/checkUpdate def check_update(device_id: str, package_name: str, local_version_code: int, device_model: str, magic_version: str, os_version: str) - dict | None: payload { packageName: package_name, versionCode: local_version_code, deviceModel: device_model, magicVersion: magic_version, osVersion: os_version, language: zh-CN } headers { Content-Type: application/json, User-Agent: MagicOSAppUpgrade/1.0 } try: resp requests.post(UPDATE_API_URL, jsonpayload, headersheaders, timeout10) resp.raise_for_status() except requests.RequestException as e: print(f[{device_id}] 请求更新接口失败: {e}) return None data resp.json() if data.get(code) ! 0: print(f[{device_id}] 接口返回错误: {data.get(message)}) return None info data.get(data, {}) if not info.get(needUpdate): print(f[{device_id}] 已是最新版本) return None latest_code info.get(latestVersionCode, 0) if latest_code local_version_code: return None return { new_version_name: info.get(latestVersionName), new_version_code: info.get(latestVersionCode), apk_url: info.get(apkUrl), diff_url: info.get(diffUrl), file_md5: info.get(fileMd5), release_note: info.get(releaseNote, ) }这段逻辑有两点值得注意接口返回needUpdate并不代表一定需要下载还得自己拿latestVersionCode跟本地比对一次因为服务端逻辑可能有缓存延迟防御性判断永远不会错。requests.post的json参数会自动序列化字典不要自己写json.dumps再放到data里那样Content-Type会变成application/x-www-form-urlencoded容易被服务端拒绝。3.4 下载新版本APK并确认完整性比对出版本有更新后下一步是下载安装包。下载这个步骤容易出问题的是大文件超时和完整性校验。import hashlib def download_apk(url: str, save_path: str, expected_md5: str ) - bool: 下载 APK并校验 MD5如果提供 headers { User-Agent: Mozilla/5.0 (Linux; Android 14) AppleWebKit/537.36 } resp requests.get(url, headersheaders, streamTrue, timeout(5, 120)) resp.raise_for_status() md5 hashlib.md5() with open(save_path, wb) as f: for chunk in resp.iter_content(chunk_size1024 * 256): f.write(chunk) md5.update(chunk) if expected_md5: actual_md5 md5.hexdigest().lower() if actual_md5 ! expected_md5.lower(): raise ValueError(fMD5 校验失败期望 {expected_md5}实际 {actual_md5}) return True这里我把streamTrue加上是为了避免一次性加载几百兆APK到内存尤其是批量下载时内存会爆。timeout(5, 120)表示连接超时5秒读取超时120秒这个配置在弱网环境下比较友好不然一个包下载到一半卡住整个脚本都要等很久。MD5校验有没有必要有。即使你是从正经商店API下载也挡不住运营商或者代理中间件把包给你换成旧的。老老实实校验一下哈希比装完了才发现版本不对要省事得多。3.5 用ADB安装APK到设备APK下载到本地后最后一步就是安装。我优先推荐adb install而非先adb push再pm install因为adb install的失败信息更清晰。def install_apk(device_id: str, apk_path: str) - bool: cmd [adb, -s, device_id, install, -r, -d, apk_path] result subprocess.run(cmd, capture_outputTrue, textTrue, timeout120) if result.returncode ! 0: print(f[{device_id}] 安装失败: {result.stdout} {result.stderr}) return False return Success in result.stdout参数解释-r允许覆盖安装保留数据和缓存。-d允许降低版本安装。这里注意如果你测试的包意外装了一个更高版本商店API返回的可能反而低于当前版本这时候-d就很有用。但在生产环境中不建议使用-d会导致应用数据错乱。如果你担心adb install在部分荣耀EMUI/MagicOS上有兼容性问题可以改成下面这种方式adb -s device push apk /data/local/tmp/tmp.apk adb -s device shell pm install -r /data/local/tmp/tmp.apk实测下来对于荣耀MagicOS 8.0设备两种方式差别不大用adb install更省事。3.6 批量更新与并发控制单台设备逻辑跑通之后批量处理加一个线程池就好from concurrent.futures import ThreadPoolExecutor, as_completed DEVICES [ {id: HONOR90A, package: com.example.app, model: HONOR 90}, {id: HONOR80B, package: com.example.app, model: HONOR 80}, ] def process_device(device): dev_id device[id] local_ver get_local_version(dev_id, device[package]) print(f[{dev_id}] 当前版本码 {local_ver}) update_info check_update( dev_id, device[package], local_ver, device[model], MagicOS 8.0, 14 ) if not update_info: return dev_id, no_update apk_path f/tmp/{dev_id}_app.apk download_apk(update_info[apk_url], apk_path, update_info[file_md5]) ok install_apk(dev_id, apk_path) return dev_id, updated if ok else failed if __name__ __main__: with ThreadPoolExecutor(max_workers4) as executor: futures [executor.submit(process_device, d) for d in DEVICES] for future in as_completed(futures): dev_id, status future.result() print(f{dev_id}: {status})线程数量不要开太多因为ADB本身是USB协议并发过高经常会出现device offline或者adb server崩溃。我被这个问题坑过不止一次后来老老实实把max_workers控制在4到6台再配合重试机制稳定性才上来。4. 踩坑记录与排查技巧4.1 荣耀手机连接电脑后adb devices不显示设备这个问题高发在MagicOS 8.0 Android 14的荣耀手机上我自己第一次也卡了很久。表面现象是手机插上USB后手机端弹出是否允许USB调试点击允许后adb devices依然空列表。排查步骤按顺序来检查手机端“开发者选项”里的“USB调试”是否打开最好顺便把“仅充电模式下允许ADB调试”也打开。确认USB连接模式下拉通知栏点USB连接方式必须选择“传输文件(MTP)”不能是“仅充电”。如果是Windows去设备管理器看是否有带感叹号的设备有就装HONOR USB Driver。重启ADB服务adb kill-server然后adb start-server。如果仍不出现把手机端“撤销USB调试授权”再重新插拔第二次弹窗时勾选“始终允许”。这一套下来99%的设备都能正常连通。剩余1%可能是USB线本身只支持充电找一根带数据功能的线就好了。4.2 “api error: 400 invalid schema for function artifact”是哪出了问题有段时间我在用某个CI平台的API对接脚本时遇到的报错信息是api error: 400 invalid schema for function artifact这个报错本质上不是你的代码写错了而是请求提交流程里某些字段没按API预定义的数据结构传。结合我当时的调试经验这类400错误通常来自三个原因必填字段缺失比如API要求artifact_id是整数你传了字符串“123”自然校验不过。字段格式非法API的schema正则会校验一些特殊字符尤其是函数名、路径名里出现空格、中文、特殊符号时容易报错。坐标越界或使用保留字比如字段值以__开头结尾被视为非法。排查这类问题有一个通用套路把API报错信息返回的schema拆出来逐字段对照。比如它提示^(?!__.*__$)[^\p{Cc}]$意思就是“字段值不能以双下划线开头和结尾不能包含控制字符”。你只需检查请求字段里有没有这类违例值修正就好。回到荣耀商店API场景如果你模拟更新接口时也遇到类型类似的400错误多半是versionCode误传成了字符串或者packageName带了空格。用我代码里构造payload的方式让requests自动处理JSON类型基本不会踩这个坑。4.3 安装时报INSTALL_FAILED_UPDATE_INCOMPATIBLE自动化更新最让人头疼的报错就是INSTALL_FAILED_UPDATE_INCOMPATIBLE这个错误的意思是要安装的APK和已经安装的App签名不一致系统不允许覆盖安装。场景常见于你从商店下载的是正式签名包但手机上装的是测试签名包或者反过来。不同渠道包签名不一致。解决方法也很直接如果签名不一样adb install -r是救不了的要么先adb uninstall 包名再安装但会丢失应用数据要么用adb install -r -d强制降级覆盖不一定总能成功。更稳妥的是一开始就统一签名。测试机装上正式签名包商店推送的正式包才能直接覆盖。自己公司App做内测就用同一个jks/keystore打包不要每次测试换测试key。我自己的建议是尽量保证所有自动化入口的APK签名一致。否则脚本跑了一半卡在安装阶段比手动更新还难受。4.4 接口返回字段为空或缺失还有一个高频坑商店API某个时段返回的JSON里少了latestVersionCode字段导致脚本直接TypeError: NoneType object is not subscriptable。处理方式很简单在解析data时用.get()默认值兜底并在拿到apkUrl为空时跳过下载apk_url info.get(apkUrl) or info.get(url, ) if not apk_url: print(接口未返回下载地址跳过本次更新) return None服务端的稳定性是不可控的我们能做的只有让自己的代码尽量宽容该跳过就跳过该重试就重试。我给下载请求加了两次重试间隔3秒实测能把因为网络抖动导致的失败率压到5%以下。5. 进阶把自动化更新嵌入CI流程5.1 定时巡检任务手动执行脚本只是第一步真正有价值的是让它每天定时跑一遍。在Linux服务器上用crontab拉起来最简单0 2 * * * cd /opt/app_updater /usr/bin/python3 main.py logs/updater.log 21每台设备每天凌晨2点自动检查更新早上到公司所有机器上的App都是最新版这个体验真的很省心。如果有多台电脑同时管理可以把日志都推到统一的地方方便查看。5.2 接入Jenkins或GitLab CI如果团队已经用了Jenkins可以加一个FreeStyle Job构建步骤拉取代码后执行python main.py --device-list devices.json --notify webhook在GitLab CI里写成.gitlab-ci.yml也很快update-app: stage: deploy script: - python main.py --device-list devices.json only: - schedules不过要注意一点CI环境默认往往是无头环境没有手机接入所以最好是用一个固定的物理机或者服务器当ADB中转通过adb connect连接局域网内的设备需要手机和电脑在同一网段并开启“网络调试”端口。不然你的Runner根本发现不了手机。5.3 和内测分发结合后来我还把“商店API更新”这层抽象了一下如果想推广到自己公司App的自动化测试中完全可以用同一个脚本框架把“更新API”替换成自建的分发服务。这样App测试包只会来自你自己的OTA服务而不会依赖商店的发布节奏。自建服务主要做三件事存一份最新APK和它的MD5。提供一个“检查版本”的接口类型和前面示例一样。生成不同版本的下载链接。这样脚本完全复用只是把UPDATE_API_URL改成内网地址即可。做多了你会发现自动化的价值不在于用了多高深的技术而在于把“检查 下载 安装”这个重复劳动固化成一条可靠流水线。写在最后的一点经验这套脚本在我这边稳定跑了大半年帮我把几十台设备的版本管理从每天半小时压缩到了零。过程中踩过的坑不少但最想提醒后来者的就两条一是不要迷恋“神奇接口”能拿到正式授权最好拿不到就围绕自己的包和ADB做自动安装一样能解决80%的测试环境问题二是所有自动化方案里版本比对逻辑一定要围绕versionCode展开别跟versionName较劲。如果后续你想做得更顺手可以给脚本加个Web小页面把当前设备状态、版本号、更新历史都显示出来这样团队里其他同事也能看到“昨晚自动更新了哪几台”比看日志人性化太多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价