文章目录

  • 进程通信
    • 1.1 进程空间划分
    • 1.2 跨进程通信IPC
    • 1.3 Linux跨进程通信
    • 1.4 Android进程通信
  • Binder跨进程通信
    • 2.1 Binder简介
    • 2.2 Binder驱动
    • 2.3 Binder原理
  • AIDL编程Demo
    • 3.1 服务端
    • 3.2 客户端

在操作系统中,进程与进程间的内存和数据都是不共享的。这样做的目的,是为了避免进程间相互操作数据的现象发生,从而引起各自的安全问题。为了实现进程隔离,采用了虚拟地址空间,两个进程各自的虚拟地址不同,从逻辑上来实现彼此间的隔离。两个进程之间要进行通信,就需要采用特殊的通信机制:进程间通信(IPC:Inter-Process Communication,即进程间通信或跨进程通信,简称 IPC)。

进程通信

1.1 进程空间划分

一个进程空间分为 用户空间 & 内核空间(Kernel),即把进程内 用户 & 内核 隔离开来。二者的区别:

  1. 进程间用户空间的数据不可共享,所以用户空间 = 不可共享空间
  2. 进程间内核空间的数据可共享,所以内核空间 = 可共享空间,所有进程共用1个内核空间

进程内 用户空间 & 内核空间 进行交互 需通过 系统调用,主要通过函数:

  1. copy_from_user():将用户空间的数据拷贝到内核空间
  2. copy_to_user():将内核空间的数据拷贝到用户空间

1.2 跨进程通信IPC

  • 进程隔离:为了保证 安全性 & 独立性,一个进程 不能直接操作或者访问另一个进程,即 Android 的进程是相互独立、隔离的
  • 跨进程通信(IPC:Inter-Process Communication):即进程间需进行数据交互、通信。

跨进程通信的基本原理:

1.3 Linux跨进程通信

我们知道,Android系统就是基于Linux内核实现的,咱们先简单了解一下Linux系统的IPC方式。虽然不同的资料对各种方式的名称和种类说法不完全相同,但是主要来说有如下6种方式:(1)管道 Pipe;(2)信号Signal;(3)信号量Semaphore;(4)消息队列Message Queue;(5)共享内存Shared Memmory;(6)套接字Socket。读者若想深入了解这些方式,可以自行查阅,这里不展开介绍,只需要知道有这六种方式即可。

1.4 Android进程通信

Android IPC的方式在不同的资料中有不同的说法,但从大方向上看,可以归纳为如下四种(这里仅对各种方式做简单介绍和优劣对比,对具体如何使用,不做讲解):

1、Activity方式

Activity是四大组件中使用最频繁的,咱们先从它说起。使用Activity方式实现,就是使用startActivity()来启动另外一个进程的Activity。我们在使用App的使用,往往会遇到如下几种情形:(1)浏览器中看到一篇比较不错的文章,分享到微信朋友圈或者微博;(2)在某个App中点击某个网址,然后界面跳转到浏览器中进行阅读;(3)使用美团外卖app,看到店家的电话,点击联系商家时,跳转到了电话拨打界面…这样的操作再频繁不过了。这些就是通过startActivity的方式从一个App,跳转到了另外一个App的Activity,从而实现了跨进程通信。

我们知道,在调用startActivity(Intent intent)的时候,intent有两个类型:显式Intent和隐式Intent。显式Intent的使用方式如下,用于进程内组件间通信:

Intent intent = new Intent(this,OtherActivity.class);
startActivity(intent);

隐式intent的使用方式如下,用于IPC:

Intent intent = new Intent();
intent.setAction(Intent.ACTION_CALL);
startActivity(intent); //startActivityForResult()同样,这里不赘述

Intent.ACTION_CALL就是字符串常量“android.intent.action.CALL”,这种方式通过setAction的方式来启动目标app的Activity,上述代码就是启动电话app的拨号界面,有时候还可以带上电话号码等参数。由上可知,Activity实现跨进程通信的方式,适合于不同App之间功能界面的跳转。

2、Content provider(后面简称CP)方式

当我们开发App需要用到联系人,多媒体信息等数据的时候,往往会通过系统提供Uri,采用CP的方式去获取。Android系统中,数据主要存储在自带的SqlLite数据库中。应用要共享SqlLite中的数据给其他App操作(增、删、改、查),就要用到CP,也就是说,CP主要用于跨进程数据库共享。Android系统提供了很多的CP来供其它App使用,如多媒体信息、联系人、日历等。如下图显示了Android系统提供的CP,包名都是以"com.android.providers“开头的:

这些用于共享的数据其实都是存储在系统数据库中的,如下显示了meida CP中的数据库:

App开发者也可以自定义CP,把自己的数据提供给其它app使用,也可以自己定义操作权限,如只允许其它app读取自己的数据,而不允许修改等。CP的使用场景,是提供数据共享。CP本质上还是在操作数据库,数据存储在sdcard中,所以建立连接和操作数据都是耗时操作,所以注意开辟子线程去操作。当数据库中数据有变化时,Content Observer监听到数据库变化也是有一定的滞后。

3、Broadcase方式

Broadcast使用非常简单,注册好广播,添加上action,就可以等着接收其他进程发出的广播。发送和接收广播时,还可以借助Intent来携带数据。但是广播的使用存在很多问题,被很多程序员吐槽,甚至鄙夷,所以选择用广播进行跨进程通信,是下下策。下面盘点一下Broadcast的槽点:

  1. Broadcast是一种单向的通信方式。当一个程序发送广播后,其他应用只能被动地接收,无法向发送者反馈。
  2. Broadcast非常消耗系统资源,会导致系统性能下降。
  3. 速度慢,容易造成系统ANR。且除了Parall Broadcast外,无法保证接收到的时间,甚至不一定能收得到。
  4. 如果使用Ordered Broadcast,一个Receiver执行时间过长,会影响后面接收者的接收时间,甚至还有可能被中间某个Receiver拦截,导致后面Receiver无法接收到。
  5. 发送者无法确定谁会接收该广播,而接收者也无发确认是谁发来的广播。
  6. 如果是静态注册的广播,一个没有开启的进程,都有可能被该广播激活。

总而言之,言而总之,使用Broadcast来实现跨进程通信,是下下之策!

4、Service方式

启动Service的方式有多种,有的用于跨进程通信,有的用于进程内部模块之间的通信,下面仅简单介绍一下跨进程通信的方式。

(1)startService()方式

Intent startIntent = new Intent ();
ComponentName componentName = new ComponentName(string packageName,string serviceClassName);
startIntent.setComponent(componentName );
startService( startIntent);

该方式启动远程Service实现跨进程通信,耦合度比较低,功能及代码结构清晰,但是存在以下缺点:

  • 没有好的机制立即返回执行结果,往往Service完成任务后,还需要其他方式向Client端反馈。

  • Service端无法识别Client端是谁,只知道有启动命令,但无法知道是谁下的命令。

  • 在已经创建好Service情况下,每次调用startService,就会执行onStartCommand()生命周期方法,相比于bindService,效率低下。

  • 如果Client端忘记调用stopService()了,那么该Service会一直运行下去,这也是一个隐患。

    所以,针对上述缺点,往往建议startService()方式用于同一个App内,跨进程的方式用后面将要讲到的AIDL来实现。

(2)bindService、Handler、Messager结合

这种方式也可以实现跨进程通信,据说和AIDL具有相同的功效,但比AIDL使用简单,详细可以阅读博文【Android总结篇系列:Android Service】。

(3)AIDL

这种方式也是 bindService() 启动方式的一种使用情况,也是广受程序员们推崇的方式。前面说 startService() 和 Broadcast 如何不好,就是为了衬托 AIDL 如何的好!这是本文的主角,本文后面会专门详细讲解,这里不赘述。

【总结】从如上的介绍来看,其实 Android 中跨进程通信的实现,就是利用四大组件来实现的。对方式的选择,我们总结一下:

  • 如果跨进程需要界面上的交互操作,用隐式startActivity()方式实现。
  • 如果需要共享数据,用Content Provider方式实现。
  • 排除前两种情形,就用AIDL。
  • 仅仅为了完成功能,又确实不会用AIDL的,就用Broadcast吧!!!虽然很low,但比实现不了功能还是强多了。

Binder跨进程通信

传统的跨进程通信需拷贝数据2次,但 Binder 机制只需1次,主要是使用到了内存映射,具体下面会详细说明。

跨进程通信的核心原理:内存映射,具体请看文章:操作系统:图文详解 内存映射

对比 Linux (Android基于Linux)上的其他进程通信方式(管道、消息队列、共享内存、信号量、Socket),Binder 机制的优点有:

2.1 Binder简介

Binder 跨进程通信机制 模型 基于 Client - Server 模式:

此处重点讲解 Binder 驱动的作用和 原理:

2.2 Binder驱动

2.3 Binder原理

Client进程、Server进程 & Service Manager 进程之间的交互 都必须通过Binder驱动(使用 open 和 ioctl文件操作函数),而非直接交互。具体原因:

  • Client进程、Server进程 & Service Manager进程属于进程空间的用户空间,不可进行进程间交互;
  • Binder驱动 属于 进程空间的 内核空间,可进行进程间 & 进程内交互。

所以,原理图可表示为以下(虚线表示并非直接交互—):

Binder驱动 & Service Manager进程 属于 Android基础架构(即系统已经实现好了);而Client 进程 和 Server 进程 属于Android应用层(需要开发者自己实现)。所以,在进行跨进程通信时,开发者只需自定义Client & Server 进程并显式使用上述3个步骤,最终借助 Android的基本架构功能就可完成进程间通信:

AIDL编程Demo

前面我们讲到,为了克服 Linux 中 IPC 各种方式的缺点,在Android中引入了Binder机制。但是当说起Binder在Android中的使用时,几乎所有的资料都是在说 AIDL 的使用。AIDL 的全称是 Android Interface Definition Language,即Android接口定义语言,是 Binder 机制实现 Android IPC 时使用比较广泛的工具。

本节将以一个 Demo 演示一下 AIDL 的基本实现步骤。源码地址:https://pan.baidu.com/s/1CyE_8-T9TDQLVQ1TDAEX2A 提取码:4auk 。如下两个图展示了该 Demo 的结构图和 AIDL 关键文件:

建立两个App,分别为Client端和Server端。

​ 这个比较 好理解,Server端就是包含了Service真正干活的那一端;Client端就是通过远程操控指挥的那一端,分别在不同的App中。如下图所示:

3.1 服务端

1、在Server端main目录下建立aidl文件夹以及.aidl文件,并copy一份到Client端,如图6.1中②处所示结构。注意,Client端和Server端②处是一模一样的。另外,AS中提供了快捷方式创建aidl文件,在main处点击右键 > New > AIDL > AIDL File文件,按照提示给aidl文件命名即可自动创建完成,可以看到文件路径也是该项目的包名。

这里给aidl命名为IDemoService.aidl,这里需要注意的是命名规范,一般都是以“I”开头,表示是一个接口,其内容如下:

//========== IDemoService.aidl========
package com.songwei.aidldemoserver;
// Declare any non-default types here with import statements
interface IDemoService {void setName(String name);String getName();
}

2、Server端创建Service文件 AidlService.java,如图6.1中③处所示,代码如下:

package com.songwei.aidldemoserver;import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;public class AidlService extends Service {private final static String TAG = "aidlDemo";public AidlService() {}@Overridepublic void onCreate() {super.onCreate();Log.i(TAG, "server:[onCreate]");}@Overridepublic IBinder onBind(Intent intent) {// TODO: Return the communication channel to the service.Log.i(TAG, "server:[onBind]");return new MyBinder();}@Overridepublic boolean onUnbind(Intent intent) {Log.i(TAG, "server:[onUnbind]");return super.onUnbind(intent);}@Overridepublic void onDestroy() {super.onDestroy();Log.i(TAG, "server:[onDestroy]");}class MyBinder extends IDemoService.Stub {private String mName = "";public void setName(String name) throws RemoteException{Log.i(TAG, "server:[setName]");mName = name;}@Overridepublic String getName() throws RemoteException {Log.i(TAG, "server:[getName]");return mName;}}
}

为了下文分析流程及生命周期,在其中各个方法中都添加了Log。同时,在Server端的AndroidManifest.xml文件中添加该Service的注册信息(注意exported属性值,如果有“intent-filter”,则默认值为true,否则为false。所以这里其实可以去掉,因为有“intent-filter”,其默认值就是true):

<serviceandroid:name=".AidlService"android:exported="true"><intent-filter><action android:name="com.songwei.aidl" /></intent-filter>
</service>

3、编译Sever端和Client端App,生成IDemoService.java文件。

​ 当编译的时候,AS会自动为我们生成IDemoService.java文件,如图6.1和图6.2中④处所示。当你打开该文件的时候,是不是看到了如下场景?

惊不惊喜?意不意外?是不是一脸懵逼,大惊,卧槽,这尼玛啥玩意啊?

AIDL 是 Android 接口定义语言,IDemoService.java 是一个java中的interface(接口),现在是不是若有所思了呢?AIDL 正是定义了 IDemoService.java 这个接口!!! 这个接口文件就是 AIDL 帮助咱们生成的 Binder 相关代码,这些代码就是用来帮助实现 Client 端和 Server 端通信的。前面第2步中提到的IDemoService.aidl文件,其作用就是作为原料通过AIDL来生成这些你貌似看不懂的代码的,第3步中的 AidlService.java 和后续在 Client 端App连接Server端App的时候,其实这个aidl文件就从来没有出现过,也就是说,它已经没有什么价值了。所以说,AIDL 的作用就是用来自动生成 Binder 相关接口代码的,而不需要开发者手动编写。有些教程中说,可以不使用AIDL而手动编写这份Binder代码,AIDL不是Binder实现通信所必需的,笔者也没有尝试过手动编写,如果读者您想挑战,可以尝试一下!

咱们继续!打开IDemoService.java文件后,点击主菜单兰Code > Reformat Code (或 Ctrl + Alt +L快捷健),你会发现画面变成了下面这个样子:

/** This file is auto-generated.  DO NOT MODIFY.* Original file: D:\\ASWorkspace\\testDemo\\aidldemoclient\\src\\main\\aidl\\com\\songwei\\aidldemoserver\\IDemoService.aidl*/
package com.songwei.aidldemoserver;public interface IDemoService extends android.os.IInterface {/*** Local-side IPC implementation stub class.*/public static abstract class Stub extends android.os.Binder implements com.songwei.aidldemoserver.IDemoService {private static final java.lang.String DESCRIPTOR = "com.songwei.aidldemoserver.IDemoService";/*** Construct the stub at attach it to the interface.*/public Stub() {this.attachInterface(this, DESCRIPTOR);}/*** Cast an IBinder object into an com.songwei.aidldemoserver.IDemoService interface,* generating a proxy if needed.*/public static com.songwei.aidldemoserver.IDemoService asInterface(android.os.IBinder obj) {if ((obj == null)) {return null;}android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);if (((iin != null) && (iin instanceof com.songwei.aidldemoserver.IDemoService))) {return ((com.songwei.aidldemoserver.IDemoService) iin);}return new com.songwei.aidldemoserver.IDemoService.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_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;}}return super.onTransact(code, data, reply, flags);}private static class Proxy implements com.songwei.aidldemoserver.IDemoService {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;}

惊不惊喜?意不意外?这下是不是有种似曾相识的赶脚?这就是一个很普通的java中的接口文件而已,结构也非常简单。

神秘的面纱揭开一层了吧!后面在讲完Client端和Server端的连接及通信后,还会继续深入剖析这个文件。

3.2 客户端

Client端通过ClientActivity活动类来连接Server端AidlService并通信。ClientActivity.java 的内容如下,布局文件在此省略,比较简单,就两个按钮,一个用于绑定,一个用于解绑,看Button命名也很容易分辨:

package com.songwei.aidldemoclient;import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.os.RemoteException;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;import com.songwei.aidldemoserver.IDemoService;public class ClientActivity extends AppCompatActivity {private final static String TAG = "aidlDemo";private Button mBindBtn, mUnBindBtn;private IDemoService mDemoService;private boolean mIsBinded = false;private ServiceConnection mConn = new ServiceConnection() {//当与远程Service绑定后,会回调该方法。@Overridepublic void onServiceConnected(ComponentName componentName, IBinder binder) {Log.i(TAG, "client:[onServiceConnected]componentName=" + componentName);mIsBinded = true;//得到一个远程Service中的Binder代理,而不是该Binder实例mDemoService = IDemoService.Stub.asInterface(binder);try {//远程控制设置name值mDemoService.setName("Andy Song");//远程获取设置的name值String myName = mDemoService.getName();Log.i(TAG, "client:[onServiceConnected]myName=" + myName);} catch (RemoteException e) {e.printStackTrace();}}//该回调方法一般不会调用,如果在解绑的时候,发现该方法没有调用,不要惊慌,因为该方法的调用时机是Service被意外销毁时,比如内存不足时。@Overridepublic void onServiceDisconnected(ComponentName name) {Log.i(TAG, "client:[onServiceDisconnected]");mIsBinded = false;mDemoService = null;}};@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);mBindBtn = (Button) findViewById(R.id.btn_bind);mBindBtn.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {//Android5.0及以后,出于对安全的考虑,Android系统对隐式启动Service做了限制,需要带上包名或者类名,这一点需要注意。Intent intent = new Intent();intent.setAction("com.songwei.aidl");intent.setPackage("com.songwei.aidldemoserver");bindService(intent, mConn, BIND_AUTO_CREATE);}});mUnBindBtn = (Button) findViewById(R.id.btn_unbind);mUnBindBtn.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {//解除绑定,当调用unbindService时,一定要判断当前service是否是binded的,如果没有,就会报错。if (mIsBinded) {unbindService(mConn);mDemoService = null;mIsBinded = false;}}});}
}

代码中对一些关键和容易忽略的地方做了注释,可以结合起来进行理解。

End 运行

​ 运行的时候,需要先启动Service端进程,才能在Client端中点击“绑定”的时候绑定成功。完成一次“绑定”和“解绑”,得到的log如下所示:

01-08 15:29:43.109 13532-13532/com.songwei.aidldemoserver I/aidlDemo: server:[onCreate]
01-08 15:29:43.110 13532-13532/com.songwei.aidldemoserver I/aidlDemo: server:[onBind]
01-08 15:29:43.113 13299-13299/com.songwei.aidldemoclient I/aidlDemo: client:[onServiceConnected]componentName=ComponentInfo{com.songwei.aidldemoserver/com.songwei.aidldemoserver.AidlService}
01-08 15:29:43.114 13532-13547/com.songwei.aidldemoserver I/aidlDemo: server:[setName]
01-08 15:29:43.114 13532-13546/com.songwei.aidldemoserver I/aidlDemo: server:[getName]
01-08 15:29:43.114 13299-13299/com.songwei.aidldemoclient I/aidlDemo: client:[onServiceConnected]myName=Andy Song
01-08 15:36:07.570 13299-13299/com.songwei.aidldemoclient I/aidlDemo: client:[onServiceDisconnected]

可以结合前面的 ClientActivity.java 和 AidlService.java 代码中的添加的log,来理解一下这个流程。当然,最好是能够按照上面的步骤,亲自动手实现一遍,比看10遍更有效果。

参考文章:

  1. Android性能篇之(七)Android跨进程通信篇;
  2. Android跨进程通信:图文详解 Binder机制 原理。

Android跨进程通信Binder机制与AIDL实例相关推荐

  1. Android 跨进程通信大总结

    转载请标明出处:http://blog.csdn.net/zhaoyanjun6/article/details/111553746 本文出自[赵彦军的博客] 文章目录 1.Android进程 2.修 ...

  2. 【朝花夕拾】Android跨进程通信总结篇

    前言 原文:https://www.cnblogs.com/andy-songwei/p/10256379.html 只要是面试高级工程师岗位,Android跨进程通信就是最受面试官青睐的知识点之一. ...

  3. 【朝花夕拾】Android性能篇之(七)Android跨进程通信篇

    前言 转载请声明,转自[https://www.cnblogs.com/andy-songwei/p/10256379.html],谢谢! 只要是面试高级工程师岗位,Android跨进程通信就是最受面 ...

  4. 【朝花夕拾】Android性能篇之(七)Android跨进程通信篇...

    前言 原文:https://www.cnblogs.com/andy-songwei/p/10256379.html 只要是面试高级工程师岗位,Android跨进程通信就是最受面试官青睐的知识点之一. ...

  5. Android 跨进程通信(一)

    Android 跨进程通信 Android 本身提供一四种方式进行实现跨进程通信,他们也分别是Android的四大组件.分别是:Activity,Content Provider,Broadcast和 ...

  6. 使用AIDL+动态代理+运行时注解+反射 反手撸一套Android跨进程通信框架

    IPC 前言 跨进程通信方式 跨进程通信框架 涉及到的技术 使用Request-Response思想 IPCRequest IPCResponse RemoteService 服务端 客户端 附带 项 ...

  7. 【Binder】Android 跨进程通信原理解析

    前言 在Android开发的过程中,用到跨进程通信的地方非常非常多,我们所使用的Activity.Service等组件都需要和AMS进行跨进程通信,而这种跨进程的通信都是由Binder完成的. 甚至一 ...

  8. Android跨进程通信

    一 多进程之间的通信 由于不同进程所拥有的地址是两块不同的地址空间,所以不能直接通过共享内存共享数据了. Linux常用跨进程通信方式:管道,信号量,共享内存,socket Android常用跨进程通 ...

  9. Android - 跨进程通信(IPC) 另一种便捷实现 详解

    文章目录 1. 写在前面 2. 跨进程通信的实现 3. 扩展思考 4. 参考资料 1. 写在前面 看到此图有何感想,这是另一种便捷的实现方式,我们先来看看其它的几种方式. Android 进程间通信 ...

最新文章

  1. 百度 什么是主成分分析
  2. windbg设断点命令详解(bp, bu, bm, ba 以及bl, bc, bd, be)
  3. 如何在ADF中将参数传递给ActionListener
  4. atsl android auto吗,二十万就能开上凯迪拉克ATS-L,还有人在纠结买ATS还是GS?
  5. Python-Matplotlib可视化(8)——图形的输出与保存
  6. 常用JQUERY插件大全
  7. 【论文翻译】Image Super-Resolution Using Deep Convolutional Networks
  8. linux软件有什么特点是什么,Linux系统,Win7系统,DOX系统各有什么特点?哪个系统好点?...
  9. 联想电脑无法正常开机常见现象和方案汇总(拯救者R7000)
  10. 发几个flashxp注册码
  11. ClientToScreen 和ScreenToClient 用法
  12. Ti杯电子竞赛前期准备工作
  13. 移除联想M5210阵列卡(3650M5)的缓存模块以开启JBOD模式
  14. iOS 阿里云上传图片
  15. 虚拟机KALI2022.2下安装GVM
  16. 一亩三分地 新手上路 网站规则 - 满分5大米(适用于所有用户) 答案 新手入门
  17. 银河麒麟中的录屏软件
  18. javaweb商城实现在线支付
  19. 蓝桥杯 — IAP15F2K61S2-89C52 转换板说明文件
  20. Win7系统修复启动项命令

热门文章

  1. 【电子量产工具】6. 业务系统
  2. 大学计算机模拟考试常见试题与解析
  3. urlencoded、json 格式详解
  4. 一部手机就可以完成,台词混剪赚钱,一个月挣了5000多
  5. Echarts - 将图表网格线设置成虚线(图表横线网格)
  6. 程序员接私活必备的10款开源前端后台框架
  7. 题目:有四个数字:1、2、3、4,能组成多少个互不相同且无重复数字的三位数?各是多少?
  8. kotlin的三目运算
  9. 母亲节|TcaplusDB祝天下母亲节日快乐!
  10. 用excel画简单曲线图的实践