进阶之路 | 奇妙的IPC之旅
前言
本文已经收录到我的Github个人博客,欢迎大佬们光临寒舍:
学习清单:
- IPC的基础概念
- 多进程和多线程的概念
- Android中的序列化机制和Binder
- Android中的IPC方式
- Binder连接池的概念及运用
- 各种IPC的优缺点
一.为什么要学习IPC?
IPC是Inter-Process Communication的缩写,含义是进程间通信,是指两个进程之间进行数据交换的过程。
有些读者可能疑惑: "那什么是进程呢?什么是线程呢?多进程和多线程有什么区别呢?"
- 进程:是资源分配的最小单位,一般指一个执行单元,在PC和移动设备上指一个程序或应用。
- 线程:CPU调度的最小单位,线程是一种有限的系统资源。
两者关系:一个进程可包含多个线程,即一个应用程序上可以同时执行多个任务。
- 主线程(UI线程):UI操作
- 有限个子线程:耗时操作
注意:不可在主线程做大量耗时操作,会导致ANR(应用无响应)。解决办法:将耗时任务放在线程中。
IPC不是Android所特有的,Android中最有特色的IPC方式是Binder。而日常开发中涉及到的知识:AIDL,插件化,组件化等等,都离不开Binder。由此可见,IPC是挺重要的。
二.核心知识点归纳
2.1 Android中的多进程模式
Q1:开启多线程的方式:
- (常用)在
AndroidMenifest中给四大组件指定属性android:process
precess的命名规则:
- 默认进程:没有指定该属性则运行在默认进程,其进程名就是包名。
- 以“:”为命名开头的进程:“:”的含义是在进程名前面加上包名,属于当前应用私有进程
- 完整命名的进程:属于全局进程,其他应用可以通过ShareUID方式和他跑在用一个进程中(需要ShareUID和签名相同)。
- (不常用)通过JNI在native层fork一个新的进程。
Q2:多进程模式的运行机制:
Andoird为每个进程分配了一个独立的虚拟机,不同虚拟机在内存分配上有不同的地址空间,这也导致了不同虚拟机中访问同一个对象会产生多份副本。
带来四个方面的问题:
- 静态变量和单例模式失效-->原因:不同虚拟机中访问同一个对象会产生多份副本。
- 线程同步机制失效-->原因:内存不同,线程无法同步。
- SharedPreference的可靠性下降-->原因:底层是通过读写XML文件实现的,发生并发问题。
- Application多次创建-->原因:Android系统会为新的进程分配独立虚拟机,相当于应用重新启动了一次。
2.2 IPC基础概念
这里主要介绍三方面内容:
- Serializable
- Parcelable
- Binder
只有熟悉这三方面的内容,才能更好理解
IPC的各种方式
2.2.1 什么是序列化
- 含义:序列化表示将一个对象转换成可存储或可传输的状态。序列化后的对象可以在网络上进行传输,也可以存储到本地。
- 使用场景:需要通过
Intent和Binder等传输类对象就必须完成对象的序列化过程。 - 两种方式:实现
Serializable/Parcelable接口。
2.2.2 Serializable接口
Java提供的序列化接口,使用方式比较简单:
- 实体类实现
Serializable- 手动设置/系统自动生成
serialVersionUID
//Serializable Demo
public class Person implements Serializable{
private static final long serialVersionUID = 7382351359868556980L;
private String name;
private int age;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
这里特别注意一下
serialVersionUID:
- 含义:是
Serializable接口中用来辅助序列化和反序列化过程。- 注意:原则上序列化后的数据中的
serialVersionUID要和当前类的serialVersionUID相同才能正常的序列化。当类发生非常规性变化(修改了类名/修改了成员变量的类型)的时候,序列化失败。
2.2.3 Parcelable接口
是
Android中的序列化接口,使用的时候,类中需要实现下面几点:
- 实现
Parcelable接口- 内容描述
- 序列化方法
- 反序列化方法
public class User implements Parcelable {
public int userId;
public String userName;
public boolean isMale;
public Book book;
public User() {
}
public User(int userId, String userName, boolean isMale) {
this.userId = userId;
this.userName = userName;
this.isMale = isMale;
}
//返回内容描述 return 0 即可
public int describeContents() {
return 0;
}
//序列化
public void writeToParcel(Parcel out, int flags) {
out.writeInt(userId);
out.writeString(userName);
out.writeInt(isMale ? 1 : 0);
out.writeParcelable(book, 0);
}
//反序列化
public static final Parcelable.Creator<User> CREATOR = new Parcelable.Creator<User>() {
//从序列化的对象中创建原始对象
public User createFromParcel(Parcel in) {
return new User(in);
}
public User[] newArray(int size) {
return new User[size];
}
};
//从序列化的对象中创建原始对象
private User(Parcel in) {
userId = in.readInt();
userName = in.readString();
isMale = in.readInt() == 1;
book = in.readParcelable(Thread.currentThread().getContextClassLoader());
}
@Override
public String toString() {
return String.format("User:{userId:%s, userName:%s, isMale:%s}, with child:{%s}",
userId, userName, isMale, book);
}
}
2.2.4 Serializable和Parcelable接口的比较
Serializable接口 |
Parcelable接口 |
|
|---|---|---|
| 平台 | Java | Andorid |
| 序列化原理 | 将一个对象转换成可存储或者可传输的状态 | 将对象进行分解,且分解后的每一部分都是传递可支持的数据类型 |
| 优缺点 | 优点:使用简单 缺点:开销大(因为需要进行大量的IO操作) | 优点:高效 缺点:使用麻烦 |
| 使用场景 | 将对象序列化到存储设备或者通过网络传输 | 主要用在内存序列化上 |
2.2.5 Binder
Q1:Binder是什么?
- 从API角度:是一个类,实现
IBinder接口。 - 从IPC角度:是
Android中的一种跨进程通信方式。 - 从Framework角度:是
ServiceManager,连接各种Manager和相应ManagerService的桥梁。 - 从应用层:是客户端和服务端进行通信的媒介。客户端通过它可获取服务端提供的服务或者数据。
Q2:Android是基于Linux内核基础上设计的,却没有把管道/消息队列/共享内存/信号量/Socket等一些IPC通信手段作为Android的主要IPC方式,而是新增了Binder机制,其优点有:
A1:传输效率高、可操作性强
传输效率主要影响因素是内存拷贝的次数,拷贝次数越少,传输速率越高。几种数据传输方式比较
| 方式 | 拷贝次数 | 操作难度 |
|---|---|---|
| Binder | 1 | 简易 |
| 消息队列 | 2 | 简易 |
| Socket | 2 | 简易 |
| 管道 | 2 | 简易 |
| 共享内存 | 0 | 复杂 |
从Android进程架构角度分析:对于消息队列、Socket和管道来说,数据先从发送方的缓存区拷贝到内核开辟的缓存区中,再从内核缓存区拷贝到接收方的缓存区,一共两次拷贝,如图:

对Binder来说:数据从发送方的缓存区拷贝到内核的缓存区,而接收方的缓存区与内核的缓存区是映射到同一块物理地址的,节省了一次数据拷贝的过程
A2:实现C/S架构方便
Linux的众IPC方式除了Socket以外都不是基于C/S架构,而Socket主要用于网络间的通信且传输效率较低。Binder基于C/S架构 ,Server端与Client端相对独立,稳定性较好。
A3:安全性高
传统
LinuxIPC的接收方无法获得对方进程可靠的UID/PID,从而无法鉴别对方身份;而Binder机制为每个进程分配了UID/PID且在Binder通信时会根据UID/PID进行有效性检测。
Q3:Binder框架定义了哪四个角色呢?
A1:Server&Client
服务器&客户端。在
Binder驱动和Service Manager提供的基础设施上,进行Client-Server之间的通信。
A2:ServiceManager:
服务的管理者,将
Binder名字转换为Client中对该Binder的引用,使得Client可以通过Binder名字获得Server中Binder实体的引用。

A3:Binder驱动
- 与硬件设备没有关系,其工作方式与设备驱动程序是一样的,工作于内核态。
- 提供
open()、mmap()、poll()、ioctl()等标准文件操作。 - 以字符驱动设备中的
misc设备注册在设备目录/dev下,用户通过/dev/binder访问该它。 - 负责进程之间
binder通信的建立,传递,计数管理以及数据的传递交互等底层支持。 - 驱动和应用程序之间定义了一套接口协议,主要功能由
ioctl()接口实现,由于ioctl()灵活、方便且能够一次调用实现先写后读以满足同步交互,因此不必分别调用write()和read()接口。 - 其代码位于
linux目录的drivers/misc/binder.c中。
ioctl(input/output control)是一个专用于设备输入输出操作的系统调用,该调用传入一个跟设备有关的请求码,系统调用的功能完全取决于请求码
Q4:Binder 工作原理是什么
- 服务器端:在服务端创建好了一个
Binder对象后,内部就会开启一个线程用于接收Binder驱动发送的消息,收到消息后会执行onTranscat(),并按照参数执行不同的服务端代码。 Binder驱动:在服务端成功创建Binder对象后,Binder驱动也会创建一个mRemote对象(也是Binder类),客户端可借助它调用transcat()即可向服务端发送消息。- 客户端:客户端要想访问
Binder的远程服务,就必须获取远程服务的Binder对象在Binder驱动层对应的mRemote引用。当获取到mRemote对象的引用后,就可以调用相应Binder对象的暴露给客户端的方法。

当发出远程请求后客户端会挂起,直到返回数据才会唤醒
Client
Q5:当服务端进程异常终止的话,造成Binder死亡的话,怎么办?
在客户端绑定远程服务成功后,给Binder设置死亡代理,当Binder死亡的时候,我们会收到通知,从而重新发起连接请求。
private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient(){
@Override
public void binderDied(){
if(mBookManager == null){
return;
}
mBookManager.asBinder().unlinkToDeath(mDeathRecipient,0);
mBookManager = null;
// TODO:这里重新绑定远程Service
}
}
mService = IBookManager.Stub.asInterface(binder);
binder.linkToDeath(mDeathRecipient,0);
2.3 Android中的IPC方式
Android中的IPC方式有很多种,但本质都是基于Binder构建的

2.3.1 Bundle
- 原理:
Bundle底层实现了Parcelable接口,它可方便的在不同的进程中传输。 - 注意:Bundle不支持的数据类型无法在进程中被传递。
小课堂测试:在A进程进行计算后的结果不是
Bundle所支持的数据类型,该如何传给B进程?答案: 将在A进程进行的计算过程转移到B进程中的一个
Service里去做,这样可成功避免进程间的通信问题。
Intent和Bundle的区别与联系:
Intent底层其实是通过Bundle进行传递数据的- 使用难易:
Intent比较简单,Bundle比较复杂Intent旨在数据传递,bundle旨在存取数据
2.3.2 文件共享
- 概念:两个进程通过读/写同一个文件来交换数据。比如A进程把数据写入文件,B进程通过读取这个文件来获取数据。
- 适用场景:对数据同步要求不高的进程之间进行通信,并且要妥善处理并发读/写的问题。
- 特殊情况:
SharedPreferences也是文件存储的一种,但不建议采用。因为系统对SharedPreferences的读/写有一定的缓存策略,即在内存中有一份该文件的缓存,因此在多进程模式下,其读/写会变得不可靠,甚至丢失数据。
2.3.3 AIDL
2.3.3.1 概念
AIDL(Android Interface Definition Language,Android接口定义语言):如果在一个进程中要调用另一个进程中对象的方法,可使用AIDL生成可序列化的参数,AIDL会生成一个服务端对象的代理类,通过它客户端实现间接调用服务端对象的方法。
2.3.3.2 支持的数据类型
- 基本数据类型
String和CharSequence
想了解
String和CharSequence区别的读者,可以看下这篇文章:String和CharSequence的区别
ArrayList、HashMap且里面的每个元素都能被AIDL支持- 实现
Parcelable接口的对象 - 所有
AIDL接口本身
注意:除了基本数据类型,其它类型的参数必须标上方向:
in、out或inout,用于表示在跨进程通信中数据的流向。
2.3.3.3 两种AIDL文件
- 用于定义
Parcelable对象,以供其他AIDL文件使用AIDL中非默认支持的数据类型的。 - 用于定义方法接口,以供系统使用来完成跨进程通信的。
注意:
- 自定义的
Parcelable对象必须把Java文件和自定义的AIDL文件显式的import进来,无论是否在同一包内。AIDL文件用到自定义Parcelable的对象,必须新建一个和它同名的AIDL文件,并在其中声明它为Parcelable类型。
2.3.3.4 本质,关键类和方法
a:本质是系统提供了一套可快速实现Binder的工具。
b:关键类和方法是什么?
AIDL接口:继承IInterface。Stub类:Binder的实现类,服务端通过这个类来提供服务。Proxy类:服务器的本地代理,客户端通过这个类调用服务器的方法。asInterface():客户端调用,将服务端的返回的Binder对象,转换成客户端所需要的AIDL接口类型对象。
返回对象:
- 若客户端和服务端位于同一进程,则直接返回
Stub对象本身;- 否则,返回的是系统封装后的
Stub.proxy对象。
asBinder():返回代理Proxy的Binder对象。onTransact():运行服务端的Binder线程池中,当客户端发起跨进程请求时,远程请求会通过系统底层封装后交由此方法来处理。transact():运行在客户端,当客户端发起远程请求的同时将当前线程挂起。之后调用服务端的onTransact()直到远程请求返回,当前线程才继续执行。

2.3.3.5 实现方法
如果感兴趣的读者想要了解具体的
AIDL实现IPC的流程,笔者分享一篇文章:android跨进程通信(IPC):使用AIDL
A.服务端:
- 创建一个
aidl文件; - 创建一个
Service,实现AIDL的接口函数并暴露AIDL接口。
B.客户端:
- 通过
bindService绑定服务端的Service; - 绑定成功后,将服务端返回的
Binder对象转化成AIDL接口所属的类型,进而调用相应的AIDL中的方法。
总结:服务端里的某个
Service给和它绑定的特定客户端进程提供Binder对象,客户端通过AIDL接口的静态方法asInterface()将Binder对象转化成AIDL接口的代理对象,通过这个代理对象就可以发起远程调用请求。
2.3.3.6 可能产生ANR的情形
A.客户端:
- 调用服务端的方法是运行在服务端的
Binder线程池中,若主线程所调用的方法里执行了较耗时的任务,同时会导致客户端线程长时间阻塞,易导致客户端ANR。 - 在
onServiceConnected()和onServiceDisconnected()里直接调用服务端的耗时方法,易导致客户端ANR。
B.服务端:
- 服务端的方法本身就运行在服务端的
Binder线程中,可在其中执行耗时操作,而无需再开启子线程。 - 回调客户端
Listener的方法是运行在客户端的Binder线程中,若所调用的方法里执行了较耗时的任务,易导致服务端ANR。
解决客户端频繁调用服务器方法导致性能极大损耗的办法:实现观察者模式。
即当客户端关注的数据发生变化时,再让服务端通知客户端去做相应的业务处理。
2.3.3.7 解注册失败的问题
- 原因:
Binder进行对象传输实际是通过序列化和反序列化进行,即Binder会把客户端传递过来的对象重新转化并生成一个新的对象,虽然在注册和解注册的过程中使用的是同一个客户端传递的对象,但经过Binder传到服务端后会生成两个不同的对象。另外,多次跨进程传输的同一个客户端对象会在服务端生成不同的对象,但它们在底层的Binder对象是相同的。 - 解决办法:当客户端解注册的时候,遍历服务端所有的
Listener,找到和解注册Listener具有相同的Binder对象的服务端Listener,删掉即可。
需要用到
RemoteCallBackList:Android系统专门提供的用于删除跨进程listener的接口。其内部自动实现了线程同步的功能。
2.3.4 Messager
Q1.什么是Messager?
A1:Messager是轻量级的IPC方案,通过它可在不同进程中传递Message对象。
Messenger.send(Message);
Q2:特点是什么?
- 底层实现是
AIDL,即对AIDL进行了封装,更便于进行进程间通信。 - 其服务端以串行的方式来处理客户端的请求,不存在并发执行的情形,故无需考虑线程同步的问题。
- 可在不同进程中传递
Message对象,Messager可支持的数据类型即Messenge可支持的数据类型。
Messenge可支持的数据类型:
arg1、arg2、what字段:int型数据obj字段:Object对象,支持系统提供的Parcelable对象setData:Bundle对象
- 有两个构造函数,分别接收
Handler对象和Binder对象。
Q3:实现的方法:
读者如果对
Messenger的具体使用感兴趣的话,可以看下这篇文章:IPC-Messenger使用实例
A1:服务端:
- 创建一个
Service用于提供服务; - 其中创建一个
Handler用于接收客户端进程发来的数据; - 利用
Handler创建一个Messenger对象; - 在
Service的onBind()中返回Messenger对应的Binder对象。
A2:客户端:
通过
bindService绑定服务端的Service;通过绑定后返回的
IBinder对象创建一个Messenger,进而可向服务器端进程发送Message数据。(至此只完成单向通信)在客户端创建一个
Handler并由此创建一个Messenger,并通过Message的replyTo字段传递给服务器端进程。服务端通过读取Message得到Messenger对象,进而向客户端进程传递数据。(完成双向通信)
Q4:缺点:
- 主要作用是传递
Message,难以实现远程方法调用。 - 以串行的方式处理客户端发来的消息的,不适合高并发的场景。
解决方式:使用
AIDL的方式处理IPC以应对高并发的场景
2.3.5 ContentProvider
ContentProvider是Android提供的专门用来进行不同应用间数据共享的方式,底层同样是通过Binder实现的。
- 除了
onCreate()运行在UI线程中,其他的query()、update()、insert()、delete()和getType()都运行在Binder线程池中。 CRUD四大操作存在多线程并发访问,要注意在方法内部要做好线程同步。- 一个
SQLiteDatabase内部对数据库的操作有同步处理,但多个SQLiteDatabase之间无法同步。
2.3.6 Socket
Socket不仅可以跨进程,还可以跨设备通信
Q1:使用类型是什么?
- 流套接字:基于
TCP协议,采用流的方式提供可靠的字节流服务。 - 数据流套接字:基于
UDP协议,采用数据报文提供数据打包发送的服务。
Q2:实现方法是什么?
A1:服务端:
- 创建一个
Service,在线程中建立TCP服务、监听相应的端口等待客户端连接请求; - 与客户端连接时,会生成新的
Socket对象,利用它可与客户端进行数据传输; - 与客户端断开连接时,关闭相应的
Socket并结束线程。
A2:客户端:
- 开启一个线程、通过
Socket发出连接请求; - 连接成功后,读取服务端消息;
- 断开连接,关闭
Socket。
2.3.7 优缺点比较
| 名称 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
Bundle |
简单易用 | 只能传输Bundle支持的数据类型 |
四大组件间的进程间通信 |
| 文件共享 | 简单易用 | 不适合高并发场景,无法做到进程间的即时通信 | 无并发访问,交换简单数据且实时性不高 |
AIDL |
支持一对多并发和实时通信 | 使用稍复杂,需要处理线程同步 | 一对多且有RPC需求 |
Messenger |
支持一对多串行通信 | 不能很好处理高并发,不支持RPC,只能传输Bundle支持的数据类型 |
低并发的一对多 |
ContentProvider |
支持一对多并发数据共享 | 可理解为受约束的AIDL |
一对多进程间数据共享 |
Socket |
支持一对多并发数据共享 | 实现细节繁琐 | 网络数据交换 |
2.4 Binder连接池
有多个业务模块都需要
AIDL来进行IPC,此时需要为每个模块创建特定的aidl文件,那么相应的Service就会很多。必然会出现系统资源耗费严重、应用过度重量级的问题。因此需要Binder连接池,通过将每个业务模块的Binder请求统一转发到一个远程Service中去执行的方式,从而避免重复创建Service。
Q1:工作原理是什么?
每个业务模块创建自己的AIDL接口并实现此接口,然后向服务端提供自己的唯一标识和其对应的Binder对象。服务端只需要一个Service,服务器提供一个queryBinder接口,它会根据业务模块的特征来返回相应的Binder对像,不同的业务模块拿到所需的Binder对象后就可进行远程方法的调用了。

Q2:实现方式是什么?
读者如果对具体的实现方式感兴趣的话,可以看一下这篇文章:Android IPC机制(四):细说Binder连接池
- 为每个业务模块创建
AIDL接口并具体实现 - 为
Binder连接池创建AIDL接口IBinderPool.aidl并具体实现 - 远程服务
BinderPoolService的实现,在onBind()返回实例化的IBinderPool实现类对象 Binder连接池的具体实现,来绑定远程服务- 客户端的调用
三.碎碎念
恭喜你,已经完成了这次奇妙的
IPC之旅了,如果你感到对概念还是有点模糊不清的话,没关系,很正常,不用太纠结于细节,你可以继续进行下面的旅程了,未来的你,再看这篇文章,也许会有更深的体会,到时候就会有茅舍顿开的感觉了。未来的你,一定会更优秀!!!路漫漫其修远兮,吾将上下而求索。《离骚》--屈原
如果文章对您有一点帮助的话,希望您能点一下赞,您的点赞,是我前进的动力
本文参考链接:
- String和CharSequence的区别
- android跨进程通信(IPC):使用AIDL
- IPC-Messenger使用实例
- Android IPC机制(四):细说Binder连接池
- 《Android 开发艺术探索》
- 要点提炼|开发艺术之IPC
进阶之路 | 奇妙的IPC之旅的更多相关文章
- 进阶之路 | 奇妙的Window之旅
前言 本文已经收录到我的Github个人博客,欢迎大佬们光临寒舍: 我的GIthub博客 学习清单: Window&WindowManagerService Window&Window ...
- 进阶之路 | 奇妙的View之旅
前言 本文已经收录到我的Github个人博客,欢迎大佬们光临寒舍: 我的GIthub博客 学习清单: View是什么 View的位置参数 View的触控 View的滑动 涉及以下各个知识点: View ...
- 进阶之路 | 奇妙的Animation之旅
前言 本文已经收录到我的Github个人博客,欢迎大佬们光临寒舍: 我的GIthub博客 学习清单: 动画的种类 自定义View动画 View动画的特殊使用场景 属性动画 使用动画的注意事项 一.为什 ...
- 进阶之路 | 奇妙的Thread之旅
前言 本文已经收录到我的Github个人博客,欢迎大佬们光临寒舍: 我的GIthub博客 需要已经具备的知识: Thread的基本概念及使用 AsyncTask的基本概念及使用 学习清单: 线程概述 ...
- 进阶之路 | 奇妙的Handler之旅
前言 本文已经收录到我的Github个人博客,欢迎大佬们光临寒舍: 我的GIthub博客 需要已经具备的知识: Handler的基本概念及使用 学习导图: 一.为什么要学习Handler? 在Andr ...
- 进阶之路 | 奇妙的Activity之旅
前言 本文已经收录到我的Github个人博客,欢迎大佬们光临寒舍: 我的GIthub博客 本篇文章需要已经具备的知识: Activity的基本概念 AndroidManifest.xml的基本概念 学 ...
- 进阶之路 | 奇妙的Drawable之旅
前言 本文已经收录到我的Github个人博客,欢迎大佬们光临寒舍: 我的GIthub博客 学习清单: Drawable简介 Drawable分类 自定义Drawable 一.为什么要学习Drawabl ...
- GO语言的进阶之路-爬虫进阶之路
GO语言的进阶之路-爬虫进阶之路 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 网络爬虫是一种自动获取网页内容的程序,是搜索引擎的重要组成部分.我们今天要介绍的就是一个简单的网络爬 ...
- SSH进阶之路
[SSH进阶之路]Hibernate基本原理(一) 在开始学Hibernate之前,一直就有人说:Hibernate并不难,无非是对JDBC进一步封装.一句不难,难道是真的不难还是眼高手低 ...
随机推荐
- Django admin的简单使用
1.汉化网站 位置:setting.py LANGUAGE_CODE = 'zh-hans' 2.注册站点(添加表) 位置:admin.py from app01 import models admi ...
- PHP——foreach
一.foreach 在PHP中foreach循环语句,常用于遍历数组,一般有两种使用方式:不取下标.取下标 (一)只取值,不取下标 <?php foreach (数组 as 值){ //执行的任 ...
- xlwings excel(二)
常用函数和方法 Book 工作簿常用的apiwb=xw.books[‘工作簿名称'] wb.activate()激活为当前工作簿 wb.fullname 返回工作簿的绝对路径 wb.name 返回工作 ...
- 做.net的成为 微软mvp 是一个目标吧。
mvp 的评比 需要好多好多 绩效考核 比如博客排名,比如发表的文章数.
- 用CSS实现横向滚动条
在进行app制作时,需要使用横向滚动条是内容展示更完善 首先,这是html代码: 这是CSS代码: 要点: 设置显示内容的宽 white-space是防止内容自动折行 overflow-y设置为hid ...
- [集训]Trominoes,钩子公式运用
题意 用这四种骨牌密铺n*m的正方形矩阵,可以不选,求方案数.n*m<=1E8.多组询问. 思考 用如上的表达难以进行计算,尝试转化为一种新的组合解释. 若从右上角开始填起,我们强制要求里面的轮 ...
- [集训]dance
题意 http://uoj.ac/problem/77 思考 显然能转化为最小割模型.若没有pi的代价,则对于第i个格子,可以让源点连向第i个点,容量为黑色收益,再连向汇点,容量为白色收益.再考虑pi ...
- Centos与Ubuntu
共同点 1.两个系统都分别有桌面系统与服务器系统,不过ubuntu的桌面从外观上来看要比centos的漂亮 不同点 1.centos中新建的普通用户是没有sudo权限的,如果想让普通用户拥有sudo权 ...
- wirkshark过滤规则
https://blog.csdn.net/wojiaopanpan/article/details/69944970
- java内存模型梳理
java内存模型 内存模型和内存结构区别 它们是两个概念. 内存模型是和jvm多线程相关的. 内存结构是指的jvm内存结构. 内存模型的作用 内存模型简称JMM JMM是决定一个线程对共享变量的写入时 ...