写点什么

优酷鸿蒙开发实践 | 鸿蒙卡片开发

发布于: 2021 年 10 月 14 日
优酷鸿蒙开发实践 | 鸿蒙卡片开发


作者:苎麻


 如标题所述,我们将持续更新《优酷鸿蒙开发实践》系列文章。本文为系列首篇技术文章,后续文章包括:鸿蒙/Android 混合打包技术实践,多屏互动技术实践等,欢迎持续关注【阿里巴巴移动技术】。”

背景

随着华为 Harmony OS2.0 的发布,各大厂商纷纷抢先与华为展开合作。优酷作为国内领先的长视频在线视听平台,与华为公司长期以来保持紧密的合作,共同为消费者带来优质的影音娱乐体验。因此,优酷技术团队也在第一时间投入对鸿蒙系统以及鸿蒙开发者 SDK 的研究。优酷技术团队经过多轮的头脑风暴,利用鸿蒙的某些新特性展开鸿蒙应用开发的尝试。


鸿蒙 OS 支持应用以 Ability 为单位进行部署。Ability 分为两种类型:FA(Feature Ability)和 PA(Particle Ability)。FA/PA 是应用的基本组成单元,能够实现特定的业务功能。FA 有 UI 界面,而 PA 无 UI 界面。


每种类型为开发者提供了不同的模板,以便实现不同的业务功能。


鸿蒙 OS 的应用软件包以 APP Pack(Application Package)形式发布,它是由一个或多个 HAP(HarmonyOS Ability Package)以及描述每个 HAP 属性的 pack.info 组成。HAP 是 Ability 的部署包,HarmonyOS 应用代码围绕 Ability 组件展开。



鸿蒙工程通过鸿蒙打包工具链打包后,其产物格式即为 HAP。


当前,包含有鸿蒙 FA/PA 的优酷鸿蒙版已经在华为鸿蒙应用市场上架,鸿蒙混合包在应用市场上会显示为“含 HarmonyOS 服务”。如果 App 是 100% Pure 鸿蒙 App,其 Icon 右下角会有 HMOS 字样。


在手机桌面上的优酷 Icon 轻轻上滑,会弹出一个鸿蒙卡片,向用户推荐最近的热剧,点击卡片能快速拉起半屏落地页显示更多信息,点击落地页则跳转到优酷客户端的相应落页面。


点击卡片上的图钉按钮,还可以将这个 FA 卡片固定在桌面上。




这个 FA 是 100%利用鸿蒙 API 编写的,可以脱离优酷主客独立运行。由于 FA 卡片有极其严格的体积限制,而使用 native 的库体积则会过大。最终,我们的 Widget 通过一个 Webview,加载 JS 版本的前端网络库去请求优酷内部的网络接口,获取到数据后再使用鸿蒙的 Native 图形图像 API 去绘制 Native 界面。


这个桌面 Widget 与 iOS 桌面 Widget 的区别在于,它不依赖于优酷主客即可运作。即使优酷主客不被启动,卡片的数据也能够更新。

鸿蒙卡片的开发模式

在鸿蒙系统上,触摸优酷主客的应用图标向上滑动,可以唤起优酷的鸿蒙卡片。实现这一点需要卡片的实现代码与优酷主客做混合打包,一起提交到应用市场。


而如果要实现服务中心免安装使用,则需要卡片的独立包总大小要小于 10M。这一体积限制使得很多 Native 库都无法引入,否则无法将体积控制在红线之内。


最终,优酷鸿蒙卡片的代码放在一个工程中,方便跟优酷主客进行混合打包。同时,优酷鸿蒙卡片的代码仅依赖极少数的二、三方库(例如 JSON 解析、图片缓存等),以减小体积。

卡片样式

鸿蒙系统提供 4 中大小不同的卡片,根据占用桌面图标数量的不同,分别是: 1x2、2x2、2x4、4x4。优酷卡片实现了其中两种: 2x2 和 2x4,其中 2x2 的卡片是必选项。


下图显示了两种不同样式的卡片,以及不同的出现场景。


声明卡片

跟 Android 的应用微件类似,鸿蒙的卡片也需要在一个配置文件中声明。在一个鸿蒙应用中,每个模块都有自己的配置文件,位于该模块的代码 main 目录下,名字为 config.json。


在配置文件中,每个模块有一个 abilities 属性,其值是一个数组,数组的每一个对象都定义了一个 Ability。卡片就定义在其中一个 Ability 中:


{  ...        "formsEnabled": true,        "forms": [          {            "landscapeLayouts": [              "$layout:youku_widget_2_2",              "$layout:youku_widget_2_4"            ],            "isDefault": true,            "defaultDimension": "2*2",            "name": "youku_widget",            "description": "$string:yk_widget_description",            "colorMode": "auto",            "type": "Java",            "supportDimensions": [              "2*2",              "2*4"            ],            "portraitLayouts": [              "$layout:youku_widget_2_2",              "$layout:youku_widget_2_4"            ],            "updateEnabled": true,            "updateDuration": 1          }        ],  ...}
复制代码


鸿蒙系统中,卡片用 Form 来表示。上述声明中,formsEnabled 用于指示这个 Ability 是用于定义卡片的。forms 数组用来定义一系列的卡片。通常多个卡片可以定义在一个数组元素中。其中 landscapeLayouts、portraitLayouts、supportDimensions 用于定义卡片的布局文件和大小,updateEnabled、updateDuration 用于控制卡片的数据更新,updateDuration 的单位是半小时。

生命周期

在鸿蒙系统上,卡片的生命周期比普通的 Page Ability 要简单很多,只有三个相关的回调:


/** * 创建卡片时的回调。 * 在intent中,存有创建卡片的一些重要参数,可以通过Intent.getXXXParam()方法获取。 * AbilitySlice.PARAM_FORM_IDENTITY_KEY: long类型,用于唯一标识一个卡片 * AbilitySlice.PARAM_FORM_NAME_KEY: String类型,卡片名称,即在config.json中定义的name属性 * AbilitySlice.PARAM_FORM_DIMENSION_KEY: int类型,卡片大小标识, * 取值范围是1-4,分别表示1x2、2x2、2x4、4x4 */protected ProviderFormInfo onCreateForm(Intent intent)
/** * 更新卡片时的回调。 * 这里的formId就是onCreateForm中的AbilitySlice.PARAM_FORM_IDENTITY_KEY参数。 */protected void onUpdateForm(long formId)
/** * 删除卡片时的回调。 * 这里的formId就是onCreateForm中的AbilitySlice.PARAM_FORM_IDENTITY_KEY参数。 */protected void onDeleteForm(long formId)
复制代码

传输卡片内容

卡片的创建和显示通常由桌面(或者服务中心、搜索)发起,而决定显示内容的是优酷卡片这个模块,内容提供方和显示方不在同一个进程,甚至由不同开发者开发。在 Android 上也是一样的情况。


在这种情况下,一般都是内容提供方通过远程 View 的方式将内容渲染到内容显示方的,鸿蒙系统上这个跨进程的数据传输行为是由 ComponentProvider 来实现的。


创建 ComponentProvider 有两种方式:


// 第一种: 在onCreateForm()时,先创建一个卡片对应的ProviderFormInfo实例。// 再通过ProviderFormInfo的实例拿到向它传输数据的ComponentProvider。ProviderFormInfo form = new ProviderFormInfo(layoutId, context);ComponentProvider cp = form.getComponentProvider();
// 第二种: 在onUpdateForm()时,直接创建出一个ComponentProvider。ComponentProvider cp = new ComponentProvider(layoutId, context);
复制代码


需要注意的问题 1


其中设置 IntentAgent 时需要注意,通常一个布局中会有多个 View 来覆盖根布局的矩形区域。如果设置了 IntentAgent 的 View 没有覆盖满根布局,则未覆盖区域被点击时,系统也会响应点击,默认调起这个卡片所属的 Ability,传入的 Intent 只包含 formId。


针对这个默认调起的 Ability,一般有两种方式解决:一是确保设置了 IntentAgent 的 View 覆盖满根布局;二是 Ability 提供兜底方案,例如页面做成透明,并且自动退出。


需要注意的问题 2


在创建 IntentAgent 时,需要提供一个 IntentAgentInfo 实例。这个 IntentAgentInfo 创建时的第一个参数是一个 int 类型的请求代码,这个代码必须保持各个卡片的不同点击区都不一样。否则后设置的 IntentAgent 会覆盖先前设置的同一请求代码的 IntentAgent。


需要注意的问题 3


如果是跟优酷主客混合打包,卡片的布局文件中,View 的 id 必须跟主客中所有的 id 不同,否则系统会无法正确更新布局文件中对应的 View。

打开中转页

由于系统的限制,点击卡片打开的页面必须是纯鸿蒙应用中的页面,无法直接打开 Android 应用页面。优酷卡片的点击,目的是打开优酷主客的播放页。


在这里我们做了分类:


  • 当用户未安装优酷主客时,显示一个中转页,提供下载按钮供用户跳转到华为应用市场去下载优酷主客,当用户安装完优酷主客回来时,下载按钮变成选集列表,对单集视频则变成播放按钮;

  • 当用户已安装优酷主客时,中转页自动打开优酷主客的播放页,并退出。

数据请求

在优酷主客中,网络数据的请求都是通过统一的网络库访问的。由于优酷鸿蒙卡片并未集成网络库,优酷鸿蒙卡片必须使用其他方式请求网络接口。


要实现在鸿蒙上发起数据请求,有两个方案:


  • 一是针对每个数据请求接口,封装一个新的 HTTP Open API 接口,客户端可以通过 HTTP(S)直接访问;

  • 二是客户端通过 H5 页面里的 JS 版 Network 库发起数据请求。


考虑到将来在鸿蒙系统上有可能实现更多其他的需求,且第一种方案有安全性的风险,所以最终我们采用了第二种方案。


前端业务使用的 JS 版本的网络库,其使用方式是通过 JS 中的 Promise 机制来实现异步回调,但是这种方式在 Java 中并不好实现对应的调用结构。所以这里需要有一层封装,将网络请求结果通过简单回调来通知请求方。相应的在 Java 侧需要对 WebView 注册全局的 JS 对象,实现 JS 对象的回调方法,打通 JS -> Java 的调用通路。


这个方案在纸面上看着还不错,但是在实际使用中会发现有严重的性能瓶颈。WebView 本身是一个很重的控件,在进程中首次创建的时候会比较耗时,有很多的 so 加载、初始化等工作。加载 HTML 是一个网络请求,耗时在百毫秒级,而加载并解析完 HTML 以后,还要再加载 JS 版本的网络库,又是一次网络访问。等 JS 网络库加载并解释执行后,才可以正常服务调用方。


要在这个过程中进行优化,这里有主动权的地方就是加载 HTML 和 JS 网络库这两个文件。在鸿蒙系统中,WebView 可以通过设置 WebAgent 来实现特定 URL 的劫持,将其转化为读取本地资源中的 HTML 和 JS 文件。


public class LoadAgent extends WebAgent {    // ...
@Override public ResourceResponse processResourceRequest(WebView webView, ResourceRequest request) { // mInterceptor用于识别HTML和JS网络库的URL,并返回本地资源中的HTML和JS。 ResourceResponse response = mInterceptor.intercept(request); if (response != null) { return response; } return super.processResourceRequest(webView, request); }}
复制代码


这可以将两个百毫秒级的串行操作缩减为毫秒级,大大减少 JS 版本的网络库的初始化时间。

数据缓存

从网络请求返回的卡片数据,除了用于即时渲染卡片之外,还会被保存一份到本地存储中。如果下一次发起网络请求的时候,无法正常访问网络(例如手机重启后一时连不上网络),则可以使用缓存中的卡片数据先渲染一下,使用户不至于完全看不到内容。这就需要有一套卡片数据的缓存管理能力。


针对卡片数据的特点,我们使用了两个数据库表来存储卡片的缓存数据。根据卡片大小不同,请求中会提供不同的参数给服务端。反过来说,同样大小的卡片发出请求的参数是相同的,也就是说同样大小的卡片请求得到的数据是相同的。所以有一个表用来存储不同大小的卡片数据,每个卡片大小对应一条记录,包括唯一标识、卡片大小、请求返回的数据、时间戳等。


系统不限制用户向桌面添加卡片的数量,同时在服务中心也可以有已经添加到桌面的卡片。所以同样的卡片数据是可以被显示在多个卡片上的。数据库需要有一个表来记录每一个卡片的信息,包括卡片的唯一标识、卡片大小、数据表中对应的记录等。



如果在 Android 中实现过 ContentProvider,一般都会比较熟悉 SQLite 数据库。通常 ContentProvider 需要管理大量、不同类型且互相有关联的数据,这种需求用 SQLite 来实现最合适了。这里管理卡片数据的缓存也具有同样的特征,并且鸿蒙系统也提供了 SQLite 数据库的使用接口。典型的数据库初始化操作如下:


// StoreConfig最常见的作用是配置数据库名字。也可以配置存储模式、加密等高级需求。StoreConfig config = StoreConfig.newDefaultConfig(DB_NAME_FORM_STORE);
// RdbOpenCallback用于定义创建数据库、升级数据库结构版本等时机的回调。RdbOpenCallback callback = new FormStoreOpenCallback(context);
DatabaseHelper helper = new DatabaseHelper(context);
// RdbStore是数据库的封装类,最终的增删改查操作都通过它来进行。RdbStore store = helper.getRdbStore(config, CURRENT_VERSION, callback);
复制代码


具体的增删改查操作就不一一列举了。

数据更新

前面声明卡片一节提到了 config.json 中,updateEnabled、updateDuration 定义了卡片的数据更新机制。


其中 updateEnabled 用于指定是否通过系统来自动更新卡片数据,如果希望由应用自身触发数据更新,这个可以设置为 false。优酷卡片的场景是希望系统能够自动更新卡片数据的,所以设为了 true。


在 updateEnabled 设为 true 的情况下,updateDuration 才有意义。updateDuration 用于指定更新的时间间隔。鸿蒙系统还支持固定时间更新,通过指定 scheduledUpateTime 来设置更新时间。updateDuration 和 scheduledUpateTime 只能选择其中一种方式。


优酷卡片选择了用 updateDuration 指定更新间隔。为了避免将来使用卡片的用户多了,对服务端产生过大的压力,更新间隔被控制在 4 小时,这样用户在上午、下午、晚上等不同时段去看卡片时,内容都会有更新。


但是有些情况下,优酷卡片自身的逻辑也会更新卡片数据,所以为了避免两种更新策略冲突而导致更密集的更新,或者长时间不更新,updateDuration 被指定为 1,即每半小时系统就会调用一次 onUpdateForm()。在 onUpdateForm()中,会判断上一次实际发生更新的时间,使更新间隔保持在 4 小时左右。

容错处理

考虑到一些极端情况,例如用户安装优酷后,在没有网络的情况下就添加了桌面卡片。此时卡片的数据请求是没有返回的,同时由于刚安装,也没有缓存数据,所以卡片展示不出任何数据,只有灰色的打底图作为背景。此时如果点击卡片,也没有任何视频信息,也就无法跳转到某个特定视频的播放页,只能显示一个加载失败的提示,等用户网络恢复后,通过刷新得到有效数据。


展望

现在优酷鸿蒙版的桌面卡片已经随着鸿蒙系统的发布,正式上线了。在鸿蒙系统的手机上,从华为应用市场安装的优酷主客就已经附带了优酷卡片的能力。


由于这是一个全新的开发技术栈,早期发布的应用肯定会有一些改进空间。从现在看来主要有以下一些方面:


  1. 性能

  2. 由于数据请求和埋点用到了 JS 库,并且在 WebView 中运行,这使得运行时效率比 Java 要低,还要处理 WebView 与外界的交互,对性能有较大影响。虽然已经有了一些措施来减少这方面的影响,但是后续还是需要继续挖掘潜力

  3. 监控

  4. 后续还需要补足 JS 侧崩溃等信息收集的能力。

  5. 线上配置能力

  6. 优酷主客可以通过各种远程配置平台下发各种配置信息。而鸿蒙上由于体积限制无法自带相关的库。今后需要考虑使用其他方式实现远程配置能力。


最后,10 月 20 日将上线《优酷鸿蒙开发实践》系列技术文章第二篇,为大家介绍如何实现 Android/鸿蒙混合打包的流程。感谢关注,我们下篇技术实践再见。


关注我们,每周 3 篇移动技术实践 &干货给你思考!

发布于: 2021 年 10 月 14 日阅读数: 95
用户头像

还未添加个人签名 2018.07.07 加入

阿里巴巴移动&终端技术官方账号。

评论

发布
暂无评论
优酷鸿蒙开发实践 | 鸿蒙卡片开发