bounty将于明天到期。回答此问题可获得+100的声望奖励。JustSightseeing正在寻找来自知名来源的答案。
(The完整的代码可以在这里找到(没有leakCanary依赖):https://github.com/Dawwit0001/HiltMultiModule)的数据
我创建了两个片段,一个登录片段和一个注册片段,每当用户打开应用程序时,登录屏幕都会显示。如果用户导航到注册屏幕,创建一个帐户,然后导航回登录屏幕,就会发生泄漏。我不知道为什么会发生泄漏,但我发现当我将登录片段中的“savedInstanceState”替换为null(在onViewCreated中)时,不会发生泄漏。
倾城泄露:
┬───
│ GC Root: Input or output parameters in native code
│
├─ dalvik.system.PathClassLoader instance
│ Leaking: NO (InternalLeakCanary↓ is not leaking and A ClassLoader is never
│ leaking)
│ ↓ ClassLoader.runtimeInternalObjects
├─ java.lang.Object[] array
│ Leaking: NO (InternalLeakCanary↓ is not leaking)
│ ↓ Object[728]
├─ leakcanary.internal.InternalLeakCanary class
│ Leaking: NO (MainActivity↓ is not leaking and a class is never leaking)
│ ↓ static InternalLeakCanary.resumedActivity
├─ winged.example.hiltmultimodule.MainActivity instance
│ Leaking: NO (RegisterFragment↓ is not leaking and Activity#mDestroyed is
│ false)
│ mApplication instance of winged.example.hiltmultimodule.di.
│ HiltMultiModuleApplication
│ mBase instance of androidx.appcompat.view.ContextThemeWrapper
│ ↓ ComponentActivity.mOnConfigurationChangedListeners
├─ java.util.concurrent.CopyOnWriteArrayList instance
│ Leaking: NO (RegisterFragment↓ is not leaking)
│ ↓ CopyOnWriteArrayList[4]
├─ androidx.fragment.app.FragmentManager$$ExternalSyntheticLambda0 instance
│ Leaking: NO (RegisterFragment↓ is not leaking)
│ ↓ FragmentManager$$ExternalSyntheticLambda0.f$0
├─ androidx.fragment.app.FragmentManagerImpl instance
│ Leaking: NO (RegisterFragment↓ is not leaking)
│ ↓ FragmentManager.mParent
├─ winged.example.feature_login.register.RegisterFragment instance
│ Leaking: NO (Fragment#mFragmentManager is not null)
│ componentContext instance of dagger.hilt.android.internal.managers.
│ ViewComponentManager$FragmentContextWrapper, wrapping activity winged.
│ example.hiltmultimodule.MainActivity with mDestroyed = false
│ ↓ Fragment.mSavedViewState
│ ~~~~~~~~~~~~~~~
├─ android.util.SparseArray instance
│ Leaking: UNKNOWN
│ Retaining 417.7 kB in 4154 objects
│ ↓ SparseArray.mValues
│ ~~~~~~~
├─ java.lang.Object[] array
│ Leaking: UNKNOWN
│ Retaining 417.6 kB in 4152 objects
│ ↓ Object[9]
│ ~~~
├─ android.widget.TextView$SavedState instance
│ Leaking: UNKNOWN
│ Retaining 416.1 kB in 4113 objects
│ ↓ TextView$SavedState.text
│ ~~~~
├─ android.text.SpannableStringBuilder instance
│ Leaking: UNKNOWN
│ Retaining 416.0 kB in 4109 objects
│ ↓ SpannableStringBuilder.mSpans
│ ~~~~~~
├─ java.lang.Object[] array
│ Leaking: UNKNOWN
│ Retaining 36 B in 1 objects
│ ↓ Object[0]
│ ~~~
├─ android.text.method.PasswordTransformationMethod$Visible instance
│ Leaking: UNKNOWN
│ Retaining 415.4 kB in 4099 objects
│ ↓ PasswordTransformationMethod$Visible.mText
│ ~~~~~
├─ androidx.emoji2.text.SpannableBuilder instance
│ Leaking: UNKNOWN
│ Retaining 415.4 kB in 4098 objects
│ ↓ SpannableStringBuilder.mSpans
│ ~~~~~~
├─ java.lang.Object[] array
│ Leaking: UNKNOWN
│ Retaining 76 B in 1 objects
│ ↓ Object[0]
│ ~~~
├─ android.widget.TextView$ChangeWatcher instance
│ Leaking: UNKNOWN
│ Retaining 16 B in 1 objects
│ ↓ TextView$ChangeWatcher.this$0
│ ~~~~~~
├─ com.google.android.material.textfield.TextInputEditText instance
│ Leaking: UNKNOWN
│ Retaining 410.0 kB in 3980 objects
│ View not part of a window view hierarchy
│ View.mAttachInfo is null (view detached)
│ View.mID = R.id.repeatPasswordTIET
│ View.mWindowAttachCount = 1
│ mContext instance of androidx.appcompat.view.ContextThemeWrapper, wrapping
│ activity winged.example.hiltmultimodule.MainActivity with mDestroyed =
│ false
│ ↓ View.mParent
│ ~~~~~~~
├─ android.widget.FrameLayout instance
│ Leaking: UNKNOWN
│ Retaining 1.0 kB in 15 objects
│ View not part of a window view hierarchy
│ View.mAttachInfo is null (view detached)
│ View.mWindowAttachCount = 1
│ mContext instance of androidx.appcompat.view.ContextThemeWrapper, wrapping
│ activity winged.example.hiltmultimodule.MainActivity with mDestroyed =
│ false
│ ↓ View.mParent
│ ~~~~~~~
├─ com.google.android.material.textfield.TextInputLayout instance
│ Leaking: UNKNOWN
│ Retaining 381.0 kB in 3284 objects
│ View not part of a window view hierarchy
│ View.mAttachInfo is null (view detached)
│ View.mID = R.id.repeatPasswordTIL
│ View.mWindowAttachCount = 1
│ mContext instance of androidx.appcompat.view.ContextThemeWrapper, wrapping
│ activity winged.example.hiltmultimodule.MainActivity with mDestroyed =
│ false
│ ↓ View.mParent
│ ~~~~~~~
╰→ androidx.constraintlayout.widget.ConstraintLayout instance
Leaking: YES (ObjectWatcher was watching this because winged.example.
feature_login.register.RegisterFragment received Fragment#onDestroyView()
callback (references to its views should be cleared to prevent leaks))
Retaining 2.5 kB in 59 objects
key = 16bf9a7e-c3de-4737-a5c2-8933c6fed9d3
watchDurationMillis = 132084
retainedDurationMillis = 127081
View not part of a window view hierarchy
View.mAttachInfo is null (view detached)
View.mID = R.id.mainCL
View.mWindowAttachCount = 1
mContext instance of dagger.hilt.android.internal.managers.
ViewComponentManager$FragmentContextWrapper, wrapping activity winged.
example.hiltmultimodule.MainActivity with mDestroyed = false
METADATA
Build.VERSION.SDK_INT: 30
Build.MANUFACTURER: unknown
LeakCanary version: 2.10
App process name: winged.example.hiltmultimodule
Class count: 18527
Instance count: 115319
Primitive array count: 86210
Object array count: 17808
Thread count: 21
Heap total bytes: 16303680
Bitmap count: 4
Bitmap total bytes: 228214
Large bitmap count: 0
Large bitmap total bytes: 0
Db 1: open /data/user/0/winged.example.
hiltmultimodule/databases/HiltMultiModuleDB
Stats: LruCache[maxSize=3000,hits=40347,misses=84973,hitRate=32%]
RandomAccess[bytes=4231371,reads=84973,travel=25038680029,range=19100784,size=25
202710]
Analysis duration: 6049 ms
我还在学习,所以任何信息/可能的原因/解决方案将不胜感激,谢谢:)
编辑:
基本片段:
abstract class BaseFragment<T : ViewDataBinding>(@LayoutRes private val fragmentRes: Int) : Fragment() {
private var _binding: T? = null
val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = DataBindingUtil.inflate(inflater, fragmentRes, container, false)
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
fun navigateTo(targetDestination: Int) {
findNavController().navigate(targetDestination)
}
fun navigateUp() {
findNavController().navigateUp()
}
}
登录片段:
@AndroidEntryPoint
class LoginFragment : BaseFragment<FragmentLoginBinding>(R.layout.fragment_login) {
private val viewModel: LoginViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setUpLogInButton()
setUpTextRedirection()
observeForLoginEvents()
}
private fun setUpTextRedirection() {
binding.signUpTV.setOnClickListener {
navigateTo(R.id.registerFragment)
}
}
private fun setUpLogInButton() {
binding.logInBTN.setOnClickListener {
val email = binding.emailTIET.extractText()
val password = binding.passwordTIET.extractText()
if(email.isAValidEmail() && password.isNotBlank()) {
viewModel.logIn(LoginCredentials(mail = email, password = password))
}
}
}
private fun observeForLoginEvents() {
viewModel.loginEvent.observe(viewLifecycleOwner) { result ->
if(result.isSuccess) {
/* Adding some kind of "Main Screen" module would be an idea
but as I've stated previously, this is just a small "test" project
showing off architecture, so I hope you will forgive me <3
(PS: if you are reading this and there still isn't that module, you can make a PR
and add it)*/
Toast.makeText(requireContext(), "Success!", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(requireContext(), "No matching account", Toast.LENGTH_SHORT).show()
}
}
}
}
寄存器片段:
@AndroidEntryPoint
class RegisterFragment: BaseFragment<FragmentRegisterBinding>(R.layout.fragment_register) {
private val viewModel: RegisterViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setUpCreateAccountButton()
setUpTextRedirection()
observeRegisterEvents()
}
private fun setUpTextRedirection() {
binding.logInTV.setOnClickListener {
navigateTo(R.id.loginFragment)
}
}
private fun setUpCreateAccountButton() {
binding.createAnAccountBTN.setOnClickListener {
val email = binding.emailTIET.extractText()
val password = binding.passwordTIET.extractText()
val repeatedPassword = binding.repeatPasswordTIET.extractText()
if(email.isAValidEmail() && (password == repeatedPassword) && password.isNotEmpty()) {
viewModel.saveUser(
LoginCredentials(mail = email, password = password)
)
}
}
}
private fun observeRegisterEvents() {
viewModel.registerEvent.observe(viewLifecycleOwner) { result ->
if(result.isSuccess) {
navigateTo(R.id.loginFragment)
} else {
Toast.makeText(requireContext(), "Something went wrong", Toast.LENGTH_SHORT).show()
}
}
}
正如您可能注意到的,BaseFragment类有一个对视图的引用(绑定变量),但它在onDestoryView中释放了它,所以我认为这应该是有效的,而且在泄漏中它并没有“抱怨”绑定本身
1条答案
按热度按时间ih99xse11#
Found the culprit. The problem comes from an
EditText
that has the input typeandroid:inputType="textPassword"
or any other variant that has a password. In this case, it is one of theTextInputLayout
instance, that has aTextInputEditText
. But: It may need to be combined with the usage of Emoji Library , because the class has an element with the typeandroidx.emoji2.text.SpannableBuilder
that belongs to the Emoji library.The
TextInputEditText
's text is spannable, which means it's not a simple string, it's an object. An object, that can beParcelable
, which means its state can be saved. And, it looks like its actually saved here. No idea how though, sinceParcelable
limits which types can be saved.The memory leak appears to be on the
TextInputEditText
with the IDR.id.repeatPasswordTIET
. In your layout file, you can also search for@+id/repeatPasswordTIET
or@id/repeatPasswordTIET
to find the specific one.Why the leak?
TextView
's (or more likelyEditText
's) have a tendency to not remove their listeners once they are not needed. It's just not configured that way, maybe due to expecting the callers to remove the listeners themselves once they are not needed. A lot of other listeners get cleared once they are not needed, but theTextWatcher
is an exception unfortunately.Examining the leak canary trace,
android.text.method.PasswordTransformationMethod$Visible instance
has aandroidx.emoji2.text.SpannableBuilder
which contains an array, and one of the entries points toandroid.widget.TextView$ChangeWatcher instance
which then shows theTextInputEditText
that is leaked. It is leaked because in the same trace, you can see that the listener is saved toandroid.widget.TextView$SavedState instance
, which I assume gets restored in a future fragment.I actually tried to fetch the value myself, but wasn't able to do it. The saved state did not hold the listener.
Although, I have a potential solution: Delete every listener when the view is not necessary anymore.
Potential solution:
What the class does: It caches all the listeners added in a list, and allows you to call
clearTextChangedListeners()
once it is not needed. (I tried to do this automatically but the lifecycle got confusing once fragments, nested recyclerviews etc... got involved so I left it here)Usage:
Swap with your layouts'
TextInputEditText
with this class, and at your fragment'sonDestroyView
, calleditText.clearTextChangedListeners()
.It should solve your problem, however it's the Android world. It might not.