有时候,手机比人更诚实。
我本来只是想做一件很简单的事:看一下自己今天到底在各个 App 上花了多少时间。
系统自带的“数字健康”当然能看,但它太像一个温柔的提醒工具:告诉你“今天用久了”,然后就没有然后了。第三方统计 App 也能看,但问题更多:常驻后台、耗电、被系统杀、数据断流,还要跟 HyperOS 的后台管理斗智斗勇。
所以我决定换一种方式。
不用第三方 App,不跑自动化测试框架,不模拟点击 UI。 直接从 Android 系统底层拿数据。
也就是:
adb shell dumpsys usagestats --daily然后用 Python 清洗、聚合、可视化。
本来以为只是一次普通的手机使用时长统计,结果数据一出来,我沉默了。
因为它告诉我:
王者荣耀今天使用了 63135 分钟。
换算一下,大概是:
44 天。
一天里面打了 44 天王者。
这已经不是自律问题了,这是时空连续性问题。

很多人想统计手机使用时间,第一反应是去应用商店装一个工具。
但问题是,这类 App 本身也会变成“被统计对象”。
它要常驻后台,要拿权限,要持续监听,还要和系统的省电策略反复拉扯。尤其是小米这种后台管理比较强的系统环境里,今天能跑,明天可能就被冻住。
还有一种方案是 Appium 这类自动化测试框架。
但用它来统计使用时间,就有点像为了切一片面包,先开一台挖掘机。
Appium 更适合做 UI 自动化测试。它需要驱动环境,需要模拟操作,需要维持一个相对重的测试链路。只是为了读几个使用时长数字,实在没必要把事情搞这么重。
我想要的是一个更干净的方案:
电脑发起请求,手机返回底层日志,Python 负责清洗和展示。
它不依赖第三方统计 App,不需要手机端长期运行服务,也不会额外占用前台资源。
思路很朴素:
既然 Android 系统自己已经记录了使用行为,那我为什么不直接去读系统记录?
这次用的是局域网无线 ADB。
也就是说,电脑和手机在同一个 Wi-Fi 下,不用插数据线,也能直接和手机建立调试连接。
大致流程是这样:
命令大概是这样:
adb connect 192.168.xx.xx:xxxxx终端返回 connected 之后,电脑和手机之间的通道就打通了。
接下来,用 Python 调用 ADB:
adb shell dumpsys usagestats --daily把系统吐出来的原始日志拿回来,再用正则把包名和 totalTimeUsed 字段抠出来,最后转成分钟数。
技术上没有什么玄学。
真正玄学的是数据本身。
清洗之后,Top 10 长这样:

如果只是看“分钟”这个单位,可能还没有那么刺激。
换算成现实时间之后,画风就变了:
看到这里,我第一反应不是“我怎么用这么多”。
而是:
这玩意儿肯定不是今天。
因为一天只有 1440 分钟。
王者荣耀一个应用就 63135 分钟,相当于把今天扩展成了 44 天。 微信也不甘示弱,直接给我算出了 33 天。
如果这些都是真的,那我不是手机重度用户。
我是时间管理局漏网之鱼。
这组数据最有意思的地方,不在于它多吓人,而在于它暴露了一个问题:
系统底层日志里的“daily”,不一定等于我们口语里的“今天”。
很多人看到 --daily,会自然理解成“当天数据”。
但在 Android 的 UsageStatsManager 里,事情没有这么简单。系统会维护不同粒度的统计桶,比如 daily、weekly、monthly、yearly。不同 ROM、不同系统版本、不同缓存策略下,最终输出的数据可能并不是一个干净的自然日切片。
尤其是在定制系统里,可能还会叠加几类影响:
第一,跨天结算不一定按我们想象的方式发生。 某些前台使用事件可能在统计桶里继续滚动,直到系统认为它该被归档或重算。
第二,重启、系统更新、缓存策略,都会影响日志聚合结果。 你看到的是“系统当前吐出来的统计值”,不一定是“今天零点到现在的真实使用值”。
第三,时区、系统时间、网络环境变化,也可能让时间窗口变得更复杂。 如果手机、电脑、网络代理、系统时间之间发生过明显切换,日志里的时间戳边界就更容易变得不直观。
所以,这组数据最合理的解释不是:
我今天真的打了 44 天王者。
而是:
我拿到的是一个被系统统计口径、缓存窗口、历史聚合共同影响过的使用时长结果。
这就很有意思了。
因为它提醒我们一件事:
数据看起来很客观,但数据口径并不天然诚实。
虽然它不适合被当成严格的“今日使用时长”,但它不是废数据。
恰恰相反,它很有价值。
它至少说明了三个问题。
哪怕不把它理解成一天的数据,只要看排序,就能看出使用重心。
王者荣耀、微信、aweme、QQ、video、bili 都排在前面。
这说明手机真正被高频占用的场景非常清楚:
游戏、社交、短视频、视频内容。
这些应用不是偶尔打开一下,而是在系统统计里留下了明显的前台痕迹。
换句话说,手机不是简单的工具。
它更像一台随身携带的注意力分发机器。
googlequicksearchbox 排到了第四。
这说明搜索框并不是摆设。
很多时候,我们以为自己打开手机是在“解决问题”:查资料、搜答案、找方法。 但搜索很容易变成另一种形式的缓冲区。
比如:
“如何减少手机使用时间” “怎么戒游戏” “怎么提高专注力” “睡前玩手机怎么办”
然后搜索完,收藏一下,关掉页面。
继续打开王者荣耀。
搜索不一定带来行动。
有时候,它只是让人短暂感觉:
我已经开始改变了。
“系统桌面”排在第五。
这类数据通常不显眼,但很真实。
很多时候我们并不是一打开手机就进入某个 App,而是在桌面上来回滑、找图标、看通知、切任务、犹豫下一步干什么。
这类时间很碎。
碎到你不会主动记得。
但系统会记得。
它说明手机使用不是一个个清晰的动作,而是一连串微小的注意力漂移。
我把清洗后的头部数据丢给大模型,让它做一个复盘。
它没有急着下结论,第一反应和我差不多:
这不可能是一天的数据。
然后它做了一个很朴素的换算:
王者荣耀 63135 分钟,约等于 43.8 天。 微信 47211 分钟,约等于 32.8 天。 aweme 15593 分钟,约等于 10.8 天。
这组数据如果被标记为“今日”,那就一定有统计口径问题。
但如果把它看成某种更长周期的累积痕迹,它又确实像一份数字生活切片:
游戏提供即时反馈,社交提供持续连接,短视频提供低成本刺激,搜索提供“我正在处理问题”的心理安慰。
这个判断很扎心。
因为它不像是在分析一台手机。
更像是在分析一个人如何分配自己的注意力。

完整脚本分成几块:
dumpsys usagestats --daily;核心解析逻辑大概是这样:
pattern = re.compile(r'(?:package|pkg)=([\w\.]+).*?totalTime(?:Used)?=["\']?([^"\'\s>]+)["\']?')这行正则主要做两件事:
第一,匹配应用包名。 第二,抓取对应的 totalTime 或 totalTimeUsed 字段。
然后再把包名映射成更容易读的中文名,比如:
APP_MAP = {"com.tencent.mm": "微信","com.tencent.mobileqq": "QQ","com.tencent.tmgp.sgame": "王者荣耀","com.miui.home": "系统桌面",}最后把所有应用按分钟数倒序排一下,就能得到一个比较直观的榜单。
这里需要提醒一句:
这个脚本适合做个人探索和数据观察,不建议直接把结果当作严格审计数据。
原因前面已经说过:系统日志的统计窗口和聚合口径,可能和我们日常理解的“今天”不完全一致。
如果你真的想做更严谨的统计,应该增加两层处理:
一层是时间窗口过滤。 比如明确只保留今天零点到当前时间之间的事件。
另一层是事件级别解析。 不要只看系统聚合后的 totalTimeUsed,而是进一步解析前台进入、前台退出等事件,再自己计算持续时间。
这样得到的结果,才更接近真正意义上的“今日使用时长”。
说实话,44 天这个数字当然夸张。
它有标题效果,也有传播点。
但这次实验真正让我在意的,不是这个异常值本身。
而是我突然意识到:
我们平时对手机使用的感知,是非常不可靠的。
你以为只是刷了一会儿。 系统记下来的是几十次打开、切换、停留。
你以为只是搜个资料。 实际可能是在搜索、跳转、分心、返回桌面之间来回循环。
你以为自己还挺克制。 数据一摊开,才发现注意力早就被拆成了碎片。
手机最厉害的地方,不是让你一次性沉迷 10 个小时。
而是让你每次只多看 3 分钟。
多到你不觉得痛。 少到你不愿意管。 最后累积成一个你不太敢相信的数字。
不用一上来就写脚本,也不用立刻搞复杂自动化。
你可以先问自己三个问题:
第一,我手机里排名第一的 App 是什么?
不是你以为的那个,而是真实统计里的那个。
第二,我打开手机最多的理由是什么?
是沟通、娱乐、工作、学习,还是单纯不知道该干什么?
第三,我有没有把“搜索解决方案”当成了“已经开始改变”?
这个问题尤其重要。
因为搜索会给人一种非常隐蔽的满足感:
我查了。 我懂了。 我收藏了。 我下次一定改。
然后下次继续重复。
这次小实验,表面上是用 Python 扒了一次小米 15 的底层日志。
实际上更像是给自己的手机做了一次注意力体检。
结果当然不完美。 数据口径有坑,系统统计有误差,--daily 也不一定等于真正的今天。
但它依然有意义。
因为它把那些平时模糊的、碎片化的、不愿承认的使用习惯,变成了一组冷冰冰的数字。
代码不会安慰你。 日志不会给你找借口。 图表也不会替你自律。
它只会把事实摆出来。
至于要不要改变,那是另一回事。
但至少从这一刻开始,我很难再假装自己只是“随便玩一会儿”。
下面是这次用到的脚本版本,主要适配 Android / HyperOS 的 usagestats 输出,用来做个人数据探索。
import subprocessimport reimport jsonimport osimport requestsimport matplotlib.pyplot as pltAPP_MAP = {"com.tencent.mm": "微信","com.tencent.mobileqq": "QQ","com.tencent.tmgp.sgame": "王者荣耀","com.bilibili.app.in": "哔哩哔哩","com.miui.home": "系统桌面","com.android.settings": "系统设置","com.coolapk.market": "酷安","com.github.android": "GitHub","com.android.chrome": "Chrome浏览器"}defload_credentials(): possible_paths = ["api/credentials.json", "credentials.json"]for path in possible_paths:if os.path.exists(path):with open(path, "r", encoding="utf-8") as f:return json.load(f)raise FileNotFoundError("未找到 credentials.json 配置文件")defget_adb_usagestats():try: check_device = subprocess.run( ["adb", "devices"], capture_output=True, text=True ) devices = [ line for line in check_device.stdout.strip().split("\n")[1:]if line.strip() ]ifnot devices or"offline"in devices[0]: print("未检测到在线设备,请先确认 adb connect 是否成功")returnNone result = subprocess.run( ["adb", "shell", "dumpsys", "usagestats", "--daily"], capture_output=True, text=True, encoding="utf-8", errors="ignore" )return result.stdoutexcept Exception as e: print(f"ADB 调用失败:{e}")returnNonedefparse_time_to_minutes(time_str): time_str = time_str.strip()if":"in time_str: parts = list(map(int, time_str.split(":")))if len(parts) == 3:return parts[0] * 60 + parts[1] + parts[2] / 60if len(parts) == 2:return parts[0] + parts[1] / 60try: ms = int(time_str)if ms > 0:return ms / 1000 / 60except ValueError:passreturn0defparse_stats(raw_text):ifnot raw_text:return {} pattern = re.compile(r'(?:package|pkg)=([\w\.]+).*?totalTime(?:Used)?=["\']?([^"\'\s>]+)["\']?' ) app_minutes = {}for line in raw_text.split("\n"): match = pattern.search(line)ifnot match:continue pkg_name = match.group(1) time_str = match.group(2) minutes = parse_time_to_minutes(time_str)if minutes > 0.1: readable_name = APP_MAP.get(pkg_name, pkg_name.split(".")[-1]) app_minutes[readable_name] = app_minutes.get(readable_name, 0) + minutesreturn dict( sorted(app_minutes.items(), key=lambda x: x[1], reverse=True) )defshow_visualization(data):ifnot data:return top_data = dict(list(data.items())[:10]) apps = list(top_data.keys()) minutes = list(top_data.values()) plt.rcParams["font.sans-serif"] = ["SimHei", "Microsoft YaHei", "Arial"] plt.rcParams["axes.unicode_minus"] = False plt.figure(figsize=(10, 6))try: colors = plt.colormaps.get_cmap("Set3")(range(len(apps)))except AttributeError: colors = plt.cm.get_cmap("Set3")(range(len(apps))) bars = plt.barh( apps[::-1], minutes[::-1], color=colors[::-1], edgecolor="gray", height=0.5 ) plt.xlabel("当日累计使用时间(分钟)", fontsize=11) plt.title("小米 15 今日应用使用时长 Top 10", fontsize=13, fontweight="bold")for bar in bars: width = bar.get_width() plt.text( width + 0.3, bar.get_y() + bar.get_height() / 2,f"{width:.1f} min", va="center", ha="left", fontsize=9, color="#333333" ) plt.tight_layout() plt.show()defask_ai(credentials, usage_data): url = f"{credentials['BASE_URL']}/chat/completions" headers = {"Authorization": f"Bearer {credentials['API_KEY']}","Content-Type": "application/json" } brief_data = dict(list(usage_data.items())[:8]) prompt = ("这是我的手机应用使用时长数据,单位是分钟:\n"f"{json.dumps(brief_data, ensure_ascii=False, indent=2)}\n""请从数据异常、使用习惯、注意力分配三个角度做一份简洁复盘。" ) payload = {"model": credentials["MODEL"],"messages": [ {"role": "system", "content": "你是一个严谨但不说教的个人数据分析助手。"}, {"role": "user", "content": prompt} ] }try: response = requests.post(url, json=payload, headers=headers)if response.status_code == 200: print("\n================ AI 数字生活复盘 ================\n") print(response.json()["choices"][0]["message"]["content"].strip())else: print(f"AI 接口返回异常:{response.status_code}")except Exception as e: print(f"调用大模型失败:{e}")if __name__ == "__main__": config = load_credentials() raw_log = get_adb_usagestats() cleaned_data = parse_stats(raw_log) print(json.dumps(cleaned_data, indent=4, ensure_ascii=False))if cleaned_data: ask_ai(config, cleaned_data) show_visualization(cleaned_data)后台回复「底层追踪」,获取完整无线 ADB 配置步骤和 Python 清洗脚本。