利用Rust与Flutter开发一款小工具

news/2024/7/20 21:08:36 标签: flutter, rust, Android, ios

在这里插入图片描述

1.起因

起因是年前看到了一篇Rust + iOS & Android|未入门也能用来造轮子?的文章,作者使用Rust做了个实时查看埋点的工具。其中作者的一段话给了我启发:

无论是 LookinServer 、 Flipper 等 Debug 利器,还是 Flutter / Web Debug Tools,都是在电脑上调试 App。那我们也可以用类似的方式,把实时埋点数据显示在电脑上,不再局限于同一块屏幕。

我司目前的埋点走查是在测试盒子中有一个埋点查看页面,Debug包在数据上报的同时会将信息临时保存起来。当进入这个页面时会以列表的形式展示出来。并且iOS 和Android的页面展示和使用方式也略有不同。

后面我觉得这样进入退出页面查看不方便,就将页面改成了悬浮窗。虽然方便了一些,但是也发现了新的问题:

  • 手机上屏幕大小有限,悬浮窗只有屏幕的一半,可展示信息有限。
  • 悬浮窗会遮挡页面,有时不便于点击页面上的按钮。

刚好前阵子升级了手机系统到Android 13,发现log在控制台都打印不出来了(后面发现App适配到13就正常了。。)。所以有了一个想法,使用Rust通过WebSocket进行数据发送,使用Flutter实现服务端接收App发送的信息并显示出来。

当然了,如果我们的应用是flutter写的,可以直接使用Dart的ffi来直接调用Rust函数。这个我后面有时间会单独写一篇来分享。

2.实现

之所以选择RustFlutter是看中它们的跨平台能力。使用Rust进行WebSocket数据发送,就不用Android和iOS端去重复开发这个功能,只需要简单调用即可,并且Rust有许多开箱即用的库。

Flutter的跨平台能力就更不用说了。比如这个小工具我就可以一套代码输出Windows和macOS两个平台的安装包,保证接收端逻辑和UI的一致。

发送端

Rust部分

关于Rust库的打包以及双端的使用可以看我上一篇分享的Rust库交叉编译以及在Android与iOS使用。这里主要说一下具体的实现代码。

首先是添加WebSocket 库 ws-rs依赖到Cargo.toml文件:

[dependencies]
ws = "0.9.2"
# 全局的静态变量
lazy_static = "1.4.0"

实现代码如下:

rust">use std::collections::HashMap;
use std::sync::Mutex;
use std::{ffi::CStr, os::raw::c_char};
use ws::{connect, Handler, Sender, Handshake, Result, Message, CloseCode, Error};
use ws::util::Token;
#[macro_use]
extern crate lazy_static;

lazy_static! {
    static ref DATA_MAP: Mutex<HashMap<String, Sender>> = {
        let map: HashMap<String, Sender> = HashMap::new();
        Mutex::new(map)
    };
}

struct Client {
    sender: Sender,
    host: String,
}

impl Handler for Client {
    fn on_open(&mut self, _: Handshake) -> Result<()> {
        DATA_MAP.lock().unwrap().insert(self.host.to_owned(), self.sender.to_owned());
        Ok(())
    }

    fn on_message(&mut self, msg: Message) -> Result<()> {
        println!("<receive> '{}'. ", msg);
        Ok(())
    }

    fn on_close(&mut self, _code: CloseCode, _reasonn: &str) {
        DATA_MAP.lock().unwrap().remove(&self.host);
    }

    fn on_timeout(&mut self, _event: Token) -> Result<()> {
        DATA_MAP.lock().unwrap().remove(&self.host);
        self.sender.shutdown().unwrap();
        Ok(())
    }

    fn on_error(&mut self, _err: Error) {
        DATA_MAP.lock().unwrap().remove(&self.host);
    }

    fn on_shutdown(&mut self) {
        DATA_MAP.lock().unwrap().remove(&self.host);
    }

}

#[no_mangle]
pub extern "C" fn websocket_connect(host: *const c_char) {
    let c_host = unsafe { CStr::from_ptr(host) }.to_str().unwrap();
    if let Err(err) = connect(c_host, |out| {
        Client {
            sender: out,
            host: c_host.to_string(),
        }
    }) {
        println!("Failed to create WebSocket due to: {:?}", err);
    }
}

#[no_mangle]
pub extern "C" fn send_message(host: *const c_char, message: *const c_char) {
    let c_message = unsafe { CStr::from_ptr(message) }.to_str().unwrap();
    let c_host = unsafe { CStr::from_ptr(host) }.to_str().unwrap();
    let binding = DATA_MAP.lock().unwrap();
    let sender = binding.get(&c_host.to_string());
    
    match sender {
        Some(s) => {
            if s.send(c_message).is_err() {
                println!("Websocket couldn't queue an initial message.")
            };
        } ,
        None => println!("None")
    }
}

#[no_mangle]
pub extern "C" fn websocket_disconnect(host: *const c_char) {
    let c_host = unsafe { CStr::from_ptr(host) }.to_str().unwrap();
    DATA_MAP.lock().unwrap().remove(&c_host.to_string());
}

简单实现了连接,发送,断开连接三个方法。思路是连接成功后会将发送结构体(Sender)保存在Map中,每次发送时先检查是否连接再发送。这样也就实现了连接多台设备,一对多发送的功能。

Android还需要添加对应的JNI方法:

rust">#[cfg(target_os = "android")]
#[allow(non_snake_case)]
pub mod android {
    extern crate jni;

    use self::jni::objects::{JClass, JString};
    use self::jni::JNIEnv;
    use super::*;

    #[no_mangle]
    pub unsafe extern "C" fn Java_com_weilu_utils_EventLogUtils_sendMessage(
        env: JNIEnv,
        _: JClass,
        host: JString,
        message: JString,
    ) {
        send_message(
            env.get_string(host)
                .expect("invalid pattern string")
                .as_ptr(),
            env.get_string(message)
                .expect("invalid pattern string")
                .as_ptr(),
        );
    }

    #[no_mangle]
    pub unsafe extern "C" fn Java_com_weilu_utils_EventLogUtils_connect(
        env: JNIEnv,
        _: JClass,
        host: JString,
    ) {
        websocket_connect(
            env.get_string(host)
                .expect("invalid pattern string")
                .as_ptr(),
        );
    }

    #[no_mangle]
    pub unsafe extern "C" fn Java_com_weilu_utils_EventLogUtils_disconnect(
        env: JNIEnv,
        _: JClass,
        host: JString,
    ) {
        websocket_disconnect( 
            env.get_string(host)
                .expect("invalid pattern string")
                .as_ptr(),
        );
    }
}

至此,发送端部分完成。打包集成进项目就可以使用了。

Android_190">Android部分

Android端调用代码如下:

public class EventLogUtils {

    static {
        System.loadLibrary("event_log_kit");
    }

    private static native void sendMessage(final String host, final String message);
    private static native void connect(final String host);
    private static native void disconnect(final String host);

    private static List<String> addressList = null;

    public static List<String> getAddressList() {
        return addressList;
    }

    /**
     * 保存 IP 地址,传空时断开所有连接
     */
    public static void saveAddress(String address) {
        if (TextUtils.isEmpty(address)) {
            if (addressList != null) {
                for (String url : addressList) {
                    disconnect(url);
                }
            }
            addressList = null;
            return;
        }
        // 多个地址逗号隔开
        if (address.contains(",")) {
            addressList = new ArrayList<>(Arrays.asList(address.split(",")));
        } else {
            addressList = new ArrayList<>();
            addressList.add(address);
        }

        for (String url : addressList) {
            // 子线程调用,可替换为其他方案,这里使用了线程池
            Executor.getExecutor().getExecutorService().submit(new Runnable() {
                @Override
                public void run() {
                    // 循环,如果意外断开,自动重连
                    while (addressList != null) {
                        connect("ws://" + url);
                    }
                    // 工具连接彻底断开
                }
            });
        }
    }

    /**
     * 发送信息
     */
    public static void sendMessage(String message) {
        if (addressList == null) {
            return;
        }
        for (String url : addressList) {
            sendMessage("ws://" + url, message);
        }
    }
}

代码也比较简单,连接方法在子线程调用,如果发现连接断开会自动重连。

iOS部分就不具体说明了,实现思路一样的。

接收端

首先是发送数据的定义,发送的是json格式字符串。定义的主要参数如下:

class EventLogEntity {
  /// event/log
  String type = '';
  /// 事件名称或log tag
  String? name;
  /// 手机型号
  String? deviceModel;
  /// 时间戳
  int time = 0;
  String data = '';

  ...
}
  • type:用于区分数据类型,目前分为埋点事件与log。
  • name:事件名称或log tag,用于数据的筛选。
  • deviceModel:设备名用于区分数据来源,如果有多个设备同时发送数据可以便于分类。
  • time:时间戳,用于数据排序。

其他参数可以根据自己的需求添加,比如log的等级,数据展示时展开或者收起。

UI组件我使用了fluent_ui,它提供了原生Windows应用风格的组件,比较适合桌面端程序。状态管理使用flutter_riverpod。

具体的代码实现就不多说了,主要说一下核心的数据接收部分。

// https://doc.xuwenliang.com/docs/dart-flutter/2499
class WebSocketManager{

  HttpServer? requestServer;

  Future startWebSocketListen() async {
    final String ip = '192.168.31.232';
    final String port = '51203';
    stopWebSocketListen();
    //HttpServer.bind(主机地址,端口号)
    requestServer = await HttpServer.bind(ip, int.parse(port)).catchError((error) {
      debugPrint('bind error: $error');
    });
    await for(HttpRequest request in requestServer!) {
      serveRequest(request).catchError((error){
        debugPrint('listen error: $error');
      });
    }
  }

  void stopWebSocketListen() {
    requestServer?.close();
    requestServer = null;
  }

  Future serveRequest(HttpRequest request) {
    //判断当前请求是否可以升级为WebSocket
    if (WebSocketTransformer.isUpgradeRequest(request)) {
      //升级为webSocket
      return WebSocketTransformer.upgrade(request).then((webSocket) {
        //webSocket消息监听
        webSocket.listen((msg) async {
          debugPrint('listen:$msg');
		  if (webSocket.closeCode == null) {
            // 这里可以回复客户端消息
            webSocket.add('收到');
          }
          // 可以在这里解析数据,刷新页面
		  ...
        });
      });
    } else {
      return Future((){});
    }
  }
}

然后为了便于使用,避免使用者自己查询填写ip,我们需要获取当前设备ip地址:

  Future<String> getDeviceIp() async {
    String ip = "";
    if (!kIsWeb) {
      for (var interface in await NetworkInterface.list()) {
        for (var address in interface.addresses) {
          ip = address.address;
        }
      }
    }
    return ip;
  }

端口可以给个默认值或者自己随便输入一个,然后可以用shared_preferences插件保存用户配置。下次启动时就自动连接了。
请添加图片描述
手机端可以实现一个输入连接地址的页面,输入电脑端的ip和端口号后就可以发送数据了。或者扫描二维码连接。

3.成果展示

目前实现功能如下:

  • 可同时接收多台设备发送数据,数据按机型名称分类展示。
  • 数据的筛选,搜索(关键字高亮)。
  • 搜索记录的保存。
  • json数据格式化展示。

请添加图片描述


因为小工具在公司内部使用,所以就不开源完整的代码了。有了文章中的核心代码,你可以根据自己的需求实现。也不必局限于这些功能,你完全可以通过Rust和Flutter的跨平台能力开发更多功能,本篇也只是抛砖引玉。

如果本篇对你有所启发帮助,不妨点赞支持一下。如果你有好的想法,也欢迎评论交流。

4.参考

  • Rust + iOS & Android|未入门也能用来造轮子?

http://www.niftyadmin.cn/n/81421.html

相关文章

大数据处理学习笔记1.5 掌握Scala内建控制结构

文章目录零、本讲学习目标一、条件表达式&#xff08;一&#xff09;语法格式&#xff08;二&#xff09;执行情况&#xff08;三&#xff09;案例演示任务1、根据输入值的不同进行判断任务2、编写Scala程序&#xff0c;判断奇偶性二、块表达式&#xff08;一&#xff09;语法格…

LVGL WIN32模拟器环境搭建

LVGL WIN32模拟器环境搭建LVGL简介环境搭建IDE 选择模拟器代码下载PC模拟器搭建LVGL简介 LVGL是一个跨平台、轻量级、易于移植的图形库。因其支持大量特性和其易于裁剪&#xff0c;配置开关众多&#xff0c;且版本升级较快&#xff0c;不同版本之间存在一定的差异性&#xff0…

【第31天】SQL进阶-写优化- 插入优化(SQL 小虚竹)

回城传送–》《31天SQL筑基》 文章目录零、前言一、练习题目二、SQL思路&#xff1a;SQL进阶-写优化-插入优化解法插入优化禁用索引语法如下适用数据库引擎非空表&#xff1a;禁用索引禁用唯一性检查语法如下适用数据库引擎禁用外键检查语法如下适用数据库引擎批量插入数据语法…

Redis服务器配置

服务器基础配置服务器端设定 设置服务器以守护进程的方式运行daemonize yes|no 绑定主机地址bind 127.0.0.1 设置服务器端口号port 6379 设置数据库数量databases 16日志配置 设置服务器以指定日志记录级别loglevel debug|verbose|notice|warning开发期 debug 线上no…

【数据结构】二叉树-堆实现及其堆的应用(堆排序topK问题)

文章目录一、堆的概念及结构二、堆的实现1.结构的定义2.堆的初始化3.堆的插入4.堆的向上调整5.堆的删除6.堆的向下调整7.取出堆顶元素8.返回堆的元素个数9.判断堆是否为空10.打印堆中的数据11.堆的销毁三、完整代码1.Heap.h2.Heap.c3.test.c四、堆排序1.堆排序2.建堆3.选数4.完…

【C++】关联式容器——map和set的使用

文章目录一、关联式容器二、键值对三、树形结构的关联式容器1.set2.multiset3.map4.multimap四、题目练习一、关联式容器 序列式容器&#x1f4d5;:已经接触过STL中的部分容器&#xff0c;比如&#xff1a;vector、list、deque、forward_list(C11)等&#xff0c;这些容器统称为…

100天精通Python(数据分析篇)——第76天:Pandas数据类型转换函数pd.to_numeric(参数说明+实战案例)

文章目录专栏导读一、to_numeric参数说明0. 介绍1. arg1&#xff09;接收列表2&#xff09;接收一维数组3&#xff09;接收Series对象2. errors1&#xff09;errorscoerce2&#xff09;errors ignore3. downcast1&#xff09;downcastinteger2&#xff09;downcastsigned3&…

Java多线程——Thread类的基本用法

一.线程的创建继承Thread类//继承Thread类class MyThread extends Thread{Overridepublic void run() {System.out.println("线程运行的代码");} } public class Demo1 {public static void main(String[] args) {MyThread t new MyThread();t.start();//启动线程&a…