Titanium's Blog


  • Home

  • About

  • Tags

  • Categories

  • Archives

  • Search

单例模式的五种Java实现

Posted on 2019-09-20 | In Design Pattern | | Visitors:

定义

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

特点

  • 单例类仅有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例
  • 单例类的唯一实例必须由自己创建
  • 避免对共享资源的多重占用
  • 单例对象在堆上分配的内存空间只有在程序终止后才会释放(不要滥用单例)

使用场景

  • 需频繁实例化/销毁的对象
  • 有状态的工具类对象
  • 频繁访问数据库/文件的对象

实例

  • 网站的计数器采用单例模式,以保证同步。
  • windows的Recycle Bin(回收站)是典型的单例应用。
  • 应用程序的日志应用一般用单例模式实现。由于共享的日志文件一直处于打开状态,只能有一个实例去操作,否则内容不好追加。
  • 数据库连接池一般也是采用单例模式。数据库软件系统中使用数据库连接池,主要是节省打开或者关闭数据库连接所引起的效率损耗
  • 多线程的线程池一般采用单例模式

饿汉模式

饿汉模式指的是,使用之前初始化,类加载时就完成了初始化

特点

  • 类加载比较慢,会占用较多的内存
  • 基于classloader的机制避免了同步问题
  • 由于没有使用锁,执行效率高

代码

1
2
3
4
5
6
7
8
public class Singleton {  
private static Singleton instance = new Singleton();
private Singleton (){
}
public static Singleton getInstance() {
return instance;
}
}

懒汉式 Lazy Initialization

指的是在用的时候才实例化,即懒加载。

特点

  • 需要使用锁来保证线程安全,这会牺牲效率

代码

1
2
3
4
5
6
7
8
9
10
public class Singleton {  
private static Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

双重检查模式

  1. 第一次检查
    • 避免没必要的同步,如果不是null的话,线程不会被阻塞,直接返回单例。
    • 只有在单例是null的情况下,需要同步构建一个单例
  2. 第二次检查

    由于进行了同步,在第一个进入临界区的线程建立单例后,为了防止后面其他被阻塞的线程重复创建单例,因此进行第二次检查:不为null就直接返回。

  3. volatile保证有序性

    由于singleton=new Singleton()语句不具有原子性,该语句可分为三步:

    • 分配内存空间
    • 初始化对象
    • 将内存空间的地址赋给对应的引用

      有可能指令重排后,初始化对象放在了最后一步,而此时singleton已经不是null了,因此可能有其他的线程成功跳过了第一次检查,返回了一个没有正确初始化的实例。
      而volatile可以禁止指令重排,保证所有线程都可以获取到正确实例化的单例对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Singleton { 
private volatile Singleton singleton; // volatile ensure executing order
private Singleton() {
}
public static Singleton getInstance() {
if (singleton == null) { // first check
synchronized (Singleton.class) {
if (singleton == null) { // second check
singleton = new Singleton();
}
}
}
return singleton;
}
}

静态内部类模式

推荐使用

可以达到与DCL同样的效果,而且实现简单。

  • 同步问题:利用了ClassLoader的机制避免了同步问题(classloader可以保证一个类只被加载一次)
  • 懒加载:而静态内部类只在被调用的时候,才会被加载,因而保证了懒加载
1
2
3
4
5
6
7
8
9
10
11
12
public class Singleton { 
// Holder
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}

private Singleton (){}

public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}

微信小程序-mpvue+koa2框架搭建本地开发环境

Posted on 2019-09-20 | | Visitors:

在本地搭建开发环境,方便开发进行

搭建mpvue环境

  1. 首先获取小程序AppId
    AppId可去往微信公众平台获取,在设置一栏的开发设置下

2.初始化mpvue
该步骤需要实现安装过nodejs,与vue,具体安装步骤见vuejs的三个安装方法

进入到项目目录后输入

vue init mpvue/mpvue-quickstart

然后根据提示逐步输入信息

project name 即项目名称

wxmp appid 即微信小程序appid,从微信公众平台中获得。

剩下的选项直接敲回车键即可。

3.安装依赖
在项目目录下 输入

npm install
Bug:在进行这一步的时候出现了一个问题,错误提示如下:

于是我开始使用管理身份运行cmd,继续install,但仍然出现同样的错误,这时候使用

npm cache clean —force
清楚完npm的缓存后发现运行正常了,等待一段时间后得到如下结果

安装依赖完成,这一步在项目文件夹下下载了node_modules文件夹

4.使用微信小程序web开发工具打开项目

打开后即可看到mpvue的demo实例小程序

到这里说明mpvue弄好了。

二、搭建本地开发环境

1.下载demo
地址:https://console.qcloud.com/lav2/dev

下载完成后将里面的server文件夹复制到项目的根目录下。

2.建立数据库
使用mysql数据库

打开cmd窗口,cd到mysql的安装路径
cd C:\Program Files\MySQL\MySQL Server 5.7\bin
登录mysql数据库
mysql -u root -p
输入密码后进入数据库

建立数据库
create database CAuth;
建立名为CAuth的数据库,由于在server/config.js中已经配置好数据库,所以命名数据库为CAuth,若想用其他名字,也需要在config.js中对应修改,config.js文件中mysql的配置如下:

/**

  • MySQL 配置,用来存储 session 和用户信息
  • 若使用了腾讯云微信小程序解决方案
  • 开发环境下,MySQL 的初始密码为您的微信小程序 appid
    */
    mysql: {
    host: ‘localhost’,
    port: 3306,
    user: ‘root’,
    db: ‘cAuth’,
    pass: ‘你数据库的密码’,
    char: ‘utf8mb4’
    },
    注意要对应修改pass,否则初始化数据库时会无法登录。

3.配置config.js
在server/config.js中的CONF中添加如下代码:

serverHost: ‘localhost’,
tunnelServerUrl: ‘’,
tunnelSignatureKey: ‘27fb7d1c161b7ca52d73cce0f1d833f9f5b5ec89’,
// 腾讯云相关配置可以查看云 API 秘钥控制台:https://console.cloud.tencent.com/capi
qcloudAppId: ‘您的腾讯云 AppID’,
qcloudSecretId: ‘您的腾讯云 SecretId’,
qcloudSecretKey: ‘您的腾讯云 SecretKey’,
wxMessageToken: ‘weixinmsgtoken’,
networkTimeout: 30000,
修改以下三项,腾讯云相关配置可以查看云 API 秘钥控制台

4.初始化环境
首先安装依赖

切换到服务端代码目录

cd server

安装依赖

npm install

安装全局依赖

npm install -g nodemon

初始化数据库

node tools/initdb.js

5.测试本地环境是否搭建成功
1.首先可以在server/controllers目录下新建test.js文件内容如下:

module.exports = async (ctx) => {
ctx.state.data={
msg:’hello 小程序后台’
}
}
2.然后在server/routes/index.js文件中添加一句

router.get(‘/test’,controllers.test)
3.在cmd窗口中,cd到server文件夹下输入:

npm run dev
成功结果如下:

4.打开浏览器输入 http://localhost:5757/weapp/test

若看见下图

则说明配置成功。

JSONP简介

Posted on 2019-08-13 | In Front-end , Javascript | | Visitors:

JSONP

JSONP(JSON with padding) 是一种跨域访问的机制。

跨域访问

定义:跨域是指从一个域名的网页去请求另一个域名的资源。比如从http://www.baidu.com/ 页面去请求 http://www.google.com 的资源。跨域的严格一点的定义是:只要 协议,域名,端口有任何一个的不同,就被当作是跨域。

跨站攻击

传统上,浏览器的实现禁止 XMLHttpRequest 请求跨域访问,因为这样会引发安全问题,如跨站攻击。
跨站攻击例:

  • A网站通过以下请求发帖:http://example.com/bbs/create_post.php?title=标题&content=内容
  • 若Attacker在B网站发帖包含以下链接:http://example.com/bbs/create_post.php?title=我是脑残&content=哈哈
  • 则只要在B网站点了以上链接的victim,都会不知情的发帖。由于example网站保存了cookie,因此有victim的authentication

跨域使用场景

有时公司内部有多个不同的子域,比如一个是location.company.com,而应用是放在app.company.com , 这时想从 app.company.com去访问 location.company.com 的资源就属于跨域。

如何跨域

首先,要知道HTML中的<script>、<img> 标签的src属性是可以跨域访问的。JSONP正是利用了这一点进行跨域访问。

为了便于客户端使用数据,逐渐形成了一种非正式传输协议,人们把它称作JSONP,该协议的一个要点就是允许用户传递一个callback参数给服务端,然后服务端返回数据时会将这个callback参数作为函数名来包裹住JSON数据,这样客户端就可以随意定制自己的函数来自动处理返回数据了。

JSONP缺点

  • 由于script标签仅支持get方法,因此JSONP也仅支持get请求
  • 需要服务器配合返回指定格式的数据

简单实现

  • 客户端

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    <!DOCTYPE html>
    <html>
    <head>
    <title></title>
    <script type="text/javascript">
    // 得到航班信息查询结果后的回调函数
    var flightHandler = function(data){
    alert('你查询的航班结果是:票价 ' + data.price + ' 元,' + '余票 ' + data.tickets + ' 张。');
    };
    //把callback函数赋给window对象,供script回调
    window.flightHandler = flightHandler;

    // 提供jsonp服务的url地址(不管是什么类型的地址,最终生成的返回值都是一段javascript代码)
    var url = "http://flightQuery.com/jsonp/flightResult.aspx?code=CA1998&callback=flightHandler";

    // 创建script标签,设置其属性
    var script = document.createElement('script');
    script.setAttribute('src', url);
    // 把script标签加入head,此时调用开始
    document.getElementsByTagName('head')[0].appendChild(script);
    </script>
    </head>
    <body></body>
    </html>
  • 服务器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    var http = require('http');
    var urllib = require('url');

    var port = 8080;
    var data = {'data':'world'};

    http.createServer(function(req,res){
    var params = urllib.parse(req.url,true);
    if(params.query.callback){
    console.log(params.query.callback);
    //jsonp
    var str = params.query.callback + '(' + JSON.stringify(data) + ')';
    res.end(str);
    } else {
    res.end();
    }

    }).listen(port,function(){
    console.log('jsonp server is on');
    });

JQuery实现

$.getJSON

1
2
3
$.getJSON("http://crossdomain.com/services.php?callback=?", function(json){
alert('您查询到航班信息:票价: ' + json.price + ' 元,余票: ' + json.tickets + ' 张。');
});

$.ajax

1
2
3
4
5
6
7
8
9
10
11
12
13
$.ajax({
type: "get",
url: "http://flightQuery.com/jsonp/flightResult.aspx?code=CA1998",
dataType: "jsonp",
jsonp: "callback",//传递给请求处理程序或页面的,用以获得jsonp回调函数名的参数名(一般默认为:callback)
jsonpCallback:"flightHandler",//自定义的jsonp回调函数名称,默认为jQuery自动生成的随机函数名,也可以写"?",jQuery会自动为你处理数据
success: function(json){
alert('您查询到航班信息:票价: ' + json.price + ' 元,余票: ' + json.tickets + ' 张。');
},
error: function(){
alert('fail');
}
});

AngularJS实现

1
2
3
4
5
6
$http.jsonp('https://public-api.wordpress.com/rest/v1/sites/wtmpeachtest.wordpress.com/posts?callback=JSON_CALLBACK')
.success(function(data){
alert('success:'+data);
}).error(function(err){
alert('error:'+err);
});

重排与重绘

Posted on 2019-08-12 | In Front-end , HTML/CSS | | Visitors:

浏览器渲染过程

  1. 解析HTML,生成DOM树,解析CSS,生成CSSOM树
  2. 将DOM树和CSSOM树结合,生成渲染树(Render Tree)
  3. Layout(排布):根据生成的渲染树,进行排布(Layout),得到节点的几何信息(位置,大小)
  4. Painting(绘制):根据渲染树以及排布得到的几何信息,得到节点的绝对像素
  5. Display:将像素发送给GPU,展示在页面上。(这一步其实还有很多内容,比如会在GPU将多个合成层合并为同一个层,并展示在页面中。而css3硬件加速的原理则是新建合成层)

Browser Rendering Process

重排与重绘

任何改变用来构建渲染树的信息都会导致一次重排或重绘。

  • 重排
    部分渲染树(或者整个渲染树)需要重新分析并且节点尺寸需要重新计算
  • 重绘
    由于节点的几何属性发生改变或者由于样式发生改变,例如改变元素背景色时,屏幕上的部分内容需要更新

注意:

  • 重绘不一定导致重排,重排一定导致重绘
  • 重排开销比重绘大
实例 重排 重绘
bstyle.padding = "20px"; ⭐ ⭐
bstyle.border = "10px solid red"; ⭐ ⭐
bstyle.color = "blue"; ⭐
bstyle.backgroundColor = "#red"; ⭐
bstyle.fontSize = "2em"; ⭐ ⭐
document.body.appendChild(document.createTextNode('dude!')); ⭐ ⭐
浏览器尺寸发生改变 ⭐ ⭐
元素位置或尺寸发生改变 ⭐ ⭐
新增或删除可见元素 ⭐ ⭐
激活CSS伪类 ⭐ ⭐
visibility、outline、background-color ⭐

最小化重排重绘

浏览器的优化

浏览器会基于你的脚本要求创建一个变化的队列,然后分批去展现。通过这种方式许多需要一次重排的变化就会整合起来,最终只有一次重排会被计算渲染。浏览器可以向已有的队列中添加变更并在一个特定的时间或达到一个特定数量的变更后执行。

但是当程序员在代码中,需要直接获取样式信息时,为了提供最新的样式信息,浏览器被迫刷新队列,这时会引发一次重排重绘。

优化方法

  • 不要逐个修改样式,通过改变类名或编辑cssText方式来修改

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // bad 
    var left = 10, top = 10;
    el.style.left = left + "px";
    el.style.top = top + "px";

    // change class name
    el.className += " theclassname";

    // edit cssText
    el.style.cssText += "; left: " + left + "px; top: " + top + "px;";
  • 修改大量属性时,先通过display:none将元素隐藏(触发一次重排),批量修改完属性后再通过display显示出来(触发第二次重排),这样修改大量属性也只触发两次重排。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function appendDataToElement(appendToElement, data) {
    let li;
    for (let i = 0; i < data.length; i++) {
    li = document.createElement('li');
    li.textContent = 'text';
    appendToElement.appendChild(li);
    }
    }
    const ul = document.getElementById('list');
    ul.style.display = 'none';
    appendDataToElement(ul, data);
    ul.style.display = 'block';
  • 使用文档片段(Document Fragment),构建子树,在子树中修改,完成后再接入DOM中

    1
    2
    3
    4
    const ul = document.getElementById('list');
    const fragment = document.createDocumentFragment();
    appendDataToElement(fragment, data);
    ul.appendChild(fragment);
  • 复制要更新的节点,修改节点的副本,修改完成后,再使用副本替换树上的节点。

    1
    2
    3
    4
    const ul = document.getElementById('list');
    const clone = ul.cloneNode(true);
    appendDataToElement(clone, data);
    ul.parentNode.replaceChild(clone, ul);
  • 不要频繁取样式,样式取出来后存在变量里。

    优化前,每次循环的时候,都读取了box的一个offsetWidth属性值,然后利用它来更新p标签的width属性。这就导致了每一次循环的时候,浏览器都必须先使上一次循环中的样式更新操作生效,才能响应本次循环的样式读取操作。每一次循环都会强制浏览器刷新队列。

    1
    2
    3
    4
    5
    function initP() {
    for (let i = 0; i < paragraphs.length; i++) {
    paragraphs[i].style.width = box.offsetWidth + 'px';
    }
    }

    优化后

    1
    2
    3
    4
    5
    6
    const width = box.offsetWidth;
    function initP() {
    for (let i = 0; i < paragraphs.length; i++) {
    paragraphs[i].style.width = width + 'px';
    }
    }
  • 对于多次重排的元素,比如说动画。使用绝对定位脱离文档流,使其不影响其他元素。

Apollo自动驾驶平台:订阅点云数据实现避障

Posted on 2019-07-26 | In Autonomous Driving | | Visitors:

Cyber RT

Apollo平台的代码在3.5版本及以后完全摒弃了ROS(Robot Operating System),而是将整个平台基于Baidu自行研发的Cyber RT系统上。Cyber RT相较于ROS系统有以下优点:

  • 实时性高,时延小:自动驾驶对于时延的要求十分高,而ROS系统主要针对机器人,其对时延的要求较低,这也是baidu研发Cyber RT的初衷。
  • 调度确定性:ROS的任务调度具有不确定性,任务的执行顺序取决于系统,而Cyber RT将任务调度从内核空间移至用户空间,调度可以与算法逻辑密切结合。

Cyber RT vs ROS

Cyber RT ROS 含义
Component 无 组件之间通过 Cyber channel 通信
Channel Topic channel 用于管理数据通信,用户可以通过 publish/subscribe 相同的 channel 来通信
Node Node 每一个模块包含 Node 并通过 Node 来通信。一个模块通过定义 read/write 和/或 service/client 使用不同的通信模式
Reader/Writer Publish/Subscribe 订阅者模式。往 channel 读写消息的类。 通常作为 Node 主要的消息传输接口
Service/Client Service/Client 请求/响应模式,支持节点间双向通信
Message Message Cyber RT 中用于模块间通信的数据单元。其实现基于 protobuf
Parameter Parameter Parameter 服务提供全局参数访问接口。该服务基于 service/client 模式
Record file Bag file 用于记录从 channel 发送或接收的消息。 回放 record file 可以重现之前的操作行为
Launch file Launch file 提供一种启动模块的便利途径。通过在 launch file 中定义一个或多个 dag 文件,可以同时启动多个 modules
Task 无 异步计算任务
CRoutine 无 协程,优化线程使用与系统资源分配
Scheduler 无 任务调度器,用户空间
Dag file 无 定义模块拓扑结构的配置文件

Apollo 代码结构

整体框架

Apollo 5.0 Github 开源代码

  • /cyber:Baidu研发的Cyber RT系统
  • /data
    • /bag:可以用于放置录制的数据包,用于仿真模拟。可以通过cyber_recorder play -f /data/bag/test_filename来播放数据包
    • /log:保存着运行的日志文件。
  • /docs:大量的代码文档、操作文档、说明书等。
  • /docker:有关docker操作的一些脚本文件
  • /modules:核心模块,包含bridge、calibaration、canbus、common、contrib、control、data、dreamview、drivers、guardian、localization、map、monitor、perception、planning、prediction、routing、v2x等模块
  • /scripts:包含调试、运行的一些脚本。

模块的结构

一个模块通常有/conf、/dag、/proto、/launch四个子目录。

/launch

以下为modules/control/launch/control.launch的内容,该文件主要指示了一个dag文件。

1
2
3
4
5
6
7
<cyber>
<module>
<name>control</name>
<dag_conf>/apollo/modules/control/dag/control.dag</dag_conf>
<process_name>control</process_name>
</module>
</cyber>

若是想要单独启动某一模块,如control模块,可以通过以下脚本启动。当然也可以在浏览器里Dreamview界面里打开control模块的开关来启动control模块,但在命令行以脚本启动control模块可以看到输出的日志信息,便于错误排查

1
cyber_luanch module/control/launch/control.launch

/dag

dag文件里主要指示了模块的配置文件(flag_file_path属性),还是以control模块为例,以下为modules/control/dag/control.dag的内容

1
2
3
4
5
6
7
8
9
10
11
module_config {
module_library : "/apollo/bazel-bin/modules/control/libcontrol_component.so"
timer_components {
class_name : "ControlComponent"
config {
name: "control"
flag_file_path: "/apollo/modules/control/conf/control.conf"
interval: 10
}
}
}

/proto

proto是协议protobuf的简写,里面定义了发布的数据格式,protobuf是google团队开发的用于高效存储和读取结构化数据的工具。

/conf

里面主要包含模块的配置文件,包含了模块的一些参数,类似于在命令行执行脚本给的参数。由control.dag文件指向的主配置文件control.conf部分内容如下:

1
2
3
--flagfile=/apollo/modules/common/data/global_flagfile.txt
--control_conf_file=/apollo/modules/control/conf/scout_conf.pb.txt
--enable_speed_station_preview=false

这里的参数名可以在模块对应的common下的flags文件里定义,也可以在modules下的common模块里定义。比如control/common/目录下有control_gflags.h和 control_gflags.cc两个文件,分别用来声明参数名,和定义参数并给定参数默认值。

比如若要自定一个模块配置参数x_min,首先需要在control_gflags.h里声明:

1
DECLARE_double(x_min);

然后在control_gflags.cc里定义,中间参数为默认值,第三个参数为对参数的描述,类似于帮助文档:

1
DEFINE_double(x_min,0.0001,"x min");

然后若要使用参数x_min,则可以在其他文件里include control_flags.h 这个头文件,然后通过FLAGS_x_min来使用其值,如果需要更改x_min的值,则可以在control.conf文件里修改,如将其改为-5.0,可以增加以下一行。由于这些都是命令行参数,因此修改这些值,是不需要重新编译代码的,方便调参。

1
--x_min=-5.0

代码编写

主要更改代码的控制模块modules/control/,在控制模块通过订阅激光雷达的点云数据,获取小车正前方的的激光点云,判断若前方有障碍物则将让小车停下来。

修改control/Build文件

由于要使用激光点云数据,需要额外增加依赖项,在/modules/control/Build文件里找到cc_library,在其deps数组里增加

1
"//modules/drivers/proto:sensor_proto",

sensor_proto这个库的名字可以在modules/drivers/proto/Build文件下的cc_proto_library里找到。

初始化PointCloud Reader

Cyber RT采用Publish/Subscribe的通信机制,不同的模块在各自的Channel上发布Publish数据,其他模块可以通过订阅Subscribe来获取Channel上的数据。
而在订阅前,当然需要先指定一个Reader,由Reader去订阅数据并读取数据。control模块已经定义了chassis_reader_用于读取车底盘的数据、localization_reader_用于读取定位数据,可以获取车辆的ENU坐标、trajectory_reader_用于获取轨迹数据等reader,接下来就可以仿照这些reader定义自己的point_cloud_reader_了。
首先在modules/control/control_component.h文件里声明reader,需注意命名空间的问题。

1
std::shared_ptr<Reader<apollo::drivers::PointCloud>>point_cloud_reader_;

然后在modules/control/control_component.cc文件里的Init函数里对reader进行初始化:

1
2
3
4
5
6
7
8
// Point cload initialization
cyber::ReaderConfig pointcloud_reader_config;
// 设置reader的channel name,reader通过这个topic来寻找点云数据所在的channel
pointcloud_reader_config.channel_name = FLAGS_lidar_16_front_center_topic;
pointcloud_reader_config.pending_queue_size = FLAGS_pointcloud_pending_queue_size;
// 实例化Reader
point_cloud_reader_ = node_->CreateReader<apollo::drivers::PointCloud>(pointcloud_reader_config, nullptr);
CHECK(point_cloud_reader_ != nullptr);

  • FLAGS_开头的为gflags库定义的参数,gflags是使用Google的用于处理命令行参数的库,Apollo的代码里广泛运用了该库,大部分模块下都有一个/common文件夹下面有对应的gflags文件,里面定义了该模块使用的参数,使用gflags来配置模块参数的好处是更改参数后,无需编译,只需要重新启动对应模块,即可应用该参数。
  • FLAGS_lidar_16_front_center_topic:16线激光雷达正前方的点云数据所使用的topic名。
  • FLAGS_pointcloud_pending_queue_size为自定义的参数,默认值为10,设置方式参考上面设置x_min的方法。

如何找到对应的topic名呢?

  • velodyne.launch

    首先,drivers下有一个激光的子模块velodyne,在modules/drivers/velodyne,在其launch/目录下有不同激光雷达的启动文件,由于小车使用的是16线激光雷达,所以需要16线激光雷达的启动文件,velodyne.launch为16线激光雷达的启动文件,该文件指向了一个dag文件velodyne.dag。

  • velodyne.dag

    为了简化操作,只使用激光的正前方的数据,找到#16_front_center,其config里有一个config_file_path属性,指向了对应的配置文件velodyne16_front_center_conf.pb.txt.

  • velodyne16_front_center_conf.pb.txt

    改文件给出了对应的topic名

    1
    convert_channel_name: "/apollo/sensor/lidar16/front/center/PointCloud2"

    而该topic已在/modules/common/adapters/adapters_gflags.cc里定义了参数名:

    1
    2
    3
    DEFINE_string(lidar_16_front_center_topic,
    "/apollo/sensor/lidar16/front/center/PointCloud2",
    "front center 16 beam lidar topic name");

    由于代码的版本不同,可能没有该topic的FLAGS,不过可以直接用其值代替。

获取点云数据

点云数据格式

目前控制模块已经订阅了激光模块的点云数据,激光会通过订阅的channel发消息给控制模块,控制模块收到后需要解析该消息。

消息的格式定义在对应的proto文件里,点云消息的格式在/modules/drivers/proto/pointcloud.proto里,其内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
syntax = "proto2";
package apollo.drivers;

import "modules/common/proto/header.proto";

message PointXYZIT {
optional float x = 1 [default = nan];
optional float y = 2 [default = nan];
optional float z = 3 [default = nan];
optional uint32 intensity = 4 [default = 0];
optional uint64 timestamp = 5 [default = 0];
}

message PointCloud {
optional apollo.common.Header header = 1;
optional string frame_id = 2;
optional bool is_dense = 3;
repeated PointXYZIT point = 4;
optional double measurement_time = 5;
optional uint32 width = 6;
optional uint32 height = 7;
}

  • 可以看到点云里面有头部header、帧id frame_d、还包括点 point
  • 点的格式为PointXYZIT其包含x、y、z、intensity、timestamp。
  • 点由repeated修饰,说明point可以重复,有多个,可以当作是点的数组

获取消息对应属性的数据

假设收到了激光雷达发来的点云消息msg。

  • 所有repeated修饰的属性都有大小,可以通过msg->point_size()来获取点云里点的数量。
  • 索引点云:msg->point(i);
  • 获取点的坐标:msg->point(i).x();

获取数据代码

思路:非常简单的障碍物识别,获取点云数据后,若在小车前进方向某一范围内有点,说明前方有障碍物,点的数量超过一定值后就让小车停下来。

为了方便调参,将范围的参数(x_min,x_max,…,z_max)全部提取出来放到control_gflags.cc文件里定义,这样就可以很方便的在control.conf文件里面修改了。

注意:小车的正前方为为x的正方向、左方为y的正方向、上方为Z的正方向。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 开始监听channel上的数据
point_cloud_reader_->Observe();
// 获取最近收到的数据
const auto &point_cloud_msg = point_cloud_reader_->GetLatestObserverd();

int point_num = 0;
for( int i = 0; i < point_cloud_msg->point_size(); ++i)
{
float x = point_cloud_msg->point(i).x();
float y = point_cloud_msg->point(i).y();
float z = point_cloud_msg->point(i).z();

if( x > FLAGS_x_min && x < FLAGS_x_max && y > FLAGS_y_min &&
y < FLAGS_y_max && z > FLAGS_z_min && z < FLAGS_z_max){
point_num++;
}

if(point_num>20){
std::cout<< "Obstacle detected. set vehicle speed to zero."<<std::endl;
// 设置车速为0
control_command.set_speed(0.0);
// 设置车的角速度为0
control_command.set_angular_velocity(0.0);
}
}

到此,代码就完成了。
将代码编译

1
bash apollo.sh build

编译通过后就可以上车测试了。

仿真测试

若没有小车进行实测,可以先用录制的bag数据进行仿真测试。

  • 播放数据

    1
    cyber_recorder play -f /data/bag/test.00000
  • 启动控制模块

    1
    cyber_launch modules/control/launch/control.launch

    或者

    1
    bash control.sh start_fe

注意:AINFO/ADEBUG打印输出的内容在控制台上看不到,需要用std::cout打印输出。由于没有和小车相连,对应的车地盘数据获取不到,会出现chassis msg not ready的错误信息,这不影响。

上车测试

  • 首先录制一条小车运行的轨迹
  • 让小车自己跑起来,测试小车会不会在遇到障碍物后停下来

注意:这里可以用仿真测试里的命令行的形式启动control模块,而不是在dreamview里打开control模块的开关,这样可以方便查看控制台日志信息。

实操效果

The `` tag is not supported by your browser.

Apollo自动驾驶平台:循迹

Posted on 2019-07-23 | In Autonomous Driving | | Visitors:

线性二次调节器LQR

线性二次调节器(Linear Quadratic Regulator 或LQR)是基于模型的控制器,它通过控制车辆状态来使小车运动的横向误差和朝向误差最小化。Apollo使用LQR进行横向控制。横向控制包含四个参数:横向误差 $cte$、横向误差变化率 $\dot{cte}$、朝向误差 $\theta$和朝向误差变化率 $\dot{\theta}$,这四个参数构成1×4的列向量$x$。变化率与导数相同,用变量名上面的一个点来代表。该车有三个控制输入:转向、加速和制动,这个三个控制输入集合称为$u$。

LQR-X-U

$x$和$u$与自身相乘,这样负值也会产生正平方,为每个二次项分配权重,并将它们加在一起。

最优的$u$应该最小化二次项的和随时间的积分。

$Q$和$R$代表$x$和$u$的权重集合。$x^T$和$u^T$是转置矩阵,这意味着它们几乎与$x$和$u$相同,只是重新排列以便矩阵相乘。$x$乘以$x^T$,$u$乘以$u^T$,实质上是将每个矩阵乘以它自己。
$Q$在Apollo代码中对应modules/control/conf/scout_conf.pb.txt文件下的下面配置参数

1
2
3
4
matrix_q: 0.001
matrix_q: 0.0
matrix_q: 0.01
matrix_q: 0.0

$R$则对应

1
r: 0.00001

循迹过程中可以修改以上参数以使得小车更加稳定地进行循迹。

在LQR中,控制方法被描述为$u=-Kx$。其中,$K$代表一个复杂的skeme,代表如何从$x$计算出$u$。所以找到一个最优的u就是找到一个最优的$K$。

循迹操作流程

  1. 将操作者电脑连接至开发套件(小车)的无线网络中(小车自带一个路由)
  2. 操作电脑使用ssh远程登录至小车,若是直接在小车的系统上进行操作可以免去步骤1和2.
  3. 进入到apollo代码的根目录下,启用docker环境, 并进入docker环境

    1
    2
    bash docker/scripts/dev_start.sh
    bash docker/scripts/dev_into.sh
  4. 启动DreamView,执行bootstrap.sh脚本

    1
    bash scripts/bootstrap.sh
  5. 在浏览器中打开DreamView,输入小车的IP地址:8888即可(若是在小车系统上,直接用localhost:8888即可访问)。

  6. 在Module Controller下打开CAN Bus、Localization、GPS.

    注意:

    • 为保证可以接受到GPS信号,尽量在开阔处进行测试。
    • 若有修改参数,无需重新编译代码,直接将对应的模块重新打开一下即可。
    • 注意观察Task菜单下的Module Delay,若出现红色的延时,重新打开一下对应的模块。
  7. 将小车停至合适的位置(想要循迹的初始位置),操作者将Module Controller下的data recorder(录制运行时的数据)和RTK recorder(录制轨迹)打开后,就可以开始使用遥控器操控小车行驶来录制轨迹数据了。
  8. 录制完毕后,将data recorder、RTK recorder模块关闭,将小车停至循迹的初始点,打开Module Controller下的RTK replay、control,用遥控器将小车切入自动驾驶模式。然后在DreamView下的Task菜单下点击Start Auto,小车便可以开始自动循迹。

    注意:循迹期间需密切关注小车的行驶情况,遇险时应立即使用遥控接管或拍下小车后面的急停按钮。

Apollo自动驾驶平台:工控机软件系统安装教程

Posted on 2019-07-22 | In Autonomous Driving | | Visitors:

简介

此系列博客将记录如何使用百度Apollo自动驾驶开发者套件完成循迹功能。循迹指的是让车辆/机器人自己照着原来设定的路线行驶,因此首先要进行录制路线Record,然后再进行循迹Replay,以及完成简单的避障功能。

Apollo的开发者套件,也就是一辆小车,如下图所示, 拥有一个激光雷达,两个GPS天线,一个IMU,一个毫米波雷达,3个摄像头用于感知环境,小车同时配备一个遥控器用于控制小车,小车有两种模式:遥控模式和自动驾驶模式。

Apollo开发者套件

工控机软件系统安装

Ubantu系统安装

由于Apollo平台的代码均基于Linux,因此需要安装一个Linux系统,此处推荐Ubantu,官方用的也是Ubantu系统,官方教程推荐的是Ubantu14.04.3系统,但此系统较老旧,可能出现一些奇怪的问题(比如无法上网),因此还是推荐使用Ubantu16.04. 下面部分是Ubantu双系统的安装,使用虚拟机的话需要保证配置,不然跑不起来代码,至少需要给Ubantu系统分配70G的存储空间,据说两核至少需要8小时才可以编译Apollo平台。

软件准备

  • Ubantu16.04
  • 引导盘制作软件链接: https://pan.baidu.com/s/12KEntHWK8RZ1wSnVE6Aatw 提取码: 7vvb

步骤

  • 引导盘制作
    可以参考教程.
  • 磁盘分区

    1. 在“我的电脑”上右键单击,点“管理”。
    2. 进入下面界面后,点“存储->磁盘管理”。

      磁盘管理

    3. 选择一个空间足够(>=70G,最好100G)的非系统盘,右键单击,选择压缩卷。

      压缩卷

    4. 在查询完可用压缩空间后,会弹出下列对话框,输入想要压缩的空间,然后点击确认即分区完成(压缩的空间也就是预留给Ubantu的存储空间)

      压缩

  • Ubantu安装

    1. 将启动盘插入电脑
    2. 重启电脑,并尝试进入BIOS系统,进入方式不同电脑不一样,通常进入方式为F12/F2/DEL/Enter这几个键,可以多重启几次几个键都试试。重启后疯狂敲这些键就好啦。
    3. 进入后,选择USB启动,然后选择Ubantu即可进入Ubantu系统的安装向导。
    4. 然后照着向导依次选择语言、时区等等就OK啦。

      注意:Ubantu安装时,会要求填用户名和密码,这个密码一定要牢记,后面获取root权限需要该密码。

Docker安装

只需在Ubantu里运行Apollo提供的脚本即可,脚本链接

1
bash ./install_docker.sh

Apollo源代码编译

首先获取Apollo源代码,约1G左右,下载时间较长,耐心等待。

1
git clone https://github.com/ApolloAuto/apollo.git

下载完后,cd进入代码的根目录下,依次执行以下脚本,执行时间较长。

1
2
3
bash docker/scripts/dev_start.sh  
bash docker/scripts/dev_into.sh
bash apollo.sh build

编译完成后,执行下面命令打开DreamView交互平台。

1
bash scripts/bootstrap.sh

启动成功的提示信息:

1
2
3
4
Start roscore...
Launched module monitor.
Launched module dreamview.
Dreamview is running at http://localhost:8888

在浏览器上访问http://localhost:8888,即可打开DreamView。DreamView界面如下所示:

Dreamview user interface

到此环境就配置完毕, 下一个教程介绍如何使用DreamView完成循迹

参考

Apollo开源代码git仓库的官方教程。

HTTP 强缓存vs弱缓存

Posted on 2019-07-22 | In Computer Network , HTTP | | Visitors:

HTTP缓存

类型

  • 强缓存(本地缓存)
  • 弱缓存(协商缓存)

流程

访问静态资源时的流程

  1. 强缓存阶段:先在本地查找是否有该资源,若有,且Expires和Cache-Control都满足要求,则命中强缓存,返回200,直接返回强缓存中的数据,不会向服务器发出请求。
  2. 弱缓存阶段:在本地缓存找到该资源,发送http请求到服务器询问该资源是否有更新,若服务器判断没有更新,返回304Not Modified。则浏览器继续使用该资源。
  3. 缓存失败阶段:若在本地没有找到对应资源,或者资源已过期/更新,则服务器返回该资源。

强缓存VS弱缓存

  • 状态码
    强缓存返回200
    弱缓存返回304
  • 强缓存不需要发出http请求,而弱缓存需要。

强缓存

使用Expires和cache-control来控制。优先级:pragma>cache-control>Expires

  • Expires

    服务器为资源设置一个Expire日期,在这Expire日期之前可以将资源视作最新。此字段是为了兼容旧版本HTTP才保留的,在http1.0时配合pragma使用,pragma:no-cache表示不缓存,pragma的优先级大于Expires

  • Cache-Control

    Cache-Control是http1.1为了弥补Expires的缺陷而加入的(http1.0时,使用pragma),当Expires和Cache-Control同时存在时,Cache-Control的优先级高于Expires。

Cache-Control取值 描述
public 服务器端和浏览器端都可以缓存
private 只有浏览器可以缓存
no-cache 强制浏览器在使用cache拷贝之前先提交一个http请求到原服务器进行确认,类似于弱缓存
only-if-cached 表明客户端只接受已缓存的响应,并且不需要向服务器检查是否有更新的拷贝
max-age=60 单位:秒,缓存的有效期,超过这个时间后将被认为过期。此选项会覆盖Expires字段的过期日期
no-store 不缓存,使用协商缓存
must-revalidate 缓存必须在使用之前验证旧资源的状态,并且不可使用过期资源

弱缓存

有关字段 Last-Modified/If-Modified-Since(HTTP1.0)、Etag/If-None-Match(HTTP1.1)

  • Last-Modified和Etag为响应头部字段,分别对应请求头部字段中的if-Modifed-Since/If-None-Match。

Last-Modified/If-Modified-Since存在缺陷,当资源的实际内容没有改变而仅仅只是时间变化了(比如只是打开了文件,并保存了一下,尽管内容没有变化但还是修改过),还是需要重新请求资源。因此为了解决这个缺陷,http1.1提出了Etag/If-None-Match,Etag类似于文件的hash,为文件提供了一个指纹,只检查文件内容是否一致。

流程:

  1. 当没有命中强缓存时,进入弱缓存阶段
  2. 浏览器首次访问网站时,服务器会在响应头部中附上Last-Modified/Etag
  3. 弱缓存阶段,浏览器再次访问时,会发送If-Modified-Since/If-None-Match去询问是否有改变,有则重新获取,否则使用本地缓存。

缓存与浏览器刷新

  • F5刷新,会使强缓存失效,浏览器进行协商缓存。
  • Ctrl+F5刷新,为强制刷新,强缓存与弱缓存均失效,浏览器总会发送HTTP请求向服务器获取最新数据。

CSS:清除浮动的方法

Posted on 2019-07-14 | In Front-end , HTML/CSS | | Visitors:

CSS 浮动

浮动的框可以向左或向右移动,直到它的外边缘碰到包含框或另一个浮动框的边框为止。

浮动属性的本意是:让文字像流水一样环绕浮动元素

特点

包裹性

对于父元素而言,通常宽度默认为100%,高度适应内容,而对父元素使用float之后,父元素的宽度也会自适应内容,将内容包裹起来。

float
float-2

对应的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html>
<head>
<style type="text/css">
.div1 {
background:#000080;
border:1px solid red;
float: left;
}
.left {
width:200px;
height:200px;
background:#DDD;
}
</style>
</head>
<body>
<div class="div1">
<div class="left">Left</div>
</div>
</body>
</html>

高度塌陷

使用float属性之后,浮动元素脱离文档的普通流,他的父级元素无法获得浮动元素的高度,因而错误的认为里面没有元素,因而出现高度塌陷问题。

height collapse

对应代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html>
<head>
<style type="text/css">
.div1 {
background:#000080;
border:1px solid red;
}
.left {
width:200px;
height:200px;
background:#DDD;
float:left;
}
</style>
</head>
<body>
<div class="div1">
<div class="left">Left</div>
</div>
</body>
</html>

清除浮动

为了避免高度塌陷的问题,可以通过多种方式清除浮动

一、父元素固定height

既然是父元素高度塌陷,那么最简单直接的方式是将父元素的高度固定下来。但这种方式缺乏灵活性,适合固定高度的布局。

二、空标签+clear:both

在父级元素里面添加一个空的div标签,并设置div标签的clear属性为both。该方法的浏览器支持性较好,但是当浮动布局元素较多时,需要很多空div标签,因此不推荐使用。
代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!DOCTYPE html>
<html>
<head>
<style type="text/css">
.div1 {
background:#000080;
border:1px solid red;
}
.left {
width:200px;
height:200px;
background:#DDD;
float:left;
}
.clearfloat{clear:both} /* new */
</style>
</head>
<body>
<div class="div1">
<div class="left">Left</div>
</div>
<div class="clearfloat"></div> <!--new-->
</body>
</html>

效果:

clear both

三、父级元素添加overflow

直接为父级元素添加属性overflow,设置值为hidden或auto即可。此方法的原理是利用overflow:auto来触发BFC(块级格式化上下文),而在BFC渲染区域内,盒子的排布会呈现一定的规则,其中一条就是BFC在计算高度时,会算上浮动元素的高度,因此可以用出发BFC的方式来清除浮动。

  • hidden:缺点是无法和position一起使用,超出的部分会被隐藏掉。
  • auto: 缺点是当子元素高度大于父元素时,会出现滚动条。

四、定义伪元素 ::after

  • 兼容性:IE8以上才支持::after

目前主流采用此方法,浏览器兼容较好,推荐使用此方法解决浮动问题。伪元素::after用于在CSS选择器选中的元素后面创建一个伪元素,该元素在显示上类似于真实的元素,只是不会出现在dom树上. 清除浮动的原理与第一种方法相同,相当于在浮动元素后面增加了一个clear:both的空元素.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<!DOCTYPE html>
<html>
<head>
<style type="text/css">
.div1 {
background:#000080;
border:1px solid red;
}
.left {
width:200px;
height:200px;
background:#DDD;
float:left;
}
/* solution */
.clearfloat::after{
/* 伪元素默认为行内元素 */
display:block;
/* 清除浮动 */
clear:both;
/* 设置伪元素的内容,可以为任意值 */
content:".";
/* 不让伪元素显示出来 */
visibility:hidden;
height:0
}
.clearfloat{
/* 触发 hasLayout,为了兼容IE6,IE7 */
zoom:1
}
</style>
</head>
<body>
<div class="div1 clearfloat">
<div class="left">Left</div>
</div>
</body>
</html>

String Matching Algorithm——Sunday

Posted on 2019-07-10 | In Data Structure & Algorithm | | Visitors:

算法比较

对长为m的源串,长为n的子串进行匹配

算法 最坏时间复杂度 最优时间复杂度
暴力双循环 O(m*n) \
KMP O(m+n) \
Sunday O(m*n) O(m/n)

KMP算法

核心:充分利用已知的信息,通过构建部分匹配表(Partial Match Table)来控制匹配位。

可参考以下Youtube视频:

虽然懂了KMP的大概思路,但写出来的代码还是一堆bug呀,果然还是功力不够,不过还好有Sunday算法,简单易懂,速度又快!

Sunday算法

Sunday算法是对BM算法(Boyer-Moore算法)的进一步优化,是现代文字处理器中常用的字符串匹配算法。

核心:尽可能的跳过较多的字符串

参数介绍:

  • 主串S
  • 模式串P
  • i 为主串S的的index,j为模式串P的index。
  • m 指着模式串最后一位与主串对应的位置的下一位(现在不理解,可以看下面的实例)。
  • slen:主串S的长度
  • plen: 模式串P的长度

算法步骤

  • 首先初始状况是i=j=0,i指着S的首位,j指着P的首位,m则指着S的第plen+1位,及第6位
  • 判断S[i]与P[j]是否相等。
    • 若相等则i++,j++,比较下一位。
    • 若相等,且j==plen-1,也就是比较完毕了,则说明找到了目标,直接返回i-j即可。
    • 若是不相等,则需要看S[m]字符是否在P中出现
      • 若没出现,则直接将j指向S[m]的下一位,i指向m+1的位置,j则重新置0,m则更新为 新的i+plen。
      • 若出现了,还是要想办法尽可能大的将P往后滑动。首先,找到模式串P中与S[m]字符相等字符的位置,然后将P串中的该字符与S[m]字符对齐。对齐时,P向右滑动了多少格,i更新的时候就加几,j则重置为0,m还是用更新后的i加plen:新i+plen。
      • 更新后若出现m>slen的情况,说明已经比较完毕,仍没有找到目标,则return -1。

实例讲解

参数

  • S: string matching algorithm
  • P: rithm
  • slen: 25
  • plen: 5

首先将i指向S首位,j指向P首位,即i=j=0. m则指向S的第plen位,也就是第5位。

初始化 i=0, j=0, m=5, 状态如下所示

S s t r i n g m a t c h i n g a l g o r i t h m
i m
P r i t h m
j

比较 S[0] 与 P[0],发现不等,则直接看 S[5] 是否在P中出现,'rithm'里是没有'g'的,故直接将P移到m的下一位。

第一步更新后 i=6, j=0, m=11, 状态如下所示

S s t r i n g m a t c h i n g a l g o r i t h m
i m
P r i t h m
j

比较S[6]与P[0],发现' '不等于'r',且S[11]在'rithm'的第3位出现。则需要将P中的'h'与S中的'h'对齐,只需要将P串向右移动两格(对应于i=m-3=8)

第二步更新后 i=8, j=0, m=13, 状态如下所示

S s t r i n g m a t c h i n g a l g o r i t h m
i m
P r i t h m
j

同样S[8]!=P[0],且S[13]为'n'不存在于'rithm',因此直接将P滑到m的下一位

第三步更新后 i=14, j=0, m=19

S s t r i n g m a t c h i n g a l g o r i t h m
i m
P r i t h m
j

再次比较, S[14]!=P[0], 且S[19]不在P中,再次将P滑到m的下一位。

第四步更新后 i=20, j=0, m=25

S s t r i n g m a t c h i n g a l g o r i t h m
i m
P r i t h m
j

这时候可以看到,'rithm'是匹配上了,一步一步比较,i与j逐步加1,直至j=4时,比较完毕,得到结果i-j=24-4=20。

LeetCode 题源

LeetCode 28. Implement strStr()

AC 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Solution {
public:
int strStr(string haystack, string needle) {
if(needle.empty()){
return 0;
}
int slen=haystack.length();
int plen=needle.length();
int i=0,j=0,k,m=plen; // i 指 haystack,j 指 needle,
while(i<slen){
if(haystack[i]!=needle[j]){
for(k=plen-1;k>=0;k--){
if(needle[k]==haystack[m])
break;
}
i=m-k;
j=0;
m=i+plen;
if(m>slen){
return -1;
}
}else{
if(j==plen-1){
return i-j;
}
i++,j++;
}
}
return -1;

}
};

12
Titanium

Titanium

You shoot me down, but I won't fall. I am Titanium.

14 posts
11 categories
42 tags
GitHub E-Mail
© 2020 Titanium
Powered by Hexo
|
Theme — NexT.Gemini v5.1.4
visitors times