一、IPC介绍

IPC是Inter-Process Communication的缩写,含义是进程间通信或者跨进程通信,是指两个进程之间进行数据交换的过程。那么为什么需要开启多进程呢?原因有二,其一是一个应用因为某些原因需要独立运行在某个进程中。其二可能是加大一个应用可使用的内存空间,早期的一些版本,一个进程最大的内存空间是16MB。

1、Android中开启多进程

Android中开启多进程的方法只有一个,那就是给四大组件在Manifest中指定android:process属性,除此之外没有其他办法,也就说没有办法给一个线程或者一个实体类指定其运行时的进程。当然,还有另外一种非常规办法开启多进程,那就是通过JNI在Native层fork一个新的进程,这种方式不是常规的,因此暂时不考虑这种方式。

<activity android:name="com.zbh.process.MainActivity" ><intent-filter><action android:name="" /><category android:name="" /></intent-filter>
</activity>
<activity android:name="com.zbh.process.SecondActivity" android:process=":remote" >
</activity>
<activity android:name="com.zbh.process.ThirdActivity" android:process="com.zbh.process.remote" >
</activity>

这个Manifest对应的应用启动后,启动了SecondActivity和ThirdActivity后,会有3个进程存在。进程1是,其名字是com.zbh.process主进程,即默认进来时的进程。进程2是名字为com.zbh.process:remote的进程。进程3是名字为com.zbh.process.remote的进程。那么进程2和进程3有什么区别呢?
:是一种简写,是当前主进程的附属进程,属于私有进程。而com.zbh.process.remote是普通进程,属于全局进程。私有进程,其他应用组件是不可以和它跑在同一个进程中的。而全局进程,其他应用是可以通过ShareUID的形式和它跑在同一个进程中。当然,两个应该通过ShareUID的形式跑在同一个进程是有要求的,需要这两个应用有相同的ShareUID以及签名才可以。ShareUID可以在这个应用的Manifest中进行设置。

2、多进程产生的影响

(1)静态成员或单例模式失效
每个进程都会分配一个独立的虚拟机,不同的虚拟机在内存上有不同的地址空间,这就导致在不同的虚拟机中访问同一个类对象,会产生多份副本。那么假设进程1修改了静态变量的值,进程2再去读的时候,由于是多份副本,它读到的是自己进程里的那份副本,所以就会出现值没有改变的现象。
(2)线程同步机制失效
这个问题的出现和(1)是一样的,既然都不是同一块内存,那么不管锁对象还是锁全局类都没办法保证线程同步,因为不同进程锁的对象不是同一个。
(3)SharedPreferences可靠性下降
这是因为SP不支持两个进程同时去执行读写操作,否则会导致一定几率的数据丢失,也就是并发问题不支持。
(4)Application会多次创建
多进程模式下,不同的进程组件会拥有独立的虚拟机、Application和内存空间。每一个进程的启动,都对应一个Application的创建。

二、Serializable和Parcelable接口

1、Serializable

Serializable是Java提供的一个序列化接口,使用非常简单。

public class Book implements Serializable {/*** serialVersionUID的作用是保证反序列的时候会校验失败* 序列化的时候,把数据从内存写到流里,会把这个ID也写进去* 反序列化的时候,把数据从流里读出来存刀内存对象里,* 在这之前会拿Book对象里的这个ID和流里的ID进行对比和校验,* 看是否一致来判断是否是同一个类对象*/private static final long serialVersionUID = 4676767961613L;public String name;public int id;public int pageCount;public String author;}
// 序列化过程
try {Book book = new Book("Android开发艺术探讨", 1, 507, "任玉刚");ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("cache.txt"));oos.writeObject(book);oos.close();
} catch (Exception e) {
}// 反序列化过程
try {ObjectInputStream ois = new ObjectInputStream(new FileInputStream("cache.txt"));Book book = (Book) ois.readObject();ois.close();
} catch (Exception e) {
}
// 可以得出一个结论是,序列化前和反序列化后,得到的并不是同一个对象。

2、Parcelable

public class MyBook implements Parcelable {private String mName;private String mAuthor;public MyBook(String name, String author) {this.mName = name;this.mAuthor = author;}@Overridepublic int describeContents() {return 0;}/*** 序列化到Parcel中*/@Overridepublic void writeToParcel(Parcel dest, int flags) {dest.writeString(mName);dest.writeString(mAuthor);}/*** 通过CREATOR的createFromParcel反序列化到对象内存中*/public static final Parcelable.Creator<MyBook> CREATOR = new Creator<MyBook>() {@Overridepublic MyBook createFromParcel(Parcel source) {String name = source.readString();String author = source.readString();return new MyBook(name, author);}@Overridepublic MyBook[] newArray(int size) {return new MyBook[size];}};}

三、AIDL基本使用

普通的Service的使用不涉及到Binder进程间通信的问题,所以比较简单。而远程服务是基于Binder进行跨进程通信的,所以我们用AIDL来分析Binder的工作机制。
(1)先建一个服务端的项目,AIDL_Server
(2)建一个AIDL的目录及IPerson.aidl,其内容如下

/**
* IPerson.aidl
*/
interface IPerson {void setName(String name);String getName();
}

(3)创建一个service

public class MyService extends Service {private Binder mBinder;@Overridepublic void onCreate() {super.onCreate();Log.i("zhangbh", "service 启动了");mBinder = new IPerson.Stub() {@Overridepublic String getName() throws RemoteException {return "Hello ABC";}@Overridepublic void setName(String name) throws RemoteException {Log.i("zhangbh", "name is:" + name);}};}@Overridepublic IBinder onBind(Intent intent) {return mBinder;}}

(4)配置Manifest

<serviceandroid:name=".MyService"android:enabled="true"android:exported="true"><intent-filter><action android:name="test.service.start.action" /><category android:name="android.intent.category.DEFAULT" /></intent-filter>
</service>

(5)建一个客户端的项目,AIDL_Client,把AIDL_Server项目aidl目录下所有文件直接复制过来
(6)开启服务及调用服务

public class MainActivity extends AppCompatActivity {private IPerson mPerson;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);findViewById(R.id.tv_start).setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {startService();}});findViewById(R.id.tv_send).setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {try {mPerson.setName("Hello world");} catch (Exception e) {}}});findViewById(R.id.tv_get).setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {try {Log.i("zhangbh", mPerson.getName());} catch (Exception e) {}}});}private void startService() {Intent intent = new Intent();intent.setAction("test.service.start.action");intent.setPackage("com.zbh.aidl_server");intent.addCategory(Intent.CATEGORY_DEFAULT);bindService(intent, new ServiceConnection() {@Overridepublic void onServiceConnected(ComponentName name, IBinder service) {Log.i("zhangbh", "服务绑定成功");mPerson = IPerson.Stub.asInterface(service);}@Overridepublic void onServiceDisconnected(ComponentName name) {}}, BIND_AUTO_CREATE);}
}

(7)这样子就是借助AIDL完成了跨进程通信了,核心是Binder,但是怎么用,在哪里用,我们是没有看到,其实这些AIDL通过模板给我们生成了固定的代码。其源码如下

/**
* 在Build目录下自动生成的IPerson接口
*/
public interface IPerson extends android.os.IInterface {/*** Local-side IPC implementation stub class.*/public static abstract class Stub extends android.os.Binder implements com.zbh.aidl_server.IPerson {private static final java.lang.String DESCRIPTOR = "com.zbh.aidl_server.IPerson";/*** Construct the stub at attach it to the interface.*/public Stub() {this.attachInterface(this, DESCRIPTOR);}/*** Cast an IBinder object into an com.zbh.aidl_server.IPerson interface,* generating a proxy if needed.*/public static com.zbh.aidl_server.IPerson asInterface(android.os.IBinder obj) {if ((obj == null)) {return null;}android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);if (((iin != null) && (iin instanceof com.zbh.aidl_server.IPerson))) {return ((com.zbh.aidl_server.IPerson) iin);}return new com.zbh.aidl_server.IPerson.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 {java.lang.String descriptor = DESCRIPTOR;switch (code) {case INTERFACE_TRANSACTION: {reply.writeString(descriptor);return true;}case TRANSACTION_setName: {data.enforceInterface(descriptor);java.lang.String _arg0;_arg0 = data.readString();this.setName(_arg0);reply.writeNoException();return true;}case TRANSACTION_getName: {data.enforceInterface(descriptor);java.lang.String _result = this.getName();reply.writeNoException();reply.writeString(_result);return true;}default: {return super.onTransact(code, data, reply, flags);}}}private static class Proxy implements com.zbh.aidl_server.IPerson {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;}@Overridepublic void setName(java.lang.String name) throws android.os.RemoteException {android.os.Parcel _data = android.os.Parcel.obtain();android.os.Parcel _reply = android.os.Parcel.obtain();try {_data.writeInterfaceToken(DESCRIPTOR);_data.writeString(name);mRemote.transact(Stub.TRANSACTION_setName, _data, _reply, 0);_reply.readException();} finally {_reply.recycle();_data.recycle();}}@Overridepublic java.lang.String getName() throws android.os.RemoteException {android.os.Parcel _data = android.os.Parcel.obtain();android.os.Parcel _reply = android.os.Parcel.obtain();java.lang.String _result;try {_data.writeInterfaceToken(DESCRIPTOR);mRemote.transact(Stub.TRANSACTION_getName, _data, _reply, 0);_reply.readException();_result = _reply.readString();} finally {_reply.recycle();_data.recycle();}return _result;}}static final int TRANSACTION_setName = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);static final int TRANSACTION_getName = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);}public void setName(java.lang.String name) throws android.os.RemoteException;public java.lang.String getName() throws android.os.RemoteException;
}

IPerson接口继承于IInterface接口,所有在Binder中传输的接口都要继承IInterface接口。IPerson接口里一共有一个静态内部类Stub和setName/getName方法,其中setName和getName就是我们在IPerson.aidl文件里声明的接口方法,最核心的就是这个静态内部类Stub,接下来我们就分析一个这个静态内部类Stub。

客户端ServiceConnected时拿的的IBinder要分为两种情况:
情况一,服务端和客户端在同一个进程中,那么这个IBinder实际上就是我们在服务端里创建并通过onBind返回的Stub对象。
情况二,服务端和客户端不在同一个进程中,即远程调用,这种情况就属于跨进程通信了。那么这个IBinder实际上是BinderProxy代理类,这个创建过程对我们是隐藏的。
从上面的代码我们可以看出,Stub实际上就一个Binder类,同时还实现了IPerson接口。Stub类主要有几个核心方法,
(1)构造方法里把DESCRIPTOR传递给父类Binder,DESCRIPTOR是一个唯一标识,一般用当前aidl包名+接口名
(2)asInterface,把Binder转化成当前的IPerson对象。这里会判断是否是跨进程还是同一进程。如果是同一进程,则直接返回我们传进来的IBinder,即我们服务端里创建的Stub对象。如果不是同一进程,这个时候就会创建一个Proxy对象,并且传入IBinder,上面已经分析过了,IBinder即BinderProxy。
(3)asBinder,这个没啥好说的,就是获取当前的Binder对象。
(4)Proxy是一个代理类,实现了IPerson接口,重写了setName和getName方法,而其构造方法里的参数remote是在asInterface中传入的,即BinderProxy。当我们在客户端中调用setName时,由于客户端的IPerson就是Proxy对象,因此会直接调用Proxy类的setName方法。接着把数据封装到Parcel里,调用mRemote的transact方法,上面说了,此时的mRemote就是BinderProxy对象,相当于调用BinderProxy的transact方法,BinderProxy会调用onTransact,即将Stub的onTransact方法。接下来就交由Binder驱动去完成数据包的传递工作给到Service了。
(5)onTransact这个方法是运行在服务端,通过code参数来确定客户端请求的目标方法是哪个。reply表示写入返回值。当客户端请求成功,这个方法就会返回true。如果这个方法返回false,表示客户端请求失败。

四、Binder机制

1、Linux进程空间划分
一个进程空间分为 用户空间 & 内核空间(Kernel),即把进程内 用户 & 内核 隔离开来,所有进程共用1个内核空间
进程间,用户空间的数据不可共享
进程间,内核空间的数据可共享
进程内 用户空间 & 内核空间 进行交互 需通过 系统调用,主要通过函数:
copy_from_user():将用户空间的数据拷贝到内核空间
copy_to_user():将内核空间的数据拷贝到用户空间

为了保证 安全性 & 独立性,一个进程 不能直接操作或者访问另一个进程,即Android的进程是相互独立、隔离的。
传统跨进程通信的基本原理

而使用Binder进行跨进程通信作用如下:连接 两个进程,实现了mmap()系统调用,主要负责 创建数据接收的缓存空间 & 管理数据接收缓存,传统的跨进程通信需拷贝数据2次,但Binder机制只需1次,主要是使用到了内存映射。

所以Binder驱动一共有两个作用
1.创建接受缓存区
2.通知client和service数据准备就绪
3.管理线程

五、Android中的IPC方式

1、Bundle

我们知道,四大组件中Activity、Service、Receiver都是支持在Intent中传递Bundle数据的,由于Bundle实现了Parcelable接口,所以它可以在不同进程间传输。

2、使用共享文件

共享文件也是一种不错的IPC方式,两个进程通过读/写同一个文件来交换数据,比如A进程把数据写入文件,B进程通过读取这个文件来获取数据。通过文件共享的方式来共享数据对文件格式是没有具体要求的,比如可以是文本文件,也可以是XML文件,只要读/写约定数据格式即可。
通过文件共享的方式也是有局限性的,比如并发读/写的问题。有可能读的时候获取的不一定是最新写入的数据。因此我们要尽量避免并发写这种情况的发生或者考虑使用线程同步来限制多个线程的写操作。结论是,文件共享方式对数据同步要求不高的进程之间进行通信,并且要妥善处理好并发问题。

3、Messenger

Messenger可以翻译为信使,通过它可以在不同进程中传递Message,在Message中放入我们需要传递的数据,就可以轻松地实现数据在进程间传递了。Messenger是一种轻量的IPC解决方案,它的底层实现仍然是AIDL,它对AIDL进行了封装,使得我们可以更简便地进行进程间通信。同时,由于她一次处理一个请求,所以服务端也不用考虑线程同步问题,不存在并发执行的情况。
(1)服务端

public class MyService extends Service {private Messenger messenger;@Overridepublic void onCreate() {super.onCreate();// 创建一个Messengermessenger = new Messenger(new MessengerHandler());}private static class MessengerHandler extends Handler {@Overridepublic void handleMessage(Message msg) {// 这里处理客户端发来的消息Bundle bundle = msg.getData();// 得到客户端传过来的消息Hello worldString value = bundle.getString("key"); // 获取到客户端的信使,给它回消息Messenger clientMessenger = msg.replyTo;Message clientMsg = Message.obtain();Bundle clientBundle = new Bundle();clientBundle.putString("reply", "给客户端回消息了...");clientMsg.setData(clientBundle);try {clientMessenger.send(clientMsg);} catch (Exception e) {}}}@Overridepublic IBinder onBind(Intent intent) {// new Stubreturn messenger.getBinder();}
}

(2)客户端

@Override
protected void onCreate() {Intent intent = new Intent(this, MyService.class);bindService(intent, new ServiceConnection() {@Overridepublic void onServiceConnected(ComponentName name, IBinder service) {// IMessenger.Stub.asInterface(target);Messenger messenger = new Messenger(service);Message msg = Message.obtain();// 发送BundleBundle bundle = new Bundle();bundle.putString("key", "Hello world");msg.setData(bundle);// 设置这个是用于服务端回消息用的msg.replyTo = new Messenger(new ClientReplyHandler());try {messenger.send(msg);} catch (Exception e) {}}@Overridepublic void onServiceDisconnected(ComponentName name) {}}, BIND_AUTO_CREATE);
}private static class ClientReplyHandler extends Handler {@Overridepublic void handleMessage(Message msg) {// 服务端回的消息在这里接收到Bundle bundle = msg.getData();// 获取到服务端回的消息String reply = bundle.getString("reply");}
}

4、AIDL

Messenger是以串行的方式来处理客户端发来的消息的,如果大量的消息同时发送到服务端,那么使用Messenger就不太合适了。同时,Messenger的作用主要是传递消息,很多时候我们需要跨进程调用服务端的某个方法,这种情况Messenger是做不到,而AIDL可以实现这些,虽然Messenger本质上是AIDL,但是由于对AIDL做了封装方便使用的同时,也出现了一定的局限性。关于AIDL的介绍,这个上面已经花费大量篇幅讲解过了,这里就不重复了。

5、ContentProvider

(1)基本使用

// 应用A暴露的ContentProvider,供别的应用访问
/*** 定义一个ContentProvider*/
public class HistoryContentProvider extends ContentProvider {// 主机地址private static final String authority = "com.zbh.provider";private static UriMatcher mMatcher = new UriMatcher(UriMatcher.NO_MATCH);private static final int MATCH_CODE = 1;static {// 添加访问的规则// com.zbh.provider/historymMatcher.addURI(authority, "history", MATCH_CODE);}@Overridepublic boolean onCreate() {// 运行在主线程里// 其他五个方法都是运行在Binder的线程池的线程里return true;}@Overridepublic Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {int code = mMatcher.match(uri);if (code == MATCH_CODE) {EZLog.i("===query===");}return null;}@Overridepublic String getType(Uri uri) {return null;}@Overridepublic Uri insert(Uri uri, ContentValues values) {return null;}@Overridepublic int delete(Uri uri, String selection, String[] selectionArgs) {return 0;}@Overridepublic int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {return 0;}
}
// 注册到系统里
<providerandroid:authorities="com.zbh.provider"android:exported="true"android:name=".receiver.HistoryContentProvider" />
// 应用B作为调用者,使用如下
// 协议content,主机名、路径和暴露的要一致
private String uri = "content://com.zbh.provider/history";
Cursor cursor = getContentResolver().query(Uri.parse(uri), null, null, null, null);
if (cursor != null) {cursor.close();
}

6、Socket

六、Binder连接池

假设现在有10个不同的模块,都需要使用AIDL来进行进程间的通信,那我们该怎么处理?按照AIDL的方式一个一个来实现?需要创建10个Service?Service也是系统四大组件,这样对系统开销就非常大,明显不是一种可取的办法。
每个业务模块创建自己的AIDL接口并实现该接口,而Service需要和另外一个AIDL接口来绑定,然后根据不同的查询类型,来返回对应业务模块的接口引用。

/**
* IBinderPool.aidl
*/
interface IBinderPool {IBinder queryBinder(int type);
}
/**
* IComputer.aidl
*/
interface IComputer {void add(int x, int y);
}
/**
* IBook.aidl
*/
interface IBook {void setName(String name);String getName();
}
/**
* IComputer实现类
*/
public class ComputerImpl extends IComputer.Stub {@Overridepublic void add(int x, int y) throws RemoteException {Log.i("zhangbh", "computer : add");}
}
/**
* IBook实现类
*/
public class BookImpl extends IBook.Stub {@Overridepublic void setName(String name) throws RemoteException {Log.i("zhangbh", "book : " + name);}@Overridepublic String getName() throws RemoteException {return "Book name is ABC";}}
/**
* Service类
*/
public class MyService extends Service {private Binder mBinder;@Overridepublic void onCreate() {super.onCreate();Log.i("zhangbh", "service 启动了");// 实现IBinderPoolmBinder = new IBinderPool.Stub() {@Overridepublic IBinder queryBinder(int type) throws RemoteException {// 根据不同类型,返回具体业务的接口实现类if (type == 0) {return new ComputerImpl();} else {return new BookImpl();}}};}@Overridepublic IBinder onBind(Intent intent) {return mBinder;}
}
/**
* 客户端调用如下
*/
private void startService() {Intent intent = new Intent();intent.setAction("test.service.start.action");intent.setPackage("com.zbh.aidl_server");intent.addCategory(Intent.CATEGORY_DEFAULT);bindService(intent, new ServiceConnection() {@Overridepublic void onServiceConnected(ComponentName name, IBinder service) {Log.i("zhangbh", "服务绑定成功");try {// 获取Service返回的IBinder,实际上是IBinderPoolmBinderPool = IBinderPool.Stub.asInterface(service);// 调用IBinderPool的queryBinder方法,获取对应的业务BinderIBinder computerBinder = mBinderPool.queryBinder(0);// 获取到具体的Computer BinderIComputer computer = IComputer.Stub.asInterface(computerBinder);computer.add(5, 3);IBinder bookBinder = mBinderPool.queryBinder(1);IBook book = IBook.Stub.asInterface(bookBinder);book.setName("ABC");Log.i("zhangbh", book.getName());} catch (Exception e) {}}@Overridepublic void onServiceDisconnected(ComponentName name) {}}, BIND_AUTO_CREATE);
}

IPC、Binder及AIDL原理机制相关推荐

  1. 插件化知识储备-Binder和AIDL原理

    前言 插件化技术火热已久,为什么会有插件化,时势造英雄吧,随着移动互联网的快速发展,业务的飞速增长,如何在有限时间给用户提供高质量的APP,当线上出现各种BUG,如何快速修复并发布上线,插件化的意义也 ...

  2. Android10.0 Binder通信原理(十)-AIDL原理分析-Proxy-Stub设计模式

    1.概述 上一节我们写了一个AIDL的示例,实现了两个应用之间的通信,这一节我们就来一起探讨下AIDL是如何生效的. 2.什么是AIDL AIDL:Android Interface Definiti ...

  3. Android Service和Binder、AIDL

    为什么80%的码农都做不了架构师?>>>    Android Service和Binder.AIDL 人收藏此文章, 关注此文章发表于3个月前 , 已有 206次阅读 共 个评论  ...

  4. Android探索之旅 | AIDL原理和实例讲解

    前言 为使应用程序之间能够彼此通信,Android提供了IPC (Inter Process Communication,进程间通信)的一种独特实现: AIDL (Android Interface ...

  5. 一篇文章了解相见恨晚的 Android Binder 进程间通讯机制

    概述 最近在学习Binder机制,在网上查阅了大量的资料,也看了老罗的Binder系列的博客和Innost的深入理解Binder系列的博客,都是从底层开始讲的,全是C代码,虽然之前学过C和C++,然而 ...

  6. 浅析 Android 中 Binder 的上层原理

    纸上得来终觉浅,绝知此事要躬行 Binder 一直是我心里的一个坎儿,因为不管是 Android 中的哪个组件,都总是会或多或少的涉及 Binder.对于 Binder 是 Android 为了提升其 ...

  7. IPC Binder

    Binder Android IPC Linux 内核 驱动 摘要 Binder 是Android系统进程间通信(IPC)方式之一.Linux已经拥有管道,system V IPC,socket等IP ...

  8. Android Binder驱动的工作机制之要旨

    最近,看了不少Android内核分析的书籍.文章及Android源程序.感觉自己对Android Binder的工作机制算是有了个彻底的理解. 但是,自己是花了很多时间和精力之后才达到这一点的.对于大 ...

  9. J2EE JVM加载class文件的原理机制

    JVM加载class文件的原理机制 1.Java中的所有类,必须被装载到jvm中才能运行,这个装载工作是由jvm中的类装载器完成的,类装载器所做的工作实质是把类文件从硬盘读取到内存中 2.java中的 ...

最新文章

  1. 标准caffe中实现darknet相关层。caffe和darknet模型的相互转换和加速(分类、检测、分割)
  2. shell提示符的个性化设定
  3. AngularJS快速入门指南14:数据验证
  4. 微博polg什么意思_成都网站代运营是什么意思?-建站
  5. [Docker]Docker快速上手学习笔记
  6. 可以这样给DataGrid加个序号列。
  7. IDEA 载入jQuery的方法
  8. mac查看mysql+utf8_Mac上修改MySQL默认字符集为utf8
  9. 走上这条路,也许是缘份
  10. 西瓜书+实战+吴恩达机器学习(二十)随机算法(拉斯维加斯方法、蒙特卡罗方法)
  11. 图解如何安装Oracle 10g的
  12. Oracle作业job 没有自动调度起来
  13. 使用 Java Annotation 定制 Ant Junit Report
  14. html动态背景gif图片,gif动态背景
  15. R语言连续变量正态性检验
  16. Specificity and sensitivity
  17. yy安全中心官网首页登录html,YY安全中心
  18. win10-LTSC2019装机必备操作和软件备忘录
  19. 物流小程序设计开发的功能明细与方案
  20. 图解HIVE页面单跳转化率

热门文章

  1. 【论文笔记】Question Answering over Freebase with Multi-Column Convolutional Neural Networks
  2. Token系列 - 加密猫智能合约源码分析
  3. 桌面文件删除不掉的解决方案
  4. 计算机联锁控制台功能,计算机联锁控制台的改进及应用
  5. canvas练习笔记之手绘熊本熊
  6. 数据库原理与技术(专升本)-含答案
  7. C. Petya and Exam
  8. 仿照京东导航条html+css
  9. 茂名天源石化宣传“世界急救日”活动 普及急救知识
  10. linux pppd源码下载_linux pppd脚本配置