2023年4月

一致性读视图是InnoDB在实现MVCC用到的虚拟结构,用于读提交(RC)和可重复度(RR)隔离级别的实现。

一致性视图没有物理结构,主要是在事务执行期间用来定义该事物可以看到什么数据。

一、Read View

事务在正式启动的时候我们会创建一致性视图,该一致性视图是基于整个库的。

1、transaction id

InnodDB的每个事务都有一个唯一的事务ID,叫做transaction id,该ID在事务开始的时候向InnoDB申请,并且按照申请顺序严格递增。

每行数据都会有多个版本,每次事务更新数据的时候都会生成一个新的数据版本,并且把transaction id赋值给这个数据版本的事务id,称为row trx_id。

上图是一条行数据的多个版本,最新的版本是 V4。

其中U3、U2、U1代表的是undo log,V1、V2、V3在物理上并不真实存在,而是在需要的时候通过V4配合undo log计算获得。

2、ReadView如何工作

ReadView中主要包含4个比较重要的内容:

  • m_ids:表示在生成ReadView时当前系统中活跃的读写事务的事务id列表。
  • min_trx_id:表示在生成ReadView时当前系统中活跃的读写事务中最小的事务id,也就是m_ids中的最小值。
  • max_trx_id:表示生成ReadView时系统中应该分配给下一个事务的id值。
  • creator_trx_id:表示生成该ReadView的事务的事务id。

在访问某条记录时,按照下边的步骤判断记录的某个版本是否可见:

  • 如果被访问版本的trx_id属性值与ReadView中的creator_trx_id值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。
  • 如果被访问版本的trx_id属性值小于ReadView中的min_trx_id值,表明生成该版本的事务在当前事务生成ReadView前已经提交,所以该版本可以被当前事务访问。
  • 如果被访问版本的trx_id属性值大于ReadView中的max_trx_id值,表明生成该版本的事务在当前事务生成ReadView后才开启,所以该版本不可以被当前事务访问。
  • 如果被访问版本的trx_id属性值在ReadView的min_trx_id和max_trx_id之间,那就需要判断一下trx_id属性值是不是在m_ids列表中,如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问。

3、记录未提交的场景

如果某个版本的数据对当前事务不可见的话,那就顺着版本链找到下一个版本的数据,继续按照上边的步骤判断可见性,依此类推,直到版本链中的最后一个版本。

如果最后一个版本也不可见的话,那么就意味着该条记录对该事务完全不可见,查询结果就不包含该记录。

二、不同隔离级别下生成ReadView区别

在MySQL中,READ COMMITTED和REPEATABLE READ隔离级别的区别就是它们生成ReadView的时机不同。

1、可重复读隔离

MySQL的默认隔离级别是RR(可重复读),按照 可重复读的语义,每个事务启动的时候只能看到已经提交的事务,并且在本事务执行的过程中,不可以读取到其他事务的更新操作。

在InnoDB 中,为每个事务构造了一个  当前事务ID数组的快照,就是记录事务开启时,当前正在执行的事务ID 的集合。数组里面 trx_id 最小的记为 低水位,trx_id 最大的 + 1 记为高水位。如下图所示:

对于一个新事务而言,所读取到的记录版本的 trx_id 可能有以下几种情况:

1、在绿色区域:说明数据版本在事务开始前已提交,当前版本是可见的。

2、在红色区域:说明数据版本在事务开始后变更的,当前版本是不可见的。

3、在橙色区域:包含 2 种情况。

A、如果 数据版本的 trx_id 在数组中,说明是正在执行的事务,不可见。

B、如果 数据版本的 trx_id 不在数组中,说明是已经提交的事务,可见。

可以看出,InnoDB 利用了 UndoLog 数据多版本的特点,实现了快速创建快照的能力。

2、读已提交

对可重复读来说,事务只有在第一次进行读操作时才会生成一个ReadView,后续的读操作都会重复使用这个ReadView。

也就是说,如果在此期间有其他事务提交了,那么对于可重复读来说也是不可见的,因为对它来说,事务活跃状态在第一次进行读操作时就已经确定下来,后面不会修改了。

对读已提交来说,事务中的每次读操作都会生成一个新的ReadView。

也就是说,如果这期间某个未提交事务Commit了,那么它就会从ReadView中移除,添加到已提交事务中,这样确保RC级别下事务每次读操作都能读到已经提交的数据。

参考资料:

《高性能 MySQL》第一章第 4 节;

极客时间《MySQL 实战 45 讲》

数字营销,也就是网络营销,即在线推广你的产品或服务。

其核心的是:
在正确的客户面前在线推广正确的产品
Right Customer Right Product Online

一、关于市场研究

file
网络营销中,最重要的第一步是市场研究(Marketing Research)。
包括以下两个方面:

  • 谁是目标客户?(Who the target customer)
  • 在哪里找到他们?(Where to find them)

目标客户的定义,就是对你提供的产品或服务感兴趣的受众群体。

成功的营销,最关键的是在正确的受众面前展示正确的产品。
而不是广撒网,漫无目的的展开营销。

例如:你的产品是专属 SaaS 行业公司的 CRM 系统。投放「CRM For SaaS Company」这个搜索词,肯定比「客户关系管理系统」「CRM」等词更佳精准。

二、客户画像定义浅谈

file
要知道正确的受众是谁,需要了解客户画像。客户画像是网络营销中非常重要的概念,指的是理解目标客户的特征和需求。

如果产品已经上线一段时间,可以通过数据收集、客户访谈打标签或谷歌统计等工具,分析付费客户的画像。

但产品都没上线,肯定是没有产品付费客户对应的画像。那就应该通过以下维度大致勾画出付费客户画像,比如

  • 性别
  • 年龄段
  • 工作(行业、职位、年收入等)
  • 兴趣
  • 社会属性、消费行为、心理特征等等

然后分析,客户对应的各个痛点是什么?他们的目标是什么?产品这块如何提供解决方案。

这才不会在网络营销中,浪费人力财力和时间成本。只有先清楚客户画像,然后去投放、去 SEO 或内容营销等其他方式才能够精准。

这里是强调客户画像对网络营销中的指导作用,其实客户画像对点有很高的价值:

  • 精细化运营、提高用户服务全生命周期的每个环节
  • 更好洞察需求、优化现有的产品和服务

三、小结

在进行网络营销之前,必须确定正确的客户画像。如果画像不准确,可以及时调整并迭代,以精准地吸引目标客户并提供优质的服务。

总结来说,客户画像不仅在网络营销中具有指导作用,而且还对于精细化运营和提高用户服务全生命周期的每个环节,以及优化现有产品和服务有很高的价值。

原文

https://bysocket.com/saas-digital-marketing-target-customer/

出处:公号「程序员泥瓦匠」
博客:
https://bysocket.com/

内容涵盖 Java 后端技术、Spring Boot、Spring Cloud、微服务架构、运维开发、系统监控等相关的研究与知识分享。


springboot:2.3.12.RELEASE
中内嵌的
tomcat-embed-core:9.0.46
为例,进行分析

1 概述

1.0 关键依赖包

  • spring-boot-autoconfigure
    : 2.3.12.RELEASE
  • spring-boot
    : 2.3.12.RELEASE
  • spring-context
    : 5.2.15.RELEASE
  • spring-webmvc
    : 5.2.15.RELEASE
  • tomcat-embed-core
    :9.0.46
  • tomcat-embed-jasper
    :9.0.46

1.1 内嵌 Web Server 的优势

我们在使用 springboot 开发 web 项目时,大多数时候采用的是内置的 Tomcat (当然也可配置支持内置的 jett y),内置 Tomcat 有什么好处呢?

  • 方便微服务部署,减少繁杂的配置
  • 方便项目启动,不需要单独下载web容器,如Tomcat,jetty等。

1.2 Web Server 的优化思路

针对目前的容器优化,可以从以下几点考虑:

  • 1、线程数

首先,线程数是一个重点,每一次HTTP请求到达Web服务器,Web服务器都会创建一个线程来处理该请求,该参数决定了应用服务同时可以处理多少个HTTP请求。
比较重要的有两个:1) 初始线程数; 2) 最大线程数。

  • 初始线程数:保障启动的时候,如果有大量用户访问,能够很稳定的接受请求。
  • 最大线程数:用来保证系统的稳定性。
  • 2、超时时间

超时时间:用来保障连接数不容易被压垮。
如果大批量的请求过来,延迟比较高,很容易把线程数用光,这时就需要提高超时时间。
这种情况在生产中是比较常见的 ,一旦网络不稳定,宁愿丢包也不能把服务器压垮。

  • 3、JVM优化

1.3 Tomcat Web Server的核心配置参数

min-spare-threads

默认 10
最小备用线程数,tomcat启动时的初始化的线程数。

max-threads

默认 200
Tomcat可创建的最大的线程数,每一个线程处理一个请求;
超过这个请求数后,客户端请求只能排队,等有线程释放才能处理。
建议:这个配置数可以在服务器CUP核心数的 200~250 倍之间

accept-count

默认 100
当调用Web服务的HTTP请求数达到tomcat的最大线程数时,还有新的HTTP请求到来,这时tomcat会将该请求放在等待队列中
这个acceptCount就是指能够接受的最大等待数
如果等待队列也被放满了,这个时候再来新的请求就会被tomcat拒绝(connection refused)。

max-connections

这个参数是指在同一时间,tomcat能够接受的最大连接数。(最大线程数+排队数)
一般这个值要大于 (max-threads)+(accept-count)。

connection-timeout

1 默认值: 60S or 20S
2 参数定义: 与客户端建立连接后,Tomcat 等待客户端请求的时间。 如果客户端没有请求进来,等待一段时间后断开连接,释放线程。
3 备注说明: Tomcat 中 等效于 : socket.soTimeout (SO_TIMEOUT) => 即: 为 socket 调用 read() 等待读取的时间
4 入口类:

keepAliveTimeout

Tomcat 在关闭连接(Connector)之前,等待另一个请求的时间

  • HTTP 1.0

http协议的早期是,每开启一个http链接,是要进行一次socket,也就是新启动一个TCP链接。

  • HTTP 1.1

1 特性:长连接 (现主流浏览器的默认协议)
2 使用keep-alive可以改善这种状态,即在一次TCP连接中可以持续发送多份数据而不会断开连接。通过使用keep-alive机制,可以减少tcp连接建立次数。
3 如果浏览器支持keepalive的话,那么请求头中会有: Connection: Keep-Alive
4 对于keepalive的部分,主要集中在Connection属性当中,这个属性可以设置两个值:

  • close (告诉WEB服务器或者代理服务器,在完成本次请求的响应后,断开连接,不要等待本次连接的后续请求了)。
  • keepalive (告诉WEB服务器或者代理服务器,在完成本次请求的响应后,保持连接,等待本次连接的后续请求)。
    5 keep-alive与TIME_WAIT的关系?
  • 使用http keep-alive,可以减少服务端TIME_WAIT数量(因为由服务端httpd守护进程主动关闭连接)。道理很简单,相较而言,启用keep-alive,建立的tcp连接更少了,自然要被关闭的tcp连接也相应更少了。
  • 什么是TIME_WAIT呢?
    • 通信双方建立TCP连接后,主动关闭连接的一方就会进入TIME_WAIT状态。
    • 客户端主动关闭连接时,会发送最后一个ack后,然后会进入TIME_WAIT状态,再停留2个MSL时间,进入CLOSED状态。
  • 那么这个TIME_WAIT到底有什么作用呢?主要原因:
    • a)可靠地实现TCP全双工连接的终止
    • b)允许老的重复分节在网络中消逝
      6 截止目前,我们讨论的是 http 1.1 request/response header 的 keep-alive 选项;而 tcp协议 也有keepalive的概念。
http keep-alive与tcp keep-alive,不是同一回事,意图不一样。

http keep-alive是为了让tcp活得更久一点,以便在同一个连接上传送多个http,提高socket的效率。

而tcp keep-alive是TCP的一种检测TCP连接状况的保鲜机制。

tcp keep-alive保鲜定时器,支持三个系统内核配置参数:
	echo 1800 > /proc/sys/net/ipv4/tcp_keepalive_time
	echo 15 > /proc/sys/net/ipv4/tcp_keepalive_intvl
	echo 5 > /proc/sys/net/ipv4/tcp_keepalive_probes
	
keepalive是TCP保鲜定时器,当网络两端建立了TCP连接之后,闲置idle(双方没有任何数据流发送往来)了tcp_keepalive_time后,服务器内核就会尝试向客户端发送侦测包,来判断TCP连接状况(有可能客户端崩溃、强制关闭了应用、主机不可达等等)。如果没有收到对方的回答(ack包),则会在 tcp_keepalive_intvl后再次尝试发送侦测包,直到收到对对方的ack,如果一直没有收到对方的ack,一共会尝试 tcp_keepalive_probes次,每次的间隔时间在这里分别是15s, 30s, 45s, 60s, 75s。如果尝试tcp_keepalive_probes,依然没有收到对方的ack包,则会丢弃该TCP连接。TCP连接默认闲置时间是2小时,一般设置为30分钟足够了。
总结一下,实际上tcp keep-alive是一个协议级别的心跳检测实现,当超过规定的时间,tcp就断开,而这边是讨论的http的keepalive,描述的http高层多次tcp链接共享,根本不是一个网络层级的东西,一定注意不要混淆。

1.4 springboot --> tomcat 源码分析

spring-boot-autoconfigure : 2.3.12.RELEASE

-> org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration
    @ConditionalOnClass({Tomcat.class, UpgradeProtocol.class})
    public static class TomcatWebServerFactoryCustomizerConfiguration { [*]
        @Bean
        public TomcatWebServerFactoryCustomizer tomcatWebServerFactoryCustomizer(Environment environment, ServerProperties serverProperties){
            return new TomcatWebServerFactoryCustomizer(environment, serverProperties);
        }
    }
    
-> org.springframework.boot.autoconfigure.web.embedded.TomcatWebServerFactoryCustomizer
    + 关系: public class TomcatWebServerFactoryCustomizer implements WebServerFactoryCustomizer<ConfigurableTomcatWebServerFactory>, Ordered { /** ... **/ }
    + 属性:
        private final Environment environment;
        private final org.springframework.boot.autoconfigure.web.ServerProperties serverProperties; [*]
    + 方法:
        public void customize(ConfigurableTomcatWebServerFactory factory) { 
            ServerProperties properties = this.serverProperties;
            ServerProperties.Tomcat tomcatProperties = properties.getTomcat();
                --> Tomcat { // 内部类
                    private final Threads threads = new Threads();
                    ...
                    private int maxConnections;
                    private int acceptCount;
                    ...
                    private Duration connectionTimeout;
                    ...
                    private Charset uriEncoding;
                    --> Threads { // 内部类
                        private int max = 200;
                        private int minSpare = 10;
                    }
                }
            PropertyMapper propertyMapper = PropertyMapper.get();
            
            ServerProperties.Tomcat.Threads threadProperties = tomcatProperties.getThreads();
            ...
            propertyMapper.from(threadProperties::getMax).when(this::isPositive).to((maxThreads) -> {
                this.customizeMaxThreads(factory, threadProperties.getMax());
            });
            ...
            propertyMapper.from(threadProperties::getMinSpare).when(this::isPositive).to((minSpareThreads) -> {
                this.customizeMinThreads(factory, minSpareThreads);
            });
            ...
            propertyMapper.from(tomcatProperties::getMaxHttpFormPostSize).asInt(DataSize::toBytes).when((maxHttpFormPostSize) -> {
                return maxHttpFormPostSize != 0;
            }).to((maxHttpFormPostSize) -> {
                this.customizeMaxHttpFormPostSize(factory, maxHttpFormPostSize);
            });
            ...
            propertyMapper.from(tomcatProperties::getAccesslog).when(ServerProperties.Tomcat.Accesslog::isEnabled).to((enabled) -> {
                this.customizeAccessLog(factory);
            });
            ...
            propertyMapper.from(tomcatProperties::getUriEncoding).whenNonNull().to(factory::setUriEncoding);
            ...
            propertyMapper.from(tomcatProperties::getConnectionTimeout).whenNonNull().to((connectionTimeout) -> {
                this.customizeConnectionTimeout(factory, connectionTimeout);
            });
            ...
            propertyMapper.from(tomcatProperties::getMaxConnections).when(this::isPositive).to((maxConnections) -> {
                this.customizeMaxConnections(factory, maxConnections);
            });
            ...
            propertyMapper.from(tomcatProperties::getAcceptCount).when(this::isPositive).to((acceptCount) -> {
                this.customizeAcceptCount(factory, acceptCount);
            });
        }
        
        private void customizeAcceptCount(ConfigurableTomcatWebServerFactory factory, int acceptCount) {
            factory.addConnectorCustomizers(new TomcatConnectorCustomizer[]{(connector) -> {
                ProtocolHandler handler = connector.getProtocolHandler();
                if (handler instanceof AbstractProtocol) {
                    AbstractProtocol<?> protocol = (AbstractProtocol)handler;
                    protocol.setAcceptCount(acceptCount);
                }
    
            }});
        }
        ...
        private void customizeMaxConnections(ConfigurableTomcatWebServerFactory factory, int maxConnections) {
            factory.addConnectorCustomizers(new TomcatConnectorCustomizer[]{(connector) -> {
                ProtocolHandler handler = connector.getProtocolHandler();
                if (handler instanceof AbstractProtocol) {
                    AbstractProtocol<?> protocol = (AbstractProtocol)handler;
                    protocol.setMaxConnections(maxConnections);
                }
    
            }});
        }
        ...

X 参考文献

很多刚接触Nordic nRF5 SDK的初学者出于对新平台的不熟悉,会觉得这个SDK很难,本文讲浅析nRF5 SDK中例程的架构,让初学者能够快速上手SDK。

在开始之前,先推荐阅读观看下面这些文章和视频,这些文章和视频都出自Nordic中国区的FAE,强烈推荐。

1、【[nRF5] nRF5 SDK Getting Start-哔哩哔哩】
https://b23.tv/FxfJBVW

2、【Nordic nRF5 SDK和softdevice介绍】
https://www.cnblogs.com/iini/p/9096666661.html

3、

nRF5 SDK软件架构及softdevice工作原理


https://www.cnblogs.com/iini/p/9332463.html

其实在

nRF5 SDK软件架构及softdevice工作原理

】这篇文章中已经总结了SDK的软件架构,这里我直接引用出来:

“当我们开发Nordic平台的BLE应用时,主要需要做两件事:

  1. 第1件事:初始化。为了简化初始化工作,Nordic SDK所有模块初始化时,只需要将相应API输入结构体参数清0即可完成初始化工作,也就是说,只要你保证初始化参数为0,蓝牙协议栈就可以工作起来,这对很多Nordic初学者来说,大大减轻了开发工作量。
  2. 第2件事:写蓝牙事件回调处理函数。一般来说,你的应用逻辑都是放在蓝牙事件回调处理函数中,所以写好回调处理函数代码,你的开发工作就完成了大半了。”

本文将通过介绍 bsp 和 ble_app_uart 这两个例程来分析nRF5 SDK的例程架构,这两个例程可以在nRF5_SDK_17.1.0_ddde560\examples\peripheral和nRF5_SDK_17.1.0\examples\ble_peripheral下找到。

一、bsp 例程浅析

bsp是不带协议栈的裸机例程,所以main函数非常简单,下面具体分析一下这几个函数的作用。

int main(void)
{
clock_initialization();

uint32_t err_code
=app_timer_init();
APP_ERROR_CHECK(err_code);

APP_ERROR_CHECK(NRF_LOG_INIT(NULL));
NRF_LOG_DEFAULT_BACKENDS_INIT();

NRF_LOG_INFO(
"BSP example started.");
bsp_configuration();
while (true)
{
NRF_LOG_FLUSH();
__SEV();
__WFE();
__WFE();
//no implementation needed }
}

1、clock_initialization是直接通过配置寄存器来配置低频时钟,这里是使用了外部32.768kHz晶振做为时钟源。需要注意的是,因为bsp是不带协议栈的裸机工程,所以如果用到低频时钟源的时候,需要对其初始化。

/**@brief Function for initializing low frequency clock.*/
voidclock_initialization()
{
NRF_CLOCK
->LFCLKSRC = (CLOCK_LFCLKSRC_SRC_Xtal <<CLOCK_LFCLKSRC_SRC_Pos);
NRF_CLOCK
->EVENTS_LFCLKSTARTED = 0;
NRF_CLOCK
->TASKS_LFCLKSTART = 1;while (NRF_CLOCK->EVENTS_LFCLKSTARTED == 0)
{
//Do nothing. }
}

在带协议栈的例程中,因为协议栈会用到低频时钟源。协议栈初始化的时候调用了nrf_sdh_enable_request,我们可以从该函数中看到在sd_softdecice_enable写入低频时钟配置对低频时钟源进行了初始化,所以在带协议栈的例程中只要初始化协议栈即可,不用再初始化低频时钟源。

ret_code_t nrf_sdh_enable_request(void)
{
ret_code_t ret_code;
if(m_nrf_sdh_enabled)
{
returnNRF_ERROR_INVALID_STATE;
}

m_nrf_sdh_continue
= true;//Notify observers about SoftDevice enable request. if (sdh_request_observer_notify(NRF_SDH_EVT_ENABLE_REQUEST) ==NRF_ERROR_BUSY)
{
//Enable process was stopped. returnNRF_SUCCESS;
}
//Notify observers about starting SoftDevice enable process. sdh_state_observer_notify(NRF_SDH_EVT_STATE_ENABLE_PREPARE);
  //配置低频时钟的参数
nrf_clock_lf_cfg_t
const clock_lf_cfg ={
.source
=NRF_SDH_CLOCK_LF_SRC,
.rc_ctiv
=NRF_SDH_CLOCK_LF_RC_CTIV,
.rc_temp_ctiv
=NRF_SDH_CLOCK_LF_RC_TEMP_CTIV,
.accuracy
=NRF_SDH_CLOCK_LF_ACCURACY
};

CRITICAL_REGION_ENTER();
#ifdef ANT_LICENSE_KEY
ret_code
= sd_softdevice_enable(&clock_lf_cfg, app_error_fault_handler, ANT_LICENSE_KEY);#elseret_code= sd_softdevice_enable(&clock_lf_cfg, app_error_fault_handler);#endifm_nrf_sdh_enabled= (ret_code ==NRF_SUCCESS);
CRITICAL_REGION_EXIT();
if (ret_code !=NRF_SUCCESS)
{
returnret_code;
}

m_nrf_sdh_continue
= false;
m_nrf_sdh_suspended
= false;//Enable event interrupt.//Interrupt priority has already been set by the stack. softdevices_evt_irq_enable();//Notify observers about a finished SoftDevice enable process. sdh_state_observer_notify(NRF_SDH_EVT_STATE_ENABLED);returnNRF_SUCCESS;
}

完成低频时钟的配置后,接下来是调用 app_timer_init 对定时器进行初始化。这个函数是Nordic的库中已经封装好的函数,所以在main直接调用即可,nRF52系列有 Timer0-Timer4 一共5个Timer可以用,这里需要注意的是协议栈开启后会占用Timer0。

这里推荐阅读Nordic中国区FAE写的一篇文章,详细介绍了app_timer的用法和常见问题:
https://www.cnblogs.com/iini/p/9347460.html

2、NRF_LOG_INIT 和 NRF_LOG_DEFAULT_BACKENDS_INIT 的作用是Log模块初始化,调用这两个函数之后,就可以在代码中调用 NRF_LOG_INFO、NRF_LOG_ERROR、NRF_LOG_WARNING、NRF_LOG_DEBUG 这几个函数来打印LOG。Noridc在SDK的代码中通过上述的四个LOG等级写入了大量的日志,通过打印不同级别的LOG日志,可以帮助开发者调试和快速找到代码不合理的设计导致的出错。

关于如何调试Debug问题,可以参考Nordic中国区FAE的文章
https://www.cnblogs.com/iini/p/9279618.html

我们也可以在bsp的main函数中看到,LOG模块初始化完成后,调用NRF_LOG_INFO打印了“BSP example started.”

3、接下来就是bsp的初始化,这里也是调用了SDK中的库函数bsp_init,在里面写入了两个参数。

/**@brief Function for initializing bsp module.*/
voidbsp_configuration()
{
uint32_t err_code;

err_code
= bsp_init(BSP_INIT_LEDS |BSP_INIT_BUTTONS, bsp_evt_handler);
APP_ERROR_CHECK(err_code);
}

bsp_init的函数定义在bsp.h中,查看其注释可以知道两个入参分别是使用外设的类型和回调函数。在main 中 BSP_INIT_LEDS 和 BSP_INIT_BUTTONS 分别是对LED和BUTTON进行了初始化。

/**@brief       Function for initializing BSP.
*
* @details The function initializes the board support package to allow state indication and
* button reaction. Default events are assigned to buttons.
* @note Before calling this function, you must initiate the following required modules:
* - @ref app_timer for LED support
* - @ref app_gpiote for button support
*
* @param[in] type Type of peripherals used.
* @param[in] callback Function to be called when button press/event is detected.
*
* @retval NRF_SUCCESS If the BSP module was successfully initialized.
* @retval NRF_ERROR_INVALID_STATE If the application timer module has not been initialized.
* @retval NRF_ERROR_NO_MEM If the maximum number of timers has already been reached.
* @retval NRF_ERROR_INVALID_PARAM If GPIOTE has too many users.
* @retval NRF_ERROR_INVALID_STATE If button or GPIOTE has not been initialized.
*/uint32_t bsp_init(uint32_t type, bsp_event_callback_t callback);

下面我们来看看bsp的回调函数bsp_evt_handler,在回调中使用了按键中断,DK板上的Button1和Button2按下的时候,会产生相对应的事件。

/**@brief Function for handling bsp events.*/
voidbsp_evt_handler(bsp_event_t evt)
{
uint32_t err_code;
switch(evt)
{
caseBSP_EVENT_KEY_0:if (actual_state !=BSP_INDICATE_FIRST)
actual_state
--;elseactual_state=BSP_INDICATE_LAST;break;caseBSP_EVENT_KEY_1:if (actual_state !=BSP_INDICATE_LAST)
actual_state
++;elseactual_state=BSP_INDICATE_FIRST;break;default:return; //no implementation needed }
err_code
=bsp_indication_set(actual_state);
NRF_LOG_INFO(
"%s", (uint32_t)indications_list[actual_state]);
APP_ERROR_CHECK(err_code);
}

我们可以在bsp_event_t中找到全部bsp事件。

/**@brief BSP events.
*
* @note Events from BSP_EVENT_KEY_0 to BSP_EVENT_KEY_LAST are by default assigned to buttons.
*/

typedef enum{
BSP_EVENT_NOTHING
= 0, /**< Assign this event to an action to prevent the action from generating an event (disable the action).*/BSP_EVENT_DEFAULT,/**< Assign this event to an action to assign the default event to the action.*/BSP_EVENT_CLEAR_BONDING_DATA,/**< Persistent bonding data should be erased.*/BSP_EVENT_CLEAR_ALERT,/**< An alert should be cleared.*/BSP_EVENT_DISCONNECT,/**< A link should be disconnected.*/BSP_EVENT_ADVERTISING_START,/**< The device should start advertising.*/BSP_EVENT_ADVERTISING_STOP,/**< The device should stop advertising.*/BSP_EVENT_WHITELIST_OFF,/**< The device should remove its advertising whitelist.*/BSP_EVENT_BOND,/**< The device should bond to the currently connected peer.*/BSP_EVENT_RESET,/**< The device should reset.*/BSP_EVENT_SLEEP,/**< The device should enter sleep mode.*/BSP_EVENT_WAKEUP,/**< The device should wake up from sleep mode.*/BSP_EVENT_SYSOFF,/**< The device should enter system off mode (without wakeup).*/BSP_EVENT_DFU,/**< The device should enter DFU mode.*/BSP_EVENT_KEY_0,/**< Default event of the push action of BSP_BUTTON_0 (only if this button is present).*/BSP_EVENT_KEY_1,/**< Default event of the push action of BSP_BUTTON_1 (only if this button is present).*/BSP_EVENT_KEY_2,/**< Default event of the push action of BSP_BUTTON_2 (only if this button is present).*/BSP_EVENT_KEY_3,/**< Default event of the push action of BSP_BUTTON_3 (only if this button is present).*/BSP_EVENT_KEY_4,/**< Default event of the push action of BSP_BUTTON_4 (only if this button is present).*/BSP_EVENT_KEY_5,/**< Default event of the push action of BSP_BUTTON_5 (only if this button is present).*/BSP_EVENT_KEY_6,/**< Default event of the push action of BSP_BUTTON_6 (only if this button is present).*/BSP_EVENT_KEY_7,/**< Default event of the push action of BSP_BUTTON_7 (only if this button is present).*/BSP_EVENT_KEY_LAST=BSP_EVENT_KEY_7,
} bsp_event_t;

在 bsp 原始例程中的bsp回调函数中,按下Button1是倒序显示bsp_indication_t这个结构体中定义好的LED状态并在串口打印bsp_indication_t中的事件。

/**@brief BSP indication states.
*
* @details See @ref examples_bsp_states for a list of how these states are indicated for the PCA10028/PCA10040 board and the PCA10031 dongle.
*/typedefenum{
BSP_INDICATE_FIRST
= 0,
BSP_INDICATE_IDLE
= BSP_INDICATE_FIRST, /**< See \ref BSP_INDICATE_IDLE.*/BSP_INDICATE_SCANNING,/**< See \ref BSP_INDICATE_SCANNING.*/BSP_INDICATE_ADVERTISING,/**< See \ref BSP_INDICATE_ADVERTISING.*/BSP_INDICATE_ADVERTISING_WHITELIST,/**< See \ref BSP_INDICATE_ADVERTISING_WHITELIST.*/BSP_INDICATE_ADVERTISING_SLOW,/**< See \ref BSP_INDICATE_ADVERTISING_SLOW.*/BSP_INDICATE_ADVERTISING_DIRECTED,/**< See \ref BSP_INDICATE_ADVERTISING_DIRECTED.*/BSP_INDICATE_BONDING,/**< See \ref BSP_INDICATE_BONDING.*/BSP_INDICATE_CONNECTED,/**< See \ref BSP_INDICATE_CONNECTED.*/BSP_INDICATE_SENT_OK,/**< See \ref BSP_INDICATE_SENT_OK.*/BSP_INDICATE_SEND_ERROR,/**< See \ref BSP_INDICATE_SEND_ERROR.*/BSP_INDICATE_RCV_OK,/**< See \ref BSP_INDICATE_RCV_OK.*/BSP_INDICATE_RCV_ERROR,/**< See \ref BSP_INDICATE_RCV_ERROR.*/BSP_INDICATE_FATAL_ERROR,/**< See \ref BSP_INDICATE_FATAL_ERROR.*/BSP_INDICATE_ALERT_0,/**< See \ref BSP_INDICATE_ALERT_0.*/BSP_INDICATE_ALERT_1,/**< See \ref BSP_INDICATE_ALERT_1.*/BSP_INDICATE_ALERT_2,/**< See \ref BSP_INDICATE_ALERT_2.*/BSP_INDICATE_ALERT_3,/**< See \ref BSP_INDICATE_ALERT_3.*/BSP_INDICATE_ALERT_OFF,/**< See \ref BSP_INDICATE_ALERT_OFF.*/BSP_INDICATE_USER_STATE_OFF,/**< See \ref BSP_INDICATE_USER_STATE_OFF.*/BSP_INDICATE_USER_STATE_0,/**< See \ref BSP_INDICATE_USER_STATE_0.*/BSP_INDICATE_USER_STATE_1,/**< See \ref BSP_INDICATE_USER_STATE_1.*/BSP_INDICATE_USER_STATE_2,/**< See \ref BSP_INDICATE_USER_STATE_2.*/BSP_INDICATE_USER_STATE_3,/**< See \ref BSP_INDICATE_USER_STATE_3.*/BSP_INDICATE_USER_STATE_ON,/**< See \ref BSP_INDICATE_USER_STATE_ON.*/BSP_INDICATE_LAST=BSP_INDICATE_USER_STATE_ON
} bsp_indication_t;

4、最后就是while死循环中的函数,NRF_LOG_FLUSH是用来处理缓存中的LOG,__SEV() 和 __WFE() 是ARM的指令,用来上报事件和在低功耗下等待事件发生。

    while (true)
{
NRF_LOG_FLUSH();
__SEV();
__WFE();
__WFE();
//no implementation needed }

5、练习:

原始 bsp 例程中的bsp回调函数对于初学者而言不太友好,我们可以写一个简单的 bsp 回调函数,在按下DK板上对应的按键1-4的时候,在RTT打印相应的Log。

voidbsp_evt_handler(bsp_event_t evt)
{
switch(evt)
{
caseBSP_EVENT_KEY_0:
NRF_LOG_INFO(
"Button 1 is pressed");break;caseBSP_EVENT_KEY_1:
NRF_LOG_INFO(
"Button 2 is pressed");break;caseBSP_EVENT_KEY_2:
NRF_LOG_INFO(
"Button 3 is pressed");break;caseBSP_EVENT_KEY_3:
NRF_LOG_INFO(
"Button 4 is pressed");break;default:return;
}
}

将 bsp 例程中的 bsp_evt_handler 替换为上面的代码,编译并下载到DK中,打开串口工具,按下对应的按键就可以看到串口打印相应的LOG。

小结:熟悉了 bsp 这个例程,我们再去看examples\ble_peripheral下其他的外设例程,就会发现架构是相同的,都是先初始化外设,写入外设的配置参数,然后再根据相应事件结构体中给出的事件编写回调函数,在回调中处理中断发生时要做的事即可。

二、ble_app_uart 例程浅析

ble_app_uart 是带协议栈的一个例程,此例程是一个较为实用的例程,它使用了NUS服务(Nordic UATR Service)可以通过串口实现蓝牙的上下行收发数据,因此只要改变发送的外设,就可以改为其他外设,如SPI或者I2C来收发数据。熟悉了 bsp 例程,你就会发现 ble_app_uart 例程是一个放大版的 bsp 例程,下面我们将分析这个例程。

1、首先来看main函数,从这个例程中就更可以看出NRF5 SDK的例程软件架构,在main中初始化了所有的功能模块,然后在初始化中配置相应模块的回调,当回调中的事件触发时来做中断处理。

int main(void)
{
boolerase_bonds;//Initialize. uart_init();
log_init();
timers_init();
buttons_leds_init(
&erase_bonds);
power_management_init();
ble_stack_init();
gap_params_init();
gatt_init();
services_init();
advertising_init();
conn_params_init();
//Start execution. printf("\r\nUART started.\r\n");
NRF_LOG_INFO(
"Debug logging for UART over RTT started.");
advertising_start();
//Enter main loop. for(;;)
{
idle_state_handle();
}
}

2、第一个函数 uart_init 中是定义了串口相关的一些参数,如串口引脚、流控、波特率等参数,然后把这些参数写入APP_UART_FIFO_INIT来初始化串口。

/**@brief  Function for initializing the UART module.*/
/**@snippet [UART Initialization]*/
static void uart_init(void)
{
uint32_t err_code;
app_uart_comm_params_t
const comm_params ={
.rx_pin_no
=RX_PIN_NUMBER,
.tx_pin_no
=TX_PIN_NUMBER,
.rts_pin_no
=RTS_PIN_NUMBER,
.cts_pin_no
=CTS_PIN_NUMBER,
.flow_control
=APP_UART_FLOW_CONTROL_DISABLED,
.use_parity
= false,#if defined (UART_PRESENT).baud_rate=NRF_UART_BAUDRATE_115200#else.baud_rate=NRF_UARTE_BAUDRATE_115200#endif};

APP_UART_FIFO_INIT(
&comm_params,
UART_RX_BUF_SIZE,
UART_TX_BUF_SIZE,
uart_event_handle,
APP_IRQ_PRIORITY_LOWEST,
err_code);
APP_ERROR_CHECK(err_code);
}

APP_UART_FIFO_INIT的定义如下,要初始化串口需要写入五个参数,串口相关参数的结构体、RX和TX的buffer大小、回调函数、IRQ优先级。

/**@brief Macro for safe initialization of the UART module in a single user instance when using
* a FIFO together with UART.
*
* @param[in] P_COMM_PARAMS Pointer to a UART communication structure: app_uart_comm_params_t
* @param[in] RX_BUF_SIZE Size of desired RX buffer, must be a power of 2 or ZERO (No FIFO).
* @param[in] TX_BUF_SIZE Size of desired TX buffer, must be a power of 2 or ZERO (No FIFO).
* @param[in] EVT_HANDLER Event handler function to be called when an event occurs in the
* UART module.
* @param[in] IRQ_PRIO IRQ priority, app_irq_priority_t, for the UART module irq handler.
* @param[out] ERR_CODE The return value of the UART initialization function will be
* written to this parameter.
*
* @note Since this macro allocates a buffer and registers the module as a GPIOTE user when flow
* control is enabled, it must only be called once.
*/ #define APP_UART_FIFO_INIT(P_COMM_PARAMS, RX_BUF_SIZE, TX_BUF_SIZE, EVT_HANDLER, IRQ_PRIO, ERR_CODE) \ do\
{ \
app_uart_buffers_t buffers; \
staticuint8_t rx_buf[RX_BUF_SIZE]; \staticuint8_t tx_buf[TX_BUF_SIZE]; \
\
buffers.rx_buf
=rx_buf; \
buffers.rx_buf_size
= sizeof(rx_buf); \
buffers.tx_buf
=tx_buf; \
buffers.tx_buf_size
= sizeof(tx_buf); \
ERR_CODE
= app_uart_init(P_COMM_PARAMS, &buffers, EVT_HANDLER, IRQ_PRIO); \
}
while (0)

接下来我们来看uart的事件,从这里我们可以看出,串口收到数据后会产生一个事件APP_UART_DATA_READY

typedef enum{
APP_UART_DATA_READY,
/**< An event indicating that UART data has been received. The data is available in the FIFO and can be fetched using @ref app_uart_get.*/APP_UART_FIFO_ERROR,/**< An error in the FIFO module used by the app_uart module has occured. The FIFO error code is stored in app_uart_evt_t.data.error_code field.*/APP_UART_COMMUNICATION_ERROR,/**< An communication error has occured during reception. The error is stored in app_uart_evt_t.data.error_communication field.*/APP_UART_TX_EMPTY,/**< An event indicating that UART has completed transmission of all available data in the TX FIFO.*/APP_UART_DATA,/**< An event indicating that UART data has been received, and data is present in data field. This event is only used when no FIFO is configured.*/} app_uart_evt_type_t;

所以我们可以在回调函数中使用这个事件,当产生这个事件的时候说明串口有数据发过来,可以调用app_uart_get来接收串口数据,收到数据后再调用ble_nus_data_send向蓝牙发送串口收到的数据。

/**@brief   Function for handling app_uart events.
*
* @details This function will receive a single character from the app_uart module and append it to
* a string. The string will be be sent over BLE when the last character received was a
* 'new line' '\n' (hex 0x0A) or if the string has reached the maximum data length.
*/ /**@snippet [Handling the data received over UART]*/ void uart_event_handle(app_uart_evt_t *p_event)
{
staticuint8_t data_array[BLE_NUS_MAX_DATA_LEN];static uint8_t index = 0;
uint32_t err_code;
switch (p_event->evt_type)
{
caseAPP_UART_DATA_READY:
UNUSED_VARIABLE(app_uart_get(
&data_array[index]));
index
++;if ((data_array[index - 1] == '\n') ||(data_array[index- 1] == '\r') ||(index>=m_ble_nus_max_data_len))
{
if (index > 1)
{
NRF_LOG_DEBUG(
"Ready to send data over BLE NUS");
NRF_LOG_HEXDUMP_DEBUG(data_array, index);
do{
uint16_t length
=(uint16_t)index;
err_code
= ble_nus_data_send(&m_nus, data_array, &length, m_conn_handle);if ((err_code != NRF_ERROR_INVALID_STATE) &&(err_code!= NRF_ERROR_RESOURCES) &&(err_code!=NRF_ERROR_NOT_FOUND))
{
APP_ERROR_CHECK(err_code);
}
}
while (err_code ==NRF_ERROR_RESOURCES);
}

index
= 0;
}
break;caseAPP_UART_COMMUNICATION_ERROR:
APP_ERROR_HANDLER(p_event
->data.error_communication);break;caseAPP_UART_FIFO_ERROR:
APP_ERROR_HANDLER(p_event
->data.error_code);break;default:break;
}
}

3、log_init、timers_init、buttons_leds_init这三个函数的作用分别在 bsp 例程中已经有过详细介绍,在此不多做赘述。

4、power_management_init中调用了nrf_pwr_mgmt_init(),其函数定义如下,主要是用于初始化低功耗管理模块,在实际的应用中可以直接使用。

/**@brief   Function for initializing power management.
*
* @warning Depending on configuration, this function sets SEVONPEND in System Control Block (SCB).
* This operation is unsafe with the SoftDevice from interrupt priority higher than SVC.
*
* @retval NRF_SUCCESS
*/ret_code_t nrf_pwr_mgmt_init(void);

5、ble_stack_init 是一个非常重要的函数,其作用是初始化BLE协议栈,对于初学者而言不需要过多去查看协议栈初始化的具体细节,只要照搬例程中的初始化函数即可,这些需要关注的是回调函数ble_evt_handler

/**@brief Function for the SoftDevice initialization.
*
* @details This function initializes the SoftDevice and the BLE event interrupt.
*/ static void ble_stack_init(void)
{
ret_code_t err_code;

err_code
=nrf_sdh_enable_request();
APP_ERROR_CHECK(err_code);
//Configure the BLE stack using the default settings.//Fetch the start address of the application RAM. uint32_t ram_start = 0;
err_code
= nrf_sdh_ble_default_cfg_set(APP_BLE_CONN_CFG_TAG, &ram_start);
APP_ERROR_CHECK(err_code);
//Enable BLE stack. err_code = nrf_sdh_ble_enable(&ram_start);
APP_ERROR_CHECK(err_code);
//Register a handler for BLE events. NRF_SDH_BLE_OBSERVER(m_ble_observer, APP_BLE_OBSERVER_PRIO, ble_evt_handler, NULL);
}

这里我们分析一下例程中的ble_evt_handler,首先来看ble_evt_t这个结构体,这是一个非常复杂的结构体,给出了Gap、Gattc、Gatts、L2CAP等会产生的所有事件对应的结构体。

/**@brief Common BLE Event type, wrapping the module specific event reports.*/typedefstruct{
ble_evt_hdr_t header;
/**< Event header.*/union
{
ble_common_evt_t common_evt;
/**< Common Event, evt_id in BLE_EVT_* series.*/ble_gap_evt_t gap_evt;/**< GAP originated event, evt_id in BLE_GAP_EVT_* series.*/ble_gattc_evt_t gattc_evt;/**< GATT client originated event, evt_id in BLE_GATTC_EVT* series.*/ble_gatts_evt_t gatts_evt;/**< GATT server originated event, evt_id in BLE_GATTS_EVT* series.*/ble_l2cap_evt_t l2cap_evt;/**< L2CAP originated event, evt_id in BLE_L2CAP_EVT* series.*/} evt;/**< Event union.*/} ble_evt_t;

这里涉及到BLE协议相关的知识,简单来说BLE协议栈的host层可以从下到上分为L2CAP、ATT、GAP、GATT四层,一般而言只需要应用程序中只需要处理GAP和Gatt层的事件即可。

对于初学者而言,在没有详细了解蓝牙协议的情况下,建议照搬例程中已经写好的ble_evt_handler函数,只在应用层在ble_evt_handler的事件中根据自己的需求加入一些逻辑处理,例如BLE连接、断开后串口打印数据或是改变某个GPIO的状态。协议栈产生的GAP和GATT事件在ble_gap.h、ble_gattc.h、ble_gatts.h这三个文件中,感兴趣的朋友可以自己去这两个文件中查看所有事件和其注释,这里我们只分析例程中的回调给出的这些事件的作用。

/**@brief Function for handling BLE events.
*
* @param[in] p_ble_evt Bluetooth stack event.
* @param[in] p_context Unused.
*/ static void ble_evt_handler(ble_evt_t const * p_ble_evt, void *p_context)
{
uint32_t err_code;
switch (p_ble_evt->header.evt_id)
{
caseBLE_GAP_EVT_CONNECTED:
NRF_LOG_INFO(
"Connected");
err_code
=bsp_indication_set(BSP_INDICATE_CONNECTED);
APP_ERROR_CHECK(err_code);
m_conn_handle
= p_ble_evt->evt.gap_evt.conn_handle;
err_code
= nrf_ble_qwr_conn_handle_assign(&m_qwr, m_conn_handle);
APP_ERROR_CHECK(err_code);
break;caseBLE_GAP_EVT_DISCONNECTED:
NRF_LOG_INFO(
"Disconnected");//LED indication will be changed when advertising starts. m_conn_handle =BLE_CONN_HANDLE_INVALID;break;caseBLE_GAP_EVT_PHY_UPDATE_REQUEST:
{
NRF_LOG_DEBUG(
"PHY update request.");
ble_gap_phys_t
const phys ={
.rx_phys
=BLE_GAP_PHY_AUTO,
.tx_phys
=BLE_GAP_PHY_AUTO,
};
err_code
= sd_ble_gap_phy_update(p_ble_evt->evt.gap_evt.conn_handle, &phys);
APP_ERROR_CHECK(err_code);
}
break;caseBLE_GAP_EVT_SEC_PARAMS_REQUEST://Pairing not supported err_code =sd_ble_gap_sec_params_reply(m_conn_handle, BLE_GAP_SEC_STATUS_PAIRING_NOT_SUPP, NULL, NULL);
APP_ERROR_CHECK(err_code);
break;caseBLE_GATTS_EVT_SYS_ATTR_MISSING://No system attributes have been stored. err_code = sd_ble_gatts_sys_attr_set(m_conn_handle, NULL, 0, 0);
APP_ERROR_CHECK(err_code);
break;caseBLE_GATTC_EVT_TIMEOUT://Disconnect on GATT Client timeout event. err_code = sd_ble_gap_disconnect(p_ble_evt->evt.gattc_evt.conn_handle,
BLE_HCI_REMOTE_USER_TERMINATED_CONNECTION);
APP_ERROR_CHECK(err_code);
break;caseBLE_GATTS_EVT_TIMEOUT://Disconnect on GATT Server timeout event. err_code = sd_ble_gap_disconnect(p_ble_evt->evt.gatts_evt.conn_handle,
BLE_HCI_REMOTE_USER_TERMINATED_CONNECTION);
APP_ERROR_CHECK(err_code);
break;default://No implementation needed. break;
}
}

下面是我直接从SDK中拷贝了这些事件的注释,从注释中可以看出这些事件基本是把BLE连接过程中会发生的事件列了出来。

BLE_GAP_EVT_CONNECTED                      /**< Connected to peer
BLE_GAP_EVT_DISCONNECTED   /**< Disconnected from peer.
BLE_GAP_EVT_PHY_UPDATE_REQUEST        /**< PHY Update Request.
BLE_GAP_EVT_SEC_PARAMS_REQUEST      /**< Request to provide security parameters.
BLE_GATTS_EVT_SYS_ATTR_MISSING     /**< A persistent system attribute access is pending.
BLE_GATTC_EVT_TIMEOUT /**< Timeout event.
BLE_GATTS_EVT_TIMEOUT /**< Peer failed to respond to an ATT request in time.

(1)、手机或者其他主机设备后,BLE协议栈会产生BLE_GAP_EVT_CONNECTED这个事件,在这个事件产生后,回调函数在应用层对其的处理是首先使用LOG函数打印连接信息。然后调用bsp_indication_set改变DK板上LED的状态,让LED状态进入BSP_INDICATE_CONNECTED这个连接状态,通过查看 bsp.c 中的相关函数,我们可以知道这个连接状态是让DK板上的LED1进入常亮状态。最后是调用nrf_ble_qwr_conn_handle_assign,这个函数的功能是用于将连接句柄m_conn_handle分配给 Queued Writes 模块。 简单来说,这个m_conn_handle相当于协议栈给连接的对端设备分配的号码,nrf_ble_qwr_conn_handle_assign的作用就是把这个号码与 Queued Writes 模块关联起来,Queued Writes 模块用于处理对端BLE设备在GATT上的操作。

        caseBLE_GAP_EVT_CONNECTED:
NRF_LOG_INFO(
"Connected");
err_code
=bsp_indication_set(BSP_INDICATE_CONNECTED);
APP_ERROR_CHECK(err_code);
m_conn_handle
= p_ble_evt->evt.gap_evt.conn_handle;
err_code
= nrf_ble_qwr_conn_handle_assign(&m_qwr, m_conn_handle);
APP_ERROR_CHECK(err_code);
break;
NRF_BLE_QWR_DEF(m_qwr);                        /**< Context for the Queued Write module.*/

(2)、手机或者其他主机设备后,BLE协议栈会产生BLE_GAP_EVT_DISCONNECTED这个事件,从例程中可以看到,在这个事件产生后,回调函数对其的处理是使用LOG函数打印断开连接的信息,然后重置m_conn_handle的值。

caseBLE_GAP_EVT_DISCONNECTED:
NRF_LOG_INFO(
"Disconnected");//LED indication will be changed when advertising starts. m_conn_handle =BLE_CONN_HANDLE_INVALID;break;

(3)、BLE_GAP_EVT_PHY_UPDATE_REQUEST这个事件主要是针对对端设备请求更新PHY的速率,当对端设备请求更新PHY速率后,协议栈会产生此事件,从例程中可以看到,在这个事件产生后对其的处理是配置PHY参数,并调用sd_ble_gap_phy_update更新PHY速率。

        caseBLE_GAP_EVT_PHY_UPDATE_REQUEST:
{
NRF_LOG_DEBUG(
"PHY update request.");
ble_gap_phys_t
const phys ={
.rx_phys
=BLE_GAP_PHY_AUTO,
.tx_phys
=BLE_GAP_PHY_AUTO,
};
err_code
= sd_ble_gap_phy_update(p_ble_evt->evt.gap_evt.conn_handle, &phys);
APP_ERROR_CHECK(err_code);
}
break;

(4)、BLE_GAP_EVT_SEC_PARAMS_REQUEST是安全相关参数请求事件,在配对信息交换阶段,BLE_GAP_EVT_SEC_PARAMS_REQUEST事件会由协议栈上报给应用层。在这个事件中,从机会把自己的信息与主机进行交换,其中就包含了从机的IO能力、配对完成是否绑定等信息。

        caseBLE_GAP_EVT_SEC_PARAMS_REQUEST://Pairing not supported
            err_code =sd_ble_gap_sec_params_reply(m_conn_handle, BLE_GAP_SEC_STATUS_PAIRING_NOT_SUPP, NULL, NULL);
APP_ERROR_CHECK(err_code);
break;

(5)、BLE_GATTS_EVT_SYS_ATTR_MISSING这是事件是当没有存储系统属性时,协议栈上报此事件,回调函数中的处理是调用sd_ble_gatts_sys_attr_set函数来设置系统属性。

        caseBLE_GATTS_EVT_SYS_ATTR_MISSING://No system attributes have been stored.
            err_code = sd_ble_gatts_sys_attr_set(m_conn_handle, NULL, 0, 0);
APP_ERROR_CHECK(err_code);
break;

(6)、BLE_GATTC_EVT_TIMEOUT和BLE_GATTS_EVT_TIMEOUT是当主机和从机连接超时协议栈产生的事件,回调函数中的处理是调用sd_ble_gap_disconnect函数来断开连接。

        caseBLE_GATTC_EVT_TIMEOUT://Disconnect on GATT Client timeout event.
            err_code = sd_ble_gap_disconnect(p_ble_evt->evt.gattc_evt.conn_handle,
BLE_HCI_REMOTE_USER_TERMINATED_CONNECTION);
APP_ERROR_CHECK(err_code);
break;caseBLE_GATTS_EVT_TIMEOUT://Disconnect on GATT Server timeout event. err_code = sd_ble_gap_disconnect(p_ble_evt->evt.gatts_evt.conn_handle,
BLE_HCI_REMOTE_USER_TERMINATED_CONNECTION);
APP_ERROR_CHECK(err_code);
break;

6、gap_params_init函数是用来配置gap的相关参数,包括设备名称、连接间隔、Slave latency、监督超时时间、GAP连接的安全模式等。一般来说,配置比较多的是设备名称、连接间隔、Slave latency这三个参数,其他沿用例程中的配置即可。

/**@brief Function for the GAP initialization.
*
* @details This function will set up all the necessary GAP (Generic Access Profile) parameters of
* the device. It also sets the permissions and appearance.
*/ static void gap_params_init(void)
{
uint32_t err_code;
ble_gap_conn_params_t gap_conn_params;
ble_gap_conn_sec_mode_t sec_mode;

BLE_GAP_CONN_SEC_MODE_SET_OPEN(
&sec_mode);

err_code
= sd_ble_gap_device_name_set(&sec_mode,
(
const uint8_t *) DEVICE_NAME,
strlen(DEVICE_NAME));
APP_ERROR_CHECK(err_code);

memset(
&gap_conn_params, 0, sizeof(gap_conn_params));

gap_conn_params.min_conn_interval
=MIN_CONN_INTERVAL;
gap_conn_params.max_conn_interval
=MAX_CONN_INTERVAL;
gap_conn_params.slave_latency
=SLAVE_LATENCY;
gap_conn_params.conn_sup_timeout
=CONN_SUP_TIMEOUT;

err_code
= sd_ble_gap_ppcp_set(&gap_conn_params);
APP_ERROR_CHECK(err_code);
}

7、gatt_init用于初始化GATT库。其中,nrf_ble_gatt_init 函数用于初始化GATT协议栈,gatt_evt_handler 函数用于处理GATT事件。nrf_ble_gatt_att_mtu_periph_set函数用于设置GATT服务器的MTU大小。

/**@brief Function for initializing the GATT library.*/
void gatt_init(void)
{
ret_code_t err_code;

err_code
= nrf_ble_gatt_init(&m_gatt, gatt_evt_handler);
APP_ERROR_CHECK(err_code);

err_code
= nrf_ble_gatt_att_mtu_periph_set(&m_gatt, NRF_SDH_BLE_GATT_MAX_MTU_SIZE);
APP_ERROR_CHECK(err_code);
}

在gatt_init的回调函数gatt_evt_handler中主要处理当ATT MTU更新之后的事,这里ATT MTU是L2CAP层的内容,对于初学者而言不需要去深入了解,在自己的项目中只需要照搬例程中的代码即可。

/**@brief Function for handling events from the GATT library.*/
void gatt_evt_handler(nrf_ble_gatt_t * p_gatt, nrf_ble_gatt_evt_t const *p_evt)
{
if ((m_conn_handle == p_evt->conn_handle) && (p_evt->evt_id ==NRF_BLE_GATT_EVT_ATT_MTU_UPDATED))
{
m_ble_nus_max_data_len
= p_evt->params.att_mtu_effective - OPCODE_LENGTH -HANDLE_LENGTH;
NRF_LOG_INFO(
"Data len is set to 0x%X(%d)", m_ble_nus_max_data_len, m_ble_nus_max_data_len);
}
NRF_LOG_DEBUG(
"ATT MTU exchange completed. central 0x%x peripheral 0x%x",
p_gatt
->att_mtu_desired_central,
p_gatt
->att_mtu_desired_periph);
}

7、services_init也是一个非常重要的函数,从机端所有的服务都是在这里添加并初始化,Nordic为我们提供了一个库在SDK根目录下的components\ble\ble_services这个文件夹中,这里有常用的一些服务的库,例如DIS(Devies Information、Service)、DFU、HIDS等,所以想要在从机添加服务只需要在services_init函数中调用对应的服务初始化接口,然后编写对应服务的回调函数即可,具体可以参考NUS服务的初始化。

需要注意的是在这里的nrf_ble_qwr_init是用来初始化之前提到的Queued Writes 模块的函数,在初始化服务的时候首先要初始化Queued Writes模块。

/**@snippet [Handling the data received over BLE]*/


/**@brief Function for initializing services that will be used by the application.*/
static void services_init(void)
{
uint32_t err_code;
ble_nus_init_t nus_init;
nrf_ble_qwr_init_t qwr_init
= {0};//Initialize Queued Write Module. qwr_init.error_handler =nrf_qwr_error_handler;

err_code
= nrf_ble_qwr_init(&m_qwr, &qwr_init);
APP_ERROR_CHECK(err_code);
//Initialize NUS. memset(&nus_init, 0, sizeof(nus_init));

nus_init.data_handler
=nus_data_handler;

err_code
= ble_nus_init(&m_nus, &nus_init);
APP_ERROR_CHECK(err_code);
}

这里ble_app_uart例程只使用了NUS服务,所以我们来看看NUS服务的回调函数nus_data_handler做了什么,当产生BLE_NUS_EVT_RX_DATA这个事件时,p_evt->params.rx_data.p_data这个指针收到BLE的数据,回调函数中调用app_uart_put这个函数将收到的数据打印到串口。

/**@brief Function for handling the data from the Nordic UART Service.
*
* @details This function will process the data received from the Nordic UART BLE Service and send
* it to the UART module.
*
* @param[in] p_evt Nordic UART Service event.
*/ /**@snippet [Handling the data received over BLE]*/ static void nus_data_handler(ble_nus_evt_t *p_evt)
{
if (p_evt->type ==BLE_NUS_EVT_RX_DATA)
{
uint32_t err_code;

NRF_LOG_DEBUG(
"Received data from BLE NUS. Writing data on UART.");
NRF_LOG_HEXDUMP_DEBUG(p_evt
->params.rx_data.p_data, p_evt->params.rx_data.length);for (uint32_t i = 0; i < p_evt->params.rx_data.length; i++)
{
do{
err_code
= app_uart_put(p_evt->params.rx_data.p_data[i]);if ((err_code != NRF_SUCCESS) && (err_code !=NRF_ERROR_BUSY))
{
NRF_LOG_ERROR(
"Failed receiving NUS message. Error 0x%x.", err_code);
APP_ERROR_CHECK(err_code);
}
}
while (err_code ==NRF_ERROR_BUSY);
}
if (p_evt->params.rx_data.p_data[p_evt->params.rx_data.length - 1] == '\r')
{
while (app_uart_put('\n') ==NRF_ERROR_BUSY);
}
}

}

8、advertising_init也是一个非常重要的函数,所有的广播参数都在这里设置,包括广播周期、广播包中的相关参数等,配置的方式通过配置相对应的参数,然后调用ble_advertising_init去设置。这些需要注意的是,配置完广播参数之后,还要调用ble_advertising_conn_cfg_tag_set来更改即将用于连接的连接设置标记。

/**@brief Function for initializing the Advertising functionality.*/
static void advertising_init(void)
{
uint32_t err_code;
ble_advertising_init_t init;

memset(
&init, 0, sizeof(init));

init.advdata.name_type
=BLE_ADVDATA_FULL_NAME;
init.advdata.include_appearance
= false;
init.advdata.flags
=BLE_GAP_ADV_FLAGS_LE_ONLY_LIMITED_DISC_MODE;

init.srdata.uuids_complete.uuid_cnt
= sizeof(m_adv_uuids) / sizeof(m_adv_uuids[0]);
init.srdata.uuids_complete.p_uuids
=m_adv_uuids;

init.config.ble_adv_fast_enabled
= true;
init.config.ble_adv_fast_interval
=APP_ADV_INTERVAL;
init.config.ble_adv_fast_timeout
=APP_ADV_DURATION;
init.evt_handler
=on_adv_evt;

err_code
= ble_advertising_init(&m_advertising, &init);
APP_ERROR_CHECK(err_code);

ble_advertising_conn_cfg_tag_set(
&m_advertising, APP_BLE_CONN_CFG_TAG);
}

广播相关参数的设置都在ble_advertising_init_t这个结构体中,一般用到比较多的是config、advata和srdata,config中主要是涉及配置广播周期参数,广播超时时间等,advata和srdata主要是配置广播包和扫描响应包。一般而言,建议是把数据放在广播包中,但广播包只有31个字节,如果你的广播数据比较长,例如你希望在广播包加入设备名称、UUID、地址等数据,如果在广播包中放不下全部数据,也可以把不重要的数据放在扫描响应包中,广播包和扫描响应包的长度都是31个字节。

typedef struct{
ble_advdata_t advdata;
/**< Advertising data: name, appearance, discovery flags, and more.*/ble_advdata_t srdata;/**< Scan response data: Supplement to advertising data.*/ble_adv_modes_config_t config;/**< Select which advertising modes and intervals will be utilized.*/ble_adv_evt_handler_t evt_handler;/**< Event handler that will be called upon advertising events.*/ble_adv_error_handler_t error_handler;/**< Error handler that will propogate internal errors to the main applications.*/} ble_advertising_init_t;

广播初始化中还有一个广播的回调函数on_adv_evt,这个回调主要处理在广播模式下的事件,例程中只罗列了两个简单的事件,BLE_ADV_EVT_FAST是在快速广播模式下,改变LED灯的状态,进入BSP_INDICATE_ADVERTISING模式,从bsp.c中可以得知此状态是让DK板上的LED1闪烁。BLE_ADV_EVT_IDLE是广播超时之后产生的事件,广播超时时间可以在上面结构体的config中找到相应参数去配置,例程中对此的处理是此事件产生后,进入休眠模式。

/**@brief Function for handling advertising events.
*
* @details This function will be called for advertising events which are passed to the application.
*
* @param[in] ble_adv_evt Advertising event.
*/ static voidon_adv_evt(ble_adv_evt_t ble_adv_evt)
{
uint32_t err_code;
switch(ble_adv_evt)
{
caseBLE_ADV_EVT_FAST:
err_code
=bsp_indication_set(BSP_INDICATE_ADVERTISING);
APP_ERROR_CHECK(err_code);
break;caseBLE_ADV_EVT_IDLE:
sleep_mode_enter();
break;default:break;
}
}

9、conn_params_init这个函数的作用是初始化连接参数,连接参数包括了BLE主机与从机连接过程中协商的一些参数,例如连接间隔、从启动事件(连接或通知开始)到第一次调用sd_ble_gap_conn_param_update的时间、第一次之后每次调用sd_ble_gap_conn_param_update之间的时间、在放弃协商之前尝试的次数等参数。

/**@brief Function for initializing the Connection Parameters module.*/
static void conn_params_init(void)
{
uint32_t err_code;
ble_conn_params_init_t cp_init;

memset(
&cp_init, 0, sizeof(cp_init));

cp_init.p_conn_params
=NULL;
cp_init.first_conn_params_update_delay
=FIRST_CONN_PARAMS_UPDATE_DELAY;
cp_init.next_conn_params_update_delay
=NEXT_CONN_PARAMS_UPDATE_DELAY;
cp_init.max_conn_params_update_count
=MAX_CONN_PARAMS_UPDATE_COUNT;
cp_init.start_on_notify_cccd_handle
=BLE_GATT_HANDLE_INVALID;
cp_init.disconnect_on_fail
= false;
cp_init.evt_handler
=on_conn_params_evt;
cp_init.error_handler
=conn_params_error_handler;

err_code
= ble_conn_params_init(&cp_init);
APP_ERROR_CHECK(err_code);
}

具体的连接参数可以在ble_conn_params_init_t这个结构体中找到。对于初学者而言,如果你对BLE协议不熟悉,对这些参数具体的用途不清楚,这里建议照搬例程中的代码,不去做改动,只需要知道此函数的作用即可。

/**@brief Connection Parameters Module init structure. This contains all options and data needed for
* initialization of the connection parameters negotiation module.
*/typedefstruct{
ble_gap_conn_params_t
* p_conn_params; //!< Pointer to the connection parameters desired by the application. When calling ble_conn_params_init, if this parameter is set to NULL, the connection parameters will be fetched from host. uint32_t first_conn_params_update_delay; //!< Time from initiating event (connect or start of notification) to first time sd_ble_gap_conn_param_update is called (in number of timer ticks). uint32_t next_conn_params_update_delay; //!< Time between each call to sd_ble_gap_conn_param_update after the first (in number of timer ticks). Recommended value 30 seconds as per BLUETOOTH SPECIFICATION Version 4.0. uint8_t max_conn_params_update_count; //!< Number of attempts before giving up the negotiation. uint16_t start_on_notify_cccd_handle; //!< If procedure is to be started when notification is started, set this to the handle of the corresponding CCCD. Set to BLE_GATT_HANDLE_INVALID if procedure is to be started on connect event. bool disconnect_on_fail; //!< Set to TRUE if a failed connection parameters update shall cause an automatic disconnection, set to FALSE otherwise. ble_conn_params_evt_handler_t evt_handler; //!< Event handler to be called for handling events in the Connection Parameters. ble_srv_error_handler_t error_handler; //!< Function to be called in case of an error. } ble_conn_params_init_t;

在ble_conn_params_init_t中有两个回调函数,分别是事件回调和错误回调,在例程中对应的是on_conn_params_evt和conn_params_error_handler,这两个回调函数用于处理连接过程中的事件和连接参数错误的情况。

可以看到在 on_conn_params_evt 中,只对BLE_CONN_PARAMS_EVT_FAILED这个事件做了处理,查看注释可以知道此事件是连接参数协商失败会产生的,对其的处理是调用sd_ble_gap_disconnect断开连接。

conn_params_error_handler是对连接参数错误的回调,例程中对其处理是直接调用 APP_ERROR_HANDLER 来检查错误。

/**@brief Function for handling an event from the Connection Parameters Module.
*
* @details This function will be called for all events in the Connection Parameters Module
* which are passed to the application.
*
* @note All this function does is to disconnect. This could have been done by simply setting
* the disconnect_on_fail config parameter, but instead we use the event handler
* mechanism to demonstrate its use.
*
* @param[in] p_evt Event received from the Connection Parameters Module.
*/ static void on_conn_params_evt(ble_conn_params_evt_t *p_evt)
{
uint32_t err_code;
if (p_evt->evt_type ==BLE_CONN_PARAMS_EVT_FAILED)
{
err_code
=sd_ble_gap_disconnect(m_conn_handle, BLE_HCI_CONN_INTERVAL_UNACCEPTABLE);
APP_ERROR_CHECK(err_code);
}
}
/**@brief Function for handling errors from the Connection Parameters module.
*
* @param[in] nrf_error Error code containing information about what went wrong.
*/ static voidconn_params_error_handler(uint32_t nrf_error)
{
APP_ERROR_HANDLER(nrf_error);
}

10、完成初始化所有模块的初始化之后,例程中是调用了 printf 和 NRF_LOG_INFO 打印了Log,二者的区别是printf直接在串口打印,而NRF_LOG_INFO是需要开启Log打印模块的。advertising_start的作用是开启广播,例程中的advertising_start函数是调用了ble_advertising_start这个函数来实现开启广播的。

/**@brief Function for starting advertising.*/
static void advertising_start(void)
{
uint32_t err_code
= ble_advertising_start(&m_advertising, BLE_ADV_MODE_FAST);
APP_ERROR_CHECK(err_code);
}

11、最后就是for循环中的代码,在带协议栈的例程中,是调用idle_state_handle来实现CPU在低功耗状态等待和上报事件,其效果和 __SEV() 和 __WFE() 类似。但是因为在BLE协议栈开启后,CPU要优先处理协议栈相关的任务,且协议栈开启后不能直接操作底层寄存器和指令,所以需要调用协议栈提供的接口来实现, 这就是nrf_pwr_mgmt_run的作用 ,它用于处理空闲模式并进入System ON睡眠模式,在BLE状态下,如果CPU处于空闲状态,就会进入sd_app_evt_wait()函数,来等待协议栈上报事件。这就是之前 power_management_ini t中要调用 nrf_pwr_mgmt_init 对低功耗管理模块初始化的原因,因为要在for循环中使用nrf_pwr_mgmt_run。

/**@brief Function for handling the idle state (main loop).
*
* @details If there is no pending log operation, then sleep until next the next event occurs.
*/ static void idle_state_handle(void)
{
if (NRF_LOG_PROCESS() == false)
{
nrf_pwr_mgmt_run();
}
}

12、练习

介绍完ble_app_uart这个例程,我们来做一个小练习,修改设备的广播名称、连接间隔、广播周期。

(1)广播名称在例程中是在 gap_params_init 函数中通过sd_ble_gap_device_name_set来设置的,例程中是用了DEVICE_NAME这个宏定义来控制,所以只需要更改这个宏定义即可。

#define DEVICE_NAME                     "Nordic_UART"                               
err_code
= sd_ble_gap_device_name_set(&sec_mode,
(
const uint8_t *) DEVICE_NAME,
strlen(DEVICE_NAME));

(2)、连接间隔同样是在gap_params_init 函数配置,通过配置ble_gap_conn_params_t中的min_conn_interval和max_conn_interval来实现,这是一个范围值,之所以是一个范围值是为了兼容不同的主机设备,因为不同的主机设备的连接间隔是不同的。ble_app_uart例程中默认的范围值是20-75ms,我们可以通过修改最小连接间隔和最大连接间隔来修改实际的连接间隔。连接间隔越大,功耗越低,但是相对应的,连接时间和发送数据的速率会降低。

#define MIN_CONN_INTERVAL            MSEC_TO_UNITS(20, UNIT_1_25_MS)/**< Minimum acceptable connection interval (20 ms), Connection interval uses 1.25 ms units. */
#define MAX_CONN_INTERVAL            MSEC_TO_UNITS(75, UNIT_1_25_MS)/**< Maximum acceptable connection interval (75 ms), Connection interval uses 1.25 ms units. */ 
gap_conn_params.min_conn_interval
=MIN_CONN_INTERVAL;
gap_conn_params.max_conn_interval
= MAX_CONN_INTERVAL;

(3)、广播周期则是在advertising_init中通过配置ble_advertising_init_t中的ble_adv_fast_interval或者ble_adv_slow_interval来实现,需要注意的是ble_app_uart中使用的是fast advertising,相对应的还有slow advertising,二者的区别只是广播数据包的发送频率,fast advertising的发送频率比slow advertising的发送频率更高。

#define APP_ADV_INTERVAL           64  /**< The advertising interval (in units of 0.625 ms. This value corresponds to 40 ms). */
init.config.ble_adv_fast_interval
= APP_ADV_INTERVAL;

小结:从ble_app_uart的例程就更可以看出,Nordic的例程的软件架构主要就是相应模块的初始化和回调函数,在开发自己的工程的时候,建议在SDK中找一个和你实际产品类似例程,在其基础上去做开发,而不是新建一个新的工程从零开始,因为例程中已经完成了主体软件架构的搭建,可以直接使用例程中BLE部分的相关初始化代码和回调函数,只需要根据实际情况去做微调一些参数即可,剩余的只是开发者在相应的例程中根据自己产品的实际需求去添砖加瓦即可。

作者:京东科技 孙凯

一、前言

相信很多前端开发者在做项目时同时也都做过页面性能优化,这不单是前端的必备职业技能,也是考验一个前端基础是否扎实的考点,而性能指标也通常是每一个开发者的绩效之一。尤其马上接近年关,页面白屏时间是否过长、首屏加载速度是否达标、动画是否能流畅运行,诸如此类关于性能更具体的指标和感受,很可能也是决定着年底你能拿多少年终奖回家过年的
晴雨表

关于性能优化,我们一般从以下四个方面考虑:

  1. 开发时性能优化

  2. 编译时性能优化

  3. 加载时性能优化

  4. 运行时性能优化

而本文将从第三个方面展开,讲一讲哪些因素将影响到页面加载总时长,谈到总时长,那总是避免不了要谈及
window.onload
,这不但是本文的重点,也是常见
页面性能监控工具中必要的API之一
,如果你对自己页面加载的总时长不满意,欢迎读完本文后在评论区交流。

二、关于 window.onload

这个挂载到
window
上的方法,是我刚接触前端时就掌握的技能,我记得尤为深刻,当时老师说,“对于初学者,只要在这个方法里写逻辑,一定没错儿,它是整个文档加载完毕后执行的生命周期函数”,于是从那之后,几乎所有的练习demo,我都写在这里,也确实没出过错。


MDN
上,关于
onload
的解释是这样的:load 事件在整个页面及所有依赖资源如样式表和图片都已完成加载时触发。它与
DOMContentLoaded
不同,后者只要页面 DOM 加载完成就触发,无需等待依赖资源的加载。该事件不可取消,也不会冒泡。

后来随着前端知识的不断扩充,这个方法后来因为有了“更先进”的
DOMContentLoaded
,在我的代码里而逐渐被替代了,目前除了一些极其特殊的情况,否则我几乎很难用到
window.onload
这个API,直到认识到它影响到页面加载的整体时长指标,我才又一次拾起来它。

三、哪些因素会影响 window.onload

本章节主要会通过几个常用的业务场景展开描述,但是有个前提,就是如何准确记录各种类型资源加载耗时对页面整体加载的影响,为此,有必要先介绍一下前提。

为了准确描述资源加载耗时,我在本地环境启动了一个用于资源请求的
node
服务,所有的资源都会从这个服务中获取,之所以不用远程服务器资源的有主要原因是,使用本地服务的资源可以在访问的资源链接中设置延迟时间,如访问脚本资源
http://localhost:3010/index.js?delay=300
,因链接中存在
delay=300
,即可使资源在300毫秒后返回,这样即可准确控制每个资源加载的时间。

以下是
node
资源请求服务延迟相关代码,仅仅是一个中间件:

const express = require("express")
const app = express()

app.use(function (req, res, next) {
    Number(req.query.delay) > 0
        ? setTimeout(next, req.query.delay)
        : next()
})

  • 场景一
    : 使用 async 异步加载脚本场景对 onload 的影响
    示例代码:

      <!DOCTYPE html>
      <html lang="en">
      <head>
          <meta charset="UTF-8">
          <meta http-equiv="X-UA-Compatible" content="IE=edge">
          <meta name="viewport" content="width=device-width, initial-scale=1.0">
          <title>test</title>
    
          <!-- 请求时长为1秒的js资源 -->
          <script src="http://localhost:3010/index.js?delay=1000" async></script>
      </head>
      <body>
      </body>
      </html>
    
    

    浏览器表现如下:
    alt
    通过上图可以看到,瀑布图中深蓝色竖线表示触发了
    DOMContentLoaded
    事件,而红色竖线表示触发了
    window.onload
    事件(下文中无特殊情况,不会再进行特殊标识),由图可以得知使用了 async 属性进行脚本的异步加载,仍会影响页面加载总体时长。

  • 场景二
    :使用 defer 异步加载脚本场景对 onload 的影响
    示例代码:

      <!DOCTYPE html>
      <html lang="en">
      <head>
          <meta charset="UTF-8">
          <meta http-equiv="X-UA-Compatible" content="IE=edge">
          <meta name="viewport" content="width=device-width, initial-scale=1.0">
          <title>test</title>
    
          <!-- 请求时长为1秒的js资源 -->
          <script src="http://localhost:3010/index.js?delay=1000" defer></script>
      </head>
      <body>
      </body>
      </html>
    
    

    浏览器表现如下:
    alt
    由图可以得知使用了 defer 属性进行脚本的异步加载,除了正常的在
    DOMContentLoaded
    之后触发脚本执行,也影响页面加载总体时长。

  • 场景三
    :异步脚本中再次加载脚本,也就是常见的动态加载脚本、样式资源的情况
    html 代码保持不变,
    index.js
    内示例代码:

    const script = document.createElement('script')
    
    // 请求时长为0.6秒的js资源
    script.src = 'http://localhost:3010/index2.js?delay=600'
    script.onload = () => {
        console.log('js 2 异步加载完毕')
    }
    document.body.appendChild(script)
    
    

    结果如下:
    alt
    从瀑布图可以看出,资源的连续加载,导致了onload事件整体延后了,这也是我们再页面中非常常见的一种操作,通常懒加载一些不重要或者首屏外的资源,其实这样也会导致页面整体指标的下降。

    不过值得强调的一点是,这里有个有意思的地方,如果我们把上述代码进行改造,删除最后一行的
    document.body.appendChild(script)
    ,发现 index2 的资源请求并没有发出,也就是说,
    脚本元素不向页面中插入,脚本的请求是不会发出的
    ,但是也会有反例,这个我们下面再说。

    在本示例中,后来我又把脚本请求换成了 css 请求,结果是一致的。

  • 场景四
    :图片的懒加载/预加载
    html 保持不变,index.js 用于加载图片,内容如下:

    const img = document.createElement('img')
    
    // 请求时长为0.5秒的图片资源
    img.src = 'http://localhost:3010/index.png?delay=500'
    document.body.appendChild(img)
    
    

    结果示意:
    alt
    表现是与场景三一样的,这个不再多说,但是有意思的来了,不一样的是,经过测试发现,哪怕删除最后一行代码:
    document.body.appendChild(img)

    不向页面中插入元素,图片也会发出请求
    ,也同样延长了页面加载时长,所以部分同学就要注意了,这是一把双刃剑:当你真的需要懒加载图片时,可以少写最后一行插入元素的代码了,但是如果大量的图片加载请求发出,哪怕不向页面插入图片,也真的会拖慢页面的时长。

    趁着这个场景,再多说一句,一些埋点数据的上报,也正是借着图片有不需要插入dom即可发送请求的特性,实现成功上传的。

  • 场景五
    :普通接口请求
    html 保持不变,index.js 内容如下:

    // 请求时长为500毫秒的请求接口
    fetch('http://localhost:3010/api?delay=500')
    
    

    结果如下图:
    alt
    可以发现普通接口请求的发出,并不会影响页面加载,但是我们再把场景弄复杂一些,见场景六。

  • 场景六
    :同时加载样式、脚本,脚本加载完成后,内部http接口请求,等请求结果返回后,再发出图片请求或修改dom,这也是更贴近生产环境的真实场景
    html 代码:

    <!DOCTYPE html>
    
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>test</title>
    
        <!-- 请求时长为1.2秒的css -->
        <link rel="stylesheet" href="http://localhost:3010/index.css?delay=1200">
    
        <!-- 请求时长为0.4秒的js -->
        <script src="http://localhost:3010/index.js?delay=400" async></script>
    </head>
    <body>
    </body>
    </html>
    
    

    index.js 代码:

    async function getImage () {
        // 请求时长为0.5秒的接口请求
        await fetch('http://localhost:3010/api?delay=500')
    
        const img = document.createElement('img')
        // 请求时长为0.5秒的图片资源
        img.src = 'http://localhost:3010/index.png?delay=500'
        document.body.appendChild(img)
    
    }
    
    getImage()
    
    

    结果图如下:
    alt

    如图所示,结合场景五记的结果,虽然普通的 api 请求并不会影响页面加载时长,但是因为api请求过后,重新请求了图片资源(或大量操作 dom),依然会导致页面加载时间变长。这也是我们日常开发中最常见的场景,页面加载了js,js发出网络请求,用于获取页面渲染数据,页面渲染时加载图片或进行dom操作。

  • 场景七
    :页面多媒体资源的加载
    示例代码:

    <!DOCTYPE html>
    
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>test</title>
    </head>
    <body>
        <video src="http://localhost:3010/video.mp4?delay=500" controls></video>
    </body>
    </html>
    
    

    结果如图:
    alt

    对于视频这种多媒体资源的加载比较有意思,
    video 标签对于资源的加载是默认开启 preload 的
    ,所以资源会默认进行网络请求(如需关闭,要把 preload 设置为 none ),可以看到红色竖线基本处于图中绿色条和蓝色条中间(实际上更偏右一些),图片绿色部分代表资源等待时长,蓝色部分代表资源真正的加载时长,且蓝色加载条在onload的竖线右侧,这说明多媒体的资源确实影响了 onload 时长,但是又没完全影响,因为设置了500ms的延迟返回资源,所以 onload 也被延迟了500ms左右,但一旦视频真正开始下载,这段时长已经不记录在 onload 的时长中了。

    其实这种行为也算合理,毕竟多媒体资源通常很大,占用的带宽也多,如果一直延迟 onload,意味着很多依赖 onload 的事件都无法及时触发。

    接下来我们把这种情况再复杂一些,贴近实际的生产场景,通常video元素是包含封面图 poster 属性的,我们设置一张延迟1秒的封面图,看看会发生什么,结果如下:
    alt
    不出意外,果然封面图影响了整体的加载时长,魔鬼都在细节中,
    封面图也需要注意优化压缩

  • 场景八
    :异步脚本和样式资源一同请求
    示例代码:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>test</title>
    
        <!-- 请求时长为1秒的css -->
        <link rel="stylesheet" href="http://localhost:3010/index.css?delay=1000">
    
        <!-- 请求时长为0.5秒的js -->
        <script src="http://localhost:3010/index.js?delay=500" async></script>
    </head>
    <body>
    </body>
    </html>
    
    

    浏览器表现如下:
    alt
    可以看出 css 资源虽然没有阻塞脚本的加载,但是却延迟了整体页面加载时长,其中原因是css资源的加载会影响 render tree 的生成,导致页面迟迟不能完成渲染。
    如果尝试把 async 换成 defer,或者干脆使用同步的方式加载脚本,结果也是一样,因结果相同,本处不再举例。

  • 场景九
    :样式资源先请求,再执行内联脚本逻辑,最后加载异步脚本
    我们把场景八的代码做一个改造,在样式标签和异步脚本标签之间,加上一个只包含空格的内联脚本,让我们看看会发生什么,代码如下:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <script>
            console.log('页面js 开始执行')
        </script>
    
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>test</title>
    
        <!-- 请求时长为1秒的css -->
        <link rel="stylesheet" href="http://localhost:3010/index.css?delay=2000">
    
        <!-- 此标签仅有一个空格 -->
        <script> </script>
    
        <!-- 请求时长为0.5秒的js -->
        <script src="http://localhost:3010/index.js?delay=500" async></script>
    </head>
    <body>
    </body>
    </html>
    
    

    index.js 中的内容如下:

    console.log("脚本 js 开始执行");
    
    

    结果如下,这是一张 GIF,加载可能有点慢:
    alt
    这个结果非常有意思,他到底发生了什么呢?


    1. 脚本请求是0.5秒的延迟,样式请求是2秒

    2. 脚本资源是 async 的请求,异步发出,应该什么时候加载完什么时候执行

    3. 但是图中的结果却是等待样式资源加载完毕后才执行


    答案就在那个仅有一个空格的脚本标签中
    ,经反复测试,如果把标签换成注释,也会出现一样的现象,如果是一个完全空的标签,或者根本没有这个脚本标签,那下方的index.js 通过 async 异步加载,并不会违反直觉,加载完毕后直接执行了,所以这是为什么呢?

    这其实是因为样式资源下方的 script 虽然仅有一个空格,但是被浏览器认为了它内部可能是包含逻辑,一定概率会存在样式的修改、更新 dom 结构等操作,因为样式资源没有加载完(被延迟了2秒),导致同步 js (只有一个空格的脚本)的执行被阻塞了,众所周知页面的渲染和运行是单线程的,既然前面已经有了一个未执行完成的 js,所以也导致了后面异步加载的 js 需要在队列中等待。这也就是为什么 async 虽然异步加载了,但是没有在加载后立即执行的原因。

  • 场景十
    :字体资源的加载
    示例代码:

    <!DOCTYPE html>
    
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>test</title>
        <style>
            @font-face {
                font-family: font-custom;
                src: url('http://localhost:3010/font.ttf?delay=500');
            }
    
            body {
                font-family: font-custom;
            }
        </style>
    </head>
    <body></body>
    </html>
    
    

    结果如下:
    alt
    可以看到,此情况下字体的加载是对 onload 有影响的,然后我们又测试了一下只声明字体、不使用的情况,也就是删除上面代码中 body 设置的字体,发现这种情况下,字体是不会发出请求的,仅仅是造成了代码的冗余。

四、总结

前面列举了大量的案例,接下来我们做个总结,实质性影响 onload 其实就是几个方面。

  1. 图片资源的影响毋庸置疑,无论是在页面中直接加载,还是通过 js 懒加载,只要加载过程是在 onload 之前,都会导致页面 onload 时长增加。

  2. 多媒体资源的等待时长会被记入 onload,但是实际加载过程不会。

  3. 字体资源的加载会影响 onload。

  4. 网络接口请求,不会影响 onload,但需要注意的是接口返回后,如果此时页面还未 onload,又进行了图片或者dom操作,是会导致 onload 延后的。

  5. 样式不会影响脚本的加载和解析,只会阻塞脚本的执行。

  6. 异步脚本请求不会影响页面解析,但是脚本的执行同样影响 onload。

五、优化举措

  1. 图片或其他资源的预加载可以通过 preload 或 prefetch 请求,这两种方式都不会影响 onload 时长。

  2. 一定注意压缩图片,页面中图片的加载速度可能对整体时长有决定性影响。

  3. 尽量不要做串行请求,没有依赖关系的情况下,推荐并行。

  4. 中文字体包非常大,可以使用
    字蛛
    压缩、或用图片代替。

  5. 静态资源上 cdn 很重要,压缩也很重要。

  6. 删除你认为可有可无的代码,没准哪一行代码就会影响加载速度,并且可能很难排查。

  7. 视频资源如果在首屏以外,不要开启预加载,合理使用视频的 preload 属性。

  8. async 和 defer 记得用,很好用。

  9. 非必要的内容
    ,可以在 onload 之后执行,是时候重新拾起来这个 api 了。