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
相关产品推荐
相关产品推荐

