原文連接: http://keeganlee.me/post/architecture/20160303html
架構因人而異,不一樣的架構師大多會有不一樣的見解;架構也因項目而異,不一樣的項目需求不一樣,相應的架構也會不一樣。然而,有些東西仍是通用的,是全部架構師都須要考慮的,也是全部項目都會有的需求,好比API如何設計?架構如何分層?開發環境和生產環境如何分離?這幾年,我負責研發過的App,有餐飲類的、社交類的、智能家居類的、電商類的、新聞媒體類的等等。當有了必定的經驗以後,你總會有一些本身的心得體會。而如下內容就是根據個人這些經歷提煉出來的關於以上幾個問題方面的經驗總結,內容很少,旨在拋磚引玉。 android
一個App,最核心的東西,其實就是數據,而數據的主要來源,就是API。我以前負責的項目,由於API的坑已經受過了很多苦,所以,以後對App項目的架構設計我都會先從API開始。 算法
設計API第一個須要考慮的是API的安全機制。我負責的上一個項目,由於API的安全問題,就被人攻擊了兩次。以後通過分析,主要存在兩個漏洞:一是由於缺乏對調用者進行安全驗證的方式,二是由於數據傳輸不夠安全。那麼,制定API的安全機制,主要就是爲了解決這兩個問題: 數據庫
第一個問題的解決方案,我主要採用設計簽名的方式。對每一個客戶端,Android、iOS、WeChat,分別分配一個AppKey和 AppSecret。須要調用API時,將AppKey加入請求參數列表,並將AppSecret和全部參數一塊兒,根據某種簽名算法生成一個簽名字符串,而後調用API時把該簽名字符串也一塊兒帶上。服務端收到請求以後,根據請求中的AppKey查詢相應的AppSecret,按照一樣的簽名算法,也生成一個簽名字符串,當服務端生成的簽名和請求帶過來的簽名一致的時候,那就表示這個請求的調用者是通過本身受權的,證實這個請求是安全的。並且,每一個端都有一個Key,也方便不一樣端的標識和統計。爲了防止AppSecret被別人獲取,這個AppSecret通常寫死在代碼裏面。另外,簽名算法也須要有必定的複雜度,不能輕易被別人破解,最好是採用本身規定的一套簽名算法,而不是採用外部公開的簽名算法。另外,在參數列表中再加入一個時間戳,還能夠防止部分重放攻擊。 編程
第二個問題的解決方案,主要就是採用HTTPS了。HTTPS由於添加了SSL安全協議,自動對請求數據進行了壓縮加密,在必定程序能夠防止監聽、防止劫持、防止重發,主要就是防止中間人攻擊。蘋果從iOS9開始,默認就採用HTTPS了。而關於在Android中如何使用 HTTPS,Google官方也給出了不少安全建議。不過,大部分App並無按照安全建議去實現,主要就是沒有對SSL證書進行安全性檢查,這就成爲了一個很大的漏洞,中間人利用此漏洞用假證書就能夠經過檢查,從而能夠劫持到全部數據了。所以,爲了安全考慮,建議對SSL證書進行強校驗,包括簽名CA是否合法、域名是否匹配、是否是自簽名證書、證書是否過時等。 api
API返回的數據,通常都是採用JSON格式進行傳輸。然而,JSON的值只有六種數據類型: 數組
我遇到過的,關於API的坑有大部分就是由於JSON數據和實體對象轉化時出錯致使的,並且是各類各樣的錯誤都有,其中不乏有一些很奇葩的錯誤。 緩存
最麻煩的就是處理Date類型,由於JSON自己沒有Date類型,所以,JSON庫將Date類型的數據序列化時會轉爲String。這時,不一樣環境,不一樣平臺,以及用不一樣的JSON解析庫,轉換後的結果常常會不一樣。好比,你在開發機上可能獲得的結果是」2016-1-1 17:11:11」,但放到服務器後結果卻變成了「Jan 1,2016 5:11:11 PM」 ,客戶端進行反序列化時無疑會失敗。後來,我取消了全部Date類型,統一採用時間戳表示,就再沒有轉化的煩惱了。 安全
另外,接口的開發人員有時候會將一些數據錯誤地轉換爲了String,致使客戶端使用時因類型錯誤而異常。例如,原本是數字的1,被轉成了"1",客戶端作運算時就會出錯,或用switch判斷時也會出錯,或其餘沒法轉換的狀況發生時;例如,爲空時JSON正確地表示應該是null,但若是轉爲了String就變成了"null",那問題就來了,我遇到的由於這個錯誤的轉換致使的程序奔潰已經好幾回了,第一次的時候,查了一成天才定位到問題所在。 服務器
還有,由於接口的開發人員不一樣,不少時候還會出現不一樣接口同一個意思的參數名稱卻不一樣。好比,對於有分頁數據的接口,通常都有當前頁的參數,A開發人員可能將參數命名爲currentPage,第一頁是從0開始;B開發人員在另外一個接口則命名爲currPage,第一頁卻從1開始;C開發人員在另外一個接口又命名爲presentPage,第一頁又是從0開始。客戶端的開發人員看到也是醉了。
每一個技術團隊通常都會有一份接口協議文檔,主要內容包括每一個接口的描述、入參、輸出結果等,但通常並不嚴謹,不少地方沒有統一標準,從而容易出現不少坑。所以,有一份統一標準且嚴格執行的接口協議很是重要。協議的內容除了規定每一個接口,包括接口中每一個數據具體的數據類型,還須要規定一套共用的數據字典,以及其餘須要統必定義的信息,好比簽名算法等。一旦有了這份統一標準且嚴格執行的接口協議,不少問題都將迎刃而解。
咱們已經不止一次由於接口發生變更而致使舊版本的App出錯的問題,並且變更不必定是修改了接口自己,有多是底層增長了一種新的數據結構,接口把新數據也返回給客戶端了,但客戶端舊版本是解析不了的,從而就致使出錯了。
爲了解決接口的兼容性問題,須要作好接口版本控制。實現上,通常有兩種作法:
平時小版本的更新,就採用第一種方式,咱們的作法是根據不一樣版本號作不一樣分支處理。大版本的更新,則用第二種方式,這時候,基本就是一套全新的接口系統了,跟舊版本是相對獨立的。
當版本愈來愈多時,維護就會成爲一個大問題,咱們沒那麼多精力去維護全部版本,所以,太舊的版本通常就不會再維護了。這時候,若是有用戶還在使用即將廢棄的舊版本,須要提醒用戶升級到新版本。
API的設計完成以後,接下來我就會考慮App項目的總體架構了。總體如何架構,我也曾經作過很多嘗試。早期的時候,Android就是將全部操做都放在Activity裏完成,包括界面數據處理、業務邏輯處理、調用API。後來發現Activity愈來愈臃腫,代碼愈來愈複雜,很難維護。因而就開始思考如何拆分,如何才能作到鬆耦合高內聚。
前面也說過,一個App的核心就是數據,那麼,從App對數據處理的角色劃分出發,最簡單的劃分就是:數據管理、數據加工、數據展現。相應的也就有了三層架構:數據層、業務層、展現層。它們之間的關係以下圖,數據層是三層中的最底層,往下,它接入API;往上,它向業務層交付數據。業務層夾在三層中間,屬於數據的加工廠,將數據層提供上來的數據加工成展現層須要展現的數據。展現層處於三層中的最上層,主要就是將從業務層取得的數據展現到界面上。
數據層是數據管理者,主要任務就是封裝API,並將數據結果交付給上層,中間會再加個數據緩存。整個主流程以下圖:
調用網絡API時,還要判斷網絡狀態,根據不一樣狀態作不一樣處理。若是網絡不可用,就無需發起請求了。網絡可用時,也要區分是鏈接WIFI仍是鏈接移動網絡。鏈接移動網絡時,通常須要限制調用比較耗流量的請求。曾經,咱們沒有對移動網絡狀態下的請求進行限制,結果,測試時流量 DuangDuangDuang地一會兒就不見了十幾M。鏈接WIFI時,則無需設置這種限制,並且還能夠預先請求一些接口,好比請求當前分頁數據時,能夠將下一頁的數據也預先請求。
緩存也須要緩存策略,不一樣的接口須要作不一樣的緩存處理。首先,緩存只適用於獲取數據的接口,對於修改數據的接口則不適用。其次,不一樣接口緩存時間通常也不一樣,對於不多變更的數據緩存時間能夠設置長一些,而頻繁變更的數據緩存時間則比較短,甚至不進行緩存。最後,緩存數據由於比較多,咱們通常保存在數據庫,而對於調用頻率高、最新的數據,還會在內存中也擁有一份緩存,不過緩存時間比較短。請求緩存數據時,會先檢查內存緩存中有沒有,有則直接將緩存的數據返回,沒有才從數據庫獲取。
那麼,如何將數據交付給業務層呢?這是整個數據層模塊與外部交互的部分,當與外部交互的時候,通常都要符合面向接口編程的原則,所以只要提供開放的數據接口就能夠了。對於接口的參數須要說明一下,上面提到的參數有appKey、version、currentPage這幾個,還有簽名sign、時間戳time,其實能夠分爲兩類:系統參數和業務參數。像appKey、version、sign、time這些屬於系統參數,而 currentPage,或username之類的則屬於業務參數。數據層開放的數據接口的參數只須要包含業務參數就能夠了,業務層並不須要關心繫統參數是什麼,系統參數在數據層內部封裝API時指定就能夠了。
業務層是數據加工者,主要就是從數據層獲取數據,而後通過業務邏輯處理後轉化成展現層須要的數據。業務層由於夾在數據層和展現層中間,起着承上啓下的做用。也所以,業務層很容易淪落爲只是一個數據的中轉站,主要就是由於對業務層具體的做用和職責沒有理解清楚。
這裏用一個例子來講明業務層具體的工做吧,就舉個用戶註冊的例子。用戶註冊時,界面上須要用戶提供手機號、短信驗證碼、密碼、確認密碼。那麼,最簡單的操做就是,帶上這些參數調用數據層的註冊接口。好了,問題來了,註冊接口並無提供確認密碼的參數。那好,調用註冊接口以前先判斷下密碼和確認密碼是否一致,不一致則返回錯誤提示給用戶,一致了才調用註冊接口。好了,第二個問題來了,用戶等網絡請求等了一段時間後,請求結果返回說手機號少了一位。下一次,又等了一段時間,此次又返回說手機號多了一位。就由於一個小錯誤要讓用戶等那麼久,用戶確定有意見。後臺也有意見,各類非法的請求都發過來,是嫌服務器壓力不夠大啊。那好,調用接口以前對這些參數作有效性檢查吧,手機號要規範,短信驗證碼只能爲六位數字,密碼不能少於六位。終於註冊成功了,第三個問題又來了,註冊接口是沒有返回用戶的accessToken的,只有登陸接口才會返回。讓用戶手動再登陸一下?這用戶體驗不太好啊。正確的姿式應該是註冊成功後再自動調用一次登陸接口,若是由於網絡問題第一次登陸失敗,後面還須要再自動調用多一次,若是仍是調用失敗,才讓用戶手動登陸。
上面的例子中,對參數的有效性檢查,註冊成功後的自動登陸,都屬於業務邏輯的處理,也就是說都是業務層的工做。
業務層交付給展現層的數據也是經過接口的方式,不過,和數據層交付給業務層時不一樣的是:交付給展現層的數據應該是經過異步回調返回的。由於獲取數據是一個比較耗時的任務,經過異步回調纔不會阻塞UI主線程。
展現層做爲數據展現者,它只要關心數據如何展現就能夠了。不過,數據如何展現卻不是那麼簡單。展現層是三層架構中最複雜的一層了,要考慮的東西遠遠多於其餘兩層,涉及的東西包括但不限於界面佈局、屏幕適配、圖片資源、文本資源、顏色資源等等。在開發一段時間後,展現層出現代碼混亂是最多見的。所以,作好展現層,就須要保持高質量的代碼。要保持高質量代碼,我以爲至少應該遵循幾條基本的原則:
所謂無規矩不成方圓,展現層的設計,要從開發規範開始。一份好的開發規範,是保證代碼有較高的可讀性的基礎。iOS方面,蘋果已經有一套 Coding Guidelines ,主要屬於命名方面的規範。當咱們制定本身的開發規範時,首先就要遵照蘋果的這份規範,在此基礎上再加上本身的規範。Android方面,我也在個人博客中分享過一套( Android技術積累:開發規範 ),主要分爲書寫規範、命名規範、註釋規範三部分。
最重要的不是開發規範的制定,而是開發規範的執行。若是沒有按照開發規範去執行,那開發規範就等於形同虛設,那代碼混亂的問題依然得不到解決。
說到單一性,面向對象設計中,有一個基本原則就是單一職責原則,它規定一個類應該只有一個發生變化的緣由。保持單一性是減低耦合度的關鍵標準,其目的就是各方面的解耦。而我這裏說的單一性不僅是規定類的單一,也包括界面的單1、方法的單1、資源文件的單一等。
界面的單一,首先是界面的佈局和界面的數據應該分離。另外,界面數據的獲取和展現也應該分離。一句話,保持界面的單一性就是要保持界面上每一個維度都作好分離,從界面的佈局,到數據的獲取,數據的檢查,數據的展現。
方法的單一,則表現爲一個方法是對一個行爲的封裝。行爲又能夠拆分爲多個步驟,每一個步驟其實也是更細化的行爲。所以,方法嵌套方法是一種常態。那麼,保持方法的單一性,關鍵不在於怎麼定義這個方法的行爲,而在於這個行爲要怎麼拆分紅更細的行爲。舉個例子,一般在Activity的onCreate 方法,作初始化操做,細分出來就分爲了:控件的初始化、邏輯變量的初始化、數據的初始化。數據的初始化又能夠再細分:數據的獲取、數據的展現。每一個細化的行爲都應該封裝爲一個獨立的方法,這樣,才真正符合方法的單一性。
資源文件的單一,主要是指Android的各種資源文件,包括存放字符串的strings.xml,存放字符串數組的arrays.xml,存放顏色值的colors.xml,存放尺寸值的dimens.xml,等等。資源文件的單一,是說全部相關的資源信息要在資源文件裏定義並引用到代碼或佈局文件裏,而不是在代碼或佈局文件裏直接定義。這樣作,能夠很方便地作各類適配和修改,好比支持國際化,好比不一樣分辨率的屏幕用不一樣尺寸值。iOS則沒有提供和Android同樣的資源文件分離的機制,但能夠參考Android的作法本身去實現。
每一個App項目,至少都會有兩個環境:測試環境和生產環境。多的甚至有四個環境:開發環境、測試環境、預生產環境和生產環境。開發人員常常須要在環境之間切換,測試人員也一樣。常常出現測試人員今天須要測試環境的最新版本,叫App開發人員打包一個給她,明天須要切換到生產版本,再叫App開發人員打包一個生產環境的給她。咱們知道,一個App,在一臺手機上要麼只能是測試環境的,要麼只能是生產環境的。測試人員要測試兩個環境,只能不斷替換不一樣環境的同個App,這實在太麻煩了。爲了解決此問題,最好的方案就是環境分離,不一樣環境有不一樣的App。
一個App的惟一標識,Android是用包名,iOS是用Bundle Identify。那麼,在一個系統想安裝不一樣環境的App,只要每一個環境App的包名和Bundle Identify不一樣便可。好比,生產版的包名和Bundle Identify命名爲com.mydomain.myapp,測試版的包名和Bundle Identify則命名爲com.mydomain.myapp.beta,這樣,Android和iOS都會識別爲兩個不一樣的App了。
不過,只改包名和Bundle Identify是不夠的,應用圖標和應用名稱也要修改,否則安裝以後很難區分哪一個App是哪一個環境的。通常作法就是,非生產環境的App圖標就是在生產圖標的基礎上添加一個環境標籤,同時App的應用名稱也是在生產的基礎上添加環境後綴名。另外,由於包名和Bundle Identify不一樣了,微信、微博、百度地圖等這些第三方平臺也都須要爲不一樣環境的App分別申請不一樣的appID。
實現上,最笨的方法就是拷貝當前工程,而後修改,缺陷很明顯,維護成本很高。不過,好在Android和iOS都有很方便的修改方式。
Android有了Gradle,能夠設置多個不一樣的Flavors,每一個Flavor都有一個applicationId屬性,其實就是App的包名。好比,生產版和測試版的設置以下:
productFlavors { myapp { applicationId "com.mydomain.myapp" } myappBeta { applicationId 'com.mydomain.myapp.beta' } }
這樣,其實就有兩個App了。而後,源代碼新建一個和main同級的目錄,命名爲myappBeta,而後,將圖標、名稱和第三方設置之類的,和main保持同樣的位置、文件名、屬性等,就能夠替換成環境相關的了。
iOS則能夠經過建立多個環境的Target來實現環境分離,不一樣Target能夠設置不一樣的Bundle Identify、Bundle display name、更換圖標。另外,每一個Target也各自有本身的一份plist文件的,環境變量和第三方設置之類的,均可以設置在相應的plist文件裏。
至此,關於App架構方面的經驗總結就先講這麼多了。其中,部份內容在我以往的博客上也已經有所體現,有興趣的讀者能夠前往個人博客瞭解並歡迎參與討論。