過去的一年多時間里都在做SDK, 這一年從Web開發(fā)轉(zhuǎn)到Android開發(fā)也算是成功的轉(zhuǎn)型了, 被坑了很多次, 也坑了很多人很多次, 在各種互坑的過程中學(xué)到了些東西. 寫在這里也算是對過去一年坑別人的一次反省(阿彌陀佛~~).
SDK有一個很大的特點(diǎn), 它的用戶是程序猿(這是我大學(xué)時代夢寐以求的呢); 而App的用戶則通常是普通用戶. 我們知道, 有的程序猿很懶, 有的程序猿又喜歡追求完美, 簡單來說吧, 就是程序猿太tm難伺候了. 這就直接導(dǎo)致了SDK開發(fā)的一些特殊性. 下面簡單談?wù)勥^去在項目中遇到的覺得應(yīng)該注意的一些點(diǎn), 大部分可能不只是SDK開發(fā)才會遇到的, 即使是App開發(fā), 如果你寫出來的接口別人用起來比較爽, 那也是不錯的事呢.
我們的SDK是給游戲用的, 所以后面我會習(xí)慣性稱SDK的使用者為游戲. 另外說明一點(diǎn)是我們的SDK封裝了很多其他外部SDK.
錯誤碼/錯誤信息 設(shè)計
我們當(dāng)初的錯誤碼設(shè)計可以簡單分為三個階段:
- 直接透傳外部SDK的錯誤碼 : 這樣有幾個問題, 外部各個平臺錯誤碼可能會重復(fù), 而且錯誤碼層次不一致, 游戲需要理解大量的錯誤碼.
- 簡單轉(zhuǎn)換歸類外部平臺錯誤碼: 將外部平臺錯誤碼做歸類整合變?yōu)槲覀冏约旱腻e誤碼. 這個階段按我們內(nèi)部邏輯區(qū)分錯誤碼(我不會告訴你們, 其實就是拍腦袋想的分類).
上面兩個階段是已經(jīng)完成了, 也就是說我們的現(xiàn)狀是第二個階段, 其實這種方式的問題現(xiàn)在已經(jīng)開始逐漸顯現(xiàn)出來, 目前出現(xiàn)的主要問題在于游戲收到這些錯誤碼以后自己還是需要?dú)w類, 例如某些錯誤碼游戲需要重新登錄, 有的錯誤碼游戲可以嘗試重試, 有的錯誤碼則游戲可以忽略. 也就是說從我們的錯誤碼到游戲邏輯之間是需要一次轉(zhuǎn)換的, 而這次轉(zhuǎn)換交給了游戲, 不同的游戲的這個轉(zhuǎn)換關(guān)系不同最終導(dǎo)致了一些使用上的混亂.
說到這里就到了我目前的一個想法了, 錯誤碼按業(yè)務(wù)邏輯來區(qū)分, 直接通過錯誤碼告訴游戲下一步應(yīng)該做什么, 而不是上一步發(fā)生了什么, 了解上一步發(fā)生什么問題的目的就是要知道下一步需要做什么. 但是如果錯誤碼這樣簡化以后必然還會出問題, 如果游戲想要處理得很精細(xì)怎么辦? 我對這個問題給出的答案是二級錯誤碼, 第一級錯誤碼是簡化錯誤碼, 偏向業(yè)務(wù)邏輯, 供很懶的程序猿用, 他不用理解中間任何事情, 第二級錯誤碼是詳細(xì)錯誤碼, 偏內(nèi)部實現(xiàn), 供最求完美的程序猿使用, 兩者互不干擾.
除了上述錯誤碼的問題外, 伴隨著錯誤碼的還有錯誤信息, 錯誤信息要在前期定位好的他們的作用, 是用于開發(fā)內(nèi)部定位問題OR提示用戶, 如果是用于提示用戶, 最好是做成可配置的, 因為可能涉及到國際化之類的問題, 另外如果是用于開發(fā)內(nèi)部定位問題, 則應(yīng)該注意里面應(yīng)該包含足夠的錯誤信息. 例如之前給一個同事聊天了解到他們的錯誤信息設(shè)計方法是, 首先錯誤碼分段: 10000, 20000, 30000 …, 然后某個模塊如果發(fā)生錯誤, 則將錯誤碼返回到下游模塊, 下游模塊將上游的錯誤碼加到錯誤信息中, 并將錯誤碼轉(zhuǎn)換成本模塊錯誤碼, 繼續(xù)返回給調(diào)用者, 依次類推, 這樣可以保證任何一層調(diào)用者, 只要收到了錯誤信息, 可以迅速看出來是哪個模塊出現(xiàn)了問題.
上面說的略多, 總結(jié)一下:
錯誤碼: 分級, 一層簡化錯誤碼引導(dǎo)用戶做后續(xù)流程, 一層詳細(xì)錯誤碼詳細(xì)告訴用戶發(fā)生了什么(不需要的用戶可以完全忽略這個錯誤碼).
錯誤信息: 基于錯誤碼分段, 可將錯誤碼整合到錯誤信息中, 一次請求經(jīng)過的所有模塊錯誤碼全都包含在內(nèi), 方便定位問題.
同步接口設(shè)計
在設(shè)計一個對外接口之前肯定要考慮的幾件事:
- 接口名稱
- 接口輸入
- 接口輸出
關(guān)于接口名稱, 我們之前將自己的接口全都帶了統(tǒng)一的前綴, 現(xiàn)在想起來, 這種做飯真是傻的可愛~, 不過之前接口名稱上也有做的還不錯的, 我們每個接口名稱都很長, 基本上說了這個接口在干什么, 這點(diǎn)對于SDK 接口來說真的太重要的了, 一個好得接口名稱可以讓你少些好幾頁的文檔.
接口輸入主要指接口參數(shù), 為什么說主要呢, 因為我個人覺得如果某個接口用到了全局的某些值, 應(yīng)該將其看作本接口的輸入, 這些全局的值是會影響接口輸出的. 而且明確的意識到自己寫的接口里面用到那些外部全局的東西絕對不是一件壞事.
下面簡單說說我自己在參數(shù)設(shè)計上遇到的幾個坑:
第一個是我在某接口里面加入了大部分游戲不使用, 這個設(shè)計最后造成的問題是大部分游戲會來咨詢那個參數(shù)填寫什么, 即使我告訴他們填空, 他們對于那個接口還是會較為謹(jǐn)慎. 第二次遇到類似的需求, 我沒有繼續(xù)這樣做, 我的做法是保持接口參數(shù)為大部分游戲使用的參數(shù), 增加一個接口讓特殊需要的游戲傳入他們特殊需要的, 如此保證大部分游戲不會對參數(shù)產(chǎn)生疑惑. 這種方式效果好壞還需要實際使用場景驗證.
第二個參數(shù)相關(guān)的是我在一個接口里面放了6個String的參數(shù), 結(jié)果自己代碼里面參數(shù)順序弄亂了, 因此也導(dǎo)致了多個B(內(nèi))U(牛)G(滿)單(面). 出現(xiàn)這種問題很重要的一個愿意在于, 在接口參數(shù)很多的情況下, 我還是按Eclipse的格式去格式化代碼了, 格式化出來的樣子差不多如下:
public void methodA(String aaa, String dweqr, String qewrsfa, String werjopiu,
String ewqoruqwe, String ewrqwer, String werjopi) {
}
隨手敲的代碼~~, 如果是上面一段, 讓我去看參數(shù)順序我肯定會暈的, 特別是調(diào)用變多的時候. 所以這樣些是很容易出錯的, 我自己經(jīng)歷了上面幾個bug, 目前想到的一個最簡單的辦法(不根治此問題)是超過3個參數(shù)每個參數(shù)折行, 不論是方法申明還是調(diào)用, 代碼如下:
public void methodA(
String aaa,
String dweqr,
String qewrsfa,
String werjopiu,
String ewqoruqwe,
String ewrqwer,
String werjopi) {
}
這樣看起來可能沒那么漂亮, 甚至有的人可能會覺得我在湊代碼行, 但是這樣確實能讓我更清楚的看出來參數(shù)順序. 但是... 其實這樣也還是很容易出錯好吧. 有別的方法嗎? 目前我想到仍為在項目中使用的方法就是, 用一個簡單對象封裝參數(shù), 例如上面例子:
// 方法定義
public void methodA(SimpleBean data) {
}
// 參數(shù)封裝為對象
class SimpleBean{
String aaae;
String dweqr;
String qewrsfa;
String werjopiue;
String ewqoruqwe;
String ewrqwer;
String werjopi;
}
// 調(diào)用
SimpleBean bean = new SimpleBean();
bean.aaae = aaae;
bean.dweqr = dweqr;
…
bean.werjopi = werjopi;
methodA(bean);
經(jīng)過這樣一次封裝, 可以讓給接口傳值變?yōu)闉閰?shù)對象的屬性賦值, 這樣賦值出錯的可能應(yīng)該是遠(yuǎn)小于直接傳參的.
總結(jié)一下上面說的兩個參數(shù)相關(guān)的問題:
- 如果一個接口的參數(shù)使用頻度較為固定且差異較大, 可以考慮按使用頻度拆分參數(shù), 保證讓絕大多數(shù)人使用上簡潔易用的接口.
- 參數(shù)較多時, 可以考慮調(diào)整代碼風(fēng)格, 或者用對象封裝參數(shù)來避免參數(shù)順序錯亂的BUG.
最后關(guān)于輸出呢, 函數(shù)的輸出可以是返回值或者回調(diào), 對于同步接口, 通常是直接將輸出放入返回值中. 如果是異步接口則可能同時具有返回值和回調(diào). 異步接口的返回值將在下面繼續(xù)探討.
異步接口設(shè)計
在設(shè)計異步接口之前一定要想清楚確實需要異步接口嗎? 因為如果你提供一個阻塞的同步接口, 使用者可以自己開一個線程自己將其變?yōu)楫惒浇涌谑褂? 但是如果你提供的異步接口, 使用者像將其改為同步就比較麻煩, 回調(diào)太多也會導(dǎo)致代碼較為混亂, 所以我認(rèn)為非必要不提供異步接口, 或者說為了方便, 同一個接口提供異步和同步兩種.
異步接口返回值設(shè)計
通常情況下異步接口的返回值會用來標(biāo)識參數(shù)是否合法, 如果參數(shù)不合法直接返回false并且不會有回調(diào). 我覺得這里最重要的一點(diǎn)是: 規(guī)則統(tǒng)一, 一定要在實現(xiàn)前確定規(guī)則, 例如參數(shù)錯誤是給錯誤回調(diào) OR 直接返回false不回調(diào), 出錯如何回調(diào), 錯誤碼如何定義(見前面錯誤碼部分), 成功如何回調(diào), 并且保證如果參數(shù)正確一定要有回調(diào)(如果此調(diào)用有外部依賴最好是在SDK內(nèi)部做定時器處理超時, 超時后給調(diào)用者回調(diào)), 因為很有可能游戲調(diào)用以后不做超時處理等SDK回調(diào), 一旦出現(xiàn)SDK沒有回調(diào)的情況, 則將造成玩家無限等待.
異步接口回調(diào)設(shè)計
在回調(diào)設(shè)計這一塊我們也是踩盡了坑....
回調(diào)第一坑: 沒有請求標(biāo)識
在設(shè)計之處由于沒有考慮到游戲連續(xù)調(diào)用多次接口的情況, 回調(diào)里面沒有任何內(nèi)容標(biāo)識請求, 導(dǎo)致游戲一旦多次調(diào)用異步接口, 將無法對應(yīng)請求和返回, 這很傻, 但是我們確實犯了這個錯. 其實這里的問題和上面提到的各個模塊錯誤碼要跟隨錯誤信息的道理是一樣的, 一定要讓你的錯誤/調(diào)用有據(jù)可行, 清楚的知道它從哪里來, 到哪里去.
回調(diào)第二坑: 使用公用回調(diào)對象
可能有人在看到上面請求標(biāo)識的時候可能會像, 如果我本身就是ObjA調(diào)用, 那回調(diào)回到ObjA, 這樣即使沒有請求標(biāo)識我也能區(qū)分所有請求啊, 這就不得不說第二個坑了, 由于我們開發(fā)過程中, 所有調(diào)用和回調(diào)都需要用jni封裝, 所以為了方便, 我們會讓游戲設(shè)置一個全局回調(diào)對象, 所有回調(diào)都通過這個回調(diào)對象發(fā)起調(diào)用, 也正式因為如此, 本段之前說的那種通過不同對象來識別不同請求的方法在我們SDK再一次行不通.
上面兩個問題繼續(xù)在困擾著我們, 目前想來有幾個方式優(yōu)化上面兩個問題:
- 每個異步接口傳入各自的回調(diào)對象, 以此來保證一個請求清晰對應(yīng)一個回調(diào).
- 如果還是需要使用公共回調(diào)對象, 增加字段來標(biāo)識請求, 例如調(diào)用時同步返回一個請求ID, 回調(diào)時候時候?qū)D此ID帶回來(startActivityForResult中的requestCode就是這樣的). 其實通常情況如果可以把所有請求的數(shù)據(jù)帶回來是更好的方式.
環(huán)境規(guī)劃和管理
除了代碼層面的一些問題, 各種環(huán)境的管理也是必不可少的, 例如我們的環(huán)境變化過程如下(下面會細(xì)說如此變化的原因):
- 正式環(huán)境
- 正式環(huán)境+聯(lián)調(diào)環(huán)境
- 正式環(huán)境+聯(lián)調(diào)環(huán)境+開發(fā)環(huán)境
- 正式環(huán)境+聯(lián)調(diào)環(huán)境+開發(fā)環(huán)境+測試環(huán)境
開始階段, 項目最開始只有一個環(huán)境, 后來遇到游戲自己測試數(shù)據(jù)和正式的數(shù)據(jù)會相互影響, 于是增加聯(lián)調(diào)環(huán)境給游戲聯(lián)調(diào)使用, 相當(dāng)于游戲的測試環(huán)境.
后來, 由于SDK內(nèi)部開發(fā)節(jié)奏較快, 經(jīng)常更新聯(lián)調(diào)環(huán)境代碼, 多次出現(xiàn)聯(lián)調(diào)環(huán)境不可用, 造成大量的聯(lián)調(diào)咨詢, 于是想到需要保持游戲聯(lián)調(diào)環(huán)境的穩(wěn)定, 新增開發(fā)環(huán)境用于內(nèi)部開發(fā)使用.
再后來, 有專業(yè)測試人員接入, 為了保證聯(lián)調(diào)環(huán)境的穩(wěn)定, 沒經(jīng)過測試的版本肯定是不能放到聯(lián)調(diào)/正式環(huán)境上的, 也就是說測試肯定不能到聯(lián)調(diào)或者正式環(huán)境上測試, 那如果到開發(fā)環(huán)境測試同樣還是會遇到問題(開發(fā)環(huán)境經(jīng)常變), 這樣就必須要再增加一個環(huán)境給測試使用.
最后各種環(huán)境使用的流程如下:
- 開發(fā)在開發(fā)環(huán)境完成開發(fā)
- 將開發(fā)環(huán)境版本部署到測試環(huán)境
- 測試在測試環(huán)境測試版本(此時開發(fā)可以在開發(fā)環(huán)境繼續(xù)開發(fā), 互不影響)
- 測試環(huán)境版本測試通過以后發(fā)布(當(dāng)然是灰度的)到聯(lián)調(diào)環(huán)境和正式環(huán)境
具體需要那些環(huán)境需要的根據(jù)各自的業(yè)務(wù)場景確定, 首先必須要確定有哪幾類人會使用這些環(huán)境, 做好歸類, 然后再梳理清楚他們之前是否不能相互影響, 那些是互斥的, 那些可以公用環(huán)境, 這樣來最終確定需要那些環(huán)境. 在項目前期做好這些規(guī)劃能省很多事, 我們在整個環(huán)境變更的過程中也吃了不少的虧.
版本開發(fā)/發(fā)布策略管理
關(guān)于版本管理, 我想說的就只有一句: 一定要灰度…
最開始我們每次出版本都是發(fā)給全部游戲, 然后… 一(必)旦(然)有BUG, 無窮無盡的咨詢, 無窮無盡的吐槽, 壓力非常大. 這樣的壓力也導(dǎo)致了經(jīng)常在趕緊急版本, 這就必然會打亂版本的節(jié)奏, 最終的結(jié)果只有一個, 開發(fā)加班到吐血.
后來我們討論每次發(fā)布版本灰度以后, 其實吧, 就是每次先找少量游戲讓我們坑一下, 坑完我們填一遍坑, 然后我們在全量, 這樣發(fā)出去的版本一半不會出現(xiàn)很大的BUG或者說不會出現(xiàn)大量的BUG. 這應(yīng)該算是我們做的比較成功的一點(diǎn).
開發(fā)規(guī)范/流程
越早定義規(guī)范越好, 規(guī)范中不明確的點(diǎn)團(tuán)隊內(nèi)及時溝通, 保持團(tuán)隊內(nèi)對規(guī)范的理解一致. 出現(xiàn)問題嘗試梳理規(guī)范(這里不是指那些條條框框, 規(guī)范一定要實用), 但是規(guī)范如果讓人執(zhí)行, 那肯定是不可能完全靠譜的, 所以..... 能用代碼做的一定交給代碼做, 例如我們的SDK打包過程, 可以定義一個規(guī)范說明要放那些文件, 文件夾如何命名等等, 我們前期確實這樣做了, 結(jié)果是基本上每次都會有一點(diǎn)遺漏, 后來用了一個腳本打包, 從那以后生活美好了許多. 故能用代碼做的就用代碼做, 代碼比人更可信.