Android内存优化(二)系统进程之logd的native memory优化

针对C1logd进程内存占用过大的问题,通过调整LogBuffer大小降低内存消耗,并分析改动前后对日志记录的影响。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

最近的测试发现,C1 logd进程内存占用比Mate9 logd进程内存占用大很多,详细数据如下:
内存最大值(KB) 内存最小值(KB) 内存平均值(KB)
C1 39794 21985 39353
Mate9 2804 2565 2719

怀疑有native内存泄露,或者内存老化问题出现,于是做了针对性的Native内存泄露测试,发现跑两个小时的固定场景,很容易出现下面这段trace

Begin trace
size 4111590 count:49857 parentcnt:51505
0x00008c0c debug_malloc bionic/libc/malloc_debug/malloc_debug.cpp:310 /system/lib64/libc_malloc_debug.so
0x00083070 operator new(unsigned long) external/libcxxabi/src/cxa_new_delete.cpp:44 (discriminator 1) /system/lib64/libc++.so 
0x0000ac4c LogBufferElement system/core/logd/LogBufferElement.cpp:47 /system/bin/logd 
0x00007498 LogBuffer::log(log_id, log_time, unsigned int, int, int, char const*, unsigned short) system/core/logd/LogBuffer.cpp:209 (discriminator 3) /system/bin/logd 
0x00006168 LogListener::onDataAvailable(SocketClient*) system/core/logd/LogListener.cpp:112 (discriminator 1) /system/bin/logd 
0x00004200 SocketListener::runListener() system/core/libsysutils/src/SocketListener.cpp:245 /system/lib64/libsysutils.so 
0x00003cf8 SocketListener::threadStart(void*) system/core/libsysutils/src/SocketListener.cpp:147 /system/lib64/libsysutils.so 
0x00075c54 __pthread_start(void*) bionic/libc/bionic/pthread_create.cpp:198 (discriminator 1) /system/lib64/libc.so 
0x0001e0fc __start_thread bionic/libc/bionic/clone.cpp:41 (discriminator 1) /system/lib64/libc.so 
0xfffffffffffffffc ??? ??? ??? 
End Trace
Begin trace
size 2920176 count:52146 parentcnt:53794
0x00008c0c debug_malloc bionic/libc/malloc_debug/malloc_debug.cpp:310 /system/lib64/libc_malloc_debug.so
0x00083070 operator new(unsigned long) external/libcxxabi/src/cxa_new_delete.cpp:44 (discriminator 1) /system/lib64/libc++.so 
0x00007474 LogBuffer::log(log_id, log_time, unsigned int, int, int, char const*, unsigned short) system/core/logd/LogBuffer.cpp:209 (discriminator 1) /system/bin/logd 
0x00006168 LogListener::onDataAvailable(SocketClient*) system/core/logd/LogListener.cpp:112 (discriminator 1) /system/bin/logd 
0x00004200 SocketListener::runListener() system/core/libsysutils/src/SocketListener.cpp:245 /system/lib64/libsysutils.so 
0x00003cf8 SocketListener::threadStart(void*) system/core/libsysutils/src/SocketListener.cpp:147 /system/lib64/libsysutils.so 
0x00075c54 __pthread_start(void*) bionic/libc/bionic/pthread_create.cpp:198 (discriminator 1) /system/lib64/libc.so 
0x0001e0fc __start_thread bionic/libc/bionic/clone.cpp:41 (discriminator 1) /system/lib64/libc.so 
0xfffffffffffffffc ??? ??? ??? 
End Trace
Begin trace
size 1249464 count:52061 parentcnt:53709
0x00008c0c debug_malloc bionic/libc/malloc_debug/malloc_debug.cpp:310 /system/lib64/libc_malloc_debug.so
0x00083070 operator new(unsigned long) external/libcxxabi/src/cxa_new_delete.cpp:44 (discriminator 1) /system/lib64/libc++.so 
0x000075f4 std::_1::_allocate(unsigned long) external/libcxx/include/new:168 /system/bin/logd 
0x00006168 LogListener::onDataAvailable(SocketClient*) system/core/logd/LogListener.cpp:112 (discriminator 1) /system/bin/logd 
0x00004200 SocketListener::runListener() system/core/libsysutils/src/SocketListener.cpp:245 /system/lib64/libsysutils.so 
0x00003cf8 SocketListener::threadStart(void*) system/core/libsysutils/src/SocketListener.cpp:147 /system/lib64/libsysutils.so 
0x00075c54 __pthread_start(void*) bionic/libc/bionic/pthread_create.cpp:198 (discriminator 1) /system/lib64/libc.so 
0x0001e0fc __start_thread bionic/libc/bionic/clone.cpp:41 (discriminator 1) /system/lib64/libc.so 
0xfffffffffffffffc ??? ??? ??? 
End Trace

从上面的backtrace看,主要描述的是Logd如何处理客户端的log过程,以Java层的Log.d(smg)举例,主要执行流程:

Log.java d()​->Log.java println_native()->android_util_Log.cpp android_util_Log_println_native()->logger_write.c __android_log_buf_write​()->logger_write.c write_to_log() -> logger_write.c __write_to_log_init​() -> logger_write.c​ __write_to_log_initialize()​ -> logger_write.c __write_to_log_daemon​() 从逻辑看,就是往socket不断的写log,写给谁呢?在__write_to_log_initialize()​ ​方法中会调用 config_write.c __android_log_config_write​(),初始化logdLoggerWrite​,从logd_write.c中可以看出,logdLoggerWrite​是个socket,
strcpy(un.sun_path, “/dev/socket/logdw”);
往socket里面写了,谁会去处理呢?
logd进程启动的源码中可知,LogListener​ listens on /dev/socket/logdw for client​,可见下面源码
// LogListener listens on /dev/socket/logdw for client
// initiated log messages. New log entries are added to LogBuffer
// and LogReader is notified to send updates to connected clients.

LogListener *swl = new LogListener(logBuf, reader);
LogListener是SocketListener类型,初始化完成后会runListener​,当socket中有信息时,执行流:
SocketListener::runListener​() -> LogListener::onDataAvailable​() -> LogBuffer::log()
在LogBuffer::log()方法中,new LogBufferElement​,(这部分就跟上面的backtrace对应上了),并将其添加至LogBufferElementCollection​中,在LogBufferElement​构造方法中,会根据msg信息new char[],将信息存在LogBuffer~
存起来的Log,什么时候会消费呢?
在代码中,调用LogBuffer::erase()会释放,从单词意思也能猜到,这个方法什么时候调用呢?
LogBuffer::maybePrune() -> LogBuffer::prune() -> LogBuffer::erase()​
而LogBuffer::maybePrune()​调用,只有在 LogBuffer::log()​,也就是说,上面的Java层调用Log.d()就可以触发这个逻辑,触发的条件在LogBuffer::maybePrune()​方法中

// Prune at most 10% of the log entries or maxPrune, whichever is less.
//
// mLogElementsLock must be held when this function is called.
void LogBuffer::maybePrune(log_id_t id) {
    size_t sizes = stats.sizes(id);
    unsigned long maxSize = log_buffer_size(id);
    if (sizes > maxSize) {
        size_t sizeOver = sizes - ((maxSize * 9) / 10);
        size_t elements = stats.realElements(id);
        size_t minElements = elements / 100;
        if (minElements < minPrune) {
            minElements = minPrune;
        }
        unsigned long pruneRows = elements * sizeOver / sizes;
        if (pruneRows < minElements) {
            pruneRows = minElements;
        }
        if (pruneRows > maxPrune) {
            pruneRows = maxPrune;
        }
        prune(id, pruneRows);
    }
}

从代码可知,当对应的LogBuffer size超过了maxSize​,进行裁剪log,释放一部分LogBufferElement​~其实到这可以看出,logd是log池,只有池子满了,才会主动释放一部分内存
系统中有多少LogBuffer呢?最大值又是多少呢?
LogBuffer::init()方法中,会对LogBuffer初始化,从初始化的过程看,有7个LogBuffer,分别是:

static const char *LOG_NAME[LOG_ID_MAX] = {
    [LOG_ID_MAIN] = "main",
    [LOG_ID_RADIO] = "radio",
    [LOG_ID_EVENTS] = "events",
    [LOG_ID_SYSTEM] = "system",
    [LOG_ID_CRASH] = "crash",
    [LOG_ID_SECURITY] = "security",
    [LOG_ID_KERNEL] = "kernel",
};

根据系统属性设置maxsize,从C1上看下跟logd相关的属性:
设置逻辑贴代码比较多,直接看一个说明文档的注释:
persist.logd.size number ro Global default size of the buffer for
all log ids at initial startup, at
runtime use: logcat -b all -G
ro.logd.size number svelte default for persist.logd.size. Larger
platform default sizes than 256KB are
known to not scale well under log spam
pressure. Address the spam first,
resist increasing the log buffer.
persist.logd.size. number ro Size of the buffer for log
ro.logd.size. number svelte default for persist.logd.size.
初始化过程执行完,每个LogBuffer的最大值如下:
main:2M
radio:4M
events:2M
system:4M
crash:1M
security:2M
kernel:2M

如果每个LogBuffer都填满,所有LogBuffer内存占用近20MB,再加上分配内存的中间开销,占用内存可能会非常大~

再来看下Mate9的logd相关设置:
Mate9没有设置LogBuffer的大小,如果不设置,LogBuffer大小使用默认值,即256KB
如果将C1的logd相关属性和Mate9保持一致后,测试结果如下:
内存最大值(KB) 内存最小值(KB) 内存平均值(KB)
C1 修改版 7894 4877 6610
Mate9 2804 2565 2719
​修改LogBuffer后的效果比较明显,内存值会下降很多,但同样会带来log丢失的问题。也就是 LogBuffer::prune()方法带来的问题
裁剪的主要逻辑:先删黑名单和Log数最多的UID对应的Log,而且先删oldest的log,如果仍不满足,再根据白名单的配置,白名单的不删除,其他都删除,直到满足条件

LogBuffer最大值意味着存放log信息的多少,比如我们生成bugreport时,会dumpstate,取system log,event log,radio log等信息,
LogBuffer比较小的话,取出来的信息会相对少一些,可能会对开发解问题造成影响。
也就说,改动前会看到2个小时前的event log,system log,改动配置后可能就会看到近20分钟的event log,system log

看一下实际情况,跑了几个小时的场景后,一台C1是16号开发版包,logd内存:46614K​,一个C1是修改logd后的开发版包,logd内存6794K,
抓取两个C1的bugreport
11-16开发版第一个bugreport
system log起始时间 event log起始时间 radio起始时间
15:00-17:52 15:20-17:52 10:27-17:5​2
11-16开发版第一个bugreport
system log起始时间 event log起始时间 radio起始时间
15:00-18:07 15:21-18:07 10:27-18:07
修改logd属性的11-16号开发版第一个bugreport
system log起始时间 event log起始时间 radio起始时间
17:26-17:53 17:33-17:53 15:48-17:53
修改logd属性的11-16号开发版第二个bugreport
system log起始时间 event log起始时间 radio起始时间
17:26-18:07 17:41-18:07 16:03-18:07
从数据可以看出,未做修改的版本system log会记录4个小时左右,event log会记录3个半小时左右,radio会记录7个半小时,而修改logd属性的版本system log会记录30分钟左右,
event log会记录20分钟左右,radio会记录2个小时,整体比未做修改的版本时间要短,符合预期

开发版上log比较多,如果是稳定版,由于log少情况会好一些~从数据看,也是可以接受

因此,建议将RAM较小的外发版机型,将LogBuffer size调小,或者直接调至256KB,将内存影响降至最低~

<think>好的,用户现在想了解如何在安卓系统上实现进程内存读取,并且希望看到相关的代码。之前我已经提供了Windows平台上的进程内存读取方案,现在需要调整到安卓环境。 首先,安卓系统与Windows不同,它基于Linux内核,所以内存读取的方法会有所区别。在Linux系统中,通常通过访问/proc/[pid]/mem文件来实现,但需要相应的权限。安卓系统可能还需要考虑root权限的问题,因为普通应用无法直接读取其他进程内存。 用户之前可能已经了解过Windows下的实现,现在想迁移到安卓,可能是因为他们正在开发跨平台工具或移动应用相关的功能。需要确保提供的方案在安卓上可行,并且考虑到不同版本的安卓系统可能存在的差异。 接下来,我需要考虑在安卓上实现进程内存读取的几种可能方法。常见的方法包括使用ptrace系统调用、直接读取/proc/[pid]/mem文件,或者利用安卓的NDK进行本地代码开发。由于用户提到后续切换成安卓代码,可能需要使用JNI和C/C++结合的方式。 需要注意的是,普通应用没有权限访问其他进程内存,除非设备已root。因此,代码示例可能需要运行在root过的设备上,或者应用具有root权限。这一点必须在回答中明确指出,避免用户在不具备条件的情况下尝试。 另外,安卓的权限管理比较严格,即使使用ptrace,也需要相应的权限配置。可能需要修改应用的AndroidManifest.xml文件,或者通过su命令获取超级用户权限。 接下来,我需要提供具体的代码示例。例如,使用C编写一个本地库,通过JNI在Java/Kotlin中调用。代码需要包括打开目标进程、读取内存的步骤。同时,要处理错误情况,如权限不足或内存不可读的情况。 还需要考虑不同架构的兼容性,比如ARM和x86的区别,以及如何编译本地代码为SO库。用户可能需要使用NDK来编译代码,并集成到安卓项目中。 最后,需要提醒用户注意安全性和合法性,修改或读取其他进程内存在很多情况下可能违反用户协议或法律法规,特别是在游戏或商业应用中。确保用户明确这些风险,并遵守相关规定。 总结步骤: 1. 分析安卓与Windows内存读取的差异。 2. 确定实现方法:ptrace或/proc文件系统。 3. 提供需要root权限的警告。 4. 编写C代码示例,使用ptrace或文件操作。 5. 集成到安卓应用中的方法,使用NDK和JNI。 6. 处理权限和错误情况。 7. 强调安全和法律问题。</think>以下是针对Android系统进程内存读取实现指南,包含从基础原理到具体代码的完整方案: --- ### **Android内存读取原理图** ``` [应用层] → JNI接口 → [Native层] ↓ ptrace()/proc文件系统 ↓ Linux内核内存管理 ``` --- ### **Android与Windows内存读取对比** | 特性 | Windows | Android | |----------------|-----------------------|----------------------| | 核心API | ReadProcessMemory | ptrace()/proc文件系统 | | 权限要求 | Administrator | root权限 | | 进程隔离机制 | 用户账户控制(UAC) | SELinux | | 典型实现方式 | Win32 API | NDK + JNI | --- ### **Android实现方案** #### 前置条件: 1. 已root的Android设备 2. 开启`ptrace`系统调用权限 3. 项目配置NDK开发环境 --- ### **方案一:使用ptrace系统调用** #### 1. 创建native-lib.cpp ```cpp #include <jni.h> #include <sys/ptrace.h> #include <unistd.h> #include <android/log.h> #define LOG_TAG "MemoryReader" #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__) extern "C" JNIEXPORT jbyteArray JNICALL Java_com_example_memreader_MemoryUtils_readMemory( JNIEnv* env, jobject /* this */, jint pid, jlong address, jint size ) { // 附加到目标进程 if (ptrace(PTRACE_ATTACH, pid, NULL, NULL) == -1) { LOGD("Attach failed: %s", strerror(errno)); return NULL; } waitpid(pid, NULL, 0); // 等待进程暂停 // 创建缓冲区 jbyteArray result = env->NewByteArray(size); jbyte* buffer = env->GetByteArrayElements(result, NULL); // 逐字读取内存 long* ptr = (long*)address; for (int i = 0; i < size/sizeof(long); i++) { long data = ptrace(PTRACE_PEEKDATA, pid, ptr + i, NULL); if (data == -1 && errno) { LOGD("Read error at %p: %s", ptr+i, strerror(errno)); break; } memcpy(buffer + i*sizeof(long), &data, sizeof(long)); } ptrace(PTRACE_DETACH, pid, NULL, NULL); // 解除附加 env->ReleaseByteArrayElements(result, buffer, 0); return result; } ``` #### 2. Java层封装 ```java public class MemoryUtils { static { System.loadLibrary("native-lib"); } public native static byte[] readMemory(int pid, long address, int size); // 示例:读取自身进程内存 public static void readSelfMemory() { long mallocAddr = 0x12345678L; // 示例地址 byte[] data = readMemory(android.os.Process.myPid(), mallocAddr, 4); Log.d("Memory", "Read value: " + bytesToInt(data)); } private static int bytesToInt(byte[] bytes) { return ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).getInt(); } } ``` --- ### **方案:通过/proc文件系统** ```cpp extern "C" JNIEXPORT jbyteArray JNICALL Java_com_example_memreader_MemoryUtils_procRead( JNIEnv* env, jobject, jint pid, jlong address, jint size ) { char mem_path[32]; sprintf(mem_path, "/proc/%d/mem", pid); FILE* mem_file = fopen(mem_path, "r"); if (!mem_file) { LOGD("Open /proc/%d/mem failed: %s", pid, strerror(errno)); return NULL; } // 定位到目标地址 if (fseek(mem_file, address, SEEK_SET) != 0) { fclose(mem_file); return NULL; } // 读取数据 jbyteArray result = env->NewByteArray(size); jbyte* buffer = env->GetByteArrayElements(result, NULL); size_t read = fread(buffer, 1, size, mem_file); fclose(mem_file); env->ReleaseByteArrayElements(result, buffer, 0); return (read == size) ? result : NULL; } ``` --- ### **Android实现关键点** 1. **权限配置**: ```xml <!-- AndroidManifest.xml --> <uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.ACCESS_SUPERUSER"/> ``` 2. **Root权限获取**: ```java public static boolean requestRoot() { Process process = null; try { process = Runtime.getRuntime().exec("su"); DataOutputStream os = new DataOutputStream(process.getOutputStream()); os.writeBytes("exit\n"); os.flush(); process.waitFor(); return (process.exitValue() == 0); } catch (Exception e) { return false; } } ``` 3. **地址获取方法**: - 通过`/proc/[pid]/maps`解析内存布局 ```cpp void parse_maps(int pid) { char path[32]; sprintf(path, "/proc/%d/maps", pid); FILE* maps = fopen(path, "r"); while(fgets(line, sizeof(line), maps)) { // 示例行:00400000-0040a000 r-xp 00000000 08:01 1077152 /bin/cat sscanf(line, "%lx-%lx", &start, &end); } fclose(maps); } ``` --- ### **Android内存读取限制对策** | 限制类型 | 解决方案 | |----------------|----------------------------| | SELinux限制 | 修改`/system/etc/selinux`配置 | | 非root环境 | 使用`debuggable`进程注入 | | ASLR地址随机化 | 通过maps文件计算基址偏移 | | 内存保护标记 | 临时修改`/proc/[pid]/mem`权限 | --- ### **完整工作流程** 1. 获取root权限 2. 查找目标进程PID ```shell adb shell ps | grep com.target.app ``` 3. 解析内存映射 ```java // 读取/proc/[pid]/maps获取模块基地址 ``` 4. 计算动态地址 ```java long finalAddress = baseAddress + offset; ``` 5. 执行内存读取 ```java byte[] data = MemoryUtils.readMemory(pid, finalAddress, 4); ``` --- ### **高级技巧:内存监控** ```cpp // 使用inotify监控/proc/[pid]/maps变化 int fd = inotify_init(); inotify_add_watch(fd, "/proc/目标pid/maps", IN_MODIFY); while(true) { struct inotify_event* event = read(fd); if(event->mask & IN_MODIFY) { // 触发内存布局变化回调 } } ``` --- ### **安全警告** 1. 修改系统内存可能导致应用崩溃 2. 频繁内存读取可能触发安全检测 3. 部分厂商系统(如华为EMUI)会限制/proc访问 4. Android 11+ 对非SDK接口的限制增强 建议仅在root后的测试设备上使用这些方法,并遵守《移动终端安全技术规范》等相关法规。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值