### 一、跨平台技术的出现背景 ##### 1. 原生开发 Android基于Java或者Kotlin, iOS基于Objective-C或Swift,直接调用各自平台的SDK开发的应用程序; - 优点:能快速访问各种硬件功能(GPS、摄像头)速度快、性能高、可以实现复杂动画及绘制,整体用户体验好 - 缺点: 开发成本高,一个应用要维护两套代码,一致性差 为了加快产品的开发周期,提高开发效率,保持多端的一致性,逐渐出现了多种跨平台技术,下面选取一些常用技术作为对比: ##### 2. H5+原生混合开发 H5技术在各类app中使用的频率非常高,通过原生的网页加载控件WebView (Android)或WKWebView(iOS)来加载H5页面,而WebView实质上就是一个浏览器内核,其JavaScript依然运行在一个权限受限的沙箱中,所以对于大多数系统能力都没有访问权限,如无法访问文件系统、不能使用蓝牙等。所以我们通常要使用JsBridge来进行两端的通信,虽然web技术栈社区及资源丰富,但是性能不够好,对于复杂用户界面或动画,容易出现卡顿和掉帧。 ##### 3. React Native React Native (简称RN)是Facebook于2015年4月开源的跨平台移动应用开发框架,是Facebook早先开源的JS框架 React 在原生移动应用平台的衍生产物,目前支持iOS和Android两个平台。RN使用Javascript语言,类似于HTML的JSX,以及CSS来开发移动应用。 ##### 4. Weex Weex是阿里巴巴于2016年发布的跨平台移动端开发框架,思想及原理和React Native类似,最大的不同是语法层面,Weex支持Vue语法和Rax语法,Rax 的 DSL(Domain Specific Language) 语法是基于 React JSX 语法而创造。与 React 不同,在 Rax 中 JSX 是必选的,它不支持通过其它方式创建组件,所以学习 JSX 是使用 Rax 的必要基础。而React Native只支持JSX语法。 ###二 、Flutter的优势 Flutter和以上几种跨平台技术最大的不同是其自行实现一套渲染框架,可通过调用skia等方式完成自渲染,而不依赖于原生控件,也不用依赖于webview,使用的语言也从JavaScript变成了Dart。 ##### 1. 自绘引擎Skia Flutter使用Skia作为其2D渲染引擎,Skia是Google的一个2D图形处理函数库,包含字型、坐标转换,以及点阵图都有高效能且简洁的表现,Skia是跨平台的,并提供了非常友好的API,目前Google Chrome浏览器和Android均采用Skia作为其绘图引擎。 ##### 2. Dart语言 Flutter使用Dart而不是JavaScript作为开发语言,主要有如下考虑: - 基于JIT的快速开发周期:Flutter在开发阶段采用,采用JIT模式,这样就避免了每次改动都要进行编译,极大的节省了开发时间; - 基于AOT的发布包: Flutter在发布时可以通过AOT生成高效的ARM代码以保证应用性能。而JavaScript则不具有这个能力。 ##### 3. 总结: |技术类型 | UI渲染方式 | 性能 | 开发效率| 动态化| 框架代表| | ---- | ---- | --- | --- | --- | --- | | H5 | WebView 渲染 | 一般 | 高 | 支持 | Cordova、Ionic | | JavaScript+原生渲染 | 好 | 中 | 中 | 支持 | RN、Weex | | Flutter | 好 | 高 | 高 | 不支持 | Flutter | ### 三、Flutter框架结构 ![image.png](https://upload-images.jianshu.io/upload_images/2894274-5c0f0a71d9689ede.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) #####1. Flutter Framework - 底下两层(Foundation和Animation、Painting、Gestures)在Google的一些视频中被合并为一个dart UI层,对应的是Flutter中的dart:ui包,它是Flutter引擎暴露的底层UI库,提供动画、手势及绘制能力。 - Rendering层,这一层是一个抽象的布局层,它依赖于dart UI层,Rendering层会构建一个UI树,当UI树有变化时,会计算出有变化的部分,然后更新UI树,最终将UI树绘制到屏幕上,这个过程类似于React中的虚拟DOM。Rendering层可以说是Flutter UI框架最核心的部分,它除了确定每个UI元素的位置、大小之外还要进行坐标变换、绘制(调用底层dart:ui)。 - Widgets层是Flutter提供的的一套基础组件库,在基础组件库之上,Flutter还提供了 Material 和Cupertino两种视觉风格的组件库。而我们Flutter开发的大多数场景,只是和这两层打交道。 #####2. Flutter Engine - 这是一个纯 C++实现的 SDK,其中包括了 Skia引擎、Dart运行时、文字排版引擎等。在代码调用 dart:ui库时,调用最终会走到Engine层,然后实现真正的绘制逻辑。 ###四、Flutter 基础 - [Flutter for Android开发者](https://flutterchina.club/flutter-for-android/) - [Flutter for iOS开发者](https://flutterchina.club/flutter-for-ios/) - [Flutter for Web开发者](https://flutterchina.club/web-analogs/) ###五、 混合环境配置 在实际开发中,对于这种新型技术,为了降低引入的风险,一般都会逐步地迁移和体验,这样就涉及到混合项目的搭建,native和flutter两端的通信交互问题,下面以主项目MyApp为例,记录一下混合接入步骤: 开发环境: ``` [✓] Flutter (Channel unknown, v1.12.13+hotfix.6, on Mac OS X 10.15.3 19D76, locale zh-Hans-CN) [✓] Android toolchain - develop for Android devices (Android SDK version 29.0.2) [✓] Android Studio (version 3.5) ``` ######1. [ 国内镜像配置](https://flutter.dev/community/china)(强烈推荐,可以极大提高分支切换和升级速度。以下两者任选其一,有时候一个不行可以切换另外一个) 镜像1: >export PUB_HOSTED_URL=https://pub.flutter-io.cn export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn 镜像2: > export FLUTTER_STORAGE_BASE_URL=https://mirrors.sjtug.sjtu.edu.cn export PUB_HOSTED_URL=https://dart-pub.mirrors.sjtug.sjtu.edu.cn ######2. 在`MyApp/app/build.gradle` 添加so库相关过滤 ``` android { //... defaultConfig { ndk { // Filter for architectures supported by Flutter. abiFilters 'armeabi-v7a', 'arm64-v8a' } } } ``` ###### 3. 进入到`MyApp的父级文件夹`下,创建my_flutter module ``` cd some/path/ flutter create -t module my_flutter ``` ###### 4. 在`MyApp/app/build.gradle` 文件中增加java8支持 ``` android { //... compileOptions { sourceCompatibility 1.8 targetCompatibility 1.8 } } ``` ###### 5. 在`MyApp/settings.gradle`中,添加以下依赖 ``` include ':app' // assumed existing content setBinding(new Binding([gradle: this])) // new evaluate(new File( // new settingsDir.parentFile, // new 'my_flutter/.android/include_flutter.groovy' // new )) // new ``` ###### 6. 在`MyApp/app/build.gradle`中,依赖flutter 模块 ``` implementation project(':flutter') ``` ###### 7. 在AndroidManifest.xml中注册FlutterActivity ``` ``` ###### 8. 任意页面添加跳转代码,以在TestActivity中为例 ``` myButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { startActivity( FlutterActivity.createDefaultIntent(TestActivity.this) ); } }); ``` ###六、 混合栈管理,FlutterBoost的使用 ![image.png](https://upload-images.jianshu.io/upload_images/2894274-2f7ba0316f8c4596.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 为了实现高效、便捷的栈管理,推荐使用闲鱼开源的[FlutterBoost](https://github.com/alibaba/flutter_boost/blob/master/README_CN.md),下面以Android support包和Flutter Version v1.12.13为例,记录使用过程(实际接入的时候,androidx或者support在不同的flutter version下会出现一些不兼容的情况,增加了试错成本)。 ######1. 在`my_flutter/pubspec.yaml`目录下添加flutter端依赖 ``` flutter_boost: git: url: 'https://github.com/alibaba/flutter_boost.git' ref: 'task/task_v1.12.13_support_hotfixes' ``` ###### 2. 在Android端 Application的onCreate中添加初始化代码 ``` public class MyApp extends Application{ @Override public void onCreate() { initFlutterBoost() } } private void initFlutterBoost() { INativeRouter router = new INativeRouter() { @Override public void openContainer(Context context, String url, Map urlParams, int requestCode, Map exts) { String assembleUrl = Utils.assembleUrl(url, urlParams); PageRouter.openPageByUrl(context, assembleUrl, urlParams); } }; FlutterBoost.BoostLifecycleListener boostLifecycleListener = new FlutterBoost.BoostLifecycleListener() { @Override public void beforeCreateEngine() { } @Override public void onEngineCreated() { FlutterBoost.instance().engineProvider().getPlugins().add(new FlutterLocalStoragePlugin()); FlutterBoost.instance().engineProvider().getPlugins().add(new UserManagerPlugin()); } @Override public void onPluginsRegistered() { } @Override public void onEngineDestroy() { } }; Platform platform = new FlutterBoost .ConfigBuilder(this, router) .isDebug(true) .whenEngineStart(FlutterBoost.ConfigBuilder.ANY_ACTIVITY_CREATED) .renderMode(FlutterView.RenderMode.texture) .lifecycleListener(boostLifecycleListener) .build(); FlutterBoost.instance().init(platform); } ``` ###### 3.在AndroidManifest.xml中注册BoostFlutterActivity ``` ``` ######4.Flutter端路由注册 ``` class _MyAppState extends State { @override void initState() { super.initState(); FlutterBoost.singleton.registerPageBuilders({ 'userInfoPage': (pageName, params, _) { print("userInfo params:$params"); return UserInfoPage(params); }, 'flutter://mineFragment': (pageName, params, _) { return MinePage(params); }, }); } @override Widget build(BuildContext context) { return MaterialApp( title: 'Test-Flutter', builder: FlutterBoost.init(postPush: _onRoutePushed), home: Container()); } } ``` ######5.Native端路由注册 ``` public class PageRouter { public final static Map pageName = new HashMap () {{ put("flutter://userInfoPage","userInfoPage"); }}; public static final String NATIVE_LOGIN_ACTIVITY_URL = "native://loginActivity"; public static boolean openPageByUrl(Context context, String url, Map params) { return openPageByUrl(context, url, params, 0); } public static boolean openPageByUrl(Context context, String url, Map params, int requestCode) { String path = url.split("\\?")[0]; Log.i("openPageByUrl", path); try { if (pageName.containsKey(path)) { Intent intent = BoostFlutterActivity.withNewEngine().url(pageName.get(path)).params(params) .backgroundMode(BoostFlutterActivity.BackgroundMode.opaque).build(context); if (context instanceof Activity) { Activity activity = (Activity) context; activity.startActivityForResult(intent, requestCode); } else { context.startActivity(intent); } return true; } else if (url.startsWith(NATIVE_LOGIN_ACTIVITY_URL)) { Intent intent = new Intent(context, LoginActivity.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); return true; } return false; } catch (Throwable t) { return false; } } } ``` ###### 6.创建一个FlutterFragment ``` FlutterFragment mMineFragment = new FlutterFragment.NewEngineFragmentBuilder().url("flutter://mineFragment").params(params).build(); ``` ######7.从Flutter页面启动一个NativeActivity ``` FlutterBoost.singleton.open("native://loginActivity"); ``` ######8.从Flutter页面启动一个FlutterActivity ``` FlutterBoost.singleton.open("flutter://userInfoPage", urlParams: _userInfo); ``` 可以看到FlutterBoost为Native和Flutter的跳转提供了统一标准,我们只需要在Native和Flutter两端维护一个路由表,就可以方便地实现混合栈的管理和参数传递 ###七、Android与Flutter的通信 在实际的开发过程中,我们时常要从flutter端访问native的数据或者硬件能力,这个时候需要使用到Platform Channel进行通信 ##### 1. Platform Channel 分类和简介 - BasicMessageChannel: 支持字符串和半结构化的数据传递 - MethodChannel: 支持方法调用,既可以从Flutter发平台发起方法调用,也可以从平台代码向Flutter发起调用 - EventChannel: 支持数据流通信 ##### 2. 插件的编写 使用频率最高的主要是MethodChannel,下面是其主要流程示意图 ![image.png](https://upload-images.jianshu.io/upload_images/2894274-5fddb4acff5b5575.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 下面我们以MethodChannel的方式,实现一个退出登录的功能 ######2.1 Flutter端注册一个MethodeChannel并调用 ``` class UserManager { MethodChannel _methodChannel; static UserManager _instance; UserManager._internal() { if (_methodChannel == null) { _methodChannel = new MethodChannel(ChannelConstants.FLUTTER_USER_MANAGER); } } static UserManager getInstance() { if (_instance == null) { _instance = new UserManager._internal(); } return _instance; } Future logout() async { bool logout = await _methodChannel.invokeMethod("logout"); return logout; } } void _logout() async { bool isLogout = await UserManager.getInstance().logout(); if(isLogout){ print("退出登录成功") }else{ } } ``` ###### 2.2 Android端插件的编写 ``` public class UserManagerPlugin implements MethodChannel.MethodCallHandler, FlutterPlugin { @Override public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { final MethodChannel methodChannel = new MethodChannel(FlutterBoost.instance().engineProvider().getDartExecutor().getBinaryMessenger(), ChannelConstants.FLUTTER_USER_MANAGER); methodChannel.setMethodCallHandler(this); } @Override public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { } @Override public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { if (call.method.equals("logout")) { //这里的UserManager是Android端的实现,是真正的退出登录,清空token等用户数据 UserManager.getInstance().logout(); //这里给Flutter回调结果 result.success(true); } } } ``` ###### 2.3 Android端的插件注册 在之前Application中执行的FlutterBoost的初始化代码的onEnginCreated回调中注册 ``` FlutterBoost.BoostLifecycleListener boostLifecycleListener = new FlutterBoost.BoostLifecycleListener() { @Override public void beforeCreateEngine() { } @Override public void onEngineCreated() { FlutterBoost.instance().engineProvider().getPlugins().add(new UserManagerPlugin()); } @Override public void onPluginsRegistered() { } @Override public void onEngineDestroy() { } }; ``` 这样我们在Flutter端调用UserManager.logout时,会通过注册的MethodChannel通道,调用到Native端的UserManager.logout方法,然后通过result.success(true)拿到调用结果 具体Platform Channel的底层实现原理,后面会用单独的篇幅介绍 ![image.png](https://upload-images.jianshu.io/upload_images/2894274-a340d9697b59b522.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) ![image.png](https://upload-images.jianshu.io/upload_images/2894274-1b05cf7a2f84b6f7.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)