1
votes

I've written a sample app to familiarize myself with Dagger Android. I have 2 activities with a simple fragment each, a Main Activity that has a button to launch a Details activity, which has a button that just finishes the activity. What I see is that when I finish the Details activity, it is still in memory, and every time I click the button to go to Details, a new instance is created (as expected), but not released when exiting.

I have defined the Dagger modules as follows:

AppComponent:

@Component(modules = [AppModule::class,
    AndroidSupportInjectionModule::class,
    BuilderModule::class,
    StorageModule::class])
@ApplicationScope
interface AppComponent {
    @Component.Builder
    interface Builder {

        @BindsInstance
        fun application(application: DaggerSampleApp): Builder

        fun build(): AppComponent
    }

    fun inject(app: DaggerSampleApp)
}

AppModule:

@Module(subcomponents = [MainActivitySubComponent::class,
    MainFragmentSubComponent::class,
    DetailActivitySubComponent::class,
    DetailFragmentSubComponent::class])
@ApplicationScope
class AppModule {

    @Provides
    @ApplicationScope
    fun provideContext(application: DaggerSampleApp): Context {
        return application.applicationContext
    }
}

BuilderModule:

@Module
@ApplicationScope
abstract class BuilderModule {
    @Binds
    @IntoMap
    @ActivityKey(MainActivity::class)
    abstract fun bindMainActivityInjectorFactory(builder: MainActivitySubComponent.Builder): AndroidInjector.Factory<out Activity>

    @Binds
    @IntoMap
    @FragmentKey(MainFragment::class)
    internal abstract fun bindMainFragmentInjectorFactory(builder: MainFragmentSubComponent.Builder): AndroidInjector.Factory<out Fragment>

    @Binds
    @IntoMap
    @ActivityKey(DetailActivity::class)
    abstract fun bindDetailActivityInjectorFactory(builder: DetailActivitySubComponent.Builder): AndroidInjector.Factory<out Activity>

    @Binds
    @IntoMap
    @FragmentKey(DetailFragment::class)
    internal abstract fun bindDetailFragmentInjectorFactory(builder: DetailFragmentSubComponent.Builder): AndroidInjector.Factory<out Fragment>
}

Details Activity component:

@ActivityScope
@Subcomponent(modules = [DetailActivityModule::class, BitmapCacheModule::class])
interface DetailActivitySubComponent : AndroidInjector<DetailActivity> {
    @Subcomponent.Builder
    abstract class Builder : AndroidInjector.Builder<DetailActivity>()
}

Details Fragment component:

@ActivityScope
@Subcomponent(modules = [DetailFragmentModule::class, BitmapCacheModule::class])
interface DetailFragmentSubComponent : AndroidInjector<DetailFragment> {
    @Subcomponent.Builder
    abstract class Builder : AndroidInjector.Builder<DetailFragment>()
}

Details activity:

class DetailActivity : AppCompatActivity(), HasSupportFragmentInjector {
    @Inject
    lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>

    @Inject
    lateinit var appContext: Context

    @Inject
    lateinit var prefs: Prefs

    @Inject
    lateinit var bitmapCache: BitmapCache

    override fun onCreate(savedInstanceState: Bundle?) {
        AndroidInjection.inject(this)
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_detail)
        if (savedInstanceState == null) {
            supportFragmentManager
                    .beginTransaction()
                    .replace(R.id.container, DetailFragment.newInstance(), DetailFragment.FRAGMENT_TAG)
                    .commit()
        }

    }

    override fun supportFragmentInjector(): AndroidInjector<Fragment> {
        return fragmentDispatchingAndroidInjector
    }
}

Details fragment:

class DetailFragment : Fragment() {

    @Inject
    lateinit var appContext: Context

    @Inject
    lateinit var prefs: Prefs

    @Inject
    lateinit var bitmapCache: BitmapCache

    override fun onAttach(context: Context?) {
        AndroidSupportInjection.inject(this)
        super.onAttach(context)
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {

        return inflater.inflate(R.layout.fragment_detail, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        view.findViewById<Button>(R.id.button).setOnClickListener {
            activity?.finish()
        }
    }

    companion object {
        const val FRAGMENT_TAG = "main_fragment"

        fun newInstance() = DetailFragment()
    }
}

So why is the Details activity not released?

enter image description here

1
LGTM, I suggest that you add LeakCanary to your app to see where it is actually coming from: github.com/square/leakcanary - David Medenjak
Have you performed GC before performing heap dump? - azizbekian
Yes, I did a GC before the dump. - Francesc
I'll add LeakCanary to see if it sheds any light. - Francesc
It's possible the reference to the activity is not released because it is being bound in an anonymous inner class (the OnClickListener). Have a look at the generated bytecode to see if the OnClickLIstener is maintaining a reference to Activity rather than getting it via this.getActivity() - David Rawson

1 Answers

1
votes

After digging a bit more, it turns out there is no leak, I'm not sure why the memory profiler shows 3 allocations and no deallocations.

To test that there is no leak I overrode the finalize method in DetailsActivity and added a log to console - when I trigger a GC from the memory analyzer, after having left the Details activity, I see the log being printed, so the activity is being garbage collected.

I'll have to spend some time to figure out the profiler output for the details activity to better understand it, but at least the memory leak is disproved.