通过这篇文章你可以了解到:
- 设计模式:工厂和命令
- 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));
}
}
}
至此,旧方案架构的简化过程到这里就完成了,它看起来更像是一个工厂设计模式。后续对于桥的迭代开发只需要两步:
-
自定义业务桥继承基类桥
-
Manager内将桥添加到集合
这样封装之后看似更加符合面向对象思想了,实际上还有很多的缺点,大家可以先思考一下?
有哪些缺点
-
每新增一个桥都需要去更新BridgeApiManager内的代码
-
维护了两个桥的集合,一个在Manager、一个在JsBridge。遍历Manager内的集合,再注册到JsBridge的集合,当有桥调用时,再遍历JsBridge的集合。效率不高
-
当桥越来越多时(后期我们的桥已经两百多个)。两个集合一个四百多个对象,对内存开销并不小。
-
JsBridge一次调用过程存在三次手握(不了解的请查看上一篇文章)
综上,我决定对JS-SDK进行一次手术,为了让他适应时代的发展。
基于命令模式的重构
前面介绍了目前方案存在的问题,重构要达成的目标也就显而易见了:
-
一次请求,即可把想要执行的动作发送给对方
-
只维护一个桥容器,避免造成性能浪费(空间和速度)
-
不需要频繁的改动代码去注册业务桥
针对问题1,可以使用addJavascriptInterface的方式替代URL拦截的方式。底层架构设计,采用命令模式来实现,这样2和3的问题就都解决了。当然在重构的过程中也必须遵循SOLID设计原则。
什么是命令模式?
将一个请求封装为一个对象,从而使我们可用不同的请求对客户进行参数化;对请求排队或者记录请求日志,以及支持可撤销的操作。命令模式是一种对象行为型模式,其别名为动作(Action)模式或事务(Transaction)模式。
翻译成人话:发送者可以将调用的命令,参数封装成对象;接受者解析对象,取出命令和参数去执行对应的方法。
实现过程拆解
先上一张流程图:
新增一个实体类,用来解析命令对象:
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();
}
搞定!