Android与Web通信架构设计:基于APT与命令模式的JS-SDK设计

167 阅读8分钟

通过这篇文章你可以了解到:

  • 设计模式:工厂和命令
  • SOLID设计原则和架构设计的思路
  • APT的实战运用

写在前面

本文讲述了摒弃JsBridge框架所做的一次重构,如果想要理解重构背景的话,建议先对JsBridge的原理有一个简单了解,这里贴出一篇关于介绍其原理的文章链接 Android与Web通信框架--JsBridge原理

如果只对标题所示的架构设计感兴趣的话,可直接从第三节--基于命令模式的重构开始

在正文开始之前,先来给大家介绍一下重构的背景.

什么是JS-SDK

Native端提供给Web端调用的一系列API,以方便Web端使用Native的能力。如:H5需要获取一张相册图片,我可以直接调用封装好的Native方法。JS-SDK常用于移动办公类软件的工作台内各大H5应用和小程序类的软件。

为什么要重构

设计一个JS-SDK必然要涉及到双端通信,我们现在的通信方案是基于JsBridge实现的。无论是JsBridge本身还是我们的二次开发都存在若干缺点,并且随着业务发展壮大,这些缺点也都暴露的更加明显。

基于JsBridge进行JS-SDK架构的缺点

关于JsBridge实现的原理请点击Android与Web通信框架--JsBridge原理,它暴露了registerHandler方法,方便调用者使用。

webView.registerHandler("login", new BridgeHandler() {
        @Override
        public void handler(String data, CallBackFunction function) {
            Log.i(TAG, "handler = login, data from web = " + data);
            function.onCallBack("login exe, response data from Java");
        }
   });

每增加一个业务桥,都需要调用一次registerHandler,同时我们的Activity或者Fragment代码冗余量也会越来越大。所以在当时为了方便后续其它同学的迭代开发,降低沟通和维护成本,我在JS-SDK的初版架构中基于JsBridge做了一个二次封装。

旧方案架构

为了好向大家引出旧方案的缺点,这里向大家简化一下实现过程。

新增加了一个接口,抽象出桥名称和业务桥的逻辑执行体:

public interface IBridgeApi {
    String getFunctionName();
    BridgeHandler getBridgeHandler();
}

用一个抽象类实现 IBridgeApi,重写getBridgeHandler方法,以免反复创建BridgeHandler:

public abstract class BaseBridgeApi implements IBridgeApi {
    @Override
    public BridgeHandler getBridgeHandler() {
        return new BridgeHandler() {
            @Override
            public void handler(String data, CallBackFunction function) {
                callBack = function;
                try {
                    ......
                    String params = getString(data, BridgeApiManager.PARAM_PAY_LOAD);
                    processData(params)
                    ......
                } catch (LXApiException e) {
                    reportError(e);
                }
            }
    }

    public abstract void processData(String params) throws BridgeApiException;
}

用业务桥继承抽象类即可实现桥的开发:

public class PreviewImageBridge extends BaseBridgeApi {
    @Override
    public String getFunctionName() {
        return "previewImage";
    }
    @Override
    public void processData(String params) throws ApiException {
        //方法逻辑
        ......
        //成功调用reportSuccess
        reportSuccess(object);
        //失败调用
        reportError();
    }
}

到这里业务桥的代码还得不到执行,再提供一个桥管理类:

public class BridgeApiManager {
    ......
    //Initialization Demand Holder单例
    private static class HolderInstance {
        private static BridgeApiManager manager = new BridgeApiManager();
    }

    private BridgeApiManager() {

    }

    public BridgeApiManager getInstace () {
        return HolderInstance.manager;
    }

    ...
    public synchronized List<IBridgeApi> availableOperations() {
        List<IBridgeApi> apis = new ArrayList<>();
        ......
        apis.add(new PreviewImageBridge());
        ......
     }
    ...
public class CommonWebViewFragment {
    private void registerBridge() {
        for (IBridgeApi msg : BridgeApiManager.getInstance().availableOperations()) {
            //调用JsBridge提供的register方法注册
            web.registerHandler(msg.getFunctionName(), msg.getBridgeHandler(this));
        }    
    }
}

至此,旧方案架构的简化过程到这里就完成了,它看起来更像是一个工厂设计模式。后续对于桥的迭代开发只需要两步:

  1. 自定义业务桥继承基类桥

  2. Manager内将桥添加到集合

这样封装之后看似更加符合面向对象思想了,实际上还有很多的缺点,大家可以先思考一下?

有哪些缺点

  1. 每新增一个桥都需要去更新BridgeApiManager内的代码

  2. 维护了两个桥的集合,一个在Manager、一个在JsBridge。遍历Manager内的集合,再注册到JsBridge的集合,当有桥调用时,再遍历JsBridge的集合。效率不高

  3. 当桥越来越多时(后期我们的桥已经两百多个)。两个集合一个四百多个对象,对内存开销并不小。

  4. JsBridge一次调用过程存在三次手握(不了解的请查看上一篇文章)

综上,我决定对JS-SDK进行一次手术,为了让他适应时代的发展。

基于命令模式的重构

前面介绍了目前方案存在的问题,重构要达成的目标也就显而易见了:

  1. 一次请求,即可把想要执行的动作发送给对方

  2. 只维护一个桥容器,避免造成性能浪费(空间和速度)

  3. 不需要频繁的改动代码去注册业务桥

针对问题1,可以使用addJavascriptInterface的方式替代URL拦截的方式。底层架构设计,采用命令模式来实现,这样2和3的问题就都解决了。当然在重构的过程中也必须遵循SOLID设计原则。

什么是命令模式?

将一个请求封装为一个对象,从而使我们可用不同的请求对客户进行参数化;对请求排队或者记录请求日志,以及支持可撤销的操作。命令模式是一种对象行为型模式,其别名为动作(Action)模式或事务(Transaction)模式。

翻译成人话:发送者可以将调用的命令,参数封装成对象;接受者解析对象,取出命令和参数去执行对应的方法

实现过程拆解

先上一张流程图:

流程图.png

新增一个实体类,用来解析命令对象:

public class JsBridgeMessage {
    public String command;
    public JsonObject paramsObj;
}

新增描述业务桥功能的命令接口和一个回调接口:

public interface IBridgeCommand {
    /**
     *@param paramsObj 参数的Json
     *@param callback 回调
     */
    public void exec(JsonObject paramsObj, IBridgeCallbackInterface callback);
}
public interface IBridgeCallbackInterface {
    /**
     * callback 回调key
     * params   参数 json 格式
     */
    void handleBridgeCallback(String callback, String params);

    // 从参数中获取回调 key 的方法
    default String getCallbackKey(JsonObject params) {
        if (params == null) {
            return null;
        }
        if (!params.has("bridgeCallback")) {
            return null;
        }
        return params.get("bridgeCallback").getAsString();
    }
}

假如我们需要调用一个弹Toast的Native方法,新建一个ToastCommand继承IBridgeCommand:

public class ToastCommand implementation IBridgeCommand {
    @Override
    public void exec(JsonObject paramsObj, IBridgeCallbackInterface callback) {
        if (paramsObj == null && !paramsObj.has("message")) {
            retrun;
        }
        String message = paramsObj.get("message");
        ToastUtils.showShort(message);

        String key = getCallbackKey(params);
        if (TextUtils.isEmpty(key) || callback == null) {
            return;
        }
        Map<String, String> data = Map.of("message", "showToast is success!!")
        callback.handleBridgeCallback(key, Gson.toJson(data)    
    }
}

BaseWebView中添加接收命令和处理回调的方法:

public class BaseWebView extends WebView {

    public BaseWebView(@NonNull Context context) {
        super(context);

        addJavascriptInterface(this, "receiveCommand");
    }    

    @JavascriptInterface
    public void receiveCommand(String json) {
        ......

        //将json字符串转换成实体对象
        JsBridgeMessage jsBridgeInfo = Gson.fromJson(json, JsBridge);
        //转交给命令分发器
        JsBridgeInvokeDispatcher.getInstance().dispatchCommand(webView, jsBridgeInfo);

        ......
    }
    //执行js方法,传递回调参数
    public void postBridgeCallback(String key, String data) {
        post(() -> {
            evaluateJavascript("javascript:window.jsBridge.postBridgeCallback(`" + key + "`, `" + data + "`)", 
                    null);
        });
    }
}

命令分发器类简化代码:

public class JsBridgeInvokeDispatcher {
    ......

    public void dispatchCommand(View webView, JsBridgeMessage message) {
        //校验参数合法性
        if (checkMessage(message)) {
            //执行命令
            excuteCommand(view, message)
        }
          .....
    }

    private void checkMessage(JsBridgeMessage message) {

    }

    private void excuteCommand(View webView, JsBridgeMessage message) {
        IBridgeCallbackInterface callback = new IBridgeCallbackInterface() {

            @Override
            public void handleBridgeCallback(String callback, String params) {
                view.postBridgeCallback(callback, params);
            }
        };

        //分发给命令的具体执行者
        BridgeCommandHandler.getInstance().handleBridgeInvoke(message.command, message.params, callback)
    }

}

根据SOLID设计原则中的单一指责原则,执行命令再抽出一个类,复制命令的执行和注册:

public class BridgeCommandHandler {

    private static BridgeCommandHandler instance;

    // 用于切线程
    private final Handler mHandle = new Handler(Looper.getMainLooper());
    private final ArrayMap<String, IBridgeCommand> mCommandMap;

    private BridgeCommandHandler() {
        mCommandMap = new ArrayMap<>();
        mCommandMap.put("showToast", new ToastCommand());
    }

    // 单例
    public static BridgeCommandHandler getInstance() {
        ......
    }

    // 暴露给外部方法 分发调用
    public void handleBridgeInvoke(String command, JsonObject params) {
        // map 中存在命令 则执行
        if (mCommandMap.containsKey(command)) {
            mHandle.post(() -> { // 切换到主线程 获取命令 执行
                mCommandMap.get(command).exec(
                    params, callback
                );
            });
        }
    }
}

大功告成

现在回过头再看看文章的三个要求:

  • 一次请求,即可把想要执行的动作发送给对方

    • 完成,native将方法挂到window,JS可通过获取window对象调用命令接收器
  • 只维护一个桥容器,避免造成性能浪费(空间和速度)

    • 完成,只有命令执行器中一个容器
  • 不需要频繁的改动代码去注册业务桥

    • emmm,好像未完成,需要频繁的改动执行的代码注册

好吧,我们接着把剩下的一个问题ko掉。

APT实现自动注册

关于APT是什么,我在这里不作详细介绍,只简单引用一下官方的解释。感兴趣的可以去网上查阅资料。

APT(Annotation Processing Tool)是一种处理注释的工具,它对源代码文件进行检测找出其中的Annotation,使用Annotation进行额外的处理。 Annotation处理器在处理Annotation时可以根据源文件中的Annotation生成额外的源文件和其它的文件(文件具体内容由Annotation处理器的编写者决定),APT还会编译生成的源文件和原来的源文件,将它们一起生成class文件。

也就是说,可以利用APT扫描自定义注解,自动生成注册功能桥的代码,避免我们去手动注册。

实现思路简介

在apt-annotations模块下自定义一个注解:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE) 
public @interface JsBridgeCommand {
    String name(); //桥的名字
}

apt-processor模块中定义一个自定义注解处理器,扫描带有@JsBridgeCommand注解的类,组装成一个带有注册功能的类:

@Override
public boolean process(Set<? extends TypeElement> p0, RoundEnvironment roundEnvironment) {
    Map<String, String> commandMap = new HashMap<>();
    //将带有@JsBridgeCommand注解的类名和桥名加入commandMap
    for (Element element : roundEnvironment.getElementsAnnotatedWith(JsBridgeCommand.class)) {
        if (element instanceof TypeElement) {
            TypeElement item = (TypeElement) element;
            String clz = item.getQualifiedName().toString();
            System.out.println(TAG + " getClz = " + clz);

            JsBridgeCommand annotation = item.getAnnotation(JsBridgeCommand.class);
            String name = annotation.name();
            System.out.println(TAG + " getName = " + name);

            commandMap.put(name, clz);
        }
    }

    String packageName = "com.2014.demo.apt";
    //生成方法名
    MethodSpec.Builder registerMethodBuilder = MethodSpec.methodBuilder("autoRegist")
        .addComment("web jsbridge command auto load")
        //生成返回值类型
        .returns(ClassName.get("android.util", "ArrayMap")
                //定义Map泛型,
                .parameterizedBy(String.class, ClassName.get("com.1024.demo.web.bridge", "IBridgeCommand")))
         //生产创建map代码
        .addStatement("ArrayMap commandMap = new ArrayMap<String, IBridgeCommand>()");
    //遍历前面的存放桥名和类名的commandMap,生产桥名和根据类名生产的实例对象的代码
    for (Map.Entry<String, String> entry : commandMap.entrySet()) {
        registerMethodBuilder.addStatement("try {", "Class<?> clazz = Class.forName($S);",
                "Object obj = clazz.getDeclaredConstructor().newInstance();",
                "commandMap.put($S, obj);",
                "} catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {",
                "    e.printStackTrace();",
                "}", entry.getKey(), entry.getValue());
    }

    registerMethodBuilder.addStatement("return commandMap");

    TypeSpec.Builder companionObject = TypeSpec.anonymousClassBuilder("")
        .addModifiers(Modifier.STATIC)
        .addMethod(registerMethodBuilder.build());

    TypeSpec.Builder clazzBuilder = TypeSpec.classBuilder("JsBridgeUtil")
        .addType(companionObject.build());

    JavaFile javaFile = JavaFile.builder(packageName, clazzBuilder.build())
        .build();

    try {
        // 假设mFilerUtils有一个方法可以直接返回Filer
        mFilerUtils.getFiler().write(javaFile); // 注意:这里假设mFilerUtils有一个getFiler方法
    } catch (IOException e) {
        e.printStackTrace();
    }

    return false;
}

接下来我们就能直接在我们的功能桥上加上注解,实现自动注册了:

@JsBridgeCommand(name = "showToast")
class ToastCommand implements IBridgeCommand {
    // ...
}

编译一下项目,会自动生成一个类:

public class JsBridgeUtil {
    public static ArrayMap autoRegister() {
        ArrayMap<String, IBridgeCommand> commandMap = new ArrayMap<>();
        try {
            Class<?> clazz = Class.forName("com.1024.demo.web.bridge.ToastCommand");
            Object obj = clazz.getDeclaredConstructor().newInstance();
            arrayMap.put(“showToast”, obj);
        } catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
            e.printStackTrace();
        }ArrayMap commanMap = new ArrayMap<String, IBridgeCommand>
    }
}

调整一下BridgeCommandHandler类的代码

private BridgeCommandHandler() {
        //mCommandMap = new ArrayMap<>();
        //mCommandMap.put("showToast", new ToastCommand());

        JsBridgeUtil.autoRegister();
    }

搞定!