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

Android中LoginActivity因SemClipboardManager引发内存泄漏的解决方法

解决LoginActivity因SemClipboardManager导致的内存泄漏问题

我的Android应用中,LoginActivity已收到Activity#onDestroy()回调且mDestroyed为true,但检测到内存泄漏。泄漏链显示SemClipboardManager持有该Activity的Context引用,具体泄漏日志如下:

┬───
│ GC Root: Global variable in native code
│
├─ com.samsung.android.content.clipboard.SemClipboardManager$2 instance
│    Leaking: UNKNOWN
│    Retaining 204.4 kB in 4075 objects
│    Anonymous subclass of android.sec.clipboard.IClipboardDataPasteEvent$Stub
│    ↓ SemClipboardManager$2.this$0
│                            ~~~~~~
├─ com.samsung.android.content.clipboard.SemClipboardManager instance
│    Leaking: UNKNOWN
│    Retaining 204.4 kB in 4074 objects
│    mContext instance of ....presentation.ui.login.LoginActivity with mDestroyed = true
│    ↓ SemClipboardManager.mContext
│                          ~~~~~~~~
╰→ ....presentation.ui.login.LoginActivity instance
​     Leaking: YES (ObjectWatcher was watching this because ....presentation.ui.login.LoginActivity
​     received Activity#onDestroy() callback and Activity#mDestroyed is true)
​     Retaining 202.6 kB in 4027 objects
​     key = e0f29b52-1c73-4933-9856-a987582ccb1d
​     watchDurationMillis = 22643
​     retainedDurationMillis = 17638
​     mApplication instance of ....presentation.App
​     mBase instance of androidx.appcompat.view.ContextThemeWrapper

LoginActivity的代码如下:

@AndroidEntryPoint
class LoginActivity : AppCompatActivity(R.layout.activity_login) {
    private val lessonsViewModel by viewModels<LoginViewModel>()
    private var context: Context? = null
    private var binding: ActivityLoginBinding? = null
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityLoginBinding.inflate(layoutInflater)
        setContentView(binding?.root)
        context = this@LoginActivity
        context?.let {
            SemEmergencyManagerLeakingActivity.applyFix(context!!.applicationContext as Application)
        }
        with(binding!!) {
            btnSignIn.setOnClickListener {

                if (edtEMail.text.isNullOrBlank() || edtEMail.text.isNullOrEmpty()) {
                    txtInputUserName.error = getString(R.string.error_user_name)
                    if (!edtPasswords.text.isNullOrBlank() && !edtPasswords.text.isNullOrEmpty()) {
                        txtInputPassword.error = null
                    }
                } else if (edtPasswords.text.isNullOrBlank() || edtPasswords.text.isNullOrEmpty()) {
                    txtInputPassword.error = getString(R.string.error_user_password)
                    if (!edtEMail.text.isNullOrBlank() && !edtEMail.text.isNullOrEmpty()) {
                        txtInputUserName.error = null
                    }
                } else if (
                    !edtEMail.text.isNullOrBlank() &&
                    !edtEMail.text.isNullOrEmpty() &&
                    !edtPasswords.text.isNullOrBlank() &&
                    !edtPasswords.text.isNullOrEmpty()
                ) {
                    hideBtnSignIn()
                    callAPI(edtEMail.text.toString(), edtPasswords.text.toString())
                }
            }
        }

    }

    private fun callAPI(userName: String, password: String) {
        lifecycleScope.launch {
            val response = lessonsViewModel.getLoginValues(userName, password)
            response.observe(this@LoginActivity) {
                when (it) {
                    is Resource.Success -> {
                        it.data?.let { vl ->
                            hideProgressBar()
                            disableBtnSignIn()
                            lessonsViewModel.saveTokensLogged(
                                TokensDataStore(
                                    vl.token!!,
                                    vl.privatetoken!!
                                )
                            )
                            val intent = Intent(context, MainActivity::class.java)
                            startActivity(intent)
                            finish()
                        }
                    }
                    is Resource.Error -> {
                        it.message?.let { msg ->
                            hideProgressBar()
                            showBtnSignIn()
                            when (msg) {
                                getString(R.string.invalid_login) -> {
                                    Toast.makeText(
                                        context,
                                        R.string.error_user_pass,
                                        Toast.LENGTH_LONG
                                    )
                                        .show()
                                }
                                getString(R.string.service_not_available) -> {
                                    Toast.makeText(
                                        context,
                                        R.string.error_service_not_available,
                                        Toast.LENGTH_LONG
                                    )
                                        .show()
                                }
                                else -> {
                                    Toast.makeText(context, msg, Toast.LENGTH_LONG)
                                        .show()
                                }
                            }
                        }
                    }
                    is Resource.Loading -> {
                        hideBtnSignIn()
                        showProgressBar()
                    }
                }
            }
        }
    }

    private fun showBtnSignIn() {
        binding!!.btnSignIn.visibility = View.VISIBLE
    }

    private fun hideBtnSignIn() {
        binding!!.btnSignIn.visibility = View.INVISIBLE
    }

    private fun disableBtnSignIn() {
        binding!!.btnSignIn.isEnabled = false
    }

    private fun hideProgressBar() {
        binding!!.prLogin.visibility = View.INVISIBLE
    }

    private fun showProgressBar() {
        binding!!.prLogin.visibility = View.VISIBLE
    }

    override fun onDestroy() {
        context = null
        binding = null
        super.onDestroy()
    }
}

问题分析

从泄漏链可以明确,SemClipboardManager实例持有了LoginActivity的Context引用,导致Activity销毁后无法被GC回收。这是三星设备特有的系统服务泄漏问题,SemClipboardManager是三星定制的剪贴板管理类,内部可能因注册了剪贴板事件监听而长期持有传入的Context。

修复方案

1. 替换Activity Context为Application Context

优先使用生命周期与应用一致的Application Context,避免Activity Context被长期持有:

  • 移除LoginActivity中不必要的context成员变量
  • 所有需要Context的场景(如Toast、Intent、系统服务调用),直接使用applicationContext

修改后的关键代码:

// 移除private var context: Context? = null

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = ActivityLoginBinding.inflate(layoutInflater)
    setContentView(binding?.root)
    // 直接传入Application Context
    SemEmergencyManagerLeakingActivity.applyFix(applicationContext as Application)
    
    with(binding!!) {
        btnSignIn.setOnClickListener {
            // 原有逻辑不变
        }
    }
}

private fun callAPI(userName: String, password: String) {
    lifecycleScope.launch {
        val response = lessonsViewModel.getLoginValues(userName, password)
        response.observe(this@LoginActivity) {
            when (it) {
                is Resource.Success -> {
                    it.data?.let { vl ->
                        // ...
                        val intent = Intent(applicationContext, MainActivity::class.java)
                        startActivity(intent)
                        finish()
                    }
                }
                is Resource.Error -> {
                    it.message?.let { msg ->
                        // ...
                        Toast.makeText(applicationContext, msg, Toast.LENGTH_LONG).show()
                    }
                }
                // ... 其他分支逻辑不变
            }
        }
    }
}

2. 清理LiveData观察者

Activity销毁时,主动移除ViewModel中LiveData的观察者,避免观察者持有Activity引用:

private var loginObserver: Observer<Resource<*>>? = null

private fun callAPI(userName: String, password: String) {
    lifecycleScope.launch {
        val response = lessonsViewModel.getLoginValues(userName, password)
        loginObserver = Observer {
            // 原有逻辑不变
        }
        response.observe(this@LoginActivity, loginObserver!!)
    }
}

override fun onDestroy() {
    binding = null
    // 移除观察者
    loginObserver?.let { observer ->
        lessonsViewModel.getLoginValues("", "").removeObserver(observer)
    }
    super.onDestroy()
}

注意:如果getLoginValues每次调用都会生成新的LiveData实例,此方法可能无效,建议在ViewModel中维护一个固定的LiveData实例,而非每次请求创建新实例。

3. 三星设备专属清理(可选,谨慎使用)

若前两种方案无效,可尝试通过反射清理SemClipboardManager中的Context引用,但此方式存在系统版本兼容性风险:

override fun onDestroy() {
    super.onDestroy()
    binding = null
    // 仅在三星设备执行
    if (Build.BRAND.equals("samsung", ignoreCase = true)) {
        try {
            val clipboardService = getSystemService(Context.CLIPBOARD_SERVICE)
            if (clipboardService is SemClipboardManager) {
                val mContextField = SemClipboardManager::class.java.getDeclaredField("mContext")
                mContextField.isAccessible = true
                mContextField.set(clipboardService, null)
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.26 06:39:54