作者:包月東
簡(jiǎn)介
? JS FA調(diào)用Java PA一般通過(guò)FeatureAbility這種傳統(tǒng)方式調(diào)用。本文先回顧傳統(tǒng)方式,在傳統(tǒng)方式的痛點(diǎn)上引入兩種新方式。
傳統(tǒng)方式
方法說(shuō)明
常見(jiàn)的JS FA調(diào)用Java Pa一般通過(guò)以下三個(gè)方法
(1) FeatureAbility.callAbility(OBJECT):調(diào)用PA能力。
(2) FeatureAbility.subscribeAbilityEvent(OBJECT, Function):訂閱PA能力。
(3) FeatureAbility.unsubscribeAbilityEvent(OBJECT):取消訂閱PA能力。
1.能常用于同步調(diào)用方法
比如:
//實(shí)現(xiàn)(Java)
public int add(int a, int b) {
return a + b;
}
//調(diào)用:
add: async function (data) {
let params = getRequestAction(ACTION_MESSAGE_CODE_ADD);
params.data = data;
var ret = await FeatureAbility.callAbility(params)
console.log("result:" + JSON.parse(ret) + "," + ret)
return JSON.parse(ret)
}
2.常用于調(diào)用異步方法,因?yàn)椋?)是通過(guò)監(jiān)聽的方式實(shí)現(xiàn)異步回調(diào),所以在不需要監(jiān)聽時(shí)最好調(diào)用(3)取消
比如:
//實(shí)現(xiàn)(Java)
public void addAsync(int a, int b, Callback cb) {
new Thread(() -> {
int ret = a + b;//假設(shè)這里是一個(gè)耗時(shí)操作
cb.reply(ret );
}).start();
}
//調(diào)用(JS):
addAsync: async function (data,func) {
let params = getRequestAction(ACTION_MESSAGE_CODE_ADDAsync);
params.data = data;
FeatureAbility.subscribeAbilityEvent(params, (callbackData) => {
var callbackJson = JSON.parse(callbackData);
//console.log('getAirlineOpenCity is: ' + JSON.stringify(callbackJson.data));
func(callbackJson.data)
})
},
實(shí)現(xiàn)步驟
在開發(fā)過(guò)程中應(yīng)用中常用Internal Ability,現(xiàn)以Internal Ability說(shuō)明。
Internal Ability需要實(shí)現(xiàn)的步驟:
(1)在java中創(chuàng)建java類繼承AceInternalAbility,設(shè)置setInternalAbilityHandler回調(diào),在回調(diào)方法onRemoteRequest中通過(guò)命令碼code匹配對(duì)應(yīng)的方法名,Parcel中解析方法參數(shù);
(2)在AbilityPackage或者Ability的生命周期對(duì)java類對(duì)象進(jìn)行注冊(cè)和反注冊(cè);
(3)在JS中創(chuàng)建對(duì)應(yīng)JS類,調(diào)用FeatureAbility.callAbility,F(xiàn)eatureAbility.subscribeAbilityEvent傳遞命令碼和方法參數(shù);
痛點(diǎn)
從實(shí)現(xiàn)步驟看出,建立Js FA到Java PA的通道,比較繁瑣,以下操作如果能夠由系統(tǒng)自動(dòng)完成,那么用戶只關(guān)注java端方法實(shí)現(xiàn)、js端調(diào)用,使用就簡(jiǎn)便很多。
(1)進(jìn)行注冊(cè),反注冊(cè);
(2)解析方法名,方法參數(shù);
(3)返回值需要通過(guò)parcel進(jìn)行回傳;
注意點(diǎn)
-
java給js端返回結(jié)果(回消息)時(shí),僅支持String。
public boolean onRemoteRequest(int code, MessageParcel data, MessageParcel reply, MessageOption option) { switch (code) { case PLUS: { // 返回結(jié)果當(dāng)前僅支持String,對(duì)于復(fù)雜結(jié)構(gòu)可以序列化為ZSON字符串上報(bào) reply.writeString(ZSONObject.toZSONString(result));
雖然reply擁有writeInt,writeBoolean等等方法,但目前支持writeString。
-
被調(diào)用的java方法運(yùn)行在非UI線程。
public boolean onRemoteRequest(int code, MessageParcel data, MessageParcel reply, MessageOption option) { LogUtil.info(TAG, "isMain:"+(EventRunner.current() == EventRunner.getMainEventRunner()));
通過(guò)在onRemoteRequest處打印當(dāng)前線程是否UI線程,我們可以得出js調(diào)用過(guò)來(lái)在java代碼處是非UI線程的,如果在此處進(jìn)行UI操作,就會(huì)有一定的風(fēng)險(xiǎn)。
如果存在UI操作,可以context.getUITaskDispatcher().syncDispatch(Runnable)或者new EventHandler(EventRunner.getMainEventRunner()).postTask方式處理。
//方式1:uiTaskDispatcher,需要context context.getUITaskDispatcher().syncDispatch(task); //方式2:EventHandler new EventHandler(EventRunner.getMainEventRunner()).postTask(task);
-
Parcel的大小是200kb,如果超過(guò)200kb(實(shí)際沒(méi)這么多),會(huì)拋異常,如下:
官網(wǎng)對(duì)于Parcel的介紹
The default capacity of a Parcel instance is 200KB. If you want more or less, use setCapacity(int) to change it.
Note: Only data of the following data types can be written into or read from a Parcel: byte, byteArray, short, shortArray, int, intArray, long, longArray, float, floatArray, double, doubleArray, boolean, booleanArray, char, charArray, String, StringArray, PlainBooleanArray, Serializable, Sequenceable, and SequenceableArray.
解決辦法:增大parcel的容量
if (message.length() > 100 * 1000) { int capacity = (int) (message.length() * 2.1f); boolean setFlag = data.setCapacity(capacity); LogUtil.info(TAG, "SYNC capacity:" + capacity + " " + setFlag); }
-
CallAbility的TF_ASYNC 與Subscribe的區(qū)別
雖然兩個(gè)看上去都像異步,但是從調(diào)用上看CallAbility的TF_ASYNC仍然是同步的,只是沒(méi)有使用onRemoteRequest的reply方式進(jìn)行回傳。private boolean replyResult(MessageParcel reply, MessageOption option, int code, String data) { if (option.getFlags() == MessageOption.TF_SYNC) { // SYNC if (data.length() > 100 * 1000) { int capacity = (int) (data.length() * 2.1f); boolean setFlag = reply.setCapacity(capacity); LogUtil.info(TAG, "SYNC capacity:" + capacity + " " + setFlag); } reply.writeString(data); } else { // ASYNC MessageParcel responseData = MessageParcel.obtain(); if (data.length() > 100 * 1000) { int capacity = (int) (data.length() * 2.1f); boolean setFlag = responseData.setCapacity(capacity); LogUtil.info(TAG, "ASYNC capacity:" + capacity + " " + setFlag); } responseData.writeString(data); IRemoteObject remoteReply = reply.readRemoteObject(); try { remoteReply.sendRequest(code, responseData, MessageParcel.obtain(), new MessageOption()); } catch (RemoteException exception) { LogUtil.error(TAG, "RemoteException", exception); return false; } finally { responseData.reclaim(); } } return true; } private void replySubscribeMsg(int code, String message) { MessageParcel data = MessageParcel.obtain(); MessageParcel reply = MessageParcel.obtain(); MessageOption option = new MessageOption(); if (message.length() > 100 * 1000) { int capacity = (int) (message.length() * 2.1f); boolean setFlag = data.setCapacity(capacity); LogUtil.info(TAG, "SYNC capacity:" + capacity + " " + setFlag); } data.writeString(message); // 如果僅支持單FA訂閱,可直接觸發(fā)回調(diào):remoteObjectHandler.sendRequest(100, data, reply, option); try { remoteObject.sendRequest(code, data, reply, option); } catch (RemoteException e) { e.printStackTrace(); } reply.reclaim(); data.reclaim(); }
新方式一: js2java-codegen
從上面實(shí)現(xiàn)java,js通信的步驟可以看出,我們?cè)趈ava端對(duì)js傳過(guò)來(lái)的方法、方法參數(shù)進(jìn)行解析,解析完值后如果有返回值還需要通過(guò)Parcel回傳。要是能夠系統(tǒng)自動(dòng)幫我們實(shí)現(xiàn)對(duì)方法名,方法參數(shù)的解析,那就省去一大部分工作。
正好,鴻蒙推出了js2java-codegen這個(gè)注解。
條件
Toolchains的2.2.0.3以上
實(shí)現(xiàn)步驟
-
在module下的gradle開啟js2java-codegen。
ohos { ... defaultConfig { .... javaCompileOptions { annotationProcessorOptions { arguments = ['jsOutputDir': rootDir.path+'./entry/src/main/js/default/generated'] // 方式1設(shè)置生成路徑 //arguments = ['jsOutputDir': project.file("src/main/js/default/generated")] //方式2設(shè)置路徑 } } } compileOptions { f2pautogenEnabled true // 此處為啟用js2java-codegen工具的開關(guān) }
注:jsOutputDir的路徑一定要配置好,否則下面的編譯階段會(huì)報(bào)以下錯(cuò)誤。
-
編寫提供給js調(diào)用的java類。
@InternalAbility(registerTo = "com.freesonwill.facallpa.MainAbility") public class JsBridgeX { private static final String TAG = "JsBridgeX"; @ContextInject AbilityContext abilityContext; public int add(int a, int b) { return a + b; }
解釋:
1)使用InternalAbility注解java類,注解處理器會(huì)根據(jù)此類生成對(duì)應(yīng)JsBridgeXStub.java和JsBridgeX.js,這兩個(gè)類幫我們建立了通信通道;
2)屬性registerTo設(shè)為想要注冊(cè)到的Ability類的全稱。因?yàn)殚_發(fā)中我們可能用到context相關(guān)的方法,比如啟動(dòng)一個(gè)Ability。這里的registerTo和下面的@ContextInject配合使用,使被修飾的abilityContext指向MainAbility;
-
編譯
點(diǎn)擊Build -> Build HAP(s)/APP(s) -> Build HAP(s),js2java-codegen工具會(huì)為我們生成通信通道,JsBridgeXStub.java和JsBridgeX.js。
-
JsBridgeXStub.java
public class JsBridgeXStub extends AceInternalAbility { public static final String BUNDLE_NAME = "com.freesonwill.facallpa"; public static final String ABILITY_NAME = "com.freesonwill.facallpa.JsBridgeXStub"; ... private AbilityContext abilityContext; public JsBridgeXStub() { super(BUNDLE_NAME, ABILITY_NAME); } public boolean onRemoteRequest(int code, MessageParcel data, MessageParcel reply, MessageOption option) { Map<String, Object> result = new HashMap<String, Object>(); switch(code) { case OPCODE_add: { java.lang.String zsonStr = data.readString(); ZSONObject zsonObject = ZSONObject.stringToZSON(zsonStr); int a = zsonObject.getObject("a", int.class); //解析add方法第1個(gè)參數(shù) int b = zsonObject.getObject("b", int.class); //解析add方法第2個(gè)參數(shù) result.put("code", SUCCESS); result.put("abilityResult", service.add(a, b));//OPCODE_add 對(duì)應(yīng)servcie.add方法 break;}
點(diǎn)開JsBridgeXStub.java,我們看到,在onRemoteRequest這里仍然是方法名,方法參數(shù)解析。
-
JsBridgeX.js
const BUNDLE_NAME = 'com.freesonwill.facallpa'; const ABILITY_NAME = 'com.freesonwill.facallpa.JsBridgeXStub'; ... const sendRequest = async (opcode, data) => { var action = {}; action.bundleName = BUNDLE_NAME; action.abilityName = ABILITY_NAME; action.messageCode = opcode; action.data = data; action.abilityType = ABILITY_TYPE_INTERNAL; action.syncOption = ACTION_SYNC; return FeatureAbility.callAbility(action); //仍然是熟悉的FeatureAbility.callAbility方法 } class JsBridgeX { async add(a, b) { if (arguments.length != 2) { throw new Error("Method expected 2 arguments, got " + arguments.length); } let data = {}; data.a = a; data.b = b; const result = await sendRequest(OPCODE_add, data); return JSON.parse(result); }
我們看到,這里使用了FeatureAbility.callAbility,action的data屬性中包含了要調(diào)用的方法名(add)和方法參數(shù)(a,b)。
-
-
使用
import JsBridgeX from '../../generated/JsBridgeX.js'; const bridge = new JsBridgeX() ... //使用方式1:promise+then bridge.add(a, b).then(ret => { prompt.showToast({ message: `${a}+$ = ${ret.abilityResult}` }) }) //使用方式2:await var ret = await bridge.add(a, b); prompt.showToast({ message: `${a}+$ = ${ret.abilityResult}` })
注意點(diǎn)
-
返回值從abilityResult屬性獲取,如上ret.abilityResult。
prompt.showToast({ message: `${a}+$ = ${ret.abilityResult}` })
-
只支持同步方法FeatureAbility.callAbility,不支持FeatureAbility.subscribeAbilityEvent、FeatureAbility.unsubscribeAbilityEvent。從目前官網(wǎng)資料看,生成的方法都是FeatureAbility.callAbility方式。
-
void方法,private,protected,default訪問(wèn)權(quán)限的方法不會(huì)生成。
public void add(int a, int b) {//add 方法不會(huì)生成 return a + b; } int add(int a, int b) {//private,protected,default的方法不會(huì)生成 return a + b; }
-
生成對(duì)應(yīng)的js方法都是async的。
async add(a, b) { if (arguments.length != 2) { throw new Error("Method expected 2 arguments, got " + arguments.length); } let data = {}; data.a = a; data.b = b; const result = await sendRequest(OPCODE_add, data); return JSON.parse(result); }
-
非public方法通過(guò)編譯不會(huì)暴露給js,即生成的js代碼沒(méi)有對(duì)應(yīng)方法,如果想要public方法也不想暴露給js用,可以使用@ExportIgnore。
@ExportIgnore public boolean helloChar(){ return true; }
-
只支持文件中public的頂層類,不支持接口類和注解類。
-
跟傳統(tǒng)的調(diào)用方式不同,js端必須new 一個(gè)實(shí)例,通過(guò)實(shí)例調(diào)用方法。
const bridge = new JsBridgeX() bridge.add(a, b)
新方式二:LocalParticleAbility
不同于上面的js2java-codegen這種方式,LocalParticleAbility在系統(tǒng)內(nèi)部已經(jīng)建立好通道,不需要編譯生成額外的代碼,在java端實(shí)現(xiàn),在js端調(diào)用就行,比js2java-codegen更加簡(jiǎn)單。
條件
從API Version 6 開始支持。
實(shí)現(xiàn)步驟
-
java端實(shí)現(xiàn)接口LocalParticleAbility,添加方法。
package com.freesonwill.facallpa.biz; public class MyLocalParticleAbility implements LocalParticleAbility { //interface public int add(int a, int b) { return a + b; } public void addAsync(int a, int b, Callback cb) { new Thread(() -> { int ret = a + b;//假設(shè)這里是一個(gè)耗時(shí)操作 cb.reply(ret ); }).start(); }
注:這里列舉了同步和異步的兩種方式,異步需要LocalParticleAbility.Callback的reply方法返回。
-
注冊(cè)。
public class MainAbility extends AceAbility { @Override public void onStart(Intent intent) { super.onStart(intent); MyLocalParticleAbility.getInstance().register(this); } @Override public void onStop() { super.onStop(); MyLocalParticleAbility.getInstance().deregister(this); } }
-
js端調(diào)用。
a) 創(chuàng)建LocalParticleAbility對(duì)象
this.javaInterface = createLocalParticleAbility('com.freesonwill.facallpa.biz.MyLocalParticleAbility');
b)調(diào)用方法
- 調(diào)用同步方法
add(a, b) { this.javaInterface.add(a, b).then(ret => { prompt.showToast({ message: `${a}+$ = ${ret}` }) }) }, 或者 async add(a, b) { let ret = await this.javaInterface.add(a, b); console.log("rst:" + JSON.stringify(ret)) },
- 調(diào)用異步方法
addAsync(a, b) { this.javaInterface.addAsync(1, 2, rst => { console.log("rst:" + JSON.stringify(rst)) }) },
從上面可以看出LocalParticleAbility既支持同步又支持異步,彌補(bǔ)了js2java-codegen的不足。
注意點(diǎn)
- 目前從官網(wǎng)中js端的createLocalParticleAbility找不到,目前調(diào)試不行。
- API Version 6 開始支持。
- 需要注冊(cè)和反注冊(cè)。
總結(jié)
- 傳統(tǒng)JS FA的調(diào)用方式需要關(guān)注方法名,方法參數(shù)的傳遞解析,比較復(fù)雜,后續(xù)大概率會(huì)被LocalParticleAbility這種簡(jiǎn)潔方式替換。
- js2java-codegen提供了APT技術(shù)幫我們生成通道代碼,但是受限于不能實(shí)現(xiàn)異步,函數(shù)必須有返回值等,實(shí)用性不強(qiáng)。
- LocalParticleAbility優(yōu)勢(shì)很大,是未來(lái)的方向,目前雖然提供了文檔,但是在DevEco Studio 3.0端 SDK 6仍然不能成功。
項(xiàng)目地址
https://gitee.com/freebeing/facallpa.git
參考
https://developer.harmonyos.com/cn/docs/documentation/doc-references/parcel-0000001054519018
https://developer.harmonyos.com/cn/docs/documentation/doc-guides/ui-js2java-codegen-0000001171039385
https://developer.huawei.com/consumer/cn/codelabsPortal/carddetails/js2java-codegen
更多原創(chuàng)內(nèi)容請(qǐng)關(guān)注:開鴻 HarmonyOS 學(xué)院
入門到精通、技巧到案例,系統(tǒng)化分享HarmonyOS開發(fā)技術(shù),共建鴻蒙生態(tài),歡迎投稿和訂閱,讓我們一起攜手前行共建鴻蒙生態(tài)。
【本文正在參與51CTO HarmonyOS技術(shù)社區(qū)創(chuàng)作者激勵(lì)-星光計(jì)劃1.0】
想了解更多關(guān)于鴻蒙的內(nèi)容,請(qǐng)?jiān)L問(wèn):
51CTO和華為官方合作共建的鴻蒙技術(shù)社區(qū)
https://harmonyos.51cto.com/#bkwz
::: hljs-center
:::
本文摘自 :https://blog.51cto.com/h