主頁 > 移動端開發 > 深入理解Android跨行程通信-Binder機制

深入理解Android跨行程通信-Binder機制

2021-09-04 07:46:32 移動端開發

說到binder,很多Android開發者會覺得很復雜,因為binder橫跨了整個Android系統架構,從framework到kernel,binder無處不在,在日常的面試程序中,binder也是一個繞不開的話題,為啥binder這么重要,跨行程通信方式已經有了管道、socket、共享記憶體等,為啥Android還要使用binder,今天,我們帶著這些問題深入了解下Binder機制,

引子

日常應用

我們平時開發中,可能一些代碼反復寫了N多次了,所以寫起來很流暢,但我們不會注意這些流程,例如:

ActivityA 啟動 ActivityB 的程序中,我們通過intent,傳遞資料,搞定,兩個activity可以相互通信了,想過沒有,為什么使用intent序列化之后才能傳遞資料?這里面就涉及到binder,因為AMS和app是兩個行程,

我們現在很多大型app都是多行程,而多行程相互之間要通信,就需要binder,一些小企業基本用不到多行程app,而大廠就會涉及到了,例如:微信,微博等 他們都是多行程開發的,多行程有什么好處呢?

首先,系統會給每個app行程分配一個jvm,而這個jvm的大小是系統給定的,當我們一個app的使用記憶體變大后,記憶體不夠用,就會出現崩潰,而我們使用多行程時,就會給app開辟更多的記憶體,

其次,多行程開發實作了行程隔離,當一個子行程crash掉后,不會影響主行程,

最后,多行程開發使得行程保活幾率提升,一個行程被殺后,其他行程會相互拉,

Binder的優勢

linux自帶了很多IPC跨行程通信方式(管道,信號量,socket,共享記憶體),Android為啥還要搞一個binder出來?自然時binder的性能要優于其他方式,

binder只需要拷貝一次,

在性能方面,共享記憶體 > Binder > socket

在安全方面,Binder 是最安全的,他會給每個app都分配單獨的UID,并支持實名和匿名,Socket用的PID,不靠譜,

Binder拷貝一次的流程

整個Linux系統分為用戶空間和內核空間 用戶空間的資源是獨立的 內核空間的資源是共享的,

可以看到,binder通過mmap技術,將內核空間的虛擬地址和server端的虛擬地址映射到同一塊物理記憶體上,而這塊物理記憶體的大小是由server端決定的,

mmap

Linux通過將一個虛擬記憶體區域與磁盤上的物件關聯起來,以初始化 這個虛擬記憶體區域的內容,這個程序稱為記憶體映射(memory mapping),

對檔案進行mmap,會在行程的虛擬記憶體分配地址空間,創建映射關系, 實作這樣的映射關系后,就可以采用指標的方式讀寫操作這一段記憶體,而系統會自動回寫到對應的檔案磁盤上

Binder框架

要想搞明白binder的底層實作,必須搞懂整個binder框架,

這張圖就是整個binder的框架結構,從framework到kernel,經歷了jni和native的跨越,

接下來我們從下往上依次分析binder的整個程序,

binder原始碼決議

Kernel層

我們先看下binder驅動的流程圖:

從上圖可以看到binder驅動最重要的四個方法:binder_init()、binder_open()、binder_mmap()、binder_ioctl(). 接下來我們依次分析這四個方法,看他們做了哪些事情,

1. binder_init()

static int __init binder_init(void)
{
	int ret;
	char *device_name, *device_names;
	struct binder_device *device;
	struct hlist_node *tmp;
        
        // 首先創建名字為“binder”的單執行緒作業佇列,
	binder_deferred_workqueue = create_singlethread_workqueue("binder");
	if (!binder_deferred_workqueue)
		return -ENOMEM;
	
	...
	
	/*
	 * 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;
	}
	strcpy(device_names, binder_devices_param);
	
	while ((device_name = strsep(&device_names, ","))) {
		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;

}

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設備
	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;

	ret = misc_register(&binder_device->miscdev);
	if (ret < 0) {
		kfree(binder_device);
		return ret;
	}

	hlist_add_head(&binder_device->hlist, &binder_devices);

	return ret;
}
  1. 為binder設備分配記憶體,

  2. 初始化設備

  3. 將binder設備加入binder_devices鏈表

2. 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;
	get_task_struct(current);
	proc->tsk = current;
	INIT_LIST_HEAD(&proc->todo);
	init_waitqueue_head(&proc->wait);
	proc->default_priority = task_nice(current);
	binder_dev = container_of(filp->private_data, struct binder_device,
				  miscdev);
	proc->context = &binder_dev->context;
	
	binder_lock(__func__);
	
	binder_stats_created(BINDER_STAT_PROC);
	hlist_add_head(&proc->proc_node, &binder_procs);
	proc->pid = current->group_leader->pid;
	INIT_LIST_HEAD(&proc->delivered_death);
	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;

}
  1. 初始化binder_proc物件,binder_proc這個結構體就是用來保存binder行程的資訊,

  2. 將當前行程資訊保存在proc里

  3. filp->private_data = proc;

  4. 添加到binder_procs鏈表中

3. binder_mmap()

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  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
	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);
	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;
}
  1. 通過用戶空間的虛擬記憶體大小,分配一塊內核的虛擬記憶體

  2. 分配一塊物理記憶體 ------4KB

  3. 把這塊物理記憶體分別映射到用戶空間的虛擬記憶體 和 內核的虛擬記憶體

4. binder_ioctl()

讀寫操作 BINDER_WRITE_READ

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_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;
}
  1. 進入休眠,直到中斷喚醒

  2. 根據當前行程的pid, 從binder_proc中查找binder_thread,如果當前執行緒已經加入到proc的執行緒佇列,則直接回傳;如果不存在binder_thread,則創建binder_thread,并將其添加到當前的proc,

  3. 進行binder的讀寫操作

    1. 首先將用戶空間資料ubuf拷貝到bwr中,

    2. 當寫快取中有資料,則執行binder寫操作

    3. 當讀快取中有資料,則執行binder讀操作

    4. 行程todo佇列不為空,則喚醒該佇列中的執行緒

    5. 把內核空間資料bwr拷貝到ubuf

JNI層

我們知道,native層的binder要想和framework層通信,首先得通過jni注冊,這個注冊在zygote行程啟動時,通過app_main.cpp中的main方法,

frameworks/base/cmds/app_process/app_main.cpp
// 186
int main(int argc, char* const argv[])
// 248 將zygote標志位置為true,
if (strcmp(arg, "--zygote") == 0) {
 zygote = true;
}
// 306 運行AndroidRuntime.cpp的start方法
if (zygote) {
 runtime.start("com.android.internal.os.ZygoteInit", args, zygote);
}

然后呼叫startReg方法來完成jni方法的注冊,

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

static const RegJNIRec gRegJNI[] = {
	...
    REG_JNI(register_android_os_Binder),
    ...
};

可以看到,register_android_os_Binder 這個是binder的jni方法,我們進入看下:

代碼路徑在:frameworks/base/core/jni/android_util_Binder.cpp

int register_android_os_Binder(JNIEnv* env)
{
    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;

}

這里分為三個階段:

1.int_register_android_os_Binder

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

static int int_register_android_os_Binder(JNIEnv* env)
{
    jclass clazz = FindClassOrDie(env, kBinderPathName);

    gBinderOffsets.mClass = MakeGlobalRefOrDie(env, clazz);
    gBinderOffsets.mExecTransact = GetMethodIDOrDie(env, clazz, "execTransact", "(IJJI)Z");
    gBinderOffsets.mObject = GetFieldIDOrDie(env, clazz, "mObject", "J");
    
    return RegisterMethodsOrDie(
        env, kBinderPathName,
        gBinderMethods, NELEM(gBinderMethods));

}
  1. 通過kBinderPathName查找檔案,此時的檔案是:“android/os/Binder”,回傳class物件,

  2. 通過gBinderOffsets結構體,保存Java層Binder類的資訊,為JNI層訪問Java層提供通道,

  3. 通過RegisterMethodsOrDie,將為gBinderMethods陣列完成映射關系,從而為Java層訪問JNI層提供通道,

2.int_register_android_os_BinderInternal

const char* const kBinderInternalPathName = "com/android/internal/os/BinderInternal";

static int int_register_android_os_BinderInternal(JNIEnv* env)
{
    jclass clazz = FindClassOrDie(env, kBinderInternalPathName);

    gBinderInternalOffsets.mClass = MakeGlobalRefOrDie(env, clazz);
    gBinderInternalOffsets.mForceGc = GetStaticMethodIDOrDie(env, clazz, "forceBinderGc", "()V");
    
    return RegisterMethodsOrDie(
        env, kBinderInternalPathName,
        gBinderInternalMethods, NELEM(gBinderInternalMethods));

}
  1. 通過kBinderInternalPathName查找檔案,回傳class物件

  2. 通過gBinderInternalOffsets結構體,保存java層binder類的資訊,為JNI層訪問Java層提供通道

  3. 通過RegisterMethodsOrDie,為gBinderInternalMethods陣列完成映射關系,從而為Java層訪問JNI層提供通道

3. int_register_android_os_BinderProxy

const char* const kBinderProxyPathName = "android/os/BinderProxy";

static int int_register_android_os_BinderProxy(JNIEnv* env)
{
    jclass clazz = FindClassOrDie(env, "java/lang/Error");
    gErrorOffsets.mClass = MakeGlobalRefOrDie(env, clazz);

    clazz = FindClassOrDie(env, kBinderProxyPathName);
    gBinderProxyOffsets.mClass = MakeGlobalRefOrDie(env, clazz);
    gBinderProxyOffsets.mConstructor = GetMethodIDOrDie(env, clazz, "<init>", "()V");
    gBinderProxyOffsets.mSendDeathNotice = GetStaticMethodIDOrDie(env, clazz, "sendDeathNotice",
            "(Landroid/os/IBinder$DeathRecipient;)V");
    
    gBinderProxyOffsets.mObject = GetFieldIDOrDie(env, clazz, "mObject", "J");
    gBinderProxyOffsets.mSelf = GetFieldIDOrDie(env, clazz, "mSelf",
                                                "Ljava/lang/ref/WeakReference;");
    gBinderProxyOffsets.mOrgue = GetFieldIDOrDie(env, clazz, "mOrgue", "J");
    
    clazz = FindClassOrDie(env, "java/lang/Class");
    gClassOffsets.mGetName = GetMethodIDOrDie(env, clazz, "getName", "()Ljava/lang/String;");
    
    return RegisterMethodsOrDie(
        env, kBinderProxyPathName,
        gBinderProxyMethods, NELEM(gBinderProxyMethods));

}
  1. 通過kBinderProxyPathName查找檔案,回傳class物件

  2. 通過gBinderProxyOffsets結構體,保存Java層binder類的資訊,為JNI層訪問java層提供通道,

  3. 通過RegisterMethodsOrDie,為gBinderProxyMethods陣列完成映射關系,從而為java層訪問JNI層提供通道

Native層

ServiceManager服務的注冊

service_manager就是一個大管家,負責管理各種服務,包括系統服務(AMS、PMS等),service_manage的handle = 0,啟動servicemanager通過決議init.rc,進入service_manager.c的main方法

int main(int argc, char **argv)
{
    struct binder_state *bs;

    // 打開binder驅動,申請128KB位元組大小的記憶體空間,進行記憶體映射
    bs = binder_open(128*1024);
    if (!bs) {
        ALOGE("failed to open binder driver\n");
        return -1;
    }
    
    // 設定servicemanager為binder大管家
    if (binder_become_context_manager(bs)) {
        ALOGE("cannot become context manager (%s)\n", strerror(errno));
        return -1;
    }
    
  	...
    
    // 進行無限回圈,處理client發來的請求
    binder_loop(bs, svcmgr_handler);
    
    return 0;

}

上面的main方法 我們可以看到,SM的注冊主要分為三個步驟:

  1. 打開驅動 記憶體映射,設定記憶體大小為128kb (ServiceManager服務的大小)

  2. 設定servicemanager為大管家

  3. 開啟監聽 不斷輪詢

首先,先看下他是如何將servicemanager設定為大管家的,進入代碼:

int binder_become_context_manager(struct binder_state *bs)
{
    return ioctl(bs->fd, BINDER_SET_CONTEXT_MGR, 0);
}

static int binder_ioctl_set_ctx_mgr(struct file *filp)
{
	...
        
    // 創建SM物體  node
	context->binder_context_mgr_node = binder_new_node(proc, 0, 0);
	
    ...
        
	return ret;
}

static struct binder_node *binder_new_node(struct binder_proc *proc,
					   binder_uintptr_t ptr,
					   binder_uintptr_t cookie)
{
	struct rb_node **p = &proc->nodes.rb_node;
	struct rb_node *parent = NULL;
	struct binder_node *node;

	while (*p) {
		parent = *p;
		node = rb_entry(parent, struct binder_node, rb_node);
	
		if (ptr < node->ptr)
			p = &(*p)->rb_left;
		else if (ptr > node->ptr)
			p = &(*p)->rb_right;
		else
			return NULL;
	}
	
    // 申請記憶體 創建node物件
	node = kzalloc(sizeof(*node), GFP_KERNEL);
	if (node == NULL)
		return NULL;
	binder_stats_created(BINDER_STAT_NODE);
	rb_link_node(&node->rb_node, parent, p);
	rb_insert_color(&node->rb_node, &proc->nodes);
	node->debug_id = ++binder_last_id;
	node->proc = proc;
	node->ptr = ptr;
	node->cookie = cookie;
	node->work.type = BINDER_WORK_NODE;
	INIT_LIST_HEAD(&node->work.entry);
	INIT_LIST_HEAD(&node->async_todo);
	binder_debug(BINDER_DEBUG_INTERNAL_REFS,
		     "%d:%d node %d u%016llx c%016llx created\n",
		     proc->pid, current->pid, node->debug_id,
		     (u64)node->ptr, (u64)node->cookie);
	return node;

}
  1. 創建binder_node結構體物件

  2. proc -> binder_node

  3. 創建 work 和 todo ====》類似 messageQueue

其次,看SM如何進入loop等待狀態:這里很關鍵的一個命令:BC_ENTER_LOOPE命令

void binder_loop(struct binder_state *bs, binder_handler func)
{
    int res;
    struct binder_write_read bwr;
    uint32_t readbuf[32];

    // 1.將bwr結構體初始化為0
    bwr.write_size = 0;
    bwr.write_consumed = 0;
    bwr.write_buffer = 0;
    
    readbuf[0] = BC_ENTER_LOOPER;
    // 2.設定執行緒的狀態為loop狀態
    binder_write(bs, readbuf, sizeof(uint32_t));
    
    for (;;) {
        // 3. read_size 不為0, 進入binder_thread_read
        bwr.read_size = sizeof(readbuf);
        bwr.read_consumed = 0;
        bwr.read_buffer = (uintptr_t) readbuf;
        
        /* 不斷地 binder讀資料,沒有資料會進入休眠狀態 */
        res = ioctl(bs->fd, BINDER_WRITE_READ, &bwr);
    
        if (res < 0) {
            ALOGE("binder_loop: ioctl failed (%s)\n", strerror(errno));
            break;
        }
    
        res = binder_parse(bs, 0, (uintptr_t) readbuf, bwr.read_consumed, func);
        if (res == 0) {
            ALOGE("binder_loop: unexpected reply?!\n");
            break;
        }
        if (res < 0) {
            ALOGE("binder_loop: io error %d %s\n", res, strerror(errno));
            break;
        }
    }

}

void binder_loop(struct binder_state *bs, binder_handler func)
{
    int res;
    struct binder_write_read bwr;
    uint32_t readbuf[32];

    // 1.將bwr結構體初始化為0
    bwr.write_size = 0;
    bwr.write_consumed = 0;
    bwr.write_buffer = 0;
    
    readbuf[0] = BC_ENTER_LOOPER;
    // 2.設定執行緒的狀態為loop狀態
    binder_write(bs, readbuf, sizeof(uint32_t));
    
    for (;;) {
        // 3. read_size 不為0, 進入binder_thread_read
        bwr.read_size = sizeof(readbuf);
        bwr.read_consumed = 0;
        bwr.read_buffer = (uintptr_t) readbuf;
        
        /* 不斷地 binder讀資料,沒有資料會進入休眠狀態 */
        res = ioctl(bs->fd, BINDER_WRITE_READ, &bwr);
    
        if (res < 0) {
            ALOGE("binder_loop: ioctl failed (%s)\n", strerror(errno));
            break;
        }
    
        res = binder_parse(bs, 0, (uintptr_t) readbuf, bwr.read_consumed, func);
        if (res == 0) {
            ALOGE("binder_loop: unexpected reply?!\n");
            break;
        }
        if (res < 0) {
            ALOGE("binder_loop: io error %d %s\n", res, strerror(errno));
            break;
        }
    }

}
  • 進入kernel層的binder.c, 通過BINDER_WRITE_READ命令,呼叫binder_ioctl_write_read函式, 前面我們判斷了write_buffer是有值的,所以執行binder_thread_write,

binder_thread_write

static int binder_thread_write(struct binder_proc *proc,
			struct binder_thread *thread,
			binder_uintptr_t binder_buffer, size_t size,
			binder_size_t *consumed)
{
	uint32_t cmd;
	struct binder_context *context = proc->context;
	void __user *buffer = (void __user *)(uintptr_t)binder_buffer;
	void __user *ptr = buffer + *consumed;
	void __user *end = buffer + size;

while (ptr < end && thread->return_error == BR_OK) {
    /* 獲取命令,即 BC_ENTER_LOOPER */
	if (get_user(cmd, (uint32_t __user *)ptr))  
		return -EFAULT;
	...
        
	switch (cmd) {
	 
          ...
              
           case BC_ENTER_LOOPER:
			binder_debug(BINDER_DEBUG_THREADS,
				     "%d:%d BC_ENTER_LOOPER\n",
				     proc->pid, thread->pid);
			if (thread->looper & BINDER_LOOPER_STATE_REGISTERED) {
				thread->looper |= BINDER_LOOPER_STATE_INVALID;
				binder_user_error("%d:%d ERROR: BC_ENTER_LOOPER called after BC_REGISTER_LOOPER\n",
					proc->pid, thread->pid);
			}
            /* 設定執行緒loop狀態為回圈狀態 */
			thread->looper |= BINDER_LOOPER_STATE_ENTERED;
			break;
            
            ...
    }
}
  • 此時,第一步已經完成,就是寫入了執行緒的loop狀態,再回到binder_loop函式中,進入for回圈,將read_size賦值了,read_size 不為0, 進入binder_thread_read,再看這個函式
static int binder_thread_read(struct binder_proc *proc,
			      struct binder_thread *thread,
			      binder_uintptr_t binder_buffer, size_t size,
			      binder_size_t *consumed, int non_block)
{
    ...
        
    if (*consumed == 0) {
        /* 設定命令為BR_NOOP */
		if (put_user(BR_NOOP, (uint32_t __user *)ptr))
			return -EFAULT;
		ptr += sizeof(uint32_t);
	}
    
    retry:
    /* wait_for_proc_work為true */
	wait_for_proc_work = thread->transaction_stack == NULL &&
				list_empty(&thread->todo);
    
    ...
        /* 準備就緒的執行緒數 + 1 */
        if (wait_for_proc_work)
		proc->ready_threads++;

    ...
        if (non_block) {  /* 非阻塞操作,servicemanager是阻塞的,所以進入else */
			if (!binder_has_proc_work(proc, thread))
				ret = -EAGAIN;
		} else
			ret = wait_event_freezable_exclusive(proc->wait, binder_has_proc_work(proc, thread));
} 

總結大致做了以下作業:

  1. 寫入狀態Loop

  2. 去讀資料:binder_thread_read:ret = wait_event_freezable_exclusive(proc->wait, binder_has_proc_work(proc, thread)); 進入等待

此時,servicemanager已經注冊完成,準備就緒了,

整體流程圖如下:

ServiceManager服務的獲取

獲取ServiceManager是通過defaultServiceManager()方法來完成的,

sp<IServiceManager> defaultServiceManager()
{
    /* 單例模式,如果不為空,直接回傳 */
    if (gDefaultServiceManager != NULL) return gDefaultServiceManager;
    

    {
        AutoMutex _l(gDefaultServiceManagerLock);
        while (gDefaultServiceManager == NULL) {
            
            /* 這里分三步走 */
            gDefaultServiceManager = interface_cast<IServiceManager>(
                ProcessState::self()->getContextObject(NULL));
            if (gDefaultServiceManager == NULL)
                sleep(1);
        }
    }
    
    return gDefaultServiceManager;

}
1. ProcessState::self()
sp<ProcessState> ProcessState::self()
{
    Mutex::Autolock _l(gProcessMutex);
    /* 單例模式 */
    if (gProcess != NULL) {
        return gProcess;
    }
    gProcess = new ProcessState;
    return gProcess;
}

這一步很簡單,就是實體化ProcessState物件,

    : mDriverFD(open_driver())
    , mVMStart(MAP_FAILED)
    , mThreadCountLock(PTHREAD_MUTEX_INITIALIZER)
    , mThreadCountDecrement(PTHREAD_COND_INITIALIZER)
    , mExecutingThreadsCount(0)
    , mMaxThreads(DEFAULT_MAX_BINDER_THREADS)
    , mManagesContexts(false)
    , mBinderContextCheckFunc(NULL)
    , mBinderContextUserData(NULL)
    , mThreadPoolStarted(false)
    , mThreadPoolSeq(1)
{
    if (mDriverFD >= 0) {
        // XXX Ideally, there should be a specific define for whether we
        // have mmap (or whether we could possibly have the kernel module
        // availabla).
#if !defined(HAVE_WIN32_IPC)
        // mmap the binder, providing a chunk of virtual address space to receive transactions.
        mVMStart = mmap(0, BINDER_VM_SIZE, PROT_READ, MAP_PRIVATE | MAP_NORESERVE, mDriverFD, 0);
        if (mVMStart == MAP_FAILED) {
            // *sigh*
            ALOGE("Using /dev/binder failed: unable to mmap transaction memory.\n");
            close(mDriverFD);
            mDriverFD = -1;
        }
#else
        mDriverFD = -1;
#endif
    }

    LOG_ALWAYS_FATAL_IF(mDriverFD < 0, "Binder driver could not be opened.  Terminating.");
}

可以看到,初始化物件時,首先呼叫open_driver打開驅動,其次通過mmap記憶體映射,給 binder分配一塊大小為 **(1M-8K)**的虛擬地址空間,用來接收事務,進入open_driver看下:

static int open_driver()
{
    int fd = open("/dev/binder", O_RDWR);
    if (fd >= 0) {
        fcntl(fd, F_SETFD, FD_CLOEXEC);
        int vers = 0;
        status_t result = ioctl(fd, BINDER_VERSION, &vers);
        if (result == -1) {
            ALOGE("Binder ioctl to obtain version failed: %s", strerror(errno));
            close(fd);
            fd = -1;
        }
        if (result != 0 || vers != BINDER_CURRENT_PROTOCOL_VERSION) {
            ALOGE("Binder driver protocol does not match user space protocol!");
            close(fd);
            fd = -1;
        }
        size_t maxThreads = DEFAULT_MAX_BINDER_THREADS; /* 15 */
        result = ioctl(fd, BINDER_SET_MAX_THREADS, &maxThreads);
        if (result == -1) {
            ALOGE("Binder ioctl to set max threads failed: %s", strerror(errno));
        }
    } else {
        ALOGW("Opening '/dev/binder' failed: %s\n", strerror(errno));
    }
    return fd;
}

呼叫open打開 /dev/binder設備,建立與內核的 Binder驅動的互動通道,通過ioctl設定binder驅動,能支持的最大執行緒數為15個,

2. getContextObject(NULL)

通過gProcess物件,呼叫getContextObject(NULL)函式,而這個函式做什么事情,

sp<IBinder> ProcessState::getContextObject(const sp<IBinder>& /*caller*/)
{
    return getStrongProxyForHandle(0);
}

回傳一個getStrongProxyForHandle函式,引數為0,獲取service_manager服務,看下這個函式:

{
    sp<IBinder> result;

    AutoMutex _l(mLock);

    /* 查找handle對應的資源項 */
    handle_entry* e = lookupHandleLocked(handle);

    /* e不為空 進入if */
    if (e != NULL) {
       
        IBinder* b = e->binder;
        if (b == NULL || !e->refs->attemptIncWeak(this)) {
            if (handle == 0) {
              
                Parcel data;
                /* 通過ping操作測驗binder是否準備就緒 */
                status_t status = IPCThreadState::self()->transact(
                        0, IBinder::PING_TRANSACTION, data, NULL, 0);
                if (status == DEAD_OBJECT)
                   return NULL;
            }

            /* 創建BpBinder物件 */
            b = new BpBinder(handle); 
            e->binder = b;
            if (b) e->refs = b->getWeakRefs();
            result = b;
        } else {
           
            result.force_set(b);
            e->refs->decWeak(this);
        }
    }

    return result;
}

主要的任務就是創建了BpBinder物件,(客戶端)

3. interface_cast< IServiceManager >()
template<typename INTERFACE>
inline sp<INTERFACE> interface_cast(const sp<IBinder>& obj)
{
    return INTERFACE::asInterface(obj);
}

可以看到,這是一個模板函式,就相當于java里的泛型,我們指定泛型型別為IServiceManager

#define IMPLEMENT_META_INTERFACE(INTERFACE, NAME)                       \
    const android::String16 I##INTERFACE::descriptor(NAME);             \
    const android::String16&                                            \
            I##INTERFACE::getInterfaceDescriptor() const {              \
        return I##INTERFACE::descriptor;                                \
    }                                                                   \
    android::sp<I##INTERFACE> I##INTERFACE::asInterface(                \
            const android::sp<android::IBinder>& obj)                   \
    {                                                                   \
        android::sp<I##INTERFACE> intr;                                 \
        if (obj != NULL) {                                              \
            intr = static_cast<I##INTERFACE*>(                          \
                obj->queryLocalInterface(                               \
                        I##INTERFACE::descriptor).get());               \
            if (intr == NULL) {                                         \
                intr = new Bp##INTERFACE(obj);                          \
            }                                                           \
        }                                                               \
        return intr;                                                    \
    }                                                                   \
    I##INTERFACE::I##INTERFACE() { }                                    \
    I##INTERFACE::~I##INTERFACE() { }    

展開即可得:

const android::String16
IServiceManager::descriptor(“android.os.IServiceManager”);
const android::String16& IServiceManager::getInterfaceDescriptor() const
{
  return IServiceManager::descriptor;
}
android::sp<IServiceManager> IServiceManager::asInterface(const
android::sp<android::IBinder>& obj)
{
   android::sp<IServiceManager> intr;
   if(obj != NULL) {
     intr = static_cast<IServiceManager *>(
       obj->queryLocalInterface(IServiceManager::descriptor).get());
     if (intr == NULL) {
       // 等價于 new BpServiceManager(BpBinder)
       intr = new BpServiceManager(obj);
     }
   }
   return intr;
}
IServiceManager::IServiceManager () { }
IServiceManager::~ IServiceManager() { }

從展開得函式中看到,這一步主要作業就是new BpServiceManager(BpBinder),

真正通過remote.transcat 遠程呼叫,而remote等價BpBinder,

Java層

java層的服務注冊是從SystemServer開始的,

服務的注冊和獲取 以AMS為例

AMS的注冊

1. getIServiceManager()
private static IServiceManager getIServiceManager() {
        if (sServiceManager != null) {
            return sServiceManager;
        }

        // Find the service manager
        sServiceManager = ServiceManagerNative.asInterface(BinderInternal.getContextObject());
        return sServiceManager;
    }

這里又分為兩步:

  1. BinderInternal.getContextObject()

    進入native方法:

static jobject android_os_BinderInternal_getContextObject(JNIEnv* env, jobject clazz)
{
    sp<IBinder> b = ProcessState::self()->getContextObject(NULL);
    return javaObjectForIBinder(env, b);  /*  回傳BinderProxy物件 */
}
1.  ProcessState::self()->getContextObject(NULL): 創建一個BpBinder

2.  javaObjectForIBinder(env, b):系結BinderProxy和BpBinder
  1. ServiceManagerNative.asInterface
 static public IServiceManager asInterface(IBinder obj)
    {
        if (obj == null) {
            return null;
        }
        IServiceManager in =
            (IServiceManager)obj.queryLocalInterface(descriptor);
        if (in != null) {
            return in;
        }
        
        return new ServiceManagerProxy(obj);
    }

這里,我們得知obj就是BinderProxy,所以看下BinderProxy的queryLocalInterface方法

 public IInterface queryLocalInterface(String descriptor) {
        return null;
    }

這個方法回傳一個null,所以直接new ServiceManagerProxy(BinderProxy),

總結: getIServiceManager() 最侄訓傳:new ServiceManagerProxy(BinderProxy),

2. addService(name, service, false)
public void addService(String name, IBinder service, boolean allowIsolated)
    throws RemoteException {
    Parcel data = Parcel.obtain();
    Parcel reply = Parcel.obtain();
    data.writeInterfaceToken(IServiceManager.descriptor);
    data.writeString(name);
    data.writeStrongBinder(service);
    data.writeInt(allowIsolated ? 1 : 0);
    mRemote.transact(ADD_SERVICE_TRANSACTION, data, reply, 0);
    reply.recycle();
    data.recycle();
}
  1. data.writeStrongBinder(service); service == AMS 將AMS放入data中,

  2. mRemote.transact mRemote == BinderProxy

  3. 獲取BpBinder ---- IPCThreadState::transact

    1. writeTransactionData 往out中寫入BC_TRANSACTION命令,在write里處理BC_TRANSACTION命令

    2. waitForResponse

      1. talkWithDriver — 非常重要

        • binder_transaction

          1. handle == 0 ----> SM

          2. 獲取target_node

          3. 獲取proc物件

          4. 獲取todo 和 wait

          5. 創建t,tcomplete

          6. 資料拷貝**(真正一次拷貝的地方)**

          7. binder_transaction_binder -----> handle

          8. thread -> transaction_stack = t; -----> 方便SM找到客戶端

          9. t->work.type = BINDER_WORK_TRANSACTION; 給SM做事

          10. tcomplete->type = BINDER_WORK_TRANSACTION_COMPLETE; 給客戶端掛起

          11. wake_up_interruptible(target_wait); 喚醒SM

      2. client掛起

        1. 處理BR_NOOP、BR_TRANSACTION_COMPLETE命令

        2. wait_event_freezable ----- 掛起

      3. sm處理添加服務

        1. BINDER_WORK_TRANSACTION,要處理的 cmd == BR_TRANSACTION

        2. reply初始化

        3. res = func(bs, txn, &msg, &reply); — 函式指標,指向svcmgr_handler

          作用:獲取或者添加 service

          1. sm 是用 svclist 保存所有服務的
        4. binder_send_reply — BC_REPLY

        5. t->work.type = BINDER_WORK_TRANSACTION; — 給Client list_add_tail(&t->work.entry, target_list); tcomplete->type = BINDER_WORK_TRANSACTION_COMPLETE; – 給SM — 被掛起 list_add_tail(&tcomplete->entry, &thread->todo);

        6. wake_up_interruptible(target_wait); – 喚醒 Client

      4. client 被喚醒

附上一張 addService 的流程圖:

3. SM 處理 onTransact

IPCThreadState::executeCommand

  • error = reinterpret_cast<BBinder*>(tr.cookie)->transact(tr.code, buffer, &reply, tr.flags);

  • JavaBBinder.onTransact — C++

  • jboolean res = env->CallBooleanMethod(mObject, gBinderOffsets.mExecTransact, code, reinterpret_cast(&data), reinterpret_cast(reply), flags); – Binder.java.execTransact 方法

至此,binder的整理流程就分析完了,說實話,邏輯很復雜,可能第一次看完 一頭霧水,多看幾次,把整體流程理清楚就會明白很多,

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/297332.html

標籤:其他

上一篇:初識 Jetpack Compose(一) :Hello,Jetpack Compose!

下一篇:入職百度了!5年Android開發的求職之路,分享一波面試心得

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:40:31 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:40:11 more
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:39:36 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:39:13 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:16:23 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:16:15 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:15:46 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:14:53 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:14:08 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:08:34 more