Android性能优化系列-腾讯matrix-流量监控之TrafficPlugin源码分析

news2025/1/13 10:06:43

前言

本篇进行matrix框架的网络流量监控模块的代码分析。你可能想,为什么需要对流量进行监控呢?我们平常进行的网络接口请求都是一些必要的操作,监控它的意义何在?首先我们要明确流量监控的对象是什么,是上行(发请求消耗的流量)和下行(接收到服务器返回的数据流量)这两块消耗的用户流量。通过这个监控,我们可以清晰的看到每个接口在每次调用时所消耗的流量的具体值,有了这个数据之后,我们可以从两个维度来分析流量问题。第一,明确单次接口请求是否存在过多消耗流量的情况,从而促进网络数据包的体积优化;第二,从多次请求的维度来看,也能帮助我们定位是否存在单个接口请求数量异常的问题,从而定位代码存在的业务逻辑问题。笔者在万能钥匙的时候,就曾遇到过类似的情况,某接口在应用启动阶段频繁调用,导致用户流量消耗过多被用户投诉的问题。试想假如当时做了流量监控,那么对开发团队来说,就可以更高效的定位到问题所在。

言归正传,我们进入今天的代码分析,分析的对象时matrix中的TrafficPlugin,我们从它的几个关键方法入手。

  • 静态代码块
  • start
  • stop

静态代码块

static {
    System.loadLibrary("matrix-traffic");
}

根据加载的名称matrix-traffic找到MatrixTraffic.cc这个c++的class,loadLibrary方法执行的时候会进入到JNI_OnLoad方法,JNI_OnLoad方法通常用来做一些准备性的工作,用于后边c++层和Java层的一个互调,下面是一部分关键代码,可以看到,这里将Java层的TrafficPlugin保存为全局引用,并获取了它的setStackTrace方法备用,并动态注册了一些Java层到native层方法的映射关系。

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) {
    jclass trafficCollectorCls = env->FindClass("com/tencent/matrix/traffic/TrafficPlugin");
    if (!trafficCollectorCls)
        return -1;
    //保存TrafficPlugin的jclass对象为全局引用
    gJ.TrafficPlugin = static_cast<jclass>(env->NewGlobalRef(trafficCollectorCls));
    //保存TrafficPlugin的setStackTrace方法为全局引用
    gJ.TrafficPlugin_setFdStackTrace =
            env->GetStaticMethodID(trafficCollectorCls, "setStackTrace", "(Ljava/lang/String;Ljava/lang/String;)V");
    //动态注册一些Java层方法到native方法的映射关系
    if (env->RegisterNatives(
            trafficCollectorCls, TRAFFIC_METHODS, static_cast<jint>(NELEM(TRAFFIC_METHODS))) != 0)
        return -1;
    return JNI_VERSION_1_6;
} 

start

通过nativeInitMatrixTraffic方法调用进入native层,根据上边JNI_OnLoad动态注册的映射关系找到MatrixTraffic.cc中的nativeInitMatrixTraffic方法。

@Override
public void start() {
    //这里可以设置需要过滤的so
    String[] ignoreSoFiles = trafficConfig.getIgnoreSoFiles();
    //进入native层
    nativeInitMatrixTraffic(trafficConfig.isRxCollectorEnable(), trafficConfig.isTxCollectorEnable(), trafficConfig.willDumpStackTrace(), trafficConfig.willDumpNativeBackTrace(), trafficConfig.willLookupIpAddress(), ignoreSoFiles);
}

MatrixTraffic.cc中的nativeInitMatrixTraffic方法。

static void nativeInitMatrixTraffic(JNIEnv *env, jclass, jboolean rxEnable, jboolean txEnable, jboolean dumpStackTrace, jboolean dumpNativeBackTrace, jboolean lookupIpAddress, jobjectArray ignoreSoFiles) {
    //启动loop循环线程
    TrafficCollector::startLoop(dumpStackTrace == JNI_TRUE, lookupIpAddress == JNI_TRUE);
    //是否dump native堆栈
    sDumpNativeBackTrace = (dumpNativeBackTrace == JNI_TRUE);
    //需要过滤的so
    ignoreSo(env, ignoreSoFiles);
    //通过hook socket实现对网络请求的拦截
    hookSocket(rxEnable == JNI_TRUE, txEnable == JNI_TRUE);
}

startLoop

首先startLoop启动循环线程

void TrafficCollector::startLoop(bool dumpStackTrace, bool lookupIpAddress) {
    thread loopThread(loop);
    loopThread.detach();
}

loop循环线程是作为一个消费者线程出现的,我们先跳过这个方法的具体实现,先把生产者生产数据的过程看一下。

void loop() {
    while (loopRunning) {
        if (msgQueue.empty()) {
            queueMutex.lock();
        } else {
            ...
        }
    }
}

hookSocket

网络请求最终都是通过底层socket进行发起的,所以通过hook socket的方式可以拦截到所有的网络请求,这里是用了plt hook的方式,什么是plt hook可以参考 爱奇艺的xhook框架介绍 。为了使代码更简洁,用…省略了部分代码。

static void hookSocket(bool rxHook, bool txHook) {
    //连接和关闭
    xhook_grouped_register(..., ".*\.so$", "connect",(void *) my_connect, (void **) (&original_connect));
    xhook_grouped_register(..., ".*\.so$", "close",(void *) my_close, (void **) (&original_close));
    //接收的数据监控
    if (rxHook) {
        xhook_grouped_register(..., ".*\.so$", "read",(void *) my_read, (void **) (&original_read));
        xhook_grouped_register(..., ".*\.so$", "recv",(void *) my_recv, (void **) (&original_recv));
        xhook_grouped_register(..., ".*\.so$", "recvfrom",(void *) my_recvfrom, (void **) (&original_recvfrom));
        xhook_grouped_register(..., ".*\.so$", "recvmsg",(void *) my_recvmsg, (void **) (&original_recvmsg));
    }
    //上传的数据监控
    if (txHook) {
        xhook_grouped_register(..., ".*\.so$", "write",(void *) my_write, (void **) (&original_write));
        xhook_grouped_register(..., ".*\.so$", "send",(void *) my_send, (void **) (&original_send));
        xhook_grouped_register(.., ".*\.so$", "sendto",(void *) my_sendto, (void **) (&original_sendto));
        xhook_grouped_register(.., ".*\.so$", "sendmsg",(void *) my_sendmsg, (void **) (&original_sendmsg));
    }
}

可以看到这里hook了socket的一些关键方法,这些方法被hook之后,当方法再次被调用的时候,我们就可以拦截到它的执行,从而做一些额外的处理。

  • connect
  • close
  • read
  • recv
  • recvfrom
  • recvmsg
  • write
  • send
  • sendto
  • sendmsg

connect

int my_connect(int fd, sockaddr *addr, socklen_t addr_length) {
    TrafficCollector::enQueueConnect(fd, addr, addr_length);
    return original_connect(fd, addr, addr_length);
}

通过调用TrafficCollector的enQueueConnect方法记录本次socket连接的信息,将MSG_TYPE_CONNECT类型,文件描述符,socket地址,调用栈等信息封装成TrafficMsg存入msgQueue队列。

void TrafficCollector::enQueueConnect(int fd, sockaddr *addr, socklen_t addr_length) {
    //将MSG_TYPE_CONNECT类型,文件描述符,socket地址,调用栈等信息封装成TrafficMsg存入msgQueue队列
    shared_ptr<TrafficMsg> msg = make_shared<TrafficMsg>(MSG_TYPE_CONNECT, fd, addr->sa_family, getKeyAndSaveStack(fd), 0);
    msgQueue.push(msg);
    queueMutex.unlock();
}

close

int my_close(int fd) {
    TrafficCollector::enQueueClose(fd);
    return original_close(fd);
}

通过调用TrafficCollector的enQueueClose方法记录本次socket关闭的信息,将MSG_TYPE_CLOSE类型,文件描述符封装成TrafficMsg存入msgQueue队列。

void TrafficCollector::enQueueClose(int fd) {
    shared_ptr<TrafficMsg> msg = make_shared<TrafficMsg>(MSG_TYPE_CLOSE, fd, 0, "", 0);
    msgQueue.push(msg);
    queueMutex.unlock();
}

read

ssize_t my_read(int fd, void *buf, size_t count) {
    ssize_t ret = original_read(fd, buf, count);
    TrafficCollector::enQueueRx(MSG_TYPE_READ, fd, ret);
    return ret;
}

type为MSG_TYPE_READ,调用TrafficCollector的enQueueRx方法。

void enQueueMsg(int type, int fd, size_t len) {
    shared_ptr<TrafficMsg> msg = make_shared<TrafficMsg>(type, fd, 0, getKeyAndSaveStack(fd), len);
    msgQueue.push(msg);
    queueMutex.unlock();
}

recv

ssize_t my_recv(int sockfd, void *buf, size_t len, int flags) {
    ssize_t ret = original_recv(sockfd, buf, len, flags);
    TrafficCollector::enQueueRx(MSG_TYPE_RECV, sockfd, ret);
    return ret;
}

type为MSG_TYPE_RECV,调用TrafficCollector的enQueueRx方法。

void enQueueMsg(int type, int fd, size_t len) {
    shared_ptr<TrafficMsg> msg = make_shared<TrafficMsg>(type, fd, 0, getKeyAndSaveStack(fd), len);
    msgQueue.push(msg);
    queueMutex.unlock();
}

recvfrom

ssize_t my_recvfrom(int sockfd, void *buf, size_t len, int flags,
                    struct sockaddr *src_addr, socklen_t *addrlen) {
    ssize_t ret = original_recvfrom(sockfd, buf, len, flags, src_addr, addrlen);
    TrafficCollector::enQueueRx(MSG_TYPE_RECVFROM, sockfd, ret);
    return ret;
}

type为MSG_TYPE_RECVFROM,调用TrafficCollector的enQueueRx方法。

recvmsg

ssize_t my_write(int fd, const void *buf, size_t count) {
    ssize_t ret = original_write(fd, buf, count);
    TrafficCollector::enQueueTx(MSG_TYPE_WRITE, fd, ret);
    return ret;
}

type为MSG_TYPE_RECVMSG,调用TrafficCollector的enQueueRx方法。

write

ssize_t my_send(int sockfd, const void *buf, size_t len, int flags) {
    ssize_t ret = original_send(sockfd, buf, len, flags);
    TrafficCollector::enQueueTx(MSG_TYPE_SEND, sockfd, ret);
    return ret;
}

type为MSG_TYPE_WRITE,调用TrafficCollector的enQueueRx方法。

send

ssize_t my_send(int sockfd, const void *buf, size_t len, int flags) {
    ssize_t ret = original_send(sockfd, buf, len, flags);
    TrafficCollector::enQueueTx(MSG_TYPE_SEND, sockfd, ret);
    return ret;
}

type为MSG_TYPE_SEND,调用TrafficCollector的enQueueRx方法。

sendto

ssize_t my_sendto(int sockfd, const void *buf, size_t len, int flags,
                  const struct sockaddr *dest_addr, socklen_t addrlen) {
    ssize_t ret = original_sendto(sockfd, buf, len, flags, dest_addr, addrlen);
    TrafficCollector::enQueueTx(MSG_TYPE_SENDTO, sockfd, ret);
    return ret;
}

type为MSG_TYPE_SENDTO,调用TrafficCollector的enQueueRx方法。

sendmsg

ssize_t my_sendmsg(int sockfd, const struct msghdr *msg, int flags) {
    ssize_t ret = original_sendmsg(sockfd, msg, flags);
    TrafficCollector::enQueueTx(MSG_TYPE_SENDMSG, sockfd, ret);
    return ret;
}

type为MSG_TYPE_SENDMSG,调用TrafficCollector的enQueueRx方法。

enQueueRx

可以看到上边除了connect和close方法外,其他都调用到了TrafficCollector的enQueueRx方法,我们看看这个方法做了什么。

void TrafficCollector::enQueueTx(int type, int fd, size_t len) {
    enQueueMsg(type, fd, len);
}

void enQueueMsg(int type, int fd, size_t len) {
    shared_ptr<TrafficMsg> msg = make_shared<TrafficMsg>(type, fd, 0, getKeyAndSaveStack(fd), len);
    msgQueue.push(msg);
    queueMutex.unlock();
}

enQueueRx方法在不断的封装TrafficMsg对象并存入队列msgQueue,每个方法间区别在于type的定义不同。所以到这里我们可以有一个简单的结论了: 被hook的这些方法,执行时会不断的获取socket此时的信息,封装成TrafficMsg对象存入队列供消费者线程进行消费,所以这里扮演的角色就是生产者线程。此时我们回头再去看loop线程。

loop

loop线程就是上边提到的消费者线程,消费线程不断的循环,当msgQueue中有数据时,就开始做进一步的处理。

void loop() {
    while (loopRunning) {
        if (msgQueue.empty()) {
            queueMutex.lock();
        } else {
            shared_ptr<TrafficMsg> msg = msgQueue.front();
            if (msg->type == MSG_TYPE_CONNECT) {
                //socket开始连接,以文件描述符为key, 地址为value存入fdFamilyMap中
                fdFamilyMap[msg->fd] = msg->sa_family;
            } else if (msg->type == MSG_TYPE_READ) {
                //接收数据,开始read,假如fdFamilyMap存在,这个fd,说明已连接过
                if (fdFamilyMap.count(msg->fd) > 0) {
                    appendRxTraffic(msg->threadName, msg->len);
                }
            } else if (msg->type >= MSG_TYPE_RECV && msg->type <= MSG_TYPE_RECVMSG) {
                //接收数据
                if (fdFamilyMap[msg->fd] != AF_LOCAL) {
                    appendRxTraffic(msg->threadName, msg->len);
                }
            } else if (msg->type == MSG_TYPE_WRITE) {
                //写入数据
                if (fdFamilyMap.count(msg->fd) > 0) {
                    appendTxTraffic(msg->threadName, msg->len);
                }
            } else if (msg->type >= MSG_TYPE_SEND && msg->type <= MSG_TYPE_SENDMSG) {
                //写入数据
                if (fdFamilyMap[msg->fd] != AF_LOCAL) {
                    appendTxTraffic(msg->threadName, msg->len);
                }
            } else if (msg->type == MSG_TYPE_CLOSE) {
                //关闭
                fdThreadNameMapLock.lock();
                fdThreadNameMap.erase(msg->fd);
                fdThreadNameMapLock.unlock();
                fdFamilyMap.erase(msg->fd);
            }
            msgQueue.pop();
        }
    }
}

从上边代码的注释可以看到,关键的两个方法是上传数据时调用的appendTxTraffic用来记录上行的数据流量,接收数据时调用的appendRxTraffic方法用来记录下行的数据流量,所以读或写的过程也就是不断的实时记录流量的过程。

appendTxTraffic

以线程名为key, 流量值为value存入txTrafficInfoMap中。

void appendTxTraffic(const string& threadName, long len) {
    txTrafficInfoMapLock.lock();
    txTrafficInfoMap[threadName] += len;
    txTrafficInfoMapLock.unlock();
}

appendRxTraffic

以线程名为key, 流量值为value存入rxTrafficInfoMap中。

void appendRxTraffic(const string& threadName, long len) {
    rxTrafficInfoMapLock.lock();
    rxTrafficInfoMap[threadName] += len;
    rxTrafficInfoMapLock.unlock();
}

看到这里感觉有点奇怪了,怎么只是将数据记录到对应的map中,什么时候取的数据?在TrafficPlugin.java中我们可以找到这个方法getTrafficInfoMap,它的返回值是HashMap,其实就是上边提到的存储起来的流量信息。

getTrafficInfoMap

public HashMap<String, String> getTrafficInfoMap(int type) {
    //进入native层的方法
    return nativeGetTrafficInfoMap(type);
}

来到TrafficCollector的getTrafficInfoMap方法。

static jobject nativeGetTrafficInfoMap(JNIEnv *env, jclass, jint type) {
    return TrafficCollector::getTrafficInfoMap(type);
}

可以看到,下面的逻辑很清晰,通过构造一个Java层的HashMap对象,并将指定类型的信息从c++层的map对象中转移到HashMap中,这样一来,就实时的拿到了当前流量消耗的数据,数据包含线程名和流量值,拿到线程名后可以通过getStackTraceMap拿到线程名和堆栈信息的映射关系,从而获取到实时的调用堆栈信息。

jobject TrafficCollector::getTrafficInfoMap(int type) {
    ...
    if (type == TYPE_GET_TRAFFIC_RX) {
        //接收的数据
        for (auto & it : rxTrafficInfoMap) {
            //线程名
            jstring threadName = env->NewStringUTF(it.first.c_str());
            //流量长度,是一个数值型
            jstring traffic = env->NewStringUTF(to_string(it.second).c_str());
            env->CallObjectMethod(jHashMap, mapPut, threadName, traffic);
        }
    } else if (type == TYPE_GET_TRAFFIC_TX) {
        //上传的数据
        for (auto & it : txTrafficInfoMap) {
            jstring threadName = env->NewStringUTF(it.first.c_str());
            jstring traffic = env->NewStringUTF(to_string(it.second).c_str());
            env->CallObjectMethod(jHashMap, mapPut, threadName, traffic);
        }
    }
    return jHashMap;
}

看一个效果图

stop

stop方法做的就是清理资源的工作了,因为核心功能都在native层,所以清理的工作还是会进入native层做处理,第一跳出循环线程,第二清理内存中的map映射表,至此,流量监控的代码分析完成。

@Override
public void stop() {
    nativeReleaseMatrixTraffic();
}

static void nativeReleaseMatrixTraffic(JNIEnv *env, jclass) {
    //停止循环线程
    TrafficCollector::stopLoop();
    //清理所有的映射表
    TrafficCollector::clearTrafficInfo();
}

总结

流量监控的实现方式是通过hook c++层socket的发起和接收相关的方法,拦截到对应方法从而对过程中涉及到的流量信息进行采集,采集到的数据就可以实时的获取到,以做进一步的分析。

Android 学习笔录

Android 性能优化篇:https://qr18.cn/FVlo89
Android Framework底层原理篇:https://qr18.cn/AQpN4J
Android 车载篇:https://qr18.cn/F05ZCM
Android 逆向安全学习笔记:https://qr18.cn/CQ5TcL
Android 音视频篇:https://qr18.cn/Ei3VPD
Jetpack全家桶篇(内含Compose):https://qr18.cn/A0gajp
OkHttp 源码解析笔记:https://qr18.cn/Cw0pBD
Kotlin 篇:https://qr18.cn/CdjtAF
Gradle 篇:https://qr18.cn/DzrmMB
Flutter 篇:https://qr18.cn/DIvKma
Android 八大知识体:https://qr18.cn/CyxarU
Android 核心笔记:https://qr21.cn/CaZQLo
Android 往年面试题锦:https://qr18.cn/CKV8OZ
2023年最新Android 面试题集:https://qr18.cn/CgxrRy
Android 车载开发岗位面试习题:https://qr18.cn/FTlyCJ
音视频面试题锦:https://qr18.cn/AcV6Ap

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1105045.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【学习笔记】RabbitMQ-5 消息的可靠性投递 以及示例代码

参考资料 RabbitMQ官方网站RabbitMQ官方文档噼咔噼咔-动力节点教程 文章目录 八、RabbitMQ的确认机制 -confirm8.1 Confirm 模式简介8.2 具体代码设置8.2.1 **设置思路**&#xff1a;8.2.2 **代码实现**8.2.2.1 开启生产者的确认模式.8.2.2.2 实现接口ComfirmCallback8.2.2.3 配…

Transformer模型 | Transformer模型描述

谷歌推出的BERT模型在11项NLP任务中夺得SOTA结果,引爆了整个NLP界。而BERT取得成功的一个关键因素是Transformer的强大作用。谷歌的Transformer模型最早是用于机器翻译任务,当时达到了SOTA效果。Transformer改进了RNN最被人诟病的训练慢的缺点,利用self-attention机制实现快…

浪涌防护:TSS管的工作原理与应用?|深圳比创达EMC

浪涌防护&#xff1a;TSS管的工作原理与应用&#xff1f;相信不少人是有疑问的&#xff0c;今天深圳市比创达电子科技有限公司就跟大家解答一下&#xff01; 一、TSS工作原理 TSS半导体放电管是一种电压开关型瞬态抑制二极管&#xff0c;即涌压抑制晶体管&#xff0c;或称为导…

大中小企业自招人力及劳务派遣招聘

抖音直播招聘报白是一种通过直播方式展示职位信息并与求职者互动的招聘方式。在抖音平台上&#xff0c;企业或人力资源公司可利用直播将职位以视频直播的方式展现&#xff0c;这种方式可给求职者带来更强的代入感和真实性&#xff0c;解决其对岗位真假难辨的信任问题。 图片 …

SCB-Dataset3 公开 学生课堂行为数据集: A Benchmark for Detecting Student Classroom Behavior

公开 学生课堂行为数据集 SCB-Dataset 2 Student Classroom Behavior dataset b站&#xff1a;https://www.bilibili.com/video/BV1D34y1g76E/ arxiv: https://arxiv.org/pdf/2310.02522.pdf github: https://github.com/Whiffe/SCB-dataset 百度云&#xff1a;https://pan…

如何选择适合的发电机测试设备?

选择适合的发电机测试设备需要考虑电机的额定功率和负载需求&#xff0c;选择能够满足需求的测试设备&#xff0c;确保测试设备的功率范围覆盖发电机的额定功率&#xff0c;并有一定的余量。常见的发电机测试项目包括电压、电流、频率、功率因数、转速、温度等参数的测试&#…

PCB布线时如何保证100M以上信号的稳定性?

PCB布线时是电子工程中非常重要的环节&#xff0c;对于保证信号的稳定性和完整性至关重要&#xff0c;若电子工程师遇上100M以上信号的布线需求&#xff0c;该如何设计来保证其稳定性&#xff1f;下面或许能给你些参考。 1、选择合适的传输介质 对高速信号&#xff0c;选择合适…

基于 SaaS 搭建的党建小程序源码系统 带完整的搭建教程

随着互联网技术的发展和应用的普及&#xff0c;传统的党建模式已经难以满足现代社会的需求。为了更好地服务党员和群众&#xff0c;提高党组织的凝聚力和战斗力&#xff0c;基于 SaaS搭建的党建小程序源码系统应运而生。小程序的出现可以很好的解决大多数问题&#xff0c;方便了…

数字孪生与智慧城市:重塑未来城市生活的奇迹

今天&#xff0c;我们将探讨数字孪生和智慧城市两个颠覆性技术&#xff0c;它们正引领着未来城市生活的巨大变革。随着科技的飞速发展&#xff0c;数字孪生和智慧城市成为实现可持续发展和提升居民生活质量的关键策略。 数字孪生&#xff1a;实现现实与虚拟的完美融合 数字孪生…

AI工具在工作中的“大作用”

现如今科技的发展让我们的生活越来越便利&#xff0c;一些AI工具的出现&#xff0c;更对我们的工作有莫大的帮助。 AI工具的辅助就像给上班族提供了一种更加高级的“摸鱼方法”&#xff0c;大大提高了打工人的工作效率。如果有一种什么都能回答你&#xff0c;甚至能帮助你完成…

用例图包含关系、扩展关系、泛化关系解析(最全总结,非常详细)

一、用例图中的各种关系 a&#xff09;参与者与用例间的关联关系&#xff1a;参与者与用例之间的通信&#xff0c;也成为关联或通信关系。 b&#xff09;用例与用例之间的关系&#xff1a;包含关系&#xff08;include&#xff09;、扩展关系&#xff08;extend&#xff09;、…

智慧机场航线监测系统:提升航空运输安全与效率的新一步

在当今世界&#xff0c;空中出行已经成为越来越多人生活的一部分。人们频繁地乘坐飞机来往各地&#xff0c;全球航空旅行需求不断增长&#xff0c;航空运输业已经变得越来越复杂。在这个复杂性不断增强的行业中&#xff0c;智慧机场应用航线监测系统成为了航空领域关键的发展趋…

LeetCode2652——倍数之和

LeetCode2562 自己的解法&#xff1a; 官方给的解法&#xff08;不需要创建额外的数组&#xff0c;更为简洁&#xff0c;效率更高&#xff09;&#xff1a;

操作指南 | 如何通过Moonbeam DApp在OpenGov投票

除了Polkassembly或Polkadot.js以外&#xff0c;Moonbeam自己的DApp也可以直接参与链上治理。该界面简洁完整&#xff0c;对用户来说非常方便。 首先进入https://apps.moonbeam.network/moonbeam&#xff0c;连接你的钱包至DApp。Moonbeam DApp支持很多类型的钱包&#xff0c;…

Unity游戏开发中ngui和ugui区别与优缺点详解

Unity3D是一款跨平台的游戏开发引擎&#xff0c;它支持多种平台&#xff0c;包括PC、移动设备和主机。在Unity3D中&#xff0c;UI系统是游戏开发中非常重要的一部分&#xff0c;它负责游戏中的用户界面的显示和交互。 对惹&#xff0c;这里有一个游戏开发交流小组&#xff0c;…

rust学习特殊的地方——函数返回值

概念 Rust 中的函数定义以 fn 开始&#xff0c;后跟着函数名和一对圆括号。大括号告诉编译器函数体在哪里开始和结束。 特殊的地方——函数返回值 错误的写法 正解1 去掉分号 fn main() {let x plus_one(5);println!("The value of x is: {}", x); }fn plus_…

AI巧破网络诈骗?闭门研讨会报名丨青源Workshop第26期

青源Workshop丨No.26 AI反诈与智能风控&#xff1a;信息安全的矛与盾 AI红利接踵而至&#xff0c;安全风险如影随形。过去几年&#xff0c;人工智能技术的迅速发展催生了包括金融、电子商务、社交网络、医疗保健等众多应用场景。AI应用落地带来新安全风险&#xff0c;安全防护难…

JVS规则引擎及智能BI又更新新功能啦!赶紧来试试

规则引擎更新功能 新增: 1.复合变量新增排序、排名功能 可以按照特定的顺序对数据进行排列&#xff0c;确定规则的优先级&#xff0c;可以提高数据处理效率&#xff0c;帮助分析人员更好地了解数据分布和趋势。 2.决策流新增动态日志功能 动态日志可以记录规则执行的过程和…

品牌创意二维码营销活动:MoneyLion 在纽约全城“撒钱”,月增百万级曝光!

在2023年4月——金融知识月&#xff0c;MoneyLion 在纽约策划了一场轰动全城的“撒钱”活动&#xff01; 在开始介绍这场极具创意的活动之前&#xff0c;我们先来了解一下MoneyLion这家公司。MoneyLion 是一家私营金融科技公司&#xff0c;为消费者提供贷款、财务咨询和投资服…

vue实现在页面拖拽放大缩小div并显示鼠标在div的坐标

1、功能要求&#xff1a; 实现在一个指定区域拖拽div,并可以放大缩小&#xff0c;同时显示鼠标在该div里的坐标&#xff0c;如图可示 缩小并拖动 2、实现 <div class"div_content" ref"div_content"><div class"div_image" id"…