一直以來,跨平臺技術被普遍探索與研究。時至今日,在不涉及界面層面的跨平臺技術上,C++跨平臺技術仍被普遍採用。Kotlin Multiplatform做爲一種新興技術,也開始在跨平臺的領域上展示出本身獨有的優點。本文基於自身分別使用兩種方式進行跨平臺項目開發的實際體驗,對兩種跨平臺技術作了簡要分析對比。html
對於C++跨平臺技術,你們對其原理應該都比較熟悉,再也不贅述。
Kotlin Multiplatform主要分爲Kotlin/JVM
、Kotlin/Native
與Kotlin/JS
,其中Kotlin/JVM
是咱們最爲熟悉的,也被廣大Android開發人員普遍使用。Kotlin/Native
再也不基於JVM平臺,而是使用Kotlin編譯器,將Kotlin代碼編譯成LLVM IR,再配合LLVM backend,最終編譯成平臺的原生二進制文件,不依賴虛擬機,執行效率媲美原生程序。其基本原理以下圖:java
Kotllin Multiplatform目前已經支持的平臺:android
Android (可編譯生成Linux So文件,也可基於Kotlin/JVM編譯成aar)ios
iOS 9.0+(Arm32, Arm64, x86_64)git
MacOS(x86_64)github
Linux編程
Windows(mingw x86_64, x86)json
WebAssembly (wasm32)xcode
Kotlin Multiplatform主要基於Kotlin語言,而C++跨平臺主要基於C++進行開發。Kotlin做爲一種現代型語言,在開發上的體驗是要優於C++的。站在一個新手的角度(有其餘語言的開發基礎,好比Java/OC/Swift),你能夠用一週的時間學習熟悉Kotlin語法並開始項目實戰,可是很難用一週時間學習熟悉C++後就比較有信心地開始項目實戰。使用Kotlin進行開發,你只須要熟悉Kotlin基本語法、一些基礎庫的使用加上一小部分高階函數的使用就能夠開始進行開發,而使用C++進行開發,首先須要熟悉C++的基本語法(C++自己的語法也很是的多),再須要去理解指針與引用兩個概念,特別是指針(指針做爲C++開放給用戶的一種能力,在給用戶賦予了更多開發權力與自由的同時也形成了不少的問題,C++項目的一大部分問題都是因爲指針使用不當形成的),隨後須要去理解手動內存管理(或者直接去理解智能指針,這也是須要花時間去理論與熟悉的),隨後才能夠慢慢地進行開發。markdown
從兩種語言的學習成本與開發體驗上來看,Kotlin優於C++。
做爲跨平臺技術,項目架構的區別主要體如今平臺相關與平臺無關代碼的組織上。C++跨平臺技術並無一種固定的項目架構,不一樣的開發者可能採用不一樣的項目架構,以本身參與的一個C++跨平臺項目爲例,其項目架構以下圖:
能夠看到,平臺相關性代碼被分離到了不一樣的目錄中,有效實現代碼隔離。對於平臺相關代碼的實現,將其頭文件定義在公共代碼中,再在不一樣平臺去分別進行實現,以一個簡單的Log實現爲例:
// 公共層定義, ComLogger.h
namespace cut {
class ComLogger {
public:
// 函數定義
void d(const char *fmt, ...);
...
};
}
// android層實現, AndroidLogger.cpp,代碼放在cut_android中
#include <cut/ComLogger.h>
#include <android/log.h>
// 函數實現
void cut::ComLogger::d(const char * fmt, ...) {
va_list params;
va_start(params, fmt);
__android_log_vprint(ANDROID_LOG_DEBUG, TAG, fmt, params);
va_end(params);
}
複製代碼
打包時,再利用CmakeLists指定不一樣的源代碼進行打包。好比打包Android產物時,只打包cut_android與cut目錄下的源碼,打包iOS產物時,只打包cut_ios與cut目錄下的源碼。
不一樣於C++跨平臺,Kotlin Multiplatform跨平臺技術已經制定了一種項目架構
能夠看到,平臺相關代碼與平臺無關代碼也實現了代碼隔離。對於平臺相關代碼的實現,其提供了expect/actual機制,在公共層定義expect接口,不一樣平臺層分別調用平臺相關接口去進行實現。仍是以log實現舉例:
// commonMain,定義在公共代碼層,至關於一個接口聲明
expect class PlatformLogger() {
fun logDebug(tag: String, message: String)
}
// androidMain,在android層,至關於Android平臺上的接口實現
actual class PlatformLogger {
actual fun logDebug(tag: String, message: String) {
android.util.Log.d(tag, message)
}
}
複製代碼
上述對比能夠看出,二者項目架構的本質實際上是一致的,都是平臺無關放到一個目錄,平臺相關單獨放到不一樣平臺目錄中,且對於平臺相關的接口與方法實現也比較一致,都是公共層定義,平臺層不一樣實現。不一樣的是,C++跨平臺須要開發者本身去搭建這樣一套項目架構配置,即須要本身去編寫CmakeLists.txt
相似的配置文件去實現,而Kotlin Multiplatform則基於gradle配置提供好了這樣的項目配置,開發者的配置成本不多。
另外一方面,C++跨平臺的平臺相關代碼隔離其實只是一種約束而已,其並未在編譯期間真正地實現了代碼隔離。好比一位Android開發者在公共代碼層直接include <jni.h>
後調用Android平臺特有的JNI方法,若是隻在Android平臺上測試,其實都是看不出問題的,這個時候若是編譯iOS平臺,就會編譯不經過。因此,對於C++跨平臺而言,須要開發者本身去在編譯期靜態檢查代碼,防止開發者在公共代碼層使用到平臺相關庫。而對於Kotlin Multiplatform跨平臺,在公共代碼層是訪問不到任何平臺相關的代碼的,自然支持代碼隔離。
由上述分析,從項目的配置成原本看,Kotlin Multiplatform小於C++。
C++做爲一個歷史悠久、應用普遍的開發語言,在漫長的計算機發展中已經沉澱了至關一部分優秀的第三方庫,在跨平臺項目中碰到的一些通用的基礎能力能夠直接藉助於成熟的第三方庫,如json解析、網絡請求等。
Kotlin Multiplatform做爲一種新技術,社區成立時間較短,目前沉澱的第三方庫比較少,開發者可以使用的通用的基礎能力較少,社區還不夠豐富,目前已有的KM庫存檔: KN庫存檔。Kotlin Multiplatform開發者也意識到了這個問題,因此其開發了cinterop這個工具,可以把c語言直接編譯成Kotlin/Native庫,讓kotlin直接調用。因此Kotlin Multiplatform是可使用全部C語言庫的,但對於C++庫,KN暫時還並不支持。不過,能夠經過接口包裝的形式讓K/N使用C++庫。
在平臺相關庫的使用上,相對於C++跨平臺,Kotlin Multiplatform是能夠很是方便地使用平臺相關庫的,好比Android平臺上使用Okhttp進行網絡請求,只須要在android依賴上加入對Okhttp的依賴,並在androidMain的實現中調用Okhttp進行實現便可。固然,C++跨平臺也可使用平臺相關庫,不過實現稍顯麻煩,在Android平臺上體現爲須要藉助一層JNI,且項目配置也略顯複雜。
對於跨平臺庫而言,模型統一多是最大的問題了。好比簡單的請求網絡後獲得一個json,將這個json序列化成一個模型實體類,且這個模型實體類會在各個平臺中進行使用,須要在平臺層包裝一個實體類,以C++跨平臺的一個UserInfo實體類爲例:
// 模型定義,存在於公共代碼層,UserInfo.h
class UserInfo {
private:
std::string name;
public:
UserInfo() {}
~UserInfo() = default;
const std::string & get_name() { return name; }
void set_name(const std::string & name) { this->name = name; }
}
// Android層,使用Java包裝UserInfo,提供給業務方調用。UserInfo.java
public class UserInfo {
public String getName() {
return getNameFromJNI();
}
public void setName(String name) {
setNameFromJNI(name);
}
}
複製代碼
能夠看到,爲了實現可以給不一樣平臺層提供平臺層相關的調用,須要在不一樣層編寫相應的包裝類。包裝類的編寫並不複雜,主要問題在於工做量大(模型類越多工做量越大),且很差維護,當跨平臺層的模型實體類修改一個字段時,每一個平臺包裝類都須要進行相應修改,維護成本大。因此出現了QuickType之類的模型自動生成,能夠根據本身定義的模型參數配置,自動生成UserInfo.h
和UserInfo.java
,大幅度減小維護成本,但模型自動成本框架的引入與配置、維護也會帶來比較大的額外的成本 。
一樣的需求場景,使用Kotlin/Native跨平臺,其簡要代碼以下:
// 模型定義,存在於公共代碼層,UserInfo.kt
data class UserInfo(
var name: String = ""
)
// 生成的libkntest.h文件
struct {
libkntest_KType* (*_type)(void);
libkntest_kref_sample_UserInfo (*UserInfo)(const char* name);
const char* (*get_name)(libkntest_kref_sample_UserInfo thiz);
void (*set_name)(libkntest_kref_sample_UserInfo thiz, const char* set);
libkntest_KBoolean (*equals)(libkntest_kref_sample_UserInfo thiz, libkntest_kref_kotlin_Any other);
libkntest_KInt (*hashCode)(libkntest_kref_sample_UserInfo thiz);
const char* (*toString)(libkntest_kref_sample_UserInfo thiz);
} UserInfo;
複製代碼
能夠看到,Kotlin/Native框架編譯生成平臺相關庫時自動生成了包裝類,提供給平臺相關業務方調用。相較於C++跨平臺,KN跨平臺減去了模型自動成本框架的引入與配置成本。
另外一方面,對於C++跨平臺在Android平臺上,模型統一會形成頻繁的JNI調用,會帶來一些額外的性能損耗。固然,Kotlin Multiplatform在iOS和PC平臺上的模型統一也會帶來額外的性能損耗。
C++被開發者詬病良久的一點就是手動內存管理了,new與delete、malloc與free的成對使用成爲C++開發者在開發時須要常常關注的一點,然而仍是會常常性出現內存泄漏或野指針問題。因此最頑固的C++也推出了智能指針,經過引用計數的形式幫助開發者實現自動內存管理,然而彷佛也只是必定程度上緩解了這個問題。
Kotlin Multiplatform採用了自動內存管理,內部經過引用計數的方式實現自動內存管理,因此在編寫純Kotlin代碼的時候是不須要去考慮內存管理的。可是,因爲Kotlin/Native能夠調用C語言,而C語言又是一個手動內存管理的語言,因此在Kotlin調用C時,手動內存分配成爲一件必不可少的事情,其提供成對的內存分配與釋放函數:
// 分配Native內存
nativeHeap.alloc
// 釋放native內存
nativeHeap.free
複製代碼
固然,Kotlin/Native提供了一種更爲友好的方式:memScope
做用域。memScope
的做用是當memScope
的做用域結束的時候,自動釋放在裏面分配的全部native內存。如:
// memScoped結束時buffer會自動釋放
memScoped {
val buffer = allocArrayOf(destArray)
result = fread(buffer, destArray.size.toULong(), 1u, filePointer).toInt()
resultString = buffer.toKString()
return resultString
}
複製代碼
C++的多線程,能夠直接使用pthread
庫,也能夠藉助其餘第三方多線程庫,具體開發時能夠不關心具體運行平臺。 Kotlin Multiplatform的多線程模型不一樣主要體如今Kotlin/Native上,K/N提供了一個多線程框架,叫作Worker
,其內部也是基於pthread實現的,一個Worker
對應一個pthread
,其基本用法以下:
//1.建立一個worker實例
val worker = Worker.start()
//2.執行一個異步任務
val future = worker.execute(TransferMode.SAFE, {"Hello"}) {
it + ", Kotlin/Native"
}
//3. 獲取返回值
future.consume {
println("Result: $it")
}
複製代碼
Worker的使用仍是比較簡單,但使用Worker時,坑主要在於K/N變量的共享性:Kotlin/Native 實現了嚴格的可變性檢測,對象要麼不可變,要麼在同一時刻只在單個線程中訪問(mutable XOR global),使得其具體使用也與平時的開發過程有必定不一樣,固然這樣也有好處,K/N把本來在運行時機率性出現的問題,變成了運行時必現的問題,利於發現問題;更近一步把問題暴露在編譯期就友好多了(固然這樣作也是有點激進,K/N被吐槽最多的地方也在這裏)。
整體來講,在混合編程中,多線程是Kotlin/Native中坑最多的地方,也是對開發者最不友好的地方。
對於C++跨平臺而言,各個平臺對C++平臺代碼斷點調試的支持程度較好。Xcode中在C++中設置斷點與在OC中設置斷點並沒有區別,Android Studio也可在JNI層設置斷點進行調試,不過存在一系列問題(attach緩慢,容易斷掉鏈接等)。
對於Kotlin Multiplatform,在Android平臺上,調試即爲原生調試,極爲方便;在XCode中,也可利用插件:xcode-kotlin方便地調試Kotlin代碼;對於PC平臺,目前K/N並不支持在可視化的斷點調試,即在Clion/VS中並不能調試Kotlin代碼,須要經過lldb或者gdb去進行調試,或者使用kotlin編寫測試代碼,直接運行在PC平臺進行調試。
對比基於Kotlin 1.3.71版本。
能夠看到,Kotlin Multiplatform的最終產物平臺相關,對平臺相關的業務方接入更加友好。
不包含任何業務代碼,引發的額外包體積增長以下表:
Kotlin Multiplatform主要在iOS與PC平臺上會有額外的包體積增長,這主要是因爲kotlin-runtime引入的,其內部主要包含一些Kotlin/Native GC相關代碼與Kotlin基礎庫等。
編寫簡單的測試程序,測試代碼爲「檢測一個int32值,檢測其二進制表示中含有多少個1,從0一直檢測到100000000」,Kotlin版本測試代碼以下:
fun test(): Int {
var sum = 0
// 循環一億次
for (i in 0 until 1_0000_0000) {
sum += getInt32TrueCount(i)
}
return sum
}
private fun getInt32TrueCount(value: Int): Int {
if (value == 0) {
return 0
}
return getByteTrueCount(value and 0xFF) +
getByteTrueCount((value shr 8) and 0xFF) +
getByteTrueCount((value shr 16) and 0xFF) +
getByteTrueCount((value shr 24) and 0xFF)
}
private fun getByteTrueCount(value: Int): Int {
if (value == 0) {
return 0
}
val a = (value and 0x1)
val b = ((value and 0x2) shr 1)
val c = ((value and 0x4) shr 2)
val d = ((value and 0x8) shr 3)
val e = ((value and 0x10) shr 4)
val f = ((value and 0x20) shr 5)
val g = ((value and 0x40) shr 6)
val h = ((value and 0x80) shr 7)
return a + b + c + d + e + f + g + h
}
複製代碼
C++實現以下:
int getByteTrueCount(int value) {
if (value == 0) {
return 0;
}
int a = (value & 0x1);
int b = ((value & 0x2) >> 1);
int c = ((value & 0x4) >> 2);
int d = ((value & 0x8) >> 3);
int e = ((value & 0x10) >> 4);
int f = ((value & 0x20) >> 5);
int g = ((value & 0x40) >> 6);
int h = ((value & 0x80) >> 7);
return a + b + c + d + e + f + g + h;
}
int getInt32TrueCount(int value) {
if (value == 0) {
return 0;
}
return getByteTrueCount(value & 0xFF) +
getByteTrueCount((value >> 8) & 0xFF) +
getByteTrueCount((value >> 16) & 0xFF) +
getByteTrueCount((value >> 24) & 0xFF);
}
int test() {
int sum = 0;
for (int i = 0; i < 100000000; ++i) {
sum += getInt32TrueCount(i);
}
return sum;
}
複製代碼
在Kotlin/C++內部使用for循環一億次,測試結果以下表:
在外部使用for循環一億次,測試結果以下表(即頻繁地進行跨語言調用):
從上述兩張耗時對比表能夠看出,在Android系統上,JNI調用存在必定性能損耗,短期內頻繁進行JNI調用性能較差;在iOS平臺上,調用Kotlin存在必定性能損耗,但性能損耗明顯小於JNI調用的性能損耗。
編寫頻繁的對象建立銷燬測試程序進行測試,測試程序以下:
//kotlin版本, UserInfo爲上文提到的數據類
fun allocObject() {
val user1 = UserInfo(name = "hello")
val user2 = UserInfo(name = "test")
val user3 = UserInfo(name = "hello")
val user4 = UserInfo(name = "world")
val user5 = UserInfo(name = "hello")
}
fun testAllocObject() {
// 循環一千萬次
for (i in 0 until 10000000) {
testAllocObject()
}
}
// C++版本
void allocObject() {
UserInfo *userInfo1 = new UserInfo("hello");
UserInfo *userInfo2 = new UserInfo("test");
UserInfo *userInfo3 = new UserInfo("hello");
UserInfo *userInfo4 = new UserInfo("world");
UserInfo *userInfo5 = new UserInfo("hello");
delete userInfo1;
delete userInfo2;
delete userInfo3;
delete userInfo4;
delete userInfo5;
}
void testAllocObject() {
for (int i = 0; i < 10000000; i++) {
allocObject();
}
}
複製代碼
測試結果以下表:
從二者的原理與編譯產物上分析,理論上,性能對比應以下表:
基於上述的幾個維度的分析,C++跨平臺與Kotlin Mutiplatform各有優劣。對於一個跨平臺項目的技術選型,若是穩定性、可靠性是最須要關心的,那已經發展地十分紅熟的C++跨平臺成爲首選;若是項目主要運行在Android平臺上,且項目開發者對Kotlin也比較熟悉(好比Android開發者),那麼Kotlin Multiplatform也是一個不錯的技術嘗試。整體來講,C++跨平臺最爲成熟穩定,性能也高效,而Kotlin Multiplatform做爲一種新技術,具有不錯的前景,也是一種不錯的跨平臺技術嘗試。
互娛研發正在大量招聘客戶端研發(安卓/iOS)
點擊連接進行內推:內推連接
發送簡歷到: wangchengyi.1@bytedance.com