ArkUI 与 跨平台UI技术的发展

本文为稀土技能社区首发签约文章,30天内制止转载,30天后未获授权制止转载,侵权必究!

番外篇

在学习完前面几篇关于ArkUI的文章后,咱们来学习一篇番外篇,首要叙述鸿蒙开展的进程以及相关跨渠道技能的开展,了解ArkUI背面的规划思路以及与跨渠道技能的联系

这些年,写过Compose,也了解过Flutte、ReactNative ,可是我在ArkUI中,背面看到了跨渠道技能的另一个方向,希望本期文章可以给你不相同的视觉

ArkUI与跨渠道UI技能的开展

可能你现已看过许多跨渠道的文章了,或许现已接触了许多跨渠道的结构,比方ReactNative,Flutter等,但其实跨渠道UI永远离不开三个中心,这儿我把其称为UI的言语+ UI表明 + 烘托引擎。

UI言语:用来生成UI表明的载体,比方咱们经过Java/Kotlin 可以描绘View的行为,也可以经过Dart描绘Widget的行为,也可以经过 ,JS言语经过ReactNative 描绘生成的控件。

UI表明:即描绘一个或许多个烘托引擎的笼统。比方表述一个图形的长宽、色彩、父子包含联系等

烘托引擎:烘托引擎广义上来说,具有与底层图形交互才能的且具有图形指令生成的,咱们都可以称为烘托引擎。比方OpenGLES。

这儿值得注意的是,UI表明与烘托引擎是可以独立的。比方Android开发中,咱们即便不运用View,也仍旧可以经过OpenGL的才能进行图形烘托,画出自己想要的东西。

而UI的言语,它也可以说是独立的,比方咱们可以经过写JS的办法来写Android,也可以经过JS的办法来写“Flutter”,做到这些仅仅需求咱们进一步的修改算了。

为什么挑选ArkTS

在ArkUI的挑选的言语终究被主推为ArkTS,这儿咱们也可以简略以为,抛去华丽的外壳之后,便是“JS”。这儿许多人会有一个误区,便是JS那么慢,它能支撑起来APP的运转吗?

一个言语慢不慢,其实并不取决于言语自身,而是取决于它的行为,正常情况下的JS代码,一般是以下图办法运转的。

ArkUI 与 跨渠道UI技能的开展

咱们可以看到,许多要害的过程,比方生成机器码等,都是运转时才会触发的,比方传统JS在网页运转,你是需求花费必定的时间去解析JS文件然后再去真正运转的,这也是为什么咱们会有一个固有形象JS很慢的一个原因。而它也有一个好处便是,既然解析代码发生在运转时,咱们只需求替换代码文件,就能做到运转时的动态化,这种动态化才能满意了许多明晰的需求,比方网页需求常常动态改写。

可是关于APP来说,运转时的开支往往是不行接受的,比方加载的等候时间就会损失许多用户,这是由于移动端的性质决定的,正常网速好的情况下,用户可能没有感知到网页加载与APP原生的区别,可是假如在一些杂乱的环境,比方地铁站这种弱网条件下还去改写网站,那么这儿的体会是十分差的(毕竟很少有人会在地铁站用电脑等pc设备刷网页,可是你一点会带有手机)。一起APP尽管也有动态化的需求,可是关于厂商来说,动态化才能往往也是不行猜测的双刃剑,谁也不想一个学习APP第二天就变成了“菠菜”软件。因而关于手机厂商来说,动态化的支撑并不是必要的。因而AOT通常代表”Ahead of Time”,提早编译好机器码,便是为了处理这种运转时的JIT消耗而发生。

那么JS能不能做到”Ahead of Time”呢?当然是可以的,比方ReactNative的Hermes 引擎就支撑直接解析编译好的字节码。相同,ArkUI挑选的ArkTS,也是往这方面去靠拢,打破固有的js很慢的形象。

ArkUI 与 跨渠道UI技能的开展

ArkTS跟一般咱们写的Java/Kotlin 没有太多不同(只要你是机器码,运转速度都基本相同,仅仅生成的指令有少部分区别算了,你乃至可以直接在鸿蒙写汇编)。它也是提早生成好了适配鸿蒙系统的机器码,在安装的时分就可以直接安装然后避免JS在运转时解析的损耗,满意一个APP言语的规划需求。ArkUI中的Ark Runtime,也满意了一个基本JS引擎解析的才能,然后为后边构建生态打好了根底。

ArkUI的不得不做出跨渠道挑选

跨渠道技能开展至今,按照烘托引擎 与 UI表明的不同组合,首要分为三个部分,

  1. 以网页为中心的webkit

  2. 以系统Native烘托引擎为主,具有以JS编写的UI表明代表ReactNative

  3. 以自制作引擎(Flutter Engine)为主,以Dart编写的UI表明的Flutter

以上三种挑选,ArkUI终究是在旧的道路上挑选,仍是另谋出路呢?

JS引擎与Webkit烘托

网页的开展史比移动端APP要长,一起也愈加老练,由于手机系统自身就默许带着JS解析引擎的浏览器或许JS引擎解析才能的根底控件,WebView。JS引擎系统内置有许多,比方V8,jsc,JavaScriptCore等。移动端手机自身就可以带着浏览器以及JS解析引擎,基于以上特点,开发者其实可以很快把前端的HTML文件进行复用,到达在Android/iOS身上,因而这是一种天然的跨渠道办法。JS侧与Native侧相同可以采用JS Bridge的办法,进行交互。比方在Android,其实实质上便是C++与JNI交互。C++的引擎可以直接履行javascript的解析,得到成果,相同可以经过JNI调用起Java环境的办法。

可是缺点也很明显,该有的损耗仍是在,比方WebView的初始化,以及网页的加载,下载js文件网页文件等,因而这个方向的优化大部分在WebView的预热,或许拦截URL解析,采纳本地资源替换远端下载的资源(俗称“离线包”)等用于处理加载问题,一起烘托侧仍是按照HTML的办法进行烘托。大致的过程分为以下几大步,构建 DOM 树,解析 CSS 文件,构建烘托树,webkit制作 UI。

JS引擎与Native烘托

这类的办法代表是ReactNative,由于在前端在开展时长或许老练度上,其实老练度现已很高了,一起大部分公司也会配备前端开发人员,React便是Web开发中很老练的结构之一。React中编写的代码,其实就可以作为UI的表明了,它具有完好的树形结构(比方组件之间的依靠联系,父子组件的联系,关于一个组件的表述比方巨细,长宽,色彩),因而假如咱们可以把这部分已有的UI表明(见最初界说)运用起来,那么将削减不少本钱。因而ReactNative诞生了,它充分运用JS引擎的性质,即生成UI表明的才能,进行了进一步笼统。在烘托的时分,经过把一系列的UI表明转化为Native View的表明,运用了Native的烘托才能进行复用。

比方你写一个ReactNative的Text,ReactNative 可以简略了解为帮你把这些TS/JS代码经过之前界说好的映射联系变成了原生的写法

import React, {useState} from 'react';
import {Text, StyleSheet} from 'react-native';
const TextInANest = () => {
  const [titleText, setTitleText] = useState("Bird's Nest");
  const bodyText = 'This is not really a bird nest.';
  const onPressTitle = () => {
    setTitleText("Bird's Nest [pressed]");
  };
  return (
    <Text style={styles.baseText}>
      <Text style={styles.titleText} onPress={onPressTitle}>
        {titleText}
        {'n'}
        {'n'}
      </Text>
      <Text numberOfLines={5}>{bodyText}</Text>
    </Text>
  );
};
const styles = StyleSheet.create({
  baseText: {
    fontFamily: 'Cochin',
  },
  titleText: {
    fontSize: 20,
    fontWeight: 'bold',
  },
});
export default TextInANest;

ReactNative的Text其实就转化为了原生的Text,比方Android渠道的ReactTextView。仅仅这些针对React的JS UI表明会被结构内部聚合,处理成一个个针对原生View特点的设置

public class ReactTextView extends AppCompatTextView implements ReactCompoundView {
  private static final ViewGroup.LayoutParams EMPTY_LAYOUT_PARAMS =
      new ViewGroup.LayoutParams(0, 0);
  // https://github.com/aosp-mirror/platform_frameworks_base/blob/master/core/java/android/widget/TextView.java#L854
  private static final int DEFAULT_GRAVITY = Gravity.TOP | Gravity.START;
  private boolean mContainsImages;
  private int mNumberOfLines;
  private TextUtils.TruncateAt mEllipsizeLocation;
  private boolean mAdjustsFontSizeToFit;
  private float mFontSize;
  private float mMinimumFontSize;
  private float mLetterSpacing;
  private int mLinkifyMaskType;
  private boolean mNotifyOnInlineViewLayout;
  private boolean mTextIsSelectable;
  private boolean mShouldAdjustSpannableFontSize;
  private ReactViewBackgroundManager mReactBackgroundManager;
  private Spannable mSpanned;
....
换算成
ReactTextView.setText
ReactTextView.setColor
ReactTextView.setFontSize

举一个很简略的比方就知道了,它其实便是自界说View,你可以在XML文件中写许多自界说的特点,然后View构建时解析这些xml特点并赋值的行为与ReactNative做的工作实质都是相同的,便是针对UI的表明进行替换。

经过直接对接到手机系统自身供给的View,你可以不用关心烘托引擎的处理,由于不同系统会根据自己供给的UI笼统进行制作,这一步跟原生开发是相同的。ReactNative的跨渠道就体现在它提早界说好了一系列的自界说原生控件,然后只需求更具UI表明不同改动自界说原生控件的特点即可。

这种思维也是跨渠道计划中最首要也是运用最广的思维,你可以自界说许多自己的原生控件,你也可以运用其他的JS引擎,只要可以产出UI表明就可以适配控件。乃至你还可以扔掉JS,界说自己的言语,比方你自己写一个文件:

<pika image width:100 height:100>

咱们在原生中需求读取这个文件,遇到pika最初的下一个要害字,比方image,你就new 一个Image控件出来一个意思,只需求处理好映射联系即可。当然,JS引擎供给了一种额定的才能,便是JS与Native的通讯,其实实质便是C++与上层言语的通讯,比方C++与Java。由于大部分JS引擎是C++/C写的,引擎自身写了关于JS的解析逻辑之外,也可以经过一些接口,比方JNI调用起Java供给的办法。

因而你也可以用C/C++去编写自己所谓的“引擎”,让其才能愈加充分。

<pika image width:100 height:100 click:java function:x:f>

比方解析到以java function最初的,我就运用JNI的才能去java中找x类,f办法 ,这些都是可以做到的,仅仅完成起来要杂乱,解析的协议要合适,一起现有的JS引擎与JS言语更老练没必要自己造轮子。许多大厂探索的跨渠道办法,其实思维都是大差不差,实质上仍是UI表明与适配Native View 然后复用Native烘托引擎,比方可以适配鸿蒙的Taro,Compose Multiplatform 。至于UI表明的生成,其实可以有许多,并不只限制在JS,你可以经过适配Compose runtime与鸿蒙组件,用Compose写鸿蒙,你可以用Yoga,用js写Flutter,只要你能处理好UI表明的适配(尽管处理可能会很烦碎),这点是希望咱们知道的。

Dart VM + Flutter引擎

现代Flutter,其实包括Dart编写Widget 与Flutter引擎,这儿的Flutter引擎包含了dart相关的,比方framework.dartobject.dart 等相关的中心烘托完成,还有C++编写的Flutter Engine等。后边我都以“Flutter引擎”作为这些的表述的集合,并不是只包含Flutter Engine。一般Flutter引擎指的是(由C++编写的Flutter Engine),可是这儿笔者有意把render相关的放在了Flutter引擎这个大类中,读者们可以注意一下。

Flutter 凭借渠道相关的烘托View,比方Android 中的SurfaceView,比方FlutterSurfaceView,或许TextureView这些可以供给烘托环境的View,比方供给OpenGLES环境,可以把生成的制作指令直接在这些View上履行,然后到达自烘托的意图。这些制作指令生成依靠于由Dart编写的Widget经过层层转化,比方三棵树后,经过笼统的Layer生成这些制作指令,它并不与原生的一般View交互,比方Flutter的文本可以直接生成满意OpenGLES的文本制作一系列履行,然后画出想要的东西。

OpenGLES咱们也是可以直接运用的,比方咱们界说好SurfaceView,就可以在里面进行编译(OpenGL Shading Language) 全称OpenGL上色言语,编写极点上色器与片元上色器,就可以高兴的用点、线、三角形 去画出咱们想要的UI(手动狗头)。尽管写上色器言语制作出图形是比较杂乱的,可是咱们仍旧是可以在不运用一般View的根底上去画出咱们想要的东西的,比方咱们直接相机给到的流制作场景,不运用原生组件情况下*

ArkUI 与 跨渠道UI技能的开展

极点上色器
layout(location = 0) in vec4 a_Position;
layout(location = 1) in vec2 a_CameraTexCoord;
// The virtual scene texture coordinate is unused in the background shader, but
// is defined in the BackgroundRenderer Mesh.
layout(location = 2) in vec2 a_VirtualSceneTexCoord;
out vec2 v_CameraTexCoord;
void main() {
  gl_Position = a_Position;
  v_CameraTexCoord = a_CameraTexCoord;
}
片元上色器
#extension GL_OES_EGL_image_external_essl3 : require
precision mediump float;
uniform samplerExternalOES u_CameraColorTexture;
in vec2 v_CameraTexCoord;
layout(location = 0) out vec4 o_FragColor;
void main() { o_FragColor = texture(u_CameraColorTexture, v_CameraTexCoord); }
外部调用疏忽,咱们可以找到许多这种case 

回到正题,Flutter其实在UI表明上与烘托引擎上都采纳了不相同的办法,经过Dart生成的UI表明可以天然的对接Flutter烘托引擎。

尽管如此,Dart可以说并不是一个本钱最低的UI表明,一起Flutter Engine 自身其实也依靠了许多由Flutter自身的产品,这些产品都是由framework.dart 或许object.dart 发生的,比方UI的改写逻辑。Flutter中,Dart的UI表明与烘托引擎其实结构上不存在耦合,可是产品上存在耦合,怎样了解呢?你有必要要用Dart去写UI表明,比方Widget的产品LayerTree才可以被FlutterEngine 所识别发生一系列的Scene,感兴趣可以看 scene_builder

作为Flutter的一部分,FlutterEngine的规划十分优秀,也经过了这么多年的开展,于是开发者们也有想过把FlutterEngine作为单独一部分,把UI表明层替换成非Dart编写,是不是也可以呢?

因而,一个奇思妙想出现了,假如咱们可以复用Flutter Engine(烘托引擎部分),一起把Dart替换掉,比方用其他言语也可以完美适配Flutter Engine,岂不是妙哉?比方用TS写Flutter,用Kotlin写Flutter?可不行行呢?

没错,可行,它便是ArkUI的技能起点!

JS引擎 + Flutter 引擎 + 多引擎

笔者阅读了OpenHarmony中关于ArkUI大量的Commit,得出ArkUI的架构布局进程,归于笔者自己的观点,当然华为官方的自身对ArkUI的规划其实还未揭露,咱们可以顺着笔者的思路去看待ArkUI的规划进程~

ArkUI 无论是发布的机遇,仍是华为对其自身的定位,都是一步棋局。咱们最初有讲到,ArkUI不得不做出跨渠道挑选,这是有必要的挑选。在鸿蒙系统发布前期,并没有一个可以杰出循环的生态,因而想要开展自己的系统,那么有必要要在现已的系统上一步步做出变化才行,因而有必要要可以把ArkUI在Android上面跑通。

而ArkUI作为鸿蒙的UI结构,底层上有必要要可以做到一起对接Android系统,一起也要可以对接今后自界说系统的做出预备,因而架构上的挑选是一个大难题。咱们上面学习了三种跨渠道的办法,都是可以参阅的典范。

Flutter作为一个杰出的跨渠道结构,一起Flutter引擎自身就可以对接到Android系统自身,那么假如ArkUI可以快速搬迁到FlutterEngine上,那么适配Android UI系统这个问题就迎刃而解了。

可是搬迁到FlutterEngine上,有几个难点

  1. FlutterEngine大量依靠了Flutter framework中的概念,比方framework.dart,还有UI烘托的适配,而这些是用dart言语编写的。直接采纳Dart编写UI表明,无论是开发者生态仍是后续定制化,都比不上TS/JS,因而,需求把dart这部分搬迁出来,用C++重写

  2. 架构上要支撑多引擎,这对架构规划上愈加严苛

  3. 界说好TS/JS 编写的组件,供给以C++为根底的组件封装,对标dart编写的根底控件

  4. 适配好所有Flutter相关的线程模型,满意Engine需求

这便是ArkUI的挑选,经过ArkTS编写的控件,会经过ArkUI的封装,直接把TS产品对接到引擎上,比方鸿蒙4.0就可以经过默许的Flutter Engine直接适配Android相关的UI展现,在这个过程中,Flutter UI烘托相关的一些好的流程,ArkUI也进行了保留,比方element 更新。一起也把Flutter的三颗树适配为C++的三棵树,ArkUI更新请看这篇

RefPtr<Element> Element::UpdateChildWithSlot(
    const RefPtr<Element>& child, const RefPtr<Component>& newComponent, int32_t slot, int32_t renderSlot)
{
    // Considering 4 cases:
    // 1. child == null && newComponent == null  -->  do nothing
    假如Element 为 null 而且 Component为null,则什么也不做
    if (!child && !newComponent) {
        return nullptr;
    }
    // 2. child == null && newComponent != null  -->  create new child configured with newComponent
    新增:child == null && newComponent != null:经过Component树立对应的element
    if (!child) {
        auto newChild = InflateComponent(newComponent, slot, renderSlot);
        ElementRegister::GetInstance()->AddElement(newChild);
        return newChild;
    }
    // 3. child != null && newComponent == null  -->  remove old child
   删去:child != null && newComponent == null:移除elemnt
    if (!newComponent) {
        ElementRegister::GetInstance()->RemoveItemSilently(child->GetElementId());
        DeactivateChild(child);
        return nullptr;
    }
    // 4. child != null && newComponent != null  -->  update old child with new configuration if possible(determined by
    //    [Element::CanUpdate]), or remove the old child and create new one configured with newComponent.
    更新:child != null && newComponent != null
    auto context = context_.Upgrade();
    不支撑更新,那么删去旧的element,添加新的element
    if (!child->CanUpdate(newComponent)) {
        // Can not update
        auto needRebuildFocusElement = AceType::DynamicCast<Element>(GetFocusScope());
        if (context && needRebuildFocusElement) {
            context->AddNeedRebuildFocusElement(needRebuildFocusElement);
        }
        ElementRegister::GetInstance()->RemoveItemSilently(child->GetElementId());
        DeactivateChild(child);
        auto newChild = InflateComponent(newComponent, slot, renderSlot);
        ElementRegister::GetInstance()->AddElement(newChild);
        return newChild;
    }
    .....
    可以更新
     auto newChild = DoUpdateChildWithNewComponent(child, newComponent, slot, renderSlot);
    if (newChild != nullptr) {
        newChild->SetElementId(newComponent->GetElementId());
        ElementRegister::GetInstance()->AddElement(newChild);
    }
    return newChild;
    ..... 
    return newChild;
}

ArkUI保留了Flutter的规划精华,也做出了许多自己的创新,比方TS控件到C++控件的转化,以及简化了Flutter的页面更新流程,一起在这个根底上也笼统出自己的烘托引擎,以Rosen最初,这个烘托引擎后续就可以替换Flutter引擎,完成愈加杂乱且愈加贴合鸿蒙系统的UI工作。

JS引擎 + Flutter引擎+多引擎的办法,它是ArkUI前史开展的产品,它既保留了Flutter中实践的优点,完成了Flutter引擎与TS的适配,可以在Android系统跑通整个系统,一起最重要的是它为后续切换自己的引擎供给了可能。

总结

经过本章,咱们经过学习当前移动端跨渠道计划的完成,到ArkUI的技能挑选,咱们明晰的了解到了ArkUI背面的规划理念以及规划初衷,ArkUI的适配FlutterEngine的办法,也为咱们后续测验拿掉Dart 自界说其他言语适配到Flutter Engine做出了典范,有必定的参阅价值。