- 原文地址:Things I wish I knew when I started building Android SDK/Libraries
- 原文作者:本文已獲作者 Nishant Srivastava 授權
- 譯文出自:掘金翻譯計劃
- 譯者:jifaxu
- 校對者:BoilerYao, gaozp
當釋出安卓開源庫時我希望知道的東西
一切要從安卓開發者開發自己的“超酷炫應用”開始說起,他們中的大多數會在這個過程中遇到一系列問題,而他們中的一些人,會提出可能的解決方案。。
事情是這樣的,如果你和我一樣認為這個問題足夠重要,並且沒有已知的解決方案,那麼我將以模組化的方法抽象整個解決方案,這就是一個安卓庫了。這樣以後當我再次遇到這個問題時,我就可以很輕鬆的重用這個解決方案了了
到目前為止一切都好。現在你有一個庫了,也許只是拿來自用,或者你認為別人也會遇到這個問題,然後你對外發布了這個庫(開原始碼)。我相信(更確切的說看上去是這樣)很多人認為這就算大功告成了。
錯了! 這一點是大多數人通常弄錯的地方。你的安卓庫將被一些不在你身邊的開發者使用,他們只是想用你的庫來解決同樣的問題。你的庫的 API 設計的越好,它被使用的概率就越大,因為它不會讓使用者感到困惑。從一開始就應該明確的是,為了讓他人順利地開始使用這個庫,你需要做些什麼。
為什麼會發生這種事?
開發者在第一次釋出安卓庫的時候通常不會注意 API 的設計,至少他們中的大多數都不會。倒不是因為漠不關心,而是因為他們都只是新手,又沒有一個可以參考的 API 設計規範。之前我也陷入了同樣的僵局,所以我可以理解找不到相關資料的沮喪。
我剛好做了一個開源庫(你可以在這個地址檢視)所以有一些經驗。我給出了一個對於每一個 Android API 庫的開發者來說,都應該牢記的簡要列表(它們中的一部分同樣適用於通用的 API 設計)。
需要注意的是,我的列表並不完善。它只包含了我遇到過並且希望在一開始就明確的一些問題,當我有了新的經驗後我也會來更新這篇部落格。
在我們正式開始之前,個所有人在構建安卓庫時都會面臨的最基本問題,那就是:
你為什麼要建立一個安卓庫?
額……
好吧,無論何時都不是非要建立一個庫。在開始之前好好想想它能給你帶來什麼價值。問問自己下面幾個問題:
有沒有現成的解決方案?
如果你回答是有,那麼考慮下使用已有的解決方案吧。
如果現有方案無法完美解決你的問題,即使在這種情況下,最好也是從 fork 程式碼開始,修改它以解決你的問題。
向現有的庫中提交(Pull Request)你所做的修補,對你來說將是一個很好的加分點,同時也會讓整個社群從中受益。
如果你的回答是沒有,那麼就可以開始編寫安卓庫了。之後與世界分享你的成果以便別人也可以使用它。
你的 artifact 有哪些打包方式
在開始之前,你需要決定以什麼樣的方式向開發者釋出你的 artifact。
讓我在這裡解釋一下這篇部落格中的一些概念。先解釋下 artifact。
在通用軟體術語中,artifact 是在軟體開發過程中產出的一些東西,可以是相關文件或者一個可執行檔案。
在 Maven 術語中,artifact 是編譯的輸出,jar
,war
,arr
或者別的可執行檔案。
讓我們看下可選項
- Library Project:你必須獲取程式碼並連結到你的工程裡。這是最靈活的方式,你可以修改它的程式碼,但也引入了與上游更改同步的問題。
- JAR:Java Archive 是一個專門將很多 Java 類以及後設資料放到一起的包檔案。
- AAR:Android Archive 類似於 JAR,但有些額外的功能。和 JAR 不同,AAR 可以儲存安卓資源和 manifest 檔案,這允許你分享諸如佈局和 drawable 等資原始檔。
我們有了 artifact 了,然後呢?這些 artifact 應該放在哪裡呢?
開玩笑……
你有好幾種選擇,每種都有優缺點。讓我們一個一個看。
本地 ARR
如果你不想將你的庫提交到任何倉庫裡,你可以產生一個 arr
檔案並直接使用它。閱讀 StackOverflow 上的一個回答學習如何實現。
簡單來說,將 arr 檔案放到 libs 資料夾裡(沒有就建立),然後在 build.gradle 中新增如下程式碼:
dependencies {
compile(name:'nameOfYourAARFileWithoutExtension', ext:'aar')
}
repositories{
flatDir{
dirs 'libs'
}
}複製程式碼
隨之而來的就是無論何時你想要分享你的安卓庫時你都繞不過你的 arr
檔案了(這可不是分享你的安卓庫的好方式)。
儘可能的避免這麼做,因為它容易引發很多問題,尤其是程式碼庫的可管理性和可維護性。
另一個問題是這種方式沒辦法保證你的使用者使用的程式碼是最新的。
更不用說整個過程漫長而且容易出現人為錯誤,而我們僅僅是往專案中新增一個庫。
本地/遠端 Maven 倉庫
如果你只想給自己用這個安卓庫該怎麼做? 解決辦法是部署一個自己的 artifact 倉庫(在這裡瞭解如何去做)或者使用 GitHub 或者 Bitbucket 作為你自己的 maven 庫(在這裡)。
再次強調,這只是用來發布自用包的方法。如果你想要與他人分享,那這不是你需要的方式。
這種方式的第一個問題是你的 artifact 是存放在私有倉庫裡的,為了讓別人訪問到你的庫(library)你不得不給他們訪問整個倉庫(repository)的許可權,這可能會導致安全問題。
第二個問題是別人要想用你的庫就得在他的 build.gradle
檔案里加上額外的語句。
allprojects {
repositories {
...
maven { url '
http://url.to_your_hosted_artifactory_instance.maven_repository' }
}
}複製程式碼
說實話這樣比較麻煩,而我們都希望事情簡單一點。這種方式在釋出安卓庫的時候比較迅速但是為別人的使用增加了額外步驟。
Maven Central, Jcenter 或 JitPack
現在最簡單的釋出方式是通過 JitPack,你可能會想去試試。JitPack 從你的公開 git 倉庫中拉取程式碼,check out 最新的 release 程式碼,編譯並生成 artifact,最後將它釋出到它自己的 maven 庫中。
但是它和 local/remote 倉庫存在同樣的問題,要使用的話必須在根 build.gradle
中新增額外內容。
allprojects {
repositories {
...
maven { url 'https://www.jitpack.io' }
}
}複製程式碼
你可以從這兒瞭解該如何釋出你的安卓庫至 JitPack。
另一個選擇就是 Maven Central 或者 Jcenter。
我個人建議你使用 Jcenter,因為它有著完善的文件和良好的管理,同時它也是安卓專案的預設倉庫(除非誰改了預設選項)。
如果你釋出到 Jcenter,bintray 公司提供將庫同步到 Maven Central 的選項。一旦成功釋出到 Jcenter 上,在 build.gradle
中加上如下程式碼就可以很方便的使用了。
dependencies {
compile 'com.github.nisrulz:awesomelib:1.0'
}複製程式碼
你可以在這兒瞭解如何釋出你的安卓庫至 Jcenter。
基礎的東西說完了,現在讓我們來討論一下在編寫安卓庫的時候需要注意的問題。
避免多引數
每個安卓庫通常都需要用一些引數來進行初始化,為了達到這個目的,你可能會在建構函式或者新建一個 init 方法來接受這些引數。這麼做的時候請考慮以下問題
向 init() 方法傳遞超過 2-3 個引數會讓使用者感到頭大。 因為很難記住每個引數的用處和順序,這也為將 int 型資料傳給了 String 型別的引數之類的錯誤埋下了隱患。
// 不要這麼做
void init(String apikey, int refresh, long interval, String type);
// 這樣做
void init(ApiSecret apisecret);複製程式碼
ApiSecret
是一個實體類,定義如下
public class ApiSecret {
String apikey;
int refresh;
long interval;
String type;
// constructor
/* you can define proper checks(such as type safety) and
* conditions to validate data before it gets set
*/
// setter and getters
}複製程式碼
或者你可以使用 建造者模式
。
你可以閱讀這篇文章以瞭解更多建造者模式的知識。JOSE LUIS ORDIALES 在這篇文章裡深入討論了該如何在你的程式碼中實現建造者模式。
易用性
當構建你的安卓庫時,請關注庫的易用性和暴露出的方法,它們應該具有以下特點:
- 符合直觀
安卓庫中的程式碼做了些什麼都應該以某種形式反饋給使用者,可以是日誌輸出,也可以是檢視的變化,這根據庫的型別來決定。如果它做了一些難以理解的事,那麼對開發者來說這個庫就沒有起作用。你的程式碼應該按照使用者想的那樣來工作,即使使用者沒有檢視文件。
- 一致性
程式碼應該易於理解,同時避免在版本迭代的過程中發生劇烈的變化。遵循 sematic versioning。
- 易於使用,難以誤用
就實現與首次使用而言,它應該是易於理解的。暴露給使用者的方法應該經過充分的檢查以保證使用者只會用它幹它應該做的事情,避免方法被使用者錯誤使用。在某些需要用到的東西不存在的時候,提供合理的預設設定和處理方案。公開的方法應該經過充分的檢查以保證使用者不會。
簡而言之
簡單。
最小化許可權
在每個開發者都在向使用者申請很多的許可權時,你得停下來想一想你是不是真的需要這些額外的許可權。這一點尤其需要注意。
- 儘可能的請求更少的許可權。
- 使用 Intent 讓專用程式為你工作並返回結果。
- 基於你獲得的許可權啟用你的功能。避免因為許可權不足導致的崩潰。可以的話,在請求許可權之前先讓使用者知道你為什麼需要這些許可權。儘量在沒有獲得許可權的時候進行功能回退。
通過如下方式檢查是否具有某個許可權。
public boolean hasPermission(Context context, String permission) {
int result = context.checkCallingOrSelfPermission(permission);
return result == PackageManager.PERMISSION_GRANTED;
}複製程式碼
有些開發者可能會說他是真的需要某個特定許可權,在這種情況下該怎麼辦呢?庫程式碼應該對所有需要這個功能的應用是通用的。如果你需要某個危險許可權來獲取某些資料,而這些資料是庫的使用者可以提供的,那麼你就應該提供一個方法來接收這些資料。這種時候你就不應該強迫開發者去申請他不想申請的許可權了。當沒有許可權時,提供功能回退(無法達到但是儘量接近預期效果)的實現。
/* Requiring GET_ACCOUNTS permission (as a requisite to use the
* library) is avoided here by providing a function which lets the
* devs to get it on their own and feed it to a function in the
* library.
*/
MyAwesomeLibrary.getEmail("username@emailprovider.com");複製程式碼
最小化條件
現在,我們有一個功能需要裝置具有某種特性。通常我們會在 manifest 檔案中進行如下定義
<uses-feature android:name="android.hardware.bluetooth" />複製程式碼
當你在安卓庫程式碼中這麼寫的時候問題就來了,它會在構建的過程中與應用的 manifest 檔案合併,並導致那些沒有藍芽功能的裝置無法從 Play 商店中下載它。這樣會導致之前對大部分使用者可見的 app 此時卻僅僅對一部分使用者可見,就只是因為引用了你的庫。
這可不是我們想要的。所以我們得解決它。不要在 manifest 檔案中寫 uses-feature,在執行時檢查是否有這個功能
String feature = PackageManager.FEATURE_BLUETOOTH;
public boolean isFeatureAvailable(Context context, String feature) {
return context.getPackageManager().hasSystemFeature(feature);
}複製程式碼
這種方式就不會引起 Play 商店的過濾。
作為一個額外功能提供是當這個功能不可用時在庫程式碼中不去呼叫相關方法或者使用替代的回撥方法。這對於庫的開發者和使用者來說是一種雙贏的局面。
多版本支援
現在到底有多少種版本?
如果你的庫中存在只能在特定版本中執行的程式碼,你應該在低版本的裝置中禁用這些程式碼。
一般的做法是通過定義 minSdkVersion
和 targetSdkVersion
來指定支援版本。你應在在程式碼中檢查版本,來決定是否啟動某個功能,或者提供回退。
// Method to check if the Android Version on device is greater than or equal to Marshmallow.
public boolean isMarshmallow(){
return Build.VERSION.SDK_INT>= Build.VERSION_CODES.M;
}複製程式碼
不要在正式版中輸出日誌
就是不要這麼做。
幾乎每次被要求去測試一個應用或者 Android Library 工程時我都會發現他們把所有在日誌裡輸出了所有東西,這可是釋出版啊。(譯註:在正式版中列印日誌是不必要的,可能影響效能,還可能帶來安全問題)
根據經驗,永遠不要在正式版中輸出日誌。你應該配合使用 build-variants 和 timber 來實現釋出版和除錯版中的不同日誌輸出。一個更簡單的解決方案是提供一個 debuggable
標誌位來讓開發者設定以開關安卓庫中的日誌輸出。
// In code
boolean debuggable = false;
MyAwesomeLibrary.init(apisecret,debuggable);
// In build.gradle
debuggable = true複製程式碼
發生錯誤的時候讓使用者知道
經常有開發者不在日誌裡輸出錯誤和異常資訊,我遇到過很多次這種情況。這讓安卓庫的使用者在除錯的過程中感到十分的頭疼。雖然上面說了不要在釋出版中輸出日誌,但是你得理解無論是在釋出版還是除錯版中錯誤和異常資訊都需要輸出。如果你真的不願意在釋出版中輸出,至少在初始化的時候提供一個方法來讓使用者啟用日誌。
void init(ApiSecret apisecret,boolean debuggable){
...
try{
...
}catch(Exception ex){
if(debuggable){
// This is printed only when debuggable is true
ex.printStackTrace();
}
}
....
}複製程式碼
當你的安卓庫崩潰的時候要立刻向使用者顯示異常,而不是掛起並做一些處理。避免寫一些會阻塞主程式的程式碼。
當發生錯誤時及時退出並禁用功能
我的意思是當你的程式碼掛掉後,嘗試進行檢查和處理,從而使這些有問題的程式碼僅僅會導致你提供的庫中的一些功能被禁用而不是讓整個APP崩潰。
捕獲特定的異常
接上一條建議,你可以看到上面那段程式碼裡我使用了 try-catch 語句。Catch 語句只是簡單的捕獲了所有的 Exception
。一個異常與另一個異常之間並沒有什麼太大的區別。因此,必須要根據手頭的需求捕獲特定型別的異常。比如:NULLPointerException
, SocketTimeoutException
, IOException
等等。
對網路狀況差的情況進行處理
這很重要,嚴肅點!
如果你的安卓庫需要進行網路請求,一個很容易忽視的情況就是網速較慢或者請求無相應。
據我觀察,開發者總會假設網路暢通。舉個例子吧,你的安卓庫需要從伺服器上獲取配置檔案來進行初始化。如果你忽略了在網路狀態差的時候沒法下載配置檔案,那麼你的程式碼就可能因為獲取不了配置檔案而崩潰。如果你進行了網路狀態檢查並進行處理,那麼就能為你的庫的使用者省很多事。
儘可能的批量處理你的網路請求,避免多次請求。這能夠節省很多電量,再看下這個。
通過將 JSON 與 XML 轉成 Flatbuffers 來節省資料傳輸量。
避免將大型庫作為依賴
這一點不需要太多的解釋。就像安卓開發者都知道的那樣,一個安卓應用最多隻能有 65k 方法。如果你依賴了一個大型的庫,那麼會對使用你的庫的應用帶來兩個不期望的影響。
- 你會讓應用的方法數將會大大增加,即使你的庫只有很少一些方法,但是你依賴的庫中的方法也被算上了。
- 如果因為引入你的庫而導致方法數達到了 65k,那麼應用開發者不得不去使用 multi-dex。相信我,沒人想用 multi-dex 的。
在這種情況下,為了解決一個問題你引入了一個更大的問題,你的庫的使用者將會轉而去使用別的庫。
避免引用不是必需的庫
我覺得這應該時一條大家都知道的規則了,是不是?不要讓你的安卓庫因為引入了不需要的庫而膨脹。但是需要注意的是即使你需要依賴,讓你的使用者傳遞性地下載這些依賴(因為用了你的庫而不得不去下載另一個庫)。比如,那些沒有和你的庫繫結的依賴。
那麼現在的問題就是如果沒有和我們的庫繫結那麼我們如何去使用它?
答案很簡單,要求使用者在編譯的時候提供你需要的依賴。可能不是每個使用者都需要這個依賴提供的方法,對於這些使用者來說,如果你找不到這些依賴,你只需要禁用某些方法就行了。對於那些需要的使用者,它們會在 build.gradle
提供依賴。
如何實現它? 檢查 classpath
private boolean hasOKHttpOnClasspath() {
try {
Class.forName("com.squareup.okhttp3.OkHttpClient");
return true;
} catch (ClassNotFoundException ex) {
ex.printStackTrace();
}
return false;
}複製程式碼
接下來,你可以使用 provided
(Gradle v2.12 或更低)或者 compileOnly
(Gradle v2.12+)(閱讀完整內容),以便在編譯時獲取依賴庫內定義的類。
dependencies {
// for gradle version 2.12 and below
provided 'com.squareup.okhttp3:okhttp:3.6.0'
// or for gradle version 2.12+
compileOnly 'com.squareup.okhttp3:okhttp:3.6.0'
}複製程式碼
還有要注意的是,只有當依賴是單純的 Java 依賴的時候你才能使用這種控制依賴的方法。比如,如果你在編譯時引入安卓庫,你就沒法引用它的依賴庫或者資原始檔,這些都必須在編譯前被加入。只有依賴是一個純 Java 依賴(僅僅由 Java 類組成)時,才可以通過在編譯的過程中加入 ClassPath 來使用。
不要阻塞啟動過程
沒開玩笑
我指的不要應用一啟動就立刻初始化你的安卓庫。這麼做會降低應用的啟動速度,即使應用什麼都沒做就只是初始化了你的庫。
解決辦法是不要在主執行緒裡進行初始化工作,可以新建一個執行緒,更好的辦法是使用 Executors.newSingleThreadExecutor()
讓執行緒數量保持唯一。
另一個解決辦法是根據需要初始化你的安卓庫,比如只有在使用到的時候載入/初始化它們。
優雅地移除方法和功能
不要在版本迭代的過程中移除 public
方法,這會導致使用你的庫的應用無法使用,而開發者並不知道什麼導致了這個問題。
解決方案:使用 @Deprecated
來標註方法並給出在未來版本的棄用計劃。
使你的程式碼可測試
確定你的程式碼裡有測試例項,這不是一個規則,而是一個常識,你應該在你的每一個應用和庫中這麼做。
使用 Mock 來測試你的程式碼,避免 final 類,不要有靜態方法等等。
基於介面編寫你的 public API 使你的安卓庫能交換實現,反過來讓你的程式碼可測試,比如,在測試的時候,你可以很容易地提供 mock 實現。
為每一個東西編寫文件
作為安卓庫的建立者你很瞭解你的程式碼,但是使用者不會很瞭解,除非你讓他們去閱讀你的程式碼(而你永遠也不應該這麼做)。
編寫文件,包括使用時的每個細節,你實現的每個功能。
- 建立一個
Readme.md
檔案並將其放在庫的根目錄下。 - 為程式碼裡所有
public
寫javadoc
註釋。它們應該包括
public
方法的目的傳入的引數
返回的資料
- 提供一個示例應用來演示這個庫的功能以及如何使用。
- 確定你有一個詳細的修改日誌。放在
release
記錄裡的特殊的版本 tag 裡都比較合適。
GitHub 裡 Sensey 庫的 Release 部分截圖
這是 Sensey 的 release 連結
提供一個極簡的示例應用
這都不用說了。始終提供一個最簡潔的示例程式,這是開發者在學習使用你的庫的過程中接觸的第一個東西。它越簡單就越好理解。讓這個程式看起來花哨或者把示例程式碼寫得很複雜只會背離它最初的目的,它只是一個如何使用庫的例子。
考慮加一個 License
很多時候開發者都忘了 License 這部分。這是別人決定要不要採納你的庫的一個因素。
如果你決定使用一種帶限制的協議,比如 GRL,這意味著無論誰只要修改了你的程式碼那他必須要將修改提交到你的程式碼庫中。這樣的限制阻礙了安卓庫的使用,開發者傾向於避免使用這樣的程式碼庫。
解決辦法是使用諸如 MIT 或者 Apache 2 這樣更為開放的協議。
在這個簡單的網站閱讀有關協議的知識,以及關於你的程式碼需要的 copyright。
最後,獲取反饋
是的,你聽到了!
起初,你的安卓庫是用來滿足自己的需求的。一旦你釋出出去讓別人用,你將會發現大量的問題。從你的庫的使用者那裡聽取意見收集反饋。基於這些意見在保持原有目的不變的情況下考慮增加新的功能和修復一些問題。
總結
簡而言之,你需要在編碼過程中注意以下幾點
- 避免多引數
- 易用
- 最小化許可權
- 最小化前置條件
- 多版本支援
- 不要在釋出版中列印日誌
- 在崩潰的時候給使用者反饋
- 當發生錯誤時及時退出並禁用功能
- 捕獲特定異常
- 處理網路不良的情況
- 避免依賴大型庫
- 除非特別需要,不要引入依賴
- 避免阻塞啟動過程
- 優雅地移除功能和特性
- 讓程式碼可測試
- 完善的文件
- 提供極簡的示例應用
- 考慮加個協議
- 獲取反饋
根據經驗,你的庫應該依照 SPOIL 原則
簡單(Simple)—— 簡潔而清晰的表達
目的(Purposeful)—— 解決問題
開源(OpenSource)—— 自由訪問,免費協議
習慣(Idiamatic)—— 符合正常使用習慣
邏輯(Logical) —— 清晰有理
我在曾經某個時候從某位作者的演示裡看到這個,但我想不起來他是誰了。因為它很有意義並以很簡潔的方式提供了圖片所以當時我記了筆記。如果你知道他是誰,在下面評論,我會將他的連結加上。
最後的思考
我希望這篇部落格給那些正在開發更好的安卓庫的開發者們帶來幫助。安卓社群從開發者每天釋出的庫中獲得了很大的益處。如果每個人都開始注意他們 API 設計,學會為使用者(其他的安卓開發者)考慮,我們將會迎來一個更好的生態。
這個教程是基於我開發安卓庫的經驗。我很想知道你關於這些觀點的意見。歡迎留下評論。
如果你有什麼建議或者想讓我加一些內容,請讓我知道。
Till then keep crushing code ?
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、React、前端、後端、產品、設計 等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃。