RxJava是一個實現Java響應式編程的庫,讓非同步事件以序列的形式組織。MVP則通常用來將View業務層與Model層分離開來,兩者結合起來可輕鬆實現業務解耦、線程式控制制、單元測試等等強大功能 ...
本文來自於騰訊bugly開發者社區,非經作者同意,請勿轉載,原文地址:http://dev.qq.com/topic/57bfef673c1174283d60bac0
Dev Club 是一個交流移動開發技術,結交朋友,擴展人脈的社群,成員都是經過審核的移動開發工程師。每周都會舉行嘉賓分享,話題討論等活動。
本期,我們邀請了騰訊IEG Android 開發工程師——戴俊,為大家分享《基於RxJava的一種MVP實現》。
分享內容簡介:
RxJava是一個實現Java響應式編程的庫,讓非同步事件以序列的形式組織。MVP則通常用來將View業務層與Model層分離開來,兩者結合起來可輕鬆實現業務解耦、線程式控制制、單元測試等等強大功能
內容大體框架:
- Android開發框架的選擇
- 如何一步步搭建MVP分層框架
- 使用RxJava來進行線程式控制制
- 結語
下麵是本期分享內容整理
Hello,大家好,我是戴俊。目前在IEG騰訊動漫主要負責Android端的開發工作。
第一次進行這種微信群的分享,如果有任何疑問,歡迎大家在分享結束後提問。下麵開始我們今天的分享。
1. Android開發框架的選擇
我們知道原生Android開發已經是一個基礎的MVC框架,所以在項目剛開始開發的時候並沒有遇到太多問題。
對一個經典的Android MVC框架來講,它的結構大概是下麵這樣(圖片來自參考文獻)
這樣的結構下,Activity層既承擔了View層的一部分工作(因為XML作為View層的一部分功能實在太弱了),又承擔Controller層的工作,因此當業務變化時,Activity層會極劇膨脹。
拿我們項目早期的例子,一個Activity曾經最多達到了2000到3000行,重構的時候極其痛苦。
要解決這個問題,主要的辦法有兩種:
- 第一種是分層
- 第二種是模塊化。
兩個方法最終要實現的都是解耦。分層講的是縱向層面上的解耦,模塊化則是橫向上的解耦。
我們今天要討論的MVP就是一種通過分層來進行解耦的框架。
2. 如何一步步搭建MVP分層框架
如果你是個習慣了讀文檔的老司機,可以直接參考下麵幾篇文章
- Android Application Architecture
- Android Architecture Blueprints - Github
- Google官方MVP示例之TODO-MVP - 簡書
- todo-mvp - github
- dev-todo-mvp-rxjava - github
當然如果覺得看官方的示例太麻煩,那麼下麵我們就來講解一下如何實現一個簡單的MVP構架。
這是一個比較典型的MVP結構圖(圖片來自參考文獻),相比於第一張圖,多了兩個層,一個是Presenter和DataManager層。
多出這兩個層到底有什麼作用,下麵我們來用代碼說明。
首先我們假設有一個從服務端獲取字元串並顯示的手機上的簡單功能。下麵是主界面的代碼
Activity裡面包含了幾個文件,一個是View層的對外介面MainView,一個是P層的Presenter。
首先看View層的對外介面文件
因為這個功能比較簡單,只需要在設備上顯示一個字元串,所以只有一個介面方法onShowString(),再看P層代碼
從上面三個文件可以看到,View層通過註冊Listener將自己的介面MainView交給了Presenter, 而Presenter層持有Model層的也只是一個介面。通過Presenter層將業務層與展現層隔離了開來,這樣的好處是什麼?
我們知道介面的一個作用通常是用來抽象行為,對外部屏蔽實現細節。所以對於View層來說,業務細節被屏蔽了,對業務層來說,展示細節被屏蔽了。而對於處於中間的Presenter層來說,它就像一個介面拼裝器,把View層發出的請求傳遞給業務層,把業務層返回的數據又送還給View層展示,至於前後兩端怎麼實現的,它才不用關心。
介面的第二個作用是可以用來切換實現。我們先看下麵的代碼。
從上面三個文件可以看到,業務層對外的只有一個介面,實現卻有兩個(DataSourceImpl和DataSourceTestImpl)。從名字大家就能看出來有什麼作用了,一個是正常環境的業務層實現,一個是測試環境的業務層實現。
這裡我們設想一個場景:
開發同學接到一個新的需求,設計稿也輸出完成了,然而後臺的介面卻遲遲沒到,怎麼辦? 現在通過MVP,我們把業務層實現切換到DataSourceTestImpl,是不是可以先自己假寫數據,調好一切前端和交互,然後泡一杯咖啡等後臺同學把介面寫完聯調?或者有時候為了重現一個bug,要線上上寫一條臟數據,測試完再刪除?
類似的應用場景其實有非常非常多,這裡我們就看到了使用介面解耦的一個好處了,不僅把業務層和展示層解耦開來,還把Android開發同其它的一切的外部數據依賴都解耦開來。
這裡我想提到之前討論過的單元測試問題,很多同學反饋項目開發過程中沒有做過,或者沒有時間精力去做單元測試,或者因為業務變化太大導致無法做單元測試。其實在我們項目中也遇到過樣的問題,但其實通過這樣分層之後,才發現單元測試其實是完全可以推進的,也完全不用再擔心測試的時候會把臟數據寫到線上的問題了。
到現在為止一個基於MVP簡單框架就搭建完成了,但其實還遺留了一個比較大的問題。
很多同學可能已經發現了,Presenter層在調用業務層的時候是直接調用的,而Android規定,主線程是無法直接進行網路請求,會拋出NetworkOnMainThreadException異常。
所以在presenter層,我們需要進行一項線程切換的工作,這樣才能保證“所有的IO操作都應當線上程中完成,主線程只負責頁面渲染的工作”這一優化準則。
當然,Android本身提供一些方案,比如下麵這種:
通過新建子線程進行IO讀寫獲取數據,然後通過主線程的Looper將結果通過傳回主線程進行展示,這種方案是勉強也行得通的。
但問題也有,一是線程需要額外管理,不可能每次發請求都要開啟一個線程;二是適應性差,假如數據請求有先後依賴,有並行的情況,這樣的寫法變得髒亂無比。
好在有了RxJava ,可以比較方便的解決這個問題。
3. 使用RxJava來進行線程式控制制
RxJava是一個天生用來做非同步的工具,相比AsyncTask,Handler等,它的優點就是簡潔,無比的簡潔。在Android中使用RxJava需要加入下麵兩個依賴。
compile 'io.reactivex:rxjava:1.0.14'
compile 'io.reactivex:rxandroid:1.0.1'
這裡我們直接介紹如何使用RxJava解決這個問題,在presenter中修改方法getData()。
簡單解釋一下,dataAction是我們的數據業務邏輯,viewAction是界面的顯示邏輯,通過RxJava的傳遞和變換,dataAction會在由RxJava管理的IO線程—Schedulers.io() 中執行,而viewAction則會在UI線程—AndroidSchedulers.mainThread()中執行。
RxJava當然不止這麼簡單,還有別的玩法,比方說進入一個界面的時候,需要先載入緩存的數據,然後再從網路獲取更新的數據進行刷新。有的時候,可能還需要處理IO過程中的異常情況,加入RxJava的異常處理參數。
RxJava的使用場景遠不止這些,線程變換、數據變換、介面順序依賴、介面併發請求這些要求對它來說都是小菜一碟。當然,有些同學可能覺得RxJava入手有些困難,代碼也會變得不那麼直觀,但相信只要大家慢慢熟悉它之後,它就會變得無比討人喜歡。
下麵列出了一些常見的RxJava的常用場景,其實還有更多的其它功能等待著大家去挖掘。
- 取數據先檢查緩存的場景
- 需要等到多個介面併發取完數據,再更新
- 一個介面的請求依賴另一個API請求返回的數據
- 界面按鈕需要防止連續點擊的情況
- 響應式的界面
- 複雜的數據變換
上面這些功能都可以通過RxJava來輕鬆完成。具體的使用就不再多講了,大家可以參考下麵的文章:(Google文章名就可以了)
1.給 Android 開發者的 RxJava 詳解
2.RxJava 與 Retrofit 結合的最佳實踐
3.RxJava使用場景小結
4.How To Use RxJava
結語
至此為止,通過MVP+RxJava的組合,我們已經構建出一個比MVC更靈活的Android項目開發框架,好處大概有以下幾點:
- 每層各自獨立,通過介面通信
- 實現與介面分離實現,不同場景(正式,測試)掛載不同的實現,方便測試寫假數據
- 所有的業務邏輯都在非UI線程中進行,最大限度減少IO操作對UI的影響
- 使用RxJava可以將複雜的調用進行鏈式組合,解決多重回調嵌套問題
以上就是我今天的分享,內容上可能還有不足甚至不夠好的地方,歡迎大家指出,一起討論學習。
這裡也順便打個廣告,歡迎大家下載騰訊動漫App,這裡有最新最熱的國漫日漫,支持正版,你我共用。
問答環節
Q1:對於這樣的一個MVP項目,它裡面的包結構怎樣規劃比較清晰合理呢?
包結構的通常分法有兩種:一種是按功能模塊分,把某一個功能的presenter, activity,view層介面放到一起;一種是按類型分,P層M層和V層分成三個包。實際項目應用,我個人傾向於第一種,這種無論是開發過程,還是排查問題都會方便很多。當然,不同的項目還是有不同的分法的,不一而論。
Q2:耗時操作可能引起的記憶體泄露問題,請問是如何處理的。
Q3:用mvp時,請問你們在哪裡釋放一些引用,防止記憶體泄露的
Q4:p持有v的引用,請問怎麼解決Activity的記憶體泄露問題?
Q5:網特別慢的時候,應用退出,但網路請求還沒結束,p層回調持有上下文造成記憶體泄露,一般怎麼解決啊。
這幾個問題其實比較類似,我們在實際項目中,presenter會隨著activity的生命周期進行銷毀,比如在onDestroy方法中對presenter進行置空和引用解綁, 當然我們可以給所有的Presenter寫一個共有父類BasePresenter,專門來處理這個問題。
Q6:需求包含列表頁的時候,列表項也是按照mvp的思想來分層,還是封裝成模塊比較合適
目前我們的做法是直接封裝成模塊,簡單的問題不宜過度設計
Q7:想問一下騰訊動漫這個app目前用的就是您講的這個架構嗎,在實際用的過程中有遇到什麼問題嗎
是的,我們已經使用了這個架構。實際使用過程中,經常會糾結的問題是業務邏輯層要不要再次獨立分層。
Q8:項目中做測試是好事,但我覺得建議去掉TestImpl測試文件。如果項目打包時,打到包里,會導致包變大,這種測試建議用node寫個簡單的服務,不知道嘉賓你咋看?
是的。正式項目中,可以通過註解,或者proguard或者gradle的配置將這些測試文件不打到包里。Node寫服務的話是不是又要搭環境,這裡的做法就是不使用任何外部環境依賴。
Q9:mvp一般都是activity和Fragment加入presenter層,那麼列表adapter里的邏輯是否也要加上presenter層呢
Adapter其實跟View更接近的一個東西,它是用來處理重覆顯示問題。一般來說,我們傳給adapter的數據完好能直接顯示的,建議在業務邏輯層將數據拼裝好再傳進去。
答:Adapter其實跟View更接近的一個東西,它是用來處理重覆顯示問題。一般來說,我們傳給adapter的數據完好能直接顯示的,建議在業務邏輯層將數據拼裝好再傳進去。
Q10:我們項目中採用了MVP但是沒有用RxJava,m與p層採用回調方式,這樣m通過回調間接引用p,p層有v的引用。如果在網路情況不好頻繁打開關閉頁面在網路請求結束前是否會有記憶體泄漏問題。rx是否能解決這個問題。還有當網路結束回調時v對應activity destory了怎麼辦。每次都去判斷activity狀態嗎?
Rx不能解決記憶體泄漏的問題,前面2.3.7問題都提到了,通常的做法是在activity層銷毀的時候進行解綁。回調時activity destory的話,我們現在的做法是對view層介面進行一次空值判定。如果有更好的辦法,也歡迎大家提出來討論
Q11:有時候例如自定義view依賴於伺服器返回的model,裡面也有很多根據model屬性去繪製的過程,這種情況怎麼處理?在P層拋出一個model的get方法嗎?
自定義的View跟Activity一樣,我們統稱為View層。上面的例子中View層只有一個介面MainView,實際項目中,View層可能會實現好幾個介面。對一個經常會被利用的自定義View,會額外給它新建一個介面。
Q12:你的例子中p層實現中getDate()方法對數據進行了處理,是否m層只是單純的獲取原始數據,對於數據上的業務也放入到p層中處理,有沒有好的方式能夠復用有關數據業務的這塊邏輯
嗯,這個問題我們確實也遇到了。在項目實際操作過程中,如果有比較複雜業務流程,我會單獨再分離出一層業務層,業務層再去調用dataSource取數據。如果只是單純的取數據展示,現在這樣就夠了,儘量避免過度設計。
Q13:為了更好的解偶每一層,你們用MVP時 是否每層都有自己的數據結構,如果有的話,層與層之間的數據結構轉換開銷大不大?
目前來講,大部分的業務都是一個數據結構穿透使用的,偶爾會有數據結構重新封裝, 影響不大。我個人判斷的話,相比IO處理,數據結構的轉換開銷還是小的,而且,如果有很多複雜轉換的話,保證不要在UI線程中做,也不會太大問題。
Q14:activity與p層用介面的方式銜接的價值在哪?另外如何界定展現方法在哪調用?比如頁面需要顯示一個標題,內容是從之前頁面傳過來的,那是在activity接收後就直接顯示?還是先傳遞到p層再回調activity的顯示方法?感謝
價值在於,把presenter 與activity解耦之後,我可以在別的activity使用這個presenter層邏輯,也可以在這個activity 里調用其它頁面的presenter方法。如果是前頁傳過來的,直接顯示就好,不做過度設計。
Q15:rxJava使用lamaba的語法格式的話貌似會將代碼縮減很多,請問嘉賓有試過這種方式嗎?這個對項目的性能會有什麼影響嗎?因為我試用過幾次後一直出現oom的問題
lambda表達式會讓語法看起來更簡潔,非常推薦使用。但我們的項目目前只能使用jdk 7,悲傷。如果後面我們有機會切換的話,可以再一起分享一下。
Q16:rxjava怎麼實現隊列像handler message那樣,就是隊列執行,不是併發執行?
rxJava中的just方法和from方法都是以隊列形式發出事件。我猜你想問的問題可能是:一個介面的請求依賴另一個API請求返回的數據,這就是嵌套回調問題。可以找下大頭鬼Bruce的一篇文章,《RxJava使用場景小結》,裡面有介紹的,這裡不詳細討論了。
更多精彩內容歡迎關註bugly的微信公眾賬號:
騰訊 Bugly是一款專為移動開發者打造的質量監控工具,幫助開發者快速,便捷的定位線上應用崩潰的情況以及解決方案。智能合併功能幫助開發同學把每天上報的數千條 Crash 根據根因合併分類,每日日報會列出影響用戶數最多的崩潰,精准定位功能幫助開發同學定位到出問題的代碼行,實時上報可以在發佈後快速的瞭解應用的質量情況,適配最新的 iOS, Android 官方操作系統,鵝廠的工程師都在使用,快來加入我們吧!
-
詳細解釋: ...
-
gitHub地址:https://github.com/lily1010/vue_learn/tree/master/lesson04 一 雙括弧用來數據綁定 (1)寫法一: {{message}},這種可以實時響應 (2)寫法二: {{*message}},單次插值,今後的數據變化就不會再引起插值 ...
-
由於一個中文是兩個字元組成,在編碼不一致的情況下會引發字元的“重新”組合,(半個漢字的編碼字元與後面的字元組合生成新的“文字”)引發原本的結束符合“變異”,從而導致找不到結束符號,使得後面的CSS就會失效。小技巧1:CSS中出現的亂碼都是由於CSS字元編碼與頁面的字元編碼不一致所引起的,因此最直接的 ...
-
1. a href="javascript:void(0);" onclick="js_method()" 這種方法是很多網站最常用的方法,也是最周全的方法,onclick方法負責執行js函數,而void是一個操作符,void(0)返回undefined,地址不發生跳轉。而且這種方法不會像第一種方法 ...
-
× 目錄 [1]特征 [2]快捷訪問 [3]文檔寫入 前面的話 文檔節點document,隸屬於表示瀏覽器的window對象,它表示網頁頁面,又被稱為根節點。本文將詳細介紹文檔節點document的內容 特征 文檔節點的三個node屬性——nodeType、nodeValue、nodeName分別是 ...
-
知識講解: split() 方法將字元串分割為字元串數組,並返回此數組。 語法: stringObject.split(separator,limit) 參數說明: 註意:如果把空字元串 ("") 用作 separator,那麼 stringObject 中的每個字元之間都會被分割。 我們將按照不同 ...
-
先看效果圖吧 我們要實現一個自定義的再一個圓形中繪製一個弧形的自定義View,思路是這樣的: 先要創建一個類ProgressView,繼承自View類,然後重寫其中的兩個構造方法,一個是一個參數的,一個是兩個參數的,因為我們要在xml文件中使用該自定義控制項,所以必須要定義這個兩個參數的構造函數。創建 ...
-
一、__block理解: Blocks可以訪問局部變數,但是不能修改, 聲明block的時候實際上是把當時的臨時變數又複製了一份, 在block里即使修改了這些複製的變數,也不影響外面的原始變數。即所謂的閉包。 如果修改局部變數,需要加__block。 API Reference對__block變數 ...