当前位置: 首页 > news >正文

Android Framework-进程间通信——Binder

我们知道,同一个程序中的两个函数之间能直接调用的根本原因是处于相同的内存空间中。
比如有以下两个函数A和B:

/*Simple.c*/
void A()
{ B(); }
void B()
{ }

因为是在一个内存空间中,虚拟地址的映射规则完全一致,所以函数A和B之间的调用关系很简单,如图所示。
在这里插入图片描述
两个不同的进程,如某Application1和ActivityManagerService(所在的进程),它们是没有办法直接通过内存地址来访问到对方内部的函数或者变量的,如图所示。
在这里插入图片描述
既然无法“直接”访问到对方进程的内存空间,那有没有“间接”的方法呢?简而言之,这就是Binder所要做的工作,如图6-3所示。
在这里插入图片描述
Binder是Android中使用最广泛的IPC机制。Binder包括:
Binder驱动;
Service Manager;
Binder Client;
Binder Server。
如果统观Binder中的各个组成元素,就会惊奇地发现它和TCP/IP网络有很多相似之处:
Binder驱动→路由器;
Service Manager→DNS;
Binder Client→客户端;
Binder Server→服务器。
TCP/IP中一个典型的服务连接过程(比如客户端通过浏览器访问Google主页)如图6-4所示。
在这里插入图片描述
Client向DNS查询Google.com的IP地址。DNS将查询结果返回Client。Client发起连接。Client在得到Google.com的IP地址后,就可以据此来向Google服务器发起连接了。Router是整个通信结构中的基础,它所担负的责任是将数据包投递到用户设定的目标IP中。
首先,在TCP/IP参考模型中,对于IP层及以上的用户来说,IP地址是他们彼此沟通的凭证——任何用户在整个互联网中的IP标志符都是唯一的。
其次,Router是构建一个通信网络的基础,它可以根据用户填写的目标IP正确地把数据包发送到位。
最后,DNS角色并不是必需的,它的出现是为了帮助人们使复杂难记的IP地址与可读性更强的域名建立关联,并提供查询功能。而客户端能使用DNS的前提是它已经正确配置了DNS服务器的IP地址。
Binder的原型如图6-5所示
在这里插入图片描述
Binder的本质目标用一句话来描述,就是进程1(客户端)希望与进程2(服务器)进行互访。但因为它们之间是跨进程(跨网络)的,所以必须借助于Binder驱动(路由器)来把请求正确投递到对方所在进程(网络)中。而参与通信的进程们需要持有Binder“颁发”的唯一标志(IP地址),如图6-6所示。
在这里插入图片描述
和TCP/IP网络类似,Binder中的“DNS”也并不是必需的——前提是客户端能记住它要访问的进程的Binder标志(IP地址);而且要特别注意这个标志是“动态IP”,这就意味着即使客户端记住了本次通信过程中目标进程的唯一标志,下一次访问仍然需要重新获取,这无疑加大了客户端的难度。“DNS”的出现可以完美地解决这个问题,用于管理Binder标志与可读性更强的“域名”间的对应关系,并向用户提供查询功能。
在Binder机制中,DNS的角色就是Service Manager
在这里插入图片描述
既然Service Manager是DNS,那么它的“IP地址”是什么呢?Binder机制对此做了特别规定。Service Manager在Binder通信过程中的唯一标志永远都是0。

智能指针

智能指针在整个Android工程中使用很广泛,特别是在Binder的源码实现中更可谓“比比皆是”。
1.1智能指针的设计理念
Java和C/C++的一个重大区别,就是它没有“指针”的概念。这并不代表Java不需要使用指针,而是这个“超级武器”被隐藏了起来。
C/C++项目中常见的指针问题可以归纳为:
指针没有初始化,new了对象后没有及时delete,野指针
Android中的智能指针实现,包括强指针和弱指针两种:
1.2 强指针sp
frameworks/native/include/utils/StrongPointer.h

template <typename T>
class sp
{
public:inline sp() : m_ptr(0) { }sp(T* other);/*常用构造函数*//*其他构造函数*/sp();/*析构函数*/sp& operator = (T* other);// 重载运算符“=”…inline T& operator* () const { return *m_ptr; }// 重载运算符“*”inline T* operator-> () const { return m_ptr; }// 重载运算符“->”inline T* get() const { return m_ptr; }private:template<typename Y> friend class sp;template<typename Y> friend class wp;void set_pointer(T* ptr);T* m_ptr;
};

运算符等号的实现为:

template<typename T>
sp<T>& sp<T>::operator = (T* other)
{if (other) other->incStrong(this);/*增加引用计数*/if (m_ptr) m_ptr->decStrong(this);m_ptr = other;return *this;
}

析构函数

template<typename T>
sp<T>::sp()
{if (m_ptr) m_ptr->decStrong(this);/*减小引用计数*/
}

1.3 弱指针wp
有个场景,父对象parent指向子对象child,然
后子对象又指向父对象,这就存在了循环引用的现象。

struct CDad 
{ CChild *myChild;
}; 
struct CChild 
{CDad *myDad;
};

假设这两个类都具有引用计数器的功能。
因为CDad指向了CChild,所以后者的引用计数器不为零。
而CChild又指向了CDad,同样也会使它的计数器不为零。
内存回收者发现两者都是处于“被需要”的状态,当然不能释放,从而形成了恶性循环。
解决这个矛盾的一种有效方法是采用“弱引用”。具体措施如下。
CDad使用强指针来引用CChild,而CChild只使用弱引用来指向父类。双方规定当强引用计数为0时,不论弱引用是否为0都可以delete自己(在Android系统中这个规则是可以调整的)。这样只要有一方得到了释放,就可以成功避免死锁。会不会导致野指针的问题?没错,的确会有这方面的顾虑。比如CDad因为强指针计数已经到0,根据规则生命周期就结束了;但此时CChild还持有其父类的弱引用,显然如果CChild此时用这个指针来访问CDad将引发致命的问题。鉴于此,我们还要特别规定:
弱指针必须先升级为强指针,才能访问它所指向的目标对象。

template <typename T>
class wp
{
public:typedef typename RefBase::weakref_type weakref_type;inline wp() : m_ptr(0) { }wp(T* other);//构造函数/*其他构造函数省略*/wp(); wp& operator = (T* other);//运算符重载void set_object_and_refs(T* other, weakref_type* refs);sp<T> promote() const;/*升级为强指针*//*其他方法省略*/
private:template<typename Y> friend class sp;template<typename Y> friend class wp;T* m_ptr;weakref_type* m_refs;
};

总结
1.智能指针分为强指针sp和弱指针wp两种。
2.通常情况下目标对象的父类是RefBase——这个基类提供了一个weakref_impl类型的引用计数器,可以同时进行强弱引用的控制(内部由mStrong和mWeak提供计数)。
3.当incStrong增加强引用的,也会增加弱引用。
4.当incWeak时只增加弱引用计数。
5.使用者可以通过extendObjectLifetime设置引用计数器的规则,不同规则下对删除目标对象的时机判断也是不一样的。
6.使用者可以根据程序需求来选择合适的智能指针类型和计数器规则

进程间的数据传递载体——Parcel

进程间的数据传递,如果只是一个int型数值,不断复制直到目标进程即可。但如果是某个对象呢?我们可以想象下,同一进程间的对象传递都是通过引用来做的,因而本质上就是传递了一个内存地址。这种方式在跨进程的情况下就无能为力了。由于采用了虚拟内存机制,两个进程都有自己独立的内存地址空间,所以跨进程传递的地址值是无效的。
进程间的数据传递是Binder机制中的重要一环,Android系统中担负这一重任的就是Parcel。Parcel是一种数据的载体,用于承载希望通过IBinder发送的相关信息(包括数据和对象引用)。如果把对象在进程A中占据的内存相关数据打包起来,然后寄送到进程B中,由B在自己的进程空间中“复现”这个对象,是否可行呢?Parcel就具备这种打包和重组的能力。
1.Parcel设置相关
存入的数据越多,Parcel所占内存空间也就越大。我们可以通过以下方法来进行相关设置。
dataSize():获取当前已经存储的数据大小。

setDataCapacity (int size):设置Parcel的空间大小,显然存储的数据不能大于这个值。

setDataPosition (int pos):改变Parcel中的读写位置,必须介于0和dataSize()间。

dataAvail():当前Parcel中的可读数据大小。

dataCapacity():当前Parcel的存储能力。

dataPosition():数据的当前位置值,有点类似于游标。

dataSize():当前Parcel所包含的数据大小。
2.Primitives
原始类型数据的读写操作。比如:
writeByte(byte):写入一个byte。

readByte():读取一个byte。

writeDouble(double):写入一个double。

readDouble():读取一个double。
3.Primitive Arrays
原始数据类型数组的读写操作通常是先写入用4个字节表示的数据大小值,接着才写入数据本身。另外,用户既可以选择将数据读入现有的数组空间中,也可以让Parcel返回一个新的数组。此类方法如下
writeBooleanArray(boolean[]):写入布尔数组。

readBooleanArray(boolean[]):读取布尔数组。

boolean[]createBooleanArray():读取并返回一个布尔数组。

writeByteArray(byte[]):写入字节数组。

writeByteArray(byte[], int, int):和上面几个不同的是,这个函数最后面的两个参数分别表示数组中需要被写入的数据起点以及需要写入多少。

readByteArray(byte[]):读取字节数组。

byte[]createByteArray():读取并返回一个数组。
如果写入数据时系统发现已经超出了Parcel的存储能力,它会自动申请所需的内存空间,并扩展dataCapacity;而且每次写入都是从dataPosition()开始的。
4.Parcelables
遵循Parcelable协议的对象可以通过Parcel来存取,如开发人员经常用到的bundle就是继承自Parcelable的。与这类对象相关的Parcel操作包括:
writeParcelable(Parcelable, int):将这个Parcelable类的名字和内容写入Parcel中,实际上它是通过回调此Parcelable的writeToParcel()方法来写入数据的。

readParcelable(ClassLoader):读取并返回一个新的Parcelable对象。

writeParcelableArray(T[], int):写入Parcelable对象数组。

readParcelableArray(ClassLoader):读取并返回一个Parcelable对象数组
5.Bundles
上面已提到,Bundle继承自Parcelable,是一种特殊的type-safe的容器。Bundle的最大特点就是采用键值对的方式存储数据,并在一定程度上优化了读取效率。这个类型的Parcel操作包括:
writeBundel(Bundle):将Bundle写入parcel。

readBundle():读取并返回一个新的Bundle对象。

readBundle(ClassLoader):读取并返回一个新的Bundle对象,ClassLoader用于Bundle获取对应的Parcelable对象。
6.Active Objects
Parcel的另一个强大武器就是可以读写Active Objects。什么是Active Objects呢?通常我们存入Parcel的是对象的内容,而Active Objects写入的则是它们的特殊标志引用。所以在从Parcel中读取这些对象时,大家看到的并不是重新创建的对象实例,而是原来那个被写入的实例。可以猜想到,能够以这种方式传输的对象不会很多,目前主要有两类。
(1)Binder。Binder一方面是Android系统IPC通信的核心机制之一,另一方面也是一个对象。利用Parcel将Binder对象写入,读取时就能得到原始的Binder对象,或者是它的特殊代理实现(最终操作的
还是原始Binder对象)。与此相关的操作包括:
writeStrongBinder(IBinder)
writeStrongInterface(IInterface)
readStrongBinder()

(2)FileDescriptor。FileDescriptor是Linux 中的文件描述符,可以通过Parcel的如下方法进行传递。
writeFileDescriptor(FileDescriptor), readFileDescriptor()
因为传递后的对象仍然会基于和原对象相同的文件流进行操作,因而可以认为是Active Object的一种。
7.Untyped Containers
它是用于读写标准的任意类型的java容器。包括:
writeArray(Object[]), readArray(ClassLoader), writeList(List),readList(List, ClassLoader)等

Parcel所支持的类型很多,足以满足开发者的数据传递请求。如果要给Parcel找个类比的话,它更像集装箱。理由如下:
1.货物无关性
即它并不排斥所运输的货物种类,电子产品可以,汽车也行,或者零部件也同样接受。
2.不同货物需要不同的打包和卸货方案
比如运载易碎物品和坚硬物品的装箱卸货方式就肯定会有很大不同。
3.远程运输和组装
集装箱的货物通常是要跨洋运输的,有点类似于Parcel的跨进程能力。不过集装箱运输公司本身并不负责所运送货物的后期组装。举个例子,汽车厂商需要把一辆整车先拆卸成零部件后才能进行装货运输;到达目的地后,货运公司只需要把货物完整无缺地交由接收方即可,并不负有组装成整车的义务。而Parcel则更加敬业,它会依据协议(打包和重组所用的协议必须是配套的)来为接收方提供完整还原
出原始数据对象的业务。
writeString的实现原理
结合一个范例来详细讲解writeString的实现原理,范例是ServiceManagerProxy的getService()方法中对Parcel的操作

Parcel data = Parcel.obtain();
…
data.writeInterfaceToken(IServiceManager.descriptor);
data.writeString(name);

第一句代码用于获得一个Parcel对象,最终是创建了一个本地的Parcel实例,并做了全面的初始化操作。

第二句中的writeInterfaceToken用于写入IBinder接口标志,所带参数是String类型的,如IServiceManager.descriptor =“android.os.IServiceManager”。

第三句通过writeString在Parcel中写入需要向ServiceManager查询的Service名称

Parcel在整个IPC中的内部传递过程比较烦琐,特别在承载Binder数据时更是需要多次转换,因而容易让人失去方向。但不管过程如何曲折,有一点是始终不变的。那就是:写入方和读取方所使用的协议必须是完全一致的。

来看看写入方(ServiceManagerProxy)都“装”了些什么东西到“集装箱”中:

status_t Parcel::writeInterfaceToken(const String16& interface)
{writeInt32(IPCThreadState::self()->getStrictModePolicy()
|STRICT_MODE_PENALTY_GATHER);return writeString16(interface);
}

等价于:
writeInterfaceToken→writeInt32(policyvalue)+writeString16(interface)

其中interface就是"android.os.IServiceManager"。

status_t Parcel::writeInt32(int32_t val)
{return writeAligned(val);
}

这个函数的实现很简单——只包含了一句代码。从函数名来判断,它是将val值按照对齐方式写入Parcel的存储空间中。换句话说,就是将数据写入mDataPos起始的mData中(当然,内部还需要判断当前的存储能力是否满足要求、是否要申请新的内存等):

status_t Parcel::writeString16(const String16& str)
{return writeString16(str.string(), str.size());
}
status_t Parcel::writeString16(const char16_t* str, size_t len)
{if (str == NULL) return writeInt32 (-1); //str不为空status_t err = writeInt32(len); //先写入数据长度if (err == NO_ERROR) {len *= sizeof(char16_t); //长度*单位大小=占用的空间uint8_t* data =
(uint8_t*)writeInplace(len+sizeof(char16_t));if (data) {memcpy(data, str, len);/*将数据复制到data所指向的位置中
*/*reinterpret_cast<char16_t*>(data+len) = 0;return NO_ERROR;}err = mError;}return err;
}

整个writeString16的处理过程并不难理解:首先要填写数据的长度,占据4个字节;然后计算出数据所需占据的空间大小;最后才将数据复制到相应位置中——writeInplace就是用来计算复制数据的目标地址的。
写入一个String(writeString16)的步骤:
writeInt32(len);
memcpy;
padding(有些情况下不需padding。而且源码实现中这一步是在memcpy之前)。
回到ServiceManagerProxy中的getService里:

data.writeInterfaceToken(IServiceManager.descriptor);
data.writeString(name);

我们把上面两个语句进行分解,就得到写入方的工作了:
WriteInterfaceToken=writeInt32(policyvalue)+writeString16(interface)
writeString16(interface) = writeInt32(len)+写入数据本身+填充

读取方是Service_manager.c

int svcmgr_handler(struct binder_state *bs, struct binder_txn
*txn,struct binder_io *msg, struct binder_io *reply)
{…uint32_t strict_policy;…strict_policy = bio_get_uint32(msg); //取得policy值s = bio_get_string16(msg, &len); //取得一个String16,即上面写入的interfaceif ((len != (sizeof(svcmgr_id) / 2)) ||memcmp(svcmgr_id, s, sizeof(svcmgr_id))) {/*判断Interface是否正确?*/fprintf(stderr,"invalid id %s\n", str8(s));return -1;}

上面代码段用于判断收到的interface是否正确。其中:
uint16_t svcmgr_id[] = {
‘a’,‘n’,‘d’,‘r’,‘o’,‘i’,‘d’,‘.’,‘o’,‘s’,‘.’,
‘I’,‘S’,‘e’,‘r’,‘v’,‘i’,‘c’,‘e’,‘M’,‘a’,‘n’,‘a’,‘g’,‘e’,‘r’
};
和前面的"android.os.IServiceManager"是一样的:

switch(txn->code) {case SVC_MGR_GET_SERVICE:case SVC_MGR_CHECK_SERVICE:s = bio_get_string16(msg, &len);//获取要查询的service nameptr = do_find_service(bs, s, len, txn->sender_euid);if (!ptr)break;bio_put_ref(reply, ptr);return 0;

可以看到,ServiceManager对数据的读取过程和数据的写入过程确实完全一致。

Binder驱动与协议

Android系统是基于Linux内核的,因而它所依赖的Binder驱动也必须是一个标准的Linux驱动。具体而言,Binder Driver会将自己注册成一个misc device,并向上层提供一个/dev/binder节点——值得一提的是,Binder节点并不对应真实的硬件设备。Binder驱动运行于内核态,可以提供open(),ioctl(),mmap()等常用的文件操作。
Linux中的字符设备通常要经过alloc_chrdev_region(),cdev_init()等一系列操作才能在内核中注册自己。而misc类型驱动则相对简单,只需要调用misc_register()就可轻松解决。比如Binder中与驱动注册相关的代码:

/*drivers/staging/android/Binder.c*/
static struct miscdevice binder_miscdev = {.minor = MISC_DYNAMIC_MINOR, /*动态分配次设备号*/.name = "binder", /*驱动名称*/.fops = &binder_fops /*Binder驱动支持的文件操作*/
};
static int __init binder_init(void)
{ …ret = misc_register(&binder_miscdev); /*驱动注册*/}

Binder驱动还需要填写file_operations结构体。如下所示:

/*drivers/staging/android/Binder.c*/
static const struct file_operations binder_fops = {.owner = THIS_MODULE,.poll = binder_poll,.unlocked_ioctl = binder_ioctl,.mmap = binder_mmap,.open = binder_open,.flush = binder_flush,.release = binder_release,
};

Binder驱动总共为上层应用提供了6个接口——其中使用最多的就是binder_ioctl,binder_mmap和binder_open。
1.1 打开Binder驱动——binder_open

/*如果没有特别说明,以下的函数都在Binder.c中*/
static int binder_open(struct inode *nodp, struct file *filp)
{struct binder_proc *proc;…proc = kzalloc(sizeof(*proc), GFP_KERNEL);/*分配空间*/if (proc == NULL)return -ENOMEM;

Binder驱动已经为用户创建了一个它自己的binder_proc实体,之后用户对Binder设备的操作将以这个对象为基础。
1.2  binder_mmap
mmap()可以把设备指定的内存块直接映射到应用程序的内存空间中。对于Binder驱动来说,上层用户调用的mmap()最终就对应了binder_mmap()。假设有两个进程A和B,其中进程B通过open()和mmap()后与Binder驱动建立了联系,如图6-13所示。
在这里插入图片描述
1.对于应用程序而言,它通过mmap()返回值得到一个内存地址(当然这是虚拟地址),这个地址通过虚拟内存转换(分段、分页)后最终将指向物理内存的某个位置。
2.对于Binder驱动而言,它也有一个指针(binder_proc->buffer)指向某个虚拟内存地址。而经过虚拟内存转换后,它和应用程序中指向的物理内存处于同一个位置。

这时Binder和应用程序就拥有了若干共用的物理内存块。换句话说,它们对各自内存地址的操作,实际上是在同一块内存中执行的。
我们再把进程A加进来
在这里插入图片描述
右半部分Binder驱动通过copy_from_user(),把进程A中的某段数据复制到其binder_proc->buffer所指向的内存空间中。因为binder_proc->buffer在物理内存中的位置和进程B是共享的,因而进程B可以直接访问到这段数据。也就是说,Binder驱动只用了一次复制,就实现了进程A和B间的数据共享。
1.3  binder_ioctl
这是Binder接口函数中工作量最大的一个,它承担了Binder驱动的大部分业务。
在这里插入图片描述
在这里插入图片描述

“DNS”服务器——ServiceManager(Binder Server)

ServiceManager(以下简称SM)的功能可以类比为互联网中的“DNS”服务器,“IP地址”为0。另外,和DNS本身也是服务器一样,SM也是一个标准的Binder Server。
1.1 ServiceManager的启动
既然是DNS,那么在用户可以浏览网页前就必须就位。SM也是同样的道理,它要保证在有人使用Binder机制前就处于正常的工作状态。那么,具体来说它是什么时候运行起来的呢?我们很自然地会想到应该是在init程序解析init.rc时启动的。事实的确如此。如下所示:

/*init.rc*/
service servicemanager /system/bin/servicemanagerclass coreuser systemgroup systemcriticalonrestart restart zygoteonrestart restart mediaonrestart restart surfaceflingeronrestart restart drm

这个servicemanager是用C/C++编写的,源码路径在工程的/frameworks/native/cmds/ servicemanager目录中,可以先来看看它的make文件:

LOCAL_PATH:= $(call my-dir)
…
include $(CLEAR_VARS)
LOCAL_SHARED_LIBRARIES := liblog
LOCAL_SRC_FILES := service_manager.c binder.c 
LOCAL_MODULE := servicemanager /*生成的可执行文件名为
servicemanager*/
include $(BUILD_EXECUTABLE)/*编译可执行文件*/

1.2 ServiceManager的构建
先来看看SM启动后都做了哪些工作:

/*frameworks/native/cmds/servicemanager/Service_manager.c*/
int main(int argc, char **argv)
{struct binder_state *bs;void *svcmgr = BINDER_SERVICE_MANAGER;bs = binder_open(128*1024);if (binder_become_context_manager(bs)) { /*将自己设置为
Binder“大管家”,整个Android 系统只允许一个ServiceManager存在,因而如果后面还有人
调用这个函数就会失败*/ALOGE("canno t become context manager (%s)\n",
strerror(errno));return -1;}svcmgr_handle = svcmgr;binder_loop(bs, svcmgr_handler); //进入循环,等待客户的请求return 0;
}

main函数里主要做了以下几件事:
打开Binder设备,做好初始化;
将自己设置为Binder大管家;
进入主循环。
那么,具体来说需要做哪些初始化呢?

/*frameworks/native/cmds/servicemanager/Binder.c */
struct binder_state *binder_open(unsigned mapsize)
{struct binder_state *bs; /*这个结构体记录了SM中有关于Binder的所有
信息,如fd、map的大小等*/bs = malloc(sizeof(*bs));…bs->fd = open("/dev/binder", O_RDWR); //打开Binder驱动节点…bs->mapsize = mapsize; //mapsize是SM自己设的,为128*1024,即
128Kbs->mapped = mmap(NULL, mapsize, PROT_READ, MAP_PRIVATE, bs-
>fd, 0);return bs;
fail_map:close(bs->fd); //关闭file
fail_open:free(bs);return 0;
}

根据上面代码段中的参数设置可知:
1.由Binder驱动决定被映射到进程空间中的内存起始地址;
2.映射区块大小为128KB;
3.映射区只读;
4.映射区的改变是私有的,不需要保存文件;
5.从文件的起始地址开始映射

下面来看看main函数中的第二步操作,即将servicemanager注册成Binder机制的“大管家”:

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

所有准备工作已经就绪,SM可以开始等待客户端的请求——这一部分工作才是SM的重点和难点。我们先从binder_loop()入手来看看SM是如何处理请求的(分段阅读):

void binder_loop(struct binder_state *bs, binder_handler func)
{int res;struct binder_write_read bwr; /*这是执行BINDER_WRITE_READ命令
所需的数据格式*/unsigned readbuf[32];/*一次读取容量*/

在Binder Server进入循环前,它要先告知Binder驱动这一状态变化。下面这段代码就是为了完成这项工作:

bwr.write_size = 0;//这里只是先初始化为0,下面还会再赋值bwr.write_consumed = 0;bwr.write_buffer = 0; readbuf[0] = BC_ENTER_LOOPER;/*命令*/binder_write(bs, readbuf, sizeof(unsigned));

然后SM就进入了循环。循环体中需要做些什么?
1.从消息队列中读取消息。
2.如果消息是“退出命令”,则马上结束循环;如果消息为空,则继续读取或者等待一段时间后再读取;如果消息不为空且不是退出命令,则根据具体情况进行处理。
3.如此循环往复直到退出。
不过SM中没有消息队列,它的“消息”(或称“命令”)是从Binder驱动那里获得的:

for (;;) {bwr.read_size = sizeof(readbuf);/*readbuf的大小为32个
unsigned*/bwr.read_consumed = 0;bwr.read_buffer = (unsigned) readbuf;/*读取的消息存储到
readbuf中*/res = ioctl(bs->fd, BINDER_WRITE_READ, &bwr); //读取“消
息”…res = binder_parse(bs, 0, 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;}}
}/*binder_loop结束*/

由此可见,SM遵循以下几个步骤
1.从Binder驱动读取消息
通过发送BINDER_WRITE_READ命令实现——这个命令既可读取也可写入,具体要看bwr.write_size和bwr.read_size。因为这里write_size的初始化值为0,而read_size为sizeof(readbuf),所以Binder驱动只执行读取操作。
2.处理消息
调用binder_parse来解析消息。
3.不断循环,而且永远不会主动退出(除非出现致命错误)
1.3 获取ServiceManager服务——设计思考
如果要访问SM(Binder Server)的服务,流程应该是怎么样的呢?
无非就是以下几步:

  1. 打开Binder设备;
  2. 执行mmap;
  3. 通过Binder驱动向SM发送请求(SM的handle为0);
  4. 获得结果。

不要怀疑,核心工作确实只有这些。不过一些具体细节还需要再商榷,比如:

  1. 向SM发起请求的Binder Client可能是Android的APK应用程序,所以SM必须要提供Java层接口
  2. 如果每个Binder Client都要亲力亲为地执行以上几个步骤来获取SM服务,那么可想而知会浪费不少时间。Android系统当然也想到了这一点,所以它会提供更好的封装来使整个SM调用过程更精简实用。
  3. 如果应用程序代码中每次使用SM服务(或者其他Binder Server服务),都需要打开一次Binder驱动、执行mmap,其后果就是消耗的系统资源会越来越多,直到崩溃。一个有效的解决办法是每个进程只允许打开一次Binder设备,且只做一次内存映射——所有需要使用Binder驱动的线程共享这一资源。

问题转化为:如果让我们来设计一个符合上述要求的BinderClient,应该怎么做?
1.ProcessState和IPCThreadState
首先能想到的是要创建一个类来专门管理每个应用进程中的Binder操作——更为重要的是,执行Binder驱动的一系列命令对上层用户必须是“透明的”。这个类就是ProcessState。仅有ProcessState是不够的,进程中的每一个线程都应该有与Binder驱动自由沟通的权利——而且基于Binder的IPC是阻塞的,这样能保证个别thread在做进程间通信时不会卡死整个应用程序。与Binder驱动进行实际命令通信的是IPCThreadState。
2.Proxy
有了上面两个类,应用程序现在可以与Binder驱动通信了。原则上我们还是可以通过发送BINDER_WRITE_READ等Binder支持的命令来与其交互,从而一步步得到SM提供的服务。到还需要对SM提供的服务进行封装,把这个SM服务的封装取名为ServiceManagerProxy。

/*应用程序获取SM服务示例*/
//Step1. 创建ServiceManagerProxy
ServiceManagerProxy sm = new ServiceManagerProxy(new BpBinder(HANDLE));
//Step2. 通过ServiceManagerProxy获取SM的某项服务
IBinder wms_binder = sm.getService("window");

应用程序只需要两步就可以得到ServiceManager提供的服务。
如何实现这个目标:
(1)ServiceManagerProxy的接口。ServiceManagerProxy所能提供的服务和服务端的SM必须是一致的,如getService,addService等。把这些方法提取出来,就是ServiceManagerProxy的接口。我们给
它取名为IServiceManager,如下所示(大家先忽略它的参数):

public interface IServiceManager 
{public IBinder getService(String name) throws
RemoteException;public IBinder checkService(String name) throws
RemoteException;public void addService(String name, IBinder service,
boolean allowIsolated)throws RemoteException;public String[] listServices() throws RemoteException;
}

很显然,ServiceManagerProxy需要继承自IServiceManager,如图所示。
在这里插入图片描述
(2)接口实现。以getService为例,要取得ServiceManager的这个服务,至少有两部分工作。
1.与Binder建立关系
因为进程中已经有了ProcessState和IPCThreadState这两个专门与Binder驱动通信的类,所以Java层代码使用Binder驱动实际上是基于它们来完成的。我们称为BpBinder。
2.向Binder发送命令,从而获得SM提供的服务。
总结如下:

  1. Binder架构
    它的主体包括驱动、SM、Binder Client和Binder Server。
  2. Binder驱动
  3. Service Manager
    SM既是Binder框架的支撑者,同时也是一个标准的Server。

可以用一张图来概括Binder机制,如图所示(获取SM服务(主要组成元素))。
在这里插入图片描述
图中Client表示Binder Client,即使用Binder机制来获取服务的客户端。它的内部结构由下而上依次为
ProcessState/IPCThreadState→BpBinder→Proxy→User。不论是Client或者Service Manager,它们的工作都是基于Binder Driver完成的。
1.4 ServiceManagerProxy
前一小节思考“设计意图”时,我们曾通过一小段伪代码描述了ServiceManagerProxy的一种实现方案——Android系统中的具体实现与此基本类似,只不过它在ServiceManagerProxy上又加了一层封装,即ServiceManager.java。
这样应用程序使用ServiceManager就更加方便了,连ServiceManagerProxy对象都不用创建,如下所示:
ServiceManager.getService(name);
getService的内部实现:

 public static IBinder getService(String name) {try {IBinder service = sCache.get(name);//查询缓存if (service != null) {return service;//从缓存中找到结果,直接返回} else {return
getIServiceManager().getService(name);//向SM发起查询}} catch (RemoteException e) {Log.e(TAG, "error in getService", e);}return null;}
private static IServiceManager getIServiceManager() {if (sServiceManager != null) {return sServiceManager;//返回一个IServiceManager对象}// Find the service managersServiceManager
=ServiceManagerNative.asInterface(BinderInternal.getContextObjec
t());return sServiceManager;}

ServiceManagerNative

/*frameworks/base/core/java/android/os/ServiceManagerNative.java
*/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);}

从这个函数的注释可以发现,它负责将一个Binder对象转换成IServiceManager,并在必要的时候创建ServiceManagerProxy。
ServiceManagerProxy必定是要与Binder驱动通信的,因而它的构造函数中传入了IBinder对象

public ServiceManagerProxy(IBinder remote) {mRemote = remote;
}

可以看到,它只是简单地记录了这个IBinder对象。这就像我们电话订餐一样,IBinder是餐馆的电话号码,通常都是先把它记下来,等需要的时候再通过这个号码获取餐馆提供的服务。比如getService()这个接口:

public IBinder getService(String name) throws
RemoteException {Parcel data = Parcel.obtain();Parcel reply = Parcel.obtain();data.writeInterfaceToken(IServiceManager.descriptor);data.writeString(name);mRemote.transact(GET_SERVICE_TRANSACTION, data, reply,
0);/*利用IBinder对象执行命令*/IBinder binder = reply.readStrongBinder();reply.recycle();data.recycle();return binder;}

这个函数实现分为以下3部分。

  1. 准备数据
  2. IBinder.transact
    利用IBinder的transact将请求发送出去,而不用理会Binder驱动的open,mmap以及一大堆具体的Binder协议中的命令。所以这个IBinder一定会在内部使用ProcessState和IPCThreadState来与Binder驱动进行通信
  3. 获取结果

实际工作只有下面这句:

mRemote.transact(GET_SERVICE_TRANSACTION, data, reply, 0);

这里需要注意一下,就是客户端和服务器端所使用的业务代码要一致,如上面的GET_SERVICE_TRANSACTION。它的定义是:

int GET_SERVICE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION;

按照IBinder的定义:

int FIRST_CALL_TRANSACTION = 0x00000001;

所以这个业务码是1。再来看Service_manager.c中的业务码说明:

enum {SVC_MGR_GET_SERVICE = 1, //对应的就是上面的那个业务SVC_MGR_CHECK_SERVICE,SVC_MGR_ADD_SERVICE,SVC_MGR_LIST_SERVICES,
};

这样客户端与服务器端的业务代码就保持一致了。
1.5 IBinder和BpBinder
在创建ServiceManagerProxy时,传入了一个IBinder对象,然后借助于它的transact方法,可以方便地与Binder驱动进行通信。那么,IBinder内部是如何实现的?
Binder提供的功能可以统一在IBinder中表示,至少要有如下接口方法:

/*frameworks/base/core/java/android/os/IBinder.java*/
public interface IBinder {public IInterface queryLocalInterface(String descriptor);public boolean transact(int code, Parcel data, Parcel
reply, int flags)throws Remote Exception;}

此外,还应该有获取IBinder对象的一个类,即BinderInternal。提供的相应方法是:

/*frameworks/base/core/java/com/android/internel/os/BinderIntern
al.java*/
public class BinderInternal {public static final native IBinder getContextObject();}

对应的native方法:

/*frameworks/base/core/jni/android_util_Binder.cpp*/
static jobject
android_os_BinderInternal_getContextObject(JNIEnv* env, jobject
clazz)
{sp<IBinder> b = ProcessState::self()-
>getContextObject(NULL);return javaObjectForIBinder(env, b);
}

是通过ProcessState来实现的,把ProcessState中创建的对象转化成Java层的IBinder对象。
IBinder只是一个接口类,显然还会有具体的实现类继承于它。在Native层,这就是BpBinder (BpBinder.cpp);而在Java层,则是Binder.java中的BinderProxy。事实上,ProcessState::self() ->getContextObject(NULL)返回的就是一个BpBinder对象。
在这里插入图片描述
BinderProxy和BpBinder分别继承自Java和Native层的IBinder接口。其中BpBinder是由ProcessState创建的,而BinderProxy是由javaObjectForIBinder()函数通过JNI的NewObject()创建的。
分析源码,mRemote->transact,调用BinderProxy的transact方法,真正的实现还是在android_util_Binder.cpp中,最后就是通过BpBinder.transact来处理用户的Binder请求:

/*frameworks/native/libs/binder/BpBinder.cpp*/
status_t BpBinder::transact(uint32_t code, const Parcel& data,
Parcel* reply, uint32_t flags)
{// Once a binder has died, it will never come back to life.if (mAlive) {status_t status=IPCThreadState::self()-
>transact(mHandle,code,data,reply, flags);if (status == DEAD_OBJECT) mAlive = 0;return status;}return DEAD_OBJECT;
}

最终还是通过IPCThreadState及ProcessState来实现的。
1.6 ProcessState和IPCThreadState
实现ProcessState的关键点在于。

  1. 保证同一个进程中只有一个ProcessState实例存在;而且只有在ProcessState对象创建时才打开Binder设备以及做内存映射。
  2. 向上层提供IPC服务。
  3. 与IPCThreadState分工合作,各司其职。

大多数程序都有IPC的需要,而进程间通信本身又是非常烦琐的,因而Android系统特别为程序进程使用Binder机制封装了两个实现类,即ProcessState和IPCThreadState。从名称上可以看出,前者是进程
相关的,而后者是线程相关的。ProcessState负责打开Binder驱动设备,进行mmap()等准备工作;而如何与Binder驱动进行具体的命令通信则由IPCThreadState来完成。

Binder客户端——Binder Client

1.Binder是什么
它是众多进程间通信的一种。进程间通信是每个操作系统都需要提供的。
2.应用程序与Binder
Binder的最大“消费者”是Java层的应用程序。图6-21虽然简单,却概括了Binder的实质,即它是进程间的通信桥梁。接下来我们
还需要沿着这条线索继续挖掘。图中所示的进程1是一种泛指,那么一个进程需要满足什么条件,或者说要做哪些准备工作才有资格使用Binder呢?从应用开发人员的角度来看,这似乎并不在他们的考虑范围,因为一般情况下他们可以在程序代码的任何位置通过bindService,startActivity以及sendBroadcast等一系列接口方法来实现与其他进程的交互,如图6-22所示。
在这里插入图片描述

有了Binder Driver,Service Manager的努力,以及Android系统专门面向应用开发提供的Binder强有力的封装,才能使应用程序之间顺利地进行无缝通信。我们从四大组件中就可以看出一些端倪。

  • Activity

通过startActivity可以启动目标进程。

  • Service

任何应用程序都可以通过startService或bindService来启动特定的服务,而不论后者是不是跨进程的。

  • Broadcast
    任何应用程序都可以通过sendBroadcast来发送一个广播,且无论广播处理者是不是在同一个进程中。
  • intent
    四大组件的上述操作中,多数情况下并不会特别指明需要由哪个目标应用程序来响应请求——它们会先通过一个被称为“intent”的
    对象表达出“意愿”,然后由系统找出最匹配的应用进程来完成相关工作。这样的设计极大地增强了系统的灵活性。

下面将会选取bindService为例,向大家充分揭示隐藏在这些接口背后的Binder内部原理。
在这里插入图片描述
由图可知,整个框架被一分为二,分别代表了Binder机制中对应用程序可见和隐藏的两部分。
为了让大家可以更清楚地看到整个“隐藏部分”的内部实现,接下来选取一个范例进行剖析。如图6-25所示,Application1中的某个
Activity通过bindService(intent)来试图启动符合intent描述的Service服务——最终Application2中的Service将被运行。
在这里插入图片描述
应用程序如何能依托bindService来启动系统中其他进程提供的Service呢?必定需要以下几个步骤才能完成这一操作目标。

  • Step1. 应用程序填写Intent,调用bindService发出请求。
  • Step2. 收到请求的bindService(此时还在应用程序的运行空间中)将与Activity ManagerService(AMS)取得联系。为了获得AMS的Binder句柄值,还要事先调用ServiceManager.getService,这里就已经涉及进程间通信了。在得到AMS的句柄值后,程序才能真正地向它发起请求
  • Step3. AMS基于特定的“最优匹配策略”,从其内部存储的系统所有服务组件的资料中找到与Intent最匹配的一个,然后向它发送
    Service绑定请求(这一步也是进程间通信)——注意,如果目标进程还不存在的话,AMS还要负责把它启动起来。
  • Step4. “被绑定”的服务进程需要响应绑定,执行具体操作,并在成功完成后通知AMS;然后由后者再回调发起请求的应用程序(回调接口是ServiceConnection)

由此可见,一个看似简单的bindService原来内部“大有乾坤”。但是为什么Application1在Activity中只需要调用bindService即可,
而丝毫不见上述的烦琐过程呢?
基于Activity应用程序的继承关系如图6-26所示
在这里插入图片描述
Activity继承关系的“根”是Context。bindService自然也是包含在Context里面的。具体而言,Context只是提供了抽象的接口,功能则是在ContextWrapper中实现的:

/*frameworks/base/core/java/android/content/ContextWrapper.java*/public boolean bindService(Intent service, ServiceConnection
conn,int flags) {return mBase.bindService(service, conn, flags); //mBase是什么?}

上述变量mBase也是一个Context对象,最新版本中是由ContextImpl来实现的(bindService直接调用bindServiceAsUser):

/*frameworks/base/core/java/android/app/ContextImpl.java*/public boolean bindServiceAsUser(Intent
service,ServiceConnection conn,int flags,UserHandle user) {int res =
ActivityManagerNative.getDefault().bindService(mMainThread.getApplicationThread(),
getActivityToken(),service,
service.resolveTypeIfNeeded(getContentResolver()),sd, flags, userId); /*ActivityManager出现了,证明了我们猜测的第2步*/}

那么,应用程序又是如何找到AMS并与之建立联系的呢?和ServiceManager一样,AMS也同样提供了ActivityManagerNative和ActivityManagerProxy,具体如下:

/*frameworks/base/core/java/android/app/ActivityManagerNative.ja
va*/static public IActivityManager getDefault() {return gDefault.get(); /*得到默认的IActivityManager对象*/}

这个gDefault.get()得到的是什么?

/*frameworks/base/core/java/android/app/ActivityManagerNative.ja
va*/private static final Singleton<IActivityManager> gDefault
=new Singleton <IActivityManager>() {/*Singleton,即“单实例”是一种常见的设计模式,它保证某个对象只会被创建
一次。当调用gDefault.get()时,会先进行内部判断:如果该对象已经存在,就直接
返回它的现有值;否则才需要通过内部create()新建一个对象实例*/protected IActivityManager create() {IBinder b = ServiceManager.getService("activity");/*通过
ServiceManager Service取得ActivityManagerService的
IBinder对象*/…IActivityManager am = asInterface(b); /*创建一个可用的
ActivityManagerProxy*/return am;}};

ActivityManagerNative的作用之一,就是帮助调用者方便快速地取得一个ActivityManagerProxy。
在gDefault这个单实例中,获取一个有效的IActivityManager对象需要两个步骤,即:

  • 得到IBinder(BpBinder);
  • 将IBinder转化为Iinterface(在这个场景中,是IactivityManager)。

顺便说一下,ActivityManagerNative的另一个作用是为ActivityManagerService的实现提供便利。如果仔细观察,就会发现
ActivityManagerNative里有如下方法:

public boolean onTransact(int code, Parcel data, Parcel reply,
int flags)throws Remote Exception {switch (code) {case START_ACTIVITY_TRANSACTION:{int result = startActivity(app, intent,
resolvedType,grantedUriPermissions, grantedMode,
resultTo, resultWho,requestCode, onlyIfNeeded, debug,
profileFile, profileFd, autoStopPro filer);}

这样在AMS里只要继承自ActivityManagerNative,就已经将用户的业务请求码与自己的内部实现函数连接了起来,是不是很方便?源代码如下:

/*frameworks/base/services/java/com/android/server/am/ActivityMa
nagerService.java*/
public final class ActivityManagerService extends
ActivityManagerNative/*果然继承了ActivityManagerNative*/implements Watchdog.Monitor,
BatteryStatsImpl.BatteryCallback {

因而可以这么说,ActivityManagerNative(其他服务的Native也是一样的)既是面向调用者的,也是面向服务实现本身的,只不过这个Native的名称取得容易产生歧义。
经过上面代码的分析,Application1和Application2的进程间通信还应该再加上ServiceManager和ActivityManagerService的支持,
如图6-27所示
在这里插入图片描述
当应用程序需要通过ServiceManager来查询某个Binder Server时,调用的是getService方法。直接面向应用程序的是Service
Manager.java,它提供了多个static接口供调用者获取ServiceManager提供的服务,如Service Manager.getService。这些
静态函数内部通过getIServiceManager来得到ServiceManagerProxy对象——后者作为SM的本地代理,将利用IBinder来“穿越”JNI层调用到对应的BpBinder,进而使用ProcessState和IPCThreadState的相关接口,最终经由Binder驱动完成与ServiceManager的通信。
在这里插入图片描述
bindService 调用流程

Android接口描述语言——AIDL

AIDL是Android Interface Description Language的简写。从名称上看它是一种语言,而且是专门用于描述接口的语言。准确地说,它是用于定义客户端/服务端通信接口的一种描述语言。
通过一个范例来分析采用AIDL究竟可以为Binder Server带来哪些便利以及它的内部实现原理。以WindowManagerService为例
(1)WMS是在SystemServer中启动的
(2) 看看AIDL是如何保证接口的一致性的。使用AIDL首先要书写一个*.aidl文件来描述这个Server。比如:

/*IWindowManager.aidl*/
interface IWindowManager
{…
IWindowSession openSession(in IInputMethodClient client,in
IInputContext inputContext);}

上述代码段只保留了openSession一个接口方法。这个IWindowManager.aidl文件经过工具转化后,成为以下内容:

/*IWindowManager.java*/
public interface IWindowManager extends android.os.IInterface
{
public static abstract class Stub extends android.os.Binder
//Stub表示一个“桩”
implements android.view.IWindowManager
{
public static android.view.IWindowManager
asInterface(android.os.IBinder obj)
{}
@Override public android.os.IBinder asBinder()
{
return this;
}
@Override public boolean onTransact(int code, android.os.Parcel
data, 
android.os.Parcel reply, int flags) throws
android.os.RemoteException
{
switch (code)
{
case TRANSACTION_openSession:
{}}
return super.onTransact(code, data, reply, flags);
}
private static class Proxy implements
android.view.IwindowManager
//Proxy就是“代理”,我们已经多次讲解
{
private android.os.IBinder mRemote;
…
@Override public android.os.IBinder asBinder()
{
return mRemote;
}
@Override public android.view.IWindowSessionopenSession(com.android.internal.view.IInputMethodClient
client,com.android.internal.view.IInputContext inputContext) throws
android.os.RemoteException
{}
} //Proxy结束
}//Stub结束static final int TRANSACTION_openSession =
(android.os.IBinder.FIRST_CALL_TRANSACTION + 3); 
/*自动分配业务码,大家可以和ServiceManager中的手工分配做下对比*/
}//IWindowManager结束
  • IWindowManager
    一般以大写字母I开头的表示一个Interface。在AIDL中,所有的服务接口都继承于Iinterface,然后在此基础上声明与此Server服务相关的方法。比如IWindowManager中除了两个嵌套类外,其末尾还包含了它提供的服务openSession、getRealDisplaySize、hasSystemNavBar等接口的原型。
  • IWindowManager.Stub
    还记得ServiceManagerNative吗?Stub的作用和它类似。它包含了一个嵌套类(Stub.Proxy),以及各种常用的接口方法(如asInterface,asBinder等),其中最重要的一个就是onTransact。我们知道ServiceManagerNative是同时面向服务器和客户端的,Stub也同样如此。在实际使用中,一个Binder Server的实现类通常继承自Stub。而Stub又继承自Binder并实现了该Server的IXX接口,如IWindowManager的实现类WindowManagerService:
public class WindowManagerService extends IWindowManager.Stub
  • IWindowManager.Stub.Proxy
    Proxy即代理,功能和ServiceManager Proxy类似。因而这个类是面向Binder Client的,它可以让调用者轻松地构造出Binder Server的本地代理对象
    具体如图所示(基于AIDL的Binder Server)
    在这里插入图片描述
    通过分析aidl文件以及由它转化生成的java接口文件,我们知道一个AIDL接口包括了IwindowManager,IWindowManager.Stub和IWindowManager.Stub.Proxy三个重要类。后两者分别面向于WMS的服务端和Binder Client本地代理的实现,且都继承于IWindowManager,因而就保证了Client和Server是在完全一致的服务接口上进行通信的。
    (3)如何与Binder驱动交互的
    通过解析,我们发现原来系统服务进程在一开始就调用了:
ProcessState::self()->startThreadPool();
IPCThreadState::self()->joinThreadPool();

它们将导致程序最终进入一个类似于binder_loop的主循环。因而,在这个进程中的Binder对象都可以不用单独与驱动进行交互
(4)Client如何准确地访问到目标进程中的Binder Server
有两种方式可以让Binder Server可知,其一是通过在SM中注册,也就是WMS所属的情况。其二就是匿名Server实现。对于实名的Server,当它利用addService来把自身注册到SM中时,会“路过”Binder驱动,而后者就会按计划把这一Binder对象链接到proc-> nodes,以及target_proc-> refs_by_desc和target_proc-> refs_by_node中。在这一场景中,proc是系统服务进程,而target_proc则是SM。也就是说,SM中拥有了一个描述WMS的binder_node的引用。这样当一个Binder Client通过getService向SM发起查询时,后者就可以准确地告知这个调用者它想访问的WMS节点所在的位置。

匿名Binder Server

通过addService把自己注册到了Service Manager中,因而任何BinderClient都可以通过SM的getService接口来获取它的一个引用。称这种类型的Binder Server为“实名”Server。不在Service Manager中注册,称之为“匿名”的Binder Server。匿名性带来的一个直接好处是安全系数的提高,如某个应用程序提供了某种Server服务,但并不希望面向公众开放。

相关文章:

Android Framework-进程间通信——Binder

我们知道&#xff0c;同一个程序中的两个函数之间能直接调用的根本原因是处于相同的内存空间中。 比如有以下两个函数A和B&#xff1a; /*Simple.c*/ void A() { B(); } void B() { }因为是在一个内存空间中&#xff0c;虚拟地址的映射规则完全一致&#xff0c;所以函数A和B之…...

有趣的小知识(二)浏览器内的秘密:了解Cookie基础知识

一、简介 Cookie是一种小型的文本文件&#xff0c;由Web服务器发送给Web浏览器&#xff0c;并存储在用户的计算机硬盘上。它通常用于记录用户的偏好、登录状态、购物车信息等&#xff0c;以便在用户下次访问该网站时能够提供更好的用户体验。Cookie通常包含网站的名称、Cookie的…...

Spring框架

DI:依赖注入IOC:控制反转AOP:面向切面IOC容器&#xff1a;存放管理各种对象Spring优势&#xff1a;低耦合。&#xff08;降低组件之间的关联性&#xff0c;实现软件各层之间的解耦&#xff09;声明式事务管理&#xff08;基于AOP来管理&#xff09;和其他框架的整合&#xff08…...

mysql8的表锁排查

information_schema.innodb_trx ##正在运行的事务信息。 sys.innodb_lock_waits ##处于锁等待的关联事务信息。 performance_schema.threads ##SQL线程及线程号、进程号、OS线程号等信息 # 查询锁的情况 select * from performance_schema.data_locks where object_name =t_xxx…...

【C语言】深度理解指针(上)

前言&#x1f30a;谈到指针&#xff0c;想必大家都不陌生。它不仅是C语言的重难点&#xff0c;还是不少C初学者的噩梦。本期我们将深度探讨一些较为复杂的指针以及指针的妙用&#xff0c;带领大家感受指针的魅力&#x1f61d;。首先&#xff0c;我们先来复习复习指针的概念&…...

最近我的视频播放浅学总结

因为想做一个类似苹果的同播共享功能&#xff0c;这一段时间对音视频做了一些浅浅的学习&#xff0c;现简单总结记录。 我的需求是找到一个尽可能简单的方案来两人播放一段视频&#xff0c;并且能够进度和操作同步&#xff0c;所以基本不能有延迟&#xff0c;同时能够显示WebV…...

【C/C++基础知识点】输出n位斐波那契数列

目录 前言什么是斐波那契数列兔子的故事小知识点收尾前言 在软件行业已经有快十年,技术虽然一般般,但是足够应付额解决编程入门的相关问题! 都说十年磨一剑,积累到一定经验,是时候发挥自己的价值,给予入门的同行些许的帮助! 为什么要写收费专栏,其实原因很简单,时间就…...

C语言拔高知识——指针的进阶(万字大文超详细)

在之前的文章中&#xff0c;我已经讲解过了初阶指针的内容&#xff0c;今天就来讲一讲指针的进阶&#xff01; 上篇指针地址&#xff1a;保姆式指针讲解&#xff0c;超详细&#xff0c;适合初学者_指针详解_陈大大陈的博客-CSDN博客 目录 1. 字符指针 2. 指针数组 3. 数组指…...

程序员推荐的良心网站合集!(第二期)

今天来给大家推荐几个程序员必看的国外良心网站合集第二期合集。 Semantic Schoolar 由微软联合创始人Paul Allen开发的免费学术搜索引擎&#xff0c;不仅可以通过时间线快速定位想要的文献&#xff0c;还有强大的筛选功能可以精准的找到自己想要的文献&#xff0c;想要什么搜…...

【Java核心知识】spring boot整合Mybatis plus + Phoenix 访问Hbase与使用注意

为了Phoenix能让开发者通过SQL访问Hbase而不必使用原生的方式&#xff1f;引用Phoenix官网上的一句话&#xff1a;SQL is just a way of expressing what you want to get not how you want to get it. 即SQL不是一种数据操作技术&#xff0c;而是一种特殊的表达方式。只是表示…...

lua实现游戏全局鼠标点击效果

前言 最近在优化项目,策划提了一个需求,需要实现一个通用点击特效。 尝试1 首先想到的是改变鼠标指针样式,这个以前学过,还有点印象,以前刚开始学unity的时候,记得看到过一个方法可以改变游戏中鼠标指针样式。 方法如下:选择“Edit”——>“Project Setting”,打…...

MyBatis源码分析(二、续)SqlSource创建流程,SQL如何解析?如何将#{id}变成?的

文章目录实例一、SqlSource处理入口二、SqlSource处理逻辑1、XMLScriptBuilder 构造方法2、解析动态sql3、DynamicSqlSource4、RawSqlSource解析sql&#xff08;1&#xff09;parse方法解析sql写在后面实例 此处我们分析的sql&#xff1a; <select id"selectBlog&quo…...

用 C 语言开发一门编程语言 — 函数库的设计与实现

目录 文章目录目录前言前文列表基础功能演示数字运算变量与代数运算列表处理Lambda 函数条件分支字符串源文件加载函数库列表处理函数库条件分支函数库数学库前言 通过开发一门类 Lisp 的编程语言来理解编程语言的设计思想&#xff0c;本实践来自著名的《Build Your Own Lisp》…...

网络层IP协议与数据链路层以太网协议

文章目录一、IP协议IP地址地址管理路由选择DNS二、以太网协议以太网帧MTU一、IP协议 IP协议是我们网络层的代表协议&#xff0c;今天我们就来一起学习一下吧&#xff0c;我们这里介绍的主要是IPv4协议。 版本&#xff1a;指定IP协议的版本&#xff0c;版本的取值只有4&#x…...

JDK动态代理详解

1.什么是动态代理 可能很多小伙伴首次接触动态代理这个名词的时候&#xff0c;或者是在面试过程中被问到动态代理的时候&#xff0c;不能很好的描述出来&#xff0c;动态代理到底是个什么高大上的技术。不方&#xff0c;其实动态代理的使用非常广泛&#xff0c;例如我们平常使用…...

实时的软件生成 —— Prompt 编程打通低代码的最后一公里?

PS&#xff1a;这也是一篇畅想&#xff0c;虽然经过了一番试验&#xff0c;依旧有一些不足&#xff0c;但是大体上站得住脚。传统的软件生成方式需要程序员编写大量的代码&#xff0c;然后进行测试、发布等一系列繁琐的流程。而实时生成技术则是借助人工智能技术&#xff0c;让…...

互联网工程师 1480 道 Java 面试题及答案整理 ( 2023 年 整理版)

最近很多粉丝朋友私信我说&#xff1a;熬过了去年的寒冬却没熬过现在的内卷&#xff1b;打开 Boss 直拒一排已读不回&#xff0c;回的基本都是外包&#xff0c;薪资还给的不高&#xff0c;对技术水平要求也远超从前&#xff1b;感觉 Java 一个初中级岗位有上千人同时竞争&#…...

Spark开发

第一步&#xff1a;创建RDD Spark提供三种创建RDD方式&#xff1a;** 集合、本地文件、HDFS文件** 使用程序中的集合创建RDD&#xff0c;主要用于进行测试&#xff0c;可以在实际部署到集群运行之前&#xff0c;自己使用集合构造一些测试数据&#xff0c;来测试后面的spark应…...

Tornado异步框架

简介&#xff1a; tornado是Python的web框架。tornado和主流的web服务器框架有明显的区别&#xff1a;它是非阻塞式服务器&#xff0c;而且速度非常快&#xff0c;得力于其非阻塞的方式和epoll的运用tornado可以每秒处理数以千计的连接&#xff08;号称&#xff09; 基本配置 …...

openpnp - error - 吸嘴没下降到板子上, 就将元件松开

文章目录openpnp - error - 吸嘴没下降到板子上, 就将元件松开概述笔记ENDopenpnp - error - 吸嘴没下降到板子上, 就将元件松开 概述 以前用过国内一家openpnp厂家出的设备, 他们家的openpnp是自己改过的. 贴片流程已经走过一遍. 这次还是按照以前记录的笔记, 按照国内那家的…...

【Java】yyyy-MM-dd HH:mm:ss 时间格式 时间戳 全面解读超详细

时间格式 时间格式(协议)描述gg时期或纪元。y不包含纪元的年份。不具有前导零。yy不包含纪元的年份。具有前导零。yyyy包含纪元的四位数的年份。M月份数字。一位数的月份没有前导零。MM月份数字。一位数的月份有一个前导零。MMM月份的缩写名称&#xff0c;在AbbreviatedMonthN…...

快鲸SCRM发布口腔企业私域运营解决方案

口腔企业普遍面临着以下几方面运营痛点问题 1、获客成本居高不下&#xff0c;恶性竞争严重 2、管理系统落后&#xff0c;人员流失严重 3、客户顾虑多、决策时间长 4、老客户易流失&#xff0c;粘性差 以上这些痛点&#xff0c;不得不倒逼口腔企业向精细化运营客户迈进。 …...

Verilog实现组合逻辑电路

在verilog 中可以实现的数字电路主要分为两类----组合逻辑电路和时序逻辑电路。组合逻辑电路比较简单&#xff0c;仅由基本逻辑门组成---如与门、或门和非门等。当电路的输入发生变化时&#xff0c;输出几乎&#xff08;信号在电路中传递时会有一小段延迟&#xff09;立即就发生…...

2023前端菜鸟笔试血泪史html5-one--找到工作前都更新

1.说说对html语义化的理解 什么的HTML语义化&#xff0c;顾名思义&#xff0c;HTML语义化就是可以不通过了解HTML的内容&#xff0c;就可以知道这个部分所代表的的意义。 HTML语义化的意义&#xff1a;在使用HTML标签构建页面时&#xff0c;避免大篇幅的使用无语义的标签。 …...

蓝牙调试工具集合汇总

BLE 该部分主要分享一下常用的蓝牙调试工具&#xff0c;方便后续蓝牙抓包及分析。 目录 1 hciconfig 2 hcitool 3 hcidump 4 hciattach 5 btmon 6 bluetoothd 7 bluetoothctl 1 hciconfig 工具介绍&#xff1a;hciconfig&#xff0c;HCI 设备配置工具 命令格式&…...

Java 获取文件后缀名【一文总结所有方法】

✅作者简介&#xff1a;2022年博客新星 第八。热爱国学的Java后端开发者&#xff0c;修心和技术同步精进。 &#x1f34e;个人主页&#xff1a;Java Fans的博客 &#x1f34a;个人信条&#xff1a;不迁怒&#xff0c;不贰过。小知识&#xff0c;大智慧。 &#x1f49e;当前专栏…...

UML常见图的总结

一、概述 UML&#xff1a;Unified Modeling Language&#xff0c;统一建模语言&#xff0c;支持从需求分析开始的软件开发的全过程。是一个支持模型化和软件系统开发的图形化语言、为软件开发的所有阶段提供模型化和可视化支持&#xff0c;包括由需求分析到规格&#xff0c;到…...

WebRTC系列-工具系列之音频相关工具

文章目录 1. audio_util数据格式转换类2. WavFile文件读写类2.1 读取wav文件2.2 写入wav文件这篇文章主要介绍WebRTC中一些音频工具这些,大部分都在 common_audio目录下,这个文件夹下提供音频的大量算法,包括sinc重采样算法,音频数据格式的转换:例如 float转int16_t格式等…...

7 线性回归及Python实现

1 统计指标 随机变量XXX的理论平均值称为期望: μE(X)\mu E(X)μE(X)但现实中通常不知道μ\muμ, 因此使用已知样本来获取均值 X‾1n∑i1nXi.\overline{X} \frac{1}{n} \sum_{i 1}^n X_i. Xn1​i1∑n​Xi​.方差variance定义为&#xff1a; σ2E(∣X−μ∣2).\sigma^2 E(|…...

适合小团队协作、任务管理、计划和进度跟踪的项目任务管理工具有哪些?

适合小团队协作、任务管理、计划和进度跟踪的项目任务管理工具有哪些? 大家可以参考这个模板&#xff1a;http://s.fanruan.com/irhj8管理项目归根结底在管理人、物&#xff0c;扩展来说便是&#xff1a; 人&#xff1a;员工能力、组织机制&#xff1b; 物&#xff1a;项目内…...

网络营销平台搭建方案网站/网页广告调词平台多少钱

1. 概述 最近开始学习自定义View&#xff0c;看到现在公司项目上的一个动画效果&#xff0c;顿时想到其实可以自己画&#xff0c;于是就开始着手优&#xff08;zhuang&#xff09;化&#xff08;bi&#xff09;这个动画。 动画如下&#xff1a; 其实很简单对不对&#xff0c;但…...

描述网站建设的基本流程图/关键词分类

站内搜索,欢迎使用Loading转载于:https://www.cnblogs.com/jinho/archive/2010/04/07/1705867.html...

国际网站怎么注册免费的/谷歌浏览器安卓版

接着上一篇在同一台服务器上配置app1.domain.com和app2.domain.com多个web应用&#xff0c;这里记录下在nginx下给express web app配置静态代理和文件压缩。目的 为了减少node.js对静态文件的处理和加快前端下载静态资源的速度&#xff0c;决定使用nginx做静态代理&#xff0c;…...

贵阳专业做网站的公司/市场营销策划方案3000字

科目编号&#xff1a;8638 座位号 2017-2018学年度第二学期期末考试 薪酬制度与薪酬管理 试题 2018年 7 月 一、单选题&#xff08;本大题共10小题&#xff0c;每小题3分&#xff0c;共计30分&#xff09; &#xff08;★请考生务必将答案填入到下面对应序号的答题框中★&…...

怎么查那些人输入做网站/seo技术培训江门

神经网络的梯度下降&#xff08;Gradient descent for neural networks&#xff09; 假设单隐层神经网络会有W[1]W^{[1]}W[1]&#xff0c;b[1]b^{[1]}b[1]&#xff0c;W[2]W^{[2]}W[2]&#xff0c;b[2]b^{[2]}b[2]这些参数&#xff0c;还有个nxn_xnx​表示输入特征的个数&…...

帮人做网站要怎么赚钱/浙江短视频seo优化网站

当前有效matplotlib版本为&#xff1a;3.4.1。 figure函数概述 在pyplot模块中&#xff0c;figure函数用于创建新的图形&#xff0c;或激活已存在的图形。 函数的签名为matplotlib.pyplot.figure(numNone, figsizeNone, dpiNone, facecolorNone, edgecolorNone, frameonTrue…...