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的回撥並沒有執行,記憶體洩漏的問題已經解決了。