優酷鴻蒙開發實踐 | 鴻蒙卡片開發

阿里巴巴移動技術發表於2021-10-19

作者:苧麻

 如標題所述,我們將持續更新《優酷鴻蒙開發實踐》系列文章。本文為系列首篇技術文章,後續文章包括:鴻蒙/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. 效能\
    由於資料請求和埋點用到了JS庫,並且在WebView中執行,這使得執行時效率比Java要低,還要處理WebView與外界的互動,對效能有較大影響。雖然已經有了一些措施來減少這方面的影響,但是後續還是需要繼續挖掘潛力
  2. 監控\
    後續還需要補足JS側崩潰等資訊收集的能力。
  3. 線上配置能力\
    優酷主客可以通過各種遠端配置平臺下發各種配置資訊。而鴻蒙上由於體積限制無法自帶相關的庫。今後需要考慮使用其他方式實現遠端配置能力。

最後,10月20日將上線《優酷鴻蒙開發實踐》系列技術文章第二篇,為大家介紹如何實現Android/鴻蒙混合打包的流程。感謝關注,我們下篇技術實踐再見。

關注我們,每週 3 篇移動技術實踐&乾貨給你思考!

相關文章