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

About Memory Management in Fully Reactive Apps

pakoito
July 06, 2017

About Memory Management in Fully Reactive Apps

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. This version also adds comparisons to Architecture Components!

Fully Reactive Apps: https://speakerdeck.com/pakoito/fully-reactive-apps

pakoito

July 06, 2017
Tweet

More Decks by pakoito

Other Decks in Programming

Transcript

  1. 2

  2. 3

  3. 4

  4. 5

  5. @pacoworks A quick recap of JVM memory management Equivalence between

    classes and lambdas Identifying leaks in reactive apps Designing solutions that respect memory <—— Quiz here! 6
  6. @pacoworks TLDR: Mark & Sweep Objects have relations to each

    other: they have explicit fields, or they’re implicitly captured and referenced from an outer scope When an object doesn’t have any pointer to it it’s considered to be ready for collection Garbage Collection algorithm marks objects ready for collection and then sweeps them from memory It’s not deterministic but it’s reliable 8
  7. @pacoworks GC Roots - the unsweepables Some objects are marked

    as Garbage Collection Roots, an indication that they will not be considered for collection All objects referenced by roots transitively won’t be collected either Most common roots: Locals and parameters in stack frames Static fields Threads spawned by the app 9
  8. @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 10
  9. @pacoworks “Most leaks in Android are caused by inappropriately retaining

    a callback called Activity.” – Maybe Paco 11
  10. @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 assures that Activity doesn’t get collected 12
  11. @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 13
  12. @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 14
  13. @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); } } 15
  14. @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 16
  15. @pacoworks Open class… class RequestUseCase { private final INetworkService network;

    private final IDataBase db; public RequestUseCase(INetworkService network, IDataBase db) { this.network = network; this.db = db; } Observable<Result> operation(String id) { return Observable.just(id) .flatMap(id -> network.request(id)) .flatMap(result -> db.store(result)); } } All business logic Retained fields that are only transitive dependencies 19
  16. @pacoworks …or closures? static Observable<Result> operation( String id, final INetworkService

    network, final IDataBase db) { return Observable.just(id) .flatMap(id -> network.request(id)) .flatMap(result -> db.store(result)); } Lambdas capture the dependencies Doesn’t require to write an entire class Dependencies are retained only for as long as the Observable is alive 20
  17. @pacoworks Open class or closures? class MyActivity { @Inject INetworkService

    network; @Inject IDataBase db; private MyUseCase useCase; public void onCreate() { /* Creates a transitive ownership node */ useCase = new MyUseCase(network, db); } public void onResume() { useCase.operation("1") .subscribe(result -> displayResult(result)); } } class MyActivity { @Inject INetworkService network; @Inject IDataBase db; public void onResume() { /* No new ownerships */ operation("1", network, db) .subscribe(result -> displayResult(result)); } } 21
  18. @pacoworks Properties of closures Fancy name for capturing local dependencies

    in lambdas & single-method anonymous classes Can be used as a poor man’s class on the spot Values are captured as implicit final fields Retained values are encapsulated privately Reusable through functional patterns 22
  19. @pacoworks Reusing functions - Partial Application Function<String, Observable<Result>> applied =

    apply((network, db, id) -> operation(network, db, id), /* These values get pre-filled */ network, db); Observable.merge( applied.call("1"), applied.call("2"), applied.call("fancy") ).subscribe(result -> show(result)); Creates a partially applied function object during runtime Captures some dependencies inside a closure that calls the method passed Like RequestUseCase but as a reusable pattern 23
  20. @pacoworks But… …classes are supposed to encapsulate state and behaviour

    By externalising the state and services to be injected for testing purposes you have already broken this principle …what if my class encapsulates multiple behaviours for the same fields? The fields are the state, your methods are your operations. If you extract the state and pass it to each function explicitly you should get the same result 24
  21. @pacoworks Perfect functional world Dependencies should be injected as instances

    of a function, plain data, or an Observable State is external and separate to behaviour, services are pure functions Scope dependencies and avoid Singletons simple fields in Activity and Application with a module like in Dagger 26
  22. @pacoworks Injecting static methods /* Capture statics */ Observable<Int> operation()

    { NetworkService.request() .doOnError(e -> RxLogger.logError(e)); } /* Pass globals as functions */ Observable<Int> operation( Callable<Observable<Result>> network, Consumer<Throwable> logger) { return network.call() .doOnError(logger); } ... /* Call it with a function object */ operation( () -> NetworkService.request(), e -> RxLogger.logError(e)); Solves dependencies on static methods Decouples so no testing libraries like PowerMock are required Doesn’t solve any memory management issues Interfaces may be different in RxJava1 and Kotlin 27
  23. @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); }); } operation(() -> wrap(ConnectivityService.getInstance(activity)) ); 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 28
  24. @pacoworks Those pesky Threads Transitive dependencies and Statics are mostly

    design problems that can be fixed before they touch application logic Threads are a runtime behaviour problem that is framework and paradigm agnostic in the JVM Our UI framework is properly single-threaded Observable as our abstraction for concurrency 30
  25. @pacoworks Dissecting Observable creation An Observable receives an Observer, which

    contains a function to execute upon subscription Each operation like map() or scan() creates its own nested Observer Operations like subscribeOn() schedule thread changes inside closures parent.setDisposable( scheduler.scheduleDirect( () -> source .subscribe(parent) )); ———> ———> 31
  26. @pacoworks Dissecting Observable unsubscription Sequentially destroys all nested Observers in

    the chain Dispose is called in each one of them to set them to null Garbage Collection gets rid of them eventually Scheduled operations are still running but their results are not sent forward /** * Atomically disposes the Disposable in the field if not already disposed. * @param field the target field * @return true if the current thread managed to dispose the Disposable */ public static boolean dispose(AtomicReference<Disposable> field) { Disposable current = field.get(); Disposable d = DISPOSED; if (current != d) { current = field.getAndSet(d); if (current != d) { if (current != null) { current.dispose(); } return true; } } return false; } 32
  27. @pacoworks The Observable memory lifecycle Gets created with a bunch

    of functions nested in Observers Runs synchronously or asynchronously depending on the operations defined by the Observers Upon completion or cancellation it nulls all the nested references inside, including threaded ones 33
  28. @pacoworks Quiz 3 public class StateHolder { PublishSubject<String> myStrings =

    PublishSubject.create(); } whatever retains StateHolder 40
  29. @pacoworks Quiz 4 public void onCreate() { Observable.interval( 1, TimeUnit.SECOND,

    Schedulers.computation()) .subscribe(num -> show(num)); } 41
  30. @pacoworks Quiz 4 public void onCreate() { Observable.interval( 1, TimeUnit.SECOND,

    Schedulers.computation()) .subscribe(num -> show(num)); } thread in computation 42
  31. @pacoworks Quiz 4 public void onCreate() { Observable.interval( 1, TimeUnit.SECOND,

    Schedulers.computation()) .subscribe(num -> show(num)); } thread in computation LOOKS LIKE A POTENTIAL LEAK TO ME 43
  32. @pacoworks Terminating Observables “Natural causes” Retain a Subscription and cancel

    it manually Using one of the operators like takeUntil() 44
  33. @pacoworks Cancelling a Subscriber Simple solution, not far from the

    OOP way For most cases it plays nice with the Activity lifecycle You have twenty lovely MV* libraries with variations of same idea 45
  34. @pacoworks Cancelling a Subscriber Simple solution, not far from the

    OOP way For most cases it plays nice with the Activity lifecycle You have twenty twenty-one lovely MV* libraries with variations of same idea 46
  35. @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 47
  36. @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 48
  37. @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 49
  38. @pacoworks Fixing threads 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 50
  39. @pacoworks Fixing Singletons with takeUntil() operation(() -> 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 51
  40. @pacoworks High level overview A Business Logic layer where data

    is transformed according to an specification A View layer as a way to interact with the user A Services layer to interact with the system 53
  41. @pacoworks High level overview A Business Logic layer where data

    is transformed according to an specification A View layer as a way to interact with the user A Services layer to interact with the system JUST FOCUSING ON MEMORY TODAY 54
  42. @pacoworks The passive View Reactive world mandates a purely passive

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

    passive view A View can have two operations: output and input 56
  44. @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 57
  45. @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 58
  46. @pacoworks The View-Service duality A View layer as a way

    to interact with the user A Services layer to interact with the system 59
  47. @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 60
  48. @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 61
  49. @pacoworks Problems with Services They are bound to the part

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

    of the code they talk to: Hold a Context for IO Bound to the Activity lifecycle Hold a reference to a widget Opinionated 3rd party libraries <—— Wrap or replace 63
  51. @pacoworks Fixing Activity problems in Architecture Components 66 Hold a

    Context for IO Bound to the Activity lifecycle <———
  52. @pacoworks Fixing widget problems in view Services @Bind(R.id.my_bview) View label;

    Observable<Void> viewClicks = null; void onCreate() { viewClicks = RxView.clicks(label); } @Override Observable<Element> viewClicks() { // Leak! The Observable retains // a direct reference to the label return viewClicks; } VIEW RXVIEW BUSINESS LOGIC 67
  53. @pacoworks Fixing widget problems in view Services PublishRelay<Void> clicksRelay =

    PublishRelay.create(); void onCreate() { // Not a leak! The Observable lives in // the view’s onClickListener and // is collected alongside the view RxView.clicks(label) .subscribe(clicksRelay); } Observable<Element> viewClicks() { // Only the Relay is passed along return clicksRelay.asObservable(); } VIEW RXVIEW BUSINESS LOGIC PUBLISH 68
  54. @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 69
  55. @pacoworks Our business logic function static void subscribePagination( PaginationState state,

    Observable<Unit> endOfPage, Function<Int, Observable<List<User>>> service) { // Chain with switchMap doSM( // For every new state () -> state.elements, // Wait for the end of the page elements -> endOfPage.first(), // Get latest page elements, click -> state.pages.first(), // Request and append new page elements, click, page -> service.apply(page) .map { elements.plus(it) } .doOnNext { state.pages.call(page + 1) } ) // Reapply state .subscribe(state.elements) } Coupled only with functions, observables and data Single responsibility No leaky direct interaction with UI No UI thread required 71
  56. @pacoworks Subscribing our business logic class PaginationScreen { PaginationState state

    = new PaginationState(); PublishRelay<Unit> endOfPagePRelay = PublishRelay.create(); @Inject NetworkService network; public PaginationScreen() { /* Run only once per creation */ subscribePagination( state, endOfPagePRelay, page -> network.requestMore(page)); } public void onCreate() { /* Android code to forward end of page to relay */ } } Apply values to state rather than directly to UI Relay to proxy and avoid passing the view NetworkService is passed as a function 72
  57. @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> 73
  58. @pacoworks Binding our UI class PaginationScreen { BehaviorRelay<Lifecycle> lifecycle =

    BehaviorRelay.create(); PaginationState state = new PaginationState(); public PaginationScreen() { /* Subscription code */ } public void onCreate() { /* Android code to initialize UI and forward to relays */ /* Bind state once the UI is ready */ bind( lifecycle, mainThread(), state.elements, elements -> updateList(elements)); } void updateList(List<Elements> elements) { ... } } Our UI dependency is passed as a function Binding only after UI is ready 74
  59. @pacoworks Fully Reactive Apps Subscriptions to business logic will run

    until origin stored in PaginationState disappears 76
  60. @pacoworks Quiz 3 - Reminder public class PaginationState { BehaviorRelay<String>

    elements = BehaviorRelay.create(); BehaviorRelay<String> pages = BehaviorRelay.create(); } screen retains PaginationState 77
  61. @pacoworks Fully Reactive Apps Subscriptions to business logic will run

    until origin stored in PaginationState disappears That means that they’ll survive across rotation as long as the state is retained too! 78
  62. @pacoworks Fully Reactive Apps Subscriptions to business logic will run

    until origin stored in PaginationState disappears That means that they’ll survive across rotation as long as the state is retained too! Lifecycle-bound services are transparent 79
  63. @pacoworks Fully Reactive Apps Subscriptions to business logic will run

    until origin stored in PaginationState disappears That means that they’ll survive across rotation as long as the state is retained too! Lifecycle-bound services are transparent Bind with UI runs between CREATE and DESTROY and will be called again onCreate() 80
  64. @pacoworks Fully Reactive Apps: http://www.pacoworks.com/ 2016/11/02/fully-reactive-apps-at- droidcon-uk-2016-2/ Modern Garbage Collection:

    https://blog.plan99.net/modern- garbage-collection-911ef4f8bd8e Sample project: https://github.com/pakoito/ FunctionalAndroidReference pacoworks.com @pacoworks github.com/pakoito Slides: http://tinyurl.com/RxMemMadrid17 81