13. NDK API与系统库:log库的使用、Android系统库列表、链接系统库、__android_log_print()详解

各位同学,欢迎来到第13章。今天咱们聊聊NDK开发里最基础、也最离不开的东西——系统库和日志打印。

说实话,我刚开始做NDK开发那会儿,最头疼的就是调试。Java层有Logcat,C/C++层呢?啥也没有。代码崩了,连个错误信息都看不到。后来才知道,原来Android系统给NDK开发者准备了一套完整的系统库,其中就包括log库。

这一章,我会把Android系统库的体系、怎么链接它们、以及最常用的日志API讲清楚。嗯,内容有点多,但都是干货。

13.1 Android系统库列表

Android系统库,说白了就是系统预编译好的.so文件。它们放在设备的/system/lib或/system/lib64目录下。NDK开发时,我们通过头文件和链接器来使用它们。

我整理了一份最常用的系统库列表,你写代码时大概率都会碰到:

库名 头文件 功能说明
liblog.so android/log.h 日志输出到Logcat
libz.so zlib.h 数据压缩/解压缩
libdl.so dlfcn.h 动态加载.so文件
libm.so math.h 数学函数库
libc.so 标准C头文件 C标准库(Bionic实现)
libc++.so C++标准头文件 C++标准库
libEGL.so EGL/egl.h OpenGL ES窗口管理
libGLESv2.so GLES2/gl2.h OpenGL ES 2.0图形库
libOpenSLES.so SLES/OpenSLES.h 音频处理
libjnigraphics.so android/bitmap.h 直接操作Java Bitmap像素

这里有个细节要注意:Android的libc不是glibc,而是Bionic。Bionic是Google专门为Android定制的C标准库,体积更小、性能更好。但有些glibc特有的函数,Bionic里没有。我在移植一个Linux库到Android时,就踩过这个坑——代码里用了glibc的某些扩展函数,编译直接报错。

13.2 链接系统库的两种方式

在CMake里链接系统库,其实就两种方式。我分别说说。

13.2.1 使用find_library + target_link_libraries

这种方式最规范,也是Google官方推荐的。CMake会去NDK的sysroot里找对应的库文件。

# 在CMakeLists.txt中
find_library(log-lib log)
find_library(z-lib z)

target_link_libraries(
    my_native_lib
    ${log-lib}
    ${z-lib}
)

我个人习惯用这种方式。为什么呢?因为CMake会自动处理库的路径和ABI适配。你换不同架构编译时,它自动找对应的.so,省心。

13.2.2 直接指定库名

如果你确定库名不会变,也可以直接写:

target_link_libraries(
    my_native_lib
    log
    z
)

CMake会自动在前面加lib、后面加.so。说白了就是语法糖。但我不建议新手这么写——万一哪天库名改了,你找错都不好找。

我的小技巧: 在CMakeLists.txt里,我习惯把系统库的链接放在一个变量里统一管理。比如 set(SYSTEM_LIBS log z dl m),然后target_link_libraries时直接引用这个变量。这样代码更整洁,也方便后期维护。

13.3 log库的使用

好了,重点来了。log库是NDK开发者的「眼睛」。没有它,你连程序跑没跑起来都不知道。

13.3.1 引入头文件

使用log库前,先包含头文件:

#include <android/log.h>

这个头文件里定义了日志相关的宏和函数。嗯,其实核心就一个函数——__android_log_print。

13.3.2 __android_log_print() 详解

这个函数的签名如下:

int __android_log_print(
    int prio,           // 日志优先级
    const char* tag,    // 日志标签
    const char* fmt,    // 格式化字符串,类似printf
    ...                 // 可变参数
);

返回值是写入的字节数,失败返回负数。不过说实话,我很少检查这个返回值——日志打印失败的概率极低。

prio参数有以下几个可选值,定义在android/log.h中:

优先级常量 对应Logcat级别 使用场景
ANDROID_LOG_VERBOSE V 最详细的调试信息,发布时通常关闭
ANDROID_LOG_DEBUG D 开发调试用,发布时建议关闭
ANDROID_LOG_INFO I 普通信息,比如模块初始化完成
ANDROID_LOG_WARN W 警告,程序还能跑但可能有隐患
ANDROID_LOG_ERROR E 错误,功能不可用
ANDROID_LOG_FATAL F 致命错误,程序即将崩溃

看个完整示例:

#include <android/log.h>

#define LOG_TAG "MyNativeLib"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)

void process_data(int value) {
    LOGD("process_data called with value = %d", value);
    
    if (value < 0) {
        LOGE("Invalid value: %d, must be non-negative", value);
        return;
    }
    
    // 正常处理逻辑
    LOGD("Processing value %d successfully", value);
}

这里我用了宏定义来简化调用。LOGD和LOGE分别对应调试和错误日志。你想想看,如果每次都要写ANDROID_LOG_DEBUG和LOG_TAG,多累啊。

我曾经踩过的坑: 在正式发布版本里,一定要把DEBUG级别的日志去掉。怎么去?可以在CMake里加一个编译选项,通过#ifdef控制。比如:
#ifdef NDEBUG
    #define LOGD(...) ((void)0)
#else
    #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#endif
这样Release构建时,LOGD就变成了空操作,不会产生任何性能开销。

13.4 知识体系结构图

下面这张图,我把本章的核心知识点串起来了。你看一遍,心里就有谱了。

NDK系统库与日志API知识体系 NDK系统库 系统库列表 liblog.so libz.so libdl.so libm.so libc.so libc++.so 链接系统库 find_library 直接指定库名 log库API __android_log_print 优先级常量 tag标签 格式化输出 掌握系统库 = 掌握NDK开发的基石

13.5 实战:在CMake中集成log库

光说不练假把式。咱们写一个完整的CMakeLists.txt,把log库用起来。

cmake_minimum_required(VERSION 3.18)
project("logdemo")

# 设置C++标准
set(CMAKE_CXX_STANDARD 17)

# 查找系统库
find_library(log-lib log)
find_library(z-lib z)

# 添加自己的库
add_library(
    native-log
    SHARED
    src/main/cpp/native_log.cpp
)

# 链接系统库
target_link_libraries(
    native-log
    ${log-lib}
    ${z-lib}
)

# 指定头文件路径(如果有自己的头文件)
target_include_directories(
    native-log
    PRIVATE
    src/main/cpp/include
)

对应的C++代码:

// native_log.cpp
#include <jni.h>
#include <android/log.h>

#define TAG "NativeLog"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)

extern "C" JNIEXPORT void JNICALL
Java_com_example_logdemo_MainActivity_nativeInit(JNIEnv* env, jobject thiz) {
    LOGI("Native initialization started");
    
    // 模拟一些操作
    int result = 42;
    LOGI("Computation result: %d", result);
    
    // 模拟错误
    if (result < 0) {
        LOGE("Unexpected negative result: %d", result);
    }
    
    LOGI("Native initialization completed");
}

这段代码编译后,你在Logcat里过滤"NativeLog"标签,就能看到输出的日志了。

核心要点:
  • 系统库是Android系统预编译的.so,通过NDK头文件调用
  • CMake链接系统库推荐用find_library + target_link_libraries
  • __android_log_print是NDK日志的核心API,支持printf风格的格式化
  • 用宏封装日志调用,可以统一管理tag和优先级
  • 发布版本务必去掉DEBUG日志,避免性能损耗和信息泄露

好了,这一章的内容就到这儿。系统库这块儿,说白了就是「知道有哪些、知道怎么用、知道怎么链接」。你把这些搞清楚了,后面遇到OpenGL、音频处理、动态加载这些高级话题时,就不会觉得陌生了。


公众号:蓝海资料掘金营,微信deep3321