1. 程式人生 > >RxJava(十三)RxJava導致Fragment Activity記憶體洩漏問題

RxJava(十三)RxJava導致Fragment Activity記憶體洩漏問題

RxJava系列文章目錄導讀:

一般我們在實際的開發中,RxJava和Retrofit2結合使用的比較多,因為他們可以無縫整合,例如我們下面的一個網路請求:

public interface OtherApi {

    @GET("/timeout")
    Observable<Response> testTimeout(@Query("timeout") String timeout);
}

private void getSomething(){
    subscription = otherApi.testTimeout("10000")
            .subscribe(new
Action1<Response>() { @Override public void call(Response response) { String content = new String(((TypedByteArray) response.getBody()).getBytes()); Log.d("RxJavaLeakFragment", RxJavaLeakFragment.this + ":" + content); } }, new
Action1<Throwable>() { @Override public void call(Throwable throwable) { throwable.printStackTrace(); } }); }

上面的程式碼非常簡單,用過Retrofit2和RxJava一眼就看明白了,我們知道還需要在介面destroy的時候,把subscription反登出掉,避免記憶體洩漏,如:

@Override
public
void onDestroy() { super.onDestroy(); if (subscription != null && !subscription.isUnsubscribed()) { subscription.unsubscribe(); Log.d("RxJavaLeakFragment", "subscription.unsubscribe()"); } }

但是這真的能避免記憶體洩漏嗎?下面我們來做一個實驗。

操作步驟:我們進入某個介面(Activity、Fragment),點選按鈕請求網路,故意讓該網路請求執行10秒,在網路返回前,我們關閉介面。

後端程式碼如下:


//如果使用者傳進來的timeout>0則當前執行緒休眠timeout,否則休眠20秒
@WebServlet("/timeout")
public class TimeoutServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;

    protected void doGet(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
        String timeout = request.getParameter("timeout");
        long to = getLong(timeout);
        if (to <= 0) {
            to = 20000;
        }
        try {
            Thread.sleep(to);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        ResponseJsonUtils.json(response, "timeout success");
    }

    protected void doPost(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
        doGet(request, response);

    }

    static long getLong(String value) {
        try {
            return Long.parseLong(value);
        } catch (Exception e) {
        }
        return -1;
    }

}

Fragment的程式碼如下:

//點選按鈕請求網路,在成功回撥方法裡輸出伺服器返回的結果和當前Fragment的物件

@Override
public void onClick(View v) {
    super.onClick(v);
    switch (v.getId()) {
        case R.id.btn_request_netword_and_pop:
            if (otherApi == null) {
                otherApi = ApiServiceFactory.createService(OtherApi.class);
            }
            subscription = otherApi.testTimeout("10000")
                    .subscribe(new Action1<Response>() {
                        @Override
                        public void call(Response response) {
                            String content = new String(((TypedByteArray) response.getBody()).getBytes());
                            Log.d("RxJavaLeakFragment", RxJavaLeakFragment.this + ":" + content);
                        }
                    }, new Action1<Throwable>() {
                        @Override
                        public void call(Throwable throwable) {
                            throwable.printStackTrace();
                        }
                    });
            break;
    }
}

//使用者按返回按鈕關閉當前介面,subscription執行unsubscribe()方法

@Override
public void onDestroy() {
    super.onDestroy();
    if (subscription != null && !subscription.isUnsubscribed()) {
        subscription.unsubscribe();
        Log.d("RxJavaLeakFragment", "subscription.unsubscribe()");
    }
}

點選網路請求按鈕後,立馬關閉當前介面,等待我們設定的超時時間10秒,測試輸出結果如下:

D/Retrofit: ---> HTTP GET http://10.1.67.34:8080/android_mvvm_server/timeout?timeout=10000
D/Retrofit: Authorization: test
D/Retrofit: ---> END HTTP (no body)
I/DpmTcmClient: RegisterTcmMonitor from: com.android.okhttp.TcmIdleTimerMonitor
D/RxJavaLeakFragment: subscription.unsubscribe()
D/Retrofit: <--- HTTP 200 http://10.1.67.34:8080/android_mvvm_server/timeout?timeout=10000 (10086ms)
D/Retrofit: : HTTP/1.1 200 OK
D/Retrofit: Content-Type: text/plain;charset=UTF-8
D/Retrofit: Date: Tue, 28 Mar 2017 11:06:07 GMT
D/Retrofit: Server: Apache-Coyote/1.1
D/Retrofit: Transfer-Encoding: chunked
D/Retrofit: X-Android-Received-Millis: 1490699154102
D/Retrofit: X-Android-Response-Source: NETWORK 200
D/Retrofit: X-Android-Selected-Protocol: http/1.1
D/Retrofit: X-Android-Sent-Millis: 1490699144047
D/Retrofit: "timeout success"
D/Retrofit: <--- END HTTP (17-byte body)
D/RxJavaLeakFragment: RxJavaLeakFragment{60678c5}:"timeout success"

最後一行日誌道出了真相,雖然我們關閉了介面,但是回撥依然對Fragment有引用,所以當伺服器返回介面的時候,依然可以列印Fragment的物件。

Rxjava 為我們提供onTerminateDetach操作符來解決這樣的問題,在RxJava 1.1.2版本還沒有這個操作符的,在RxJava1.2.4是有這個操作符。

/**
* Nulls out references to the upstream producer and downstream Subscriber if
     * the sequence is terminated or downstream unsubscribes.
*/
@Experimental
public final Observable<T> onTerminateDetach() {
    return create(new OnSubscribeDetach<T>(this));
}

上面的註釋意思就是說 當執行了反註冊unsubscribes或者傳送資料序列中斷了,解除上游生產者與下游訂閱者之間的引用。

所以onTerminateDetach操作符要和subscription.unsubscribe() 結合使用,因為不執行subscription.unsubscribe()的話,onTerminateDetach就不會被觸發。

所以只要呼叫onTerminateDetach()即可,如下所示:

subscription = otherApi.testTimeout("10000")
    .onTerminateDetach()
    .subscribe(new Action1<Response>() {
        @Override
        public void call(Response response) {
            String content = new String(((TypedByteArray) response.getBody()).getBytes());
            Log.d("RxJavaLeakFragment", RxJavaLeakFragment.this + ":" + content);
        }
    }, new Action1<Throwable>() {
        @Override
        public void call(Throwable throwable) {
            throwable.printStackTrace();
        }
    });

測試結果如下 :

Retrofit: ---> HTTP GET http://10.1.67.34:8080/android_mvvm_server/timeout?timeout=10000
Retrofit: Authorization: test
Retrofit: ---> END HTTP (no body)
DpmTcmClient: RegisterTcmMonitor from: com.android.okhttp.TcmIdleTimerMonitor
RxJavaLeakFragment: subscription.unsubscribe()
Retrofit: <--- HTTP 200 http://10.1.67.34:8080/android_mvvm_server/timeout?timeout=10000 (10165ms)
Retrofit: : HTTP/1.1 200 OK
Retrofit: Content-Type: text/plain;charset=UTF-8
Retrofit: Date: Tue, 28 Mar 2017 11:20:46 GMT
Retrofit: Server: Apache-Coyote/1.1
Retrofit: Transfer-Encoding: chunked
Retrofit: X-Android-Received-Millis: 1490700033441
Retrofit: X-Android-Response-Source: NETWORK 200
Retrofit: X-Android-Selected-Protocol: http/1.1
Retrofit: X-Android-Sent-Millis: 1490700023314
Retrofit: "timeout success"
Retrofit: <--- END HTTP (17-byte body)

從日誌可以看出,雖然伺服器返回了資料,但是RxJava Action1的回撥並沒有執行,記憶體洩漏的問題已經解決了。