跨平台技术方案浅析

我正在参加跨端技术专题征文活动,详情查看:juejin.cn/post/710123…

写在最前

跨平台其实是牺牲部分功能和体验,换取开发速度和一致性的权衡,并不是业务开发的银弹。 -- 宁愿写两遍代码,也不用 C++ 跨 iOS、Android 平台开发?

现阶段跨平台方案尤其是Flutter是比较流行的话题,本文对现阶段跨平台方案做一个基本的盘点,并介绍一下目前热度最高的Flutter的开发特点和优缺点以及Android 开发人员比较感兴趣的Kotlin Multiplatform。如果你想了解目前主流的跨平台方案或者正在做跨平台方案调研,那这篇文章有可能会帮助你。

现阶段跨平台方案简介

WebView容器

使用Native自带的WebView来做渲染,并且通过JSBradge调用native平台的能力。这种方案拥有很好的动态化能力,但启动时需要预加载。常见方案比如Cordova,DCloud等小程序容器。

解释性语言做Native渲染

在Android UI库和iOS UI库之上创建一个抽象层,意图消除每个平台上表现的不一致。应用代码常被写成类似JavaScript的解释性语言,且基于Android 和iOS原生代码来展示UI。可以达到比WebView方案更好的性能,同时也可以服务器下发JS代码,实现热更新的能力。比如React Native,Weex。其中有一些技术方案可以支持编译成小程序,而且也支持编译成使用Weex渲染的App,例如Uni-app,Chameleon。

自建引擎

脱离native的UI组件,使用渲染引擎直接绘制界面。这种方案的优势是执行时不需要中间语言来中转,所以会得到比JS Bridge更好的性能,并且消除了平台UI组件的差异。但是实现热更新能力较为困难。常见方案Flutter,Unity。Unity除了可以绘制3d内容外也拥有类似Flutter开发的组件包UIWidgets,可以得到类似Flutter的开发体验,但目前生态较差。

跨平台共享代码

使用平台都支持的语言(C/C++)或者将语言编译为多个平台特定语言(Java,Swift,JS等)实现跨平台共享代码。解决方案C/C++,Xamarin,Kotlin Multiplatform 。C/C++写业务逻辑门槛较高,并且调用原生库较为困难。Xamarin是由微软维护,并且使用C#编写跨平台业务逻辑 what is xamarin 。国内关注的人不多并且生态不完善,并且需要团队有成熟的.NET技术体系,并且据目前调研Xamarin无法渐进式使用,必须将整个应用迁移至Xamarin。微软要在将xamarin融合进入.net 6并且完成后更名为MAUI。但MAUI目前仍在预览版本,在移动互联网的浪潮下,.net生态掌握的不多,对此感兴趣可以关注下最新进展MAUI GITHUB。Kotlin Multiplatform会在下面章节介绍。

Flutter

Flutter框架简介

Flutter的目标是同一套代码同时运行在Android和iOS系统上,并且随着技术的演进目前在Flutter3.0 上也正式支持了Web、Windows、macOS、Linux的平台。

Flutter绕过了系统UI Widget库,将Dart代码编译成平台代码,利用Skia引擎直接绘制Flutter自己的Widget。因此Flutter会有好的性能表现和更好的多端UI一致性。

Flutter在Web提供了两种渲染方式,HTML渲染和CanvasKit渲染。 CanvasKit渲染和桌面端的原理类似,但会额外增加2MB的引擎大小,并且这种方式要处理图片的跨域问题。

Flutter架构概览

KoVIrk1rpqllacYY.png

JwtJVcoMSjwDWBfC.png

项目结构

├───android
├───ios
├───lib // flutter代码
├───test
├───web
└───pubspec.yaml // 包管理文件
复制代码

声明式UI

声明式(declarative) 区别于传统命令式(imperative)

命令式编程:命令“机器”如何去做事情(how),这样不管你想要的是什么(what),它都会按照你的命令实现。

声明式编程:告诉“机器”你想要的是什么(what),让机器想出如何去做(how)。

响应式编程:跟以上没有必然的联系,主要强调事件驱动。

万物皆Widget

Dart

Flutter使用了Dart语言来进行应用开发,Dart有着不少 Java、Kotlin 和 JS 的影子。Dart 可以编译成 ARM 和 x86 代码,因此 Dart 移动应用程序可以在 iOS,Android 及 更高版本上实现本地运行。 对于 web 应用程序,Dart 可以转换为 JavaScript。基础语法不再赘述详情参考官方文档。这里介绍Dart几个特性。

空安全

Dart在2.12之后引入了空安全声明。

// 非空
String content = "Talk is cheap. Show me the code.";
print("string length ${content.length}");
// 可空
String? content = null;
print("content length ${content?.length ?? 0}");
复制代码

异步操作

Dart 使用aysnc 字段来表示异步方法,异步方法必须返回Future对象。使用方法如下:

// 异步方法
Future<List<Event>> getEvents(int page) async {
    // 网络请求
    return EventService.getEvents(page);
}

// 可以使用类似Promise的方式获取对象
Future<List<Event>> events = EventRepository.getUserReceivedEvents(user?.login, _page)
events.then((value) => this._events = value)
    .catchError((e) {
        print(e);
    });

// 也可以在async方法中使用await关键字获取Future内部的对象
List<Event> events = await EventRepository.getUserReceivedEvents(_page);
复制代码

要注意的是Dart是单线程模型,async方法的执行都在同一线程,因此async方法只适合执行耗时等待的操作比如网络请求,而不适合执行CPU密集型的操作,因为这样会导致线程阻塞直到async方法运行完毕。

线程模型

Dart的多线程是内存不共享的,更近似于进程的概念。线程间使用port通信。Dart提供了Isolate和基于Isolate封装的compute来进行多线程操作

void isolateTest() async {
  print("外部代码 1");
  ReceivePort port = ReceivePort();
  Isolate iso = await Isolate.spawn(isoFunc, port.sendPort);
  port.listen((message) {
    _data = message;
    port.close();
    iso.kill();
  });
}

void isoFunc(SendPort port) {
  sleep(Duration(seconds: 2));
  port.send("msg");

}

void computeTest() async {
  print("外部代码 1");

  compute(computeFunc, "msg").then((value) => print("value = $value"));

  print("外部代码 2");
}

String computeFunc(String message) {
  print("computeFunc ${message}");
  return message + "compute";
}
}
复制代码

mixin

mixin是为面向对象程序设计语言中的类提供了方法的实现,其他类可以访问mixin类的方法、变量而不必成为其子类。Mixin 的作用就是在多个类层次结构中重用类的代码。参考文章

abstract class Animal {}

mixin Run {
  void run() => print("会跑");
}

mixin Fly {
  void fly() => print("会飞");
}

// 猫可以行走,这里没有重写Run中的方法
class Cat extends Animal with Run {}
// 鸟可以行走、飞
class Bird extends Animal with Fly, Run{

  @override
  void flying() {...}
}
复制代码

常见问题及解决方案

Flutter与Native通信

Flutter自带了事件通信工具

7zljWa5c7WQvpC2U.png

官方文档

在flutter上调用方法

// channel name 便于分组
static const platform = const MethodChannel('battery');

Future<int> _getBatteryLevel() async {
  try {
    return await platform.invokeMethod('getBatteryLevel');
  } on PlatformException catch (e) {
    print("Failed to get battery level: '${e.message}'.")
    return 0;
  }
}
复制代码

在Android监听方法调用后回调结果给flutter

class MainActivity : FlutterActivity() {

    private val CHANNEL = "battery"

    override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
            if (call.method == "getBatteryLevel") {
                // todo 实现方法
                val batteryLevel = getBatteryLevel()
                if (batteryLevel != -1) {
                    result.success(batteryLevel)
                } else {
                    result.error("UNAVAILABLE", "Battery level not available.", null)
                }
            } else {
                result.notImplemented()
            }
        }
    }
}
复制代码

在iOS也是同Android类似

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

    let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
    let batteryChannel = FlutterMethodChannel(name: "battery", binaryMessenger: controller.binaryMessenger)
    batteryChannel.setMethodCallHandler({
      (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
      // Note: this method is invoked on the UI thread.
      guard call.method == "getBatteryLevel" else {
        result(FlutterMethodNotImplemented)
        return
      }
      // todo 实现方法
      self?.receiveBatteryLevel(result: result)
    })

    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}
复制代码

屏幕适配

图片分辨率适配

Flutter和图片分辨率大小适配和移动端方案类似,按照分辨率倍率放置不同的文件夹,系统会自动选择对应的文件。

flutter:
assets:
- images/cat.png
- images/2x/cat.png
- images/3.5x/cat.png
复制代码

屏幕大小适配

Flutter的使用了逻辑像素的概念。公式为分辨率宽度 = devicePixelRatio * 实测宽度,和现有方案类似,不过现阶段比较流行小程序方案的rpx适配,采用短边固定750宽度来做缩放。参考文章

在现有工程引入Flutter

在现有工程引入Flutter的时候和原生平台引入新module的方式一样,但是在一些业务场景会有一些额外的问题需要关注。

flutter页面和原生页面互跳

  1. Flutter页面跳转原生页面需使用platform channel解决。
  2. 原生页面指定Flutter页面,需要在启动Flutter引擎的时候携带参数,Flutter根据参数启动不同页面。
  3. 复杂页面栈,比如页面打开路径为: native -> flutter -> native -> flutter。这种情况为了保证栈的正确,需要打开两个Flutter页面。

内存占用较高

  1. Flutter的引擎加载需要耗费一定的内存(一个Engine 在Android有19M的占用,iOS有13M)。以前的方案每多一个Engine ,可能就会多出 19MB Android 和 13MB iOS 的占用。在Flutter2.0后第二个只需要180kb,不过不同的页面还是在不同的引擎中,第三方的混合栈管理目前的问题仍然比较多。
  2. 同样的图片Native图片加载会内存缓存,Flutter自身提供的图片库也存在缓存,这2个缓存相互隔离,导致占用的内存空间增大。有第三方的方案可以使Flutter和native共用缓存。

启动Flutter页面短暂白屏

第一次启动Flutter页面会出现短暂白屏情况,在原生App中打开Flutter页面时会表现的更加明显,所以需要在合适的时机进行Flutter引擎的预热。

在现有工程引入Flutter情况下使用flutter_boost可以解决大部分问题。

Flutter 开发框架的优点

高性能

  1. 预先(AOT)编译,运行时直接执行Native(arm)代码。
  2. 必需的同Native通信(channel)是C++层次,性能好。并且通信频率要远小于RN的方案。
  3. Skia团队针对Flutter优化。

开发成本低

  1. 代码复用率高(仅少数功能需要使用native实现,比如扫码,判断网络可用性)
  2. 支持平台多,不仅支持Android和iOS还支持Web,Windows,Linux,MacOS。

视觉稿还原度极高

  1. Flutter对Skia的控制能力,比Android传统应用通过XML间接操作Skia的能力更加直接和强大。
  2. 使用自建引擎使得各个平台的UI差异非常小。

上手难度低

Dart和其他高级语言有很大共同之处,容易上手。

轻量化游戏场景

有些业务可能会需要嵌入一些轻量化的游戏场景,比如盒马小镇之类的。这种场景使用Unity或者Cocos 2d都比较重,并且可能需要原生页面的部分页面是游戏场景这种情况使用Unity和Cocos比较困难,但使用Flutter就方便的多。官方推荐使用flame开发轻量级游戏。闲鱼之前也说要发布Flutter Candy游戏引擎,然而三年过去了。。。。。

Flutter现阶段缺点

Widget能力不足

  1. Flutter的WebView稳定性不如Native。
  2. 大量网络图片同时加载性能不好而且缺失磁盘缓存能力。
  3. 太过复杂的页面(比如电商首页)和无限滚动列表性能表现不够好。但除了少数极端的场景外,已经不用考虑Flutter开发时性能方面的问题。
  4. 一些第三方sdk没有flutter sdk提供。

包占用大小

实测在Flutter2.0版本。Flutter引擎和dart库代码在iOS大小为12.4M Android为5.6M。Web端开启CanvasKit渲染引擎大小为2.3M。因此如果探索性引入注意包体积增长。

Flutter生态还不成熟

一些native的已经成熟的功能在Flutter可能没有完善的解决方案。

Flutter目前还在高速发展,导致官方的更新速度较快,产生的破坏性更新的几率也很高,会导致Flutter升级存在一些兼容性问题。

Flutter 仍然有许多坑需要踩。比如使用CDN加载图片资源仍然比较麻烦Web FAQ

跨平台不代表不需要对平台特殊编写UI代码

很多人觉得Flutter最大的亮点是可以只用一套代码在各个平台运行,可以节省很多开发时间。但手机端和桌面端屏幕大小和操作习惯是不同的,如何让各端都拥有良好的体验,这也是我们需要考虑和应该做的。

嵌套地狱

Flutter就连Padding和Margin都是Widget。导致很容易出现层级树过深的问题。但可以通过抽离成组件或使用扩展函数解决。

Kotlin Multiplatform

简介

Kotlin Multiplatform是由JetBrains 维护的 Kotlin语言发展并孵化出来的项目。Kotlin原本的设计目标是要兼容JVM,并且与Java100%的互操作,并且要比Java更安全和更简洁。随着Kotlin的发展和完善,2017年Google宣布在Android上为Kotlin提供一级支持,在2019年Google宣布Android采用Kotlin First。后来随着Kotlin在Android的成功,Kotlin和Java互操作的技术成熟,设计目标也演变为支持全平台互操作。

Kotlin Multiplatform与以往各类知名跨平台移动开发技术有所区别。其它技术不是抽象化就是全面取代平台特定应用开发,而Kotlin Multiplatform 是对当前平台特定技术的补充,致力于取代平台无关性业务逻辑。换言之,Kotlin Multiplatform是工具箱中新增的工具,而非取代整个工具箱。 -- 出自 Netflix APP已经用上Kotlin 跨平台(KM)了

参考资料 Dropbox 放弃使用c++跨平台KM与C++

技术现状

比较遗憾的是Kotlin Multiplaform目前整体还处于Alpha阶段,但Android、iOS领域是Kotlin的首要落地场景,目前已具备可用性,常用的工具库也有相应的跨平台支持比如: ktor(网络请求)、io、序列化、并发、数据库。官方跨平台支持组件

compose-jb

Google发布了类似Flutter的Android端的声明式UI框架Jetpack Compose。得益于Jetpack Compose的开放性,JetBrains在桌面端和Web实现了Jetpack Compose的UI接口。使得Kotlin也可在桌面端、Web开发UI。也许在未来Kotlin的开发体验和开发效率能够完全超越Flutter。但目前JetBrains对compose-jb在iOS端的优先级较低。compose-jb github

使用简介

Kotlin Multiplatform分为公共模块和平台特定模块。公共模块用来存放各个平台公共逻辑,并且可以调用其他支持Kotlin Multiplatform的模块。平台特定模块(Kotlin/JVM, Kotlin/JS, Kotlin/Native)提供特定与平台的原生代码进行交互。例如:

//Common
expect fun randomUUID(): String
//Android
import java.util.*
actual fun randomUUID() = UUID.randomUUID().toString()
//iOS
import platform.Foundation.NSUUID
actual fun randomUUID(): String = NSUUID().UUIDString()
复制代码

OL2SZfFuN7UltxtU.png

工程结构

└───src
│   ├───commonMain
│   ├───jsMain
│   ├───jvmMain
│   ├───iOSMain
│   └───androidMain
└───build.gradle.kts // 构建文件
复制代码

借助Gradle可以将lib被不同平台引入,比如Web使用webpack,iOS使用Cocoapods。

D-KMP

基于声明式UI,Kotlin跨平台和MVI模式可以极大的提高共享代码的占比。详细介绍未来的APP:声明式UI+Kotlin跨平台(D-KMP)

优缺点分析

优点

  1. Kotlin Multiplatform的编译产物与原生平台一致,可以与原生平台互操作,这意味可以有原生平台的性能,并且拥有方法级别的灵活性。
  2. 对比与其他跨平台方案对于原生平台的另起炉灶重新开发来讲,Kotlin Multiplatform的方案更加平滑和灵活。
  3. Android平台现阶段大多数使用Kotlin开发,稍作改造即可跨平台共享。
  4. Kotlin的支持平台最多,甚至可以与服务端共享代码(然而实际应用场景较少)。

缺点

  1. 目前仍处于Alpha阶段,虽然有Google和JetBrains背书,不过未来尚不明朗。
  2. 对比与Flutter可以共享90%以上的代码,Kotlin Multiplatform的UI仍然需要各自实现,使用薄UI层的架构能共享75%左右。
  3. 使用Gradle构建工程,有一定的上手难度。

总结

各个平台本身存在底层架构的差异,不能真正的做到Write Once, Run Anywhere,可能会花费大量时间编写特定与平台的代码。并且跨平台的第三方库生态往往没有原生的高,也可能需要花费大量的时间来构造基础组件。最终也许会导致使用跨平台方案的并没有提升多少开发速度。

目前来看跨平台方案做业务开发主流的有React Native、Flutter。从原理上来讲Flutter会比RN所面临的平台差异性较少,所以现阶段更被推荐使用。而Kotlin只求作为原生平台的补充不要求替代原生平台的方案在未来也会有一席之地,并且当未来compose-jb获得全平台支持后Kotlin的跨平台开发体验和效率也许会超越Flutter。

总的来说,目前跨平台方案仍然处于百花齐放状态,各种方案都有优劣,都有比较合适的使用场景,并不存在某个框架一统天下的情况。技术一直在发展,作为程序员的我们不应该躲在舒适区而对不了解的技术排斥和抵触。

感谢阅读,与君共勉。

猜你喜欢

转载自juejin.im/post/7104696657983307812
今日推荐