如何应对Android面试官->玩转Binder机制,手写MMKV核心实现

从零手写实现简易版MMKV(一) 文章目录@[toc]概述预期目标技术设计编码实现SDK初始化EZKV实例初始化数据写入写入int类型value写入String类型value数据重整和文件扩容数据读取读取int类型value读取string类型value总结 概述 MMKV是支持多平台的高性能键值对持久化存储组件,其核心原理是利用mmap内存映射文件,关于它的详细介绍和更多原理参看MMKV开源git地址。 从零开始手写(其实是抄写-.-!)简易版MMKV(另起名叫EZKV),即先只关注最核心的功能(实现最小化系统),再从主干开枝散叶,逐步进 阅读详情

前言


image.png

本章主要基于面试中的常见问题,进行 Binder 机制的讲解;

Binder 是什么?


image.png

在 Android 中我们所使用的 Activity,Service 等组件都需要和 AMS 通信,这种跨进程的通信都是通过 Binder 完成的;

  1. 机制角度,Binder 是一种进程间通信机制;
  2. 驱动角度,站在内核层角度,Binder 是一个虚拟物理设备驱动;
  3. 应用层角度,Binder.java 是一个能发起通信的 Java 类。根据声明的 AIDL 文件,它会帮助我们生成一些 Binder 的子类,这些子类会帮助我们完成进程间通信;
  4. Framework、Native 角度,Binder 连接了 Client、Server、ServiceManager 和 Binder 驱动程序,形成一套 C/S 通信架构;

什么情况下使用多进程?多进程的优点有哪些

Binder 在跨进程通信中使用,那么什么情况下应该使用多进程呢?以及使用多进程有哪些优势呢?

  1. 突破进程内存限制,如图库占用内存太多(大图浏览);
  2. 功能稳定性,独立的通信进程保持长连接的稳定性(例如微信的通信,push);
  3. 规避系统内存泄漏,独立的 webview、播放器,进程阻隔内存泄露导致的问题;
  4. 隔离风险:对于不稳定的功能放入独立进程,避免导致主进程崩溃;

Binder 有什么优势?Android 为什么要用 Binder 来执行进程间通信


Linux 进程间都有哪些通信方式?

再说 Binder 优势的时候,我们先来看下 Linux 进程间都有哪些通信方式?

  1. 管道 Pipe;
  2. Socket,它是基于C/S架构做为一款通用接口,传输效率低,开销大;Socket 进程通信的时候数据需要拷贝两次;Socket 通信依赖上层协议访问接入点是开放的,是不安全的;
  3. 信号量;
  4. 共享内存:数据无需拷贝;但是实现与控制较为复杂,需要自行处理并发同步等问题;依赖上层协议(不靠谱),访问接入点开放不安全(依赖上层协议 说的是身份识别的时候,当前 app 调用服务的时候,需要传递身份标识进行身份识别,当前 app 将自己的 pid 告诉服务,那么就可以随意的产出一个 pid 告诉服务,不靠谱)访问接入点开放,就类似公共汽车,谁都可以上车一样,不安全;
  5. Binder;

而 管道、Socket、信号量,它们的通信方式原理是一样的:都是内核空间通过系统调用将用户空间的数据复制一份到内核空间(copy from user)。再通过系统调用由内核空间复制给另一个进程的用户空间(copy to user),这种传统方式的通信进行了两次拷贝;

什么是系统调用?


copy_from_user 和 copy_to_user 就是系统调用;

Binder 有哪些优势?

Android 没有采用传统的进程间通信方式,而是采用了 Binder,那么 Binder 通信有哪些优势呢?

  1. 数据拷贝一次;
  2. C/S 架构,清晰明朗。Client 与 Server 相对独立,稳定性好;
  3. 除了性能上(拷贝一次)比共享内存(无需拷贝)差一些,其他都优于共享内存;
  4. 安全性有保证,内核添加身份(UID/PID)标识(可靠),支持实名和匿名;实名:系统服务就是实名制进行binder通信,例如我们可以直接通过 getSystemService() 来获取系统服务;匿名:自己启动的服务;UID 是由系统进行分配的一个字符串;

image.png

传统方式的进程间是如何通信的?

说完 Binder 的优势之后,我们来看 Binder 是如何通信的,再说 Binder 是如何通信之前,我们需要看下进程间是如何通信的;

由于进程间的内存是隔离的,不像线程之间是内存共享的,那么我们来看下进程间的内存是如何划分的:整体分为用户空间和内核空间,用户空间内存隔离,内核空间内存共享;

什么是内存空间

用户程序代码运行的地方;

什么是内核空间

内核代码运行的地方;


用户空间和内核空间 都是虚拟内存,但是 所有 app 的内核空间映射的物理内存都是同一个;

每个进程的用户空间都是独立的,也是隔离的,每个进程的用户空间和内核空间也是隔离的,但是所有的内核空间是共享的;

为了安全,它们是隔离的(也就是说用户空间拿不到内核空间的虚拟内存数据,内核空间也拿不到用户空间的虚拟内存数据),即使用户空间崩溃了,内核空间也不受影响;

所以,进程间通信的核心原理就是:A 进程将自己用户空间的数据交给内核空间,让内核空间交给接收方进程的用户空间,从而完成进程间通信,期间发生了两次拷贝,一次是从用户空间拷贝到内核空间(copy_from_user),一次是从内核空间拷贝到用户空间(copy_to_user);

image.png

PS: 不同架构系统,内存空间和内核空间划分也不同;

32位系统:即2的32次幂,总共可访问地址为4G,内核空间为1G,用户空间为3G。

64位系统:低位0-47位才是可变的有效地址(寻址空间256T),高位48-63位全部补0或者补1。一般高位全部补0对应的是用户空间,高位全部补1对应的是内核空间;

image.png

Binder 是如何做到一次拷贝的(如何通信)?

内核空间通过系统调用将A进程用户空间的数据复制一份到内核空间(copy from user);

内核空间的虚拟内存 最终都必须要关联到同一个物理内存「所有app的内核空间都映射同一个物理空间」(利用 MMU,将虚拟内存转换成物理内存);

内核空间将数据放到内核空间的虚拟内存,内核空间的这个虚拟内存映射的物理内存地址和接收方 B 进程的用户空间中虚拟内存映射的物理内存地址是同一个(如何映射:通过 mmap(零拷贝技术) 来完成映射);

内核往里面放,就相当于放到了 B 进程用户空间的虚拟内存中,B 进程可直接访问;

image.png

传统 IO 操作

这里要额外的说一下传统的 IO 操作

传统 IO 上,用户空间是不能直接操作文件的;

所以,传统 IO 执行了两次数据拷贝,虚拟内存拷贝到内核空间虚拟内存,再由内核空间虚拟内存拷贝到物理地址的文件;

用户态到内核态,以及内核态到物理文件的两次 copy 都是耗时的,所以 IO 操作是耗时的;

mmap 原理(Binder 通信原理)

Linux 通过将一个虚拟内存区域与文件描述符关联起来,以初始化这个虚拟内存区域的内容,这个过程称为内存映射(memory mapping);

通过 mmap(系统提供的 Api) 将文件和用户空间的虚拟内存映射区域建立了一个联系,用户空间可直接操作这个文件(无需通过 copy 数据到内核空间,再由内核空间到文件的过程);

对文件进行 mmap 会在进程的虚拟内存分配地址空间创建映射关系,实现这样的映射关系之后,就可以采用指针的方式读写操作这一段内存,而系统会自动回写到对应的文件磁盘上。

mmap 本质就是:能够让虚拟内存和指定的物理内存直接联系起来;

所以 mmap 相对于传统的 IO 的优势有如下几点:

  1. mmap 对文件的读写操作只需要从磁盘到用户主存的一次数据拷贝过程,减少了数据的拷贝次数,提高了文件操作效率;
  2. mmap 使用逻辑内存对磁盘进行映射,操作内存就相当于操作文件,不需要开启线程,操作 mmap 的速度和操作内存的速度一样快;
  3. mmap 提供一段可供随时写入的内存块,App 只管往里面写数据,由操作系统(如内存不足、进程退出等时候)负责将内存回写到文件;

共享内存是如何实现零拷贝的?

讲到这里,共享内存的零拷贝技术也就知道了,发送方 接收方 内核共同映射同一块物理内存就可以实现零拷贝了;

Binder 驱动是如何启动的?


linux 中一切皆文件,所以 Binder 驱动其实也是一个文件,既然是一个文件,那么就可以像 mmap 似的,指定一块内存和这个文件进行一个联系;

Binder 驱动的启动,可以从下面这张图中更详细的理解

image.png

Binder 驱动的启动,主要是通过binder_init()、binder_open()、binder_mmap()、binder_ioctll() 四个方法来启动的;

binder_init()

Binder 启动的是 misc 设备,它是没有具体硬件的,它实际上就是一块内存,Android 为什么要使用 misc 设备,是因为注册简单;

binder_init() 这个方法具体做了什么?我们进入源码看下:

device_initcall(binder_init);

通过 device_initcall 来调用 binder_init 进行初始化启动,我们进入这个 binder_init 方法看下:

static int __init binder_init(void)
{
	int ret;
	char *device_name, *device_names;
	struct binder_device *device;
	struct hlist_node *tmp;
   // 创建一个单线程的工作队列
	binder_deferred_workqueue = create_singlethread_workqueue("binder");
	if (!binder_deferred_workqueue)
		return -ENOMEM;

	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_create_file("state",
				    S_IRUGO,
				    binder_debugfs_dir_entry_root,
				    NULL,
				    &binder_state_fops);
		debugfs_create_file("stats",
				    S_IRUGO,
				    binder_debugfs_dir_entry_root,
				    NULL,
				    &binder_stats_fops);
		debugfs_create_file("transactions",
				    S_IRUGO,
				    binder_debugfs_dir_entry_root,
				    NULL,
				    &binder_transactions_fops);
		debugfs_create_file("transaction_log",
				    S_IRUGO,
				    binder_debugfs_dir_entry_root,
				    &binder_transaction_log,
				    &binder_transaction_log_fops);
		debugfs_create_file("failed_transaction_log",
				    S_IRUGO,
				    binder_debugfs_dir_entry_root,
				    &binder_transaction_log_failed,
				    &binder_transaction_log_fops);
	}

	/*
	 * Copy the module_parameter string, because we don't want to
	 * tokenize it in-place.
	 */
	device_names = kzalloc(strlen(binder_devices_param) + 1, GFP_KERNEL);
	if (!device_names) {
		ret = -ENOMEM;
		goto err_alloc_device_names_failed;
	}
        // 执行了一次复制,将 devices 拷贝到 device_names 中,也就是从配置信息中读取 binderDevice
	strcpy(device_names, binder_devices_param);

	while ((device_name = strsep(&device_names, ","))) {
                // 初始化 binder 设备
		ret = init_binder_device(device_name);
		if (ret)
			goto err_init_binder_device_failed;
	}

	return ret;

err_init_binder_device_failed:
	hlist_for_each_entry_safe(device, tmp, &binder_devices, hlist) {
		misc_deregister(&device->miscdev);
		hlist_del(&device->hlist);
		kfree(device);
	}
err_alloc_device_names_failed:
	debugfs_remove_recursive(binder_debugfs_dir_entry_root);

	destroy_workqueue(binder_deferred_workqueue);

	return ret;
}

image.png

配置信息中的 binder_devices 就是 binder;

通过 init_binder_device 方法来初始化 Binder 设备,我们进入这个方法看下:

static int __init init_binder_device(const char *name)
{
	int ret;
	struct binder_device *binder_device;
   // 为 binder 设备分配虚拟内存
	binder_device = kzalloc(sizeof(*binder_device), GFP_KERNEL);
	if (!binder_device)
		return -ENOMEM;
   // 对 binder 设备进行初始化
   //     
	binder_device->miscdev.fops = &binder_fops;
	binder_device->miscdev.minor = MISC_DYNAMIC_MINOR;
	binder_device->miscdev.name = name;
   // 获取 uid 放到 binder_service 中 
	binder_device->context.binder_context_mgr_uid = INVALID_UID;
	binder_device->context.name = name;

	ret = misc_register(&binder_device->miscdev);
	if (ret < 0) {
		kfree(binder_device);
		return ret;
	}
    // 将 binder 设备添加到设备链表中
	hlist_add_head(&binder_device->hlist, &binder_devices);

	return ret;
}

这里用到了一个 &binder_fops 我们来看下这个数据结构

static const struct file_operations binder_fops = {
	.owner = THIS_MODULE,
	.poll = binder_poll,
	.unlocked_ioctl = binder_ioctl,
	.compat_ioctl = binder_ioctl,
        // .mmap 是native 层, binder_mmap 就是驱动层
	.mmap = binder_mmap,
	.open = binder_open,
	.flush = binder_flush,
	.release = binder_release,
};

这里将 native 层和驱动层进行了一个映射;

所以 binder_init 做了三件事情:分配内存、初始化设备、添加到设备链表binder_devices;

binder_open()

我们接下来看下 binder_open 都做了什么,我们进入源码看下

static int binder_open(struct inode *nodp, struct file *filp)
{
	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);
   // 初始化一个结构体,分配内存,用来保存一个进程信息
	proc = kzalloc(sizeof(*proc), GFP_KERNEL);
	if (proc == NULL)
		return -ENOMEM;
   // 将当前线程任务栈保存到 proc,相当于是给这个 proc 结构体进行初始化	
        get_task_struct(current);
	proc->tsk = current;
	INIT_LIST_HEAD(&proc->todo);
	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__);
   // proc 进行基数 +1
	binder_stats_created(BINDER_STAT_PROC);
   // 将 proc 添加到 binder_procs 数据链表中     
	hlist_add_head(&proc->proc_node, &binder_procs);
	proc->pid = current->group_leader->pid;
	INIT_LIST_HEAD(&proc->delivered_death);
   // 将 proc 添加到 flip 中,后续 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_open 在这里做了四件事:初始化 binder_proc 对象,将当前进程信息保存到 proc 中,将 proc 信息保存到 flip 中,将 binder_proc 添加到 binder_procs 链表中;

binder_mmap()

我们接下来看下 binder_mmap 都做了什么,我们进入源码看下:

// vma 进程的虚拟内存
static int binder_mmap(struct file *filp, struct vm_area_struct *vma)
{
	int ret;
   // area 内核的虚拟内存
	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 指针指向这一块内核空间虚拟内存     
	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
   // 分配内存
	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;
    // 分配物理内存
	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;
	}
	buffer = proc->buffer;
	INIT_LIST_HEAD(&proc->buffers);
   // buffer 添加到链表中
	list_add(&buffer->entry, &proc->buffers);
   // 标记内存可以使用了	
        buffer->free = 1;
	binder_insert_free_buffer(proc, 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_update_page_range 分配物理内存,我们进入这个方法看下:

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 页(4kb的物理内存)
		*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;
		}
           // 把物理空间映射的虚拟内核空间(mmap映射的地方)
		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, NULL);
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;
}

通过 binder_insert_free_buffer 计算出我们可以使用的 buffer 大小,我们进入这个方法看下:

static void binder_insert_free_buffer(struct binder_proc *proc,
				      struct binder_buffer *new_buffer)
{
	struct rb_node **p = &proc->free_buffers.rb_node;
	struct rb_node *parent = NULL;
	struct binder_buffer *buffer;
	size_t buffer_size;
	size_t new_buffer_size;

	BUG_ON(!new_buffer->free);

	new_buffer_size = binder_buffer_size(proc, new_buffer);

	binder_debug(BINDER_DEBUG_BUFFER_ALLOC,
		     "%d: add free buffer, size %zd, at %p\n",
		      proc->pid, new_buffer_size, new_buffer);

	while (*p) {
		parent = *p;
		buffer = rb_entry(parent, struct binder_buffer, rb_node);
		BUG_ON(!buffer->free);
           // 主要是通过这里获取可用 buffer 大小
		buffer_size = binder_buffer_size(proc, buffer);

		if (new_buffer_size < buffer_size)
			p = &parent->rb_left;
		else
			p = &parent->rb_right;
	}
	rb_link_node(&new_buffer->rb_node, parent, p);
    // 将可用的 buffer 插入到 free_buffers 中
	rb_insert_color(&new_buffer->rb_node, &proc->free_buffers);
}

binder_mmap 主要做了三件事,通过用户空间虚拟内存大小分配了一块内核空间同等大小的虚拟内存,分配了一块4kb的物理内存,把这块物理内存分别映射到用户空间的虚拟内存和内核空间的虚拟内存;

这里的 4kb 只是先分配,等到真正要使用的时候,要用多少分配多少;

binder_ioctl()

这个方法主要是用来进行文件的读写操作的,我们进入这个方法看一下:

static long binder_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
	int ret;
	struct binder_proc *proc = filp->private_data;
	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);*/

	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__);
	thread = binder_get_thread(proc);
	if (thread == NULL) {
		ret = -ENOMEM;
		goto err;
	}

	switch (cmd) {
   // 用的最多的主要是这个 case 下的 binder_ioctl_write_read 这个方法
	case BINDER_WRITE_READ:
		ret = binder_ioctl_write_read(filp, cmd, arg, thread);
		if (ret)
			goto err;
		break;
	case BINDER_SET_MAX_THREADS:
		if (copy_from_user(&proc->max_threads, ubuf, sizeof(proc->max_threads))) {
			ret = -EINVAL;
			goto err;
		}
		break;
	case BINDER_SET_CONTEXT_MGR:
		ret = binder_ioctl_set_ctx_mgr(filp);
		if (ret)
			goto err;
		break;
	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;
	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;
}

用的最多的主要是这个 case 下的 binder_ioctl_write_read 这个方法,我们进入这个方法看下:

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;
	}
	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) {
		ret = binder_thread_write(proc, thread,
					  bwr.write_buffer,
					  bwr.write_size,
					  &bwr.write_consumed);
		trace_binder_write_done(ret);
		if (ret < 0) {
			bwr.read_consumed = 0;
			if (copy_to_user(ubuf, &bwr, sizeof(bwr)))
				ret = -EFAULT;
			goto out;
		}
	}
   // 读数据
	if (bwr.read_size > 0) {
		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) {
			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);
	if (copy_to_user(ubuf, &bwr, sizeof(bwr))) {
		ret = -EFAULT;
		goto out;
	}
out:
	return ret;
}

我们接下来看下 jni 层调用流程;

我们 zygote 的启动,会通过 /system/core/rootdir/init.zygote32.rc文件中的命令进行启动,通过这个命令就会启动我们的 app_main.cpp 下的 main 方法;

service zygote /system/bin/app_process -Xzygote /system/bin --zygote --start-system-server
    class main
    socket zygote stream 660 root system
    onrestart write /sys/android_power/request_state wake
    onrestart write /sys/power/state on
    onrestart restart media
    onrestart restart netd
    writepid /dev/cpuset/foreground/tasks

我们进入这个 main 方法看下:

int main(int argc, char* const argv[])
{
    if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) < 0) {
        // Older kernels don't understand PR_SET_NO_NEW_PRIVS and return
        // EINVAL. Don't die on such kernels.
        if (errno != EINVAL) {
            LOG_ALWAYS_FATAL("PR_SET_NO_NEW_PRIVS failed: %s", strerror(errno));
            return 12;
        }
    }

    AppRuntime runtime(argv[0], computeArgBlockSize(argc, argv));
    // Process command line arguments
    // ignore argv[0]
    argc--;
    argv++;

    // Everything up to '--' or first non '-' arg goes to the vm.
    //
    // The first argument after the VM args is the "parent dir", which
    // is currently unused.
    //
    // After the parent dir, we expect one or more the following internal
    // arguments :
    //
    // --zygote : Start in zygote mode
    // --start-system-server : Start the system server.
    // --application : Start in application (stand alone, non zygote) mode.
    // --nice-name : The nice name for this process.
    //
    // For non zygote starts, these arguments will be followed by
    // the main class name. All remaining arguments are passed to
    // the main method of this class.
    //
    // For zygote starts, all remaining arguments are passed to the zygote.
    // main function.
    //
    // Note that we must copy argument string values since we will rewrite the
    // entire argument block when we apply the nice name to argv0.

    int i;
    for (i = 0; i < argc; i++) {
        if (argv[i][0] != '-') {
            break;
        }
        if (argv[i][1] == '-' && argv[i][2] == 0) {
            ++i; // Skip --.
            break;
        }
        runtime.addOption(strdup(argv[i]));
    }

    // Parse runtime arguments.  Stop at first unrecognized option.
    bool zygote = false;
    bool startSystemServer = false;
    bool application = false;
    String8 niceName;
    String8 className;

    ++i;  // Skip unused "parent dir" argument.
    while (i < argc) {
        const char* arg = argv[i++];
        // 判断是不是 zygote 启动
        if (strcmp(arg, "--zygote") == 0) {
            zygote = true;
            niceName = ZYGOTE_NICE_NAME;
        } else if (strcmp(arg, "--start-system-server") == 0) {
            startSystemServer = true;
        } else if (strcmp(arg, "--application") == 0) {
            application = true;
        } else if (strncmp(arg, "--nice-name=", 12) == 0) {
            niceName.setTo(arg + 12);
        } else if (strncmp(arg, "--", 2) != 0) {
            className.setTo(arg);
            break;
        } else {
            --i;
            break;
        }
    }

    Vector<String8> args;
    if (!className.isEmpty()) {
        // We're not in zygote mode, the only argument we need to pass
        // to RuntimeInit is the application argument.
        //
        // The Remainder of args get passed to startup class main(). Make
        // copies of them before we overwrite them with the process name.
        args.add(application ? String8("application") : String8("tool"));
        runtime.setClassNameAndArgs(className, argc - i, argv + i);
    } else {
        // We're in zygote mode.
        maybeCreateDalvikCache();

        if (startSystemServer) {
            args.add(String8("start-system-server"));
        }

        char prop[PROP_VALUE_MAX];
        if (property_get(ABI_LIST_PROPERTY, prop, NULL) == 0) {
            LOG_ALWAYS_FATAL("app_process: Unable to determine ABI list from property %s.",
                ABI_LIST_PROPERTY);
            return 11;
        }

        String8 abiFlag("--abi-list=");
        abiFlag.append(prop);
        args.add(abiFlag);

        // In zygote mode, pass all remaining arguments to the zygote
        // main() method.
        for (; i < argc; ++i) {
            args.add(String8(argv[i]));
        }
    }

    if (!niceName.isEmpty()) {
        runtime.setArgv0(niceName.string());
        set_process_name(niceName.string());
    }
    
    if (zygote) {
        // 如果是 zygote 启动,就会执行 AppRuntime(继承 AndroidRuntime) 的 start 方法
        runtime.start("com.android.internal.os.ZygoteInit", args, zygote);
    } else if (className) {
        runtime.start("com.android.internal.os.RuntimeInit", args, zygote);
    } else {
        fprintf(stderr, "Error: no class name or --zygote supplied.\n");
        app_usage();
        LOG_ALWAYS_FATAL("app_process: no class name or --zygote supplied.");
        return 10;
    }
}

如果是 zygote 启动,就会执行 AppRuntime(继承 AndroidRuntime) 的 start 方法,我们进入 AndroidRuntime 的 start 方法看下:

void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote)
{
    ALOGD(">>>>>> START %s uid %d <<<<<<\n",
            className != NULL ? className : "(unknown)", getuid());

    static const String8 startSystemServer("start-system-server");

    /*
     * 'startSystemServer == true' means runtime is obsolete and not run from
     * init.rc anymore, so we print out the boot start event here.
     */
    for (size_t i = 0; i < options.size(); ++i) {
        if (options[i] == startSystemServer) {
           /* track our progress through the boot sequence */
           const int LOG_BOOT_PROGRESS_START = 3000;
           LOG_EVENT_LONG(LOG_BOOT_PROGRESS_START,  ns2ms(systemTime(SYSTEM_TIME_MONOTONIC)));
        }
    }

    const char* rootDir = getenv("ANDROID_ROOT");
    if (rootDir == NULL) {
        rootDir = "/system";
        if (!hasDir("/system")) {
            LOG_FATAL("No root directory specified, and /android does not exist.");
            return;
        }
        setenv("ANDROID_ROOT", rootDir, 1);
    }

    //const char* kernelHack = getenv("LD_ASSUME_KERNEL");
    //ALOGD("Found LD_ASSUME_KERNEL='%s'\n", kernelHack);

    /* start the virtual machine */
    JniInvocation jni_invocation;
    jni_invocation.Init(NULL);
    JNIEnv* env;
    if (startVm(&mJavaVM, &env, zygote) != 0) {
        return;
    }
    onVmCreated(env);
    
    // 我们只需要关心这里就可以了,这里就是进行注册的逻辑
    /*
     * Register android functions.
     */
    if (startReg(env) < 0) {
        ALOGE("Unable to register all android natives\n");
        return;
    }

    /*
     * We want to call main() with a String array with arguments in it.
     * At present we have two arguments, the class name and an option string.
     * Create an array to hold them.
     */
    jclass stringClass;
    jobjectArray strArray;
    jstring classNameStr;

    stringClass = env->FindClass("java/lang/String");
    assert(stringClass != NULL);
    strArray = env->NewObjectArray(options.size() + 1, stringClass, NULL);
    assert(strArray != NULL);
    classNameStr = env->NewStringUTF(className);
    assert(classNameStr != NULL);
    env->SetObjectArrayElement(strArray, 0, classNameStr);

    for (size_t i = 0; i < options.size(); ++i) {
        jstring optionsStr = env->NewStringUTF(options.itemAt(i).string());
        assert(optionsStr != NULL);
        env->SetObjectArrayElement(strArray, i + 1, optionsStr);
    }

    /*
     * Start VM.  This thread becomes the main thread of the VM, and will
     * not return until the VM exits.
     */
    char* slashClassName = toSlashClassName(className);
    jclass startClass = env->FindClass(slashClassName);
    if (startClass == NULL) {
        ALOGE("JavaVM unable to locate class '%s'\n", slashClassName);
        /* keep going */
    } else {
        jmethodID startMeth = env->GetStaticMethodID(startClass, "main",
            "([Ljava/lang/String;)V");
        if (startMeth == NULL) {
            ALOGE("JavaVM unable to find main() in '%s'\n", className);
            /* keep going */
        } else {
            env->CallStaticVoidMethod(startClass, startMeth, strArray);

#if 0
            if (env->ExceptionCheck())
                threadExitUncaughtException(env);
#endif
        }
    }
    free(slashClassName);

    ALOGD("Shutting down VM\n");
    if (mJavaVM->DetachCurrentThread() != JNI_OK)
        ALOGW("Warning: unable to detach main thread\n");
    if (mJavaVM->DestroyJavaVM() != 0)
        ALOGW("Warning: VM did not shut down cleanly\n");
}

我们只需要关心 startReg 这里就可以了,这里就是进行 jni 的注册的逻辑;

int AndroidRuntime::startReg(JNIEnv* env)
{
    /*
     * This hook causes all future threads created in this process to be
     * attached to the JavaVM.  (This needs to go away in favor of JNI
     * Attach calls.)
     */
    androidSetCreateThreadFunc((android_create_thread_fn) javaCreateThreadEtc);

    ALOGV("--- registering native functions ---\n");

    /*
     * Every "register" function calls one or more things that return
     * a local reference (e.g. FindClass).  Because we haven't really
     * started the VM yet, they're all getting stored in the base frame
     * and never released.  Use Push/Pop to manage the storage.
     */
    env->PushLocalFrame(200);
    // 进行 jni 的注册逻辑,系统启动的时候就会注册系统的 jni 方法,这样 java 方法才能调用到 native 方法;
    if (register_jni_procs(gRegJNI, NELEM(gRegJNI), env) < 0) {
        env->PopLocalFrame(NULL);
        return -1;
    }
    env->PopLocalFrame(NULL);

    //createJavaThread("fubar", quickTest, (void*) "hello");

    return 0;
}

进行 jni 的注册逻辑,系统启动的时候就会注册系统的 jni 方法,这样 java 方法才能调用到 native 方法;我们进入这个 register_jni_procs 方法看下:

static int register_jni_procs(const RegJNIRec array[], size_t count, JNIEnv* env)
{
    for (size_t i = 0; i < count; i++) {
        // array[i] 就是 RegJNIRec 
        if (array[i].mProc(env) < 0) {
#ifndef NDEBUG
            ALOGD("----------!!! %s failed to load\n", array[i].mName);
#endif
            return -1;
        }
    }
    return 0;
}

就会调用这些方法:

image.png

这里就有一个 REG_JNI(register_android_os_Binder) binder 的注册就在这里,我们进入这个方法看下:

int register_android_os_Binder(JNIEnv* env)
{
    // 主要是这三个方法,注册binder、binderInternal、binderProxy,我们看其中一个方法就可以
    if (int_register_android_os_Binder(env) < 0)
        return -1;
    if (int_register_android_os_BinderInternal(env) < 0)
        return -1;
    if (int_register_android_os_BinderProxy(env) < 0)
        return -1;

    jclass clazz = FindClassOrDie(env, "android/util/Log");
    gLogOffsets.mClass = MakeGlobalRefOrDie(env, clazz);
    gLogOffsets.mLogE = GetStaticMethodIDOrDie(env, clazz, "e",
            "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/Throwable;)I");

    clazz = FindClassOrDie(env, "android/os/ParcelFileDescriptor");
    gParcelFileDescriptorOffsets.mClass = MakeGlobalRefOrDie(env, clazz);
    gParcelFileDescriptorOffsets.mConstructor = GetMethodIDOrDie(env, clazz, "<init>",
                                                                 "(Ljava/io/FileDescriptor;)V");

    clazz = FindClassOrDie(env, "android/os/StrictMode");
    gStrictModeCallbackOffsets.mClass = MakeGlobalRefOrDie(env, clazz);
    gStrictModeCallbackOffsets.mCallback = GetStaticMethodIDOrDie(env, clazz,
            "onBinderStrictModePolicyChange", "(I)V");

    return 0;
}

主要是这三个方法,注册binder、binderInternal、binderProxy,我们看其中一个方法就可以,int_register_android_os_Binder 进入这个方法看下:

const char* const kBinderPathName = "android/os/Binder";

static int int_register_android_os_Binder(JNIEnv* env)
{
    // 拿到 Binder 的 class 对象
    jclass clazz = FindClassOrDie(env, kBinderPathName);
    // 通过 gBinderOffsets 这个结构体,保存 java 的一些信息,可以想象成反射
    gBinderOffsets.mClass = MakeGlobalRefOrDie(env, clazz);
    gBinderOffsets.mExecTransact = GetMethodIDOrDie(env, clazz, "execTransact", "(IJJI)Z");
    gBinderOffsets.mObject = GetFieldIDOrDie(env, clazz, "mObject", "J");
    // 获取 gBinderMethods 注册 java 与 native 方法的映射关系;
    return RegisterMethodsOrDie(
        env, kBinderPathName,
        gBinderMethods, NELEM(gBinderMethods));
}
tatic const JNINativeMethod gBinderMethods[] = {
     /* name, signature, funcPtr */
    { "getCallingPid", "()I", (void*)android_os_Binder_getCallingPid },
    { "getCallingUid", "()I", (void*)android_os_Binder_getCallingUid },
    { "clearCallingIdentity", "()J", (void*)android_os_Binder_clearCallingIdentity },
    { "restoreCallingIdentity", "(J)V", (void*)android_os_Binder_restoreCallingIdentity },
    { "setThreadStrictModePolicy", "(I)V", (void*)android_os_Binder_setThreadStrictModePolicy },
    { "getThreadStrictModePolicy", "()I", (void*)android_os_Binder_getThreadStrictModePolicy },
    { "flushPendingCommands", "()V", (void*)android_os_Binder_flushPendingCommands },
    { "init", "()V", (void*)android_os_Binder_init },
    { "destroy", "()V", (void*)android_os_Binder_destroy },
    { "blockUntilThreadAvailable", "()V", (void*)android_os_Binder_blockUntilThreadAvailable }
};

可以看到 这里存放的就是 java 层可以用调用的方法;通过 int_register_android_os_Binder 这个方法 就是实现了 Java 与 native 的互相通信;

好了,binder 的启动就讲解到这里吧,我们接下来进行 MMKV 的核心实现:

MMKV 核心实现


这里直接上代码吧,核心实现其实还是比较简单的:

#include <jni.h>
#include <string>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <android/log.h>

int8_t *m_ptr;
int32_t m_size;
int m_fd;

extern "C"
JNIEXPORT void JNICALL
Java_com_test_llc_1binder_MainActivity_writeTest(JNIEnv *env, jobject thiz) {
    std::string file = "/sdcard/a.txt";

    //打开文件
    m_fd = open(file.c_str(), O_RDWR | O_CREAT, S_IRWXU);

    //获得一页内存大小
    //Linux采用了分页来管理内存,即内存的管理中,内存是以页为单位,一般的32位系统一页为 4096个字节
    m_size = getpagesize();


    //将文件设置为 m_size这么大
    ftruncate(m_fd, m_size); // 100  1000 10000

    // m_size:映射区的长度。 需要是整数页个字节    byte[]
    m_ptr = (int8_t *) mmap(0, m_size, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd,
                            0);

    std::string data("mars刚刚写入的数据");
    //将 data 的 data.size() 个数据 拷贝到 m_ptr
    //Java 类似的:
//        byte[] src = new byte[10];
//        byte[] dst = new byte[10];
//        System.arraycopy(src, 0, dst, 0, src.length);
    memcpy(m_ptr, data.data(), data.size());

    __android_log_print(ANDROID_LOG_ERROR, "mars", "写入数据:%s", data.c_str());
}

extern "C"
JNIEXPORT void JNICALL
Java_com_test_llc_1binder_MainActivity_readTest(JNIEnv *env, jobject thiz) {
    //申请内存
    char *buf = static_cast<char *>(malloc(100));

    memcpy(buf, m_ptr, 100);

    std::string result(buf);
    __android_log_print(ANDROID_LOG_ERROR, "mars", "读取数据:%s", result.c_str());

    //取消映射
    munmap(m_ptr, m_size);
    //关闭文件
    close(m_fd);
}

简历润色


深度理解 Binder 原理、mmap原理,可手写 MMKV 核心实现

下一章预告


进程通信,启动与获取SM服务

欢迎三连


来都来了,点个关注点个赞吧,你的支持是我最大的动力~~~

RK3572-Android16-记录一次OTA升级失败解决方案 这里记录一次解决RK3576 Android16 OTA 用Recovery 升级失败的一次解决案例,将部分经验作为总结。本次OTA升级耗时了几天,最终是RK给出的解决方案,再加上经验来解决了其它部分问题新平台,高一点的Android版本,可能本身有bug,多请教平台来,不要蒙着干,干不出来的。 阅读详情

相关推荐

手把手教你用Simulink搭建单相全桥逆变器(附单电流闭环控制仿真文件)

本文详细介绍了如何在Simulink中搭建单相全桥逆变器并实现单电流闭环控制。从功率主电路建模、核心器件参数设置,到控制系统的PI调节与前馈补偿,提供了完整的仿真步骤与调试技巧。文章旨在通过动手实践,帮助读者跨越理论与仿真的鸿沟,深入理解电力电子闭环控制的核心原理。

d6e7f8g9h的博客 316

聊一聊Kotlin委托的那些事,看懂这个你也是Kotlin高手

/ 今日科技快讯 /据美国《新闻周刊》报道,社交平台脸书的创始人扎克伯格日前被《新共和》(The New Republic)刊物评为“年度恶人”,理由是他创建了“世界上最糟糕、最具...

郭霖 1319

【dify】 dify环境变量配置说明

SMTP_SERVER、SMTP_PORT、SMTP_USERNAME、SMTP_PASSWORD、SMTP_USE_TLS、SMTP_OPPORTUNISTIC_TLS:当 MAIL_TYPE 为 smtp 时,SMTP服务器的相关配置。UPLOAD_IMAGE_FILE_SIZE_LIMIT、UPLOAD_VIDEO_FILE_SIZE_LIMIT、UPLOAD_AUDIO_FILE_SIZE_LIMIT:分别为上传图像、视频、音频文件的大小限制(MB)。

tonysh_zds的博客 7355

为什么-Android-要采用-Binder-作为-IPC-机制?

其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。以上分享【我的阿里P7移动开发架构师学习笔记】七大模块整套学习资料均免费分享,需要的小伙伴,我已经上传到石墨文档了,大家自取就可以了。白嫖可以,别忘了给我点个关注哈。当然我也为你们整理好了百度、阿里、腾讯、字节跳动等等互联网超级大厂的历年面试真题集锦。

2401_83621918的博客 805

AGI 之 【Dify】 之 Dify 在 Windows 端本地部署调用 Ollama 本地下载的大模型, API 形式 Python 调用 进行简单聊天接口测试

AGI,即通用人工智能(Artificial General Intelligence),是一种具备人类智能水平的人工智能系统。它不仅能够执行特定的任务,而且能够理解、学习和应用知识于广泛的问题解决中,具有较高的自主性和适应性。AGI的能力包括但不限于自我学习、自我改进、自我调整,并能在没有人为干预的情况下解决各种复杂问题。本节将介绍如何使用 Dify 平台调用 Ollama 部署的本地模型来创建一个聊天应用。通过 API 的方式发起聊天,可以让前端自由布置所需的聊天形式。

仙魁XAN 1万+

瑞数6.5逆向实战:手把手教你用RPC搞定cookie sign加密(附环境补全技巧)

本文深入解析瑞数6.5逆向实战,重点探讨如何结合环境补全与RPC调用技术来应对其动态加密机制。文章详细介绍了环境指纹采集、双重验证等核心防护原理,并提供了基于Puppeteer的RPC实现方案,手把手教你搞定cookie sign加密,附有实用的环境补全技巧与完整代码示例。

weixin_29315091的博客 1273

MMKV-KTX 项目常见问题解决方案

MMKV-KTX 项目常见问题解决方案 MMKV-KTX 是一个开源项目,旨在提供一种便捷、类型安全的方式来使用腾讯的 MMKV 存储解决方案。该项目主要使用 Kotlin 编程语言开发,结合 Kotlin 的特性,如属性委托,使得 MMKV 更加灵活和易用。 1. 项目基础介绍和主要编程语言 MMKV-KTX 是基于 MMKV 的一个 Kotlin 工具库,它通过 Kotlin 的属性委托特性,...

gitblog_00253的博客 1050

uniapp [安卓苹果App端] - 解决支付宝支付报错requestPayment:fail [payment支付宝:62009]未知错误,使用uni.requestPayment支付报错排查方案

uni-app支付宝,uniapp支付宝沙箱支付报错,uni.requestPayment,uniAPP App支付宝支付提示错误requestPayment:fail [payment支付宝:62009]未知错误,UNI-APP支付宝报错"code": -100,uniapp 解决安卓App使用uni.requestPayment实现沙箱环境支付宝支付报错,uniapp安卓app使用支付宝的沙箱环境支付时报了这个错,安卓端app支付宝支付,在沙箱测试环境测支付,提示“商家订单参数异常,请重新发起付款,选择

🏆 前端领域优质创作者 9609

Android使用Ant自动打包,kotlin中文教程

<property name=“out-signed-package” value="outdir/{outdir}/outdir/{ant 《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》 【docs.qq.com/doc/DSkNLaERkbnFoS0ZF】 完整资料开源分享 .project.name}-signed.apk" /> Creating all o...

m0_65511857的博客 1798

【Halcon-2D测量】add_metrology_object_rectangle2_measure 函数功能(用于向2D计量模型中添加旋转矩形(rectangle2类型)计量对象)

HALCON算子add_metrology_object_rectangle2_measure解析摘要 该算子是HALCON 2D计量模块的核心算子,用于添加旋转矩形计量对象并配置测量参数。主要功能包括:定义旋转矩形几何轮廓(中心坐标、主轴角度、长短半长);生成垂直于矩形边界的测量区域;设置边缘提取预处理参数(高斯平滑、振幅阈值)。关键注意事项:MeasureLength1必须小于矩形长短半长;角度参数Phi需使用弧度单位;多线程需同步模型句柄。该算子适用于方形螺母、PCB板边框等矩形零件的精密测量,需与a

weixin_45590420的博客 1223

AUTOSAR NvM 实车故障深度剖析:车载存储问题快速定位与工程解决方案

软件配置与代码逻辑无问题时,排查底层驱动与ECU硬件,步骤如下:底层驱动排查:排查Fls/Eep驱动配置,是否适配目标MCU(如TC397、S32K),编程/擦除/读取时序是否合理;检查Fls/Eep驱动错误处理逻辑,是否未处理Flash编程失败、读取错误等异常;验证Flash高速编程模式是否启用,是否能提升写入/读取速度;排查MemIf层配置,是否正确对接NvM与Fls/Eep驱动,是否存在调度延迟。

getusushu的博客 78

Android—Binder+AIDL

Binder Binder机制优点: 只需要进行一次数据拷贝,性能上仅次于共享内存。 基于C/S架构,职责明确,架构清晰,稳定性较好。 安全性好,为每个App分配UID,UID是鉴别进程身份的标志。 内存映射:(一次copy的原因) Binder IPC 机制中涉及到的内存映射通过 mmap() 来实现,mmap() 是操作系统中一种内存映射的方法。内存映射简单的讲就是将用户空间的一块内存区域映射到内核空间。映射关系建立后,用户对这块内存区域的修改可以直接反应到内核空间;反之内核空间对这段区域的.

#请假条的博客 970

微信文件自动归档!手把手教你用OpenClaw搭个自己的“数字员工”,告别重复劳动

很多人说“AI是未来”,但未来不是靠喊口号的。OpenClaw的真正价值,是让你把重复劳动交出去,把时间留给真正需要创造力的事。这事真能干,而且不难。下载OpenClaw(GitHub仓库按我写的步骤跑一遍遇到坑,评论区留言——我亲自帮你看作者后记:上周五,我让AI自动归档了文件后,终于在5点准时下班。回家路上,我给它发了条消息:“干得漂亮”。它没回,但我知道——它在默默等我的下一条指令。

2601_94871597的博客 414

Android 之 EventBus篇

EventBus篇1- 简介2- 四种线程3- 开始使用:3.1- 定义事件3.2- 发布事件3.2.1- activity-A3.2.2- activity-B 1- 简介 EventBus是一种用于Android的事件发布-订阅总线,由GreenRobot开发,Gihub地址是:EventBus。它简化了应用程序内各个组件之间进行通信的复杂度,尤其是碎片之间进行通信的问题,可以避免由于使用广播通信而带来的诸多不便。 2- 四种线程 POSTING:默认,表示事件处理函数的线程跟发布事件的线程在同一

qq_37399372的博客 2615

Data-Structures-and-Algorithm-Analysis-in-Java.pdf

作者: Weiss, Mark A. 出版社: Addison Wesley Data Structures and Algorithm Analysis in Java is an “advanced algorithms” book that fits between traditional CS2 and Algorithms Analysis courses. In the old ACM Curriculum Guidelines, this course was known as CS7. This text is for readers who want to learn good programming and algorithm analysis skills simultaneously so that they can develop such programs with the maximum amo...

win10 跳过U8环境检测的IE WebControls.rar

win10 安装u8的时候 提示IE WebControls 不通过,这个工具可以欺骗u8的检查。 跳过U8环境检测的IE WebControls。提示导入失败后,重新检测通过。

Android Binder机制详解:手写IPC通信

想要掌握一样东西,最好的方式就是阅读理解它的源码。想要掌握Android Binder,最好的方式就是写一个AIDL文件,然后查看其生成的代码。本文的思路也是来自于此。 简介 Binder是Android常用的一种进程间通信方式。当然,不使用Binder,你还可以使用Socket甚至文件来进行通信。 通常Android上的进程间通信,指的就是远程Service的调用。 开始 新建测试工...

Rvng2014的博客 445

假人有限元模型

假人模型,车辆碰撞假人有限元模型,适合做乘员安全的研究者利用

Android 存储优化 —— MMKV 集成与原理 mmap binder通信 替代SharePreference

1.如何跨进程 2.为什么效率高 3.如何扩容 4.probuffer数据结构 5.binder机制 6.用户空间和内核空间 7.内存映射 参考博客: https://github.com/733gh/Android-Notes/blob/master/MMKV框架原理解密,MMKV如何利用mmap函数实现储存优化100倍/MMKV框架原理解密之01MMKV使用.md 目前项目中在轻量级存储上使用的是 SharedPreferences, 虽然 SP 兼容性极...

深南大盗的博客 1163

android 蓝牙通讯demo

android 下蓝牙通讯demo,实现了发现蓝牙,使能蓝牙,通过蓝牙发送消息。

IO优化:使用mmap使读取文件的效率提升

在wps Android端的日子过得很快,我所在的组是IO组。因为WPS需要大量的和文档打交道,所以有专门的组负责IO。 最近想探索一下,是否可以把mmap接入到wps上。于是做了 “手写mmap进行读盘”的探索。 这里说一句,因为java 层的MappedByteBuffer 虽然也是采用了mmap。但是我发现有一个缺点是 没有提供unmap方法,很容易超过最大限制导致映射失败 我不知道为什么很多博客说MappedByteBuffer效率会高,如果你有比较demo可以证明这一点,可以在评论一起交流。 先说

共同交流与成长 9392

认真分析mmap:是什么 为什么 怎么用

mmap基础概念 mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实...

weixin_30493401的博客 5237

Android 手写Binder 教你理解android中的进程间通信

关于Binder,我就不解释的太多了,网上一搜资料一堆,但是估计还是很多人理解的有困难。今天就教你如何从 app层面来理解好Binder。 其实就从我们普通app开发者的角度来看,仅仅对于android应用层的话,Binder就是客户端和服务端进行通信的媒介。 AIDL就是我们理解Binder 最好的事例。 我们都知道 我们写好aidl 文件以后,开发工具 会自动帮我们生成好代码。实际上 我...

fanleiym的专栏 275

MMKV系列:MMKV简单使用

先看效果 引入MMKV依赖 implementation 'com.tencent:mmkv:1.0.19' 在MyApplication中初始化,并存值。不需要申请读写内存权限。 public class MyApplication extends Application { @Override public void onCreate() { super.onCreate(); String rootDir = MMKV.initialize(

知道这个有什么用? 616

Android 开发入门实践(二):Kotlin 空安全,从可空类型到安全访问

延续 NewsApp 项目中的新闻标题练习,记录 Kotlin 安全调用。

Easyhood的博客 290

Android 模块化收口:能力组合框架、手写 LifecycleOwner 与热修复共存

二十多个模块业务高度相似却各有差异,核心矛盾在于“统一与灵活”的平衡。通过角色接口组合(如IProjectAware、IVoiceAware)实现能力按需注入,新增功能零侵入;用模板方法锁定生命周期流程,确保顺序一致;结合@CallSuper与工厂方法,既约束关键逻辑,又保留子类自由。最终实现:骨架统一、填空自由,管得住业务,又不侵入业务。

mmsx990的博客 397

Android 开发入门实践(十三):用 Intent、UiState 和 Reducer 理解 MVI 的状态更新

本文记录第141—145课对MVI架构的入门实践,通过“返回列表”功能逐步构建意图(NewsIntent)、状态(NewsUiState)与状态转换(reduce)机制。基于单向数据流,用户操作生成意图,ViewModel通过Reducer根据旧状态计算新状态,并通过StateFlow更新Compose界面。代码实现清晰分离关注点,强化了对状态管理、事件驱动与数据流的理解。

Easyhood的博客 326

Android Message源码阅读

本文梳理了Android Handler机制中Message的核心设计:链表结构、消息复用池(sPool)及回收机制,重点解析了异步消息与同步屏障(SyncBarrier)的协同机制。当主线程需优先处理UI绘制等高优先级任务时,系统通过postSyncBarrier插入屏障,使Looper跳过普通同步消息,优先执行标记为异步的UI渲染任务,确保帧率流畅。该机制是保障Android界面响应性的关键技术之一。

u012116089的博客 201

安卓部署Linux性能实测:一颗移动处理器,能否胜任日常 Linux 开发?

本文通过在一加Ace5上部署NimoteCode的ARM64 Linux环境,测试了其在CoreMark、SQLite编译及并行构建中的表现。结果显示,尽管单核性能约2.34GHz,多核扩展比达6.15×,但优化编译耗时仍高达35秒,反映移动处理器虽具备一定开发能力,却受限于文件系统开销与运行环境(如PRoot)。实际开发中,非优化构建效率较高,而并行编译可显著提速。结论:安卓手机可作为轻量级开发辅助工具,但无法替代桌面环境,真正价值在于便携性与即时响应,而非全功能替代。

一个不知名码农的专栏 324

Android 17 全栈性能分析关键字与源码手册

FrameTimeline 找异常帧,ATrace 找阶段,thread_state 找等待类型,ftrace 找内核原因,Simpleperf 找 CPU 热点,驱动源码找最终触发条件;任何根因结论都必须回到同一对象、同一时间窗口和同一条依赖链。

weixin_43228946的博客 373
上一篇: 如何应对Android面试官->Handler原理解析,玩转同步屏障
下一篇: Jetpack Compose -> 无状态、状态提升,单向数据流
老A说
博客等级 码龄14年 1181粉丝 85原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值