一直以来,跨平台技术被普遍探索与研究。时至今日,在不涉及界面层面的跨平台技术上,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