在验证OpenCV remap 函数时,有一个参数的含义是复制边缘像素(BORDER_REPLICATE),也就是在无效像素区域重复复制有效像素的边缘,看起来有点像拉丝一样的效果。恰巧有一份 C++ 的代码用的就是这个参数,我在将它写成python 版本时却发现得不到一样的结果。
C++ 结果:
python 版本结果:
可以看到左上边界是黑边,没有填充颜色。 关键代码:
output_image = cv2.remap(input_image, map_x, map_y, cv2.INTER_CUBIC, cv2.BORDER_REPLICATE)
起初我以为是 map 表里面元素的问题,在几乎将两个 map 表调成一样之后发现效果还是没变。直到我看到了另一份代码可以正确生成复制像素的效果我发现只要指定参数名就可以了!
output_image = cv2.remap(input_image, map_x, map_y, interpolation=cv2.INTER_CUBIC, borderMode=cv2.BORDER_REPLICATE)
但是不指定参数名程序运行不会报错,我也就一直没发现问题。 python 代码指定参数名之后的结果:
]]>内存泄漏是指程序分配了一块内存(通常是动态分配的堆内存),但在不再需要这块内存的情况下未将其释放。内存泄漏会导致程序浪费系统内存资源,持续的内存泄漏还导致系统内存的逐渐耗尽,最终导致程序或系统崩溃。
常驻内存(Resident Set)是指进程在运行期间占用的内存大小,包括进程使用的代码、数据和其他资源。常驻内存是进程在运行期间一直驻留在内存中的部分,即使在进程不活动时也不会被释放。 常驻内存通常不会带来显著的负面影响。
下图是源码与 ELF(可执行可链接) 文件以及运行起来后内存布局的简易映射关系图。 程序中的初始化全局变量和局部静态变量被编译到 .data,未初始化的全局变量和局部静态变量编译后放在 .bss 段,代码主体和函数主题存放在 .text 段,ELF 文件内实际有很多段(参考《程序员的自我修养-链接,装载与库》第三章)。 当程序运行时,会将 ELF 文件加载到内存。不同的段会加载到内存布局中的不同位置,其中 heap 这部分就是程序员手动去动态申请和释放内存的部分。当程序员用 malloc 函数申请了一块内存,使用完之后却没有 free 它的时候,就会发生内存泄漏。内存泄漏得越多,进程中可以使用内存的空间就越少,时间长了就会导致系统响应慢,甚至程序崩溃。
在 Android 系统上通常可以用 dumpsys meminfo 命令查看进程的内存使用数据,重复 dump 后从数据的变化情况来大致判断是否有内存泄漏。
也可以借助python 或者其他一些工具将数据可视化方便查看数据变化趋势。 但这只能大致的给你展示数据变化的趋势,而非直接明白的告诉你是否发生了内存泄漏。因此我们需要更精确的工具来检测是否也有内存泄漏。
本节我们将依次介绍 Malloc Debug, libmemunreachable, Asan, HwASan, MTE, Heapprofd, Memcheck(Valigrind) 内存泄漏检测工具(https://source.android.com/docs/core/tests/debug/native-memory?hl=zh-cn)
Malloc Debug 是一种调试本机内存问题的方法。 它可以帮助检测内存损坏、内存泄漏和释放后使用问题。 Malloc Debug 通过对常规的 allocation 函数包装了一层来记录和分析内存的申请和释放。这些函数包括:malloc, free, calloc, realloc, posix_memalign, memalign, aligned_alloc, malloc_usable_size
运行程序前的设置
adb shell setprop libc.debug.malloc.options "\"backtrace guard leak_track backtrace_dump_on_exit backtrace_dump_prefix=/sdcard/heap"\"
adb shell setprop libc.debug.malloc.program xxx(进程名)
参数介绍:
执行待测试程序
E malloc_debug: +++ memtest leaked block of size 48 at 0x7a4a6a42e0 (leak 1 of 2)
E malloc_debug: Backtrace at time of allocation:
E malloc_debug: #00 pc 000000000004461c /apex/com.android.runtime/lib64/bionic/libc.so (malloc+76)
E malloc_debug: #01 pc 00000000000c83e8 /apex/com.android.runtime/lib64/bionic/libc.so (__register_atfork+40)
E malloc_debug: #02 pc 000000000005460c /apex/com.android.runtime/lib64/bionic/libc.so
E malloc_debug: #03 pc 00000000000613a0 /apex/com.android.runtime/bin/linker64
E malloc_debug: #04 pc 0000000000061144 /apex/com.android.runtime/bin/linker64
E malloc_debug: #05 pc 0000000000061144 /apex/com.android.runtime/bin/linker64
E malloc_debug: #06 pc 00000000000d5f14 /apex/com.android.runtime/bin/linker64
E malloc_debug: #07 pc 00000000000d4e0c /apex/com.android.runtime/bin/linker64
E malloc_debug: #08 pc 0000000000064004 /apex/com.android.runtime/bin/linker64
E malloc_debug: +++ memtest leaked block of size 20 at 0x793a6ae9a0 (leak 2 of 2)
E malloc_debug: Backtrace at time of allocation:
E malloc_debug: #00 pc 000000000004461c /apex/com.android.runtime/lib64/bionic/libc.so (malloc+76)
E malloc_debug: #01 pc 00000000000100b8 /data/local/tmp/memtest/memtest
E malloc_debug: #02 pc 00000000000546e8 /apex/com.android.runtime/lib64/bionic/libc.so (__libc_init+104)
E malloc_debug: Dumping to file: /sdcard/heap.19748.exit.txt
注意报告中并不是所有的 leak 都是真正的内存泄漏,有些可能是常驻内存,开发者需要自己判断。
还需注意dump 路径要有写权限
很多时候在线运行环境下so 是无符号的程序,我们需要解析 dump 文件定位代码行号
python3 native_heapdump_viewer.py --symbols ./symboldir/ ./heap.4169.exit.txt --html > memtest4169.html
–symbols 指定的是符号库/程序的路径,子目录的路径必须要在手机上的路径一致。比如可执行程序在手机里的路径是/vendor/bin/memtest,那解释时它的带符号的程序路径上需要是 ./symboldir/vendor/bin/memtest 检测出来的并不是都是泄漏,一部分是属于常驻内存,尤其对于在线程序,我们需要将程序运行不同的次数,抓出不同的log来做对比,找出真正增长的部分。
Android 的 libmemunreachable 是一个零开销的本地内存泄漏检测器。 它会在触发内存检测的时候遍历进程内存,同时将任何不可访问的块报告为泄漏。
设置属性
adb root
adb shell setprop libc.debug.malloc.program app_process
adb shell setprop wrap.[process] "\$\@“
adb shell setprop libc.debug.malloc.options backtrace=4
参数
重启应用,执行 dumpsys -t 600 meminfo –unreachable [process].(自测没有 dump 出预期结果)。下面是一个带有内存问题的输出结果。
Unreachable memory
24 bytes in 2 unreachable allocations
ABI: 'arm64'
24 bytes unreachable at 71d37787d0
first 20 bytes of contents:
71d37787d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
71d37787e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
24 bytes unreachable at 71d37797d0
first 20 bytes of contents:
71d37797d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
71d37797e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
官方提供4个接口来检测内存 C interface
C++ interface
核心函数是 GetUnreachableMemory() 其他三个函数内部都会调用此函数。 在使用添加代码的方式打印时,需要在编译代码时需要将 libmemunreachable.so 添加到动态依赖,libmemunreachable.so 文件可以在手机 /system/lib64/libmemunreachable.so 获取。
例子: 以下是一个包含内存泄漏的例子,在f 函数中申请了x, y 两块内存,在函数返回前x 被释放,y 赋值后没有被释放。
#include "./memunreachable.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
using namespace android;
void f(void);
void f(void) {
printf("[memtest] function f\n");
int* x = (int*)malloc(10 * sizeof(int));
x[0] = 0;
int* y = (int*)malloc(5 * sizeof(int));
y[0] = 0;
y[1] = 1;
y[2] = 2;
y[3] = 3;
y[4] = 4;
free(x);
}
int main(void) {
printf("[memtest] hello main\n");
f();
// C interface
printf("LogUnreachableMemory()\n");
LogUnreachableMemory(true, 100);
return 0;
}
adb log 输出(考虑排版省去时间戳) log 里显示有一个 20 bytes 的内存泄漏,20 正是5 个 int 的大小,对应申请但没有释放的 y 地址的内存。
// 新建 Collection process
31232 31231 I libmemunreachable: collecting thread info for process 31231...
31232 31231 I libmemunreachable: collection thread done
// fork 进程运行 sweeping process
31233 31233 I libmemunreachable: searching process 31231 for allocations
31233 31233 I libmemunreachable: searching done
31233 31233 I libmemunreachable: sweeping process 31231 for unreachable memory
31233 31233 I libmemunreachable: sweeping done
31233 31233 I libmemunreachable: folding related leaks
31233 31233 I libmemunreachable: folding done
// 回到 Original process 接收检测结果
31231 31231 I libmemunreachable: unreachable memory detection done
31231 31231 E libmemunreachable: 20 bytes in 1 allocation unreachable out of 1260 bytes in 7 allocations
31231 31231 E libmemunreachable: 20 bytes unreachable at 7a03454400
31231 31231 E libmemunreachable: contents:
31231 31231 E libmemunreachable: 7a03454400: 00 00 00 00 01 00 00 00 02 00 00 00 03 00 00 00 ................
31231 31231 E libmemunreachable: 7a03454410: 04 00 00 00 ....
31231 31231 E libmemunreachable: #00 pc 000000000003e238 /apex/com.android.runtime/lib64/bionic/libc.so (malloc+84)
31231 31231 E libmemunreachable: #01 pc 00000000000100b8 /data/local/tmp/memtest/memtest_libmemunreachable
31231 31231 E libmemunreachable: #02 pc 000000000004aa48 /apex/com.android.runtime/lib64/bionic/libc.so (__libc_init+100)
调用 bool LogUnreachableMemory(bool log_contents, size_t limit) 时,log_contents 传 true 会打印泄露地址的内容,也就是 contents 对应的两行内容。之后可以用 address2line 解析行号。
ASan
HWASan
MTE
Heapprofd
sudo apt-get install valgrind
使用 memcheck 的基本方法
$ valgrind --leak-check=yes ./memtest_origin
==19517== Memcheck, a memory error detector
==19517== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==19517== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==19517== Command: ./memtest_origin
==19517==
[memtest] hello main
[memtest] function f
==19517==
==19517== HEAP SUMMARY:
==19517== in use at exit: 20 bytes in 1 blocks
==19517== total heap usage: 3 allocs, 2 frees, 1,084 bytes allocated
==19517==
==19517== 20 bytes in 1 blocks are definitely lost in loss record 1 of 1
==19517== at 0x4C31B0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==19517== by 0x1086FF: f() (memtest_origin.cc:9)
==19517== by 0x108731: main (memtest_origin.cc:16)
==19517==
==19517== LEAK SUMMARY:
==19517== definitely lost: 20 bytes in 1 blocks
==19517== indirectly lost: 0 bytes in 0 blocks
==19517== possibly lost: 0 bytes in 0 blocks
==19517== still reachable: 0 bytes in 0 blocks
==19517== suppressed: 0 bytes in 0 blocks
==19517==
==19517== For counts of detected and suppressed errors, rerun with: -v
==19517== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
分为初始化和内存泄漏检测两个阶段介绍。
整体流程如下图
MaybeInitGwpAsanFromLibc(globals);
#if defined(USE_SCUDO) __libc_shared_globals()->scudo_stack_depot = __scudo_get_stack_depot_addr(); __libc_shared_globals()->scudo_region_info = __scudo_get_region_info_addr(); __libc_shared_globals()->scudo_ring_buffer = __scudo_get_ring_buffer_addr(); __libc_shared_globals()->scudo_ring_buffer_size = __scudo_get_ring_buffer_size(); #endif
// Prefer malloc debug since it existed first and is a more complete // malloc interceptor than the hooks. bool hook_installed = false; if (CheckLoadMallocDebug(&options)) { hook_installed = InstallHooks(globals, options, kDebugPrefix, kDebugSharedLib); } else if (CheckLoadMallocHooks(&options)) { hook_installed = InstallHooks(globals, options, kHooksPrefix, kHooksSharedLib); }
if (!hook_installed) { if (HeapprofdShouldLoad()) { HeapprofdInstallHooksAtInit(globals); } } else { // Record the fact that incompatible hooks are active, to skip any later // heapprofd signal handler invocations. HeapprofdRememberHookConflict(); } }
2. CheckLoadMallocDebug() 检查属性是否满足加载 lib_malloc_debug.so 的条件,检查的属性正是前面提到的两个 android prop 属性。
```C++
// malloc_common_dynamic.cpp
static bool CheckLoadMallocDebug(char** options) {
// If kDebugMallocEnvOptions is set then it overrides the system properties.
char* env = getenv(kDebugEnvOptions);
if (env == nullptr || env[0] == '\0') {
if (__system_property_get(kDebugPropertyOptions, *options) == 0 || *options[0] == '\0') {
return false;
}
// Check to see if only a specific program should have debug malloc enabled.
char program[PROP_VALUE_MAX];
if (__system_property_get(kDebugPropertyProgram, program) != 0 &&
strstr(getprogname(), program) == nullptr) {
return false;
}
} else {
*options = env;
}
return true;
}
if (!FinishInstallHooks(globals, options, prefix)) { dlclose(impl_handle); return false; } return true; }
4. LoadSharedLibrary() 函数内部 dlopen lib_malloc_debug.so
5. 之后调用 InitSharedLibrary() 查找如下names 数组中的 symbol,将查找到的 symbol 保存在全局数组变量 gfunctions 中。注意查找的函数都会加上 debug_ 前缀。
```C++
// malloc_common_dynamic.cpp
bool InitSharedLibrary(void* impl_handle, const char* shared_lib, const char* prefix, MallocDispatch* dispatch_table) {
static constexpr const char* names[] = {
"initialize",
"finalize",
"get_malloc_leak_info",
"free_malloc_leak_info",
"malloc_backtrace",
"write_malloc_leak_info",
};
for (size_t i = 0; i < FUNC_LAST; i++) {
char symbol[128];
snprintf(symbol, sizeof(symbol), "%s_%s", prefix, names[i]);
gFunctions[i] = dlsym(impl_handle, symbol);
if (gFunctions[i] == nullptr) {
error_log("%s: %s routine not found in %s", getprogname(), symbol, shared_lib);
ClearGlobalFunctions();
return false;
}
}
if (!InitMallocFunctions(impl_handle, dispatch_table, prefix)) {
ClearGlobalFunctions();
return false;
}
return true;
}
// malloc_common_dynamic.cpp
static bool InitMallocFunctions(void* impl_handler, MallocDispatch* table, const char* prefix) {
if (!InitMallocFunction<MallocFree>(impl_handler, &table->free, prefix, "free")) {
return false;
}
if (!InitMallocFunction<MallocCalloc>(impl_handler, &table->calloc, prefix, "calloc")) {
return false;
}
if (!InitMallocFunction<MallocMallinfo>(impl_handler, &table->mallinfo, prefix, "mallinfo")) {
return false;
}
if (!InitMallocFunction<MallocMallopt>(impl_handler, &table->mallopt, prefix, "mallopt")) {
return false;
}
if (!InitMallocFunction<MallocMalloc>(impl_handler, &table->malloc, prefix, "malloc")) {
return false;
}
if (!InitMallocFunction<MallocMallocInfo>(impl_handler, &table->malloc_info, prefix,
"malloc_info")) {
return false;
}
if (!InitMallocFunction<MallocMallocUsableSize>(impl_handler, &table->malloc_usable_size, prefix,
"malloc_usable_size")) {
return false;
}
...
}
InitMallocFunction() 内部作的还是去获取 malloc_debug 内的函数符号。
// malloc_common_dynamic.cpp
template<typename FunctionType>
static bool InitMallocFunction(void* malloc_impl_handler, FunctionType* func, const char* prefix, const char* suffix) {
char symbol[128];
snprintf(symbol, sizeof(symbol), "%s_%s", prefix, suffix);
*func = reinterpret_cast<FunctionType>(dlsym(malloc_impl_handler, symbol));
if (*func == nullptr) {
error_log("%s: dlsym(\"%s\") failed", getprogname(), symbol);
return false;
}
return true;
}
// If GWP-ASan was initialised, we should use it as the dispatch table for // heapprofd/malloc_debug/malloc_debug. const MallocDispatch* prev_dispatch = GetDefaultDispatchTable(); if (prev_dispatch == nullptr) { prev_dispatch = NativeAllocatorDispatch(); }
if (!init_func(prev_dispatch, &gZygoteChild, options)) { error_log(“%s: failed to enable malloc %s”, getprogname(), prefix); ClearGlobalFunctions(); return false; }
// Do a pointer swap so that all of the functions become valid at once to // avoid any initialization order problems. atomic_store(&globals->default_dispatch_table, &globals->malloc_dispatch_table); if (!MallocLimitInstalled()) { atomic_store(&globals->current_dispatch_table, &globals->malloc_dispatch_table); }
// Use atexit to trigger the cleanup function. This avoids a problem // where another atexit function is used to cleanup allocated memory, // but the finalize function was already called. This particular error // seems to be triggered by a zygote spawned process calling exit. int ret_value = __cxa_atexit(MallocFiniImpl, nullptr, nullptr); if (ret_value != 0) { // We don’t consider this a fatal error. warning_log(“failed to set atexit cleanup function: %d”, ret_value); } return true; }
### 申请/释放内存阶段
其内存泄漏的检测原理可以简单概括为:维护一个记录内存申请和释放的列表,每当申请内存时列表成员+1,内存释放时列表成员-1,程序退出时列表中还存在的成员即内存泄漏的成员。
在调用 malloc 函数时,内部判断如果 dispatch_table 不为空,调用 dispatch_table->malloc(bytes),否则调用默认malloc 函数。dispatch_table 里面存储的是 “debug_”前缀的lib_malloc_debug.so 里的函数。
```C++
// malloc_common.cpp
extern "C" void* malloc(size_t bytes) {
auto dispatch_table = GetDispatchTable();
void *result;
if (__predict_false(dispatch_table != nullptr)) {
result = dispatch_table->malloc(bytes);
} else {
result = Malloc(malloc)(bytes);
}
if (__predict_false(result == nullptr)) {
warning_log("malloc(%zu) failed: returning null pointer", bytes);
return nullptr;
}
return MaybeTagPointer(result);
}
在 malloc debug 的 debug_malloc() 函数内,内存实际在 InternalMalloc 里申请,并且会根据初始化时配置的选项选择性开启功能。
// malloc_debug.cpp
void* debug_malloc(size_t size) {
Unreachable::CheckIfRequested(g_debug->config());
if (DebugCallsDisabled()) {
return g_dispatch->malloc(size);
}
ScopedConcurrentLock lock;
ScopedDisableDebugCalls disable;
ScopedBacktraceSignalBlocker blocked;
TimedResult result = InternalMalloc(size);
if (g_debug->config().options() & RECORD_ALLOCS) {
g_debug->record->AddEntry(new MallocEntry(result.getValue<void*>(), size,
result.GetStartTimeNS(), result.GetEndTimeNS()));
}
return result.getValue<void*>();
}
InternalMalloc() 实现,可以看到下面代码中有多处根据 g_debug 的成员函数判断要执行的操作。
// malloc_debug.cpp
static TimedResult InternalMalloc(size_t size) {
if ((g_debug->config().options() & BACKTRACE) && g_debug->pointer->ShouldDumpAndReset()) {
debug_dump_heap(android::base::StringPrintf(
"%s.%d.txt", g_debug->config().backtrace_dump_prefix().c_str(), getpid())
.c_str());
}
if (size == 0) {
size = 1;
}
TimedResult result;
size_t real_size = size + g_debug->extra_bytes();
if (real_size < size) {
// Overflow.
errno = ENOMEM;
result.setValue<void*>(nullptr);
return result;
}
if (size > PointerInfoType::MaxSize()) {
errno = ENOMEM;
result.setValue<void*>(nullptr);
return result;
}
if (g_debug->HeaderEnabled()) {
result = TCALL(memalign, MINIMUM_ALIGNMENT_BYTES, real_size);
Header* header = reinterpret_cast<Header*>(result.getValue<void*>());
if (header == nullptr) {
return result;
}
result.setValue<void*>(InitHeader(header, header, size));
} else {
result = TCALL(malloc, real_size);
}
void* pointer = result.getValue<void*>();
if (pointer != nullptr) {
if (g_debug->TrackPointers()) {
PointerData::Add(pointer, size);
}
if (g_debug->config().options() & FILL_ON_ALLOC) {
size_t bytes = InternalMallocUsableSize(pointer);
size_t fill_bytes = g_debug->config().fill_on_alloc_bytes();
bytes = (bytes < fill_bytes) ? bytes : fill_bytes;
memset(pointer, g_debug->config().fill_alloc_value(), bytes);
}
}
return result;
}
在 PointData 里维护了一个全局的 pointers_ map。每次申请内存时调用 Add 函数增加 pointers_ 成员,释放内存时调用 Remove 函数移除 pointers_ 成员。申请内存时调用的Add 函数见上面的代码段PointerData::Add(pointer, size);
,释放内存时PointerData::Remove(pointer);
。
// malloc_debug.cpp
static TimedResult InternalFree(void* pointer) {
...
if (g_debug->TrackPointers()) {
PointerData::Remove(pointer);
}
...
return result;
}
退出时调用 debug_finalize() 打印内存泄漏并保存dump 文件 调用 LogLeaks() 将内存泄漏信息在log 打印,将dump 文件写入手机存储。
// malloc_debug.cpp
void debug_finalize() {
if (g_debug == nullptr) {
return;
}
// Make sure that there are no other threads doing debug allocations
// before we kill everything.
ScopedConcurrentLock::BlockAllOperations();
// Turn off capturing allocations calls.
DebugDisableSet(true);
if (g_debug->config().options() & FREE_TRACK) {
PointerData::VerifyAllFreed();
}
if (g_debug->config().options() & LEAK_TRACK) {
PointerData::LogLeaks();
}
if ((g_debug->config().options() & BACKTRACE) && g_debug->config().backtrace_dump_on_exit()) {
debug_dump_heap(android::base::StringPrintf("%s.%d.exit.txt",
g_debug->config().backtrace_dump_prefix().c_str(),
getpid()).c_str());
}
backtrace_shutdown();
// In order to prevent any issues of threads freeing previous pointers
// after the main thread calls this code, simply leak the g_debug pointer
// and do not destroy the debug disable pthread key.
}
LogLeaks() 内部调用 GetList 函数获得 pointers_ 成员,按照 allocation size 排序后返回。
// PointerData.cpp
void PointerData::LogLeaks() {
std::vector<ListInfoType> list;
std::lock_guard<std::mutex> pointer_guard(pointer_mutex_);
std::lock_guard<std::mutex> frame_guard(frame_mutex_);
GetList(&list, false);
size_t track_count = 0;
for (const auto& list_info : list) {
error_log("+++ %s leaked block of size %zu at 0x%" PRIxPTR " (leak %zu of %zu)", getprogname(),
list_info.size, list_info.pointer, ++track_count, list.size());
if (list_info.backtrace_info != nullptr) {
error_log("Backtrace at time of allocation:");
UnwindLog(*list_info.backtrace_info);
} else if (list_info.frame_info != nullptr) {
error_log("Backtrace at time of allocation:");
backtrace_log(list_info.frame_info->frames.data(), list_info.frame_info->frames.size());
}
// Do not bother to free the pointers, we are about to exit any way.
}
}
CaptureThreads() 函数遍历 pid 下所有 tid,调用 ptrace 使得线程的寄存器和内存信息可以被读取;
// ThreadCapture.cpp
bool ThreadCaptureImpl::CaptureThreads() {
TidList tids{allocator_};
bool found_new_thread;
do {
if (!ListThreads(tids)) {
ReleaseThreads();
return false;
}
found_new_thread = false;
for (auto it = tids.begin(); it != tids.end(); it++) {
auto captured = captured_threads_.find(*it);
if (captured == captured_threads_.end()) {
if (CaptureThread(*it) < 0) {
ReleaseThreads();
return false;
}
found_new_thread = true;
}
}
} while (found_new_thread);
return true;
}
CapturedThreadInfo() 函数获取线每个线程的 regs 和 stack 内容;
// ThreadCapture.cpp
bool ThreadCaptureImpl::CapturedThreadInfo(ThreadInfoList& threads) {
threads.clear();
for (auto it = captured_threads_.begin(); it != captured_threads_.end(); it++) {
ThreadInfo t{0, allocator::vector<uintptr_t>(allocator_), std::pair<uintptr_t, uintptr_t>(0, 0)};
if (!PtraceThreadInfo(it->first, t)) {
return false;
}
threads.push_back(t);
}
return true;
}
ProcessMappings() 函数读取 pid maps 内容;
// ProcessMappings.cpp
bool ProcessMappings(pid_t pid, allocator::vector<Mapping>& mappings) {
char map_buffer[1024];
snprintf(map_buffer, sizeof(map_buffer), "/proc/%d/maps", pid);
android::base::unique_fd fd(open(map_buffer, O_RDONLY));
if (fd == -1) {
return false;
}
allocator::string content(mappings.get_allocator());
ssize_t n;
while ((n = TEMP_FAILURE_RETRY(read(fd, map_buffer, sizeof(map_buffer)))) > 0) {
content.append(map_buffer, n);
}
ReadMapCallback callback(mappings);
return android::procinfo::ReadMapFileContent(&content[0], callback);
}
解析后的mapping
// Example of how a parsed line look line:
// 00400000-00409000 r-xp 00000000 fc:00 426998 /usr/lib/gvfs/gvfsd-http
格式和用dumpsys meminfo 得到的内容类似,只是这里通过一个回调函数把他们组装成了 Mapping 的数据结构。
CollectAllocations() 函数
bool MemUnreachable::CollectAllocations(const allocator::vector
for (auto it = mappings.begin(); it != mappings.end(); it++) { heap_walker_.Mapping(it->begin, it->end); }
allocator::vector
for (auto it = heap_mappings.begin(); it != heap_mappings.end(); it++) { MEM_ALOGV(“Heap mapping %” PRIxPTR “-%” PRIxPTR “ %s”, it->begin, it->end, it->name); HeapIterate(*it, & { heap_walker_.Allocation(base, base + size); }); }
for (auto it = anon_mappings.begin(); it != anon_mappings.end(); it++) { MEM_ALOGV(“Anon mapping %” PRIxPTR “-%” PRIxPTR “ %s”, it->begin, it->end, it->name); heap_walker_.Allocation(it->begin, it->end); }
for (auto it = globals_mappings.begin(); it != globals_mappings.end(); it++) { MEM_ALOGV(“Globals mapping %” PRIxPTR “-%” PRIxPTR “ %s”, it->begin, it->end, it->name); heap_walker_.Root(it->begin, it->end); }
for (auto thread_it = threads.begin(); thread_it != threads.end(); thread_it++) { for (auto it = stack_mappings.begin(); it != stack_mappings.end(); it++) { if (thread_it->stack.first >= it->begin && thread_it->stack.first <= it->end) { MEM_ALOGV(“Stack %” PRIxPTR “-%” PRIxPTR “ %s”, thread_it->stack.first, it->end, it->name); heap_walker_.Root(thread_it->stack.first, it->end); } } heap_walker_.Root(thread_it->regs); }
heap_walker_.Root(refs);
MEM_ALOGI(“searching done”);
return true; }
GetUnreachableMemory()
1. 调用 DetectLeaks() 检测泄漏,遍历 roots_ vector 里保存的 mapping ,给在 range 内的 allocator 地址加上可从 root 引用的标记;
2. 调用 Leaked() 遍历总的 allocations_ map 记录,没有被标记引用的记录被认为是泄漏的内存。记录泄漏的数量和泄漏的大小,将记录保存到 leaked vector;
```c++
// MemUnreachable.cpp
bool MemUnreachable::GetUnreachableMemory(allocator::vector<Leak>& leaks, size_t limit,
size_t* num_leaks, size_t* leak_bytes) {
MEM_ALOGI("sweeping process %d for unreachable memory", pid_);
leaks.clear();
if (!heap_walker_.DetectLeaks()) {
return false;
}
allocator::vector<Range> leaked1{allocator_};
heap_walker_.Leaked(leaked1, 0, num_leaks, leak_bytes);
MEM_ALOGI("sweeping done");
MEM_ALOGI("folding related leaks");
// ... 这部分内容还没有细看,暂时跳过
MEM_ALOGI("folding done");
std::sort(leaks.begin(), leaks.end(),
[](const Leak& a, const Leak& b) { return a.total_size > b.total_size; });
if (leaks.size() > limit) {
leaks.resize(limit);
}
return true;
}
检测泄漏 遍历 roots_ vector 里保存的 mapping ,给在 range 内的 allocator 地址加上可从 root 引用的标记;
// HeapWalker.cpp
bool HeapWalker::DetectLeaks() {
// Recursively walk pointers from roots to mark referenced allocations
for (auto it = roots_.begin(); it != roots_.end(); it++) {
RecurseRoot(*it);
}
Range vals;
vals.begin = reinterpret_cast<uintptr_t>(root_vals_.data());
vals.end = vals.begin + root_vals_.size() * sizeof(uintptr_t);
RecurseRoot(vals);
if (segv_page_count_ > 0) {
MEM_ALOGE("%zu pages skipped due to segfaults", segv_page_count_);
}
return true;
}
// 遍历总的 allocations_ map 记录,没有被标记引用的记录被认为是泄漏的内存。记录泄漏的数量和泄漏的大小,将记录保存到 leaked vector;
bool HeapWalker::Leaked(allocator::vector<Range>& leaked, size_t limit, size_t* num_leaks_out,
size_t* leak_bytes_out) {
leaked.clear();
size_t num_leaks = 0;
size_t leak_bytes = 0;
for (auto it = allocations_.begin(); it != allocations_.end(); it++) {
if (!it->second.referenced_from_root) {
num_leaks++; // 泄漏的数量
leak_bytes += it->first.end - it->first.begin; // 泄漏的总大小
}
}
size_t n = 0;
for (auto it = allocations_.begin(); it != allocations_.end(); it++) {
if (!it->second.referenced_from_root) {
if (n++ < limit) {
leaked.push_back(it->first); // 泄漏的记录
}
}
}
if (num_leaks_out) {
*num_leaks_out = num_leaks; // 更新输出
}
if (leak_bytes_out) {
*leak_bytes_out = leak_bytes; // 更新输出
}
return true;
}
需要在待测试程序中添加代码。 整体分为三个部分:初始化,记录数据,统计数据;
初始化: 设置保存统计数据的路径,记录内存的次数以及保存折线图的间隔; 记录数据: 2.1 调用 dumpsys meminfo 接口获得内存信息; 2.2 使用异步线程将记录到的信息按照记录的间隔绘图存储(防止程序奔溃没有保存出数据); 2.3 每次保存数据时检查内存数据判断是否有泄漏; 获取内存泄漏的检查结果: 返回是否有内存泄漏的检查结果;
优点:可以控制记录内存泄漏的时机,避免记录大量重复的 meminfo; 缺点:内存数据以kb 返回,可能会遗漏小 size 的泄漏。不能定位泄漏的代码行。
Malloc Hooks 允许程序拦截执行期间发生的所有分配/释放调用。 它仅适用于 Android P 及之后的系统。它的流程和Malloc Debug 可以说基本上一样的,只是设置的属性名不一样。
有两种方法可以启用这些 hooks,设置系统属性或环境变量,并运行应用程序/程序。
adb shell setprop libc.debug.hooks.enable 1
或 export LIBC_HOOKS_ENABLE=1
初始化过程和 malloc debug 类似,只是判断的属性不同;在malloc hooks 的初始化函数中将从 libc_malloc_hooks.so 解析出来的函数symbol 都存放到 MallocDispatch;
// malloc_common_dynamic.cpp
static constexpr char kHooksSharedLib[] = "libc_malloc_hooks.so";
static constexpr char kHooksPrefix[] = "hooks";
static constexpr char kHooksPropertyEnable[] = "libc.debug.hooks.enable";
static constexpr char kHooksEnvEnable[] = "LIBC_HOOKS_ENABLE";
...
// Initializes memory allocation framework once per process.
static void MallocInitImpl(libc_globals* globals) {
...
// Prefer malloc debug since it existed first and is a more complete
// malloc interceptor than the hooks.
bool hook_installed = false;
if (CheckLoadMallocDebug(&options)) {
hook_installed = InstallHooks(globals, options, kDebugPrefix, kDebugSharedLib);
} else if (CheckLoadMallocHooks(&options)) {
hook_installed = InstallHooks(globals, options, kHooksPrefix, kHooksSharedLib);
}
if (!hook_installed) {
if (HeapprofdShouldLoad()) {
HeapprofdInstallHooksAtInit(globals);
}
} else {
// Record the fact that incompatible hooks are active, to skip any later
// heapprofd signal handler invocations.
HeapprofdRememberHookConflict();
}
}
系统调用 malloc 函数时实际会调用到 hooks_malloc() 中开发者自行实现的逻辑。
void* hooks_malloc(size_t size) {
if (__malloc_hook != nullptr && __malloc_hook != default_malloc_hook) {
return __malloc_hook(size, __builtin_return_address(0));
}
return g_dispatch->malloc(size);
}
官方示例
void* new_malloc_hook(size_t bytes, const void* arg) {
return orig_malloc_hook(bytes, arg);
}
auto orig_malloc_hook = __malloc_hook;
__malloc_hook = new_malloc_hook;
Malloc Hooks 自定义内存泄漏检测逻辑 更新__malloc_hook 和__malloc_free 指向新增函数;
bool hooks_initialize(const MallocDispatch* malloc_dispatch, bool*, const char*) {
g_dispatch = malloc_dispatch;
// __malloc_hook = default_malloc_hook;
__malloc_hook = cus_malloc_hook;
__realloc_hook = default_realloc_hook;
// __free_hook = default_free_hook;
__free_hook = cus_free_hook;
__memalign_hook = default_memalign_hook;
return true;
}
在新增函数内实现检测逻辑;
// static int allocated_count = 0;
static void* cus_malloc_hook(size_t size, const void* ) {
auto malloced_addr = g_dispatch->malloc(size);
// printf 可能会产生循环调用!
// printf("[malloc hooks] malloc %p size %zu at %p\n", malloced_addr, size, malloc_return_addr);
error_log("[malloc hooks] malloc %p size %zu\n", malloced_addr, size);
// allocated_count++;
return malloced_addr;
}
static void cus_free_hook(void* pointer, const void* ) {
// printf 可能会产生循环调用!
// printf("[malloc hooks] free %p, at %p\n", pointer, free_addr);
error_log("[malloc hooks] free %p\n", pointer);
// allocated_count--;
g_dispatch->free(pointer);
}
在 hooks_finalize 打印统计信息或者 dump 信息到文件。 在这里直接打印的话可能计数和预期不同,因为 malloc hooks 记录了整个程序执行过程中的申请和释放,是多于测试程序里面申请和释放的次数的。
void hooks_finalize() {
// error_log("allocated_count %d\n", allocated_count);
}
避坑
方案:
typedef void* (malloc_func_type)(size_t size); typedef void (free_func_type)(void* p); malloc_func_type malloc_origin_ = NULL; free_func_type free_origin_ = NULL;
int enable_malloc_hook = 1; int enable_free_hook = 1; static size_t allocate_cnt = 0; void* malloc(size_t size) { void* malloced_addr = NULL; if (enable_malloc_hook) { // 避免 printf 循环调用 enable_malloc_hook = 0; allocate_cnt++; malloced_addr = malloc_origin_(size); printf(“malloc %p size %zu\n”, malloced_addr, size); enable_malloc_hook = 1; } return malloced_addr; }
void free(void* p) { if (enable_free_hook) { enable_free_hook = 0; printf(“free [%p]\n”, p); enable_free_hook = 1; } allocate_cnt–; free_origin_(p); }
void finish() { printf(“allocate_cnt %zu\n”, allocate_cnt); }
void f(void); void f(void) { // printf(“[memtest] function f\n”); int* x = (int)malloc(10 * sizeof(int)); x[0] = 0; int y = (int*)malloc(5 * sizeof(int)); y[0] = 0; free(x); }
int main(void) {
// 获取系统默认的 malloc 和 free 函数
if (malloc_origin_ == NULL) {
malloc_origin_ =
reinterpret_cast
if (free_origin_ == NULL) {
free_origin_ = reinterpret_cast
// printf(“[memtest] hello main\n”); f();
// 注册程序退出时调用的函数 atexit(finish); return 0; }
输出
```SHELL
$ ./memtest_dlsym
malloc 0x56127a995260 size 40
malloc 0x56127a995290 size 20
free [0x56127a995260]
allocate_cnt 1
[调试本地内存使用 | Android 开源项目 | Android Open Source Project](https://source.android.google.cn/docs/core/tests/debug/native-memory?hl=zh-cn) |
[调试和减少内存错误 | Android NDK | Android Developers (google.cn)](https://developer.android.google.cn/ndk/guides/memory-debug?hl=zh-cn) |
一直以来我都有个疑问,光流(optical flow)中存储的是 next 相对于 prev 的平移量(dx,dy)。但是为什么使用光流生成的 map(以下简称 flow_map)表进行 remap 操作的时候,却是用 next+flow_map 得到 prev?平移量不是 prev 到 next 的平移量吗?不应该是 prev+flow_map 得到next 吗?
坐标原点在图像左上角,假设在 prev 图像上有一个像素点为$(x_1,y_1)$,在 next 图像上对应的像素点为$(x_2,y_2)$ ,那么位移表示为 $(dx,dy)=(x_2,y_2)-(x_1,y_1)$ prev 图像
next 图像
图上整体是往左上平移的,也就是如果原点在左上角,那么dx 和dy 都为负数。
inst = cv2.optflow.createOptFlow_DeepFlow()
flow = inst.calc(input_image_gray, output_image_gray, None)
下面是 flow 里面的值,确实也都是负数
接下来我们用 flow_map 来进行重映射。
在利用 flow 进行映射之前我们需要了解两个概念,前向映射和后向映射。
前向映射(Forward Mapping)和后向映射(Inverse Mapping)是在计算机图形学和计算机视觉领域中用于处理图像和几何变换的两种不同方法。
前向映射(Forward Mapping):
- 前向映射是一种直接的方法,它将输入图像中的每个像素通过变换映射到输出图像中的相应位置。
- 在前向映射中,我们遍历输入图像中的每个像素,将其通过变换计算出在输出图像中的位置,然后将像素值复制到该位置。
- 前向映射的优点是简单直观,但在某些情况下可能会导致输出图像中的某些位置没有被填充或者有重叠。
后向映射(Inverse Mapping):
- 后向映射是一种反向的方法,它将输出图像中的每个像素通过逆变换映射到输入图像中的相应位置。
- 在后向映射中,我们遍历输出图像中的每个像素,通过逆变换计算出它在输入图像中的位置,并从输入图像中取得对应位置的像素值。
- 后向映射的优点是可以确保输出图像中的每个位置都有对应的像素值,避免了前向映射中的缺失或重叠问题。
选择使用前向映射还是后向映射取决于具体的应用需求和变换操作。前向映射通常用于简单的几何变换,而后向映射通常用于复杂的非线性变换或需要确保像素一对一映射的情况。每种方法都有其优点和限制,根据具体情况选择合适的映射方法是重要的。
以上引用内容由 ChatGPT 回答得出。那么 remap 是属于哪种映射方式?知道了它属于哪种映射方式有助于我们理解映射过程。虽然ChatGPT 给出的答案是 remap 函数是前向映射,但按照我的理解它似乎是后向映射。
我做了这样一个实验: 生成一个(x,y) 都是(100,100)的map 表,如果remap 是前向映射,那么遍历原图中所有的坐标点,并把它的像素映射到目标图上(100,100) 的位置,那么目标图像将会是一个只在(100,100)处有值,其他部分为黑色(没有值)的图像。 而实际测试上生成的目标图是个纯色的图像。它更符合后向映射的描述,即遍历目标图每个像素点,从原图上取值填充过去,对于这个map 表中所有元素都是(100,100)的映射来说,目标图上所有像素取值来源相同,就应该是个纯色图像。
鉴于 remap 函数可能是后向映射,我们不可能用 prev+flow_map 生成的map 表得到 next。反而是 next+flow_map 生成的map 表有可得到prev。 下面是一段将图像按照 flow 中表示的位移进行 remap 的代码,内含用 flow 生成 flow_map 的操作。
def cv_warp(src, flow):
# 生成网格点坐标矩阵
h, w = flow.shape[:2]
warp_grid_x, warp_grid_y = np.meshgrid(np.linspace(0, w - 1, w), np.linspace(0, h - 1, h))
# 因为flow 里面存储的是dx dy, 而remap 需要的是点到点的映射,所以需要加上x,y 坐标,也就是上面生成的网格点坐标矩阵
# axis=-1 的作用是将分开的grid_x(x0,x1,...) 和grid_y(y0,y2,...) 合并成(x0,y0),(x1,y1),... 的形式
flow_inv = flow + np.stack((warp_grid_x, warp_grid_y), axis=-1)
flow_inv = flow_inv.astype(np.float32)
warped = cv2.remap(src, flow_inv, None, cv2.INTER_CUBIC)
return warped
我们将 next 和 flow 输入到上面的函数,得到如下结果:
它基本上可以和 prev 的内容对齐。 如果我们想要从 prev remap 得到 next,则需要将 flow 的值取反再生成map 表。
OpenCV 官方文档对于 remap 函数的解释中有这么一个公式: \(dst(x,y)=src(mapx(x,y),mapy(x,y))\) 我们实际带入数据来理解一下这公式。先来看dst 图像中的第(0,0) 个像素,假设map_x(0,0) 中存储的是10,map_y(0,0) 存储的是10,那么它表示的是目标图像上第(0,0) 位置的像素值,需要在源图像上第(10,10) 中取得(不考虑插值)。你会发现这个像素从原图像的(10,10) 移动到了目标图像的(0,0),它是往左上角移动了,但实际你计算光流的话这个点的(dx,dy)=(0,0)-(10,10) 是(-10,-10),map 表中存储的是(10,10),但是flow 中存的是(-10,-10),有一点反直觉对不对? 现在我们回头看看我最初的疑问是不是合理的 “平移量不是 prev 到 next 的平移量吗?不应该是 prev+flow_map 得到next 吗?” flow 中村的是 prev 到 next 的平移量这个理解没问题,问题在于(dx,dy) 的坐标。当我们把光流的意义用下面的表达式表示\((dx,dy) = next(x,y)-prev(x',y')\)你会发现flow 中每一点的偏移量,是表示的next 图像中对应坐标中的平移量,所以当你试图用flow_map 对prev 做remap 时,其实是将当前的平移量应用到了另一个点上,这显然是错误的。 而next+flow_map 进行remap 才是合理的,同样带入数据,目标图上(0,0)的像素,要在源图像(此时为 next)的(-10,-10)取得,这表示像素向右下角移动了,和前面src 变换成dst 移动的方向相反,于是dst 还原成了src。
光流的可视化方法不唯一,可视化方法不同,则相同的颜色/亮度会表示的含义不同。 有些方法用亮度表示位移大小,有些方法用饱和度表示位移大小,也有可能相同的位移方向在不同的转换方法中表现为不同颜色。 个人认为不必过分追求统一的表示方法,与人交流时提前明确表示方法即可。 (下面光流图和上面计算光流的输入图不是同一组)
OpenCV 官方文档中的可视化方法:OpenCV: Optical Flow csdn 上某博主的可视化方法:光流介绍以及FlowNet学习笔记_光流可视化
使用 Pycharm 创建一个新的工程时默认会创建一个虚拟环境。虚拟环境的好处我们这里就不再多说了。 在创建了虚拟环境之后,我们安装的 packages 都会存放在 venv/Lib/site-packages/ 目录下,如果你使用了较多或者较大的packages 的话,这个venv 目录将会占用不少空间。 比如我之前有个项目的 requirements.txt 文件里的内容如下
opencv-python==4.8.0.76
opencv-contrib-python==4.8.0.76
opencv-contrib-python-headless==4.8.0.76
numpy~=1.21.3
scipy==1.7.3
合计占用了 406M 空间。试想一下,如果你有多个项目的话,累计占用的空间是以G 计的。 其实在通过Pycharm 创建虚拟环境时多勾选一个 inherit global site-packages 选项就可以帮我们节省空间,这个选项的意义是让我们当前的项目可以使用系统环境下安装的packages。 在重新创建了勾选给 的venv 后,虚拟环境占用14M,大大减小体积,当然前提是你系统库里面有安装当前项目所需要的packages。
只需在创建虚拟环境时加上 --system-site-packages
选项:
$ python -m venv --system-site-packages venv3
另外虚拟环境目录下会有个 pyvenv.cfg 文件,其中 include-system-site-packages = true
就代表是否可以使用系统packages,也就是和 --system-site-packages
对应的配置。
注意:不建议将所有的packages 都安装在系统环境下,这样虚拟环境就失去了意义。
最近做的项目上需要在做了某项任务之后发邮件出来通知。于是开始调查怎么用golange 代码发送邮件。
搜索一番发现有 net/smtp
,jordan-wright
和gomail
三个库可以使用,那就让我们挑一个试试吧。
废话不多说,直接上代码
package main
import (
"github.com/jordan-wright/email"
"log"
"net/smtp"
)
const (
SMTPHost = "smtp.gmail.com"
SMTPPort = ":587"
SMTPUsername = "xxx@gmail.com"
SMTPPassword = "xxxx"
)
func main() {
sendEmail()
}
func sendEmail() {
log.Println("send email")
receiver := "xxx@gmial.com"
auth := smtp.PlainAuth("", SMTPUsername, SMTPPassword, SMTPHost)
msg := []byte("Subject: here is the subject \r\n\r\n" + "这里是正文内容\r\n")
err := smtp.SendMail(SMTPHost+SMTPPort, auth, SMTPUsername, []string{receiver}, msg)
if err != nil {
log.Println("failed to send email:", err)
}
}
但是使用之后发现报错了
504 5.7.4 Unrecognized authentication type
bing 和 google 搜索了一番没有查到解决办法,于是尝试了另一个库
package main
import (
"crypto/tls"
"fmt"
"github.com/jordan-wright/email"
"log"
"net/smtp"
)
const (
SMTPHost = "smtp.gmail.com"
SMTPPort = ":587"
SMTPUsername = "xxx@gmail.com"
SMTPPassword = "xxxx"
)
func main() {
sendEmailJordan()
}
func sendEmailJordan() {
log.Println("send email with jordan")
receiver := "xxx@gmail.com"
auth := smtp.PlainAuth("", SMTPUsername, SMTPPassword, SMTPHost)
e := &email.Email{
From: fmt.Sprintf("发送者名字<%s>", SMTPUsername),
To: []string{receiver},
Subject: "这里是标题内容",
Text: []byte("这里是正文内容"),
}
//func (e *Email) Send(addr string, a smtp.Auth) error {
err := e.Send(SMTPHost+SMTPPort, auth)
if err != nil {
log.Println("failed to Send email with jordan:", err)
}
//tc:=&tls.Config{InsecureSkipVerify: true}
tc := &tls.Config{
ServerName: SMTPHost,
InsecureSkipVerify: true,
}
err = e.SendWithStartTLS(SMTPHost+SMTPPort, auth, tc)
if err != nil {
log.Println("failed to SendWithStartTLS email with jordan:", err)
}
}
因为我们的邮箱服务是StartTLS 加密方式,我还试了 SendWithStartTLS() 方法,但同样都是报 Unrecognized authentication 错误。 找了好久都没找到解法,我相信解决办法一定是有的,只是暂时我没有找到,有大佬知道怎么解决的,欢迎给我留言评论,不胜感激。
报错解不掉,于是接着尝试其他方法,本来不太抱希望的,居然成功了!
先下载gomail 包
go get gopkg.in/gomail.v2
代码:
package main
import (
"fmt"
"gopkg.in/gomail.v2"
"log"
"strconv"
)
const (
SMTPHost = "smtp.gmail.com"
SMTPPort = ":587"
SMTPUsername = "xxx@gmail.com"
SMTPPassword = "xxxx"
)
func SendMail(mailTo []string, subject string, body string) error {
// 设置邮箱主体
mailConn := map[string]string{
"user": SMTPUsername,
"pass": SMTPPassword,
"host": SMTPHost,
"port": SMTPPort,
}
port, _ := strconv.Atoi(mailConn["port"])
m := gomail.NewMessage()
m.SetHeader("From", m.FormatAddress(mailConn["user"], "xx官方")) // 添加别名
m.SetHeader("To", mailTo...) // 发送给用户(可以多个)
m.SetHeader("Subject", subject) // 设置邮件主题
m.SetBody("text/html", body) // 设置邮件正文
d := gomail.NewDialer(mailConn["host"], port, mailConn["user"], mailConn["pass"]) // 设置邮件正文
err := d.DialAndSend(m)
return err
}
func main() {
// 发送方
mailTo := []string{
"xxx<xxx@gmail.com>", // 这里最好写成邮箱收发件时的这种标记格式
}
// 邮件主题
subject := "Hello"
// 邮件正文
body := "Automatic send by Go gomail from xxx官方."
err := SendMail(mailTo, subject, body)
if err != nil {
log.Print(err)
fmt.Printf("Send fail!")
return
}
fmt.Println("send successfully!")
}
这份代码中并没有出现tls 相关的关键字,但是居然发送成功了! 我相信前两种方法应该也是能成功的,目前我还没有找到解决 Unrecognized authentication 错误的方法,希望有大佬指教一下。
enum ImreadModes {
IMREAD_UNCHANGED = -1, //!< If set, return the loaded image as is (with alpha channel, otherwise it gets cropped).//原始图像有alpha通道的话,就保存该通道数据,否则,别的flags会直接将alpha通道丢弃。
IMREAD_GRAYSCALE = 0, //!< If set, always convert image to the single channel grayscale image.
IMREAD_COLOR = 1, //!< If set, always convert image to the 3 channel BGR color image.
IMREAD_ANYDEPTH = 2, //!< If set, return 16-bit/32-bit image when the input has the corresponding depth, otherwise convert it to 8-bit.
IMREAD_ANYCOLOR = 4, //!< If set, the image is read in any possible color format.
IMREAD_LOAD_GDAL = 8, //!< If set, use the gdal driver for loading the image.
IMREAD_REDUCED_GRAYSCALE_2 = 16, //!< If set, always convert image to the single channel grayscale image and the image size reduced 1/2.
IMREAD_REDUCED_COLOR_2 = 17, //!< If set, always convert image to the 3 channel BGR color image and the image size reduced 1/2.
IMREAD_REDUCED_GRAYSCALE_4 = 32, //!< If set, always convert image to the single channel grayscale image and the image size reduced 1/4.
IMREAD_REDUCED_COLOR_4 = 33, //!< If set, always convert image to the 3 channel BGR color image and the image size reduced 1/4.
IMREAD_REDUCED_GRAYSCALE_8 = 64, //!< If set, always convert image to the single channel grayscale image and the image size reduced 1/8.
IMREAD_REDUCED_COLOR_8 = 65, //!< If set, always convert image to the 3 channel BGR color image and the image size reduced 1/8.
IMREAD_IGNORE_ORIENTATION = 128 //!< If set, do not rotate the image according to EXIF's orientation flag.
};
OpenCV 数据类型
#define CV_8U 0
#define CV_8S 1
#define CV_16U 2
#define CV_16S 3
#define CV_32S 4
#define CV_32F 5
#define CV_64F 6
#define CV_USRTYPE1 7
]]>盒子滤波,又称方框滤波 卷积最常用的一个功能就是模糊平滑功能,又称为低通滤波,它的作用是降低图像噪声。 图像噪声可能是外部像照明光源问题或者环境因素导致;也有可能是内部因素导致的,比如传感器导致某些像素缺陷。通过卷积模糊或者平滑操作可以去除图像产生这些不同类型的噪声。
blur 和 boxFilter 都是方框型滤波器,简称方波,唯一不同的是,blur是boxFilter的归一化形式。前者是归一化方框型滤波器,后者是非归一化方框型滤波器。 blur 的卷积核: \(k=\frac{1}{ksize.w*ksize.h} \begin{vmatrix} 1 & 1 & ...& 1 \\ 1 & 1 & ...& 1 \\...&...&...&...\\1 & 1 & ...& 1 \end{vmatrix}\)
boxFilter 的卷积核: \(k=\alpha \begin{vmatrix} 1 & 1 & ...& 1 \\ 1 & 1 & ...& 1 \\...&...&...&...\\1 & 1 & ...& 1 \end{vmatrix}\) where \(\alpha= \begin{cases} \frac{1}{ksize.w*ksize.h}, & \text {when normalize = true} \\ 1, & \text{otherwise} \end{cases}\)
方形模糊的卷积核所有像素的对中心像素的贡献是相等的,因为它们的权重系数都一样。一个高斯模糊核就是邻域像素根据到中心像素距离不同有着不同的权重,对输出像素的贡献不同。
取中间值
双边滤波是一种非线性、边缘保留、有效去噪声的滤波方法。
总结: 1.中值滤波对椒盐噪声有最好的去噪效果
]]>使用比较简单,直接看例子:
import cProfile
import timeline_window
cProfile.run('timeline_window.main()')
timeline_window 是我们要测试的模块,timeline_window.main() 是它的主函数。 执行完之后打印如下信息
C:\Python37\python.exe C:\Users\edison\PycharmProjects\DataViz\performace_test.py
865497 function calls (858749 primitive calls) in 2.977 seconds
Ordered by: standard name
ncalls tottime percall cumtime percall filename:lineno(function)
4 0.000 0.000 0.000 0.000 <__array_function__ internals>:2(amax)
19 0.000 0.000 0.001 0.000 <__array_function__ internals>:2(any)
17 0.000 0.000 0.001 0.000 <__array_function__ internals>:2(argwhere)
1 0.000 0.000 0.000 0.000 <__array_function__ internals>:2(array_equal)
1603 0.001 0.000 0.005 0.000 <__array_function__ internals>:2(atleast_1d)
6 0.000 0.000 0.000 0.000 <__array_function__ internals>:2(atleast_2d)
6 0.000 0.000 0.000 0.000 <__array_function__ internals>:2(concatenate)
811 0.001 0.000 0.001 0.000 <__array_function__ internals>:2(copyto)
7 0.000 0.000 0.000 0.000 <__array_function__ internals>:2(empty_like)
...
每一行依次列出了各个子函数的运行分析信息:
ncalls 调用次数
tottime 在给定函数中花费的总时间(不包括调用子函数的时间)
percall tottime除以ncalls的商
cumtime 是在这个函数和所有子函数中花费的累积时间(从调用到退出)。
percall是cumtime除以原始调用次数的商
filename:lineno(function) 提供每个函数的各自信息
import cProfile
import timeline_window
cProfile.run('timeline_window.main()', 'restats')
性能数据的文件会保存到当前目录下的restats文件中。
加载这些数据,可以进行后续的比较分析。
import pstats
from pstats import SortKey
# 加载保存到restats文件中的性能数据
p = pstats.Stats('restats')
# 打印所有统计信息
p.strip_dirs().sort_stats(-1).print_stats()
最常用的是,查看耗时最多的函数排序,比如前十个:
# 打印累计耗时最多的10个函数
p.sort_stats(SortKey.CUMULATIVE).print_stats(10)
# 打印内部耗时最多的10个函数(不包含子函数)
p.sort_stats(SortKey.TIME).print_stats(10)
Mon Dec 5 16:20:20 2022 restats
42734012 function calls (42727371 primitive calls) in 43.122 seconds
Ordered by: cumulative time
List reduced from 2541 to 10 due to restriction <10>
ncalls tottime percall cumtime percall filename:lineno(function)
290/1 0.002 0.000 43.589 43.589 {built-in method builtins.exec}
1 0.026 0.026 43.589 43.589 timeline_window.py:153(main)
1 0.000 0.000 40.626 40.626 timeline_window.py:141(load_events_from_file)
1 0.000 0.000 39.737 39.737 timeline_window.py:131(add_timestamps_scatter)
1 0.010 0.010 39.722 39.722 timeline_scatter.py:80(set_events)
801 0.005 0.000 39.419 0.049 PlotItem.py:597(addLine)
803 0.012 0.000 39.309 0.049 PlotItem.py:521(addItem)
808 0.008 0.000 39.286 0.049 ViewBox.py:402(addItem)
1628 0.205 0.000 39.004 0.024 ViewBox.py:896(updateAutoRange)
3231 10.849 0.003 38.744 0.012 ViewBox.py:1404(childrenBounds)
于是,我们找到了耗时的大头: 调用了 801 次耗时 39ms 的 PlotItem.py:597(addLine) 函数。
特点是最简单,但功能也最少。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sys
from PyQt5.QtGui import QPixmap
from PyQt5.QtWidgets import QWidget, QApplication, QVBoxLayout, QLabel
class ImageLabel(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.resize(600, 400)
self.setWindowTitle("label image")
pix = QPixmap(r'C:\fruits.jpg')
label = QLabel(self)
label.setPixmap(pix)
label.setScaledContents(True) # 自适应QLabel大小
layout = QVBoxLayout()
layout.addWidget(label)
self.setLayout(layout)
if __name__ == '__main__':
app = QApplication(sys.argv)
mainWidget = ImageLabel()
mainWidget.show()
sys.exit(app.exec_())
来源 【PyQtGraph】显示图像 特点 可以对图片进行缩放操作,继承了pyqtgraph 的一些特点功能。
"""
安装依赖库:
1. Pillow
2. PySide2
3. PyQtGraph
from https://blog.csdn.net/zhy29563/article/details/119754910
"""
import sys
import numpy as np
import pyqtgraph as pg
from PIL import Image
from PyQt5.QtWidgets import QApplication, QVBoxLayout, QPushButton, QWidget, QFileDialog
from pyqtgraph import ImageView
# 设置 PyQtGraph 显示配置
########################################################################################################################
# 设置显示背景色为白色,默认为黑色
pg.setConfigOption('background', 'w')
# 设置显示前景色为黑色,默认为灰色
pg.setConfigOption('foreground', 'k')
# 设置图像显示以行为主,默认以列为主
pg.setConfigOption('imageAxisOrder', 'row-major')
class PyQtGraphicDemo(QWidget):
def __init__(self, parent=None):
super(PyQtGraphicDemo, self).__init__(parent)
self.resize(600, 400)
# 图像显示控件
self.graphicsView = ImageView(self)
# 隐藏直方图,菜单按钮,ROI
self.graphicsView.ui.histogram.hide()
self.graphicsView.ui.menuBtn.hide()
self.graphicsView.ui.roiBtn.hide()
image = Image.open(r'C:\fruits.jpg')
if image is not None:
# 如果之前未设置显示选项以行为主,这里需要对显示图像进行转置
self.graphicsView.setImage(np.array(image))
self.verticalLayout = QVBoxLayout(self)
self.verticalLayout.addWidget(self.graphicsView)
# 设置窗口布局
self.setLayout(self.verticalLayout)
if __name__ == '__main__':
app = QApplication(sys.argv)
window = PyQtGraphicDemo()
window.show()
sys.exit(app.exec_())
来源:PyQt5-使用scrollArea实现图片查看器功能
特点是当窗口大小小于scrollArea 区域大小时有滑动条显示,可以拖动滑动条滑动界面。
但是这份代码有个缺点,就是当窗口大小大于scrollArea 区域大小时,你会发现scrollArea 以外的区域是空白的,也就是scrollArea 是固定大小的,区域外不会显示内容。注释掉 self.setFixedSize(850, 600) 可以测试看到。 这份代码的显示原理大致如下:创建一个scrollArea控件,对多张图像依次执行下面循环的操作:1. 创建一个label 显示image;2. label 添加到 一个QVBoxLayout 中,3. QVBoxLayout 作为一个临时的QWidget layout,4. 移动这个临时的 QWidget 到指定坐标。emmm 就不是很优雅。
# from https://blog.csdn.net/HG0724/article/details/116702824
import sys
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.uic import loadUi
class Picture(QMainWindow):
def __init__(self, parent=None, url=None):
super().__init__(parent)
self.url = url
self.ui()
def ui(self):
loadUi('./show_pic.ui', self)
# self.setFixedSize(850, 600)
total = len(self.url)
self.qw = QWidget()
if total % 5 == 0:
rows = int(total / 5)
else:
rows = int(total / 5) + 1
self.qw.setMinimumSize(850, 230 * rows)
for i in range(total):
photo = QPixmap(self.url[i])
# print('photo:',photo)
# photo.loadFromData(req.content)
width = photo.width()
height = photo.height()
print('width:', width, ' ', 'height:', height)
if width == 0 or height == 0:
continue
tmp_image = photo.toImage() # 将QPixmap对象转换为QImage对象
size = QSize(width, height)
# photo.convertFromImage(tmp_image.scaled(size, Qt.IgnoreAspectRatio))
photo = photo.fromImage(tmp_image.scaled(size, Qt.IgnoreAspectRatio))
# 为每个图片设置QLabel容器
label = QLabel()
label.setFixedSize(150, 200)
label.setStyleSheet("border:1px solid gray")
label.setPixmap(photo)
label.setScaledContents(True) # 图像自适应窗口大小
vl = QVBoxLayout()
vl.addWidget(label)
tmp = QWidget(self.qw)
tmp.setLayout(vl)
tmp.move(160 * (i % 5), 230 * int(i / 5))
self.scrollArea.setWidget(self.qw) # 和ui文件中名字相同
if __name__ == '__main__':
app = QApplication(sys.argv)
# 这是我的文件夹中图片的路径
import glob
url = glob.glob(r"C:\waDump\*.jpg")
pic = Picture(url=url)
pic.show()
sys.exit(app.exec_())
可以缩放窗口,图像可以随着窗口变化,但只是图像间距拉伸,每行的图片数量没有变化
# -*- coding: utf-8 -*-
import glob
import time
from PyQt5 import QtWidgets
from PyQt5.QtCore import QSize, Qt
from PyQt5.QtGui import QPixmap
from PyQt5.QtWidgets import QApplication, QLabel, QGridLayout
class Picture(QtWidgets.QWidget):
def __init__(self, parent=None):
super(Picture, self).__init__(parent)
print('Picture init')
self.setWindowTitle('All Images')
self.resize(800, 600)
# ui components
self.scrollArea = QtWidgets.QScrollArea()
self.scrollArea.setWidgetResizable(True)
self.scrollAreaWidgetContents = QtWidgets.QWidget()
# self.scrollAreaWidgetContents.setObjectName("scrollAreaWidgetContents")
self.scrollArea.setWidget(self.scrollAreaWidgetContents)
self.gridLayout = QGridLayout(self.scrollAreaWidgetContents)
self.v_layout = QtWidgets.QVBoxLayout(self)
self.v_layout.addWidget(self.scrollArea)
self.setLayout(self.v_layout)
# vars
self.max_columns = 5
def load_images(self, paths):
print('load images --start')
total = len(paths)
col = 0
row = 0
for i in range(total):
self.max_columns = total if total < 5 else 5
photo = QPixmap(paths[i])
width = photo.width()
height = photo.height()
if width == 0 or height == 0:
continue
tmp_image = photo.toImage() # 将QPixmap对象转换为QImage对象
size = QSize(width, height)
# photo.convertFromImage(tmp_image.scaled(size, Qt.IgnoreAspectRatio))
photo = photo.fromImage(tmp_image.scaled(size, Qt.IgnoreAspectRatio))
# 为每个图片设置QLabel容器
label = QLabel()
w = int(self.width() / self.max_columns * 0.8)
h = int(w * photo.height() / photo.width())
label.setFixedSize(w, h)
label.setStyleSheet("border:1px solid gray")
label.setPixmap(photo)
label.setScaledContents(True) # 图像自适应窗口大小
self.gridLayout.addWidget(label, row, col)
# 计算下一个label 位置
if col < self.max_columns - 1:
col = col + 1
else:
col = 0
row += 1
print('load images --end')
if __name__ == '__main__':
start_time = time.time()
print('main layout show')
app = QApplication([])
main_window = Picture()
main_window.show()
image_list = url = glob.glob(r"C:\waDump\*.jpg")
# 加载图像显示
main_window.load_images(image_list)
print("耗时: {:.3f}秒".format(time.time() - start_time))
app.exec_()
可以缩放窗口,图像可以随着窗口重新排列,自定义的QListWidgetItem 可以灵活自定义显示样式。 代码参考这两个博客 PyQt使用笔记(六) 可多选, 有右键复制删除功能的ListWidget 2021.03.23 和 [pyqt] 使用自定义QListWidgetItem
# -*- coding: utf-8 -*-
import time
from PyQt5 import QtWidgets, QtCore, QtGui
from PyQt5.QtCore import QSize
from PyQt5.QtCore import Qt
from PyQt5.QtCore import pyqtSignal
from PyQt5.QtGui import QCursor
from PyQt5.QtGui import QPixmap
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWidgets import QMenu, QAbstractItemView, QListWidgetItem, QListView
from PyQt5.QtWidgets import QWidget, QLabel, QVBoxLayout
class ImageListWidget(QtWidgets.QListWidget):
signal = pyqtSignal(list)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.image_cmp_widget = None
self.single_image = None
self.setWindowTitle('All Images')
self.resize(1400, 700)
self.setContextMenuPolicy(Qt.CustomContextMenu)
# 创建QMenu信号事件
self.customContextMenuRequested.connect(self.showMenu)
self.contextMenu = QMenu(self)
self.CMP = self.contextMenu.addAction('比较')
# self.CP = self.contextMenu.addAction('复制')
self.DL = self.contextMenu.addAction('删除')
# self.CP.triggered.connect(self.copy)
self.DL.triggered.connect(self.del_text)
# 设置每个item size
self.setGridSize(QtCore.QSize(220, 190))
# 设置横向list
self.setFlow(QListView.LeftToRight)
# 设置换行
self.setWrapping(True)
# 窗口size 变化后重新计算列数
self.setResizeMode(QtWidgets.QListView.Adjust)
# 设置选择模式
self.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.setIconSize(QSize(200, 150))
# 显示右键菜单
def showMenu(self, pos):
# pos 鼠标位置
# 菜单显示前,将它移动到鼠标点击的位置
self.contextMenu.exec_(QCursor.pos()) # 在鼠标位置显示
# 获取选择行的内容
def selected_text(self):
try:
selected = self.selectedItems()
texts = ''
for item in selected:
if texts:
texts = texts + '\n' + item.text()
else:
texts = item.text()
except BaseException as e:
print(e)
return
print('selected_text texts', texts)
return texts
def copy(self):
text = self.selected_text()
if text:
clipboard = QApplication.clipboard()
clipboard.setText(text)
def del_text(self):
try:
index = self.selectedIndexes()
row = []
for i in index:
r = i.row()
row.append(r)
for i in sorted(row, reverse=True):
self.takeItem(i)
except BaseException as e:
print(e)
return
self.signal.emit(row)
def mouseDoubleClickEvent(self, e: QtGui.QMouseEvent) -> None:
super().mouseDoubleClickEvent(e)
print('double click')
selected = self.selectedItems()
img_path = ''
for item in selected:
img_path = item.image_path()
if len(img_path) > 0:
# 打开新窗口显示单张图片
# self.single_image = SingleImageView(image=img_path, background=Qt.white)
# self.single_image.show()
pass
pass
def load_images(self, paths):
for i in range(len(paths)):
img_item = ImageQListWidgetItem("dump image ***", paths[i])
self.addItem(img_item)
self.setItemWidget(img_item, img_item.widget)
# 刷新界面
QApplication.processEvents()
# 自定义的item 继承自QListWidgetItem
class ImageQListWidgetItem(QListWidgetItem):
def __init__(self, name, img_path):
super().__init__()
self.img_path = img_path
# 自定义item中的widget 用来显示自定义的内容
self.widget = QWidget()
# 用来显示name
self.nameLabel = QLabel()
self.nameLabel.setText(name)
# 用来显示avator(图像)
self.avatorLabel = QLabel()
# 设置图像源 和 图像大小
img_obg = QPixmap(img_path)
width = img_obg.width()
height = img_obg.height()
scale_size = QSize(200, 150)
if width < height:
scale_size = QSize(150, 200)
self.avatorLabel.setPixmap(QPixmap(img_path).scaled(scale_size))
# 图像自适应窗口大小
self.avatorLabel.setScaledContents(True)
# 设置布局用来对nameLabel和avatorLabel进行布局
self.hbox = QVBoxLayout()
self.hbox.addWidget(self.avatorLabel)
self.hbox.addWidget(self.nameLabel)
self.hbox.addStretch(1)
# 设置widget的布局
self.widget.setLayout(self.hbox)
# 设置自定义的QListWidgetItem的sizeHint,不然无法显示
self.setSizeHint(self.widget.sizeHint())
def image_path(self):
return self.img_path
if __name__ == '__main__':
print('main layout show')
now = time.time()
app = QApplication([])
main_window = ImageListWidget()
main_window.show()
image_list = ['icon.jpg', 'icon.jpg', 'icon.jpg']
# 数据扩充
image_list = image_list + image_list + image_list + image_list
main_window.load_images(image_list)
# 绑定点击槽函数 点击显示对应item中的name
main_window.itemClicked.connect(lambda item: print('clicked item label:', item.nameLabel.text()))
print("ImageListWidget 耗时: {:.2f}秒".format(time.time() - now))
app.exec_()
# 声明timer
timer = QtCore.QTimer()
timer.setSingleShot(True)
# 在需要的地方设置定时
timer.start(600)
# 到之间后
timer.timeout.connect(self.funcA)
但是实际测试发现个问题,在多次触发这个延时之后,funcA 会多执行一次。还不知道为什么。
-- trigger -- // 第一次触发
funcA
-- trigger -- // 第二次触发
funcA
funcA
-- trigger -- // 第三次触发,每触发一次 funcA 就多执行一次。
funcA
funcA
funcA
-- trigger --
funcA
funcA
funcA
funcA
QtCore.QTimer.singleShot(600, self.funcA)
这样得到的结果是符合预期的
-- trigger --
funcA
-- trigger --
funcA
-- trigger --
funcA
-- trigger --
funcA