Android Navigation is a component within Google Jetpack that supports page Navigation in Android applications.

The application we developed chose this technical solution in the iteration of the 2.0 major version. Now the version has just been tested. While it is fresh, I would like to share with you the pits encountered.

First of all, I would like to clarify that this article is not an introduction to Navigation, nor an introduction to the principle of Navigation. It can be used as an explanation of our team’s experience in the use of Navigation, which is more suitable for students who have project practice in Navigation.

Here also welcome interested students, if there is this aspect of the exchange, welcome to contact, discuss together. The following are some personal insights from project practice, which are inevitably flawed and correct.

Advantages of Navigation

In the process of use, we feel the following advantages.

  1. Page jump performance is better. In the architecture of single Activity, the View is destroyed after each fragment is pressed. Compared with the previous Activity jump, it is lighter and requires less memory.
  2. Sharing data through the Viewmodel is much easier, without the need to pass data back and forth between pages.
  3. Unified Navigation API for finer control of jump logic.

The center of all pits

All pits associated with Navigation have a center. Normally, a Fragment is a View, and the life cycle of the View is the life cycle of the Fragment. However, in Navigation architecture, the life cycle of the Fragment is different from the life cycle of the View. When navigate reaches the new UI, the overridden UI, the View is destroyed, leaving the fragment instance intact and will be re-created when the fragment is resumed. This is the root of evil.

Eight holes were sorted out. We went through them one by one

Start with a little pit and feel some Navigation.

1. Databinding requires onDestroyView to be set to Null.

Now everyone is using the Databinding technology in Jetpack, which really helps simplify a lot of the code. The self-aware lifecycle in Jetpack allows us to update the UI only when necessary.

ViewDataBing is normally initialized in the Fragment’s onCreateView template function so that a Fragment holds a reference to the View. The fragment life cycle is different from that of a view. When a view is destroyed, it does not have to be destroyed. Therefore, be sure to set the reference to the view to NULL in the fragment.onDestroyView function. Otherwise, the view will not be recycled. The last official code to illustrate.

private var _binding: ResultProfileBinding? = null // This property is only valid between onCreateView and // onDestroyView. private val binding get() = _binding!! override fun onCreateView( inflater: LayoutInflater, container: ViewGroup? , savedInstanceState: Bundle? ) : View? { _binding = ResultProfileBinding.inflate(inflater, container, false) val view = binding.root return view } override fun onDestroyView() { super.onDestroyView() _binding = null }Copy the code

2. when Databinding encounters an error lifecycle.

Databinding is really powerful in that it can bind data to the UI, so there’s a requirement for the UI, the UI has to know its life cycle, know when it’s Active or InActive. So we must set the correct lifecycle for databinding.

Let’s take a look at some problematic code:

override fun onCreateView( inflater: LayoutInflater, container: ViewGroup? , savedInstanceState: Bundle? ) : View {_binding = HomeFragmentBinding. Inflate (inflater, container, false) binding. LifecycleOwner = this / / the problem code here!!! return binding.root }Copy the code

This code works fine and seems to be executing as expected. Even the official code is written that way. LeakCanary cannot detect memory leaks. LeakCanary can only detect memory leaks from Activity, Fragment and View instances, and there is no way to analyze common class instances.

The problem arises when databinding encounters an incorrect lifecycle. Without Navigation, the View lifecycle is the same as the Fragment lifecycle, but with Navigation, The life cycles of the two are inconsistent. Let’s look at the ViewDataBinding code that sets the lifecycleOwner.

Add an OnStartListener instance to the lifecycleOwner, because the lifecycleOwner is a fragment and will be unregistered when the fragment is destroyed. However, the View will not be unregistered when it is destroyed. OnStartListener has a reference to the ViewDataBinding, which prevents the system from reclaiming the View when it is destroyed.

The analysis logic is correct, but the result is incorrect, and the View will still be reclaimed because the OnStartListener instance holds a weak reference to the View, and the View will still be reclaimed. This is why LeakCanary did not report an error. The OnStartListener instance, however, is not so lucky, and it is this instance’s failure to reclaim that causes the memory leak.

@MainThread public void setLifecycleOwner(@Nullable LifecycleOwner lifecycleOwner) { if (mLifecycleOwner == lifecycleOwner) { return; } if (mLifecycleOwner ! = null) { mLifecycleOwner.getLifecycle().removeObserver(mOnStartListener); } mLifecycleOwner = lifecycleOwner; if (lifecycleOwner ! = null) { if (mOnStartListener == null) { mOnStartListener = new OnStartListener(this); // This instance holds an instance of ViewDataBinging, albeit a weak reference. } lifecycleOwner.getLifecycle().addObserver(mOnStartListener); // The problem here is that if lifecycle is fragment and the View is destroyed there will be no unregister. } for (WeakListener<? > weakListener : mLocalFieldObservers) { if (weakListener ! = null) { weakListener.setLifecycleOwner(lifecycleOwner); }}}Copy the code

The correct thing to do is to set the viewLifecycleOwner for the ViewDataBinding.

binding.lifecycleOwner = viewLifecycleOwner
Copy the code

By the way, how was the problem discovered? We have a code that checks the framework logic. For this problem, we check the fragment onStop function to see how many instances are listening for the lifetime of the fragment. We find that the number keeps rising, and the problem is exposed.

3. Can Glide’s self-managed life cycle be trusted?

It’s not trustworthy anymore

Glide is a very popular picture loading framework, and it has to be said that the Design of Glide’s cache is excellent, powerful, and extensible. It also manages its own life cycle by creating a fragment on the current page that loads images on onStart and caches unfinished or unfinished tasks on onStop. For re-execution in onStart, without the onDestory, of course.

Everything was perfect until Navigation came along.

glide.with(fragment).load(url).into(imageview).
Copy the code

In Navigation, if the Fragment is still in place but onDestroyView is executed, the imageView will need to be destroyed. In this case, if the image load task is not finished, the task will be cached. The task also has a strong reference to the ImageView that needs to be destroyed, causing the ImageView to fail to be destroyed, resulting in a memory leak.

How do you reproduce this problem 100% of the time? Here’s an easy way to verify this problem. To this task, add an image transformation that does nothing but sleep for 3 seconds, and within that 3 seconds, jump to another page. This will cause the current page to destory the View, but the fragment will not destory, because the task is not finished, the task will be used by Glide cache. RequestManager->RequestTracker->pendingRequests.

How to solve this problem? There is no readily available solution to this problem. Similar issues are mentioned on Glide’s website, but Glide defenders do not sound aware of the problem and have no plans for a future. Of course, we need to fix this, or our code will have this flaw.

Solution: Manage Glide’s life cycle yourself, not the invisible Fragment’s life cycle, because that’s unreliable. We wrote our own RequestManager, which is managed by the viewLifecycleOwner of the fragment passed in. It is also very convenient to use, and can be called as follows.

KGlide.with(fragment).load(url).into(imageview).
Copy the code

Source code has been simplified, posted here, please correct.

import com.bumptech.glide.manager.Lifecycle as GlideLifecycle class KGlide { companion object { private val lifecycleMap  = ArrayMap<LifecycleOwner, RequestManager>() @MainThread fun with(fragment: Fragment): RequestManager { Util.assertMainThread() val lifecycleOwner = fragment.viewLifecycleOwner if (lifecycleOwner.lifecycle.currentState == Lifecycle.State.DESTROYED) { throw IllegalStateException("View is already destroyed.") } if (lifecycleMap[lifecycleOwner] == null) { val appContext = fragment.requireContext().applicationContext  lifecycleMap[lifecycleOwner] = RequestManager( Glide.get(appContext), KLifecycle(lifecycleOwner.lifecycle), KEmptyRequestManagerTreeNode(), appContext ) } return lifecycleMap[lifecycleOwner]!! } } class KEmptyRequestManagerTreeNode : RequestManagerTreeNode { override fun getDescendants(): Set<RequestManager> { return emptySet() } } class KLifecycle(private val lifecycle: Lifecycle) : GlideLifecycle { private val lifecycleListeners = Collections.newSetFromMap(WeakHashMap<LifecycleListener, Boolean>()) private val lifecycleObserver = object : DefaultLifecycleObserver { override fun onStart(owner: LifecycleOwner) { val listeners = Util.getSnapshot(lifecycleListeners) for (listener in listeners) { listener.onStart() } } override fun onStop(owner: LifecycleOwner) { val listeners = Util.getSnapshot(lifecycleListeners) for (listener in listeners) { listener.onStop() }  } override fun onDestroy(owner: LifecycleOwner) { val listeners = Util.getSnapshot(lifecycleListeners) for (listener in listeners) { listener.onDestroy() } lifecycleMap.remove(owner) lifecycleListeners.clear() lifecycle.removeObserver(this) } } init { lifecycle.addObserver(lifecycleObserver) } override fun addListener(listener: LifecycleListener) { lifecycleListeners.add(listener) when (lifecycle.currentState) { Lifecycle.State.STARTED, Lifecycle.State.RESUMED -> listener.onStart() Lifecycle.State.DESTROYED -> listener.onDestroy() else -> listener.onStop() } } override fun removeListener(listener: LifecycleListener) { lifecycleListeners.remove(listener) } } }Copy the code

4. Can Android component lifecycle self-management be trusted?

No, trust requires a thorough understanding of the administrative details of the Android life cycle. Without enough understanding, where to trust, that is, blind trust.

We should have seen the introduction of LiveData in the Official Android documentation, so here’s an excerpt.

Livedata is an observable data holder class. Unlike a regular observable, LiveData is lifecycle-aware, meaning it respects the lifecycle of other app components, such as activities, fragments, or services. This awareness ensures LiveData only updates app component observers that are in an active lifecycle state.

It then shows us that Livedata does not cause memory leaks.

This is especially useful for activities and fragments because they can safely observe LiveData objects and not worry About Leaks — activities and fragments are owed unsubscribed when their lifecycles are destroyed.

It’s very clear. It’s very clear. If you believe the official documentation, it’s too young, too simple. LiveData will not necessarily be unregistered when lifecycleOwner is destroyed, and memory leaks will still occur. Let’s look ata piece of code where LiveData causes a memory leak.

class HomeFragment : Fragment() { private val model: NavigationViewModel by viewModels() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup? , savedInstanceState: Bundle? ) : View? { return inflater.inflate(R.layout.home_fragment, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) model.getTextValue().observe(viewLifecycleOwner){ view.findViewById<Button>(R.id.text).text = it } if (isXXX()) { findNavController().navigate(R.id.next_action) } } }Copy the code

Be careful when you get to one page and find yourself navigating to another. If written like this, it will cause a memory leak.

In this Case, the template method in fragment.onViewCreated () listens on a LiveData, which causes it to hold references to external objects. Ideally, the LivaData database will de-register when LifecycleOwner is on onDestory, but in some cases this de-register will not take place.

In the case of the code above, if the page immediately jumps to the next_action page, the previously subscribed LiveData will not be unregistered. The reason for this is that the page is in the lifecycle state when it is INITIALIZED, but the unregistration condition is that the lifecycle state of the page is at least CREATED.

void performDestroyView() { mChildFragmentManager.dispatchDestroyView(); if (mView ! = null && mViewLifecycleOwner.getLifecycle().getCurrentState() .isAtLeast(Lifecycle.State.CREATED)) { mViewLifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY); }... }Copy the code

Android’s lifecycle management can be trusted, as long as the details of state flow are thoroughly understood.

5. When ViewPager2 encounters Navigation

ViewPager is a frequently used component in application development. The Android website has a detailed description of basic usage.

It was great until Navigation.

Let’s look at the declaration of the Adapter class for ViewPager2 in the official example.

class DemoCollectionAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) {

    override fun getItemCount(): Int = 100

    override fun createFragment(position: Int): Fragment {
        // Return a NEW fragment instance in createFragment(int)
        val fragment = DemoObjectFragment()
        fragment.arguments = Bundle().apply {
            // Our object is just an integer :-P
            putInt(ARG_OBJECT, position + 1)
        }
        return fragment
    }
}
Copy the code

Not to be shy, the code in our actual project commits the same problem. This is not to say that there is a problem with the way the official website is written, but it is only under the framework of Navigation that memory leakage will occur. How did this leak happen? Let’s look at the FragmentStateAdapter constructor.

/**
 * @param fragment if the {@link ViewPager2} lives directly in a {@link Fragment} subclass.
 *
 * @see FragmentStateAdapter#FragmentStateAdapter(FragmentActivity)
 * @see FragmentStateAdapter#FragmentStateAdapter(FragmentManager, Lifecycle)
 */
public FragmentStateAdapter(@NonNull Fragment fragment) {
    this(fragment.getChildFragmentManager(), fragment.getLifecycle());
}
/**
 * @param fragmentManager of {@link ViewPager2}'s host
 * @param lifecycle of {@link ViewPager2}'s host
 *
 * @see FragmentStateAdapter#FragmentStateAdapter(FragmentActivity)
 * @see FragmentStateAdapter#FragmentStateAdapter(Fragment)
 */
public FragmentStateAdapter(@NonNull FragmentManager fragmentManager,
        @NonNull Lifecycle lifecycle) {
    mFragmentManager = fragmentManager;
    mLifecycle = lifecycle;
    super.setHasStableIds(true);
}
Copy the code

You can see that the FragmentStateAdapter ends up going through the constructor for two parameters. Lifecycle of ViewPager2’s Host lifecycle of ViewPager2’s Host lifecycle of ViewPager2’s Host If you read the previous question, you will know what the problem is. Under Navigation, the fragment and view life cycles are inconsistent. If we pass only instances of the fragment in the FragmentStateAdapter constructor, The second parameter lifecycle uses the fragment of the first parameter. But obviously, the lifecycleOwner of viewPager2’s host is the fragment’s viewlifecycleOwner, not itself.

The problem is that when the ViewPager2 instance is destroyed, the corresponding FragmentStateAdapter is not destroyed, because if you pass only one parameter, you are using the Fragment’s life cycle, and only when the Fragment exits, Before they are destroyed.

Note that FragmentStateAdapter instances cannot be set to multiple ViewPager2 objects, so when ViewPager2 is rebuilt, the Adapter cannot be reused.

These problems are actually hard to detect, and LeakCanary cannot. Fortunately, we have a tool to record in the constructor of each class that needs to be checked, and then process the record in the Finalize method of the class. If we find that a class has been constructed but does not execute the Finalize method, the class needs to be taken care of.

6. ViewPager2 Fragment reconstruction problem caused by setting Adapter

Take a look at the following code snippet:

Line1: val viewPager2: viewPager2 =...... Line2: Val Adapter: FragmentStateAdapter =...... Line3: viewPager2 adapter = adapter Line4: model. GetContentList. Observe (viewLifecycleOwner) {Line5: Adapter. Data = it Line6: adapter. NotifyDataSetChanged () Line7:}Copy the code

I don’t think you can see what’s wrong with this code, but it’s pretty conventional. Of course this code should not be a problem under non-navigation architectures. However, if the Navigation architecture is used, there will be serious problems.

To illustrate the problem scenario, if the user enters the page first and executes the code above, viewPager displays normally. Then notice the important step: on this page, navigate to another page. The current page will execute the fragment onStop, notice that the onDestory will not be executed. But onDestoryView will be executed, which means the viewPager will be destroyed, but the fragment will remain.

So what happens if you go back to the page, the fragment that was onStop will do onStart, and the fragment that was generated in Adatper will rebuild and create the View.

An unexpected thing happened. The fragment in Adatper was destroyed immediately after the reconstruction was completed. The destruction here was real destruction, and the onDestory method was implemented. If a new fragment is recreated, this is the fragment rebuilding problem. What causes this problem?

The code for destroying fragments is as follows, in the gcFragments method of the FragmentStateAdapter.

void gcFragments() { if (! mHasStaleFragments || shouldDelayFragmentTransactions()) { return; } // Remove Fragments for items that are no longer part of the data-set Set<Long> toRemove = new ArraySet<>(); for (int ix = 0; ix < mFragments.size(); ix++) { long itemId = mFragments.keyAt(ix); if (! containsItem(itemId)) { toRemove.add(itemId); mItemIdToViewHolder.remove(itemId); // in case they're still bound } } // Remove Fragments that are not bound anywhere -- pending a grace period if (! mIsInGracePeriod) { mHasStaleFragments = false; // we've executed all GC checks for (int ix = 0; ix < mFragments.size(); ix++) { long itemId = mFragments.keyAt(ix); if (! isFragmentViewBound(itemId)) { toRemove.add(itemId); } } } for (Long itemId : toRemove) { removeFragment(itemId); }}Copy the code

This function checks that fragments generated in the adapter need to be recycled because the current adatper.containsitem (id) method returns false. One more thing, this function is called when viewPager2 sets adatper. So far, the answer has come out. Because when viewPager2 sets up adatper, there’s nothing in adatper, so containsItem must return empty.

So logically correct code would look like this:

val viewPager2: ViewPager2 = ...... Model. GetContentList. Observe (viewLifecycleOwner) {if (viewPager2. Adapter = = null) {val adapter: FragmentStateAdapter = ...... adapter.data = it viewPager2.adapter = adapter } else { viewPager2.adapter.data = it } adapter.notifyDataSetChanged() }Copy the code

This code fixes the problem by filling the Adapter with data before setting the Adatper in viewPager2. This eliminates the possibility of fragments being destroyed by the containsItem() function in gcFragments that can be reused.

7. What should be paid attention to when manually managing fragments under Navigation?

In the beginning of Navigation, some fragments will still be managed manually through Add/Replace/Remove operations in FragmentManager. Part of the design of Navigation is to replace manual manipulation with unified Navigation, although the underlying manipulation of Navigation is also done through FragmentManager.

If there is manual management in your code, pay special attention to when and how you do it. The reason is that the Fragment and View life cycle are inconsistent under the Navigation framework. Putting the timing of the Fragment operation into a ViewLifeCycle can cause some unexpected results.

If the top stack page returns, the new page at the top of the stack, the original Stop Fragment will go onStart, and the entire View will be rebuilt. If you Add or Replace a fragment during the life of a ViewLifeCycle, you have to determine if the fragment that you want to operate on already exists. If the fragment already exists, After another Add or Replace operation, the fragment will be rebuilt, the original fragment will be destroyed, the new fragment will be created, and the view’s life cycle will run twice, causing unnecessary performance loss. Not only is there a loss of performance, but it also leads to problem number 4, which leads to memory leaks.

Even if these issues are noted, manually determining the situation can lead to unnecessarily complex code, so it is advisable to use the Navigation framework instead of manually navigating through FragmentManager.

8. Under the auspices of Navigation, Fragment and View are separated, how to divide property?

In the end, it’s a design problem. A View is attached to a Fragment. During the life of the Fragment from Create to Destory, there may be multiple instances of the View from Create to DeStory.

Therefore, we need to consider which variables should belong to the Fragment and which variables should belong to the View.

For example, if a page has a list, use recycleView to achieve, there is no doubt that recycleView belongs to the View, but what about the adapter of the recycleView? If the Adapter belongs to the View, then the adapter instance will be created and die as the RecycleView is created. If the adapter is placed in the fragment, no matter how many View instances will be created using the adapter in the fragment, would it be better and less costly to implement the requirements?

So how to divide this family property? The first order is fragment, and the second order is View. Objects are reused to maximize performance only when placed in fragments.

Join us

Welcome to the Bytedance mobile OS team. We are committed to being an innovator and explorer in the OS field with mobile OS as our core. We provide a robust operating platform for education, office, home and so on, enabling byte internal star products. Delve deeply into OS improvement and optimization, and explore the cutting-edge technology in OS field; At the same time, through the exploration of system optimization, a more stable system is applied to the product; In-depth analysis of the hottest intelligent hardware products, for the Internet + intelligent hardware industry to bring more possibilities!

Mobile OS team is looking for Android, iOS, server architects and r&d engineers, the most Nice working atmosphere and growth opportunities, all kinds of benefits all kinds of opportunities, base Beijing, welcome to submit resume! Email: [email protected]; Email subject: Name – Mobile OS- Position.