4.端点ENDPOINT

Endpoint类是一个单例类,应用程序必须在此类实例之前创建一个并且最多只能创建一个,然后才能执行任何操作。同样,一旦这个类被销毁,应用程序就不能调用该库的任何API。这个类是PJSUA2的核心类,它提供了以下功能:

  • 启动和关机
  • 配置的定制,如核心UA(用户代理)SIP配置,媒体配置和日志配置

本章将介绍上述功能。

要使用Endpoint类,通常应用程序不需要进行子类化(再写继承于该类的子类,简称子类化(subclass)),除非:

  • 应用程序希望实现/重载端点回调方法来获取如传输状态更改或NAT检测完成等事件,或者
  • 应用程序使用Endpoint.utilTimerSchedule()API调度计时器。在这种情况下,应用程序需要实现onTimer()回调以在定时器到期时获取通知。

4.1 实例化端点

在其他任何事情之前,必须实例化Endpoint类:

Endpoint *ep = new Endpoint;

一旦端点被实例化,可使用Endpoint.instance()静态方法获取端点实例。

4.2 创建库

通过调用其libCreate()方法来创建库:

try
{
ep->libCreate();
}
catch(Error& err)
{
cout << "Startup error: " << err.info() << endl;
}

如果发生错误,libCreate()方法将引发异常,因此我们需要使用try / catch子句来捕获异常。

4.3 初始化库并配置设置

EpConfig类提供端点配置,允许自定义以下设置:

  • UAConfig,指定核心SIP用户代理设置。
  • MediaConfig,指定各种媒体全局设置
  • LogConfig来自定义日志设置。

请注意,可以在AccountConfig中根据每个帐户进一步指定一些设置。

要自定义设置,请创建EpConfig类的实例,并在端点初始化期间指定它们(稍后将对此进行说明),例如:

EpConfig ep_cfg;
ep_cfg.logConfig.level = 5;
ep_cfg.uaConfig.maxCalls = 4;
ep_cfg.mediaConfig.sndClockRate = 16000;

接下来,通过调用libInit()初始化库:

try
{
EpConfig ep_cfg;
// Specify customization of settings in ep_cfg
ep->libInit(ep_cfg);
}
catch(Error& err)
{
cout << "Initialization error: " << err.info() << endl;
}

上面的代码片段使用默认设置初始化库。

4.4 创建一个或多个传输

在发送或接收SIP消息之前,应用程序需要创建一个或多个传输:

try 
{
TransportConfig tcfg;
tcfg.port = 5060;
TransportId tid = ep->transportCreate(PJSIP_TRANSPORT_UDP, tcfg);
}
catch(Error& err)
{
cout << "Transport creation error: " << err.info() << endl;
}

transportCreate()方法返回新创建的传输ID,它传递 传输类型和TransportConfig对象 来定制传输设置,如绑定地址和侦听端口号。没有这个,默认情况下,传输将绑定到INADDR_ANY和任何可用的端口。

除了创建无用户帐户(使用Account.create())以外,没有真正使用传输ID,稍后将对此进行说明),也许可以在应用程序想要的时候向用户显示传输列表。

4.5 创建安全传输(TLS)

要创建TLS传输,您可以使用与上述相同的方法。您可以通过修改字段TransportConfig.tlsConfig来进一步自定义TLS传输,例如设置证书文件或选择使用的密码。

try {
TransportConfig tcfg;
tcfg.port = 5061;
// Optional, set CA/certificate/private key files.
// tcfg.tlsConfig.CaListFile = "ca.crt";
// tcfg.tlsConfig.certFile = "cert.crt";
// tcfg.tlsConfig.privKeyFile = "priv.key";
// Optional, set ciphers. You can select a certain cipher/rearrange the order of ciphers here.
// tcfg.ciphers = ep->utilSslGetAvailableCiphers();
TransportId tid = ep->transportCreate(PJSIP_TRANSPORT_TLS, tcfg);
}
catch(Error& err)
{
cout << "Transport creation error: " << err.info() << endl;
}

4.5 启动库

现在我们启动库了。我们需要启动库完成初始化阶段,例如完成初始的STUN地址解析,初始化/启动声音设备等。要启动库,请调用libStart()方法:

try
{
ep->libStart();
}
catch(Error& err)
{
cout << "Startup error: " << err.info() << endl;
}

4.6 关闭库

应用程序退出后,库需要关闭,以便将资源释放回操作系统。虽然可以通过删除端点实例(内部调用libDestroy())来完成,最好是手动调用它,因为在Java或Python中,垃圾回收有问题,如前所述:

ep->libDestroy();
delete ep;

4.7 类引用

4.7.1 Endpoint

class pj::Endpoint

Endpoint (端点)表示pjsua库的一个实例

在应用程序中只能有一个pjsua库的实例,因此这个类是一个单例

公共功能(函数)

1)Endpoint()    //默认构造函数

2)virtual ~Endpoint()//虚析构函数

3)Version libVersion() const//得到库的版本

4)void libCreate()  //实例化pjsua应用程序。调用任何其他函数之前,应用程序必须调用此函数,以确保底层库被正确初始化。一旦此函数返回成功,应用程序必须在退出前调用libDestroy()。

5)pjsua_state libGetState() const //获取库状态。返回库状态

6)void libInit(const EpConfig &prmEpConfig) //使用指定的设置初始化pjsua。所有设置都是可选的,并且在未指定配置时将使用默认值。请注意,在调用此函数之前必须调用create()。

  参数  prmEpConfig -端点配置

7)void libStart() // 在所有的初始化完成后,调用该函数,以便可以做其他检查设置。应用程序可以在init()之后的任何时间调用此函数。

8)void libRegisterThread(const string &name)//将由外部或自身API创建的线程注册到库。请注意,每次调用此函数时,它将分配一些内存来存储线程描述,这只会在库被销毁时被释放。

  参数name 要分配给线程的可选名称

9)bool libIsThreadRegistered()//检查该线程是否已经被注册到库中。需要注意的是此功能只适用于使用libRegisterThread()注册的库的主线程与工作线程和外部/自身线程。

10)void libStopWorkerThreads()//停止所有工作线程

11)int libHandleEvents(unsigned msec_timeout)//对pjsua进行事件轮询,如果需要,可以阻塞调用者线程指定的最大间隔(以毫秒为单位)。如果它在pjsua_config结构里配置了工作线程(thread_cnt域),应用程序通常不需要调用这个函数,因为轮询将由这些工作线程来完成。如果EpConfig :: UaConfig :: mainThreadOnly启用,并从主线程调用此函数(默认情况下,主线程是调用libCreate()的线程),此功能也将扫描并在列表中运行任何挂起的作业。

  返回  投票期间处理的事件数。负值表示错误,应用程序可以以(status = -return_value)方式检索错误。

  参数  msec_timeout - 最长时间等待,以毫秒为单位。

12)void libDestroy(unsigned prmFlags = 0) //销毁pjsua. 建议使用应用程序在调用此功能之前执行正常shutdown(例如从SIP服务器注销帐户,终止预订订阅和挂断主动调用).但是如果发现有活动会话,此函数将执行所有这些操作以终止活动会话。此功能将等待(block)几秒钟等待远程的回复。如果没有跟踪它的状态,Application.可以多次安全地调用此函数。

  参数  prmFlags,  pjsua_destroy_flag 枚举值的组合

string utilStrError(pj_status_t prmErr) 检索指定状态代码的错误字符串。

参数 prmErr -错误代码。

13)void utilLogWrite(int prmLevel, const string &prmSender, const string &prmMsg)写一个日志消息

参数 prmLevel -日志详细程度(1-5)

prmSender -日志发送方。

prmMsg -日志消息。

14)void utilLogWrite(LogEntry &e) 写一个日志条目。

参数 e - 日志条目

15)pj_status_t utilVerifySipUri(const string &prmUri) 这是一个通用用函数,用于验证是否给出了有效的SIP URL。如果URL是有效的SIP / SIPS方案,则将返回PJ_SUCCESS

返回 PJ_SUCCESS成功,或相应的错误代码

参考 utilVerifyUri()

16)pj_status_t utilVerifyUri(const string &prmUri) 这是一个通用用函数,用于验证是否给出了有效的URL。与utilVerifySipUri()不同,如果给出tel:URI,此函数将返回PJ_SUCCESS

返回 PJ_SUCCESS成功,或相应的错误代码

PJ_SUCCESS on success, or the appropriate error code.

参考 pjsua_verify_sip_url()

参数 prmUri - URL字符串

17)Token utilTimerSchedule(unsigned prmMsecDelay, Token prmUserData)

设定具有指定间隔和用户数据的定时器。当间隔时间到时,将调用onTimer()回调。注意,回调可能由不同的线程执行,具体取决于是否启用了工作线程

返回 令牌识别定时器,可以用utilTimerCancel()取消定时器

参数 prmMsecDelay -时间间隔,单位为毫秒

prmUserData - 任意用户数据,被回馈给应用程序在回调。

18)void utilTimerCancel(Token prmToken) 使用指定的定时器令牌取消先前指定的定时器

参数 prmToken -从以前的utilTimerSchedule()调用返回的计时器令牌。

19)void utilAddPendingJob(PendingJob *job)

注册要由主线程执行的待处理作业的实用程序。如果EpConfig :: UaConfig :: mainThreadOnly为false,该作业将立即执行。

参数 job 工作类

20)IntVector utilSslGetAvailableCiphers() 获取SSL / TLS后端支持的密码列表。

21)void natDetectType(void) 这是一个在这个端点前面检测NAT类型的通用函数,一旦成功调用,此函数将异步完成,并在onNatDetectionComplete()中报告结果.在检测到NAT并调用回调后,应用程序可以通过调用natGetType()获取检测到的NAT类型。应用程序还可以在稍后再次调用natDetectType()来执行NAT检测。注意,必须启用STUN才能成功运行此功能。

22)pj_stun_nat_type natGetType() 获取natDetectType()函数检测到的NAT类型。

natDetectType()已成功完成并且已调用onNatDetectionComplete()回调函数后,此函数将仅返回有用的NAT类型。

异常:如果在检测过程中调用此函数,将引发PJ_EPENDING异常。

23)void natUpdateStunServers(const StringVector &prmServers, bool prmWait)更新STUN服务器列表。libInit()必须在调用这个函数之前调用。

参数prmServers - STUN服务器数组尝试。端点将尝试解析并联系每个STUN服务器条目,直到找到可用的条目。每个条目可能是域名,主机名,IP地址,并且可能包含可选的端口号。例如:

“pjsip.org”(域名)

“sip.pjsip.org”(主机名)

“pjsip.org:33478”(域名和非标准端口号)

“10.0.0.1:3478”(IP地址和端口号)

prmWait - 指定函数是否应该阻塞,直到得到结果。在这种情况下,函数将在分辨率完成时阻塞,并且在该函数返回之前调用onNatCheckStunServersComplete()

24)void natCheckStunServers(const StringVector &prmServers, bool prmWait, TokenprmUserData)  辅助函数,用于解析和联系每个STUN服务器条目(依次)以查找哪个可用。须先调用libInit()

  参考   natCancelCheckStunServers()

  参数 prmServers - STUN服务器数组尝试。端点将尝试解析并联系每个STUN服务器条目,直到找到可用的条目。每个条目可能是域名,主机名,IP地址,并且可能包含可选的端口号。例如:

“pjsip.org”(域名)

“sip.pjsip.org”(主机名)

“pjsip.org:33478”(域名和非标准端口号)

“10.0.0.1:3478”(IP地址和端口号)

 prmWait - 指定函数是否应该阻塞,直到得到结果。在这种情况下,函数将在分辨率完成时阻塞,并且在该函数返回之前调用回调函数。prmUserData - 任意用户数据要在回调中传回应用程序。

25)void natCancelCheckStunServers(Token token, bool notify_cb = false)取消与指定令牌匹配的待决STUN分辨率。

异常:如果没有匹配的PJ_ENOTFOUND或其他错误。

参数token -令牌匹配。 这个令牌给了natCheckStunServers()

notify_cb -布尔值,用于控制是否为已取消的方案调用回调。 当调用回调时,结果中的状态将被设置为PJ_ECANCELLED。

26)TransportId transportCreate ( pjsip_transport_type_e type , const TransportConfig & cfg ) 根据指定的设置创建并启动新的SIP传输。

返回传输ID。

参数type -传输类型。cfg -传输配置。

27)IntVector transportEnum ( )

枚举系统中当前创建的所有传输。

此函数将返回所有传输ID,然后应用程序可以调用transportGetInfo()函数来检索有关传输的详细信息。

返回 传输ID数组。

28)TransportInfo transportGetInfo ( TransportId id )

获取有关传输的信息。

返回 交通信息

参数 id - 运输ID。

29)void transportSetEnable ( TransportId id ,bool enabled )

禁用传输或重新启用它。

默认情况下,传输始终在创建后启用。 禁用传输不一定关闭套接字,它只会丢弃传入的消息,并阻止传输被用于发送传出的消息。

参数id -传输ID。

enabled -启用或禁用传输。

30)void transportClose ( TransportId id ) 关闭传输。

系统将等待所有事务关闭,同时防止新用户使用传输,并在其使用次数达到零时关闭传输。

参数id -传输ID。

31)void transportShutdown ( TransportHandle tp )

启动此传输句柄的正常关闭程序。

在正常关闭初始化后,传输获取不到新的引用。 然而,当前使用传输的现有对象仍然可以使用该传输来发送和接收数据包。 所有的对象释放对该引用后传输将立即销毁。

注意:从回调onTransportState()获取句柄后,应用程序通常会使用此API。

参数tp -传输句柄。

32)void hangupAllCalls ( void )

终止所有呼叫

这将启动所有当前活动呼叫的呼叫挂断。

33)void mediaAdd ( AudioMedia & media )

将媒体添加到媒体列表。

参数media -要添加的媒体

34)void mediaRemove ( AudioMedia & media )

从媒体列表中删除媒体。

参数media -要移除的媒体

35)bool mediaExists ( const AudioMedia & media ) const

检查媒体是否已添加到媒体列表。

返回 如果添加了媒体,则为真,否则为false。

参数media -要检查的媒体

36)unsigned  mediaMaxPorts ( ) mediaMaxPorts获取媒体端口的最大数量

返回 会议桥中媒体端口的最大数量。

37)unsigned  mediaActivePorts ( ) mediaActivePorts

获取桥活性介质端口的当前数目

返回会议桥中媒体端口的最大数量

38)const AudioMediaVector & mediaEnumPorts ( ) mediaEnumPorts

枚举所有媒体端口。

返回 媒体端口列表。

39)AudDevManager & audDevManager ( )

获取音频设备管理器的实例。

返回 音频设备管理器。

40)VidDevManager & vidDevManager ( )

获取视频设备管理器的实例。

返回 视频设备管理器。

41)const CodecInfoVector & codecEnum ( )

枚举所有支持的编解码器在系统中。

返回 编解码器信息数组。

42)void codecSetPriority ( const string& codec_id ,pj_uint8_t priority )

更改编解码优先级

参数codec_id -编解码器ID,它是唯一标识编解码器的字符串(如“speex / 8000”)。

priority -编解码器优先级0-255,其中0表示禁用编解码器。

43)CodecParam codecGetParam ( const string& codec_id ) const¶

获取编解码器参数

返回 编解码器参数。 如果没有找到编解码器,则会抛出错误 。

参数codec_id - 编解码器ID。

44)void codecSetParam ( const string& codec_id , const CodecParam param )

设置编解码器参数

参数 codec_id - 编解码器ID。

param -编解码器参数设置。 设置为NULL将编解码器参数重置为库默认设置。

45)const CodecInfoVector & videoCodecEnum ( )

枚举所有支持的视频编解码器在系统中。

返回  视频编解码器信息数组。

46)void videoCodecSetPriority ( const string& codec_id ,pj_uint8_t priority )

更改视频编解码优先级

参数codec_id - 编解码器ID,其是唯一标识编解码器的字符串(例如“H263 / 90000”)。 有关详细信息,请参阅pjsua手册或pjmedia编解码器参考。

priority - 编解码器优先级0-255,其中0表示禁用编解码器。

47)VidCodecParam getVideoCodecParam ( const string& codec_id ) const¶

获取视频编解码器参数。

返回

编解码器参数。 如果没有找到编解码器,则会抛出错误 。

参数

  • codec_id -

编解码器ID。

48)void setVideoCodecParam ( const string& codec_id , const VidCodecParam & param )

设置视频编解码器参数。

参数

  • codec_id -

编解码器ID。

  • param -

编解码器参数设置。

49)void resetVideoCodecParam ( const string& codec_id )

将视频编解码器参数重置为库默认设置。

参数

  • codec_id -

编解码器ID。

50)virtual void onNatDetectionComplete ( const OnNatDetectionCompleteParam & prm )

端点完成使用natDetectType()启动的NAT类型检测时的回调。

参数

  • prm -

包含检测结果的回调参数。

51)virtual  void onNatCheckStunServersComplete ( const OnNatCheckStunServersCompleteParam & prm )

当Endpoint完成执行调用libInit()或调用natCheckStunServers()或natUpdateStunServers()时启动的STUN服务器检查时的回调。

参数prm -回调参数

52)virtual  void onTransportState ( const OnTransportStateParam & prm )

传输状态发生变化时调用此回调。

参数prm -回调参数

53)virtual  void onTimer ( const OnTimerParam & prm )

计时器触发时回调。

计时器由utilTimerSchedule()调度 。

参数prm -回调参数

54)virtual  void onSelectAccount ( OnSelectAccountParam & prm )

应用程序可以使用此回调来覆盖用于处理传入消息的帐户。

最初,使用的帐户将由图书馆自动计算。 如果应用程序没有实现此回调,则该初始帐户将被使用,或者从此回调返回时应用程序设置无效的帐户。

请注意,目前需要帐号分配的传入消息是INVITE,MESSAGE,SUBSCRIBE和未经请求的NOTIFY。 这个回调可以在SIP事件本身的回叫之前被调用,即:来电,寻呼机,订阅或者非请求事件。

参数 prm - 回调参数

 公共静态功能

55)static Endpoint & instance ( )

检索端点的单例实例。

4.7.2 端点配置

Endpoint
struct pj::EpConfig
#include <endpoint.hpp>

endpoint配置

公共功能

  void readObject(const ContainerNode&node)

    从容器读取此对象。

    参数

    •node - 要从中写入值的容器。

  void writeObject(ContainerNode&node)

    将此对象写入容器。

    参数

      •node - 要将值写入的容器。

公有成员

  UaConfig uaConfig

    UA配置

  LogConfig logConfig

    记录配置。

  MediaConfig medConfig

    媒体配置

媒体

struct pj::MediaConfig

此结构描述媒体配置,在调用Lib :: init()时指定。

从pj :: PersistentObject继承

记录

struct pj::LogConfig

记录配置,可以(可选)在调用Lib :: init()时指定。

从pj :: PersistentObject继承

class pj::LogWriter

用于编写日志消息的界面

应用程序可以继承此类并在LogConfig结构中提供它,以实现自定义日志写入工具。

公共函数

virtual ~LogWriter()

析构器

virtual void write(const LogEntry&entry ) = 

写一个日志条目。

struct pj::LogEntry

包含由LogWriter写入的日志条目的数据。

用户代理

struct pj::UaConfig

SIP用户代理相关设置。

从pj :: PersistentObject继承

回调参数

struct pj::OnNatDetectionCompleteParam

Endpoint :: onNatDetectionComplete()回调的参数。

struct pj::OnNatCheckStunServersCompleteParam

对Endpoint :: onNatCheckStunServersComplete()回调的参数。

struct pj::OnTimerParam

端点::的OnTimer()回调的参数

struct pj::OnTransportStateParam

端点:: onTransportState()回调的参数

struct pj::OnSelectAccountParam

端点:: onSelectAccount()回调的参数

其他

struct pj::PendingJob

PJSUA2开发文档--第四章 端点ENDPOINT的更多相关文章

  1. PJSUA2开发文档--第五章 帐户(号)Accounts

    第五章 帐户(号) 帐户提供正在使用该应用程序的用户的身份(或身份).一个帐户有一个与之相关的SIP统一资源标识符(URI).在SIP术语中,该URI用作该人的记录地址( Address of Rec ...

  2. PJSUA2开发文档--第三章 PJSUA2高级API

    3. PJSUA2高级API PJSUA2是PJSUA API以上的面向对象抽象.它为构建会话发起协议(SIP)多媒体用户代理应用程序(也称为IP / VoIP软电话)提供高级API.它将信令,媒体和 ...

  3. PJSUA2开发文档--第七章 呼叫 Calls类

    7   呼叫Calls 呼叫由Call类处理 7.1 子类化Call类 要使用Call类,应用程序应创建子类,如: class MyCall : public Call { public: MyCal ...

  4. PJSUA2开发文档--第六章 媒体 Media类

    6. 媒体(Media) 媒体对象是能够产生媒体或接受媒体的对象. Media的重要子类是AudioMedia,它代表音频媒体.PJSUA2支持多种类型的音频媒体对象: 捕获设备的AudioMedia ...

  5. PJSUA2开发文档--第十一章 网络问题

    11 网络问题 11.1 IP地址更改 请参阅wiki 处理IP地址更改.请注意,本指南使用PJSUA API作为参考. 11.2 被阻止/过滤的网络 请参阅维基百科 通过阻止或过滤的VoIP网络

  6. PJSUA2开发文档--第十二章 PJSUA2 API 参考手册

    12 PJSUA2 API 参考手册 12.1 endpoint.hpp PJSUA2基本代理操作.  namespace pj PJSUA2 API在pj命名空间内. 12.1.1 class En ...

  7. PJSUA2开发文档--第九章 PJSUA2应用程序示例

    9. PJSUA2示例应用程序 9.1 示例应用程序 9.1.1 C++ pjsip-apps/src/samples/pjsua2_demo.cpp 是一个非常简单可用的C++示例应用程序. /* ...

  8. PJSUA2开发文档--第八章 好友(Buddy)类

    8  好友(存在)Buddy PJSUA2的功能是围绕Buddy类为中心展开的.该类表示一个远端好友(伙伴,一个人或一个SIP端点). 8.1 子类化Buddy类 要使用Buddy类,通常应创建子类, ...

  9. PJSUA2开发文档--第十章 媒体质量(MEDIA QUALITY)

    10 媒体质量(Media Quality) 10.1 音频质量 如果遇到音频质量问题,可尝试以下步骤: 遵循指南:使用pjsystest测试声音设备. 识别声音问题并使用以下步骤进行故障排除:检查声 ...

随机推荐

  1. [Swift]LeetCode509. 斐波那契数 | Fibonacci Number

    The Fibonacci numbers, commonly denoted F(n) form a sequence, called the Fibonacci sequence, such th ...

  2. [Swift]LeetCode992. K 个不同整数的子数组 | Subarrays with K Different Integers

    Given an array A of positive integers, call a (contiguous, not necessarily distinct) subarray of A g ...

  3. Android开发:在Eclipse中配置Android环境

    一.文件需要: https://pan.baidu.com/s/1-XCSSPW5JGyPRlvwRVSfmA 提取码:m5t8 NDK过大没有上传在这个文件里. 二.在Eclipse中配置Tools ...

  4. 51Nod-1006 最长公共子序列Lcs

    题目链接 Description 给出两个字符串A B,求A与B的最长公共子序列(子序列不要求是连续的). 比如两个串为: abcicba abdkscab ab是两个串的子序列,abc也是,abca ...

  5. 使用dotnet build时复制引用dll到生成目录

    默认配置下dotnet build只会输出项目代码的dll,依赖用的是dotnet缓存中的dll,只有dotnet publish才会把依赖的dll一起输出到生成目录. 在项目csproj文件中添加以 ...

  6. C++版 - 剑指offer 面试题23:从上往下打印二叉树(二叉树的层次遍历BFS) 题解

    剑指offer  面试题23:从上往下打印二叉树 参与人数:4853  时间限制:1秒  空间限制:32768K 提交网址: http://www.nowcoder.com/practice/7fe2 ...

  7. Vimtutor(中文版)学习笔记各章小结

    在Bash中直接输入 vimtutor 打开新世界!通常阅读完需要25-30分钟不等,像玩游戏一样学会vim,然后爱不释手 里面设置了很多例子通过实践操作来学,这里仅把vimtutor上的每一章小结记 ...

  8. 使用RESTful风格开发Java Web

    什么是RESTful风格? REST是REpresentational State Transfer的缩写(一般中文翻译为表述性状态转移),REST 是一种体系结构,而 HTTP 是一种包含了 RES ...

  9. Java——重载和重写

    前言 在程序设计中经常会遇到对对方法的重载或者重写,下面将介绍重载和重写. 重载(Overloade) 重载出现的原因 任何程序设计语言都具备的一项重要特性就是对名字的运用.当创建一个对象时,就给对象 ...

  10. 关于setState的一些记录

    在看React的官方文档的时候, 发现了这么一句话,State Updates May Be Asynchronous,于是查询了一波相关的资料, 最后归纳成以下3个问题 setState为什么要异步 ...