简体   繁体   中英

RxAndroid - fix Activity Leak?

I've distilled things down to the Activity shown below with the associated build.config and activity_main.xml.

The problem is LeakCanary is reporting a leak of the Activity if you press BACK before the counter completes, whereas letting it run to the end doesn't.

I've an idea what's going on. I know there must be an anonymous inner class created in the Activity by the Observer which survives the onPause() call because if you watch the console you can see it continuing the count. I know I've unsubscribed at this point, but the thread in the process is still running as an inner class of the Activity, which is the hallmark of a leak. This isn't just an artificial corner case for me either, in my real app the count is supposed to continue after an orientation change, so I persist what's necessary in a retained fragment and pick up the count accordingly, with the same leak. I just didn't need to show it here to simplify the example.

MainAcivity.java:

package com.otamate.rxactivityleaker;

import android.os.Bundle;
import android.os.SystemClock;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.widget.TextView;

import com.squareup.leakcanary.LeakCanary;

import rx.Observable;
import rx.Subscriber;
import rx.android.schedulers.AndroidSchedulers;
import rx.schedulers.Schedulers;

public class MainActivity extends AppCompatActivity {
    private Observable<Long> mObservable;
    private Subscriber<Long> mSubscriber;

    public TextView mTextView;
    public final static int MAX_PROGRESS = 10;
    public final static int EMIT_DELAY_MS = 1000;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        LeakCanary.install(getApplication());

        setContentView(R.layout.activity_main);

        mTextView = (TextView) findViewById(R.id.textView);

        mSubscriber = createSubscriber();
        mObservable = createObservable();

        mObservable.subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(mSubscriber);
    }

    private Observable createObservable() {
        return Observable.create (
            new Observable.OnSubscribe<Long>() {

                @Override
                public void call(Subscriber<? super Long> sub) {
                    for (long i = 1; i < MAX_PROGRESS + 1; i++) {

                        Log.d("Observable", "Progress: " + i);

                        sub.onNext(i);
                        SystemClock.sleep(EMIT_DELAY_MS);
                    }
                    sub.onCompleted();
                }
            }
        );
    }

    private Subscriber createSubscriber() {
        return new Subscriber<Long>() {

            @Override
            public void onNext(Long val) {
                Log.d("Subscriber", "Loop " + val);

                mTextView.setText("Progress: " + val);
            }

            @Override
            public void onCompleted() {
                Log.d("Subscriber", "Completed");

                mTextView.setText("Done!");
            }

            @Override
            public void onError(Throwable e) {
                Log.d("Subscriber", "Error: " + e);

                mTextView.setText("Error!");
            }
        };
    }

    @Override
    public void onPause() {
        super.onPause();

        if (mSubscriber != null) {
            Log.d("MainActivity", "onPause() Unsubscribed");

            mSubscriber.unsubscribe();
        }
    }
}

Here's activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.otamate.rxactivityleaker.MainActivity">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Idle" />
</RelativeLayout>

build.gradle:

apply plugin: 'com.android.application'

android {
    compileSdkVersion 23
    buildToolsVersion "23.0.2"

    defaultConfig {
        applicationId "com.otamate.rxactivityleaker"
        minSdkVersion 15
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:23.1.1'
    compile 'com.android.support:appcompat-v7:23.1.1'
    compile 'io.reactivex:rxandroid:0.25.0'
    debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3.1'
    releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3.1'
}

Observable.create makes it easy for you to shoot yourself in the foot .

Your problem is here:

private Observable createObservable() {
    return Observable.create (
        new Observable.OnSubscribe<Long>() {

            @Override
            public void call(Subscriber<? super Long> sub) {
                for (long i = 1; i < MAX_PROGRESS + 1; i++) {

                    Log.d("Observable", "Progress: " + i);

                    sub.onNext(i);
                    SystemClock.sleep(EMIT_DELAY_MS);
                }
                sub.onCompleted();
            }
        }
    );
}

When creating your Observable you are NOT checking if the Subscriber unsubscribed before calling onNext/onCompleted . You are also not handling back-pressure.

Check out AbstractOnSubscribe it can help you create your Observable easier.

EDIT: AbstractOnSubscribe was removed in 1.1 but maybe SyncOnSubscribe will work.

Try this

Subscription subscription;
@Override 
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    LeakCanary.install(getApplication()); 

    setContentView(R.layout.activity_main);

    mTextView = (TextView) findViewById(R.id.textView);

    mSubscriber = createSubscriber();
    mObservable = createObservable(); 

    subscription = mObservable.subscribeOn(Schedulers.io()) 
        .observeOn(AndroidSchedulers.mainThread()) 
        .subscribe(mSubscriber);
} 

Than in onPause()

@Override 
public void onPause() { 
    super.onPause(); 

    if (subscription != null) {

        subscription.unsubscribe();
    } 
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM