当前位置: 首页 > news >正文

【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模块对内存进行可达性分析,从而获取泄漏内存的起始地址和空间大小。

相关文章:

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

文章目录 前言使用效果使用apiJNI的动态注册native方法动态注册 hook的实现android_dlopen_ext和dl_iterate_phdr naive监控的实现nativeGetLeakAllocs 总结 前言 Android的native泄漏怎么检测&#xff1f;下面通过研究开源项目KOOM来一探究竟。 使用效果 未触发泄漏前的日志…...

数据结构 二叉树 力扣例题AC——代码以及思路记录

LCR 175. 计算二叉树的深 某公司架构以二叉树形式记录&#xff0c;请返回该公司的层级数。 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系统启动流程中&#xff0c;系统启动主要经历了以下几个阶段&#xff1a; 引导加载程序&#xff08;Bootloader&#xff09;启动&#xff1a; 当设备加电后&#xff0c;首先运行的是ROM Bootloader&#xff0c;它负责验证操作系统映像的完整性、初始化基本硬件并加…...

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)

在之前的文章中&#xff0c;我们学习了如何创建堆积折线图&#xff0c;饼图以及较难的瀑布图并更改图标标题。想了解的朋友可以查看这篇文章。同时&#xff0c;希望我的文章能帮助到你&#xff0c;如果觉得我的文章写的不错&#xff0c;请留下你宝贵的点赞&#xff0c;谢谢。 …...

【理解机器学习算法】之Clustering算法(K-Means)

实现 K-means 聚类从零开始涉及几个关键步骤&#xff1a;初始化质心、将点分配给最近的质心、根据分配更新质心&#xff0c;以及重复这个过程直到收敛。这里是一个基本的 Python 实现&#xff1a; K-means 算法步骤&#xff1a; 初始化质心&#xff1a;从数据点中随机选择 k …...

Transformer的前世今生 day02(神经网络语言模型、词向量)

神经网络语言模型 使用神经网络的方法&#xff0c;去完成语言模型的两个问题&#xff0c;下图为两层感知机的神经网络语言模型&#xff1a; 假设词典V内有五个词&#xff1a;“判断”、“这个”、“词”、“的”、“词性”&#xff0c;且要输出P(w_next | “判断”、“这个”、…...

【Linux】多线程编程基础

&#x1f4bb;文章目录 &#x1f4c4;前言&#x1f33a;linux线程基础线程的概念线程的优缺点线程与进程的区别 线程的创建 &#x1f33b;linux线程冲突概念互斥锁函数介绍加锁的缺点 &#x1f4d3;总结 &#x1f4c4;前言 无论你是否为程序员&#xff0c;相信多线程这个词汇应…...

【地图】腾讯地图 - InfoWindow 自定义信息窗口内容时,内容 html 嵌套混乱问题

目录 需求描述问题问题代码页面展示 解决原因解决办法解决代码页面展示 代码汇总注 需求描述 腾讯地图上画点位&#xff0c;点击点位展示弹框信息 问题 问题代码 // 打开弹框 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框架没有内置静态文件服务&#xff0c;但可以使用gin.Static或gin.StaticFS中间件来提供静态文件服务。 效果图如下&#xff1a; 一、gin 框架加载 Html 模板文件的方法 方式1&#xff1a;加载单个或多个html文件&#xff0c;需要指明具体文件名 r.LoadHTMLFiles("vie…...

#鸿蒙生态创新中心#揭幕仪式在深圳湾科技生态园举行

3 月 19 日&#xff0c;#鸿蒙生态创新中心#揭幕仪式在深圳湾科技生态园举行。 现场&#xff0c;深圳市南山区人民政府副区长李志娜发布《2024 年南山区支持鸿蒙原生应用发展首批政策措施清单》&#xff0c;从加强鸿蒙原生应用供给能力、推动鸿蒙原生应用产业集聚、完善鸿蒙原生…...

flask 继续学习

group_by group_by是一种在数据库查询或数据处理中常用的操作&#xff0c;它用于将数据按照指定的列进行分组。通过group_by操作&#xff0c;可以将数据集按照某个列的值进行分类&#xff0c;然后对每个分类进行聚合计算或其他操作。 在SQL语言中&#xff0c;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实现)

考虑了光伏发电、风力发电、电池储能和负荷需求等因素&#xff0c;与主网相连不考虑向主网售电情况。 % 微电网日前优化调度示例代码% 定义时间步长&#xff08;例如&#xff0c;每小时&#xff09; time_steps 24;% 生成模拟数据&#xff1a;光伏发电量&#xff0c;风力发电…...

MATLAB环境下基于振动信号的轴承状态监测和故障诊断

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

流畅的 Python 第二版(GPT 重译)(十二)

第五部分&#xff1a;元编程 第二十二章&#xff1a;动态属性和属性 属性的关键重要性在于&#xff0c;它们的存在使得将公共数据属性作为类的公共接口的一部分完全安全且确实可取。 Martelli、Ravenscroft 和 Holden&#xff0c;“为什么属性很重要” 在 Python 中&#xff0…...

【Python 48小时速成 2】关键字

文章目录 01. and &#xff1a;逻辑运算符&#xff0c;表示逻辑与操作。02. exec &#xff1a;内置函数&#xff0c;用于执行存储在字符串或文件中的 Python 代码。03. not &#xff1a;逻辑运算符&#xff0c;表示逻辑非操作。04. assert &#xff1a;断言语句&#xff0c;用于…...

小程序socket 全局代码

在微信小程序中&#xff0c;为了实现在整个应用范围内共享一个WebSocket连接&#xff0c;通常会将WebSocket的创建、打开、关闭以及消息收发等功能封装在一个全局模块中&#xff0c;然后在各个需要使用WebSocket功能的页面中引入并调用这个模块的方法。以下是一个简化的全局Web…...

数据挖掘|数据集成|基于Python的数据集成关键问题处理

数据挖掘|数据集成|基于Python的数据集成关键问题处理 1. 实体识别2. 数据冗余与相关性分析3. 去除重复记录4. 数据值冲突的检测与处理5. 基于Python的数据集成5.1 merge()方法5.2 Concat()方法 数据集成是把来自多个数据库或文件等不同数据源的数据整合成一致的数据存储。其中…...

深入浅出Asp.Net Core MVC应用开发系列-AspNetCore中的日志记录

ASP.NET Core 是一个跨平台的开源框架&#xff0c;用于在 Windows、macOS 或 Linux 上生成基于云的新式 Web 应用。 ASP.NET Core 中的日志记录 .NET 通过 ILogger API 支持高性能结构化日志记录&#xff0c;以帮助监视应用程序行为和诊断问题。 可以通过配置不同的记录提供程…...

Android Wi-Fi 连接失败日志分析

1. Android wifi 关键日志总结 (1) Wi-Fi 断开 (CTRL-EVENT-DISCONNECTED reason3) 日志相关部分&#xff1a; 06-05 10:48:40.987 943 943 I wpa_supplicant: wlan0: CTRL-EVENT-DISCONNECTED bssid44:9b:c1:57:a8:90 reason3 locally_generated1解析&#xff1a; CTR…...

CTF show Web 红包题第六弹

提示 1.不是SQL注入 2.需要找关键源码 思路 进入页面发现是一个登录框&#xff0c;很难让人不联想到SQL注入&#xff0c;但提示都说了不是SQL注入&#xff0c;所以就不往这方面想了 ​ 先查看一下网页源码&#xff0c;发现一段JavaScript代码&#xff0c;有一个关键类ctfs…...

【WiFi帧结构】

文章目录 帧结构MAC头部管理帧 帧结构 Wi-Fi的帧分为三部分组成&#xff1a;MAC头部frame bodyFCS&#xff0c;其中MAC是固定格式的&#xff0c;frame body是可变长度。 MAC头部有frame control&#xff0c;duration&#xff0c;address1&#xff0c;address2&#xff0c;addre…...

智慧工地云平台源码,基于微服务架构+Java+Spring Cloud +UniApp +MySql

智慧工地管理云平台系统&#xff0c;智慧工地全套源码&#xff0c;java版智慧工地源码&#xff0c;支持PC端、大屏端、移动端。 智慧工地聚焦建筑行业的市场需求&#xff0c;提供“平台网络终端”的整体解决方案&#xff0c;提供劳务管理、视频管理、智能监测、绿色施工、安全管…...

YSYX学习记录(八)

C语言&#xff0c;练习0&#xff1a; 先创建一个文件夹&#xff0c;我用的是物理机&#xff1a; 安装build-essential 练习1&#xff1a; 我注释掉了 #include <stdio.h> 出现下面错误 在你的文本编辑器中打开ex1文件&#xff0c;随机修改或删除一部分&#xff0c;之后…...

DBAPI如何优雅的获取单条数据

API如何优雅的获取单条数据 案例一 对于查询类API&#xff0c;查询的是单条数据&#xff0c;比如根据主键ID查询用户信息&#xff0c;sql如下&#xff1a; select id, name, age from user where id #{id}API默认返回的数据格式是多条的&#xff0c;如下&#xff1a; {&qu…...

高防服务器能够抵御哪些网络攻击呢?

高防服务器作为一种有着高度防御能力的服务器&#xff0c;可以帮助网站应对分布式拒绝服务攻击&#xff0c;有效识别和清理一些恶意的网络流量&#xff0c;为用户提供安全且稳定的网络环境&#xff0c;那么&#xff0c;高防服务器一般都可以抵御哪些网络攻击呢&#xff1f;下面…...

Java面试专项一-准备篇

一、企业简历筛选规则 一般企业的简历筛选流程&#xff1a;首先由HR先筛选一部分简历后&#xff0c;在将简历给到对应的项目负责人后再进行下一步的操作。 HR如何筛选简历 例如&#xff1a;Boss直聘&#xff08;招聘方平台&#xff09; 直接按照条件进行筛选 例如&#xff1a…...

Map相关知识

数据结构 二叉树 二叉树&#xff0c;顾名思义&#xff0c;每个节点最多有两个“叉”&#xff0c;也就是两个子节点&#xff0c;分别是左子 节点和右子节点。不过&#xff0c;二叉树并不要求每个节点都有两个子节点&#xff0c;有的节点只 有左子节点&#xff0c;有的节点只有…...