Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix navigation view memory leak - some live data objects from navigat… #2051

Merged
merged 3 commits into from
Sep 19, 2019

Conversation

Lebedinsky
Copy link
Contributor

@Lebedinsky Lebedinsky commented Sep 17, 2019

Description

Fixes NavigationView memory leak.

Fixes #2012

  • I have added any issue links
  • I have added all related labels (bug, feature, new API(s), SEMVER, etc.)
  • I have added the appropriate milestone and project boards

Goal

Some LiveData objects from NavigationViewModel were never released as part of the view onDestroy() causing a memory leak. The goal here is to avoid that leak.

Implementation

This PR prevents that leak unsubscribing NavigationViewModel LiveData objects previously added by removing the observers of the LifecycleOwner adding unsubscribe methods to NavigationViewSubscriber, InstructionView and SummaryBottomSheet that are called automatically when called onDestroy() method for lifecycle owner of this views. Here Lifecycle owner is NavigationView by adding to it LifecycleRegistry.

Testing

Please describe the manual tests that you ran to verify your changes

  • I have tested locally (including SNAPSHOT upstream dependencies if needed) through testapp/demo app and run all activities to avoid regressions
  • Tested in https://github.com/william-reed/mapbox-navui-leak and the leak is not reported anymore
  • I have tested via a test drive, or a simulation/mock location app
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have updated the CHANGELOG including this PR

…ion view model were never released as part of the view on destroy.
@Lebedinsky Lebedinsky added bug Defect to be fixed. navigation-ui labels Sep 17, 2019
@Lebedinsky Lebedinsky added this to the v0.42.0 milestone Sep 17, 2019
@Lebedinsky Lebedinsky marked this pull request as ready for review September 17, 2019 14:17
@codecov-io
Copy link

Codecov Report

Merging #2051 into master will increase coverage by 0.04%.
The diff coverage is 34.69%.

@@             Coverage Diff              @@
##             master    #2051      +/-   ##
============================================
+ Coverage      41.5%   41.54%   +0.04%     
- Complexity     1348     1351       +3     
============================================
  Files           286      286              
  Lines          9127     9156      +29     
  Branches        684      684              
============================================
+ Hits           3788     3804      +16     
- Misses         5020     5033      +13     
  Partials        319      319


private NavigationPresenter navigationPresenter;
private NavigationViewModel navigationViewModel;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could be declared final

NavigationViewSubscriber(NavigationPresenter navigationPresenter) {
NavigationViewSubscriber(final LifecycleOwner owner,
final NavigationViewModel navigationViewModel,
final NavigationPresenter navigationPresenter) {
this.navigationPresenter = navigationPresenter;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could be declared final

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


private NavigationPresenter navigationPresenter;
private NavigationViewModel navigationViewModel;
private LifecycleOwner lifecycleOwner;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could be declared final

@@ -65,7 +66,7 @@
*
* @since 0.7.0
*/
public class NavigationView extends CoordinatorLayout implements LifecycleObserver, OnMapReadyCallback,
public class NavigationView extends CoordinatorLayout implements LifecycleOwner, OnMapReadyCallback,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since every Activity and Fragment implements LifecycleOwner i'm not sure if this is a legit fix, considering that we already expose on* callbacks would it be reasonable to expect users to wire them up to activity or fragment lifecycle callbacks?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is exactly why I did it. When we implement LifecycleOwner in NavigationView - we remove the obligation from user to pass LifecycleOwner into this view by smth like setLifecycleOwner(). In our documentation we describes how to operate with NavigationView, and there only need to call on* callbacks. All other stuff regarding lifecycle in NavigationView we made internally and we shouldn't ask users to do any other movements. So implementing NavigationView as LifecycleOwner (for it "child" views, like IntructionView, SummaryBottomSheet and so on) allow us to minimize additional user interactions with our NavigationView for it correct work.
I mean NavigationView is smth like "parent" or container for other views (IntructionView, SummaryBottomSheet, etc.), so in this nested views we just subscribe for observing lifecycle of its "parent" (NavigationView).

Copy link
Contributor

@andrlee andrlee Sep 18, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so afaik view has much simpler lifecycle, looking at the change are we essentially forcing activity/fragment lifecycle onto view? what's the benefit of doing it? should we just simply migrate over to Fragment?
cc: @Guardiola31337

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in my perspective this view has grown beyond the scope of what any view should contain. leading to a fragment would be a logical choice. but then again there are people that use views for everything and completely avoid fragments and activities so this seems like a personal choice more than anything

@Lebedinsky Lebedinsky requested a review from andrlee September 18, 2019 10:08
Copy link
Contributor

@Guardiola31337 Guardiola31337 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking good so far @Lebedinsky Great work!

I left some comments.

@@ -206,6 +214,12 @@ public void onStop() {
}
}

@NonNull
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add Javadoc for public API?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need here, it is just overrided method for LifecycleOwner

@@ -176,18 +181,21 @@ public void onFeedbackDismissed() {
* @param navigationViewModel to which this View is subscribing
* @since 0.6.2
*/
public void subscribe(NavigationViewModel navigationViewModel) {
public void subscribe(LifecycleOwner owner, NavigationViewModel navigationViewModel) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's make sure we update the release notes and the android docs site with these changes (and add the SEMVER label here 😅), as we are breaking subscribe API here 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@@ -69,8 +76,13 @@ protected void onFinishInflate() {
bind();
}

public void subscribe(NavigationViewModel navigationViewModel) {
navigationViewModel.summaryModel.observe((LifecycleOwner) getContext(), new Observer<SummaryModel>() {
public void subscribe(LifecycleOwner owner, NavigationViewModel navigationViewModel) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as before re: SemVer

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@andrlee andrlee merged commit 9fde437 into master Sep 19, 2019
Copy link
Contributor

@Guardiola31337 Guardiola31337 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've left some nit-pickings @Lebedinsky

@@ -90,6 +91,8 @@
private boolean isMapInitialized;
private boolean isSubscribed;

private LifecycleRegistry lifecycleRegistry;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT No need for new line

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@@ -590,7 +604,7 @@ private void updateInstructionMutedState(boolean isMuted) {
int paddingBuffer = (int) resources.getDimension(R.dimen.route_overview_buffer_padding);
int instructionHeight = (int) (resources.getDimension(R.dimen.instruction_layout_height) + paddingBuffer);
int summaryHeight = (int) resources.getDimension(R.dimen.summary_bottomsheet_height);
return new int[] {leftRightPadding, instructionHeight, leftRightPadding, summaryHeight};
return new int[]{leftRightPadding, instructionHeight, leftRightPadding, summaryHeight};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


NavigationViewSubscriber(NavigationPresenter navigationPresenter) {
NavigationViewSubscriber(final LifecycleOwner owner,
final NavigationViewModel navigationViewModel,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT no need for new lines, also could we rearrange the properties to match constructor for better readability?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm agree with you, Done

@@ -103,6 +106,8 @@
private SoundButton soundButton;
private FeedbackButton feedbackButton;

private LifecycleOwner lifecycleOwner;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT no need for new line

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

public void subscribe(LifecycleOwner owner, NavigationViewModel navigationViewModel) {
lifecycleOwner = owner;
lifecycleOwner.getLifecycle().addObserver(this);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT no need for new line

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@Guardiola31337 Guardiola31337 mentioned this pull request Sep 19, 2019
11 tasks
@Guardiola31337
Copy link
Contributor

cc @william-reed for visibility Please feel free to share your feedback and if this approach aligns with what you were thinking. Thanks a lot!

@Guardiola31337
Copy link
Contributor

One thing that we missed here was:

  • I have updated the CHANGELOG including this PR

Please add the entry to the CHANGELOG @Lebedinsky when you have a change as a separate PR 🙏

@Guardiola31337 Guardiola31337 added the backwards incompatible Requires a SEMVER major version change. label Sep 19, 2019
Copy link
Contributor

@Guardiola31337 Guardiola31337 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple of comments around the tests.

private NavigationViewSubscriber theNavigationViewSubscriber;

@Before
public void setup() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


theNavigationViewSubscriber.unsubscribe();

verify(navigationViewModel.retrieveRoute()).removeObservers(eq(lifecycleOwner));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally is preferable having only 1 assertion per test 👀 https://speakerdeck.com/guardiola31337/elegant-unit-testing-mobilization-2016?slide=57

The problem of having multiple assertions are:

  • It's annoying to have to look at the stacktrace to know what's failing (which assertion failed?)
  • You don't have information about the remaining assertions (because when an assertion fails it breaks the execution of the test)

That's why is preferable to have only 1 assertion per test and one test per scenario. Basically, if one of them fails you'll know at a glance what in the code might have caused that failure and in which part (class, method, etc.).

Lebedinsky pushed a commit that referenced this pull request Sep 20, 2019
Fix code style for PR #2051. Add changes to Changelog.
@Guardiola31337 Guardiola31337 mentioned this pull request Sep 20, 2019
12 tasks

@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
void unsubscribe() {
navigationViewModel.retrieveRoute().removeObservers(lifecycleOwner);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i dont think this is necessary considering the lifecycle is tied to this view anyway

@william-reed
Copy link

Thanks for doing this, this was exactly how I had envisioned it with a custom LifecycleOwner. Appreciate it

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
backwards incompatible Requires a SEMVER major version change. bug Defect to be fixed.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Memory leak from NavigationView
5 participants