简介:memtester-4.3.0 for Android 是一份面向 Android 应用开发与系统性能调试人员的轻量级内存压力测试工具源码包,适合需要排查内存泄漏、数据损坏或竞争条件等问题的中高级开发者。资源共 23 个文件,以 sh 脚本、h 头文件与 c 源文件为主,另含 Makefile、Android.mk、README、CHANGELOG 及 memtester.8 手册等配置与说明文档,压缩包仅 31KB,结构紧凑便于集成到现有工程。该版本在原 Linux 内存检测工具基础上增加了上层接口,使 APK 可直接调用并读取输出结果,支持自定义内存大小与测试周期,实时反馈测试状态。目前已有 1324 人学习下载,读者可借此理解内存分配、填充、校验等测试流程,掌握将工具接入 Android 项目的思路,为应用稳定性验证与性能优化提供参考。
1. memtester-4.3.0 for Android:把内存压测搬到手机上到底图什么
很多人第一次听到 memtester-4.3.0 for Android,反应是「手机内存还能测出花来?」。我一开始也这么想,直到某次给一批工业平板做老化筛选,连续跑了两天应用都没复现的偶发崩溃,最后用 memtester 压了四十分钟,直接在一台机器的特定地址段上稳定报错——那一刻我才明白,内存问题不是「能不能用」的问题,是「什么时候翻车」的问题。memtester 本身是个老牌内存压测工具,4.3.0 是它在 Android 上被移植得比较成熟的一个版本,核心价值就一句话:用可配置的读写模式,把物理内存里那些平时被缓存和调度掩盖的坏点、时序余量不足、焊接虚焊逼出来。它适合三类人:做嵌入式 Android 设备出厂老化的、排查偶发死机重启的、以及想验证自己板子内存超频或降频后稳定性的。这一章不铺原理,先把「这东西在 Android 上跑起来长什么样、能给你什么结论」讲清楚,后面再拆编译、参数和踩坑。
2. 从交叉编译到 push 进设备:memtester 4.3.0 的落地链路
2.1 为什么 Android 上不能直接拿 x86 的二进制
memtester 的源码本身是纯 C,逻辑不依赖图形栈,也不碰 Android 的 Binder 和 SELinux 策略,这是它能被移植的前提。但 Android 设备绝大多数是 ARM 或 AArch64 架构,而且运行在 bionic libc 上,不是桌面 Linux 那套 glibc。你如果直接把 Ubuntu 上编译出来的可执行文件 push 进去,大概率报not executable: 64-bit ELF file或者No such file or directory——后者尤其迷惑,文件明明在,其实是动态链接器路径对不上。所以正确路径是:用 Android NDK 的 clang 工具链做交叉编译,或者干脆用-static静态链接,把 libc 依赖也打包进去。我一般倾向静态链接,省得去管目标设备上有没有对应的.so,代价是体积大一点,但 memtester 本身才几十 KB,无所谓。
2.2 用 NDK 编译出可用的 memtester 二进制
下面这套命令是我在 Linux 主机上反复用过的,NDK 版本用 r25 或 r26 都行,路径按你自己的改。关键点是--host要跟目标设备的 ABI 对上,CFLAGS里加-static和-O2。
# 假设 NDK 解压在 /opt/android-ndk-r25c export NDK=/opt/android-ndk-r25c export TOOLCHAIN=$NDK/toolchains/llvm/prebuilt/linux-x86_64 export TARGET=aarch64-linux-android export API=21 # 把编译器前缀写进环境,configure 才能认出来 export CC=$TOOLCHAIN/bin/${TARGET}${API}-clang export AR=$TOOLCHAIN/bin/llvm-ar export RANLIB=$TOOLCHAIN/bin/llvm-ranlib export STRIP=$TOOLCHAIN/bin/llvm-strip # 解压源码后进入目录 tar -xzf memtester-4.3.0.tar.gz cd memtester-4.3.0 # 静态编译,避免目标设备缺库 ./configure --host=$TARGET \ CFLAGS="-static -O2 -Wall" make -j4 $STRIP memtester # 去掉符号表,体积能小一半编译完用file memtester确认一下,输出里应该能看到ELF 64-bit LSB executable, ARM aarch64, statically linked。如果显示dynamically linked,说明-static没生效,检查是不是 configure 把 CFLAGS 覆盖了,可以改成./configure --host=$TARGET LDFLAGS="-static"。参数上,API=21对应 Android 5.0,覆盖绝大多数还在服役的设备;如果你的设备更老,降到 19 也行,但没必要,memtester 不依赖新 API。
2.3 push 进设备并跑通第一条压测命令
编译产物要进设备,最稳的方式是adb push到/data/local/tmp,这个目录在多数 Android 版本上可执行,且不需要 root 就能读写。
adb push memtester /data/local/tmp/ adb shell chmod 755 /data/local/tmp/memtester # 先跑一个最小验证:测 64MB,跑 1 轮 adb shell /data/local/tmp/memtester 64M 1如果输出里出现Loop 1:然后逐项Stuck Address、Random Value、Compare XOR等测试项,最后ok,说明链路通了。这里64M是要测的内存大小,1是循环轮数。注意这个大小不是「占用 64MB 物理内存」的意思,memtester 会去 malloc 这么大一块,然后在这块虚拟内存上做各种模式写入和校验,实际驻留多少由内核决定。所以别一上来就写4G,先小后大,确认设备扛得住再往上加。
2.4 参数怎么设:大小、轮数和测试项的取舍
memtester 的命令行格式是memtester [-p physaddr] <mem> [loops],4.3.0 支持的可选参数不多,但每个都影响结论。内存大小支持K、M、G后缀,不写默认按字节算,基本没人这么干。轮数不写就是无限循环,直到你 Ctrl+C 或者它报错退出——做老化测试时反而常用无限循环,让它跑一整夜。
真正需要权衡的是「测多大」。Android 设备的内存被系统、GPU、各种守护进程占着一部分,你不可能把 8GB 全测了。我的经验值是:先free -m看 available 有多少,然后取 available 的 60% 到 70% 作为压测上限。比如 available 显示 3.2GB,那就测2G左右。测太大,malloc 失败会直接报malloc failed;测太小,覆盖不到高地址段,坏点可能漏掉。轮数上,快速验证跑 3 到 5 轮,老化筛选至少 50 轮起步,工业场景我见过跑 200 轮的。测试项本身不用选,memtester 默认会把 Stuck Address、Random Value、Compare XOR、Compare SUB、Compare MUL、Compare DIV、Compare OR、Compare AND、Sequential Increment、Solid Bits、Block Sequential、Checkerboard、Bit Spread、Bit Flip、Walking Ones、Walking Zeroes 全跑一遍,这些模式各有侧重,比如 Checkerboard 对相邻位干扰敏感,Walking Ones 对地址线故障敏感,全跑才能覆盖全面。
3. 读懂 memtester 的输出:哪些报错是真故障,哪些是系统在捣乱
3.1 正常输出长什么样
一次健康的压测,输出节奏是这样的:先打印memtester version 4.3.0和版权信息,然后pagesize is 4096、pagesizemask is 0xfffffffffffff000,接着want 2048MB (2147483648 bytes)、got 2048MB (2147483648 bytes),再往下就是Loop 1:开头,每个测试项一行,末尾ok。全部跑完会打印Loop 1:到Loop N:的完整记录,最后没有额外总结,直接退出。如果你看到got比want小,说明 malloc 没拿到你要求的全部内存,这时候要么减小测试大小,要么先杀掉一些后台进程。
3.2 报错信息对应的硬件含义
memtester 的报错格式很固定,比如FAILURE: 0x12345678 != 0x12345679 at offset 0x0000a000.,意思是「写入的值和读回的值不一致,发生在偏移 0xa000 处」。这个 offset 是相对于 malloc 那块内存起始地址的偏移,不是物理地址。要定位到物理地址,得结合/proc/self/pagemap去查,但多数时候你不需要那么精确——只要它能稳定复现,就说明这块内存有问题。常见的几类报错:
Stuck Address失败:地址线有问题,写一个地址读出来是另一个地址的值,通常是焊接或 PCB 走线故障。Random Value或Compare XOR失败:数据线或存储单元故障,某个 bit 翻转不过来。Block Sequential失败:连续地址块内出现规律性错误,可能是行/列译码器问题。Walking Ones失败:单个 bit 在地址线上移动时出错,对地址总线故障特别敏感。
如果同一台设备每次都在同一个 offset 附近报错,基本可以判定硬件故障;如果每次 offset 都变,先怀疑是不是系统在后台回收内存或者有别的进程在抢,这时候把测试大小降下来、关掉无关应用再跑。
3.3 用脚本把压测结果落成可追溯的记录
手工看输出不现实,尤其是跑几十轮的时候。我一般写个小脚本,把 memtester 的输出重定向到文件,同时记录时间戳和设备信息,方便后面比对。
#!/system/bin/sh # 放到 /data/local/tmp/run_memtest.sh OUT=/data/local/tmp/memtest_$(date +%Y%m%d_%H%M%S).log SIZE=2G LOOPS=50 echo "=== start $(date) ===" >> $OUT echo "=== meminfo ===" >> $OUT cat /proc/meminfo >> $OUT /data/local/tmp/memtester $SIZE $LOOPS >> $OUT 2>&1 echo "=== exit code: $? ===" >> $OUT echo "=== end $(date) ===" >> $OUT这个脚本里$?是 memtester 的退出码,0 表示全部通过,非 0 表示有失败项。把日志拉回主机后,用grep -i failure就能快速筛出有问题的设备。参数上,SIZE和LOOPS按前面说的原则调,日志文件名带时间戳是为了同一台设备多次压测时不覆盖。
4. 避坑与排查:memtester 在 Android 上最容易翻车的五件事
4.1 报Permission denied或Operation not permitted
现象是 push 进去、chmod 也给了,一执行就报权限错误。原因通常是/data/local/tmp在某些定制 ROM 上被挂载成了noexec,或者 SELinux 策略限制了执行。解决办法是先adb shell mount | grep local看挂载选项,如果有noexec,换到/data/local/tmp之外的目录,比如/data/data下自己建的子目录,或者用adb shell su -c以 root 身份跑。SELinux 的问题可以用adb shell getenforce看,如果是 Enforcing,临时setenforce 0验证一下,确认是策略问题再考虑加规则,别长期关。
4.2 跑着跑着被系统杀掉
现象是 memtester 跑到一半进程消失,日志里没有 FAILURE,只有突然中断。原因是 Android 的 lowmemorykiller 或 LMKD 看到内存压力大,把 memtester 当成可回收进程杀了。解决办法有两个:一是把测试大小降到 available 的 50% 以下,给系统留余量;二是用adb shell am set-inactive之类的手段把 memtester 标记成前台,或者干脆在开发者选项里关掉后台进程限制。更彻底的做法是echo -1000 > /proc/self/oom_score_adj,但需要 root。
4.3 测试结果忽好忽坏,同一台设备两次结论相反
现象是第一次跑报 FAILURE,第二次跑全 ok,或者反过来。原因多半不是硬件,而是内存热插拔、频率调节或者温度导致的时序变化。Android 设备在温度升高时会降频,内存控制器的时序参数也可能跟着变,某些边缘单元在降频后反而稳定了。解决办法是控制变量:跑之前让设备静置到室温,关掉温控相关的守护进程(如果有 root),并且连续跑多轮取统计结果,不要用单次结果下结论。我一般要求同一台设备至少跑 3 次,3 次里 2 次以上报同一地址失败才算硬件问题。
4.4 静态编译后体积异常大或者跑不起来
现象是-static编译出来的二进制有好几 MB,push 进去还报cannot execute。原因是 NDK 的静态 libc 在某些版本上会带上调试信息和未裁剪的符号,或者你用的 API level 太低导致链接了不兼容的 stub。解决办法是编译时加-s让链接器 strip,或者编译完手动llvm-strip;API level 至少 21,别用 19 以下。如果还是跑不起来,用readelf -l memtester看 interpreter 字段,静态链接的应该是空的,如果有/system/bin/linker64说明没静态成功。
4.5 误把系统缓存波动当成内存故障
现象是 memtester 报某个 offset 失败,但那个 offset 每次都在变,而且设备日常使用完全正常。原因是 Android 的页缓存和内存压缩机制会在 memtester 运行期间动态调整页面,导致 memtester 读回的值被系统改动了——虽然 memtester 用的是匿名页,理论上不该被改,但在某些内核配置下,如果内存压力触发 swap 或者 zram,时序会乱。解决办法是压测前sync并echo 3 > /proc/sys/vm/drop_caches(需要 root),关掉 zram 或 swap,并且确保测试大小不超过物理内存的 70%。如果条件不允许,至少把测试大小降到 50% 以下再复测。
5. 把 memtester 用成产线工具:批量跑、自动判定和长期老化
单台设备手工跑只是验证,真正有价值的是把它变成产线或实验室里的批量工具。我的做法是写一个主机侧的 Python 脚本,通过 adb 并发控制多台设备,每台设备 push 同一个 memtester 二进制,跑固定大小和轮数,然后解析日志里的 FAILURE 行,自动给出 pass/fail 判定。核心逻辑不复杂:用subprocess调 adb,用concurrent.futures控制并发数,日志拉回来用正则匹配FAILURE和exit code。这样一次能跑十几台,一晚上出结果。
参数上,产线老化我一般设SIZE=1G、LOOPS=100,单台耗时大约 40 到 60 分钟,取决于内存带宽。如果是研发阶段定位问题,反而会把 SIZE 调大、LOOPS 调小,先快速看有没有稳定复现的坏点。另一个技巧是结合memtester的-p参数做物理地址定向测试,但这个需要 root 且要知道物理地址布局,一般只在深度排查时用。最后说个我自己的习惯:每次压测前先记录设备的温度和meminfo,压测后再记一次,如果两次温差超过 15 度,这次结果我会打个问号——温度对内存时序的影响,比大多数人想象的大。希望帮到你。
本文还有配套的精品资源,点击获取