软硬件交互之——收银机系统

112 阅读8分钟

版权声明】本文原发布于上海竖排网络科技有限公司官方知乎账号,由本人撰写发布,现摘抄复习,有什么问题可以直接来我掘金提问。 【摘要】一句话概括本文核心内容,前端跨平台控制收银机系统钱箱的打开和关闭。

作者:清泓

【技术剖析】

混合开发的app,当H5端写好相关app页面之后。利用AndroidWebView制作一个app壳。把网页嵌套进app壳内。利用JavaScript接口,实现H5与Android系统之间的信息传递。(此交互技术详见以下链接)

竖排CTO田甜:混合开发之JavaScriptInterfaceAndroid端交互4 赞同 · 2 评论文章

在通过javaScript接口接收到 H5发送过来的信息之后,Android端选择接收,并写好接口,对具体的业务进行区分,分析处理。这里是一个收银机的打开钱箱指令,Android端接收到之后,通过java代码,对硬件系统(我这边的硬件系统是Android端的),发送信息,系统在接收到app发送的信息之后,硬件单片机程序,对我们已经约定好的“暗号”进行分析,并作出对应的处理。二进制信号(软件)在载体(硬件)中传输,经由固体电子元件(硬件)得以执行,然后又可以以二进制信号(软件)的形式存储在载体(硬件)中。

使用Java调用DLL动态链接库的方案通常有三种:JNI, Jawin, Jacob. 其中JNI(Java NativeInterface)是Java语言本身提供的调用本地已编译的函数库的方法,本身具有跨平台性,可以在不同的机器上调用不同的本地库。

JNI的使用步骤是:

编写Java类,用该类将DLL对外提供的函数服务进行声明,其中的Java方法均声明为native,其方法签名可以自定义,不用实现函数体。

【基础范例】

以下文章是利用原生交互实现了软硬之间的交互。实际流程是:H5网页(约定好的“暗号”)——>AndroidApp——>Android硬件。利用的是最原始的Android与Android硬件的交互。

而现在需要实现的是,H5网页(约定好的暗号)——>AndroidApp(约定好的“暗号”)——>Android硬件——>网络串口——>钱箱(单片机程序(约定好的“暗号”))【这一步其实已经经过封装好,只需要对好“暗号”】

竖排CTO田甜:Android双屏异显(Presentation)与后台动态配置副屏内容2 赞同 · 3 评论文章

【关于此Activity与Presentation之间的传值问题剖析】

在收银机与H5通信解决相关业务问题中,有一个核心问题是Activity与presentation的传值问题,这个问题值得重新剖析一下,因为在我们开发的App中,有两个需要实现的点。第一点,是必须在H5端实现传递信息给移动端的软件Activity中,然后在Activity中,传值到presentation中,这还不够,当presentation接收到activity的数据的时候,通过数据(一个网址),实时打开这个网址,更新页面UI。流程大概如下:

H5网页(约定好的“暗号”)——>Android页面——>判断是否存在已打开的dialog——>判断是否存在数据变动——>?关闭进程——>?传值给dialog——>dialog数据是否存在变动——>?刷新页面——>?加载数据,更新UI

尝试解决方案一:在app端接收到h5信息之后,实时传递给dialog。dialog通过service实时通知,然后,dialog接收到消息的时候,实时变动UI.后来被否定了。基本不可实现。

尝试解决方案二:在app端接收到h5信息之后,实时传递给dialog。dialog通过Receiver实时通知,然后,dialog接收到消息的时候,实时变动UI.后来被否定了。基本不可实现。

尝试解决方案三:通过Rxbus.

Subject是非线程安全的,在并发情况下,不推荐使用通常的Subject对象,而是推荐使用SerializedSubject。

我们都知道,Rxjava基于观察者模式,上游发送数据,下游通过回调接收数据。我们试想一下,假如我们在A页面发送数据,并把产生的Observable存放在全局变量中,在B页面拿到该Observable并订阅,是不是就可以获取到A页面发送的数据了?

public class RxTest {      public volatile Observable<String> instance;  }

其中volatile关键字修饰的变量可以让线程看到的始终是最新值,线程1中对变量的最新修改,对线程2是可见的。

然后我们在Activity A页面,点击按钮发送数据,并保存在全局变量中,并打开Activity B页面。

Observable<String> observable = Observable.just("哈哈哈");
RxTest.instance = observable;
startActivity(new Intent(getContext(), TestActivity.class));

其中Activity B中的初始化中拿到该Observable并注册。

这里存在一个时间的问题。在第一个例子中,我们先是生成了Observable,把它保存在全局中,然后才打开了Activity B页面,此时B页面从全局中拿到的是刚刚生成的Observable,注册拿到数据,没问题。但是第二个例子中,两个fragment几乎是同时加载的,fragment B中初始化的时候,全局中并没有保存任何Observable

if (RxTest.instance != null) {
        RxTest.instance.subscribe(new Observer<String>() {
            @Override
            public void onSubscribe(Disposable d) {
                Log.i(TAG, "onSubscribe");
            }

            @Override
            public void onNext(String s) {
                Log.i(TAG, "onNext:" + s);
            }

            @Override
            public void onError(Throwable e) {
                Log.i(TAG, "onError");
            }

            @Override
            public void onComplete() {
                Log.i(TAG, "onComplete");
            }
        });
    }





public class RxBus {
    private static volatile RxBus instance;
    private Subject<Object, Object> bus;

    private RxBus() {
        bus = new SerializedSubject<>(PublishSubject.create());
    }

    public static RxBus getDefault() {
        if (instance == null) {
            synchronized (RxBus.class) {
                instance = new RxBus();
            }
        }
        return instance;
    }

    /**
     * 发送事件
     * @param object
     */
    public void post(Object object) {
        bus.onNext(object);
    }

    /**
     * 根据类型接收相应类型事件
     * @param eventType
     * @param <T>
     * @return
     */
    public <T> Observable toObservable(Class<T> eventType) {
        return bus.ofType(eventType);
    }
}

我们换个思路想,假如我们在A页面发送数据的,和B页面注册接收数据的,是同一个对象,问题是不是就解决了?你别说,还真有这个东西,那就是Subject。我们看下它的继承关系:

public abstract class Subject<T> extends Observable<T> implements Observer<T>

emmmm…有点雌雄同体的赶脚……

我们看到,Subject是个抽象类,它有四个实现。分别是:

PublishSubject:从哪里订阅就从哪里开始发送数据。

AsyncSubject:无论输入多少参数,永远只输出最后一个参数。

BehaviorSubject:发送离订阅最近的上一个值,没有上一个值的时候会发送默认值。

ReplaySubject:无论何时订阅,都会将所有历史订阅内容全部发出。

按照我们数据总线的需求,我们应该选择第一个PublishSubject。我们来把上面的例子改造一番:

public class RxTest {
    private static volatile RxTest mInstance;
    private volatile Subject<String> mSubject;
    private RxTest() {
        mSubject = PublishSubject.create();
    }
    public static RxTest getInstance() {
        if (mInstance == null) {
            mInstance = new RxTest();
        }
        return mInstance;
    }
    /**
     * 发送消息
     ** @param s
     */
    public void post(String s) {
        mSubject.onNext(s);
    }
public Observable<String> getObservable() {
        return mSubject;
    }
}

我们把Observable换成Subject,并且做成单例模式,保证使用同一个,定义了发送数据和获取该Subject的方法。(因为Subject实现了Observer接口,所以有OnNext()方法。因为Subject继承了Observable,获取的时候可以转成Observable)。

现在,我们在fragment A页面发送数据:

RxTest.getInstance().post("www.baidu.com");

分别在fragment B和Activity B页面中注册(注意:此时Activity B尚未创建)。

   RxTest.getInstance().getObservable()
            .subscribe(new Observer<String>() {
                @Override
                public void onSubscribe(Disposable d) {
                    Log.i(TAG,"onSubscribe");
                }
              @Override
                public void onNext(String s) {
                    Log.i(TAG,"onNext:"+s);
                }
                @Override
                public void onError(Throwable e) {
                    Log.i(TAG,"onError");
                }
                @Override
                public void onComplete() {
                    Log.i(TAG,"onComplete");
                }
            });

只是执行了订阅,并没有在onNext()中获取到发送值,为什么呢?还记得我们使用的PublishSubject的特点吗?特点是从哪里订阅就从哪里开始发送数据。我们在打开Activity B之前,发送了一条数据,然后打开后才开始订阅,这时候是不会拿到上条数据的,会获取到后续发送的数据。而如果我们是使用了ReplaySubject,就可以拿到之前所有的数据。

现在我们把这个工具优化一下,我们总不能每次发消息都只发string字符串吧…

public class RxTest {
    private static volatile RxTest mInstance;
    private final Subject<Object> mSubject;

    private RxTest() {
        mSubject = PublishSubject.create().toSerialized();
    }

    public static RxTest getInstance() {
        if (mInstance == null) {
            synchronized (RxTest.class){
                if (mInstance == null){
                    mInstance = new RxTest();
                }
            }
        }
        return mInstance;
    }

    /**
     * 发送消息
     *
     * @param event
     */
    public void post(Object event) {
        mSubject.onNext(event);
    }


    public <T> Observable<T> getObservable(final Class<T> eventType) {
        //转换为泛型为T的Observable
        return mSubject.ofType(eventType);
    }

    /**
     * 是否有观察者
     *
     * @return
     */
    public boolean hasObservers() {
        return mSubject.hasObservers();
    }
}

打开新页面还会获取到上一条发送的粘性信息。这个是可以实现的,我们可以在RxTest中维护一个hashMap,在发送粘性消息的时候,把消息对象保存在hashMap中,键为消息对象类型,值为消息对象。然后在注册粘性信息时,从hashMap中获取,处理完数据后将之移除。

到这里功能已经完成了,但是有一个隐患,假如由于某种错误导致Subject发送了onError或者onComplete,那么在所有注册了RxBus处,在接收到onError或onComplete后,不会再接收Subject后续发送的任何信息。如此该怎么办?

我们解决当前这个问题就需要用到他的RxRelay库,它和Subject的区别就是不必担心事件在onComplete或者onError后终止事件订阅关系。

implementation 'com.jakewharton.rxrelay2:rxrelay:2.0.0'

使用RxRelay优化我们的RxBus:

public class RxTest {
    private static volatile RxTest mInstance;
    private final Relay<Object> mBus;

    private RxTest() {
        this.mBus = PublishRelay.create().toSerialized();
    }

    public static RxTest getInstance() {
        if (mInstance == null) {
            synchronized (RxTest.class){
                if (mInstance == null){
                    mInstance = new RxTest();
                }
            }
        }
        return mInstance;
    }

    /**
     * 发送消息
     *
     * @param event
     */
    public void post(Object event) {
        mBus.accept(event);
    }


    public <T> Observable<T> getObservable(final Class<T> eventType) {
        //转换为泛型为T的Observable
        return mBus.ofType(eventType);
    }

    /**
     * 是否有观察者
     *
     * @return
     */
    public boolean hasObservers() {
        return mBus.hasObservers();
    }
}

在HomeActivity中保存和取消订阅事件。

public class HomeActivity extends AppCompatActivity {
    protected ArrayList<Subscription> rxBusList = new ArrayList<>();

    @Override
    protected void onDestroy() {
        super.onDestroy();

        clearSubscription();
    }

    /**
     * 取消该页面所有订阅
     */
    private void clearSubscription() {
        for (Subscription subscription : rxBusList) {
            if (subscription != null && subscription.isUnsubscribed()) {
                subscription.unsubscribe();
            }
        }
    }
}

发送的对象实体类:

public class EventBean {
    private int userId;
    private String nickName;

    public EventBean(int userId, String nickName) {
        this.userId = userId;
        this.nickName = nickName;
    }

    public int getUserId() {
        return userId;
    }

    public void setUserId(int userId) {
        this.userId = userId;
    }

    public String getNickName() {
        return nickName;
    }

    public void setNickName(String nickName) {
        this.nickName = nickName;
    }
}

发送事件方式:

RxBus.getDefault().post(new EventBean(1, "www.baidu.com"));

接收事件方式:

        Subscription subscription = RxBus.getDefault().toObservable(EventBean.class)
            .subscribe(new Action1<EventBean>() {
            @Override
            public void call(EventBean eventBean) {
                tvContent.setText(eventBean.getUserId() + "------" + eventBean.getNickName());
            }
        });
        rxBusList.add(subscription);

更多更新期待后期。。。。


[参考文献]

RxBus的使用及解析_WernerZeiss的博客-CSDN博客​blog.csdn.net/WernerZeiss/article/details/81210297


欢迎关注 技术团队的知乎账号 我们凭团队实例运作以下专栏, 必须干货!

互联网创业专栏 (我们小伙伴的创业历程)

与您一起聊技术 (APP、微信公众号、小程序、H5 技术总结)

互联网产品研发管理 (我们公司对产品结构的管理思路)

产品君的案例库(产品小伙伴深刻总结)