Upgrade to Pro — share decks privately, control downloads, hide ads and more …

SHORT - About Memory Management in Fully Reactive Apps

pakoito
October 26, 2017

SHORT - About Memory Management in Fully Reactive Apps

This is the short version of this talk. For more content and longer explanations, please follow the original slides: https://speakerdeck.com/pakoito/about-memory-management-in-fully-reactive-apps-1

This talk covers some of the memory management challenges when working with reactive apps. What are leaks, how they are created when working with Observable chains, and provide advice on how to avoid them.

pakoito

October 26, 2017
Tweet

More Decks by pakoito

Other Decks in Programming

Transcript

  1. @pacoworks An overview of the common leak causes Writing your

    own lifecycle management Architecting without lifecycle 2
  2. @pacoworks Leaks in Android Activities are god objects that contain

    large chunks of memory related to UI and services Activities cannot be constructed and have to be manually released Activities act as a glorified callback for: Activity manager Permissions Intent requests to other parts of the system They are also a service locator and give access to IO 4
  3. @pacoworks “Most leaks in Android are caused by inappropriately retaining

    a callback called Activity.” – Maybe Paco 5
  4. @pacoworks Common leak causes - Hidden dependency graphs class MyApp

    extends Application { DBService dbService; } class MyActivity extends Activity implements DBListener { @Inject DBService dbService; public void onCreate() { dbService.watchChanges("users", this); } @Override public void onChange(Object user) { ... } } Application keeps DBService alive Activity is set as a DBService callback which is never disconnected Transitive ownership ensures that Activity doesn’t get collected 6
  5. @pacoworks Common leak causes - Anonymous class captures class MyActivity

    { public void onCreate() { NetworkService.getInstance() .request(user -> updateProfile(user)); ) } void updateProfile(User user) { ... } } updateProfile is a method of the MyActivity class The compiler adds an implicit reference to the MyActivity.this object in the lambda NetworkService holds the callback inside a Singleton static 7
  6. @pacoworks Common leak causes - Inner classes class MyActivity {

    public void onCreate() { new Thread(new LongOp()).start(); } void showDetails(Result result) { ... } private class LongOp extends Runnable { void run() { Result result = longOp(); showDetails(result); } } } The compiler adds an implicit reference to MyActivity.this to access showDetails RecyclerView holds the Adapter, which holds the Activity, which holds the RecyclerView… Leaks in a Thread 8
  7. @pacoworks Fixing leaks in OOP class MyActivity implements Callback {

    private LongOp op; public void onCreate() { op = new LongOp(this); new Thread(op).start(); } public void onDestroy() { if (op != null) { op.release(); } } void call(Data data) { showDetails(data); } private showDetails(...) { ... } } class LongOp extends Runnable { private Callback callback; class LongOp(Callback callback) { this.callback = callback; } void release() { callback = null; } void run() { if (callback != null) { callback.call(longOp()); } } interface Callback { void call(Data data); } } 9
  8. @pacoworks Fixing leaks in OOP Mutable fields or box references

    i.e. WeakReference Manual management of attach-detach null checks Not thread safe without effort No real business value 10
  9. @pacoworks The operator takeUntil() Allows for fine-grained lifecycle control Needs

    an Observable signal, and cancels the Observable once the first value is received Works with the Android lifecycle with some adjustments 12
  10. @pacoworks Reactive Activity lifecycle BehaviorRelay<Lifecycle> lifecycle = BehaviorRelay.create() protected void

    onCreate(Bundle b) { if (b == null) { lifecycle.call(ENTER); } lifecycle.call(CREATE); } protected void onDestroy() { lifecycle.call(DESTROY); if (isFinishing()) { lifecycle.call(EXIT); } } Map every lifecycle state to a piece of data Store the data in an Observable that caches the latest value, like BehaviourSubject or BehaviourRelay 13
  11. @pacoworks Architecture Components Activity lifecycle Activities and fragments implement LifecycleOwner

    A LifecycleOwner provides a Lifecycle. It behaves like an Observable-lite class for our subscriptions 14
  12. @pacoworks Handling the lifecycle with takeUntil() public void onCreate() {

    lifecycle .filter(is(CREATE)) .switchMap(enter -> Observable.interval( 1, TimeUnit.SECOND, computation())) .observeOn(mainThread()) .takeUntil(lifecycle.filter(is(DESTROY))) .subscribe(num ->show(num)) } Start operation only if state is CREATE Cancel all previous intervals with switchMap() Stop the interval and listening to the lifecycle if state is DESTROY 15
  13. @pacoworks Perfect functional world Dependencies should be injected as instances

    of a function, plain data, or an Observable State is external and separate from the behavior, services are pure functions Scope dependencies and avoid Singletons simple fields in Activity and Application with a module like in Dagger 17
  14. @pacoworks High level overview A View layer as a way

    to interact with the user A Services layer to interact with the system 18
  15. @pacoworks The passive View Reactive world mandates a purely passive

    view A View can have two operations: output and input 19
  16. @pacoworks Not the passive View Reactive world mandates a purely

    passive view A View can have two operations: output and input 20
  17. @pacoworks Some passive Views // DragAndDropView void updateElements(List<Element> elements); void

    updateSelected(Set<String> selected); // RotationViewInput void showLoading(String message); void showError(String reason); void showWaiting(int seconds); void showRepositories(List<Repos> repos); Simple data formatted to be displayed on screen “I don’t care how you display it” Note these are not part of an interface 21
  18. @pacoworks The best passive View Reactive world mandates a purely

    passive view A View has one operation: display data Any reference to it is a potential cause of leaks, as it will always come with a reference to any UI widgets captured 22
  19. @pacoworks Abstracting binding State to UI void <T> bind(Observable<MyLifecycle> lifecycle,

    Scheduler mainThreadScheduler, Observable<T> state, Consumer<T> viewAction) { lifecycle .filter(is(CREATE)) .switchMap(state) .observeOn(mainThreadScheduler) .takeUntil( lifecycle .filter(is(DESTROY))) .subscribe(viewAction) } Fixes leaky UI! Bind with lifecycle assurance Add main thread too Observable<T> subscribed with Consumer<T> 23
  20. @pacoworks Binding our UI class PaginationScreen { BehaviorRelay<Lifecycle> lifecycle =

    … BehaviorRelay<PaginationState> state = … public void onCreate() { bind( lifecycle, mainThread(), state, elements -> updateList(elements)); } } Our UI dependency is passed as a function Binding starts after the UI is ready 24
  21. @pacoworks The View-Service duality A View layer as a way

    to interact with the user A Services layer to interact with the system 25
  22. @pacoworks The View-Service duality A View layer as a way

    to display information A Services layer to interact with the user and the system 26
  23. @pacoworks Some Services // DragAndDropUserInteraction Observable<Pair<Int, Int>> dragAndDropMoves(); Observable<Pair<Int, String>>

    listClicks(); // DatabaseService Single<List<Entry>> queryEntriesBefore(Date date); // ActivityScreen Observable<Lifecycle> lifecycle(); // NetworkService Single<Result> request(String id); // StateHolder Observable<State> updates(); A service is a collection of functions returning a version of Observable Decoupled from space, time, and error handling Data received is immutable and never null Signatures do not know of Android framework code Note these are not part of an interface 27
  24. @pacoworks Problems with Services They are bound to the part

    of the code they talk to: Hold a reference to a widget Hold a Context for IO Bound to the Activity lifecycle Opinionated 3rd party libraries 28
  25. @pacoworks Problems with Services They are bound to the part

    of the code they talk to: Hold a reference to a widget Hold a Context for IO Bound to the Activity lifecycle Opinionated 3rd party libraries <—— Wrap or replace 29
  26. @pacoworks Fixing widget problems in view Services Observable<Void> viewClicks =

    null; void onCreate() { viewClicks = RxView.clicks(label); } @Override Observable<Element> viewClicks() { return viewClicks; } VIEW RXVIEW BUSINESS LOGIC 30
  27. @pacoworks Fixing widget problems in view Services PublishRelay<Void> clicksRelay =

    PublishRelay.create(); void onCreate() { RxView.clicks(label) .subscribe(clicksRelay); } Observable<Element> viewClicks() { return clicksRelay.asObservable(); } VIEW RXVIEW BUSINESS LOGIC PUBLISH 31
  28. @pacoworks Fixing Activity problems in Services val service = ConnectivityService.getInstance(activity);

    fun connectivityUpdates(cb: Callback) { return service.addCb(callback); } Hold a Context for IO Bound to the Activity lifecycle 32
  29. @pacoworks Wrapping Singletons static Observable<Boolean> wrap( ConnectivityService service) { return

    Observable.create(emitter -> { final Callback callback = /* Forward value from callback */ isConnected -> emitter.onNext(isConnected); /* Remove callback when the Observable terminates */ emitter.setCancellable(() -> service.removeCb(callback)); /* Start listening */ service.addCb(callback); }); } Wraps the Singleton in a closure Matches the lifecycle of an Observable Fixes memory management problems, but it also hides them Decouples from ConnectivityService to test without using libraries like PowerMock 33
  30. @pacoworks Fixing Singletons with takeUntil() wrap(ConnectivityService.getInstance(activity)) .takeUntil(lifecycle.filter(is(DESTROY))) Ties the lifecycle

    of the Observable to the Activity Only happens on the Android layer Transparent for the Singleton and the operation Allows chains where only some Observables are tied to Activities 34
  31. @pacoworks Fixing Activity problems in Services fun connectivityUpdates() = wrap(ConnectivityService.getInstance(activity))

    .takeUntil(lifecycle.filter(is(DESTROY))) Hold a Context for IO Bound to the Activity lifecycle 35
  32. @pacoworks Fixing Activity problems in Architecture Components 36 Hold a

    Context for IO Bound to the Activity lifecycle <———
  33. @pacoworks Real problems with Services Identify which ones are leaky

    and which ones aren’t Leaky can only run while the Activity is alive Not leaky can run until required or completion 37
  34. @pacoworks Subscribing our business logic class PaginationScreen { PaginationState state

    = …; PublishRelay<Unit> endOfPagePRelay = …; BehaviorRelay<Lifecycle> lifecycle = …; @Inject NetworkService network; public PaginationScreen() { subscribePagination( state, endOfPagePRelay, page -> wrap(network.requestMore(page), lifecycle)); } } Apply values to state rather than directly to UI Relay to proxy and avoid passing the view NetworkService is passed as a function 38
  35. @pacoworks Fully Reactive Apps Subscriptions to business logic will run

    until completion or garbage collection of the origin 40
  36. @pacoworks Fully Reactive Apps Subscriptions to business logic will run

    until completion or garbage collection of the origin of the subscription That means that they’ll survive across rotation as long as the origin (i.e. state) is retained too! 41
  37. @pacoworks Fully Reactive Apps Subscriptions to business logic will run

    until completion or garbage collection of the origin of the subscription That means that they’ll survive across rotation as long as the origin (i.e. state) is retained too! Lifecycle-bound services are transparent 42
  38. @pacoworks Fully Reactive Apps Subscriptions to business logic will run

    until completion or garbage collection of the origin of the subscription That means that they’ll survive across rotation as long as the origin (i.e. state) is retained too! Lifecycle-bound services are transparent Bind with UI runs between CREATE and DESTROY and will be called again onCreate() 43
  39. @pacoworks Extended version of this talk: http://www.pacoworks.com/ 2017/03/11/about-memory-management- in-fully-reactive-apps-at-droidcon- bytes-february-17/

    Fully Reactive Apps: http://www.pacoworks.com/ 2016/11/02/fully-reactive-apps-at- droidcon-uk-2016-2/ Sample project: https://github.com/pakoito/ FunctionalAndroidReference pacoworks.com @pacoworks github.com/pakoito Slides: http://tinyurl.com/RxMemLondon17 44