深入讲解Android Property机制

侯亮

1      概述

Android系统(本文以Android 4.4为准)的属性(Property)机制有点儿类似Windows系统的注册表,其中的每个属性被组织成简单的键值对(key/value)供外界使用。

我们可以通过在adb shell里敲入getprop命令来获取当前系统的所有属性内容,而且,我们还可以敲入类似“getprop 属性名”的命令来获取特定属性的值。另外,设置属性值的方法也很简单,只需敲入“setprop 属性名 新值”命令即可。

可是问题在于我们不想只认识到这个层次,我们希望了解更多一些Property机制的运作机理,而这才是本文关心的重点。

说白了,Property机制的运作机理可以汇总成以下几句话:
1)  系统一启动就会从若干属性脚本文件中加载属性内容;
2)  系统中的所有属性(key/value)会存入同一块共享内存中;
3)  系统中的各个进程会将这块共享内存映射到自己的内存空间,这样就可以直接读取属性内容了;
4)  系统中只有一个实体可以设置、修改属性值,它就是属性服务(Property Service);
5)  不同进程只可以通过socket方式,向属性服务发出修改属性值的请求,而不能直接修改属性值;
6)  共享内存中的键值内容会以一种字典树的形式进行组织。

Property机制的示意图如下:

2      Property Service

2.1  init进程里的Property Service

Property Service实体其实是在init进程里启动的。我们知道,init是Linux系统中用户空间的第一个进程。它负责创建系统中最关键的几个子进程,比如zygote等等。在本节中,我们主要关心init进程是如何启动Property Service的。

我们查看core/init/Init.c文件,可以看到init进程的main()函数,它里面和property相关的关键动作有:
1)间接调用__system_property_area_init():打开属性共享内存,并记入__system_property_area变量;
2)间接调用init_workspace():只读打开属性共享内存,并记入环境变量;
3)根据init.rc,异步激发property_service_init_action(),该函数中会:
    l  加载若干属性文本文件,将具体属性、属性值记入属性共享内存;
    l  创建并监听socket;
4)根据init.rc,异步激发queue_property_triggers_action(),将刚刚加载的属性对应的激发动作,推入action列表。

main()中的调用关系如下:

2.1.1   初始化属性共享内存

我们可以看到,在init进程的main()函数里,辗转打开了一个内存文件“/dev/__properties__”,并把它设定为128KB大小,接着调用mmap()将这块内存映射到init进程空间了。这个内存的首地址被记录在__system_property_area__全局变量里,以后每添加或修改一个属性,都会基于这个__system_property_area__变量来计算位置。

初始化属性内存块时,为什么要两次open那个/dev/__properties__文件呢?我想原因是这样的:第一次open的句柄,最终是给属性服务自己用的,所以需要有读写权限;而第二次open的句柄,会被记入pa_workspace.fd,并在合适时机添加进环境变量,供其他进程使用,因此只能具有读取权限。

第一次open时,执行的代码如下: 
fd = open(property_filename, O_RDWR | O_CREAT | O_NOFOLLOW | O_CLOEXEC | O_EXCL, 0444); 
传给open()的参数标识里指明了O_RDWR,表示用“读写方式”打开文件。另外O_NOFOLLOW标识主要是为了防止我们打开“符号链接”,不过我们知道,__properties__文件并不是符号链接,所以当然可以成功open。O_CLOEXEC标识是为了保证一种独占性,也就是说当init进程打开这个文件时,此时就算其他进程也open这个文件,也会在调用exec执行新程序时自动关闭该文件句柄。O_EXCL标识和O_CREATE标识配合起来,表示如果文件不存在,则创建之,而如果文件已经存在,那么open就会失败。第一次open动作后,会给__system_property_area__赋值,然后程序会立即close刚打开的句柄。

第二次open动作发生在接下来的init_workspace()函数里。此时会再一次打开__properties__文件,这次却是以只读模式打开的: 
int fd = open(PROP_FILENAME, O_RDONLY | O_NOFOLLOW); 
打开的句柄记录在pa_workspace.fd处,以后每当init进程执行socket命令,并调用service_start()时,会执行类似下面的句子:

  1. get_property_workspace(&fd, &sz);   // 读取pa_workspace.fd
  2. sprintf(tmp, "%d,%d", dup(fd), sz);
  3. add_environment("ANDROID_PROPERTY_WORKSPACE", tmp);

说白了就是把 pa_workspace.fd 的句柄记入一个名叫“ ANDROID_PROPERTY_WORKSPACE ”的环境变量去。

【system/core/init/Init.c】

  1. /* add_environment - add "key=value" to the current environment */
  2. int add_environment(const char *key, const char *val)
  3. {
  4. int n;
  5. for (n = 0; n < 31; n++) {
  6. if (!ENV[n]) {
  7. size_t len = strlen(key) + strlen(val) + 2;
  8. char *entry = malloc(len);
  9. snprintf(entry, len, "%s=%s", key, val);
  10. ENV[n] = entry;
  11. return 0;
  12. }
  13. }
  14. return 1;
  15. }

这个环境变量在日后有可能被其他进程拿来用,从而将属性内存区映射到自己的内存空间去,这个后文会细说。

接下来,main()函数在设置好属性内存块之后,会调用queue_builtin_action()函数向内部的action_list列表添加action节点。关于这部分的详情,可参考其他讲述Android启动机制的文档,这里不再赘述。我们只需知道,后续,系统会在合适时机回调“由queue_builtin_action()的参数”所指定的property_service_init_action()函数就可以了。

2.1.2   初始化属性服务

property_service_init_action()函数只是在简单调用start_property_service()而已,后者的代码如下:

【core/init/Property_service.c】

  1. void start_property_service(void)
  2. {
  3. int fd;
  4. load_properties_from_file(PROP_PATH_SYSTEM_BUILD);
  5. load_properties_from_file(PROP_PATH_SYSTEM_DEFAULT);
  6. /* Read vendor-specific property runtime overrides. */
  7. vendor_load_properties();
  8. load_override_properties();
  9. /* Read persistent properties after all default values have been loaded. */
  10. load_persistent_properties();
  11. fd = create_socket(PROP_SERVICE_NAME, SOCK_STREAM, 0666, 0, 0);
  12. if(fd < 0) return;
  13. fcntl(fd, F_SETFD, FD_CLOEXEC);
  14. fcntl(fd, F_SETFL, O_NONBLOCK);
  15. listen(fd, 8);
  16. property_set_fd = fd;
  17. }

其主要动作无非是加载若干属性文件,然后创建并监听一个socket接口。

2.1.2.1  加载属性文本文件

start_property_service()函数首先会调用load_properties_from_file()函数,尝试加载一些属性脚本文件,并将其中的内容写入属性内存块里。从代码里可以看到,主要加载的文件有: 
l  /system/build.prop 
l  /system/default.prop(该文件不一定存在) 
l  /data/local.prop 
l  /data/property目录里的若干脚本

load_properties_from_file()函数的代码如下:

【core/init/Property_service.c】

  1. static void load_properties_from_file(const char *fn)
  2. {
  3. char *data;
  4. unsigned sz;
  5. data = read_file(fn, &sz);
  6. if(data != 0) {
  7. load_properties(data);
  8. free(data);
  9. }
  10. }

其中调用的read_file()函数很简单,只是把文件内容的所有字节读入一个buffer,并在内容最后添加两个字节:’\n’和0。

接着调用的load_properties()函数,会逐行分析传来的buffer,解析出行内的key、value部分,并调用property_set(),将key、value设置进系统的属性共享内存去。

我们绘制出property_service_init_action()函数的调用关系图,如下:

2.1.2.2  创建socket接口

在加载动作完成后,start_property_service ()会创建一个socket接口,并监听这个接口。

【core/init/Property_service.c】

  1. fd = create_socket(PROP_SERVICE_NAME, SOCK_STREAM, 0666, 0, 0);
  2. if(fd < 0) return;
  3. fcntl(fd, F_SETFD, FD_CLOEXEC);
  4. fcntl(fd, F_SETFL, O_NONBLOCK);
  5. listen(fd, 8);
  6. property_set_fd = fd;

这个socket是专门用来监听其他进程发来的“修改”属性值的命令的,它被设置成“非阻塞”(O_NONBLOCK)的socket。

2.1.3   初始化属性后的触发动作

既然在上一小节的property_service_init_action()动作中,系统已经把必要的属性都加载好了,那么现在就可以遍历刚生成的action_list,看看哪个刚加载好的属性可以进一步触发连锁动作。这就是init进程里为什么有两次和属性相关的queue_builtin_action()的原因。

【system/core/init/Init.c】

  1. static int queue_property_triggers_action(int nargs, char **args)
  2. {
  3. queue_all_property_triggers();
  4. /* enable property triggers */
  5. property_triggers_enabled = 1;
  6. return 0;
  7. }

【system/core/init/Init_parser.c】

  1. void queue_all_property_triggers()
  2. {
  3. struct listnode *node;
  4. struct action *act;
  5. list_for_each(node, &action_list) {
  6. act = node_to_item(node, struct action, alist);
  7. if (!strncmp(act->name, "property:", strlen("property:"))) {
  8. /* parse property name and value
  9. syntax is property:<name>=<value> */
  10. const char* name = act->name + strlen("property:");
  11. const char* equals = strchr(name, '=');
  12. if (equals) {
  13. char prop_name[PROP_NAME_MAX + 1];
  14. char value[PROP_VALUE_MAX];
  15. int length = equals - name;
  16. if (length > PROP_NAME_MAX) {
  17. ERROR("property name too long in trigger %s", act->name);
  18. } else {
  19. memcpy(prop_name, name, length);
  20. prop_name[length] = 0;
  21. /* does the property exist, and match the trigger value? */
  22. property_get(prop_name, value);
  23. if (!strcmp(equals + 1, value) ||!strcmp(equals + 1, "*")) {
  24. action_add_queue_tail(act);
  25. }
  26. }
  27. }
  28. }
  29. }
  30. }

这段代码是说,当获取的属性名和属性值,与当初init.rc里记录的某action的激发条件匹配时,就把该action插入执行队列的尾部(action_add_queue_tail(act))。

2.2  init进程循环监听socket

现在再回过头看init进程,其main()函数的最后,我们可以看到一个for(;;)循环,不断监听外界发来的命令,包括设置属性的命令。

【system/core/init/Init.c】

  1. for(;;) {
  2. . . . . . .
  3. . . . . . .
  4. nr = poll(ufds, fd_count, timeout);
  5. if (nr <= 0)
  6. continue;
  7. for (i = 0; i < fd_count; i++) {
  8. if (ufds[i].revents == POLLIN) {
  9. if (ufds[i].fd == get_property_set_fd())
  10. handle_property_set_fd();
  11. else if (ufds[i].fd == get_keychord_fd())
  12. handle_keychord();
  13. else if (ufds[i].fd == get_signal_fd())
  14. handle_signal();
  15. }
  16. }
  17. }

2.2.1   处理“ctl.”命令

当从socket收到“设置属性”的命令后,会调用上面的handle_property_set_fd()函数,代码截选如下:

【core/init/Property_service.c】

  1. void handle_property_set_fd()
  2. {
  3. prop_msg msg;
  4. . . . . . .
  5. if ((s = accept(property_set_fd, (struct sockaddr *) &addr, &addr_size)) < 0) {
  6. return;
  7. }
  8. . . . . . .
  9. switch(msg.cmd) {
  10. case PROP_MSG_SETPROP:
  11. msg.name[PROP_NAME_MAX-1] = 0;
  12. msg.value[PROP_VALUE_MAX-1] = 0;
  13. . . . . . .
  14. if(memcmp(msg.name,"ctl.",4) == 0) {
  15. . . . . . .
  16. if (check_control_perms(msg.value, cr.uid, cr.gid, source_ctx)) {
  17. handle_control_message((char*) msg.name + 4, (char*) msg.value);
  18. } else {
  19. ERROR("sys_prop: Unable to %s service ctl [%s] uid:%d gid:%d pid:%d\n",
  20. msg.name + 4, msg.value, cr.uid, cr.gid, cr.pid);
  21. }
  22. } else {
  23. if (check_perms(msg.name, cr.uid, cr.gid, source_ctx)) {
  24. property_set((char*) msg.name, (char*) msg.value);
  25. } else {
  26. ERROR("sys_prop: permission denied uid:%d  name:%s\n",
  27. cr.uid, msg.name);
  28. }
  29. . . . . . .
  30. close(s);
  31. }
  32. . . . . . .
  33. break;
  34. . . . . . .
  35. }
  36. }<span style="font-family:宋体;margin: 0px; padding: 0px;"></span>

看到了吗?设置属性时,一开始就把属性名和属性值的长度都限制了。

  1. #define PROP_NAME_MAX   32
  2. #define PROP_VALUE_MAX  92

也就是说,有意义的部分的最大字节数分别为31字节和91字节,最后一个字节先被强制设为0了。

2.2.1.1  check_control_perms()

对于普通属性而言,主要是调用property_set()来设置属性值,但是有一类特殊属性是以“ctl.”开头的,它们本质上是一些控制命令,比如启动某个系统服务。这种控制命令需调用handle_control_message()来处理。

当然,并不是随便谁都可以发出这种控制命令的,也就是说,不是谁都可以成功设置以“ctl.”开头的特殊属性。handle_property_set_fd()会先调用check_control_perms()来检查发起方是否具有相应的权限。

【core/init/Property_service.c】

  1. static int check_control_perms(const char *name, unsigned int uid, unsigned int gid, char *sctx) {
  2. int i;
  3. if (uid == AID_SYSTEM || uid == AID_ROOT)
  4. return check_control_mac_perms(name, sctx);
  5. /* Search the ACL */
  6. for (i = 0; control_perms[i].service; i++) {
  7. if (strcmp(control_perms[i].service, name) == 0) {
  8. if ((uid && control_perms[i].uid == uid) ||
  9. (gid && control_perms[i].gid == gid)) {
  10. return check_control_mac_perms(name, sctx);
  11. }
  12. }
  13. }
  14. return 0;
  15. }

可以看到,如果设置方的uid是AID_SYSTEM或者AID_ROOT,那么一般都是具有权限的。而如果uid是其他值,那么就得查control_perms表了,这个表的定义如下:

【core/init/Property_service.c】

  1. /*
  2. * White list of UID that are allowed to start/stop services.
  3. * Currently there are no user apps that require.
  4. */
  5. struct {
  6. const char *service;
  7. unsigned int uid;
  8. unsigned int gid;
  9. } control_perms[] = {
  10. { "dumpstate",AID_SHELL, AID_LOG },
  11. { "ril-daemon",AID_RADIO, AID_RADIO },
  12. {NULL, 0, 0 }
  13. };

uid为AID_SHELL的进程可以启动、停止dumpstate服务,uid为AID_RADIO的进程可以启动、停止ril-daemon服务。

2.2.1.2  handle_control_message()

在通过权限检查之后,就可以调用handle_control_message()来处理控制命令了:

【system/core/init/Init.c】

  1. void handle_control_message(const char *msg, const char *arg)
  2. {
  3. if (!strcmp(msg,"start")) {
  4. msg_start(arg);
  5. } else if (!strcmp(msg,"stop")) {
  6. msg_stop(arg);
  7. } else if (!strcmp(msg,"restart")) {
  8. msg_restart(arg);
  9. } else {
  10. ERROR("unknown control msg '%s'\n", msg);
  11. }
  12. }

假设从socket发来的命令是“ctl.start”,那么就会走到msg_start(arg)。

  1. static void msg_start(const char *name)
  2. {
  3. struct service *svc = NULL;
  4. char *tmp = NULL;
  5. char *args = NULL;
  6. if (!strchr(name, ':'))
  7. svc = service_find_by_name(name);
  8. else {
  9. tmp = strdup(name);
  10. if (tmp) {
  11. args = strchr(tmp, ':');
  12. *args = '\0';
  13. args++;
  14. svc = service_find_by_name(tmp);
  15. }
  16. }
  17. if (svc) {
  18. service_start(svc, args);
  19. } else {
  20. ERROR("no such service '%s'\n", name);
  21. }
  22. if (tmp)
  23. free(tmp);
  24. }

这里启动的service基本上都是在init.rc里说明的系统service。比如netd:

我们知道,init进程在分析init.rc文件时,会形成一个service链表,现在msg_start()就是从这个service链表里去查找相应名称的service节点的。找到节点后,再调用service_start(svc, args)。

service_start()常常会fork一个子进程,然后为它设置环境变量(ANDROID_PROPERTY_WORKSPACE):

  1. void service_start(struct service *svc, const char *dynamic_args)
  2. {
  3. . . . . . .
  4. . . . . . .
  5. pid = fork();
  6. if (pid == 0) {
  7. struct socketinfo *si;
  8. struct svcenvinfo *ei;
  9. char tmp[32];
  10. int fd, sz;
  11. umask(077);
  12. if (properties_inited()) {
  13. get_property_workspace(&fd, &sz);
  14. sprintf(tmp, "%d,%d", dup(fd), sz);
  15. add_environment("ANDROID_PROPERTY_WORKSPACE", tmp);
  16. }
  17. for (ei = svc->envvars; ei; ei = ei->next)
  18. add_environment(ei->name, ei->value);
  19. . . . . . .
其中 get_property_workspace() 的代码如下: 
  1. void get_property_workspace(int *fd, int *sz)
  2. {
  3. *fd = pa_workspace.fd;
  4. *sz = pa_workspace.size;
  5. }

大家还记得前文阐述init_workspace()时,把打开的句柄记入pa_workspace.fd的句子吧,现在就是在用这个句柄。

一切准备好后,service_start()会调用execve(),执行svc->args[0]所指定的可执行文件,然后还要再写个属性值:

  1. void service_start(struct service *svc, const char *dynamic_args)
  2. {
  3. . . . . . .
  4. . . . . . .
  5. execve(svc->args[0], (char**) arg_ptrs, (char**) ENV);
  6. . . . . . .
  7. . . . . . .
  8. svc->time_started = gettime();
  9. svc->pid = pid;
  10. svc->flags |= SVC_RUNNING;
  11. if (properties_inited())
  12. notify_service_state(svc->name, "running");
  13. }
其中的notify_service_state()的代码如下:
  1. void notify_service_state(const char *name, const char *state)
  2. {
  3. char pname[PROP_NAME_MAX];
  4. int len = strlen(name);
  5. if ((len + 10) > PROP_NAME_MAX)
  6. return;
  7. snprintf(pname, sizeof(pname), "init.svc.%s", name);
  8. property_set(pname, state);
  9. }
 

一般情况下,这种在init.rc里记录的系统service的名字都不会超过22个字节,加上“init.svc.”前缀也不会超过31个字节,所以每次启动service,都会修改相应的属性。比如netd服务,一旦它被启动,就会将init.svc.netd属性的值设为“running”。

以上是handle_control_message()处理“ctl.start”命令时的情况,相应地还有处理“ctl.stop”命令的情况,此时会调用到msg_stop()。

【system/core/init/Init.c】

  1. static void msg_stop(const char *name)
  2. {
  3. struct service *svc = service_find_by_name(name);
  4. if (svc) {
  5. service_stop(svc);
  6. } else {
  7. ERROR("no such service '%s'\n", name);
  8. }
  9. }
  1. void service_stop(struct service *svc)
  2. {
  3. service_stop_or_reset(svc, SVC_DISABLED);
  4. }
  1. static void service_stop_or_reset(struct service *svc, int how)
  2. {
  3. /* The service is still SVC_RUNNING until its process exits, but if it has
  4. * already exited it shoudn't attempt a restart yet. */
  5. svc->flags &= (~SVC_RESTARTING);
  6. if ((how != SVC_DISABLED) && (how != SVC_RESET) && (how != SVC_RESTART)) {
  7. /* Hrm, an illegal flag.  Default to SVC_DISABLED */
  8. how = SVC_DISABLED;
  9. }
  10. /* if the service has not yet started, prevent
  11. * it from auto-starting with its class
  12. */
  13. if (how == SVC_RESET) {
  14. svc->flags |= (svc->flags & SVC_RC_DISABLED) ? SVC_DISABLED : SVC_RESET;
  15. } else {
  16. svc->flags |= how;
  17. }
  18. if (svc->pid) {
  19. NOTICE("service '%s' is being killed\n", svc->name);
  20. kill(-svc->pid, SIGKILL);
  21. notify_service_state(svc->name, "stopping");
  22. } else {
  23. notify_service_state(svc->name, "stopped");
  24. }
  25. }

可以看到,停止一个service时,主要是调用kill( )来杀死服务子进程,并将init.svc.xxx属性值设为stopping。

OK,终于把init进程里,处理“ctl.”命令的部分讲完了,下面我们接着看init进程处理普通属性的部分。

2.2.2   处理属性设置命令

我们还是先回到前文init进程处理属性设置动作的地方:

  1. void handle_property_set_fd()
  2. {
  3. . . . . . .
  4. if(memcmp(msg.name,"ctl.",4) == 0) {
  5. . . . . . .
  6. } else {
  7. if (check_perms(msg.name, cr.uid, cr.gid, source_ctx)) {
  8. property_set((char*) msg.name, (char*) msg.value);
  9. } else {
  10. ERROR("sys_prop: permission denied uid:%d  name:%s\n",
  11. cr.uid, msg.name);
  12. }
  13. . . . . . .
  14. close(s);
  15. }
  16. . . . . . .
  17. break;
  18. . . . . . .
  19. }
  20. }

2.2.2.1  check_perms()

要设置普通属性,也是要具有一定权限哩。请看上面的 check_perms() 一句。该函数的代码如下:

  1. static int check_perms(const char *name, unsigned int uid, unsigned int gid, char *sctx)
  2. {
  3. int i;
  4. unsigned int app_id;
  5. if(!strncmp(name, "ro.", 3))
  6. name +=3;
  7. if (uid == 0)
  8. return check_mac_perms(name, sctx);
  9. app_id = multiuser_get_app_id(uid);
  10. if (app_id == AID_BLUETOOTH) {
  11. uid = app_id;
  12. }
  13. for (i = 0; property_perms[i].prefix; i++) {
  14. if (strncmp(property_perms[i].prefix, name,
  15. strlen(property_perms[i].prefix)) == 0) {
  16. if ((uid && property_perms[i].uid == uid) ||
  17. (gid && property_perms[i].gid == gid)) {
  18. return check_mac_perms(name, sctx);
  19. }
  20. }
  21. }
  22. return 0;
  23. }

主要也是在查表,property_perms表的定义如下:

这其实很容易理解,比如要设置“sys.”打头的系统属性,进程的uid就必须是AID_SYSTEM,否则阿猫阿狗都能设置系统属性,岂不糟糕。

2.2.2.2  property_set()

权限检查通过之后,就可以真正设置属性了。在前文“概述”一节中,我们已经说过,只有Property Service(即init进程)可以写入属性值,而普通进程最多只能通过socket向Property Service发出设置新属性值的请求,最终还得靠Property Service来写。那么我们就来看看Property Service里具体是怎么写的。

总体说来,property_set()会做如下工作:

1)  判断待设置的属性名是否合法; 
2)  尽力从“属性共享内存”中找到匹配的prop_info节点,如果能找到,就调用__system_property_update(),当然如果属性是以“ro.”打头的,说明这是个只读属性,此时不会update的;如果找不到,则调用__system_property_add()添加属性节点。 
3)  在update或add动作之后,还需要做一些善后处理。比如,如果改动的是“net.”开头的属性,那么需要重新设置一下net.change属性,属性值为刚刚设置的属性名字。 
4)  如果要设置persist属性的话,只有在系统将所有的默认persist属性都加载完毕后,才能设置成功。persist属性应该是那种会存入可持久化文件的属性,这样,系统在下次启动后,可以将该属性的初始值设置为系统上次关闭时的值。
5)  如果将“selinux.reload_policy”属性设为“1”了,那么会进一步调用selinux_reload_policy()。这个意味着要重新加载SEAndroid策略。 
6)  最后还需调用property_changed()函数,其内部会执行init.rc中指定的那些和property同名的action。

【core/init/Property_service.c】

  1. int property_set(const char *name, const char *value)
  2. {
  3. . . . . . .
  4. . . . . . .
  5. pi = (prop_info*) __system_property_find(name);
  6. if(pi != 0) {
  7. if(!strncmp(name, "ro.", 3)) return -1;
  8. __system_property_update(pi, value, valuelen);
  9. } else {
  10. ret = __system_property_add(name, namelen, value, valuelen);
  11. . . . . . .
  12. }
  13. if (strncmp("net.", name, strlen("net.")) == 0)  {
  14. if (strcmp("net.change", name) == 0) {
  15. return 0;
  16. }
  17. property_set("net.change", name);
  18. } else if (persistent_properties_loaded &&
  19. strncmp("persist.", name, strlen("persist.")) == 0) {
  20. write_persistent_property(name, value);
  21. } else if (strcmp("selinux.reload_policy", name) == 0 &&
  22. strcmp("1", value) == 0) {
  23. selinux_reload_policy();
  24. }
  25. property_changed(name, value);
  26. return 0;
  27. }

一开始当然要先找到“希望设置的目标属性”在共享内存里对应的prop_info节点啦,后续关于__system_property_update()和__system_property_add()的操作,主要都是在操作该prop_info节点,代码比较简单。prop_info的详细内容我们会在下文阐述,这里先跳过。

如果可以找到prop_info节点,就尽量将这个属性的值更新一下,除非是遇到“ro.”属性,这种属性是只读的,当然不能set。如果找不到prop_info节点,此时会为这个新属性创建若干字典树节点,包括最终的prop_info叶子。

属性写入完毕后,还要调用property_changed(),做一些善后处理:

【system/core/init/Init.c】

  1. void property_changed(const char *name, const char *value)
  2. {
  3. if (property_triggers_enabled)
  4. queue_property_triggers(name, value);
  5. }

【 system/core/init/Init_parser.c 】

  1. void queue_property_triggers(const char *name, const char *value)
  2. {
  3. struct listnode *node;
  4. struct action *act;
  5. list_for_each(node, &action_list) {
  6. act = node_to_item(node, struct action, alist);
  7. if (!strncmp(act->name, "property:", strlen("property:"))) {
  8. const char *test = act->name + strlen("property:");
  9. int name_length = strlen(name);
  10. if (!strncmp(name, test, name_length) &&
  11. test[name_length] == '=' &&
  12. (!strcmp(test + name_length + 1, value) ||
  13. !strcmp(test + name_length + 1, "*"))) {
  14. action_add_queue_tail(act);
  15. }
  16. }
  17. }
  18. }
  1. void action_add_queue_tail(struct action *act)
  2. {
  3. if (list_empty(&act->qlist)) {
  4. list_add_tail(&action_queue, &act->qlist);
  5. }
  6. }

从代码可以看出,当某个属性修改之后, Property Service 会遍历一遍 action_list 列表,找到其中匹配的 action 节点,并将之添加进 action_queue 队列。之所以会有 if (list_empty(&act->qlist)) 判断,是为了防止重复添加。下面是 init.rc 脚本中的一个片段:

【system/core/rootdir/init.rc】 

这几个就是和property相关的action,其他相关的action还有不少,我们就不列了。我们以第一个action为例来说明。如果我们修改了vold.decrypt属性的值,那么queue_property_triggers()搜索action_list时,就能找到一个名为“property:vold.decrypt=trigger_reset_main”的action节点,此时的逻辑无非是比较“冒号后的名字”、“赋值号后的值”,是否分别和queue_property_triggers()的name、value参数匹配,如果匹配,就把这个action节点添加进action_queue队列里。

3      客户进程访问属性的机制

3.1  映射“属性共享内存”的时机

现在有一个问题必须先提出来,那就是“属性共享内存”是在什么时刻映射进用户进程空间的?总不会平白无故地就可以成功调用property_get()吧。其实,为了让大家方便地调用property_get(),属性机制的设计者的确是用了一点儿小技巧,下面我们就来看看细节。

3.1.1   静态加载时的初始化

在前文介绍Init进程初始化属性共享内存时,调用了一个叫做__system_property_area_init()的函数:

【bionic/libc/bionic/System_properties.c】

  1. int __system_property_area_init()
  2. {
  3. return map_prop_area_rw();
  4. }

它映射时需要的是读写权限。而对普通进程而言,只有读权限,当然不可能调用__system_property_area_init()了。其实在System_properties.c文件中,我们还可以找到另一个长得挺像的初始化函数——__system_properties_init():

  1. int __system_properties_init()
  2. {
  3. return map_prop_area();
  4. }

它调用的map_prop_area()会把属性共享内存,以只读模式映射到用户进程空间:

  1. static int map_prop_area()
  2. {
  3. fd = open(property_filename, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
  4. . . . . . .
  5. if ((fd < 0) && (errno == ENOENT)) {
  6. fd = get_fd_from_env();
  7. fromFile = false;
  8. }
  9. . . . . . .
  10. pa_size = fd_stat.st_size;
  11. pa_data_size = pa_size - sizeof(prop_area);
  12. prop_area *pa = mmap(NULL, pa_size, PROT_READ, MAP_SHARED, fd, 0);
  13. . . . . . .
  14. result = 0;
  15. __system_property_area__ = pa;
  16. . . . . . .
  17. return result;
  18. }

其中调用的get_fd_from_env()的代码如下:

  1. static int get_fd_from_env(void)
  2. {
  3. char *env = getenv("ANDROID_PROPERTY_WORKSPACE");
  4. if (!env) {
  5. return -1;
  6. }
  7. return atoi(env);
  8. }

哇,终于看到读取“ANDROID_PROPERTY_WORKSPACE”环境变量的地方啦。不过呢,它的重要性似乎并没有我们一开始想的那么大。在map_prop_area()函数里分明写着,只有在open()属性文件不成功的情况下,才会尝试从环境变量中读取文件句柄,而一般都会open成功的。不管文件句柄fd是怎么得到的吧,反正能映射成空间地址就行。映射后的空间地址,仍然会记录在__system_property_area__全局变量中。

现在我们只需找到调用__system_properties_init()的源头就可以了。经过查找,我们发现__libc_init_common()会调用它,代码如下:

【bionic/libc/bionic/Libc_init_common.cpp】

  1. void __libc_init_common(KernelArgumentBlock& args) {
  2. . . . . . .
  3. . . . . . .
  4. _pthread_internal_add(main_thread);
  5. __system_properties_init(); // Requires 'environ'.
  6. }

这个函数可是在bionic目录里的,小技巧已经用到C库里啦。

__libc_init_common()又会被__libc_init()调用:

【bionic/libc/bionic/Libc_init_static.cpp】

  1. __noreturn void __libc_init(void* raw_args,
  2. void (*onexit)(void),
  3. int (*slingshot)(int, char**, char**),
  4. structors_array_t const * const structors) {
  5. KernelArgumentBlock args(raw_args);
  6. __libc_init_tls(args);
  7. __libc_init_common(args);
  8. . . . . . .
  9. . . . . . .
  10. call_array(structors->preinit_array);
  11. call_array(structors->init_array);
  12. . . . . . .
  13. exit(slingshot(args.argc, args.argv, args.envp));
  14. }

当一个用户进程被调用起来时,内核会先调用到C运行期库(crtbegin)层次来初始化运行期环境,在这个阶段就会调用到__libc_init(),而后才会间接调用到C程序员熟悉的main()函数。可见属性共享内存在执行main()函数之前就已经映射好了。

3.1.2   动态加载时的初始化

除了__libc_init()中会调用__libc_init_common(),还有一处会调用。

【bionic/libc/bionic/Libc_init_dynamic.cpp】

  1. __attribute__((constructor)) static void __libc_preinit() {
  2. . . . . . .
  3. __libc_init_common(*args);
  4. . . . . . .
  5. pthread_debug_init();
  6. malloc_debug_init();
  7. }

请大家注意函数名那一行起始处的__attribute__((constructor))属性,这是GCC的一个特有属性。被这种属性修饰的函数会被放置在特殊的代码段中。这样,当动态链接器一加载libc.so时,会尽早执行__libc_preinit()函数。这样一来,动态库里也可以放心调用property_get()了。

3.2  读取属性值

下面我们来集中精力研究读取属性值的部分。我们在前文留下过一个尾巴,当时对属性共享内存块里的prop_info节点,只做了非常简略的提及,现在我们就来细说它。

说白了,属性共享内存中的内容,其实被组织成一棵字典树。内存块的第一个节点是个特殊的总述节点,类型为prop_area。紧随其后的就是字典树的“树枝”和“树叶”了,树枝以prop_bt表达,树叶以prop_info表达。我们读取或设置属性值时,最终都只是在操作“叶子”节点而已。

3.2.1   “属性共享内存”里的数据结构

【bionic/libc/bionic/System_properties.c】

  1. struct prop_area {
  2. unsigned bytes_used;
  3. unsigned volatile serial;
  4. unsigned magic;
  5. unsigned version;
  6. unsigned reserved[28];
  7. char data[0];
  8. };
  9. typedef struct prop_area prop_area;
  10. struct prop_info {
  11. unsigned volatile serial;
  12. char value[PROP_VALUE_MAX];
  13. char name[0];
  14. };
  15. typedef struct prop_info prop_info;
  1. typedef volatile uint32_t prop_off_t;
  2. struct prop_bt {
  3. uint8_t namelen;
  4. uint8_t reserved[3];
  5. prop_off_t prop;
  6. prop_off_t left;
  7. prop_off_t right;
  8. prop_off_t children;
  9. char name[0];
  10. };
  11. typedef struct prop_bt prop_bt;

现在的问题是,这棵树是如何组织其枝叶的?System_properties.c文件中,有一段注释,给出了一个不算太清楚的示意图,截取如下:

看过这张图后,各位同学搞清楚了吗?反正我一开始没有搞清楚,后来只好研究代码,现在算是知道一点儿了,详情如下: 
l  一开始的prop_area节点严格地说并不属于字典树,但是它代表着属性共享内存块的起始; 
l  紧接着prop_area节点,需要有一个空白的prop_bt节点。这个是必须的噢,在前文说明init进程的main()函数的调用关系图中,我们表达了这个概念:

这个就是空节点; 
l  属性名将以‘.’符号为分割符,被分割开来。比如ro.secure属性名就会被分割成“ro”和“secure”两部分,而且每个部分用一个prop_bt节点表达。 
l  属性名中的这种‘.’关系被表示为父子关系,所以“ro”节点的children域,会指向“secure”节点。但是请注意,一个节点只有一个children域,如果它还有其他孩子,那些孩子将会和第一个子节点(比如secure节点)组成一棵二叉树。 
l  当一个属性名对应的“字典树枝”都已经形成好后,会另外创建一个prop_info节点,专门表示这个属性,该节点就是“字典树叶”。

下面我们画几张图来说明问题。比如我们现在手头有3个属性,分别为 
ro.abc.def 
ro.hhh.def 
sys.os.ccc

我们依此顺序设置属性,就会形成下面这样的树:

其中天蓝色块表示prop_area节点,桔黄色块表示prop_bt节点,浅绿色块表示prop_info节点。简单地说,父节点的children域,只指代其第一个子节点。后续从属于同一父节点的兄弟子节点,会被组织成一棵二叉子树,该二叉子树的根就是父节点的第一个子节点。我们用蓝色箭头来表示二叉子树的关系,在代码中对应prop_bt的left、right域。这么说来,以不同顺序添加属性,其实会导致最终得到的字典树在形态上发生些许变化。

prop_bt节点的name域只记录“树枝”的名字,比如“ro”、“abc”、“def”等等,而prop_info节点的name域记录的则是属性的全名,比如“ro.abc.def”。

现在我们向上面这棵字典树中再添加一个rs.ppp.qqq属性,会形成如下字典树:

“rs”节点之所以在那个位置,是基于strcmp()的计算结果。“rs”字符串比“ro”字符串大,所以进一步和“ro”的right节点(即“sys”节点)比对,“rs”又比“sys”小,所以在“sys”节点的left枝上建立了新节点。

以上是画成字典树的样子,它表示的是一种逻辑关系。而在实际的“属性共享内存”中,这些节点基本上是紧凑排列的,大体上会形成下面这样的排列关系:

说到这里,大家应该已经比较清楚属性共享内存块是怎么组织的吧。有了这种大致思路,再去看相应的代码,相信大家会轻松一点儿。

3.2.2   property_get()

在读取具体属性值时,最终会调用到property_get()函数,该函数的调用关系如下:

说白了就是先从字典树中找到感兴趣的prop_info叶子,然后把叶子里的值读出来。

4      Java层的封装

接下来我们再说说属性机制里Java层的封装。这部分比较简单,因为它主要只是在简单包装C语言层次的函数。

Java层使用的属性机制被封装在SystemProperties中:

【frameworks/base/core/java/android/os/SystemProperties.java】

  1. public class SystemProperties
  2. {
  3. public static final int PROP_NAME_MAX = 31;
  4. public static final int PROP_VALUE_MAX = 91;
  5. private static final ArrayList<Runnable> sChangeCallbacks = new ArrayList<Runnable>();
  6. private static native String native_get(String key);
  7. private static native String native_get(String key, String def);
  8. private static native int native_get_int(String key, int def);
  9. private static native long native_get_long(String key, long def);
  10. private static native boolean native_get_boolean(String key, boolean def);
  11. private static native void native_set(String key, String def);
  12. private static native void native_add_change_callback();
  13. /**
  14. * Get the value for the given key.
  15. * @return an empty string if the key isn't found
  16. * @throws IllegalArgumentException if the key exceeds 32 characters
  17. */
  18. public static String get(String key) {
  19. if (key.length() > PROP_NAME_MAX) {
  20. throw new IllegalArgumentException("key.length > " + PROP_NAME_MAX);
  21. }
  22. return native_get(key);
  23. }
  24. . . . . . .
  25. . . . . . .

我们就以上面的get()成员函数为例来说明,它基本上只是在调用native_get()函数而已,该函数对应的C语言函数可以从下表查到,就是那个SystemProperties_getS():

【frameworks/base/core/jni/android_os_SystemProperties.cpp】

  1. static JNINativeMethod method_table[] = {
  2. { "native_get", "(Ljava/lang/String;)Ljava/lang/String;",
  3. (void*) SystemProperties_getS },
  4. { "native_get", "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;",
  5. (void*) SystemProperties_getSS },
  6. { "native_get_int", "(Ljava/lang/String;I)I",
  7. (void*) SystemProperties_get_int },
  8. { "native_get_long", "(Ljava/lang/String;J)J",
  9. (void*) SystemProperties_get_long },
  10. { "native_get_boolean", "(Ljava/lang/String;Z)Z",
  11. (void*) SystemProperties_get_boolean },
  12. { "native_set", "(Ljava/lang/String;Ljava/lang/String;)V",
  13. (void*) SystemProperties_set },
  14. { "native_add_change_callback", "()V",
  15. (void*) SystemProperties_add_change_callback },
  16. };

【frameworks/base/core/jni/android_os_SystemProperties.cpp】

  1. static jstring SystemProperties_getS(JNIEnv *env, jobject clazz,
  2. jstring keyJ)
  3. {
  4. return SystemProperties_getSS(env, clazz, keyJ, NULL);
  5. }
  6. static jstring SystemProperties_getSS(JNIEnv *env, jobject clazz,
  7. jstring keyJ, jstring defJ)
  8. {
  9. int len;
  10. const char* key;
  11. char buf[PROPERTY_VALUE_MAX];
  12. jstring rvJ = NULL;
  13. if (keyJ == NULL) {
  14. jniThrowNullPointerException(env, "key must not be null.");
  15. goto error;
  16. }
  17. key = env->GetStringUTFChars(keyJ, NULL);
  18. len = property_get(key, buf, "");
  19. if ((len <= 0) && (defJ != NULL)) {
  20. rvJ = defJ;
  21. } else if (len >= 0) {
  22. rvJ = env->NewStringUTF(buf);
  23. } else {
  24. rvJ = env->NewStringUTF("");
  25. }
  26. env->ReleaseStringUTFChars(keyJ, key);
  27. error:
  28. return rvJ;
  29. }

最终调用的还是property_get()函数。

5      尾声

至此,有关Android属性机制的大体机理就讲解完毕了,希望对大家有点儿帮助。

转自http://blog.csdn.net/codefly/article/details/48379239

深入讲解Android Property机制的更多相关文章

  1. 史上最详细的Android消息机制源码解析

    本人只是Android菜鸡一个,写技术文章只是为了总结自己最近学习到的知识,从来不敢为人师,如果里面有不正确的地方请大家尽情指出,谢谢! 606页Android最新面试题含答案,有兴趣可以点击获取. ...

  2. Android绘图机制(二)——自定义View绘制形, 圆形, 三角形, 扇形, 椭圆, 曲线,文字和图片的坐标讲解

    Android绘图机制(二)--自定义View绘制形, 圆形, 三角形, 扇形, 椭圆, 曲线,文字和图片的坐标讲解 我们要想画好一些炫酷的View,首先我们得知道怎么去画一些基础的图案,比如矩形,圆 ...

  3. Android群英传笔记——第七章:Android动画机制和使用技巧

    Android群英传笔记--第七章:Android动画机制和使用技巧 想来,最 近忙的不可开交,都把看书给冷落了,还有好几本没有看完呢,速度得加快了 今天看了第七章,Android动画效果一直是人家中 ...

  4. Android消息机制

    每一个Android应用在启动的时候都会创建一个线程,这个线程被称为主线程或者UI线程,Android应用的所有操作默认都会运行在这个线程中. 但是当我们想要进行数据请求,图片下载,或者其他耗时操作时 ...

  5. 【Android 开发】: Android 消息处理机制之一: Handler 与 Message

    最近几讲内容,我们学习了Android中关于多线程的一些知识,上一讲我们讲解了异步任务 AsyncTask 的操作,Android中还提供了其他的线程操作,如Handler Message Messa ...

  6. Android渲染机制和丢帧分析

    http://blog.csdn.net/bd_zengxinxin/article/details/52525781 自己编写App的时候,有时会感觉界面卡顿,尤其是自定义View的时候,大多数是因 ...

  7. Android反射机制实现与原理

    本文介绍Android反射机制实现与原理,在介绍之前,要和Java进行比较,所以先看下Java中的反射相关知识: 一.反射的概念及在Java中的类反射 反射主要是指程序可以访问.检测和修改它本身状态或 ...

  8. 解析Android消息处理机制:Handler/Thread/Looper & MessageQueue

    解析Android消息处理机制 ——Handler/Thread/Looper & MessageQueue Keywords: Android Message HandlerThread L ...

  9. Android开发之漫漫长途 ⅥI——Android消息机制(Looper Handler MessageQueue Message)

    该文章是一个系列文章,是本人在Android开发的漫漫长途上的一点感想和记录,我会尽量按照先易后难的顺序进行编写该系列.该系列引用了<Android开发艺术探索>以及<深入理解And ...

随机推荐

  1. php之快速入门学习-3(print和echo)

    PHP echo 和 print 语句 echo 和 print 区别: echo - 可以输出一个或多个字符串 print - 只允许输出一个字符串,返回值总为 1 提示:echo 输出的速度比 p ...

  2. jQuery知识集锦

      CreateTime--2017年2月16日14:00:22Author:MarydonjQuery知识集锦1.empty()与remove()的区别 <select id="ty ...

  3. SettingsHBuilder

      迁移时间:2017年5月20日10:56:50CreateTime--2016年9月27日14:22:26Author:Marydon1.修改HBuilder开发工具快捷键工具-->首选项- ...

  4. html中文乱码问题的解决

    当我试着用html写代码的时候,发现直接保存的文件用浏览器打开时中文显示是乱码的,所以我找了一些解决方法,可是原因不太明白,所以我也就不解释了,能够自己找找原因,以下提供解决方法: 在写的html的第 ...

  5. 关于NHibernate中存在于Session中实例的3种状态的简单分析

    在使用NHibernate的时候.在Session中会有3种状态. 1. 瞬时状态 (Transient) 由 new 命令开辟内存空间的对象,也就是平时所熟悉的普通对象. 如: Student st ...

  6. 域名无法解析 Linux临时或永久修改DNS

    最近给VPS重装了系统,因为服务商不提供DHCP,所以只好手动设置IP和DNS Server.悲催的是系统重装的时候忘记了输入DNS Server,最后导致进去系统后,各种域名无法解析. Linux中 ...

  7. ocat 资源路径-时间控件

    http://www.htmleaf.com/jQuery/Calendar-Date-Time-picker/201504251737.html

  8. 【微信小程序】:评论、回复和删除功能 -- 2017/7/14

    1.理论核心:传参->pid,评论父id需要在wxml页面传递:小程序端和WEB端不同核心:前者操纵数据,后者操纵DOM元素对象 2.不废话,直接代码:wxml <view class=& ...

  9. 微信小程序:bindtap方法传参

    1.wxml <view bindtap="pay_again" data-name="{{orderList.jid}}" data-fee=" ...

  10. 小白心目中的Java抽象类(abstract class)

    在java开发中,我们有时会定义了一个父类,这个父类只有对方法的描述,但却没有在父类中写出对方法的实现,这种被定义的方法称为抽象方法.那么理所当然,含有抽象方法的类就称为抽象类.用关键字abstrac ...