学习3D特效前的准备阶段:使用mac系统,基于cmake搭建OpenGL环境

工作中越来越多的需求是要将数据以3D的形式展示给用户,但是在实现3D特效之前,都需要有一个准备阶段。OpenGL规范描述了绘制2D和3D图形的抽象API,正所谓“工欲善其事必先利其器”,所以,学习OpenGL之前,当然是需要搭建好运行的环境。

今年的目标就是熟练掌握好OpenGL的入门知识,之前由于工作的原因,迟迟无法开展,今天终于抽空搭建好了OpenGL环境。

搭建OpenGL环境过程中,也是花费了不少时间,特别是基于cmake来搭建OpenGL的环境,网络上的资料很少,并且也没找到成功的例子,因此,这里将搭建的过程分享出来。希望能够对大家有一定的参考价值。

本文首先会介绍搭建OpenGL环境之前的准备条件,然后再总体说明搭建的流程,接着再针对每个流程进行详细的说明。这里不会涉及太多专业术语,本文的目的就是要快速搭建好环境,方便后续的学习和工作的开展。

一、准备条件

本文OpenGL环境是在mac系统搭建,并且采用cmake来组织编译环境,使用的IDE工具是QtCreator 4.9.1, mac操作系统的版本是10.13.6。

搭建opengl环境的大致步骤为,构建glfw、构建glad, 链接系统自带的OpenGL库。

glfw的作用是创建OpenGL上下文、定义窗口参数以及处理用户输入;

glad根据版本加载所有相关的OpenGL函数。

OpenGL库则是实现了实现了规范的抽象API的集合。

二、创建工程

使用qtcreator工具来创建C++工程,工程名称为StudyOpengl

然后选择cmake来编译系统,其他按照默认进行操作即可

三、构建GLFW

官方网站上下载glfw的源代码包,其代码的目录结构如下图所示

然后使用qtcreator来打开glfw源代码包中的CMakeLists.txt, 目的就是使用qtcreator来编译glfw代码,并生成静态库

加载成功glfw的工程目录

选中工程根目录,然后右键弹出的菜单,点击“构建”选项

如果编译成功,那么在编译目录下能够看到名称为libglfw3的静态库

最后将glfw的头文件和静态库拷贝到工程StudyOpengl下,到这里,glfw的构建工作已经完成,后面还需要添加glfw的头文件和库的搜索路径,这个等到整体编译的时候,再统一进行说明。

四、构建GLAD

构建glad, 为了方便,我们这里直接使用网络上提供的在线服务来进行创建,具体的在线服务可以网络上进行搜索。

进入glad的在线服务页面如下所示,语言选择C/C++, API版本选择最新的为4.6, Profile下选择Core

拖动页面到最底部,勾选“Generate a loader”选项,然后点击“GENERATE”

等待片刻,即可生成glad的压缩包,将压缩包里面的include文件夹和glad.c文件拷贝到工程StudyOpengl, 至此,glad的构建工作也完成,添加头文件和库的搜索路径也等到整体编译的时候,再统一说明。

五、链接OpenGL

链接OpenGL,我觉得是最重要的一步,这是自己摸索出来的解决方法。这里链接是mac系统自带的opengl库。

项目工程StudyOpengl中的CMakeLists.txt文件中,添加搜索OpenGL库的路径,创建变量来保存Cocoa、CoreVideo、IOKit的库路径

find_package(OpenGL REQUIRED)
set(SYS_FRAMEWORKS_DIR /System/Library/Frameworks)
set(COCOA_LIBS ${SYS_FRAMEWORKS_DIR}/Cocoa.framework)
set(COREVIDEO_LIBS ${SYS_FRAMEWORKS_DIR}/CoreVideo.framework)
set(IOKIT_LIBS ${SYS_FRAMEWORKS_DIR}/IOKit.framework)

接着添加opengl的头文件搜索路径

include_directories(${OPENGL_INCLUDE_DIR})

最后将OpenGL、Cocoa、CoreVideo、IOKit的库链接到工程。

target_link_libraries(${PROJECT_NAME}
        ${OPENGL_LIBRARIES}
        ${COCOA_LIBS}
        ${COREVIDEO_LIBS}
        ${IOKIT_LIBS})

六、整体编译

cmake文件增加glad和glfw的头文件搜索路径,首次创建函数include_sub_directories_recursively,该函数的功能是递归的将输入路径下的所有目录添加到头文件的搜索路径

###
### 添加头文件搜索路径
###
function(include_sub_directories_recursively root_dir)
        if (IS_DIRECTORY ${root_dir})
                # include_directories包含头文件的搜索路径
                message("root_dir : ${root_dir}")
                include_directories(${root_dir})
        endif()
        # 如果 RELATIVE 标志位被设定,将返回指定路径的相对路径
        file (GLOB ALL_SUB RELATIVE ${root_dir} ${root_dir}/*)
        foreach(sub ${ALL_SUB})
                if (IS_DIRECTORY ${root_dir}/${sub} AND (NOT ("${sub}" STREQUAL ".svn")) AND (NOT ("${sub}" STREQUAL ".DS_Store")) )
                        include_sub_directories_recursively(${root_dir}/${sub})
                endif()
        endforeach()
endfunction()

include_sub_directories_recursively(${PROJECT_SOURCE_DIR}/code/third)

include_directories(
        ${CMAKE_CURRENT_BINARY_DIR}
        ${PROJECT_SOURCE_DIR}
        ${PROJECT_SOURCE_DIR}/code/third/glad/include
)

添加完成头文件的搜索路径之后,接下来就来添加库的搜索路径,同样的,实现函数link_sub_directories_recursively用于将输入路径下的所有目录都添加到库的搜索路径中

###
### 添加库的查找路径
###
function(link_sub_directories_recursively root_dir)
        if (IS_DIRECTORY ${root_dir})
                # link_directories(directory1 directory2 ...)
                # 指定查找库的目录
                link_directories(${root_dir})
        endif()

        # 如果 RELATIVE 标志位被设定,将返回指定路径的相对路径
        file (GLOB ALL_SUB RELATIVE ${root_dir} ${root_dir}/*)
        foreach(sub ${ALL_SUB})
                if (IS_DIRECTORY ${root_dir}/${sub} AND (NOT ("${sub}" STREQUAL ".svn")))
                        link_sub_directories_recursively(${root_dir}/${sub})
                endif()
        endforeach()
endfunction()

link_sub_directories_recursively(${PROJECT_SOURCE_DIR}/depend)

最后记得将libglfw3.a的静态库链接到工程中

target_link_libraries(${PROJECT_NAME}
        glfw3
        pthread
        ${OPENGL_LIBRARIES}
        ${COCOA_LIBS}
        ${COREVIDEO_LIBS}
        ${IOKIT_LIBS})

为了测试环境是否生效,使用网络上一位大神写的代码来进行测试,直接将以下代码拷贝到main.cpp文件

#include <iostream>
#include "glad/glad.h"
#include "glfw3.h"

using namespace std;

void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow *window);

// settings
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;

int main()
{
    // glfw: initialize and configure
    // ------------------------------
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

    #ifdef __APPLE__
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
    #endif

    // glfw window creation
    // --------------------
    GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
    if (window == NULL)
    {
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(window);
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

    // glad: load all OpenGL function pointers
    // ---------------------------------------
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cout << "Failed to initialize GLAD" << std::endl;
        return -1;
    }

    // render loop
    // -----------
    while (!glfwWindowShouldClose(window))
    {
        // input
        // -----
        processInput(window);

        // render
        // ------
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        // glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
        // -------------------------------------------------------------------------------
        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    // glfw: terminate, clearing all previously allocated GLFW resources.
    // ------------------------------------------------------------------
    glfwTerminate();
    return 0;
}

// process all input: query GLFW whether relevant keys are pressed/released this frame and react accordingly
// ---------------------------------------------------------------------------------------------------------
void processInput(GLFWwindow *window)
{
    if(glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
        glfwSetWindowShouldClose(window, true);
}

// glfw: whenever the window size changed (by OS or user resize) this callback function executes
// ---------------------------------------------------------------------------------------------
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
    // make sure the viewport matches the new window dimensions; note that width and
    // height will be significantly larger than specified on retina displays.
    glViewport(0, 0, width, height);
}

不出意外的话,编译运行之后,可以看到如下所示的效果

六、概括总结

至此,基于mac系统,通过cmake搭建OpenGL环境已经完成。我们再来梳理下,首先通过q tcreator创建工程,接着构建glfw和glad, 然后添加glfw、glad、opengl的头文件搜索路径,最后链接glfw库和opengl的相关库。最后一点需要注意下,就是要确定glad.c文件编译进工程,否则可能会出现奇怪的编译错误提示信息。

基于实例快速理解Qt模型视图的应用


Qt的开发过程中,经常会应用到它的模型视图结构,对于初学者来说,马上理解其原理,并且进行基础的应用,还是比较难的。另外,通过网络搜索查看别人发表有关模型视图的介绍,基本上都不能解决个人的一些疑惑。基于此,本文将结合实例来说明模型设计的应用,以便能够更加深刻的理解Qt模型视图的设计原理。

任何知识,如果想要能够应用自如,个人觉得首先需要对该知识,需要有一个整体的认识。所以,首先将对模型视图结构进行总体的说明,然后再结合实例来说明其应用,并且重点介绍代理的两种应用方式,最后再进行总结。

一、模型视图简介

Qt的模型视图与MVC设计模式类似,但是又不相同,MVC设计模式包括模型、视图和控制。模型表示数据,视图表示用户界面,控制定义了用户的操作。它能够有效将数据和显示分离,提高了代码的灵活性。Qt的模型视图同样有这样的效果。但是Qt的模型视图将视图和控制放在一起,以便简化框架。另外,为了能够更好的处理用户输入,Qt的模型视图加入的代理。通过代理能够自定义item的显示和编辑。

Qt的模型视图结构如下图所示,主要包含三个部分,分别为模型、视图和代理。模式与数据源通信,给其他部件提供接口。视图从模型中通过ModelIndex获取信息,代理用来显示和编辑item。

二、模型

模型的抽象基类为QAbstractItemModel,  下图显示了其继承关系。

QAbstractTableModel是表格的抽象基类,QAbstractListMode的列表的抽象基类。QAbstractProxyModel是代理的抽象基类。QDirModel是文件和目录的模型。QSqlQueryModel是有关数据库的模型。

模型的数据可以直接存在模型中,也可以由外部输入,由独立的类管理,还可以存在文件中,甚至是数据库。

模型中角色的功能是,根据不同的角色提供不同的数据。支持以下几种角色。

1、Qt::DisplayRole,   显示文字

2、Qt::DecorationRole,绘制数据

3、Qt::EditRole,  编辑器中的数据

4、Qt::ToolTipRole, 工具提示

5、Qt::StatusTipRole, 状态栏提示

6、Qt::SizeHintRole, 尺寸提示

7、Qt::FontRole, 字体

8、Qt::TextAlignmentRole,  对齐方式

9、Qt::BackgroundRole, 背景画刷

10、Qt::ForegroundRole, 前景画刷

11、Qt::CheckStateRole, 检查框状态

12、Qt::UseRole, 用户自定义数据的起始位置

三、视图

视图的抽象基类为QAbstractItemView,  它由五个基本视图类组成,分别是QTreeeView、QHeaderView、QListView、QColumnView和QTableVie w。为了用户简单方便的使用,还提供了三个模式视图集成的类,分别是QTreeWidget、QListWidget、QTableWidget。对于变动不大,并且简单的需求可以采用这三个类来快速开发。但是对于变动比较大的需求,就不建议使用这三个类,因为它们缺乏灵活性。

四、代理

代理的抽象基类为QAbstractItemDelegate,  如果需要改变item的显示或者item的编辑行为,那么可以考虑自定义代理类。

一般自定义代理类是继承QItemDelegate可以满足大部分的需求,如果直接继承QAbstractItemDelegate,则需要更多的开发工作量。

如果想要改变item的显示,那么可以通过继承QItemDelegate,然后重载paint。

如果想要改变item的编辑行为,同样的可以继承QItemDelegate,然后重载createEditor、setEditorData、setModelData和updateEditorGeometry。

下面的实例将详细介绍代理的这两种应用方式。

五、实例

首先实现模型QAbstractTableModel和表格QtableView的结合显示数据信息的实例。为了代码的清晰度,这里model直接存储了数据。

JWeaponModel模型的定义如下所示,rowCount返回行数,columnCount返回列数,data实现返回item的数据,headerData则是实现返回标题信息。flags和setData函数是为了支持代理而添加的,后面会讲解其作用。这里可以暂时不需要太关注。

class JWeaponModel : public QAbstractTableModel
{
public:
    JWeaponModel(QObject *parent = 0);

    virtual int rowCount(const QModelIndex& parent = QModelIndex()) const;
    virtual int columnCount(const QModelIndex& parent = QModelIndex()) const;
    QVariant data(const QModelIndex &index, int role) const;
    QVariant headerData(int section, Qt::Orientation orentation, int role) const;
    Qt::ItemFlags flags(const QModelIndex &index) const;

    bool setData(const QModelIndex &index, const QVariant &value, int role);

private:
    void InitData();

private:
    QVector<short>          m_army;         // 军种索引
    QVector<short>          m_weapon;       // 武器索引
    QMap<short, QString>    m_MapArmy;      // 军种映射表
    QMap<short, QString>    m_MapWeapon;    // 武器映射表
    QStringList             m_header;
};

JWeaponModel模型的实现如下,columnCount默认写死显示三列。date根据角色Qt::DisplayRole来说显示不同数据条目的数据。headerData返回水平标题信息。

JWeaponModel::JWeaponModel(QObject *parent)
    : QAbstractTableModel(parent)
{
    m_MapArmy[1] = tr("海军");
    m_MapArmy[2] = tr("空军");

    m_MapWeapon[1] = tr("战斗机");
    m_MapWeapon[2] = tr("轰炸机");

    InitData();

}

int JWeaponModel::rowCount(const QModelIndex& parent) const
{
    return m_MapArmy.size();
}

int JWeaponModel::columnCount(const QModelIndex& parent) const
{
    return 3;
}

QVariant JWeaponModel::data(const QModelIndex &index, int role) const
{
    if (!index.isValid())
    {
        return QVariant();
    }

    if (role == Qt::DisplayRole)
    {
        switch (index.column())
        {
            case 0:
            {
                return m_MapArmy[m_army[index.row()]];
            }
            case 1:
            {
                return m_MapWeapon[m_weapon[index.row()]];
            }
            default:
            {
                return QVariant();
            }
        }
    }
    return QVariant();
}

QVariant JWeaponModel::headerData(int section, Qt::Orientation orentation, int role) const
{
    if (role == Qt::DisplayRole && orentation == Qt::Horizontal)
    {
        return m_header[section];
    }

    return QAbstractTableModel::headerData(section, orentation, role);
}

void JWeaponModel::InitData()
{
    m_header << tr("军种")  << tr("种类") << tr("部门") ;
    m_army << 1 << 2;
    m_weapon << 1 << 2;
}

// 需要添加ItemIsEditable属性,否则代理创建的部件显示不出来
Qt::ItemFlags JWeaponModel::flags(const QModelIndex &index) const
{
    return Qt::ItemIsEditable | QAbstractTableModel::flags(index);
}

bool JWeaponModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
    if (role == Qt::EditRole)
    {
            int row = index.row();
            m_MapArmy[row+1] =  value.toString();
            //LOG(INFO) << "row :" << row;
            return true;
    }

    return false;
}

主程序添加如下代码,QTableView设置模式为JWeaponModel。

JWeaponModel *p_weapon_model = new JWeaponModel();
QTableView *p_view = new QTableView();
p_view->setModel(p_weapon_model);
p_view->resize(640, 480);
p_view->show();

编译运行之后的效果如下,显示的数据均来自模型JWeaponModel。

如果想要改变item的编辑行为,支持双击的时候,变成选择框QComboBox,那么考虑使用代码,其定义如下,createEditor创建控件,setEditorData设置控件初始数据,setModelData将编辑数据写入model,  则model需要实现setData(参加JWeaponModel类的实现),这样才能将数据显示到视图。updateEditorGeometry管理控件位置。

#include <QModelIndex>
#include <QVariant>
#include <QItemDelegate>


class JEditorDelegate : public QItemDelegate
{
    Q_OBJECT

public:
    JEditorDelegate(QObject *parent = 0);
    ~JEditorDelegate() override;

    QWidget *createEditor(QWidget *parent,
                          const QStyleOptionViewItem &option,
                          const QModelIndex &index) const override;

    void setEditorData(QWidget *editor, const QModelIndex &index) const override;
    void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override;

    void updateEditorGeometry(QWidget *editor,
                              const QStyleOptionViewItem &option,
                              const QModelIndex &index) const override;
};

JEditorDelegate对应的实现如下,这里只针对表格第一列进行处理。

JEditorDelegate::JEditorDelegate(QObject *parent)
    : QItemDelegate(parent)
{

}

JEditorDelegate::~JEditorDelegate()
{

}

QWidget *JEditorDelegate::createEditor(QWidget *parent,
                      const QStyleOptionViewItem &option,
                      const QModelIndex &index) const
{
    if (index.column() == 0)
    {
        QComboBox *p_editor = new QComboBox(parent);
        p_editor->addItem(tr("one"));
        p_editor->addItem(tr("two"));
        return p_editor;
    }
    else
    {
        return QItemDelegate::createEditor(parent, option, index);
    }
}

void JEditorDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const
{
    if (index.column() == 0)
    {
        QComboBox *p_combobox = qobject_cast<QComboBox*>(editor);
        if(p_combobox)
        {
            int i = p_combobox->findText("one");
            p_combobox->setCurrentIndex(i);
        }
    }
    else
    {
        return QItemDelegate::setEditorData(editor, index);
    }
}

void JEditorDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const
{
    if (index.column() == 0)
    {
        QComboBox *p_combobox = qobject_cast<QComboBox*>(editor);
        if(p_combobox)
        {
            model->setData(index, p_combobox->currentText());
        }
    }
    else
    {
        return QItemDelegate::setModelData(editor, model, index);
    }
}

void JEditorDelegate::updateEditorGeometry(QWidget *editor,
                          const QStyleOptionViewItem &option,
                          const QModelIndex &index) const
{
    (void)index;
    editor->setGeometry(option.rect);
}

主程序添加代理,进行如下所示的修改,调用QTableView的setItemDelegate来添加代理。

JWeaponModel *p_weapon_model = new JWeaponModel();
QTableView *p_view = new QTableView();
p_view->setModel(p_weapon_model);
p_view->setItemDelegate(new JEditorDelegate());
p_view->resize(640, 480);
p_view->show();

编译运行之后,双击第一列第一行的item, 则出现如下的效果

如果想要改变item的显示,那么也是通过代理的方式来支持,其代理类定义如下,继承QItemDelegate,并重载paint。

#include <QModelIndex>
#include <QItemDelegate>

class JArrowDelegate : public QItemDelegate
{
Q_OBJECT

public:
   JArrowDelegate(QObject* parent = 0);
   virtual void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const;
};

对应的实现如下,paint实现再item中画一个黑色的竖线。

JArrowDelegate::JArrowDelegate(QObject *parent)
   : QItemDelegate(parent)
{
}

void JArrowDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const
{
    (void)index;
    //int i_row = index.row();
    int i_x = option.rect.x();
    int i_y = option.rect.y();
    int i_width = option.rect.width();
    int i_height = option.rect.height();
    QPen pen;
    pen.setWidth(2);
    pen.setColor(QColor(Qt::black));
    QStyleOptionViewItem my_option = option;

    QPainterPath path;
    QPoint point_one;
    QPoint point_two;
    point_one.setX(i_x + i_width / 2);
    point_one.setY(i_y);
    point_two.setX(i_x + i_width / 2);
    point_two.setY(i_y + i_height);

    path.moveTo(point_one);
    path.lineTo(point_two);

    painter->setPen(pen);
    painter->drawPath(path);
}

主程序添加代理JArrowDelegate,调用QTableView的接口setItemDelegateForColumn只对表格的第三列添加代理。

JWeaponModel *p_weapon_model = new JWeaponModel();
QTableView *p_view = new QTableView();
p_view->setModel(p_weapon_model);
p_view->setItemDelegate(new JEditorDelegate());
p_view->setItemDelegateForColumn(2, new JArrowDelegate());
p_view->resize(640, 480);
p_view->show();

编译运行效果如下,第三列显示了一条竖线。

六、总结

QT模型视图的原理以及使用说明的讲解就到这里。本文先说明了模型视图的结构,然后再依次说明模型、视图和代理的类层次结构。最后结合具体实例来说明其应用方式,从中可以看出模型提供数据给视图显示,而代理则可以改变数据条目的显示,并通知视图,代理还可以改变数据条目的编辑行为,并通知模型。

json其实不难,只需了解一点,就能轻松玩转它

工作过程中,经常需要使用json这种轻量级的数据交换格式,例如,通过它可以组织数据保存到配置文件,客户端和服务端通过json格式来进行通信等,但是,针对特定的需求场景,需要设计怎样的json格式比较合适呢,json到底可以支持多少种格式呢,有没有一种简单的概括,就能让使用者轻松使用它呢!

一般知识都有基本的理论结构,所以,本文首先将说明json的基本知识点,然后基于开源软件jsoncpp来说明如何构建/读写json,   再分享个人的使用心得,最后再总结json的关键要点,理解了这一点,玩转json不是问题。

一、Json简介

Json是轻量级的数据交换格式,它便于阅读和编写,它是完全独立于程序语言的文本格式。

二、Json结构

Json有两个结构, 分别是“名称/值”对的集合和值的有序列表。“名称/值”对的集合可以简单理解为对象集合,而值的有序列表可以理解为数组。

这里举一个“名称/值”对的集合的例子,它是以左大括号开始,以右大括号结束,中间是由多个“名称/值”对组成,各个“名称/值”对之间用逗号隔开。

{
    "cpu_name" : "special",
    "cpu_temp" : 40
}

举一个“值的有序列表”的例子,它是以左中括号开始,以右中括号结束,中间是由多个值组成,各个值之间用逗号隔开。

["apple", "pear", "banana "]

三、Json形式

Json主要由三种形式,分别为对象(object),  数组(array),  值(value)。

对象(object)是“名称/值”对集合,名称于值之间通过冒号隔开,另外对象是以左大括号开始,以右大括号结束。

数组(array)是值的有序集合,它是以左中括号开始,以右中括号结束。

值(value)可以是字符串(string)、数值(number)、对象(object)、数组(array)、true、false、null。这里我们会发现对象(object)里面有值(value),  数组(array)里面也有值(value),  而值(value)又包含有对象和数组,所以它们是可以嵌套的。

Json就是由上面简单的元素来组建复杂的信息元素。

四、Json例子

jsoncpp是C++语言编写的开源json库,通过该库,我们可以很容易的构建、读写json。接下来就基于jsoncpp来解释几个构建、读取json的例子。通过例子可以对json有更深的理解。jsoncpp最基本的对象就是Json::Value。

构建一个最简单的对象,然后输出整个json信息,最后读取json值,先调用isMember判断名称是否为root成员,如果是的话,那么就读取输出。

Json::Value root;
root["result"] = "true";

Json::StyledWriter styled_writer;
std::string str_json = styled_writer.write(root);
LOG(INFO) << "str_json: " << str_json.c_str();

if (root.isMember("result"))
{
    LOG(INFO) << "root[\"result\"] = " << root["result"];
}

输出的日志信息如下所示,大括号包含了一个“名称/值”对。

2020-05-02 17:59:32,670 INFO  [default] str_json: {
   "result" : "true"
}

2020-05-02 17:59:32,670 INFO  [default] root["result"] = "true"

构建嵌套对象,第一个根“名称/值”对中的“值”又是一个对象。

Json::Value root;
Json::Value value;
value["cpu_name"] = "arm";
root["cpu_info"] = value;

Json::StyledWriter styled_writer;
std::string str_json = styled_writer.write(root);
LOG(INFO) << "str_json: " << str_json.c_str();

if (root["cpu_info"].isMember("cpu_name"))
{
    LOG(INFO) << "root[\"cpu_info\"][\"cpu_name\"] = " << root["cpu_info"]["cpu_name"];
}

输出的日志信息如下所示

2020-05-02 17:59:32,670 INFO  [default] str_json: {
   "cpu_info" : {
      "cpu_name" : "arm"
   }
}

2020-05-02 17:59:32,670 INFO  [default] root["cpu_info"]["cpu_name"] = "arm"

构建三层嵌套对象,第一个根“名称/值”对中的“值”是一个对象,而该对象的“值”又是一个对象。依次类推,可以构建更多层的嵌套对象。

Json::Value root;
Json::Value value_01;
Json::Value value_02;

value_02["cell_number"]  = 255;
value_01["eye"] = value_02;
root["body"] = value_01;

Json::StyledWriter styled_writer;
std::string str_json = styled_writer.write(root);
LOG(INFO) << "str_json: " << str_json.c_str();

if (root["body"]["eye"].isMember("cell_number"))
{
    LOG(INFO) << "root[\"body\"][\"eye\"][\"cell_number\"]  = " << root["body"]["eye"]["cell_number"];
}

输出的日志信息如下所示

2020-05-02 17:59:32,670 INFO  [default] str_json: {
   "body" : {
      "eye" : {
         "cell_number" : 255
      }
   }
}

2020-05-02 17:59:32,670 INFO  [default] root["body"]["eye"]["cell_number"]  = 255

构建简单的数组,jsoncpp中构建数组是通过append的接口来创建的。读取数组之前,先调用isArray来判断对象是否为数组,如果是的话,再读取输出。这里需要注意数组的个数。从防御式编程的角度看,读取数组值之前,需要判断数组索引是否在有效范围内。

Json::Value array;
array.append("one");
array.append("two");

Json::StyledWriter styled_writer;
std::string str_json = styled_writer.write(array);
LOG(INFO) << "str_json: " << str_json.c_str();

if (array.isArray())
{
    LOG(INFO) << "array.size(): " << array.size();
    LOG(INFO) << "array[0]: " << array[0];
    LOG(INFO) << "array[1]: " << array[1];
}

输出的日志信息如下所示,从这里我们也可以确定数组是可以单独作为独立json串出现的。之前一直都有一个误区,就是认为json一定要用大括号包括起来。

2020-05-02 17:59:32,670 INFO  [default] str_json: [ "one", "two" ]

2020-05-02 17:59:32,671 INFO  [default] array.size(): 2
2020-05-02 17:59:32,671 INFO  [default] array[0]: "one"
2020-05-02 17:59:32,671 INFO  [default] array[1]: "two"

构建对象和数组组成的json。首先创建一个数组,然后将其作为对象的值。

Json::Value array;
array.append("one");
array.append("two");
array.append("three");

Json::Value root;
root["number"] = array;

Json::StyledWriter styled_writer;
std::string str_json = styled_writer.write(root);
LOG(INFO) << "str_json: " << str_json.c_str();

if (root["number"].isArray())
{
    LOG(INFO) << "root[\"number\"].size(): " << root["number"].size();
    LOG(INFO) << "root[\"number\"][0]: " << root["number"][0];
    LOG(INFO) << "root[\"number\"][1]: " << root["number"][1];
    LOG(INFO) << "root[\"number\"][2]: " << root["number"][2];
}

输出的日志信息如下所示

2020-05-02 17:59:32,671 INFO  [default] str_json: {
   "number" : [ "one", "two", "three" ]
}

2020-05-02 17:59:32,671 INFO  [default] root["number"].size(): 3
2020-05-02 17:59:32,671 INFO  [default] root["number"][0]: "one"
2020-05-02 17:59:32,671 INFO  [default] root["number"][1]: "two"
2020-05-02 17:59:32,671 INFO  [default] root["number"][2]: "three"

最后再构建稍微复杂一点的json串,它是由对象、数组、对象来组成的,即对象的值是一个数组,而数组内部的值是由对象组成。

Json::Value root;
Json::Value array;
Json::Value value_01;
Json::Value value_02;

value_01["peripheral"] = 1;
value_01["patient"] = 2;

value_02["image"] = 3;
value_02["auto"] = 4;

array.append(value_01);
array.append(value_02);

root["department"] = array;

Json::StyledWriter styled_writer;
std::string str_json = styled_writer.write(root);
LOG(INFO) << "str_json: " << str_json.c_str();

if (root["department"].isArray())
{
    LOG(INFO) << "root[\"department\"].size(): " << root["department"].size();
    LOG(INFO) << "root[\"department\"][0][\"patient\"]: " << root["department"][0]["patient"];
    LOG(INFO) << "root[\"department\"][1][\"auto\"]: " << root["department"][1]["auto"];
}

输出的日志信息如下所示

2020-05-02 17:59:32,671 INFO  [default] str_json: {
   "department" : [
      {
         "patient" : 2,
         "peripheral" : 1
      },
      {
         "auto" : 4,
         "image" : 3
      }
   ]
}

2020-05-02 17:59:32,671 INFO  [default] root["department"].size(): 2
2020-05-02 17:59:32,671 INFO  [default] root["department"][0]["patient"]: 2
2020-05-02 17:59:32,671 INFO  [default] root["department"][1]["auto"]: 4

五、使用心得

  1. 读取json值之前,先判断其有效性,可以结合断言机制,调用isMember或者isArray来进行判断。
  2. 使用数组的时候,需要特别注意数组下标。

六、总结

json主要是由对象或数组创建而成,而它们的嵌套使用就可以创建复杂的json串,根据特定场景的需求来创建适用的json格式。

是不是觉得Makefile很繁琐,一个cmake文件就可以解决

linux编译程序的时候,通常是使用Makefile文件来进行编译,这个是能够提高效率的,但是对于大中型的项目,每个文件夹下都需要创建Makefile,并且改变项目目录结构都需要调整Makefile文件,如果是小型项目的话,那花费的时间还是能够接受的,但是大中型项目要调整目录结构,这个工作量还是不小的。所以,我们可以通过使用cmake来解决这样的问题。

本文首先简单介绍什么是cmake,它可以用来干什么,接着给出一个简单的例子,让初学者对cmake有一个大致的了解,然后抛出cmake文件,并针对该cmake文件进行详细的解释,最后再进行总结,并分享个人的学习心得。

一、什么是cmake

cmake一款跨平台的编译工具,  它的全称是cross platform make,注意这里的make不是指linux下的make,使用它构建的工程,既可以生成linux下的makefile,也可以生成Mac系统的xcode的工程文件,还能够生成window的projects等。cmake并不会生成最终的软件程序,它只是生成标准的建构档,例如linux的Makefile文件。简单来说,cmake可以生产不同平台的建构档,然后再由建构档来生成最终的软件程序。

cmake组织档的取名都为CMakeLists.txt, 现在许多开源软件都采用cmake来组织代码,可见其用处还是很大的,学习了解cmake对于学习开源软件是有很大的帮助的。

二、入门例子

首先电脑上需要安装cmake软件,具体下载安装方法,可以网络搜索,这不是本文的主题,所以不进行说明。

linux上安装成功之后,可以执行命令cmake –version来查看当前的cmake版本

创建一个main.cpp文件,其内容如下所示,打印一句字符信息。

#include <stdio.h>

int main(int argc, char **argv)
{
    printf("this is the first cmake project.\n");
    return 0;
}

接着在同级目录下创建cmake文件 CMakeLists.txt

# CMake 最低版本号要求
cmake_minimum_required (VERSION 2.8)

# 项目信息,项目名称
project (EXAMPLE_01)

# 指定生成目标
add_executable(example_01 main.cpp)

为了代码干净,同级目录下创建build目录,进入build目录,执行“cmake ../”命令来生成Makefile文件,接着执行命令make编译,最后build目录下生成二进制文件example_01,  执行程序可以输出打印信息。

build目录下生成的文件内容如下,Makefile是生成的建构档,example_01是生成的可执行二进制程序。

三、cmake代替Makefile

上面只是cmake的一个简单的入门例子,还不能明显看出cmake的强大,对于中大型的项目来看,cmake的作用就比较明显,特别是相同代码不同平台的编译。

下面给出本章节将要详细解释说明的cmake文件,可以先熟悉下整体的流程,看不明白没有关系,后面将针对该文件进行详细的解释说明。

# CMake 最低版本号要求
cmake_minimum_required (VERSION 2.8.7 FATAL_ERROR)

# 项目信息,项目名称
project (EXAMPLE_02)

# CMAKE_INCLUDE_CURRENT_DIR 自动增加CMAKE_CURRENT_SOURCE_DIR和CMAKE_CURRENT_BINARY_DIR到每个目录的include路径
set(CMAKE_INCLUDE_CURRENT_DIR ON)
message("CMAKE_CURRENT_SOURCE_DIR : ${CMAKE_CURRENT_SOURCE_DIR}")
message("CMAKE_CURRENT_BINARY_DIR : ${CMAKE_CURRENT_BINARY_DIR}")
message("PROJECT_SOURCE_DIR : ${PROJECT_SOURCE_DIR}")

set(CROSS_TOOLCHAIN_PREFIX "")
set(CMAKE_C_COMPILER ${CROSS_TOOLCHAIN_PREFIX}gcc)
set(CMAKE_CXX_COMPILER ${CROSS_TOOLCHAIN_PREFIX}g++)
set(CROSS_OBJCOPY ${CROSS_TOOLCHAIN_PREFIX}objcopy)
set(CROSS_STRIP ${CROSS_TOOLCHAIN_PREFIX}strip)

set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin/linux)

# find_package()命令是用来查找依赖包
find_package(PkgConfig)

# export cmd 
message(STATUS “HOME dir: $ENV{HOME}”)
message(STATUS “LANG: $ENV{LANG}”)

# find_program查找可执行程序
find_program(CCACHE_FOUND ccache)
if(CCACHE_FOUND)
    message("found ccache")
    set_property(GLOBAL PROPERTH RULE_LAUNCH_COMPILE ccache)
    set_property(GLOBAL PROPERTH RULE_LAUNCH_LINK ccache)	
else()
    message("no found ccache")	
endif(CCACHE_FOUND)

if($ENV{DEBUG} MATCHES 1)
    message("debug build")
    set(CMAKE_BUILD_TYPE Debug)
else()
    message("release build")
    set(CMAKE_BUILD_TYPE RelWithDebInfo)
endif()

# 头文件的搜索路径
function(include_sub_directories_recursively root_dir)
    if (IS_DIRECTORY ${root_dir})
        include_directories(${root_dir})
    endif()

    file (GLOB ALL_SUB RELATIVE ${root_dir} ${root_dir}/*)
    foreach(sub ${ALL_SUB})
        if (IS_DIRECTORY ${root_dir}/${sub} AND (NOT ("${sub}" STREQUAL ".svn")))
            include_sub_directories_recursively(${root_dir}/${sub}) 
        endif()
    endforeach()	
endfunction()	

include_directories(
    ${CMAKE_CURRENT_BINARY_DIR}
    ${PROJECT_SOURCE_DIR}
)

set(CMAKE_CXX_FLAGS_MY "-pipe -march=armv7-a -mfpu=neon -DLINUX=1 -DEGL_API_FB=1 -mfloat-abi=hard -O2 -std=c++11 -Wall -W -D_REENTRANT -fPIC -Wformat -Werror")
set(CMAKE_C_FLAGS "-pipe -march=armv7-a -mfpu=neon -DLINUX=1 -DEGL_API_FB=1 -mfloat-abi=hard -O2 -Wall -W -D_REENTRANT -fPIE -Werror")
set(CMAKE_CXX_FLAGS "")

# 指定查找库的目录
function(link_sub_directories_recursively root_dir)
    if (IS_DIRECTORY ${root_dir})
        link_directories(${root_dir})
    endif()

    file (GLOB ALL_SUB RELATIVE ${root_dir} ${root_dir}/*)
    foreach(sub ${ALL_SUB})
        if (IS_DIRECTORY ${root_dir}/${sub} AND (NOT ("${sub}" STREQUAL ".svn")))
            link_sub_directories_recursively(${root_dir}/${sub}) 
        endif()
    endforeach()	
endfunction()

# 遍历匹配目录的所有子目录并匹配文件
file(GLOB_RECURSE SRC_FILES ${PROJECT_SOURCE_DIR}/*.cpp)

# 指定生成目标
add_executable(example_02 ${SRC_FILES})

# 标示链接的库
target_link_libraries(example_02
    dl
    pthread
    m)

if (CMAKE_BUILD_TYPE MATCHES Rel)
    add_custom_command(TARGET example_02
        POST_BUILD
        COMMAND mkdir -p ${EXECUTABLE_OUTPUT_PATH}/release	
        )
else()
    add_custom_command(TARGET example_02
        POST_BUILD
        COMMAND mkdir -p ${EXECUTABLE_OUTPUT_PATH}/debug	
        )
endif()

cmake_minimum_required指定cmake最低版本号要求,FATAL_ERROR 表示当发生警告时,用错误方式提示

make_minimum_required(VERSION <min>[...<max>] [FATAL_ERROR])

CMAKE_INCLUDE_CURRENT_DIR 自动增加CMAKE_CURRENT_SOURCE_DIR和CMAKE_CURRENT_BINARY_DIR到每个目录的include路径。CMAKE_INCLUDE_CURRENT_DIR默认是关闭的。

当前测试工程的目录结构如下:

02_exapmle
    |-- build
    |-- src
        |-- CMakeLists.txt
        |-- main.cpp

message可以打印输出变量信息, CMAKE_CURRENT_SOURCE_DIR、CMAKE_CURRENT_BINARY_DIR、PROJECT_SOURCE_DIR是cmake内置变量

message("CMAKE_CURRENT_SOURCE_DIR : ${CMAKE_CURRENT_SOURCE_DIR}")
message("CMAKE_CURRENT_BINARY_DIR : ${CMAKE_CURRENT_BINARY_DIR}")
message("PROJECT_SOURCE_DIR : ${PROJECT_SOURCE_DIR}")

运行后输出打印的信息, CMAKE_CURRENT_SOURCE_DIR表示当前正在处理的源代码目录,CMAKE_CURRENT_BINARY_DIR表示当前正在处理的二进制目录,PROJECT_SOURCE_DIR表示当前工程的顶层源代码目录

CMAKE_CURRENT_SOURCE_DIR : .../02_example/src
CMAKE_CURRENT_BINARY_DIR : .../02_example/build
PROJECT_SOURCE_DIR : .../02_example/src

EXECUTABLE_OUTPUT_PATH表示可执行文件输出路径

find_package()命令是用来查找依赖包, Pkg-Config维护它依赖库路径、头文件路径、编译选项、链接选项等信息。

关键字ENV查看的是当前环境变量,linux上的环境变量可以通过export命令来查看。

message(STATUS “LANG: $ENV{LANG}”)对应输出的内容为:

-- “LANG:zh_CN.UTF-8”

find_program查找可执行程序。一个名为<VAR>的cache条目会被创建用来存储该命令的结果。如果该程序被找到了,结果会存储在该变量CCACHE_FOUND

find_program(<VAR> name [path1 path2 ...])

set_property给定范围内设置一个命名属性,GLOBAL域是唯一的,PROPERTY后面紧跟着要设置的属性的名字。

set_property(<GLOBAL                      |
              DIRECTORY [<dir>]           |
              TARGET    [<target1> ...]   |
              SOURCE    [<src1> ...]      |
              INSTALL   [<file1> ...]     |
              TEST      [<test1> ...]     |
              CACHE     [<entry1> ...]    >
             [APPEND] [APPEND_STRING]
             PROPERTY <name> [value1 ...])

function,定义函数name, 并且参数为<arg1> … , 函数只有在调用的时候才起作用。

function(<name> [<arg1> ...])
 <commands>
endfunction()

include_directories包含头文件的搜索路径。

link_directories指定查找库的目录。

target_link_libraries标示链接的库。<target>必须时 add_executable() or add_library() 命令创建。<item>则是链接的库

target_link_libraries(<target> ... <item>... ...)

file产生一个匹配 <globbing-expressions> 的文件列表并将它存储到变量 <variable> 中,果 RELATIVE 标志位被设定,将返回指定路径的相对路径。file的第一个参数设置为GLOB_RECURSE,则表示遍历匹配目录的所有子目录并匹配文件。

file(GLOB <variable>
[LIST_DIRECTORIES true|false] [RELATIVE <path>] [CONFIGURE_DEPENDS]
[<globbing-expressions>...])

add_custom_command,定义一个跟指定目标target关联的新的命令,命令何时执行取决于PRE_BUILD | PRE_LINK | POST_BUILD这三个参数。

add_custom_command(TARGET target
                   PRE_BUILD | PRE_LINK | POST_BUILD
                   COMMAND command1 [ARGS] [args1...]
                   [COMMAND command2 [ARGS] [args2...] ...]
                   [WORKING_DIRECTORY dir]
                   [COMMENT comment] [VERBATIM])
PRE_BUILD - 所有其他依赖项之前运行
PRE_LINK - 其他依赖项之后运行
POST_BUILD - 目标建立之后运行

四、总结

创建cmake文件的过程,首先当然是先创建CMakeLists.txt文件,接着声明cmake的版本要求,然后设置项目信息,再根据具体场景设置相关属性以及生成的可执行目标。

上面讲解的例子中,主要涉及到cmake的几个知识点。具体如下:

  1. cmake的变量:内置变量、环境变量以及自定义变量。
  2. 查找命令:find_package、find_program
  3. 定义函数:function
  4. 查找文件:file
  5. 搜索路径:include_directories、link_directories、target_link_libraries
  6. 自定义命令:add_custom_command
  7. 设置目标: add_executable

五、学习心得

学习cmake也有一段时间,网络上也搜索了很多信息,但是总感觉说的不够明白和全面。最后发现要想全面的了解cmake,  最有效的方法就是直接查看cmake的官方文档。虽然是英文,但是只要耐心认真阅读,就会发现里面讲的很全面。然后再结合具体的例子进行消化理解就可以。而对于cmake的变量的含义,除了查看cmake官方文档之后,还可以通过message直接打印出变量信息来加深理解其含义。

基于面向对象的思想来使用结构体,将会有意想不到的效果

程序开发过程中,很多人都会接触到客户服务端模型,通常客户服务端模型是基于socket的网络通信,而网络通信是需要定义通信协议,通信协议结构一般是用结构体的方式来表示,而数据内容有的可能会使用json格式,对于嵌入式设备,数据内容更多的还是采用结构体的方式来表示。

本文首先会基于Qt提供的socket接口来实现一个简单的客户服务端模型,主要是为后面数据内容采用结构体通信的说明提供基础。接着定义通信协议结构体,然后再说明C语言方式使用结构体的方法,再介绍基于面向对象的思想来使用结构体,从而体会两者方式之间的区别,最后再介绍如何采用模版方式来更好的获取结构格式不定的数据内容。

一、客户服务端模式

客户服务端模式的机制是,服务端启动监听端口来等待客户端的连接,客户端创建socket启动连接,服务端成功接收到连接之后,等待客户端发送数据,客户端开始发送数据,服务端接收到数据,并进行解析处理。

下面会基于QT提供的socket接口来实现简单的客户服务端模型,实现之前需要在pro文件中添加network库的支持。

QT       += core gui network

1、定义实现简单的服务端类JTcpServer, 首先构造函数创建QTcpServer对象用来启动监听等待新的连接,当有新的连接请求的时候,则通过QTcpServer提供的接口nextPendingConnection来返回连接成功的socket, 然后等待客户端发送数据,如果有可读数据,那么读取数据进行处理。

// 定义服务端类
class JTcpServer : public QObject
{
    Q_OBJECT
public:
    JTcpServer();
    ~JTcpServer();

    void Start();

public slots:
    void AcceptConnection();
    void ReadClient();

private:
    QTcpServer* m_pTcpServer;
    QTcpSocket *m_pClientConnection;
};

// 实现服务端类
JTcpServer::JTcpServer()
    : QObject(nullptr)
{
    LOG(INFO) << " contructor";
    m_pTcpServer = new QTcpServer(this);
}

JTcpServer::~JTcpServer()
{
    LOG(INFO) << " decontructor";
    m_pTcpServer->close();
}

void JTcpServer::AcceptConnection()
{
    LOG(INFO) << "receive new connection";
    m_pClientConnection = m_pTcpServer->nextPendingConnection();
    if (m_pClientConnection->waitForReadyRead())
    {
        ReadClient();
    }
}

void JTcpServer::ReadClient()
{
    QString str = m_pClientConnection->readAll();
    LOG(INFO) << "str: " << str.toStdString().c_str();
}

void JTcpServer::Start()
{
    LOG(INFO) << "start tcp server";

    m_pTcpServer->listen(QHostAddress::Any, 9999);
    if (m_pTcpServer->waitForNewConnection(500000))
    {
        AcceptConnection();
    }

    LOG(INFO) << "end tcp server";
}

2、定义实现简单的客户端类,构造函数创建QTcpSocket用来连接服务端,并且发送数据。

// 定义客户端类
class JTcpClient : public QObject
{
    Q_OBJECT
public:
    JTcpClient();
    ~JTcpClient();

    void Start();

private:
    QTcpSocket* m_pclientSocket;
};

// 实现服务端类
JTcpClient::JTcpClient()
    : QObject(nullptr)
{
    LOG(INFO) << " contructor";
    m_pclientSocket = new QTcpSocket(this);
}

JTcpClient::~JTcpClient()
{
    LOG(INFO) << " decontructor";
    m_pclientSocket->close();
}

void JTcpClient::Start()
{
    LOG(INFO) << "start tcp client";

    m_pclientSocket->connectToHost(QHostAddress("127.0.0.1"), 9999);
    char ac_data[512] = {0};
    std::memcpy(ac_data, "hello everyone!", sizeof("hello everyone!"));
    m_pclientSocket->write(ac_data);
    m_pclientSocket->waitForBytesWritten();

    LOG(INFO) << "end tcp client";
}

3、完成客户端和服务端的实现,启动两个分离线程来分别执行客户端和服务端代码

// 启动服务端
std::thread thread_server( [&]{
    JTcpServer *p_tcp_server = new JTcpServer();
    p_tcp_server->Start();
} ) ;

std::this_thread::sleep_for(std::chrono::seconds(1));

// 启动客户端
std::thread thread_client( [&]{
    JTcpClient *p_tcp_client = new JTcpClient();
    p_tcp_client->Start();
} ) ;

if (thread_server.joinable())
{
    thread_server.detach();
}

if (thread_client.joinable())
{
    thread_client.detach();
}

4、启动运行之后,可以看到服务端成功打印了客户端发送的数据,这说明客户端和服务端之间是能够通信的。

[void JTcpServer::ReadClient():60] str: hello everyone!

二、通信协议

定义客户端和服务端的通信协议,它包括帧号,该帧号具有唯一性;帧类型,根据具体业务场景进行定义,比如命令帧、结果帧等;帧的来源表示帧的发送者; 帧的目的表示帧的接受者;数据帧长度则存储数据内容的具体长度; 数据则存放不定长度的数据内容。

三、基于C语言方式的结构体

基于C语言方式定义通信协议的结构体

typedef struct CFrame
{
    int iId;
    int iType;
    int iFrom;
    int iTo;
    int iDataLen;
    char data[0];
}CFRAME;

再定义数据内容的结构体

typedef struct CParam
{
    int iParam;
    char acInfo[32];
}CPARAM;

客户端构建数据,并发送。首先malloc申请内存,然后填充帧头和数据内容,最后发送数据,再free释放内存。

CFRAME *p_frame = nullptr;
p_frame = (CFRAME *)malloc(sizeof(CFRAME) + sizeof(CPARAM));
memset(p_frame, 0x00, sizeof(CFRAME) + sizeof(CPARAM));
p_frame->iId = 111;
p_frame->iType = 3;
p_frame->iFrom = 1;
p_frame->iTo = 2;
p_frame->iDataLen = sizeof(CPARAM);
CPARAM param;
param.iParam = 400;
memcpy(param.acInfo, "happy.", sizeof("happy."));
memcpy(p_frame->data,  &param, sizeof(CPARAM));

int i_write_len = m_pclientSocket->write((char *)p_frame, sizeof(CFRAME) + sizeof(CPARAM));
LOG(INFO) << "i_write_len: " << i_write_len;
m_pclientSocket->waitForBytesWritten();

free(p_frame);

服务端接收数据,并解析。服务端接收全部数据,然后解析并打印出来。由于TCP是流的方式,一般来说,先解析帧头,再解析帧数据,但是这里暂时不需要关注,所以没有考虑。

QByteArray array = m_pClientConnection->readAll();
LOG(INFO) << "array.size(): " << array.size();

CFRAME *p_frame = reinterpret_cast<CFRAME *>(array.data());
if (!p_frame)
{
    LOG(INFO) << "p_frame nullptr";
}

LOG(INFO) << "p_frame->iId: " << p_frame->iId;
LOG(INFO) << "p_frame->iType: " << p_frame->iType;
LOG(INFO) << "p_frame->iFrom: " << p_frame->iFrom;
LOG(INFO) << "p_frame->iTo: " << p_frame->iTo;
LOG(INFO) << "p_frame->iDataLen: " << p_frame->iDataLen;

CPARAM *p_param = reinterpret_cast<CPARAM *>(p_frame->data);
if (p_param)
{
    LOG(INFO) << "p_param->iParam: " << p_param->iParam;
    LOG(INFO) << "p_param->acInfo: " << p_param->acInfo;
}

四、基于面向对象的结构体

上面的方式是基于基于C语言的方式来使用结构体,接下来就来说明如何基于面向对象的方式来使用结构体。

基于面向对象方式定义通信协议的结构体

struct CXXFrame
{
    int iId;
    int iType;
    int iFrom;
    int iTo;
    int iDataLen;
    char data[0];
    CXXFrame()
        : iId(1)
        , iType(0)
        , iFrom(1)
        , iTo(2)
        , iDataLen(0)
    {}
};

基于面向对象方式定义数据内容结构体

struct JParam
{
    int iParam;
    char acInfo[32];
    JParam()
        : iParam(1)
    {
        memset(acInfo, 0x00, sizeof(acInfo));
    }
};

构建客户端数据,并且发送数据。为了避免申请内存而忘记释放,这里使用std::vector来定义数组来存储发送的数据,这样就避免忘记释放内存。

CXXFrame *p_frame = nullptr;
int i_frame_len = sizeof(CXXFrame) + sizeof(JParam);
std::vector<char> vec_frame(i_frame_len, 0);
p_frame =  reinterpret_cast<CXXFrame *>(vec_frame.data());
p_frame->iId = 111;
p_frame->iType = 3;
p_frame->iFrom = 1;
p_frame->iTo = 2;
p_frame->iDataLen = sizeof(JParam);
JParam *p_param = reinterpret_cast<JParam *>(p_frame->data);
p_param->iParam = 400;
memcpy(p_param->acInfo, "happy!", sizeof("happy!"));
m_pclientSocket->write(vec_frame.data(), vec_frame.size());
m_pclientSocket->waitForBytesWritten();

服务端接收数据,并且解析数据。

QByteArray array = m_pClientConnection->readAll();
LOG(INFO) << "array.size(): " << array.size();

CXXFrame *p_frame = reinterpret_cast<CXXFrame *>(array.data());
if (!p_frame)
{
    LOG(INFO) << "p_frame nullptr";
}

LOG(INFO) << "p_frame->iId: " << p_frame->iId;
LOG(INFO) << "p_frame->iType: " << p_frame->iType;
LOG(INFO) << "p_frame->iFrom: " << p_frame->iFrom;
LOG(INFO) << "p_frame->iTo: " << p_frame->iTo;
LOG(INFO) << "p_frame->iDataLen: " << p_frame->iDataLen;

JParam *p_param = reinterpret_cast<JParam *>(p_frame->data);
if (p_param)
{
    LOG(INFO) << "p_param->iParam: " << p_param->iParam;
    LOG(INFO) << "p_param->acInfo: " << p_param->acInfo;
}

五、模版方式获取数据内容

服务器接收数据的内容的长度是不确定,不同的应用场景对应不同的结构体。为了统一、并且简化代码量。可以实现模版方法来获取数据内容。

template<typename T>
T* GetFrameParam(CXXFrame *pFrame)
{
    return reinterpret_cast<T*>(pFrame->data);
}

那么服务端接收数据内容,就可以调用模版方法。

QByteArray array = m_pClientConnection->readAll();
LOG(INFO) << "array.size(): " << array.size();

CXXFrame *p_frame = reinterpret_cast<CXXFrame *>(array.data());
if (!p_frame)
{
    LOG(INFO) << "p_frame nullptr";
}

LOG(INFO) << "p_frame->iId: " << p_frame->iId;
LOG(INFO) << "p_frame->iType: " << p_frame->iType;
LOG(INFO) << "p_frame->iFrom: " << p_frame->iFrom;
LOG(INFO) << "p_frame->iTo: " << p_frame->iTo;
LOG(INFO) << "p_frame->iDataLen: " << p_frame->iDataLen;

//JParam *p_param = reinterpret_cast<JParam *>(p_frame->data);
JParam *p_param = GetFrameParam<JParam>(p_frame);
if (p_param)
{
    LOG(INFO) << "p_param->iParam: " << p_param->iParam;
    LOG(INFO) << "p_param->acInfo: " << p_param->acInfo;
}

六、总结

本文通过实现一个简单的客户服务端模型来说明C语言定义的结构体和面向对象定义的结构体的区别,通过面向对象定义的结构体可以在构造函数中初始化数据成员,这样就不必每次定义对象,还需要使用memset来初始化结构体,同时也避免了忘记初始化结构体的可能性。另外一个,我觉得可以很好简化代码量,并且让代码统一的一个点就是,使用模版方法来解析帧的数据内容。除了解析帧的数据内容之外,还可以通过模版方法来构建帧的数据内容,这样不同结构体,只需要一个模版方法就可以解决。所以,使用面向对象的设计语言的时候,尽量用面向对象的设计思想来开发,很多时候可以简化代码,甚至简化代码的逻辑。

由浅入深,让你全面了解状态机模式的应用原理

工作中应用到了状态机,学习过程中发现,如果状态机使用得当,那么就会事半功倍。中间也陆陆续续学习研究了状态机的相关知识。所以,在这里做个总结,同时也分享出来。

本文首先简单介绍状态机的基本知识(建议找专门专业的介绍状态机的书籍进行学习),然后基于十字转门的例子,以迁移表的方式来实现有限状态机的功能,接着再介绍经典的状态机模式,最后重点介绍boost startchart的相关知识点,boost startchart是boost实现的状态机库,它几乎支持了所有的UML中状态机的特性,主要学习的途径就是官网提供的指南,该指南信息量很大,但是学习起来有点费劲,而且例子也不够完整,所以,本文也会基于它提供的例子,比如hello world、秒表、数码相机,重新梳理总结它的应用方式,至于高级议题,可能需要再花时间进行研究。

一、状态机基本知识

一般状态机由三个元素组成:状态、事件、反应。而反应在boost startchart包括转移、动作等。一个状态可以对应一个或者多个反应。

当前状态收到事件后,执行反应,然后转变为新的状态。该流程会使用下图的方式来表示。

状态机通常都需要有历史状态,可以用来恢复,它分为浅历史和深历史两类。

历史状态是伪状态, 其目的是记住从组合状态中退出时所处的子状态, 当再次进入组合状态时, 可以直接进入这个子状态, 而不是再从组合状态的初态开始。

浅历史状态, 只记住最外层组合状态的历史,使用大写H来表示。
深历史状态, 可以记住任意深度的组合状态的历史,使用大写H和星号组合来表示。

二、迁移表

进出地铁的时候,有时候设置的是一个十字转门,十字转门默认是锁的状态,当投入硬币之后,当前十字转门就会变成解锁状态,当人通过之后,十字转门又会变成锁的状态。当十字转门是锁的状态,但是强行通过,就会发出警告信息。其状态的转换如下图所示。

接下来,我们通过迁移表的方式来说实现上图的状态机图。

首先定义实现动作类接口和实现,即unlock/lock/alarm/thanks。这里定义十字转门的控制接口JTurnstileControlInterface,主要是依据开闭原则,当动作类的功能改变的时候,只需要继承JTurnstileControlInterface接口类,然后重新实现对应的接口函数。

/// 十字转门控制接口
class JTurnstileControlInterface
{
public:
    virtual void lock() = 0;
    virtual void unlock() = 0;
    virtual void thanks() = 0;
    virtual void alarm() = 0;
};

/// 十字转门控制类
class JTurnstileControler : public JTurnstileControlInterface
{
public:
    virtual void lock() override;
    virtual void unlock() override;
    virtual void thanks() override;
    virtual void alarm() override;
};

/// 十字转门控制类的实现
void JTurnstileControler::lock()
{
    LOG(INFO) << "action lock";
}

void JTurnstileControler::unlock()
{
    LOG(INFO) << "action unlock";
}

void JTurnstileControler::thanks()
{
    LOG(INFO) << "action thanks";
}

void JTurnstileControler::alarm()
{
    LOG(INFO) << "action alarm";
}

然后定义状态和事件,LOCKED和UNLOCKED表示的是十字转门的状态,COIN和PASS表示的是十字转门收到的事件。

// 状态
static const int LOCKED = 0;
static const int UNLOCKED = 1;

// 事件
static const int COIN = 0;
static const int PASS = 1;

有了上面的基础之后,最后就可以以迁移表的方式来实现十字转门的状态机图。

定义十字转门类,构造函数接受动作类,event接收事件,Transition是存储状态转移关系的内部类。

class Turnstile
{
public:
     // 传入动作
     Turnstile(JTurnstileControlInterface* pControler);

     // 接受事件
     void event(int event);

private:
    typedef std::function<void ()> Action;

    class Transition
    {
    public:
        Transition(int curState, int event, int newState, Action action)
            : m_curState(curState)
            , m_event(event)
            , m_newState(newState)
            , m_action(action)
        {}

        int m_curState;
        int m_event;
        int m_newState;
        Action m_action;
    };

    // 添加迁移关系到vector向量
    void AddTransition(int curState, int event, int newState, Action action);

private:
    JTurnstileControlInterface* m_pTurnstileConstroler;
    std::vector<Transition*> m_vecTransition;
    int m_iState;
};

实现十字转门类,event是处理接收到事件的函数,该函数会遍历vector向量中存储的状态迁移表,如果匹配到对应的事件,那么修改当前的状态,并且执行对应的动作。

Turnstile::Turnstile(JTurnstileControlInterface* pControler)
    : m_iState(LOCKED)
{
    m_pTurnstileConstroler =  pControler;
    AddTransition(LOCKED, COIN, UNLOCKED, std::bind(&JTurnstileControlInterface::unlock, m_pTurnstileConstroler));
    AddTransition(LOCKED, PASS, LOCKED, std::bind(&JTurnstileControlInterface::alarm, m_pTurnstileConstroler));
    AddTransition(UNLOCKED, COIN, UNLOCKED, std::bind(&JTurnstileControlInterface::thanks, m_pTurnstileConstroler));
    AddTransition(UNLOCKED, PASS, LOCKED, std::bind(&JTurnstileControlInterface::lock, m_pTurnstileConstroler));
}

void Turnstile::AddTransition(int curState, int event, int newState, Action action)
{
    m_vecTransition.push_back(new Transition(curState, event, newState, action));
}

void Turnstile::event(int event)
{
    for(unsigned int i = 0; i < m_vecTransition.size(); i++)
    {
        Transition* p_trans = m_vecTransition.at(i);
        if (m_iState == p_trans->m_curState && event == p_trans->m_event)
        {
            m_iState = p_trans->m_newState;
            p_trans->m_action();
            break;
        }
    }
}

实现完成十字转门之后,现在就来验证下效果,首先创建十字转门的动作对象指针,将其传入十字转门对象的构造函数,然后调用event函数,传入事件COIN, 执行完成之后,再传入事件PASS,来查看当前动作的执行是否正确。

JTurnstileControler* p_controler = new JTurnstileControler();
Turnstile turnstile(p_controler);

turnstile.event(COIN);
turnstile.event(PASS);

最后运行程序,输出的信息如下,从中可以看到,接收到COIN事件,执行了unlock动作,接收到PASS事件,执行了lock动作,这个符合预期。

[virtual void JTurnstileControler::unlock():36] action unlock
[virtual void JTurnstileControler::lock():31] action lock

三、状态机模式

上面通过迁移表的方式来实现状态机图,接下来就来介绍状态机模式,该设计模式也是比较经典的。它将状态逻辑与动作解耦,context是上下文对象,它主要实现动作功能,该状态机的模式的主要关键点是,状态对象持有context上下文对象的指针。

定义状态基类、状态A和状态B

class Context;
/// 状态基类
class State
{
public:
    State();
    virtual ~State();
    virtual void OperationInterface(Context* ) = 0;
    virtual void OperationChangeState(Context* ) = 0;
protected:
    bool ChangeState(Context* con,State* st);
};

/// 状态对象A
class SubStateA : public State
{
public:
    SubStateA();
    virtual ~SubStateA();
    virtual void OperationInterface(Context* );
    virtual void OperationChangeState(Context*);
protected:
private:
};

/// 状态对象B
class SubStateB : public State
{
public:
    SubStateB();
    virtual ~SubStateB();
    virtual void OperationInterface(Context* );
    virtual void OperationChangeState(Context*);
protected:
private:
};

接着实现定义状态基类、状态A和状态B

///
/// State
///
State::State()
{
}

State::~State()
{
}

void State::OperationInterface(Context*)
{
}

void State::OperationChangeState(Context*)
{

}

bool State::ChangeState(Context* con,State* st)
{
    con->ChangeState(st);
    return true;
}


///
/// SubStateA
///
SubStateA::SubStateA()
{

}

SubStateA::~SubStateA()
{

}
void SubStateA::OperationInterface(Context* )
{
    LOG(INFO) << "SubStateA::OperationInterface";
}

void SubStateA::OperationChangeState(Context* con)
{
    OperationInterface(con);
    this->ChangeState(con,new SubStateB());
}


///
/// SubStateB
///
SubStateB::SubStateB()
{

}

SubStateB::~SubStateB()
{

}
void SubStateB::OperationInterface(Context* )
{
    LOG(INFO) << "SubStateB::OperationInterface";
}

void SubStateB::OperationChangeState(Context* con)
{
    OperationInterface(con);
    this->ChangeState(con,new SubStateA());
}

最后关键是定义上下文对象context,  其中声明State为Context的友元类,这表明在State类中可以访问 Context 类的 private 字段。

class Context
{
public:
    Context();
    Context(State* state);
    ~Context();
    void OprationInterface();
    void OperationChangState();
protected:

private:
    friend class State; //表明在State类中可 以访问 Context 类的 private 字段
    bool ChangeState(State* state);

private:
    State* m_pState;
};

实现上下文对象context

///
/// Context
///
Context::Context()
{
}

Context::Context(State* state)
{
    this->m_pState = state;
}

Context::~Context()
{
    delete m_pState;
}

void Context::OprationInterface()
{
    m_pState->OperationInterface(this);
}

bool Context::ChangeState(State* state)
{
    this->m_pState = state;
    return true;
}

void Context::OperationChangState()
{
    m_pState->OperationChangeState(this);
}

实现完成所有状态机相关的代码之后,现在就来验证下状态的转移效果。

State* p_state_a = new SubStateA();
Context* p_context = new Context(p_state_a);

p_context->OperationChangState();
p_context->OperationChangState();
p_context->OperationChangState();

if (p_context != nullptr)
{
    delete p_context;
    p_context = nullptr;
}

if (p_state_a != nullptr)
{
    p_state_a = nullptr;
}

运行输出的打印信息如下,状态的转移从A到B,再到A。

[virtual void SubStateA::OperationInterface(Context *):71] SubStateA::OperationInterface
[virtual void SubStateB::OperationInterface(Context *):95] SubStateB::OperationInterface
[virtual void SubStateA::OperationInterface(Context *):71] SubStateA::OperationInterface

四、boost startchart

boost startchart是boost实现的状态机库,它几乎支持了所有的UML中状态机的特性。

首先来看下一个简单的实现来初步了解其使用方法和机制。boost::statechart的状态机,它大量了引用了CRTP,  基本思想要点是:派生类要作为基类的模版参数。更详细的原理可以参考《学会了这么神奇的模版模式,让你C++模版编程之路事半功倍》。首先需要实现继承state_machine的类Machine,其初始状态为Greeting。然后再实现继承simple_state的状态Greeting。

class Greeting;
class Machine : public boost::statechart::state_machine< Machine, Greeting >
{
public:
    void Print() { LOG(INFO) << "xxx"; }
};
class Greeting : public boost::statechart::simple_state< Greeting, Machine >
{
public:
    Greeting() { LOG(INFO) << "Hello World!"; }     // entry 進入
    ~Greeting() { LOG(INFO) << "Bye Bye World!"; }  // exit 退出
};

然后看下如何启动和使用上面实现的”hello world”的功能。状态机Machine构建完成之后,需要调用initiate让它运行,并且进入初始状态Greeting。

Machine my_machine;
my_machine.initiate();

下面来实现稍微复杂的秒表功能,该秒表有两个按钮:开始/接收(Start/Stop) 和 重置(Reset), 对应有两个状态Stopped和Running。其状态图如下所示。

首先定义两个事件EvStartStop和EvReset,所有事件都要继承event

class EvStartStop : public boost::statechart::event< EvStartStop > {};
class EvReset : public boost::statechart::event< EvReset > {};

然后实现继承state_machine的秒表StopWatch状态机,其初始状态为Active。

class Active;
class StopWatch : public boost::statechart::state_machine< StopWatch, Active >
{
public:
    double ElapsedTime() const
    {
        return state_cast< const IElapsedTime & >().ElapsedTime();
    }
};

接着实现Active状态,m_dElapsedTime是记录当前秒表走的时长,simple_state接受四个参数,第一个参数当然就是Active本身,第二个参数因为Active是最外层的状态,所以要设置它所属的状态机为StopWatch,第三个参数则是设置Active的初始状态为Stopped。注意“typedef boost::statechart::transition< EvReset, Active > reactions; 的格式是固定的,表示如果收到EvReset事件,那么转移到Active状态。

/// simple_state 类模版接受四个参数:
/// 第二个参数 Active是最外层的状态,因此要被传递给所属的状态机
/// 第三个参数 指定内层的初始状态
/// 第四个参数 指定是否保存历史及保存何种历史
class Stopped;
class Active : public boost::statechart::simple_state< Active, StopWatch, Stopped >
{
public:
    typedef boost::statechart::transition< EvReset, Active > reactions;
    
    Active(): m_dElapsedTime(0.0) {}
    double ElapsedTime() const { return m_dElapsedTime; }
    double & ElapsedTime() { return m_dElapsedTime; }
    
private:
    double m_dElapsedTime;
};

定义IElapsedTime接口类,它由Running和Stopped两个状态来继承和实现

class IElapsedTime
{
public:
    virtual double ElapsedTime() const = 0;
};

实现Stopped状态,它指定Active为它的context, 这样它就会嵌套到Active中,这里实现的ElapsedTime函数,主要用于在Stopped状态下,StopWatch可以获取当前秒表的值。

class Stopped : public boost::statechart::simple_state< Stopped, Active >, public IElapsedTime
{
public:
    typedef boost::statechart::transition< EvStartStop, Running > reactions;
    virtual double ElapsedTime() const
    {
        return context< Active >().ElapsedTime();
    }
};

实现Running状态,同样的,它也指定Active为它的context, 这样它就会嵌套到Active中。注意Running状态下使用context<Active>则直接访问Running的直接外层状态Active.

class Running : public boost::statechart::simple_state< Running, Active >, public IElapsedTime
{
public:
    // 一个状态可以定义任意数量的反应,所以当反应多于一个时,要将它们放入一个mpl::list<>
    typedef boost::statechart::transition< EvStartStop, Stopped > reactions;
    
    Running(): m_StartTime(0) {}
    ~Running()
    {
        // context<>() 用于访问一个状态的直接或间接上下文
        // 这可以是直接或者间接外层状态或者状态机本身
        context< Active >().ElapsedTime() = ElapsedTime();
    }
 
    virtual double ElapsedTime() const
    {
        return context< Active >().ElapsedTime() +
        std::difftime( std::time( 0 ), m_StartTime );
    }
    
private:
    std::time_t m_StartTime;
};

完成秒表的所有实现之后,现在就可以编写测试代码来测试状态的转移情况。

StopWatch my_watch;
my_watch.initiate();
LOG(INFO) << "01 " << my_watch.ElapsedTime();
my_watch.process_event( EvStartStop() );
LOG(INFO) << "02 " << my_watch.ElapsedTime();
my_watch.process_event( EvStartStop() );
LOG(INFO) << "03 " << my_watch.ElapsedTime();
my_watch.process_event( EvStartStop() );
LOG(INFO) << "04 " << my_watch.ElapsedTime();
my_watch.process_event( EvReset() );
LOG(INFO) << "05 " << my_watch.ElapsedTime();

编译运行之后的打印信息如下,可以看出开始秒表的时长是0,发布EvStartStop事件之后,秒表的时长就不为0,当发布EvReset事件之后,秒表的时长再次变成0,说明重新进入了Active状态,m_dElapsedTime变量重置为0。

[void JTestBoost::TestStopwatch():302] 01 0
[void JTestBoost::TestStopwatch():304] 02 1.58486e+09
[void JTestBoost::TestStopwatch():306] 03 1.58486e+09
[void JTestBoost::TestStopwatch():308] 04 3.16971e+09
[void JTestBoost::TestStopwatch():310] 05 0

因为一个状态的context必须是一个完整的类型(即不可以是前向声明),所以状态机必须是由外而内进行定义,比如,上面秒表的总是从状态机(StopWatch)开始,接下来是外层的状态(Active), 最后才是外层状态的直接内层状态(Running/Stopped)。

秒表的功能已经介绍完成了,如果掌握了,就可以编写由几个状态的简单应用。对于稍多的状态,就需要“数码相机”登场了。一个状态可以由同一个事件触发的多个反应。这个就需要定制化反应。

假设一个数码相机由以下两个控制键,快门键和配置键。快门键分为快按和半按,对应事件为EvShutterHalf, EvShutterFull 和 EvShutterReleased;配置键对应事件为EvConfig。状态机图如下所示:

首先定义基本的事件,EvShutterHalf/EvShutterFull/EvShutterRelease/EvConfig/EvInFocus

class EvShutterHalf : public boost::statechart::event< EvShutterHalf > {};
class EvShutterFull : public boost::statechart::event< EvShutterFull > {};
class EvShutterRelease : public boost::statechart::event< EvShutterRelease > {};
class EvConfig : public boost::statechart::event< EvConfig > {};
class EvInFocus : public boost::statechart::event< EvInFocus > {};

实现数码相机的状态机Camera, 其初始状态为NotShooting

class NotShooting;
class Camera : public boost::statechart::state_machine< Camera, NotShooting >
{
public:
    bool IsMemoryAvailable() const { return true; }
    bool IsBatteryLow() const { return false; }
};

然后实现NotShooting状态,其初始内层状态为Idle,  注意我们这里使用了定制化反应custom_reaction,  这里只需指定事件,而实际的反应在react成员函数中实现。

class Idle;
class Shooting;
class NotShooting : public boost::statechart::simple_state<NotShooting, Camera, Idle >
{
public:
    typedef boost::statechart::custom_reaction< EvShutterHalf > reactions;
    boost::statechart::result react( const EvShutterHalf & )
    {
        LOG(INFO) << "cur NotShooting, receive EvShutterHalf";
        return transit< Shooting >();
    }
};

实现Idle状态,其外层状态为NotShooting

class Configuring;
class Idle : public boost::statechart::simple_state< Idle, NotShooting >
{
public:
    typedef boost::statechart::custom_reaction< EvConfig > reactions;
    boost::statechart::result react( const EvConfig & )
    {
        LOG(INFO) << "cur Idle, receive EvConfig";
        return transit< Configuring >();
    }
};

实现Configuring状态,其外层状态为NotShooting。

class Configuring : public boost::statechart::simple_state< Configuring, NotShooting >
{
public:
    typedef boost::statechart::custom_reaction< EvConfig > reactions;

    boost::statechart::result react( const EvConfig & )
    {
        LOG(INFO) << "cur Configuring, receive EvConfig";
        return transit< Idle >();
    }
};

实现Shooting状态,所属状态机Camera,  Shooting的初始状态为Focusing。

class Focusing;
class Shooting : public boost::statechart::simple_state< Shooting, Camera, Focusing >
{
public:
    typedef boost::statechart::custom_reaction< EvShutterRelease > reactions;
    
    boost::statechart::result react( const EvShutterRelease & )
    {
        LOG(INFO) << "cur Shooting, receive EvShutterRelease";
        return transit< Idle >();
    }
};

实现Focusing状态,其外层状态为Shooting。

class Focused;
class Focusing : public boost::statechart::simple_state< Focusing, Shooting >
{
public:
    typedef boost::mpl::list<
    boost::statechart::custom_reaction< EvInFocus >,
    boost::statechart::deferral< EvShutterFull >
    > reactions;
    boost::statechart::result react( const EvInFocus & )
    {
        LOG(INFO) << "cur Focusing, receive EvInFocus event";
        return transit< Focused >();
    }
};

实现Focused状态,其外层状态为Shooting。

class Storing;
class Focused : public boost::statechart::simple_state< Focused, Shooting >
{
public:
    typedef boost::mpl::list<
    boost::statechart::custom_reaction< EvShutterFull >> reactions;
    boost::statechart::result react( const EvShutterFull & )
    {
        if ( context< Camera >().IsMemoryAvailable() )
        {
            LOG(INFO) << "cur Focused, receive EvShutterFull event";
            return transit< Storing >();
        }
        else
        {
            std::cout << "Cache memory full. Please wait...\n";
            // 表示该事件可以被抛弃,因此,事件分派算法将停止查找反应,并且状态保存为Focused
            return discard_event();
        }
    }
};

实现Storing状态,其外层状态为Shooting。

class Storing : public boost::statechart::simple_state< Storing, Shooting >
{
public:
    Storing(){LOG(INFO) << "cur Storing";}
    ~Storing(){}
};

完成数码相机的所有实现之后,现在就可以开始进行验证效果。

Camera camera;
camera.initiate();

camera.process_event( EvShutterHalf() );
camera.process_event( EvShutterRelease() );
camera.process_event( EvConfig() );
camera.process_event( EvConfig() );

camera.process_event( EvShutterHalf() );
camera.process_event( EvInFocus() );

运行输出的信息如下,发布不同的事件,就会执行不同的反应,并且切换到其他状态。

[boost::statechart::result NotShooting::react(const EvShutterHalf &):344] cur NotShooting, receive EvShutterHalf
[boost::statechart::result Shooting::react(const EvShutterRelease &):385] cur Shooting, receive EvShutterRelease
[boost::statechart::result Idle::react(const EvConfig &):357] cur Idle, receive EvConfig
[boost::statechart::result Configuring::react(const EvConfig &):369] cur Configuring, receive EvConfig
[boost::statechart::result NotShooting::react(const EvShutterHalf &):344] cur NotShooting, receive EvShutterHalf
[boost::statechart::result Focusing::react(const EvInFocus &):401] cur Focusing, receive EvInFocus event

五、总结

至此,已经将基于迁移表实现的状态机,状态机设计模式以及boost statechart中的秒表和数码相机的功能介绍完毕。

迁移表实现的状态机关键点就是状态迁移关系正确存入映射表中,状态机设计模式则关键在于context上下文,其状态会持有该context对象指针,而boost statechart的状态机的实现,关键是要画出正确的状态图,然后依据状态图来定义事件、实现状态机、再实现外层状态,接着再实现内层状态。

这里介绍的状态机相关知识,只能说算是一个入门知识总结,更深入的议题还需要不断学习和实践来加深理解,比如异步状态机、历史、异常处理等。

六、参考资料

  1. boost startchart指南
  2. boost startchart原理
  3. <<敏捷软件开发 : 原则模式与实践>>

还在为频繁变动的需求而苦恼吗?学会这个原则,让你从容应对

工作过程中,开发人员根据需求文档完成程序的开发任务,但是,程序投入测试使用的时候,经常因为各种各样的原因,比如,用户体验不好、操作不方便等,需要变动需求,甚至添加新功能,这可能就会导致开发人员原来设计的方案不能满足新变动的需求。

那么,如何应对频繁变动的需求呢,那么就需要本文将要介绍的原则登场了,即开闭原则。

介绍开闭原则之前,首先会结合例子来讲解C++提供的std::sort排序函数的用法,主要是为辅助后续要说明的开闭原则的示例,然后介绍开闭原则的两个特性,接着再叙述开闭原则常用应用场景,最后会详细介绍一个应用开闭原则的经典例子,该例子可以细细推敲,相信对加深开闭原则的理解。

一、排序函数的用法

1、std::sort的一般用法

首先创建测试存储整数类型向量,然后写入乱序的整数数据。

std::vector<int> JDebugSort::Build()
{
    std::vector<int> vec_data;
    vec_data.push_back(2);
    vec_data.push_back(1);
    vec_data.push_back(3);
    vec_data.push_back(4);
    return vec_data;
}

调用std::sort对上面创建的向量变量进行排序。

std::vector<int> vec_data_01 = Build();
std::sort(vec_data_01.begin(), vec_data_01.end());
Print(vec_data_01);

运行程序,依次输出向量存储的数据如下图所示,可以看出std::sort默认按照升序进行排列。

如果想要降序排列,怎么办呢,首先需要自定义比较函数,具体实现如下所示

bool compare(int a, int b)
{
    return (a > b);
}

同样调用std::sort对上面的向量进行排序,但是std::sort的第三个参数为上面定义实现的比较函数compare。

std::vector<int> vec_data_02 = Build();
std::sort(vec_data_02.begin(), vec_data_02.end(), compare);
Print(vec_data_02);

再次运行程序,其输出的信息如下,可以看出std::sort已经按照降序进行排列。注意如果将compare内部实现使用的大于号(>)修改为小于号(<), 那么就会变成升序排序。

除了自定义比较函数来决定std::sort的排序顺序之外,如果排序的的类型是普通数据类型, 比如整数类型,那么可以直接使用标准库使用提供的函数std::less或者std::greater来决定是升序还是降序排列。

从输出的打印信息看,std::less是升序排列,std::greater是降序排列。

2、类内部重载

上面讲述的是std::sort的一般用法,接下来将讲解类内部重载operator<来控制升降序。假设需要对部门的ID进行排序,定义实现如下所示的部门类,该类主要存储部门id和部门的名称,并且重载了operator<运算符。

class Department
{
public:
    explicit Department(int id, const std::string &name)
        : m_id(id)
        , m_name(name)
    {}
    Department(){}
    ~Department(){}

    bool operator<(const Department& deparment) const
    {
        //return m_id < deparment.m_id; // 升序
        return m_id > deparment.m_id;  // 降序
    }

    int GetId() const { return m_id;}
    std::string GetName() const {return m_name;}

private:
    int m_id;
    std::string m_name;
};

为了验证效果,首先定义存储Department类型的向量,同样存入部门id号为乱序的部门信息

std::vector<Department> JDebugSort::BuildDeparment()
{
    std::vector<Department> vec_data;
    vec_data.push_back(Department(3,"li"));
    vec_data.push_back(Department(2,"zheng"));
    vec_data.push_back(Department(5,"xxx"));

    return vec_data;
}

调用std::sort对上面的Department类型的向量进行排序

std::vector<Department> vec_data_05 = BuildDeparment();
std::sort(vec_data_05.begin(), vec_data_05.end());
PrintDepartment(vec_data_05);

运行打印结果如下所示,输出的部门信息按照部门id进行降序排列。

3、自定义比较类

除了自定义函数来确定升降序之外,还可以自定义类来确定升降序,而自定义类需要重载operator()运算符。这里还是采用上面的部门类来进行说明。operator()运算符传入两个表示部门信息的参数,函数内部还是通过部门id号来确定升降序。

class JLess
{
public:
    bool operator()(const Department& d1, const Department& d2)
    {
        return d1.GetId() < d2.GetId(); //升序排列
    }
};

调用方法如下所示,从实际的测试结果看,std::sort会优先调用operator()中定义排序顺序,不管自定义类中是否重载了operator<运算符。

std::vector<Department> vec_data_06 = BuildDeparment();
std::sort(vec_data_06.begin(), vec_data_06.end(), JLess());
PrintDepartment(vec_data_06);

还可以定义如下所示的自定义比较类,还是内部直接通过部门对象进行比较,实际上最终是调用部门类重载的operator<运算符来确定排序顺序的。

class JLess2
{
public:
    bool operator()(const Department& d1, const Department& d2)
    {
        return d1 < d2;
    }
};

二、开闭原则的特性

开闭原则意思就是可以扩展,但是又不能修改。体现在代码上就是添加新的代码,但是不需要改变已经运行的代码。概况来说,它的两个基本特性是:1)、对于扩展是开放的,2)、对于更改是封闭的。

那么如何在不改变模块原有的代码的情况下,添加新的功能点呢?

三、开闭原则的应用

关键是抽象,即有一个抽象的基类,而可能变动的行为则由派生类来实现。

客户端与服务端的通信。client类使用的是抽象类client interface,  而实际功能由server去实现,当使用的时候,创建具体的server对象,然后将其传递给client对象,如果希望client类使用不同的server类,那么只要新的server类是从client interface类派生出来,那么新的server对象就可以传递给client对象,而且client类不需要进行任何修改。

上面的例子是遵循开闭原则,而另一个比较常见并且遵循开闭原则的是模版方法,简单来说就是,基类实现基本通用的逻辑,并且该逻辑过程包含虚函数或纯虚函数,而虚函数或者纯虚函数的具体功能则由派生子类来实现。例如,下图的模版方法,TemplateMethod是实现通用的逻辑,primitive1和primitive2则是虚函数或纯虚函数,需要子类SubClass1和SubClass2来实现。

四、经典示例

现在需要制作一个绘制正方形和圆形的应用程序,并且按照指定顺序进行绘制。那么如何实现才能遵循开闭原则呢。

根据前面介绍的std::sort用法,如果需要按照指定顺序来绘制图形,那么可以利用std::sort函数,并且自定义比较类模版,重载operator<运算符。

自定义比较类模版如下,该模版类重载operator()运算符。

template <typename T>
class Less
{
public:
    bool operator()(const T t1, const T t2)
    {
        return (*t1) < (*t2);
    }
};

定义形状基类,Draw是纯虚函数,需要子类实现,重载运算符operator<主要是为了控制绘制形状的顺序。静态成员变量m_OrderTable存放绘制形状顺序的名称。

class Shape
{
public:
    Shape();
    virtual ~Shape();
    virtual void Draw() = 0;

    bool Precedes(const Shape&) const;
    bool operator<(const Shape&) const;
private:
    static const char* m_OrderTable[];
};

实现形状基类,operator<运算符内部调用Precedes,Precedes实现升序绘制形状。m_OrderTable的赋值需要实现具体子类之后才能给出。

Shape::Shape(){}

Shape::~Shape(){}


bool Shape::Precedes(const Shape& s) const
{
    const char * this_type = typeid(*this).name();
    const char * arg_type = typeid(s).name();
    int i_thisord = -1;
    int i_argord = -1;

    int i_size = sizeof(m_OrderTable)/sizeof(m_OrderTable[0]);
    for(int i = 0; i < i_size; i++)
    {
        const char* p_table_entry = m_OrderTable[i];
        if (p_table_entry != nullptr)
        {
            if (strcmp(p_table_entry, this_type) == 0)
            {
                i_thisord = i;
            }
            if (strcmp(p_table_entry, arg_type) == 0)
            {
                i_argord = i;
            }
            if (i_thisord >= 0 && i_argord >= 0)
            {
                break;
            }
        }
    }
    return i_thisord < i_argord;
}

bool Shape::operator<(const Shape& s) const
{
    return Precedes(s);
}

定义实现正方形

/// 定义
class Square : public Shape
{
public :
    Square();
    virtual ~Square() override;
    virtual void Draw() override;
};

/// 实现
Square::Square(){}

Square::~Square(){}

void Square::Draw()
{
    LOG(INFO) << "draw Square";
}

定义实现圆形

/// 定义
class Circle : public Shape
{
public :
    Circle();
    virtual ~Circle() override;
    virtual void Draw() override;
};

/// 实现
Circle::Circle(){}

Circle::~Circle(){}

void Circle::Draw()
{
    LOG(INFO) << "draw Circle";
}

实现完成正方形和圆形之后,就可以给m_OrderTable赋值,其先后顺序就确定了对应形状的绘制顺序。

const char* Shape::m_OrderTable [] =
{
    typeid(Circle).name(),
    typeid (Square).name()
};

实现绘制所有形状的逻辑,函数DrawAllShape接受存储类型为Shape*的向量,内部实现如下所示,调用std::sort对向量进行排序,然后再循环调用向量中的每一个对象的Draw进行绘制。

void JDebugOCP::DrawAllShape(std::vector<Shape*> &allShape)
{
    std::vector<Shape*> order_all_shape = allShape;
    std::sort(order_all_shape.begin()
             ,order_all_shape.end()
             ,Less<Shape*>());

     std::vector<Shape*>::const_iterator iter_const;
     for(iter_const = order_all_shape.begin(); iter_const != order_all_shape.end(); iter_const++)
     {
         (*iter_const)->Draw();
     }
}

最后使用的方式如下,创建存储各个形状的的向量,并且创建的形状不需要按照顺序。再将其向量传入上面定义实现的函数DrawAllShape。

std::vector<Shape*> vec_shape;
vec_shape.push_back(new Square());
vec_shape.push_back(new Circle());

DrawAllShape(vec_shape);

运行的结果如下,程序按照m_OrderTable赋值的顺序绘制图形。并且后续添加新的形状,并且指定输出顺序,那么也只要调整驱动表m_OrderTable即可,其他代码都不需要改变,这也满足了开闭原则。

[virtual void Circle::Draw():66] draw Circle
[virtual void Square::Draw():57] draw Square

五、总结

std::sort默认按照升序进行排列,如果重载使用大于号,那么按照降序排列,如果使用小于号,那么按照升序排列。开发过程中,遵循开闭原则能够有效解决预防频繁变动的需求,开闭原则的特性就是:对于扩展是开放的,对于更改是封闭的。开闭原则的关键就是抽象,抽象体现在C++就是虚函数或者纯虚函数。

没想到bind的功能这么强大,赶紧来看看

std::bind是C++11中一个函数模版,就像函数适配器,接受一个可调用对象(callable object),生成一个新的可调用对象。通过它,我们可以实现类似传统的函数指针,函数回调等功能,并且能够降低代码的复杂度。

本文首先详细说明std::bind的基本用法以及解释使用过程中疑问点,然后再介绍如何利用传统函数指针搭建基础结构,再说明如何用std::bind来代替函数指针,最后介绍如何用std::bind来实现函数回调的功能。

一、std::bind的基本用法

首先看下std::function, 它就是std::bind返回的新的可调用对象。如下图,定义实现了普通加法函数Add,  然后将该函数指针赋值给std::function类型的变量,这里可以注意到,使用了Add和&Add进行赋值。两者是等效的,这是因为使用Add的时候,会隐式转换成函数指针。

static int Add(int a, int b)
{
    return (a+b);
}

std::function<int (int, int)> fun = Add;
std::function<int (int, int)> fun2 = &Add;
LOG(INFO) << "fun(1, 1):"<< fun(1, 1);
LOG(INFO) << "fun2(1, 2):"<< fun2(1, 2);

运行程序之后的输出信息,可以看出std::function类型的变量的使用与普通函数的使用是一样的。

[2020-01-05 17:43:05,243189] [void JDebugBind::StartDebug():184] fun(1, 1):2
[2020-01-05 17:43:05,243206] [void JDebugBind::StartDebug():185] fun2(1, 2):3

我们不直接采用普通函数对std::function进行赋值,而是采用stb::bind,首先看下简单的实例,其中std::placeholders::_1和std::placeholders::_2是占位符,代表函数的入参。如果调用的时候,需要传递具体实参,那么就需要使用placeholders来占位。这里需要注意std::placeholders::_1并不是代表函数的第一个入参数,至于为什么,请继续往下阅读,下面将会通过实例进行阐述。

std::function<int (int, int)> fun3 = std::bind(Add, std::placeholders::_1, std::placeholders::_2);
std::function<int (int, int)> fun4 = std::bind(&Add, std::placeholders::_1, std::placeholders::_2);

LOG(INFO) << "fun3(1, 3):"<< fun3(1, 3);
LOG(INFO) << "fun4(1, 4):"<< fun4(1, 4);

如果函数的第二个入参是一个固定值,那么第一个入参就需要使用占位符std::placeholders::_1,如下所示,函数第二个参数固定位数值5,那么使用std::function类型变量的时候,也只需要传递一个参数,该参数代表Add函数的第一个参数。

std::function<int (int)> fun5 = std::bind(Add, std::placeholders::_1, 5);
LOG(INFO) << "fun5(1):"<< fun5(1);

如果Add函数的第一个入参是一个固定值,那么第二个入参就需要使用占位符std::placeholders::_1(注意不是std::placeholders::_2),如下所示,函数第一个参数固定位数值6,那么使用std::function类型变量的时候,也只需要传递一个参数,该参数代表Add函数的第二个参数。

std::function<int (int)> fun6 = std::bind(Add, 6, std::placeholders::_1);
LOG(INFO) << "fun6(1):"<< fun6(1);

当然,如果函数Add的两个参数都是固定值,那么使用std::function类型变量的时候,就不需要参数了。

std::function<int()> fun7 = std::bind(Add, 3, 7);
LOG(INFO) << "fun7():"<< fun7();

这里有个小技巧,如果不想要书写std::function那么繁琐的信息表示,那么可以采用auto代替,但是注意不要滥用auto.

auto fun8 = std::bind(Add, std::placeholders::_1, std::placeholders::_2);
LOG(INFO) << "fun8(1, 8):"<< fun8(1,8);

二、std::bind的扩展

上面说明的是stb::bind使用普通函数的方法,那么如果是类的成员函数呢?应该如何使用呢?首先s td::bind的第一个参数是类成员函数指针,第二个参数为类对象的指针,其他的用法与使用普通函数的用法是一样的。

class JBindClass
{
public:
    int Multi(int a, int b)
    {
        return (a * b);
    }
};


JBindClass bind_class;
auto fun9 = std::bind(&JBindClass::Multi, &bind_class, std::placeholders::_1, std::placeholders::_2);
LOG(INFO) << "fun9(1, 9):"<< fun9(1,9);

std::bind参数值是默认按照值传递的,首先实现函数Print, 该函数的入参是一个引用,函数内部将参数自增1,然后输出打印信息。接着再通过输出std::bind使用前后日志信息来确认是否是按照值传递。

static void Print(int &value)
{
    value++;
    LOG(INFO) << value;
}

int i_value = 10;
LOG(INFO) << "before i_value:" << i_value;
std::function<void()> fun10 = std::bind(Print,i_value);
fun10();
LOG(INFO) << "after i_value:" << i_value;

从输入的打印信息看,std::bind使用前后的信息没有发生变化,说明std::bind是默认按照值传递的。

[void JDebugBind::StartDebug():207] before i_value:10
[void Print(int &):24] 11
[void JDebugBind::StartDebug():210] after i_value:10

如果想要按照引用来传递变量,应该如何操作呢,那么就是std::ref登场的时候,std::ref是用于包装引用传递的值。

LOG(INFO) << "before i_value:" << i_value;
std::function<void()> fun11 = std::bind(Print,std::ref(i_value));
fun11();
LOG(INFO) << "after i_value:" << i_value;

从输出打印信息看,采用std::ref传递变量之后,std::bind使用前后的信息发生变化了。

[void JDebugBind::StartDebug():213] before i_value:10
[void Print(int &):24] 11
[void JDebugBind::StartDebug():216] after i_value:11

另外补充一点,std::cref用于包装const引用传递的值。

static void Printc(const int &value)
{
    LOG(INFO) << value;
}

int i_value_c = 12;
std::function<void()> fun12 = std::bind(Printc,std::cref(i_value_c));
fun12();

三、传统函数指针

函数指针变量用于存储函数指针,以便后续的调用。有时候可以利用它实现多个消息对象的处理,并且一定程度满足开闭原则。

首先实现抽象基类JAbstractBaseTest,接着再实现继承JAbstractBaseTest的两个子类JObjA和JObjB

/// 基类
class JAbstractBaseTest
{
public:
    JAbstractBaseTest(){}
    virtual ~JAbstractBaseTest(){}

    virtual void run() = 0;

};

/// 子类JObjA
class JObjA: public JAbstractBaseTest
{
public:
    void run(){LOG(INFO) << "JObjA Run";}

    static JAbstractBaseTest* create_instance()
    {
        return new JObjA();
    }

};

/// 子类JObjB
class JObjB: public JAbstractBaseTest
{
public:
    void run(){LOG(INFO) << "JObjB Run";}

    static JAbstractBaseTest* create_instance()
    {
        return new JObjB();
    }
};

完成上面的测试类,接着实现基础的框架,定义函数指针CreateObj,该函数指针用于动态创建对象,然后再分别实现初始化创建对象的函数指针映射表以及通过id从映射表中获取函数对象的两个函数。

class JDebugMain
{
public:
    JDebugMain()
    {
        InitObj();
    }

    // 定义函数指针
    typedef JAbstractBaseTest* (*CreateObj)();
    
    enum E_OBJ_ID
    {
        E_OBJ_A,
        E_OBJ_B,
    };

    // 初始化创建对象的函数指针映射表
    void InitObj()
    {
         m_mapRegisterClass[E_OBJ_A] = &JObjA::create_instance;
         m_mapRegisterClass[E_OBJ_B] = &JObjB::create_instance;
    }

    // 通过id从映射表中获取函数对象
    JAbstractBaseTest* GetObj(E_OBJ_ID eObjId)
    {
        std::map<E_OBJ_ID,CreateObj>::iterator iter;
        iter = m_mapRegisterClass.find(eObjId);
        if (iter != m_mapRegisterClass.end())
        {
            return m_mapRegisterClass[eObjId]();
        }
        return nullptr;
    }

private:
    std::map<E_OBJ_ID, CreateObj> m_mapRegisterClass;
};

使用调用方式如下,通过id获取对象指针,然后执行对象的run函数。通过这样的方式,可以做到主体循环不变,如果需要添加新的对象处理,那么只要实现新的类,然后添加到映射表中即可。

JDebugMain debug_main;
JAbstractBaseTest* p_obj = debug_main.GetObj(JDebugMain::E_OBJ_A);
if (p_obj)
{
    p_obj->run();
    delete p_obj;
    p_obj = nullptr;
}

四、std::bind代替函数指针

std::bind和std::function的结合,可以实现函数指针的功能。通过using Funtor = std::function<void (void)>来实现类似函数指针的声明。其中Funtor表示std::function<void (void)>的别名。然后在初始化表函数InitTab中,通过使用std::bind将类的函数成员一一映射到map中。

/// 类定义
class JDebugBind
{
public:
    using Funtor = std::function<void (void)>;
    enum
    {
        E_TEST_FUN_01,
        E_TEST_FUN_02,
    };

    JDebugBind();
    /// 根据测试id来执行对应的测试函数
    void RunTest(int iType);

protected:
    void Test01();
    void Test02();

private:
    void InitTab();

private:
    std::map<int, Funtor> m_mapTab;
};

/// 类实现
JDebugBind::JDebugBind()
{
    InitTab();
}

void JDebugBind::InitTab()
{
    m_mapTab.clear();
    m_mapTab[E_TEST_FUN_01] = std::bind(&JDebugBind::Test01, this);
    m_mapTab[E_TEST_FUN_02] = std::bind(&JDebugBind::Test02, this);
}

void JDebugBind::RunTest(int iType)
{
    std::map<int, Funtor>::iterator iter;
    for(iter = m_mapTab.begin(); iter != m_mapTab.end(); iter++)
    {
        if (iType == iter->first)
        {
            iter->second();
        }
    }
}

void JDebugBind::Test01()
{
    LOG(INFO) << "Test01";
}

void JDebugBind::Test02()
{
    LOG(INFO) << "Test02";
}

调用JDebugBind的方式如下,只需要传递函数的id给函数RunTest,即可执行到对应的函数。同样的,后续如果想要添加新的功能,那么只要实现新的函数,并且将其添加到map中即可。

JDebugBind debug_bind;
debug_bind.RunTest(JDebugBind::E_TEST_FUN_01);

五、std::bind实现函数回调

函数回调在编程实现是一个特别重要的特性,它经常会在一些架构中使用到。而std::bind是可以实现函数回调的特性的。下图实现的类JDebugCallback中,构造函数接受一个类型为std::function的参数之后,将其赋值给类的成员函数m_callback,后续调用函数Start的时候,Start函数内部再调用m_callback,从而实现函数回调。这里只是一个简单的例子说明,可能还不能充分看到函数回调的强大。希望这里作为一个引入,后续在实际工作中,再慢慢的体会。

class JDebugCallback
{
public:
    JDebugCallback(std::function<void()> callback)
        : m_callback(callback)
    { }

    void Start()
    {
        m_callback();
    }

private:
    std::function<void()> m_callback;
};

最后看下怎么使用JDebugCallback类,实现类两个函数CallBack01和CallBack02,然后通过std::bind传递给JDebugCallback,接着JDebugCallback对象调用Start来执行传递进来的函数。

static void CallBack01()
{
    LOG(INFO) << "CallBack01";
}

static void CallBack02()
{
    LOG(INFO) << "CallBack02";
}

JDebugCallback debug_cb_01(std::bind(CallBack01));
debug_cb_01.Start();

JDebugCallback debug_cb_02(std::bind(CallBack02));
debug_cb_02.Start();

五、总结

至此,C++11提供的std::bind的用法和扩展已经介绍完毕,虽然工作中有各种各样的需求场景,但是只要掌握了知识的基本原理,就能够以不变应万变。本文介绍了std::bind的各种基本应用场景,并结合了例子进行说明,相信应该已经说明白了。

学会了这么神奇的模版模式,让你C++模版编程之路事半功倍

最近由于开发工作的需要,项目引入了boost::statechart的状态机,它大量了引用了CRTP,  它的全称是Curiously Recurring Template Pattern,奇异递归模版模式,C++模版编程中很常用的一种用法。那么它神奇的地方到底在哪里呢,接下来就一一来揭开它神秘的面纱。

一、奇异递归模版模式的简介

奇异递归模版模式的基本思想要点是:派生类要作为基类的模版参数。它是C++模版编程中常用的手法。理解它之后,学习模版编程过程中也会事半功倍,而不会觉得云里雾里的。

二、奇异递归模版模式的基本格式

奇异递归模版模式的基本格式如下:JCrtpDerived继承JCrtpBase,并且JCrtpDerived作为基类JCrtpBase的模版参数。通过这样的方式,基类就可以使用子类的方法。并且不需要使用到虚函数,一定程度上减少程序的开销。

template <typename T>
class JCrtpBase
{
public:
};

class JCrtpDerived : public JCrtpBase<JCrtpDerived>
{
public:

};

三、奇异递归模版模式的入门

从上面的给出的奇异递归模版模式的基本格式中可以看出,子类是作为基类的模版参数,但是如果传递给基类的模版参数不是基类的子类,那就会造成混乱错误。如下图所示,JCrtpDerived2子类继承了基类JCrtpBase,但是传递给基类的模版参数不是JCrtpDerived2。

template <typename T>
class JCrtpBase
{
public:
    void Do()
    {
        T* derived = static_cast<T *>(this);
    }

};

class JCrtpDerived1 : public JCrtpBase<JCrtpDerived1>
{
public:

};

class JCrtpDerived2 : public JCrtpBase<JCrtpDerived1>
{
public:

};

那么如何解决上面的问题呢,可以将基类的默认构造函数设置为私有,并且模版参数T设置为基类的友元。通过这样的方式,基类的构造函数只能由模版参数T调用。当创建JCrtpDerived2子类对象的时候,会调用基类的构造函数,而这时候发现JCrtpDerived2不是基类的友元,那么就无法调用基类构造函数而出错。

template <typename T>
class JCrtpBase
{
public:
    void Do()
    {
        T* derived = static_cast<T *>(this);
    }

private:
   JCrtpBase();
   friend T;
};

调用运行JCrtpDerived2,就会出现错误

JCrtpDerived1 crtp_derived1;
crtp_derived1.Do();

JCrtpDerived2 crtp_derived2;
crtp_derived2.Do();

四、奇异递归模版模式的应用场景

1、静态多态

奇异递归模版模式可以实现静态多态的效果,顾名思义,就是有多态的特性,但是不需要使用虚函数,是编译的时候确定,因此,能够减少运行时的开销。接下来就来看看两个示例。

基类JCrtpBase实现函数Do,该函数内部对象通过static_cast转换为模版参数对象,模版参数对象再调用对应的实现函数,而模版参数对象由子类来实现。

template <typename T>
class JCrtpBase
{
public:
    void Do()
    {
        T* derived = static_cast<T *>(this);
        derived->DoSomething();
    }

private:
    JCrtpBase(){}
    friend T;
};

class JCrtpDerived1 : public JCrtpBase<JCrtpDerived1>
{
public:
    void DoSomething()
    {
        LOG(INFO) << "I'am is JCrtpDerived1";
    }

};

class JCrtpDerived2: public JCrtpBase<JCrtpDerived2>
{
public:
    void DoSomething()
    {
        LOG(INFO) << "I'am is JCrtpDerived2";
    }
};

调用运行的效果如下所示,从中可以看出,对象调用基类的函数,而基类函数实际上又去调用子类的函数DoSomething。基于这样的思想,我们可以将通用的逻辑放在基类Do中实现,而不同的放到对应的子类函数DoSomething实现。

/// 调用   
JCrtpDerived1 crtp_derived1;
crtp_derived1.Do();

JCrtpDerived2 crtp_derived2;
crtp_derived2.Do();

/// 运行信息
[void JCrtpDerived1::DoSomething():33] I'am is JCrtpDerived1
[void JCrtpDerived2::DoSomething():43] I'am is JCrtpDerived2

这样需要注意的一点是,如果子类再被其他子类继承,那么其他子类就不能按照上面的方式实现。具体可以看下示例:JCrtpSub子类再继承JCrtpDerived1。

class JCrtpSub: public JCrtpDerived1
{
public:
    void DoSomething()
    {
        LOG(INFO) << "I'am is JCrtpSub";
    }
};

调用运行的效果如下所示,JCrtpSub调用基类的函数Do,但是运行没有调用到JCrtpSub类自身的函数DoSomething。

/// 调用
JCrtpDerived1 crtp_derived1;
crtp_derived1.Do();

JCrtpDerived2 crtp_derived2;
crtp_derived2.Do();

JCrtpSub ctrp_sub;
ctrp_sub.Do();

/// 运行信息
[void JCrtpDerived1::DoSomething():33] I'am is JCrtpDerived1
[void JCrtpDerived2::DoSomething():43] I'am is JCrtpDerived2
[void JCrtpDerived1::DoSomething():33] I'am is JCrtpDerived1

上面的例子是子类调用基类函数,由基类再转换调用子类函数,效果类似于策略模式。下面将要说明的例子,更像多态特性,但是不需要虚函数。基类和子类都实现相同的函数DoSomething

template <typename T>
class JCrtpBase
{
public:
    void DoSomething()
    {
        static_cast<T *>(this)->DoSomething();
    }

private:
    JCrtpBase(){}
    friend T;
};

class JCrtpDerived1 : public JCrtpBase<JCrtpDerived1>
{
public:
    void DoSomething()
    {
        LOG(INFO) << "I'am is JCrtpDerived1";
    }

};

class JCrtpDerived2: public JCrtpBase<JCrtpDerived2>
{
public:
    void DoSomething()
    {
        LOG(INFO) << "I'am is JCrtpDerived2";
    }
};

然后实现模版方法,该方法入参为基类JCrtpBase的引用,内部调用基类函数DoSomething。

template<typename T>
void DoAction(JCrtpBase<T> &ctrpbase)
{
    ctrpbase.DoSomething();
}

调用运行效果如下,向模版方法传递不同的子类,调用对应子类的函数。

/// 调用
JCrtpDerived1 crtp_derived1;
JCrtpDerived2 crtp_derived2;
DoAction(crtp_derived1);
DoAction(crtp_derived2);

// 打印信息
[void JCrtpDerived1::DoSomething():38] I'am is JCrtpDerived1
[void JCrtpDerived2::DoSomething():48] I'am is JCrtpDerived2

2、boost::statechart状态机

Boost.Statechart大量使用了CRTP,   派生类必须作为第一个参数传递给所有基类模版,Boost.Statechart状态机后续考虑作为一个专题来研究讨论。

struct Greeting : sc::simple_state< Greeting, Machine >

3、std::enable_shared_from_this特性

C++的特性enable_shared_from_this通常是用于在当你要返回一个被shared_ptr管理的对象。JObj继承enable_shared_from_this,并且JObj作为参数模版传递给enable_shared_from_this,这里就运用到了CRTP。

class JObj : public std::enable_shared_from_this<JObj>
{
public:
    std::shared_ptr<JObj> GetObj() {
        return shared_from_this();
    }
};

正确的调用方式,JObj是被shared_ptr管理,因此,如果要获取对象,那么JObj需要继承enable_shared_from_this。

std::shared_ptr<JObj> share_obj1 = std::make_shared<JObj>();
// JObj对象被shared_ptr管理,因此,如果要获取对象,那么JObj需要继承enable_shared_from_this
std::shared_ptr<JObj> share_obj2 = share_obj1->GetObj();

五、总结

到这里,奇异递归模版模式已经基本讲解完成,我们首先介绍了它的基本格式,使用注意要点,然后重点讲解了它的应用场景,包括静态多态、boost::statechart状态机、std::enable_shared_from_this特性。理解了奇异递归模版模式,不但有利于模版编程的学习,而且对于以后应用的开发也是有好处的。

最全面的android入门知识,请好好收藏

Android一款基于Linux的开放源代码的操作系统,主要用于移动设备,现在许多公司都会基于Android做各种定制开发工作。所以,在开发工作之前,需要全面熟悉了解Android的基础知识,有了基础入门知识之后,才能做好方案设计,并且利于以后更加深入的学习发展。

古人说,工欲善其事,必先利其器,所以,本文首先会介绍环境搭建流程,再简单介绍android的系统架构,接着实现第一个程序Hello World!, 让新手对android在视觉上有个比较清晰的概念。然后再介绍程序的目录功能。紧接着就是本文的重头戏,分别介绍布局管理器、android的重要程序组件、Activity的生命周期、Service的生命周期、BroadcastRecevier的应用,Intent的应用。最后再扩展的知识点,包括分辨率问题、应用程序签名。

一、环境搭建

  • 安装JDK7

  • 下载Eclipse(eclipse-jee-kepler-SR1-win32),Eclipse不需要安装, 下载解压后即可使用
  • 安装ADT扩充套件, 双击eclipse.exe,点击help -> install new software -> Add -> 在Location处输入网址:http://dl-ssl.google.com/android/eclipse/site.xml

  • 下载android SDK, 解压SDK,设定SDK: window -> preferences -> android -> 在SDK Location选择SDK路径

  • 安装mysql
  • 安装tomcat,修改默认端口号为8010,数据库驱动(mysql-connector-java-5.1.13-bin.jar ,用于tomcat和mysql之间的连接)放到目录..\Apache Software Foundation\Tomcat 8.0\lib下

修改文件tomcat-users.xml

  • 设置环境变量,计算机->右键属性—>环境变量->修改变量,进行如下设置

如果想要测试效果,那么在cmd下输入以下命令 java,javac,java-version

  • 上面搭建的环境,只是在本机上,如果开发的程序涉及到服务器端,并且需要将程序推荐给别人使用,就需要购买空间和域名(当然,现在也有免费的空间,但是受到限制,无法商用)

二、 系统架构

Android平台是基于Linux内核。上图是其系统架构。大致可以分为四层。从下到上为:

  1. Linux内核层: 包含了Linux内核和一些驱动模块(比如蓝牙驱动,USB驱动等)
  2. Libraries层: 提供动态库, Android运行时库,Dalvik虚拟机等。这一层大部分都是用C或者C++编写,也可以称为Native层(init、Audio系统、Surface系统)
  3. Framework层: 大部分用Java语言编写。是Android平台的基石。(zygote、system server等)
  4. Application层:与用户之间交互。都是用Java开发。(MediaProvider等)

Java世界与Native世界的关系

  1. JAVA通过JNI层调用Linux的系统调用来完成对应的功能
  2. JAVA世界经过JNI层通过IPC方式与Native世界交互。而IPC方法就是是Binder。
  3. JNI是Java Native Interface的是缩写, 即“Java本地调用”。简单来说,java程序中的函数可以调用Native写的函数。Native程序中的函数可以调用Java层的函数。

三、开始第一个程序

  1. 双击Eclipse, 启动Eclipse
  2. 选择File -> New -> Project, 在打开的对话框中,选择Android -> Android Application Project

  1. 程序创建完成,我们来看看效果,首先利用android 提供虚拟设备管理,创建虚拟机,并运行
  2. 右键工程, Debug as -> Android Application, 运行效果如下
  3. (如果工程暂时不要,可以选择工程,右键“Close Porject,可以提高打开esclipse的速度,当工程多的时候,这个效果特别明显)

    四、程序目录介绍

    总体目录

目录及文件 说明
Src 代码目录,代码编写、功能实现的地方
Assets 存放资源文件,例如歌曲等
Bin 存放编译生成的二进制文件
Libs
Res 布局文件
AndroidManifest.xml 关键配置文件,包括组件的声明,版本的定义,权限的声明等

代码文件说明

程序启动的时候,会调用onCreatesetContentView,进行界面的初始化,后面会详细说明activity的生命周期。onCreateOptionMenuonOptionsItemSelected是选项菜单的实现。

布局文件说明

Android提供了一组View类,用作视图的容器。每个布局实现管理器子布局的大小和位置的特定策略。

布局管理器 说明
LinearLayout 以水平或者垂直的方式组织其子控件
FrameLayout 显示单一帧
TableLayout 以行和列的方式组织其子控件
AbsoluteLayout 绝对布局,兼容性不好,不建议使用
RelativeLayout 相对布局,容器中的控件相

 

Android中,控件通常是在layout目录下定义,但是代码中如何使用呢? 这里先解释下两个定义的不同

“@+”表示新声明, “@”表示引用,例如:

“@+id/tv” 表示新声明一个id, 是id名为tv的组件

“@id/tv” 表示引用id名为tv的组件,比较常用于布局

定义编辑框
<EditText
android:id="@+id/search_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:hint="@string/search_example" />
 
代码中引用:
private EditText text_search;          
text_search = (EditText) findViewById(R.id.search_bar);

AndroidManifest文件说明

<application></application>是进行组件的声明,例如activity, service, brocadcast等注意,如果涉及到网络的交互,要在该文件中加入如下权限:

<uses-permission android:name="android.permission.INTERNET"></uses-permission>

六、 布局管理器

LinearLayout线性布局

线性布局是最常用的一种。此布局会保持组件之间的间隔以及组件之间互相对齐。显示组件的方式有垂直于水平两种,可以通过orientation进行设定。

垂直方式:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="horizontal"
     >
    <Button
        android:layout_width="fill_parent"
        android:layout_height = "wrap_content"
        android:text = "Button1"></Button>
    <Button
        android:layout_width="fill_parent"
        android:layout_height = "wrap_content"
        android:text = "Button2"></Button>  
    <Button
        android:layout_width="fill_parent"
        android:layout_height = "wrap_content"
        android:text = "Button2"></Button>          
</LinearLayout>

水平方式:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="horizontal"
     >
    <Button
        android:layout_width="wrap_content"
        android:layout_height = "wrap_content"
        android:text = "Button1"></Button>
    <Button
        android:layout_width="wrap_content"
        android:layout_height = "wrap_content"
        android:text = "Button2"></Button>  
    <Button
        android:layout_width="wrap_content"
        android:layout_height = "wrap_content"
        android:text = "Button2"></Button>          
</LinearLayout>

FrameLayout单帧布局

单帧布局新定义的组件永远放在屏幕的左上角,后一个组件总会将前一个组件覆盖

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
     >
    <Button
        android:layout_width="fill_parent"
        android:layout_height = "wrap_content"
        android:text = "Button1"></Button>
    <Button
        android:layout_width="wrap_content"
        android:layout_height = "fill_parent"
        android:text = "Button2"></Button>  
    <Button
        android:layout_width="wrap_content"
        android:layout_height = "wrap_content"
        android:text = "Button3"></Button>          
</FrameLayout>

TableLayout表格布局

表格布局就像一个表格。由TableRow组成,每个TableRow代表一行。

<TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
     >
     <TableRow>
        <Button android:text = "Button1"></Button>
        <Button android:text = "Button2"></Button>
        <Button android:text = "Button3"></Button>             
     </TableRow>
 
     <TableRow>
        <Button android:text = "Button4"></Button>
        <Button android:text = "Button5"></Button>
        <Button android:text = "Button6"></Button>             
     </TableRow>   
 
     <TableRow>
        <Button android:text = "Button7"></Button>
        <Button android:text = "Button8"></Button>
        <Button android:text = "Button9"></Button>             
     </TableRow>            
</TableLayout>

AbsoluteLayout绝对布局

绝对布局,组件的位置可以准确指定在屏幕的x/y坐标。但是这种布局兼容性不好。

<AbsoluteLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
     >
    <Button
        android:layout_width="wrap_content"
        android:layout_height = "fill_parent"
        android:text = "Button1"
        android:layout_x="100dp"></Button>
    <Button
        android:layout_width="fill_parent"
        android:layout_height = "wrap_content"
        android:text = "Button2"
        android:layout_y="100dp"></Button>   
</AbsoluteLayout>

RelativeLayout相对布局

相对布局,是一种比较常用的比较,每个组件都可以指定相对于其他组件或者父组件的位置(通过ID来指定)。一个组件的位置,至少要确定组件“左右”与“上下”两个位置才可以准确确定组件位置

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text = "Button1"
        android:id="@+id/btn1"></Button>
    <Button
        android:layout_width="fill_parent"
        android:layout_height = "wrap_content"
        android:text = "Button2"
        android:id="@+id/btn2"
        android:layout_below="@id/btn1"></Button>
    <Button
        android:layout_width="wrap_content"
        android:layout_height = "wrap_content"
        android:text = "Button3"
        android:id="@+id/btn3"
        android:layout_below="@id/btn2"
        android:layout_alignRight="@id/btn2"></Button>  
    <Button
        android:layout_width="wrap_content"
        android:layout_height = "wrap_content"
        android:text = "Button4"
        android:id="@+id/btn4"
        android:layout_below="@id/btn3"
        android:layout_alignParentRight="true"></Button>
    <Button
        android:layout_width="wrap_content"
        android:layout_height = "wrap_content"
        android:text = "Button5"
        android:id="@+id/btn5"
        android:layout_below="@id/btn4"
        android:layout_centerHorizontal="true"></Button>
</RelativeLayout>

七、程序组件简介

Activity简介

  • 应用程序中的每个屏幕显示都通过几次和扩展基类Activity来实现
  • Activity利用View来实现应用程序的GUI。应用程序通过GUI向用户显示信息,用户通过GUI向应用程序发出指令和响应

    Service简介

  • Service是具有一段较长生命周期且没有用户界面的程序
  • Service继承自android.app.Service类
  • Service不能自己启动
  • 启动和关闭Service的流程
StartService()启动service
stopService() 关闭service
stopSelf() service自身调用关闭
bindservice() 将context对象(如activity)绑定到指定的service
这样的话,context对象消亡,service也会停止运行

BroadcastReceiver简介

  • BroadcastReceiver是用户接受广播通知的组件
  • BroadcastReceiver是对发送出来的Broadcast进行过滤接收并响应的一类组件
  • 如果想要接受到广播,首先要注册BroadcastReceiver,注册的方式有两种,一种是静态的在AndroidManifest.xml中用<receiver>标签声明注册,并设置过滤器。另一种方式,动态的设置一个IntentFilter对象,然后在需要注册的地方调用registerReceiver,取消注册的地方调用unregisterReceiver方法。
  • 如何发生广播呢?首次,在要发送信息的地方,封装一个Intent对象,然后调用sendBroadcast方法吧Intetn对象以广播的形式发送出去。这样的话,所有已经注册的BroadcastReceiver会检查注册时的IntentFilter是否与发送的Intent向匹配,如果匹配则调用onRecevie方。

    ContentProvider简介

    ContentProvider能将应用程序特定的数据提供给另一个应用程序使用。

    Intent连接组件的纽带

    Intent是一种运行时绑定机制,它能在程序运行的过程中连接两个不同的组件

    Intent的主要组成部分:

组成 描述
组件名称 Intent目标组件的名称
Action(动作) Intent所触发动作名字的字符串
Data(数据) 描述Intent要操作的数据URI和数据类型
Category(类别) 对被请求组件的额外描述信息
Extra(附加信息) 附加额外信息

八、Activity的生命周期

Activity生命周期的七个函数:

函数 说明
onCreate Activity初次创建时被调用,一般在这里创建view, 初始化布局,设置监听器。如果Activity首次调用,那么其后会调用onStart,

如果Activity是停止后重新刷新,那么其后调用onRestart()

onStart() 当Activity对用户即将可见时被调用,其后调用onResume()
onRestart() 当Activity停止后重新显示被调用,其后调用onStart()
onResume() 当用户能在界面中进行操作的时候调用
onPause() 当系统要启动一个其他的Activity时调用,这个方法被用来停止动画和其他占用CPU资源的事情
onStop() 当另一个Activity恢复并遮盖住当前Activity,导致其对用户不再可见时调用
onDestory() 当前的Activity被销毁前所调用的最后一个方法,或者进程终止时调用

为了详细说明生命周期的变化,创建“MyFirstProject”的项目,添加两个类“MainActivity.java”和“OtherActivity.java”,这两个类都继承Activity,并实现了上面的七 函数,在每个生命周期函数中添加了一个Log打印语句,方便观察周期变化。MainActivity添加了一个按钮,用于跳转到OtherActivity

MainActivity.java关键代码
public class MainActivity extends Activity implements OnClickListener {
    private Button btn;
 
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        Log.v("MainActivity", "onCreate");
        btn = (Button) findViewById(R.id.Main_btn);
        btn.setOnClickListener(this);
    }
 
    @Override
    public void onClick(View arg0) {
        if (arg0 == btn) {
            Intent intent = new Intent();
            intent.setClass(this, OtherActivity.class);
            this.startActivity(intent);
        }
    }
 
    @Override
    protected void onDestroy() {
        // TODO Auto-generated method stub
        super.onDestroy();
        Log.v("MainActivity", "onDestroy");
    }
 
    @Override
    protected void onPause() {
        // TODO Auto-generated method stub
        super.onPause();
        Log.v("MainActivity", "onPause");
    }
 
    @Override
    protected void onRestart() {
        // TODO Auto-generated method stub
        super.onRestart();
        Log.v("MainActivity", "onRestart");
    }
 
    @Override
    protected void onResume() {
        // TODO Auto-generated method stub
        super.onResume();
        Log.v("MainActivity", "onResume");
    }
 
    @Override
    protected void onStart() {
        // TODO Auto-generated method stub
        super.onStart();
        Log.v("MainActivity", "onStart");
    }
 
    @Override
    protected void onStop() {
        // TODO Auto-generated method stub
        super.onStop();
        Log.v("MainActivity", "onStop");
    }
 
}
OtherActivity.java 关键代码
public class OtherActivity extends Activity implements OnClickListener {
    private Button btn;
 
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.other);
        Log.v("OtherActivity", "onCreate");
        btn = (Button) findViewById(R.id.Other_btn);
        btn.setOnClickListener(this);
    }
 
    @Override
    public void onClick(View arg0) {
        if (arg0 == btn) {
            this.finish();
        }
    }
 
    @Override
    protected void onDestroy() {
        // TODO Auto-generated method stub
        super.onDestroy();
        Log.v("OtherActivity", "onDestroy");
    }
 
    @Override
    protected void onPause() {
        // TODO Auto-generated method stub
        super.onPause();
        Log.v("OtherActivity", "onPause");
    }
 
    @Override
    protected void onRestart() {
        // TODO Auto-generated method stub
        super.onRestart();
        Log.v("OtherActivity", "onRestart");
    }
 
    @Override
    protected void onResume() {
        // TODO Auto-generated method stub
        super.onResume();
        Log.v("OtherActivity", "onResume");
    }
 
    @Override
    protected void onStart() {
        // TODO Auto-generated method stub
        super.onStart();
        Log.v("OtherActivity", "onStart");
    }
 
    @Override
    protected void onStop() {
        // TODO Auto-generated method stub
        super.onStop();
        Log.v("OtherActivity", "onStop");
    }
 
}

 效果图如下所示

1)首次启动项目,进入MainActivity

02-16 08:35:49.673: V/MainActivity(1303): onCreate
02-16 08:35:49.673: V/MainActivity(1303): onStart
02-16 08:35:49.693: V/MainActivity(1303): onResume

2)按下手机上的“Back”键

02-16 08:40:21.593: V/MainActivity(1303): onPause
02-16 08:40:24.413: V/MainActivity(1303): onStop
02-16 08:40:24.413: V/MainActivity(1303): onDestroy

3)重新打开程序,单击手机上的Home

02-16 08:42:55.133: V/MainActivity(1303): onPause
02-16 08:43:03.983: V/MainActivity(1303): onStop

4)单击程序图标

02-16 08:44:21.963: V/MainActivity(1303): onRestart
02-16 08:44:21.963: V/MainActivity(1303): onStart
02-16 08:44:21.973: V/MainActivity(1303): onResume

上面的情况是单个Activity的时候,下面讲述两个Activity的情况

(1)打开OtherActivity

02-16 08:47:38.743: V/MainActivity(1303): onPause
02-16 08:47:41.423: V/OtherActivity(1303): onCreate
02-16 08:47:41.473: V/OtherActivity(1303): onStart
02-16 08:47:41.473: V/OtherActivity(1303): onResume
02-16 08:47:43.833: V/MainActivity(1303): onStop

2)在OtherActivity,按下“Back”按钮或者按下“关闭当前Activity”按钮

02-16 08:51:22.223: V/OtherActivity(1303): onPause
02-16 08:51:22.583: V/MainActivity(1303): onRestart
02-16 08:51:22.583: V/MainActivity(1303): onStart
02-16 08:51:22.593: V/MainActivity(1303): onResume
02-16 08:51:24.493: V/OtherActivity(1303): onStop
02-16 08:51:24.493: V/OtherActivity(1303): onDestroy

OtherActivity的主题风格,设置成对话框的形式,效果图如下所示

1)打开OtherActivity

02-16 09:04:23.103: V/MainActivity(1390): onPause
02-16 09:04:23.743: V/OtherActivity(1390): onCreate
02-16 09:04:23.743: V/OtherActivity(1390): onStart
02-16 09:04:23.753: V/OtherActivity(1390): onResume

从上面可以看出,如果新打开的Activity不能完全覆盖前面的Activity,  那么前面的Activity就不会调用onStop这个生命周期。

九、Service的生命周期

下面通过创建项目,在service的各个状态回调方法中加入log信息,了解其生命周期

  • 首先创建service类
public class SampleService extends Service {
    final String TAG = "Service";
    
    @Override
    public IBinder onBind(Intent intent) {
        Log.d(TAG, "onBind");
        return null;
    }
    @Override
    public boolean onUnbind (Intent intent) {
        Log.d(TAG, "onUnbind");
        return super.onUnbind(intent);
    }
    @Override
    public void onRebind (Intent intent) {
        super.onRebind(intent);
        Log.d(TAG, "onRebind");
    }
    @Override
    public void onCreate () {
        super.onCreate();
        Log.d(TAG, "onCreate");
    }
    @Override
    public void onDestroy () {
        super.onDestroy();
        Log.d(TAG, "onDestroy");
    }
    @Override
    public void onStart (Intent intent, int startId) {
        super.onStart(intent, startId);
        Log.d(TAG, "onStart");
    }
}
  •   Activity设置监听按钮
public class MainActivitySampleService extends Activity {
    OnClickListener listener;
    ServiceConnection connection;
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.ch6ex1);
        // 定义ServiceConnection对象,用于绑定Service
        connection = new ServiceConnection(){
            @Override
            public void onServiceConnected(ComponentName arg0, IBinder arg1) {
            }
            @Override
            public void onServiceDisconnected(ComponentName arg0) {
            }
        };
        /*定义Button的点击监听器*/
        listener = new OnClickListener(){
            @Override
            public void onClick(View v) {
                Intent i = new Intent(MainActivitySampleService.this,SampleService.class);
                switch (v.getId()) {
                case R.id.startService:
                    startService(i);
                    break;
                case R.id.stopService:
                    stopService(i);
                    break;
                case R.id.bindService:
                    bindService(i, connection, BIND_AUTO_CREATE);
                    break;
                case R.id.unbindService:
                    unbindService(connection);
                    break;
                default:
                    break;
                }
            }
        };
        /*设置点击监听器*/
     findViewById(R.id.startService).setOnClickListener(listener);
        findViewById(R.id.stopService).setOnClickListener(listener);
        findViewById(R.id.bindService).setOnClickListener(listener);
        findViewById(R.id.unbindService).setOnClickListener(listener);
    } 
}

  • 按下四个按钮的流程信息
StartService:
03-29 14:46:33.801: D/Service(17346): onCreate
03-29 14:46:33.802: D/Service(17346): onStart
 
stopService:
03-29 14:47:31.801: D/Service(17346): onDestroy
 
bindService:
03-29 14:48:13.689: D/Service(17346): onCreate
03-29 14:48:13.713: D/Service(17346): onBind
 
unbindService:
03-29 14:48:41.871: D/Service(17346): onUnbind
03-29 14:48:41.871: D/Service(17346): onDestroy

十、BroadcastRecevier的应用

下面通过一个简单的例子来说明,如何处理广播消息。利用android系统启动完毕时,会发送一个action为ACTION_BOOT_COMPLETED的Intent,来实现开机自启动服务。

  • 首次,为了能够接收广播,需要在AndroidManifest.xml中,加入权限
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
  • 然后创建类MyBootRecevier并在onReceiver方法中启动服务
public class MyBootReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        Intent i = new Intent(context,SampleService.class);
        context.startService(i);
    }
}
  • 最后,采用静态注册的方法注册MyBootReceiver
<receiver android:name=".MyBootReceiver">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED" />
    </intent-filter>
</receiver>

十一、 Intent的应用

1、 利用intent在两个Activity之间传递数据

首先,创建intent, bundle对象,bundle存入数据,并通过intent将数据传递给RoutePlanActivity
        Int iLatitude = 10;
        Int iLongtitude = 20;
        Intent intent = new Intent();
        Bundle bundle = new Bundle();
        bundle.putInt("latitude", iLatitude);
        bundle.putInt("longtitude", iLongtitude);
        intent.putExtras(bundle);      
        intent.setClass(MainContentHuntSheActivity.this, RoutePlanActivity.class);
        startActivity(intent); 
 
RoutePlanActivity接收到消息,进行如下处理
Bundle myBundle = this.getIntent().getExtras();
int iLatitude   = myBundle.getInt("latitude");
int iLongtitude = myBundle.getInt("longtitude");

2、 通过intent,实现信息分享

// 分享短串结果
 Intent it = new Intent(Intent.ACTION_SEND);
   
 String str = "冬日暖曲,一款时尚音乐软件,界面简洁实用哦!";
 it.putExtra(Intent.EXTRA_TEXT, str);
 it.setType("text/plain");
 ((Activity)mainActionView.getContext()).startActivity(Intent.createChooser(it, "将短串分享到"));

十二、分辨率问题

Android资源文件中,各个文件存放的分辨率

目录 说明
drawable-ldpi 240×320
drawable-ldpi 320×480
drawable-hdpi 480×800、480×854
drawable-xhdpi 至少960*720
drawable-xxhdpi 1280×720

长度单位dp、sp和px的区别

  • dp也就是dip,这个和sp基本类似。

如果设置表示长度、高度等属性时可以使用dp或sp。但如果设置字体,需要使用sp。

  • dp是与密度无关,sp除了与密度无关外,还与scale无关。如果屏幕密度为160,这时dp和sp和px是一样的。
1dp=1sp=1px,但如果使用px作单位,如果屏幕大小不变(假设还是3.2寸),而屏幕密度变成了320。那么原来TextView的宽度设成160px,在密度为320的3.2寸屏幕里看要比在密度为160的3.2寸屏幕上看短了一半。
但如果设置成160dp或160sp的话。系统会自动将width属性值设置成320px的。也就是160 * 320 / 160。其中320 / 160可称为密度比例因子。
  • 如果使用dp和sp,系统会根据屏幕密度的变化自动进行转换

十三、应用程序签名

Android应用程序要发布,并被别人使用,需要进行签名,下面将说明如何进行签名

  • 生产私钥,Android的SDK中,有个工具keytool.exe,专门用来生产私钥。打开cmd, 进入工具keytool.exe的所在目录,执行以下命令,那么,在当前目录就会生成文件test.keystore
keytool -genkey -dname "CN=Zijun Li,OU=Zijun Li,O=Zijun Li,L=shenzhen,S=guangdong,C=0086" -storepass 密码 -keystore test.keystore -keyalg RSA -keypass 密码 -validity 15000
  • 然后在eclipse中,选中项目,右击鼠标,选择Android Tools ->Export Unsigned Application Package…,然后,按照提示执行,最后导出的apk,即为签名的apk,可以提供给别人使用或者上传应用商店

十四、推荐学习

  • 通过android SDK中的api demo进行学习
  • 反编译别人的apk,学习别人的代码,目前有两种方法(方法一,dex2jar和jd-gui; 方法二,apktool)