鸿蒙跨平台框架ArkUI-X

打印 上一主题 下一主题

主题 1016|帖子 1016|积分 3048

01
  引言
  目前,移动端主流跨平台方案有Flutter、React Native、uni-app等等,另有刚推出不久的Compose-Multiplatform,真所谓是百花齐放。这些框架各有特点,技能实现各有差别,比如Flutter通过Dart编写的UI形貌对接Flutter渲染引擎,React Native 则是借助大前端成熟的发展配景,利用JS引擎生成UI形貌,渲染时转化为原生控件,复用了原生渲染能力。至于选择哪种框架实现跨平台取决于项目的详细需求、开发团队的技能和偏好。今天我们探索一个新的框架——ArkUI-X。
  02
  ArkTS、ArkUI、ArkUI-X
  在探索ArkUI-X之前,先相识一下ArkTS、ArkUI、ArkUI-X三者关系:
  

  • ArkTS 是华为基于TypeScript自研的开发语言,重要用于Harmony应用层开发。ArkTS在保持原 TS 根本语法风格的底子上,对 TS 的动态类型特性施加了更严格的约束,引入静态类型,减少运行时的类型检查,有助于性能提升;
  • ArkUI 是一套声明式UI开发框架。包含一系列UI组件(Text、Image)、状态管理(State、LocalStorage)、界面绘制、交互事件以及及时界面预览工具;
  • ArkUI-X 进一步将 ArkUI 扩展到多个 OS 平台,目前支持 OpenHarmony、HarmonyOS、Android、iOS,后续会逐步增长更多平台支持。
  简单来说,开发者基于ArkUI框架,使用ArkTS语言进行编码,构建OpenHarmony/HarmonyOS应用,为了让应用运行到Android iOS上,利用 ArkUI-X 框架实现各OS平台的适配和构建。接下来我们将创建一个 ArkUI-X 简易Demo,通过Demo来探索ArkUI如何绘制组件到屏幕,ArkUI-X如何扩展ArkUI到Android平台,ArkUI-X如何与Android体系能力交互。
  03
  快速上手
  3.1 环境搭建
  

  • DevEco Studio
  起首从DevEco Studio官方网站(https://developer.huawei.com/consumer/cn/deveco-studio/)下载并安装DevEco Studio,必要选择4.0.0以上版本,以支持 ArkUI-X 套件。然后在DevEco Studio内部,分别下载HarmonyOS SDK 和 ArkUI-X,非合作企业开发者下载OpenHarmony SDK 和 ArkUI-X。
  
下载OpenHarmony SDK示例:

  

  
下载ArkUI-X示例:

  

  • Android Studio
  因为终极要打出Android的apk实现跨平台,所以必要安装Android Studio。安装步调对客户端小伙伴都已经得心应手了,强调一点:必要额外配置ANDROID_HOME(Android SDK安装路径)到体系环境变量中,这是步调一中DevEco Studio编译项目的必备条件之一。
  

  • Xcode 同理,使用Xcode导入iOS项目,打IPA格式的安装包。
  

  3.2 创建工程
    完成上述的环境搭建后,就可以创建工程了。可以通过 ArkUI-X 底子模板进行创建。  
     创建工程    创建完成后,得到这样一个工程结构:
  
     项目结构   

  • entry:存放的是应用步伐入口、核心业务逻辑以及资源文件。跨平台的公共源代码放在entry下,这点和纯Harmony项目结构一致;
  • .arkui-x/android:是一个尺度的Android项目结构。但目前文件还不完备,必要Build App/Hap之后,才会生成更为完备的Android项目;
  • .arkui-x/iOS: 是一个尺度的iOS项目结构,包含project.pbxproj。
  Build App/Hap之后,在build目录下生成的后缀名.hap文件,可安装到HarmonyOS平台上。别的平台必要单独打包,.arkui-x下的Android项目导入到Android Studio中打出.apk;.arkui-x下的iOS项目导入到Xcode中打出.IPA。至此完成三个平台的打包工作。也就是说,在Build App过程中,ArkUI-X框架会把entry里公用ArkTS代码和resource,打入到各平台的安装包或项目文件中。
  应用调试方面,目前只支持HarmonyOS调试,Android和iOS暂不支持ArkTS调试。
  着实上述的环境搭建、新建项目、编译打包和安装调试,另有另一种方式实现——ACE Tools,是一套为ArkUI-X开发者提供的命令行工具。详情参见:ACE Tools快速指南(https://gitee.com/arkui-x/docs/blob/master/zh-cn/application-dev/quick-start/start-with-ace-tools.md)
  04
  窥伺ArkUI的绘制过程
  在相识 ArkUI-X 实现跨平台之前,我们先简单过一遍ArkTS编写的UI界面,是如何绘制到OpenHarmony/HarmonyOS上的,以上面的Demo为例:
  1. // Index.ets
  2. @Entry
  3. @Component
  4. struct Index {
  5.   @State message: string = '今天星期五'
  6.   
  7.   build() {
  8.     Row() {
  9.       Column() {
  10.         Text(this.message)
  11.           .fontSize(50)
  12.           .fontWeight(FontWeight.Bold)
  13.       }
  14.       .width('100%')
  15.     }
  16.     .height('100%')
  17.   }
  18. }
复制代码
Build App / Hap 之后得到hap包,和Android apk类似,对hap包进行解压得到modules.abc字节码文件 (Ark Byte Code),再对字节码文件进行16进制解析,可以看到这样一段代码:
  1. class Index extends ViewPU {
  2.     constructor(parent, params, __localStorage, elmtId = -1, paramsLambda = undefined, extraInfo) {
  3.         super(parent, __localStorage, elmtId, extraInfo);
  4.         this.__message = new ObservedPropertySimplePU('今天星期五', this, "message");
  5.         this.setInitiallyProvidedValue(params);
  6.     }
  7.     setInitiallyProvidedValue(params: Index_Params) {
  8.         if (params.message !== undefined) {
  9.             this.message = params.message;
  10.         }
  11.     }
  12.     aboutToBeDeleted() {
  13.         this.__message.aboutToBeDeleted();
  14.         SubscriberManager.Get().delete(this.id__());
  15.     }
  16.     private __message: ObservedPropertySimplePU<string>;
  17.     initialRender() {   
  18.         // 编译器根据ArkTS中对组件的描述部分,重新生成js
  19.         this.observeComponentCreation2((elmtId, isInitialRender) => {
  20.             Row.create();
  21.             Row.height('100%');
  22.         }, Row);
  23.         this.observeComponentCreation2((elmtId, isInitialRender) => {
  24.             Column.create();
  25.             Column.width('100%');
  26.         }, Column);
  27.         this.observeComponentCreation2((elmtId, isInitialRender) => {
  28.             Text.create(this.message);
  29.             Text.fontSize(50);
  30.             Text.fontWeight(FontWeight.Bold);
  31.         }, Text);
  32.         Text.pop();
  33.         Column.pop();
  34.         Row.pop();
  35.     }
  36. }
复制代码
可以看出,我们用@Component编写的组件,颠末ArkCompiler编译后,会生成一个继续自ViewPU的js类,这个过程和kotlin颠末kotlin编译器生成java有点类似。ViewPU位于ArkUI框架的arkui_ace_engine
  (https://gitee.com/openharmony/arkui_ace_engine/tree/master)仓, 部分代码如下:
  1. // 位于 frameworks/bridge/declarative_frontend/state_mgmt/src/lib/partial_update/pu_view.ts
  2. abstract class ViewPU extends PUV2ViewBase implements IViewPropertiesChangeSubscriber, IView {
  3.   constructor(parent: IView, localStorage: LocalStorage, 
  4.               elmtId: number = UINodeRegisterProxy.notRecordingDependencies, 
  5.               extraInfo: ExtraInfo = undefined) {
  6.     super(parent, elmtId, extraInfo); 
  7.     this.id_ = SubscriberManager.MakeId() : elmtId;
  8.     if (localStorage) {
  9.       this.localStorage_ = localStorage;
  10.     }
  11.     SubscriberManager.Add(this);
  12.   }
  13.     
  14.   public initialRenderView(): void {
  15.     this.obtainOwnObservedProperties();
  16.     this.initialRender();
  17.     ...
  18.   }
  19.     
  20.   // 编译器生成的js类会实现该方法,主要是对组件的描述
  21.   protected abstract initialRender(): void;
  22.     
  23.   // implements IMultiPropertiesChangeSubscriber UI状态变化回调
  24.   viewPropertyHasChanged(varName: PropertyInfo, dependentElmtIds: Set<number>): void {
  25.     if (dependentElmtIds.size && !this.isFirstRender()) { // 第一次走initialRenderView()
  26.       if (!this.dirtDescendantElementIds_.size && !this.runReuse_) {
  27.         this.markNeedUpdate(); // 更新UI 最终调到C++
  28.       }
  29.       ...
  30.     }
  31.     
  32.     let cb = this.watchedProps.get(varName);
  33.     if (cb && typeof cb === 'function') {
  34.       // ArkTS中@Prop @Watch('xxx') value 的invoke
  35.       cb.call(this, varName);
  36.     }
  37.   }
  38. }
复制代码
从上面ViewPU的部分代码,可以提取几点信息:
  

  • 构造方法第一个参数parent: IView,阐明整个UI结构中存在子父组件概念,其内部维护一个子父组件关系链,这点和别的UI框架一样;
  • 构造逻辑依次是:
  

  • 生成element Id,用于局部革新;
  • 定义localStorage对象,用于页面状态共享;
  • SubscriberManager.Add(this),添加订阅,监听状态变化。
  状态变化后,回调viewPropertyHasChanged,更新UI并实行@Watch装饰器逻辑。
  更多实现可查察源码,当然只必要看看流程即可,因为代码的commit频次比力高,每次打开看细节都大概有所变化,而且还存在许多同名v2类、v2方法。总之,ViewPU 是所有组件的基类,ViewPU 继续自 PUV2ViewBase,PUV2ViewBase 继续自 NativeViewPartialUpdate。从下面代码块的解释得知,UI渲染将在 C++ 里面实现,将通过NAPI(NAPI 和 JNI 类似)完成js与C++的交互。
  1. // 位于 frameworks/bridge/declarative_frontend/state_mgmt/src/lib/puv2_common/puv2_view_base.ts 和 puv2_view_native_base.d.ts
  2. // implemented in C++  for release
  3. abstract class PUV2ViewBase extends NativeViewPartialUpdate {
  4.     ...
  5. }
  6. /**
  7.  * NativeViewPartialUpdate aka JSViewPartialUpdate C++ class exposed to JS
  8.  *  all definitions in this file are framework internal
  9.  */
  10. declare class NativeViewPartialUpdate {
  11.   constructor();
  12.   markNeedUpdate(): void; // 更新UI
  13.   finishUpdateFunc(elmtId: number): void;
  14.   ...
  15.   static create(newView: NativeViewPartialUpdate): void;
  16. }
复制代码
markNeedUpdate()重要负责UI革新,追踪一下它的实现逻辑:
  1. // 位于 frameworks/bridge/declarative_frontend/jsview/js_view.cpp
  2. void JSViewPartialUpdate::JSBind(BindingTarget object)
  3. {
  4.     JSClass<JSViewPartialUpdate>::Declare("NativeViewPartialUpdate");
  5.     JSClass<JSViewPartialUpdate>::StaticMethod("create", &JSViewPartialUpdate::Create, opt);
  6.     JSClass<JSViewPartialUpdate>::Method("markNeedUpdate", &JSViewPartialUpdate::MarkNeedUpdate);
  7. }
  8. void JSViewPartialUpdate::MarkNeedUpdate()
  9. {
  10.     needsUpdate_ = ViewPartialUpdateModel::GetInstance()->MarkNeedUpdate(viewNode_);
  11. }
复制代码
  1. // 位于 frameworks/bridge/declarative_frontend/jsview/models/view_partial_update_model_impl.cpp
  2. bool ViewPartialUpdateModelImpl::MarkNeedUpdate(const WeakPtr<AceType>& node)
  3. {
  4.     auto weakElement = AceType::DynamicCast<ComposedElement>(node);
  5.     auto element = weakElement.Upgrade();
  6.     if (element) {
  7.         element->MarkDirty();
  8.     }
  9.     return true;
  10. }
复制代码
ComposedElement 是 pipeline 记录组件信息的对象,weakElement.Upgrade() 将 ComposedElement 放入 pipeline 中,终极通过图形渲染引擎(OpenGL ES、Skia)完成体现。详细代码参考frameworks/core/pipeline/base/composed_element.cpp。
  05
  Android跨平台的实现
  以上是一个hap包通过ArkUI完成渲染的大致过程,回到跨平台ArkUI-X,类似的ArkTS代码是如何运行在Android设备上的呢?
  打开Android项目,看到assets下存放着ArkCompiler编译产物,和上一节中对.hap包解压后得到的文件一模一样。这是在编译环节中,编译脚本copy一份modules.abc字节码和resource到Android工程下,作为Android应用资源,打包时将以assets形式打入apk。
  1. src/main/assets/arkui-x
  2.    ├── entry
  3.    |   ├── ets
  4.    |   |   ├── modules.abc
  5.    |   |   └── sourceMaps.map
  6.    |   ├── resouces.index
  7.    |   ├── resouces
  8.    |   └── module.json
  9.    └── systemres
复制代码
如何在Android上实行modules.abc字节码呢?打开libs,发现ArkUI相关的so动态库和jar包。
  1. libs
  2.    ├── armabi-v7a
  3.    |   ├── libarkui_android.so
  4.    |   └── libhilog.so
  5.    └── arkui_android_adapter.jar
复制代码
其中libarkui_android.so 是 arkui_ace_engine、arkui_napi 、foundation/appframework、arkui_for_android... 所编译出的动态库,是运行和界面渲染的须要环境。另一个arkui_android_adapter.jar的功能是:Android Application必要继续arkui_android_adapter.jar包所提供的StageApplication。StageApplication用于初始化资源路径以及加载配置信息。Activity必要继续arkui_android_adapter.jar包所提供的StageActivity,StageActivity重要功能是将Android中Activity的生命周期与Harmony中Ability的生命周期进行映射。除此之外,arkui_android_adapter.jar适配了体系平台能力,如粘贴板、软键盘、字体、存储、日志。这里有个疑问:ArkUI-X是如何实现与Android体系之间的交互呢?ArkTS和Java没有相互调用的能力,为了实现ArkTS和Java交互,必要ArkTS与C++交互,C++再与Java交互,调用链为ArkTS  -> NAPI -> C++ -> JNI -> Java,反之亦然,看起来十分复杂。ArkUI-X提供一套桥接能力,对于开发者来说,并不消关心这些封装逻辑,现实开发过程中,就像是ArkTS和Java直接交互。
  下面通过粘贴板的例子,探究它的详细实现过程。我们给体系粘贴板设置数据——'明天星期六',使用ArkTS实现如下,看看终极是如何调到Android Framework 给粘贴板设置数据的api:ClipboardManager#setPrimaryClip()。
  1. import pasteboard from '@ohos.pasteboard';
  2. Button('拷贝到粘贴板')
  3.   .onClick(() => {
  4.     let pasteData: pasteboard.PasteData = pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, '明天星期六')
  5.     let systemPasteboard: pasteboard.SystemPasteboard = pasteboard.getSystemPasteboard()
  6.     systemPasteboard.setData(pasteData)
  7.   })
复制代码
这段代码对接ArkUI的调用链和上一节中渲染UI类似,就不再赘述了。粘贴板相关api 在 arkui_ace_engine中定义成抽象接口,如下:
  1. // 位于 arkui_ace_engine/frameworks/core/common/clipboard/clipboard.h
  2. namespace OHOS::Ace {
  3.     class Clipboard : public AceType {
  4.         DECLARE_ACE_TYPE(Clipboard, AceType);
  5.     public:
  6.         ~Clipboard() override = default;
  7.         
  8.         virtual void SetData(
  9.             const std::string& data, CopyOptions copyOption = CopyOptions::InApp, bool isDragData = false) = 0;
  10.     }
  11. }
复制代码
ArkUI-X 分平台对接口进行不同实现,Android的实现在arkui_for_android堆栈中,其定义和实现如下,可见粘贴板设置数据api SetData()终极到ClipboardJni::SetData(data)
  1. // 位于 arkui_for_android/capability/java/jni/clipboard/clipboard_impl.h
  2. #include "core/common/clipboard/clipboard.h"
  3. namespace OHOS::Ace::Platform {
  4.     class ClipboardImpl final : public Clipboard {
  5.     public:
  6.         explicit ClipboardImpl(const RefPtr<TaskExecutor>& taskExecutor) : Clipboard(taskExecutor) {}
  7.         ~ClipboardImpl() override = default;
  8.     
  9.         void SetData(
  10.             const std::string& data, CopyOptions copyOption = CopyOptions::InApp, bool isDragData = false) override;
  11.     }
  12. }
复制代码
  1. // 位于 arkui_for_android/capability/java/jni/clipboard/clipboard_impl.cpp
  2. #include "adapter/android/capability/java/jni/clipboard/clipboard_impl.h"
  3. #include "adapter/android/capability/java/jni/clipboard/clipboard_jni.h"
  4. namespace OHOS::Ace::Platform {
  5.     void ClipboardImpl::SetData(const std::string& data, CopyOptions copyOption, bool isDragData)
  6.     {
  7.          // 对 SetData 实现,最终ClipboardJni::SetData(data)
  8.         taskExecutor_->PostTask(
  9.             [data] { ClipboardJni::SetData(data); }, TaskExecutor::TaskType::PLATFORM, "ArkUI-XClipboardImplSetData");
  10.     }
  11. }
复制代码
再通过JNI实现C++和Java交互:
  1. // 位于 arkui_for_android/capability/java/jni/clipboard/clipboard_jni.cpp
  2. static const char CLIPBOARD_PLUGIN_CLASS_NAME[] = "ohos/ace/adapter/capability/clipboard/ClipboardPluginBase";
  3. static const JNINativeMethod METHODS[] = {
  4.     { .name = "nativeInit", .signature = "()V", .fnPtr = reinterpret_cast<void*>(ClipboardJni::NativeInit) },
  5. };
  6. static const char METHOD_SET_DATA[] = "setData";
  7. static const char SIGNATURE_SET_DATA[] = "(Ljava/lang/String;)V";
  8. // JNI_OnLoad
  9. bool ClipboardJni::Register(std::shared_ptr<JNIEnv> env)
  10. {
  11.     // 动态注册 nativeInit 方法,java侧调用
  12.     jclass clazz = env->FindClass(CLIPBOARD_PLUGIN_CLASS_NAME);
  13.     bool ret = env->RegisterNatives(clazz, METHODS, ArraySize(METHODS)) == 0;
  14.     return true;
  15. }
  16. void ClipboardJni::NativeInit(JNIEnv* env, jobject object)
  17. {
  18.     jclass clazz = env->GetObjectClass(object);
  19.     g_pluginMethods.setData = env->GetMethodID(clazz, METHOD_SET_DATA, SIGNATURE_SET_DATA);
  20. }
  21. bool ClipboardJni::SetData(const std::string& data)
  22. {
  23.     auto env = JniEnvironment::GetInstance().GetJniEnv();
  24.     jstring jData = env->NewStringUTF(data.c_str());
  25.     // 反射调用 ClipboardPluginAosp.java setData方法
  26.     env->CallVoidMethod(g_clipboardObj.get(), g_pluginMethods.setData, jData);
  27.     if (jData != nullptr) {
  28.         env->DeleteLocalRef(jData);
  29.     }
  30.     return true;
  31. }
复制代码
终极通过ClipboardManager#setData() ,将ArkTS中设置的内容,给到Android的体系粘贴板。
  1. // 位于 arkui_for_android 仓库打出的 arkui_android_adapter.jar 包中
  2. public class ClipboardPluginAosp extends ClipboardPluginBase {
  3.     private final ClipboardManager clipManager;
  4.     
  5.     public ClipboardPluginAosp(Context context) {
  6.         this.clipManager = (ClipboardManager context.getSystemService(Context.CLIPBOARD_SERVICE);
  7.         nativeInit();
  8.     }
  9.     @Override
  10.     public void setData(String data) {
  11.         if (clipManager != null) {
  12.             ClipData clipData = ClipData.newPlainText(null, data);
  13.             clipManager.setPrimaryClip(clipData);
  14.         }
  15.     }
  16. }
复制代码
iOS平台的实现和Android平台类似,原理都是相通的。
  06
  小结
  通过这一章,我们学到了ArkUI-X的环境搭建、项目创建和打包流程,探索了ArkTS编写的项目,编译后字节码文件如何与ArkUI对接,相识了ArkUI-X在Android平台上的实现方案,以及ArkUI-X如何适配体系平台能力。ArkUI-X 属于后来者,设计之初应该借鉴过别的跨平台方案,汲取了优秀设计,才形成目前的形态。今后在跨平台的实现上,我们又多了一种选择。
  参考

  

  • ArkUI-X堆栈地址:https://gitee.com/arkui-x
  • ArkUI-arkui_ace_engine堆栈地址:https://gitee.com/openharmony/arkui_ace_engine
  • 深入明白arkui_ace_engine:https://juejin.cn/post/7305235970286485515

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

杀鸡焉用牛刀

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表