實際生產(chǎn)中有關(guān)于 Android SDK開發(fā)的總結(jié)

1、開篇與設(shè)計

一、前言

在本章中你可以了解到如何設(shè)計一個SDK,定義與第三方交互的數(shù)據(jù)結(jié)構(gòu)、數(shù)據(jù)預(yù)處理。了解到SDK與APP交互的通訊機制,包括整個流程的交互邏輯設(shè)計,規(guī)避信任問題。更能從這些設(shè)計中了解到對應(yīng)的通用技術(shù)要點與設(shè)計思路。

二、SDK的定義與場景

2.1、SDK的定義


SDK全稱 Software Development Kit,廣義上的SDK 是為特定的軟件包、軟件框架、硬件平臺、操作系統(tǒng)等建立應(yīng)用程序時所使用的開發(fā)工具的集合;狹義上的 SDK 則是基于系統(tǒng)基礎(chǔ)組件進行二次開發(fā)封裝的、獨立的、能夠完成特定功能并返回相關(guān)數(shù)據(jù)的一組工具的集合。

說人話,SDK一般就是我們常見的jar包、so庫、aar包,大到一套JDK,小到只有一個方法的jar包,都可以稱之為SDK。SDK的本質(zhì)是一系列方法、邏輯的集合,對資源與API封裝后的產(chǎn)物。

2.2、SDK的場景

SDK的定義我們了解了,那么SDK在行業(yè)內(nèi)的場景有哪些呢?SDK廣泛存在于2B產(chǎn)品中(此處禁止聯(lián)想...)。

如推送SDK,支付SDK,地圖SDK、OCR-SDK、人臉識別SDK、游戲SDK等等。標準化的、可復(fù)用的SDK產(chǎn)品,廣泛提高了中小型公司開發(fā)APP的效率。

三、SDK接口設(shè)計

設(shè)計一個SDK,有兩個明確的原則貫穿始終:

一是:最小可用性原則,即用最少的代碼,如無必要勿增實體;
二是:最少依賴性原則,即用最低限度的外部依賴,如無必要勿增依賴。

首先我們需要明確一下這個SDK的職責(zé)與邊界,定義與宿主App的交互參數(shù)。即SDK接收什么?輸出什么?
舉個例子:



關(guān)鍵要點:

出入?yún)⒍加幸粋€token,用于本次調(diào)用的關(guān)聯(lián)與憑證。
前端SDK設(shè)計入?yún)r,應(yīng)當盡量減少前端參數(shù)交互,相關(guān)參數(shù)盡量在獲取token時傳入后端服務(wù),以此保持SDK接口調(diào)用的簡潔性與調(diào)用的靈活性。

3.1、出入?yún)⒃O(shè)計

大多數(shù)情況下,出入?yún)⑿枰换ザ鄠€字段。那么就產(chǎn)生了數(shù)據(jù)組裝的問題。

因此,必須設(shè)計一個便捷、清晰的數(shù)據(jù)結(jié)構(gòu)。

SDK出入?yún)⒈举|(zhì)上是交互一組鍵值對(Key-Value),因此可以考慮的方式有:

3.1.1、傳遞一個Map

生成一個Map,并設(shè)置對應(yīng)的Key-Value??尚?,但缺點是暴露了過多生成參數(shù)的細節(jié)與內(nèi)部邏輯,Map的生成與KEY的設(shè)置相對不可控,也埋下了數(shù)據(jù)不一致的隱患。



Tips:采用HashMap實例在activity間傳輸時,需要使用bundle.putSerializable(KEY_REQ, value);

3.1.2、傳遞一個String

生成一個JSON格式的字符串,跟Map方案的缺點類似,組裝過程中還容易產(chǎn)生JSON格式問題。

3.1.3、傳遞一個自定義實體類

通過SDK模塊內(nèi)置專用實體類的實例,動態(tài)設(shè)置相關(guān)參數(shù),交互類的實例。該放方案簡單易用,屏蔽了生成與設(shè)置的細節(jié),直接通過簡單的set方法接口為其賦值,并且可以在賦值時進行數(shù)據(jù)校驗、限定。
入?yún)?gòu)建示例:

//構(gòu)建請求實體
final ReqContent req = new ReqContent();
req.setToken("a_valid_token");
req.setSignature("a_valid_signature");

返參獲取示例:

//獲取返回實體
ResultContent result = getResult(intent);
int code = result.getCode();
String message = result.getMessage();
String token = result.getToken();

Tips1:采用自定義實體類作為出入?yún)⒃赼ctivity間傳輸時,無法直接傳遞實體,需要經(jīng)過序列化。有兩種方法可以實現(xiàn)

1、實體類需要實現(xiàn) JDK下的Serializable接口,通過bundle傳輸:

bundle.putSerializable(KEY_REQ, req);

2、實體類需要實現(xiàn) Android SDK下的Parcelable接口,通過bundle傳輸:bundle.putParcelable(KEY_REQ, req);

Tips2:需要注意的是:Intent 中的 Bundle 是使用 Binder 機制進行數(shù)據(jù)傳送的,有大小限制,所以務(wù)必不要進行大數(shù)據(jù)傳輸,建議最大不超過500K,越小越好,避免傳輸圖片。(實測不同版本有不同限制,500K不是一個確定的數(shù)目,感興趣的可以自己測試一下)

3.2、調(diào)用封裝設(shè)計

SDK的調(diào)用,需遵循簡單、封裝的原則。

主要有兩種類型,一是單純的、無界面的、不與用戶交互的代碼邏輯,如常見的加密、摘要生成SDK等等,直接通過一個最簡單的方法調(diào)用后直接返回即可。

二是復(fù)雜的、有界面、直接接觸用戶的SDK,此類SDK也是本章重點討論的類型。

背景:SDK內(nèi)部有一系列承載不同業(yè)務(wù)邏輯的Activity,第三方通過啟動SDK的入口Activity進行SDK調(diào)用與業(yè)務(wù)分發(fā),那么如何做一個優(yōu)雅的調(diào)用封裝?

封裝前的調(diào)用:

//繁瑣的調(diào)用代碼
Bundle bundle = new Bundle();
//這里要暴露或者約定一個靜態(tài)KEY
bundle.putParcelable(KEY_REQ, request);
Intent intent = new Intent(context, EntryActivity.class);
//屏蔽轉(zhuǎn)場動畫,讓畫面跳轉(zhuǎn)更和諧
intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
intent.putExtras(bundle);
context.startActivity(intent);

封裝后的調(diào)用:

//簡潔的調(diào)用代碼
MySDK.launchSdk(context,req);

//將復(fù)雜邏輯或細節(jié)內(nèi)置于SDK中,對外提供封裝好的靜態(tài)方法即可
public static void launchSdk(Context context, ReqContent request) {
    Bundle bundle = new Bundle();
    bundle.putParcelable(KEY_REQ, request);
    Intent intent = new Intent(context, EntryActivity.class);
    //屏蔽轉(zhuǎn)場動畫,讓畫面跳轉(zhuǎn)更和諧
    intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
    intent.putExtras(bundle);
    context.startActivity(intent);
}

如此一來,不僅簡化了調(diào)用,還屏蔽了一系列中間過程的模板代碼,何樂而不為呢?SDK多寫幾行代碼,可以方便開發(fā)者千萬家。這里只是簡單提供了一個思路,具體流程具體分析,從這里發(fā)散開去即可,但是也要避免過度封裝,導(dǎo)致擴展、、兼容性的問題。

3.3、數(shù)據(jù)返回設(shè)計

數(shù)據(jù)返回是SDK設(shè)計中不可缺失的一環(huán)。遵循的是“調(diào)用即響應(yīng)”的原則,響應(yīng)具體可以分為返回碼、拋異常。切勿設(shè)計出無響應(yīng)、無回調(diào)的SDK邏輯。SDK內(nèi)部流程處理完畢后必須給調(diào)用方一個返回,業(yè)務(wù)才能繼續(xù)。具體來講,處理了if,就必須關(guān)注else,處理了case,就必須關(guān)注default,這也是良好的java編程習(xí)慣的一部分。

業(yè)務(wù)型SDK的數(shù)據(jù)返回機制需要怎么設(shè)計呢?首先我們看下需求背景:

需要在App與SDK的Activity間進行數(shù)據(jù)傳輸
調(diào)取SDK后,需要執(zhí)行一段異步邏輯,結(jié)束后需及時關(guān)閉;
需要在邏輯代碼處理過程中隨時隨地的構(gòu)造返回參數(shù)并返回;
SDK代碼需集成于各類第三方App,需要通用的交互機制,避免引起更多兼容問題。

采用startActivityForResult()? 理論可行,但不夠靈活,棄用。

采用EventBus ? 避免引入非必須第三方依賴,暫時放棄。

采用BroadcastReceiver?Android 內(nèi)置的四大組件之一,為組件間交互而生。而且靈活方便,與入?yún)⒎椒ê魬?yīng),可以從Intent中傳輸數(shù)據(jù)。

廣播又可以分為全局廣播和應(yīng)用內(nèi)廣播,全局廣播可以跨App傳遞,基于Binder機制,在這個場景下顯得過于大材小用。而應(yīng)用內(nèi)廣播,基于Handler機制,僅限于同一應(yīng)用內(nèi)的數(shù)據(jù)交互,相比于全局廣播無需進程間通信,效率更高、也無需擔(dān)心其他應(yīng)用接收廣播帶來的安全性問題。下面來看下應(yīng)用實例:

發(fā)送數(shù)據(jù)

//SDK內(nèi)部封裝的靜態(tài)方法,提供給SDK內(nèi)部模塊統(tǒng)一調(diào)用
public static void sendResult(String token, int code, String message) {

    ResultContent result = new ResultContent();
    result.setToken(token);
    result.setCode(code);
    result.setMessage(message);

    Bundle bundle = new Bundle();
    bundle.putParcelable(KEY_RESULT, result);

    Intent intent = new Intent();
    intent.setAction(MY_SDK_RECEIVER_ACTION);
    intent.putExtras(bundle);
    LocalBroadcastManager.getInstance(MySDK.getContext()).sendBroadcast(intent);
}

接收數(shù)據(jù)

//SDK內(nèi)部封裝的注冊方法,提供給APP模塊統(tǒng)一調(diào)用
public static void registerReceiver(BroadcastReceiver receiver) {
    IntentFilter filter = new IntentFilter();
    filter.addAction(MY_SDK_RECEIVER_ACTION);
    LocalBroadcastManager.getInstance(getContext()).registerReceiver(receiver, filter);
}

//SDK內(nèi)部封裝的反注冊方法,提供給APP模塊統(tǒng)一調(diào)用,如果在onCreate()方法中注冊,則在onDestroy()方法中反注冊。
public static void unregisterReceiver(BroadcastReceiver receiver) {
    LocalBroadcastManager.getInstance(getContext()).unregisterReceiver(receiver);
}


//APP 中定義的廣播接收器,用于接收返回參數(shù)
public class MyReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        ResultContent result = MySDKHelper.getResult(intent);
        if (result == null) {
            return;
        }
        int code = result.getCode();
        String message = result.getMessage();
        String token = result.getToken();
        String log = String.format(Locale.CHINA,"返回碼: %d 返回信息: %s Token: %s", code, message, token);
        Log.d(TAG, log);
    }
}

四、參數(shù)過濾設(shè)計

參數(shù)過濾是指在入?yún)⑦^程中對入?yún)?shù)據(jù)進行過濾,避免無意義后續(xù)業(yè)務(wù)流程,及時提供調(diào)用反饋。

常見的過濾方式有:非空檢測、數(shù)據(jù)類型檢測,數(shù)據(jù)格式檢測。還可以通過自定義注解的方式對參數(shù)進行標記,通過編譯器的檢測就可以及時糾正數(shù)據(jù)類型。

一個簡單的自定義注解示例

public class SourceAnnotationDemo {

    public static final String TYPE_A = "A";
    public static final String TYPE_B = "B";

    private String type;

    @StringDef({TYPE_A, TYPE_B})
    @Retention(RetentionPolicy.SOURCE)
    public @interface MyType {
    }

    public String getType() {
        return type;
    }

    //限定了該方法的參數(shù)只能是設(shè)置限定的TYPE_A或TYPE_B,否則編譯器提示錯誤
    public void setType(@MyType String type) {
        this.type = type;
    }
}

五、交互流程設(shè)計

“前端沒有絕對的安全!”

前端沒有絕對的安全,因此我們在設(shè)計SDK的整體交互邏輯時,必須考慮返回結(jié)果的合法性問題。

5.1、錯誤的返回

在錯誤的返回中,由于錯誤的結(jié)果理論上并不會對業(yè)務(wù)造成損失,比如支付失敗,業(yè)務(wù)處理流程上并不會產(chǎn)生異常結(jié)果,當做失敗處理即可。

5.2、成功的返回

在成功的返回中,由于成功的結(jié)果理論上是繼續(xù)業(yè)務(wù)流程的依據(jù),比如支付成功后發(fā)貨行為。因此必須嚴格判斷返回的可靠性。

在前端代碼中,容易被破解者通過Hook等非正常手段篡改或者偽造返回,因此不能直接信任前端返回碼。一般支付的流程,都是類似的設(shè)計。

5.3、參考流程


流程要點:

交互憑證(Token)從后端接口交互中返回,用于對應(yīng)當次調(diào)用,此時若有更多參數(shù)需要獲取,也應(yīng)當從該接口中傳入,保持前端接口的簡潔性與數(shù)據(jù)交互的靈活性;
SDK處理完內(nèi)部邏輯后,返回前端返回碼通知App流程結(jié)束,App根據(jù)返回碼類型來確定是否需要進一步確認。此處也可以精簡前端返回,將更多業(yè)務(wù)字段從后端查詢確認接口中返回,保持前端接口的簡潔性與數(shù)據(jù)交互的靈活性。

本章介紹了SDK的接口設(shè)計,參數(shù)過濾,以及合時信任鏈下的流程設(shè)計。

接下來將繼續(xù)介紹SDK開發(fā)中對于異常與錯誤返回的討論, Exception or ErrorCode?It's a question.

2、Exception or ErrorCode

一、前言

本章介紹了Java/Android中的異常以及在SDK開發(fā)中是如何根據(jù)異常的特性進行融合設(shè)計的思考。

注意,本章并不涉及詳細地異常介紹以及異常發(fā)生后代碼執(zhí)行地順序問題,而是基于異常的基本特性與職責(zé)邊界,結(jié)合業(yè)務(wù)開發(fā)的實際情況,試圖探討拋異常與錯誤碼的選擇。

二、異常的概念

首先,我們來認識一下異常:
異常是指在程序運行期間發(fā)生的意外事件,它會中斷程序(當前方法或作用域)正常執(zhí)行的流程。



異常的層次

Checked Exception,比如最常見的IOException,這種異常是指需要調(diào)用處顯式處理的類型,要么用try catch捕獲,要么用 throw 再拋出去;
Unchecked Exception指的是所有繼承自Error(包含自身)或者RuntimeException(包含自身)的類,比如我們常見的NullPointerException就屬于這類。這些異常不強制在調(diào)用處顯式處理,但是也可以通過try catch處理;
Throwable是Exception和Error的父類。

2.1、APP開發(fā)者看待異常的角度

在APP的開發(fā)中,似乎我們總是被動的根據(jù)編譯器的提示編寫異常代碼,只是為了讓編譯器通過。為什么編譯器跟我們過不去?因為這些提示都是因為對于的操作拋出了Checked類型的異常,需要手動顯式進行處理。



在Java開發(fā)中,NullPointerException,有部分人簡稱NPE,是每個開發(fā)者都很熟悉的異常,也是讓人深惡痛絕的異常。
Java程序員:要么正在debug NPE,要么就在debug NPE的路上。

2.2、SDK開發(fā)者看待異常的角度

在SDK開發(fā)中,除了要面臨APP開發(fā)中那些對異常的處理,還需要充分考慮被異常中斷了的那些邏輯對整體交互流程產(chǎn)生的影響。

SDK自身的邏輯與APP密不可分,假如因為SDK內(nèi)部發(fā)生的異常,中斷了SDK內(nèi)部流程,而又未告知APP這顯然是不可接受的。因此,我們必須謹慎處理SDK內(nèi)部發(fā)生的異常,原則是不應(yīng)該把異常內(nèi)部消化,而應(yīng)該向外拋出,以同樣異常的形式或者自定義的返回碼。

2.3、Exception or ErrorCode,這是一個問題

在SDK的邏輯設(shè)計中,既可以通過異常告知開發(fā)者,也可以通過返回碼告知開發(fā)者,那么如何選擇呢?下面我們通過兩個例子來探索處理的方式

場景1:在SDK的入?yún)⒅?,需要APP傳入手機號碼,并校驗參數(shù)合法性
場景2: 在SDK內(nèi)部,需要獲取APP ID并進行白名單校驗

這兩種場景下,都需要在SDK的調(diào)用流程中處理并反饋,而反饋可以通過返回碼,也可以直接拋異常(系統(tǒng)內(nèi)置異?;蜃远x異常)孰好孰壞?

筆者認為,在我們實際的SDK開發(fā)中,應(yīng)該通過對相關(guān)錯誤進行分類,來決定采用異常交互還是返回碼交互。

自定義異常是一種比較“重”的告知方式,因為異常意味著中斷。返回碼是一種比較“輕”的告知方式,也是最不容易引起開發(fā)者重視的告知方式。

常見SDK開發(fā)者沒有清晰的返回碼對照表,亦常見APP開發(fā)者沒有完整地處理全部返回情形。因此,不可逆轉(zhuǎn)的錯誤建議采用異常處理機制,直接中斷程序執(zhí)行流程讓問題徹底暴露出來,避免發(fā)生更多預(yù)期之外的錯誤。

具體概括為:

在與業(yè)務(wù)有關(guān)的邏輯且涉及用戶(注意,非開發(fā)者,下同)可以重試的部分,應(yīng)該采用返回碼告知開發(fā)者,方便其進行后續(xù)處理。比如,用戶輸入了不符合手機號碼規(guī)則的信息,就可以通過我們的自定義檢查邏輯來返回對應(yīng)的返回碼,集成SDK的開發(fā)者針對處理即可。

在與業(yè)務(wù)無關(guān)的邏輯且一旦發(fā)生用戶無法自行處理的部分,可以采用拋出自定義異常的處理方式。比如,SDK開發(fā)著對APP的包名校驗,這是一個面向開發(fā)者的校驗邏輯,因此不妨直接拋出自定義異常,坦然讓APP崩潰,讓開發(fā)者從錯誤堆棧信息中找到問題。這種情形,也許就是Unchecked Exception的使用場景吧。

Exception or ErrorCode,這個問題想必大家也有了一個感性認識,也歡迎耐心看至此處的讀者發(fā)表自己的見解。

3、初始化

一、前言

本章系統(tǒng)介紹SDK的幾種初始化方式,以及SDK開發(fā)歷程中由于“誤入歧途”而了解到的Java多重繼承、ContentProvider用于SDK初始化的姿勢等。通過認識初始化到初始化的幾種方式,進而試圖探索初始化的本質(zhì)和最優(yōu)解。

二、初始化的概念

在日常的開發(fā)中,可以看到我們常用的第三方SDK在使用前都需要一個初始化的步驟。那么為什么要初始化?初始化到底做了啥?

筆者認為:初始化的本質(zhì)是將App的上下文(Context)注入到SDK中,使其能通過這個上下文訪問到App的資源與服務(wù)。也包括在初始化時調(diào)用SDK方法進行相關(guān)選項的自定義配置。

三、初始化的幾種方式

3.1、慣性思維下的歧途——自定義Application

在App開發(fā)中,我們一般會從自定義Application中獲取應(yīng)用的全局上下文,用于相關(guān)資源和服務(wù)的獲取。示例如下:

public class App extends Application {

    @SuppressLint("StaticFieldLeak")
    private static Context sContext;

    @Override
    public void onCreate() {
        super.onCreate();
        sContext = this;
    }

    public static Context getContext() {
        return sContext;
    }
}

SDK在一定程度上是App的子集,通過抽象出App的部分業(yè)務(wù)與邏輯封裝而成。因此,慣性思維下,容易將這段代碼直接在SDK內(nèi)部實現(xiàn)。殊不知,這就是歧途開始的路口...

在自定義Application對象時,需要手動在AndroidManifest.xml的application節(jié)點中,通過android:name=".App"指定,否則不會加載這個自定義的Application。

在慣性思維下,SDK開發(fā)者很容易想到一個解決方案:讓App來指定加載SDK內(nèi)置自定義Application對象不就行了?那么問題來了,他App已經(jīng)有了自己的自定義Application了呢?

再次慣性下去:那讓App繼承SDK的Application不就行了?那么問題又來了,他App已經(jīng)繼承了其他SDK的自定義Application了呢?

繼續(xù)慣性下去,已經(jīng)繼承了一個?那就再繼承一個,來個Application多繼承吧?一查,Java沒提供多個類繼承的直接支持,但是也能曲線實現(xiàn):

通過接口實現(xiàn) + 反射 的方式來創(chuàng)建代理Application對象,曲線實現(xiàn)Application的多繼承,由于代碼較多,這里就不貼源碼了。

解決方案:
https://github.com/brucevanfdm/ApplicationProxyDemo

至此,在慣性三連下,成功誤入歧途。不是說沒解決問題,只是解決的過于粗暴,友善度直線下降...

我們再回頭看下,SDK初始化的本質(zhì)就是為SDK注入一個App的上下文,而不是注入一個自定義Application。路肯定是走歪了的,但是我們也從歪路中學(xué)到了一些奇奇怪怪的知識,安慰下自己,也算是有收獲吧。換個思路,繼續(xù)往前走吧!

3.2、普遍采用的靜態(tài)方法初始化

再復(fù)習(xí)一下SDK初始化的本質(zhì):注入App上下文,用于獲取相關(guān)資源及服務(wù)。那么,為什么要在Application里初始化呢?
我們完全可以自定義一個“偽Application類”,再通過靜態(tài)方法來注入應(yīng)用上下文(一般為Application Context)。示例如下:

public class MySDK {

    private static Context sContext;

    private MySDK() {
    }

    public static void initSdk(Context context) {
                //獲取ApplicationContext防止內(nèi)存泄漏
        sContext = context.getApplicationContext();
        initSomething();

    }

    public static Context getContext() {
        return sContext;
    }

    private static void initSomething() {
        //init something
    }

}

如此一來,便可愉快初始化了:

public class App extends Application {

    @Override
    public void onCreate() {
        super.onCreate();

        //初始化SDK
        MySDK.initSdk(this);

    }
}

如此一來,別說獲取App的上下文,初始化SDK的相關(guān)邏輯也有地方寫了,友善度up up up。

3.3、“無侵入”的ContentProvider初始化方案

相信細心的朋友發(fā)現(xiàn)了,有些SDK看起來直接引入就行,也沒有進行初始化呀?那他們是怎么做的呢?答案正是利用的ContentProvider獲取了應(yīng)用的上下文??磦€示例



以上截圖來自著名工具類SDK:AndroidUtilCode ,其實還有更多采用類似方式初始化的SDK,比如利用此方法進行初始化的鼻祖Firebase(貌似是),其他就不再一一舉例了,大家平時也可以留意一下。
那么問題來了,為啥在ContentProvider就可以做初始化,并且獲取到application context的呢?且看幾段源碼



ActivityThread-handleBindApplication

ActivityThread-installProvider



ContentProvider-attachInfo
截了三段源碼,可以看到App的啟動過程中加載了provider,并且傳了一個Application實例進去,最終在ContentProvider中調(diào)用了onCreate()方法。因此,在自定義的ContentProvider中,通過getContext()方法就可以獲取到Application的實例了。

其實從這段源碼中,我們也可以看到,ContentProvider中的onCreate()方法是先于Application中的onCreate()方法執(zhí)行的(注意:此時Application對象已經(jīng)創(chuàng)建)。

關(guān)于App啟動耗時的優(yōu)化思路,是不是又多了一個關(guān)注點?

說完了源碼再來看下SDK工程中怎么配置吧(其實開頭的截圖已經(jīng)有了):

public class MySDKInitProvider extends FileProvider{

    @Override
    public boolean onCreate() {
        //初始化
        MySDK.initSdk(getContext());
        return super.onCreate();
    }
}

在SDK Module下的manifest中注冊,編譯時將會合并至主工程項目

<provider
    android:name=".MySDKInitProvider"
    android:authorities="${applicationId}.MySDKInitProvider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/my_sdk_provider_paths" />
</provider>

這里需要注意的是Provider的authorities千萬別寫死,否則兩個引入同樣SDK的App就無法共存了,這大概是SDK最不該犯的錯誤之一吧!

3.4、初始化新姿勢-App Startup

關(guān)注Jetpack的同學(xué)可能有留意到,不久前谷歌新增了一個組件—— App Startup 。
App Startup提供了一種在應(yīng)用程序啟動時高效、直接初始化組件的方法。SDK開發(fā)人員和APP開發(fā)人員都可以使用App Startup簡化啟動順序并顯式設(shè)置初始化順序。App Startup還允許通過定義共享的ContentProvider統(tǒng)一組件的初始化,大大縮短應(yīng)用啟動時間。
該組件的發(fā)布,也可以看到谷歌試圖為混亂的ContentProvider初始化填坑,為更加可控的初始化填坑。雖然將Provider用于初始化方便了調(diào)用人員,看似“無侵入”,實則“強侵入”。

試想一下,在追求App性能與啟動速度的場景中,多個SDK同時利用各自定義的ContentProvider實現(xiàn)“自啟動”,還要在各種有先后順序與依賴的SDK初始化下做優(yōu)化,那滋味恐怕也不好受吧?

由于該組件還處于alpha的階段,就不太建議用于生產(chǎn)環(huán)境中了。

4、個性化配置

一、SDK自定義配置

1.1、什么是自定義配置

在SDK開發(fā)中,常見的需求是提供一系列配置方法,用于自定義SDK行為。比如切換調(diào)試/正式模式,啟動/關(guān)閉某些功能。



某推送SDK自定義配置方法示例

1.2、設(shè)計一個配置方法

前面我們介紹了自定義配置的概念,并且參考了某推送SDK實現(xiàn)的自定義配置方法。相信大家也能據(jù)此思想實現(xiàn)自己SDK的配置了吧!

但是,這種方式未免不太過癮,我們平時開發(fā)的時候也可以看到Java代碼中有一種很爽的調(diào)用方式,隨便舉個例子:

StringBuilder builder = new StringBuilder();
builder.append("one").append("two").append("three").length();

可以看到,連續(xù)的流式調(diào)用API很方便也很簡潔。這種API實現(xiàn)方式又稱為流式接口(fluent interface)是軟件工程中面向?qū)ο驛PI的一種實現(xiàn)方式。那么問題來了,這么好的API設(shè)計思想,為什么不用到我們的SDK中來,讓開發(fā)者爽一把呢?

先來回顧一下SDK配置的本質(zhì):SDK配置方法的本質(zhì)是為SDK相關(guān)功能提供默認配置,并且接收開發(fā)者的自定義配置,用于修改默認邏輯。

所以我們的方法中,不僅包含默認選項,還要包含修改方方法。話不多說,先上一份模板實例:

配置方法

public class MySDKConfig {

    //默認配置
    private static boolean sDebug = false;
    private static long sTimeout = 8000L;

    private static final MySDKConfig.Config CONFIG = new MySDKConfig.Config();

    public static class Config {
        private Config() {
        }

        /**
         * 設(shè)置調(diào)試模式
         *
         * @param isDebug 模式
         * @return Config
         */
        public MySDKConfig.Config setDebug(final boolean isDebug) {
            sDebug = isDebug;
            return this;
        }

        /**
         * 設(shè)置超時時間
         *
         * @param timeout 超時時間
         * @return Config
         */
        public MySDKConfig.Config setTimeout(final long timeout) {
            //此處演示了邊界值的處理方式
            long minTimeout = 3000L;
            if (timeout < minTimeout) {
                sTimeout = minTimeout;
            } else {
                sTimeout = timeout;
            }
            return this;
        }
    }

    public static boolean isDebug() {
        return sDebug;
    }

    public static long getTimeout() {
        return sTimeout;
    }

    public static MySDKConfig.Config getConfig() {
        return CONFIG;
    }

}

調(diào)用示例

//一行代碼,流式調(diào)用
MySDKConfig.getConfig().setDebug(true).setTimeout(8000L);

從源碼實例可以看到,我們提供了一些默認配置。并通過靜態(tài)內(nèi)部類來實現(xiàn)自定義配置,并且在外層提供了getter方法,將配置提供給SDK其他模塊調(diào)用。

其中實現(xiàn)流式調(diào)用的關(guān)鍵就是每個setter方法中都返回了this對象本身,就這樣實現(xiàn)了流式API接口。

在SDK開發(fā)的場景中,由于需要配置的內(nèi)容多,還涉及到默認配置,特別適合采用流式API配置方法構(gòu)建自定義配置。其實回頭一想,這種設(shè)計思想其實不就是簡化版建造者(Builder)模式的使用場景嗎?

下面是我整理出來的一些資料,想要更多可以點擊我的github從而了解更多進階的可能哦。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容