Keeping a reference to a View in a Fragment causes

2020-06-17 05:31发布

Somebody told me the following, but I am a bit perplexed.

Please, would you be able to confirm or dispute it?

(the Fragment is not retained via setRetainInstance()


At the moment it is a common practice to initialize views in Fragments like this:

private lateinit var myTextView: TextView

fun onViewCreated(view: View, bundle: Bundle) {

     ...

     myTextView = view.findViewById(R.id.myTextViewId)

     ...

}

And then we never nullify this property. Though this is a common practise, it is causing a memory leak.

Background to this:

Let's say, FragmentA has a reference to a childView of it's View, as an instance field. Navigation from fragment A to B is executed by FragmentManager using a specific FragmentTransaction. Depending on the type of transaction, the Manager might want to kill the View only but still persist the instance of FragmentA(see below lifecycle part where it says "The fragment returns to the layout from the back stack"). When user navigates back from FragmentB to FragmentA, the previous instance of FragmentA will be brought to the front, but a new View will be created.

The issue is that if we keep instance to our view in the lateinit property and never clear the reference to it, the view cannot be fully destroyed, causing memory leak.

6条回答
ら.Afraid
2楼-- · 2020-06-17 06:01

Please, is there an official answer on the matter?

An official answer regarding this matter is,

The Memory Profiler is a component in the Android Profiler that helps you identify memory leaks and memory churn that can lead to stutter, freezes, and even app crashes.

In this documentation, android officials teach you how to figure out memory leak by yourself so that they don't have to answer on each and every test cases a user may perform. Besides you can use LeakCanary which does a great job in detecting memory leak.

For your convenience, I performed a heap analysis (a similar but extended version of your use case). Before showing the analysis report I would like to give a step by step basic overview of how the memories will be de/allocated in your case,

  1. On open FragmentA: It's content/root View and the TextView will be allocated into the memory.

  2. On Navigate to FragmentB: onDestroyView() of FragmentA will be called, but FragmentA's View can not be destroyed because the TextView holds a strong reference to it and FragmentA holds a strong reference to TextView.

  3. On Navigate back to FragmentA from FragmentB: The previous allocation of the View and TextView will be cleared. At the same time, they will get new allocations as the onCreateView() is called.

  4. On Back press from FragmentA: The new allocations will be cleared as well.

Answer to your question:

In step 2, we can see there is a memory leak as the retain memory of View is not freed up what it was supposed to be. On the other hand, from step 3 we can see that the memory is recovered as soon as the user returns back to the Fragment. So we can figure out that this kind of memory leak persist till the FragmentManager brings the Fragment back.

Example / Statical Analysis

To test your case, I have created an application. My application has an Activity with a Button and a FrameLayour which is the container for fragments. Pressing the Button will replace the container with FragmentA. FragmentA contains a Button, pressing that will replace the container with FragmentB. FragmentB has a TextView which is stored in the fragment as an instance field.

enter image description here

This report is based on the following operation performed on the above application (Only the Views that I created i.e. ConstraintLayout, Framelayout, Button and TextView are taken in consideration),

  1. Opened the app: Activity visible
  2. Pressed the Button in the Activity: FragmentA visible
  3. Pressed the Button in FragmentA: FragmentB visible and FragmentA onDestroyView()
  4. Pressed the Button in the Activity: 2nd instance FragmentA visible and FragmentB onDestroyView(). (This is the same as step 2 in the previous example except, FragmentB acts as A and the 2nd instance of FragmentA acts as B)
  5. Pressed the Button in the 2nd instance FragmentA: 2nd instance of FragmentB visible and 2nd instance of FragmentA onDestroyView().
  6. Pressed Back Button: 2nd instance of FragmentA visible and 2nd instance of FragmentB onDetach()
  7. Pressed Back Button: 1st instance of FragmentB visible and 2nd instance of FragmentA onDetach()
  8. Pressed Back Button: 1st instance of FragmentAvisible and 1st instance of FragmentB onDetach()
  9. Pressed Back Button: 1st instance of FragmentA onDetach()
  10. Pressed Back Button: which closed the application.

Observation

If you look into the report you can see, in step 1, each and every view lived until the app is closed. In step 2, the View of FragmentA i.e. FrameLayout and it's child, Button are allocated and got both cleared in step 3 which is expected. In step 3, the View of FragmentB i.e. FrameLayout and its child TextView is allocated but did not get cleared in step 4 hence, caused memory leak but cleared in step 7 when it's View is created again and allocated newly created View. On the other hand, the Views that are created in step 5 just got cleared in step 6 causing no memory leak, because the fragment was detached and they didn't prevent the fragment from being cleared up.

Conclusion

We observed that the leak from saving views in fragment lasts until the user returns back to the fragment. When the fragment is brought back i.e. onCreateView() is called, the leak is recovered. On the other hand, no leak happens when the fragment is on top and can only go back. Based on it, we can make the following conclusion,

  • When there is no forward transaction from a fragment, there is nothing wrong with saving views as a strong reference as they will be cleared in onDetach()
  • If there is a forward transaction we can store weak references of views so that they are cleared in onDestroyView()

P.S. If you don't understand heap dump, please watch Google I/O 2011: Memory management for Android Apps. Also, this link provides valuable information about memory leak.

I hope my answer helped you clear your confusion. Let me know if you still have confusion?

查看更多
家丑人穷心不美
3楼-- · 2020-06-17 06:07

I don't think there is a direct official answer, but the answer is in the documentation and it can be easily proven. If you check fragment lifecycle and description of onDestroyView method, it says:

  1. After onDestroyView is called the layout is detached and next time fragment attaches the view is recreated.
  2. The fragment can actually come back from the back stack and onCreateView is called in this case.

That basically means if you keep any view references in the fragment instance while the fragment is in back stack, you are preventing these views from being garbage collected.

If you are still not convinced, you can take a look at the source code of AOSP. For example ListFragment and PreferenceFragment both set all views references to null in onDestroyView().

In addition it's pretty easy to prove there is a memory leak. Just create a fragment with an image view, which displays a full-size image. Keep a reference in your fragment and navigate to another fragment with a fragment transaction. Then tap force garbage collection button and create a heap dump with the memory profiler tool and you'll see your fragment is actually holding image view and the image itself, preventing them from being collected. If you repeat the same steps, but setting the reference to null in onDestroyView(), you'll see your fragment's retained size is much smaller and no ImageView instance is present in the heap anymore.

查看更多
Anthone
4楼-- · 2020-06-17 06:11

From a practical point of View: The main reason for memory leaks is keeping static fields, in particular static Context, which should be avoided. Within an attached Fragment, this usually is not even required. static fields should be called .close() and set to null before super.onDetach() or super.onDestroy(). One does not even have keep handles to any views, when using data-binding. In Kotlin there are also synthetic accessors for that. Keeping handles to views is not required at all, which renders the question obsolete. This was required before both of these existed. Use lint, ktlint or leakcanary or memory-profiler to find potential memory leaks.

查看更多
The star\"
5楼-- · 2020-06-17 06:11

Disclaimer:

I will post another answer because I think I couldn't extract the exact use case from the question in the first read. I am waiting for the approval of the edit request I made to this question to know I understood the question properly. I am keeping this answer because I believe there are some useful tips and links that might help someone. On the other hand, in certain cases my answer is right.

Keeping a reference to a View in a Fragment causes memory leaks?

ABSOLUTELY NOT since the field is declared private, no object from outside of the Fragment i.e. Activity can access a hard reference of it from the Fragment object. Therefore, it will not prevent the Fragment object from being garbage collected.

You might ask, will it cause memory leak when I use this reference in an async callback?

My answer would be, yes it will cause memory leak for keeping reference inside the async callback but not due to keeping its reference in the Fragment. However, this memory leak will also happen even if you don't store the View reference in the Fragment.

In general, to avoid memory leak, you should watch out the following simple patterns,

  1. Never reference views inside Async callbacks
  2. Never reference views from static objects
  3. Avoid storing views in a collection that stores values as hard references

This official video, DO NOT LEAK VIEWS (Android Performance Patterns Season 3 ep6) will help you understand it better.

查看更多
叛逆
6楼-- · 2020-06-17 06:15

Edit2

Indirect official information:

  • methods of FragmentTransaction commitAllowingStateLoss() and commitNowAllowingStateLoss() indicate that the Fragment added to backstack with saved state if no explicitly stated otherwise.

  • method of Fragment setRetainInstance(boolean retain) indicates that Fragment can save its state between orientation changes and other Activity recreations - it means that FragmentManager exists relatively independently of Activitys life cycle and can store Fragment state even if Activity is destroyed.

  • A small remark in the description of onDestroyView

    Internally it is called after the view's state has been saved but before it has been removed from its parent.

Which indicates the exact time of saving Fragment state.

All these points combined almost explicitly state that there is a state of a Fragment view and it is stored in a memory between navigation events.

Edit2 end

The first issue is in your question.

You state that the code you provided is a common practice to initialise views in Fragments. Well it is not a common practice at all. It is an old and outdated way Google only uses in its samples and examples. While it is enough for samples it is no good for the production.

  • One of the current official standards for Kotlin from Google is to init views in fragments or activities via synthetics. Google even use this approach in its modern samples and examples. There is a method called clearFindViewByIdCache() that is able to get rid of all strong synthetic references when you need it(most commonly in onDestroyView).

  • The second standard is to use Android Data Binding via <layout></layout>, <data></data> tags in layout xml files and ViewModels in code. It is applicable both for Kotlin and Java and pretty easy and straightforward. One of the reasons it was done is to get rid of memory leaks while using the old "standard way", make it easy to retain state on configuration changes and unify approach to modern UI layer implementation. To completely get rid of possible memory leak you will have to nullify bindings in onDestroyView though. If implemented well! it handles all the stuff out of the box including memory leaks(their absence), retaining view state on config changes, updating UI with relevant data from either network or database via LivaData, general communication with the UI, handling Android JetPack features and many more. Along with the rest of JetPack features currently it is a Google recommended way to create Android apps

  • There is a third semiofficial approach - usage of Butterknife. If implemented well it also is able to handle correct releasing of UI resources to avoid common memory leaks related to UI. The library has bind()(in onCreateView) and unbind()(in onDestroyView) methods that handle the stuff you mentioned in your question.

  • The last in this answer(but not in production)) method is to use WeakReference, SoftReference or PhantomReference - it is a general Java programming technique to avoid memory leaks and to allow GC of objects. It is not very common practice in Android but it is still a good way to handle strong references locks.

Bonus!) Not to worry about onDestroyView you can use delegation technique and AutoClearedValue in Kotlin.

We can declare auto-clearing properties like this:

var myTextView by autoCleared<TextView>()

…and set their value just like we would for a simple property:

myTextView = view.findViewById(R.id.myTextViewId)

So now regarding whether the code in your question causes memory leaks. Well it most certainly do. It is not even a subject to dispute. It is not stated anywhere officially because it is considered as a common knowledge since it is just how the JWM and Android base classes work.

Edit

Some people in answers claim that there is no leak. Well in the traditional Android understanding - there is no leak - not Activity nor Fragment are leaked - fragment references are alive and placed where they need to be - in fragment managers back stack.

The problem is - the leak still persists. It is not a traditional one thus LeakCanary won't find it. But you can find it in debug and profiling. It is still a leak though. Strong references to the views inside a fragment retain during the back stack transaction thus - they store their objects. While ordinary text views or buttons are not that heavy for the heap - the ones that store images - are quite the opposite - they can fill the heap pretty fast. It happens because Android wants to save the most of the fragments view state to restore it as fast as it possible - so the user won't see the blank screen. There may be also an issue when two layouts of the same fragment are present in the view hierarchy and the references refer to the old layout that is quite below and currently invisible. It was my bugs since I handled navigation and storing states in a bad and a wrong manner, but it shows that the old view may be present in the heap.

Before the Android Jet Pack era this leak was one to ignore, since there were no that extensive fragments usage and navigation between them. So the heap could handle the resources. But now with single Activity approach this may become one of the main reasons of OutOfMemoryError using content heavy fragments without clearing resources in onDestroyView().

Hope it clarifies some corners.

Edit end

Hope it helps.

查看更多
贼婆χ
7楼-- · 2020-06-17 06:15

There's a Fragment lifecycle method called onDestroyView which you should override to release any reference to the views.

Generally you should only use lateinit var view references if your Fragment is permanently added to the Activity and it will not be removed.

Kotlin View Binding extensions already solve this problem by automatically clearing view cache inside onDestroyView.

查看更多
登录 后发表回答