深入剖析Android中init进程实现的C语言源码

概述

init是一个进程,确切的说,它是Linux系统中用户空间的第一个进程。由于Android是基于Linux内核的,所以init也是Android系统中用户空间的第一个进程。init的进程号是1。作为天字第一号进程,init有很多重要的工作:

  • init提供property service(属性服务)来管理Android系统的属性。
  • init负责创建系统中的关键进程,包括zygote。

以往的文章一上来就介绍init的源码,但是我这里先从这两个主要工作开始。搞清楚这两个主要工作是如何实现的,我们再回头来看init的源码。

这篇文章主要是介绍init进程的属性服务。

跟init属性服务相关的源码目录如下:

    system/core/init/
    bionic/libc/bionic/
    system/core/libcutils/

属性服务

在windows平台上有一个叫做注册表的东西,它可以存储一些类似key/value的键值对。一般而言,系统或者某些应用程序会把自己的一些属性存储在注册表中,即使系统重启或应用程序重启,它还能根据之前在注册表中设置的属性值,进行相应的初始化工作。

Android系统也提供了类似的机制,称之为属性服务(property service)。应用程序可以通过这个服务查询或者设置属性。我们可以通过如下命令,获取手机中属性键值对。

adb shell getprop

例如红米Note手机的属性值如下:

[ro.product.device]: [lcsh92_wet_jb9]
[ro.product.locale.language]: [zh]
[ro.product.locale.region]: [CN]
[ro.product.manufacturer]: [Xiaomi]

在system/core/init/init.c文件的main函数中,跟属性服务的相关代码如下:

property_init();
queue_builtin_action(property_service_init_action, "property_service_init");

接下来,我们分别看一下这两处代码的具体实现。
属性服务初始化
创建存储空间

首先,我们先来看一下property_init函数的源码(/system/core/init/property_service.c):

void property_init(void)
{
  init_property_area();
}

property_init函数中只是简单的调用了init_property_area方法,接下来我们看一下这个方法的具体实现:

static int property_area_inited = 0;
static workspace pa_workspace;
static int init_property_area(void)
{
  // 属性空间是否已经初始化
  if (property_area_inited)
    return -1;

  if (__system_property_area_init())
    return -1;

  if (init_workspace(&pa_workspace, 0))
    return -1;

  fcntl(pa_workspace.fd, F_SETFD, FD_CLOEXEC);

  property_area_inited = 1;
  return 0;
}

从init_property_area函数,我们可以看出,函数首先判断属性内存区域是否已经初始化过,如果已经初始化,则返回-1。如果没有初始化,我们接下来会发现有两个关键函数__system_property_area_init和init_workspace应该是跟内存区域初始化相关。那我们分别分析一下这两个函数具体实现。

__system_property_area_init

__system_property_area_init函数位于/bionic/libc/bionic/system_properties.c文件中,具体代码实现如下:

struct prop_area {
  unsigned bytes_used;
  unsigned volatile serial;
  unsigned magic;
  unsigned version;
  unsigned reserved[28];
  char data[0];
};
typedef struct prop_area prop_area;
prop_area *__system_property_area__ = NULL;

#define PROP_FILENAME "/dev/__properties__"
static char property_filename[PATH_MAX] = PROP_FILENAME; 

#define PA_SIZE (128 * 1024)

static int map_prop_area_rw()
{
  prop_area *pa;
  int fd;
  int ret;

  /**
   * O_RDWR ==> 读写
   * O_CREAT ==> 若不存在,则创建
   * O_NOFOLLOW ==> 如果filename是软链接,则打开失败
   * O_EXCL ==> 如果使用O_CREAT是文件存在,则可返回错误信息
   */
  fd = open(property_filename, O_RDWR | O_CREAT | O_NOFOLLOW | O_CLOEXEC | O_EXCL, 0444);
  if (fd < 0) {
    if (errno == EACCES) {
      abort();
    }
    return -1;
  }

  ret = fcntl(fd, F_SETFD, FD_CLOEXEC);
  if (ret < 0)
    goto out;

  if (ftruncate(fd, PA_SIZE) < 0)
    goto out;

  pa_size = PA_SIZE;
  pa_data_size = pa_size - sizeof(prop_area);
  compat_mode = false;

  // mmap映射文件实现共享内存
  pa = mmap(NULL, pa_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
  if (pa == MAP_FAILED)
    goto out;

  /*初始化内存地址中所有值为0*/
  memset(pa, 0, pa_size);
  pa->magic = PROP_AREA_MAGIC;
  pa->version = PROP_AREA_VERSION;
  pa->bytes_used = sizeof(prop_bt);

  __system_property_area__ = pa;

  close(fd);
  return 0;

out:
  close(fd);
  return -1;
}

int __system_property_area_init()
{
  return map_prop_area_rw();
}

代码比较好理解,主要内容是利用mmap映射property_filename创建了一个共享内存区域,并将共享内存的首地址赋值给全局变量__system_property_area__。

关于mmap映射文件实现共享内存IPC通信机制,可以参考这篇文章:mmap实现IPC通信机制
init_workspace

接下来,我们来看一下init_workspace函数的源码(/system/core/init/property_service.c):

typedef struct {
  void *data;
  size_t size;
  int fd;
}workspace;

static int init_workspace(workspace *w, size_t size)
{
  void *data;
  int fd = open(PROP_FILENAME, O_RDONLY | O_NOFOLLOW);
  if (fd < 0)
    return -1;

  w->size = size;
  w->fd = fd;
  return 0;
}

客户端进程访问属性内存区域

虽然属性内存区域是init进程创建的,但是Android系统希望其他进程也能够读取这块内存区域里的内容。为了做到这一点,init进程在属性区域初始化过程中做了如下两项工作:

把属性内存区域创建在共享内存上,而共享内存是可以跨进程的。这一点,在上述代码中是通过mmap映射/dev/__properties__文件实现的。pa_workspace变量中的fd成员也保存了映射文件的句柄。
    如何让其他进程知道这个共享内存句柄呢?Android先将文件映射句柄赋值给__system_property_area__变量,这个变量属于bionic_lic库中的输出的一个变量,然后利用了gcc的constructor属性,这个属性指明了一个__lib_prenit函数,当bionic_lic库被加载时,将自动调用__libc_prenit,这个函数内部完成共享内存到本地进程的映射工作。

只讲原理是不行的,我们直接来看一下__lib_prenit函数代码的相关实现:

void __attribute__((constructor)) __libc_prenit(void);
void __libc_prenit(void)
{
  // ...
  __libc_init_common(elfdata); // 调用这个函数
  // ...
}

__libc_init_common函数为:

void __libc_init_common(uintptr_t *elfdata)
{
  // ...
  __system_properties_init(); // 初始化客户端的属性存储区域
}

__system_properties_init函数有回到了我们熟悉的/bionic/libc/bionic/system_properties.c文件:

static int get_fd_from_env(void)
{
  char *env = getenv("ANDROID_PROPERTY_WORKSPACE");

  if (! env) {
    return -1;
  }

  return atoi(env);
}

static int map_prop_area()
{
  bool formFile = true;
  int result = -1;
  int fd;
  int ret;

  fd = open(property_filename, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
  if (fd >= 0) {
    /* For old kernels that don't support O_CLOEXEC */
    ret = fcntl(fd, F_SETFD, FD_CLOEXEC);
    if (ret < 0)
      goto cleanup;
  }

  if ((fd < 0) && (error == ENOENT)) {
    fd = get_fd_from_env();
    fromFile = false;
  }

  if (fd < 0) {
    return -1;
  }

  struct stat fd_stat;
  if (fstat(fd, &fd_stat) < 0) {
    goto cleanup;
  }

  if ((fd_stat.st_uid != 0)
      || (fd_stat.st_gid != 0)
      || (fd_stat.st_mode & (S_IWGRP | S_IWOTH) != 0)
      || (fd_stat.st_size < sizeof(prop_area))) {
    goto cleanup;
  }

  pa_size = fd_stat.st_size;
  pa_data_size = pa_size - sizeof(prop_area);

  /*
   * 映射init创建的属性内存到本地进程空间,这样本地进程就可以使用这块共享内存了。
   * 注意:映射时制定了PROT_READ属性,所以客户端进程只能读属性,不能设置属性。
   */
  prop_area *pa = mmap(NULL, pa_size, PROT_READ, MAP_SHARED, fd, 0);

  if (pa == MAP_FAILED) {
    goto cleanup;
  }

  if ((pa->magic != PROP_AREA_MAGIC) || (pa->version != PROP_AREA_VERSION && pa->version != PROP_AREA_VERSION_COMPAT)) {
    munmap(pa, pa_size);
    goto cleanup;
  }

  if (pa->version == PROP_AREA_VERSION_COMPAT) {
    compat_mode = true;
  }

  result = 0;

  __system_property_area__ = pa;
cleanup:
  if (fromFile) {
    close(fd);
  }

  return result;
}

int __system_properties_init()
{
  return map_prop_area();
}

通过对源码的阅读,可以发现,客户端通过mmap映射,可以读取属性内存的内容,但是没有权限设置属性。那客户端是如何设置属性的呢?这就涉及到下面要将的属性服务器了。
属性服务器的分析

init进程会启动一个属性服务器,而客户端只能通过与属性服务器的交互来设置属性。
启动属性服务器

先来看一下属性服务器的内容,它由property_service_init_action函数启动,源码如下(/system/core/init/init.c&&property_service.c):

static int property_service_init_action(int nargs, char **args)
{
  start_property_service();
  return 0;
}

static void load_override_properties()
{
#ifdef ALLOW_LOCAL_PROP_OVERRIDE
  char debuggable[PROP_VALUE_MAX];
  int ret;

  ret = property_get("ro.debuggable", debuggable);
  if (ret && (strcmp(debuggable, "1") == 0)) {
    load_properties_from_file(PROP_PATH_LOCAL_OVERRIDE);
  }
#endif
}

static void load_properties(char *data)
{
  char *key, *value, *eol, *sol, *tmp;

  sol = data;
  while ((eol = strchr(sol, '\n'))) {
    key = sol;
    // 赋值下一行的指针给sol
    *eol ++ = 0;
    sol = eol;

    value = strchr(key, '=');
    if (value == 0) continue;
    *value++ = 0;

    while (isspace(*key)) key ++;
    if (*key == '#') continue;
    tmp = value - 2;
    while ((tmp > key) && isspace(*tmp)) *tmp-- = 0;

    while (isspace(*value)) value ++;
    tmp = eol - 2;
    while ((tmp > value) && isspace(*tmp)) *tmp-- = 0;

    property_set(key, value);
  }
}

int create_socket(const char *name, int type, mode_t perm, uid_t uid, gid_t gid)
{
  struct sockaddr_un addr;
  int fd, ret;
  char *secon;

  fd = socket(PF_UNIX, type, 0);
  if (fd < 0) {
    ERROR("Failed to open socket '%s': %s\n", name, strerror(errno));
    return -1;
  }

  memset(&addr, 0, sizeof(addr));
  addr.sun_family = AF_UNIX;
  snprintf(addr.sun_path, sizeof(addr.sun_path), ANDROID_SOCKET_DIR"/%s", name);

  ret = unlink(addr.sun_path);
  if (ret != 0 && errno != ENOENT) {
    goto out_close;
  }

  ret = bind(fd, (struct sockaddr *)&addr, sizeof(addr));
  if (ret) {
    goto out_unlink;
  }
  chown(addr.sun_path, uid, gid);
  chmod(addr.sun_path, perm);

  return fd;

out_unlink:
  unlink(addr.sun_path);
out_close:
  close(fd);
  return -1;
}

#define PROP_PATH_SYSTEM_BUILD "/system/build.prop"
#define PROP_PATH_SYSTEM_DEFAULT "/system/default.prop"
#define PROP_PATH_LOCAL_OVERRIDE "/data/local.prop"
#define PROP_PATH_FACTORY "/factory/factory.prop"

void start_property_service(void)
{
  int fd;

  load_properties_from_file(PROP_PATH_SYSTEM_BUILD);
  load_properties_from_file(PROP_PATH_SYSTEM_DEFAULT);
  load_override_properties();
  /*Read persistent properties after all default values have been loaded.*/
  load_persistent_properties();

  fd = create_socket(PROP_SERVICE_NAME, SOCK_STREAM, 0666, 0, 0);
  if (fd < 0) return;
  fcntl(fd, F_SETFD, FD_CLOEXEC);
  fcntl(fd, F_SETFL, O_NONBLOCK);

  listen(fd, 8);
  property_set_fd = fd;
}

从上述代码可以看到,init进程除了会预写入指定文件(例如:system/build.prop)属性外,还会创建一个UNIX Domain Socket,用于接受客户端的请求,构建属性。那这个socket请求是再哪里被处理的呢?
答案是:在init中的for循环处已经进行了相关处理。

服务端处理设置属性请求

接收属性设置请求的地方是在init进程中,相关代码如下所示:

int main(int argc, char **argv)
{
  // ...省略不相关代码

  for (;;) {
    // ...
    for (i = 0; i < fd_count; i ++) {
      if (ufds[i].fd == get_property_set_fd())
        handle_property_set_fd();
    }
  }
}

从上述代码可以看出,当属性服务器收到客户端请求时,init进程会调用handle_property_set_fd函数进行处理,函数位置是:system/core/init/property_service.c,我们来看一下这个函数的实现源码:

void handle_property_set_fd()
{
  prop_msg msg;
  int s;
  int r;
  int res;
  struct ucred cr;
  struct sockaddr_un addr;
  socklen_t addr_size = sizeof(addr);
  socklen_t cr_size = sizeof(cr);
  char *source_ctx = NULL;

  // 接收TCP连接
  if ((s = accept(property_set_fd, (struct sockaddr *) &addr, &addr_size)) < 0) {
    return;
  }

  // 接收客户端请求数据
  r = TEMP_FAILURE_RETRY(recv(s, &msg, sizeof(msg), 0));
  if (r != sizeof(prop_msg)) {
    ERROR("sys_prop: mis-match msg size received: %d expected : %d errno: %d\n", r, sizeof(prop_msg), errno);
    close(s);
    return;
  }

  switch(msg.cmd) {
  case PROP_MSG_SETPROP:
    msg.name[PROP_NAME_MAX - 1] = 0;
    msg.value[PROP_VALUE_MAX - 1] = 0;

    if (memcmp(msg.name, "ctl.", 4) == 0) {
      close(s);
      if (check_control_perms(msg.value, cr.uid, cr.gid, source_ctx)) {
        handle_control_message((char*) msg.name + 4, (char*) msg.value);
      } else {
        ERROR("sys_prop: Unable to %s service ctl [%s] uid:%d gid:%d pid:%d\n", msg.name + 4, msg.value, cr.uid, cr.gid, cr.pid);
      }
    } else {
      if (check_perms(msg.name, cr.uid, cr.gid, source_ctx)) {
        property_set((char *) msg.name, (char*) msg.value);
      }
      close(s);
    }
    break;
  default:
    close(s);
    break;
  }
}

当客户端的权限满足要求时,init就调用property_set进行相关处理。property_set源码实现如下:

int property_set(const char *name, const char *value)
{
  prop_info *pi;
  int ret;

  size_t namelen = strlen(name);
  size_t valuelen = strlen(value);

  if (! is_legal_property_name(name, namelen)) return -1;
  if (valuelen >= PROP_VALUE_MAX) return -1;

  // 从属性空间中寻找是否已经存在该属性值
  pi = (prop_info*) __system_property_find(name);
  if (pi != 0) {
    // ro开头的属性被设置后,不允许再被修改
    if (! strncmp(name, "ro.", 3)) return -1;

    __system_property_update(pi, value, valuelen);
  } else {
    ret = __system_property_add(name, namelen, value, valuelen);
  }

  // 有一些特殊的属性需要特殊处理,例如net.和persist.开头的属性
  if (strncmp("net.", name, strlen("net.")) == 0) {
    if (strcmp("net.change", name) == 0) {
      return 0;
    }
    property_set("net.change", name);
  } else if (persistent_properties_loaded && strncmp("persist.", name, strlen("persist.")) == 0) {
    write_persistent_property(name, value);
  }
  property_changed(name, value);
  return 0;
}

属性服务器端的工作基本到这里就完成了。最后,我们来看一下客户端是如何发送设置属性的socket请求。
客户端发送请求

客户端设置属性时是调用了property_set(“sys.istest”, “true”)方法。从上述分析可知,该方法实现跟服务器端的property_set方法不同,该方法一定是发送了socket请求,该方法源码位置为:/system/core/libcutils/properties.c:

int property_set(const char *key, const char *value)
{
  return __system_property_set(key, value);
}

可以看到,property_set调用了__system_property_set方法,这个方法位于:/bionic/libc/bionic/system_properties.c文件中:

struct prop_msg
{
  unsigned cmd;
  char name[PROP_NAME_MAX];
  char value[PROP_VALUE_MAX];
};
typedef struct prop_msg prop_msg;

static int send_prop_msg(prop_msg *msg)
{
  struct pollfd pollfds[1];
  struct sockaddr_un addr;
  socklen_t alen;
  size_t namelen;
  int s;
  int r;
  int result = -1;

  s = socket(AF_LOCAL, SOCK_STREAM, 0);
  if (s < 0) {
    return result;
  }

  memset(&addr, 0, sizeof(addr));
  namelen = strlen(property_service_socket);
  strlcpy(addr.sun_path, property_service_socket, sizeof(addr.sun_path));
  addr.sun_family = AF_LOCAL;
  alen = namelen + offsetof(struct sockaddr_un, sun_path) + 1;

  if (TEMP_FAILURE_RETRY(connect(s, (struct sockaddr *) &addr, alen)) < 0) {
    close(s);
    return result;
  }

  r = TEMP_FAILURE_RETRY(send(s, msg, sizeof(prop_msg), 0));

  close(s);
  return result;
}

int __system_property_set(const char *key, const char *value)
{
  int err;
  prop_msg msg;

  if (key == 0) return -1;
  if (value == 0) value = "";
  if (strlen(key) >= PROP_NAME_MAX) return -1;
  if (strlen(value) >= PROP_VALUE_MAX) return -1;

  memset(&msg, 0, sizeof(msg));
  msg.cmd = PROP_MSG_SETPROP;
  strlcpy(msg.name, key, sizeof(msg.name));
  strlcpy(msg.value, value, sizeof(msg.value));

  err = send_prop_msg(&msg);
  if (err < 0) {
    return err;
  }
  return 0;
}
(0)

相关推荐

  • Android init.rc文件详解及简单实例

    Android init.rc文件详解 本文主要来自$ANDROID_SOURCE/system/init/readme.txt的翻译. 1 简述 Android init.rc文件由系统第一个启动的init程序解析,此文件由语句组成,主要包含了四种类型的语句:Action,Commands,Services,Options.在init.rc文件中一条语句通常是占据一行.单词之间是通过空格符来相隔的.如果需要在单词内使用空格,那么得使用转义字符"\",如果在一行的末尾有一个反斜杠,那么

  • Android init.rc文件简单介绍

    Android init.rc文件简单介绍 init.rc脚本是由Android中linux的第一个用户级进程init进行解析的. init.rc 文件并不是普通的配置文件,而是由一种被称为"Android初始化语言"(Android Init Language,这里简称为AIL)的脚本写成的文件. 该文件在ROM中是只读的,即使有了root权限,可以修改该文件也没有.因为我们在根目录看到的文件只是内存文件的镜像.也就是说,android启动后,会将init.rc文件装载到内存.而修改

  • Android中init.rc文件的解析 分享

    对init.rc的解析是在parse_config(): [system/core/init/init_parser.c]中进行的.解析发生在init全过程中的哪个阶段,参看<Android init进程启动过程分析>. 一.解析过程 1.      扫描init.rc中的token 找到其中的 文件结束EOF/文本TEXT/新行NEWLINE,其中的空格' '.'\t'.'\r'会被忽略,#开头的行也被忽略掉: 而对于TEXT,空格' '.'\t'.'\r'.'\n'都是TEXT的结束标志.

  • Android Init进程对信号的处理流程详细介绍

    Android  Init进程对信号的处理流程 在Android中,当一个进程退出(exit())时,会向它的父进程发送一个SIGCHLD信号.父进程收到该信号后,会释放分配给该子进程的系统资源:并且父进程需要调用wait()或waitpid()等待子进程结束.如果父进程没有做这种处理,且父进程初始化时也没有调用signal(SIGCHLD, SIG_IGN)来显示忽略对SIGCHLD的处理,这时子进程将一直保持当前的退出状态,不会完全退出.这样的子进程不能被调度,所做的只是在进程列表中占据一个

  • Android miniTwitter登录界面开发实例

    本文要演示的Android开发实例是如何完成一个Android中的miniTwitter登录界面,下面将分步骤讲解怎样实现图中的界面效果,让大家都能轻松的做出美观的登录界面. 先贴上最终要完成的效果图: miniTwitter登录界面的布局分析 首先由界面图分析布局,基本可以分为三个部分,下面分别讲解每个部分. 第一部分是一个带渐变色背景的LinearLayout布局,关于背景渐变色就不再贴代码了,效果如下图所示: 第二部分,红色线区域内,包括1,2,3,4 如图所示: 红色的1表示的是一个带圆

  • 深入剖析Android中init进程实现的C语言源码

    概述 init是一个进程,确切的说,它是Linux系统中用户空间的第一个进程.由于Android是基于Linux内核的,所以init也是Android系统中用户空间的第一个进程.init的进程号是1.作为天字第一号进程,init有很多重要的工作: init提供property service(属性服务)来管理Android系统的属性. init负责创建系统中的关键进程,包括zygote. 以往的文章一上来就介绍init的源码,但是我这里先从这两个主要工作开始.搞清楚这两个主要工作是如何实现的,我

  • Android中图片压缩方案详解及源码下载

    Android中图片压缩方案详解及源码下载 图片的展示可以说在我们任何一个应用中都避免不了,可是大量的图片就会出现很多的问题,比如加载大图片或者多图时的OOM问题,可以移步到Android高效加载大图及多图避免程序OOM.还有一个问题就是图片的上传下载问题,往往我们都喜欢图片既清楚又占的内存小,也就是尽可能少的耗费我们的流量,这就是我今天所要讲述的问题:图片的压缩方案的详解. 1.质量压缩法 设置bitmap options属性,降低图片的质量,像素不会减少 第一个参数为需要压缩的bitmap图

  • 深入剖析Android中Service和Thread区别

    Service既不是进程也不是线程,它们之间的关系如下: 可能有的朋友会问了,既然是长耗时的操作,那么Thread也可以完成啊.没错,在程序里面很多耗时工作我们也可以通过Thread来完成,那么还需要Service干嘛呢.接下来就为大家解释以下Service和Thread的区别. 首先要说明的是,进程是系统最小资源分配单位,而线程是则是最小的执行单位,线程需要的资源通过它所在的进程获取. Service与Thread的区别: Thread:Thread 是程序执行的最小单元,可以用 Thread

  • 深入理解Python虚拟机中复数(complex)的实现原理及源码剖析

    目录 复数数据结构 复数的操作 复数加法 复数取反 Repr 函数 总结 复数数据结构 在 cpython 当中对于复数的数据结构实现如下所示: typedef struct { double real; double imag; } Py_complex; #define PyObject_HEAD PyObject ob_base; typedef struct { PyObject_HEAD Py_complex cval; } PyComplexObject; typedef struc

  • Java编程删除链表中重复的节点问题解决思路及源码分享

    一. 题目 在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针. 二. 例子 输入链表:1->2->3->3->4->4->5 处理后为:1->2->5 三. 思路 个人感觉这题关键是注意指针的指向,可以定义一个first对象(值为-1,主要用于返回操作后的链表),first.next指向head,定义一个last同样指向first(主要用于操作记录要删除节点的前一个节点),定义一个p指向head,指向当前节点.

  • Android编程实现网络图片查看器和网页源码查看器实例

    本文实例讲述了Android编程实现网络图片查看器和网页源码查看器.分享给大家供大家参考,具体如下: 网络图片查看器 清单文加入网络访问权限: <!-- 访问internet权限 --> <uses-permission android:name="android.permission.INTERNET"/> 界面如下: 示例: public class MainActivity extends Activity { private EditText image

  • java向文件中追加内容与读写文件内容源码实例代码

    java向文件中追加内容与读写文件内容源码实例代码 向文件尾加入内容有多种方法,常见的方法有两种: RandomAccessFile类可以实现随机访问文件的功能,可以以读写方式打开文件夹的输出流 public void seek(long pos)可以将读写指针移到文件尾,参数Pos表示从文件开头以字节为单位测量的偏移位置,在该位置文件指针. public void write(int pos)将数据写到读写指针后面,完成文件的追加.参数pos表示要写入的Byte 通过FileWrite打开文件

  • Python中getpass模块无回显输入源码解析

    本文主要讨论了python中getpass模块的相关内容,具体如下. getpass模块 昨天跟学弟吹牛b安利Python标准库官方文档的时候偶然发现了这个模块.仔细一看内容挺少的,只有两个主要api,就花了点时间阅读了一下源码,感觉挺实用的,在这安利给大家. getpass.getpass(prompt='Password: ', stream=None) 调用该函数可以在命令行窗口里面无回显输入密码.参数prompt代表提示字符串,默认是'Password: '.在Unix系统中,strea

  • RxJava中map和flatMap的用法区别源码解析

    目录 前言: 作用 使用方法: map flatMap 源码分析 map flatMap 结语 前言: RxJava中提供了大量的操作符,这大大提高了了我们的开发效率.其中最基本的两个变换操作符就是map和flatMap.而其他变换操作符的原理基本与map类似. map和flatMap都是接受一个函数作为参数(Func1)并返回一个被观察者Observable Func1的< I,O >I,O模版分别为输入和输出值的类型,实现Func1的call方法对I类型进行处理后返回O类型数据,只是fla

随机推荐