#星光计划1.0# HarmonyOS JS FA调用PA新方式 原创 精华

发布于 2021-10-26 09:59
浏览
11收藏

作者:包月东

简介

​ JS FA调用Java PA一般通过FeatureAbility这种传统方式调用。本文先回顾传统方式,在传统方式的痛点上引入两种新方式。

传统方式

方法说明

常见的JS FA调用Java Pa一般通过以下三个方法

(1) FeatureAbility.callAbility(OBJECT):调用PA能力。
(2) FeatureAbility.subscribeAbilityEvent(OBJECT, Function):订阅PA能力。
(3) FeatureAbility.unsubscribeAbilityEvent(OBJECT):取消订阅PA能力。

1.能常用于同步调用方法

比如:

//实现(Java)
public int add(int a, int b) {
    return a + b;
}

//调用:
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.常用于调用异步方法,因为(2)是通过监听的方式实现异步回调,所以在不需要监听时最好调用(3)取消

比如:

//实现(Java)
public void addAsync(int a, int b, Callback cb) {
    new Thread(() -> {
        int ret = a + b;//假设这里是一个耗时操作
        cb.reply(ret );
    }).start();
}

//调用(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)
    })
},

实现步骤

在开发过程中应用中常用Internal Ability,现以Internal Ability说明。

Internal Ability需要实现的步骤:

(1)在java中创建java类继承AceInternalAbility,设置setInternalAbilityHandler回调,在回调方法onRemoteRequest中通过命令码code匹配对应的方法名,Parcel中解析方法参数;

(2)在AbilityPackage或者Ability的生命周期对java类对象进行注册和反注册;

(3)在JS中创建对应JS类,调用FeatureAbility.callAbility,FeatureAbility.subscribeAbilityEvent传递命令码和方法参数;

痛点

从实现步骤看出,建立Js FA到Java PA的通道,比较繁琐,以下操作如果能够由系统自动完成,那么用户只关注java端方法实现、js端调用,使用就简便很多。

(1)进行注册,反注册;

(2)解析方法名,方法参数;

(3)返回值需要通过parcel进行回传;

注意点

  1. java给js端返回结果(回消息)时,仅支持String。

    public boolean onRemoteRequest(int code, MessageParcel data, MessageParcel reply, MessageOption option) {
      switch (code) {
        case PLUS: {
          // 返回结果当前仅支持String,对于复杂结构可以序列化为ZSON字符串上报
          reply.writeString(ZSONObject.toZSONString(result));
    

    虽然reply拥有writeInt,writeBoolean等等方法,但目前支持writeString

  2. 被调用的java方法运行在非UI线程

    public boolean onRemoteRequest(int code, MessageParcel data, MessageParcel reply, MessageOption option) {
        LogUtil.info(TAG, "isMain:"+(EventRunner.current() == EventRunner.getMainEventRunner()));
    

    通过在onRemoteRequest处打印当前线程是否UI线程,我们可以得出js调用过来在java代码处是非UI线程的,如果在此处进行UI操作,就会有一定的风险。

    如果存在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);
    
  3. Parcel的大小是200kb,如果超过200kb(实际没这么多),会抛异常,如下:

    #星光计划1.0# HarmonyOS JS FA调用PA新方式-开源基础软件社区

    官网对于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);
    }
    
  4. CallAbility的TF_ASYNC 与Subscribe的区别
    虽然两个看上去都像异步,但是从调用上看CallAbility的TF_ASYNC仍然是同步的,只是没有使用onRemoteRequest的reply方式进行回传。

    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订阅,可直接触发回调:remoteObjectHandler.sendRequest(100, data, reply, option);
        try {
            remoteObject.sendRequest(code, data, reply, option);
        } catch (RemoteException e) {
            e.printStackTrace();
        }
        reply.reclaim();
        data.reclaim();
    }
    

新方式一: js2java-codegen

从上面实现java,js通信的步骤可以看出,我们在java端对js传过来的方法、方法参数进行解析,解析完值后如果有返回值还需要通过Parcel回传。要是能够系统自动帮我们实现对方法名,方法参数的解析,那就省去一大部分工作。

正好,鸿蒙推出了js2java-codegen这个注解。

条件

Toolchains的2.2.0.3以上

#星光计划1.0# HarmonyOS JS FA调用PA新方式-开源基础软件社区

实现步骤

  1. 在module下的gradle开启js2java-codegen。

    ohos {
    	...
        defaultConfig {
    		....
            javaCompileOptions {
                annotationProcessorOptions {
                    arguments = ['jsOutputDir': rootDir.path+'./entry/src/main/js/default/generated'] // 方式1设置生成路径
                    //arguments = ['jsOutputDir': project.file("src/main/js/default/generated")] //方式2设置路径
                }
            }
        }
        compileOptions {
            f2pautogenEnabled  true // 此处为启用js2java-codegen工具的开关
        }
    

    注:jsOutputDir的路径一定要配置好,否则下面的编译阶段会报以下错误。

    #星光计划1.0# HarmonyOS JS FA调用PA新方式-开源基础软件社区

  2. 编写提供给js调用的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类,注解处理器会根据此类生成对应JsBridgeXStub.java和JsBridgeX.js,这两个类帮我们建立了通信通道;

    2)属性registerTo设为想要注册到的Ability类的全称。因为开发中我们可能用到context相关的方法,比如启动一个Ability。这里的registerTo和下面的@ContextInject配合使用,使被修饰的abilityContext指向MainAbility;

  3. 编译

    点击Build -> Build HAP(s)/APP(s) -> Build HAP(s),js2java-codegen工具会为我们生成通信通道,JsBridgeXStub.java和JsBridgeX.js。

    #星光计划1.0# HarmonyOS JS FA调用PA新方式-开源基础软件社区

    • 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个参数
            int b = zsonObject.getObject("b", int.class); //解析add方法第2个参数
            result.put("code", SUCCESS);
            result.put("abilityResult", service.add(a, b));//OPCODE_add 对应servcie.add方法
            break;}
      

      点开JsBridgeXStub.java,我们看到,在onRemoteRequest这里仍然是方法名,方法参数解析。

    • 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属性中包含了要调用的方法名(add)和方法参数(a,b)。

  4. 使用

    import JsBridgeX from '../../generated/JsBridgeX.js';
    const bridge = new JsBridgeX()
    
    ...
    //使用方式1:promise+then
    bridge.add(a, b).then(ret => {
        prompt.showToast({
            message: `${a}+${b} = ${ret.abilityResult}`
        })
    })
    //使用方式2:await
    var ret = await bridge.add(a, b);
    prompt.showToast({
        message: `${a}+${b} = ${ret.abilityResult}`
    })
    

注意点

  1. 返回值从abilityResult属性获取,如上ret.abilityResult。

    prompt.showToast({
        message: `${a}+${b} = ${ret.abilityResult}`
    })
    
  2. 只支持同步方法FeatureAbility.callAbility,不支持FeatureAbility.subscribeAbilityEvent、FeatureAbility.unsubscribeAbilityEvent。从目前官网资料看,生成的方法都是FeatureAbility.callAbility方式。

  3. void方法,private,protected,default访问权限的方法不会生成。

    public void add(int a, int b) {//add 方法不会生成
     	return a + b;
    }
    
    int add(int a, int b) {//private,protected,default的方法不会生成
        return a + b;
    }
    
  4. 生成对应的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);
    }
    
  5. 非public方法通过编译不会暴露给js,即生成的js代码没有对应方法,如果想要public方法也不想暴露给js用,可以使用**@ExportIgnore**。

    @ExportIgnore
    public boolean helloChar(){
        return true;
    }
    
  6. 只支持文件中public的顶层类,不支持接口类和注解类。

  7. 跟传统的调用方式不同,js端必须new 一个实例,通过实例调用方法。

    const bridge = new JsBridgeX()
    bridge.add(a, b)
    

新方式二:LocalParticleAbility

不同于上面的js2java-codegen这种方式,LocalParticleAbility在系统内部已经建立好通道,不需要编译生成额外的代码,在java端实现,在js端调用就行,比js2java-codegen更加简单。

条件

从API Version 6 开始支持。

实现步骤

  1. java端实现接口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;//假设这里是一个耗时操作
                cb.reply(ret );
            }).start();
        }
    

    注:这里列举了同步和异步的两种方式,异步需要LocalParticleAbility.Callback的reply方法返回。

  2. 注册。

    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);
        }
    }
     
    
  3. js端调用。

    a) 创建LocalParticleAbility对象

    this.javaInterface = createLocalParticleAbility('com.freesonwill.facallpa.biz.MyLocalParticleAbility');
    

    b)调用方法

    • 调用同步方法
    add(a, b) {
        this.javaInterface.add(a, b).then(ret => {
            prompt.showToast({
                message: `${a}+${b} = ${ret}`
            })
        })
    },
    或者  
    async add(a, b) {
        let ret = await this.javaInterface.add(a, b);
        console.log("rst:" + JSON.stringify(ret))
    },
    
    • 调用异步方法
    addAsync(a, b) { 
        this.javaInterface.addAsync(1, 2, rst => {
            console.log("rst:" + JSON.stringify(rst))
        })
    },
    

    从上面可以看出LocalParticleAbility既支持同步又支持异步,弥补了js2java-codegen的不足。

注意点

  1. 目前从官网中js端的createLocalParticleAbility找不到,目前调试不行。
  2. API Version 6 开始支持。
  3. 需要注册和反注册。

总结

  1. 传统JS FA的调用方式需要关注方法名,方法参数的传递解析,比较复杂,后续大概率会被LocalParticleAbility这种简洁方式替换。
  2. js2java-codegen提供了APT技术帮我们生成通道代码,但是受限于不能实现异步,函数必须有返回值等,实用性不强。
  3. LocalParticleAbility优势很大,是未来的方向,目前虽然提供了文档,但是在DevEco Studio 3.0端 SDK 6仍然不能成功。

项目地址

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

https://developer.harmonyos.com/cn/docs/documentation/doc-references/js-apis-localparticleability-overview-0000001064156060

更多原创内容请关注:开鸿 HarmonyOS 学院

入门到精通、技巧到案例,系统化分享HarmonyOS开发技术,共建鸿蒙生态,欢迎投稿和订阅,让我们一起携手前行共建鸿蒙生态。

【本文正在参与51CTO HarmonyOS技术社区创作者激励-星光计划1.0】

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
9
收藏 11
回复
举报
回复
添加资源
添加资源将有机会获得更多曝光,你也可以直接关联已上传资源 去关联
    相关推荐