当前位置:   article > 正文

鸿蒙系统调研

鸿蒙系统调研

一、基础知识

1.鸿蒙系统和安卓对比?

(1)安卓系统是基于Linux宏内核设计,鸿蒙系统基于微内核,包括Linux内核(手机操作系统内核)和LiteOS内核(智能硬件内核)。

(2)安卓和鸿蒙系统都是基于安卓开源项目AOSP(Android Open Source Project)进行开发的,在鸿蒙系统上可以安装运行安卓apk.

2.Openharmony 和 HarmonyOS区别?

(1)OpenHarmony是鸿蒙的基础能力是开源的,OpenHarmony2.0去除了AOSP所以只支持hap文件安装运行,不支持apk安装运行。HarmonyOS是华为手机鸿蒙系统,基于OpenHarmony开发,不是开源的,支持AOSP,可以兼容安卓apk安装运行。OpenHarmony开源地址:OpenHarmony: OpenHarmony是由开放原子开源基金会(OpenAtom Foundation)孵化及运营的开源项目,目标是面向全场景、全连接、全智能时代,搭建一个智能终端设备操作系统的框架和平台,促进万物互联产业的繁荣发展。,不是https://github.com/Awesome-HarmonyOS/HarmonyOS。

(2)HarmonyOS有两种UI框架,Java和JS。而OpenHarmony只能通过JS和eTS开发,不能使用Java进行开发。项目目录结构有区别,没有java相关。

图为Openharmony 和 HarmonyOS关系

3.鸿蒙系统硬件互助,资源共享如何实现?

硬件互助,资源共享是鸿蒙最大特点,关键技术包括分布式软总线、分布式设备虚拟化、分布式数据管理、分布式任务调度,分布式连接能力等

4.研究鸿蒙系统涉及哪些技术原理?

从技术上讲,鸿蒙系统硬件互助资源共享,背后涉及分布式系统相关概念:分布式,事务,本地事务,分布式事务,CAP/Base理论,强一致性/弱一致性/最终一致性,RPC,分布式任务调度。

前端:Flex布局,跨端方案vue/flutter,mvvm对象观察, 模板渲染过程(模板语法—>抽象语法树—>渲染函数(h函数)—>虚拟节点Vdom—>diff/patch—>真实节点Dom), 三棵树原则,JS引擎,线程模型等理论。

5.分布式软总线

(1)软总线技术最大特点是不同协议的异构网络进行组网。传统场景下,需要蓝牙传输的两台设备必须都具有蓝牙,需要WiFi传输的设备必须都具有WiFi。而蓝牙/WiFi之间是无法进行数据通信的。软总线提出蓝牙/WiFi融合网络组网技术(架构如下图所示),解决了不同协议设备进行数据通信的问题。使得多个鸿蒙设备能够自动构建一个逻辑全连接网络,用户或者业务开发者无需关心组网方式与物理协议,只需聚焦于业务逻辑的实现,无需关注组网方式与底层协议。软总线模块实现的能力有:服务发布、数据传输、安全通信。

(2)分布式软总线是基于开源COAP(Constrained Application Protocol)协议,C语言实现的,属于一种底层基于UDP的应用层协议。鸿蒙使用COAP协议因为考虑到运行harmonyOS的设备除了硬件性能较好的手机、电脑等设备外,还有资源受限的物联网设备,这些设备的ram、rom相对较小。coap协议支持轻量的可靠传输,采用coap协议,可以扩大组网的范围。

代码路径:code-v3.2-Beta1\OpenHarmony\foundation\communication\dsoftbus\components\nstackx_mini\nstackx_ctrl\include\coap_discover\coap_def.h

(3)软总线屏蔽了各种设备底层协议的差异,如不同设备支持WIFI,蓝牙,USB,BT,NFC等协议无法互通,但通过软总线可以使支持不同设备协议互通,使得应用层不用在关心设备的底层协议。

注:真机实测需要不同设备登录同一华为账号,设置-超级终端-允许附近设备发现我,同时在同一局域网或蓝牙。远程模拟器不需要登录同一华为账号,默认在一个局域网。

分布式软总线示意图如下:

根据软总线示意图得出如下几点结论:

a.协议栈和软硬协同层屏蔽各种硬件设备协议差异。

b.总线中枢负责解析命令,完成设备间发现与连接,基于COAP协议的设备发现功能。

c.安全模块负责通信的加解密;设备认证模块通过交换主控设备与配件设备的身份标识来建立点对点信任关系,设备认证模块关键技术点:HiChain机制数据接收管理、PAKE协议、STS协议流程

d.提供统一的基于Session的认证、传输功能,上层业务系统可以通过sessionId收发数据或获取其相关基本属性,实现业务消息、流、控制指令等操作交互。

e.软总线代码路径:code-v3.2-Beta1\OpenHarmony\foundation\communication\dsoftbus\core,discovery模块:提供基于COAP协议的设备发现机制;connection模块:提供基于COAP协议的设备连接机制;authentication模块:提供设备认证机制和知识库管理功能;transmission:模块基于系统内核提供的socket通信,向authmanager模块提供设备认证通道管理和设备认证数据的传输;向业务模块提供session管理和基于session的数据收发功能,并且通过GCM模块的加密功能提供收发报文的加解密保护;adapter:操作系统适配层。参考连接1参考连接2

f.需要注意的是,软总线启动后,设备在发现阶段基于coap协议使用udp数据报进行通讯。当设备认证通过,确认连接后,将会使用TCP协议进行更加安全可靠的通讯

(4)基于分布式软总线设备间交互过程示意图如下:

具体过程:发现端(如手机端)在广播发起discover请求后,使用coap协议在局域网内发送广播 ;被发现端(智能设备端)设备使用PublishService接口发布服务,接收端收到广播后,发送coap协议单播给发现端;发现端设备收到报文会更新设备信息 建立连接

6.分布式数据管理

鸿蒙系统数据管理有三种方式:关系型数据库SQLite及ORM,轻量级数据存储Preferences,分布式数据库支持KV数据模型,是一种NoSQL类型数据库。

概念: 事务,本地事务,分布式事务(不在一个JVM), mysql主从同步读写分离分库分表,CAP理论,dubbo,rocketmq,seata方案。

(1)分布式数据库事务支持本地事务(和传统的数据库事务概念一致)和同步事务。同步事务是指在设备之间同步数据时,以本地事务为单位进行同步,一次本地事务的修改要么都同步成功,要么都同步失败.

(2)根据分布式系统CAP理论需满足数据一致性。在分布式场景中一般会涉及多个设备,组网内设备之间看到的数据是否一致称为分布式数据库的一致性。分布式数据库一致性可以分为强一致性弱一致性最终一致性。鸿蒙实现最终一致性即满足AP。

(3)分布式数据服务提供了两种同步方式:手动同步自动同步

  • 手动同步:由应用程序调用sync接口来触发,需要指定同步的设备列表和同步模式。同步模式分为PULL_ONLY(将远端数据拉到本端)、PUSH_ONLY(将本端数据推送到远端)和PUSH_PULL(将本端数据推送到远端同时也将远端数据拉取到本端

  • 自动同步:由分布式数据库自动将本端数据推送到远端,同时也将远端数据拉取到本端来完成数据同步,同步时机包括设备上线、应用程序更新数据等,应用不需要主动调用sync接口。

(4)分布式数据管理基于分布式软总线的能力,实现应用程序数据和用户数据的分布式管理。用户数据不再与单一物理设备绑定,业务逻辑与数据存储分离。通过分布式数据管理使得数据实时同步到不同设备,该过程不需要业务逻辑参与实现。

分布式数据同步过程如下: 

(5)分布式数据库最终一致性

7.分布式任务调度

分布式任务调度基于分布式软总线、分布式数据管理等技术特性,构建统一的分布式服务管理(发现、同步、注册、调用)机制,支持对跨设备的应用进行远程启动、远程调用、远程连接以及迁移等操作,能够根据不同设备的能力选择合适的设备运行分布式任务。分布式任务调度示意图如下:

通过分布式任务调度可以实现跨端分布式计算,解决不同终端算力问题,实现硬件互助。跨端分布式算力示意图如下: 

如图分布式任务调度,有如下特点:

  • 允许多个HarmonyOS设备协同计算资源分担以及实时的任务调度
  • 能随时方便的发现和启用周边闲置的设备
  • 将周边的设备组建成算力和差异化功能的资源池
  • 为用户的高体验应用提供随需算力和特定能力的分布式卸载和协同能力
  • 组合成能胜任各种新业务场景需求的超级终端

分布式调度协议:服务端开发常见的分布式调度RPC协议有阿里开源dubbo,京东杰夫JSF协议。HarmonyOS分布式系统采用的极简D2D传输协议栈,(可以理解为鸿蒙实现的私有通信RPC协议),相较于传统协议栈做了许多简化处理,包括压缩协议封装、增加协议处理的硬件亲和性,通过智能预测配合节电机制做预热处理,避免冷启动等。获得5-10倍的压缩数据同步传输速度提升,实现亚毫秒级的无线通信时延。极减协议D2D和传统TCP比较如下:

采用D2D协议相较传统的TCP协议,优点如下

缩短协议路径:精简协议处理流程,软件处理时延减少50%

减少线程调度:减少收发侧线程调度,线程调度时延减少55%

芯片按需预热:感知设备与业务状态,芯片处理时延减少80%。

进一步学习参考:https://forum.gitlink.org.cn/forums/7218/detail

8.鸿蒙系统对比Android?

鸿蒙

Android

Ability

Activity

AbilitySlice

Fragment

Component

View

ComponentContainer

ViewGroup

AbilityPackage

Application

Intent

Intent

config.json

Manifeast.xml

FA/PA

Activity/Service

SQLite/SharePreference/ORM

SQLite/SharePreference/ORM

公共事件

广播机制

EventHandler

Handler

EventRunner

Looper机制

idl

aidl

原子化服务/服务卡片

小程序/快捷键

二、应用实例(java)

1.应用流转迁移

项目:harmonyos-tutorial\samples\ContinueRemoteFACollaboration

效果:

                                          图1                                                   图2 

 如图,点击图1中“迁移”按钮,会打开pad设备应用,同时将数据迁移显示到pad对应UI处。图2中可以继续编辑内容,点击“迁移”会再次迁移到图1设备中继续显示。

重点代码:

  1. /**
  2. * 设备迁移与回迁
  3. */
  4. public class MainAbilitySlice extends AbilitySlice implements IAbilityContinuation {
  5. private static final String TAG = MainAbilitySlice.class.getSimpleName();
  6. private static final HiLogLabel LABEL_LOG = new HiLogLabel(HiLog.LOG_APP, 0x00001, TAG);
  7. private static final String MESSAGE_KEY = "com.waylau.hmos.continueremotefacollaboration.slice.MESSAGE_KEY";
  8. private String message;
  9. private boolean isContinued;
  10. private TextField messageTextField;
  11. @Override
  12. public void onStart(Intent intent) {
  13. super.onStart(intent);
  14. super.setUIContent(ResourceTable.Layout_ability_main);
  15. HiLog.info(LABEL_LOG, "onStart");
  16. // 监听跨端迁移FA的事件
  17. Button buttonContinueRemoteFA = (
  18. Button) findComponentById(ResourceTable.Id_button_continue_remote_fa);
  19. buttonContinueRemoteFA.setClickedListener(listener -> continueRemoteFA());
  20. // 设置输入框内容
  21. messageTextField = (TextField) findComponentById(ResourceTable.Id_message_textfield);
  22. if (isContinued && message != null) {
  23. messageTextField.setText(message);
  24. }
  25. }
  26. private void continueRemoteFA() {
  27. HiLog.info(LABEL_LOG, "before startRemoteFA");
  28. String deviceId = DeviceUtils.getDeviceId();
  29. HiLog.info(LABEL_LOG, "get deviceId: %{public}s", deviceId);
  30. if (deviceId != null) {
  31. // 发起迁移流程
  32. // continueAbility()是不可回迁的
  33. // continueAbilityReversibly() 是可以回迁的
  34. continueAbility(deviceId);
  35. }
  36. }
  37. @Override
  38. public void onActive() {
  39. super.onActive();
  40. }
  41. @Override
  42. public void onForeground(Intent intent) {
  43. super.onForeground(intent);
  44. }
  45. @Override
  46. public boolean onStartContinuation() {
  47. HiLog.info(LABEL_LOG, "onStartContinuation");
  48. // 重写
  49. return true;
  50. }
  51. @Override
  52. public boolean onSaveData(IntentParams intentParams) {
  53. HiLog.info(LABEL_LOG, "onSaveData");
  54. // 重写
  55. // 保存回迁后恢复状态必须的数据
  56. intentParams.setParam(MESSAGE_KEY, messageTextField.getText());
  57. return true;
  58. }
  59. @Override
  60. public boolean onRestoreData(IntentParams intentParams) {
  61. HiLog.info(LABEL_LOG, "onRestoreData");
  62. // 重写
  63. // 传递此前保存的数据
  64. if (intentParams.getParam(MESSAGE_KEY) instanceof String) {
  65. message = (String) intentParams.getParam(MESSAGE_KEY);
  66. isContinued = true;
  67. }
  68. return true;
  69. }
  70. @Override
  71. public void onCompleteContinuation(int i) {
  72. HiLog.info(LABEL_LOG, "onCompleteContinuation");
  73. // 终止
  74. terminate();
  75. }
  76. }

 原理:

(1)实现IAbilityContinuation接口,重写onSaveData、onRestoreData、onCompleteContinuation等方法。其中,onSaveData用于保存迁移传递必须的数据; onRestoreData恢复获取传递此前保存的数据。onCompleteContinuation方法用于终止Page。

(2)点击事件中获取组网设备ID,DeviceUtils.getDeviceId();

(3)调用迁移api,continueAbility (deviceId)后会触发IAbilityContinuation相关回调方法

(4)应用层不需要处理联网协议,底层基于软总线COAP协议完成数据传递,实现数据迁移。

特点:应用层不需要处理组网,联网等复杂过程降低开发难度,简单api调用就可以完成近场设备间数据UI迁移

应用场景:

  • 在外时手机上编辑邮件,到家后迁移到平板上继续编辑

  • 在外时手机玩游戏,到家后迁移到平板上继续玩

  • 在家里智慧屏上看视频,出门时迁移到手机上继续观看

  • 手机视频通话迁移到智慧屏,更沉浸地视频聊天

  • 听歌,看电影,玩游戏,应用流转数据迁移至至电视继续播放,玩游戏

2.分布式数据库

项目:codelabs\DistributeDatabaseDraw

效果:

如图,在一组互联的设备中,在其中一个设备上写字画画会实时同步至其他设备,不是投屏。

重点代码:

MainAbilitySlice.java

  1. public class MainAbilitySlice extends AbilitySlice {
  2. private static final String TAG = MainAbilitySlice.class.getName();
  3. private static final int PERMISSION_CODE = 20201203;
  4. private static final int DELAY_TIME = 10;
  5. private static final String STORE_ID_KEY = "storeId";
  6. private static final String POINTS_KEY = "points";
  7. private static final String COLOR_INDEX_KEY = "colorIndex";
  8. private static final String IS_FORM_LOCAL_KEY = "isFormLocal";
  9. private static String storeId;
  10. private DependentLayout canvas;
  11. private Image transform;
  12. private KvManager kvManager;
  13. private SingleKvStore singleKvStore;
  14. private Text title;
  15. private DrawPoint drawl;
  16. private Button back;
  17. @Override
  18. public void onStart(Intent intent) {
  19. super.onStart(intent);
  20. super.setUIContent(ResourceTable.Layout_ability_main);
  21. storeId = STORE_ID_KEY + System.currentTimeMillis();
  22. findComponentById();
  23. requestPermission();
  24. initView(intent);
  25. initDatabase();
  26. initDraw(intent);
  27. }
  28. private void initView(Intent intent) {
  29. boolean isLocal = !intent.getBooleanParam(IS_FORM_LOCAL_KEY, false);
  30. if (!isLocal) {
  31. storeId = intent.getStringParam(STORE_ID_KEY);
  32. }
  33. title.setText(isLocal ? "本地端" : "远程端");
  34. transform.setVisibility(isLocal ? Component.VISIBLE : Component.INVISIBLE);
  35. }
  36. private void requestPermission() {
  37. if (verifySelfPermission(DISTRIBUTED_DATASYNC) != IBundleManager.PERMISSION_GRANTED) {
  38. if (canRequestPermission(DISTRIBUTED_DATASYNC)) {
  39. requestPermissionsFromUser(new String[]{DISTRIBUTED_DATASYNC}, PERMISSION_CODE);
  40. }
  41. }
  42. }
  43. private void findComponentById() {
  44. if (findComponentById(ResourceTable.Id_canvas) instanceof DependentLayout) {
  45. canvas = (DependentLayout) findComponentById(ResourceTable.Id_canvas);
  46. }
  47. if (findComponentById(ResourceTable.Id_transform) instanceof Image) {
  48. transform = (Image) findComponentById(ResourceTable.Id_transform);
  49. }
  50. if (findComponentById(ResourceTable.Id_title) instanceof Text) {
  51. title = (Text) findComponentById(ResourceTable.Id_title);
  52. }
  53. if (findComponentById(ResourceTable.Id_back) instanceof Button) {
  54. back = (Button) findComponentById(ResourceTable.Id_back);
  55. }
  56. transform.setClickedListener(component -> {
  57. DeviceSelectDialog dialog = new DeviceSelectDialog(MainAbilitySlice.this);
  58. dialog.setListener(deviceIds -> {
  59. if (deviceIds != null && !deviceIds.isEmpty()) {
  60. // 启动远程页面
  61. startRemoteFas(deviceIds);
  62. // 同步远程数据库
  63. singleKvStore.sync(deviceIds, SyncMode.PUSH_ONLY);
  64. }
  65. dialog.hide();
  66. });
  67. dialog.show();
  68. });
  69. }
  70. /**
  71. * Initialize art boards
  72. *
  73. * @param intent Intent
  74. */
  75. private void initDraw(Intent intent) {
  76. int colorIndex = intent.getIntParam(COLOR_INDEX_KEY, 0);
  77. drawl = new DrawPoint(this, colorIndex);
  78. drawl.setWidth(MATCH_PARENT);
  79. drawl.setWidth(MATCH_PARENT);
  80. canvas.addComponent(drawl);
  81. drawPoints();
  82. drawl.setOnDrawBack(points -> {
  83. if (points != null && points.size() > 1) {
  84. String pointsString = GsonUtil.objectToString(points);
  85. LogUtils.info(TAG, "pointsString::" + pointsString);
  86. if (singleKvStore != null) {
  87. singleKvStore.putString(POINTS_KEY, pointsString);
  88. }
  89. }
  90. });
  91. back.setClickedListener(component -> {
  92. List<MyPoint> points = drawl.getPoints();
  93. if (points == null || points.size() <= 1) {
  94. return;
  95. }
  96. points.remove(points.size() - 1);
  97. for (int i = points.size() - 1; i >= 0; i--) {
  98. if (points.get(i).isLastPoint()) {
  99. break;
  100. }
  101. points.remove(i);
  102. }
  103. drawl.setDrawParams(points);
  104. String pointsString = GsonUtil.objectToString(points);
  105. if (singleKvStore != null) {
  106. singleKvStore.putString(POINTS_KEY, pointsString);
  107. }
  108. });
  109. }
  110. // 获取数据库中的点数据,并在画布上画出来
  111. private void drawPoints() {
  112. List<Entry> points = singleKvStore.getEntries(POINTS_KEY);
  113. for (Entry entry : points) {
  114. if (entry.getKey().equals(POINTS_KEY)) {
  115. List<MyPoint> remotePoints = GsonUtil.jsonToList(singleKvStore.getString(POINTS_KEY), MyPoint.class);
  116. getUITaskDispatcher().delayDispatch(() -> drawl.setDrawParams(remotePoints), DELAY_TIME);
  117. }
  118. }
  119. }
  120. /**
  121. * Receive database messages
  122. *
  123. * @since 2021-04-06
  124. */
  125. private class KvStoreObserverClient implements KvStoreObserver {
  126. @Override
  127. public void onChange(ChangeNotification notification) {
  128. LogUtils.info(TAG, "data changed......");
  129. drawPoints();
  130. }
  131. }
  132. private void initDatabase() {
  133. // 创建分布式数据库管理对象
  134. KvManagerConfig config = new KvManagerConfig(this);
  135. kvManager = KvManagerFactory.getInstance().createKvManager(config);
  136. // 创建分布式数据库
  137. Options options = new Options();
  138. options.setCreateIfMissing(true).setEncrypt(false).setKvStoreType(KvStoreType.SINGLE_VERSION);
  139. singleKvStore = kvManager.getKvStore(options, storeId);
  140. // 订阅分布式数据变化
  141. KvStoreObserver kvStoreObserverClient = new KvStoreObserverClient();
  142. singleKvStore.subscribe(SubscribeType.SUBSCRIBE_TYPE_ALL, kvStoreObserverClient);
  143. }
  144. /**
  145. * Starting Multiple Remote Fas
  146. *
  147. * @param deviceIds deviceIds
  148. */
  149. private void startRemoteFas(List<String> deviceIds) {
  150. Intent[] intents = new Intent[deviceIds.size()];
  151. for (int i = 0; i < deviceIds.size(); i++) {
  152. Intent intent = new Intent();
  153. intent.setParam(IS_FORM_LOCAL_KEY, true);
  154. intent.setParam(COLOR_INDEX_KEY, i + 1);
  155. intent.setParam(STORE_ID_KEY, storeId);
  156. Operation operation = new Intent.OperationBuilder()
  157. .withDeviceId(deviceIds.get(i))
  158. .withBundleName(getBundleName())
  159. .withAbilityName(MainAbility.class.getName())
  160. .withFlags(Intent.FLAG_ABILITYSLICE_MULTI_DEVICE)
  161. .build();
  162. intent.setOperation(operation);
  163. intents[i] = intent;
  164. }
  165. startAbilities(intents);
  166. }
  167. @Override
  168. protected void onStop() {
  169. super.onStop();
  170. kvManager.closeKvStore(singleKvStore);
  171. }
  172. }

DrawPoint,java

  1. public class DrawPoint extends Component implements Component.DrawTask {
  2. private static final int STROKE_WIDTH = 15;
  3. private static final int TIME = 200;
  4. private static final int EVENT_MSG_STORE = 0x1000002;
  5. private final Color[] paintColors = new Color[]{Color.RED, Color.BLUE, Color.BLACK};
  6. private List<MyPoint> points = new ArrayList<>(0);
  7. private Paint paint;
  8. private OnDrawCallBack callBack;
  9. private Timer timer = null;
  10. private TimerTask timerTask = null;
  11. private final EventHandler handler = new EventHandler(EventRunner.current()) {
  12. @Override
  13. protected void processEvent(InnerEvent event) {
  14. if (EVENT_MSG_STORE == event.eventId) {
  15. callBack.callBack(points);
  16. }
  17. }
  18. };
  19. /**
  20. * Drawl constructor
  21. *
  22. * @param context context
  23. * @param colorIndex colorIndex
  24. */
  25. public DrawPoint(Context context, int colorIndex) {
  26. super(context);
  27. init(colorIndex);
  28. }
  29. public List<MyPoint> getPoints() {
  30. return points;
  31. }
  32. /**
  33. * SelectResult
  34. *
  35. * @since 2020-12-03
  36. */
  37. public interface OnDrawCallBack {
  38. /**
  39. * touchListener
  40. *
  41. * @param points points
  42. */
  43. void callBack(List<MyPoint> points);
  44. }
  45. /**
  46. * setPoints
  47. *
  48. * @param myPoints myPoints
  49. */
  50. public void setDrawParams(List<MyPoint> myPoints) {
  51. this.points = myPoints;
  52. invalidate();
  53. }
  54. /**
  55. * setOnDrawBack
  56. *
  57. * @param onCallBack onCallBack
  58. */
  59. public void setOnDrawBack(OnDrawCallBack onCallBack) {
  60. this.callBack = onCallBack;
  61. }
  62. private void init(int colorIndex) {
  63. paint = new Paint();
  64. paint.setAntiAlias(true);
  65. paint.setStyle(Paint.Style.STROKE_STYLE);
  66. paint.setStrokeWidth(STROKE_WIDTH);
  67. addDrawTask(this);
  68. Color color = getRandomColor(colorIndex);
  69. setTouchEventListener((component, touchEvent) -> {
  70. scheduledTask();
  71. int crtX = (int) touchEvent.getPointerPosition(touchEvent.getIndex()).getX();
  72. int crtY = (int) touchEvent.getPointerPosition(touchEvent.getIndex()).getY();
  73. MyPoint point = new MyPoint(crtX, crtY);
  74. point.setPaintColor(color);
  75. switch (touchEvent.getAction()) {
  76. case TouchEvent.POINT_MOVE:
  77. points.add(point);
  78. break;
  79. case TouchEvent.PRIMARY_POINT_UP:
  80. points.add(point);
  81. point.setLastPoint(true);
  82. callBack.callBack(points);
  83. onTimerFinish();
  84. break;
  85. default:
  86. break;
  87. }
  88. invalidate();
  89. return true;
  90. });
  91. }
  92. /**
  93. * scheduled task start
  94. */
  95. public void scheduledTask() {
  96. if (timer == null && timerTask == null) {
  97. timer = new Timer();
  98. timerTask = new TimerTask() {
  99. @Override
  100. public void run() {
  101. handler.sendEvent(EVENT_MSG_STORE);
  102. }
  103. };
  104. timer.schedule(timerTask, 0, TIME);
  105. }
  106. }
  107. /**
  108. * Canceling a Scheduled Task
  109. */
  110. public void onTimerFinish() {
  111. timer.cancel();
  112. timer = null;
  113. timerTask = null;
  114. }
  115. @Override
  116. public void onDraw(Component component, Canvas canvas) {
  117. draw(points, canvas);
  118. }
  119. private void draw(List<MyPoint> myPoints, Canvas canvas) {
  120. if (myPoints == null || myPoints.size() <= 1) {
  121. return;
  122. }
  123. Point first = null;
  124. Point last = null;
  125. for (MyPoint myPoint : myPoints) {
  126. paint.setColor(myPoint.getPaintColor());
  127. float finalX = myPoint.getPositionX();
  128. float finalY = myPoint.getPositionY();
  129. Point finalPoint = new Point(finalX, finalY);
  130. if (myPoint.isLastPoint()) {
  131. first = null;
  132. last = null;
  133. continue;
  134. }
  135. if (first == null) {
  136. first = finalPoint;
  137. } else {
  138. if (last != null) {
  139. first = last;
  140. }
  141. last = finalPoint;
  142. canvas.drawLine(first, last, paint);
  143. }
  144. }
  145. }
  146. private Color getRandomColor(int index) {
  147. return index > paintColors.length - 1 ? paintColors[0] : paintColors[index];
  148. }
  149. }

原理:

(1)初始化分布式数据库initDatabase(),订阅消息监听

(2)点击共享后,获取设备列表,启动远程页面startRemoteFas(deviceIds)

(3)绘制过程中在DrowPoint中TouchEventListener收集坐标点信息

(3)每过200ms触发一次任务回调中在drawl.setOnDrawBack回调中将绘图轨迹坐标写入分布式数据库singleKvStore.putString(POINTS_KEY, pointsString)

(4)其他设备,在消息监听订阅回调KvStoreObserver回调函数onchange中,获取分布式数据库中数据并完成绘制

(5)分布式数据库底层通过最终一致性方案,完成数据同步传递。

特点:多设备实时协同操作(不是投屏)

应用场景:师生课堂互动

3.分布式任务调度

项目:codelabs\RemoteInputDemo

效果:

                 图1 视频播放器                                                        图2 手机输入法

如图,图1为电视视频筛选页面,通过图2的手机输入法输入文本同步到图1视频搜索输入框。

重点代码:

MainAbilitySlice.java

  1. public class MainAbilitySlice extends AbilitySlice implements PermissionBridge.OnPermissionStateListener {
  2. private static final String ABILITY_NAME = "com.huawei.codelab.RemoteInputAbility";
  3. private static final String MOVIE_PLAY_ABILITY = "com.huawei.codelab.MoviePlayAbility";
  4. private static final String MAIN_ABILITY = "com.huawei.codelab.MainAbility";
  5. private static final int FOCUS_PADDING = 8;
  6. private static final int SEARCH_PADDING = 3;
  7. private static final int LIST_INIT_SIZE = 16;
  8. private static final String TAG = MainAbilitySlice.class.getName();
  9. private static final Lock MOVE_LOCK = new ReentrantLock();
  10. private MainAbilitySlice.MyCommonEventSubscriber subscriber;
  11. private TextField tvTextInput;
  12. private ScrollView scrollView;
  13. private DirectionalLayout keyBoardLayout;
  14. private TableLayout movieTableLayout;
  15. private Size size;
  16. private final AbilityMgr abilityMgr = new AbilityMgr(this);
  17. // 搜索的到的影片
  18. private final List<ComponentPointData> movieSearchList = new ArrayList<>(LIST_INIT_SIZE);
  19. // 当前焦点所在位置
  20. private ComponentPointData componentPointDataNow;
  21. // 注册流转任务管理服务后返回的Ability token
  22. private int abilityToken;
  23. // 用户在设备列表中选择设备后返回的设备ID
  24. private String selectDeviceId;
  25. // 获取流转任务管理服务管理类
  26. private IContinuationRegisterManager continuationRegisterManager;
  27. // 设置监听FA流转管理服务设备状态变更的回调
  28. private final IContinuationDeviceCallback callback = new IContinuationDeviceCallback() {
  29. @Override
  30. public void onDeviceConnectDone(String deviceId, String s1) {
  31. selectDeviceId = deviceId;
  32. abilityMgr.openRemoteAbility(selectDeviceId, getBundleName(), ABILITY_NAME);
  33. getUITaskDispatcher().asyncDispatch(() -> continuationRegisterManager.updateConnectStatus(abilityToken, selectDeviceId,
  34. DeviceConnectState.IDLE.getState(), null));
  35. }
  36. @Override
  37. public void onDeviceDisconnectDone(String deviceId) {
  38. }
  39. };
  40. // 设置注册FA流转管理服务服务回调
  41. private final RequestCallback requestCallback = new RequestCallback() {
  42. @Override
  43. public void onResult(int result) {
  44. abilityToken = result;
  45. }
  46. };
  47. @Override
  48. public void onStart(Intent intent) {
  49. super.onStart(intent);
  50. super.setUIContent(ResourceTable.Layout_ability_main);
  51. new PermissionBridge().setOnPermissionStateListener(this);
  52. // 全屏设置
  53. WindowManagerUtils.setWindows();
  54. // 初始化布局
  55. initComponent();
  56. // 初始化按钮组件
  57. initKeyBoardComponent(keyBoardLayout);
  58. // 初始化影片图片组件
  59. initMovieTableComponent(movieTableLayout);
  60. // 事件订阅
  61. subscribe();
  62. }
  63. private void registerTransService() {
  64. continuationRegisterManager = getContinuationRegisterManager();
  65. // 增加过滤条件
  66. ExtraParams params = new ExtraParams();
  67. String[] devTypes = new String[]{ExtraParams.DEVICETYPE_SMART_PAD, ExtraParams.DEVICETYPE_SMART_PHONE};
  68. params.setDevType(devTypes);
  69. // 注册FA流转管理服务
  70. continuationRegisterManager.register(getBundleName(), params, callback, requestCallback);
  71. }
  72. private void initMovieTableComponent(TableLayout tableLayout) {
  73. int index = 0;
  74. while (index < tableLayout.getChildCount()) {
  75. DirectionalLayout childLayout = null;
  76. if (tableLayout.getComponentAt(index) instanceof DirectionalLayout) {
  77. childLayout = (DirectionalLayout) tableLayout.getComponentAt(index);
  78. }
  79. ComponentPointData componentPointData = new ComponentPointData();
  80. Component component = null;
  81. int indexMovie = 0;
  82. while (childLayout != null && indexMovie < childLayout.getChildCount()) {
  83. Component comChild = childLayout.getComponentAt(indexMovie);
  84. indexMovie++;
  85. if (comChild instanceof Text) {
  86. componentPointData = ComponentPointDataMgr
  87. .getConstantMovie(((Text) comChild).getText()).orElse(null);
  88. continue;
  89. }
  90. component = findComponentById(comChild.getId());
  91. }
  92. if (componentPointData != null && component != null) {
  93. componentPointData.setComponentId(component.getId());
  94. }
  95. ComponentPointDataMgr.getComponentPointDataMgrs().add(componentPointData);
  96. index++;
  97. }
  98. }
  99. private void initKeyBoardComponent(DirectionalLayout directionalLayout) {
  100. int index = 0;
  101. while (index < directionalLayout.getChildCount()) {
  102. DirectionalLayout childLayout = null;
  103. if (directionalLayout.getComponentAt(index) instanceof DirectionalLayout) {
  104. childLayout = (DirectionalLayout) directionalLayout.getComponentAt(index);
  105. }
  106. int indexButton = 0;
  107. while (childLayout != null && indexButton < childLayout.getChildCount()) {
  108. if (childLayout.getComponentAt(indexButton) instanceof Button) {
  109. Button button = (Button) childLayout.getComponentAt(indexButton);
  110. buttonInit(button);
  111. indexButton++;
  112. }
  113. }
  114. index++;
  115. }
  116. }
  117. private void initComponent() {
  118. if (findComponentById(ResourceTable.Id_scrollview) instanceof ScrollView) {
  119. scrollView = (ScrollView) findComponentById(ResourceTable.Id_scrollview);
  120. }
  121. if (findComponentById(ResourceTable.Id_TV_input) instanceof TextField) {
  122. tvTextInput = (TextField) findComponentById(ResourceTable.Id_TV_input);
  123. // 初始化默认选中效果
  124. ComponentPointData componentPointData = new ComponentPointData();
  125. componentPointData.setComponentId(tvTextInput.getId());
  126. componentPointData.setPointX(0);
  127. componentPointData.setPointY(1);
  128. tvTextInput.requestFocus();
  129. componentPointDataNow = componentPointData;
  130. ComponentPointDataMgr.getComponentPointDataMgrs().add(componentPointDataNow);
  131. // 点击事件触发设备选取弹框
  132. tvTextInput.setClickedListener(component -> showDevicesDialog());
  133. }
  134. if (findComponentById(ResourceTable.Id_keyBoardComponent) instanceof DirectionalLayout) {
  135. keyBoardLayout = (DirectionalLayout) findComponentById(ResourceTable.Id_keyBoardComponent);
  136. }
  137. if (findComponentById(ResourceTable.Id_tableLayout) instanceof TableLayout) {
  138. movieTableLayout = (TableLayout) findComponentById(ResourceTable.Id_tableLayout);
  139. }
  140. if (findComponentById(ResourceTable.Id_image_ten) instanceof Image) {
  141. Image image = (Image) findComponentById(ResourceTable.Id_image_ten);
  142. size = image.getPixelMap().getImageInfo().size;
  143. }
  144. }
  145. private void showDevicesDialog() {
  146. ExtraParams extraParams = new ExtraParams();
  147. extraParams.setDevType(new String[]{ExtraParams.DEVICETYPE_SMART_TV,
  148. ExtraParams.DEVICETYPE_SMART_PHONE});
  149. extraParams.setDescription("远程遥控器");
  150. continuationRegisterManager.showDeviceList(abilityToken, extraParams, result -> LogUtils.info(TAG, "show devices success"));
  151. }
  152. private void buttonInit(Button button) {
  153. if (button.getId() == ResourceTable.Id_del) {
  154. findComponentById(button.getId()).setClickedListener(component -> {
  155. if (tvTextInput.getText().length() > 0) {
  156. tvTextInput.setText(tvTextInput.getText().substring(0, tvTextInput.getText().length() - 1));
  157. }
  158. });
  159. } else if (button.getId() == ResourceTable.Id_clear) {
  160. findComponentById(button.getId()).setClickedListener(component -> tvTextInput.setText(""));
  161. } else {
  162. findComponentById(button.getId()).setClickedListener(component -> tvTextInput.append(button.getText()));
  163. }
  164. }
  165. @Override
  166. public void onActive() {
  167. super.onActive();
  168. }
  169. @Override
  170. public void onForeground(Intent intent) {
  171. super.onForeground(intent);
  172. }
  173. @Override
  174. public void onPermissionGranted() {
  175. registerTransService();
  176. }
  177. @Override
  178. public void onPermissionDenied() {
  179. terminate();
  180. }
  181. /**
  182. * MyCommonEventSubscriber
  183. *
  184. * @since 2020-12-03
  185. */
  186. class MyCommonEventSubscriber extends CommonEventSubscriber {
  187. MyCommonEventSubscriber(CommonEventSubscribeInfo info) {
  188. super(info);
  189. }
  190. @Override
  191. public void onReceiveEvent(CommonEventData commonEventData) {
  192. Intent intent = commonEventData.getIntent();
  193. int requestType = intent.getIntParam("requestType", 0);
  194. String inputString = intent.getStringParam("inputString");
  195. if (requestType == ConnectManagerIml.REQUEST_SEND_DATA) {
  196. tvTextInput.setText(inputString);
  197. } else if (requestType == ConnectManagerIml.REQUEST_SEND_SEARCH) {
  198. // 如果当前选中的是文本框则搜索影片;如果当前选中的是影片则播放影片;X轴坐标为0当前选中文本框;X轴大于1当前选中影片
  199. if (componentPointDataNow.getPointX() == 0) {
  200. // 调用大屏的搜索方法
  201. searchMovies(tvTextInput.getText());
  202. return;
  203. }
  204. // 播放影片
  205. abilityMgr.playMovie(getBundleName(), MOVIE_PLAY_ABILITY);
  206. } else {
  207. // 移动方向
  208. String moveString = intent.getStringParam("move");
  209. MainCallBack.movePoint(MainAbilitySlice.this, moveString);
  210. }
  211. }
  212. }
  213. /**
  214. * goBack
  215. */
  216. public void goBack() {
  217. clearLastBackg();
  218. scrollView.scrollTo(0, 0);
  219. componentPointDataNow = ComponentPointDataMgr.getMoviePoint(0, 1).orElse(null);
  220. findComponentById(ResourceTable.Id_TV_input).requestFocus();
  221. abilityMgr.returnMainAbility(getBundleName(), MAIN_ABILITY);
  222. }
  223. /**
  224. * move
  225. *
  226. * @param pointX pointX
  227. * @param pointY pointY
  228. */
  229. public void move(int pointX, int pointY) {
  230. MOVE_LOCK.lock();
  231. try {
  232. // 设置焦点滚动
  233. if (pointX == 0 && componentPointDataNow.getPointX() > 0) {
  234. scrollView.fluentScrollByY(pointY * size.height);
  235. }
  236. if (componentPointDataNow.getPointX() == 0 && pointX == 1) {
  237. scrollView.scrollTo(0, 0);
  238. }
  239. // 设置背景
  240. if (componentPointDataNow.getPointX() + pointX == 0) {
  241. setBackGround(componentPointDataNow.getPointX() + pointX, 1);
  242. } else {
  243. setBackGround(componentPointDataNow.getPointX() + pointX, componentPointDataNow.getPointY() + pointY);
  244. }
  245. } finally {
  246. MOVE_LOCK.unlock();
  247. }
  248. }
  249. private void setBackGround(int pointX, int pointY) {
  250. ComponentPointData componentPointDataNew = ComponentPointDataMgr.getMoviePoint(pointX, pointY).orElse(null);
  251. if (componentPointDataNew == null) {
  252. return;
  253. }
  254. // 清除上次选中的效果
  255. clearLastBackg();
  256. componentPointDataNow = componentPointDataNew;
  257. if (findComponentById(componentPointDataNow.getComponentId()) instanceof Image) {
  258. Image newImage = (Image) findComponentById(componentPointDataNow.getComponentId());
  259. newImage.setPadding(FOCUS_PADDING, FOCUS_PADDING, FOCUS_PADDING, FOCUS_PADDING);
  260. } else {
  261. Component component = findComponentById(componentPointDataNow.getComponentId());
  262. component.requestFocus();
  263. }
  264. }
  265. private void clearLastBackg() {
  266. Component component = null;
  267. if (findComponentById(componentPointDataNow.getComponentId()) instanceof TextField) {
  268. TextField textField = (TextField) findComponentById(componentPointDataNow.getComponentId());
  269. textField.clearFocus();
  270. } else {
  271. component = findComponentById(componentPointDataNow.getComponentId());
  272. component.setPadding(0, 0, 0, 0);
  273. }
  274. // 如果是搜索出来的还是保持搜索到的背景
  275. for (ComponentPointData componentPointData : movieSearchList) {
  276. if (component != null && componentPointData.getComponentId() == component.getId()) {
  277. component.setPadding(SEARCH_PADDING, SEARCH_PADDING, SEARCH_PADDING, SEARCH_PADDING);
  278. }
  279. }
  280. }
  281. private void searchMovies(String text) {
  282. if (text == null || "".equals(text)) {
  283. return;
  284. }
  285. // 清空上次搜索结果及背景效果
  286. clearHistroyBackGround();
  287. for (ComponentPointData componentPointData : ComponentPointDataMgr.getComponentPointDataMgrs()) {
  288. if (MovieSearchUtils.isContainMovie(componentPointData.getMovieName(), text)
  289. || MovieSearchUtils.isContainMovie(componentPointData.getMovieFirstName(), text)) {
  290. movieSearchList.add(componentPointData);
  291. Component component = findComponentById(componentPointData.getComponentId());
  292. component.setPadding(SEARCH_PADDING, SEARCH_PADDING, SEARCH_PADDING, SEARCH_PADDING);
  293. }
  294. }
  295. if (movieSearchList.size() > 0) {
  296. componentPointDataNow = movieSearchList.get(0);
  297. Component component = findComponentById(componentPointDataNow.getComponentId());
  298. component.setPadding(FOCUS_PADDING, FOCUS_PADDING, FOCUS_PADDING, FOCUS_PADDING);
  299. } else {
  300. Component component = findComponentById(componentPointDataNow.getComponentId());
  301. component.requestFocus();
  302. }
  303. }
  304. private void clearHistroyBackGround() {
  305. // 清空上次搜索的结果
  306. for (ComponentPointData componentPointData : movieSearchList) {
  307. Component component = findComponentById(componentPointData.getComponentId());
  308. component.setPadding(0, 0, 0, 0);
  309. }
  310. movieSearchList.clear();
  311. // 去掉当前焦点背景
  312. clearLastBackg();
  313. }
  314. private void subscribe() {
  315. MatchingSkills matchingSkills = new MatchingSkills();
  316. matchingSkills.addEvent(EventConstants.SCREEN_REMOTE_CONTROLL_EVENT);
  317. matchingSkills.addEvent(CommonEventSupport.COMMON_EVENT_SCREEN_ON);
  318. CommonEventSubscribeInfo subscribeInfo = new CommonEventSubscribeInfo(matchingSkills);
  319. subscriber = new MainAbilitySlice.MyCommonEventSubscriber(subscribeInfo);
  320. try {
  321. CommonEventManager.subscribeCommonEvent(subscriber);
  322. } catch (RemoteException e) {
  323. LogUtils.error("", "subscribeCommonEvent occur exception.");
  324. }
  325. }
  326. private void unSubscribe() {
  327. try {
  328. CommonEventManager.unsubscribeCommonEvent(subscriber);
  329. } catch (RemoteException e) {
  330. LogUtils.error(TAG, "unSubscribe Exception");
  331. }
  332. }
  333. @Override
  334. protected void onStop() {
  335. super.onStop();
  336. unSubscribe();
  337. }
  338. }

RemoteInputAbilitySlice.java 

  1. public class RemoteInputAbilitySlice extends AbilitySlice {
  2. private static final int SHOW_KEYBOARD_DELAY = 800;
  3. private static final int INIT_SIZE = 8;
  4. private ConnectManager connectManager;
  5. private TextField textField;
  6. private String deviceIdConn;
  7. private Component okButton;
  8. private Component leftButton;
  9. private Component rightButton;
  10. private Component upButton;
  11. private Component downButton;
  12. private Component goBackButton;
  13. private Component closeButton;
  14. @Override
  15. public void onStart(Intent intent) {
  16. super.onStart(intent);
  17. super.setUIContent(ResourceTable.Layout_ability_remote_input);
  18. // 获取大屏ID用于配对
  19. deviceIdConn = intent.getStringParam("localDeviceId");
  20. // 全屏设置
  21. WindowManagerUtils.setWindows();
  22. initView();
  23. initListener();
  24. showKeyBoard();
  25. initConnManager();
  26. }
  27. @Override
  28. public void onActive() {
  29. super.onActive();
  30. }
  31. @Override
  32. public void onForeground(Intent intent) {
  33. super.onForeground(intent);
  34. }
  35. private void initView() {
  36. if (findComponentById(ResourceTable.Id_remote_input) instanceof TextField) {
  37. textField = (TextField) findComponentById(ResourceTable.Id_remote_input);
  38. textField.requestFocus();
  39. }
  40. okButton = findComponentById(ResourceTable.Id_ok_button);
  41. leftButton = findComponentById(ResourceTable.Id_left_button);
  42. rightButton = findComponentById(ResourceTable.Id_right_button);
  43. upButton = findComponentById(ResourceTable.Id_up_button);
  44. downButton = findComponentById(ResourceTable.Id_down_button);
  45. goBackButton = findComponentById(ResourceTable.Id_go_back);
  46. closeButton = findComponentById(ResourceTable.Id_close_fa);
  47. }
  48. private void initListener() {
  49. // 监听文本变化,远程同步
  50. textField.addTextObserver((ss, ii, i1, i2) -> {
  51. Map<String, String> map = new HashMap<>(INIT_SIZE);
  52. map.put("inputString", ss);
  53. connectManager.sendRequest(ConnectManagerIml.REQUEST_SEND_DATA, map);
  54. });
  55. okButton.setClickedListener(component -> {
  56. // 点击OK按钮
  57. buttonClickSound();
  58. String searchString = textField.getText();
  59. Map<String, String> map = new HashMap<>(INIT_SIZE);
  60. map.put("inputString", searchString);
  61. connectManager.sendRequest(ConnectManagerIml.REQUEST_SEND_SEARCH, map);
  62. });
  63. leftButton.setClickedListener(component -> {
  64. // 点击左键按钮
  65. sendMoveRequest(Constants.MOVE_LEFT);
  66. });
  67. rightButton.setClickedListener(component -> {
  68. // 点击右键按钮
  69. sendMoveRequest(Constants.MOVE_RIGHT);
  70. });
  71. upButton.setClickedListener(component -> {
  72. // 点击向上按钮
  73. sendMoveRequest(Constants.MOVE_UP);
  74. });
  75. downButton.setClickedListener(component -> {
  76. // 点击向下按钮
  77. sendMoveRequest(Constants.MOVE_DOWN);
  78. });
  79. goBackButton.setClickedListener(component -> {
  80. // 返回大屏主页
  81. sendMoveRequest(Constants.GO_BACK);
  82. });
  83. closeButton.setClickedListener(component -> {
  84. // 返回主页
  85. sendMoveRequest(Constants.GO_BACK);
  86. terminateAbility();
  87. });
  88. }
  89. private void sendMoveRequest(String direction) {
  90. buttonClickSound();
  91. Map<String, String> map = new HashMap<>(INIT_SIZE);
  92. map.put("move", direction);
  93. connectManager.sendRequest(ConnectManagerIml.REQUEST_SEND_MOVE, map);
  94. }
  95. private void showKeyBoard() {
  96. getUITaskDispatcher().delayDispatch(() -> textField.simulateClick(), SHOW_KEYBOARD_DELAY);
  97. }
  98. private void initConnManager() {
  99. connectManager = ConnectManagerIml.getInstance();
  100. connectManager.connectPa(this, deviceIdConn);
  101. }
  102. private void buttonClickSound() {
  103. // 步骤1:实例化对象
  104. SoundPlayer soundPlayer = new SoundPlayer("packageName");
  105. // 步骤2:播放键盘敲击音,音量为1.0
  106. soundPlayer.playSound(SoundPlayer.SoundType.KEY_CLICK, 1.0f);
  107. }
  108. }

原理:

(1)MainAbilitySlice#onStart生命周期中会做事件注册,类似Android中注册广播接收subscribe()

(2)在事件接收者onReceiveEvent中处理来自手机输入法输入的数据/请求查询/播放等命令。如果是数据则显示在电视端文本输入框中,并搜索该影片。

(3)MainAbilitySlice#callback当监听到设备组网连接成功后,打开RemoteInputAbilitySlice该页面就是手机端输入页面,在initConnManager中会打开PA类似Android中service,在IAbilityConnection中回拿到远端电视端的引用

IRemoteObject接口,类似Android中拿到Ibinder接口。后续会用该接口给电视端发送数据

(4)手机端输入页面监听用户输入文本变化后会connectManager.sendRequest(ConnectManagerIml.REQUEST_SEND_DATA, map),通过代理最终通过IRemoteObject接口给电视端发数据。后续到(2)电视onReceiveEvent接收到数据后处理对应命令

(5)按照华为官网描述该过程基于分布式任务调度和公共事件,将远端移动操作请求发送给TV端,类比Android通过binder机制完成跨进程Service通信,鸿蒙分布式调度底层通过极简D2D传输协议栈。

特点:夸设备分布式任务调度,硬件互助

应用场景:冰箱通过手机输入法输入,家电使用手机相机作为输入等

三、前端JS/eTS部分

前端模板渲染过程

前端工程师开发模板页面,到界面显示的几个主要经过如下阶段,如下图:

如图,前端同学写的页面模板,在编译阶段先后会经过抽象语法树AST将前端页面转化成树形结构JS对象(大JSON),然后编译生成渲染函数(h函数)。运行时渲染函数会返回虚拟节点VDom,有更新会有相关diff操作,最后patch到真实dom节点来渲染页面。

方舟开发框架ArkUI

鸿蒙系统除了支持Java外,还支持以js/ts方式来开发应用(注:openHarmony不支持Java)。方舟开发框架(简称:ArkUI),是一套UI开发框架,提供开发者进行应用UI开发时所必须的能力。方舟开发框架提供了两种开发方式分别是基于JS扩展的类Web开发范式(采用经典的HML、CSS、JavaScript三段式开发方式)和基于TS扩展的声明式开发范式(类似Flutter页面开发)。方舟开发框架ArkUI如下图:

可以看出,声明式开发范式TS无需JS Framework进行页面DOM管理,渲染。

线程模型

每个ACE JS应用的进程,包含唯一一个Platform线程和若干后台线程组成的异步任务线程池:

● Platform线程:当前平台的主线程,也就是应用的主线程,主要负责平台层的交互、应用生命周期以及窗口环境的创建

● 后台线程池:一系列后台任务,用于一些低优先级的可并行异步任务,如网络请求、Asset资源加载等。除此之外,每个实例还包括一系列专有线程

● JS线程:JS前端框架的执行线程,应用的JS逻辑以及应用UI界面的解析构建都在该线程执行

● UI线程:引擎的核心线程,组件树的构建以及整个渲染管线的核心逻辑都在该线程:包括渲染树的构建、布局、绘制以及动画调度

● GPU线程:现代的渲染引擎,为了充分发挥硬件性能,都支持GPU硬件加速,在该线程上,会通过系统的窗口句柄,创建GPU加速的OpenGL环境,负责将整个渲染树的内容光栅化,直接将每一帧的内容渲染合成到该窗口的Surface上并送显

● IO线程:主要为了异步的文件IO读写,同时该线程会创建一个离屏的GL环境,这个环境和 GPU线程的GL环境是同一个共享组,可以共享资源,图片资源解码的内容可直接在该线程上传生成GPU纹理,实现更高效的图片渲染

ACE UI框架

                  图1 官网Web开发范式的方舟开发框架                      图2 ACE UI框架图

如上图,图1图2大体相似,为ACE(Ability Cross-platform Environment (元能力跨平台执行环境)) UI框架:

ACE UI框架的整体架构如下图所示,主要由前端框架层、桥接层、引擎层和平台抽象层四大部分组成,下面我们一一介绍。

前端框架层

该层主要包括相应的开发范式(比如主流的类Web开发范式),组件/API,以及编程模型MVVM(Model-View-ViewModel),底层可以理解是个阉割版的mini-vue、JS 引擎使用的是 QuickJS,比V8/jscore更加轻量级,是一款由三星开发的嵌入式 JS 引擎,官网QuickJS Javascript Engine。代码路径OpenHarmony\foundation\arkui\ace_engine\frameworks\bridge\js_frontend\engine。

前端框架包括两部分,运行在js线程的js Framework及JS 引擎和 运行在UI线程的前端框架对接部分。如下图:

如图,在ACE UI的轻量化实现中,通过前端框架核心下沉C++化,减小JS部分的内存占用,使用C++进行更为严格的内存分配与管理,并且采用更为轻量的JS引擎,UI部分采用轻量的UIKit并结合轻量图形引擎,达到内存非常轻量占用的目标。同时,前端框架对接层通过ACE引擎层提供的Component组件实现前端组件定义的能力。Component是一个由C++实现的UI组件的声明式描述,描述了UI组件的属性及样式,用于生成组件的实体元素。每一个前端组件会对接到一个Composed Component,表示一个组合型的UI组件,通过不同的子Component组合,构造出前端对应的Composed组件。

页面渲染与更新diff/patch操作遵循三棵树原则,如下:

三棵树分别为Component树,Element树和Render树,可以对比Flutter/动态化跨端方案react native等三棵树,及diff/patch增量更新。

如上图所示,目标要将整个Page的Component描述挂载到StageElement上,如果当前Stage下还未有任何Element节点,就会递归逐个节点生成Component对应的Element节点。对于组合类型的ComposedElement,则同时会把Element的引用记录到一个Composed Map中,方便后续更新时快速查找。对于可见类型的容器节点或渲染节点,则会创建对应的RenderNode,并挂在Render树上。当生成了当前页面的Element树和Render树,页面渲染构建的完整过程就结束了。

桥接层

该层主要是作为一个中间层,实现前端开发范式到底层引擎(包括UI后端,语言&运行时)的对接

引擎层

该层主要包含两部分:UI后端引擎和语言执行引擎。

1.由C++构建的UI后端引擎,包括动画解析、DOM(Document Object Model)树构建、布局计算、渲染命令构建与绘制、事件管理等能力UI组件、布局视图、动画事件、自绘制渲染管线和渲染引擎 。

在渲染方面,为不同前端框架提供灵活的UI能力,这部分通过C++组件组合而成。通过底层组件的按需组合,布局计算和渲染并行化,并结合上层开发范式实现了视图变化最小化的局部更新机制,从而实现高效的UI渲染。

除此之外,引擎层还提供了组件的渲染管线、动画、主题、事件处理等基础能力。目前复用了Flutter引擎提供基础的图形渲染能力、字体管理、文字排版等能力,底层使用Skia或其他图形库实现,并通过OpenGL实现GPU硬件渲染加速。

在多设备UI适配方面,通过多种原子化布局能力(自动折行、隐藏、等比缩放等),多态UI控件(描述统一,表现形式多样),以及统一交互框架(不同的交互方式归一到统一的事件处理)来满足不同设备的形态差异化需求。

另外,引擎层也包含了能力扩展基础设施,来实现自定义组件以及系统API的能力扩展;语言&运行时执行引擎。可根据需要切换到不同的运行时执行引擎,满足不同设备的能力差异化需求。

平台抽象层

适配层主要完成对平台层进行抽象,提供抽象接口,可以对接到系统平台。比如:事件对接、渲染管线对接和系统生命周期对接等。

前端开发参考技术:

1.Flex布局:Flex 布局语法教程 | 菜鸟教程

2.MVVM:

3.响应式原理:深入响应式原理 — Vue.js

鸿蒙JS/eTS开发例子:

1.分布式新闻客户端(JS)

2.分布式新闻客户端(eTS)

四、鸿蒙系统带给我们的思考

新硬件

  • 软件定义硬件,软件服务决定了需要的硬件组合

  • 设备不在独立运作,设备间实现系统级融合,单点设备为分布式软总线上一个功能单元

  • 硬件组合体灵活按需适应不同场景需求

新交互

  • 人和设备间,体现在用户使用设备不在点对点使用,通过智能终端(手机/智慧屏)来和多设备交互

  • 设备间交互,体现在多设备融为一个分布式系统,根 据业务场景需要使用系统中不同设备提供的能力,设备间交互更加便捷智能协同

新服务

  • 服务不在局限于单设备能提供的能力,可动态扩展

  • 服务不会因为环境场景变化而中断,通过技术手段实现跨设备 不间断持续提供服务

  • 通过原子化服务,实现设备间服务转发流转

1+8+N战略

华为提出鸿蒙1+8+N战略,N个智能终端将化繁为简成为1个智慧助理。对我们也有参考借鉴意义,在我们的多终端设备中基于分布式软总线,通过智能终端(手机/智慧屏)来作为设备中枢,控制其他N多智能终端,形成一整套终端服务。

五、我们如何做

系统占有率

         图1·各操作系统09—21年市场占有率              图2·移动端操作系统09—21年市场占有率

根据图1,操作系统要长期能再市场存活必须保持市场占有率在16%以上否则会慢慢被淘汰,而图2移动端截止2021年Android市场份额接近73%,ios接近27%。华为内部在2021年为鸿蒙设下的生死线,是16%的市场占有率。

  • 增量市场来看,需要覆盖新用户2.1亿左右,实际卖出5000万(更多是老用户EMUI升级而来),市占约为3.8%。

  • 存量市场来看,截止至2021年底,全球智能手机的保有量超过30亿,华为鸿蒙总计覆盖了2.2亿,这个市占率大约为7.3%左右。

  • 再看整个智能设备市场,即含手机、电视、手表、手环等等这些鸿蒙涉及到的市场,这个市场的目前是安卓系统占了60%以上的份额。而这些智能设备在全球的存量约为60亿以上,华为鸿蒙一共覆盖了3.2亿,其比例大约为5.5%

综上所述,华为鸿蒙系统要能在市场上稳定存在,达到16%的市场占有率仍然处于困难阶段。

鸿蒙系统存在问题

图1                                                                图2

图1,图2为某专业机构对华为手机/鸿蒙系统从用户使用者角度出发做的市场调研,根据调研情况及相关资料显示,华为鸿蒙系统存在以下较为突出问题:

  • 系统兼容性差,部分应用在Android上没问题,在鸿蒙上容易崩溃

  • 不流畅,部分app卡顿较为明显

  • 耗电量大问题,鸿蒙官网解释说有后台任务系统优化,过2天会正常

参考链接:艾媒舆情|2021年鸿蒙系统市场竞争力状况与消费者行为调研报告 鸿狼

业界态度及相关建议

业界态度

  • 手机厂商中兴、小米、OPPO、vivo等表示不会使用鸿蒙系统(原因: 自研,鸿蒙没有全部开源, 生态,竞争关系),更重要原因是手机对生态依赖强,Android生态更加健全

  • 美的,苏泊尔,九阳等部分产品使用Openharmony,家电对应用生态依赖不是强依赖关系, 可以根据自身需要自研鸿蒙app即可。同时,家电都在一个局域网更符合鸿蒙分布式特点

关注行业内鸿蒙系统动向,手机APP应用层适配支持鸿蒙

  • 智家,三翼鸟等多款app积极适配支持鸿蒙系统

尝试局域网内APP间相互通信技术研究,并在合适的场景中落地

  • 鸿蒙系统本质完成了跨设备跨进程app间相互访问,数据传递

  • 基于Android系统进行相关技术研究,如APP应用层实现鸿蒙软总线设备间通信基础能力

  • 在具体业务中落地

长期来看,实现海尔家电各设备间硬件互助,资源共享特性

  • 除了通过lot云控来实现手机对家电控制外,长远来看可以将家电各设备都拉入软总线体系, 建立完善的设备间发现机制(设备上线,下线监听),设备间能力数据信息共享,组成一个分布式 智能终端系统,通过不同设备间基础软总线,rpc能力硬件互助调用,给用户带来更加极致的用户体验。

参考资料

公告 | 华为开发者联盟

https://forum.gitlink.org.cn/forums/6699/detail

渲染引擎分析 - 鸿蒙(OpenHarmony) JS UI 源码阅读笔记

HDC2021技术分论坛:跨端分布式计算技术初探

https://blog.csdn.net/weixin_46669761/category_11766782.html

《鸿蒙操作系统分布式软总线技术》 调研报告 - 永野芽郁男友 - 博客园

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小蓝xlanll/article/detail/314607
推荐阅读
相关标签
  

闽ICP备14008679号