Bionic API 入门指南

527 阅读23分钟

上一章学习如何log、debug和分析代码。本章中将会学习Android NDK提供的native API。

Bionic是Android提供的符合POSIX标准由C/C++实现的C库。Bionic是Google为Android实现的BSD标准的C库。Bionic的名字来源于BSD C 库和Linux处理线程、进程和signal的混合。

Bionic对于native应用开发至关重要,因为他提供了在Android平台上开发native代码最基础的API。在接下来的章节中,会接触到Bionic提供的各种各样的API。在使用具体的API之前,先大体看一下这些标准库。

标准库预览

标准库提供了编程语言常用的构造、算法、数据结构和构筑于硬件和操作系统的抽象接口,例如网络访问、多线程、内存管理和文件IO。依赖于编程语言本身的设计哲学,标准库之间有很多不同。标准库为应用开发提供了很多方便。

每种语言都有标准库。Java的Java Class Library(JCL)提供了排序,字符串操作和IO等。Android framework也基于JCL提供了应用开发的标准库。

对于C语言,ANSI C标准定义了标准库的范围。这个标准库就是C standard library,简写libc。实现C语言伴随着实现libc。除了libc实现,POSIX C 库声明了额外的构造用于POSIX兼容系统。

另一个C库?

Google创建了一个新的C库而不是使用现成的GNU C Library(glibc)或者C Library for Embedded Linux(uClibc)的原因可以总结为以下三点:
  1. License:glibc和uClibc都是LGPL协议,这限制了他们只能被使用在专有的应用。Bionic是BSD协议,不会限制任何使用
  2. Speed:Bionic为移动计算而生。专门为小型CPU和有限的内存设备打造。
  3. Size:Bionic将简单的哲学贯彻到底。它通过轻量封装和更少的API提供了对内核的访问,和其他实现相比更轻量。

二进制兼容性

尽管也是一个C标准库,但是Bionic和其他C标准库并不兼容。其他C库生成的文件和静态库并不能动态加载到Bionic中。

此外,单独静态链接其他C标准库的代码(不和Bionic混合)可以正常运行在Android上,但是如果在运行时动态加载库就不行了。

提供的功能

Bionic提供了C标准库中的宏,类型定义,方法和一些Android专用的功能:
  1. Memory Management: 内存管理
  2. File Input and Output: 文件IO
  3. String Manipulation:字符串操作
  4. Mathematics:数学库
  5. Date and Time:时间
  6. Process Controll:进程控制
  7. Signal Handler:信号控制
  8. Socket Networking:Socket网络
  9. Multithreading:多线程
  10. Users and Groups:用户和用户group
  11. System Configuration:系统配置
  12. Name Service Switch:命名服务切换?

缺失了什么?

Bionic是为Android专门设计的,所以不是所有的C library中的方法都支持。Android NDK 文档提供了缺失的方法列表,但是这样的信息也能在头文件中找到。Bionic头文件在ANDROID_NDK_HOME文件夹下platforms/android-/arch-/usr/include文件夹中。

这个文件夹中的每一个头文件都清楚地标明了缺失的方法。例如,stdio.h中缺失的方法表示如下:

#if 0 /* MISSING FROM BIONIC */
char *ctermid(char *); 
char *cuserid(char *); 
#endif /* MISSING */

头文件中if 预处理指令用于disable后面的两行代码,并且相关的comment提示了缺失的方法。此外,Android NDK documentation也引用了那些在Bionic中只实现了很少一部分或者没有实现的方法(Stub)。

知识点:Stub是指实现不完整的方法。

Memory Management 内存管理

内存是进程能够获取的基础资源。对Java应用来说,内存由JVM管理。内存在new对象的时候分配,在GC的时候自动回收。但是,在native代码中,应用需要自行管理拥有的内存。正确的管理内存非常重要,否则内存紧张会导致应用稳定性和性能表现。

Memory Allocation 内存分配

C/C++支持三种形式的内存分配:
  1. Static allocation(静态分配):代码中定义的静态和全局变量就是应用启动的时候静态分配的。
  2. Automatic allocation(自动分配):方法传参和局部变量在方法调用的时候被自动分配 ,在方法结束的时候被自动释放。
  3. Dynamic allocation(动态分配):静态和自动分配都基于需要的内存区域是固定大小的,这个大小可以在编译期间被确定。动态分配用于运行时动态确定大小的内存。

接下来主要关注C/C++中的动态分配。

C动态分配内存管理

C语言本身没有提供内置的动态内存管理。Bionic C library提供了C代码动态内存管理的方法。

C动态内存分配

C可以通过libc中的`malloc` 方法在运行时动态分配内存:
 void* malloc(size_t size);

使用这个方法需要导入stdlib.h 头文件。malloc需要一个表明需要分配内存大小的参数,单位是byte,然后返回一个指针指向这个新分配的内存:

/* Include standard C library header. */ 
#include < stdlib.h> 
... 
    /* Allocate an integer array of 16 elements. */ 
    int* dynamicIntArray = (int*) malloc(sizeof(int) * 16);
if (NULL == dynamicIntArray) { 
    /* Unable to allocate enough memory. */ 
    ... 
    } else { 
    /* Use the memory through the integer pointer. */ 
    *dynamicIntArray = 0; 
    dynamicIntArray[8] = 8; 
    ...
    /* Free the memory allocation. */ 
    free(dynamicIntArray); 
    dynamicIntArray = NULL; 
}

提示:malloc是以byte为单位分配内存的,通过sizeof可以返回内存中数据类型(如int)的大小

如果无法申请指定大小的内存,malloc会返回NULL来表明申请失败。所以调用malloc后要通过返回值检查分配是否成功。一旦申请内存成功,动态申请内存可以通过指针来操作这个内存,直到这块内存被释放。

C释放动态内存

动态申请的内存在应用不在需要的时候需要被手动释放掉。standard C library(libc)中的`free` 方法就是用于动态释放内存的:
void free(void* memory);

free 方法需要传入一个之前动态申请内存返回的指针:

 int* dynamicIntArray = (int*) malloc(sizeof(int) * 16); 
 ... 
 /* Use the allocated memory. */ 
 ... 
free(dynamicIntArray);
 dynamicIntArray = NULL;

注意,即使是在释放掉内存后,指向内存的指针dynamicIntArray 仍然指向原来的内存地址。后续通过这个指针访问内存会引起segmentation violation。在释放完内存后将立刻这个指针置为NULL防止后续不经意使用到是一个非常好的习惯。

C中改变已动态分配的内存

一旦内存动态分配完毕,如果需要改变它的大小,就需要通过standard C library提供的`realloc` 方法:
void* realloc(void* memory, size_t size);

根据传入的size动态分配内存扩大或者缩小。这个方法需要传入两个参数,原始分配内存和新的size:

 int* dynamicIntArray = (int*) malloc(sizeof(int) * 16); 

int* newDynamicIntArray = (int*) realloc(
    dynamicIntArray, sizeof(int) * 32);
if (NULL == newDynamicIntArray) { 
    /* Unable to reallocate enough memory. */ 
    ... 
    } else { 
    /* Update the memory pointer. */ 
    dynamicIntArray = newDynamicIntArray; 
    ... 
    }

realloc 返回指向重新分配的内存指针。这个方法会在保留内存中值的情况下可能在内存空间中申请新地址,在这种情况下会返回新的地址。如果方法调用失败,原来的内存不变,返回NULL。

C++动态内存管理

C++提供内置的动态内存管理支持。在C++中可以使用`new`和`delete`关键字来管理动态内存。

当使用C++的时候,推荐使用C++中的关键字而不是standard C library(libc)来管理内存对象。和libc中的方法不一样,C++通过关键字的内存管理方法是类型可感知的并且支持C++对象生命周期。为了动态分配内存,new 关键字会调用C++ class的构造函数,与之相对,delete 关键字会调用class的析构函数来释放内存。

C++动态分配内存

C++通过new data type来动态分配内存:
int* dynamicInt = new int;
if (NULL == dynamicInt) { 
    /* Unable to allocate enough memory. */ 
    ... 
} else { 
    /* Use the allocated memory. */ 
    *dynamicInt = 0; 
    ... 
}

如果需要申请一个array,后面需要跟上[数组大小]:

int* dynamicIntArray = new int[16];
 if (NULL == dynamicIntArray) { 
     /* Unable to allocate enough memory. */ 
     ... 
 } else { 
     /* Use the allocated memory. */ 
     dynamicIntArray[8] = 8; 
     ... 
 }

C++释放动态内存

动态申请内存需要手动通过C++ `delete` 关键字释放内存:
delete dynamicInt;
dynamicInt = 0;

delete[] dynamicIntArray;
dynamicIntArray = 0;

注意,释放单个对象和数组的方法deletedelete[] ,错误地使用释放方法会导致内存泄漏。

C++改变已动态分配内存

C++并未像C那样提供内置的动态重新分配内存支持。内存的分配基于数据类型的大小和数据的数量。如果在运行过程中需要动态的改变数据的数量,推荐使用Standard Template Library(STL)中的集合类。

内存分配和释放的方法需要匹配

`malloc`申请的内存需要通过`free` 释放,`new` 申请的内存需要通过`delete` 释放。开发者需要严格的按照上面的匹配来进行内存管理,否则会造成未知的异常。

Standard File I/O 标准文件IO

native代码可以通过standard C library(libc)提供的Standard File I/O(stdio)方法访问文件系统。standard C library(libc)提供了两种I/O:
  1. Low-level I/O:原始的I/O方法有着对数据源更精细的访问
  2. Stream I/O: 更高等级适合处理数据流的有buffer的I/O

Stream I/O更加灵活,通常处理文件也更加方便。本章将会重点关注Stream I/O,Low-level I/O将会在后续章节的socket里面部分介绍。

Standard Stream

native代码有三个已有的stream来使用stream I/O。这些 stream代表着native应用的标准input和output渠道。他们分别在下面三个头文件中定义:
  1. stdin : 应用Standard input stream
  2. stdout: 应用Standard output stream
  3. stderror: 应用Standard error stream

因为native应用作为Android这样一个图形化界面系统的一个模块运行,stream显得不那么有用。但是集成已有的代码时,开发者需要搞清楚图形化界面背后的各种标准stream对于解决问题会非常有帮助。

使用Stream I/O

Stream I/O的构造和方法在standard C library(libc)的`stdio.h` 中定义。为了在native代码中使用stream I/O,需要再代码中导入该头文件:
 #include <stdio.h>

由于历史原因,在standard C library中代表stream的数据结构叫做FILE ,而不是stream。FILE对象中包含了stream I/O链接的所有需要的信息。FILE对象通过stream I/O方法创建和维护,并且一般不会直接通过应用的代码交互。

打开Stream

通过stream的`fopen` 方法,stream可以访问一个新的或者已存在的文件。`fopen` 方法需要两个参数:文件的名称和打开方式,并且返回一个指向stream的指针:
FILE* fopen(const char* filename, const char* opentype);

fopen的第二个参数是一个用于控制文件如何被打开的标识字符串。包括以下类型:

  1. r:以只读模式打开已经存在的文件
  2. w:以只写模式打开文件。如果文件已经存在,内容会被清空,大小变为0
  3. a:以append模式打开文件。文件的内容会得到保存,新的输出内容会append到文件的末尾。如果文件不存在,会创建一个新文件。
  4. r+:以读-写模式打开文件
  5. w+:以读-写模式打开文件。如果文件已经存在,内容会被清空,大小为0。
  6. a+:以读和append模式打开文件。读的时候文件的起始位置在文件开头,append的时候文件起始位置在文件末尾。

注意:如果文件以r+、w+或者a+这三种双重模式打开文件时,在读和写切换的时候要调用fflush 方法清空buffer。

如果文件在指定的模式下打开失败,fopen 方法返回NULL指针。在成功的情况下返回一个FILE指针指向stream:

 #include <stdio.h> 
 ... 
FILE* stream = fopen("/data/data/com.example.hellojni/test.txt", "w");
 if (NULL == stream) 
 { 
 /* File could not be opened for writing. */ 
 } 
 else 
 { 
 /* Use the stream. */ 
 /* Close the stream. */ 
 }

一旦stream打开,就会通过它读写直到这个stream close。

向Stream写入

Stream I/O提供了四个写方法。接下来会简单过一遍这些方法:
向Stream写入数据块
`fwrite` 方法用于向Stream中写入数据块:
 size_t fwrite(const void* data, size_t size, size_t count, FILE* stream);

下面的代码显示了向stream写入count个每个大小为sizeof的data buffer:

char data[] = { 'h', 'e', 'l', 'l', 'o', '\n' }; 
 size_t count = sizeof(data) / sizeof(data[0]); 
/* Write data to stream. */
if (count ! = fwrite(data, sizeof(char), count, stream))
 { 
 /* Error occured while writing to stream. */ 
 }

方法会返回最终向stream中写入的元素数量。在成功的情况下,返回的个数等于写入的个数。

向Stream中写入字符串
`fputs`方法可以向stream中写入null结尾的字符串:
int fputs(const char* data, FILE* stream);

/* Writing character sequence to stream. */
if (EOF == fputs("hello\n", stream))
 { 
 /* Error occured while writing to the stream. */ 
 }

如果字符串写入失败,fputs 方法返回EOF。

向stream写入单个字符
`fputc`方法可以向stream写入单个字符或者byte:
 int fputc(int c, FILE* stream);

fputc 方法把单个字符c当成一个integer并且在写入stream之前转化成一个unsigned char:

char c = 'c'; 
/* Writing a single character to stream. */
if (c ! = fputc(c, stream))
{ 
 /* Error occured while writing character to string. 
}

如果向stream写入character成功,fputc 方法返回character本身,否者返回EOF。

向Stream写入格式化数据
`fpintf` 方法可以被用于向stream写入格式和可变数量的参数的数据:
int fprintf(FILE* stream, const char* format, ...);

fpintf 需要传入一个指向stream的指针,格式化字符串和可变数量(格式化字符串中指定的数量)的参数。格式化字符串中包含普通的字符和格式化占位符。在后续的stream写入中普通字符保持不变。格式化占位符会被fprintf 方法替换成为相应的参数。常见的占位符如下:

  1. %d,%i: 格式化integer为signed decimal
  2. %u:格式化unsigned integer 为 unsigned decimal
  3. %o:格式化unsigned integer 为 octal
  4. %x:格式化unsigned integer 为 hexadecimal
  5. %c:格式化integer 为单个character
  6. %f:格式化double precision为 floating point number
  7. %e:格式化double precision为固定格式
  8. %s:打印NULL结尾的字符串array
  9. %p:将给定的指针作为内存地址打印出来
  10. %%:写入一个%字符

格式化字符串中的占位符的顺序和类型要和后续提供的参数相匹配:

 /* Writes the formatted data. */ 
 if (0 > fprintf(stream, "The %s is %d.", "number", 2)) 
 { 
 /* Error occurred while writing formatted data. */ 
 }

fprint 方法返回写入stream的字符数量。发生错误的时候返回负值。关于更多的格式可参考以下链接:pubs.opengroup.org/onlinepubs/…

Flushing the Buffer
Stream I/O 会把需要写入文件的数据累积起来,然后异步写入,而不是立刻写入。同样的,在读取文件的时候也是通过block的形式读取,而不是一个字符一个字符地读取。就是buffering。

刷新buffer意味着将累积的数据写入底层的文件。Flush会在以下情况自动完成:

  1. 正常关闭应用
  2. 向行缓冲区中写入新行
  3. 当buffer满的时候
  4. 当stream 关闭的时候

Stream I/O提供了fflush 方法来在需要的时候刷新缓冲区:

 int fflush(FILE* stream);

下面的代码显示如何获取stream指针然后刷新output buffer。

 char data[] = { 'h', 'e', 'l', 'l', 'o', '\n' }; 
 size_t count = sizeof(data) / sizeof(data[0]); 
 /* Write data to stream. */ 
 fwrite(data, sizeof(char), count, stream); 
/* Flush the output buffer. */
if (EOF == fflush(stream))
 { 
 /* Error occured while flushing the buffer. */ 
 }

从Stream中读取

和向stream中写入一样,stream I/O也提供了四个读取的方法。
从Steam中读取数据块
`fread` 方法用于从stream中读取数据块:
size_t fread(void* data, size_t size, size_t count, FILE* stream);

fread 方法从stream中读取count个每个大小为size的数据元素到data buffer中。这个方法会返回读取元素的个数:

 char buffer[5]; 
 size_t count = 4; 
/* Read 4 characters from the stream. */
if (count ! = fread(buffer, sizeof(char), count, stream))
 { 
 /* Error occured while reading from the stream. */ 
 } 
 else 
 { 
 /* Null terminate. */ 
 buffer[4] = NULL; 
 /* Output buffer. */ 
 MY_LOG_INFO("read: %s", buffer); 
 }

如果成功的话,返回的数量和传入的count一致。

从Stream中读取字符串
`fget`方法从stream中读取newline-terminated 字符结尾的字符串。
 char* fgets(char* buffer, int count, FILE* stream);

fget 方法从strem中读取最多count-1个字符和一个newline 字符到buffer中:

 char buffer[1024]; 
/* Read newline terminated character sequence from the stream. */
if (NULL == fgets(buffer, 1024, stream))
 { 
     /* Error occured while reading the stream. */ 
 } 
 else 
 { 
     MY_LOG_INFO("read: %s", buffer); 
 }

如果调用成功返回buffer的指针,否则返回NULL。

从Stream中读取单个字符
`fgetc`方法用于从stream中读取单个字符(single unsigned char)。
int fgetc(FILE* stream);

fgetc方法从stream中读取单个字符,并转化为intger返回:

 unsigned char ch; 
 int result; 
 /* Read a single character from the stream. */ 
 result = fgetc(stream); 
 if (EOF == result) 
 { 
 /* Error occured while reading from the stream. */ 
 } 
 else 
 { 
 /* Get the actual character. */ 
 ch = (unsigned char) result; 
 }

如果调用成功返回代表字符的integer,失败返回EOF。

从Stream中读取格式化数据
`fscanf`方法用于从stream中读取格式化数据。这个方法和`fprintf`类似,但是过程是反的:
int fscanf(FILE* stream, const char* format, ...);

这个方法从stream中读取format格式的字符,并将满足条件的占位字符赋值给后续的可变数量的变量。占位符如下:

  1. %d,%i: 读取一个signed decimal
  2. %u:读取 unsigned decimal
  3. %o:读取 octal 转化为unsigned integer
  4. %x:读取hexadecimal为unsigned integer
  5. %c:读取单个character
  6. %f:读取floating point number
  7. %e:读取固定格式的浮点数
  8. %s:读取string
  9. %%:读取%

示例代码:

char s[5]; 
 int i; 
 /* Stream has "The number is 2" */ 
/* Reads the formatted data. */
if (2 ! = fscanf(stream, "The %s is %d", s, &i))
 { 
 /* Error occured while reading formatted data. */ 
 }

如果方法调用成功,返回读取到的item的数量。如果发生错误,返回EOF。

检查文件的结束
当读取文件的时候,`feof`方法可以被用于检查stream是否设置了end-of-file指示符。
 int feof(FILE* stream);

feof方法接受一个stream指针,如果没有到达stream末尾,返回一个非0值,否者返回0表示到达stream的结尾:

 char buffer[1024]; 
/* Until the end of the file. */
while (0 == feof(stream))
 { 
 /* Read and output string. */ 
 fgets(buffer, 1024, stream); 
 MY_LOG_INFO("read: %s", buffer); 
 }

Seeking Position

通过`fseek`方法可以改变stream中的位置:
int fseek(FILE* stream, long offset, int whence);

fseek 方法需要传入一个stream指针,相对偏移量和相对模式。相对模式的值有以下三种:

  1. SEEK_SET:相对于stream开始的偏移量
  2. SEEK_CUR:相对于当前位置的偏移量
  3. SEEK_END:相对于结尾的偏移量

下面的样例代码写入4个字符,回退4个字符,然后在用另外四个字符覆盖:

 /* Write to the stream. */ 
 fputs("abcd", stream); 
/* Rewind for 4 bytes. */
fseek(stream, -4, SEEK_CUR);
 /* Overwrite abcd with efgh. */ 
 fputs("efgh", stream);

这里缺少了错误检查。fseek 在调用成功的时候返回0,任何非0返回值都表示错误。

错误检查

大多数stream I/O 方法会返回EOF来表示错误或者表示达到文件结尾。可以通过`ferror` 方法检查前一个操作是否发生了错误:
int ferror(FILE* stream);

ferror方法会在发生错误后返回非 0值。

/* Check for the errors. */
if (0 ! = ferror(stream))
 { 
 /* Error occured on the previous request. */ 
 }

关闭Streams

`fclose` 用于关闭stream。输出buffer中包含的任何内容都会被写入stream,输入buffer中包含的任何内容都会被丢弃:
int fclose(FILE* stream);

fclose 方法接受一个stream指针作为参数。在调用成功的情况下返回0,返回EOF表示发生错误:

if (0 ! = fclose(stream))
 { 
 /* Error occured while closing the stream. */ 
 }

因为磁盘空间不足导致输出buffer向stream写入失败会引起关闭stream错误。同样检查fclose 返回值是一个好习惯。

和进程交互

Bionic允许native代码开启进程并和其他native进程交互。Native代码可以执行shell commands,也可以在后台执行一个进程,并且与之交互。本章中会简单介绍相关方法。

执行Shell Command

`system` 方法可以用于执行shell command。为了使用这个方法,`stdlib.h` 头文件需要被导入:
 #include <stdlib.h>

system执行shell command方法会阻塞native代码直到被执行的command执行结束:

 int result; 
/* Execute the shell command. */
result = system("mkdir /data/data/com.example.hellojni/temp");
 if (−1 == result || 127 == result) 
 { 
 /* Execution of the shell failed. */ 
 }

和子进程通信

`system`命令没有提供发送指令给子进程或者从子进程接收指令的通信渠道。native代码只是等待command执行结束。在某些场景下,提供native代码进程间通信是非常有用的。

popen 方法被用于在父进程和子进程间打开一个双向通信的管道。为了使用这个方法,需要导入stdio.h头文件:

 FILE *popen(const char* command, const char* type);

popen方法接收需要被执行的方法和通信渠道类型的参数,返回一个指向stream的指针。在方法执行错误的情况下会返回NULL。下面的代码显示了通过之前讲过的stream I/O方法像和文件交互一样和子进程交互:

#include <stdio.h> 
... 
    FILE* stream; 
/* Opening a read-only channel to ls command. */
stream = popen("ls", "r");
if (NULL == stream) 
{ 
    MY_LOG_ERROR("Unable to execute the command."); 
}
else 
{ 
    char buffer[1024]; 
    int status; 
    /* Read each line from command output. */ 
    while (NULL ! = fgets(buffer, 1024, stream)) 
    { 
        MY_LOG_INFO("read: %s", buffer); 
    } 
    /* Close the channel and get the status. */
    status = pclose(stream);
    MY_LOG_INFO("process exited with status %d", status); 
}

注意:popen 返回的stream默认是完全buffer的,开发者在需要的时候需要flush相关buffer。

当子进程执行结束,stream应当使用pclose关闭:

 int pclose(FILE* stream);

这个方法接收stream 指针作为参数,等待子进程执行结束并且返回。

系统配置

Android系统以key-value的形式保存一些系统属性。Bionic提供了一系列方法用于native代码查询。为了使用这些方法,系统属性头文件首先需要被导入:
 #include <sys/system_properties.h>

系统属性头文件声明了必要的方法和结构。每个系统属性包含了最长PROP_NAME_MAX的字符key和最长PROP_VALUE_MAX的字符VALUE。

通过名称获取系统属性值

`__system_property_get` 方法用于通过name寻找系统属性:
 int __system_property_get(const char* name, char* value);

如下面代码所示,这个方法将null结尾的属性值拷贝到提供的值指针中,并且返回属性的size。整个复制的长度不会超过PROP_VALUE_MAX:

char value[PROP_VALUE_MAX]; 
/* Gets the product model system property. */
if (0 == __system_property_get("ro.product.model", value))
{ 
    /* System property is not found or it has an empty value. */ 
} 
else 
{ 
    MY_LOG_INFO("product model: %s", value); 
}

如果属性未定义,会返回0。

通过名称获取系统属性

`__system_property_find__` 可以用于一个直接指向系统属性的指针
const prop_info* __system_property_find(const char* name);

这个方法会通过名称搜索系统属性,返回一个指向这个名称的指针,如果没有找到,返回NULL。返回的指针在系统整个生命周期都有效,可以通过缓存下来的方式避免后续再次查找。__system_property_read 方法可以用于从这个指针中获取系统属性值:

const prop_info* property; 
/* Gets the product model system property. */
property = __system_property_find("ro.product.model");
if (NULL == property) 
{ 
    /* System property is not found. */ 
} 
else 
{ 
    char name[PROP_NAME_MAX]; 
    char value[PROP_VALUE_MAX]; 
    /* Get the system property name and value. */
    if (0 == __system_property_read(property, name, value))
    { 
        MY_LOG_INFO("%s is empty."); 
    } 
    else 
    { 
        MY_LOG_INFO("%s: %s", name, value); 
    } 
}

__system_property_read 方法需要传入一个系统属性指针和两个字符串指针:

int __system_property_read(const prop_info* pi, char* name, char* value);

这个方法会拷贝null结尾的字符串指针,并且返回value的size。整个拷贝的值长度不会超过PROP_VALUE_MAX。name参数是可选的。如果传入了name字符串指针,这个方法会拷贝名称到这个指针指向的内存中,长度不会超过PROP_NAME_MAX。

Users and Groups

Linux内核支持多用户。尽管Android是给单个手持设备使用的,但是使用了基于用户的权限控制模型。
  1. Android在虚拟机沙盒中运行应用并且将他们当成系统中的不同用户。依赖于基于用户的权限控制模型,Android可以很轻松的做到防止应用访问其他应用的数据和内存。
  2. 服务和硬件资源的保护也是基于用户的权限控制实现的。每种资源都有自己的保护用户组。在应用部署过程中,应用向系统请求使用这些资源。如果应用不在相应的资源使用用户组内,则无法访问相应资源。

Bionic提供了users 和 groups相关信息的方法支持,大多数这些方法都只有不完整实现的方法(stub)。本章中将会介绍其中关键的方法,首先需要导入unistd.h 头文件:

#include <unistd.h>

获取应用User、Group ID 和 User Name

每个安装的应用都有从10000开始的自己的user和group ID。低于10000的ID是给系统服务用的。user ID可以通过`getuid` 方法获取,group ID可以通过`getgid`方法获取。每个安装的应用都会被赋予一个以app_<应用ID>的user name。例如,user ID为10040的应用user name是app_40。user name可以通过`getlogin` 方法获取:
uid_t uid; 
/* Get the application user ID. */
uid = getuid();
MY_LOG_INFO("Application User ID is %u", uid);

gid_t gid; 
/* Get the application group ID. */
gid = getgid();
MY_LOG_INFO("Application Group ID is %u", gid);

char* username; 
/* Get the application user name. */ 
username = getlogin(); 
MY_LOG_INFO("Application user name is %s", username);

进程间通信 IPC

BIonic没有提供支持System V进程间通信(IPC)的支持,这样做是为了避免拒绝服务攻击和内核资源泄露。尽管不支持System V IPC,Android系统通过Binder来提供对IPC的支持。Android应用通过Binder接口来和系统、服务以及其他应用通信。

总结

本章中粗略的浏览了Bionic提供的一些API和功能,Bionic是Google为Android系统开发的BSD风格的C 衍生library。大概了解了Bionic提供给native应用的libc方法,包括内存管理,标准I/O,进程控制,系统配置和user/group管理方法。此外,Bionic还提供了对多线程和网络的支持,后续的章节会单独讲解。