GoldyMark

GoldyMark 查看完整档案

广州编辑  |  填写毕业院校巴图鲁  |  Java工程师 编辑 goldymark.github.io 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

GoldyMark 回答了问题 · 2018-09-05

数据表设计的一个小小疑问

从领域模型来看,余额不是用户自身的属性,【用户】依赖【余额】,【余额】关联【用户】,所以分开存储更合理。如果因为余额的变更而引起用户信息改变,或删除用户后造成用户余额不可访问,这个听起来是有问题的。况且用户和余额可能分别有各自的状态。

关注 13 回答 11

GoldyMark 提出了问题 · 2018-04-17

如何在dubbo filter里面注入spring bean

我们需要根据应用做用户行为分析,想通过dubbo filter注入当前用户的id,并且从consumer隐式传参给provider,但发现dubbo filter里面无法注入spring bean,请问有方法可以实现吗?

关注 1 回答 0

GoldyMark 赞了文章 · 2018-04-11

在数据库中存储一棵树,实现无限级分类

原文发表于我的博客: https://blog.kaciras.com/article/6/store-tree-in-database

在一些系统中,对内容进行分类是必需的功能。比如电商就需要对商品做分类处理,以便于客户搜索;论坛也会分为很多板块;门户网站、也得对网站的内容做各种分类。

分类对于一个内容展示系统来说是不可缺少的,本博客也需要这么一个功能。众所周知,分类往往具有从属关系,比如铅笔盒钢笔属于笔,笔又是文具的一种,当然钢笔还可以按品牌来细分,每个品牌下面还有各种系列...

这个例子中从属关系具有5层,从上到下依次是:文具-笔-钢笔-XX牌-A系列,但实际中分类的层数却是无法估计的,比如生物中的界门纲目科属种有7级。显然对分类的级数做限制是不合理的,一个良好的分类系统,其应当能实现无限级分类。

本博客的分类标签

在写自己的博客网站时,刚好需要这么一个功能,听起来很简单,但是在实现时却发现,用关系数据库存储无限级分类并非易事。

1.需求分析

首先分析一下分类之间的关系是怎样的,很明显,一个分类下面有好多个下级分类,比如笔下面有铅笔和钢笔;那么反过来,一个下级分类能够属于几个上级分类呢?这其实并不确定,取决于如何对类型的划分。比如有办公用品和家具,那么办公桌可以同时属于这两者,不过这会带来一些问题,比如:我要显示从顶级分类到它之间的所有分类,那么这种情况下就很难决定办公用品和家具显示哪一个,并且如果是多对一,实现上将更加复杂,所以这里还是限定每个分类仅有一个上级分类。

现在,分类的关系可以表述为一父多子的继承关系,正好对应数据结构中的树,那么问题就转化成了如何在数据库中存储一棵树,并且对分类所需要的操作有较好的支持。

对于本博客来说,分类至少需要以下操作:

  1. 对单个分类的增删改查等基本操作
  2. 查询一个分类的直属下级和所有下级,在现实某一分类下所有文章时需要使用
  3. 查询出由顶级分类到文章所在分类之间的所有分类,并且是有序的,用于显示在博客首页文章的简介的左下角
  4. 查询分类是哪一级的,比如顶级分类是1,顶级分类的直属下级是2,再往下依次递增
  5. 移动一个分类,换句话说就是把一个子树(或者仅单个节点)移动到另外的节点下面,这个在分类结构不合理,需要修改时使用
  6. 查询某一级的所有分类

在性能的衡量上,这些操作并不是平等的。查询操作使用的更加频繁,毕竟分类不会没事就改着玩,性能考虑要以查询操作优先,特别是2和3分别用于搜索文章和在文章简介中显示其分类,所以是重中之重。

另外,每个分类除了继承关系外,还有名字,简介等属性,也需要存储在数据库中。每个分类都有一个id,由数据库自动生成(自增主键)。

无限级多分类多存在于企业应用中,比如电商、博客平台等,这些应用里一般都有缓存机制,对于分类这种不频繁修改的数据,即使在底层数据库中存在缓慢的操作,只要上层缓存能够命中,一样有很快的响应速度。但是对于抽象的需求:在关系数据库中存储一棵树,并不仅仅存在于有缓存的应用中,所以设计一个高效的存储方案,仍然有其意义。

下面就以这个卖文具的电商的场景为例,针对这6条需求,设计一个数据库存储方案(对过程没兴趣可以直接转到第4节)。

2.一些常见设计方案的不足

2.1 直接记录父分类的引用

在许多编程语言中,继承关系都是一个类仅继承于一个父类,添加这种继承关系仅需要声明一下父类即可,比如JAVA中extends xxx。根据这种思想,我们仅需要在每个分类上添加上直属上级的id,即可存储它们之间的继承关系。

父id字段存储继承关系

表中parent即为直属上级分类的id,顶级分类设为0。这种方案简单易懂,仅存在一张表,并且没有冗余字段,存储空间占用极小,在数据库层面是一种很好的设计。

那么再看看对操作的支持情况怎么样,第一条单个增改查都是一句话完事就不多说了,删除的话记得把下级分类的id全部改成被删除分类的上级分类即可,也就多一个UPDATE。

第二条可就麻烦了,比如我要查文具的下级分类,预期结果是笔、铅笔、钢笔三个,但是并没有办法通过文具一次性就查到铅笔盒钢笔,因为这两者的关系间接存储在笔这个分类里,需要先查出直属下级(笔),才能够往下查询,这意味着需要递归,性能上一下子就差了很多。

第三条同样需要递归,因为通过一个分类,数据库中只存储了其直属父类,需要通过递归到顶级分类才能获取到它们之间的所有分类信息。

综上所述,最关键的两个需求都需要使用性能最差的递归方式,这种设计肯定是不行的。但还是继续看看剩下的几条吧。

第4个需求:查询分类是哪一级的?这个还是得需要递归或循环,查出所有上级分类的数量即为分类的层级。

移动分类倒是非常简单,直接更新父id即可,这也是这种方案的唯一优势了吧...如果你的分类修改比查询还多不妨就这么做吧。

最后一个查询某一级的所有分类,对于这个设计简直是灾难,它需要先找出所有一级分类,然后循环一遍,找出所有一级分类的子类就是二级分类...如此循环直到所需的级数为之。所以这种设计里,这个功能基本是废了。

这个方式也是一开始就能想到的,在数据量不大(层级不深)的情况下,因为其简单直观的特点,不失为一个好的选择,不过对于本项目来说还不够(本项目立志成为一流博客平台!!!)。

2.2 路径列表

从2.1节中可以看出,__之所以速度慢,就是因为在分类中仅仅存储了直属上级的关系,而需求却要查询出非直属上级。__针对这一点,我们的表中不仅仅记录父节点id,而是将它到顶级分类之间所有分类的id都保存下来。这个字段所保存的信息就像文件树里的路径一样,所以就叫做path吧。

路径列表设计

如图所示,每个分类保存了它所有上级分类的列表,用逗号隔开,从左往右依次是从顶级分类到父分类的id。

查询下级时使用Like运算符来查找,比如查出所有笔的下级:

SELECT id,name FROM pathlist WHERE path LIKE '1,%'

一句话搞定,LIKE的右边是笔的path字段的值加上模糊匹配,并且左联接能够使用索引,的效率也过得去。

查询笔的直属下级也同样可以用LIKE搞定:

SELECT id,name FROM pathlist WHERE path LIKE '%2'

而找出所有上级分类这个需求,直接查出path字段,然后在应用层里分割一下即可获得获得所有上级,并且顺序也能保证。

查询某一级的分类也简单,因为层级越深,path就越长,使用LENGTH()函数作为条件即可筛选出合适的结果。反过来,根据其长度也能够计算出分类的级别。

移动操作需要递归,因为每一个分类的path都是它父类的path加上父类的id,将分类及其所有子分类的path设为其父类的path并在最后追加父类的id即可。

在许多系统中都使用了这种方案,其各方面都具有可以接受的性能,理解起来也比较容易。但是其有两点不足:1.就是不遵守数据库范式,将列表数据直接作为字符串来存储,这将导致一些操作需要在上层解析path字段的值;2.就是字段长度是有限的,不能真正达到无限级深度,并且大字段对索引不利。如果你不在乎什么范式,分类层级也远达不到字段长度的限制,那么这种方案是可行的。

2.3 前序遍历树

这是一种在数据库里存储一棵树的解决方案。它的思想不是直接存储父节点的id,而是以前序遍历中的顺序来判断分类直接的关系。

前序遍历树

假设从根节点开始以前序遍历的方式依次访问这棵树中的节点,最开始的节点(“Food”)第一个被访问,将它左边设为1,然后按照顺序到了第二个阶段“Fruit”,给它的左边写上2,每访问一个节点数字就递增,访问到叶节点后返回,在返回的过程中将访问过的节点右边写也写上数字。这样,在遍历中给每个节点的左边和右边都写上了数字。最后,我们回到了根节点“Food”在右边写上18。下面是标上了数字的树,同时把遍历的顺序用箭头标出来了。

我们称这些数字为左值和右值(如,“Meat”的左值是12,右值是17),这些数字包含了节点之间的关系。因为“Red”有3和6两个值,所以,它是有拥有1-18值的“Food”节点的后续。同样的,可以推断所有左值大于2并且右值小于11的节点,都是有2-11的“Fruit” 节点的后续。这样,树的结构就通过左值和右值储存下来了。

这里就不贴表结构了,这种方式不如前面两种直观。效率上,查询全部下级的需求被很好的解决,而直属下级也可以通过添加父节点id的parent字段来解决。

因为左值更大右值更小的节点是下级节点,反之左值更小、右值更大的就是上级,故需求3:查询两个分类直接的所有分类可以通过左右值作为条件来解决,同样是一次查询即可。

添加新分类和删除分类需要修改在前序遍历中所有在指定节点之后的节点,甚至包括非父子节点。而移动分类也是如此,这个特性就非常不友好,在数据量大的情况下,改动一下可是很要命的。

查询某一级的所有分类,和查询分类是哪一级的,这两个需求无法解决,只能通过parent字段想第一种方式一样慢慢遍历。

综上所述,对于本项目而言,它还不如第二种,所以这个很复杂的方案也得否决掉。

3.新方案的思考

上面几种方案最接近理想的就是第二种,如果能解决字段长度问题和不符合范式,以及需要上层参与处理的问题就好了。不过不要急,先看看第二种方案的的优缺点的本质是什么。

在分析第二种方案的开头就提到,要确保效率,必须要在分类的信息中包含所有上级分类的信息,而不能仅仅只含有直属上级,所以才有了用一个varchar保存列表的字段。但反过来想想,数据库的表本身不就是用来保存列表这样结构化数据集合的工具吗,为何不能做一张关联表来代替path字段呢?

在路径列表的设计中,关键字段path的本质是存储了两种信息:一是所有上级分类的id,二是从顶级分类到每个父分类的距离。 所以另增一张表,含有三个字段:一个是本分类的所有上级的id,一个是本分类的id,再加上该分类到每个上级分类的距离。这样这张表就能够起到与path字段相同的作用,而且还不违反数据库范式,最关键的是它不存在字段长度的限制!

经过一番折腾,终于找到了这个比较完美的方案。事实上在之后的查阅资料中,发现这个方案早就在一些系统中使用了,名叫ClosureTable。

4.基于ClosureTable的无限级分类存储

ClosureTable直译过来叫闭包表?不过不重要,ClosureTable以一张表存储节点之间的关系、其中包含了任何两个有关系(上下级)节点的关联信息

ClosureTable演示

定义关系表CategoryTree,其包含3个字段:

  • ancestor 祖先:上级节点的id
  • descendant 子代:下级节点的id
  • distance 距离:子代到祖先中间隔了几级

这三个字段的组合是唯一的,因为在树中,一条路径可以标识一个节点,所以可以直接把它们的组合作为主键。以图为例,节点6到它上一级的节点(节点4)距离为1在数据库中存储为ancestor=4,descendant=6,distance=1,到上两级的节点(节点1)距离为2,于是有ancestor=1,descendant=6,distance=2,到根节点的距离为3也是如此,最后还要包含一个到自己的连接,当然距离就是0了。

这样一来,不尽表中包含了所有的路径信息,还在带上了路径中每个节点的位置(距离),对于树结构常用的查询都能够很方便的处理。下面看看如何用用它来实现我们的需求。

4.1 子节点查询

查询id为5的节点的直属子节点:

SELECT descendant FROM CategoryTree WHERE ancestor=5 AND distance=1

查询所有子节点:

SELECT descendant FROM CategoryTree WHERE ancestor=5 AND distance>0

查询某个上级节点的子节点,换句话说就是查询具有指定上级节点的节点,也就是ancestor字段等于上级节点id即可,第二个距离distance决定了查询的对象是由上级往下那一层的,等于1就是往下一层(直属子节点),大于0就是所有子节点。这两个查询都是一句完成。

4.2 路径查询

查询由根节点到id为10的节点之间的所有节点,按照层级由小到大排序

SELECT ancestor FROM CategoryTree WHERE descendant=10 ORDER BY distance DESC

查询id为10的节点(含)到id为3的节点(不含)之间的所有节点,按照层级由小到大排序

SELECT ancestor FROM CategoryTree WHERE descendant=10 AND 
distance<(SELECT distance FROM CategoryTree WHERE descendant=10 AND ancestor=3) 
ORDER BY distance DESC

查询路径,只需要知道descendant即可,因为descendant字段所在的行就是记录这个节点与其上级节点的关系。如果要过滤掉一些则可以限制distance的值。

4.3 查询节点所在的层级(深度)

查询id为5的节点是第几级的

SELECT distance FROM CategoryTree WHERE descendant=5 AND ancestor=0

查询id为5的节点是id为10的节点往下第几级

SELECT distance FROM CategoryTree WHERE descendant=5 AND ancestor=10

查询层级(深度)非常简单,因为distance字段就是。直接以上下级的节点id为条件,查询距离即可。

4.4 查询某一层的所有节点

查询所有第三层的节点

SELECT descendant FROM CategoryTree WHERE ancestor=0 AND distance=3

这个就不详细说了,非常简单。

4.5 插入

插入和移动就不是那么方便了,当一个节点插入到某个父节点下方时,它将具有与父节点相似的路径,然后再加上一个自身连接即可。

所以插入操作需要两条语句,第一条复制父节点的所有记录,并把这些记录的distance加一,因为子节点到每个上级节点的距离都比它的父节点多一。当然descendant也要改成自己的。

例如把id为10的节点插入到id为5的节点下方(这里用了Mysql的方言)

INSERT INTO CategoryTree(ancestor,descendant,distance) (SELECT ancestor,10,distance+1 FROM CategoryTree WHERE descendant=5)

然后就是加入自身连接的记录。

INSERT INTO CategoryTree(ancestor,descendant,distance) VALUES(10,10,0)

4.6 移动

节点的移动没有很好的解决方法,因为新位置所在的深度、路径都可能不一样,这就导致移动操作不是仅靠UPDATE语句能完成的,这里选择删除+插入实现移动。

另外,在有子树的情况下,上级节点的移动还将导致下级节点的路径改变,所以移动上级节点之后还需要修复下级节点的记录,这就需要递归所有下级节点。

删除id=5节点的所有记录

DELETE FROM CategoryTree WHERE descendant=5

然后配合上面一节的插入操作实现移动。具体的实现直接上代码吧。

ClosureTableCategoryStore.java是主要的逻辑,这里只展示部分代码

    /**
     * 将一个分类移动到目标分类下面(成为其子分类)。被移动分类的子类将自动上浮(成为指定分类
     * 父类的子分类),即使目标是指定分类原本的父类。
     * <p>
     * 例如下图(省略顶级分类):
     *       1                                     1
     *       |                                   / | \
     *       2                                  3  4  5
     *     / | \             move(2,7)               / \
     *    3  4  5         --------------->          6   7
     *         / \                                 /  / | \
     *       6    7                               8  9  10 2
     *      /    /  \
     *     8    9    10
     *
     * @param id 被移动分类的id
     * @param target 目标分类的id
     * @throws IllegalArgumentException 如果id或target所表示的分类不存在、或id==target
     */
    public void move(int id, int target) {
        if(id == target) {
            throw new IllegalArgumentException("不能移动到自己下面");
        }
        moveSubTree(id, categoryMapper.selectAncestor(id, 1));
        moveNode(id, target);
    }

    /**
     * 将一个分类移动到目标分类下面(成为其子分类),被移动分类的子分类也会随着移动。
     * 如果目标分类是被移动分类的子类,则先将目标分类(连带子类)移动到被移动分类原来的
     * 的位置,再移动需要被移动的分类。
     * <p>
     * 例如下图(省略顶级分类):
     *       1                                     1
     *       |                                     |
     *       2                                     7
     *     / | \           moveTree(2,7)         / | \
     *    3  4  5         --------------->      9  10  2
     *         / \                                   / | \
     *       6    7                                 3  4  5
     *      /    /  \                                     |
     *     8    9    10                                   6
     *                                                    |
     *                                                    8
     *
     * @param id 被移动分类的id
     * @param target 目标分类的id
     * @throws IllegalArgumentException 如果id或target所表示的分类不存在、或id==target
     */
    public void moveTree(int id, int target) {
        /* 移动分移到自己子树下和无关节点下两种情况 */
        Integer distance = categoryMapper.selectDistance(id, target);
        if (distance == null) {
            // 移动到父节点或其他无关系节点,不需要做额外动作
        } else if (distance == 0) {
            throw new IllegalArgumentException("不能移动到自己下面");
        } else {
            // 如果移动的目标是其子类,需要先把子类移动到本类的位置
            int parent = categoryMapper.selectAncestor(id, 1);
            moveNode(target, parent);
            moveSubTree(target, target);
        }

        moveNode(id, target);
        moveSubTree(id, id);
    }

    /**
     * 将指定节点移动到另某节点下面,该方法不修改子节点的相关记录,
     * 为了保证数据的完整性,需要与moveSubTree()方法配合使用。
     *
     * @param id 指定节点id
     * @param parent 某节点id
     */
    private void moveNode(int id, int parent) {
        categoryMapper.deletePath(id);
        categoryMapper.insertPath(id, parent);
        categoryMapper.insertNode(id);
    }

    /**
     * 将指定节点的所有子树移动到某节点下
     * 如果两个参数相同,则相当于重建子树,用于父节点移动后更新路径
     *
     * @param id     指定节点id
     * @param parent 某节点id
     */
    private void moveSubTree(int id, int parent) {
        int[] subs = categoryMapper.selectSubId(id);
        for (int sub : subs) {
            moveNode(sub, parent);
            moveSubTree(sub, sub);
        }
    }

其中的categoryMapper 是Mybatis的Mapper,这里只展示部分代码

    /**
     * 查询某个节点的第N级父节点。如果id指定的节点不存在、操作错误或是数据库被外部修改,
     * 则可能查询不到父节点,此时返回null。
     *
     * @param id 节点id
     * @param n 祖先距离(0表示自己,1表示直属父节点)
     * @return 父节点id,如果不存在则返回null
     */
    @Select("SELECT ancestor FROM CategoryTree WHERE descendant=#{id} AND distance=#{n}")
    Integer selectAncestor(@Param("id") int id, @Param("n") int n);

    /**
     * 复制父节点的路径结构,并修改descendant和distance
     *
     * @param id 节点id
     * @param parent 父节点id
     */
    @Insert("INSERT INTO CategoryTree(ancestor,descendant,distance) " +
            "(SELECT ancestor,#{id},distance+1 FROM CategoryTree WHERE descendant=#{parent})")
    void insertPath(@Param("id") int id, @Param("parent") int parent);

    /**
     * 在关系表中插入对自身的连接
     *
     * @param id 节点id
     */
    @Insert("INSERT INTO CategoryTree(ancestor,descendant,distance) VALUES(#{id},#{id},0)")
    void insertNode(int id);

    /**
     * 从树中删除某节点的路径。注意指定的节点可能存在子树,而子树的节点在该节点之上的路径并没有改变,
     * 所以使用该方法后还必须手动修改子节点的路径以确保树的正确性
     *
     * @param id 节点id
     */
    @Delete("DELETE FROM CategoryTree WHERE descendant=#{id}")
    void deletePath(int id);

5.结语

在分析推论后,终于找到了一种既有查询简单、效率高等优点,也符合数据库设计范式,而且是真正的无限级分类的设计。本方案的写入操作虽然需要递归,但相比于前序遍历树效率仍高出许多,并且在本博客系统中分类不会频繁修改。可见对于在关系数据库中存储一棵树的需求,ClosureTable是一种比较完美的解决方案。

完整的JAVA实现代码见 https://github.com/Kaciras/ClosureTableCateogryStore

查看原文

赞 123 收藏 264 评论 10

GoldyMark 赞了问题 · 2018-04-08

Java可能比C++快吗?为什么?

这是一段用C++写的计算十万以内的回文素数算法。

#include <iostream>
using namespace std;
int main()
{
    int input_num=100000;
    int pp_count=0;
    for(int each=2; each<=input_num; each++)
    {
        int factorization_lst=0;
        for(int factor=1; factor<=each; factor++)
            if(each%factor==0&&!(factor>each/factor))
                factorization_lst++;
        if(factorization_lst==1)
        {
            int antitone=0,each_cpy=each;
            while(each_cpy)
            {
                antitone=antitone*10+each_cpy%10;
                each_cpy/=10;
            }
            if(antitone==each)
            {
                pp_count++;
                cout<<pp_count<<':'<<each<<endl;
            }
        }
    }
    return 0;
}

稍微做一下修改的Java版,加了计时相关的部分。

public class main {
    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        int input_num = 100000;
        int pp_count = 0;
        for (int each = 2; each <= input_num; each++) {
            int factorization_lst = 0;
            for (int factor = 1; factor <= each; factor++)
                if (each % factor == 0 && !(factor > each / factor))
                    factorization_lst++;
            if (factorization_lst == 1) {
                int antitone = 0, each_cpy = each;
                while (each_cpy != 0) {
                    antitone = antitone * 10 + each_cpy % 10;
                    each_cpy /= 10;
                }
                if (antitone == each) {
                    pp_count++;
                    System.out.println(pp_count + ":" + each);
                }
            }
        }
        System.out.println(System.currentTimeMillis() - start);
    }
}

执行结果:
Codeblocks
eclipse
同样的算法,C++用了230s,Java只用了124s。这是为什么呢,不是说C++的速度更快吗?

注:运行环境是树莓派3B的官方raspbian(在我的笔记本上运行过,但仅相差一秒不明显,java17s),C++和Java分别用的默认仓库的codeblocks和eclipse(都不是最新版本,eclipse的版本是2012年的3.8.1,codeblocks是2016年的16.01),gcc已经默认开启了-O2优化选项,但还是如此相差悬殊。已经看过类似于这样的解释文章。但还是不太明白。我的代码只有一个main,没有内联函数。Java编译器难道不也是只分指令集的吗,怎么能够编译出更加优化的字节码呢?而且这段代码,Java还能怎么优化呢?


追加:
按照@Untitled(sf没有艾特的功能吗)的提示,做下一个实验证明JIT对Java执行速度的影响。这次使用命令行直接编译,绕过IDE的影响。个人感觉两分钟仅输出百来行的话IO操作对速度的影响可忽略不计。
(由于这次图片屡次上传失败因此只贴出shell相关操作,加上C++编译结果)

pi@raspberrypi:~/workspace/testjava/src $ javac main.java
pi@raspberrypi:~/workspace/testjava/src $ java main
1:2
# 省略计算输出
113:98689
110494
# 110秒,比在eclipse中执行的速度还快,接下来禁用JIT
pi@raspberrypi:~/workspace/testjava/src $ java -Xint main
1:2
# 省略计算输出
113:98689
797514
# 797秒,明显慢于使用JIT的
pi@raspberrypi:~/workspace/testjava/src $ 
# C++编译
pi@raspberrypi:~/cpplearn $ g++ -o main main.cpp
pi@raspberrypi:~/cpplearn $ time ./main
1:2
# 省略计算输出
113:98689

real    4m5.606s
user    4m5.581s
sys    0m0.000s
#245秒,接下来启用-O2选项
pi@raspberrypi:~/cpplearn $ g++ -O2 -o main main.cpp
pi@raspberrypi:~/cpplearn $ time ./main
1:2
# 省略计算输出
113:98689

real    3m50.631s
user    3m50.384s
sys    0m0.010s
# 230秒,快了一点,和在codeblocks编译的速度差不多
pi@raspberrypi:~/cpplearn $ 

JIT确实是大幅度提升了Java的执行速度。(从797110)

看了一下JIT的相关资料(1,2),感觉就算是这样,也不过就是不经过JVM直接执行了Java代码,这和C++的编译原理不是一样的吗?最多只是持平,怎么还会快这么多呢
其实我不懂怎么反汇编,所以也不知道这怎么回事。我的循环也不是空的。可能的话,我想知道Java的JIT是怎么加快执行这段代码的速度的。


追加:
经过几次实验,发现在x86/x64架构中无论是在Windows还是Linux,实体机还是虚拟机,C++的速度在总体上都比Java更胜一筹。arm的设备我除了树莓派,剩下的只有Android手机了。我准备在一台诺基亚7(骁龙630,4GB,原生Android 8.0,无root,已经尽可能关掉所有后台应用,在我看来是相当稳定的测试环境。)上面进行测试。用来测试的软件有两个在手机上运行的IDE(部署Linux Deploy还是太麻烦了)AIDE (用来编译Java代码)和CIDE (用来编译C++代码,编译器为aarch64的gcc7.2)。

由于在CIDE无法显示程序执行时间,因此这次在C++代码也加入了计时

#include <iostream>
#include <ctime>
using namespace std;
int main()
{
    clock_t start = clock();
    int input_num = 100000;
    int pp_count = 0;
    for (int each = 2; each <= input_num; each++)
    {
        int factorization_lst = 0;
        for (int factor = 1; factor <= each; factor++)
            if (each % factor == 0 && !(factor > each / factor))
                factorization_lst++;
        if (factorization_lst == 1)
        {
            int antitone = 0, each_cpy = each;
            while (each_cpy)
            {
                antitone = antitone * 10 + each_cpy % 10;
                each_cpy /= 10;
            }
            if (antitone == each)
            {
                pp_count++;
                cout << pp_count << ':' << each << endl;
            }
        }
    }
    cout << 1000*(clock() - start) / CLOCKS_PER_SEC;
    return 0;
}

优化选项改成使用-O3(默认为-Os)

执行结果:(这已经是我挑选出来所用时间最短的了)

C++用了43s

Java用了37s

.....
(已经经过多次测试)


追加:
听从Untitled的建议使用clang编译(Raspbian默认没有安装,还得自己apt install clang一下)
速度有了质的飞跃。(但还没越过Java)
不使用优化选项:3m22s(202s)
使用-O2选项:3m05s(185s)(使用-O3与-O2的执行速度是差不多的)

顺带一提,我再次执行java版时去掉计时的那两行代码,

    //long start = System.currentTimeMillis();
    //System.out.println(System.currentTimeMillis() - start);

然后使用time命令计时,结果时间延长了零点几秒...


追加:
今晚身体不适,但还是抽出一点时间写了Android上的测试应用。(下载)
在编写过程中,我已经尽量保证了公平。
因为今晚急着早点休息,暂时未进行充分的测试(但大体上C++比Java快很多)。大家可以自行下载测试一下,晚些时候我再发布一下详细测试结果。

主要代码:

MainActivity.java

package ryuunoakaihitomi.javacppperfcomparison;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.WindowManager;
import android.widget.Toast;

import java.util.Timer;
import java.util.TimerTask;

public class MainActivity extends Activity {

    public static final String TAG = "JCPC";

    static {
        System.loadLibrary("native-lib");
    }

    @SuppressWarnings("ConstantConditions")
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
        setContentView(R.layout.activity_main);
        getActionBar().setTitle("logcat -s JCPC");
        Log.i(TAG, "Finding palindromic primes within 100,000.(Waiting for 3s)");
        new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        final long jTime = pcTimer(true);
                        final long cTime = pcTimer(false);
                        runOnUiThread(new Runnable() {
                            @SuppressLint("DefaultLocale")
                            @Override
                            public void run() {
                                Toast.makeText(getApplicationContext(), String.format("java:%d\ncpp:%d", jTime, cTime), Toast.LENGTH_LONG).show();
                                finish();
                            }
                        });
                    }
                }).start();
            }
        }, 3000);
    }

    public native void cpp();

    long pcTimer(boolean isJava) {
        long lStart = System.currentTimeMillis();
        if (isJava)
            Java.kernel();
        else
            cpp();
        long lTime = System.currentTimeMillis() - lStart;
        Log.i(TAG, "total time:" + lTime);
        return lTime;
    }
}

Java.java

package ryuunoakaihitomi.javacppperfcomparison;

public class Java {

    static {
        System.loadLibrary("native-lib");
    }

    static void kernel() {
        int iInputNumber = 100000;
        int iPalprimeCount = 0;
        for (int iEach = 2; iEach <= iInputNumber; iEach++) {
            int iFactorizationList = 0;
            for (int iFactor = 1; iFactor <= iEach; iFactor++)
                if (iEach % iFactor == 0 && !(iFactor > iEach / iFactor))
                    iFactorizationList++;
            if (iFactorizationList == 1) {
                int iAntitone = 0, iEachCopy = iEach;
                while (iEachCopy != 0) {
                    iAntitone = iAntitone * 10 + iEachCopy % 10;
                    iEachCopy /= 10;
                }
                if (iAntitone == iEach) {
                    iPalprimeCount++;
                    ResultPrint(iPalprimeCount, iEach);
                }
            }
        }
    }

    public static native void ResultPrint(int c, int e);
}

native-lib.cpp

#include <jni.h>
#include <android/log.h>
#include <string>

using namespace std;

void kernel();

void kernel_log(string, int, int);

extern "C" JNIEXPORT void

JNICALL
Java_ryuunoakaihitomi_javacppperfcomparison_MainActivity_cpp(
        JNIEnv *,
        jobject /* this */) {
    kernel();
}

void kernel() {
    int input_num = 100000;
    int pp_count = 0;
    for (int each = 2; each <= input_num; each++) {
        int factorization_lst = 0;
        for (int factor = 1; factor <= each; factor++)
            /*Expression can be simplified to 'factor <= each / factor' less... (Ctrl+F1)
            This inspection finds the part of the code that can be simplified, e.g. constant conditions, identical if branches, pointless boolean expressions, etc.*/
            if (each % factor == 0 && factor <= each / factor)
                factorization_lst++;
        if (factorization_lst == 1) {
            int antitone = 0, each_cpy = each;
            while (each_cpy) {
                antitone = antitone * 10 + each_cpy % 10;
                each_cpy /= 10;
            }
            if (antitone == each) {
                pp_count++;
                kernel_log("c", pp_count, each);
            }
        }
    }
}

void kernel_log(string t, int c, int e) {
    __android_log_print(ANDROID_LOG_DEBUG, "JCPC", "%s %d:%d", t.c_str(), c, e);
}

extern "C"
JNIEXPORT void JNICALL
Java_ryuunoakaihitomi_javacppperfcomparison_Java_ResultPrint(JNIEnv *, jobject, jint c,
                                                             jint e) {
    kernel_log("j", c, e);
}

追加:

准备环境:

  • 测试之前已经完全运行过一次
  • 禁用Xposed,暂时冻结了占用后台的应用,电量至少在30%保证稳定供电

实验三次取各自的最小值,实验结果:
说明:表格前四列的值均来自于android.os.Build中对应名称的常量

MODELMANUFACTURERDISPLAYSDK_INTJava耗时C++耗时
GT-I9300samsunglineage_i9300-userdebug 7.1.2 NJH47F 0f9e26b89925192169171928
Redmi 4AXiaomiNJH47F256600931907
m2MeizuFlyme 6.3.0.0A223772234654
A2softwinner升级版四核2G运存19239865202402
Redmi Note 3XiaomiOPM1.171019.018272229918105
TA-1041HMD Global00CN_1_34E263731020234
HTC 802thtcLRX22G release-keys2148211125279

可以看出,绝大多数的arm Android设备运行C++的速度快过Java。但是最后这一行的结果超出了预料。
仅仅是测试之一,读数主要看日志

这个设备的CPU是骁龙600。(好奇怪......)

另:我前两天买了一个香橙派zero plus,用的全志H5。C++45s,java70s

我的所有arm设备已经测试完成,我能不能得到以下结论。

在一小部分的arm指令集架构设备中,Java的运行速度会快于C++。

想知道原因。

关注 14 回答 8

GoldyMark 赞了文章 · 2018-03-24

SegmentFault 讲堂一周岁:Keep learning

图片描述

一转眼,我入职 SegmentFault 快接近一年。再回想一下,SegmentFault 讲堂也一周岁了,是时候捋一捋我们这一年都干了些啥,来和我一起回顾下你与讲堂的交集吧~

SegmentFault 讲堂成长轨迹

2017 年 3 月,讲堂正式上线。

2017 年 3 月 14 日,帅气的歪果仁讲师,直播了第一场 Live 讲座。

2017 年 4 月 18 日,讲座折扣券和免费券功能上线。(偷笑脸,可以省点💰了)

2017 年 4 月 27 日,讲座邀请好友获得分成功能上线,每成功邀请一人,你将从讲师所得中获取 30% 的分成。(这回不光可以省💰,还可以赚💰)

2017 年 6 月 20 日,讲座评分和收藏功能上线。(滴!一键收藏我喜欢的讲座,为我支持的讲师打 call )

2017 年 8 月 26 日,老猫发起了第一场视频讲座。

2017 年 9 月 17 日,小马哥发起了第一个系列讲座。

2017 年 9 月 22 日,讲座免费试看功能上线,所有已生成录播的讲座,你均可在购买前试看。(剁手前终于可以瞅瞅讲座的质量啦!)

2017 年 11 月 11 日 - 11 月 13 日,我们搞了个事情:讲堂优惠活动。

2018 年 1 月 2 日 - 1 月 3 日,上线了三堂免费公开课。

以上罗列了一些较为重要的成长节点,产品上的优化还有很多啦 ^O^

你的学习轨迹

这一年里,你可能学习过:

基于 Vue.js 2.x 的 iView 组件开发实践

Java 微服务实践 - Spring Boot 系列(一)初体验

深入剖析 iOS 编译 Clang / LLVM

1 个人如何运维年交易额 30 亿的金融平台

云计算,大数据,人工智能的相遇,相识,相知

如何“四两拨千斤”做好项目管理

......

SegmentFault 讲堂和讲师收到的

SF 讲堂上线以来,讲堂团队收到许多 SFer 的宝贵建议,每一条建议,讲堂 PM 都在思考,也在不断地打磨产品以达到 SFer 的期待。而讲师们,在为大家传道授业时,也收到了许多学员的肯定与支持。

图片描述

图片描述

图片描述

图片描述

你从讲堂中获得的

这一年里,你可能 get 了:

求职面试的奇技淫巧

网站架构思路和设计理念

正确而优雅的撸码姿势

某些技术原理或概念

迅速找到 Bug 并解决的能力

......

一年讲座盘点

这一年,SF 讲堂一共上线了 300 多堂讲座,技术领域上涵盖前端开发、后端开发、移动端开发、运维、大数据等。讲座从内容类型上也分成了四大类:知识体系、项目实战、职业规划、综合。以下,我将一部分优秀讲座分类成专题形式,给大家盘点下。

求职&面试

程序员价值最大化 - 如何在面试中脱颖而出(限时优惠中) @周梦康

硬实力让大家的能力得到提升,软实力让大家的 价值 (工资)得到提升。💪
面试失败了不要怪面试官不识货,因为面试+笔试是面试官唯一能认定你能力的途径。
最近收到过不少简历,也面试了不少人,之前自己也面过不少公司,觉得很有必要把一些经验分享给大家,避免大家走弯路,走错路。

前端面试攻略:避免求职中的“非战斗减员” @Meathill

“非战斗减员”指的就是在未发生战斗的情况下,因为地形、后勤、疫病、自然灾害等导致的部队减员的情况。所谓“出师未捷身先死,长使英雄泪满襟”。在面试求职的时候,有些岗位我们达不到对方的要求,被刷下来很正常;但也有一些机会,明明自己能力是够的,但是连面试的机会都没拿到,我就称之为“非战斗减员”。

造成求职中“非战斗减员”的因素比较多,有些是因为求职者本人比较懒,有些是大家对招聘本身不了解。不过结果都一样,稍不留神,机会就会从手边溜走。这次分享,将介绍我筛选简历、面试别人和我自己作为候选人的经验,帮助大家尽可能避免踩进这些坑。

亚马逊资深面试官教你如何面试 @凯威的讲堂

技术面试是大家很熟悉的过程,相信每个人面试前总是有点紧张。面对心仪的公司,很怕面挂了被关到小黑屋,一年或者半年之内都不能再面试。

有的时候觉得面试官出题不可理喻,有时候觉得自己答得挺好却挂了。到底怎么样才能与面试官愉快地面试?本期讲座,我就将和大家分享帮你面试通关的独门诀窍。

前端面试攻略:肉老师的面试题详解 @Meathill

我的面试题由日常积累而来,包含HTML、CSS、布局、JS、框架、优化、开发习惯等等方面。可深可浅,根据招聘需求来实时调整。我用这套面试题面试了大约200人,有现场也有电话,对它的覆盖面基本满意。事后基本也验证得到验证。

为了照顾初段同学,这次还会分享我对面试的理解,简单的博弈论如零和博弈多和博弈等,方便大家在技术之外提升自己。

PHP笔试面试题精选(一) @纸牌屋弗兰克

本次课程主要围绕 PHP 面试和笔试中经常会出现的一些知识点,但是面试官会在笔试题基础上深入扩展,那么你知道如何更好的回答让面试官满意吗?

面试题目收集自腾讯,迅雷,美图等公司的笔试面试题,以及本人面试经历中印象中的知识点,同时也分享一些面试的经验,相信对你一定有很大的参考价值。本期题目重点涉及基础知识,安全,跨域,及两个简单的设计模式。

实战开发

python爬虫之实战花瓣网 @kimg1234

花瓣网爬虫的实战,主要介绍:1.如何爬取异步加载的网页;2.如何解析请求中的参数;3.headers中的Accept如何应用;4.如何优雅的获取JavaScript中的内容;5.如何解决爬取网页过程中遇到的问题。

Vue实战:打造属于你的博客发布系统 @jrainlau

本次讲座主要针对具有一定Vue.js开发基础的同学。

相信大家已经看过不少关于Vue.js的相关介绍,但可能一直没有灵感或机会去深入尝试。这次讲座将会从0开始,一步一步教你如何通过Vue.js去打造一款先进的博客发布系统。

相比于制作一个“博客页面”,我更倾向于从“工程化”的角度去阐述一个完整的Vue.js项目。从功能设计,环境搭建,编码规范,到具体的项目开发,每一步都值得我们关注。

【前端工程化】玩转Webpack配置 @jrainlau

本次讲座将会从实际项目出发,使用主流的“三个配置文件”的办法,从零开始教你如何进行webpack配置,最终搭建一套完整的开发/生产构建环境。
讲座难度适中,对新手友好,更适合对前端工程化感兴趣,想要加深对webpack理解的同学。

Spring Boot + Redis 实现 论坛系统 @拿客_三产

本系列课程虽然是从实战出发来实现一个论坛系统,但是受限于课时、受众水平不一等原因,课程讲述的内容还是局限于单机 Web 应用,对高并发、集群等内容涉及较少。但是本系列课程的初衷并不是完全手把手交给大家论坛系统的实现,主要侧重点其实是为大家介绍我在学习实践过程中总结出的一套学习技术的思路。

课程知识点:Spring Boot、Spring data redis、Spring Security、Druid 数据库连接池、Mybatis、Kotlin。

被three.js玩坏的地球(限时优惠中) @Chaos

该教程为three.js 可视化入门,讲解了three.js 最常用的可视化领域,也就是制作一个地球,包括绘制点,飞线,以及柱图的绘制。

前端人成长之路

前端工程师的自我修养(限时优惠中) @小胡子哥

这些年,前端领域尘土飞扬,有的公司开始宣扬「大前端」理念;也有公司合并前端和客户端更名为「端团队」;工程师们也在追求着「全栈」的名号……
前端在变,如何在变化中寻求不变,立身于前端的不败之地?小胡子哥将和大家聊一聊前端工程师的自我修养。

前端程序员应该懂点 V8 知识 @justjavac

对于每个前端程序员来讲都有一个终极理想,那就是搞懂 javascript 引擎是如何工作的。javascript 性能经过了两次飞跃:第 1 次飞跃是 2008 年 V8 发布,第 2 次则是 2017 年的 WebAssembly。不过WebAssembly 到底能不能掀起前端的波澜还是未知数,但是 V8 对前端的贡献大家都有目共睹。

讲座主要内容:1.我为什么要研究V8;;2.V8 为什么这么快?;3.动态语言如何进行快速算数运算;4.如何编写高性能的 JS 代码;5.ES 新特质以及 V8 对 ES 新特性的支持;6.可读性 VS 高性能。

Web前端职业技能与规划 @碧青_Kwok

本次分享是总结一下自己从一个前端小白,历经近年前端的快速发展,期间有效学习与实践的经验,并分享对前端这个行业的冷静思考,与对新“入坑”的同学提出一些建议。内容分两大块,分别是前端开发的技能体系和我眼中的前端职业素养与规划。

前端工程师应掌握的网络知识 @碧青_Kwok

网络协议是 Web 技术的基础设施,虽然大多数前端工程师不用直接面向 HTTP、TCP 这些协议编程,但在问题排查、性能优化等方面的能力,必须建立在对网络知识熟练的掌握和理解的基础上。

本课程将讲解前后端通信回路的各项关键网络环节,分析协议及策略(缓存、安全等)等性能及影响,提出优化建议与最佳实践。内容受众:适合网络基础比较薄弱,或在网络性能方面有深入了解意愿的同学参加。

前端知识巩固

JavaScript 异步编程 @王顶

异步编程对于网站前端开发来说,重要性可能还不是太明显,毕竟前端页面的逻辑相对比较简单,也就是 AJAX 应用涉及到远程资源的请求,用到一些异步编程的技术。但是,对于 JavaScript 结合 Node.js,做服务器端编程来说,异步编程就是必须要掌握的了。如果不掌握 JavaScript 异步编程,基本上 Node.js 开发是玩不转的。也就是说,不掌握 JavaScript 异步编程,Node.js 不算入门。本讲座主要介绍四种异步编程的方法以及三种流程控制的实现方式。

前端面试攻略:JavaScript 排序与搜索 @Meathill

从事前端开发的同学很多从页面仔入门,比如说我,自学比例很大,有些时候会无意中忽视一些基础,比如算法、数据结构。这些欠缺在某些时候就会显得很致命,比如说面试,或者处理大量数据的场景。所以希望这样的一场分享能够帮助大家夯实原本不太扎实的基础,将来的开发之路更加顺畅。

这次分享的主要内容有:排序、搜索、例题解析。内容受众:初级前端程序员,有编程基础,能阅读 JS。

javascript面向对象必知必会 @ghostwu

内容包括javascript面向对象常见知识:1,变量提升(也叫词法解释);2,this详解;3,图解对象;4,原型对象(prototype) 与 隐式原型(__proto__)详解;5,原型链查找规则;6,图解3种引用类型( 函数,对象,数组 );7,函数表达式,立即表达式,闭包,模块化开发。

写 CSS 也要开脑洞:万能的 :checked + label @Meathill

你可能不知道,网上那些看起来高大上的表单控件,实现的机制都是 :checked + label。这一对 CSS3 新增的选择器帮助我们将纯 CSS 组件的版图拓展出去一大块。再复合其它的元素和选择器,比如 flexbox、~ 、动画,我们可以开发出更多又好看又好用兼容性又好的表单控件。

通过学习本次分享,您将学会:1.CSS 预处理工具 Stylus 的使用;2.了解到 CSS3 若干新增元素;3.CSS 动画基础。

深入理解布局神器 flexbox @一歩

将一个属性作为一个主题是不是太夸张了?

No,No,No。flexbox 布局相关属性不是一般的多,概念看了一遍又一遍,到实际操作还是无从下手。

本次课程主要向大家讲解 flex 布局的方方面面,从概念到实战。理论和实践相结合,讲解概念的同时进行代码演示。

彻底掌握 JS 异步处理 Promise 和 Async-Await @一歩

本课程旨在让大家快速地学会Promise、Async-Await的使用,脱离ES5时代的回调地狱。
适用人群:前端切图仔、nodejs 业务仔、没事闲的想体验一下ES6 ES7的新特性的。课程风格:撸码+理论。

来,我们一起实现一个 Promise @充电大喵

本次分享将带大家实现一个能够通过所有测试的 Promise/A+ 类,同时也会讲解标准中的一些设定,深入你对 Promise/A+ 标准的理解。

在分享中,你将学习到如下内容:1.Promise 的实现;Promise 标准中一些设计的原因;2.为什么不同的 Promise 库可以交互(即相互调用而不会出错);3.Promise 中常用 helper 函数的实现(如 race,all,catch 等)4.如何测试你自己实现的 Promise 库;5.Promise 与 Deferred 对象的区别及联系;6.其它与 Promise 相关的知识点。

Promise 的 N 种用法 @Meathill

现在大部分浏览器和 Node.js 都已原生支持 Promise,很多类库也开始返回 Promise 对象,即使面对 IE,也有各种降级适配策略。如果您现在还不会使用 Promise,那么我建议您尽快学习一下。

本次分享我准备结合近期的一些开发经验,总结一下 Promise 常见用法,介绍一下我踩过的坑。分享大纲如下:1.什么是 Promise;2.为什么要用 Promise;3.Promise 详解;4.简单范例;5.复杂加载过程;6.改进代码可读性;7.常见错误。

[公益]学习 Vue 你需要知道的 webpack 知识 @KingMario

学习 Vue,诚如其作者尤雨溪在《新手向:Vue 2.0 的建议学习顺序》中突出强调的,了解前端生态/工程化,了解 Webpack 的概念和配置相当重要,本讲座根据在 SegmentFault 回答的各种实际项目中遇到的问题进行归纳和总结,介绍学习 Vue 你需要知道的 webpack 知识,同时也会介绍 Vue-cli 命令行使用 webpack 项目模板所创建项目的配置相关知识、概念和技巧。

组合火力的威力——Vue Dropdown 组件开发示例 @KingMario

本次讲座通过一个 Dropdown 组件开发的演练,展示 Vue 框架在类绑定语法、数据、响应、事件、组件内容、父子组件间通信以及生命周期钩子等方面多种组合火力的威力,解决组件开发中遭遇的常见问题。

面向人群:1.有一定 Vue 开发基础,熟悉其声明式模板语法,了解实例数据、计算属性、watcher……概念和使用方法,了解事件绑定方法及常用修饰符。2.了解 Vue 组件开发,了解组件 props 选项、父子组件间通信方式、通过 slot 进行内容分发。3.对于开发通用 UI 组件感兴趣,或者工作中有基于现有 UI 样式重新造轮子的需求。

Node.js 应用开发系列

Node.js 是 JavaScript 语言的服务器运行环境。Node.js 提供的 API 可以帮助我们快速、高效的构建服务器应用程序。当然,前提是我们能熟练使用 JavaScript 编程语言。本系列讲座由 王顶 讲授,目前已更新至第 14 节。

王顶老师:河北师范大学软件学院讲师,河北师范大学物联网研究院技术总监,拥有微软认证 MCSE、MCP、MCT。

Node.js 应用开发系列(01):Node.js 简介

Node.js 应用开发系列(02):全局对象编程入门

Node.js 应用开发系列(03):Buffer 编程入门

Node.js 应用开发系列(04):模块管理入门

Node.js 应用开发系列(05):事件编程入门

Node.js 应用开发系列(06):流操作入门

Node.js 应用开发系列(07):文件 I/O 操作入门

Node.js 应用开发系列(08):网络编程入门

Node.js 应用开发系列(09):子进程操作入门

Node.js 应用开发系列(10):web 应用开发(上)

Node.js 应用开发系列(10):web 应用开发(下)

Node.js 应用开发系列(11):单元测试入门

Node.js 应用开发系列(12):调试程序入门

Node.js 应用开发系列(14):压缩与解压缩

Java 微服务实践系列

SegmentFault 讲堂里最火的系列讲座之一。讲师:小马哥,一线互联网公司技术专家,十余年 Java EE 从业经验,架构师、微服务布道师。目前主要负责微服务技术实施、架构衍进、基础设施构建等。重点关注云计算、微服务以及软件架构等领域。通过SUN Java(SCJP、SCWCD、SCBCD)以及Oracle OCA等认证。

Spring Boot 为系列讲座,二十节专题直播,时长高达50个小时,包括目前最流行技术,深入源码分析,授人以渔的方式,帮助初学者深入浅出地掌握,为高阶从业人员抛砖引玉。

Spring Cloud 系列课程致力于以实战的方式覆盖所有功能特性,结合小马哥十余年的学习方法和工作经验,体会作者设计意图。结合源码加深理解,最终达到形成系统性的知识和技术体系的目的。

学员评价:相对于世面上的快餐视频、快餐书籍来说,小马哥讲得很入微,好的不仅仅是能帮你找工作,而且是帮你找一个好的工作。——铁拳阿牛

Java 微服务实践 - Spring Boot / Spring Cloud(限时优惠中)

Java 微服务实践 - Spring Boot 系列(限时优惠中)

Java 微服务实践 - Spring Cloud 系列(限时优惠中)

PHPer 进阶之路

PHP单元测试与测试驱动开发 @vimac

这次讲座将分享 PHPUnit 来编写单元测试, 以及通过单元测试的方式来进行测试驱动开发。
内容介绍:1.单元测试是什么;2.为什么要进行单元测试;3.单元测试怎么做;4.如何通过单元测试来进行测试驱动开发。

PHP 进阶之路 - 零基础构建自己的服务治理框架(上)(限时优惠中) @周梦康

PHP 进阶之路 - 零基础构建自己的服务治理框架(下)(限时优惠中) @周梦康

什么是服务治理?
总是听别人分享他们大项目中总会用到服务器治理框架?
大概明白,又不太明白,总有种雾里看花的感觉?
面试的时候老问,深入了又答不上来?
那么这堂课将为你揭开这些困惑!

PHP 进阶之路(限时优惠中) @周梦康

从简单重复的业务中跳出来,看一看架构师是如何工作的,你有多久没有投资自己了。
本系列从大中型项目的架构梳理,到性能提升实战,然后在更大体系的系统下,构造并使用服务治理框架。最后不要拘泥于一门语言,使用 java 快速构建一套 api 服务。

PHP开发者轻松掌握composer三部曲 @阿北

这个系列从composer的安装、使用、命令以及发布各个角度讲解composer的相关知识,提高开发速度。一包烟、一门知识,它们同样重要。

玩转yii2的rbac系列课程 @阿北

作为一个后端,rbac是必须要学的,很多框架都内置了这个权限管理的机制,我们的yii2也一样。
本系列从yii2的acf到rbac,将yii2中的权限管理进行了全面的讲解,同时最后为你提供一个当前最稳定的yii2-admin rbac扩展,让你理念实战两不误。

后端知识巩固

后端工程师必备知识 — 索引(上) @王子亭

后端工程师必备知识 — 索引(下) @王子亭

这个系列分为上下两集,介绍了各种类型的索引能够加速怎样的查询,帮助后端开发者更好地利用索引改进查询性能。上半部分包括对于索引的基本原理介绍、由单个字段构成的索引,以及区分度这个概念。下半部分包括多个字段构成的复合索引、常见的慢查询、数据库性能优化的思路。

Learn Clojure: The Easy Way @jiacai2050

我是 2013年 从 SICP 开始接触 Lisp,之后一直在不断探索这门古老但富有生命力的语言,现在的工作也是以 Clojure 为技术栈的后端开发,深深被其优雅、强大的表达力所吸引,Clojure 作为 21 世纪的 Lisp 方言,除了具有原始 Lisp 的优势,其设计之初就把并发作为一重要特性,不可变的数据结构,STM 都是十分优秀的设计,我已经等不及向大家展示这门语言了。

这应该是国内第一套介绍 Clojure 的视频,我尽了最大能力去整理资料,涵盖 Clojure 语言的方方面面,做到知其然知其所以然,希望为各位学习 Clojure 提供些许帮助。

Redis 系列讲座合集 @拿客_三产

为什么要学习Redis?

Redis 最为目前炙手可热的 Key-Value 数据库,常用做缓存、Session共享中间件,分布式锁等等。

很多企业都要求要熟悉 Redis 的使用。所以学会使用 Redis 可以使你更具竞争力,Java、PHP、Python等主流编程语言开发的项目中 Redis 都有普遍应用,学习 Redis 可以在企业眼中更具吸引力。虽然 Redis 受到开发者和企业的喜爱,但是在实际应用中却局限于缓存等常见场景,并且大多数开发人员对 Redis 的使用场景以及调优一知半解。

本系列课程主要由浅及深为大家提供更多 Redis 应用场景以及相关调优方法。

容器技术

本系列课程主要面向一线的开发和运维人员,帮助开发和运维掌握 Kubernetes 的使用和维护,了解Kubernetes的架构,了解如何扩展 Kubernetes。本系列讲座由 青云QingCloud 讲授,目前已更新至第 5 节。

讲师:王渊命,青云 QingCloud 知行学院讲师,青云 QingCloud 容器平台负责人,曾任新浪微博架构师、微米技术总监、Grouk 技术负责人,他是云与容器的深度实践者,重度工具控。目前在青云 QingCloud 负责容器平台的相关开发,目标是让各种容器平台更好地运行在 QingCloud 之上。

预备课:深入理解 Docker 内部原理及网络配置

第一课:10个小时,深入掌握Kubernetes以及Kubernetes应用实践

第二课:Kubernetes 的安装和运维

第三课:Kubernetes 的网络和存储

第四课:Kubernetes 的 API Spec 以及安全机制

Android 开发必修课

本系列课程主要面向 Android 初学者,旨在帮助大家搞懂 Android 开发中的方方面面。本系列讲座由 阿里巴巴千牛安卓 讲授,讲师们均为阿里巴巴资深无线开发工程师,目前已更新至第 4 节。

Android 资源文件那些事儿

Android 线程同步那些事儿

Android 开发之Activity那些事儿

Android 进程保活那些事儿

如需观看更多讲座 >>> 请乘坐电梯直达

写在最后

这一年,感谢你陪伴着 SegmentFault 讲堂一起成长,看着技术哥哥们修复一个个八阿哥,看着 PM 优化一个个功能点。同时,我们欢迎大家给 SF 讲堂提出更多改进的建议,你的发声是我们前进的动力。

讲师招募令:我们欢迎更多资深的技术人士来 SF 讲堂分享自己的技术知识与心得。如果你具有三年以上的技术从业资历,并在某一技术领域有一定沉淀,可申请成为 SF 讲师,给大家分享你的所思所得。

PS:正值求职季,祝愿跳槽的童鞋们都能找到一个钱多、顺心的工作 ↖(^ω^)↗

查看原文

赞 213 收藏 67 评论 72

GoldyMark 回答了问题 · 2018-03-05

IntelliJ IDEA中如何去除这样的警告(域未赋值)

Alt + enter

关注 4 回答 5

GoldyMark 赞了回答 · 2018-02-04

如果就一行代码 但是很多地方复用 这种情况怎么处理呢 封装成一个全局方法吗 还是直接写?~

前提:有这么一段代码是很多地方都用到的,而且不存在很多每个地方个性化的改变

如果是我,我会放到一个地方,然后统一从一个地方调用(可以是全局,也可以是util之类),目的是为了:

  1. 减少以后可能会有的扩展的难度。万一以后发现需要加更多的东西,那么改起来方便

  2. 减少修改时的工作量。如果要改href,那么就不需要一个一个找去替换,减少出错的可能

  3. 便于debug。你可以准确的知道是从哪里跳转的,而不是很多地方都可能跳转。如果莫名其妙跳转,打断点就直接打到这一个地方,然后向上找就能找到问题所在。而不是需要打很多断点到不同的地方。

  4. 这种简单的逻辑,明显代码的方便简洁的作用远远大于代码的性能,所以我不会考虑性能的问题

关注 7 回答 5

GoldyMark 收藏了文章 · 2018-02-01

状态机引擎选型

状态机引擎选型

date: 2017-06-19 15:50:18

概念

有限状态机是一种用来进行对象行为建模的工具,其作用主要是描述对象在它的生命周期内所经历的状态序列,以及如何响应来自外界的各种事件。在电商场景(订单、物流、售后)、社交(IM消息投递)、分布式集群管理(分布式计算平台任务编排)等场景都有大规模的使用。

状态机的要素
状态机可归纳为4个要素,即现态、条件、动作、次态。“现态”和“条件”是因,“动作”和“次态”是果。详解如下:
①现态:是指当前所处的状态。
②条件:又称为“事件”。当一个条件被满足,将会触发一个动作,或者执行一次状态的迁移。
③动作:条件满足后执行的动作。动作执行完毕后,可以迁移到新的状态,也可以仍旧保持原状态。动作不是必需的,当条件满足后,也可以不执行任何动作,直接迁移到新状态。
④次态:条件满足后要迁往的新状态。“次态”是相对于“现态”而言的,“次态”一旦被激活,就转变成新的“现态”了。

Finite_State_Machine_Logic

状态机动作类型
进入动作(entry action):在进入状态时进行
退出动作:在退出状态时进行
输入动作:依赖于当前状态和输入条件进行
转移动作:在进行特定转移时进行

为什么需要状态机

有限状态机是一种对象行为建模工具,适用对象有一个明确并且复杂的生命流(一般而言三个以上状态),并且在状态变迁存在不同的触发条件以及处理行为。从我个人的使用经验上,使用状态机来管理对象生命流的好处更多体现在代码的可维护性、可测试性上,明确的状态条件、原子的响应动作、事件驱动迁移目标状态,对于流程复杂易变的业务场景能大大减轻维护和测试的难度。

技术选型

有限状态机的使用场景很丰富,但在技术选型的时候我主要调研了squirrel-foundation(503stars)spring-statemachine(305stars)stateless4j(293stars),这三款finite state machine是github上stars top3的java状态机引擎框架,下面我的一些对比结果。

stateless4j

核心模型

stateless4j是这三款状态机框架中最轻量简单的实现,来源自stateless(C#版本的FSM)

stateless4j.jpeg

  • StateRepresentation状态表示层,状态对应,注册了每状态的entry exit action,以及该状态所接受的triggerBehaviours;
  • StateConfiguration状态节点的配置实例,通过StateMachineConfig.configure创建,由stateRepresentation组成;
  • StateMachineConfig状态机配置,负责了全局状态机的创建以及保存,维护了了state到对应StateRepresentation的映射,通过当前状态找到对应的stateRepresentation,再根据triggerBehaviours执行相应的entry exit action;
  • StateMachine状态机实例,不可共享,记录了状态机实例的当前状态,并通过statemachine实例来响应事件;

核心实现

protected void publicFire(T trigger, Object... args) {
    ...
    //获取triggerBehaviour, destination/trigger/guard
    AbstractTriggerBehaviour<S, T> triggerBehaviour = getCurrentRepresentation().tryFindHandler(trigger);
    if (triggerBehaviour == null) {
        //异常流程,当前state无法处理trigger
        unhandledTriggerAction.doIt(getCurrentRepresentation().getUnderlyingState(), trigger);
        return;
    }
    S source = getState();
    OutVar<S> destination = new OutVar<>();
    //状态迁移,设置目标状态
    if (triggerBehaviour.resultsInTransitionFrom(source, args, destination)) {
        Transition<S, T> transition = new Transition<>(source, destination.get(), trigger);
        //执行source的exit action
        getCurrentRepresentation().exit(transition);
        //执行stateMutator函数回调,设置当前状态为目标destination
        setState(destination.get());
        //执行destination的entry action
        getCurrentRepresentation().enter(transition, args);
    }
}

优缺点

优点

  • 足够轻量,创建StateMachine实例开销小;
  • 支持基本的事件迁移、exit/entry action、guard、dynamic permit(相同的事件不同的condition可到达不同的目标状态);
  • 核心代码千行左右,基于现有代码二次开发的难度也比较低;

缺点

  • 支持的动作只包含了entry exit action,不支持transition action;
  • 在状态迁移的模型中缺少全局的observer(缺少interceptor扩展点),例如要做state的持久化就很恶心(扩展stateMutator在设置目标状态的同时完成持久化的方案将先于entry进行persist实际上并不是一个好的解决方案);
  • 状态迁移的模型过于简单,这也导致了本身支持的action和提供的扩展点有限;

结论

  • stateless4j足够轻量,同步模型,在app中使用比较合适,但在服务端解决复杂业务场景上stateless4j确实略显单薄。

spring statemachine

核心模型

spring-statemachine是spring官方提供的状态机实现。

  • StateMachineStateConfigurer 状态定义,可以定义状态的entry exit action;
  • StateMachineTransitionConfigurer 转换定义,可以定义状态转换接受的事件,以及相应的transition action;
  • StateMachineConfigurationConfigurer 状态机系统配置,包括action执行器(spring statemachine实例可以accept多个event,存储在内部queue中,并通过sync/async executor执行)、listener(事件监听器)等;
  • StateMachineListener 事件监听器(通过Spring的event机制实现),监听stateEntered(进入状态)、stateExited(离开状态)、eventNotAccepted(事件无法响应)、transition(转换)、transitionStarted(转换开始)、transitionEnded(转换结束)、stateMachineStarted(状态机启动)、stateMachineStopped(状态机关闭)、stateMachineError(状态机异常)等事件,借助listener可以trace state transition;
  • StateMachineInterceptor 状态拦截器,不同于StateMachineListener被动监听,interceptor拥有可以改变状态变化链的能力,主要在preEvent(事件预处理)、preStateChange(状态变更的前置处理)、postStateChange(状态变更的后置处理)、preTransition(转化的前置处理)、postTransition(转化的后置处理)、stateMachineError(异常处理)等执行点生效,内部的PersistingStateChangeInterceptor(状态持久化)等都是基于这个扩展协议生效的;
  • StateMachine 状态机实例,spring statemachine支持单例、工厂模式两种方式创建,每个statemachine有一个独有的machineId用于标识machine实例;需要注意的是statemachine实例内部存储了当前状态机等上下文相关的属性,因此这个实例不能够被多线程共享;

核心实现

spring-statemachine.jpeg
AbstractStateMachine#sendEventInternal acceptEvent事件响应

private boolean sendEventInternal(Message<E> event) {
    ...
    try {
        //stateMachineInterceptor事件预处理
        event = getStateMachineInterceptors().preEvent(event, this);
    } catch (Exception e) {
        ...
    }
    if (isComplete() || !isRunning()) {
        notifyEventNotAccepted(buildStateContext(Stage.EVENT_NOT_ACCEPTED, event, null, getRelayStateMachine(), getState(), null));
        return false;
    }
    boolean accepted = acceptEvent(event);
    stateMachineExecutor.execute();
    if (!accepted) {
        notifyEventNotAccepted(buildStateContext(Stage.EVENT_NOT_ACCEPTED, event, null, getRelayStateMachine(), getState(), null));
    }
    return accepted;
}

AbstractStateMachine#acceptEvent 使用队列存储事件

protected synchronized boolean acceptEvent(Message<E> message) {
    State<S, E> cs = currentState;
    ...
    for (Transition<S,E> transition : transitions) {
        State<S,E> source = transition.getSource();
        Trigger<S, E> trigger = transition.getTrigger();
        if (cs != null && StateMachineUtils.containsAtleastOne(source.getIds(), cs.getIds())) {
            //校验当前状态能否接受trigger
            if (trigger != null && trigger.evaluate(new DefaultTriggerContext<S, E>(message.getPayload()))) {
                //存储迁移事件
                stateMachineExecutor.queueEvent(message);
                return true;
            }
        }
    }
    ...
}

DefaultStateMachineExecutor#scheduleEventQueueProcessing 事件处理

private void scheduleEventQueueProcessing() {
    TaskExecutor executor = getTaskExecutor();
    if (executor == null) {
        return;
    }
    Runnable task = new Runnable() {
        @Override
        public void run() {
            boolean eventProcessed = false;
            while (processEventQueue()) {
                //event queue -> tigger queue
                eventProcessed = true;
                //最终的transition得到处理,包括interceptor的preTransition、postTransition以及listener的事件通知都在这个过程中被执行
                //具体实现可参看DefaultStateMachineExecutor.handleTriggerTrans以及AbstractStateMachine中executor的回调实现
                processTriggerQueue();
                while (processDeferList()) {
                    processTriggerQueue();
                }
            }
            if (!eventProcessed) {
                processTriggerQueue();
                while (processDeferList()) {
                    processTriggerQueue();
                }
            }
            taskRef.set(null);
            if (requestTask.getAndSet(false)) {
                scheduleEventQueueProcessing();
            }
        }
    };
    if (taskRef.compareAndSet(null, task)) {
        //默认实现为sync executor,执行上面的task
        executor.execute(task);
    } else {
        requestTask.set(true);
    }
}

优缺点

优点

  • Easy to use flat one level state machine for simple use cases.
  • Hierarchical state machine structure to ease complex state configuration.
  • State machine regions to provide even more complex state configurations.
  • Usage of triggers, transitions, guards and actions.
  • Type safe configuration adapter.
  • Builder pattern for easy instantiation for use outside of Spring Application context
  • Recipes for usual use cases
  • Distributed state machine based on a Zookeeper
  • State machine event listeners.
  • UML Eclipse Papyrus modeling.
  • Store machine config in a persistent storage.
  • Spring IOC integration to associate beans with a state machine.
  • listener、interceptor机制方便状态机monitor以及持久化扩展;

缺点

结论

  • spring statemachine由spring组织孵化,长远来看应该会逐渐走上成熟,但目前而言确实太年轻,离业务的落地使用上确实还有太多坑要踩,鉴于这些原因我也没有选择这个方案。

squirrel-foundation

核心模型

squirrel-foundation是一款很优秀的开源产品,推荐大家阅读以下它的源码。相较于spring statemachine,squirrel的实现更为轻量,设计域也很清晰,对应的文档以及测试用例也很丰富。
StateMachineBuilderFactory:StateMachineBuilder工厂类,负责解析状态定义,根据状态定义创建对应的StateMachineBuilder();
StateMachineBuilder:StateMachine构造器,可复用构造器,所有状态机由生成器创建相同的状态机实例共享相同的状态定义;
StateMachine:状态机实例,通过StateMachineBuilder创建,轻量级内存实例,不可共享;支持对afterTransitionCausedException、beforeTransitionBegin、afterTransitionCompleted、afterTransitionEnd、afterTransitionDeclined beforeActionInvoked、afterActionInvoked事件的自定义全局处理流程,作用类似于spring statemachine中的inteceptor;
Condition:squirrel支持动态的transition,同一个state接受相同的trigger,statecontext不一样,到达的目标状态也可以不一样;
StateMachineListener:全局事件监听,包括了TransitionBeginListener、TransitionCompleteListener、TransitionExceptionListener等几类用于监听transition的不同阶段的监听器;

核心实现

image
squirrel的事件处理模型与spring-statemachine比较类似,squirrel的事件执行器的作用点粒度更细,通过预处理,将一个状态迁移分解成exit trasition entry 这三个action event,再递交给执行器分别执行(这个设计挺不错)。
部分核心代码
AbstractStateMachine#internalFire

private void internalFire(E event, C context, boolean insertAtFirst) {
    ...
    if(insertAtFirst) {
        queuedEvents.addFirst(new Pair<E, C>(event, context));
    } else {
        //事件队列
        queuedEvents.addLast(new Pair<E, C>(event, context));
    }
    //事件消费,采用这种模型用来支持sync/async事件消费
    processEvents();
}

AbstractStateMachine#processEvents

private void processEvents() {
    //statemachine是否空闲
    if (isIdle()) {
        writeLock.lock();
        //标记状态机正在忙碌,避免同一个状态机实例的事件消费产生挣用
        setStatus(StateMachineStatus.BUSY);
        try {
            Pair<E, C> eventInfo;
            E event;
            C context = null;
            while ((eventInfo=queuedEvents.poll())!=null) {
                // response to cancel operation
                if(Thread.interrupted()) {
                    queuedEvents.clear();
                    break;
                }
                event = eventInfo.first();
                context = eventInfo.second();
                processEvent(event, context, data, executor, isDataIsolateEnabled);
            }
            ImmutableState<T, S, E, C> rawState = data.read().currentRawState();
            if(isAutoTerminateEnabled && rawState.isRootState() && rawState.isFinalState()) {
                terminate(context);
            }
        } finally {
            //标记空闲
            if(getStatus()==StateMachineStatus.BUSY)
                setStatus(StateMachineStatus.IDLE);
            writeLock.unlock();
        }
    }
}

AbstractStateMachine#processEvent

private boolean processEvent(E event, C context, StateMachineData<T, S, E, C> originalData,
            ActionExecutionService<T, S, E, C> executionService, boolean isDataIsolateEnabled) {
    ...
    try {
        //执行StateMachine中定义的transitionBegin回调
        beforeTransitionBegin(fromStateId, event, context);
        //执行注册的listener中transitionBegin回调
        fireEvent(new TransitionBeginEventImpl<T, S, E, C>(fromStateId, event, context, getThis()));
        //明确事件是否可被accept
        TransitionResult<T, S, E, C> result = FSM.newResult(false, fromState, null);
        StateContext<T, S, E, C> stateContext = FSM.newStateContext(this, localData, 
                fromState, event, context, result, executionService);
        //执行Condition确认目标状态,生成exit state--transition-->entry state 三个内部事件,通过executor的actionBucket存储
        fromState.internalFire(stateContext);
        toStateId = result.getTargetState().getStateId();
        if(result.isAccepted()) {
            //真正执行actionBucket中存储的exit--transition-->entry action
            executionService.execute();
            localData.write().lastState(fromStateId);
            localData.write().currentState(toStateId);
            //执行listener的transitionComplete回调
            fireEvent(new TransitionCompleteEventImpl<T, S, E, C>(fromStateId, toStateId, 
                    event, context, getThis()));
            //执行StateMachine中声明的transitionCompleted函数回调
            afterTransitionCompleted(fromStateId, getCurrentState(), event, context);
            return true;
        } else {
            //事件无法被处理
            fireEvent(new TransitionDeclinedEventImpl<T, S, E, C>(fromStateId, event, context, getThis()));
            afterTransitionDeclined(fromStateId, event, context);
        }
    } catch (Exception e) {
        //标记statemachine状态为ERROR, 不再响应事件处理直至恢复
        setStatus(StateMachineStatus.ERROR);
        lastException = (e instanceof TransitionException) ? (TransitionException) e :
            new TransitionException(e, ErrorCodes.FSM_TRANSITION_ERROR, 
                    new Object[]{fromStateId, toStateId, event, context, "UNKNOWN", e.getMessage()});
        fireEvent(new TransitionExceptionEventImpl<T, S, E, C>(lastException, fromStateId, 
                localData.read().currentState(), event, context, getThis()));
        afterTransitionCausedException(fromStateId, toStateId, event, context);
    } finally {
        executionService.reset();
        fireEvent(new TransitionEndEventImpl<T, S, E, C>(fromStateId, toStateId, event, context, getThis()));
        //执行StateMachine中声明的transitionEnd函数回调
        afterTransitionEnd(fromStateId, getCurrentState(), event, context);
    }
    return false;
}

优缺点

优点

  • 代码写的不错,设计域很清晰,测试case以及项目文档都比较详细;
  • 功能该有的都有,支持exit、transition、entry动作,状态转换过程被细化为tranistionBegin->exit->transition->entry->transitionComplete->transitionEnd,并且提供了自定义扩展机制,能够方便的实现状态持久化以及状态trace等功能;
  • StateMachine实例创建开销小,设计上就不支持单例复用,因此状态机的本身的生命流管理也更清晰,避免了类似spring statemachine复用statemachine导致的deadlock之类的问题;
  • 代码量适中,扩展和维护相对而言比较容易;

缺点

  • 注解方式定义状态转换,不支持自定义状态枚举、事件枚举;
  • interceptor的实现粒度比较粗,如果需要对特定状态的某些切入点进行逻辑处理需要在interceptor内部进行逻辑判断,例如在transitionEnd后某些状态下需要执行一些特定action,需要transitionEnd回调中分别处理;

结论:

  • 目前项目已经使用squirrel-foundation完成改造并上线,后面会详细介绍下项目中是如何落地实施squirrel-foundation状态机改造以及如何与spring集成的一些细节;

转载请注明出处
qrcode_for_gh_7133ad70f318_258.jpg

查看原文

GoldyMark 赞了文章 · 2018-01-23

5分钟把任意网站变成桌面软件

以前,开发一个桌面软件要花费大量的人力和时间。现在,随着web技术的快速发展,很多业务逻辑已经在网站上实现。既然如此,能不能把网站快速转变成软件呢?这方面的实践已经有很多,早期的Qt,后来的Electron,都可以实现跨平台桌面软件的开发。不就是内嵌一个浏览器么?能不能快一些?再快一些?今天,给大家介绍一个工具,让你5分钟之内就把一个网站变成一个可安装的桌面软件。

制作软件

让我们以https://segmentfaut.com这个网站为例来制作我们的软件。

安装工具

一句话搞定:

npm i -g nativefier

开始制作

一句话搞定:

nativefier "https://segmentfault.com"

运行软件

好了,软件制作好了,看看效果吧:

图片描述

就是这么简单,有没有?

可选步骤

以上是必经步骤,以下是可选步骤。

作人不可过于懒惰,进门之后,多多少少还是需要调整一下的。Nativefier提供了很多选项可供设置,包括应用软件名称、图标、初始窗口尺寸、是否全屏等等等等,具体可以到官网查询。

同时,在设置好这些选项之后,为了便于以后调整和使用,最好是做一个批处理脚本:

#!/bin/bash

nativefier --name "SegmentFault" "https://segmentfault.com/"

调整完参数之后,重新运行这个脚本就可以了。

制作安装包

制作完软件之后,我们得到是一个名为SegmentFault.app的应用程序,虽然已经可以执行了,但看上去不够专业,专业的安装包都是.dmg为后缀的文件,接下来我们就来制作一个.dmg

打开Mac自带的磁盘工具,新建一个空白映像

图片描述

初始时的大小设置为200MB,因为缺省的100MB放不下安装文件,但是这个尺寸后面可以压缩,所以即使设置为300MB也没关系的。

clipboard.png

建好之后,双击图标打开这个文件,把刚才上面做好的SegmentFault.app拷贝进去,然后再在里面建立一个指向Applications文件夹的快捷方式,右键菜单点击显示选项,勾选『始终以图标显示方式打开』,调整图标大小,在最下面挑选一张带箭头的图片作为背景。

clipboard.png

最后,再次打开磁盘工具,先推出刚才的这个文件,然后点击菜单『映像』-『转换』,把它压缩一下,一个完美的dmg安装包就制作好了。

clipboard.png

新的安装包大小大约是51MB,我把它上传到百度网盘了,有需要的同学可以下载安装试用一下。Windows的安装包我就不制作了,制作软件方法类似,只是在制作安装包的时候,Windows要稍微麻烦一些。

怎么样,制作一个桌面软件是不是很容易呢?你也来学着把贵司的网站变成软件吧!

查看原文

赞 84 收藏 484 评论 36

GoldyMark 回答了问题 · 2018-01-03

解决我是一个前端,现在要做android和ios的app,用哪个技术好

最近我们也面临相似的选择问题,目前选型方向定在Weex、DCloud、Native Script上面。

RN学习曲线比较陡峭,而且公司前端的技术栈都是偏向Vue;Ionic是Angular方向,不适合公司前端技术栈。所以没把两者考虑进去。

目前我们的做法是先做几个小demo,体验一下开发效率、运行效率和维护难易度,然后再由前端进行选择。

关注 12 回答 11

认证与成就

  • 获得 150 次点赞
  • 获得 31 枚徽章 获得 2 枚金徽章, 获得 10 枚银徽章, 获得 19 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2014-12-17
个人主页被 2k 人浏览