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
    About Memory Management
    in Fully Reactive Apps
    Paco Estevez - 2017
    1
    The shorter version

    View full-size slide

  2. @pacoworks
    An overview of the common leak causes
    Writing your own lifecycle management
    Architecting without lifecycle
    2

    View full-size slide

  3. @pacoworks
    Memory
    challenges in
    Android
    3

    View full-size slide

  4. @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

    View full-size slide

  5. @pacoworks
    “Most leaks in Android are caused by inappropriately
    retaining a callback called Activity.”
    – Maybe Paco
    5

    View full-size slide

  6. @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

    View full-size slide

  7. @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

    View full-size slide

  8. @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

    View full-size slide

  9. @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

    View full-size slide

  10. @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

    View full-size slide

  11. @pacoworks
    A touch of
    reactive
    programming
    11

    View full-size slide

  12. @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

    View full-size slide

  13. @pacoworks
    Reactive Activity lifecycle
    BehaviorRelay 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

    View full-size slide

  14. @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

    View full-size slide

  15. @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

    View full-size slide

  16. @pacoworks
    Designing
    Fully Reactive
    Apps
    16

    View full-size slide

  17. @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

    View full-size slide

  18. @pacoworks
    High level overview
    A View layer as a way to interact with the user
    A Services layer to interact with the system
    18

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  21. @pacoworks
    Some passive Views
    // DragAndDropView
    void updateElements(List elements);
    void updateSelected(Set selected);
    // RotationViewInput
    void showLoading(String message);
    void showError(String reason);
    void showWaiting(int seconds);
    void showRepositories(List 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

    View full-size slide

  22. @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

    View full-size slide

  23. @pacoworks
    Abstracting binding State
    to UI
    void bind(Observable lifecycle,
    Scheduler mainThreadScheduler,
    Observable state,
    Consumer 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
    subscribed with
    Consumer
    23

    View full-size slide

  24. @pacoworks
    Binding our UI
    class PaginationScreen {
    BehaviorRelay lifecycle = …
    BehaviorRelay 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

    View full-size slide

  25. @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

    View full-size slide

  26. @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

    View full-size slide

  27. @pacoworks
    Some Services
    // DragAndDropUserInteraction
    Observable> dragAndDropMoves();
    Observable> listClicks();
    // DatabaseService
    Single> queryEntriesBefore(Date date);
    // ActivityScreen
    Observable lifecycle();
    // NetworkService
    Single request(String id);
    // StateHolder
    Observable 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

    View full-size slide

  28. @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

    View full-size slide

  29. @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

    View full-size slide

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

    View full-size slide

  31. @pacoworks
    Fixing widget problems in
    view Services
    PublishRelay clicksRelay =
    PublishRelay.create();
    void onCreate() {
    RxView.clicks(label)
    .subscribe(clicksRelay);
    }
    Observable viewClicks() {
    return clicksRelay.asObservable();
    }
    VIEW RXVIEW
    BUSINESS LOGIC PUBLISH
    31

    View full-size slide

  32. @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

    View full-size slide

  33. @pacoworks
    Wrapping Singletons
    static Observable 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

    View full-size slide

  34. @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

    View full-size slide

  35. @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

    View full-size slide

  36. @pacoworks
    Fixing Activity problems
    in Architecture Components
    36
    Hold a Context for
    IO
    Bound to the
    Activity lifecycle
    <———

    View full-size slide

  37. @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

    View full-size slide

  38. @pacoworks
    Subscribing our business
    logic
    class PaginationScreen {
    PaginationState state = …;
    PublishRelay endOfPagePRelay = …;
    BehaviorRelay 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

    View full-size slide

  39. @pacoworks
    Recap
    39

    View full-size slide

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

    View full-size slide

  41. @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

    View full-size slide

  42. @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

    View full-size slide

  43. @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

    View full-size slide

  44. @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

    View full-size slide