背景知识

为了更好的理解binder,我们要先澄清一下概念,因为Android 基于Linux内核,我们有必要了解相关知识。

进程隔离

进程隔离是为了保护操作系统进程之间互不干扰而设计的,这个技术是为了避免进程A写入进程B准备的,进程隔离的实现,使用了虚拟地址空间,进程A的虚拟地址和进程B的虚拟地址不同,这样就防止进程A的数据写入进程B,操作系统之间不同进程之间,数据不共享,对于每一个进程来说,都天真的以为自己独享了整个系统,不知道其他进程的存在,因此一个进程与其他进程进行通信,需要一种特殊的方式才可以

用户空间/内核空间

Linux Kernel 是操作系统的核心,独立于普通应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。

对于Kernel这么一个高安全级别的东西,显然是不允许其他应用程序随便调用或访问的,所以需要对Kernel提供一定的保护机制,这个保护机制用来告诉那些应用程序,你只可以访问某些许可的资源,不可以访问不许可的资源,于是就把Kernel和上层应用程序抽象的隔离开,分别称之为 Kernel Space 和 User Space

系统调用/内核态/用户态

虽然在逻辑上抽离了用户空间、内核空间,但不可避免的是,仍有用户空间需要访问内核空间的资源,比如程序访问文件。该怎么办?

用户空间访问内核空间的唯一方式就是系统调用 ,通过这一统一入口,所有资源访问都是在内核控制下进行,以免导致用户程序对系统资源的越权访问,从而保障了系统的安全,稳定

当一个任务进程进行系统代码调用而陷入内核代码执行时,我们就称进程处于内核运行状态,简称内核态,此时处理器处于特权级别最高的(0)状态,内核代码中执行,当进程在执行用户自己的代码时,称其处于用户状态,简称用户态,此时处理器在特权最低(3)状态,处理器正在特权高的时候才能执行那些特权cpu指令

内核模块/驱动

通过系统调用可使用户空间访问内核空间,如果一个用户空间访问另一个用户空间该怎么办?我们首先想到的是通过内核添加支持,因为内核是共享的,传统的Linux机制比如Socket ,管道,都可以支持的,但是Binder并不是Linux内核的一部分,他是怎么做到访问内核空间的?Linux的动态可加载内核模块机制解决了这个问题,模块是具有独立功能的程序,他可以被单独编译,但不能独立运行,它在运行时被链接到内核作为内核的一部分在内核空间运行,这样,Android 系统通过添加一个内核模块运行在内核空间,用户进程之间通过这个模块作为桥梁,就可以完成通信。

在Android 系统中,这个运行在内核空间的,负责各个用户进程通过Binder通信的内核模块叫做Binder驱动

驱动程序一般指的是设备驱动程序,是一种可以使计算机和设备通信的特殊程序,相当于硬件的接口,操作系统只有通过这个接口才能操作硬件设备

驱动就是操作硬件的接口,为了支持Binder通信过程,binder使用了一种硬件,因此这个模块称之为驱动。

为什么使用Binder

Android 使用的Linux内核有很多种的跨进程通信的方式,那么为什么还要单独搞出一个Binder出来呢?主要有俩点,性能和安全,在移动设备上,广泛的使用跨进程通信肯定对通信体制本身提出了严格的要求;Binder相对于传统的Socket方式,更加高效,另外,传统 的进程通信方式对通信双方身份并没有做出严格的验证,只要在上层协议上进行架设,,比如Socket通信的ip地址是客户端手动输入的,可以进行伪造,而binder机制从协议本身就支持对通信双方的身份验证,从而大大提高了安全性,这个也是Android权限模型的基础。

Binder通信模型

Android 系统 Binder机制中的四个组件,Client,Server,ServerManager,和Binder驱动关系如下

1 Client ,Server,ServerManager,实现用户空间,Binder驱动程序实现在内核空间中

2 Binder驱动和ServerManager 在Android 平台已经实现,开发者只需要在用户空间实现自己的Client和Server

3 Binder驱动程序提供设备文件/dev/binder与用户空间交互,Client ,Server,ServerManager 通过open和ioctl文件操作函数与binder驱动程序进行通信。

4 Client和Server 进程通信通过binder驱动程序间接实现

5 ServerManager 是一个守护进程,用来管理Server,并像Client提供查询Server能力

6 ServerManager 建立,首先要有一个进程向驱动提出成为SM。驱动同意后,SM负责管理Server,不过现在是空的

7 各个Server向SM注册,每个Server端进程启动后,向SM报告,比如我是zhangsan,要找我请返回0X1234,其他Server亦是如此,这样SM就建立一张表,对应着Server的名字和地址

8 Client 要与Server通信,首先询问SM ,请告诉我如何联系zhangsan,SM收到后返回一地址0X1234,Client收到后就可以和Server通信

那么Binder驱动在干什么呢,Client和SM通信,Client和Server通信,Server和SM通信都是通过Binder驱动,驱动默默无闻,但做着最重要的工作,驱动是整个通信的核心,因此完成跨进程通信的秘密全部隐藏在驱动里面,这个稍后讨论

Binder机制跨进程原理

上文给出了Binder的通信模型,指出了通信过程的四个角色: Client, Server, SM, driver; 但是我们仍然不清楚Client到底是如何与Server完成通信的

两个运行在用户空间的进程A和进程B如何完成通信呢?内核可以访问A和B的所有数据;所以,最简单的方式是通过内核做中转;假设进程A要给进程B发送数据,那么就先把A的数据copy到内核空间,然后把内核空间对应的数据copy到B就完成了;用户空间要操作内核空间,需要通过系统调用;刚好,这里就有两个系统调用:copy_from_user, copy_to_user

但是,Binder机制并不是这么干的。讲这么一段,是说明进程间通信并不是什么神秘的东西。那么,Binder机制是如何实现跨进程通信的呢?

Binder驱动为我们做了一切。

假设Client进程想要调用Server进程的object对象的一个方法add;对于这个跨进程通信过程,我们来看看Binder机制是如何做的。 (通信是一个广泛的概念,只要一个进程能调用另外一个进程里面某对象的方法,那么具体要完成什么通信内容就很容易了。)

首先,Server进程要向SM注册;告诉自己是谁,自己有什么能力;在这个场景就是Server告诉SM,它叫zhangsan,它有一个object对象,可以执行add 操作;于是SM建立了一张表:zhangsan这个名字对应进程Server;

然后Client向SM查询:我需要联系一个名字叫做zhangsan的进程里面的object对象;这时候关键来了:进程之间通信的数据都会经过运行在内核空间里面的驱动,驱动在数据流过的时候做了一点手脚,它并不会给Client进程返回一个真正的object对象,而是返回一个看起来跟object一模一样的代理对象objectProxy,这个objectProxy也有一个add方法,但是这个add方法没有Server进程里面object对象的add方法那个能力;objectProxyadd只是一个傀儡,它唯一做的事情就是把参数包装然后交给驱动。(这里我们简化了SM的流程,见下文)

但是Client进程并不知道驱动返回给它的对象动过手脚,毕竟伪装的太像了,如假包换。Client开开心心地拿着objectProxy对象然后调用add方法;我们说过,这个add什么也不做,直接把参数做一些包装然后直接转发给Binder驱动。

驱动收到这个消息,发现是这个objectProxy;一查表就明白了:我之前用objectProxy替换了object发送给Client了,它真正应该要访问的是object对象的add方法;于是Binder驱动通知Server进程,调用你的object对象的add方法,然后把结果发给我,Sever进程收到这个消息,照做之后将结果返回驱动,驱动然后把结果返回给Client进程;于是整个过程就完成了。

由于驱动返回的objectProxy与Server进程里面原始的object是如此相似,给人感觉好像是直接把Server进程里面的对象object传递到了Client进程;因此,我们可以说Binder对象是可以进行跨进程传递的对象

但事实上我们知道,Binder跨进程传输并不是真的把一个对象传输到了另外一个进程;传输过程好像是Binder跨进程穿越的时候,它在一个进程留下了一个真身,在另外一个进程幻化出一个影子(这个影子可以很多个);Client进程的操作其实是对于影子的操作,影子利用Binder驱动最终让真身完成操作。

理解这一点非常重要;务必仔细体会。另外,Android系统实现这种机制使用的是代理模式, 对于Binder的访问,如果是在同一个进程(不需要跨进程),那么直接返回原始的Binder实体;如果在不同进程,那么就给他一个代理对象(影子);我们在系统源码以及AIDL的生成代码里面可以看到很多这种实现。

另外我们为了简化整个流程,隐藏了SM这一部分驱动进行的操作;实际上,由于SM与Server通常不在一个进程,Server进程向SM注册的过程也是跨进程通信,驱动也会对这个过程进行暗箱操作:SM中存在的Server端的对象实际上也是代理对象,后面Client向SM查询的时候,驱动会给Client返回另外一个代理对象。Sever进程的本地对象仅有一个,其他进程所拥有的全部都是它的代理。

一句话总结就是:Client进程只不过是持有了Server端的代理;代理对象协助驱动完成了跨进程通信。

Binder到底是什么?

Binder对于使用者来言,Server端的Binder和Client端的Binder 没有任何区别,一个Binder对象就代表了所有,不用关心实现的细节,甚至不用关心Binder驱动和SM的存在,这就是抽象

1 通常意义上讲,Binder是指的一种通信机制,我们说的aidl使用Binder进行通信,指的就是这种Binder的IPC机制

2 对于Server进程来说,Binder指的是Binder本地对象

3 对于Client进程来说,Binder指的是Binder代理对象,他只是Binder本地对象的一个远程代理,对这个Binder代理对象进行操作,会通过驱动最终发送到Binder本地对象去完成,对一个拥有Binder对象使用者来说,它无需关心Binder是代理还是本地,对于代理对象和本地对象操作并没有区别。

4 对于传输过程来言,Binder是可以进行跨进程传递的对象,Binder驱动会对具有跨进程传递的能力的对象进行处理,自动完成代理对象和本地对象的转换。

驱动里的Binder

我们现在知道,Server里的Binder指的是Binder本地对象,Client里的Binder指的是Binder代理对象,Binder在进行跨进程传递的时候,Binder驱动自动完成这俩种类型的转换,因此,Binder驱动必然保存了每一个,跨进程Binder对象的信息,在驱动中,Binder本地对象的代表是

一个叫做Binder_node的数据结构,Binder代理对象是一个binder_ref的数据结构,有的地方吧binder本地对象成为Binder实体,把Binder代理对象称作Binder引用(句柄),其实指的是Binder对象在驱动中的表现形式,理解就好

深入理解Java层的Binder

IBinder/IInterface/Binder/BinderProxy/Stub

我们使用AIDL接口的时候,经常会接触到这些类,那么这每个类代表的是什么呢?

1 IBinder是一个接口 ,他代表的是一种跨进程通信的能力,只要实现了这个接口,就能让这个对象进行跨进程传递,这是驱动底层支持的,跨进程数据流经驱动的时候,驱动会识别IBinder类型的数据,从而完成本地Binder和代理Binder的转换

2 IBinder负责数据传输,那么Client和Server的调用契约是什么呢?这里的IInterface代表的就是远程Server有什么能力,具体来说就是aidl里面的接口。

3 java层的Binder其实就是代表的本地Binder,BinderProxy是Binder的内部类,它代表远程进程的Binder的本地代理,俩个都继承IBinder,所以都具有跨进程通信的能力。在跨越进程的时候,Binder驱动会自动完成俩个对象的装换。在Client进程

onServiceConnected 这个方法返回的Binder指的是BinderProxy。复制代码

4 在使用aidl的时候,编译工具会给我们生成一个Stub的静态内部类,这个类继承了Binder,说明他是一个Binder本地对象,他实现了IInterface接口,说明他远程Server的能力,具体的IInterface相关需要我们自己去实现。

aidl的过程分析

现在我们通过一个aidl的使用,来分析整个通信过程中,各个角色到底做了什么,aidl到底是如何完成通信的。

首先定一个一个简单的aidl接口:

1
2
3
4
5
复制代码
// ICompute.aidl
package com.example.test.app;
interface ICompute {int add(int a, int b);
}
复制代码

然后用编译工具编译之后,可以得到对应的ICompute.java类,看看系统给我们生成的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
复制代码
package com.example.test.app;public interface ICompute extends android.os.IInterface {/*** Local-side IPC implementation stub class.*/public static abstract class Stub extends android.os.Binder implements com.example.test.app.ICompute {private static final java.lang.String DESCRIPTOR = "com.example.test.app.ICompute";/*** Construct the stub at attach it to the interface.*/public Stub() {this.attachInterface(this, DESCRIPTOR);}/*** Cast an IBinder object into an com.example.test.app.ICompute interface,* generating a proxy if needed.*/public static com.example.test.app.ICompute asInterface(android.os.IBinder obj) {if ((obj == null)) {return null;}android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);if (((iin != null) && (iin instanceof com.example.test.app.ICompute))) {return ((com.example.test.app.ICompute) iin);}return new com.example.test.app.ICompute.Stub.Proxy(obj);}@Overridepublic android.os.IBinder asBinder() {return this;}@Overridepublic boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException {switch (code) {case INTERFACE_TRANSACTION: {reply.writeString(DESCRIPTOR);return true;}case TRANSACTION_add: {data.enforceInterface(DESCRIPTOR);int _arg0;_arg0 = data.readInt();int _arg1;_arg1 = data.readInt();int _result = this.add(_arg0, _arg1);reply.writeNoException();reply.writeInt(_result);return true;}}return super.onTransact(code, data, reply, flags);}private static class Proxy implements com.example.test.app.ICompute {private android.os.IBinder mRemote;Proxy(android.os.IBinder remote) {mRemote = remote;}@Overridepublic android.os.IBinder asBinder() {return mRemote;}public java.lang.String getInterfaceDescriptor() {return DESCRIPTOR;}/*** Demonstrates some basic types that you can use as parameters* and return values in AIDL.*/@Overridepublic int add(int a, int b) throws android.os.RemoteException {android.os.Parcel _data = android.os.Parcel.obtain();android.os.Parcel _reply = android.os.Parcel.obtain();int _result;try {_data.writeInterfaceToken(DESCRIPTOR);_data.writeInt(a);_data.writeInt(b);mRemote.transact(Stub.TRANSACTION_add, _data, _reply, 0);_reply.readException();_result = _reply.readInt();} finally {_reply.recycle();_data.recycle();}return _result;}}static final int TRANSACTION_add = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);}/*** Demonstrates some basic types that you can use as parameters* and return values in AIDL.*/public int add(int a, int b) throws android.os.RemoteException;
}
复制代码

系统帮我们生成了这个文件之后,我们只需要继承ICompute.Stub这个抽象类,实现它的方法,然后在Service 的onBind方法里面返回就实现了AIDL。这个Stub类非常重要,具体看看它做了什么。

Stub继承自Binder,这就表明这个Stub是Binder本地对象,然后实现了ICompute接口,IComputer实现了IInterface,因此他携带Client需要的功能(这里是add),此类每部有一个proxy,也就是BinderProxy。

然后看看asInterface方法,看一下这个方法做了什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码
/*** Cast an IBinder object into an com.example.test.app.ICompute interface,* generating a proxy if needed.*/
public static com.example.test.app.ICompute asInterface(android.os.IBinder obj) {if ((obj == null)) {return null;}android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);if (((iin != null) && (iin instanceof com.example.test.app.ICompute))) {return ((com.example.test.app.ICompute) iin);}return new com.example.test.app.ICompute.Stub.Proxy(obj);
}复制代码

先看函数的参数 IBinder obj 这个是驱动给我们的,就是我们bind 一个service后onServiceConnected方法给我们的Binder,

如果是Binder代理对象,那就是
BinderProxy

类型;然后,正如上面自动生成的文档所说,它会试着查找Binder本地对象,如果找到,说明Client和Server都在同一个进程,这个参数直接就是本地对象,直接强制类型转换然后返回,如果找不到,说明是远程对象(处于另外一个进程)那么就需要创建一个Binde代理对象,让这个Binder代理实现对于远程对象的访问。一般来说,如果是与一个远程Service对象进行通信,那么这里返回的一定是一个Binder代理对象,这个IBinder参数的实际上是BinderProxy;

我们在来看看add方法的实现,在stub里面add是一个抽象方法,我么那些需要继承这个类去实现他,如果Client和Server是同一个进程,就直接调用这个方法,如果不是一个进程,这中间发生了什么 ,Client是如何调用Server方法的。

我们知道,远程调用需要用BinderProxy去完成的,这里面的Proxy就是,Proxy里的add如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码
Override
public int add(int a, int b) throws android.os.RemoteException {android.os.Parcel _data = android.os.Parcel.obtain();android.os.Parcel _reply = android.os.Parcel.obtain();int _result;try {_data.writeInterfaceToken(DESCRIPTOR);_data.writeInt(a);_data.writeInt(b);mRemote.transact(Stub.TRANSACTION_add, _data, _reply, 0);_reply.readException();_result = _reply.readInt();} finally {_reply.recycle();_data.recycle();}return _result;
}复制代码

它首先用Parcel把数据序列化了,然后调用了transact方法;这个transact到底做了什么呢?这个Proxy类在asInterface方法里面被创建,前面提到过,如果是Binder代理那么说明驱动返回的IBinder实际是BinderProxy, 因此我们的Proxy类里面的mRemote实际类型应该是BinderProxy;通过BinderProxy的transcact方法,把需要调用的方法,方法需要参数,方法的返回值都一同传给驱动,这时通信就交给驱动来完成了,这时Client就陷入内核态,Client进程add方法挂起等待返回,驱动经过一系列操作后唤醒Server进程,调用Server进程本地对象的onTranscact方法(由Server端线程池完成),我们再看Binder本地对象的onTranscact方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码
@Override
public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException {switch (code) {case INTERFACE_TRANSACTION: {reply.writeString(DESCRIPTOR);return true;}case TRANSACTION_add: {data.enforceInterface(DESCRIPTOR);int _arg0;_arg0 = data.readInt();int _arg1;_arg1 = data.readInt();int _result = this.add(_arg0, _arg1);reply.writeNoException();reply.writeInt(_result);return true;}}return super.onTransact(code, data, reply, flags);
}
复制代码

在Server进程里面,onTransact根据调用号(每个aidl都有一个编号,在跨进程的时候,不会传递函数,而是传递函数编号代表调用的是哪个函数)调用相关函数,在这个例子里面,调用了Binder本地对象的add方法,这个方法把结果返回给驱动,驱动唤起Client进程里面的线程,并把结果返回,于是一次进程调用就完成了。

至此,你应该对aidl里面,各个类和各个角色有一定的了解,他总是一种固定的模式,一个需要跨进程传递的对象,一定继承自Binder,如果是IBinder本地对象,一定实现了IInterface接口,如果是代理对象,一定是实现了IInterface接口,并持有IBinder对象引用.

Proxy和Stub不同,虽然都即是IInterface有是IBinder,不同的是stub采用继承(is IBinder),Proxy采用是组合(has IBinder),他们均实现了IInterface所有函数,不同的是,stub使用了策略模式虚函数,让子类实现函数,而Proxy使用了组合模式,为什么stub采用继承,而Proxy采用组合?事实上stub本身就是一个Binder,它本身就是一个能跨越进程边界传输的对象,所以它得继承IBinder实现transact这个函数从而得到跨越进程的能力(这个能力由驱动赋予)。Proxy类使用组合,是因为他不关心自己是什么,它也不需要跨越进程传输,它只需要拥有这个能力即可,要拥有这个能力,只需要保留一个对IBinder的引用。如果把这个过程做一个类比,在封建社会,Stub好比皇帝,可以号令天下,他生而具有这个权利(不要说宣扬封建迷信。。)如果一个人也想号令天下,可以,“挟天子以令诸侯”。为什么不自己去当皇帝,其一,一般情况没必要,当了皇帝其实限制也蛮多的是不是?我现在既能掌管天下,又能不受约束(Java单继承);其二,名不正言不顺啊,我本来特么就不是(Binder),你非要我是说不过去,搞不好还会造反。最后呢,如果想当皇帝也可以,那就是asBinder了。在Stub类里面,asBinder返回this,在Proxy里面返回的是持有的组合类IBinder的引用。

再去翻阅系统的ActivityManagerServer的源码,就知道哪一个类是什么角色了:IActivityManager是一个IInterface,它代表远程Service具有什么能力,ActivityManagerNative指的是Binder本地对象(类似AIDL工具生成的Stub类),这个类是抽象类,它的实现是ActivityManagerService;因此对于AMS的最终操作都会进入ActivityManagerService这个真正实现;同时如果仔细观察,ActivityManagerNative.java里面有一个非公开类ActivityManagerProxy, 它代表的就是Binder代理对象;是不是跟AIDL模型一模一样呢?那么ActivityManager是什么?他不过是一个管理类而已,可以看到真正的操作都是转发给ActivityManagerNative进而交给他的实现ActivityManagerService 完成的。,

参考 :Binder学习指南

Android进程间通信(IPC)机制Binder简要介绍和学习计划

Android Binder概述相关推荐

  1. Android Binder 全解析(1) -- 概述

    摘要 如果各位玩过<炉石传说>,那么可能对法师的职业卡「不稳定的传送门」很有印象,特别是没有欧洲玩家,经常能够拿到其他职业的强力单卡.Android 也提供了传送门,让我们可以像使用本地方 ...

  2. Android Binder设计与实现 - 实现篇(1)

    本文属于原创作品,转载请注明出处并放于明显位置,原文地址:http://www.cnblogs.com/albert1017/p/3849585.html 前言 在学习Android的Binder机制 ...

  3. Android Binder 分析——匿名共享内存(Ashmem)

    前面分析了 binder 中用来打包.传递数据的 Parcel,一般用来传递 IPC 中的小型参数和返回值.binder 目前每个进程 mmap 接收数据的内存是 1M,所以就算你不考虑效率问题用 P ...

  4. Anciroid的IPC机制-Binder概述

    在Linux系统中,是以进程为单位分配和管理资源的.出于保护机制,一个进程不能直接访问另一个进程的资源,也就是说,进程之间互相封闭.但是,在一个复杂的应用系统中,通常会使用多个相关的进程来共同完成一项 ...

  5. Android Binder 分析——匿名共享内存(好文)

    原址 文章目录 1. 原理概述 2. java 层接口 3. native 层接口 4. kernel 驱动 5. 例子 前面分析了 binder 中用来打包.传递数据的 Parcel,一般用来传递 ...

  6. 由浅入深 学习 Android Binder(十一) binder线程池

    Android Binder系列文章: 由浅入深 学习 Android Binder(一)- AIDL 由浅入深 学习 Android Binder(二)- bindService流程 由浅入深 学习 ...

  7. Android Binder设计与实现 - 设计篇

    本文属于原创作品,转载请注明出处并放于明显位置,原文地址:http://www.cnblogs.com/albert1017/p/3849585.html 前言 在学习Android的Binder机制 ...

  8. 由浅入深 学习 Android Binder(一)- AIDL

    Android Binder系列文章: 由浅入深 学习 Android Binder(一)- AIDL 由浅入深 学习 Android Binder(二)- bindService流程 由浅入深 学习 ...

  9. Android Binder(也许是最容易理解的)

    该文章是一个系列文章,是本人在Android开发的漫漫长途上的一点感想和记录,我会尽量按照先易后难的顺序进行编写该系列.该系列引用了<Android开发艺术探索>以及<深入理解And ...

最新文章

  1. 2012 12 12 (109)闪回操作
  2. 【Pthon入门学习】多级菜单小例子
  3. c++ 反射_Java代码审计基础之反射
  4. 使用命令行快速找出class文件所在的jar文件
  5. mysql将查到的数据删除_MySQL数据库的基本操作——增、删、改、查
  6. jQuery 表单选择器
  7. 我对CSS选择器的认识
  8. bool类型0和1真假_MySQL整理5—数据类型和运算符
  9. JEECG 社区开源项目下载(总览)
  10. 修复GRUB [转]
  11. nginx清缓存,网站刷新不过来时用
  12. SpringCloud Alibaba之Sentinelt组件
  13. 计算机视觉教程6-1:图解双目视觉系统与立体校正原理
  14. java 遍历json串_Java遍历Json数据
  15. 流程图、数据关系绘图神器yEd
  16. 各种进制换算成十进制
  17. BNUZ自动打卡系统
  18. 最能激发员工的不是薪水,而是成就感
  19. 计算机操作处于挂起状态,计算机重新启动操作可能处于挂起状态怎么办?
  20. 信息系统集成解决医院系统

热门文章

  1. android 横竖屏限制如何配置
  2. 关于刘冬大侠Spring.NET系列学习笔记3的一点勘正
  3. java 读取文件,内容方置Person 并写到另外地址
  4. 设计模式之“代理模式”
  5. NDK/JNI demo ( 五 ) ORB_SLAM2在Android上的移植过程
  6. asp.net 对xml文件的读写,添加,修改,删除操作
  7. C++中标准模板库std::pair的实现
  8. 【Qt】Log4Qt(一)下载、编译
  9. 【linux】串口编程(一)——配置串口
  10. java 二进制模块_深入Node模块Buffer-学会操作二进制