一、C 语言在 Android 中的不可替代性 尽管 Android NDK 开发越来越多地使用 C++,C 语言仍然是系统编程的基石:
Linux 内核全部用 C 编写 。Android 基于 Linux 内核,内核模块和驱动都是 C。
Android 的 Bionic libc 是用 C 实现的。所有系统调用(open、read、write、mmap、ioctl 等)的封装都是 C。
性能关键路径 。ART 运行时的垃圾回收(GC)、JIT 编译器的代码生成、HAL 层的硬件通信,都是 C/汇编。
ABI 稳定性 。C 的 ABI 比 C++ 稳定得多。JNI 的接口是 C 接口(JNIEnv 是 C 函数指针表)。跨语言 FFI(Foreign Function Interface)几乎都基于 C ABI。
二进制体积 。C 运行时极简(Android 的 libc.so 约 500KB),而 C++ 的 STL 会增加显著体积。
AOSP 源码中 bionic/ 目录(Android 的 C 库)和 system/core/ 中的大量工具都用 C 编写。
二、函数指针与回调 函数指针是 C 语言实现多态和回调的基础机制:
#include <stdio.h> typedef int (*compare_func_t ) (const void *, const void *) ;void sort (void *base, size_t nmemb, size_t size, compare_func_t cmp) { } int compare_int (const void *a, const void *b) { return (*(int *)a - *(int *)b); } int compare_string (const void *a, const void *b) { return strcmp (*(const char **)a, *(const char **)b); } int main () { int arr[] = {5 , 2 , 8 , 1 , 3 }; sort(arr, 5 , sizeof (int ), compare_int); char *strs[] = {"banana" , "apple" , "cherry" }; sort(strs, 3 , sizeof (char *), compare_string); return 0 ; }
在 Android 的 JNI 环境中,函数指针常用于 native 层的回调注册:
typedef void (*on_data_received_cb) (const uint8_t *data, size_t len, void *user_data) ;typedef struct { on_data_received_cb callback; void *user_data; } DataListener; void register_listener (DataListener *listener, on_data_received_cb cb, void *user_data) { listener->callback = cb; listener->user_data = user_data; } void on_packet_arrived (DataListener *listener, const uint8_t *packet, size_t len) { if (listener->callback) { listener->callback(packet, len, listener->user_data); } }
2.1 函数指针数组——状态机 typedef enum { STATE_IDLE, STATE_CONNECTING, STATE_CONNECTED, STATE_CLOSING, } ConnectionState; typedef void (*state_handler_t ) (void *context) ;static state_handler_t state_handlers[] = { [STATE_IDLE] = handle_idle, [STATE_CONNECTING] = handle_connecting, [STATE_CONNECTED] = handle_connected, [STATE_CLOSING] = handle_closing, }; void dispatch_state (ConnectionState state, void *context) { if (state < sizeof (state_handlers) / sizeof (state_handlers[0 ])) { state_handlers[state](context); } }
三、内存布局与四区模型 理解 C 语言的内存布局是调试 native crash 和内存相关 bug 的基础。Android 进程的内存空间(虚拟地址空间)布局如下:
高地址 ┌──────────────────┐ │ Kernel Space │ (3GB-4GB in 32-bit, vast in 64-bit) ├──────────────────┤ │ Stack (↓下增) │ 局部变量、函数返回地址 │ ... │ │ ↓ │ │ │ │ ↑ │ │ ... │ │ Heap (↑上增) │ malloc / free ├──────────────────┤ │ BSS (未初始化) │ 未初始化/零初始化的全局变量和静态变量 ├──────────────────┤ │ Data (已初始化) │ 已初始化的全局变量和静态变量 ├──────────────────┤ │ Text (代码段) │ 可执行指令(只读) └──────────────────┘ 低地址
四区模型 :
区域
存储内容
生命周期
管理方式
栈 (Stack)
局部变量、函数参数、返回地址
函数返回时自动销毁
编译器自动管理
堆 (Heap)
malloc/calloc/realloc 分配的内存
手动 free 或进程退出
程序员显式管理
静态区 (Data/BSS)
全局变量、static 变量
程序整个运行期
编译器分配
代码区 (Text)
可执行指令、只读数据(字符串常量)
程序整个运行期
只读
int global_initialized = 42 ; int global_uninitialized; static int static_var = 100 ; const char *msg = "Hello" ; void demo () { int local = 10 ; static int counter = 0 ; counter++; char *heap_mem = (char *)malloc (1024 ); free (heap_mem); }
在 Android NDK 开发中,常见的崩溃类型与内存布局直接相关:
Stack Overflow :递归太深或局部数组过大(Android 默认线程栈大小约 1MB)。
Heap Corruption :double free、use-after-free、buffer overflow。
Segmentation Fault(SIGSEGV) :访问 NULL 指针、访问已释放的内存、写只读内存。
四、位运算与位域 C 语言的位运算是嵌入式开发和协议解析的利器。Android 的 Binder 协议、HAL 接口标志位、编解码器的比特流操作都大量使用位运算。
#define FLAG_A (1 << 0) #define FLAG_B (1 << 1) #define FLAG_C (1 << 2) uint32_t flags = 0 ;flags |= FLAG_A; flags |= (FLAG_B | FLAG_C); flags &= ~FLAG_A; if (flags & FLAG_B) { } flags ^= FLAG_C; uint8_t extracted = (flags >> 4 ) & 0x0F ;size_t aligned = (size + 3 ) & ~3 ;
位域(Bit Field)可以更自然地表达硬件寄存器和协议头:
struct ip_header { uint8_t version_ihl; uint8_t dscp_ecn; uint16_t total_length; uint16_t identification; uint16_t flags_fragment; uint8_t ttl; uint8_t protocol; uint16_t header_checksum; uint32_t source_ip; uint32_t dest_ip; }; struct ip_header_bitfield { unsigned int ihl : 4 ; unsigned int version : 4 ; unsigned int ecn : 2 ; unsigned int dscp : 6 ; }; #define IP_VERSION(hdr) (((hdr)->version_ihl & 0xF0) >> 4) #define IP_IHL(hdr) ((hdr)->version_ihl & 0x0F)
五、C 预处理器宏 宏是 C 语言元编程的手段。在 Android 底层代码中非常常见:
#define MAKE_FUNC(name) native_##name #define STRINGIFY(x) #x #define TO_STRING(x) STRINGIFY(x) #define SAFE_FREE(p) do { \ if ((p) != NULL) { \ free(p); \ (p) = NULL; \ } \ } while (0) #ifdef DEBUG #define LOG_TAG "NativeLib" #include <android/log.h> #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__) #define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__) #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) #else #define LOGD(...) ((void)0) #define LOGW(...) ((void)0) #define LOGE(...) ((void)0) #endif #define STATIC_ASSERT(cond) _Static_assert(cond, #cond) STATIC_ASSERT(sizeof (int ) == 4 );
六、setjmp/longjmp 与异常处理 C 语言没有像 C++ 的 try-catch 异常机制,但 setjmp/longjmp 提供了非本地跳转(non-local goto),可以实现异常安全:
#include <setjmp.h> static jmp_buf g_error_jmp;void process_data (const char *filename) { if (setjmp(g_error_jmp) != 0 ) { printf ("Error occurred during processing, rolling back...\n" ); rollback_transaction(); return ; } open_file(filename); parse_header(); process_body(); } void parse_header () { if (header_corrupted()) { longjmp(g_error_jmp, 1 ); } }
实际中,Android 的 libpng(PNG 解码库)、libjpeg-turbo(JPEG 编解码器)等 C 库内部使用 setjmp/longjmp 实现错误处理。当解码失败时,从深层递归/循环中直接跳回错误处理点,避免了逐层返回和检查返回值的繁琐。
注意:longjmp 不会调用 C++ 对象的析构函数(不会栈展开),所以在 C++ 中使用需要特别小心。在 C 中,使用 setjmp/longjmp 时需要确保不会泄漏已分配的资源(通常在 setjmp 调用处维护一个资源列表用于回滚)。
七、pthread 线程基础 Android 的 Bionic 完整支持 POSIX 线程(pthread)。虽然 Android NDK 可以使用 C++11 的 std::thread,但 pthread 仍然是系统级线程操作的基础:
#include <pthread.h> #include <android/log.h> typedef struct { int thread_id; const char *name; void *(*start_routine)(void *); void *arg; } ThreadParams; static void *thread_entry (void *arg) { ThreadParams *params = (ThreadParams *)arg; pthread_setname_np(pthread_self(), params->name); __android_log_print(ANDROID_LOG_INFO, "NativeThread" , "Thread %d '%s' started" , params->thread_id, params->name); void *result = params->start_routine(params->arg); __android_log_print(ANDROID_LOG_INFO, "NativeThread" , "Thread %d '%s' finished" , params->thread_id, params->name); return result; } int create_thread (ThreadParams *params) { pthread_t thread; pthread_attr_t attr; pthread_attr_init(&attr); pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); int ret = pthread_create(&thread, &attr, thread_entry, params); pthread_attr_destroy(&attr); return ret; } static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;static int ready = 0 ;void producer () { pthread_mutex_lock(&mutex); ready = 1 ; pthread_cond_signal(&cond); pthread_mutex_unlock(&mutex); } void consumer () { pthread_mutex_lock(&mutex); while (!ready) { pthread_cond_wait(&cond, &mutex); } pthread_mutex_unlock(&mutex); }
八、面试常问题目 Q1: 栈(Stack)和堆(Heap)的区别?在 Android NDK 中如何选择?
栈由编译器自动管理,分配极快(移动栈指针),函数返回时自动回收,但空间有限(Android 中每个线程约 1MB 默认栈大小)。堆由 malloc/free 手动管理,分配较慢(需要查找空闲块),空间大(受限于进程虚拟地址空间),但容易内存泄漏和碎片化。规则:小的固定大小对象放栈上,大的动态大小对象放堆上。Android NDK 中大型数组(如解码后的图片缓冲区)必须用堆分配。
Q2: 函数指针和回调的典型应用场景?
函数指针在 C 中用于实现策略模式和多态:(1) 排序/查找算法的比较函数(qsort、bsearch);(2) 事件驱动的回调注册(JNI 回调 Java 方法本质就是函数指针+JNI 环境);(3) 状态机的状态转移表;(4) 插件系统的接口抽象(动态库加载通过 dlsym 获取函数指针);(5) 信号处理函数(signal handler)。
Q3: C 的宏和 C++ 的模板有什么区别?
宏是预处理器文本替换,没有类型检查,容易产生边界效应(需要用括号保护参数和整体),调试困难(编译错误指向展开后的代码)。模板是编译器层面的代码生成,有类型检查,生成的代码经过了完整的语义分析,错误信息更有意义。宏可以生成任意的代码文本(包括控制流和声明),模板主要适用于泛型算法和类型安全的容器。在 C 语言中宏是唯一的代码生成手段,在 C++ 中优先使用模板。
Q4: Android Bionic libc 和 glibc 有什么区别?
Bionic 是 Android 定制的 C 库(源码路径:bionic/libc/),专为移动设备优化。主要区别:(1) Bionic 体积更小(~500KB vs glibc 的几 MB);(2) Bionic 没有完整的 locale 支持;(3) Bionic 的 pthread 实现简化(基于 futex 而非 NPTL);(4) Bionic 对一些 POSIX 函数没有实现或有限制(如 system() 在某些 Android 版本被限制);(5) Bionic 的 DNS 解析集成到 netd 守护进程。这些区别使得一些 Linux 程序移植到 Android 时需要额外适配。
参考源码路径:
Bionic libc:bionic/libc/
Bionic pthread:bionic/libc/bionic/pthread_create.cpp
Bionic malloc (jemalloc/scudo):bionic/libc/bionic/jemalloc_wrapper.cpp
Linux 内核内存管理:kernel/msm-*/mm/
AOSP 原生工具:system/core/toolbox/
Binder 协议定义:frameworks/native/libs/binder/