移步系列Android跨进程通信IPC系列

  • 1、Binder驱动简述
  • 2、Binder驱动的核心函数
  • 3、Binder驱动的结构体
  • 4、Binder驱动通信协议
  • 5、Binder驱动内存
  • 6、附录:关于misc

驱动层的原路径(这部分代码不在AOSP中,而是位于Linux内核代码中)

  • /kernel/drivers/android/binder.c
  • /kernel/include/uapi/linux/android/binder.h
    或者
  • /kernel/drivers/staging/android/binder.c
  • /kernel/drivers/staging/android/uapi/binder.h

1 Binder驱动简述

1.1 简述

  • Binder驱动是Android专用的,但底层的驱动架构与Linux驱动一样。
  • Binder驱动在misc设备上进行注册,作为虚拟字符设备,没有直接操作硬件,只对设备内存做处理。
  • 主要工作是:
    1. 驱动设备的初始化(binder_init)
    1. 打开(binder_open)
    1. 映射(binder_mmap)
    1. 数据操作(binder_ioctl)。
5713484-a63bf619f32338a3.png

1.2 系统调用

  • 用户态的程序调用Kernel层驱动是需要陷入内核态,进行系统调用(system call,后面简写syscall),比如打开Binder驱动方法的调用链为:open——> _open()——> binder_open() 。
  • open() 为用户态的函数,_open()便是系统调用(syscall)中的响应的处理函数,通过查找,调用内核态中对应的驱动binder_open()函数,至于其他的从用户态陷入内核态的流程也基本一致。


    5713484-13ac71586128bd39.png

    当用户空间调用open()函数,最终会调用binder驱动的binder_open()函数;mmap()/ioctl()函数也是同理,Binder的系统中的用户态进入内核态都依赖系统调用过程。

1.3 Binder驱动的四个核心方法

1.3.1 binder_init()函数

代码在/kernel/drivers/android/binder.c
代码如下:

//kernel/drivers/android/binder.c      4216行static int __init binder_init(void){    int ret;    //创建名为binder的工作队列    binder_deferred_workqueue = create_singlethread_workqueue("binder");    //   ****  省略部分代码  ****    binder_debugfs_dir_entry_root = debugfs_create_dir("binder", NULL);    if (binder_debugfs_dir_entry_root)        binder_debugfs_dir_entry_proc = debugfs_create_dir("proc",                         binder_debugfs_dir_entry_root);    if (binder_debugfs_dir_entry_root) {        //在debugfs文件系统中创建一系列的问题件        //   ****  省略部分代码  ****    }     //   ****  省略部分代码  ****    while ((device_name = strsep(&device_names, ","))) {       //binder设备初始化        ret = init_binder_device(device_name);        if (ret)            //binder设备初始化失败            goto err_init_binder_device_failed;    }    return ret;}
  • debugfs_create_dir是指在debugfs文件系统中创建一个目录,返回的是指向dentry的指针。
  • 当kernel中禁用debugfs的话,返回值是 -%ENODEV。默认是禁用的。如果需要打开,在目录/kernel/arch/arm64/configs/下找到目标defconfig文件中添加一行CONFIG_DEBUG_FS=y,再重新编译版本,即可打开debug_fs。

1.3.2 init_binder_device()函数解析

//kernel/drivers/android/binder.c      4189行static int __init init_binder_device(const char *name){    int ret;    struct binder_device *binder_device;    binder_device = kzalloc(sizeof(*binder_device), GFP_KERNEL);    if (!binder_device)        return -ENOMEM;    binder_device->miscdev.fops = &binder_fops;    binder_device->miscdev.minor = MISC_DYNAMIC_MINOR;    binder_device->miscdev.name = name;    binder_device->context.binder_context_mgr_uid = INVALID_UID;    binder_device->context.name = name;        //注册 misc设备    ret = misc_register(&binder_device->miscdev);    if (ret < 0) {        kfree(binder_device);        return ret;    }    hlist_add_head(&binder_device->hlist, &binder_devices);    return ret;}

通过调用misc_register()函数来注册misc设备,miscdevice结构体,便是前面注册misc设备时传递进去的参数

1.3.2.1binder_device的结构体

binder_device的结构体

//kernel/drivers/android/binder.c      234行struct binder_device {    struct hlist_node hlist;    struct miscdevice miscdev;    struct binder_context context;};

在binder_device里面有一个miscdevice

//设备文件操作结构,这是file_operation结构binder_device->miscdev.fops = &binder_fops;   // 次设备号 动态分配binder_device->miscdev.minor = MISC_DYNAMIC_MINOR;// 设备名binder_device->miscdev.name = name;// uidbinder_device->context.binder_context_mgr_uid = INVALID_UID;//上下文的名字binder_device->context.name = name;

1.3.2.2 file_operations的结构体

file_operations 结构体,指定相应文件操作的方法

/kernel/drivers/android/binder.c       4173行static const struct file_operations binder_fops = {    .owner = THIS_MODULE,    .poll = binder_poll,    .unlocked_ioctl = binder_ioctl,    .compat_ioctl = binder_ioctl,    .mmap = binder_mmap,    .open = binder_open,    .flush = binder_flush,    .release = binder_release,};

1.3.3 binder_open()函数解析

开打binder驱动设备

/kernel/drivers/android/binder.c     3456行static int binder_open(struct inode *nodp, struct file *filp){        // binder进程        struct binder_proc *proc;        struct binder_device *binder_dev;        binder_debug(BINDER_DEBUG_OPEN_CLOSE, "binder_open: %d:%d\n",             current->group_leader->pid, current->pid);        //为binder_proc结构体在再分配kernel内存空间        proc = kzalloc(sizeof(*proc), GFP_KERNEL);        if (proc == NULL)             return -ENOMEM;        get_task_struct(current);        //当前线程的task保存在binder进程的tsk        proc->tsk = current;        proc->vma_vm_mm = current->mm;        //初始化todo列表        INIT_LIST_HEAD(&proc->todo);         //初始化wait队列        init_waitqueue_head(&proc->wait);        // 当前进程的nice值转化为进程优先级        proc->default_priority = task_nice(current);        binder_dev = container_of(filp->private_data, struct binder_device,miscdev);        proc->context = &binder_dev->context;        //同步锁,因为binder支持多线程访问        binder_lock(__func__);        //BINDER_PROC对象创建+1        binder_stats_created(BINDER_STAT_PROC);        //将proc_node节点添加到binder_procs为表头的队列        hlist_add_head(&proc->proc_node, &binder_procs);        proc->pid = current->group_leader->pid;        INIT_LIST_HEAD(&proc->delivered_death);        //file文件指针的private_data变量指向binder_proc数据        filp->private_data = proc;        //释放同步锁        binder_unlock(__func__);        if (binder_debugfs_dir_entry_proc) {        char strbuf[11];        snprintf(strbuf, sizeof(strbuf), "%u", proc->pid);/** proc debug entries are shared between contexts, so* this will fail if the process tries to open the driver* again with a different context. The priting code will* anyway print all contexts that a given PID has, so this* is not a problem.*/        proc->debugfs_entry =debugfs_create_file(strbuf,S_IRUGO,binder_debugfs_dir_entry_proc,(void *)(unsigned long)proc->pid,&binder_proc_fops);    }    return 0;}
  • 创建binder_proc对象,并把当前进程等信息保存到binder_proc对象,该对象管理IPC所需的各种新并有用其他结构体的跟结构体;
  • 再把binder_proc对象保存到文件指针filp,以及binder_proc加入到全局链表 binder_proc。
5713484-40182d6c6cda6724.png
  • Binder驱动通过static HIST_HEAD(binder_procs);,创建了全局的哈希链表binder_procs,用于保存所有的binder_procs队列,每次创建的binder_proc对象都会加入binder_procs链表中。

1.3.4 binder_mmap()函数解析

  • binder_mmap(文件描述符,用户虚拟内存空间)
  • 主要功能:
  • 首先在内核虚拟地址空间,申请一块与用户虚拟内存相同的大小的内存;
  • 然后申请1个page大小的物理内存,再将同一块物理内存分别映射到内核虚拟内存空间和用户虚拟内存空间,从而实现了用户空间的buffer与内核空间的buffer同步操作的功能。
/kernel/drivers/android/binder.c      3357行static int binder_mmap(struct file *filp, struct vm_area_struct *vma){    int ret;    //内核虚拟空间    struct vm_struct *area;    struct binder_proc *proc = filp->private_data;    const char *failure_string;    struct binder_buffer *buffer;    if (proc->tsk != current)        return -EINVAL;    //保证映射内存大小不超过4M    if ((vma->vm_end - vma->vm_start) > SZ_4M)        vma->vm_end = vma->vm_start + SZ_4M;    binder_debug(BINDER_DEBUG_OPEN_CLOSE,             "binder_mmap: %d %lx-%lx (%ld K) vma %lx pagep %lx\n",             proc->pid, vma->vm_start, vma->vm_end,             (vma->vm_end - vma->vm_start) / SZ_1K, vma->vm_flags,             (unsigned long)pgprot_val(vma->vm_page_prot));    if (vma->vm_flags & FORBIDDEN_MMAP_FLAGS) {        ret = -EPERM;        failure_string = "bad vm_flags";        goto err_bad_arg;    }    vma->vm_flags = (vma->vm_flags | VM_DONTCOPY) & ~VM_MAYWRITE;    mutex_lock(&binder_mmap_lock);    if (proc->buffer) {        ret = -EBUSY;        failure_string = "already mapped";        goto err_already_mapped;    }     //分配一个连续的内核虚拟空间,与进程虚拟空间大小一致    area = get_vm_area(vma->vm_end - vma->vm_start, VM_IOREMAP);    if (area == NULL) {        ret = -ENOMEM;        failure_string = "get_vm_area";        goto err_get_vm_area_failed;    }     //指向内核虚拟空间的地址    proc->buffer = area->addr;      //地址便宜量=用户空间地址-内核空间地址    proc->user_buffer_offset = vma->vm_start - (uintptr_t)proc->buffer;      // 释放锁    mutex_unlock(&binder_mmap_lock);#ifdef CONFIG_CPU_CACHE_VIPT    if (cache_is_vipt_aliasing()) {        while (CACHE_COLOUR((vma->vm_start ^ (uint32_t)proc->buffer))) {            pr_info("binder_mmap: %d %lx-%lx maps %p bad alignment\n", proc->pid, vma->vm_start, vma->vm_end, proc->buffer);            vma->vm_start += PAGE_SIZE;        }    }#endif        //分配物理页的指针数组,大小等于用户虚拟内存/4K    proc->pages = kzalloc(sizeof(proc->pages[0]) * ((vma->vm_end - vma->vm_start) / PAGE_SIZE), GFP_KERNEL);    if (proc->pages == NULL) {        ret = -ENOMEM;        failure_string = "alloc page array";        goto err_alloc_pages_failed;    }    proc->buffer_size = vma->vm_end - vma->vm_start;    vma->vm_ops = &binder_vm_ops;    vma->vm_private_data = proc;    // 分配物理页面,同时映射到内核空间和进程空间,目前只分配1个page的物理页    if (binder_update_page_range(proc, 1, proc->buffer, proc->buffer + PAGE_SIZE, vma)) {        ret = -ENOMEM;        failure_string = "alloc small buf";        goto err_alloc_small_buf_failed;    }     // binder_buffer对象,指向proc的buffer地址    buffer = proc->buffer;     //创建进程的buffers链表头    INIT_LIST_HEAD(&proc->buffers);     //将binder_buffer地址  加入到所属进程的buffer队列    list_add(&buffer->entry, &proc->buffers);    buffer->free = 1;     //将空闲的buffer放入proc->free_buffer中    binder_insert_free_buffer(proc, buffer);      // 异步可用空间大小为buffer总体大小的一半    proc->free_async_space = proc->buffer_size / 2;    barrier();    proc->files = get_files_struct(current);    proc->vma = vma;    proc->vma_vm_mm = vma->vm_mm;    /*pr_info("binder_mmap: %d %lx-%lx maps %p\n",         proc->pid, vma->vm_start, vma->vm_end, proc->buffer);*/    return 0;//错误跳转err_alloc_small_buf_failed:    kfree(proc->pages);    proc->pages = NULL;err_alloc_pages_failed:    mutex_lock(&binder_mmap_lock);    vfree(proc->buffer);    proc->buffer = NULL;err_get_vm_area_failed:err_already_mapped:    mutex_unlock(&binder_mmap_lock);err_bad_arg:    pr_err("binder_mmap: %d %lx-%lx %s failed %d\n",           proc->pid, vma->vm_start, vma->vm_end, failure_string, ret);    return ret;}

binder_mmap通过加锁,保证一次只有一个进程分享内存,保证多进程间的并发访问。其中user_buffer_offset是虚拟进程地址与虚拟内核地址的差值,也就是说同一物理地址,当内核地址为kernel_addr,则进程地址为proc_addr=kernel_addr+user_buffer_offset。

1.3.4.1 binder_update_page_range()函数解析

/kernel/drivers/android/binder.c      567行static int binder_update_page_range(struct binder_proc *proc, int allocate,                    void *start, void *end,                    struct vm_area_struct *vma){    void *page_addr;    unsigned long user_page_addr;    struct page **page;    struct mm_struct *mm;    binder_debug(BINDER_DEBUG_BUFFER_ALLOC,             "%d: %s pages %p-%p\n", proc->pid,             allocate ? "allocate" : "free", start, end);    if (end <= start)        return 0;    trace_binder_update_page_range(proc, allocate, start, end);    if (vma)        mm = NULL;    else        mm = get_task_mm(proc->tsk);    if (mm) {        down_write(&mm->mmap_sem);        vma = proc->vma;        if (vma && mm != proc->vma_vm_mm) {            pr_err("%d: vma mm and task mm mismatch\n",                proc->pid);            vma = NULL;        }    }    if (allocate == 0)        goto free_range;    if (vma == NULL) {        pr_err("%d: binder_alloc_buf failed to map pages in userspace, no vma\n",            proc->pid);        goto err_no_vma;    }    //  **************** 这里是重点********************    for (page_addr = start; page_addr < end; page_addr += PAGE_SIZE) {        int ret;        page = &proc->pages[(page_addr - proc->buffer) / PAGE_SIZE];        BUG_ON(*page);         // 分配物理内存        *page = alloc_page(GFP_KERNEL | __GFP_HIGHMEM | __GFP_ZERO);        if (*page == NULL) {            pr_err("%d: binder_alloc_buf failed for page at %p\n",                proc->pid, page_addr);            goto err_alloc_page_failed;        }        // 物理空间映射到内核空间        ret = map_kernel_range_noflush((unsigned long)page_addr,                    PAGE_SIZE, PAGE_KERNEL, page);        flush_cache_vmap((unsigned long)page_addr,                (unsigned long)page_addr + PAGE_SIZE);        if (ret != 1) {            pr_err("%d: binder_alloc_buf failed to map page at %p in kernel\n",                   proc->pid, page_addr);            goto err_map_kernel_failed;        }        user_page_addr =            (uintptr_t)page_addr + proc->user_buffer_offset;         // 物理空间映射到虚拟进程空间        ret = vm_insert_page(vma, user_page_addr, page[0]);        if (ret) {            pr_err("%d: binder_alloc_buf failed to map page at %lx in userspace\n",                   proc->pid, user_page_addr);            goto err_vm_insert_page_failed;        }        /* vm_insert_page does not seem to increment the refcount */    }    if (mm) {        up_write(&mm->mmap_sem);        mmput(mm);    }    return 0;free_range:         for (page_addr = end - PAGE_SIZE; page_addr >= start;         page_addr -= PAGE_SIZE) {        page = &proc->pages[(page_addr - proc->buffer) / PAGE_SIZE];        if (vma)            zap_page_range(vma, (uintptr_t)page_addr +                proc->user_buffer_offset, PAGE_SIZE);err_vm_insert_page_failed:        unmap_kernel_range((unsigned long)page_addr, PAGE_SIZE);err_map_kernel_failed:        __free_page(*page);        *page = NULL;err_alloc_page_failed:        ;    }err_no_vma:    if (mm) {        up_write(&mm->mmap_sem);        mmput(mm);    }    return -ENOMEM;}

主要工作可用下面的图来表达:

5713484-3893b4d3f7e47b8a.png
  • binder_update_page_range 主要完成工作:分配物理内存空间,将物理空间映射到内核空间,将物理空间映射到进程空间。当然
  • binder_update_page_range 既可以分配物理页面,也可以释放物理页面

1.3.5 binder_ioctl()函数解析

在分析binder_ioctl()函数之前,建议大家看下我的上篇文章Android跨进程通信IPC之10——Binder相关结构体简介
了解相关的结构体,为了便于查找,这些结构体之间都留有字段的存储关联的结构,下面的这幅图描述了这里说到的内容

5713484-1e999ff8f503c4e7.png

  • binder_ioctl()函数负责在两个进程间收发IPC数据和IPC reply数据
  • ioctl(文件描述符,ioctl命令,数据类型)
  • ioctl文件描述符,是通过open()方法打开Binder Driver后返回值。
  • ioctl命令和数据类型是一体,不同的命令对应不同的数据类型

下面这些命令中BINDER_WRITE_READ使用最为频繁,也是ioctl的最为核心的命令。


ioctl.png
//kernel/drivers/android/binder.c       3239行static long binder_ioctl(struct file *filp, unsigned int cmd, unsigned long arg){    int ret;    struct binder_proc *proc = filp->private_data;        //binder线程    struct binder_thread *thread;    unsigned int size = _IOC_SIZE(cmd);    void __user *ubuf = (void __user *)arg;    /*pr_info("binder_ioctl: %d:%d %x %lx\n",            proc->pid, current->pid, cmd, arg);*/    if (unlikely(current->mm != proc->vma_vm_mm)) {        pr_err("current mm mismatch proc mm\n");        return -EINVAL;    }    trace_binder_ioctl(cmd, arg);        //进入休眠状态,直到中断唤醒    ret = wait_event_interruptible(binder_user_error_wait, binder_stop_on_user_error < 2);    if (ret)        goto err_unlocked;    binder_lock(__func__);        //获取binder_thread    thread = binder_get_thread(proc);    if (thread == NULL) {        ret = -ENOMEM;        goto err;    }    switch (cmd) {        //进行binder的读写操作    case BINDER_WRITE_READ:        ret = binder_ioctl_write_read(filp, cmd, arg, thread);        if (ret)            goto err;        break;        //设置binder的最大支持线程数    case BINDER_SET_MAX_THREADS:        if (copy_from_user(&proc->max_threads, ubuf, sizeof(proc->max_threads))) {            ret = -EINVAL;            goto err;        }        break;        //设置binder的context管理者,也就是servicemanager称为守护进程    case BINDER_SET_CONTEXT_MGR:        ret = binder_ioctl_set_ctx_mgr(filp);        if (ret)            goto err;        break;        //当binder线程退出,释放binder线程    case BINDER_THREAD_EXIT:        binder_debug(BINDER_DEBUG_THREADS, "%d:%d exit\n",                 proc->pid, thread->pid);        binder_free_thread(proc, thread);        thread = NULL;        break;         //获取binder的版本号    case BINDER_VERSION: {        struct binder_version __user *ver = ubuf;        if (size != sizeof(struct binder_version)) {            ret = -EINVAL;            goto err;        }        if (put_user(BINDER_CURRENT_PROTOCOL_VERSION,                 &ver->protocol_version)) {            ret = -EINVAL;            goto err;        }        break;    }    default:        ret = -EINVAL;        goto err;    }    ret = 0;err:    if (thread)        thread->looper &= ~BINDER_LOOPER_STATE_NEED_RETURN;    binder_unlock(__func__);    wait_event_interruptible(binder_user_error_wait, binder_stop_on_user_error < 2);    if (ret && ret != -ERESTARTSYS)        pr_info("%d:%d ioctl %x %lx returned %d\n", proc->pid, current->pid, cmd, arg, ret);err_unlocked:    trace_binder_ioctl_done(ret);    return ret;}

1.3.5.1 binder_get_thread()函数解析

从binder_proc中查找binder_thread,如果当前线程已经加入到proc的线程队列则直接返回,如果不存在则创建binder_thread,并将当前线程添加到当前的proc

//kernel/drivers/android/binder.c      3026行static struct binder_thread *binder_get_thread(struct binder_proc *proc){    struct binder_thread *thread = NULL;    struct rb_node *parent = NULL;    struct rb_node **p = &proc->threads.rb_node;        //根据当前进程的pid,从binder_proc中查找相应的binder_thread    while (*p) {        parent = *p;        thread = rb_entry(parent, struct binder_thread, rb_node);        if (current->pid < thread->pid)            p = &(*p)->rb_left;        else if (current->pid > thread->pid)            p = &(*p)->rb_right;        else            break;    }    if (*p == NULL) {                // 新建binder_thread结构体        thread = kzalloc(sizeof(*thread), GFP_KERNEL);        if (thread == NULL)            return NULL;        binder_stats_created(BINDER_STAT_THREAD);        thread->proc = proc;                //保持当前进程(线程)的pid        thread->pid = current->pid;        init_waitqueue_head(&thread->wait);        INIT_LIST_HEAD(&thread->todo);        rb_link_node(&thread->rb_node, parent, p);        rb_insert_color(&thread->rb_node, &proc->threads);        thread->looper |= BINDER_LOOPER_STATE_NEED_RETURN;        thread->return_error = BR_OK;        thread->return_error2 = BR_OK;    }      return thread;}

1.3.5.2 binder_ioctl_write_read()函数解析

对于ioctl()方法中,传递进来的命令是cmd = BINDER_WRITE_READ时执行该方法,arg是一个binder_write_read结构体

/kernel/drivers/android/binder.c      3134行static int binder_ioctl_write_read(struct file *filp,                unsigned int cmd, unsigned long arg,                struct binder_thread *thread){    int ret = 0;    struct binder_proc *proc = filp->private_data;    unsigned int size = _IOC_SIZE(cmd);    void __user *ubuf = (void __user *)arg;    struct binder_write_read bwr;    if (size != sizeof(struct binder_write_read)) {        ret = -EINVAL;        goto out;    }         //把用户控件数据ubuf拷贝到bwr    if (copy_from_user(&bwr, ubuf, sizeof(bwr))) {        ret = -EFAULT;        goto out;    }    binder_debug(BINDER_DEBUG_READ_WRITE,             "%d:%d write %lld at %016llx, read %lld at %016llx\n",             proc->pid, thread->pid,             (u64)bwr.write_size, (u64)bwr.write_buffer,             (u64)bwr.read_size, (u64)bwr.read_buffer);        //如果写缓存中有数据    if (bwr.write_size > 0) {                //执行binder写操作        ret = binder_thread_write(proc, thread,                      bwr.write_buffer,                      bwr.write_size,                      &bwr.write_consumed);        trace_binder_write_done(ret);                //如果写失败,再将bwr数据写回用户空间,并返回        if (ret < 0) {            bwr.read_consumed = 0;            if (copy_to_user(ubuf, &bwr, sizeof(bwr)))                ret = -EFAULT;            goto out;        }    }        //当读缓存有数据    if (bwr.read_size > 0) {                 //执行binder读操作        ret = binder_thread_read(proc, thread, bwr.read_buffer,                     bwr.read_size,                     &bwr.read_consumed,                     filp->f_flags & O_NONBLOCK);        trace_binder_read_done(ret);        if (!list_empty(&proc->todo))            wake_up_interruptible(&proc->wait);                 //如果读失败        if (ret < 0) {                        //当读失败,再将bwr数据写回用户空间,并返回            if (copy_to_user(ubuf, &bwr, sizeof(bwr)))                ret = -EFAULT;            goto out;        }    }    binder_debug(BINDER_DEBUG_READ_WRITE,             "%d:%d wrote %lld of %lld, read return %lld of %lld\n",             proc->pid, thread->pid,             (u64)bwr.write_consumed, (u64)bwr.write_size,             (u64)bwr.read_consumed, (u64)bwr.read_size);         //将内核数据bwr拷贝到用户空间ubuf    if (copy_to_user(ubuf, &bwr, sizeof(bwr))) {        ret = -EFAULT;        goto out;    }out:    return ret;}
5713484-a3647098d270c5e0.png
  • 1 首先把用户空间的数据拷贝到内核空间bwr
  • 2 其次当bwr写缓存中有数据,则执行binder写操作。如果写失败,则再将bwr数据写回用户控件,并退出。
  • 3 再次当bwr读缓存中有数据,则执行binder读缓存;当读失败,再将bwr数据写会用户空间,并退出。
  • 4 最后把内核数据拷贝到用户空间。

1.3.6 总结

本章主要讲解了binder驱动的的四大功能点

  • 1 binder_init :初始化字符设备
  • 2 binder_open:打开驱动设备,过程需要持有binder_main_lock同步锁
  • 3 binder_mmap: 申请内存空间,该过程需要持有binder_mmap_lock同步锁;
  • 4 binder_ioctl:执行相应的io操作,该过程需要持有binder_main_lock同步锁;当处于binder_thread_read过程,却读缓存无数据则会先释放该同步锁,并处于wait_event_freezable过程,等有数据到来则唤醒并尝试持有同步锁。

2 Binder驱动通信

2.1 Binder驱动通信简述

  • Client进程通过RPC(Remote Procedure Call Protocol) 与Server通信,可以简单地划分为三层: 1、驱动层 2、IPC层 3、业务层。下图的doAction()便是Client与Server共同协商好的统一方法;其中handle、RPC数据、代码、协议、这4项组成了IPC层的数据,通过IPC层进行数据传输;而真正在Client和Server两端建立通信的基础是Binder Driver

模型如下图:


5713484-dae563576b4d9821.png

2.2 Binder驱动通信协议

5713484-68a3eda6f5f95cc3.png

Binder协议包含在IPC数据中,分为两类:

  • BINDER_COMMAND_PROTOCOL:binder请求码,以"BC_" 开头,简称"BC码",从IPC层传递到 Binder Driver层;
  • BINDER_RETURN_PROTOCOL: binder响应码,以"BR_"开头,简称"BR码",用于从BinderDirver层传递到IPC层

Binder IPC 通信至少是两个进程的交互:

  • client进程执行binder_thread_write,根据BC_XXX 命令,生成相应的binder_work;
  • server进程执行binder_thread_read,根据binder_work.type类型,生成BR_XXX,发送用户处理。

如下图:


5713484-9b6b2b8f120c1b60.png

其中binder_work.type共有6种类型

//kernel/drivers/android/binder.c      240行struct binder_work {    struct list_head entry;    enum {        BINDER_WORK_TRANSACTION = 1        BINDER_WORK_TRANSACTION_COMPLETE,        BINDER_WORK_NODE,        BINDER_WORK_DEAD_BINDER,        BINDER_WORK_DEAD_BINDER_AND_CLEAR,        BINDER_WORK_CLEAR_DEATH_NOTIFICATION,    } type;};

这里用到了上面提到两个函数一个是binder_thread_write()函数,另一个是binder_thread_read函数骂我们就来详细研究下:

2.3 通信函数

2.3.1 binder_thread_write() 函数详解

  • 请求处理过程是通过binder_thread_write()函数,该方法用于处理Binder协议的请求码。当binder_buffer存在数据,binder线程的写操作循环执行
    代码在 kernel/drivers/android/binder.c 2248行。
  • 代码太多了就不全部粘贴了,只粘贴重点部分,代码如下:
static int binder_thread_write(){    while (ptr < end && thread->return_error == BR_OK) {        //获取IPC数据中的Binder协议的BC码        if (get_user(cmd, (uint32_t __user *)ptr))            return -EFAULT;        switch (cmd) {            case BC_INCREFS: ...            case BC_ACQUIRE: ...            case BC_RELEASE: ...            case BC_DECREFS: ...            case BC_INCREFS_DONE: ...            case BC_ACQUIRE_DONE: ...            case BC_FREE_BUFFER: ...                        case BC_TRANSACTION:            case BC_REPLY: {                struct binder_transaction_data tr;                //拷贝用户控件tr到内核                copy_from_user(&tr, ptr, sizeof(tr));                binder_transaction(proc, thread, &tr, cmd == BC_REPLY);                break;            case BC_REGISTER_LOOPER: ...            case BC_ENTER_LOOPER: ...            case BC_EXIT_LOOPER: ...            case BC_REQUEST_DEATH_NOTIFICATION: ...            case BC_CLEAR_DEATH_NOTIFICATION:  ...            case BC_DEAD_BINDER_DONE: ...            }        }    }}

对于 请求码为BC_TRANSCATION或BC_REPLY时,会执行binder_transaction()方法,这是最频繁的操作。那我们一起来看下binder_transaction()函数。

2.3.1.1 binder_transaction() 函数详解

这块的代码依旧很多,我就只粘贴重点了,全部代码在/kernel/drivers/android/binder.c 1827行。

static void binder_transaction(struct binder_proc *proc,               struct binder_thread *thread,               struct binder_transaction_data *tr, int reply){    //根绝各种判断,获取如下信息:    //目标进程    struct binder_proc *target_proc;      // 目标线程    struct binder_thread *target_thread;      // 目标binder节点    struct binder_node *target_node;        //目标TODO队列    struct list_head *target_list;    // 目标等待队列         wait_queue_head_t *target_wait;       //*** 省略部分代码 ***       //分配两个结构体内存    t = kzalloc(sizeof(*t), GFP_KERNEL);    if (t == NULL) {        return_error = BR_FAILED_REPLY;        goto err_alloc_t_failed;    }    binder_stats_created(BINDER_STAT_TRANSACTION);    *tcomplete = kzalloc(sizeof(*tcomplete), GFP_KERNEL);    //*** 省略部分代码 ***    //从target_proc分配一块buffer    t->buffer = binder_alloc_buf(target_proc, tr->data_size,     //*** 省略部分代码 ***    for (; offp < off_end; offp++) {     //*** 省略部分代码 ***        switch (fp->type) {        case BINDER_TYPE_BINDER: ...        case BINDER_TYPE_WEAK_BINDER: ...        case BINDER_TYPE_HANDLE: ...        case BINDER_TYPE_WEAK_HANDLE: ...        case BINDER_TYPE_FD: ...        }    }    //分别给target_list和当前线程TODO队列插入事务    t->work.type = BINDER_WORK_TRANSACTION;    list_add_tail(&t->work.entry, target_list);    tcomplete->type = BINDER_WORK_TRANSACTION_COMPLETE;    list_add_tail(&tcomplete->entry, &thread->todo);    if (target_wait)        wake_up_interruptible(target_wait);    return;}

replay的过程会找到 target_thread,非reply则一般找到target_proc,对于特殊的嵌套binder call会根据transaction_stack来决定是否插入事物到目标进程。

2.3.1.2 BC_PROTOCOL 请求码

Binder请求码,在binder.h里面的366行。是用binder_driver_command_protocol 来定义的,是用于应用程序向Binder驱动设备发送请求消息,应用程序包含Client端和Server端,以BC_开头,供给17条 ( - 表示目前不支持请求码)

5713484-ff408b3f4cf683ee.png

重点说几个:

  • BC_FREE_BUFFER:通过mmap()映射内存,其中ServiceMananger映射的空间大小为128K其他Binder应用的进程映射的内存大小为8K-1M,Binder驱动基于这块映射的内存采用最佳匹配算法来动态分配和释放,通过binder_buffer结构体中free字段来表示相应的buffer是空闲还是已分配状态。对于已分配的buffer加入binder_proc中的allocated_buffers红黑树;对于空闲的buffers加入到binder_proch中的free_buffer红黑树。当应用程序需要内存时,根据所需内存大小从free_buffers中找到最合适的内存,并放入allocated_buffers树;当应用程序处理完后,必须尽快用BC_FREE_BUFFER命令来释放该buffer,从而添加回free_buffers树中。
  • BC_INCREFS、BC_ACQUIRE、BC_RELEASE、BC_DECREFS等请求码的作用是对binder的 强弱引用的技术操作,用于实现强/弱 指针的功能。
  • 对于参数类型 binder_ptr_cookie是由binder指针和cookie组成
  • 对于参数类型 binder_ptr_cookie是由binder指针和cookie组成
  • BC_ENTER_LOOPER: binder主线程(由应用层发起)的创建会向驱动发送该消息;joinThreadPool()过程创建binder主线程;
  • BC_REGISTER_LOOPER: Binder用于驱动层决策而创建新的binder线程;joinThreadPool()过程,创建非binder主线程;
  • BC_EXIT_LOOPER:退出Binder线程,对于binder主线程是不能退出的;joinThreadPool的过程出现timeout,并且非binder主线程,则会退出该binder线程。

2.3.1.3 binder_thread_read()函数详解

响应处理过程是通过binder_thread_read()函数,该方法根据不同的binder_work->type以及不同的状态生成不同的响应码
代码太多了,我只截取了部分代码,具体代码在/kernel/drivers/android/binder.c 2650行

static int binder_thread_read(){     //*** 省略部分代码 ***    // 根据 wait_for_proc_work 来决定wait在当前线程还是进程的等待队列    if (wait_for_proc_work) {         //*** 省略部分代码 ***        ret = wait_event_freezable_exclusive(proc->wait, binder_has_proc_work(proc, thread));          //*** 省略部分代码 ***    } else {        //*** 省略部分代码 ***        ret = wait_event_freezable(thread->wait, binder_has_thread_work(thread));       //*** 省略部分代码 ***    }   //*** 省略部分代码 ***    while (1) {         //*** 省略部分代码 ***         //当&thread->todo和&proc->todo都为空时,goto到retry标志处,否则往下执行:    if (!list_empty(&thread->todo)) {        w = list_first_entry(&thread->todo, struct binder_work,entry);    } else if (!list_empty(&proc->todo) && wait_for_proc_work) {        w = list_first_entry(&proc->todo, struct binder_work,entry);    } else {        /* no data added */        if (ptr - buffer == 4 &&             !(thread->looper & BINDER_LOOPER_STATE_NEED_RETURN))                goto retry;        break;    }        struct binder_transaction_data tr;        struct binder_transaction *t = NULL;        switch (w->type) {          case BINDER_WORK_TRANSACTION: ...          case BINDER_WORK_TRANSACTION_COMPLETE: ...          case BINDER_WORK_NODE: ...          case BINDER_WORK_DEAD_BINDER: ...          case BINDER_WORK_DEAD_BINDER_AND_CLEAR: ...          case BINDER_WORK_CLEAR_DEATH_NOTIFICATION: ...        }        ...    }done:    *consumed = ptr - buffer;        //当满足请求线程  加上 已准备线程数 等于0  并且 其已启动线程小于最大线程数15,并且looper状态为已注册或已进入时创建新的线程。    if (proc->requested_threads + proc->ready_threads == 0 &&        proc->requested_threads_started < proc->max_threads &&        (thread->looper & (BINDER_LOOPER_STATE_REGISTERED |         BINDER_LOOPER_STATE_ENTERED)) /* the user-space code fails to */         /*spawn a new thread if we leave this out */) {        proc->requested_threads++;        binder_debug(BINDER_DEBUG_THREADS,                 "%d:%d BR_SPAWN_LOOPER\n",                 proc->pid, thread->pid);                //生成BR_SPAWN_LOOPER命令,用于创建新的线程        if (put_user(BR_SPAWN_LOOPER, (uint32_t __user *)buffer))            return -EFAULT;        binder_stat_br(proc, thread, BR_SPAWN_LOOPER);    }    return 0;}
  • 当transaction堆栈为空,且线程todo链表为空,且non_block=false时,意味着没有任务事物需要处理,会进入等待客户端请求的状态。当有事物需要处理时便会进入循环处理过程,并生成相应的响应码。

  • 在Binder驱动层面,只有进入binder_thread_read()方法时,同时满足以下条件,才会生成BR_SPAWN_LOOPER命令,当用户态进程收到该命令则会创新新线程:

  • binder_proc的requested_threads线程数为0
  • binder_proc的ready_threads线程数为0
  • binder_proc的requested_threads_started个数小于15(即最大线程个数)
  • binder_thread的looper状态为BINDER_LOOPER_STATE_REGISTERED或者BINDER_LOOPER_STATE_ENTERED
  • 那么问题来了,什么时候处理的响应码?通过上面的Binder通信协议图,可以知道处理响应码的过程是在用户态处理,下一篇文章会讲解到用户控件IPCThreadState类中IPCThreadState::waitForResponse()和IPCThreadState::executeCommand()两个方法共同处理Binder协议的18个响应码

2.3.1.4 BR_PROTOCOL 响应码

  • binder响应码,在binder.h里面的278行是用enum binder_driver_return_protocol来定义的,是binder设备向应用程序回复的消息,应用程序包含Client端和Serve端,以BR_开头,合计18条。
    5713484-18c1965c5e32c306.png

几个难点:

  • BR_SPAWN_LOOPER:binder驱动已经检测到进程中没有线程等待即将到来的事物。那么当一个进程接收到这条命令时,该进程必须创建一条新的服务线程并注册该线程,在接下来的响应过程会看到合何时生成该响应嘛
  • BR_TRANSACTION_COMPLETE:当Client端向Binder驱动发送BC_TRANSACTION命令后,Client会受到BR_TRANSACTION_COMPLETE命令,告知Client端请求命令发送成功能;对于Server向Binder驱动发送BC_REPLY命令后,Server端会受到BR_TRANSACTION_COMPLETE命令,告知Server端请求回应命令发送成功。
  • BR_READ_REPLY: 当应用层向Binder驱动发送Binder调用时,若Binder应用层的另一个端已经死亡,则驱动回应BR_DEAD_BINDER命令。
  • BR_FAILED_REPLY:当应用层向Binder驱动发送Binder调用是,若transcation出错,比如调用的函数号不存在,则驱动回应BR_FAILED_REPLY。

3 Binder内存

Binder内存我主要分3个内容来依次讲解,分别为:

    1. mmap机制
    1. 内存分配
    1. 内存释放

3.1 mmap机制

上面从代码的角度阐释了binder_mmap,也是Binder进程通信效率高的核心机制所在,如下图:


5713484-e431534704111fb7.png
5713484-b3b1f8e3bdb5f402.png
  • 虚拟进程地址空间(vm_area_struct)和虚拟内核地址空间(vm_struct)都映射到同一块物理内存空间。
  • 当Client端与Server端发送数据时,Client(作为数据发送端)先从自己的进程空间把IPC通信数据copy_from_user拷贝到内核空间,而Server端(作为数据接收端)与内核共享数据,不再需要拷贝数据,而是通过内存地址空间的偏移量,即可获悉内存地址,整个过程只要发生一次内存拷贝。
  • 对于进程和内核虚拟地址映射到同一个物理内存的操作是发生在数据接收端,而数据发送端还是需要将用户态的数据复制到内核态
5713484-cb5801de7e750c73.png

3.2 内存分配

  • Binder内存分配方法通过binder_alloc_buf()方法,内存管理单元为binder_buffer结构体,只有在binder_transaction过程中才需要分配buffer。
  • 具体代码在/kernel/drivers/android/binder.c 678行,我选取了重点部分
static struct binder_buffer *binder_alloc_buf(struct binder_proc *proc,                          size_t data_size, size_t offsets_size, int is_async){      //*** 省略部分代码 ***      //如果不存在虚拟地址空间为,则直接返回    if (proc->vma == NULL) {        pr_err("%d: binder_alloc_buf, no vma\n",               proc->pid);        return NULL;    }            data_offsets_size = ALIGN(data_size, sizeof(void *)) +        ALIGN(offsets_size, sizeof(void *));        //非法的size,直接返回    if (data_offsets_size < data_size || data_offsets_size < offsets_size) {        binder_user_error("%d: got transaction with invalid size %zd-%zd\n",                proc->pid, data_size, offsets_size);        return NULL;    }    size = data_offsets_size + ALIGN(extra_buffers_size, sizeof(void *));        //非法的size,直接返回    if (size < data_offsets_size || size < extra_buffers_size) {        binder_user_error("%d: got transaction with invalid extra_buffers_size %zd\n",                  proc->pid, extra_buffers_size);        return NULL;    }        //如果 剩余的异步空间太少,以至于满足需求,也直接返回    if (is_async &&        proc->free_async_space < size + sizeof(struct binder_buffer)) {        binder_debug(BINDER_DEBUG_BUFFER_ALLOC,                 "%d: binder_alloc_buf size %zd failed, no async space left\n",                  proc->pid, size);        return NULL;    }    //从binder_buffer的红黑树丛中,查找大小相等的buffer块        while (n) {        buffer = rb_entry(n, struct binder_buffer, rb_node);        BUG_ON(!buffer->free);        buffer_size = binder_buffer_size(proc, buffer);        if (size < buffer_size) {            best_fit = n;            n = n->rb_left;        } else if (size > buffer_size)            n = n->rb_right;        else {            best_fit = n;            break;        }    }   //如果内存分配失败,地址为空    if (best_fit == NULL) {        pr_err("%d: binder_alloc_buf size %zd failed, no address space\n",            proc->pid, size);        return NULL;    }    if (n == NULL) {        buffer = rb_entry(best_fit, struct binder_buffer, rb_node);        buffer_size = binder_buffer_size(proc, buffer);    }    binder_debug(BINDER_DEBUG_BUFFER_ALLOC,             "%d: binder_alloc_buf size %zd got buffer %p size %zd\n",              proc->pid, size, buffer, buffer_size);    has_page_addr =        (void *)(((uintptr_t)buffer->data + buffer_size) & PAGE_MASK);    if (n == NULL) {        if (size + sizeof(struct binder_buffer) + 4 >= buffer_size)            buffer_size = size; /* no room for other buffers */        else            buffer_size = size + sizeof(struct binder_buffer);    }    end_page_addr =        (void *)PAGE_ALIGN((uintptr_t)buffer->data + buffer_size);    if (end_page_addr > has_page_addr)        end_page_addr = has_page_addr;    if (binder_update_page_range(proc, 1,        (void *)PAGE_ALIGN((uintptr_t)buffer->data), end_page_addr, NULL))        return NULL;    rb_erase(best_fit, &proc->free_buffers);    buffer->free = 0;    binder_insert_allocated_buffer(proc, buffer);    if (buffer_size != size) {        struct binder_buffer *new_buffer = (void *)buffer->data + size;        list_add(&new_buffer->entry, &buffer->entry);        new_buffer->free = 1;        binder_insert_free_buffer(proc, new_buffer);    } binder_debug(BINDER_DEBUG_BUFFER_ALLOC,             "%d: binder_alloc_buf size %zd got %p\n",              proc->pid, size, buffer);    buffer->data_size = data_size;    buffer->offsets_size = offsets_size;    buffer->extra_buffers_size = extra_buffers_size;    buffer->async_transaction = is_async;    if (is_async) {        proc->free_async_space -= size + sizeof(struct binder_buffer);        binder_debug(BINDER_DEBUG_BUFFER_ALLOC_ASYNC,                 "%d: binder_alloc_buf size %zd async free %zd\n",                  proc->pid, size, proc->free_async_space);    }    return buffer;}

3.3 内存释放

内存释放相关函数:

  • binder_free_buf() : 在/kernel/drivers/android/binder.c 847行
  • binder_delete_free_buffer():在/kernel/drivers/android/binder.c 802行
  • binder_transaction_buffer_release():在/kernel/drivers/android/binder.c 1430行

4 附录:关于misc

4.1 linux子系统-miscdevice

  • 杂项设备(miscdevice) 是嵌入式系统中用的比较多的一种设备类型。

  • 在Linux驱动中把无法归类的五花八门的设备定义为混杂设备(用miscdevice结构体表述)

  • Linux内核所提供的miscdevice有很强的包容性,各种无法归结为标准字符设备的类型都可以定义为miscdevice,譬如NVRAM,看门狗,实时时钟,字符LCD等,就像一组大杂烩。

  • 在Linux内核里把所有misc设备组织在一起,构成一个子系统(subsys),统一进行管理。在这个子系统里的所有miscdevice类型的设备共享一个主设备号MISC_MAJOR(即10),这次设备号不同。

  • 在内核中用struct miscdevice表示miscdevice设备,具体的定义在内核头文件"include/linux/miscdevice.h"中

//https://github.com/torvalds/linux/blob/master/include/linux/miscdevice.h    63行struct miscdevice  {    int minor;    const char *name;    const struct file_operations *fops;    struct list_head list;    struct device *parent;    struct device *this_device;    const char *nodename;    mode_t mode;};
  • miscdevice的API的实现在drivers/char/misc.c中,misc子系统的初始化,以及misc设备的注册,注销等接口都实现在这个文件中。
  • 通过阅读这个文件,miscdevice类型的设备实际上就是对字符设备的简单封装,最直接的证据可以看misdevice子系统的初始化函数misc_init(),在这个函数里,撇开其他代码不看,其中有如下两行关键代码:
//drivers/char/misc.c  279行...misc_class = class_create(THIS_MODULE, "misc");...if (register_chrdev(MISC_MAJOR,"misc",&misc_fops))

代码解析:

  • 第一行,创建了一个名字叫misc的类,具体表现是在/sys/class目录下创建一个名为misc的目录。以后美注册一个自己的miscdevice都会在该目录下新建一项。
  • 第二行,调用register_chrdev为给定的主设备号MISC_MAJOR(10)注册0~255供256个次设备号,并为每个设备建立一个对应的默认的cdev结构,该函数是2.6内核之前的老函数,现在已经不建议使用了。由此可见misc设备其实也就是主设备号是MISC_MAJOR(10)的字符设备。从面相对象的角度来看,字符设备类是misc设备类的父类。同时我们也主要注意到采用这个函数注册后实际上系统最多支持有255个驱动自定义的杂项设备,因为杂项设备子系统模块自己占用了一个次设备号0。查看源文件可知,目前内核里已经被预先使用的子设备号定义在include/linux/miscdevice.h的开头
  • 在这个子系统中所有的miscdevice设备形成了一个链表,他们共享一个主设备号10,但它们有不同的次设备号。对设备访问时内核根据次设备号查找对应的miscdevice设备。这一点我们可以通过阅读misc子系统提供的注册接口函数misc_register()和注销接口函数misc_deregister()来理解。
  • 在这个子系统中所有的miscdevice设备不仅共享了主设备号,还共享了一个misc_open()的文件操作方法。
//drivers/char/misc.c       161行static const struct file_operations misc_fops = {    .owner        = THIS_MODULE,    .open        = misc_open,};
  • 该方法结构在注册设备时通过register_chrdev(MISC_MAJOR,"misc",&misc_fops)传递给内核。但不要以为所有的miscdevice都使用相同的文件open方法,仔细阅读misc_open()我们发现该函数内部会检查驱动模块自已定义的miscdevice结构体对象的fops成员,如果不为空将其替换掉缺省的,参考函数中的new_fops = fops_get(c->fops);以及file->f_op = new_fops;语句。如此这般,以后内核再调用设备文件的操作时就会调用该misc设备自己定义的文件操作函数了。这种实现方式有点类似java里面函数重载的概念。

4.2 采用miscdevice开发设备驱动的方法

  • 第一步,定义自己misc设备的文件操作函数以及file_operations结构体对象。如下:
static const struct file_operations my_misc_drv_fops = {    .owner    = THIS_MODULE,    .open    = my_misc_drv_open,    .release = my_misc_drv_release,    //根据实际情况扩充 ...};
  • 第二步,定义自己的misc设备对象,如下:
static struct miscdevice my_misc_drv_dev = {    .minor    = MISC_DYNAMIC_MINOR,    .name    = KBUILD_MODNAME,    .fops    = &my_misc_drv_fops,};
  • .minor 如果填充MISC_DYNAMIC_MINOR,则由内核动态分配次设备号,否则根据你自己定义的指定。
  • .name 给定设备的名字,也可以直接利用内核编译系统的环境变量KBUILD_MODNAME。
  • .fops设置为第一步定义的文件操作结构体的地址
  • 第三步,注销和销毁
    驱动模块一般在模块初始化函数中调用misc_register()注册自己的misc设备。实例代码如下:
ret = misc_register(&my_misc_drv_dev);if (ret < 0) {    //失败处理}

注意在misc_register()函数中有如下语句

misc->this_device = device_create(misc_class, misc->parent, dev,                  misc, "%s", misc->name);

这句话配合前面在misc_init()函数中的misc_class = class_create(THIS_MODULE, "misc");

这样,class_create()创建了一个类,而device_create()就创建了该类的一个设备,这些都涉及linux内核的设备模型和sys文件系统额概念,暂不展开,我们只需要知道,如此这般,当该驱动模块被加载(insmod)时,和内核态的设备模型配套运行的用户态有个udev的后台服务会自动在/dev下创建一个驱动模块中注册的misc设备对应的设备文件节点,其名字就是misc->name。这样就省去了自己创建设备文件的麻烦。这样也有助于动态设备的管理。

驱动模块可以在模块卸载函数中调用misc_deregister()注销自己的misc设备。实例代码如下:

misc_deregister(&my_misc_drv_dev);

在这个函数中会自动删除`/dev下的同名设备文件。

4.3 总结

杂项设备作为字符设备的封装,为字符设备提供简单的编程接口,如果编写新的字符驱动,可以考虑使用杂项设备接口,简单粗暴,只需要初始化一个miscdevice的结构体设备对象,然后调用misc_register注册就可以了,不用的时候,调用misc_deregister进行卸载。

参考

Android跨进程通信IPC之8——Binder驱动

更多相关文章

  1. C语言函数的递归(上)
  2. Android(安卓)APN开发流程分析
  3. adb的一些命令
  4. 浅谈Android的TabHost
  5. [转]如何在移动设备程序中嵌入盈利广告,开始挣钱了 android iphon
  6. LinuxMint下让adb工具识别android设备
  7. Android(安卓)Power Management
  8. Android中Intent的setData,setType和setDataAndType的用法
  9. android看不见main函数怎么办?程序异常了,能够不提示“xxx软件停止

随机推荐

  1. Android(安卓)Fragment 简洁版 list
  2. android每隔5s显示时间
  3. android ScrollView 多张图片之间有空白
  4. android监听edittext输入事件
  5. android studio 复制项目
  6. android 使用百度地图画轨迹
  7. Android中getChildAt()方法介绍
  8. Android(安卓)联系人的增删修改
  9. Android:下载网络图片
  10. Android获得屏幕分辨率的两种方法