本文會不定期更新,推薦watch下項目。如果喜歡請star,如果覺得有紕漏請提交issue,如果你有更好的點子可以提交pull request。
本文的示例代碼主要是基于作者的經驗來編寫的,若你有其他的技巧和方法可以參與進來一起完善這篇文章。
本文固定連接:https://github.com/tianzhijiexian/Android-Best-Practices
一、需求背景
目前技術圈的人或多或少都開發過庫項目,無論是因為要靠它來找工作,還是通過其進行學習交流,亦或是借此來招搖撞騙,總之開發第三方庫這件事已經變得越來越流行了。
我個人是很討厭技術圈娛樂化的,star那么多,issue沒人管的現象比比皆是。其實解決issue才能促使實踐者更快的學習,僅僅炫耀自己的star數毫無意義。因此,我便寫了我開發第三方庫的經驗,希望本文能幫助到大家。
二、需求
我認為一個好的庫作者應該是能滿足以下需求的:
- 避免重復造輪子
- 謹慎設計API
- 避免引入其他庫
- 盡量用注解代替枚舉
- 資源文件加上特殊前綴
- 提供可插拔依賴的方案
- 將Manifest中的參數變量化
- 有多個相關依賴,做聚合依賴
- 根據需求考慮是否提供no-op
- 僅僅在debug模式中引入代碼
- 使用JitPack做庫的托管倉庫
- 嚴格限制庫的大小和方法數
- 快速解決issue,多和提問者溝通
- 不斷完善,堅持更新
三、實現
避免重復造輪子
當你產生了一個新奇的想法,想要實現它之前請用一天的時間去分析自己想法的優缺點,然后去Github上搜索有沒有類似的庫,或者是通過群組來詢問相關信息。你必須知道,當你提出一個想法的時候,別人很可能也已經想過了,差別就在于別人是否已經實現。如果實在沒有找到和你要做的庫類似的東西,那么就開始干吧!
如果有現成的,那么我就不做
如果你搜索到github上有個和你想做的庫類似的東西,你完全可以了解其實現后拿來就用,這會節約你很多的時間。我自己寫過一個Gradle插件,當我寫的差不多的時候我突然意識到github上可能有現成的,所以我立刻停止了開發,進行搜索。果真找到了一個十分類似的庫,閱讀源碼后發現其思路和我幾乎完全一致。
如果現成的不夠好,我可以讓它變的更好
當我們搜索到了一個和自己想法類似的庫后,很可能會發現它和自己的想法有些差異,或者是有些bug。這時候千萬不要呵呵一聲,然后自己開始狂妄的寫代碼。我更加希望的是通過issue聯系到作者,提出問題,如果可能的話給出自己的解決方案和pr。我們的時間很寶貴,為何不花時間來維護同一個東西呢。
如果現成的太糟,由我來讓其脫胎換骨
Github上有很多很多作者,那么自然就產生了社會性。我的提交和留言很可能被作者無視,或者作者早就轉行了,他的項目等于死了。遇到這樣的情況,我的做法是frok人家的代碼,然后自己開始維護。DebugDrawer就是一個例子。他原本的作者已經很久不更新了,而且issue也沒人回復,不得已的情況下我只能自己維護了。自己拿來維護可以,但一定要記得fork人家的代碼,你要時刻記住,你是踩在前人的肩膀上,不要狂妄。
謹慎設計API
如果你的庫是給人用的,那么請在設計api層面多花點時間。因為一旦有人用了你的庫,你就有了歷史負擔。如果你后期隨意地改變方法名和參數,使用者會來打你的,所以經驗就很重要了。我可以簡單給出一個建議:把內部類寫到參數靠后的位置,把context放在參數的前部:
public interface Test{
// context在前,內部類在后
void test(Context context, View.OnClickListener listener);
}
如果后期實在要改名字和廢棄方法,可以采用@Deprecated
來做標記,把要變動的東西先標記為廢棄,過了幾個版本后再刪除掉。
避免引入其他庫
你的代碼本身就是一個庫了,因此我強烈不建議你的代碼還引入別人的庫。友盟推送的代碼就是一個典型的反例,一個推送庫引入了okhttp、okio等其他庫,臃腫不堪,完全沒有讓我使用的欲望。
一個第三方庫引入其他第三方庫有很多壞處,使用者可能會遇到版本沖突的問題(比如:友盟反饋和友盟推送同時使用),方法數還會極速增多。
你可能會說appcompat
這個庫基本所有第三方庫都會引入的,有沒有什么好的辦法可以避免呢?好在我們有provided
關鍵字。provided
可以將你需要的庫引入,但是并不會將其打包到aar里面以CommonAdapter為例:
dependencies {
provided 'com.android.support:recyclerview-v7:23.2.1'
provided 'com.android.databinding:baseLibrary:1.0'
provided "org.projectlombok:lombok:1.12.6"
}
CommonAdapter依賴了三個庫,但是都用了私有依賴的方式來做的。
首先,我能確定使用這個庫的人,肯定使用了recyclerView,所以我通過私有依賴的方式將recyclerView的代碼剔除,那么recyclerView的最終版本由使用者來定。
其次,如果使用者的項目使用了DataBinding這個庫,那么可以采用數據綁定的形式來做界面的更新操作,但我并非強制使用者必須依賴db,所以我也將db的庫剔除,并且在代碼里做了這樣的判斷:
public class DataBindingJudgement {
public static final boolean SUPPORT_DATABINDING;
static {
boolean hasDependency;
try {
Class.forName("android.databinding.ObservableList");
hasDependency = true;
} catch (ClassNotFoundException e) {
hasDependency = false;
}
SUPPORT_DATABINDING = hasDependency;
}
}
public CommonAdapter(@Nullable List<T> data, int viewTypeCount) {
if (DataBindingJudgement.SUPPORT_DATABINDING && data instanceof ObservableList) {
// 判斷是否有db的依賴
((ObservableList<T>) data).addOnListChangedCallback(new ObservableList.OnListChangedCallback<ObservableList<T>>() {
@Override
public void onChanged(ObservableList<T> sender) {
notifyDataSetChanged();
}
});
}
//...
}
上面的代碼的意思是如果你用了db,那么commonAdapter就支持了數據綁定,如果沒有用到db,也不影響,還可以用傳統方式來做。
最后,我為了增加代碼可維護性,我引入了lombok。無論使用者是否用了lombok,都和我無關,這種情況采用provided的方式也是最合理的。
盡量用注解代替枚舉
通過注解代替枚舉可以減少內存開銷,并且在AS越來越智能的提示下,編碼方式和枚舉幾乎一致。以ShareLoginLib為例,我在編碼的時候會用注解來進行參數的表示,這里一般會使用有限值的int來做。
@Retention(RetentionPolicy.SOURCE)
@IntDef({ContentType.TEXT, ContentType.PIC, ContentType.WEBPAGE, ContentType.MUSIC})
public @interface ContentType {
int TEXT = 1, PIC = 2, WEBPAGE = 3, MUSIC = 4;
}
但是如果你需要將注解暴露給使用者,那么我推薦采用string的形式來做,因為string的值不會像int那樣隨著打包而改變,此外string有很高的可讀性。在AS目前還沒智能識別變量的情況下,我強烈建議用有限值的String來代替枚舉。
@Retention(RetentionPolicy.SOURCE)
@StringDef({LoginType.WEIXIN, LoginType.WEIBO, LoginType.QQ})
public @interface LoginType {
String WEIXIN = "WEIXIN", WEIBO = "WEIBO", QQ = "QQ";
}
給資源文件加上特殊前綴
第三方庫的資源會和使用者的項目進行合并,資源的名字需要特殊注意,以DebugDrawer為例,我在layout資源前面都會加特殊的前綴(debugDrawer->dd)
除了layout文件,color等也應該注意,庫作者多注意這些細節點,會給使用者省去很多麻煩,減少不必要的沖突和問題。
在access文件中也應該建立一個子目錄:
這樣可以防止多個庫用了同一個同一個資源,然后產生覆蓋的問題。
提供可插拔依賴的方案
我們制作的庫很可能會用到回調,我希望給已經使用了rxjava項目的使用者rxjava的回調,給沒有使用rxjava的用戶提供默認的接口回調。
首先,通過provided來依賴rxjava:
provided 'io.reactivex:rxjava:1.1.3'
然后在代碼中提供使用rxjava和傳統的兩種方法:
這樣使用者就可以選擇性的使用不同的方法來接收回調了。
將Manifest中的參數變量化
在制作第三方登錄、分享的SDK時,我發現需要在manifest中定義一些key,但是我不希望寫死,而是交由使用者進行填寫。因此我將key變量化:
使用的時候只需要在gradle中進行如下配置即可:
android {
compileSdkVersion 23
buildToolsVersion '23.0.2'
defaultConfig {
applicationId "com.liulishuo.engzo"
minSdkVersion 15
targetSdkVersion 23
manifestPlaceholders = [
// 這里需要換成:tencent+你的AppId
"tencentAuthId": "tencent123456",
]
}
}
最終打包生成的manifest就會自動合并成下圖:
值得注意的是${applicationId}是一個默認的變量,隨著實際項目中的參數而定,所以在需要在manifest中指定具體包名的時候可以采取如下方式:
實際項目中強烈建議把這個值定寫成包名:
有多個相關依賴,做聚合依賴
DebugDrawer有多個用來debug的庫,使用者可以根據選擇進行依賴。但是這些庫都是和DebugDrawer密切相關的,所以開發者應該建議使用者將他們應該一并依賴進來,這樣以后刪除庫的時候也很方便。
debugCompile(["com.github.tianzhijiexian:DebugDrawer:1.0.0",
"jp.wasabeef:takt:1.0.1",
"com.jakewharton.scalpel:scalpel:1.4.6"
])
根據需求考慮是否提供no-op
如果你開發的庫可能只需要在debug時才用到,但庫提供的類或方法需要寫入現有的代碼中,那么就可以采用no-op的方案。所謂no-op就是希望某些代碼僅僅存在于debug環境中,在release版本中,可能就是保留了了一些代碼接口,但是并不提供實現。
以leakcanary為例,它的文檔中就給出了no-op的依賴。
dependencies {
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.4-beta2'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta2'
testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta2'
}
我舉一個no-op的例子:
// debug版依賴的代碼
public void doSomeThing(){
int i = 1,sum;
for(int i =0;i<100000;i++){
sum += i;
}
// 省略一千行代碼
}
// release版依賴的代碼
public void doSomeThing(){
// no-op
}
no-op的庫僅僅提供了一個方法殼,讓不需要出現再release包中的代碼消失。
僅僅在debug模式中引入代碼
如果你的庫代碼僅僅需要出現在debug模式中,并且對于使用者現有的代碼沒任何影響,那么你可以建議使用你的庫的人通過debugCompile
進行依賴。
以stetho為例,我先將其用debug模式進行依賴。
debugCompile "com.facebook.stetho:stetho:1.3.1"
然后在src下建立debug/java的目錄,接著建立一個DebugApplication的類:
public class DebugApplication extends ReleaseApplication {
@Override
public void onCreate() {
super.onCreate();
Stetho.initialize(
Stetho.newInitializerBuilder(this)
.enableDumpapp(Stetho.defaultDumperPluginsProvider(this))
.enableWebKitInspector(
Stetho.defaultInspectorModulesProvider(this)).build());
}
}
最后,在manifest文件中進行application的替換:
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
>
<application
android:name=".DebugApplication"
android:allowBackup="true"
android:icon="@drawable/debug_icon"
tools:replace="android:name,android:icon"
/>
</manifest>
這樣我們就會在debug時自動用debugApplication作為application對象,并且使用里面stetho的代碼,在release版本中還是采用ReleaseApplication
,以此來減少無用代碼的引入。
使用JitPack做庫的托管倉庫
我們的代碼大多都是存在github上面,jitpack可以快速將你的github項目變成可以被使用者進行依賴的庫。
這是我的一個庫的例子:https://jitpack.io/#tianzhijiexian/AppBar
我們可以通過tag和commit進行庫版本的選擇,選擇完畢后就可以看到依賴的方式:
jitpack讓我們提交庫變得簡單快速,但需要注意它并不能支持多個module的庫,這是一個劣勢。
jitpack還提供了java文檔的在線瀏覽,如果你的庫需要提供文檔支持,那么它絕對是一個很好的選擇。
配置的方式是在lib的build.gradle中添加如下代碼:
// build a jar with source files
task sourcesJar(type: Jar) {
from android.sourceSets.main.java.srcDirs
classifier = 'sources'
}
task javadoc(type: Javadoc) {
failOnError false
source = android.sourceSets.main.java.sourceFiles
classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
classpath += configurations.compile
}
javadoc {
options {
encoding "UTF-8"
charSet 'UTF-8'
author true
version true
links "http://docs.oracle.com/javase/7/docs/api"
}
}
// build a jar with javadoc
task javadocJar(type: Jar, dependsOn: javadoc) {
classifier = 'javadoc'
from javadoc.destinationDir
}
artifacts {
archives sourcesJar
archives javadocJar
}
接著把代碼push到github上后我們就可以在線瀏覽文檔了。
如果你想詳細了解jitpack,jitpack的官方doc寫得很清楚,查看即可。
Tips
下面說個小的tips,我們希望使用者可以明確知道當前工程最新的版本是多少,但是每次手動改readme很麻煩,jitpack可以通過插入link的方式來自動獲得jitpack上的最新版本。進入https://jitpack.io/#tianzhijiexian/UIBlock/58d865ecbd 選擇完版本后就可以看到最下方的提示了。
最后將svg粘貼到readme開頭就可以了,這個標簽還可以讓我們快速從工程跳轉到jitpack,十分實用。
嚴格限制庫的大小和方法數
以我自身的經驗,一個小型庫的方法數不應該超過300,所以需要時刻留意自己是否在做一個單一功能的庫。這個300自然不是權威指標,我只是希望庫開發者應該盡可能讓自己的庫輕量干凈,減少使用者引入庫的負擔。一個第三方庫的方法數和大小都是使用者會考慮的點,所以我推薦使用:MethodsCount來進行庫方法數目的檢測:
我們還可以通過圖表來量化自己庫的方法數和大小,下面就是ShareLoginLib的走勢圖:
MethodsCount還提供了as插件以便于我們了解自己依賴的庫大小,安裝后的效果如下:
也可以采用谷歌推薦的方式進行依賴關系的檢測(不常用):
支持SourceGraph,讓使用者可以快速瀏覽項目代碼
Github一個不好的地方就是代碼是不能相互跳轉的,所以閱讀起來很累,如果我要引入一個庫,那么就必須clone下來然后通過idea打開才行。這樣的流程對于庫的前期調研來說成本很高,所以我希望利用SourceGraph讓在線閱讀代碼的體驗提升一個量級。
一個簡單的演示:
你在安裝完SourceGraph的Chrome插件后,就會發現支持SourceGraph的代碼上方就會顯示一個icon。
現在,你就可以利用sourcegraph進行跳轉和插件文檔了。
想要體驗更多,可以瀏覽:
https://github.com/tianzhijiexian/CommonAdapter/blob/master/adapter/src/main/java/kale/adapter/BasePagerAdapter.java
快速解決issue,多和提問者溝通
一個優秀的開源庫自然要經歷很多issue,作為庫開發者需要對issue有一定的敏感度,不要因為自己太忙而放任不管。
我分享下我的做法:
- 如果提問者是理解上的問題,可以在解答后更新到ReadMe中,以防止別人有同樣的疑問。
- 如果是小的bug,那么最好快速修復,并且由提交者驗證問題,驗證后由提交者關閉issue。
- 如果是難以解決的bug,或最近自己沒有時間,那么應快速告知提問者,說明情況。
不斷完善,堅持更新
完善和維護一個庫確實需要很大的精力,如果你的庫是真的希望給別人用的,那么就應該能有付出時間和精力的準備。因為你既然做了這件事,那么就需要為此負責。我平時也非常忙,但是我還是努力地做著這些事情,所以我相信你也可以的!