Android 内存压力工具

做一个「真能顶得住」的 Android 内存压力工具

工程路径:MemoryUseDemo

适用场景:低内存策略验证、LMK / 杀进程观察、整机内存压力测试

做系统稳定性、低内存策略相关验证时,经常需要人为制造内存压力。听起来简单------申请一大块内存不释放就行。真落地后会发现:

  • 申请数字涨上去了,过一会儿 RSS 又掉下来
  • 单进程刚到 六七百 MB 就申请失败
  • 多进程方案写了,界面却一直停在「等待汇报」,全是 0

这个小工具就是为解决这些问题写的:用 native 内存 + 持续踩页 + 多进程分摊 + 前台服务保活,尽量把压力做「实」。


目录

  1. 它解决什么问题
  2. 踩过的坑:为什么形不成压力
  3. 整体架构
  4. [关键实现一:Native 申请与持续踩页](#关键实现一:Native 申请与持续踩页)
  5. 关键实现二:多进程突破单进程上限
  6. [关键实现三:前台服务保活(API 34+ 易踩坑)](#关键实现三:前台服务保活(API 34+ 易踩坑))
  7. 关键实现四:跨进程状态汇总
  8. 怎么用
  9. 小结

1. 它解决什么问题

常见诉求:

  • 把整机可用内存压到紧张,观察行为变化
  • 验证低内存回调、进程回收、前后台策略
  • 需要「持续」压力,而不是申请完就被系统慢慢卸掉

工具能力:

能力 说明
按总目标申请 输入总 MB,自动拆分到各进程
多进程分摊 1~8 个独立进程,突破单进程额度
持续踩页 达标后周期性写回每一页,对抗换出/回收
状态汇总 展示总申请量、总 RSS / Swap,以及每进程明细

2. 踩过的坑:为什么形不成压力

2.1 共享内存:引用还在,物理页没了

第一版用 SharedMemory:映射后按页写一遍,数字很好看。系统一紧张,ashmem 类页往往可被回收,RSS 掉下去,压力也随之消失

2.2 大图:也不一定「实」

第二版换 Bitmap。像素多在 native,看起来更靠谱,但在部分机型上仍可能被换出;申请线程达标就停、不再访问,RSS 一样会慢慢降。

2.3 单进程额度天花板

即使用更实的 native 缓冲,单进程大约 600MB 左右 就容易申请失败。要 GB 级压力,必须多进程。

2.4 结论

  1. 选不容易被当缓存丢掉的占用方式(优先 ByteBuffer.allocateDirect
  2. 占完之后还要 持续访问(踩页)
  3. 单进程不够就 多进程均分
  4. 子进程必须用正确类型的 前台服务,否则起不来

3. 整体架构

text 复制代码
┌─────────────────────┐
│     MainActivity    │  拆分总目标、启停服务、读状态文件汇总
│   (主进程,几乎不占) │
└──────────┬──────────┘
           │ startForegroundService × N
           ▼
┌──────────┴──────────┐
│  MemWorker0 :mem0   │──► MemoryAllocator(申请 + 踩页)
│  MemWorker1 :mem1   │──► 写 files/mem_workers/worker_N.txt
│  MemWorker2 :mem2   │
│  ...                │
│  MemWorker7 :mem7   │
└─────────────────────┘
  • 主进程:只负责调度与 UI,不扛大内存
  • 子进程:各自申请、各自保压、各自汇报
  • 状态文件 :同 UID 下 filesDir 共享,用来跨进程通信(简单够用)

4. 关键实现一:Native 申请与持续踩页

核心类:MemoryAllocator.kt

4.1 分块申请 DirectBuffer

优先 allocateDirect(不走 Java 堆上限那一套);失败再退回堆上的 ByteArray

kotlin 复制代码
private fun allocateChunk(chunkSize: Int): ByteBuffer? {
    try {
        return ByteBuffer.allocateDirect(chunkSize)
    } catch (_: OutOfMemoryError) {
    }
    try {
        val bytes = ByteArray(chunkSize)
        return ByteBuffer.wrap(bytes)
    } catch (_: OutOfMemoryError) {
        return null
    }
}

申请后立刻 按页写入,确保物理页真正落下来(页大小按 4KB):

kotlin 复制代码
private fun touchPages(buffer: ByteBuffer, value: Byte) {
    val size = buffer.capacity()
    var offset = 0
    while (offset < size) {
        buffer.put(offset, value)
        offset += PAGE_SIZE   // 4096
    }
    if (size > 0) {
        buffer.put(size - 1, value)
    }
}

4.2 达标后不退出:keepResident

这是「RSS 慢慢掉」的对症药。达到目标后进入循环,每隔 500ms 对所有块再踩一遍:

kotlin 复制代码
fun start(targetBytes: Long) {
    stop()
    synchronized(state) {
        state.targetBytes = targetBytes
        state.status = 1
        state.stopRequested = false
        state.worker = Thread {
            allocateUntilTarget(targetBytes)
            if (!isStopRequested() && getAllocatedBytes() > 0) {
                keepResident()   // 关键:持续保压
            }
        }.apply {
            name = "MemoryAllocator"
            isDaemon = false
            start()
        }
    }
}

private fun keepResident() {
    var stamp = 1
    while (!isStopRequested()) {
        val snapshot = synchronized(state) { state.buffers.toList() }
        val value = stamp.toByte()
        for (buffer in snapshot) {
            if (isStopRequested()) return
            touchPages(buffer, value)
        }
        stamp = if (stamp >= 127) 1 else stamp + 1
        Thread.sleep(KEEP_INTERVAL_MS)  // 500ms
    }
}

如何解读界面上的 RSS / Swap:

  • RSS 降、Swap 升 → 多半被换出
  • 踩页正常时,RSS 应被重新顶回去

5. 关键实现二:多进程突破单进程上限

5.1 Manifest:每个 Service 绑独立进程

最多 8 个 worker,用 android:process=":memN" 拉起私有进程。类型使用 specialUse(测试工具场景):

xml 复制代码
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

<service
    android:name=".MemWorker0"
    android:exported="false"
    android:foregroundServiceType="specialUse"
    android:process=":mem0">
    <property
        android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
        android:value="Local memory pressure testing" />
</service>

<service
    android:name=".MemWorker1"
    android:exported="false"
    android:foregroundServiceType="specialUse"
    android:process=":mem1">
    <property
        android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
        android:value="Local memory pressure testing" />
</service>
<!-- MemWorker2 ... MemWorker7 同理 -->

对应多个空子类,方便在 Manifest 里分别声明进程:

kotlin 复制代码
open class MemoryHoldService : Service() { /* 共用逻辑 */ }

class MemWorker0 : MemoryHoldService()
class MemWorker1 : MemoryHoldService()
// ...
class MemWorker7 : MemoryHoldService()

companion object {
    val workerClasses: Array<Class<out MemoryHoldService>> = arrayOf(
        MemWorker0::class.java,
        MemWorker1::class.java,
        // ...
        MemWorker7::class.java,
    )
}

5.2 主界面:均分目标并启动

kotlin 复制代码
fun startHolding(targetMb: Long, processCount: Int) {
    stopAllWorkers()
    AllocationReporter.clearAll(this)

    totalTargetMb = targetMb
    activeWorkerCount = processCount

    val base = targetMb / processCount
    val remainder = targetMb % processCount
    for (index in 0 until processCount) {
        val workerTarget = base + if (index < remainder) 1 else 0
        if (workerTarget <= 0) continue

        val serviceIntent = Intent(this, MemoryHoldService.workerClasses[index])
            .setAction(MemoryHoldService.ACTION_START)
            .putExtra(MemoryHoldService.EXTRA_TARGET_MB, workerTarget)
            .putExtra(MemoryHoldService.EXTRA_WORKER_INDEX, index)
        ContextCompat.startForegroundService(this, serviceIntent)
    }
}

举例:总目标 2048MB、进程数 4 → 每进程约 512MB。单进程容易在 ~600MB 触顶时,用 4~8 个进程更容易摸到 GB 级。

5.3 停止:用 stopService,不要「发 STOP 再 START」

早期用 startService(STOP)误拉起 子进程,再和 startForegroundService(START) 抢时机,容易导致子进程起不来或秒停。正确做法:

kotlin 复制代码
private fun stopAllWorkers() {
    for (index in 0 until MemoryHoldService.MAX_WORKERS) {
        stopService(Intent(this, MemoryHoldService.workerClasses[index]))
    }
    AllocationReporter.clearAll(this)
}

6. 关键实现三:前台服务保活(API 34+ 易踩坑)

子进程若不当前台服务跑,很容易被系统立刻收回。

targetSdk 34+ / API 34+ 上,Manifest 声明了 foregroundServiceType 后,startForeground 必须带上对应类型,否则服务直接崩溃------表现就是界面一直「等待汇报」、全是 0。

kotlin 复制代码
private fun promoteToForeground(targetMb: Long) {
    val notification: Notification = NotificationCompat.Builder(this, CHANNEL_ID)
        .setSmallIcon(R.mipmap.ic_launcher)
        .setContentTitle(getString(R.string.notification_title_worker, workerIndex + 1))
        .setContentText(getString(R.string.notification_text_worker, targetMb))
        .setOngoing(true)
        .build()

    // 关键:必须带 FOREGROUND_SERVICE_TYPE_SPECIAL_USE,与 Manifest 一致
    ServiceCompat.startForeground(
        this,
        NOTIFICATION_ID_BASE + workerIndex,
        notification,
        ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE,
    )
}

服务启动流程(简化):

kotlin 复制代码
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
    workerIndex = intent?.getIntExtra(EXTRA_WORKER_INDEX, 0) ?: 0
    val targetMb = intent?.getLongExtra(EXTRA_TARGET_MB, 0) ?: 0

    writeStatus(targetMb = targetMb, allocatedMb = 0, chunkCount = 0, status = 1)

    try {
        promoteToForeground(targetMb)
    } catch (error: Throwable) {
        writeStatus(targetMb = targetMb, allocatedMb = 0, chunkCount = 0, status = 3)
        stopSelf()
        return START_NOT_STICKY
    }

    if (targetMb > 0) {
        holding = true
        MemoryAllocator.start(targetMb * MEBIBYTE)
        startReporting()
    }
    return START_NOT_STICKY
}

可用 adb shell ps -A | grep memoryusedemo 确认是否出现 :mem0:mem1 等进程。


7. 关键实现四:跨进程状态汇总

各子进程的 MemoryAllocator 是进程内单例,主进程读不到。做法是:每个 worker 周期性把状态写到:

text 复制代码
/data/data/<package>/files/mem_workers/worker_N.txt

同应用多进程共享同一 filesDir,主界面定时 readAll 汇总。

7.1 写入

kotlin 复制代码
object AllocationReporter {
    fun write(
        context: Context,
        workerIndex: Int,
        targetMb: Long,
        allocatedMb: Long,
        chunkCount: Int,
        rssMb: Long,
        swapMb: Long,
        status: Int,
    ) {
        val file = statusFile(context, workerIndex)
        file.parentFile?.mkdirs()
        file.writeText(
            buildString {
                append("targetMb=").append(targetMb).append('\n')
                append("allocatedMb=").append(allocatedMb).append('\n')
                append("chunkCount=").append(chunkCount).append('\n')
                append("rssMb=").append(rssMb).append('\n')
                append("swapMb=").append(swapMb).append('\n')
                append("status=").append(status).append('\n')
            }
        )
    }

    private fun statusDir(context: Context): File {
        return File(context.applicationContext.filesDir, "mem_workers")
    }

    private fun statusFile(context: Context, workerIndex: Int): File {
        return File(statusDir(context), "worker_$workerIndex.txt")
    }
}

7.2 读取 RSS / Swap

子进程从 /proc/self/status 读本进程数据写入文件:

kotlin 复制代码
private fun readProcessMemoryMb(): Pair<Long, Long> {
    var rssKb = 0L
    var swapKb = 0L
    File("/proc/self/status").forEachLine { line ->
        when {
            line.startsWith("VmRSS:") -> rssKb = parseKb(line)
            line.startsWith("VmSwap:") -> swapKb = parseKb(line)
        }
    }
    return rssKb / 1024L to swapKb / 1024L
}

主界面把各 worker 的 allocatedMb / rssMb / swapMb 求和,并列出每进程明细。


8. 怎么用

  1. 安装并打开「内存占用工具」
  2. 填写 总目标内存(MB)
  3. 填写 进程数(1~8);单进程易触顶时建议 4 / 6 / 8
  4. 点「开始占用」,观察总申请量与各进程状态
  5. 测完点「释放内存」

注意:

  • 通知栏可能出现多个前台通知(一进程一个),属预期现象
  • 整机内存不够时,部分子进程被杀是正常现象
  • 「已申请」是逻辑进度;是否真正吃内存,以 RSS(及 Swap 变化) 为准
  • 仅建议在本机测试环境使用,可能造成卡顿或杀进程

快速验证命令示例:

bash 复制代码
# 启动并带参数(示例:总 512MB,2 进程)
adb shell am start -n com.zgh.memoryusedemo/.MainActivity \
  --el target_mb 512 --ei process_count 2

# 查看是否拉起子进程
adb shell ps -A | grep memoryusedemo

# 查看状态文件
adb shell run-as com.zgh.memoryusedemo cat files/mem_workers/worker_0.txt

9. 小结

问题 对策 对应代码
申请后被回收 / 换出 DirectBuffer + 持续踩页 MemoryAllocator.touchPages / keepResident
单进程几百 MB 触顶 Manifest 多进程 + 均分启动 android:process + startHolding
子进程起不来、界面全 0 前台服务带类型;用 stopService 停止 ServiceCompat.startForeground(...SPECIAL_USE)
主界面看不到子进程进度 写共享目录状态文件 AllocationReporter

做内存压力 Demo,难点不在「申请」本身,而在:占得实、留得住、撑得大、启得来。把这四件事做对,「假占用」才能变成「真压力」。


附:主要源码文件

文件 职责
MemoryAllocator.kt 单进程内申请 / 踩页 / 保压
MemoryHoldService.kt + MemWorker0..7 前台服务、拉起独立进程
AllocationReporter.kt 跨进程状态文件读写
MainActivity.kt UI、拆分目标、汇总展示
AndroidManifest.xml 权限、多进程 Service 声明
相关推荐
雅客李9 小时前
2026云手机延迟数据测评 安卓云手机远程操控实测哪个最好
android·智能手机
YM52e10 小时前
鸿蒙Flutter Card组件:Material设计风格卡片
android·学习·flutter·华为·harmonyos·鸿蒙
Xzaveir1 天前
不要用一个状态表示“号码已认证”:企业号码身份的四域模型
android·人工智能
(Charon)1 天前
【C++】手写 MySQL 连接池(二):同步连接的获取、加锁与释放
android
zhangjin11201 天前
AOSP下载
android
Mico181 天前
MySQL 8.0.35 GTID 主从复制搭建-基于GITD
android·mysql·adb
YXL1111YXL1 天前
LeakCanary 源码解析检测泄露工作机制(一)
android·leakcanary
且随疾风前行.1 天前
Android Binder 驱动 - 内核驱动层源码初探
android·网络·binder
额恩661 天前
AI 智能体从零搭建实战教程——扣子
android·rxjava·coze