最近在做一個 sdk,有這樣一個需求是,sdk 中有5個功能子產品,在對外打包的時候可以自由的選擇 sdk 中包含任意的幾個功能子產品。
比如給業務方 A 的 sdk 包中包含功能1/2/3,給業務方 B 的 sdk 包中包含功能2/3/4。
思考良久,覺得下面這種結構是符合需求的,簡單描述一下。
- 最上層是 api 層,包含整個 sdk 初始化,以及每個子產品對外提供服務的 api 代碼,在這一層根據功能子產品定義宏,使用宏區分初始化哪些部分代碼,以及哪些 api 可用。
- 下一層是對應的5個功能子產品各自的代碼,互相之間無依賴。
- 再下層是涉及到多個子產品的公用代碼,有兩種情況,一種是被部分子產品依賴,比如 common-module-1,還有一種是被所有子產品依賴,比如 common-module-3,和 api 層一樣,在 common-module-3 這種全局依賴的子產品裡根據功能子產品定義宏,使用宏區分初始化哪些代碼可用。

整個結構介紹完了,在實際編譯的過程中,比如想編譯包含功能1/4的sdk,則編譯情況如下

接下來就是代碼實作,在Android和iOS中如何實作這種組織結構呢,仔細思考一下,這個方案其實涉及三個點
- 靈活的依賴關系
- 區分編譯代碼 - 動态修改宏定義
- 可合并的目标産物
Android
1. 靈活的依賴關系
首先想到的是像 mPaaS 一樣的子產品化開發,mPaaS 提供核心層,其他 Bundle 之間的調用有兩種形式。
- 通過依賴引入
- 通過在核心層注冊 Service,其他子產品可以 findService 後使用
但是 mPaaS 是基于 Project 來做的,每個子產品的産物是一個類似 aar 的包含代碼和資源的包。目前這個需求倒是不需要如此重量級,是以考慮基于同一個 Project,分 module 的形式來實作。
Android 的依賴是比較容易解決的,因為 Gradle 本身是支援可程式設計的。是以 build.gradle 腳本可以寫成如下的樣式(依賴類型的 embed 是另外一個原因,這個後面再講)。
dependencies {
embed project(':sdk-api')
embed project(':sdk-common')
if ("${SEC_SDK_FUNCS}".indexOf("APDID") != -1) {
embed project(':sdk-apdid')
}
if ("${SEC_SDK_FUNCS}".indexOf("DYNAMIC") != -1) {
embed project(':sdk-dynamic')
}
if ("${SEC_SDK_FUNCS}".indexOf("DEVICECOLOR") != -1) {
embed project(':sdk-devicecolor')
}
if ("${SEC_SDK_FUNCS}".indexOf("SIGN") != -1) {
embed project(':sdk-sign')
}
if ("${SEC_SDK_FUNCS}".indexOf("SECSTORE") != -1) {
embed project(':sdk-secstore')
}
}
2. 區分編譯代碼
另外一個需要解決的問題是頂層和底層的宏定義,因為頂層和底層代碼是共用的,是以,在 Android 上有三種方法實作這種功能。
- 通過反射實作
- 通過 BuildConfig + if 語句實作功能分流
- 通過編譯器腳本實作類宏定義功能
首先反射實作是被我否定的,因為反射會增加額外的調用消耗和異常,而且通過反射實作會在 api 層暴露過多的代碼細節,不利于代碼的安全性。
其次通過 BuildConfig 和 if判斷條件,BuildConfig 可以通過編譯腳本生成到代碼中。
android {
defaultConfig {
...
buildConfigField "String", "VERSION_NAME", "1.0" // 主版本号
...
}
}
public final class BuildConfig {
...
public static final String VERSION_NAME = "1.0";
...
}
因為 BuildConfig 是 final static 的常量,是以這種常量加簡單的判斷條件,在 java 編譯期是會被優化掉的,也就是說,如下代碼。
@Override
protected void onCreate(Bundle savedInstanceState) {
if (BuildConfig.VERSION_NAME == "1.0") {
Log.d("test", "#1");
} else {
Log.d("test", "not #1");
}
if (BuildConfig.VERSION_NAME.equals("1.0")) {
Log.d("test", "#2");
} else {
Log.d("test", "not #2");
}
if (BuildConfig.VERSION_NAME == "1.0" || BuildConfig.VERSION_NAME == "2.0") {
Log.d("test", "#3");
} else {
Log.d("test", "not #3");
}
if (isMatch()) {
Log.d("test", "#4");
} else {
Log.d("test", "not #4");
}
}
public static boolean isMatch() {
if (BuildConfig.VERSION_NAME == "1.0") {
return true;
}
return false;
}
編譯之後的代碼如下
public void onCreate(Bundle bundle) {
Log.d("test", "#1");
Log.d("test", "#2");
Log.d("test", "#3");
if (isMatch()) {
Log.d("test", "#4");
} else {
Log.d("test", "not #4");
}
}
public static boolean isMatch() {
return true;
}
可以看出來,簡單的判斷,與或否都會在編譯期優化掉,隻保留可達的分支,但是如果通過方法調用,即使方法内部也是簡單的與或否判斷,依然無法優化掉。
是以可以看出來,BuildConfig存在以下一些問題
- 判斷條件多的時候無法提取公共方法
- 無法在方法體外部生效,比如import、變量聲明的地方
第二個問題是比較緻命的,這就導緻了一些依賴無法被徹底移除,是以還是尋找一種類似 C 宏定義的方案,在編譯期生效,可以在任何地方使用,好在 github 上已經有人提供了對應的解決方案。
https://github.com/dannyjiajia/gradle-java-preprocessor-plugin根據說明,隻需要在build.gradle中聲明symbols
preprocessor {
verbose true
sourceDir file("src/main/java") //required
targetDir file("src/main/java") //required
symbols "GLOBAL","GLOBAL_2" //symbols is valid for each flavors
}
在代碼中就可以使用了
//#ifdef FREE_VERSION
Log.i("sample","I am Free Version");
//#else
Log.i("sample","I am not Free Version");
//#endif
編譯期,未符合條件的地方會被注釋掉,如下
//#ifdef FREE_VERSION
//@ Log.i("sample","I am Free Version");
//#else
Log.i("sample","I am not Free Version");
//#endif
這個方案存在什麼問題呢,主要是易讀和易維護性的問題,不是java的原生支援,是以IDE也不會有額外的顯示優化,想想你的代碼中有大量的注釋代碼是種什麼感覺,好在我們隻是在頂層和底層的共用代碼中少量使用宏定義,還是可以接受的。
3. 可合并的目标産物
最後再說一下代碼産物,期望是不管内部的功能子產品有哪些,但是對外打出的 sdk 都是一個,但是我們知道采用 module 形式組織的項目,每個 module 都會産出一個 aar,是以這裡就用到了一個 aar 合并的方案,依然是萬能的 github。
https://github.com/vigidroid/fat-aar-plugin前面依賴關系中提到的 embed 就是這種方案定制的依賴辨別,通過 fat-aar 可以合并産出的所有 aar 為一個最終的 aar。
iOS
和Android類似,iOS 的依賴關系也可以通過 Project 或者 Target(類似module)兩種形式實作,基于這兩種形式,Xcode 的子產品化有三種形式
- project
- target - framework
- target - static library
下面依次來研究一下這三種模式是否能滿足我們的需求
Project模式
和 Android 一樣,以 Project 的形式組織項目,會導緻整個 SDK 的組織複雜度極高,代碼分布在多個代碼庫裡,對于我們這種開發 SDK 的需求是有點得不償失的。
但是像MPaaS iOS版就是一Project來分割子產品的,然後通過Pod管理依賴,是以,涉及的分組合作的大體量項目,可以考慮這種模式。
Target模式
如果項目在一個 Project 中,就要考慮以 Target 來分隔功能子產品,Target 的産物分為兩種形式 Framework 和 StaticLibrary,下面我們來看下這兩種形式的利弊。
Target-Framework模式
Framework 的産物是 xxx.framework,頭檔案和源檔案都被打進 Framework 包中,接入方依賴 Framework 後可以直接調用裡面提供的方法。

但是 Framework 不适用于目前我們的多子產品需求,因為以 Framework 作為産物是無法合并,分子產品後最終打出的産物是多個 Framework,業務方接入功能需要多個 Framework 接入,提高了接入的複雜度。
第二個問題是無法實作多層依賴,即 A依賴B、B依賴C,則A需要同時引入B和C的 Framework,在多子產品的情況下也會導緻整個項目依賴很複雜。
Target-StaticLibrary模式
StaticLibrary 模式的産物是 xxx.a,.c .m等源檔案被編譯進.a中,但是并不包含頭檔案,頭檔案需要單獨提供,接入方依賴 .a 時需要手動設定 header files search path 來引用頭檔案才可以調用内部提供的功能。是以隻用 StaticLibrary 也是無法滿足需求的。
我們目前的需求是希望提供給業務方一個獨立可用的包,像 Framework 這種既包含源檔案又包含頭檔案,但是内部編譯進來的源檔案又是可選的,于是就需要組合 Framework 和 StaticLibrary 兩種形式一起使用。
在最外層包一個 Framework 的target,依賴各個 StaticLibrary,在 Framework 中提供所有的.h檔案,源檔案是在各個 StaticLibrary 中的。但是需要注意的是,這樣最終這種模式中,各個.c檔案是各個StaticLibrary編譯時單獨編譯的,是以在合并到 Framework 時,如果多個類同名會出現類沖突。
動态指定包含功能的SDK結構調研

最後的組織結構像這樣

動态依賴關系
XCode的所有配置都是寫在project.pbxproj檔案中的,是以編譯腳本隻需要修改project.pbxproj檔案,然後手動調用xcodebuild指令進行編譯。
project.pbxproj的格式看似雜亂,但其實是按照一定的規則進行組合的,如下圖所示,project.pbxproj是通過index互相組織的,通過index就可以一層層的找到對應項目依賴設定的位置。

簡單一點,可以通過直接編輯檔案的方式來修改,但是要一層層的記錄index,然後找到對應的index再修改。遇事不決github搜一搜,發現python有一個解析project.pbxproj檔案的庫。
pip install pbxproj
于是整個過程就變成了
project = XcodeProject.load(xxx.xcodeproj/project.pbxproj)
# 配置依賴
APPSecuritySDK_Frameworks = 0
# 找到依賴的index
PBXNativeTarget = project.objects._sections["PBXNativeTarget"]
for target in PBXNativeTarget:
if target._get_comment() == "APPSecuritySDK":
for phase in target.buildPhases:
if phase._get_comment() == "Frameworks":
APPSecuritySDK_Frameworks = phase
# 修改依賴
for phase in project.objects._sections["PBXFrameworksBuildPhase"]:
if phase.get_id() == APPSecuritySDK_Frameworks:
for file in phase.files.copy():
module_name = re.findall(r"(?<=libAPPSecuritySDK\-).+(?=\.a)", file._get_comment())
if len(module_name) > 0:
module_name = module_name[0]
if module_name and module_name not in contain_functions and module_name != "Common":
print('REMOVE MODULE DEPEDENCES :' + module_name)
phase.files.remove(file)
這一塊是比較簡單的,因為oc原生支援宏定義,XCode對宏定義的顯示優化也比較好。
需要解決的一個問題是如何通過編譯腳本動态的修改宏定義,還是和上面動态修改依賴關系一樣,我們先分析下宏定義在pbxproj中的結構

然後用python來解析pbxproj檔案
project = XcodeProject.load(xxx.xcodeproj/project.pbxproj)
functions_DEFINITIONS = configDefinitions(params)
# 在XCBuildConfiguration中的每個GCC_PREPROCESSOR_DEFINITIONS子產品都加入對應的宏定義
functions_DEFINITIONS = ["MACRO_1", "MACRO_2"]
XCBuildConfiguration = project.objects._sections["XCBuildConfiguration"]
for obj in XCBuildConfiguration:
GCC_PREPROCESSOR_DEFINITIONS = obj.buildSettings["GCC_PREPROCESSOR_DEFINITIONS"]
if GCC_PREPROCESSOR_DEFINITIONS is None:
obj.buildSettings.__setitem__("GCC_PREPROCESSOR_DEFINITIONS", functions_DEFINITIONS)
else:
if type(GCC_PREPROCESSOR_DEFINITIONS) is list:
GCC_PREPROCESSOR_DEFINITIONS.extend(functions_DEFINITIONS)
elif type(GCC_PREPROCESSOR_DEFINITIONS) is str:
old_definition = GCC_PREPROCESSOR_DEFINITIONS
functions_DEFINITIONS.append(old_definition)
obj.buildSettings.__delitem__("GCC_PREPROCESSOR_DEFINITIONS")
obj.buildSettings.__setitem__("GCC_PREPROCESSOR_DEFINITIONS", functions_DEFINITIONS)
else:
print('INVALID DEFINITIONS TYPE')
這樣就可以在編譯前在每個 Target 加上指定的宏定義。
之前讨論的依賴關系,最終選擇 StaticLibrary 的模式有很大一部分原因也是因為 StaticLibrary 可以被合并到最終的 Framework 産物中。
StaticLibrary 的産物是 .a,.a 檔案中隻包含類編譯後的 .o 檔案,而當 StaticLibrary 被一個 Framework 依賴時,StaticLibrary 中的 .o 檔案會被合并到 Framework 中,最終的編譯産物就是一個 Framework。
總結
以上就是這種結構 SDK 在 Android 和 iOS 上各自的實作方案,如果有更好的方案,歡迎與我交流。