Binder
- Binder框架概述
- 服务端
- Binder驱动
- 客户端
- 设计服务端和客户端
- 设计服务端
- 客户端设计
- Binder与Service
- Service
- AIDL 保证包裹内参数顺序
- IMusicPlayerService
- Proxy
- Stub
- 系统服务中的Binder对象
- ServiceManger管理的服务
- 理解Manger
- 功能快捷键
- 合理的创建标题,有助于目录的生成
- 如何改变文本的样式
- 插入链接与图片
- 如何插入一段漂亮的代码片
- 生成一个适合你的列表
- 创建一个表格
- 设定内容居中、居左、居右
- SmartyPants
- 创建一个自定义列表
- 如何创建一个注脚
- 注释也是必不可少的
- KaTeX数学公式
- 新的甘特图功能,丰富你的文章
- UML 图表
- FLowchart流程图
- 导出与导入
- 导出
- 导入
Binder框架概述
Binder是一种架构,这种架构提供了服务端接口、Binder驱动、客户端接口三个模块,如图所示。
服务端
首先来看服务端。一个Binder服务端实际上就是一个Binder类的对象,该对象一旦创建,内部就启动一个隐藏线程。
该线程接下来会接收Binder驱动发送的消息,收到消息后,会执行到Binder对象中的onTransact()函数,并按照该函数的参数执行不同的服务函数。因此,要实现一个Binder服务,就必须重载onTransact()方法。
重载
onTransact()
函数的主要内容是把onTransact()
函数的参数转换为服务函数的参数,而onTransact()
函数的参数来源是客户端调用transact()
函数时输入的,因此,如果transact()
有固定格式的输入,那么onTransact()
就会有固定格式的输出。
Binder驱动
任意一个服务端Binder对象被创建时,同时会在Binder驱动中创建一个mRemote对象,该对象的类型也是Binder类。客户端要访问远程服务时,都是通过mRemote对象。
客户端
客户端要想访问远程服务,必须获取远程服务在Binder驱动中对应的mRemote引用。获得该mRemote对象后,就可以调用其transact()方法,而在Binder驱动中,mRemote对象也重载了transact()方法,重载的内容主要包括以下几项。
-
以线程间消息通信的模式,向服务端发送客户端传递过来的参数。
-
挂起当前线程,当前线程正是客户端线程,并等待服务端线程执行完指定服务函数后通知(notify)。
-
接收到服务端线程的通知,然后继续执行客户端线程,并返回到客户端代码区。
从这里可以看出,对应用程序开发员来讲,客户端似乎是直接调用远程服务对应的Binder,而事实上则是通过Binder驱动进行了中转。即存在两个Binder对象,一个是服务端的Binder对象,另一个则是Binder驱动中的Binder对象,所不同的是Binder驱动中的对象不会再额外产生一个线程。
客户端如何获取到Binder驱动中对应的mRemote引用?
Binder驱动如何发送消息到服务端?
设计服务端和客户端
设计服务端
服务端是一个Binder类对象,只要基于Binder类新建一个Server类即可。以下以设计一个MusicPlayerService类为例。
假设该Service仅提供两个方法:start(String filePath)和stop(),那么该类的代码可以如下:
public class MusicPlayerService extends Binder {
@Override
protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
switch (code) {
case 1000:
data.enforceInterface("MusicPlayerService");
String filePath = data.readString();
start(filePath);
// replay.writeXXX();
break;
}
return super.onTransact(code, data, reply, flags);
}
public void start(String filePath) {
}
public void stop() {
}
}
code变量是客户端和服务端约定好的,用于标识客户端期望调用服务端的哪个函数,
这里假定1000是双方约定要调用start()函数的值。
enforceInterface()是为了某种校验,它与客户端的writeInterfaceToken()对应。
readString()用于从包裹中取出一个字符串。取出filePath变量后,就可以调用服务端的start()函数了。
如果该客户端期望服务端返回一些结果,则可以在返回包裹reply中调用Parcel提供的相关函数写入相应的结果。
当要启动该服务时,只需要初始化一个MusicPlayerService对象即可。比如可以在主Activity里面初始化一个MusicPlayerService,然后运行,此时可以发现多运行了一个线程,Binder-Thread3。
如果不创建MusicPlayerService,则只有2个Binder对象对应的线程。即Binder-Thread1 和 Binder-Thread2。
这两个进程是怎么来的?
客户端设计
要想使用服务端,首先要获取服务端在Binder驱动中对应的mRemote变量的引用。获得该变量的引用后,就可以调用该变量的transact()方法。该方法的函数原型如下:
public final boolean transact(int code, Parcel data, Parcel reply,int flags)
其中data表示的是要传递给远程Binder服务的包裹(Parcel),远程服务函数所需要的参数必须放入这个包裹中。包裹中只能放入特定类型的变量,这些类型包括常用的原子类型,比如String、int、long等。除了一般的原子变量外,Parcel还提供了一个writeParcel()方法,可以在包裹中包含一个小包裹。因此,要进行Binder远程服务调用时,服务函数的参数要么是一个原子类,要么必须继承于Parcel类,否则,是不能传递的。
因此,对于MusicPlayerService的客户端而言,可以如下调用transact()方法。
IBinder mRemote = null;
String filePath = "/sdcard/music/heal_the_world.mp3";
int code = 1000;
Parcel data = Parcel.obtain();
Parcel reply = Parcel.obtain();
data.writeInterfaceToken("MusicPlayerService");
data.writeString(filePath);
mRemote.transact(code, data, reply, 0);
IBinder binder = reply.readStrongBinder();
reply.recycle();
data.recycle();
首先,包裹不是客户端自己创建的,而是调用Parcel.obtain()申请的。
data和reply变量都由客户端提供,reply变量用户服务端把返回的结果放入其中。
writeInterfaceToken
()方法标注远程服务名称,理论上讲,这个名称不是必需的,因为客户端既然已经获取指定远程服务的Binder引用,那么就不会调用到其他远程服务。该名称将作为Binder驱动确保客户端的确想调用指定的服务端。
writeString
()方法用于向包裹中添加一个String变量。注意,包裹中添加的内容是有序的,这个顺序必须是客户端和服务端事先约定好的,在服务端的onTransact
()方法中会按照约定的顺序取出变量。
接着调用transact()方法。调用该方法后,
客户端线程进入Binder驱动,Binder驱动就会挂起当前线程,并向远程服务发送一个消息,消息中包含了客户端传进来的包裹。服务端拿到包裹后,会对包裹进行拆解,然后执行指定的服务函数,执行完毕后,再把执行结果放入客户端提供的reply包裹中。然后服务端向Binder驱动发送一个notify的消息,从而使得客户端线程从Binder驱动代码区返回到客户端代码区。
transact()的最后一个参数的含义是执行IPC调用的模式,分为两种:一种是双向,用常量0表示,其含义是服务端执行完指定服务后会返回一定的数据;另一种是单向,用常量1表示,其含义是不返回任何数据。
最后,客户端就可以从reply中解析返回的数据了,同样,返回包裹中包含的数据也必须是有序的,而且这个顺序也必须是服务端和客户端事先约定好的。
Binder与Service
以上手工编写Binder服务端和客户端的过程存在两个重要问题。
第一,客户端如何获得服务端的Binder对象引用。
第二,客户端和服务端必须事先约定好两件事情:
-
服务端函数的参数在包裹中的顺序。
-
服务端不同函数的int型标识。也就是transact方法中的code参数的值。
Service
那么,Service类是如何解决本节开头所提出的两个重要问题的呢?
首先,AmS提供了startService()函数用于启动客户服务,而对于客户端来讲,可以使用以下两个函数来和一个服务建立连接,其原型在android.app. ContextImpl类中。
@Override
public ComponentName startService(Intent service) {
try {
ComponentName cn = ActivityManagerNative.getDefault().startService(
mMainThread.getApplicationThread(), service,
service.resolveTypeIfNeeded(getContentResolver()));
if (cn != null && cn.getPackageName().equals("!")) {
throw new SecurityException(
"Not allowed to start service " + service
+ " without permission " + cn.getClassName());
}
return cn;
} catch (RemoteException e) {
return null;
}
}
该函数用于启动intent
指定的服务,而启动后,客户端暂时还没有服务端的Binder引用,因此,暂时还不能调用任何服务功能。
@Override
public boolean bindService(Intent service, ServiceConnection conn,
int flags) {
IServiceConnection sd;
if (mPackageInfo != null) {
sd = mPackageInfo.getServiceDispatcher(conn, getOuterContext(),
mMainThread.getHandler(), flags);
} else {
throw new RuntimeException("Not supported in system context");
}
try {
int res = ActivityManagerNative.getDefault().bindService(
mMainThread.getApplicationThread(), getActivityToken(),
service, service.resolveTypeIfNeeded(getContentResolver()),
sd, flags);
if (res < 0) {
throw new SecurityException(
"Not allowed to bind to service " + service);
}
return res != 0;
} catch (RemoteException e) {
return false;
}
}
该函数用于绑定一个服务,这就是第一个重要问题的关键所在。其中第二个参数是一个interface类,该interface的定义如以下代码所示:
/**
* Interface for monitoring the state of an application service. See
* {@link android.app.Service} and
* {@link Context#bindService Context.bindService()} for more information.
* <p>Like many callbacks from the system, the methods on this class are called
* from the main thread of your process.
*/
public interface ServiceConnection {
/**
* Called when a connection to the Service has been established, with
* the {@link android.os.IBinder} of the communication channel to the
* Service.
*
* @param name The concrete component name of the service that has
* been connected.
*
* @param service The IBinder of the Service's communication channel,
* which you can now make calls on.
*/
public void onServiceConnected(ComponentName name, IBinder service);
public void onServiceDisconnected(ComponentName name);
}
请注意该interface中的onServiceConnected()方法的第二个变量Service。当客户端请求AmS启动某个Service后,该Service如果正常启动,那么AmS就会远程调用ActivityThread类中的ApplicationThread对象,调用的参数中会包含Service的Binder引用,然后在ApplicationThread中会回调bindService中的conn接口。因此,在客户端中,可以在onServiceConnected()方法中将其参数Service保存为一个全局变量,从而在客户端的任何地方都可以随时调用该远程服务。这就解决了第一个重要问题,即客户端如何获取远程服务的Binder引用。
AIDL 保证包裹内参数顺序
关于第二个问题,Android的SDK中提供了一个aidl工具,该工具可以吧一个aidl文件转换为一个Java类文件,在该Java类文件,同时重载了transact和onTransact()方法,统一了存入包裹和读取包裹参数,从而使设计者可以把注意力放到服务代码本身上。
接下来看aidl工具都做了什么。如本章第一节示例,此处依然假设要编写一个MusicPlayerService服务,服务中包含两个服务函数,分别是start()和stop()。那么,可以首先编写一个IMusicPlayerService.aidl文件。如以下代码所示:
package com.haiii.android.client;
interface IMusicPlayerService{
boolean start(String filePath);
void stop();
}
该文件的名称必须遵循一定的规范,第一个字母"I"不是必需的,但是,为了程序风格的统一,"I"的含义是IInterface类,即这是一个可以提供访问远程服务的类。后面的命名–MusicPlayerService对应的是服务的类名,可以是任意的,但是,aidl工具会以该名称命名输出的Java类。
aidl文件的语法基本类似于Java,package指定输出后的Java文件对应的包名。如果该文件需要引用其他Java类,则可以使用import关键字,但需要注意的是,包裹内只能写入以下三个类型的内容:
- Java原子类型,如int、long、String等变量。
- Binder引用。
- 实现了Parcelable的对象。
因此,基本上来讲,import所引用的Java类也只能是以上三个类型。
interface为关键字,有时会在interface前面加一个oneway,代表该service提供的方法都是没有返回值的,即都是void类型。
下面看看该aidl生成的IMusicPlayerService.java文件的代码。如下所示:
package com.haiii.client;
public interface IMusicPlayerService extends android.os.IInterface {
/**
* Local-side IPC implementation stub class.
*/
public static abstract class Stub extends android.os.Binder
implements com.haiii.client.IMusicPlayerService {
private static final java.lang.String DESCRIPTOR =
"com.haiii.client.IMusicPlayerService";
/**
* Construct the stub at attach it to the interface.
*/
public Stub() {
this.attachInterface(this, DESCRIPTOR);
}
/**
* Cast an IBinder object into an com.haiii.client.IMusicPlayerService interface,
* generating a proxy if needed.
*/
public static com.haiii.client.IMusicPlayerService
asInterface(android.os.IBinder obj) {
if ((obj == null)) {
return null;
}
android.os.IInterface iin =
(android.os.IInterface) obj.queryLocalInterface(DESCRIPTOR);
if (((iin != null) && (iin instanceof com.haiii.client.IMusicPlayerService))) {
return ((com.haiii.client.IMusicPlayerService) iin);
}
return new com.haiii.client.IMusicPlayerService.Stub.Proxy(obj);
}
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 INTERFACE_TRANSACTION: {
reply.writeString(DESCRIPTOR);
return true;
}
case TRANSACTION_start: {
data.enforceInterface(DESCRIPTOR);
java.lang.String _arg0;
_arg0 = data.readString();
boolean _result = this.start(_arg0);
reply.writeNoException();
reply.writeInt(((_result) ? (1) : (0)));
return true;
}
case TRANSACTION_stop: {
data.enforceInterface(DESCRIPTOR);
this.stop();
reply.writeNoException();
return true;
}
}
return super.onTransact(code, data, reply, flags);
}
private static class Proxy implements com.haiii.client.IMusicPlayerService {
private android.os.IBinder mRemote;
Proxy(android.os.IBinder remote) {
mRemote = remote;
}
public android.os.IBinder asBinder() {
return mRemote;
}
public java.lang.String getInterfaceDescriptor() {
return DESCRIPTOR;
}
public boolean start(java.lang.String filePath) throws android.os.RemoteException {
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
boolean _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
_data.writeString(filePath);
mRemote.transact(Stub.TRANSACTION_start, _data, _reply, 0);
_reply.readException();
_result = (0 != _reply.readInt());
} finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
public void stop() throws android.os.RemoteException {
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
try {
_data.writeInterfaceToken(DESCRIPTOR);
mRemote.transact(Stub.TRANSACTION_stop, _data, _reply, 0);
_reply.readException();
} finally {
_reply.recycle();
_data.recycle();
}
}
}
static final int TRANSACTION_start = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
static final int TRANSACTION_stop = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
}
public boolean start(java.lang.String filePath) throws android.os.RemoteException;
public void stop() throws android.os.RemoteException;
}
这些代码主要完成以下三个任务。
IMusicPlayerService
定义一个
Java interface
,内部包含aidl文件所声明的服务函数,类名称为IMusicPlayerService
,并且该类基于IInterface
接口,即需要提供一个asBinder
()函数。
Proxy
定义一个Proxy类,该类将作为客户端程序访问服务端的代理。所谓的代理主要就是为了前面所提到的第二个重要问题–统一包裹内写入参数的顺序。
Stub
定义一个Stub类,这是一个abstract类,基于Binder类,并且实现了
IMusicPlayerService
接口,主要由服务端来使用。该类之所以要定义为一个abstract类,是因为具体的服务函数必须由程序员实现,因此,IMusicPlayerService
接口中定义的函数在Stub类中可以没有具体实现。同时,在Stub类中重载了onTransact
()方法,由于transact
()方法内部给包裹内写入参数的顺序是由aidl工具定义的,因此,在onTransact
()方法中,aidl工具自然知道应该按照何种顺序从包裹中取出相应参数。
在Stub类中还定义了一些int常量,比如TRANSACTION_start,这些常量与服务函数对应,transact()和onTransact()方法的第一个参数code的值即来源于此。
在Stub类中,除了以上所述的任务外,Stub还提供了一个asInterface()函数。提供这个函数的作用是这样的:
提供这个函数的原因是服务端提供的服务除了其他进程可以使用外,在服务进程内部的其他类也可以使用该服务,对于后者,显然是不需要经过IPC调用,而可以直接在进程内部调用的,而Binder内部有一个queryLocalInterface(String description)函数,该函数是根据输入的字符串判断该Binder对象是否是一个本地的Binder引用。
创建服务时,服务端进程内部创建一个Binder对象,Binder驱动中也会创建一个Binder对象。如果从客户端进程获取服务端进程的Binder,则只会返回Binder驱动中的Binder对象,而如果从服务端进程内部获取Binder对象,则会获取服务端本身的Binder对象。
因此,asInterface()函数正是利用了queryLocalInterface()方法,提供了一个统一的接口。无论是远程客户端还是服务端内部进程,当获取Binder对象后,可以把获取的Binder对象作为asInterface()的参数,从而返回一个IMusicPlayerService接口,该接口要么使用Proxy类,要么直接使用Stub所实现的相应服务函数。
系统服务中的Binder对象
在应用程序中,经常使用getSystemService(String serviceName)
方法获取一个系统服务,那么,这些系统服务的Binder引用是如何传递给客户端的呢?
须知系统服务并不是通过startService()
启动的。getSystemService()
函数的实现是在ContextImpl类中,该函数所返回的Service比较多,具体可参照源码。这些Service一般都由ServiceManager管理。
ServiceManger管理的服务
ServiceManager是一个独立进程,其作用如名称所示,管理各种系统服务,管理的逻辑如下:
ServiceManager本身也是一个Service,Framework提供了一个系统函数,可以获取该Service对应的Binder引用,那就是BinderInternal.getContextObject()。
该静态函数返回ServiceManager后,就可以通过ServiceManager提供的方法获取其他系统Service的Binder引用。
其他系统服务在启动时,首先把自己的Binder对象传递给ServiceManager,即所谓的注册(addService)。
下面查看获取一个Service{IMPUT_METHOD_SERVICE}:
if (INPUT_METHOD_SERVICE.equals(name)) {
return InputMethodManager.getInstance(this);
static public InputMethodManager getInstance(Looper mainLooper) {
synchronized (mInstanceSync) {
if (mInstance != null) {
return mInstance;
}
IBinder b = ServiceManager.getService(Context.INPUT_METHOD_SERVICE);
IInputMethodManager service = IInputMethodManager.Stub.asInterface(b);
mInstance = new InputMethodManager(service, mainLooper);
}
return mInstance;
}
即通过ServiceManager
获取InputMethod Service
对应的Binder对象b,然后再将该Binder对象作为IInputMethodManager.Stub.asInterface()
的参数,返回一个IInputMethodManager
的统一接口。
ServiceManager.getService()
的代码如下:
public static IBinder getService(String name) {
try {
IBinder service = sCache.get(name);
if (service != null) {
return service;
} else {
return getIServiceManager().getService(name);
}
} catch (RemoteException e) {
Log.e(TAG, "error in getService", e);
}
return null;
}
即首先从sCache
缓存中查看是否有对应的Binder 对象,有则返回,没有则调用getIServiceManager().getService(name)
,函数getIServiceManager()
即用于返回系统中唯一的ServiceManager
对应的Binder,其代码如下:
private static IServiceManager getIServiceManager() {
if (sServiceManager != null) {
return sServiceManager;
}
// Find the service manager
sServiceManager = ServiceManagerNative.asInterface(BinderInternal.getContextObject());
return sServiceManager;
}
BinderInternal.getContextObject()
静态函数即用于返回ServiceManager对应的全局Binder对象,该函数不需要任何参数,因为它的作用是固定的。其他所有通过ServiceManager获取的系统服务的过程与以上基本类似,所不同的就是传递给ServiceManager的服务名称不同,因为ServiceManager正是按照服务的名称(String类型)来保存不同的Binder对象的。
使用addService()向ServiceManager中添加一个服务一般是在SystemService进程启动时完成的。
理解Manger
ServiceManager所管理的所有Service都是以相应的Manager返回给客户端,因此,这里简述一下Framework中关于Manager的语义。
在Android中,Manager的含义更应该翻译为经纪人,Manager所manage的对象是服务本身,因为每个具体的服务一般都会提供多个API接口 ,而Manager所manage的正是这些API。
客户端一般不能直接通过Binder引用去访问具体的服务,它需要先通过ServiceManager获取远程服务的Binder引用,然后使用这个Binder引用构造一个客户端本地可以访问的经纪人,比如前面的IInputMethodManager,然后客户端就可以通过该经纪人访问远程的服务。
通过本地Manger访问远程服务的模型图如下:
- 全新的界面设计 ,将会带来全新的写作体验;
- 在创作中心设置你喜爱的代码高亮样式,Markdown 将代码片显示选择的高亮样式 进行展示;
- 增加了 图片拖拽 功能,你可以将本地的图片直接拖拽到编辑区域直接展示;
- 全新的 KaTeX数学公式 语法;
- 增加了支持甘特图的mermaid语法1 功能;
- 增加了 多屏幕编辑 Markdown文章功能;
- 增加了 焦点写作模式、预览模式、简洁写作模式、左右区域同步滚轮设置 等功能,功能按钮位于编辑区域与预览区域中间;
- 增加了 检查列表 功能。
功能快捷键
撤销:Ctrl/Command + Z
重做:Ctrl/Command + Y
加粗:Ctrl/Command + B
斜体:Ctrl/Command + I
标题:Ctrl/Command + Shift + H
无序列表:Ctrl/Command + Shift + U
有序列表:Ctrl/Command + Shift + O
检查列表:Ctrl/Command + Shift + C
插入代码:Ctrl/Command + Shift + K
插入链接:Ctrl/Command + Shift + L
插入图片:Ctrl/Command + Shift + G
查找:Ctrl/Command + F
替换:Ctrl/Command + G
合理的创建标题,有助于目录的生成
直接输入1次#,并按下space后,将生成1级标题。
输入2次#,并按下space后,将生成2级标题。
以此类推,我们支持6级标题。有助于使用TOC
语法后生成一个完美的目录。
如何改变文本的样式
强调文本 强调文本
加粗文本 加粗文本
标记文本
删除文本
引用文本
H2O is是液体。
210 运算结果是 1024.
插入链接与图片
链接: link.
图片:
带尺寸的图片:
居中的图片:
居中并且带尺寸的图片:
当然,我们为了让用户更加便捷,我们增加了图片拖拽功能。
如何插入一段漂亮的代码片
去博客设置页面,选择一款你喜欢的代码片高亮样式,下面展示同样高亮的 代码片
.
// An highlighted block
var foo = 'bar';
生成一个适合你的列表
- 项目
- 项目
- 项目
- 项目
- 项目1
- 项目2
- 项目3
- 计划任务
- 完成任务
创建一个表格
一个简单的表格是这么创建的:
项目 | Value |
---|---|
电脑 | $1600 |
手机 | $12 |
导管 | $1 |
设定内容居中、居左、居右
使用:---------:
居中
使用:----------
居左
使用----------:
居右
第一列 | 第二列 | 第三列 |
---|---|---|
第一列文本居中 | 第二列文本居右 | 第三列文本居左 |
SmartyPants
SmartyPants将ASCII标点字符转换为“智能”印刷标点HTML实体。例如:
TYPE | ASCII | HTML |
---|---|---|
Single backticks | 'Isn't this fun?' | ‘Isn’t this fun?’ |
Quotes | "Isn't this fun?" | “Isn’t this fun?” |
Dashes | -- is en-dash, --- is em-dash | – is en-dash, — is em-dash |
创建一个自定义列表
-
Markdown
- Text-to- HTML conversion tool Authors
- John
- Luke
如何创建一个注脚
一个具有注脚的文本。2
注释也是必不可少的
Markdown将文本转换为 HTML。
KaTeX数学公式
您可以使用渲染LaTeX数学表达式 KaTeX:
Gamma公式展示 Γ ( n ) = ( n − 1 ) ! ∀ n ∈ N \Gamma(n) = (n-1)!\quad\forall n\in\mathbb N Γ(n)=(n−1)!∀n∈N 是通过欧拉积分
Γ ( z ) = ∫ 0 ∞ t z − 1 e − t d t . \Gamma(z) = \int_0^\infty t^{z-1}e^{-t}dt\,. Γ(z)=∫0∞tz−1e−tdt.
你可以找到更多关于的信息 LaTeX 数学表达式here.
新的甘特图功能,丰富你的文章
- 关于 甘特图 语法,参考 这儿,
UML 图表
可以使用UML图表进行渲染。 Mermaid. 例如下面产生的一个序列图:
这将产生一个流程图。:
- 关于 Mermaid 语法,参考 这儿,
FLowchart流程图
我们依旧会支持flowchart的流程图:
- 关于 Flowchart流程图 语法,参考 这儿.
导出与导入
导出
如果你想尝试使用此编辑器, 你可以在此篇文章任意编辑。当你完成了一篇文章的写作, 在上方工具栏找到 文章导出 ,生成一个.md文件或者.html文件进行本地保存。
导入
如果你想加载一篇你写过的.md文件,在上方工具栏可以选择导入功能进行对应扩展名的文件导入,
继续你的创作。
mermaid语法说明 ↩︎
注脚的解释 ↩︎