【Android 内存优化】 native内存泄漏监控方案源码分析

文章目录

  • 前言
  • 使用效果
  • 使用api
  • JNI的动态注册
    • native方法
    • 动态注册
  • hook的实现
      • android_dlopen_ext和dl_iterate_phdr
  • naive监控的实现
    • nativeGetLeakAllocs
  • 总结

前言

Android的native泄漏怎么检测?下面通过研究开源项目KOOM来一探究竟。

使用效果

未触发泄漏前的日志:

0 bytes in 0 allocations unreachable out of 13587448 bytes in 36227 allocations

特意触发泄漏,再次触发:

sweeping done
folding related leaks
folding done
unreachable memory detection done
208233696 bytes in 268 allocations unreachable out of 306475224 bytes in 59154 allocations
LeakRecordMap size: 0

可以看到,泄漏的检测精准到了字节级别。

KOOM文档里面写着,native的实现是使用了Google官方给出的libmemunreachable 动态库。

参考地址:https://android.googlesource.com/platform/system/memory/libmemunreachable/+/master/README.md

使用api

 LeakMonitor.INSTANCE.start()LeakMonitor.INSTANCE.checkLeaks()LeakMonitor.INSTANCE.stop()

简单行代码即可完成,但是别忘了,我们的目的是研究他是怎么实现的。

来看代码。

JNI的动态注册

native方法的定义,简单看方法名可以大概知道,有安装监控器、卸载监控器、设置阈值和获取存储泄漏数据的容器。
但是一眼看不出来nativeGetAllocIndex的作用。

native方法

  @JvmStaticprivate external fun nativeInstallMonitor(selectedList: Array<String>,ignoreList: Array<String>, enableLocalSymbolic: Boolean): Boolean@JvmStaticprivate external fun nativeUninstallMonitor()@JvmStaticprivate external fun nativeSetMonitorThreshold(size: Int)@JvmStaticprivate external fun nativeGetAllocIndex(): Long@JvmStaticprivate external fun nativeGetLeakAllocs(leakRecordMap: Map<String, LeakRecord>)

来看看native层的实现:

动态注册

jni_leak_monitor.cpp

static const JNINativeMethod kLeakMonitorMethods[] = {{"nativeInstallMonitor", "([Ljava/lang/String;[Ljava/lang/String;Z)Z",reinterpret_cast<void *>(InstallMonitor)},{"nativeUninstallMonitor", "()V",reinterpret_cast<void *>(UninstallMonitor)},{"nativeSetMonitorThreshold", "(I)V",reinterpret_cast<void *>(SetMonitorThreshold)},{"nativeGetAllocIndex", "()J", reinterpret_cast<void *>(GetAllocIndex)},{"nativeGetLeakAllocs", "(Ljava/util/Map;)V",reinterpret_cast<void *>(GetLeakAllocs)}};extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {JNIEnv *env;if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_4) != JNI_OK) {ALOGE("GetEnv Fail!");return JNI_ERR;}jclass leak_monitor;FIND_CLASS(leak_monitor, kLeakMonitorFullyName);
#define NELEM(x) (sizeof(x) / sizeof((x)[0]))if (env->RegisterNatives(leak_monitor, kLeakMonitorMethods,NELEM(kLeakMonitorMethods)) != JNI_OK) {ALOGE("RegisterNatives Fail!");return JNI_ERR;}return JNI_VERSION_1_4;
}

这里进行了JNI函数的动态注册,至于动态注册的优点,就是可以提高一点代码性能,无需通过重复查表来链接对应的native方法。

JNINativeMethod 的结构如下:

    // 方法1{"methodName1", "methodSignature1", (void*) methodPointer1},// 方法2{"methodName2", "methodSignature2", (void*) methodPointer2},// ... };

我们找到了对应的native方法,先来看看InstallMonitor方法的实现:

jni_leak_monitor.cpp

static bool InstallMonitor(JNIEnv *env, jclass clz, jobjectArray selected_array,jobjectArray ignore_array,jboolean enable_local_symbolic) {jclass leak_record;FIND_CLASS(leak_record, kLeakRecordFullyName);g_leak_record.global_ref =reinterpret_cast<jclass>(env->NewGlobalRef(leak_record));if (!CheckedClean(env, g_leak_record.global_ref)) {return false;}GET_METHOD_ID(g_leak_record.construct_method, leak_record, "<init>","(JILjava/lang/String;[Lcom/kwai/koom/nativeoom/leakmonitor/""FrameInfo;)V");jclass frame_info;FIND_CLASS(frame_info, kFrameInfoFullyName);g_frame_info.global_ref =reinterpret_cast<jclass>(env->NewGlobalRef(frame_info));if (!CheckedClean(env, g_frame_info.global_ref)) {return false;}GET_METHOD_ID(g_frame_info.construct_method, frame_info, "<init>","(JLjava/lang/String;)V");g_enable_local_symbolic = enable_local_symbolic;auto array_to_vector =[](JNIEnv *env, jobjectArray jobject_array) -> std::vector<std::string> {std::vector<std::string> ret;int length = env->GetArrayLength(jobject_array);if (length <= 0) {return ret;}for (jsize i = 0; i < length; i++) {auto str = reinterpret_cast<jstring>(env->GetObjectArrayElement(jobject_array, i));const char *data = env->GetStringUTFChars(str, nullptr);ret.emplace_back(data);env->ReleaseStringUTFChars(str, data);}return std::move(ret);};std::vector<std::string> selected_so = array_to_vector(env, selected_array);std::vector<std::string> ignore_so = array_to_vector(env, ignore_array);return CheckedClean(env, LeakMonitor::GetInstance().Install(&selected_so, &ignore_so));
}

这里代码比较长,我们只需获取重点信息。

首先定义了一个jclass变量leak_record和frame_info,并通过FIND_CLASS宏查找Java类并创建全局引用。

使用GET_METHOD_ID宏获取leak_record和frame_info类的构造方法ID,用于后续实例化对象。

定义了一个lambda函数array_to_vector,该函数接受一个JNIEnv指针和一个jobjectArray对象作为参数,将其转换为C++的字符串向量。

接着把向量传递给LeakMonitor的Install方法:

hook的实现

leak_monitor.cpp

bool LeakMonitor::Install(std::vector<std::string> *selected_list,std::vector<std::string> *ignore_list) {KCHECK(!has_install_monitor_);// Reinstall can't hook againif (has_install_monitor_) {return true;}memory_analyzer_ = std::make_unique<MemoryAnalyzer>();if (!memory_analyzer_->IsValid()) {ALOGE("memory_analyzer_ NOT Valid");return false;}std::vector<const std::string> register_pattern = {"^/data/.*\\.so$"};std::vector<const std::string> ignore_pattern = {".*/libkoom-native.so$",".*/libxhook_lib.so$"};if (ignore_list != nullptr) {for (std::string &item : *ignore_list) {ignore_pattern.push_back(".*/" + item + ".so$");}}if (selected_list != nullptr && !selected_list->empty()) {// only hook the so in selected listregister_pattern.clear();for (std::string &item : *selected_list) {register_pattern.push_back("^/data/.*/" + item + ".so$");}}std::vector<std::pair<const std::string, void *const>> hook_entries = {std::make_pair("malloc", reinterpret_cast<void *>(WRAP(malloc))),std::make_pair("realloc", reinterpret_cast<void *>(WRAP(realloc))),std::make_pair("calloc", reinterpret_cast<void *>(WRAP(calloc))),std::make_pair("memalign", reinterpret_cast<void *>(WRAP(memalign))),std::make_pair("posix_memalign",reinterpret_cast<void *>(WRAP(posix_memalign))),std::make_pair("free", reinterpret_cast<void *>(WRAP(free)))};if (HookHelper::HookMethods(register_pattern, ignore_pattern, hook_entries)) {has_install_monitor_ = true;return true;}HookHelper::UnHookMethods();live_alloc_records_.Clear();memory_analyzer_.reset(nullptr);ALOGE("%s Fail", __FUNCTION__);return false;
}

这个函数前面通过正则表达式定义了需要hook和不需要hook的动态库。

后续就是把需要hook的系统内存管理函数put到容器里面,接着传给HookHelper去实现hook。

hook_helper.cpp

bool HookHelper::HookMethods(std::vector<const std::string> &register_pattern,std::vector<const std::string> &ignore_pattern,std::vector<std::pair<const std::string, void *const>> &methods) {if (register_pattern.empty() || methods.empty()) {ALOGE("Hook nothing");return false;}register_pattern_ = std::move(register_pattern);ignore_pattern_ = std::move(ignore_pattern);methods_ = std::move(methods);DlopenCb::GetInstance().AddCallback(Callback);return HookImpl();
}void HookHelper::Callback(std::set<std::string> &, int, std::string &) {HookImpl();
}

做了一些数据准备工作,顺便加了个回调,便于后续的hook操作。
来看下HookImpl


bool HookHelper::HookImpl() {pthread_mutex_lock(&DlopenCb::hook_mutex);xhook_clear();for (auto &pattern : register_pattern_) {for (auto &method : methods_) {if (xhook_register(pattern.c_str(), method.first.c_str(), method.second,nullptr) != EXIT_SUCCESS) {ALOGE("xhook_register pattern %s method %s fail", pattern.c_str(),method.first.c_str());pthread_mutex_unlock(&DlopenCb::hook_mutex);return false;}}}for (auto &pattern : ignore_pattern_) {for (auto &method : methods_) {if (xhook_ignore(pattern.c_str(), method.first.c_str()) != EXIT_SUCCESS) {ALOGE("xhook_ignore pattern %s method %s fail", pattern.c_str(),method.first.c_str());pthread_mutex_unlock(&DlopenCb::hook_mutex);return false;}}}int ret = xhook_refresh(0);pthread_mutex_unlock(&DlopenCb::hook_mutex);return ret == 0;
}

这里就是hook的调用了,使用了爱奇艺的开源框架xhook。

看下实现类:
dlopencb.cpp

int Callback(struct dl_phdr_info *info, size_t size, void *data) {auto *pair = static_cast<std::pair<std::set<std::string> *, std::set<std::string> *> *>(data);auto origin = pair->first;auto add = pair->second;auto name = info->dlpi_name;if (name != nullptr && hookDlopen(name) && origin->insert(name).second) {add->insert(name);}return 0;
}

Callback 函数是一个回调函数,它用于迭代动态链接器的程序头部信息。它的功能如下:

接受三个参数:struct dl_phdr_info* info,size_t size,void* data。
将 data 转换为 std::pair<std::setstd::string, std::setstd::string> 类型的指针。
从 pair 中获取 origin(原始共享库集合)和 add(新增共享库集合)。
判断动态链接库的名称是否非空,并且是否需要 hookDlopen。如果是,则将其添加到 origin 集合,并且添加到 add 集合中。

dlopencb.cpp

void DlopenCb::Refresh(int source, std::string &loadLibName) {
//一开始输出日志,表示刷新操作开始。XH_LOG_INFO("Refresh start %d", source);//接着创建一个空的 addLibs 集合,用于存储新增的共享库。std::set<std::string> addLibs;pthread_mutex_lock(&add_lib_mutex);//获取 hooked_libs 和 addLibs 的指针对,并调用 dl_iterate_phdr 函数进行迭代,每次迭代调用 Callback 函数。auto callbackData =make_pair(&hooked_libs, &addLibs);dl_iterate_phdr(Callback, &callbackData);pthread_mutex_unlock(&add_lib_mutex);//如果 addLibs 集合不为空,则对 hook_mutex 进行加锁,清除现有的 xhook 钩子,并根据新增的共享库重新注册钩子。if (!addLibs.empty()) {pthread_mutex_lock(&hook_mutex);xhook_clear();//根据调试模式进行设置。if (is_debug) {xhook_enable_sigsegv_protection(0);xhook_enable_debug(1);} else {xhook_enable_sigsegv_protection(1);}for (const auto &lib : addLibs) {auto lib_ctr = lib.c_str();xhook_register(lib_ctr, "android_dlopen_ext", (void *) (HookDlopenExt), nullptr);
//      xhook_register(lib_ctr, "dlopen", (void *) (HookDlopen), nullptr);
//输出日志,表示新增的共享库已添加。XH_LOG_INFO("Refresh new lib added %s", lib_ctr);}//刷新 xhook 钩子。xhook_refresh(0);pthread_mutex_unlock(&hook_mutex);// notifyXH_LOG_INFO("Refresh hooked");pthread_mutex_lock(&callback_mutex);//对回调函数进行通知,传递新增的共享库信息。for (auto &callback:callbacks) {callback(addLibs, source, loadLibName);}pthread_mutex_unlock(&callback_mutex);} else {//如果 addLibs 集合为空,则输出日志,表示没有发现新增的共享库。XH_LOG_INFO("Refresh no lib found");}
}

前面该宏定义:

// 兼容编译失败,实际API 21以下不支持开启
#if __ANDROID_API__ < 21
void* android_dlopen_ext(const char* __filename, int __flags, const android_dlextinfo* __info) {return 0;
}
int dl_iterate_phdr(int (*__callback)(struct dl_phdr_info*, size_t, void*), void* __data) {return 0;
}
#endif

这段代码是一个条件编译,它检查了当前的 Android API 版本是否低于 21。如果当前的 Android API 版本低于 21,则定义了两个函数 android_dlopen_ext 和 dl_iterate_phdr,但这两个函数的实现只是简单地返回了 0,所以在 API 21 以下的版本中这两个函数不支持。

至于怎么理解这两个函数。

android_dlopen_ext和dl_iterate_phdr


void* android_dlopen_ext(const char* __filename, int __flags, const android_dlextinfo* __info) :

这是一个函数声明,它声明了一个名为 android_dlopen_ext 的函数,该函数用于以扩展方式动态加载共享库(动态链接库)。

参数说明如下:

  • __filename:这是一个指向要加载的共享库文件名的 C 字符串。
  • __flags:这是一个整数,用于指定加载共享库的选项标志。
  • __info:这是一个指向 android_dlextinfo 结构体的指针,该结构体用于传递扩展加载选项的详细信息。如果不需要传递额外信息,可以传入 nullptr

函数返回一个 void* 类型的指针,该指针通常用于表示加载的共享库的句柄或者标识符。在这个声明中,函数总是返回 0,表示加载失败或出错。

android_dlopen_ext 函数的具体实现通常由 Android 系统提供,它是 Android 平台上 dlopen 函数的一个扩展版本,用于支持更多的加载选项和功能。

int dl_iterate_phdr(int (*__callback)(struct dl_phdr_info*, size_t, void*), void* __data)

这是一个函数声明,它声明了一个名为 dl_iterate_phdr 的函数,该函数是用来迭代动态链接器的程序头部信息的。这个函数通常在操作系统中用于获取运行时链接器(Runtime Linker)加载的动态链接库的信息。

参数说明如下:

  • __callback: 这是一个函数指针,指向一个函数,该函数用于处理迭代过程中获取的动态链接库的信息。它接受三个参数:
    • struct dl_phdr_info*: 这是一个结构体指针,用于存储动态链接库的程序头部信息。
    • size_t: 这是一个表示结构体的大小的参数。
    • void*: 这是一个指向用户自定义数据的指针,可以在回调函数中使用。
  • __data: 这是一个指向用户自定义数据的指针,会传递给回调函数 __callback

naive监控的实现

我们终于来到了重点部分。

#define WRAP(x) x##Monitor
#define HOOK(ret_type, function, ...) \static ALWAYS_INLINE ret_type WRAP(function)(__VA_ARGS__)

这里通过宏定义了HOOK函数把前面的系统内存分配函数进行了hook,这里:

  std::vector<std::pair<const std::string, void *const>> hook_entries = {std::make_pair("malloc", reinterpret_cast<void *>(WRAP(malloc))),std::make_pair("realloc", reinterpret_cast<void *>(WRAP(realloc))),std::make_pair("calloc", reinterpret_cast<void *>(WRAP(calloc))),std::make_pair("memalign", reinterpret_cast<void *>(WRAP(memalign))),std::make_pair("posix_memalign",reinterpret_cast<void *>(WRAP(posix_memalign))),std::make_pair("free", reinterpret_cast<void *>(WRAP(free)))};

先看下hook了malloc的实现代码:

HOOK(void *, malloc, size_t size) {auto result = malloc(size);LeakMonitor::GetInstance().OnMonitor(reinterpret_cast<intptr_t>(result),size);CLEAR_MEMORY(result, size);return result;
}

执行了OnMonitor函数:

ALWAYS_INLINE void LeakMonitor::OnMonitor(uintptr_t address, size_t size) {if (!has_install_monitor_ || !address ||size < alloc_threshold_.load(std::memory_order_relaxed)) {return;}RegisterAlloc(address, size);
}

这里判断了一下阈值,加入达到阈值则执行RegisterAlloc:

ALWAYS_INLINE void LeakMonitor::RegisterAlloc(uintptr_t address, size_t size) {if (!address || !size) {return;}auto unwind_backtrace = [](uintptr_t *frames, uint32_t *frame_count) {*frame_count = StackTrace::FastUnwind(frames, kMaxBacktraceSize);};thread_local ThreadInfo thread_info;auto alloc_record = std::make_shared<AllocRecord>();alloc_record->address = CONFUSE(address);alloc_record->size = size;alloc_record->index = alloc_index_++;memcpy(alloc_record->thread_name, thread_info.name, kMaxThreadNameLen);unwind_backtrace(alloc_record->backtrace, &(alloc_record->num_backtraces));live_alloc_records_.Put(CONFUSE(address), std::move(alloc_record));
}

看下FastUnwind方法 :

KWAI_EXPORT size_t StackTrace::FastUnwind(uintptr_t *buf, size_t num_entries) {pthread_once(&once_control_tls, fast_unwind_init);auto begin = reinterpret_cast<uintptr_t>(__builtin_frame_address(0));auto end = get_thread_stack_top();stack_t ss;if (sigaltstack(nullptr, &ss) == 0 && (ss.ss_flags & SS_ONSTACK)) {end = reinterpret_cast<uintptr_t>(ss.ss_sp) + ss.ss_size;}size_t num_frames = 0;while (num_frames < kMaxBacktraceSize) {auto *frame = reinterpret_cast<frame_record *>(begin);if (num_frames < num_entries) {buf[num_frames] = GetAdjustPC(frame->return_addr);}++num_frames;if (frame->next_frame < begin + sizeof(frame_record) ||frame->next_frame >= end || frame->next_frame % sizeof(void *) != 0) {break;}begin = frame->next_frame;}return num_frames;
}

这里记录了函数调用的回溯栈相关数据,包括起始地址和大小。结合前面的方法可以知道,就是获取调用栈的信息,然后put到live_alloc_records_里面。

nativeGetLeakAllocs

分析到这里,我们回头看下外面调用的nativeGetLeakAllocs方法。分析到这里我们可以知道它会执行到这里:

std::vector<std::shared_ptr<AllocRecord>> LeakMonitor::GetLeakAllocs() {KCHECK(has_install_monitor_);auto unreachable_allocs = memory_analyzer_->CollectUnreachableMem();std::vector<std::shared_ptr<AllocRecord>> live_allocs;std::vector<std::shared_ptr<AllocRecord>> leak_allocs;// Collect live memory blocksauto collect_func = [&](std::shared_ptr<AllocRecord> &alloc_info) -> void {live_allocs.push_back(alloc_info);};live_alloc_records_.Dump(collect_func);auto is_leak = [&](decltype(unreachable_allocs)::value_type &unreachable,std::shared_ptr<AllocRecord> &live) -> bool {auto live_start = CONFUSE(live->address);auto live_end = live_start + live->size;auto unreachable_start = unreachable.first;auto unreachable_end = unreachable_start + unreachable.second;// TODO whyreturn live_start == unreachable_start ||live_start >= unreachable_start && live_end <= unreachable_end;};// Check leak allocation (unreachable && not free)for (auto &live : live_allocs) {for (auto &unreachable : unreachable_allocs) {if (is_leak(unreachable, live)) {leak_allocs.push_back(live);// Just remove leak allocation(never be free)// live->address has been confused, we need to revert it firstUnregisterAlloc(CONFUSE(live->address));}}}return leak_allocs;
}

这里分为live_allocs和leak_allocs,通过CollectUnreachableMem来获取不可达的内存信息,并保存到leak_allocs中。

到这里,核心的实现就已经走读完毕。后续的代码就是把相关的泄漏内存信息的进行一系列业务处理和打印了。但是思路和原理已经跃然纸上了,不是吗?

总结

总体来说KOOM监控native层内存泄漏的实现原理就是通过hook。hook住系统内存分配的api,然后再api的hook方法里面调用Google的memunreachable模块对内存进行可达性分析,从而获取泄漏内存的起始地址和空间大小。

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

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

相关文章

spring cloud gateway k8s优雅启停

通过配置readiness探针和preStop hook&#xff0c;实现优雅启动和停止&#xff08;滚动部署&#xff09; 1. k8s工作负载配置 readinessProbe:httpGet:path: /datetimeport: 8080scheme: HTTPinitialDelaySeconds: 30timeoutSeconds: 1periodSeconds: 30successThreshold: 1fa…

The plain HTTP request was sent to HTTPS port

异常信息 原因 错误信息 “The plain HTTP request was sent to HTTPS port” 表明客户端尝试使用未加密的HTTP协议发送请求到一个配置为使用加密的HTTPS协议的端口。 解决方案 要解决这个问题&#xff0c;需要确保使用正确的协议和端口号进行请求。应该使用的HTTPS前缀。例如…

导入excel复杂校验加异常信息返回

1.导出实现类 package com.dst.steed.fulfillment.modules.business.resourceplan.serviceimport;import com.alibaba.excel.EasyExcelFactory; import com.dst.steed.common.domain.response.Response; import com.dst.steed.common.util.*; import com.dst.steed.fulfillmen…

Spring Cloud微服务Actuator和Vue

目录 前言一、引入Actuator依赖二、暴露Actuator端点1. 配置文件2. 监控端点 三、自定义健康检查自定义健康检查类 四、vue前端代码五、监控器的优势六、监控指标的可视化1. Grafana2. Prometheus 七、安全性考虑安全配置示例 八、总结 前言 随着微服务架构的流行&#xff0c;…

官宣|阿里巴巴捐赠的 Flink CDC 项目正式加入 Apache 基金会

摘要&#xff1a;本文整理自阿里云开源大数据平台徐榜江 (雪尽)&#xff0c;关于阿里巴巴捐赠的 Flink CDC 项目正式加入 Apache 基金会&#xff0c;内容主要分为以下四部分&#xff1a; 1、Flink CDC 新仓库&#xff0c;新流程 2、Flink CDC 新定位&#xff0c;新玩法 3、Flin…

【嵌入式硬件】步进电机

1.步进电机简介 1.1步进电机基本原理 步进电机的英文是stepping motor。step的中文意思是行走、迈步。所以仅从字面上我们就可以得知,步进电机就是一步一步移动的电动机。说的官方一点儿,步进电机是一种将电脉冲信号转换成相应角位移或者线位移的电动机(直线电机)。下图为…

设计模式(十二):中介者模式(行为型模式)

Mediator&#xff0c;中介者模式&#xff1a;用一个中介对象封装一些列的对象交互。属于行为型模式 Facade&#xff0c;外观模式&#xff1a;为子系统中的一组接口提供一致的界面&#xff0c;facade 提供了一高层接口&#xff0c;这个接口使得子系统更容易使用。属于结构型模式…

灵境矩阵平台x百度---智能体(一)

什么是数据插件 大模型插件:大语言模型插件是随着大语言模型发展而诞生的全新插件。大语言模型插件的核心是Web API独立于大语言模型&#xff0c;插件开发过程不受大语言模型的约束&#xff0c;同时没有开发语言的限制&#xff0c;更加通用&#xff0c;只要WebAPI遵循RESTfuI相…

html5cssjs代码 034 自定义字体

html5&css&js代码 034 自定义字体 一、代码二、解释 这是一个带有自定义字体的网页&#xff0c;设置了页面背景颜色、文字颜色以及全局样式。它定义了三种自定义字体并通过font-face规则引入外部字体文件&#xff0c;并通过CSS类&#xff08;.f1, .f2, .f3&#xff09;…

Oracle AI Vector Search 支持使用 SQL 生成向量和计算向量相似度

Oracle AI Vector Search 支持使用 SQL 生成向量和计算向量相似度 0. 事情准备1. 使用 SQL 生成向量数据2. 使用 SQL 计算欧氏距离(Euclidean distance)3. 使用 SQL 计算余弦相似度(Cosine similarity)4. 使用 SQL 计算点积相似度 (Dot Product Similarity)5. 使用 SQL 计算曼哈…

第十二届蓝桥杯省赛CC++ 研究生组-路径

记录到每个结点的最短距离&#xff0c;以此为基础计算后续结点最优值 #include<iostream> #include<algorithm> using namespace std; typedef long long ll;ll gcd(int a, int b){if(!b) return a;return gcd(b, a % b); }int main(){ll dp[2022] {0};//dp[i]记…

如何打破SAST代码审计工具的局限性?

关键词&#xff1a;白盒测试&#xff1b;代码分析工具&#xff1b;代码扫描工具&#xff1b;静态代码检测工具&#xff1b; 在代码的世界里&#xff0c;安全问题如同潜伏的暗礁&#xff0c;随时可能让航行中的软件项目触礁沉没。SAST代码审计工具如同雷达一样&#xff0c;以其独…

inputStream.avaliable()方法网络操作读取不全BUG

一、问题描述 公司有个需求&#xff0c;就是调用方&#xff08;我&#xff09;需要把pdf文件转为Base64字符串作为参数传递为被调用方&#xff0c;以下是大致转换过程&#xff1a; URL url new URL("http://xxxx.pdf");HttpURLConnection uc (HttpURLConnection) …

docker入门(十)—— docker-compose详解

Docker Compose dockercompose官网&#xff1a;https://docs.docker.com/compose/ 什么是 docker compose Docker Compose 是用于定义和运行多容器应用程序的工具。 这是解锁简化和高效的开发和部署体验的关键。 Compose 简化了对整个应用程序堆栈的控制&#xff0c;让您能…

html js css如何使循环混播放视频时视频切换很平滑的过渡

1.html代码 <video id"video1" autoplay loop muted playsinline><source src"video1.mp4" type"video/mp4"> </video> <video id"video2" autoplay loop muted playsinline><source src"video2.mp4…

Chrome 114 带着侧边栏扩展来了

效果展示 manifest.json {"manifest_version": 3,"name": "ChatGPT学习","version": "0.0.2","description": "ChatGPT,GPT-4,Claude3,Midjourney,Stable Diffusion,AI,人工智能,AI","icons"…

全量知识系统 控制器-神经网络设计(设想及百度AI回复)

---备忘&#xff1a;权值、阈值和极值 在神经元的计算中&#xff0c;包括三种计算值&#xff1a;权值、阈值和极值。分别是为全量知识系统设计的三级神经网络中神经元不同层级的计算任务。其中权值 是全连接的性质&#xff0c;阈值是池化的性质&#xff0c;极值是卷积的性质。…

cocos 3.8开发 微信小游戏分包技巧压缩主包

Creator 版本&#xff1a; 3.8.2 目标平台&#xff1a;小游戏开发 压缩后 我不知道别人压缩几百kb是怎么做到的。不过哪个要钱。 我这个技巧不用花钱。 论坛有教程但是没有教详细怎么做。 开整&#xff01; 做一个空白的场景。然后写一个load脚本。load主场景。 从代码可…

数据结构知识总结

二叉树 满二叉树 特性 所有叶子结点都集中在二叉树的最下面一层上&#xff0c;而且结点总数为&#xff1a;2^n-1 (n为层数 / 高度&#xff09; 完全二叉树 特性 若设二叉树的高度为h&#xff0c;除第h层外&#xff0c;其他各层的节点数都达到最大个数&#xff0c;第h层有…

Http中Host,Referer,Origin和Access-Control-Allow-Origin

Http中Host&#xff0c;Referer&#xff0c;Origin和Access-Control-Allow-Origin 文章目录 Http中Host&#xff0c;Referer&#xff0c;Origin和Access-Control-Allow-OriginHost定义特性作用 Referer定义特性作用 Origin定义特性作用 Access-Control-Allow-Origin定义特性作用…