開發第三方庫最佳實踐

本文會不定期更新,推薦watch下項目。如果喜歡請star,如果覺得有紕漏請提交issue,如果你有更好的點子可以提交pull request。
本文的示例代碼主要是基于作者的經驗來編寫的,若你有其他的技巧和方法可以參與進來一起完善這篇文章。

本文固定連接:https://github.com/tianzhijiexian/Android-Best-Practices

一、需求背景

目前技術圈的人或多或少都開發過庫項目,無論是因為要靠它來找工作,還是通過其進行學習交流,亦或是借此來招搖撞騙,總之開發第三方庫這件事已經變得越來越流行了。
我個人是很討厭技術圈娛樂化的,star那么多,issue沒人管的現象比比皆是。其實解決issue才能促使實踐者更快的學習,僅僅炫耀自己的star數毫無意義。因此,我便寫了我開發第三方庫的經驗,希望本文能幫助到大家。

二、需求

我認為一個好的庫作者應該是能滿足以下需求的:

  • 避免重復造輪子
  1. 謹慎設計API
  2. 避免引入其他庫
  3. 盡量用注解代替枚舉
  4. 資源文件加上特殊前綴
  5. 提供可插拔依賴的方案
  6. 將Manifest中的參數變量化
  7. 有多個相關依賴,做聚合依賴
  8. 根據需求考慮是否提供no-op
  9. 僅僅在debug模式中引入代碼
  10. 使用JitPack做庫的托管倉庫
  11. 嚴格限制庫的大小和方法數
  12. 快速解決issue,多和提問者溝通
  13. 不斷完善,堅持更新

三、實現

避免重復造輪子

當你產生了一個新奇的想法,想要實現它之前請用一天的時間去分析自己想法的優缺點,然后去Github上搜索有沒有類似的庫,或者是通過群組來詢問相關信息。你必須知道,當你提出一個想法的時候,別人很可能也已經想過了,差別就在于別人是否已經實現。如果實在沒有找到和你要做的庫類似的東西,那么就開始干吧!

如果有現成的,那么我就不做
如果你搜索到github上有個和你想做的庫類似的東西,你完全可以了解其實現后拿來就用,這會節約你很多的時間。我自己寫過一個Gradle插件,當我寫的差不多的時候我突然意識到github上可能有現成的,所以我立刻停止了開發,進行搜索。果真找到了一個十分類似的庫,閱讀源碼后發現其思路和我幾乎完全一致。

如果現成的不夠好,我可以讓它變的更好
當我們搜索到了一個和自己想法類似的庫后,很可能會發現它和自己的想法有些差異,或者是有些bug。這時候千萬不要呵呵一聲,然后自己開始狂妄的寫代碼。我更加希望的是通過issue聯系到作者,提出問題,如果可能的話給出自己的解決方案和pr。我們的時間很寶貴,為何不花時間來維護同一個東西呢。

material design library

如果現成的太糟,由我來讓其脫胎換骨
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)

前綴-dd

除了layout文件,color等也應該注意,庫作者多注意這些細節點,會給使用者省去很多麻煩,減少不必要的沖突和問題。

在access文件中也應該建立一個子目錄:

Paste_Image.png

這樣可以防止多個庫用了同一個同一個資源,然后產生覆蓋的問題。

提供可插拔依賴的方案

我們制作的庫很可能會用到回調,我希望給已經使用了rxjava項目的使用者rxjava的回調,給沒有使用rxjava的用戶提供默認的接口回調。

首先,通過provided來依賴rxjava:

provided 'io.reactivex:rxjava:1.1.3'

然后在代碼中提供使用rxjava和傳統的兩種方法:


rxjava

這樣使用者就可以選擇性的使用不同的方法來接收回調了。

將Manifest中的參數變量化

在制作第三方登錄、分享的SDK時,我發現需要在manifest中定義一些key,但是我不希望寫死,而是交由使用者進行填寫。因此我將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就會自動合并成下圖:


merge

值得注意的是${applicationId}是一個默認的變量,隨著實際項目中的參數而定,所以在需要在manifest中指定具體包名的時候可以采取如下方式:

${applicationId}

實際項目中強烈建議把這個值定寫成包名:

applicationId

有多個相關依賴,做聚合依賴

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的類:


Paste_Image.png
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

appbar

我們可以通過tag和commit進行庫版本的選擇,選擇完畢后就可以看到依賴的方式:

compile

jitpack讓我們提交庫變得簡單快速,但需要注意它并不能支持多個module的庫,這是一個劣勢。

jitpack還提供了java文檔的在線瀏覽,如果你的庫需要提供文檔支持,那么它絕對是一個很好的選擇。


jitpack doc.png

配置的方式是在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上后我們就可以在線瀏覽文檔了。

doc.png

如果你想詳細了解jitpack,jitpack的官方doc寫得很清楚,查看即可。

Tips

下面說個小的tips,我們希望使用者可以明確知道當前工程最新的版本是多少,但是每次手動改readme很麻煩,jitpack可以通過插入link的方式來自動獲得jitpack上的最新版本。進入https://jitpack.io/#tianzhijiexian/UIBlock/58d865ecbd 選擇完版本后就可以看到最下方的提示了。

link

最后將svg粘貼到readme開頭就可以了,這個標簽還可以讓我們快速從工程跳轉到jitpack,十分實用。

readme

嚴格限制庫的大小和方法數

以我自身的經驗,一個小型庫的方法數不應該超過300,所以需要時刻留意自己是否在做一個單一功能的庫。這個300自然不是權威指標,我只是希望庫開發者應該盡可能讓自己的庫輕量干凈,減少使用者引入庫的負擔。一個第三方庫的方法數和大小都是使用者會考慮的點,所以我推薦使用:MethodsCount來進行庫方法數目的檢測:

gson

我們還可以通過圖表來量化自己庫的方法數和大小,下面就是ShareLoginLib的走勢圖:

chat.png

MethodsCount還提供了as插件以便于我們了解自己依賴的庫大小,安裝后的效果如下:


plugin

也可以采用谷歌推薦的方式進行依賴關系的檢測(不常用):

dependencies

支持SourceGraph,讓使用者可以快速瀏覽項目代碼

Github一個不好的地方就是代碼是不能相互跳轉的,所以閱讀起來很累,如果我要引入一個庫,那么就必須clone下來然后通過idea打開才行。這樣的流程對于庫的前期調研來說成本很高,所以我希望利用SourceGraph讓在線閱讀代碼的體驗提升一個量級。
一個簡單的演示:

souce.gif

你在安裝完SourceGraph的Chrome插件后,就會發現支持SourceGraph的代碼上方就會顯示一個icon。

icon.png

現在,你就可以利用sourcegraph進行跳轉和插件文檔了。

Paste_Image.png

想要體驗更多,可以瀏覽:
https://github.com/tianzhijiexian/CommonAdapter/blob/master/adapter/src/main/java/kale/adapter/BasePagerAdapter.java

快速解決issue,多和提問者溝通

一個優秀的開源庫自然要經歷很多issue,作為庫開發者需要對issue有一定的敏感度,不要因為自己太忙而放任不管。

issue

我分享下我的做法:

  • 如果提問者是理解上的問題,可以在解答后更新到ReadMe中,以防止別人有同樣的疑問。
  • 如果是小的bug,那么最好快速修復,并且由提交者驗證問題,驗證后由提交者關閉issue。
  • 如果是難以解決的bug,或最近自己沒有時間,那么應快速告知提問者,說明情況。

不斷完善,堅持更新

完善和維護一個庫確實需要很大的精力,如果你的庫是真的希望給別人用的,那么就應該能有付出時間和精力的準備。因為你既然做了這件事,那么就需要為此負責。我平時也非常忙,但是我還是努力地做著這些事情,所以我相信你也可以的!

developer_kale@foxmail.com
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,663評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,125評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,506評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,614評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,402評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,934評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,021評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,168評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,690評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,596評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,288評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,027評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,404評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,662評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,398評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,743評論 2 370

推薦閱讀更多精彩內容