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從而了解更多進階的可能哦。