在頁面切換時使用RxJava持續更新UI界面

DannyJacks 10年前發布 | 27K 次閱讀 RxJava 安卓開發 Android開發 移動開發

來自: http://www.jianshu.com/p/0d2d4969c40a

在應用開發中, 我們需要使用 后臺任務 更新 前臺界面 , 不因頁面切換而導致重新開始, 或因某些任務阻塞界面刷新, 比如顯示下載或播放進度等. 為了追求更優質的用戶體驗, 需要大量使用后臺任務, 常見的就是異步任務(AsyncTask)和后臺服務(Service), 當然還有RxJava. 我寫了一個示例, 來講講如何使用這些常用的后臺方式.

主要

(1) 使用異步任務和后臺服務更新頁面, 避免內存泄露.

(2) 使用RxJava的時間間隔\延遲發送\定制迭代, 處理后臺任務, 保存發送狀態.

示例: 旋轉屏幕更新進度條, 在摧毀頁面和新建頁面時, 保存和獲取頁面狀態.

源碼的GitHub 下載地址

旋轉屏幕

</div>

1. 基礎

Gradle配置: Lambda表達式 + Butterknife + RxJava + LeakCanary.頁面布局: Spinner選擇使用模式, ProgressBar顯示更新狀態, 可選擇啟動LeakCanary.

主邏輯:(1) 使用Fragment存儲頁面信息, 包括異步任務, RxJava的觀察者和主題.

        // 設置存儲的Fragment
        FragmentManager fm = getFragmentManager();
        mRetainedFragment = (RetainedFragment) fm.findFragmentByTag(RETAINED_FRAGMENT);

    if (mRetainedFragment == null) {
        mRetainedFragment = new RetainedFragment();
        fm.beginTransaction().add(mRetainedFragment, RETAINED_FRAGMENT).commit();
    }</pre> 

(2) 在頁面重建時, 在onResume中恢復狀態, 繼續更新進度條.

    @Override protected void onResume() {
        super.onResume();

    // 是否包含內存泄露
    if (mSTrackLeaks.isChecked()) {
        LeakCanary.install(getApplication());
    }

    mMode = mRetainedFragment.getMode();
    mCustomAsyncTask = mRetainedFragment.getCustomAsyncTask();

    mObservable = mRetainedFragment.getObservable();
    mSubject = mRetainedFragment.getSubject();
    mSubscriber = createSubscriber();

    switch (mMode) {
        case ASYNC_TASK:
            if (mCustomAsyncTask != null) {
                if (!mCustomAsyncTask.isCompleted()) {
                    mCustomAsyncTask.setActivity(this);
                } else {
                    mRetainedFragment.setCustomAsyncTask(null);
                }
            }
            break;
        case TIME_INTERVAL:
            if (mObservable != null) {
                mObservable.subscribeOn(Schedulers.io())
                        .observeOn(AndroidSchedulers.mainThread())
                        .take(MAX_PROGRESS)
                        .map(x -> x + 1)
                        .subscribe(mSubscriber);
            }
            break;
        case DELAY_EMIT:
            if (mObservable != null) {
                mObservable.subscribeOn(Schedulers.io())
                        .delay(1, TimeUnit.SECONDS)
                        .observeOn(AndroidSchedulers.mainThread())
                        .subscribe(mSubscriber);
            }
            break;
        case CUSTOM_ITERATOR:
            if (mSubject != null) {
                mSubject.subscribe(mSubscriber);
            }
        default:
            break;
    }

    setBusy(mRetainedFragment.isBusy());
}</pre> 

生命周期: onCreate -> onRestoreInstanceState -> onResume.在onResume中設置setActivity: 因為在旋轉頁面時, 會執行onRestoreInstanceState方法, 恢復旋轉屏幕之前保存的數據, 即mPbProgressBar的值, 此時再恢復狀態. 如果移到在onCreate時設置, 則會導致Progress值為0, 因為Activity并沒有開始恢復之前的數據.

2. 異步任務

啟動異常任務AsyncTask, 在doInBackground中, 調用publishProgress顯示進度, 觸發onProgressUpdate回調, 從而更新進度條.

public class CustomAsyncTask extends AsyncTask<Void, Integer, Void> {

private WeakReference<MainActivity> mActivity; // 弱引用Activity, 防止內存泄露

private boolean mCompleted = false; // 是否完成

// 設置Activity控制ProgressBar
public void setActivity(MainActivity activity) {
    mActivity = new WeakReference<>(activity);
}

// 判斷是否完成
public boolean isCompleted() {
    return mCompleted;
}

@Override
protected Void doInBackground(Void... params) {
    for (int i = 1; i < MainActivity.MAX_PROGRESS + 1; i++) {
        SystemClock.sleep(MainActivity.EMIT_DELAY_MS); // 暫停時間
        publishProgress(i); // AsyncTask的方法, 調用onProgressUpdate, 表示完成狀態
    }
    return null;
}

@Override
protected void onProgressUpdate(Integer... progress) {
    mActivity.get().setProgressValue(progress[0]); // 更新ProgressBar的值
    mActivity.get().setProgressPercentText(progress[0]); // 設置文字
}

@Override
protected void onPreExecute() {
    mActivity.get().setProgressText("開始異步任務..."); // 準備開始
    mCompleted = false;
}

@Override
protected void onPostExecute(Void result) {
    mCompleted = true; // 結束
    mActivity.get().setBusy(false);
    mActivity.get().setProgressValue(0);
}

}</pre>

注意使用WeakReference弱引用Activity, 因為線程的回收不太穩定, 如果持有Activity, 會導致長時間無法釋放, 導致內存泄露.

</div>

使用方式

    // 處理異步線程的點擊
    private void handleAsyncClick() {
        // 獲得異步線程
        mCustomAsyncTask = new CustomAsyncTask();
        mCustomAsyncTask.setActivity(this);

    // 存儲異步線程
    mRetainedFragment.setCustomAsyncTask(mCustomAsyncTask);

    // 執行異步線程
    mCustomAsyncTask.execute();
}</pre> 

存儲異步任務, 在旋轉屏幕時, 頁面重建, 可以讀取當前進度, 繼續更新.

2. 后臺服務

通過LocalBroadcastManager的Intent傳送當前狀態, 更新頁面.

public class CustomService extends IntentService {

public static final String KEY_EXTRA_BUSY = "busy";
public static final String KEY_EXTRA_PROGRESS = "progress";

private LocalBroadcastManager mLbm;

public CustomService() {
    super(CustomService.class.getSimpleName());
}

@Override protected void onHandleIntent(Intent intent) {
    mLbm = LocalBroadcastManager.getInstance(getApplicationContext());

    Intent broadcastIntent = new Intent(MainActivity.UPDATE_PROGRESS_FILTER);
    broadcastIntent.putExtra(KEY_EXTRA_BUSY, true);
    mLbm.sendBroadcast(broadcastIntent);

    for (int i = 1; i < MainActivity.MAX_PROGRESS + 1; ++i) {
        broadcastIntent = new Intent(MainActivity.UPDATE_PROGRESS_FILTER);
        broadcastIntent.putExtra(KEY_EXTRA_PROGRESS, i);
        mLbm.sendBroadcast(broadcastIntent);
        SystemClock.sleep(MainActivity.EMIT_DELAY_MS);
    }

    broadcastIntent = new Intent(MainActivity.UPDATE_PROGRESS_FILTER);
    broadcastIntent.putExtra(KEY_EXTRA_BUSY, false);
    broadcastIntent.putExtra(KEY_EXTRA_PROGRESS, 0);
    mLbm.sendBroadcast(broadcastIntent);
}

}</pre>

使用方式, 先判斷進度, 后判斷狀態.

    private void handleIntentServiceClick() {
        mTvProgressText.setText("開始消息服務...");

    Intent intent = new Intent(this, CustomService.class);
    startService(intent);
}

...

private BroadcastReceiver mUpdateProgressReceiver = new BroadcastReceiver() {
    @Override public void onReceive(Context context, Intent intent) {
        if (intent.hasExtra(CustomService.KEY_EXTRA_PROGRESS)) {
            int progress = intent.getIntExtra(CustomService.KEY_EXTRA_PROGRESS, 0);
            mPbProgressBar.setProgress(progress);
            setProgressPercentText(progress);
        }

        if (intent.hasExtra(CustomService.KEY_EXTRA_BUSY)) {
            setBusy(intent.getBooleanExtra(CustomService.KEY_EXTRA_BUSY, false));
        }
    }
};</pre> 

3. RxJava

RxJava更新進度條的方法有很多種, 可以使用時間間隔, 延遲發送, 和定制迭代器, 但是如果需要處理頁面重建的連續更新, 需要存儲PublishSubject, 使用定制迭代器即可完成.

時間間隔: 在旋轉頁面時, 會刷新數據, 重新開始.

延遲發送: 在旋轉頁面時, 會完成前一個發送后, 重新開始.

定制迭代: 在旋轉頁面時, 可以正常完成連續更新.

時間間隔

    private void handleTimeIntervalClick() {
        mTvProgressText.setText("開始時間間隔...");

    mSubscriber = createSubscriber();
    mObservable = Observable.interval(1, TimeUnit.SECONDS);

    mObservable.subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .take(MAX_PROGRESS)
            .map(x -> x + 1)
            .subscribe(mSubscriber);

    mRetainedFragment.setObservable(mObservable);
}</pre> 

Observable.interval觀察者, take終止條件, map數據加工.

延遲發送

    private void handleDelayEmitClick() {
        mTvProgressText.setText("開始延遲發射...");

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

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

    mRetainedFragment.setObservable(mObservable);
}</pre> 

觀察者發送數據時, 會延遲一秒, 即SystemClock.sleep.

    // 創建延遲觀察者
    private Observable<Long> createObservable() {
        return Observable.create(new Observable.OnSubscribe<Long>() {
            @Override public void call(Subscriber<? super Long> subscriber) {
                for (long i = 1; i < MAX_PROGRESS + 1; i++) {
                    SystemClock.sleep(EMIT_DELAY_MS);
                    subscriber.onNext(i);
                }
                subscriber.onCompleted();
            }
        });
    }

定制迭代器, 在RetainedFragment中存儲PublishSubject.

    private void handleCustomIteratorClick() {
        mTvProgressText.setText("開始定制迭代器...");

    mObservable = Observable.from(new CustomIterator());
    mSubscriber = createSubscriber();
    mSubject = PublishSubject.create();

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

    mSubject.subscribe(mSubscriber);

    mRetainedFragment.setObservable(mObservable);
    mRetainedFragment.setSubject(mSubject);
}</pre> 

定制迭代器, 重寫next方法, 返回數據.

public class CustomIterator implements Iterable<Long> {

private List<Long> mNumberList = new ArrayList<>();

public CustomIterator() {
    for (long i = 0; i < MainActivity.MAX_PROGRESS; i++) {
        mNumberList.add(i + 1);
    }
}

@Override public Iterator<Long> iterator() {
    return new Iterator<Long>() {
        private int mCurrentIndex = 0;

        @Override public boolean hasNext() {
            return mCurrentIndex < mNumberList.size() && mNumberList.get(mCurrentIndex) != null;
        }

        @Override public Long next() {
            SystemClock.sleep(MainActivity.EMIT_DELAY_MS);
            return mNumberList.get(mCurrentIndex++);
        }

        // 不允許使用
        @Override public void remove() {
            throw new UnsupportedOperationException();
        }
    };
}

}</pre>

效果動畫

動畫

</div>

比較而言, 使用異步任務容易造成內存泄露, 并且可擴展性比較小, 適合簡單的更新; 使用后臺服務比較重, 需要另起進程, 適合復雜的數據處理, 不適合更新頁面; 使用RxJava, 容易擴展, 可以控制釋放時機, 是比較不錯的選擇.

參考

That's all! Enjoy it!

</div>

 本文由用戶 DannyJacks 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!