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 声明
相关推荐
千里马学框架2 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台2 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone2 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc2 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo2 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077002 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼2 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone2 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen2 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone2 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui