咱们新年好啊,这是 2023 年榜首篇文章,对笔者来说也是一个新的技能测验,看似标题党,但确实是在实话实说 ~
起因
尽管我司从长远打算上是准备运用 Rust 来做底层及逻辑层跨端架构,但这落地时刻会拉的很长,且没有一个适宜的切入点。
现状是年底的时分咱们都了(笔者很嘴硬的没阳,但带薪病假浪费了 (o_ _)ノ),需求上也基本是阻滞状态,那就有空来折腾下 Rust,让自己锈一锈。
学习上从零开始拜读 《Rust 圣经》,但在根底入门篇看了一大半之后,真实是看不下去了…学而不思则惘,尽管搞了解了最要害的一切权和借用,但不到项目里用一下就等于啥也不会[手动狗头]。
刚好有一个由来已久的事务痛点,我司的埋点是否契合预期,这个验证在移动端上是较为杂乱的,原因有以下几点:
- 埋点传输进程中是加密的,很难经过抓包的形式看到埋点是否正确。
- 埋点在移动端是有缓存的,并不会实时上报。
- 埋点在数据入库的时分也是有延迟的,在平台上查询埋点也需求必定时刻本钱。更重要的是,埋点需求验证各种时机是否正确,那需求实时性更高的办法。
而在跨端开发越来越频繁的今日,对立也就越来越凸显,对于 H5 开发的同学或许 Flutter 开发的同学来讲,埋点出现查不到或许埋入过错的问题后,需求消耗许多的时刻和精力来排查原因,所以迫切需求一个能实时查看埋点是否正确的办法。
考虑
最早的想法是让埋点实时显现在 App 上,但这存在以下的问题:
- App 屏幕真实不大,没有更多的空间能完整显现当时的埋点信息,假如做成需求翻开页面来查看的办法,实时性也不高。
- 需求额定开发页面,尽管能够用 Flutter 来减少双端开发本钱,但也是需求必定的开发本钱。
那有没有一种办法能够解决呢?
思路换一下,无论是 LookinServer 、 Flipper 等 Debug 利器,仍是 Flutter / Web Debug Tools,都是在电脑上调试 App。那咱们也能够用类似的办法,把实时埋点数据显现在电脑上,不再局限于同一块屏幕。
通讯办法
咱们需求的是一个尽量简单的通讯,所以方针上选定运用 WebSocket,且上述解决方案也大都是这样的挑选。
技能选型
在大方向上,首先排除掉 iOS 、Android 原生开发的办法来完成。
而 Flutter 并不适合做底层及逻辑层,它更适合做页面。
Rust 更契合咱们的需求,能够覆盖了一切终端,功用又高,并且不会加入到线上出产包中,很适合做 Rust 在移动端的榜首步落地。
唯一的问题便是还不会,所以需求花时刻探索下。
当时效果
翻开 Rust 服务端履行程序,等候衔接:
在 App 上输入要衔接的 IP 及 端口号:
服务端履行程序会实时显现收到的埋点信息:
完成进程
笔者在之前现已搭建好了 Rust 的开发环境,也成功的跑通了 “Hello World”,所以装备部分并不在本文的介绍中。
由于是完全陌生的技能领域,所以对笔者来说有2个需求解决的难点:
- Rust WebSocket 完成。
- Rust 与 iOS / Android 通讯。
WebSocket
笔者落地方案的时分并没有了解 Rust WebSocket 原理,本着快速落地的办法,先去找了 《Rust 圣经》里边的日常开发三方库精选,里边有这三个推荐库:
库好是好,也都是出产级的库,但笔者 Rust 功力几近于无,要不是 Demo 跑不动,要不是了解不了,改不动 (o_ _)ノ。
后边查到一个完成尤为简单的库,刚好能拿来就用。
[dependencies]
ws = "0.9.2"
传送门(github.com/housleyjk/w…
服务端
服务端的效果便是接收 App 来的音讯并显现,用配套的 ws-rs 库 Rust 服务端代码,很容易完成:
if let Err(error) = listen(address, |out| {
...
}) {
// 告诉用户故障
println!("创建Websocket失败,原因: {:?}", error);
}
address 是 ip + 端口号,趁便打印出来,让 App 输入连入。
let address = format!("{}:{}", get_ip().unwrap(), get_available_port());
println!("当时地址为:{}", address);
get_ip()
获取本机 ip 地址。
get_available_port()
获取本机可用的端口号。
办法网上搜的,或许不是是最佳的办法:
最终把收到的信息格式化输出即可,用 serde_json
库做 json 解析:
...
// 处理程序需求获取 out 的一切权,因此咱们运用 move
move |msg: ws::Message| {
let text = msg.as_text();
match text {
Ok(_t) => {
// 格式化
let json: Value = serde_json::from_str(_t).unwrap();
println!(
"触发埋点\n event_id: {}\n event_name: {}\n attributes: {}\n\n",
json["event_id"],
json["event"],
serde_json::to_string(&json["attributes"]).unwrap()
);
// 运用输出通道发送音讯
out.send(msg)
}
Err(_) => todo!(),
}
}
...
然后履行 cargo run --release
打包成履行程序。
客户端
客户端代码也很简单,衔接服务端的 ip + 端口,并发送音讯即可。
...
if let Err(error) = connect(c_host, |out| {
// 将WebSocket翻开时要发送的音讯排队
if out.send(c_message).is_err() {
println!("[gaoding-log-view-kit]: 无法初始音讯排队")
}
// 封闭衔接
out.close(CloseCode::Normal)
})
...
这儿感叹下,Rust 中真的是轮子众多,集成度又高,真的是不明白所以然也能够拆箱即用。
Native 通讯
上面的 websocket 进程很顺利的完成了,但怎么在 App 中发送 ws 音讯?网上包含里有许多的资料,但基本都是同一份来历,看翻译的不如看原文更具体,能少踩坑。
iOS 传送门
Android 传送门
这2篇文章以及 git 源码 会教你 App 怎么让 Rust 打印个 “Hello World”,但里边也有一些遇到的弯弯绕绕需求注意。
Rust + iOS
先来看 iOS 是怎么做的。
前面的装备进程文章讲的很具体,这儿略过。
比较要害的几个环节:
库形式装备
[lib]
crate-type = ["staticlib", "cdylib"]
入口更改为 src/lib.rs
,没有就创建一个。(这儿能够不必 lib.rs 做文件名吗?笔者还没具体了解,有了解的同学能够谈论告知下)
露出办法
use std::{ffi::CStr, os::raw::c_char};
...
#[no_mangle]
pub extern "C" fn send_wind_info(message: *const c_char, host: *const c_char) {
...
}
露出 send_wind_info(...)
发送埋点信息办法,且供给2个参数,一个是当时音讯、一个是发送的域名。
理论上,host 参数在 Rust 的内存持有即可,但这还写不出来 … 当然咱们把这个持有放到 Native 来做即能够绕过去。
打包及制造头文件
终端履行 cargo lipo --release
命令就可拿到 .a 文件,Rust 仍是很方便的。
然后咱们需求供给文件来给 iOS 调用:
api.h
#include <stdint.h>
/// 发送埋点信息
void send_wind_info(const char *message, const char *host);
Cocoapods
按文章上说的硬链到项目里,太令人难受了,咱们当然挑选用 pod 本地库引用的形式。
podspec 要害部分:
...
s.source_files = 'GDLogViewKit/Classes/**/*.{m,h}'
s.vendored_libraries = 'GDLogViewKit/Library/**/*.a'
s.public_header_files = 'GDLogViewKit/Classes/*.h'
...
再封装
GDLogViewKit.h / GDLogViewKit.m 供给的便是 api.h 的封装,毕竟外部直接调用 api.h 的 send_wind_info(...)
办法真实不够高雅。
#import "api.h"
static NSString *connectAddress;
...
+ (void)sendWindMessage:(NSString *)message {
if (!connectAddress || !message) {
return;
}
send_wind_info([message cStringUsingEncoding:NSUTF8StringEncoding], [connectAddress cStringUsingEncoding:NSUTF8StringEncoding]);
}
connectAddress
便是用于保存 ip + 端口的静态变量。
iOS 上仍是很简单的,也或许由于笔者毕竟是一个 iOSer [手动狗头]。整体上便是构造一个 ObjectiveC – C – Rust 通讯进程。
Rust + Android
Android 就崎岖了许多,笔者对 Android 并不算熟,遇到了挺多问题,这儿逐个记录下。
NDK 装备
首先要找到自己 Android SDK 的安装目录,然后找到里边的 NDK 文件夹
然后履行文章中的语句来生成到一个目录下(这个目录依据文章所说生成到项目目录下)。
可是,
依据文章来履行这三句总是失败 – -!
我也测验用 make-standalone-toolchain.sh
shell 脚本也不行
报 ERROR: Failed to create toolchain
这个过错网上搜了下也没有人解释原因,最终想到,我在 python 开发上用的是 python3, 会不会是这个原因?
python3 'xxx/make_standalone_toolchain.py' --api 26 --arch arm64 --install-dir NDK/arm64
成功了 …
再往下照文章履行即可。
JNI
Android 与 iOS 不同的是,多了一层 JNI,相当于 JAVA – JNI – C – Rust,这一部分曾经并没有接触过,一开始靠硬写,后边发现能够经过 javac
来生成。
先在 Cargo.toml 上添加 jni 东西装备,能简洁咱们编写 jni
[target.'cfg(target_os="android")'.dependencies]
jni = { version = "0.20.0", default-features = false }
在 src/lib.rs 添加 Android 胶水代码:
打包 so 库
学文章的办法,添加一个 cargo-config.toml
再履行拷贝命令 cp cargo-config.toml ~/.cargo/config
(但这个很古怪,是每个项目都要履行吗?有懂的同学谈论告知下)。
然后调用生成 so 库命令
cargo build --target aarch64-linux-android --release
cargo build --target armv7-linux-androideabi --release
cargo build --target i686-linux-android --release
Gradle
同样,咱们也制造一个独立的 gradle 库
将 so 库放入到 jniLibs 目录下,这儿也有一个坑
文章上是放到这个位置,但咱们项目里需求放入到如下位置才干被读取到
据 Android 开发同学说是不同的项目上依赖包装备不同导致的。
再封装
跟 iOS 一样,封装的东西办法:
GDLogViewKit.java
package com.gaoding.log.view.kit;
/**
* 日志可视化东西包
*/
public class GDLogViewKit {
static {
// 库引用
System.loadLibrary("gaoding_log_view_kit");
}
private static String kAddress = null;
// native 办法,对应 JNI
private static native void sendWindInfo(final String message, final String host);
...
public static void sendWindMessage(String message) {
if (kAddress == null) {
return;
}
GDLogViewKit.sendWindInfo(message, kAddress);
}
}
总结
一个简单的埋点实时可视化的轮子就算是做好了,信任它能在项目中起到应有的效果,真的要再感叹 Rust 生态环境的强大,也坚定了后续学习 Rust 的决计。
后续扩展上,能够抛弃 Rust 的服务端,WS 服务上到内部平台上,来把轮子做大。也能够扩展到其他日志信息,乃至能够供给控制指令来操作 App。由于是 Rust,所以能很方便的集成到 Web、桌面等其他终端应用上。
缺乏的点,仍是由于 Rust 没入门,继续啃圣经,先测验把 ip 地址缓存的功用放到 Rust 中,待修炼飞升后,把上述进程加入到跨端东西链全家桶中 …
别的,以上代码已开源 git 传送门,有爱好的能够了解。
感谢阅读,假如对你有用请点个赞 ❤️