You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android应用内存持续上涨(Profiler中Others类)排查与优化咨询

Android应用内存持续增长排查与优化问题

应用背景

我正在开发一款Android应用,核心功能包括:

  • 从API获取详情;
  • 通过MQTT接收更新;
  • 根据接收的数据展示内容,具体包括:
    • 使用ExoPlayer播放视频;
    • 使用Glide加载图片;
    • 使用WebView展示网页及YouTube视频;
  • 将展示的内容详情上报至OpenSearch。

应用启动时,Android Profiler显示内存占用约200MB,但运行数小时后内存持续上涨。Profiler检测显示0内存泄漏,但内存图中的“Others”类别占比随时间不断增长。我已附上应用运行3小时后的堆转储支配树截图。

问题

  1. 如何定位Android Profiler中“Others”类别内存增长的原因?
  2. 使用ExoPlayer、Glide、WebView及MQTT的应用,有哪些内存管理最佳实践?
  3. 该问题是否与Native内存分配(如位图、文件句柄)而非Java堆内存相关?

相关代码

@SuppressLint("SetTextI18n", "SetJavaScriptEnabled")
@OptIn(UnstableApi::class)
fun startLoop() {
    // 如果该分区初始有视频内容,初始化ExoPlayer并绑定PlayerView
    if (hasVideoInitially) {
        val loadControl = DefaultLoadControl.Builder()
            .setBufferDurationsMs(
                2000, 2000, 1000, 2000
            )
            .build()

        val renderersFactory = DefaultRenderersFactory(context)
            .setEnableDecoderFallback(true)

        exoPlayer =
            ExoPlayer.Builder(context, renderersFactory)
                .setLoadControl(loadControl)
                .build()
        playerView = PlayerView(context).apply {
            useController = false
            setShutterBackgroundColor(Color.TRANSPARENT)
            layoutParams = FrameLayout.LayoutParams(
                FrameLayout.LayoutParams.MATCH_PARENT,
                FrameLayout.LayoutParams.MATCH_PARENT
            )
        }
        playerView?.player = exoPlayer
        partitionContainer.addView(playerView)
    }

    // 创建用于展示图片/PDF的ImageView
    imageView = ImageView(context).apply {
        layoutParams = FrameLayout.LayoutParams(
            FrameLayout.LayoutParams.MATCH_PARENT,
            FrameLayout.LayoutParams.MATCH_PARENT
        )
        scaleType = ImageView.ScaleType.FIT_XY
        visibility = GONE
    }
    partitionContainer.addView(imageView)

    // 创建用于展示HTML内容的WebView
    webView = WebView(context).apply {
        layoutParams = FrameLayout.LayoutParams(
            FrameLayout.LayoutParams.MATCH_PARENT,
            FrameLayout.LayoutParams.MATCH_PARENT
        )
        visibility = GONE
        settings.javaScriptEnabled = true
        settings.domStorageEnabled = true
        settings.cacheMode = WebSettings.LOAD_NO_CACHE
        settings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
        WebView.setWebContentsDebuggingEnabled(true)
    }
    partitionContainer.addView(webView)

    // 创建“无内容”提示TextView,初始隐藏
    noContentTextView = TextView(context).apply {
        text = "暂无内容可播放。"
        textSize = 18f
        setTextColor(Color.CYAN)
        gravity = Gravity.CENTER
        visibility = GONE
    }
    partitionContainer.addView(noContentTextView)

    // 启动主循环
    scope.launch {
        while (isActive) {
            try {
                // 加锁获取当前内容的快照
                val snapshot = contentLock.withLock { contentList }

                if (snapshot.isEmpty()) {
                    // 如果从未播放过内容,仅等待;如果之前播放过,显示“无内容”提示
                    if (hasEverPlayedContent) {
                        noContentTextView?.visibility = VISIBLE
                        noContentTextView?.bringToFront()
                    }
                    delay(2000)
                } else if (snapshot.size == 1 && snapshot[0].contentType.lowercase(Locale.ROOT) == "widget") {

                    // 无限展示widget
                    displayWidgetIndefinitely(snapshot[0])
                    // 退出循环
                    break
                } else {
                    // 隐藏“无内容”提示
                    noContentTextView?.visibility = GONE

                    // 遍历内容快照
                    for (item in snapshot) {
                        if (!isActive) break  // 若协程被取消则退出循环
                        if (!hasFiredFirstContentPlayed) {
                            hasFiredFirstContentPlayed = true
                            onFirstContentPlayed?.onFirstContentPlayed()
                        }
                        hasEverPlayedContent = true
                        displayContent(item)
                    }
                }
            } catch (e: Exception) {
                FirebaseCrashlytics.getInstance().recordException(e)
            }
        }
    }
}

回答

1. 定位“Others”类别内存增长的方法

Android Profiler的“Others”类别包含Native内存、未被追踪的Java对象、系统分配内存等,可通过以下步骤定位:

  • Native内存专项分析:
    打开Profiler的Memory标签,切换到Native Memory视图,记录内存分配快照,对比运行前后的Native内存变化;用adb shell dumpsys meminfo <包名>查看Native Heap、Graphics、Code字段的增长情况,这些都属于“Others”范畴。
  • 堆转储Native层分析:
    生成堆转储后,切换到Profiler的Native标签查看支配树,定位占用内存较大的Native对象,比如ExoPlayer解码器、WebView Chromium内核对象、Glide位图Native内存。
  • 资源句柄排查:
    用adb shell lsof -p <进程ID>查看打开的文件句柄、Socket连接,检查是否有未释放的MQTT连接、视频文件句柄。
  • 场景隔离测试:
    单独启用视频播放、图片加载、WebView展示功能,观察“Others”内存是否单独增长,锁定问题模块。

2. 各组件内存管理最佳实践

ExoPlayer

  • 复用Player实例:避免频繁创建销毁,建议在页面生命周期内复用;播放完成或页面销毁时必须调用release()释放解码器、缓冲资源。
  • 优化缓存与加载控制:配置合理的LoadControl缓冲时长,用CacheDataSource限制缓存大小,避免无限制缓存视频数据。
  • 绑定生命周期:将ExoPlayer与Activity/Fragment生命周期绑定,后台时暂停播放并调用releaseSurface()释放渲染资源。

Glide

  • 主动清理资源:页面销毁时调用Glide.with(context).clear(imageView),避免ImageView持有Bitmap引用。
  • 限制内存缓存:通过GlideBuilder设置内存缓存上限,比如setMemoryCache(LruResourceCache(maxSize)),防止占用过多内存。
  • 按需加载尺寸:加载图片时指定目标ImageView的尺寸,用override(width, height)避免加载过大Bitmap。

WebView

  • 正确销毁实例:不再使用时调用webView.destroy()而非仅隐藏,同时清理缓存与Cookie:webView.clearCache(true)、CookieManager.getInstance().removeAllCookies(null)。
  • 禁用冗余功能:关闭不需要的JavaScript、DOM Storage,静态内容场景可直接禁用JavaScript减少内存占用。
  • 生命周期同步:后台时调用webView.onPause()停止页面渲染与JS执行,恢复时调用webView.onResume()。

MQTT

  • 复用连接实例:保持单一MQTT连接,避免频繁创建;断开时调用client.disconnect()释放Socket资源。
  • 控制消息队列:设置合理的消息缓存大小,避免积压过多未处理消息导致内存上涨。
  • 清理订阅:页面销毁时取消不必要的MQTT订阅,避免回调持有上下文引用。

3. 是否与Native内存相关?

大概率是Native内存导致的:

  • Profiler的“Others”类别核心组成就是Native内存,而你提到Java堆无泄漏,符合Native内存未被回收的特征。
  • 你使用的组件均依赖大量Native内存分配:
    ExoPlayer的视频解码器、帧缓冲,Glide加载的Bitmap(Android 8.0+默认存储在Native层),WebView的Chromium内核,这些资源若未正确释放,会持续占用Native内存且无法被Java GC回收。

内容的提问来源于stack exchange,提问作者karthik vs

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 00:18:10