从MVC到DDD的架构演进 - 木小丰 - 博客园

mikel阅读(443)

来源: 从MVC到DDD的架构演进 – 木小丰 – 博客园

DDD这几年越来越火,资料也很多,大部分的资料都偏向于理论介绍,有给出的代码与传统MVC的三层架构差异较大,再加上大量的新概念很容易让初学者望而却步。本文从MVC架构角度来讲解如何演进到DDD架构。

从DDD的角度看MVC架构的问题

代码角度:

  • 瘦实体模型:只起到数据类的作用,业务逻辑散落到service,可维护性越来越差;
  • 面向数据库表编程,而非模型编程;
  • 实体类之间的关系是复杂的网状结构,成为大泥球,牵一发而动全身,导致不敢轻易改代码;
  • service类承接的所有的业务逻辑,越来越臃肿,很容易出现几千行的service类;
  • 对外接口直接暴露实体模型,导致不必要开放内部逻辑对外暴露,就算有DTO类一般也是实体类的直接copy;
  • 外部依赖层直接从service层调用,字段转换、异常处理大量充斥在service方法中;

项目管理角度:

  • 交付效率:越来越低;
  • 稳定性差:不好测试,代码改动的影响范围不好预估;
  • 理解成本高:新成员介入成本高,长期会导致模块只有一个人最熟悉,离职成本很大;

第一层:初出茅庐

以上的问题越来越严重,很多人开始把眼光转向DDD,于是埋头啃了几本大部头的书,对以下概念有了基本的了解:

  • 统一语言
  • 限界上下文
  • 领域、子域、支撑域
  • 聚合、实体、值对象
  • 分层:用户接口层、应用层、领域层、基础层

于是把MVC架构进行了改造,演进成DDD的分层架构。

DDD分层架构:

image

image

 

MVC架构到DDD分层架构的映射:

image

image

 

至此,算了基本入门了DDD架构,扩展性也得到了一定的提升。不过随着业务的发展,不断冒出新的问题:

  • 一段业务逻辑代码,到底应该放到应用层还是领域层?
  • 领域服务当成原来的MVC中的service层,随着业务不断发展,类也在不断膨胀,好像还是老样子啊?
  • 聚合包含多个实体类,这个接口用不到这么多实体,为了性能还是直接写个SQL返回必要的操作吧,不过这样貌似又回到了MVC模式
  • 既然实体类可以包含业务逻辑、领域服务也可以放业务逻辑,那到底放哪里?
  • 资料上说领域层不能有外部依赖,要做到100%单测覆盖,可是我的领域服务中需要用到外部接口、中央缓存等等,那这不就有了外部依赖了吗?

第二层:草船借箭(战术设计)

带着问题不断学习他人经验,并不断的尝试,逐渐get到以下技能:

1、领域层

领域(domain)是个模块,包含以下组成部分,传统的service按功能可能拆分到任何一个地方,各司其职。

  • 1个聚合
  • 1到多个实体
  • 若干值对象
  • 多个DomainService
  • 1个Factory:新建聚合
  • 1个Repository:聚合仓储服务
聚合根(AggregateRoot)

聚合本身也是一个实体,聚合可以包含其他实体,其他实体不能脱离聚合而单独提供服务,比如一篇文章下的评论,评论必须从属于文章,没有文章也就没有评论。仓库层(repository)也必须是以聚合为核心提供服务的;

实体:可以理解为一张数据库表,必须有主键;

值对象:没有主键,依附于实体而存在,比如用户实体下住址对象,一般在数据库中已json字符串的形式存在;最常见的值对象是枚举;

仓库服务(repository)

资源库是聚合的仓储机制,外部世界通过资源库,而且只能通过资源库来完成对聚合的访问。资源库以聚合的整体管理对象。因此,一个聚合只能有一个资源库对象,那就是以聚合根命名的资源库。除此之外的其他对象,都不应该提供资源库对象。仓储服务的实现一般有Spring Data JPA、Mybatis两种方式。

如果是用Spring Data JPA实现,直接使用JPA注解@OneToOne、@OneToMany,配合fetch配置,即可一个方法查询出所有的关联实体。

如果是用Mybatis实现,那么repository需要加入多个mapper的引用,再手动做拼装。

这里有一个经典的Hibernate笛卡尔积问题,答案是在聚合根中,一般不会加在大量的关联实体对象。如果确实需要查询关联对象而关联对象又比较多怎么办呢?在DDD中有一个CQRS(Command-Query Responsibility Segregation)模式,是一种读写分离模式,在此场景中需要将查询操作放到查询命令中分页查询。

当然CQRS也是一个很复杂模式,不应照搬他人方案,而是根据自己的业务场景选择适合自己的方案,以下列举了CQRS的几种应用模式:

image

image

 

工厂服务(factory)

作用是创建聚合,只传入必要的参数,工厂服务内部隐藏复杂的创建逻辑。简单的聚合可以直接通过new、静态方法等创建,不是必须由factory创建。

领域服务

单个实体对象能处理的逻辑放到实体里,多个实体或有交互的场景放到领域服务里。

领域服务可不可以调用仓储层或外部接口? 可以,但不能直接和领域服务代码放一起,领域服务模块存放API,实现放基础层(infrastructure)。

领域服务对象不建议直接以聚合名+DomainService命名,而要以操作命令关联,比如用户保存服务命名为:UserSaveService, 审核服务:UserAuditSerivce。

2、应用层

应用层通过应用服务接口来暴露系统的全部功能。在应用服务的实现中,它负责编排和转发,它将要实现的功能委托给一个或多个领域对象来实现,它本身只负责处理业务用例的执行顺序以及结果的拼装。通过这样一种方式,它隐藏了领域层的复杂性及其内部实现机制。

比如下订单服务的方法:

  1. public void submitOrder(Long orderId) {
  2. Order order = OrderFetchService.fetchById(orderId); //获取订单对象
  3. OrderCheckSerivce.check(order); //验证订单是否有效
  4. OrderSubmitSerivce.submit(order); //提交订单
  5. ShoppingCartClearService.clear(order); //移除购物车中已购商品
  6. NotifySerivce.emailNotify(order.getUser()); //发送邮件通知买家
  7. }

对于复杂的业务来说,应用层也有几种模式:

  • 编排服务:最典型比如Drools;
  • Command、Query命令模式;
  • 业务按Rhase、Step逐层拆分模式;

image

image

 

3、Maven模块划分

基础层是比较简单一层,不过这里还有个比较疑惑的问题:按照DDD的四层架构图去划分Maven模块,基础层是最上的一层,但是基础层也要包含基础组件供其他层使用,这时基础层应该是放到最下层,直接按照这样构建Maven模块会造成循环依赖。

image

image

 

相比来说,另一个架构图更准确一些,不过依然没有直观体现Maven模块如何划分。

image

image

 

我的最佳实践是将基础层拆分两部分,一部分是基础的组件+仓储API,一部分是实现,maven模块划分图如下所示:

image

image

 

第三层:运筹帷幄(战略设计)

经过以上的两层的磨炼,恭喜你把DDD战术都学习完了,应付日常的代码开发也够了,不过作为架构师来说,探索的道路还不能止步于此,接下来会DDD战略部分。战略部分关注点有3个:

  • 统一语言
  • 领域
  • 限界上下文
1、统一语言

统一语言的重要性可以根据Jeff Patton 在《用户故事地图》中给出的一副漫画来直观的描述:

image

image

 

统一语言是提炼领域知识的输出结果,也是进行后续需求迭代及重构的基础,统一语言的建立有以下几个要点:

  • 统一语言必须以文档的形式提供出来,并且在整个项目组的各团队达成共识;
  • 统一语言必须每个中文名有对应的英文名,并且在整个技术栈保持一致;
  • 统一语言必须是完整的,包含以下要素:
    1. 领域模型的概念与逻辑;
    2. 界限上下文(Bounded Context);
    3. 系统隐喻;
    4. 职责的分层;
    5. 模式(patterns)与惯用法。
2、领域划分

以事件风暴的形式(Event Storming),列出所有的用户故事(Use Story),用户故事可通过6W模型来构建,即描写场景的 Who、What、Why、Where、When 与 hoW 六个要素。然后圈选功能相近的部分,就形成了领域,领域又根据职能不同划分为:核心域、支撑域、通用域,

具体的过程有很多参考资料,这里不再细讲,最终的输出是领域划分图,以下是一个保险业务示例:

image

image

 

3、限界上下文

限界上下文包含两部分:上下文(Context)是业务目标,限界(Bounded)则是保护和隔离上下文的边界。

比如上图中的实现部分即是限界上下文的边界,虚线部分代表了领域的边界。限界上下文没有统一的划分标准,需要的读者根据自己的业务场景来甄别如何划分。

一个上下文中包含了相同的领域知识,角色在上下文中完成动作目标;

边界体现在以下几方面:

  • 领域逻辑层:确定了领域模型的业务边界,维护了模型的完整性与一致性,从而降低系统的业务复杂度;
  • 团队合作层:限界上下文一般也是用户换分团队的依据;
  • 技术实现层:限界上下文可当成是微服务的划分边界;

DDD的不足

DDD架构作为一套先进的方法论,在很多场景能发挥很大价值,但是DDD也不是银弹。高级的架构师把DDD架构当成一种工具,结合其他架构经验一起为业务服务。

DDD的不足有几个方面:

  1. 性能:DDD是基于聚合来组织代码,对于高性能场景下,加载聚合中大量的无用字段会严重影响性能,比如报表场景中,直接写SQL会更简单直接;
  2. 事务:DDD中的事务被限定在限界上下文中,跨多个限界上下文的场景需要开发者额外考虑分布式事务问题;
  3. 难度系数高,推广成本大:DDD项目需要领域专家专家,且需要特别熟悉业务、建模、OOP,对于管理者来说评估一个人是否真的能胜任也是一件困难的事情;

总结

本文从MVC架构开始讲述了如何从演进到DDD架构,限于篇幅很多DDD的知识点没有讲到,希望大家在实践过程中能灵活运用,尽享DDD给业务带来的价值。本文如有不足之处敬请反馈。

本文链接:从MVC到DDD的架构演进

作者简介:木小丰,美团Java技术专家,专注分享软件研发实践、架构思考。欢迎关注公共号:Java研发

mysql日期加一个天数获得新的日期 - php、凯 - 博客园

mikel阅读(861)

来源: mysql日期加一个天数获得新的日期 – php、凯 – 博客园

2012-06-13 20:16 mySQL日期加一个天数获得新的日期

阅读更多

在当前的日期上加三天,天数随便改:

SELECT date_add(CURRENT_DATE(), interval 3 day);

在指定的日期上加三天:
SELECT date_add(‘2014-04-17’, interval 3 day);

在指定的具体时间上加一个时间:

如在”2012年1月1日两点”加上一个小时零十分零十秒的MySQL语句为:

select date_add(‘2014-04-17 2:00:00’, interval ‘1:10:10’ hour_second);

select date_add(日期, interval 1 day);
select date_add(日期, interval 1 hour);
select date_add(日期, interval 1 minute);
select date_add(日期, interval 1 second);
select date_add(日期, interval 1 microsecond);
select date_add(日期, interval 1 week);
select date_add(日期, interval 1 month);
select date_add(日期, interval 1 quarter);
select date_add(日期, interval 1 year);

ThinkPHP vendor 方法导入第三方类库 - 建铭博客 - 博客园

mikel阅读(450)

来源: ThinkPHP vendor 方法导入第三方类库 – 建铭博客 – 博客园

第三方类库

第三方类库指除了 ThinkPHP 框架、应用项目类库之外的其他类库,一般由第三方系统或产品提供,如 Smarty、Zend 等系统的类库等。

前面使用自动加载或 import 方法导入的类库,ThinkPHP 约定是以 .class.php 为后缀的,非这类的后缀,需要通过 import 的参数来控制。

但对第三类库,由于不会有此约定,其后缀只能认为是 php 。为了方便的引入其他框架和系统的类库,ThinkPHP 特意提供了导入第三方类库的功能。第三方类库统一放置在 ThinkPHP系统目录/Vendor 下面,并且使用 vendor 方法导入。

vendor 方法

语法:

boolen vendor(class, baseUrl, ext)

参数说明:

参数    说明
class    必须,表示要导入的类库,采用命名空间的方式。
baseUrl    可选,表示导入的基础路径,省略的话系统采用 ThinkPHP系统目录/Vendor 目录。
ext    可选,表示导入的类库后缀,默认是 .php 。

与 import 方法的区别在于,vendor 方法默认的导入路径为 ThinkPHP系统目录/Vendor 目录,默认后缀为 .php 。

个人经验分享:

当我们想在ThinkPHP中引入第三方扩展,而第三方扩展又没有按照ThinkPHP的规范在编写的时候时,就需要将第三方扩展放置到Library/Vendor目录下,当然,这是针对ThinkPHP 3.2而言,低版本则根据情况来看了。

然后需要在Controller或function中使用第三方扩展时,就可以直接使用vendor()方法来进行引用了。

第三方类库目录结构:

在function函数中使用:

第一种方法:

Vendor('Phpqrcode.phpqrcode');
复制代码
复制代码
/**
 * 生成二维码
 * @param  string  $url  url连接
 * @param  integer $size 尺寸 纯数字
 */
function qrcode($url,$size=4){
    Vendor('Phpqrcode.phpqrcode');
    if (strpos($url, 'http')===false) {
        $url='http://'.$url;
    }
    QRcode::png($url,false,QR_ECLEVEL_L,$size,2,false,0xFFFFFF,0x000000);
}
复制代码
复制代码

第二种方法:

 require './ThinkPHP/Library/Org/Nx/class.phpmailer.php';
 require './ThinkPHP/Library/Org/Nx/class.smtp.php';
复制代码
复制代码
/**
 * 发送邮件
 * @param  string $address 需要发送的邮箱地址 发送给多个地址需要写成数组形式
 * @param  string $subject 标题
 * @param  string $content 内容
 * @return boolean       是否成功
 */
function send_email($address,$subject,$content){
    $email_smtp=C('EMAIL_SMTP');
    $email_username=C('EMAIL_USERNAME');
    $email_password=C('EMAIL_PASSWORD');
    $email_from_name=C('EMAIL_FROM_NAME');
    if(empty($email_smtp) || empty($email_username) || empty($email_password) || empty($email_from_name)){
        return array("error"=>1,"message"=>'邮箱配置不完整');
    }
    require './ThinkPHP/Library/Org/Nx/class.phpmailer.php';
    require './ThinkPHP/Library/Org/Nx/class.smtp.php';
    $phpmailer=new \Phpmailer();
    // 设置PHPMailer使用SMTP服务器发送Email
    $phpmailer->IsSMTP();
    // 设置为html格式
    $phpmailer->IsHTML(true);
    // 设置邮件的字符编码'
    $phpmailer->CharSet='UTF-8';
    // 设置SMTP服务器。
    $phpmailer->Host=$email_smtp;
    // 设置为"需要验证"
    $phpmailer->SMTPAuth=true;
    // 设置用户名
    $phpmailer->Username=$email_username;
    // 设置密码
    $phpmailer->Password=$email_password;
    // 设置邮件头的From字段。
    $phpmailer->From=$email_username;
    // 设置发件人名字
    $phpmailer->FromName=$email_from_name;
    // 添加收件人地址,可以多次使用来添加多个收件人
    if(is_array($address)){
        foreach($address as $addressv){
            $phpmailer->AddAddress($addressv);
        }
    }else{
        $phpmailer->AddAddress($address);
    }
    // 设置邮件标题
    $phpmailer->Subject=$subject;
    // 设置邮件正文
    $phpmailer->Body=$content;
    // 发送邮件。
    if(!$phpmailer->Send()) {
        $phpmailererror=$phpmailer->ErrorInfo;
        return array("error"=>1,"message"=>$phpmailererror);
    }else{
        return array("error"=>0);
    }
}
复制代码
复制代码

第三种方法:

支付宝类库目录结构

vendor('Alipay.AlipaySubmit','','.class.php');

注意说明:Vendor加载的默认后缀是.php的

参数一:必须,表示要导入的类库,采用命名空间的方式

参数二:可选,表示导入的基础路径,省略的话系统采用 ThinkPHP系统目录/Vendor 目录。

参数三:可选,表示导入的类库后缀,默认是 .php 。

支付宝第三方案例代码:

复制代码
复制代码
/**
 * 跳向支付宝付款
 * @param  array $order 订单数据 必须包含 out_trade_no(订单号)、price(订单金额)、subject(商品名称标题)
 */
function alipay($order){
    vendor('Alipay.AlipaySubmit','','.class.php');
    // 获取配置
    $config=C('ALIPAY_CONFIG');
    $data=array(
        "_input_charset" => $config['input_charset'], // 编码格式
        "logistics_fee" => "0.00", // 物流费用
        "logistics_payment" => "SELLER_PAY", // 物流支付方式SELLER_PAY(卖家承担运费)、BUYER_PAY(买家承担运费)
        "logistics_type" => "EXPRESS", // 物流类型EXPRESS(快递)、POST(平邮)、EMS(EMS)
        "notify_url" => $config['notify_url'], // 异步接收支付状态通知的链接
        "out_trade_no" => $order['out_trade_no'], // 订单号
        "partner" => $config['partner'], // partner 从支付宝商户版个人中心获取
        "payment_type" => "1", // 支付类型对应请求时的 payment_type 参数,原样返回。固定设置为1即可
        "price" => $order['price'], // 订单价格单位为元
        // "price" => 0.01, // // 调价用于测试
        "quantity" => "1", // price、quantity 能代替 total_fee。 即存在 total_fee,就不能存在 price 和 quantity;存在 price、quantity, 就不能存在 total_fee。 (没绕明白;好吧;那无视这个参数即可)
        "receive_address" => '1', // 收货人地址 即时到账方式无视此参数即可
        "receive_mobile" => '1', // 收货人手机号码 即时到账方式无视即可
        "receive_name" => '1', // 收货人姓名 即时到账方式无视即可
        "receive_zip" => '1', // 收货人邮编 即时到账方式无视即可
        "return_url" => $config['return_url'], // 页面跳转 同步通知 页面路径 支付宝处理完请求后,当前页面自 动跳转到商户网站里指定页面的 http 路径。
        "seller_email" => $config['seller_email'], // email 从支付宝商户版个人中心获取
        "service" => "create_direct_pay_by_user", // 接口名称 固定设置为create_direct_pay_by_user
        "show_url" => $config['show_url'], // 商品展示网址,收银台页面上,商品展示的超链接。
        "subject" => $order['subject'] // 商品名称商品的标题/交易标题/订单标 题/订单关键字等
    );
    $alipay=new \AlipaySubmit($config);
    $new=$alipay->buildRequestPara($data);
    $go_pay=$alipay->buildRequestForm($new, 'get','支付');
    echo $go_pay;
}
复制代码
复制代码

 

不过当我将PHPMailer放在Vendor目录下后,在本机运行得好好得,最近将程序上传到服务器上时,直接提示Class ‘PHPMailer’ not found然后又在本机运行,还是正确!通过前面这片博客可以知道,我是通过vendor('PHPMailer.class#PHPMailer');

这行代码将PHPMailer引入的。既然提示找不到PHPMailer类,说明没有被正确引入。这是为什么呢?

就粗略看了一下vendor()方法的源码,这才发现其实vendor()方法也就是对import()方法进行了一次参数组装,然后还是交给了import()方法处理。查看import()方法的源码又发现,在import()方法中,对于上面传入参数的解析其实就是将’.’替换成’/’,将’#’替换成了’.’,baseurl则由vendor()方法自动补充上了,指向Vendor目录。所以上面vendor()方法中的参数最终还是被解析成了如下目录:

Library/Vendor/PHPMailer/class.PHPMailer.php

而PHPMailer的入口文件的实际目录地址为:

Library/Vendor/phpmailer/class.phpmailer.php

内容都一样的嘛!不过我用的是Linux的服务器,所以对大小写是严格区分的,这样当然不能成功导入这个类。而解决办法就是将vendor()引入改为:

vendor(‘phpmailer.class#phpmailer’)

另外对于PHPMailer使用时还要注意一点,PHPMailer如果使用SMTP方式发送邮件,需要PHP对fsockopen的支持,所以我们需要修改php.ini中disable_functions中将fscokopen删除,否则会出现运行错误:

fsockopen() has been disabled

通过PHPMailer的ErrorInfo属性可以获取到!

Thinkphp3.2.3整合phpqrcode生成二维码_一颗程序猿-CSDN博客_tp3.2生成二维码

mikel阅读(469)

来源: Thinkphp3.2.3整合phpqrcode生成二维码_一颗程序猿-CSDN博客_tp3.2生成二维码

一、下载phpqrcode
下载地址:https://sourceforge.net/projects/phpqrcode/
下载后解压放到ThinkPHP\Library\Vendor

二、使用
1、调用phpqrcode生成二维码

public function qrcode()
{
$url=”http://www.baidu.com”;
$path = “Public/Uploads/”; //本地文件存储路径
$level=3;
$size=4;
Vendor(‘phpqrcode’);
$errorCorrectionLevel =intval($level) ;//容错级别
$matrixPointSize = intval($size);//生成图片大小 //生成二维码图片
$object = new \QRcode();
$QR =”Public/Uploads/22.png”;
$object->png($url, false, $errorCorrectionLevel, $matrixPointSize, 2);
}

效果

2、生成带logo的二维码
控制器方法

/**
* 生成带背景的二维码图
*/
public function index(){
$productid = I(‘productid’);
$uid = I(‘uid’);
Vendor(‘phpqrcode’);
$value = ‘http://www.baidu.com”; //二维码内容
$errorCorrectionLevel = ‘L’;//容错级别
$matrixPointSize = 6;//生成图片大小 //生成二维码图片
$object = new \QRcode();
$codeurl = ‘code’.$uid;
$object->png($value, ‘Public/’.$codeurl.’.png’, $errorCorrectionLevel, $matrixPointSize, 2);
$logo = ‘Public/’.$codeurl.’.png’;//已经生成的原始二维码图
$QR = ‘Public/Uploads/back.png’;//准备好的logo图片
if ($logo !== FALSE) {
$QR = imagecreatefromstring(file_get_contents($QR));
$logo = imagecreatefromstring(file_get_contents($logo));
$QR_width = imagesx($QR);//二维码图片宽度
$QR_height = imagesy($QR);//二维码图片高度
$logo_width = imagesx($logo);//logo图片宽度
$logo_height = imagesy($logo);//logo图片高度
$logo_qr_width = $QR_width / 5;
$scale = $logo_width/$logo_qr_width;
$logo_qr_height = $logo_height/$scale;
$from_width = ($QR_width – $logo_qr_width) / 2; //重新组合图片并调整大小
imagecopyresampled($QR, $logo, 6, 720, 0, 0, $logo_qr_width, $logo_qr_height, $logo_width, $logo_height);
} //输出图片
$spreadimg = ‘spread’.$uid;//输出图片名字
imagepng($QR, ‘Public/’.$spreadimg.’.png’);
$this->assign(‘spreadimg’,$spreadimg);
$this->display();
}

页面显示

<!DOCTYPE html>
<html lang=”en”>
<head>
<meta charset=”UTF-8″>
<title></title>
</head>
<body>
<img src=”__PUBLIC__/{$spreadimg}.png” alt=”” style=”width:100%;height:auto”/>
</body>
</html>

效果,二维码在左下角
————————————————
版权声明:本文为CSDN博主「林猛男」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/longgeaisisi/article/details/86441481

PHP生成二维码就是这么简单,非常easy! - 掘金

mikel阅读(534)

来源: PHP生成二维码就是这么简单,非常easy! – 掘金

一、下载phpqrcode扩展库

官方下载地址:sourceforge.net/projects/ph…

二、使用phpqrcode扩展库

1、解压后,打开如下图:

2、为了方便调用,我们可以修改phpqrcode.php这个文件名,修改成“QRcode.php”,然后添加命名空间,如下:

3、把phpqrcode文件夹放到extend扩展目录

4、在代码里调用

//引用
use phpqrcode\QRcode;
//调用类库静态方法
$qrcode=QRcode::png('二维码内容',false, '容错级别', '图片大小', '外边距离(白边)	');
复制代码

5、示例

<?php
namespace app\index\controller;
use think\Controller;
use phpqrcode\QRcode;

class Qr extends Controller
{
	/**
     * 生成二维码接口
     */
	public function api(){
		$data=input('');
		!isset($data['text']) && $this->error('参数非法');
		$text  = trim($data['text']); 
		//计算图片尺寸
		$width = isset($data['width']) ? trim($data['width']):100;	
		$size  = floor($width/37*100)/100 + 0.01;
		
		$errorCorrectionLevel =intval(2) ;//容错级别 
      	$matrixPointSize = intval($size); //生成图片大小 
		$margin =0;//外边距离(白边)		
		$qrcode=QRcode::png($text,false, $errorCorrectionLevel, $matrixPointSize, $margin);
		die;
	}
}
?>
复制代码

 

作者:元歌
链接:https://juejin.cn/post/6986282985829957669
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

thinkphp:html中的if condition 嵌套写法、条件判断的各种情况(eq、neq、gt、lt、or、and) - 简书

mikel阅读(484)

gt

来源: thinkphp:html中的if condition 嵌套写法、条件判断的各种情况(eq、neq、gt、lt、or、and) – 简书

伪代码 术语符号
大于 gt
小于 lt
等于 eq
不等于 neq
或者 or
并且 and

变量start_time代表的含义是活动的开始时间,变量end_time代表的含义是活动的结束时间。两个变量都是时间戳的格式。下面就将显示状态一列的数据,各种情况下的判断条件列举出来。


等于 eq

<td>
      <if condition="$vo['start_time'] eq $vo['end_time'] ">进行中
        <else>已结束</else>
        </if>
</td>
image.png

不等于 neq

<td>
      <if condition="$vo['start_time'] neq $vo['end_time'] ">进行中
        <else>已结束</else>
        </if>
</td>
image.png

大于 gt

<td>
      <if condition="$vo['start_time'] gt 0 ">进行中
        <else>已结束</else>
        </if>
</td>
image.png

小于 lt

<td>
      <if condition="$vo['start_time'] lt 0 ">进行中
        <else>已结束</else>
        </if>
</td>
image.png

或者 or

<td>
      <if condition="($vo['start_time'] - $time gt 0) OR ($time neq 666) ">
        <else>已结束</else>
        </if>
</td>
image.png

并且 and

<td>
    <if condition="($vo['start_time'] gt 0) AND ($time eq 666) ">进行中
        <else>已结束</else>
        </if>
</td>
image.png

各种if和else的嵌套

超过一个if else的写法

<div align="center">
                    <if condition="$company_data.approval eq 0 ">
                            <!-- 审核中 -->
                            <img id="imgChange"  src="__TMPL__/public/assets/images/step2.png" alt="">
                    <elseif condition="$company_data.approval eq 1 ">
                            <!-- 审核通过 -->
                            <img id="imgChange"  src="__TMPL__/public/assets/images/step3.png" alt="">
                    <elseif condition="($company_data.approval eq 2) OR ($company_data.approval eq 99) ">
                            <!-- 审核被拒或者未认证 -->
                            <img id="imgChange"  src="__TMPL__/public/assets/images/step1.png" alt="">
                    <else />
                            <!--  其余情况 -->
                            <img id="imgChange"  src="__TMPL__/public/assets/images/step1.png" alt="">  
                   </if>         
                </div>

一个if else的写法

<if condition="$company_data.approval eq 0 ">
                <div align="center" id="step2ID" class="step2_class" style="">
                    <label class="step2Class" style="color:Red;font-size:30px;" id="step2Label">人工审核中,请等待1-2天      
                    </label>
                </div>
          <else>
                 <div align="center" id="step2ID" class="step2_class" style="display:none;">
                     <label class="step2Class" style="color:Red;font-size:30px;" id="step2Label">人工审核中,请等待1-2天  
                     </label>
                 </div>
           </else>

</if>

作者:CoderZb
链接:https://www.jianshu.com/p/d21e8fc73de5
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

Flutter 项目实战 网络请求MD5+时间戳+验证签名 十一_我很好-CSDN博客

mikel阅读(597)

来源: Flutter 项目实战 网络请求MD5+时间戳+验证签名 十一_我很好-CSDN博客

/ MD5 ( Message-Digest Algorithm ) /
MD5消息摘要算法 , 由MD4、MD3、MD2 改进而来,主要增强算法复杂度和不可逆性 , 密码散列函数 。可以生成一个128位(16个字符(BYTES))的散列值(hash value), 128位的MD5散列被表示为32位十六进制数字 , 确保信息完整一致传输 。可以被破解 , 证实MD5算法无法防止碰撞攻击,因此不适用于安全性认证,如SSL公开密钥认证或是数字签名等用途。对于高度安全性的资料 可以使用 SHA-2 算法 。

MD5在线加密 / MD5免费解密 / MD5付费解密

/ MD5验证签名 /
密钥可以是任意字符串 , 为了增加破解难度 ,客户端和服务端协商的密钥字符串长度尽量要长 。密钥分别在客户端和服务端保存了一份 。客户端获取时间戳、签名和其他必备参数添加到Map集合中 , 然后对Map集合按Key的首字母顺序排列 , 最好遍历Map集合分别获取key和value循环拼接到赋值给一个字符串变量 。通过遍历集合拼接好的字符串末尾拼接协商的密钥 , 将这个拼接好的字符串通过Md5算法加密 。

 

/ 时间戳防止重复提交 /
Redis (REmote DIctionary Server) 是 key-value 存储系统,是跨平台的非关系型数据库。开源的使用 ANSI C 语言编写、遵守 BSD 协议、支持网络、可基于内存、分布式、可选持久性的键值对(Key-Value)存储数据库 。Redis 通常被称为数据结构服务器,因为值(value)可以是字符串(String)、哈希(Hash)、列表(list)、集合(sets)和有序集合(sorted sets)等类型。

获取上次存储到redius的时间戳和客户端发起请求到时间戳进行对比 , 以此来校验时间戳是否重复 、session是否超时 。

 

/ Map集合按Key的首字母排序 /
DartPad

排序算法

排序后

/ MD5加密 /
获取 Android 平台密钥签名
打开 Flutter工程 下Android文件 配置密钥 到 gradle.properties (Project Properties)

 

 

build.gradle 下配置 flutterMd5Key (对应的别名 : FLUTTER_MD5_KEY)

 

Android Studio 菜单栏通过 build -> make project 编译项目后在BuildConfig文件下生成 静态的不可变的密钥 FLUTTER_MD5_KEY 。

 

Flutter 与 Android原生建立通信 ,获取密钥 (FLUTTER_MD5_KEY)

 

 

在main函数调用里面需要调用 函数 ensuerInitialized (WidgetFlutterBinding用于与 Flutter 引擎进行交互 ) , Flutter获取Android原生交换返回的密钥 FLUTTER_MD5_KEY 添加到 参与签名的Map集合中 , 最后遍历集合拼接签名参数进行Md5加密 。

WidgetsFlutterBinding.ensureInitialized();
pubspec.yaml 文件下配置 common_utils 依赖库 (包含了md5加密的工具类)

 

 

import ‘package:common_utils/common_utils.dart’;
import ‘package:flutter/cupertino.dart’;
import ‘package:flutter/services.dart’;

void main() async {
///时间戳
var _milSec = (DateTime.now().millisecondsSinceEpoch / 100).toStringAsFixed(0);
/// Map 集合
Map<String, dynamic> encryMap = {
‘g_key’: ‘g_value’,
‘b_key’: ‘b_value’,
‘e_key’: ‘e_value’,
‘a_key’: ‘a_value’,
‘mil_sec’:’$_milSec’
};

/// Map 集合的键存储到 List 集合
List<String> keys = encryMap.keys.toList();

/// Map 集合按照 key 排序
keys.sort((a, b) {
List<int> al = a.codeUnits;
List<int> bl = b.codeUnits;
for (int i = 0; i < al.length; i++) {
if (bl.length <= i) return 1;
if (al[i] > bl[i]) {
return 1;
} else if (al[i] < bl[i]) return -1;
}
return 0;
});
print(‘排序后的key $keys \n’);
var _signStr = ”;
keys.asMap().forEach((k, v) {
encryMap.forEach((pk, pv) {
if (v == pk) {
print(‘键集合keys索引$k 集合encryMap键$pk 集合encryMap值$pv’);
_signStr += pk + “=” + ‘${pv ?? ”}’ + “&”;
}
});
});
WidgetsFlutterBinding.ensureInitialized();
var _flutterNativeKeyValue = await FlutterPlugins.senData(‘flutterSendNavData’);
_signStr = _signStr.substring(0, _signStr.length – 1) + _flutterNativeKeyValue;
///拼接好的签名参数
print(‘拼接好的签名参数 $_signStr’);

var _encMdeSignStr=EncryptUtil.encodeMd5(_signStr);
print(‘Md5签名:$_encMdeSignStr’);
}

class FlutterPlugins {
static const MethodChannel channel =
const MethodChannel(“com.flutter.native.key”);

static Future<String> senData(String result) async {
return await channel.invokeMethod(“flutterNativeKey”, result);
}
}

MD5在线加密 对依赖库的签名算法进行验证

 

获取 IOS 平台密钥签名
使用Xcode工具打开Flutter工程里面的IOS文件夹 , 找到 AppDelegate.swift 文件打开 , 配置密钥完成Flutter从IOS获取密钥的过程

 

 

import UIKit
import Flutter

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {

var methodChannel:FlutterMethodChannel?
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)

///Flutter和IOS通信的唯一标识符 com.flutter.native.key
let vc = self.window.rootViewController as! FlutterViewController
self.methodChannel = FlutterMethodChannel.init(name: “com.flutter.native.key”, binaryMessenger: vc.binaryMessenger)

self.methodChannel!.setMethodCallHandler { (call , result) in
///Flutter调用的函数
if(call.method == “flutterNativeKey”){
///密钥
result(“qazwsxedcrfvtgbyhnujmikolp0987654321”);
}
}
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}

xcode控制台输出的加密参数

/ 调用so文件 /
在当前工程目录下 用这个命令创建flutter工程 native_add

flutter create –platforms=android,ios –template=plugin native_add

在创建的natvie_add工程里面的 ios 文件夹下 Classes 文件夹里面创建 native_add.cpp 文件

 

 

native_add.cpp文件里面配置密钥

#include <stdint.h>
#include <string.h>

#define DART_API extern “C” __attribute__((visibility(“default”))) __attribute__((used))
DART_API const char *greetString(const char *platform) {
char _platform[]=”android”;
if(strcmp(_platform,platform)==0){
return “androidqazwsxedcrfvtgbyhnujmikolp0987654321”;
}
return “iosdqazwsxedcrfvtgbyhnujmikolp0987654321”;
}
在创建的natvie_add工程里面的 Android 文件夹下 创建 CMakeLists.txt 文件用来定义如何编译源文件 , 添加 externalNativeBuild 到 android/build.gradle

 

 

native_add 工程 pubspec.yaml 文件下配置ffi依赖库 并完成依赖加载

 

native_add.dart 文件里面 导入ffi 依赖 , Android平台加载libnative_add.so库 ,工程编译后会生成.so库文件 (build/native_add/intermediates/cmake/Debug/obj路径下)

 

 

 

运行工程查看从 native_add.cpp 获取到的密钥

 

 

 

下载案例
————————————————
版权声明:本文为CSDN博主「xmiaoshen」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/u013491829/article/details/122451678

 Centos宝塔面板清理垃圾空间_Doer的博客-CSDN博客_宝塔怎么清理系统盘垃圾

mikel阅读(1016)

来源: Centos宝塔面板清理垃圾空间_Doer的博客-CSDN博客_宝塔怎么清理系统盘垃圾

宝塔总是用一段时间之后空间被占用非常多,我们明明没有产生什么数据。主要是日志占用了空间,现在我们来清理一下。

第一步、清空回收站
这一步是最简单的,打开回收站清空就行,需要注意的是回收站保留了可能误删的数据,如果重要就不要删了。
如果面板已经打不开了,可以使用指令来清理回收站:
rm -rf /www/Recycle_bin/*

第二步、清理宝塔日志
占用空间最多的主要是日志,宝塔面板的日志可以在宝塔安装【日志清理工具】,扫描之后自动清理。

第三步、清理安全日志
点击宝塔面板的左侧菜单栏【安全】,进去后在右上角可以看到一个
Web日志:/www/wwwlogs284.15 MB [tag type=“success”]清空[/tag]
这样的按钮,点击清空就可以了。

第四步、清理系统日志
CentOS系统中有两个日志服务,分别是传统的 rsyslog 和 systemd-journal
systemd-journald是一个改进型日志管理服务,可以收集来自内核、系统早期启动阶段的日志、系统守护进程在启动和运行中的标准输出和错误信息,还有syslog的日志。

该日志服务仅仅把日志集中保存在单一结构的日志文件/run/log中,由于日志是经历过压缩和格式化的二进制数据,所以在查看和定位的时候很迅速。
经过查找发现/var/log/journal目录下的日志文件占用空间非常大,达到了4G。
可以使用命令du -t 100M /var 或者 journalctl –disk-usage 来查看占用清空
这些日志文件记录了很长时间以来的systemd情况,毫无价值,我们可以使用journalctl工具来清理,具体操作方法如下:

只保留近一周的日志
journalctl –vacuum-time=1w

只保留300M的日志(可以自定义大小)
journalctl –vacuum-size=300M

直接删除 /var/log/journal/ 目录下的日志文件
rm -rf /var/log/journal/f9d400c5e1e8c3a8209e990d887d4ac1

需要注意的是,以上的指令只是暂时清理空间。

journal日志大小永久限制
第四步的方法只是暂时清理,如果需要永久限制大小,需要修改/etc/systemd/journald.conf 配置文件
详细的配置文件说明参考:Linux上 journal 可以删除吗?

永久限制日志大小
打开配置文件sudo vim /etc/systemd/journald.conf,修改参数SystemMaxUse=50M

[tag type=“success”]SystemMaxUse[/tag] 限制全部日志文件总共可以占用多少空间。
修改之后重启生效,重启后日志会自动删减到限制的大小
systemctl restart systemd-journald.service

参考博客:
Linux上 journal 可以删除吗?
Linux /var/log/日志文件太大,清理journal就行
Linux 系统 /var/log/journal/ 垃圾日志清理
————————————————
版权声明:本文为CSDN博主「Mr.Doer」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_38202733/article/details/118894356

万字长文轻松彻底入门 Flutter,秒变大前端 - 知乎

mikel阅读(614)

来源: 万字长文轻松彻底入门 Flutter,秒变大前端 – 知乎

本文真对 Flutter 的技术特性,做了一些略全面的入门级的介绍,如果你听说过Flutter,想去了解它,但是又不想去翻厚厚的API,那么本文就是为你准备的。

随着纯客户端到Hybrid技术,到RN&Weex,再到如今的Flutter技术,客户端实现技术不断前进。 在之前的一个APP项目中,因为历史原因当时选择了weex,随着使用的不断深入,我们逐渐发现了weex的渲染性能问题已经成为一个隐患和瓶颈。 而Flutter技术的不断成熟和流行,Flutter的良好的跨平台性和高性能优点,不断吸引着我们。

(本文包含以下内容,阅读完需要约18分钟)

  • 1.Flutter是啥玩意儿?
  • 2.移动端跨平台技术对比
    2.1 H5+原生APP
    2.2 RN&Weex
    2.3 Flutter
  • 3.Dart语言
  • 4.环境配置
  • 5.Hello World
    5.1 创建项目
    5.2 项目结构
    5.3 启动模拟器
    5.4 启动项目APP
    5.5 简化版的Hello World
    5.6 给页面加上状态
    5.7 小结一下
  • 6.路由
    6.1 单个页面的跳转
    6.2 更多页面跳转使用路由表
    6.3 路由传参
  • 7.widget
    7.1 Text
    7.2 Button
    7.3 Container
    7.4 Image
  • 8.布局
    8.1 Row & Column & Center 行列轴布局
    8.2 Align 角定位布局
    8.3 Stack & Positioned 绝对定位
    8.4 Flex & Expanded 流式布局
  • 9.动画
    9.1 简单动画:淡入淡出
    9.2 复杂一些的动画:放大缩小
  • 10.http请求
    10.1 HttpClient
    10.2 http
    10.3 Dio
  • 11.吐吐槽
    11.1 墙
    11.2 组件过度设计
    11.3 嵌套太多不适应
    11.4 布局修改会导致嵌套关系修改
    11.5 Dart语言升级
    11.6 不能热更新
  • 12.结语

1.Flutter是啥玩意儿?

Flutter是谷歌的移动UI框架,可以快速在iOS和Android上构建高质量的原生用户界面。

  • 具有跨平台开发特性,支持IOS、Android、Web三端。
  • 热重载特性大大提高了开发效率
  • 自绘UI引擎和编译成原生代码的方式,使得系统的运行时的高性能成为了可能
  • 使用Dart语言,目前已经支持同时编译成Web端代码,

到底值不值得跟进Flutter技术呢? 还是看下Flutter,Weex,ReactNative的搜索指数对比,大概就知道这个行业趋势了。

蓝色是Flutter,可以看出上升势头非常强劲。苦逼的前端就是这样,你不跟潮流,潮流就会把你抛弃。

2.移动端跨平台技术对比

为啥会有Flutter这种东西? 他的原理是什么? 他是怎么做到高性能的? 要明白这些问题,我们不得不从几种移动端跨平台技术的对比讲起。

2.1 H5+原生APP

技术门槛最低,接入速度最快,热更新最方便的,自然就是H5方式。APP中提供一个Webview使用H5页面的Http直连。APP和H5可以相互独立开发,JS使用Bridge与原生进行数据通信,显示界面依赖Webview的浏览器渲染。 但是带来的问题也很明显,因为是需要远程直连,那么初次打开H5页面,会有瞬间的白屏,并且Webview本身会有至少几十M的内存消耗。

当然,作为前端开发人员,在H5方式可以使用SPA单页面、懒加载、离线H5等各种前端优化手段进行性能优化,以使得H5的表现更接近原生。但是首次的瞬间白屏和内存,Bridge的通信效率低下,始终是被技术框架给局限住了。

2.2 RN&Weex

由于H5的那些弊端,爱折腾的前端工程师,祭出了RN、Weex两个大杀器, 使用原生去解析RN、Weex的显示配置,显示层、逻辑层都直接与原生数据通信。 因为抛弃了浏览器,自然渲染性能、执行性能都提升了一大截。

但是,每次遇到显示的变更,JS都还会通过Bridge和原生转一道再做渲染的调整,所以Bridge就最后成为了性能的瓶颈。在实际项目中,特别是做一些大量复杂动画处理的时候,由于渲染部分需要频繁通信,性能问题变得尤为突出。 有兴趣的同学可以去看看BindingX,里面有关于动画中数据通信效率低下导致动画帧率低下的详细说明。

2.3 Flutter

不得不佩服Google开发人员的想象力,为了达到极致性能,Flutter更前进了一步,Flutter代码编译完成以后,直接就是原生代码,并且使用自绘UI引擎原生方式做渲染。 Flutter依赖一个Skia 2D图形化引擎。Skia也是Android平台和Chrome的底层渲染引擎,所以性能方面完全不用担心。因为使用Dart做AOT编译成原生,自然也比使用解释性的JS在V8引擎中执行性能更快,并且因为去掉Bridge,没有了繁琐的数据通信和交互,性能就更前进了一步。

3.Dart语言

学习Flutter,得先了解Dart。Dart语言曾经雄心勃勃的要替换JavaScript, 但是发布的时机正好遇到JS的飞速发展,于是就逐渐沉寂,直到配合Flutter的发布,才又重新焕发了生机。

在最近2019年9月的一次Google开发者大会中,伴随着Flutter1.9的发布,目前的Dart也同时更新到了2.5版本, 提供了机器学习和对C跨平台调用的能力。总体来说,Dart语法,对于前端同学,上手还是很容易的,风格很像。

关于Dart语法,请移步传送门:dart.dev/samples

4.环境配置

无论学什么新语言,首先都是环境配置。由于Flutter出自Google,所以有一定门槛,如果在公司内安装,你还需要一个方便的代理切换工具, 比如:Proxifier 。

安装教程,参照官网:flutter.dev/docs/get-st

Flutter支持多种编辑器如:Android Studio , XCode。 但是既然作为支持跨双端的开发,个人还是推荐使用 VSCode

VSCode安装完成后,需要安装Flutter插件,和Dart插件. 在扩展窗口里,搜索Flutter,和Dart,点击“Install”即可,非常方便。

如果安装不上去,记得开启下代理。

5.Hello World

作为一个伟大的程序员,第一行代码总是从Hello World开始。^_^

5.1 创建项目:

方法1:直接使用命令创建:

flutter create projectname

方法2:使用VSCode创建:

View -> Command Palette -> Flutter:New Project 即可

注意请先打开代理,否则你的创建进度,会一直被卡住。

5.2 项目结构

将项目先拖入VSCode,看下目录结构。自动创建完成的项目中,我们看到已经自带了Android,IOS相关的运行环境。

入口主文件是main.dart. 可以打开来先熟悉下,暂时不了解没关系,后面再讲。

还有一个重要的文件是pubspec.yaml ,是项目的配置文件,这个后续也会做修改。

5.3 启动模拟器

点击VSCode右下角的模拟器,启动模拟器。(VSCode会自动找到Android环境、IOS环境下的模拟器,以及真机环境)

5.4 启动项目APP

选中Main.dart, 点击Debug-> Start Debugging , 项目就会启动调试,并在模拟器里运行。

5.5 简化版的Hello World

讲道理,Flutter一上来就用StatefulWidget做一个自增的Demo,其实是对新手不太友好。 我还是喜欢循序渐进,先删掉那些复杂的自增逻辑,我们基于StatelessWidget 只做一个最简单的静态页面显示。(什么是StatefulWidget 和StatelessWidget?后面会说)

main.dart

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget{
   @override
  Widget build(BuildContext context) {
       return Scaffold(
            appBar: AppBar(
              title: Text("我是Title"),
            ),
            body: Center(
                    child: Text(
                        'Hello World',
                    )
            )
      );
  }  
}

在上面的代码中,可以清楚看到,最简单的页面的层级关系:

MaterialApp -> MyHomePage -> Scaffold -> body -> Center -> Text

Scaffold是啥?他是Flutter的页面脚手架,你可以当HTML页面一样去理解,不同的是,他除了Body以外,还提供appBar顶部TitleBar、bottomNavigationBar底部导航栏等属性。

显示效果:

这是最简单的页面,没有交互,只有显示,但是实际业务场景中,是不太可能都是这种页面的,页面上的数据一般都是来自接口返回,然后再在页面上进行动态的渲染。 此时,就需要使用使用带状态的StatefulWidget了

5.6 给页面加上状态

给自己一个需求,按钮点击时,修改页面上显示的文字“Hello World” 变成“You Click Me”

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget{
  @override
  MyHomePageState createState() => MyHomePageState();
}

class MyHomePageState extends State<MyHomePage>{
   var msg="Hello World"; //msg默认文字
   @override
   Widget build(BuildContext context) {
       return Scaffold(
            appBar: AppBar(
              title: Text("我是Title"),
            ),
            body: Center(
                      child:Column(
                              children:<Widget>[
                                  Text(msg), //根据变量值,显示文字
                                  FlatButton(
                                      color: Colors.blue,
                                      textColor: Colors.white,
                                      //点击按钮,修改msg的文字
                                      onPressed: () {
                                        setState(() {
                                          this.msg="You Click ME";
                                        });
                                      },
                                      child: Text(
                                        "Click ME",
                                        style: TextStyle(fontSize: 20.0),
                                      ),
                                  )
                              ]
                      )
                  )
      );
  }  

}

执行效果:

上面最关键的一段代码就是这个:

 onPressed: () {
         setState(() {
                this.msg="You Click ME";
          });
 },

相信写过小程序的同学,对这个 setState 还是很眼熟的 ^_^

5.7 小结一下

StatelessWidget:无状态变更,UI静态固化的Widget, 页面渲染性能更高。
StatefulWidget:因状态变更可以导致UI变更的的Widget,涉及到数据渲染场景,都使用StatefulWidget。

为啥要分两个? StatelessWidget拥有的功能,StatefulWidget都有了啊?

答案只有一个:性能、性能、性能

在StatefulWidget里,因为要维护状态,他的生命周期比StatelessWidget更复杂,每次执行setState,都会触发
window.scheduleFrame() 导致整个页面的widget被刷新,性能就会降低。

使用过小程序的同学在这点上应该有体会,在小程序的官方文档中,会强烈建议减少setData的使用频率,以避免性能的下降。 只不过flutter更是激进,推出了StatelessWidget,并直接在该Widget里砍掉了setState的使用。

页面结构关系如下:

6.路由

实际的项目,是有多个不同的页面的,页面之间的跳转,就要用到路由了。 我们增加一个list页面,点击Home页的“Click Me”按钮,跳转到列表页list。

6.1 单个页面的跳转

增加list.dart

import 'package:flutter/material.dart';

class ListPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    //定义列表widget的list
    List<Widget> list=<Widget>[];

    //Demo数据定义
    var data=[
      {"id":1,"title":"测试数据AAA","subtitle":"ASDFASDFASDF"},
      {"id":2,"title":"测试数据bbb","subtitle":"ASDFASDFASDF"},
      {"id":3,"title":"测试数据ccc","subtitle":"ASDFASDFASDF"},
      {"id":4,"title":"测试数据eee","subtitle":"ASDFASDFASDF"},
    ];

    //根据Demo数据,构造列表ListTile组件list
    for (var item in data) {
      print(item["title"]);

      list.add( ListTile( 
          title: Text(item["title"],style: TextStyle(fontSize: 18.0) ),
          subtitle: Text(item["subtitle"]),
          leading:  Icon( Icons.fastfood, color:Colors.orange ),
          trailing: Icon(Icons.keyboard_arrow_right)
      ));
    }

    //返回整个页面
    return Scaffold(
      appBar: AppBar(
        title: Text("List Page"),
      ),
      body: Center(
        child: ListView(
          children: list,
        )
      ),
    );
  }
}

在main.dart增加list页面的引入

import 'list.dart';

修改Home页的按钮事件,增加Navigator.push跳转

FlatButton(
          color: Colors.blue,textColor: Colors.white,
          onPressed: () {    
                       Navigator.push(context, MaterialPageRoute(builder:(context) {
                                return  ListPage();
                       }));
              },
           child: Text("Click ME",style: TextStyle(fontSize: 20.0) ),
    )

核心方法就是:Navigator.push(context,MaterialPageRoute)

跳转示例:

6.2 更多页面跳转使用路由表

在MaterialApp中,有一个属性是routes,我们可以对路由进行命名,这样跳转的时候,只需要使用对应的路由名字即可,如:Navigator.pushNamed(context, RouterName)。点击两个不同的按钮,分别跳转到ListPage,和Page2去。

Main.dart修改一下如下

import 'package:flutter/material.dart';
import 'list.dart';
import 'page2.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      //路由表定义
      routes:{
        "ListPage":(context)=> ListPage(),
        "Page2":(context)=> Page2(),
      },
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget{
  @override
  MyHomePageState createState() => MyHomePageState();
}

class MyHomePageState extends State<MyHomePage>{
   @override
   Widget build(BuildContext context) {
       return Scaffold(
            appBar: AppBar(
              title: Text("我是Title"),
            ),
            body: Center(
                      child:Column(
                              children:<Widget>[
                                  RaisedButton(
                                      child: Text("Clikc to ListPage" ),
                                      onPressed: () {
                                        //根据命名路由做跳转
                                         Navigator.pushNamed(context, "ListPage");
                                      },
                                  ),
                                   RaisedButton(
                                      child: Text("Click to Page2" ),
                                      onPressed: () {
                                          //根据命名路由做跳转
                                         Navigator.pushNamed(context, "Page2");
                                      },
                                  )

                              ]
                      )
                  )
      );
  }  

}

示例:

当我们有了路由以后,就可以开始在一个项目里用不同的页面,去学习不同的功能了。

6.3 路由传参

列表页跳转到详情页,需要路由传参,这个在flutter体系里,又是怎么做的呢?

首先,在main.dart里,增加详情页DedailPage的路由配置

//路由表定义
      routes:{
        "ListPage":(context)=> ListPage(),
        "Page2":(context)=> Page2(),
        "DetailPage":(context)=> DetailPage(), //增加详情页的路由配置
      },

并修改ListPage里ListTile的点击事件,增加路由跳转传参,这里是将整个item数据对象传递

ListTile( 
          title: Text(item["title"],style: TextStyle(fontSize: 18.0) ),
          subtitle: Text(item["subtitle"]),
          leading:  Icon( Icons.fastfood, color:Colors.orange ),
          trailing: Icon(Icons.keyboard_arrow_right),
          onTap:(){
            //点击的时候,进行路由跳转传参
             Navigator.pushNamed(context, "DetailPage", arguments:item);
          },
      )

详情页DetailPage里,获取传参并显示

import 'package:flutter/material.dart';
class DetailPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
     //获取路由传参
     final Map args = ModalRoute.of(context).settings.arguments;

    return Scaffold(
      appBar: AppBar(
        title: Text("Detail Page"),
      ),
      body: 
        new Column(
          children: <Widget>[
             Text("我是Detail页面"),
             Text("id:${args['id']}" ),
             Text("id:${args['title']}"),
             Text("id:${args['subtitle']}")
          ],
        )
      );
  }
}

Demo效果:

7.widget

Flutter提供了很多默认的组件,而每个组件的都继承自widget 。 在Flutter眼里:一切都是widget。 这句看起来是不是很熟悉? 还记得在webpack里,一切都是module吗? 类似的还有java的一切都是对象。貌似任何一个技术,最后都是用哲学作为指导思想。

widget,作为可视化的UI组件,包含了显示UI、功能交互两部分。大的widget,也可以由多个小的widget组合而成。

常用的widget组件:

7.1 Text

Demo:

Text(
         "Hello world",
         style: TextStyle(
                      fontSize: 50,
                      fontWeight: FontWeight.bold,
                      color:Color(0xFF0000ff)
                  )
    ),

Text的样式,来自另一个widget:TextStyle。 而TextStyle里的color,又是另一个widget Color的实例。

如果用flutter的缩进的方法,看起来确实有点丑陋,习惯写CSS的前端同学,可以看看下面的风格:

Text( "Hello world", style: TextStyle( fontSize: 50,fontWeight: FontWeight.bold,color:Color(0xFF0000ff) ) )

写成一行,是不是就顺眼多了?这算前端恶习吗?^_^

7.2 Button

对于flutter来说,Button就提供了很多种,我们来看看他们的区别:

RaisedButton: 凸起的按钮
FlatButton:扁平化按钮
OutlineButton:带边框按钮
IconButton:带图标按钮

按钮测试页dart:

import 'package:flutter/material.dart';

class ButtonPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {

    return Scaffold(
      appBar: AppBar(
        title: Text("Button Page"),
      ),
      body: Column(
        children: <Widget>[
             RaisedButton(
                  child: Text("我是 RaiseButton" ),
                  onPressed: () {},
              ),
               FlatButton(
                  child: Text("我是 FlatButton" ),
                  color: Colors.blue,
                  onPressed: () {},
              ),
              OutlineButton(
                  child: Text("我是 OutlineButton" ),
                  textColor: Colors.blue,
                  onPressed: () {},
              ),
              IconButton(
                  icon: Icon(Icons.add),
                  onPressed: () {},
              )  
        ]
      )
    );
  }
}

Demo:

项目中要用哪个,就各取所需吧~

7.3 Container

Container是非常常用的一个widget,他一般是用作一个容器。我们先来看看他的基础属性,顺便可以想想他像HTML里的啥?

基础属性:width,height,color,child

body: Center(
        child: Container(
           color: Colors.blue,
           width: 200,
           height: 200,
           child: Text("Hello Container ",style:TextStyle(fontSize: 20,color: Colors.white)),
        )
      )

Padding

我们也可以不设置宽高,用padding在内部撑开增加留白:

Container(
           color: Colors.blue,
           padding: EdgeInsets.all(30),
           child: Text("Hello Container ",style:TextStyle(fontSize: 20,color: Colors.white)),

        )

Margin

我们还可以使用margin,在容器的外部撑开增加偏移量,

Container(
           color: Colors.blue,
           padding: EdgeInsets.all(30),
           margin: EdgeInsets.only(left: 150,top: 0,right: 0,bottom: 0),
           child: Text("Hello Container ",style:TextStyle(fontSize: 20,color: Colors.white)),
        )

Transform

我们还可以给这个矩形,使用tansform做一些变化,比如,旋转一个角度

Container(
           color: Colors.blue,
           padding: EdgeInsets.all(30),
           child: Text("Hello Container ",style:TextStyle(fontSize: 20,color: Colors.white)),
           transform: Matrix4.rotationZ(0.5)
        )

看到这里,好多前端同学要说了,好熟悉啊。 对,他就是很像Html里的一个东西:DIV,你确实可以对应的去加强理解。

7.4 Image

网络图片加载

使用NetworkImage,可以做网络图片的加载:

child:Image(
          image: NetworkImage("https://mat1.gtimg.com/pingjs/ext2020/qqindex2018/dist/img/qq_logo_2x.png"),
           width: 200.0,
        )  

本地图片加载

加载本地图片,就稍微复杂一些,首先要把图片的路径配置,加入到之前说过的pubspec.yaml配置文件里去:

加载本地图片时使用AssetImage:

child:Image(
               image: AssetImage("assets/images/logo.png"),
                width: 200.0,
            )      

也可以使用简写:

 Image.asset("assets/images/logo.png",width:200.0)

flutter提供的组件很多,这里就不一一举例说明,有兴趣的还是建议大家去看API:api.flutter.dev/

8.布局

我们已经了解了这么多组件,那么怎么绘制一个完整的页面呢? 这就到了页面布局的部分了。

8.1 Row & Column & Center 行列轴布局

字面意义也很好理解,行布局、列布局、居中布局,这些布局对于Flutter来说,也都是一个个的widget。

区别在于,row、column 是有多个children的widget, 而Center是只有 1个child的 widget。

 Row(
     children:<Widget>[]
 ) 

 Column(
     children:<Widget>[]
 )    

 Center(
      child:Text("Hello")
 )

8.2 Align 角定位布局

我们常常在Container里,需要显示的内容在左上角,左下角,右上角,右下角。 在html时代,使用CSS可以很容易的实现,但是flutter里,必须依赖Align 这个定位的Widget

右下角定位示例:

 child: Container(
           color: Colors.blue,
           width: 300,
           height: 200,
           child: Align(
                      alignment: Alignment.bottomRight,
                      child:Text("Hello Align ",style:TextStyle(fontSize: 20,color: Colors.white)),
                  )
        )

显示效果:

Alignment提供了多种定位供选择,还算是很贴心的。

8.3 Stack & Positioned 绝对定位

当然还有绝对定位的需求,这在css里,使用position:absolute就搞定了,但是在flutter里,需要借助stack+ positioned两个widget一起组合使用。

Stack: 支持元素堆叠
Positioned:支持绝对定位

child:Stack(
              children: <Widget>[
                  Image.network("https://ossweb-img.qq.com/upload/adw/image/20191022/627bdf586e0de8a29d5a48b86700a790.jpeg"),
                  Positioned(
                    top: 20,
                    right: 10,
                    child:Image.asset("assets/images/logo.png",width:200.0)
                  )
              ],
            )

8.4 Flex & Expanded 流式布局

Flex流式布局作为前端同学都熟悉,之前讲过的Row,Column,其实都是继承自Flex,也属于流式布局。

如果轴向不确定,使用Flex,通过修改direction的值设定轴向
如果轴向已确定,使用Row,Column,布局更简洁,更有语义化

Flex测试页:

class FlexPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {

    return Scaffold(
      appBar: AppBar(
        title: Text("Flex Page"),
      ),
      body:  Flex(
          direction: Axis.horizontal,
          children: <Widget>[
            Container(
              width: 30,
              height: 100,
              color: Colors.blue,
            ),
            Expanded(
              flex: 1,
              child: Container(
                height: 100.0,
                color: Colors.red,
              ),
            ),
            Expanded(
              flex: 1,
              child: Container(
                height: 100.0,
                color: Colors.green,
              ),
            ),
          ],
        ),
    );
  }
}

示例中,轴向横向排列,最左边一个固定宽度的Container,右边两个Expanded,各自占剩下的宽度的一半。

9.动画

Flutter既然说了,一切都是Widget,包括动画实现,也是一个Widget。 我们还是看一个示例

9.1 简单动画:淡入淡出:

使用flutter提供的现成的Widget:

import 'package:flutter/material.dart';

class AnimatePage extends StatefulWidget {
  _AnimatePage  createState()=> _AnimatePage();
} 

class _AnimatePage extends State<AnimatePage> {
  bool _visible=true;
  @override
  Widget build(BuildContext context) {

    return Scaffold(
      appBar: AppBar(
        title: Text("Animate Page"),
      ),
      body: 
          Center(

            child: Column(
                  children: <Widget>[
                      AnimatedOpacity(
                        opacity: _visible ? 1.0:0.0,
                        duration: Duration(milliseconds: 1000),
                        child: Image.asset("assets/images/logo.png"),
                      ),

                      RaisedButton(
                        child: Text("显示隐藏"),
                        onPressed: (){
                          setState(() {
                            _visible=!_visible;
                          });
                         },
                      ),

                  ],
                ),
          )    
      );

  }
}

其中的AnimatedOpacity就是动画透明度变化的的Widget,而被透明度控制变化的Image则是AnimatedOpacity的子元素。这个和以往前端写动画的方式,就完全不一样了,需要改变一下思维方式。

Demo效果

9.2 复杂一些的动画:放大缩小

当写复杂一些动画的时候,没有对应的widget组件,就需要自己使用Animation,和AnimationController,以及Tween来组合。

Animation: 保存动画的值和状态
AnimationController: 控制动画,包含:启动forward()、停止stop()、反向播放reverse()等方法
Tween: 提供begin,end作为动画变化的取值范围
Curve:设置动画使用曲线变化,如非匀速动画,先加速,后减速等的设定。

动画示例:

class AnimatePage2 extends StatefulWidget {
  _AnimatePage  createState()=> _AnimatePage();
} 

class _AnimatePage extends State<AnimatePage2>  with SingleTickerProviderStateMixin {

  Animation<double> animation;
  AnimationController controller;

  initState() {
    super.initState();
    controller =  AnimationController(duration:  Duration(seconds: 3), vsync: this);

     //使用弹性曲线,数据变化从0到300
     animation = CurvedAnimation(parent: controller, curve: Curves.bounceIn);
     animation = Tween(begin: 0.0, end: 300.0).animate(animation)
      ..addListener(() {
        setState(() {
        });
      });


    //启动动画(正向执行)
    controller.forward();
  }

  @override
  Widget build(BuildContext context) {

    return Scaffold(
      appBar: AppBar(
        title: Text("Animate Page"),
      ),
      body: 
          Center(
              child: Image.asset(
                  "assets/images/logo.png",
                  width: animation.value, 
                  height: animation.value
              ),
            )  
      );   
  }

  dispose() {
    //路由销毁时需要释放动画资源
    controller.dispose();
    super.dispose();
  }

}

很重要的一点,在路由销毁的时候,需要释放动画资源,否则容易导致内存泄漏

显示Demo:

10.http请求

做业务逻辑,总离不开http请求,接下来,就来看下flutter的http请求是如何做的。

10.1 HttpClient

httpClient在 dart:io库中,不需要引入第三方库就可以使用,示例代码如下:

使用示例

import 'dart:convert';
import 'dart:io';

Future _getByHttpClient() async{
    //接口地址
    const url="https://www.demo.com/api";

    //定义httpClient
    HttpClient client = new HttpClient();
    //定义request
    HttpClientRequest request = await client.getUrl(Uri.parse(url));
    //定义reponse
    HttpClientResponse response = await request.close();
    //respinse返回的数据,是字符串
    String responseBody = await response.transform(utf8.decoder).join();
    //关闭httpClient
    client.close();
    //字符串需要转化为JSON
    var json= jsonDecode(responseBody);
    return json;

} 

总的看起来,代码还是挺繁琐的,使用起来并不方便。

10.2 http

这是Dart.dev提供的第三方类库,地址:pub.dev/packages/http

需要先在pubspec.yaml里添加类库应用

dependencies:
  flutter:
    sdk: flutter
  json_annotation: ^2.0.0
  http: ^0.12.0+2

使用示例:

Future _getByDartHttp() async {
  // 接口地址
 const url="https://www.demo.com/api";//获取接口的返回值
 final response = await http.get(url);
 //接口的返回值转化为JSON
 var json = jsonDecode(response.body); 
 return json;
}

这种写法,比上面的httpClient简洁了许多。

Dio

国内使用最广泛的,还是flutterchina在github上提供的Dio第三方库,目前Star达到了5800多个。

官网地址:github.com/flutterchina

使用Dio,因为是第三方库,所以同样要先在 pubspec.yaml 添加第三方库引用。

dependencies:
  flutter:
    sdk: flutter
  json_annotation: ^2.0.0
  dio: 2.1.16

使用示例:

import 'package:dio/dio.dart';

Future _getByDio() async{

      // 接口地址
      const url="https://www.demo.com/api";

      //定义 Dio实例
      Dio dio = new Dio();
      //获取dio返回的Response
      Response response = await dio.get(url);
      //返回值转化为JSON
      var json=jsonDecode(response.data);
      return json;
}

接口调用也是比httpclient简单很多,可能由于fluterchina在他的官方教程里,极力推荐这个dio库,所以目前这个第三方库的使用情况最为广泛。和Dart.dev的http不同的是,他需要new一个Dio的实例,在创建实例的时候,还可以传入更多的扩展配置参数。

BaseOptions options = new BaseOptions(
    baseUrl: "https://www.xx.com/api",
    connectTimeout: 5000,
    receiveTimeout: 3000,
);
Dio dio = new Dio(options);

11.吐吐槽

学习Flutter的过程中,其实还是有很多坎坷和需要吐槽的地方。

11.1 墙

因为有墙在,所以在配置flutter,或者下载flutter插件和第三方库的时候,需要墙内外来回切换。

11.2 组件过度设计

提供的各种widget组件很多,但是真正核心的组件、常用的组件,也就哪些。 比如Flex 和column、row的关系,比如,Tween 与IntTween,ColorTween,SizeTween等20多个Tween子类之间的关系,你需要花很大的精力,去看每个具体子类的实现差别。

11.3 嵌套太多不适应

因为嵌套层级很多,而且布局、动画、功能都在一起,第一次上手Flutter和Dart,这种嵌套关系让人很晕菜,这个只能去慢慢克服。 另外,多开发自定义的组件,可以让嵌套关系看起来清晰一些。

11.4 布局修改会导致嵌套关系修改

前端的html+css分离世界里,不改变嵌套关系,修改CSS就可以调整布局。 但是在Flutter里因为布局也是嵌套关系,这就导致必须去改变嵌套关系。 要让嵌套更简单变动影响更小,页面拆分成子组件变得尤为重要。

11.5 Dart语言升级

没错,语言升级也会导致学习的困扰,外面的资料新旧都有,比如有些是 new Text() ,有些直接是Text() ,新手上路会很晕菜。 其实这都是Dart语言升级导致的,记住Dart升级2.X以后,都不使用new了。感兴趣的可以自己去看下Dart的升级变更说明。

11.6 不能热更新

年中的时候,Google官方宣布flutter暂不官方支持热更新,但是闲鱼团队已经有了自己的热更新方案。 关于热更新,只能静观其变了。 性能、开发效率、热更新,总是要有取舍的。即使是闲鱼团队,热更新也是付出了一点点性能下降的代价的,这是你选择flutter的初衷吗?还是那句话:权衡得失。

12.结语

随着 9 月谷歌发布 Flutter1.9 以及flutter for web,Flutter的组件化思路,使得一份代码跨三端变成可能,相信Flutter的未来会更加广阔。

这不是一篇教程,只是在学习Flutter过程中的一点体验和经历,也因为时间关系,研究并不深入,如有疏漏,还请不吝赐教。

JavaCV的摄像头实战之一:基础 - 程序员欣宸 - 博客园

mikel阅读(826)

来源: JavaCV的摄像头实战之一:基础 – 程序员欣宸 – 博客园

欢迎访问我的GitHub

这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos

关于《JavaCV的摄像头实战》系列

  • 《JavaCV的摄像头实战》顾名思义,是使用JavaCV框架对摄像头进行各种处理的实战集合,这是欣宸作为一名Java程序员,在计算机视觉(computer vision)领域的一个原创系列,通过连续的编码实战,与您一同学习掌握视频、音频、图片等资源的各种操作
  • 另外要说明的是,整个系列使用的摄像头是USB摄像图或者笔记本的内置摄像头,并非基于网络访问的智能摄像头

本篇概览

  • 作为整个系列的开篇,本文非常重要,从环境到代码的方方面面,都会为后续文章打好基础,简单来说本篇由以下内容构成:
  1. 环境和版本信息
  2. 基本套路分析
  3. 基本框架编码
  4. 部署媒体服务器
  • 接下来就从环境和版本信息开始吧

环境和版本信息

  • 现在就把实战涉及的软硬件环境交代清楚,您可以用来参考:
  1. 操作系统:win10
  2. JDK:1.8.0_291
  3. maven:3.8.1
  4. IDEA:2021.2.2(Ultimate Edition)
  5. JavaCV:1.5.6
  6. 媒体服务器:基于dockek部署的nginx-rtmp,镜像是:alfg/nginx-rtmp:v1.3.1

源码下载

名称 链接 备注
项目主页 https://github.com/zq2599/blog_demos 该项目在GitHub上的主页
git仓库地址(https) https://github.com/zq2599/blog_demos.git 该项目源码的仓库地址,https协议
git仓库地址(ssh) git@github.com:zq2599/blog_demos.git 该项目源码的仓库地址,ssh协议
  • 这个git项目中有多个文件夹,本篇的源码在javacv-tutorials文件夹下,如下图红框所示:

在这里插入图片描述

  • javacv-tutorials里面有多个子工程,《JavaCV的摄像头实战》系列的代码在simple-grab-push工程下:

在这里插入图片描述

基本套路分析

  • 全系列有多个基于摄像头的实战,例如窗口预览、把视频保存为文件、把视频推送到媒体服务器等,其基本套路是大致相同的,用最简单的流程图表示如下:

在这里插入图片描述

  • 从上图可见,整个流程就是不停的从摄像头取帧,然后处理和输出

基本框架编码

  • 看过了上面基本套路,聪明的您可能会有这样的想法:既然套路是固定的,那代码也可以按套路固定下来吧
  • 没错,接下来就考虑如何把代码按照套路固定下来,我的思路是开发名为AbstractCameraApplication的抽象类,作为《JavaCV的摄像头实战》系列每个应用的父类,它负责搭建整个初始化、取帧、处理、输出的流程,它的子类则专注帧数据的具体处理和输出,整个体系的UML图如下所示:

在这里插入图片描述

  • 接下来就该开发抽象类AbstractCameraApplication.java了,编码前先设计,下图是AbstractCameraApplication的主要方法和执行流程,粗体全部是方法名,红色块代表留给子类实现的抽象方法:

在这里插入图片描述

  • 接下来是创建工程,我这里创建的是maven工程,pom.xml如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>javacv-tutorials</artifactId>
        <groupId>com.bolingcavalry</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.bolingcavalry</groupId>
    <version>1.0-SNAPSHOT</version>
    <artifactId>simple-grab-push</artifactId>
    <packaging>jar</packaging>

    <properties>
        <!-- javacpp当前版本 -->
        <javacpp.version>1.5.6</javacpp.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.3</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-to-slf4j</artifactId>
            <version>2.13.3</version>
        </dependency>

        <!-- javacv相关依赖,一个就够了 -->
        <dependency>
            <groupId>org.bytedeco</groupId>
            <artifactId>javacv-platform</artifactId>
            <version>${javacpp.version}</version>
        </dependency>
    </dependencies>
</project>
  • 接下来就是AbstractCameraApplication.java的完整代码,这些代码的流程和方法命名都与上图保持一致,并且添加了详细的注释,有几处要注意的地方稍后会提到:
package com.bolingcavalry.grabpush.camera;

import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.bytedeco.ffmpeg.global.avutil;
import org.bytedeco.javacv.*;
import org.bytedeco.opencv.global.opencv_imgproc;
import org.bytedeco.opencv.opencv_core.Mat;
import org.bytedeco.opencv.opencv_core.Scalar;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @author will
 * @email zq2599@gmail.com
 * @date 2021/11/19 8:07 上午
 * @description 摄像头应用的基础类,这里面定义了拉流和推流的基本流程,子类只需实现具体的业务方法即可
 */
@Slf4j
public abstract class AbstractCameraApplication {

    /**
     * 摄像头序号,如果只有一个摄像头,那就是0
     */
    protected static final int CAMERA_INDEX = 0;

    /**
     * 帧抓取器
     */
    protected FrameGrabber grabber;

    /**
     * 输出帧率
     */
    @Getter
    private final double frameRate = 30;

    /**
     * 摄像头视频的宽
     */
    @Getter
    private final int cameraImageWidth = 1280;

    /**
     * 摄像头视频的高
     */
    @Getter
    private final int cameraImageHeight = 720;

    /**
     * 转换器
     */
    private final OpenCVFrameConverter.ToIplImage openCVConverter = new OpenCVFrameConverter.ToIplImage();

    /**
     * 实例化、初始化输出操作相关的资源
     */
    protected abstract void initOutput() throws Exception;

    /**
     * 输出
     */
    protected abstract void output(Frame frame) throws Exception;

    /**
     * 释放输出操作相关的资源
     */
    protected abstract void releaseOutputResource() throws Exception;

    /**
     * 两帧之间的间隔时间
     * @return
     */
    protected int getInterval() {
        // 假设一秒钟15帧,那么两帧间隔就是(1000/15)毫秒
        return (int)(1000/ frameRate);
    }

    /**
     * 实例化帧抓取器,默认OpenCVFrameGrabber对象,
     * 子类可按需要自行覆盖
     * @throws FFmpegFrameGrabber.Exception
     */
    protected void instanceGrabber() throws FrameGrabber.Exception {
        grabber = new OpenCVFrameGrabber(CAMERA_INDEX);
    }

    /**
     * 用帧抓取器抓取一帧,默认调用grab()方法,
     * 子类可以按需求自行覆盖
     * @return
     */
    protected Frame grabFrame() throws FrameGrabber.Exception {
        return grabber.grab();
    }

    /**
     * 初始化帧抓取器
     * @throws Exception
     */
    protected void initGrabber() throws Exception {
        // 实例化帧抓取器
        instanceGrabber();

        // 摄像头有可能有多个分辨率,这里指定
        // 可以指定宽高,也可以不指定反而调用grabber.getImageWidth去获取,
        grabber.setImageWidth(cameraImageWidth);
        grabber.setImageHeight(cameraImageHeight);

        // 开启抓取器
        grabber.start();
    }

    /**
     * 预览和输出
     * @param grabSeconds 持续时长
     * @throws Exception
     */
    private void grabAndOutput(int grabSeconds) throws Exception {
        // 添加水印时用到的时间工具
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

        long endTime = System.currentTimeMillis() + 1000L *grabSeconds;

        // 两帧输出之间的间隔时间,默认是1000除以帧率,子类可酌情修改
        int interVal = getInterval();

        // 水印在图片上的位置
        org.bytedeco.opencv.opencv_core.Point point = new org.bytedeco.opencv.opencv_core.Point(15, 35);

        Frame captureFrame;
        Mat mat;

        // 超过指定时间就结束循环
        while (System.currentTimeMillis()<endTime) {
            // 取一帧
            captureFrame = grabFrame();

            if (null==captureFrame) {
                log.error("帧对象为空");
                break;
            }

            // 将帧对象转为mat对象
            mat = openCVConverter.convertToMat(captureFrame);

            // 在图片上添加水印,水印内容是当前时间,位置是左上角
            opencv_imgproc.putText(mat,
                    simpleDateFormat.format(new Date()),
                    point,
                    opencv_imgproc.CV_FONT_VECTOR0,
                    0.8,
                    new Scalar(0, 200, 255, 0),
                    1,
                    0,
                    false);

            // 子类输出
            output(openCVConverter.convert(mat));

            // 适当间隔,让肉感感受不到闪屏即可
            if(interVal>0) {
                Thread.sleep(interVal);
            }
        }

        log.info("输出结束");
    }

    /**
     * 释放所有资源
     */
    private void safeRelease() {
        try {
            // 子类需要释放的资源
            releaseOutputResource();
        } catch (Exception exception) {
            log.error("do releaseOutputResource error", exception);
        }

        if (null!=grabber) {
            try {
                grabber.close();
            } catch (Exception exception) {
                log.error("close grabber error", exception);
            }
        }
    }

    /**
     * 整合了所有初始化操作
     * @throws Exception
     */
    private void init() throws Exception {
        long startTime = System.currentTimeMillis();

        // 设置ffmepg日志级别
        avutil.av_log_set_level(avutil.AV_LOG_INFO);
        FFmpegLogCallback.set();

        // 实例化、初始化帧抓取器
        initGrabber();

        // 实例化、初始化输出操作相关的资源,
        // 具体怎么输出由子类决定,例如窗口预览、存视频文件等
        initOutput();

        log.info("初始化完成,耗时[{}]毫秒,帧率[{}],图像宽度[{}],图像高度[{}]",
                System.currentTimeMillis()-startTime,
                frameRate,
                cameraImageWidth,
                cameraImageHeight);
    }

    /**
     * 执行抓取和输出的操作
     */
    public void action(int grabSeconds) {
        try {
            // 初始化操作
            init();
            // 持续拉取和推送
            grabAndOutput(grabSeconds);
        } catch (Exception exception) {
            log.error("execute action error", exception);
        } finally {
            // 无论如何都要释放资源
            safeRelease();
        }
    }
}
  • 上述代码有以下几处要注意:
  1. 负责从摄像头取数据的是OpenCVFrameGrabber对象,即帧抓取器
  2. initGrabber方法中,通过setImageWidth和setImageHeight方法为帧抓取器设置图像的宽和高,其实也可以不用设置宽高,由帧抓取器自动适配,但是考虑到有些摄像头支持多种分辨率,所以还是按照自己的实际情况来主动设置
  3. grabAndOutput方法中,使用了while循环来不断地取帧、处理、输出,这个while循环的结束条件是指定时长,这样的结束条件可能满足不了您的需要,请按照您的实际情况自行调整(例如检测某个按键是否按下)
  4. grabAndOutput方法中,将取到的帧转为Mat对象,然后在Mat对象上添加文字,内容是当前时间,再将Mat对象转为帧对象,将此帧对象传给子类的output方法,如此一来,子类做处理和输出的时候,拿到的帧都有了时间水印
  • 至此,父类已经完成,接下来的实战,咱们只要专注用子类处理和输出帧数据即可

部署媒体服务器

  • 《JavaCV的摄像头实战》系列的一些实战涉及到推流和远程播放,这就要用到流媒体服务器了,流媒体服务器的作用如下图,咱们也在这一篇提前部署好:

在这里插入图片描述

  • 关于媒体服务器的类型,我选的是常用的nginx-rtmp,简单起见,找了一台linux电脑,在上面用docker来部署,也就是一行命令的事儿:
docker run -d --name nginx_rtmp -p 1935:1935 -p 18080:80 alfg/nginx-rtmp:v1.3.1
  • 另外还有个特殊情况,就是我这边有个闲置的树莓派3B,也可以用来做媒体服务器,也是用docker部署的,这里要注意镜像要选用shamelesscookie/nginx-rtmp-ffmpeg:latest,这个镜像有ARM64版本,适合在树莓派上使用:
docker run -d --name nginx_rtmp -p 1935:1935 -p 18080:80 shamelesscookie/nginx-rtmp-ffmpeg:latest
  • 至此,《JavaCV的摄像头实战》系列的准备工作已经完成,接下来的文章,开始精彩的体验之旅吧,欣宸原创,必不让您失望~

你不孤单,欣宸原创一路相伴

https://github.com/zq2599/blog_demos