【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> ®ister_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模块对内存进行可达性分析,从而获取泄漏内存的起始地址和空间大小。
相关文章:
【Android 内存优化】 native内存泄漏监控方案源码分析
文章目录 前言使用效果使用apiJNI的动态注册native方法动态注册 hook的实现android_dlopen_ext和dl_iterate_phdr naive监控的实现nativeGetLeakAllocs 总结 前言 Android的native泄漏怎么检测?下面通过研究开源项目KOOM来一探究竟。 使用效果 未触发泄漏前的日志…...

数据结构 二叉树 力扣例题AC——代码以及思路记录
LCR 175. 计算二叉树的深 某公司架构以二叉树形式记录,请返回该公司的层级数。 AC int calculateDepth(struct TreeNode* root) {if (root NULL){return 0;}else{return 1 fmax(calculateDepth(root->left), calculateDepth(root->right));} } 代码思路 …...
Android 11系统启动流程
在Android 11系统启动流程中,系统启动主要经历了以下几个阶段: 引导加载程序(Bootloader)启动: 当设备加电后,首先运行的是ROM Bootloader,它负责验证操作系统映像的完整性、初始化基本硬件并加…...

python 爬取杭州小区挂牌均价
下载chrome驱动 通过chrome浏览器的 设置-帮助-关于Google Chrome 查看你所使用的Chrome版本 驱动可以从这两个地方找: 【推荐】https://storage.googleapis.com/chrome-for-testing-publichttp://npm.taobao.org/mirrors/chromedriver import zipfile import os import r…...

数据可视化-ECharts Html项目实战(3)
在之前的文章中,我们学习了如何创建堆积折线图,饼图以及较难的瀑布图并更改图标标题。想了解的朋友可以查看这篇文章。同时,希望我的文章能帮助到你,如果觉得我的文章写的不错,请留下你宝贵的点赞,谢谢。 …...

【理解机器学习算法】之Clustering算法(K-Means)
实现 K-means 聚类从零开始涉及几个关键步骤:初始化质心、将点分配给最近的质心、根据分配更新质心,以及重复这个过程直到收敛。这里是一个基本的 Python 实现: K-means 算法步骤: 初始化质心:从数据点中随机选择 k …...

Transformer的前世今生 day02(神经网络语言模型、词向量)
神经网络语言模型 使用神经网络的方法,去完成语言模型的两个问题,下图为两层感知机的神经网络语言模型: 假设词典V内有五个词:“判断”、“这个”、“词”、“的”、“词性”,且要输出P(w_next | “判断”、“这个”、…...

【Linux】多线程编程基础
💻文章目录 📄前言🌺linux线程基础线程的概念线程的优缺点线程与进程的区别 线程的创建 🌻linux线程冲突概念互斥锁函数介绍加锁的缺点 📓总结 📄前言 无论你是否为程序员,相信多线程这个词汇应…...

【地图】腾讯地图 - InfoWindow 自定义信息窗口内容时,内容 html 嵌套混乱问题
目录 需求描述问题问题代码页面展示 解决原因解决办法解决代码页面展示 代码汇总注 需求描述 腾讯地图上画点位,点击点位展示弹框信息 问题 问题代码 // 打开弹框 openInfoWindow(position, content) {this.infoWindow new TMap.InfoWindow({map: this.map,posit…...
Vue3、element-plus和Vue2、elementUI的一些转换
插槽 Vue3<template #default"scope"></template> <template #footer></template>Vue2<template slot-scope"scope"></template> <template slot"footer"></template>JS定义 Vue3 <script…...

Go语言gin框架中加载html/css/js等静态资源
Gin框架没有内置静态文件服务,但可以使用gin.Static或gin.StaticFS中间件来提供静态文件服务。 效果图如下: 一、gin 框架加载 Html 模板文件的方法 方式1:加载单个或多个html文件,需要指明具体文件名 r.LoadHTMLFiles("vie…...

#鸿蒙生态创新中心#揭幕仪式在深圳湾科技生态园举行
3 月 19 日,#鸿蒙生态创新中心#揭幕仪式在深圳湾科技生态园举行。 现场,深圳市南山区人民政府副区长李志娜发布《2024 年南山区支持鸿蒙原生应用发展首批政策措施清单》,从加强鸿蒙原生应用供给能力、推动鸿蒙原生应用产业集聚、完善鸿蒙原生…...
flask 继续学习
group_by group_by是一种在数据库查询或数据处理中常用的操作,它用于将数据按照指定的列进行分组。通过group_by操作,可以将数据集按照某个列的值进行分类,然后对每个分类进行聚合计算或其他操作。 在SQL语言中,group_by通常与聚…...

DockerFile遇到的坑
CMD 命令的坑 dockerfile 中的 CMD 命令在docker run -it 不会执行 CMD 命令。 FROM golang WORKDIR / COPY . ./All-in-one CMD ["/bin/sh","-c","touch /kkk.txt && ls -la"] RUN echo alias ll"ls -la" > ~/.bashrc(不…...
并网型风光储微电网日前优化调度(MATLAB实现)
考虑了光伏发电、风力发电、电池储能和负荷需求等因素,与主网相连不考虑向主网售电情况。 % 微电网日前优化调度示例代码% 定义时间步长(例如,每小时) time_steps 24;% 生成模拟数据:光伏发电量,风力发电…...

MATLAB环境下基于振动信号的轴承状态监测和故障诊断
故障预测与健康管理PHM分为故障预测和健康管理与维修两部分,PHM首先借助传感器采集关键零部件的运行状态数据,如振动信号、温度图像、电流电压信号、声音信号及油液分析等,提取设备的运行监测指标,进而实现对设备关键零部件运行状…...

流畅的 Python 第二版(GPT 重译)(十二)
第五部分:元编程 第二十二章:动态属性和属性 属性的关键重要性在于,它们的存在使得将公共数据属性作为类的公共接口的一部分完全安全且确实可取。 Martelli、Ravenscroft 和 Holden,“为什么属性很重要” 在 Python 中࿰…...
【Python 48小时速成 2】关键字
文章目录 01. and :逻辑运算符,表示逻辑与操作。02. exec :内置函数,用于执行存储在字符串或文件中的 Python 代码。03. not :逻辑运算符,表示逻辑非操作。04. assert :断言语句,用于…...
小程序socket 全局代码
在微信小程序中,为了实现在整个应用范围内共享一个WebSocket连接,通常会将WebSocket的创建、打开、关闭以及消息收发等功能封装在一个全局模块中,然后在各个需要使用WebSocket功能的页面中引入并调用这个模块的方法。以下是一个简化的全局Web…...
数据挖掘|数据集成|基于Python的数据集成关键问题处理
数据挖掘|数据集成|基于Python的数据集成关键问题处理 1. 实体识别2. 数据冗余与相关性分析3. 去除重复记录4. 数据值冲突的检测与处理5. 基于Python的数据集成5.1 merge()方法5.2 Concat()方法 数据集成是把来自多个数据库或文件等不同数据源的数据整合成一致的数据存储。其中…...

接口测试中缓存处理策略
在接口测试中,缓存处理策略是一个关键环节,直接影响测试结果的准确性和可靠性。合理的缓存处理策略能够确保测试环境的一致性,避免因缓存数据导致的测试偏差。以下是接口测试中常见的缓存处理策略及其详细说明: 一、缓存处理的核…...

Mybatis逆向工程,动态创建实体类、条件扩展类、Mapper接口、Mapper.xml映射文件
今天呢,博主的学习进度也是步入了Java Mybatis 框架,目前正在逐步杨帆旗航。 那么接下来就给大家出一期有关 Mybatis 逆向工程的教学,希望能对大家有所帮助,也特别欢迎大家指点不足之处,小生很乐意接受正确的建议&…...

CMake基础:构建流程详解
目录 1.CMake构建过程的基本流程 2.CMake构建的具体步骤 2.1.创建构建目录 2.2.使用 CMake 生成构建文件 2.3.编译和构建 2.4.清理构建文件 2.5.重新配置和构建 3.跨平台构建示例 4.工具链与交叉编译 5.CMake构建后的项目结构解析 5.1.CMake构建后的目录结构 5.2.构…...

Android15默认授权浮窗权限
我们经常有那种需求,客户需要定制的apk集成在ROM中,并且默认授予其【显示在其他应用的上层】权限,也就是我们常说的浮窗权限,那么我们就可以通过以下方法在wms、ams等系统服务的systemReady()方法中调用即可实现预置应用默认授权浮…...

【开发技术】.Net使用FFmpeg视频特定帧上绘制内容
目录 一、目的 二、解决方案 2.1 什么是FFmpeg 2.2 FFmpeg主要功能 2.3 使用Xabe.FFmpeg调用FFmpeg功能 2.4 使用 FFmpeg 的 drawbox 滤镜来绘制 ROI 三、总结 一、目的 当前市场上有很多目标检测智能识别的相关算法,当前调用一个医疗行业的AI识别算法后返回…...
Spring是如何解决Bean的循环依赖:三级缓存机制
1、什么是 Bean 的循环依赖 在 Spring框架中,Bean 的循环依赖是指多个 Bean 之间互相持有对方引用,形成闭环依赖关系的现象。 多个 Bean 的依赖关系构成环形链路,例如: 双向依赖:Bean A 依赖 Bean B,同时 Bean B 也依赖 Bean A(A↔B)。链条循环: Bean A → Bean…...

AI+无人机如何守护濒危物种?YOLOv8实现95%精准识别
【导读】 野生动物监测在理解和保护生态系统中发挥着至关重要的作用。然而,传统的野生动物观察方法往往耗时耗力、成本高昂且范围有限。无人机的出现为野生动物监测提供了有前景的替代方案,能够实现大范围覆盖并远程采集数据。尽管具备这些优势…...

Vue ③-生命周期 || 脚手架
生命周期 思考:什么时候可以发送初始化渲染请求?(越早越好) 什么时候可以开始操作dom?(至少dom得渲染出来) Vue生命周期: 一个Vue实例从 创建 到 销毁 的整个过程。 生命周期四个…...

协议转换利器,profinet转ethercat网关的两大派系,各有千秋
随着工业以太网的发展,其高效、便捷、协议开放、易于冗余等诸多优点,被越来越多的工业现场所采用。西门子SIMATIC S7-1200/1500系列PLC集成有Profinet接口,具有实时性、开放性,使用TCP/IP和IT标准,符合基于工业以太网的…...
小木的算法日记-多叉树的递归/层序遍历
🌲 从二叉树到森林:一文彻底搞懂多叉树遍历的艺术 🚀 引言 你好,未来的算法大神! 在数据结构的世界里,“树”无疑是最核心、最迷人的概念之一。我们中的大多数人都是从 二叉树 开始入门的,它…...