数据专栏

智能大数据搬运工,你想要的我们都有

科技资讯:

科技学院:

科技百科:

科技书籍:

网站大全:

软件大全:

PBR 渲染参数非常少 主要 albedo normal metalic smooth ao heightmap
Unity 渲染管线 核心函数 fragForwardInternal
Unity shader的PBR渲染 核心函数
UNITY_PBS_SHADE
支持三种函数类型
disney PBS 迪士尼
minialist micro 模型
Normal 模型
PBR渲染 参数 主要光参数是 直接光 UnityLight 和 UnityIndirect 间接光 间接光 包括 diffuse 和 specular 直接光主要是 光颜色和朝向
通过ImageBasedLight可以通过 HDR环境贴图 生成 间接光的 diffuse cubeMap 和 specular 的cubeMap
可以通过 IBL Baker 工具生成对应的环境光贴图
Unity中的 Probe 和 Reflection Probe 有类似的效果
但是没有HDR 的 贴图信息丰富, 不好创建丰富 细腻的环境光效果
软件开发
2020-05-18 19:51:00
用游戏服务器,注意事项 游戏为我们和生活带来了更多的趣味和精彩,很多游戏网站的站长在挑选租用游戏服务器时,主要看游戏服务器的什么方面,结合实际生活的各方面,注意游戏服务器提供商的信誉实力售后支持、服务器本身就看稳定性、带宽价格这些事项。
1.游戏服务器的提供商信誉实力 信誉实力在各行各业中都是最重要的,是现实中的保证。看一个游戏服务器服务商的信誉实力,可以从企业上传到网站的信誉之星,服务之星等一些证书进行查询。正规的游戏服务器运营商会形成一定的规模,如果有时间的话,为了以后各方面保障,直接去游戏服务器服务商那些考检他们的公司,从公司的大小,员工数量,工作态度,服务器信息相关交流等这些就可以大概有个了解。
2.游戏服务器稳定性 稳定是游戏服务器的前提,影响到稳定的有游戏服务器配置情况、今后的扩展、安全性能。游戏的质量越来越高,对各方面的要求也变大的。在配置方面,操作系统、应用软件、网卡、硬盘、内存、CPU等都选高一点,但也不要选得太离谱,以自己是什么游戏去定。游戏的更新也是很快的,为了可以适应游戏的变化,扩展性强的游戏服务器先看。至于安全性能,网络上的病毒、木马等种类很多,谁都不想在玩游戏时,经常被影响,所以有没有实时监控防护措施服务这也要注意。
3.游戏服务器所用带宽 无论是游戏服务器是用在大型单机下载,还是网络游戏,为了不造成传输时,带宽堵塞。宽带尽量大点,美国服务器所用一般100M、1G国际带宽可以满足传输要求。
4.游戏服务器租用价格 现在市面上游戏服务器的价格,在配置的不同、服务的不同,价格也完全不同。在游戏服务器价格上的定位,一定要理性对待。先选好提供商,然后根据游戏网站需要游戏服务器怎样的支持,进行服务器间比较,再决定。如
服务器。
5.游戏服务器售后支持 游戏服务器与其它服务器一样,当工作久了,肯定会偶尔出现故障。因此,随时都有服务技术支持和快速故障解决,这是游戏服务器最基本应该具备的。
软件开发
2020-05-18 14:17:00
客户端–发送带有 SYN 标志的数据包–一次握手–服务端 服务端–发送带有 SYN/ACK 标志的数据包–二次握手–客户端 客户端–发送带有带有 ACK 标志的数据包–三次握手–服务端
为什么要三次握手?
三次握手的目的是建立可靠的通信信道,说到通讯,简单来说就是数据的发送与接收,而三次握手最主要的目的就是双方确认自己与对方的发送与接收是正常的。
第一次握手:Client 什么都不能确认;Server 确认了对方发送正常
第二次握手:Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:自己接收正常,对方发送正常 第三次握手:Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:自己发送、接收正常, 对方发送接收正常 所以三次握手就能确认双发收发功能都正常,缺一不可。 为什么要传回SYN
接收端传回发送端所发送的 SYN 是为了告诉发送端,我接收到的信息确实就是你所发送的信号了。 传了SYN为什么还要传ACK
双方通信无误必须是两者互相发送信息都无误。传了 SYN,证明发送方到接收方的通道没有问题,但是接收方到发送 方的通道还需要 ACK 信号来进行验证。
断开一个 TCP 连接则需要“四次挥手”: 客户端-发送一个 FIN,用来关闭客户端到服务器的数据传送 服务器-收到这个 FIN,它发回一 个 ACK,确认序号为收到的序号加1 。和 SYN 一样,一个 FIN 将占用一个序号 服务器-关闭与客户端的连接,发送一个FIN给客户端 客户端-发回 ACK 报文确认,并将确认序号设置为收到序号加1 为什么要四次挥手
任何一方都可以在数据传送结束后发出连接释放的通知,待对方确认后进入半关闭状态。当另一方也没有数据再发送的时候,则发出连接释放通知,对方确认后就完全关闭了TCP连接。 举个例子:A 和 B 打电话,通话即将结束后,A 说“我没啥要说的了”,B回答“我知道了”,但是 B 可能还会有要说的 话,A 不能要求 B 跟着自己的节奏结束通话,于是 B 可能又巴拉巴拉说了一通,后 B 说“我说完了”,A 回答“知道了”,这样通话才算结束
软件开发
2020-06-03 11:27:00
在HTTP/1.0中默认使用短连接。 也就是说,客户端和服务器每进行一次HTTP操作,就建立一次连接,任务结束就中断连接。 当客户端浏览器访问的某个HTML或其他类型的Web页中包含有其他的Web资源(如JavaScript文件、图像 文件、CSS文件等),每遇到这样一个Web资源,浏览器就会重新建立一个HTTP会话。
而从HTTP/1.1起,默认使用长连接,用以保持连接特性。使用长连接的HTTP协议,会在响应头加入这行代码: Connection:keep-alive
在使用长连接的情况下,当一个网页打开完成后,客户端和服务器之间用于传输HTTP数据的TCP连接不会关闭,客户端再次访问这个服务器时,会继续使用这一条已经建立的连接。Keep-Alive不会永久保持连接,它有一个保持时间,可以在不同的服务器软件(如Apache)中设定这个时间。
实现长连接需要客户端和服务端都支持长连接。HTTP协议的长连接和短连接,实质上是TCP协议的长连接和短连接。
软件开发
2020-06-03 11:19:00
总体来说分为以下几个过程: DNS解析 TCP连接 发送HTTP请求 服务器处理请求并返回HTTP报文 浏览器解析渲染页面 连接结束
软件开发
2020-06-03 11:12:00
TCP更加可靠,因为TCP是面向连接的服务 TCP是字节流传输,UDP是数据报文段 TCP更慢,UDP更快,因为TCP要建立连接释放连接 TCP一般是文件传输,远程登录的应用场景,UDP一般是视频、直播等应用场景
软件开发
2020-06-03 11:09:00
记录下搭建rocketmq过程中遇到的坑:(集群机子代号这里列为:mq-a,mq-b,mq-a-s, mq-b-s)
rocketmq搭建的是 双主双从 模式。三台机器,机器 a 、b 分别安装 双主 ,机器c 安装 双从 。 启动三个 nameserver,双主broker-master,双从broker-slave
服务器 rocketmq 是4.7.0版本
1. 第一个坑 - 项目rokcetmq 版本和服务器rocketmq版本没对上 :rocketmq 双主双从 搭建完,从 github 上下载的 rocketmq管理后台,本地跑起来,能成功连上rocketmq。然后自己写了的一个demo,发下报错,报错信息如下:
后来发现是 demo项目于中 pom的 rocketmq 依赖是 4.3.0, 和服务器4.7.0 对不上,然后我项目改成了 4.7.0的版本依赖。然后就ok了
附上 provider生产者的 demo代码 和 consumer消费者 的 demo 代码: ========>>>> provider 生产者代码: public class TestProvider { public static void main(String[] args) { try { //Instantiate with a producer group name. DefaultMQProducer producer = new DefaultMQProducer("p1"); // Specify name server addresses. producer.setNamesrvAddr("43.230.143.17:9876;58.82.250.253:9876;58.82.208.238:9876"); //Launch the instance. producer.start(); for (int i = 0; i < 10; i++) { //Create a message instance, specifying topic, tag and message body. Message msg = new Message( "testTopic" /* Topic */, "TagA" /* Tag */, ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */ ); //Call send message to deliver message to one of brokers. SendResult sendResult = producer.send(msg); System.out.printf("%s%n", sendResult); Thread.sleep(1000); } //Shut down once the producer instance is not longer in use. producer.shutdown(); } catch (Exception e) { e.printStackTrace(); } } } ========>>>> consumer 消费者代码: public class TestConsumer { public static void main(String[] args) { try { // Instantiate with specified consumer group name. DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("c1"); // Specify name server addresses. consumer.setNamesrvAddr("58.82.250.253:9876;43.230.143.17:9876;58.82.208.238:9876"); // Subscribe one more more topics to consume. consumer.subscribe("testTopic", "*"); // Register callback to execute on arrival of messages fetched from brokers. consumer.registerMessageListener((MessageListenerConcurrently)(msgs, context) -> { // System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs.toString()); System.out.println(" Receive New Messages: " + Arrays.toString(msgs.toArray())); return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; }); //Launch the consumer instance. consumer.start(); System.out.printf("Consumer Started.%n"); } catch (Exception e) { e.printStackTrace(); } } }

软件开发
2020-06-03 10:45:00
互联网行业数据仓库/数据平台的架构
互联网行业数据仓库、数据平台的用途
1) 整合公司所有业务数据,建立统一的数据中心;
2) 提供各种报表,有给高层的,有给各个业务的;
3) 为网站或 APP 运营提供运营上的数据支持,就是通过数据,让运营及时了解网站和产品的运营效果;
4) 为各个业务提供线上或线下的数据支持,成为公司统一的数据交换与提供平台;
5) 分析用户行为数据,通过数据挖掘来降低投入成本,提高投入效果;比如广告定向精准投放、用户个性化推荐等;
6) 开发数据产品,直接或间接为公司盈利;
7) 建设开放数据平台,开放公司数据;
上面列出的内容看上去和传统行业数据仓库用途差不多,并且都要求数据仓库 / 数据平台有很好的稳定性、可靠性;
但在互联网行业,除了数据量大之外,越来越多的业务要求时效性,甚至很多是要求实时的 ,另外,互联网行业的业务变化非常快,不可能像传统行业一样,可以使用自顶向下的方法建立数据仓库,一劳永逸,它要求新的业务很快能融入数据仓库中来,老的下线的业务,能很方便的从现有的数据仓库中下线;其实,互联网行业的数据仓库就是所谓的敏捷数据仓库,不但要求能快速的响应数据,也要求能快速的响应业务;
建设敏捷数据仓库,除了对架构技术上的要求之外,还有一个很重要的方面,就是数据建模,
如果一上来就想着建立一套能兼容所有数据和业务的数据模型,那就又回到传统数据仓库的建设上了,很难满足对业务变化的快速响应。应对这种情况,一般是先将核心的持久化的业务进行深度建模(比如:基于网站日志建立的网站统计分析模型和用户浏览轨迹模型;基于公司核心用户数据建立的用户模型),
其它的业务一般都采用维度 + 宽表的方式来建立数据模型。 整体架构


逻辑上,一般都有数据采集层、数据存储与分析层、数据共享层、数据应用层。可能叫法有所不同,本质上的角色都大同小异。
我们从下往上看
数据采集
数据采集层的任务就是把数据从各种数据源中采集和存储到数据存储上,期间有可能会做一些简单的清洗。
数据源的种类比较多: 网站日志:
作为互联网行业,网站日志占的份额最大,网站日志存储在多台网站日志服务器上,
一般是在每台网站日志服务器上部署 flume agent ,实时的收集网站日志并存储到 HDFS 上; 业务数据库:
业务数据库的种类也是多种多样,有 Mysql 、 Oracle 、 SqlServer 等,这时候,我们迫切的需要一种能从各种数据库中将数据同步到 HDFS 上的工具, Sqoop 是一种,但是 Sqoop 太过繁重,而且不管数据量大小,都需要启动 MapReduce 来执行,而且需要 Hadoop 集群的每台机器都能访问业务数据库;应对此场景,淘宝开源的 DataX ,是一个很好的解决方案有资源的话,可以基于 DataX 之上做二次开发,就能非常好的解决。
当然, Flume 通过配置与开发,也可以实时的从数据库中同步数据到 HDFS 。 来自于 Ftp/Http 的数据源:
有可能一些合作伙伴提供的数据,需要通过 Ftp/Http 等定时获取, DataX 也可以满足该需求; 其他数据源:
比如一些手工录入的数据,只需要提供一个接口或小程序,即可完成;
数据存储于分析
毋庸置疑, HDFS 是大数据环境下数据仓库 / 数据平台最完美的数据存储解决方案。

离线数据分析与计算,也就是对实时性要求不高的部分,在我看来, Hive 还是首当其冲的选择,丰富的数据类型、内置函数;压缩比非常高的 ORC 文件存储格式;非常方便的 SQL 支持,使得 Hive 在基于结构化数据上的统计分析远远比 MapReduce 要高效的多,一句 SQL 可以完成的需求,开发 MR 可能需要上百行代码;

当然,使用 Hadoop 框架自然而然也提供了 MapReduce 接口,如果真的很乐意开发 Java ,或者对 SQL 不熟,那么也可以使用 MapReduce 来做分析与计算; 数据共享
这里的数据共享,其实指的是前面数据分析与计算后的结果存放的地方,其实就是关系型数据库和 NOSQL 数据库;
前面使用 Hive 、 MR 、 Spark 、 SparkSQL 分析和计算的结果,还是在 HDFS 上,但大多业务和应用不可能直接从 HDFS 上获取数据,那么就需要一个数据共享的地方,使得各业务和产品能方便的获取数据;
和数据采集层到 HDFS 刚好相反,这里需要一个从 HDFS 将数据同步至其他目标数据源的工具,同样, Sqoop,DataX 也可以满足。
另外,一些实时计算的结果数据可能由实时计算模块直接写入数据共享。
数据应用
业务产品
业务产品所使用的数据,已经存在于数据共享层,他们直接从数据共享层访问即可;
报表
同业务产品,报表所使用的数据,一般也是已经统计汇总好的,存放于数据共享层;
即席查询
即席查询的用户有很多,有可能是数据开发人员、网站和产品运营人员、数据分析人员、甚至是部门老大,他们都有即席查询数据的需求;
这种即席查询通常是现有的报表和数据共享层的数据并不能满足他们的需求,需要从数据存储层直接查询。
即席查询一般是通过 SQL 完成,最大的难度在于响应速度上,使用 Hive 有点慢,目前解决方案是 SparkSQL, Impala ,它的响应速度较 Hive 快很多,而且能很好的与 Hive 兼容。

OLAP
目前,很多的 OLAP 工具不能很好的支持从 HDFS 上直接获取数据,都是通过将需要的数据同步到关系型数据库中做 OLAP ,但如果数据量巨大的话,关系型数据库显然不行; 这时候,需要做相应的开发,从 HDFS 或者 HBase 中获取数据,完成 OLAP 的功能;
比如:根据用户在界面上选择的不定的维度和指标,通过开发接口,从 HBase 中获取数据来展示。
其它数据接口
这种接口有通用的,有定制的。比如:一个从 Redis 中获取用户属性的接口是通用的,所有的业务都可以调用这个接口来获取用户属性。

实时计算
现在业务对数据仓库实时性的需求越来越多,比如:实时的了解网站的整体流量;实时的获取一个广告的曝光和点击;在海量数据下,依靠传统数据库和传统实现方法基本完成不了,需要的是一种分布式的、高吞吐量的、延时低的、高可靠的实时计算框架; Storm, JStorm ,Spark Streaming 等实时框架已经非常成熟了。

常见思路由 scribe 、 chukwa 、 kafka 、 flume 等开源框架在前端日志服务器上收集网站日志和广告日志,实时的发送给 Storm, JStorm ,Spark Streaming ,由实时计算框架完成统计,将数据存储至 Redis ,业务通过访问 Redis 实时获取。 任务调度与监控
在数据仓库 / 数据平台中,有各种各样非常多的程序和任务,比如:数据采集任务、数据同步任务、数据分析任务等;
这些任务除了定时调度,还存在非常复杂的任务依赖关系,比如:数据分析任务必须等相应的数据采集任务完成后才能开始;数据同步任务需要等数据分析任务完成后才能开始;
这就需要一个非常完善的任务调度与监控系统,它作为数据仓库 / 数据平台的中枢,负责调度和监控所有任务的分配与运行。
元数据管理
技术元数据与业务数据,开发元数据系统。
软件开发
2020-05-17 02:15:00
DragonBonesPro制作补间动画、龙骨动画
一、开场动画
二、小丑盒子
三、跑步的人
四、跳跳羊
一、开场动画
效果 :
1.导入素材
2.将素材拖入到舞台并调整位置
3.调整图层位置
4.设置关键帧,创建补间动画
二、小丑盒子
效果 :
1.导入素材
2.将素材拖入到舞台,调整层级
3.创建骨骼

4.创建补间动画
三、跑步的人
效果 :


1.导入.json文件
2.创建手臂和腿的骨骼,层级
3.创建头部和身体的骨骼,层级(lowerbody- upperbody-head)
4.整体骨骼绑定效果

5.添加动作(run)

同理创建其他状态的动作(jump,idle,alert)
四、跳跳羊
效果 :
1.导入.json文件到舞台
2.给跳跳羊创建骨骼,层级如下图

3.选择网格模式,对跳跳羊创建网格
添加边线
添加顶点
4.制作补间动画
软件开发
2020-05-16 11:26:00
开发工具:DragonBonesPro
1.开场动画
2.小丑盒子
3.跑步的人
4.跳跳羊
一.开场动画
1.导入素材
2.将素材拖入舞台内,并调整位置以及图层
3.设置关键帧,创建补间动画
4.运行结果
二.小丑盒子动画
1.导入素材
2.将素材拖入舞台,并调整层级
3.创建骨骼,并调整场景树
4.设置关键帧,创建补间动画
5.运行结果

三.跑步的人
1.导入.json文件

2.创建手臂和腿的骨骼

3、创建头部和身体的骨骼

4.整体骨骼绑定效果和所有场景树
5.制作补间动画
最低位姿态第0帧:
最高位姿态第2帧:
第二个最低位姿态第4帧:
把第0帧的所有关键帧复制到第8帧完成剩下半步。
6.同理创建其他动作:
7.运行结果

四.跳跳羊
1.导入json素材
2.给跳跳羊创建骨骼,场景树和层级如下
3.选择网格模式,对跳跳羊创建网格,添加边线和顶点
4.添加关键帧,创建补间动画

5.运行结果

软件开发
2020-05-16 10:46:00
用DragonBonesPro制作补间动画、龙骨动画
动画补间
导入素材
整理素材位置并安排时间轴
调整关系以及创建补间动画
小丑惊吓盒
导入素材以及调整关系
创建骨骼以及创造补间动画

跑步精灵
导入数据到项目
创建骨骼 手脚部以及头和身体

建立整体从属关系
跑步动画补间

跳跃
跳跳山羊
导入素材
创建山羊骨骼以及网格

创建补间动画
软件开发
2020-05-15 20:18:00
使用DragonBonesPro制作补间动画、龙骨动画
1.开场动画
2.小丑盒子
3.跑步的人
4.跳跳羊
5.猴子荡树
一.开场动画
1.导入素材
2.将素材拖入舞台内,并调整位置以及图层

3.多处设置关键帧,创建补间动画
4.最后运行

二.小丑盒子动画
1.导入素材
2.将素材拖入舞台,并调整层级
3.创建骨骼,调整场景树

4.添加关键帧,创建补间动画
5.最后运行

三.跑步的人
1.导入json文件
2. 创建手臂和腿的骨骼
3. 创建头部和身体的骨骼
4.所有骨骼建成效果
5.所有场景树
6.添加动作(run)
7.创建其他动作
8.不同状态运行结果


四.跳跳羊
1.导入json素材
2.给羊创建骨骼,调整层级,场景树如图

3.选择网格模式,对跳跳羊创建网格
4.添加边线
5.添加里面的各个点
6.添加关键帧,创建补间动画

7.最终运行图

五.猴子荡树
1.导入素材
2.创建网格以及骨骼,将网格绑定在骨骼上

3.设置关键帧,创建补间动画
4.最终运行
软件开发
2020-05-15 19:03:00
开发软件:DragonBonesPro
1、开场动画实例
2、小丑盒子实例
3、跑步的人实例
4、跳跳样实例
1、开场动画
步骤:
1、导入素材:
2、将素材拖入到舞台并调整位置
3、调整图层位置
4、设置关键帧,创建补间动画
5、成品图

2、小丑盒子实例
步骤:
1、导入素材
2、将素材拖入到舞台,调整层级
3、创建骨骼
4、创建补间动画
5、成果图
小丑头会左摇右晃,辫子会上下摆动,同时弹簧会有收缩

3、跑步的人实例
步骤:
1、导入.json文件

2、创建手臂和腿的骨骼,层级(手臂:innerarm_upper—innerarm lower-innerarm- hand 腿部:innerleg_upper—innerleg lower-innerleg- foot)

3、创建头部和身体的骨骼,层级(lowerbody- upperbody-head)

4、整理骨骼层级
5、制作补间动画
最低位姿态第0帧:
第二个最低位姿态第4帧:
最高位姿态第2帧:
对第一帧和第三帧进行微调,使动作更流畅
用同样的方法完成另一个半步。例如把第0帧的所有关键帧复制到第8帧
创建其他动作:

4、跳跳羊实例
步骤
1、导入.json文件到舞台

2、给跳跳羊创建骨骼,层级如下图

3、选择网格模式,对跳跳羊创建网格、添加顶点
4、制作补间动画



5.运行结果
软件开发
2020-05-15 16:28:00
当我们查看别人的shader,如果没有在代码里找到声明那多半是使用了UNITY内置的文件和变量。
一、包含文件
UNITY可以使用#include 来包含部分文件,文件后缀.cginc,类似C++头文件/java的包
例如 CGPROGRAM ... #include "UnityCG.cginc" ... ENDCG

通过这种方式可以引用UNITY已经封装好的函数/变量我们可以通过 http://unity3d.com/cn/get-unity/download/archive 下载(虽然网站没法访问)
常用的UNITY内置文件
UnityCG.cginc 包含了最常用的函数结构体和宏等
UnityShaderVariables.cginc 在编译UNITY SHADER时 会自动被包含进来 ,包含了很多全局变量 如 UNITY_MATRIX_MVP转换矩阵
Lighting.cginc 包含了各种光照模型,如果编写Surface Shader的话 会被自动包含进来
HLSLSupport.cginc 在编译UNITY SHADER时被自动包含进来,声明了很多用于跨平台编译的宏和定义
UnityStandardBRDF.cginc、UnityStandardCore.cginc 这些文件里面包含了用于基于物理的渲染
-----------------------------------------------------------
其中UnityCG.cginc是最常用的组件 他里面提供了很多常用的定义和文件
常用结构体名 包含变量
appdata_base 顶点位置 顶点法线 第一组纹理坐标
appdata_tan 顶点位置 顶点法线 顶点切线 第一组纹理坐标
appdata_full 顶点位置 顶点法线 顶点切线 第四组(或者更多)纹理坐标
appdata_img
v2f_img
顶点位置 第一组纹理坐标
裁剪空间中的位置 纹理坐标
UnityCG.cginc常用的函数
函数名 描述
float3 WordSpaceViewDir(float4 v) 输入一个模型空间的顶点位置,返回在世界空间中这个点到摄像机的方向
float3 ObjSpaceViewDir(flaot4 v) 输入一个模型空间的顶点位置,返回在模型空间中这个点到摄像机的方向
float3 WordSpaceLightDir(float4 v) 仅可用于前向渲染,输入一个模型空间的顶点,返回世界空间中该点到光源的光照方向,没有被归一化
flaot3 ObjSpaceLightDir(float4 v) 仅可用于前向渲染,输入一个模型空间的顶点,返回模型空间中该点到光源的光照方向,没有被归一化
float3 UnityObjToWorldNormal(flaot3 normal) 把法线方向从模型空间转换到世界空间
float3 UnityObjToWorldDir(int float3 dir)
float3 UnityWorldToObjDir(int float3 dir)
把方向矢量从模型空间转换到世界空间中
把方向矢量从世界空间转换到模型空间中
二、内置变量
除了上述常用的函数和文件,unity还提供了很多常用的变量,如用于访问环境光、雾效、时间、光照等目的的变量,这些变量大都在UnityShaderVariables.cginc中
光照的变量还位于 Lighting.cginc 和AutoLight.cginc
三、Unity支持的语义
SV_POSITION/SV_Target/POSITION/COLOR0等等都是CG/HLSL提供的语义
可以在微软的DirectX的文档里找到说明(网址倒是能打开 ,就是没看懂)
http://msdn.microsoft.com/en-us/library/windows/desktop/bb509674(v=vs8.5).aspx#VS
语义其实就是赋给shader的一个输入输出的字符串,这个字符串表达了参数的含义,这些语义可以让shader知道从哪里读取数据,并把数据输出到哪里
他们在CG/HLSL的shader流水线中是不可获取的,需要注意的是 Unity并没有支持所有的语义
通常情况下 这些输入输出的参数并不需要特别有意义,我们可以自行决定这些变量的用途.变量本身存储什么 shader并不关心
UNITY为了方便数据传输,对一些特殊的语义进行了特殊的含义规定,例如TEXCOORD0,在顶点着色器的入参结构体a2v的内部我们用它描述texcoord,UNITY会识别该语义并把第一组纹理坐标填充给texcoord,需要注意即使语义一样出现的位置不同,意义也可能不同,如TEXCOORD0出现在输出结构体内时,这个修饰的变量由我自己定义。
在DirectX10之后出现了一种新语义, 系统语义,SV开头 System Value Semantics
例如上面的SV_POSITION修饰pos,那么就表示pos包含了可以用于光栅化的变换后的顶点坐标,即齐次裁剪空间坐标,这些语义修饰的变量是不可随便赋值的,因为流水线需要靠他们去实现一些特殊的目的,例如渲染引擎会把SV_POSITION的坐标经过光栅化后显示在屏幕上,有的时候我们会看到同样的变量在不同的shader里用不同的语义修饰,例如一些shader会使用POSITION而不是SV_POSITION来修饰顶点的着色器输出,SV_POSITION 和POSITION在大多数平台上是等价的,但在某些平台上必须使用SV_POSITION来修饰输出,同样的情况还会在COLOR0和SV_Target上出现, 所以我们尽量使用SV开头的修饰符。
语义名称 描述
POSITION 模型空间中的顶点位置,通常是float4类型
NORMAL 顶点法线,通常是float3类型
TANGENT 顶点切线,通常是float4类型
TEXCOORDn
COLOR
第n组纹理坐标,通常是float2和float4类型
顶点颜色,通常是float4或者fixed4
TEXCOORDn的n是由shader model决定的 ,在shader model2 的时候 n最大为4,在shader model 3的时候为8,在shader model 4的时候为16
通常情况下 一个模型的纹理坐标不超过2,即我们往往只使用TEXCOORD0/TEXCOORD1,在UNITY内置的appdata_full中,最多使用6个坐标纹理组
从顶点着色器传递到片元着色器的常用语义
语义 描述
SV_POSITION 裁剪空间里的顶点坐标,输出结构体必须包含这个修饰的变量,DirectX9里面是POSITION,但最好使用SV_POSITION
COLOR0 通常用于输出的第一组颜色 但是不是必须的
COLOR1
TEXCOORD0~TEXCOORD7
通常用于输出的第二组颜色,但是不是必须的
通常用于输出纹理坐标,但是不是必须的
上面的语义 除了SV_POSITION之外,其他语义对变量没有明确的要求,也就是说我们可以存储任意值到语义描述的变量中,通常我们把一些自定义数据从顶点着色器传递到片元着色器,使用TEXCOORD0修饰。
片元着色器输出时使用的常用语义 SV_Target:输出值会存储到渲染目标Render Target中,等同于DirectX9中的COLOR语义,但最好使用SV_Target。
四、如何定义复杂的变量类型
上面提到的语义多半用来描述矢量或者标量类型的变量,例如fixed2 float float4 fixed4
下面的代码给出了一些使用语义来修饰不同变量的例子
struct v2f{
float4 pos:SV_POSITION;
fixed3 color0:COLOR0;
fixed4 color1:COLOR1;
half value0:TEXCOORD0;
float2 value1:TEXCOORD1;
}
需要注意 一个语义可以使用的寄存器最多只能使用四个浮点值,因此如果我们想要定义矩阵类型,如float3*4,float4*4等变量就需要使用更多的空间,另一种方法就是把这些变量拆分成多个变量,如float4*4 可以拆分成4个float4,每个变量存储一行矩阵的元素。











软件开发
2020-05-15 14:37:00
影响服务器租用的因素有什么 ?



什么是好的 服务器租用 商呢 ? 其实对于企业来说性能稳定便是最好的要求 , 但是在选择服务商的时候也需要进行对比 , 因为不同的判断不同的考量会选择不同的服务商 , 影响服务器租用的因素有很多 , 那么应该注意什么呢 ?
宽带和防御
比如说 ,BGP 多线线路机房 ,6M 宽半的服务器 , 价格就要比 10M 兆宽带的价格便宜一些 。 宽带与防御因素 , 其实也是某些不良运营商做动手比较容易切入的地方 , 所以如果你真的遇到服务器租用价格比较低的运营商的话 , 那么很可能是商家在这个方面缩减了成本 。 所以一定不要盲目选择这种价格较低的合作伙伴 。
地域因素
也就是说即便是同一运营商 , 如果机房服务器是在不同的地域 , 那么所产生的价格也是不一样的 。 所以说 , 企业在选择服务器租用合作商的时候 , 一定要考虑一下地域因素 。 尽量选择机房离得近 , 公司比较近 , 同时租用价格还比较低这样的合作商 。
是配置因素
这里所说的配置元素 , 主要包括服务器机房线路 , 服务器处理器以及内存等等要素 , 可以说配置越高的话 , 服务器租用的价格也就越贵 , 这一点大家应该都心知肚明 。
总的来说 , 影响企业服务器租用价格的主要因素就是以上三个要素了 。 一般来讲 , 在选择服务器租用运营商的时候 , 企业工作人员要切记 , 运营商要有相关行业资格证 , 这样才能选出真正适合自己需求的高品质运营商 。
软件开发
2020-05-14 18:03:00
参考<>第五章
一、如何获取其他模型数据
在001里介绍了通过POSITION获取顶点位置坐标,如果想的到更多的模型数据,比如我们想要得到模型上每个顶点的纹理坐标和法线方向
PS:我们可以通过纹理坐标来访问纹理 法线坐标一般用来计算光照
因此我们需要给顶点着色器定义一个新的参数·这个参数将不是一个简单的数据类型
而是一个结构体 Shader "Customer/SimpleShader003"{ SubShader{ Pass{ CGPROGRAM #pragma vertex vert #pragma fragment frag struct a2v{ float4 vertex: POSITION; float3 normal:NORMAL; float4 texcoord:TEXCOORD0; }; float4 vert(a2v v): SV_POSITION{ return mul(UNITY_MATRIX_MVP,v.vertex); } fixed4 frag(): SV_Target{ return fixed4(1.0,1.0,1.0,1.0); } ENDCG } } }
在上面的代码中 声明的结构体a2v 他包含了顶点着色器需要的模型数据
在a2v的定义中,我们用到了更多的UNITY支持的语义,如
NORMAL 和TEXCOORD0,当他们作为顶点着色器的输入时都是有特定含义的
因为UNITY会根据这些语义来填充这个结构体,对于顶点着色器的输出,UNITY
支持的语义有POSITION,TANGENT,NORMAL,TEXCOORD0,TEXCOORD1,TEXCOORD2
TEXCOORD3,COLOR等。
为了使用一个自定义的结构体,我们必须使用如下格式
struct name{
Type Name:Semantic;
Type Name:Semantic;
......
};
a表示应用 v表示顶点着色器 a2v的意义就是从应用阶段传递到顶点着色器中
那么填充到POSITION,TANGENT,NORMAL的语义里的数据酒究竟是从哪里来的呢?
在UNITY中,它们是由使用该材质的MeshRender组件提供的。
在每帧调用DRAW CALL的时候,MeshRender组件会把他负责渲染的模型的数据发送给UNITY SHADER,
我们知道,一个模型通常包含了一组三角面片。(我们不知道啊QAQ)
每个三角面片由三个顶点构成,而每个顶点又包含了一些数据,例如顶点位置、发现坐标、顶点颜色等等
通过上面的方法,我们就可以在顶点着色器中访问顶点的这些模型数据。
二、顶点、片元着色器之间如何相互通信
我们在实际开发的过程中,往往希望从顶点着色器输出一些数据,例如把模型的法线纹理坐标等传递给片元着色器
这就涉及到了顶点着色器和片元着色器之间的通信
为此我们需要新建一个结构体.修改后代码如下 Shader "Customer/SimpleShader004"{ SubShader{ Pass{ CGPROGRAM #pragma vertex vert #pragma fragment frag struct a2v{ float4 vertex:POSITION; float3 normal:NORMAL; float4 texcoord:TEXCOORD0; }; //使用一个结构体来定义顶点着色器的输出 struct v2f{ float4 pos :SV_POSITION; fixed3 color:COLOR0; }; v2f vert(a2v v):SV_POSITION{ v2f o; o.pos = mul(UNITY_MATRIX_MVP,v.vertex); o.color = v.normal*0.5 + fixed3(0.5,0.5,0.5); return o; } fixed4 frag(v2f i):SV_Target{ return fixed4(i.color,1.0); } ENDCG } } }
在上面的代码中,我们声明了一个新的结构体v2f
v2f中也需要指定每个变量的语义,在本例中我们定义了SV_POSITION和COLOR0,顶点着色器的输出结构中,必须包含SV_POSITION,否则无法获取
裁剪空间的顶点坐标,COLOR0则由用户自己定义,一般是用来存颜色,例如逐顶点的漫反射颜色等,类似语义还有COLOR1
至此我们完成了从顶点着色器往片元着色器传递数据,顶点着色器是逐顶点的,片元着色器是逐片元的,因此片元着色器的输入实际是把顶点着色器的输出进行插值的结果
二、如何使用属性
unityshader和材质密不可分,shader提供了一些可以设置的参数来调试材质的效果,这些参数需要写在properties语义块里面
比如我们现在有一个新的需求,需要在面板上显示一个颜色拾取器,为此需要修改上面的代码 Shader "Customer/SimpleShader004"{ Properties{ //声明一个color类型的属性 _Color("Color Tint",Color) = (1.0,1.0,1.0,1.0) } SubShader{ Pass{ CGPROGRAM #pragma vertex vert #pragma fragment frag fixed4 _Color; struct a2v{ float4 vertex:POSITION; float3 normal:NORMAL; float4 texcoord:TEXCOORD0; }; struct v2f{ float4 pos:SV_POSITION; float3 color:COLOR0; }; v2f vert(a2v v) :SV_POSITION{ v2f o; o.pos = mul(UNITY_MATRIX_MVP,v.vertex); o.color = v.color*0.5+fixed3(0.5,0.5,0.5); return o; } fixed4 frag(v2f i):SV_Target{ fixed3 = i.color; c *=_Color.rgb; return fixed4(c,1.0); } ENDCG } } }
在上面的语句里我们定义了一个color,(1.0,1.0,1.0,1.0)代表的是白色
为了在CG代码里面访问他,我们还需要提前自己定义一个新变量 ,该变量名必须和属性里的变量名一致
ShaderLab中变量和CG类型中变量对应的关系如下所示
有时 CG变量前会有一个 uniform 关键字
uniform fixed4 _Color; 该关键字在CG里是用来修饰变量,仅仅提供该变量初始值的是如何指定和存储的相关信息,UNITY SHADER里该关键字可以省略。












软件开发
2020-05-14 17:31:00
今天正式开始学习shader, 准确的说是学习如何编写顶点/片元着色器
一、顶点/片元着色器的基本结构
shader包含subshader、shader、property、fallback等语句块  PS:每个UnityShader文件可以包含多个SubShader语义块,但至少要有一个。 当Unity需要加载这个UnityShader时, Unity会扫描所有的SubShader语义块, 然后选择一个能够在目标平台上运行的SubShader。 如果都不支持的话,Unity 就会使用FallBack语义指定的UnityShader。 同样子着色器中的Pass也是可以有很多个, 当执行子着色器内的渲染方案时,所有的Pass会被依次执行 Shader "MyShader"{ Properties{//属性} SubShader{//针对显卡A的SubShader Pass{ //设置渲染状态和标签 //开始CG代码片段 CGPROGRAM //该代码片段编译指令,例如 #pragma vertex vert #pragma fragment frag //CG代码 ENDCG //其他设置 } //其他需要的Pass } SubShader{//针对显卡B的SubShader} //上面的subshader全都失败之后用于回调的unity shader Fallback "VertexLit" }
其中最重要的是Pass语义块,绝大部分代码都是在Pass里实现的
EXAMPLE: Shader "Customer/SimpleShader"{ SubShader{ Pass{ CGPROGRAM #pragma vertex vert #pragma fragment frag float4 vert(float4 v :POSITION) :SV_POSITION{ return mul(UNITY_MATRIX_MVP,v) } fixed4 frag() : SV_Target{ return fixed4(1.0,1.0,1.0,1.0) } ENDCG } } }
上面的代码里没有用到Properties语义
Properties语义并不是必须的 可以不声明任何材质属性
subshader里没有进行标签设置,所以将使用默认的渲染和标签设置
在SubShader语义块中,我们定义了一个Pass, 这个pass里也没有设置任何自定义的渲染和标签
接着就是CGPROGRAM和ENDGC所包围的CG代码片段
#pragma vertex vert
#pragma fragment frag
这两个指令将告诉UNITY哪个函数包含了顶点着色器的代码
哪个包含了片元着色器的代码
float4介绍 参考 https://www.cnblogs.com/jiahuafu/p/6136871.html
GPU是以四维向量为基本单位来计算的。4个浮点数所组成的float4向量是GPU内置的最基本类型。使用GPU对两个float4向量进行计算,与CPU对两个整数或两个浮点数进行计算一样简单,都是只需要一个指令就可以完成。
HLSH的基本数据类型定义了float、int和bool等非向量类型,但是它们实际上都会被Complier转换成float4的向量, 只要把float4向量的其中3个数值忽略,就可以把float4类型作为标量使用 。
使用贴图坐标时,只需要二维向量,HLSL定义了float2类型作为二维向量使用。
Shader经常会用到矩阵,HLSL有一个内置类型float4x4,它可以用来表示一个4*4矩阵。float4x4并不是GPU的内置类型,float4x4实际上是由4个float4所组成的数组。其他的还有float3x3、float2x2,分表代表3*3矩阵、2*2矩阵。
Shader也可以声明数组,4*4矩阵实际上就是一个float4 m[4]的数组。注意,Shader中的所有的变量都使用寄存器,没有其他内存空间可以使用,所以越大的数组会占用越多的寄存器,甚至会超出寄存器的数量限制。
在使用float4向量中的个别数值时,可以用xyzw或rgba,都可以用来表示四维向量中的数值。但不能把它们混用,例如不能用xyba,把它视为颜色时就用rgba,否则就是用xyzw,不能把这二者混合使用。

rgba : 前三个值(红绿蓝)的范围为0到255之间的整数或者0%到100%之间的百分数。这些值描述了红绿蓝三原色在预期色彩中的量。
第四个值,alpha值,制订了色彩的透明度/不透明度,它的范围为0.0到1.0之间,0.5为半透明。
rgba(255,255,255,0)则表示完全透明的白色;
rgba(0,0,0,1)则表示完全不透明的黑色;
rgba(0,0,0,0)则表示完全不透明的白色,也即是无色;
二、顶点着色器结构
具体看一下vert函数的定义
float4 vert(float4 v: POSITION):SV_POSITION{
return mul(UNITY_MATRIX_MVP,v);
}
这个就是本例所使用的顶点着色器的代码,他是逐顶点执行的。mul(UNITY_MATRIX_MVP,v)的意思是把顶点坐标从模型空间转换为裁剪空间
vert函数输入的v包含顶点的位置 这个是通过POSITION语义指定的
他的返回值是一个float4类型的变量
POSITION/SV_POSITION都是CG/HLSL里的语义,不可省略
这些语义将要告诉系统用户需要哪些输入值以及用户输出的是什么 。 简单地说POSITION语意用于顶点着色器,用来指定这些个位置坐标值,是在变换前的顶点的object space坐标。SV_POSITION语意则用于像素着色器,用来标识经过顶点着色器变换之后的顶点坐标
本例中POSITION 将告诉UNITY 把模型的顶点坐标填充到输入参数v中
SV_POSITION将告诉UNITY顶点着色器输出的是裁剪空间中的顶点坐标
三、片元着色器结构
现在看一下frag函数
fixed4 frag() :SV_Target{
return fixed4(1.0,1.0,1.0,1.0);
}
上面的代码输出的上一个fixed4类型的变量,并且使用了SV_Target语义进行限定,SV_Target也是一个系统语义,它告诉渲染器,把用户输出的颜色存储到一个渲染目标里,这里将输出到默认的帧缓存中,片元着色器返回的fixed4类型的变量,片元着色器输出的颜色在[0,1]中,其中(0,0,0)是黑色,(1,1,1)是白色.
PS:FIXED类型数据 参考 https://www.cnblogs.com/jiahuafu/p/6237859.html
在处理图形运算,特别是3D图形生成运算时,往往要定义一个Fixed数据类型,我称它为定点数,定点数其时就是一个整形数据类型,他的作用就是把所有数 进行转换,从而得到相应类型的整型表达,然后使用定点数进行整行运算,取到最终值并将其转换回实际的基本数据类型。因此它是通过避免大量的浮点运算来加快 图形处理的一个方式。
Fixed类型说了一堆,究竟来做什么的?
比如上例中,Y轴每次都要偏移0.4,而这个数是个浮点,严重影响了运算速度。比如,我们后台有一个数,用来计量Y轴本次的坐标,就叫做变量YY吧。X每 次都加1,也就是XX++,Y每次加0.4,也就是YY+=0.4。为了提高速度,我们将YY升级到Fixed类型,YY每次加Fixed的0.4,也就 是0.4*65536=26214,然后再四舍五入到整数类型,即YY+=26214,Y=(YY+32768)>>16。这样,就得到了每 次的整数Y,并且都是整数的加减和位运算,速度非常快。
SV_Target介绍 参考 https://zhuanlan.zhihu.com/p/113237579
SV_Target SV_POSITION 这两个都是 语义绑定 (semantics binding)。什么是语义绑定呢?语义绑定可以理解为关键字,我们都知道图形渲染是按照一步一步地进行的,又叫做渲染管线。那么为什么是要一步一步地进行呢?因为GPU的架构与CPU非常不同,cpu更像是人的大脑,可以同时思考不同的问题,而GPU则相当于计算机,通过有特定的输入,通过计算得到最终的输出。GPU没有像CPU一样的堆栈来存取变量与值,所以通过语义绑定将一个计算好的值放在一个物理存储位置,再用的话就利用语义绑定去特定物理位置取出,这也是GPU不能像CPU一样工作的原因之一吧。
SV_前缀的变量代表system value的意思,在DX10+的语义绑定中被使用代表特殊的意义,SV_POSITION在用法上和POSITION是一样的,区别是 SV_POSTION一旦被作为vertex函数的输出语义,那么这个最终的顶点位置就被固定了,不得改变。

四、效果
把写好的shader文件赋值给material文件,然后新建一个3DObject
修改mesh render里面的material为我们自己创建的
效果如下
可以尝试修改shader,参考 https://zhuanlan.zhihu.com/p/85594617 Shader "Custom/SimpleShader02" { SubShader { Pass { Blend SrcAlpha OneMinusSrcAlpha CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float4 vertex : SV_POSITION; float2 uv : TEXCOORD1; }; v2f vert(appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = v.uv; return o; } float4 frag(v2f i) : SV_Target { return float4(i.uv.r, i.uv.g,0,1); } ENDCG } }
先在两个结构体里面加入一些东西
struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float4 vertex : SV_POSITION; float2 uv : TEXCOORD1; };
在vertex shader做的事情:将拿到的uv直接通过v2f传给frag函数
v2f vert(appdata v) { v2f o; o.vertex = mul(UNITY_MATRIX_MVP, v.vertex); o.uv = v.uv return o; }
接下来把uv的值填入颜色的返回值中,得到下图
float4 frag(v2f i) : SV_Target { float4 color = float4(i.uv.r, i.uv.g, 0, 1); return color; }

PS:TEXCOORD0 TEXCOORD1里的uv是什么?
纹理坐标,也就是俗称的UV,是顶点数据的一部分。
在建模软件比如3D Max中,建模时有个过程俗称展开UV,其实就是指定模型每个顶点的UV。因为纹理可以有不止一张,所以UV也可以有不止一组。
建模软件导出的模型数据自然也包含顶点的UV数据,在引擎中渲染模型之前,创建顶点缓冲时,包含了UV信息的顶点数组被复制到顶点缓冲里用于接下来的渲染。
在渲染过程中,vs阶段的输入就是(当前)顶点数据,包括各组UV。当然你可以选择性使用,比如说你只需要一组UV,就可以只选择绑定一个TEXCOORD0
texcoord0和texcoord1分别表示两层UV,有时候我们模型上的贴图需要多个图片一起贴在一处,那么贴两层就会有两层UV。


参考<>第五章
参考 https://zhuanlan.zhihu.com/p/113237579 SV_Target介绍
参考 https://zhuanlan.zhihu.com/p/85594617 shader简单着色编写
参考 https://docs.unity3d.com/Manual/SL-VertexProgramInputs.html 官方实例及API介绍
参考 https://www.zhihu.com/question/353273260/answer/876813032 TEXCOORD0 的uv是什么
参考 https://www.cnblogs.com/leeplogs/p/7339097.html TEXCOORD0
参考 https://www.cnblogs.com/jiahuafu/p/6237859.html FIXED类型的数据
软件开发
2020-05-14 11:50:00
什么是服务器日志 ? 服务器日志要怎么看 ?

什么是服务器日志
虽然现在很多站长懂得做搜索排名知识 , 但是懂得 SEO, 并不代表就懂得服务器日志了 , 那么服务器日志是什么呢 ? 其实 , 服务器日志 (server log) 是一个或多个由服务器自动创建和维护的日志文件 , 其中包含其所执行活动的列表 。
简而言之 , 服务器日记就是记录网站被访问的全过程 , 通过服务器日志 , 站长就可以知道什么时间到什么时间有哪些人来过 , 并且还知道什么搜索引擎来过 , 有没有收录你的网页 , 从你的网站工作的第一天你的日记就有了 。
假如你想做好 SEO, 那么你就要好好的了解下服务器日志了 , 因为它可以让你更了解搜索引擎爬虫 。
服务器日志怎么看
1、 开始 —— 管理工具 —— 事件查看器 —— 系统或者控制面板 —— 管理工具 —— 事件查看器 —— 系统 。
2、 在远程客户端 , 运行 IE 浏览器 , 在地址栏中输入 “https://Win2003 服务器 IP 地址 :8098”, 如 “https://192.168.1.1:8098”。
在弹出的登录对话框中输入管理员的用户名和密码 , 点击 “ 确定 ” 按钮即可登录 Web 访问接口管理界面 。
接着在 “ 欢迎使用 ” 界面中点击 “ 维护 ” 链接 , 切换到 “ 维护 ” 管理页面 , 然后点击 “ 日志 ” 链接进入 。
到日志管理页面后 , 在日志管理页面中 , 管理员可以查看 、 下载或清除 windows 2003 服务器日志 。
选择系统日志可进行查看 , 并且在日志管理页面中可列出 windows 2003 服务器的所有日志分类 , 如应用程序日志 、 安全日志 、 系统日志 、Web 管理日志等 。

软件开发
2020-05-13 17:24:00
一般来讲,配音分为广义和狭义之分。广义的配音就是指的是音乐、动效、音响、 游戏配音等等的处理。而狭义的配音则指专门为对白、独白、内心独白、解说和旁白等语言的后期配制而进行的一系列创作活动。

  影视配音和动画配音都是配音艺术,但是,这两种配音类型的创作依据却是不一样的。接下来,我们就一起了解一下影视配音和动画配音创作依据有哪些不同。
影视配音,简单来说就是替前面的演员说话,由配音员为已经完成了的电视剧中的某一人物录制台词,通过后期配音的方法准确、生动的塑造出人物形象。影视剧配音以演员的形象为依据,需要对作品有深刻的感悟和体验,是一种再创造的过程。
  动画配音是配音演员以文字形象和动画设计师绘出的画面形象,也就是视觉形象为依据,通过自身对作品的深刻理解,运用自己的声音展现动画人物造型的个性,把握语言节奏的艺术创造,而不是再创造。但是,动画配音是一种假定的艺术,不受人物形象的限制,不需要模仿生活,所以,动画配音是不会受到任何物质现实的制约。
  影视剧配音和动画配音的创作依据是不同的,影视剧配音员需要把握表演者的风格,具有很强的约束性,而动画配音员则没有那么多制约。
软件开发
2020-05-13 12:01:00
「Creator星球游戏开发社区公众号」有2年半了,一直以游戏开发技术内容为主,对于游戏开发如何挣钱还是一个小孩子。
晓衡在不断的探索中成长,计划将游戏开发、运营、挣钱的整个完整流程揉碎,掰开了一点点学习,并与大家分享其中心得!下图是晓衡粗解的小游戏开发运营挣钱模型。
长期以来,个人开发者们主要是停留在立项、开发阶段,其中开发又涉及程序、美术、策划以及测试。
大多数个人开发者,游戏一旦上线效果不好,很可能快速放弃,又开始了一个新的项目,反复轮回,徘徊在放弃的边缘!
最近,有过两款开发经验的开发者跟晓衡沟通,除了微店源码服务外,能否帮忙对接流量服务,满足前期游戏调优和后期游戏运营需要。
游戏调优是个怎么样的阶段呢?不要小看这个,他是链接游戏开发与运营的重要环节!
调优的内容大概包括: 游戏性能调优 分享裂变调优 用户留存调优 广告收益调优
当我们完成一款游戏的核心主体功能时,就可以发布上线,这时游戏可以被玩家接触到,想当于我们的种子用户,我们需要获得玩家的反馈,对游戏进行从技术到留存,再到广收益等调整。
但是在微信平台上有一个非常残酷的问题:“没有自然流量”!!!很多开发者连最初的1000个用户都需要较长的时间和巨大的精力才能达成。
由于样本量太低,并不能指导开发者对游戏的调整,从而导致很多具有潜力的游戏沦为数字垃圾安静的躺在开发者的电脑里。
经过晓衡的不懈努力终于对接到一家以儿童机器人(类似智能音箱)运营为主的流量资源,有意向寻找合适流量的小游戏主可进行流量合作。欢迎加我微信: z6346289 ,关注我的微信公众号: Creator游戏开发社区 ,晓衡在线等你!
完整原文: https://mp.weixin.qq.com/s/4GpsEidOEcYzf4AHAI6P8A
软件开发
2020-05-13 09:27:00
如何传一个固定的Vec3数组: osg::Uniform *uniform3 = new osg::Uniform(osg::Uniform::Type::FLOAT_VEC3, "arr3", 8); uniform3->setElement(0, osg::Vec3(1, 0, 0)); uniform3->setElement(1, osg::Vec3(1, 0, 1)); uniform3->setElement(2, osg::Vec3(1, 1, 0));
与一般的写法不同, Element的数目是固定的
软件开发
2020-05-12 17:58:00
常用于摄像机追随玩家 实现平滑跟踪
void Update () {
if (player != null) {
float x = Mathf.Lerp (this.transform.position.x , player.position.x , 0.2f);
float y = Mathf.Lerp (this.transform.position.y , player.position.y , 0.2f);
this.transform.position = new Vector3 (x , y , this.transform.position.z);
}
}
软件开发
2020-05-12 16:07:00
我国不同的地方有不同的地方语言,俗称“方言”,近些年来,方言配音在游戏中被广泛应用。不同的方言有其独特之处,不同的方言有不同的发音,声调也有不同。对于声调的方言有什么差异?下面 游戏方言配音小编就给大家说说吧!

  现代汉语方言大致分为七大方言区,根据各方言的声调状况,可把它们归并为两大类。
1、北方方言,各地北方话的系统比较一致。大多数地区的声调数目是四个,没有入声调;
2、南方方言,声调数目较多,许多方言都有六七个声调,大多数南方方言都有入声调。
就全国来说,声调差异很大,虽然大部分地区有阴阳上去四类,但表现的调值是不同的。
调类上看,有三类的,也有十类的。而调值的把握不是件很难的事,如果能认清自己方言中的声调,再和普通话的声调做对比。
得出他们之间的对应规律,就会取得事半功倍的效果。
举例说明:
比如阴平在你的方言里是多少,和普通话有什么区别?普通话中“天”这个阴平是高平调,而天津人发“天”时也是阴平,但调值是低平调,找到规律后就好改了。
方言是我国的语言多样性的特色,我们来自不同的地区,不同地区的人会说不同的地方语言,这是中国方言的一大特色。由于方言的声调具有差异。只有了解这些声调知识,才能让方言配音游刃有余。
软件开发
2020-05-12 11:43:00
使用scala的开发的软件
spark , apache flink , kafka 等
scala 与java 区别例子 Scala vs Java HelloWorld public class HelloWorld { public static void main(String[] args) { System.out.println("Hello World..."); } } Scala每行代码并不强求使用;结束,但是Java是必须的 object HelloWorld { def main(args : Array[String]) { println("Hello World...") } }
sacla 语法

数字类型转换
判断是否属于某个数字类型 val vs var val:值 final val 值名称:类型 = xxx var:变量 var 值名称:类型 = xxx Scala基本数据类型 Byte/Char Short/Int/Long/Float/Double Boolean
lazy
scala 函数 函数/方法的定义 def 方法名(参数名:参数类型): 返回值类型 = { // 括号内的叫做方法体 // 方法体内的最后一行为返回值,不需要使用return } 默认参数: 在函数定义时,允许指定参数的默认值 $SPARK_HOME/conf/spark-defaults.conf 可变参数: JDK5+ : 可变参数 循环表达式 to Range until
定义与使用
命名参数
调用参数可以顺序不一样
可变参数
即参数可以有任意多个 /** * 可变参数 ,可以有多个 类似 java 的 ... * @param numbers */ def sum(numbers:Int*): Unit ={
表达式
if else 等
循环表达式
def loop(): Unit ={ var num1 = 1 to 10 var num2 = 1.to(10) // 与 1 to 10 写法一样的 for (elem <- num1) { println(elem) } for( i <- 1 to 10 if i%2 == 0){ println(i) } val courses=Array("语文","数学") for (elem <- courses) { } courses.foreach(f => println(f)) courses.foreach(f => { println(f) } ) }
面向对象

占位符 _ 下划线,即 使用默认值,不赋值

类的定义与使用 package org.train.scala.c4 object SimpleApp { def main(args: Array[String]): Unit = { val person=new People(); person.name="ss" println(person.name+ person.age) person.eat() person.printInfo() } } class People{ //变量,自动生成 get /set 方法 var name="" var nick : String =_ //不可变常量,没有 get/set val age:Int = 10 def eat(): String={ name +"eat ..." } //只能在class 内可以访问,类外不可以 private [this] val gender="male" // [this] 可去掉: private val gender="male" def printInfo(): Unit ={ println(gender) } }
构造器 package com.imooc.scala.course04 object ConstructorApp { def main(args: Array[String]): Unit = { // val person = new Person("zhangsan", 30) // println(person.name + " : " + person.age + " : " + person.school) // // // val person2 = new Person("PK", 18, "M") // println(person2.name + " : " // + person2.age + " : " // + person2.school + " :" // + person2.gender // ) val student = new Student("PK", 18, "Math") println(student.name + " : " + student.major) println(student) } } // 主构造器 class Person(val name:String, val age:Int) { println("Person Constructor enter....") val school = "ustc" var gender:String = _ // 附属构造器 def this(name:String, age:Int, gender:String) { this(name, age) // 附属构造器的第一行代码必须要调用主构造器或者其他附属构造器 this.gender = gender } println("Person Constructor leave....") } class Student(name:String, age:Int, var major:String) extends Person(name, age) { println("Person Student enter....") //重写 override val school = "peking" override def toString: String = "Person: override def toString :" + school println("Person Student leave....") }
抽象类 abstract package org.train.scala.c4 object AbstractApp { def main(args: Array[String]): Unit = { new Stude2().speak; } } /** * 只有定义,没有实现 */ abstract class Person2{ //抽象方法 def speak //抽象的属性 val name:String } class Stude2 extends Person2{ override def speak: Unit = { println("sk") } override val name: String = _ }
伴生类和伴生对象 /** * 伴生类和伴生对象 * 如果有一个class,还有一个与class同名的object * 那么就称这个object是class的伴生对象,class是object的伴生类 */ //伴生类 (相对 object ApplyTest ) class ApplyTest{ } //伴生对象 (相对class ApplyTest 来说的 ) object ApplyTest{ }
apply 方法 package com.imooc.scala.course04 object ApplyApp { def main(args: Array[String]): Unit = { // for(i <- 1 to 10) { // ApplyTest.incr // } // // println(ApplyTest.count) // 10 说明object本身就是一个单例对象 val b = ApplyTest() // ==> Object.apply println("~~~~~~~~~~~") val c = new ApplyTest() println(c) c() // 类名() ==> Object.apply // 对象() ==> Class.apply } } /** * 伴生类和伴生对象 * 如果有一个class,还有一个与class同名的object * 那么就称这个object是class的伴生对象,class是object的伴生类 */ class ApplyTest{ def apply() = { println("class ApplyTest apply....") } } object ApplyTest{ println("Object ApplyTest enter....") var count = 0 def incr = { count = count + 1 } // 最佳实践:在Object的apply方法中去new Class def apply():ApplyTest = { println("Object ApplyTest apply....") // 在object中的apply中new class new ApplyTest } println("Object ApplyTest leave....") }
case class // 通常用在模式匹配 object CaseClassApp { def main(args: Array[String]): Unit = { println(Dog("wangcai").name) } } // case class不用new case class Dog(name:String) case class Dog2(name: String)
trait 接口 Java/Scala OO 封装:属性、方法封装到类中 Person: private int id, String name, Date birthday..... getter/setter eat、sleep.... 继承:父类和子类之间的关系 User extends Person exam..... 多态:***** 父类引用指向子类对象 精髓所在 开发框架的基石 Person person = new Person(); User user = new User(); Person person = new User(); Trait xxx extends ATrait with BTrait class SparkConf(loadDefaults: Boolean) extends Cloneable with Logging with Serializable ... ....
集合
数组 package com.imooc.scala.course05 object ArrayApp extends App{ // val a = new Array[String](5) // a.length // a(1) = "hello" // // val b = Array("hadoop", "spark", "storm") // // val c = Array(2,3,4,5,6,7,8,9) // c.sum // c.min // c.max // // c.mkString(",") // 可变数组 val c = scala.collection.mutable.ArrayBuffer[Int]() c += 1 c += 2 c += (3,4,5) c ++= Array(6,7,8) c.insert(0,0) c.remove(1) c.remove(0,3) c.trimEnd(2) // for(i <- 0 until c.length) { // println(c(i)) // } // for(ele <- c) { // println(ele) // } for(i <- (0 until c.length).reverse) { println(c(i)) } // println(c.toArray.mkString) }
List
nil 就是 一个空的集合的意思
package com.imooc.scala.course05 object ListApp extends App{ // val l = List(1,2,3,4,5) // // // val l5 = scala.collection.mutable.ListBuffer[Int]()//可变list // l5 += 2 // l5 += (3,4,5) // l5 ++= List(6,7,8,9) // // // l5 -= 2 // l5 -= 3 // l5 -= (1, 4) // l5 --= List(5,6,7,8) // // println(l5) // // // l5.isEmpty // l5.head // l5.tail def sum(nums:Int*):Int = { if(nums.length == 0) { 0 } else { nums.head + sum(nums.tail:_*) // _* 类型自动转换 } } // val set = scala.collection.mutable.Set[Int]() // set += 1 // set += (1,1) println(sum()) println(sum(1,2,3,4)) }
head 就是 第一个, 而 tail 就是 除了 第一个数据的 之后的所有数据
set
无序,不可重复的 list ,用法与 list 类似 // val set = scala.collection.mutable.Set[Int]() //可变set // set += 1 // set += (1,1)
map

package com.imooc.scala.course05 import scala.collection.mutable object MapApp extends App{ val a = Map("PK" -> 18, "zhangsan" -> 30) val b = Map("PK" -> 18, "zhangsan" -> 30) // val c = mutable.HashMap[String,Int]() // b.getOrElse("PK", 9) // for((key,value) <- b) { // println(key + " : " + value ) // } // for(key <- b.keySet) { // println(key + " : " + b.getOrElse(key, 9)) // } // for(value <- b.values) { // println(value) // } for((key,_) <- b) { println(key + " : " + b.getOrElse(key, 9) ) } }
Option , Some, None
object OptionApp extends App { val m = Map(1 -> 2) // println(m(1)) // println(m(2)) // println(m.get(1).get) println(m.getOrElse(2, "None")) } /** * case object None extends Option[Nothing] { def isEmpty = true def get = throw new NoSuchElementException("None.get") } final case class Some[+A](x: A) extends Option[A] { def isEmpty = false def get = x } */
Option 是一个抽象类 , 实现类是 Some ,None
Some 代表存在, None 代表空
Tuple // 元组:(.......) object TupleApp extends App{ val a = (1,2,3,4,5) for(i <- 0 until(a.productArity)) { println(a.productElement(i)) } //下标从 1 开始,不是 0 val hostPort = ("localhost",8080) hostPort._1 hostPort._2 }
模式匹配
即 match case
类似 java 的 switch case 用法,但是比JAVA的强大很多 模式匹配 Java: 对一个值进行条件判断,返回针对不同的条件进行不同的处理 变量 match { case value1 => 代码1 case value2 => 代码2 ..... case _ => 代码N }
基本模式匹配 def grade(grade : String): Unit = { grade match{ case "A" => println("99。。。") case "B" => println("89。。。") case _ => println("00。。。") } }
条件匹配 def grade(grade : String): Unit = { grade match{ case "A" => println("99。。。") case "B" => println("89。。。") //条件匹配, 双重过滤 case _ if(grade=="C") => println("cc。。。") case _ => println("00。。。") } }
Array 模式匹配 def greeting(array:Array[String]): Unit = { array match { case Array("zhangsan") => println("Hi:zhangsan") //数组必须是 zhangsan case Array(x,y) => println("Hi:" + x + " , " + y) // 数组可以有任意的2个 case Array("zhangsan", _*) => println("Hi:zhangsan and other friends...")//多个内容, case _ => println("Hi: everybody...") } }
List 模式匹配 def greeting(list:List[String]): Unit = { list match { case "zhangsan"::Nil => println("Hi:zhangsan") //只有 zhangsan case x::y::Nil => println("Hi:" + x + " , " + y) //只有2个 任意元素 case "zhangsan"::tail => println("Hi: zhangsan and other friends...") // 多个 case _ => println("Hi:everybody....") } }
类型匹配 def matchType(obj:Any): Unit = { obj match { case x:Int => println("Int") case x:String => println("String") case m:Map[_,_] => m.foreach(println) // map , key value 任意 case _ => println("other type") } }
异常处理 //IO val file = "test.txt" try{ // open file // use file val i = 10/0 println(i) } catch { case e:ArithmeticException => println("除数不能为0..") case e:Exception => println(e.getMessage) } finally { // 释放资源,一定能执行: close file }
case class 模式匹配 def caseclassMatch(person:Person): Unit = { person match { case CTO(name,floor) => println("CTO name is: " + name + " , floor is: " + floor) case Employee(name,floor) => println("Employee name is: " + name + " , floor is: " + floor) case _ => println("other") } } class Person case class CTO(name:String, floor:String) extends Person case class Employee(name:String, floor:String) extends Person case class Other(name:String) extends Person caseclassMatch(CTO("PK", "22")) caseclassMatch(Employee("zhangsan", "2")) caseclassMatch(Other("other"))
Some None 模式匹配 val grades = Map("PK"->"A", "zhangsan"-> "C") def getGrade(name:String): Unit = { val grade = grades.get(name) grade match { case Some(grade) => println(name + ": your grade is :" + grade) case None => println("Sorry....") } }
高级函数
字符串高级操作 object StringApp extends App { val s = "Hello:" val name = "PK" // println(s + name) println(s"Hello:$name") val team = "AC Milan" // 插值 println(s"Hello:$name, Welcome to $team") val b = """ |这是一个多行字符串 |hello |world |PK """.stripMargin println(b) }
查值,就是 s + “ xx $属性名 ” , 必须是 s 开头的, 属性名 必须带上 $
匿名函数
匿名参数传递给 对应函数
匿名函数传递给 add 函数 , 或者 匿名函数传递给变量都是可以的
currying 函数 // 将原来接收两个参数的一个函数,转换成2个 def sum(a:Int, b:Int) = a+b println(sum(2,3)) // 改成 currying 函数 def sum2(a:Int)(b:Int) = a + b println(sum2(2)(3))
即将参数给 拆开
高阶函数

package com.imooc.scala.course07 /** * 匿名函数: 函数是可以命名的,也可以不命名 * (参数名:参数类型...) => 函数体 */ object FunctionApp extends App { // def sayHello(name:String): Unit = { // println("Hi: " + name) // } // // sayHello("PK") // // 将原来接收两个参数的一个函数,转换成2个 // def sum(a:Int, b:Int) = a+b // println(sum(2,3)) // // def sum2(a:Int)(b:Int) = a + b // println(sum2(2)(3)) val l = List(1, 2, 3, 4, 5, 6, 7, 8) //map: 逐个去操作集合中的每个元素 // l.map((x: Int) => x + 1) // l.map((x) => x * 2) // l.map(x => x * 2) // l.map(_ * 2).foreach(println) // l.map(_ * 2).filter(_ > 8).foreach(println) // 1+2 3+3 6+4 10+5 // l.reduce(_+_) // 即 求和 sum l.reduceLeft(_-_) l.reduceRight(_-_) l.fold(0)(_-_) println( l.fold(100)(_ + _)) // 从 100 开始 以此 求和 val f = List(List(1,2),List(3,4),List(5,6)) f.flatten // // 压扁 合成一个 List // flatMap f.map(_.map(_*2)) //每个元素 乘以2 f.flatMap(_.map(_*2)) // 并合成一个 List, 每个元素 乘以2 , val txt = scala.io.Source.fromFile("/Users/rocky/imooc/hello.txt").mkString // println(txt) val txts = List(txt) // 如何使用scala来完成wordcount统计 // 链式编程:Hibernate、Spark txts.flatMap(_.split(",")).map(x => (x,1)) //... .foreach(println) }
偏函数 /** * 偏函数:被包在花括号内没有match的一组case语句 */ object PartitalFunctionApp extends App { val names = Array("Akiho Yoshizawa", "YuiHatano", "Aoi Sola") val name = names(Random.nextInt(names.length)) name match { case "Akiho Yoshizawa" => println("吉老师...") case "YuiHatano" => println("波老师....") case _ => println("真不知道你们在说什么....") } // A 输入参数类型 B 输出参数类型 // A 输入参数类型 第一个参数, B 输出参数类型 第二个参数, def sayChinese:PartialFunction[String,String] = { case "Akiho Yoshizawa" => "吉老师..." case "YuiHatano" => "波老师...." case _ => "真不知道你们在说什么...." } println(sayChinese("Akiho Yoshizawa")) }
隐式转换
即类似 java 的 动态代理 AOP
需求:为一个已存在的类添加一个新的方法
Java:动态代理
Scala:隐式转换
双刃剑
Spark/Hive/MR.... 调优 import java.io.File //import ImplicitAspect._ object ImplicitApp { // 定义隐式转换函数即可 // implicit def man2superman(man:Man):Superman = new Superman(man.name) // val man = new Man("PK") // man.fly() // implicit def file2RichFile(file: File): RichFile = new RichFile(file) // val file = new File("/Users/rocky/imooc/hello.txt") // val txt = file.read() // println(txt) } //class Man(val name: String) { // def eat(): Unit = { // println(s"man[ $name ] eat ..... ") // } //} // //class Superman(val name: String) { // def fly(): Unit = { // println(s"superman[ $name ] fly ..... ") // } //} class RichFile(val file: File) { def read() = { scala.io.Source.fromFile(file.getPath).mkString } }
隐式参数
object ImplicatParam extends App { def testParam(implicit name:String): Unit ={ println(name) } // testParam("aa") // implicit val name2 ="bb" // testParam// 不会报错,默认使用 name2 // implicit val s1="s1" // implicit val s2="s2" // testParam // 多个 会报错,不知道用 s1 还是 s2 }
隐式类 object ImplicitClassApp extends App { implicit class Calcuotor(x:Int){ def add(a:Int) = a+x } println(1.add(3)) // 可以使用调用 Calcuotor 方法 }
scala 操作外部数据
读取文件及网络数据 import scala.io.Source object FileApp { def main(args: Array[String]): Unit = { val file = Source.fromFile("/Users/rocky/imooc/hello.txt")(scala.io.Codec.ISO8859) def readLine(): Unit ={ for(line <- file.getLines()){ println(line) } } //readLine() def readChar(): Unit ={ for(ele <- file) { println(ele) } } // readChar() def readNet(): Unit ={ val file = Source.fromURL("http://www.baidu.com") for(line <- file.getLines()){ println(line) } } readNet() } }
读取mysql package org.train.scala.c9 import java.sql.{Connection, DriverManager} object MySQLApp { def main(args: Array[String]): Unit = { val url = "jdbc:mysql://localhost:3306/mysql" val username = "root" val password = "123456" var connection:Connection = null try{ // make the connection classOf[com.mysql.jdbc.Driver] connection = DriverManager.getConnection(url, username, password) // create the statement, and run the select query val statement = connection.createStatement() val resultSet = statement.executeQuery("select host,user from user") while(resultSet.next()){ val host = resultSet.getString("host") val user = resultSet.getString("user") println(s"$host, $user") } } catch { case e:Exception => e.printStackTrace() } finally { // free if(connection == null) { connection.close() } } } }
与java 写法一样的
操作xml
FIX4.2 Test
package org.train.scala.c9 import java.io.{FileInputStream, InputStreamReader} import scala.xml.XML object XMLApp { def main(args: Array[String]): Unit = { // loadXML() // readXMLAttr() updateXML() } def updateXML(): Unit ={ val xml = XML.load(this.getClass.getClassLoader.getResource("books.xml")) // println(xml) val bookMap = scala.collection.mutable.HashMap[String,String]() (xml \ "book").map(x => { val id = (x \ "@id").toString() val name = (x \ "name").text.toString bookMap(id) = name }) // for((key,value) <- bookMap) { // println(s"$key : $value") // } val newXml = {bookMap.map(updateXmlFile)} // println(newXml) XML.save("newBooks.xml", newXml) } def updateXmlFile(ele:(String,String)) = { val (id, oldName) = ele {oldName + " Programming"} } def readXMLAttr(): Unit = { val xml = XML.load(this.getClass.getClassLoader.getResource("pk.xml")) // println(xml) // header/field // val headerField = xml \ "header" \ "field" // println(headerField) // all field // val fields = xml \\ "field" // for (field <- fields) { // println(field) // } // header/field/name // val fieldAttributes = (xml \ "header" \ "field").map(_ \ "@name") // val fieldAttributes = (xml \ "header" \ "field" \\ "@name") // for (fieldAttribute <- fieldAttributes) { // println(fieldAttribute) // } //name="Logon" message // val filters = (xml \\ "message") // .filter(_.attribute("name").exists(_.text.equals("Logon"))) // val filters = (xml \\ "message") // .filter(x => ((x \ "@name").text).equals("Logon")) // for (filter <- filters) { // println(filter) // } // header/field content (xml \ "header" \ "field") .map(x => (x \ "@name", x.text, x \ "@required")) .foreach(println) } def loadXML(): Unit = { // val xml = XML.load(this.getClass.getClassLoader.getResource("test.xml")) // println(xml) // val xml = XML.load(new FileInputStream("/Users/rocky/source/scala-train/src/main/resources/test.xml")) // println(xml) val xml = XML.load( new InputStreamReader( new FileInputStream("/Users/rocky/source/scala-train/src/main/resources/test.xml") ) ) println(xml) } }
scala 结合spring boot
参考 https://gitee.com/odilil/boot-scala?_from=gitee_search
bean 注入 还可以: @Autowired val metaTableRepository : MetaTableRepository = null // 不能使用 _ 下划线 占位符来标识



软件开发
2020-06-03 11:37:00
特别要注意第二窗体对应的view的fxml文件路径写法(view和controller都放在各自的包下): @FXML protected void show2Action() throws IOException { Stage stage = new Stage(); stage.setTitle("第二个窗体实验"); AnchorPane pane = FXMLLoader.load(getClass().getResource("../javafxcontroller2.fxml"));//要注意fxml文件的类路径写法(与本JavaFXController.java的相对关系) Scene scene = new Scene(pane,300,400); stage.setScene(scene); stage.initModality(Modality.APPLICATION_MODAL);//模式窗体 stage.setOnCloseRequest(new EventHandler() { @Override public void handle(WindowEvent event) { System.out.println("window close event"); if(i++<2){ event.consume(); // i=0; } else i=0; } }); stage.show(); // stage.showAndWait(); //等待stage关闭之后才能继续运行 System.out.println("stage.showAndWait"); // stage.show(); }
软件开发
2020-06-03 11:29:00
1.触发条件: MinorGC:当Eden去满了就会触发。 FullGC:
1.调用System.gc()后,建议FullGC,但不一定会执行;
2.老年代空间满了;
3.方法区满了;
4.调用MinorGC后进入老年代的平均大小大于老年代可用内存;
软件开发
2020-05-31 23:35:00
一、ThreadLocal简介
  多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,为了保证线程安全,一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性。ThreadLocal是除了加锁这种同步方式之外的一种保证一种规避多线程访问出现线程不安全的方法,当我们在创建一个变量后,如果每个线程对其进行访问的时候访问的都是线程自己的变量这样就不会存在线程不安全问题。
  ThreadLocal是JDK包提供的,它提供线程本地变量,如果创建一乐ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题,如下图所示

二、ThreadLocal简单使用
  下面的例子中,开启两个线程,在每个线程内部设置了本地变量的值,然后调用print方法打印当前本地变量的值。如果在打印之后调用本地变量的remove方法会删除本地内存中的变量,代码如下所示
1 package test; 2 3 public class ThreadLocalTest { 4 5 static ThreadLocal localVar = new ThreadLocal<> (); 6 7 static void print(String str) { 8 // 打印当前线程中本地内存中本地变量的值 9 System.out.println(str + " :" + localVar.get()); 10 // 清除本地内存中的本地变量 11 localVar.remove(); 12 } 13 14 public static void main(String[] args) { 15 Thread t1 = new Thread( new Runnable() { 16 @Override 17 public void run() { 18 // 设置线程1中本地变量的值 19 localVar.set("localVar1" ); 20 // 调用打印方法 21 print("thread1" ); 22 // 打印本地变量 23 System.out.println("after remove : " + localVar.get()); 24 } 25 }); 26 27 Thread t2 = new Thread( new Runnable() { 28 @Override 29 public void run() { 30 // 设置线程1中本地变量的值 31 localVar.set("localVar2" ); 32 // 调用打印方法 33 print("thread2" ); 34 // 打印本地变量 35 System.out.println("after remove : " + localVar.get()); 36 } 37 }); 38 39 t1.start(); 40 t2.start(); 41 } 42 }
下面是运行后的结果:

三、ThreadLocal的实现原理
  下面是ThreadLocal的类图结构,从图中可知:Thread类中有两个变量threadLocals和inheritableThreadLocals,二者都是ThreadLocal内部类ThreadLocalMap类型的变量,我们通过查看内部内ThreadLocalMap可以发现实际上它类似于一个HashMap。在默认情况下,每个线程中的这两个变量都为null ,只有当线程第一次调用ThreadLocal的set或者get方法的时候才会创建他们(后面我们会查看这两个方法的源码)。除此之外,和我所想的不同的是,每个线程的本地变量不是存放在ThreadLocal实例中,而是放在调用线程的ThreadLocals变量里面(前面也说过,该变量是Thread类的变量)。也就是说,ThreadLocal类型的本地变量是存放在具体的线程空间上,其本身相当于一个装载本地变量的工具壳,通过set方法将value添加到调用线程的threadLocals中,当调用线程调用get方法时候能够从它的threadLocals中取出变量。如果调用线程一直不终止,那么这个本地变量将会一直存放在他的threadLocals中,所以不使用本地变量的时候需要调用remove方法将threadLocals中删除不用的本地变量。下面我们通过查看ThreadLocal的set、get以及remove方法来查看ThreadLocal具体实怎样工作的
  1、set方法源码
1 public void set(T value) { 2 //(1) 获取当前线程(调用者线程) 3 Thread t = Thread.currentThread(); 4 //(2) 以当前线程作为key值,去查找对应的线程变量,找到对应的map 5 ThreadLocalMap map = getMap(t); 6 //(3) 如果map不为null,就直接添加本地变量,key为当前线程,值为添加的本地变量值 7 if (map != null ) 8 map.set( this , value); 9 //(4) 如果map为null,说明首次添加,需要首先创建出对应的map 10 else 11 createMap(t, value); 12 }
  在上面的代码中,(2)处调用getMap方法获得当前线程对应的threadLocals(参照上面的图示和文字说明),该方法代码如下
ThreadLocalMap getMap(Thread t) { return t.threadLocals; // 获取线程自己的变量threadLocals,并绑定到当前调用线程的成员变量threadLocals上 }
  如果调用getMap方法返回值不为null,就直接将value值设置到threadLocals中(key为当前线程引用,值为本地变量);如果getMap方法返回null说明是第一次调用set方法(前面说到过,threadLocals默认值为null,只有调用set方法的时候才会创建map),这个时候就需要调用createMap方法创建threadLocals,该方法如下所示
1 void createMap(Thread t, T firstValue) { 2 t.threadLocals = new ThreadLocalMap( this , firstValue); 3 }
  createMap方法不仅创建了threadLocals,同时也将要添加的本地变量值添加到了threadLocals中。
  2、get方法源码
  在get方法的实现中,首先获取当前调用者线程,如果当前线程的threadLocals不为null,就直接返回当前线程绑定的本地变量值,否则执行setInitialValue方法初始化threadLocals变量。在setInitialValue方法中,类似于set方法的实现,都是判断当前线程的threadLocals变量是否为null,是则添加本地变量(这个时候由于是初始化,所以添加的值为null),否则创建threadLocals变量,同样添加的值为null。
1 public T get() { 2 // (1)获取当前线程 3 Thread t = Thread.currentThread(); 4 // (2)获取当前线程的threadLocals变量 5 ThreadLocalMap map = getMap(t); 6 // (3)如果threadLocals变量不为null,就可以在map中查找到本地变量的值 7 if (map != null ) { 8 ThreadLocalMap.Entry e = map.getEntry( this ); 9 if (e != null ) { 10 @SuppressWarnings("unchecked" ) 11 T result = (T)e.value; 12 return result; 13 } 14 } 15 // (4)执行到此处,threadLocals为null,调用该更改初始化当前线程的threadLocals变量 16 return setInitialValue(); 17 } 18 19 private T setInitialValue() { 20 // protected T initialValue() {return null;} 21 T value = initialValue(); 22 // 获取当前线程 23 Thread t = Thread.currentThread(); 24 // 以当前线程作为key值,去查找对应的线程变量,找到对应的map 25 ThreadLocalMap map = getMap(t); 26 // 如果map不为null,就直接添加本地变量,key为当前线程,值为添加的本地变量值 27 if (map != null ) 28 map.set( this , value); 29 // 如果map为null,说明首次添加,需要首先创建出对应的map 30 else 31 createMap(t, value); 32 return value; 33 }
  3、remove方法的实现
  remove方法判断该当前线程对应的threadLocals变量是否为null,不为null就直接删除当前线程中指定的threadLocals变量
1 public void remove() { 2 // 获取当前线程绑定的threadLocals 3 ThreadLocalMap m = getMap(Thread.currentThread()); 4 // 如果map不为null,就移除当前线程中指定ThreadLocal实例的本地变量 5 if (m != null ) 6 m.remove( this ); 7 }
  4、如下图所示:每个线程内部有一个名为threadLocals的成员变量,该变量的类型为ThreadLocal.ThreadLocalMap类型(类似于一个HashMap),其中的key为当前定义的ThreadLocal变量的this引用,value为我们使用set方法设置的值。每个线程的本地变量存放在自己的本地内存变量threadLocals中,如果当前线程一直不消亡,那么这些本地变量就会一直存在(所以可能会导致内存溢出),因此使用完毕需要将其remove掉。
四、ThreadLocal不支持继承性
  同一个ThreadLocal变量在父线程中被设置值后,在子线程中是获取不到的。(threadLocals中为当前调用线程对应的本地变量,所以二者自然是不能共享的)
1 package test; 2 3 public class ThreadLocalTest2 { 4 5 // (1)创建ThreadLocal变量 6 public static ThreadLocal threadLocal = new ThreadLocal<> (); 7 8 public static void main(String[] args) { 9 // 在main线程中添加main线程的本地变量 10 threadLocal.set("mainVal" ); 11 // 新创建一个子线程 12 Thread thread = new Thread( new Runnable() { 13 @Override 14 public void run() { 15 System.out.println("子线程中的本地变量值:"+ threadLocal.get()); 16 } 17 }); 18 thread.start(); 19 // 输出main线程中的本地变量值 20 System.out.println("mainx线程中的本地变量值:"+ threadLocal.get()); 21 } 22 }

五、InheritableThreadLocal类
  在上面说到的ThreadLocal类是不能提供子线程访问父线程的本地变量的,而InheritableThreadLocal类则可以做到这个功能,下面是该类的源码
1 public class InheritableThreadLocal extends ThreadLocal { 2 3 protected T childValue(T parentValue) { 4 return parentValue; 5 } 6 7 ThreadLocalMap getMap(Thread t) { 8 return t.inheritableThreadLocals; 9 } 10 11 void createMap(Thread t, T firstValue) { 12 t.inheritableThreadLocals = new ThreadLocalMap( this , firstValue); 13 } 14 }
  从上面代码可以看出,InheritableThreadLocal类继承了ThreadLocal类,并重写了childValue、getMap、createMap三个方法。其中createMap方法在被调用(当前线程调用set方法时得到的map为null的时候需要调用该方法)的时候,创建的是inheritableThreadLocal而不是threadLocals。同理,getMap方法在当前调用者线程调用get方法的时候返回的也不是threadLocals而是inheritableThreadLocal。
  下面我们看看重写的childValue方法在什么时候执行,怎样让子线程访问父线程的本地变量值。我们首先从Thread类开始说起
1 private void init(ThreadGroup g, Runnable target, String name, 2 long stackSize) { 3 init(g, target, name, stackSize, null , true ); 4 } 5 private void init(ThreadGroup g, Runnable target, String name, 6 long stackSize, AccessControlContext acc, 7 boolean inheritThreadLocals) { 8 // 判断名字的合法性 9 if (name == null ) { 10 throw new NullPointerException("name cannot be null" ); 11 } 12 13 this .name = name; 14 // (1)获取当前线程(父线程) 15 Thread parent = currentThread(); 16 // 安全校验 17 SecurityManager security = System.getSecurityManager(); 18 if (g == null ) { // g:当前线程组 19 if (security != null ) { 20 g = security.getThreadGroup(); 21 } 22 if (g == null ) { 23 g = parent.getThreadGroup(); 24 } 25 } 26 g.checkAccess(); 27 if (security != null ) { 28 if (isCCLOverridden(getClass())) { 29 security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION); 30 } 31 } 32 33 g.addUnstarted(); 34 35 this .group = g; // 设置为当前线程组 36 this .daemon = parent.isDaemon(); // 守护线程与否(同父线程) 37 this .priority = parent.getPriority(); // 优先级同父线程 38 if (security == null || isCCLOverridden(parent.getClass())) 39 this .contextClassLoader = parent.getContextClassLoader(); 40 else 41 this .contextClassLoader = parent.contextClassLoader; 42 this .inheritedAccessControlContext = 43 acc != null ? acc : AccessController.getContext(); 44 this .target = target; 45 setPriority(priority); 46 //(2) 如果父线程的inheritableThreadLocal不为null 47 if (inheritThreadLocals && parent.inheritableThreadLocals != null ) 48 //(3) 设置子线程中的inheritableThreadLocals为父线程的inheritableThreadLocals 49 this .inheritableThreadLocals = 50 ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); 51 this .stackSize = stackSize; 52 53 tid = nextThreadID(); 54 }
  在init方法中,首先(1)处获取了当前线程(父线程),然后(2)处判断当前父线程的inheritableThreadLocals是否为null,然后调用createInheritedMap将父线程的inheritableThreadLocals作为构造函数参数创建了一个新的ThreadLocalMap变量,然后赋值给子线程。下面是createInheritedMap方法和ThreadLocalMap的构造方法
1 static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) { 2 return new ThreadLocalMap(parentMap); 3 } 4 5 private ThreadLocalMap(ThreadLocalMap parentMap) { 6 Entry[] parentTable = parentMap.table; 7 int len = parentTable.length; 8 setThreshold(len); 9 table = new Entry[len]; 10 11 for ( int j = 0; j < len; j++ ) { 12 Entry e = parentTable[j]; 13 if (e != null ) { 14 @SuppressWarnings("unchecked" ) 15 ThreadLocal key = (ThreadLocal ) e.get(); 16 if (key != null ) { 17 // 调用重写的方法 18 Object value = key.childValue(e.value); 19 Entry c = new Entry(key, value); 20 int h = key.threadLocalHashCode & (len - 1 ); 21 while (table[h] != null ) 22 h = nextIndex(h, len); 23 table[h] = c; 24 size++ ; 25 } 26 } 27 } 28 }
  在构造函数中将父线程的inheritableThreadLocals成员变量的值赋值到新的ThreadLocalMap对象中。返回之后赋值给子线程的inheritableThreadLocals。总之,InheritableThreadLocals类通过重写getMap和createMap两个方法将本地变量保存到了具体线程的inheritableThreadLocals变量中,当线程通过InheritableThreadLocals实例的set或者get方法设置变量的时候,就会创建当前线程的inheritableThreadLocals变量。而父线程创建子线程的时候,ThreadLocalMap中的构造函数会将父线程的inheritableThreadLocals中的变量复制一份到子线程的inheritableThreadLocals变量中。

六、从ThreadLocalMap看ThreadLocal使用不当的内存泄漏问题
1、基础概念
  首先我们先看看ThreadLocalMap的类图,在前面的介绍中,我们知道ThreadLocal只是一个工具类,他为用户提供get、set、remove接口操作实际存放本地变量的threadLocals(调用线程的成员变量),也知道threadLocals是一个ThreadLocalMap类型的变量,下面我们来看看ThreadLocalMap这个类。在此之前,我们回忆一下Java中的四种引用类型,相关GC只是参考前面系列的文章( JVM相关 )
①强引用:Java中默认的引用类型,一个对象如果具有强引用那么只要这种引用还存在就不会被GC。
②软引用:简言之,如果一个对象具有弱引用,在JVM发生OOM之前(即内存充足够使用),是不会GC这个对象的;只有到JVM内存不足的时候才会GC掉这个对象。软引用和一个引用队列联合使用,如果软引用所引用的对象被回收之后,该引用就会加入到与之关联的引用队列中
③弱引用(这里讨论ThreadLocalMap中的Entry类的重点):如果一个对象只具有弱引用,那么这个对象就会被垃圾回收器GC掉(被弱引用所引用的对象只能生存到下一次GC之前,当发生GC时候,无论当前内存是否足够,弱引用所引用的对象都会被回收掉)。弱引用也是和一个引用队列联合使用,如果弱引用的对象被垃圾回收期回收掉,JVM会将这个引用加入到与之关联的引用队列中。若引用的对象可以通过弱引用的get方法得到,当引用的对象呗回收掉之后,再调用get方法就会返回null
④虚引用:虚引用是所有引用中最弱的一种引用,其存在就是为了将关联虚引用的对象在被GC掉之后收到一个通知。(不能通过get方法获得其指向的对象)
2、分析ThreadLocalMap内部实现
  上面我们知道ThreadLocalMap内部实际上是一个Entry数组 ,我们先看看Entry的这个内部类
1 /** 2 * 是继承自WeakReference的一个类,该类中实际存放的key是 3 * 指向ThreadLocal的弱引用和与之对应的value值(该value值 4 * 就是通过ThreadLocal的set方法传递过来的值) 5 * 由于是弱引用,当get方法返回null的时候意味着坑能引用 6 */ 7 static class Entry extends WeakReference> { 8 /** value就是和ThreadLocal绑定的 */ 9 Object value; 10 11 // k:ThreadLocal的引用,被传递给WeakReference的构造方法 12 Entry(ThreadLocal k, Object v) { 13 super (k); 14 value = v; 15 } 16 } 17 // WeakReference构造方法(public class WeakReference extends Reference ) 18 public WeakReference(T referent) { 19 super (referent); // referent:ThreadLocal的引用 20 } 21 22 // Reference构造方法 23 Reference(T referent) { 24 this (referent, null ); // referent:ThreadLocal的引用 25 } 26 27 Reference(T referent, ReferenceQueue queue) { 28 this .referent = referent; 29 this .queue = (queue == null ) ? ReferenceQueue.NULL : queue; 30 }
  在上面的代码中,我们可以看出,当前ThreadLocal的引用k被传递给WeakReference的构造函数,所以ThreadLocalMap中的key为ThreadLocal的弱引用。当一个线程调用ThreadLocal的set方法设置变量的时候,当前线程的ThreadLocalMap就会存放一个记录,这个记录的key值为ThreadLocal的弱引用,value就是通过set设置的值。如果当前线程一直存在且没有调用该ThreadLocal的remove方法,如果这个时候别的地方还有对ThreadLocal的引用,那么当前线程中的ThreadLocalMap中会存在对ThreadLocal变量的引用和value对象的引用,是不会释放的,就会造成内存泄漏。
  考虑这个ThreadLocal变量没有其他强依赖,如果当前线程还存在,由于线程的ThreadLocalMap里面的key是弱引用,所以当前线程的ThreadLocalMap里面的ThreadLocal变量的弱引用在gc的时候就被回收,但是对应的value还是存在的这就可能造成内存泄漏(因为这个时候ThreadLocalMap会存在key为null但是value不为null的entry项)。
  总结:THreadLocalMap中的Entry的key使用的是ThreadLocal对象的弱引用,在没有其他地方对ThreadLoca依赖,ThreadLocalMap中的ThreadLocal对象就会被回收掉,但是对应的不会被回收,这个时候Map中就可能存在key为null但是value不为null的项,这需要实际的时候使用完毕及时调用remove方法避免内存泄漏。
软件开发
2020-05-31 23:29:00
很多职场转行人员都会遇见这样那样的困惑与问题,今天小编针对同学们的问题作出了有效建议。
我是非计算机专业出身,可以学软件测试吗?
我年纪太大了,竞争不过年轻人,怎么办?
如果学完找不到工作,怎么办?
我是非计算机专业出身,可以学软件测试吗?
首先我想说下,目前国内基本上没有大学开设软件测试的专业,很多人都有误区,其实计算机专业并不属于软件测试专业。
目前这一行要求的主要还是核心技能,测试行业是属于入门容易,但想走远比较难。所以说在转行前一定要确定自己是否真的对这个行业感兴趣,这个可以让自己持续走下去。
我年纪太大了,竞争不过年轻人,怎么办?
(想要超车走捷径可以加入我们Q群(718897738)一起齐头并进加油!)
年龄并不是一个决定因素,重要的还是能力。软件测试岗位更注重的是资历,你的实操经验越多,从业经历越丰富,也就越值钱,所以是“越老越吃香”。
并且软件测试行业的职业生涯是很长远的,入行越久,职位越高,工资越高。

软件开发
2020-05-31 22:29:06
引用与对象
每种编程语言都有自己操作内存中元素的方式,例如在 C 和 C++ 里是通过指针,而在 Java 中则是通过“引用”。
在 Java 中一切都被视为了对象,但是我们操作的标识符实际上是对象的一个引用(reference)。 //创建一个引用,引用可以独立存在,并不一定需要与一个对象关联 String s;
通过将这个叫“引用”的标识符指向某个对象,之后便可以通过这个引用来实现操作对象了。 String str = new String ( "abc" ); System. out .println(str.toString());
在 JDK1.2 之前,Java中的定义很传统:如果 reference 类型的数据中存储的数值代表的是另外一块内存的起始地址,就称为这块内存代表着一个引用。
Java 中的垃圾回收机制在判断是否回收某个对象的时候,都需要依据“引用”这个概念。
在不同垃圾回收算法中,对引用的判断方式有所不同: 引用计数法:为每个对象添加一个引用计数器,每当有一个引用指向它时,计数器就加1,当引用失效时,计数器就减1,当计数器为0时,则认为该对象可以被回收(目前在Java中已经弃用这种方式了)。 可达性分析算法:从一个被称为 GC Roots 的对象开始向下搜索,如果一个对象到GC Roots没有任何引用链相连时,则说明此对象不可用。
JDK1.2 之前,一个对象只有“已被引用”和"未被引用"两种状态,这将无法描述某些特殊情况下的对象,比如,当内存充足时需要保留,而内存紧张时才需要被抛弃的一类对象。
四种引用类型
所以在 JDK.1.2 之后,Java 对引用的概念进行了扩充,将引用分为了:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4 种,这 4 种引用的强度依次减弱。
一,强引用
Java中默认声明的就是强引用,比如: Object obj = new Object (); //只要obj还指向Object对象,Object对象就不会被回收 obj = null ; //手动置null
只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足时,JVM也会直接抛出OutOfMemoryError,不会去回收。如果想中断强引用与对象之间的联系,可以显示的将强引用赋值为null,这样一来,JVM就可以适时的回收对象了
二,软引用
软引用是用来描述一些非必需但仍有用的对象。 在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常 。这种特性常常被用来实现缓存技术,比如网页缓存,图片缓存等。
在 JDK1.2 之后,用java.lang.ref.SoftReference类来表示软引用。
下面以一个例子来进一步说明强引用和软引用的区别:
在运行下面的Java代码之前,需要先配置参数 -Xms2M -Xmx3M,将 JVM 的初始内存设为2M,最大可用内存为 3M。
首先先来测试一下强引用,在限制了 JVM 内存的前提下,下面的代码运行正常 public class TestOOM { public static void main (String[] args) { testStrongReference(); } private static void testStrongReference () { // 当 new byte为 1M 时,程序运行正常 byte [] buff = new byte [ 1024 * 1024 * 1 ]; } }
但是如果我们将 byte [] buff = new byte [ 1024 * 1024 * 1 ];
替换为创建一个大小为 2M 的字节数组 byte [] buff = new byte [ 1024 * 1024 * 2 ];
则内存不够使用,程序直接报错,强引用并不会被回收
接着来看一下软引用会有什么不一样,在下面的示例中连续创建了 10 个大小为 1M 的字节数组,并赋值给了软引用,然后循环遍历将这些对象打印出来。 public class TestOOM { private static List list = new ArrayList<>(); public static void main ( String[] args ) { testSoftReference(); } private static void testSoftReference () { for ( int i = 0 ; i < 10 ; i++) { byte [] buff = new byte [ 1024 * 1024 ]; SoftReference< byte []> sr = new SoftReference<>(buff); list. add (sr); } System.gc(); //主动通知垃圾回收 for ( int i= 0 ; i < list.size(); i++){ Object obj = ((SoftReference) list. get (i)). get (); System. out .println(obj); } } }
打印结果:
我们发现无论循环创建多少个软引用对象,打印结果总是只有最后一个对象被保留,其他的obj全都被置空回收了。
这里就说明了在内存不足的情况下,软引用将会被自动回收。
值得注意的一点 , 即使有 byte[] buff 引用指向对象, 且 buff 是一个strong reference, 但是 SoftReference sr 指向的对象仍然被回收了,这是因为Java的编译器发现了在之后的代码中, buff 已经没有被使用了, 所以自动进行了优化。
如果我们将上面示例稍微修改一下: private static void testSoftReference () { byte [] buff = null ; for ( int i = 0 ; i < 10 ; i++) { buff = new byte [ 1024 * 1024 ]; SoftReference< byte []> sr = new SoftReference<>(buff); list. add (sr); } System.gc(); //主动通知垃圾回收 for ( int i= 0 ; i < list.size(); i++){ Object obj = ((SoftReference) list. get (i)). get (); System. out .println(obj); } System. out .println( "buff: " + buff.toString()); }
则 buff 会因为强引用的存在,而无法被垃圾回收,从而抛出OOM的错误。
如果一个对象惟一剩下的引用是软引用,那么该对象是软可及的(softly reachable)。垃圾收集器并不像其收集弱可及的对象一样尽量地收集软可及的对象,相反,它只在真正 “需要” 内存时才收集软可及的对象。
三,弱引用
弱引用的引用强度比软引用要更弱一些, 无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收 。在 JDK1.2 之后,用 java.lang.ref.WeakReference 来表示弱引用。
我们以与软引用同样的方式来测试一下弱引用: private static void testWeakReference () { for ( int i = 0 ; i < 10 ; i++) { byte [] buff = new byte [ 1024 * 1024 ]; WeakReference< byte []> sr = new WeakReference<>(buff); list. add (sr); } System.gc(); //主动通知垃圾回收 for ( int i= 0 ; i < list.size(); i++){ Object obj = ((WeakReference) list. get (i)). get (); System. out .println(obj); } }
打印结果:
可以发现所有被弱引用关联的对象都被垃圾回收了。
四,虚引用
虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收,在 JDK1.2 之后,用 PhantomReference 类来表示,通过查看这个类的源码,发现它只有一个构造函数和一个 get() 方法,而且它的 get() 方法仅仅是返回一个null,也就是说将永远无法通过虚引用来获取对象,虚引用必须要和 ReferenceQueue 引用队列一起使用。 public class PhantomReference extends Reference { /** * Returns this reference object's referent. Because the referent of a * phantom reference is always inaccessible, this method always returns * null. * * @return null */ public T get() { return null ; } public PhantomReference ( T referent, ReferenceQueue q) { super (referent, q); } }
那么传入它的构造方法中的 ReferenceQueue 又是如何使用的呢?
五,引用队列(ReferenceQueue)
引用队列可以与软引用、弱引用以及虚引用一起配合使用,当垃圾回收器准备回收一个对象时,如果发现它还有引用,那么就会在回收对象之前,把这个引用加入到与之关联的引用队列中去。程序可以通过判断引用队列中是否已经加入了引用,来判断被引用的对象是否将要被垃圾回收,这样就可以在对象被回收之前采取一些必要的措施。
与软引用、弱引用不同,虚引用必须和引用队列一起使用。
软件开发
2020-05-31 22:28:00
6.15日:这几天把for,while遍历,重要的函数lambda,map函数,函数的定义,等一些基础的东西学了..还有1个大节学完后,就可以学习应用的demo了..嘎嘎
6.9日:这几天学习了元组,字典,集合等数组的定义和增删查改等;下一步开始学习python的逻辑和流程了..
6.3日:学习了python列表,其实和php的数组用法是一样的,不过有很多语法糖不一样,比如增加的append,expend,删除的pop和del,修改直接通过定位下标修改,查找有count,index,in三种,然后还有个反转数组的reverse.
6.2日:学习了变量的N种查找,填充居中,替换,转换,分割,大小写,判断,编码等等方法,有点乱,幸好我有跟着写...感觉python的很多语法糖和php还是有很大区别,当然共同的也有,比如python的split和php的explode函数就一毛一样;今天已经上完第二大节了,还有8个大节.
6.1日:学习了变量N种赋值方式,,还有关于数字的计算,成功在自己本地运行了这些demo,感觉自己回到了刚学php那种感觉,我爱学习!!!还有什么比学习更快乐呢! a=333 b=c=444 #序列解包 d,e,f=555,666,777 #查看变量指向的内存地址id,如果一个变量0指向,那么就会被进行内存垃圾回收机制 print(id(b),id(c)) #删除变量(指针) #del(b) print(a,b,c,d,e,f); #取整 print(3//2) #取幂 print(2**3) #help函数 help(abs) #name=input('请输入你的姓名:') #print(name) #print(type(name))
5.31日:安装python成功,更新pip失败,我都会背下命令行了,python -m pip install --upgrade pip,命令行意思:加载pip模组,安装,更新pip.可惜一直失败,嘤嘤嘤,哭了,升级不到pip2.0,后来在百度的帮助下,解决了这个问题,执行下面两句命令行,--index-url大致意思应该是换了python的pip更新源头 pip3 install --index-url https://pypi.douban.com/simple requests python -m pip install --upgrade pip --index-url https://pypi.douban.com/simple requests

软件开发
2020-05-31 22:26:00
配置ip地址等信息在/etc/sysconfig/network-scripts/ifcfg-ens33文件里做如下配置:
命令:
vi /etc/sysconfig/network-scripts/ifcfg-ens33
需要修改的地方
BOOTPROTO="static" # 手动分配ip
ONBOOT="yes" # 该网卡是否随网络服务启动
IPADDR="192.168.220.101" # 该网卡ip地址就是你要配置的固定IP,如果你要用xshell等工具连接,220这个网段最好和你自己的电脑网段一致,否则有可能用xshell连接失败
GATEWAY="192.168.220.2" # 网关
NETMASK="255.255.255.0" # 子网掩码
DNS1="8.8.8.8" # DNS,8.8.8.8为Google提供的免费DNS服务器的IP地址
如果做了以上配置,还不能正常连接主机,不能正常连接外网请注意看下面
虚拟机上面的这个网关很重要,一定要与配置的网关一致。

注意IP地址的起始区间,保持在区间范围之内
如果还是不能正常连接外网,看下面的配置文件里面是否有内容
# 查看是否有DNS配置
cat /etc/resolv.conf
# 添加DNS配置
vim /etc/resolv.conf
# 填入DNS服务器
nameserver 8.8.8.8
nameserver 8.8.4.4
软件开发
2020-05-31 22:04:00
首先通过下面链接地址下载 Anaconda 的个人版本。
https://www.anaconda.com/products/individual

从上面下载的地址中,选择你需要的版本,目前 Windows 应该基本上都是 64 位的了。
在你下载的文件中双击运行。
欢迎界面
在弹出的界面中显示了欢迎界面。
许可证
你需要同意许可证,才能让安装继续。
选择用户
在这里你选择默认的用户就可以了。
选择安装目录
在这里将会显示默认的安装目录,Anaconda 的安装比较消耗空间,你需要确定你的磁盘有足够的空间可以按照。
设置一个路径
在安装的时候,不建议设置 PATH,因为可能因为设置 PATH 导致安装的时候出现问题,也有可能会导致 Windows 载入不同的 Python 的版本。
安装进程
你需要等待一些时间,让安装完成。
安装将会按照顺序进行。
安装完成后下一步继续
当安装完成后,可以单击下一步继续。
提示你整合 PyCharm
这一步你不需要做任何事情,下一步继续就可以了。
安装完成
最后将会提示你,安装已经完成了。
单击完成就可以了。
校验安装
在安装好以后,你可以校验安装。
在 Windows 中,你可以选择打开 Anaconda 的命令行,然后输入 conda info 命令。
输入 conda info 命令查看安装的 anaconda。
你也可以输入 python 来查看绑定的版本。
如果你能够看到所有的版本,则说明你的安装已经完成,可以开始使用了。
https://www.ossez.com/t/windows-10-anaconda-3/123
软件开发
2020-05-31 21:32:00
1.Parallel scavenge+old:年轻代采用复制算法,老年代采用标记-整理算法,是多线程的并行收集器。
它注重的是可控制的吞吐量,适合于后台运算而不需要与用户有太多的交互的时候。 吞吐量:是CPU用于运行用户线程的时间和总CPU消耗时间的比值。
2.ParNew:其它和Parallel差不多,其区别主要在于ParNew更关注 停顿时间 ,且 可以与CMS收集器组队 工作。是许多工作在Server端的首要选择。
3.CMS:采用标记-清除算法,为了 获取最短停顿时间 为目标的多线程并发的收集器。
有 初次标记(STW)-并发标记-重新标记(STW)-并发清除 四个过程:
初次标记:需要stop the world。仅仅是标记一下GC root能直接可达的对象。
并发标记:进行GC root tracing过程,标记所有可达的对象,但由于此时收集器线程和用户线程并发运行,可能会不断更新引用域,所以会对引用更新的地方做跟踪记录让重新标记阶段去处理。
重新标记:需要stop the world 。修正因用户线程运行而导致的标记变动的那一部分的标记记录。
并发清除:重新启动用户线程,同时对标记的区域做清除。
ps:上面只是参考大部分面试答案,具体的还是不太清除,比如看了一篇分析:重新标记是标记存活对象,并发清除只是将对象标为不可达,真正清除是用啥?,整个过程标记的对象是老年代对象和新生代引用的老年代对象?并发标记之后还要预清理和可控制的预清理啥的.... CMS垃圾收集器详解
缺点: 对CPU资源敏感:会占用一部分线程用于垃圾收集而使应用程序变慢,总吞吐量下降。 无法处理浮动垃圾:浮动垃圾是在标记过程之后产生的垃圾。由于用户线程运行不可避免会产生浮动垃圾,而CMS只能等到下一次收集才能去处理。 产生内存碎片:由于采用的标记-清除算法,必然会产生大量内存碎片,可能会出现老年代内存空间足够但没有连续内存无法存放而导致的full GC。
4.G1收集器:是一款面向服务器的处理器,主要针对配备了多核CPU以及大容量内存的机器。
它有以下特点 : 并行与并发:由于使用了多个CPU可以有效减少停顿时间,部分其他收集器需要定下用户线程再执行的GC操作,G1可以通过并发的方式去运行用户线程,提高吞吐量了。 空间整合:从整体上看G1采用的是标记整理算法,而局部上使用的是复制算法。这意味着G1运行期间不会产生内存碎片,收集完成后仍有规整的可用内存,避免了分配大对象时没有连续内存空间来存放而提前触发下一次GC。 可预测的停顿时间:G1相比CMS的一大优势就是他可以建立可预测的时间模型,它能够使使用者明确在M毫秒的时间片段内,G1收集的时间不超过N毫秒。 分代整合:尽管G1不需要其它收集器来分代收集,仍保留了分代的概念,会采用不同的方式收集新创建的对象和存活了一段时间的对象。 而使G1有以上特点的原因是:
1.G1相较于其他收集器有不同的内存布局:它将Java堆划分成多个相同大小的独立区域(region),尽管还是有新生代和老年代的概念,但两者之间不再是物理隔离的了,而是一部分可以不连续的region集合。
2.避免全堆扫描:G1为每个region维护了一个与之对应的记忆集(Remember Set)来记录新生代和老年代之间的引用关系从而避免了全堆扫描。
3.有优先级的区域。G1会跟踪记录每个region中的垃圾价值大小,在后台建立一个优先列表,根据允许的收集时间,优先回收价值最大的region。
这种使用了region划分内存空间以及有优先级的区域回收方式保证了G1能够在有限时间内获得尽可能高的收集效率 。
收集过程:
初次标记--并发标记--最终标记--筛选回收。
5.Serial +Serial old:新生代采用复制算法、老年代采用标记整理算法,是需要stw的,单线程的收集器,适合于单CPU下的Client模式。
参考: 深入理解JVM(3)——7种垃圾收集器 (深入解析)
一篇文章彻底搞定所有GC面试问题 (面试回答思路) 面试官问我G1回收器怎么知道你是什么时候的垃圾?
软件开发
2020-05-31 20:30:00
首先通过下面链接地址下载 Anaconda 的个人版本。
https://www.anaconda.com/products/individual

从上面下载的地址中,选择你需要的版本,目前 Windows 应该基本上都是 64 位的了。
在你下载的文件中双击运行。
欢迎界面
在弹出的界面中显示了欢迎界面。
许可证
你需要同意许可证,才能让安装继续。
选择用户
在这里你选择默认的用户就可以了。
选择安装目录
在这里将会显示默认的安装目录,Anaconda 的安装比较消耗空间,你需要确定你的磁盘有足够的空间可以按照。
设置一个路径
在安装的时候,不建议设置 PATH,因为可能因为设置 PATH 导致安装的时候出现问题,也有可能会导致 Windows 载入不同的 Python 的版本。
安装进程
你需要等待一些时间,让安装完成。
安装将会按照顺序进行。
安装完成后下一步继续
当安装完成后,可以单击下一步继续。
提示你整合 PyCharm
这一步你不需要做任何事情,下一步继续就可以了。
安装完成
最后将会提示你,安装已经完成了。
单击完成就可以了。
校验安装
在安装好以后,你可以校验安装。
在 Windows 中,你可以选择打开 Anaconda 的命令行,然后输入 conda info 命令。
输入 conda info 命令查看安装的 anaconda。
你也可以输入 python 来查看绑定的版本。
如果你能够看到所有的版本,则说明你的安装已经完成,可以开始使用了。
软件开发
2020-06-01 08:50:00
0.前言
前篇介绍了一些数据库的基本概念和以及一些常见的数据库,让我们对数据库有了一个初步的认识。这一篇我们将继续为C#数据操作的基础填上一个空白-SQL语句。
SQL(Structured Query Language,结构化查询语言)是一种特定的编程语言,用于管理数据库系统,操作数据甚至编写一些程序。
当然,一方面因为时间问题,一方面因为各大数据库的区别(当然了,还有就是个人对SQL研究并不是那么深)所以这一篇就从SQL的基本操作入手,带领大家一起看看SQL的世界。
1. SQL的分类
在SQL的世界里,被分割为两个部分:DML(Data Manipulation Language 数据操纵语言)、DDL(Database Definition Language 数据定义语言)。当然,也有很多其他的分法,这里参照了机械工业出版社出版的《计算机科学丛书- 数据库系统概念》。
1.1 DML
数据操纵语言,用户可以凭此来访问或者操纵那些被结构化存储起来的数据。DML提供了以下功能: 对存储在数据库的数据进行检索(select) 在数据库中添加新的数据(insert) 修改数据库中的数据(update) 删除数据库中的某些数据(delete)
简单的概括起来就是增删改查,对于开发而言这是一项枯燥乏味的工作,当然也是每个程序必不可少的工作。如果你见到这个词:crud,不要诧异,这是开发对增删改查的一种缩写(create,read,update,delete)。
在技术的演变过程中,为了更快更好的增删改查,有一些大牛开发出了一系列的ORM框架,比如C#里最出名的EntityFramework、与Hibernate同源的NHibernate等等。
1.2 DDL
数据定义语言,用户可以用来创建数据库、修改数据库属性、删除数据库,新建表、视图,修改表、视图,删除表、视图等。与DML不同的是,DDL操作的对象从数据转变成了承载数据的实体或者与操作数据的实体。
还有与DML不同的一点是,DDL更多的会使用 create、alter、drop等关键字(分别用来 创建、修改、销毁)。
1.3 方言
如今的城市人们来自五湖四海,有的人用普通话,有的人还是一口流利的家乡话。与之相同的就是在数据库这个江湖里,各大门派都在标准SQL里添加了自己的东西,让SQL成了一个操持着五湖四海的方言的大家族。比如说微软的Transcat-SQL和PL/SQL。
2. 一些简单操作
这里先简单介绍一下通用SQL下的操作:
2.1 创建数据库 create database test;
这是一个简单的创建数据库的SQL语句,这是标准SQL的一部分。效果就是创建一个名字为test的数据库,字符集等属性是系统的默认值。
当然,在SQL Server里可以通过以下方式指定字符集: create database 数据库名 DEFAULT CHARACTER SET gbk COLLATE gbk_chinese_ci; -- 使用gbk CREATE DATABASE 数据库名 DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci; -- 使用utf8
这是在开发过程中最常用的创建数据库方式。
2.2 创建表
数据表是数据库里最重要的一个实体,我们大概演示一下如何通过sql语句创建一个表: create table demo ( [key] int identity primary key , [name] varchar(20) ) go
SQL 创建表的格式如下: create table <表名> ( [属性名] <类型> [params...配置] )
如果有第二个属性,则在第一个之后添加一个逗号,然后继续按照格式声明。
其中 属性名和类型是必须的,配置则可有可无。
常见配置项: identity 表示该列是个自增列,一般是起始1,增长步长为1 primary key 表示该列是主键列,只能有一个主键 not null 表示该字段非空,如果是空值进来则会报错 unique 表示该字段的值不能出现重复
而数据类型则因为数据库不同会有一些细微的差别,所以这里就不错过多介绍了。
2.3 查询
一个简单的查询: select * from demo;
表示查询该表的所有数组。
然后更进一步,可以限制查询条件: select * from demo where <条件>;
注意一下这里的条件里的等值判断用的是一个等号,而不像开发语言里用的是双等号。
这时候发现我们用不了那么多的字段,然后筛选出要显示的字段: select <字段01>,<字段02>,<字段03> from [表名] where <条件>
这里简单介绍一下查询,当然还有很多没有一一介绍,在后续的章节会把这部分补齐的。
2.4 添加数据
在查询之前,我们得先保证数据表里有数据,所以我们看看如何插入数据吧。
插入单条记录: insert into [表名](<字段1>,<字段2>,<字段3>) values('值1','值2','值3')
在表名后面跟括号,括号内写入要插入值的字段,然后values关键字后面用括号包裹起来的一组值便是要插入的值。插入值要与字段名一一对应。
如果要插入多条记录呢? insert into [表名](<字段1>,<字段2>,<字段3>) values('值1','值2','值3'),('值1','值2','值3')
如果需要插入多条的话,将数据用括号包裹起来,然后依次跟在values后面。
2.5 修改数据
当我们发现插入的数据有问题的时候或者因为业务的进行,数据库表里的数据需要更新,这时候我们可以参照以下方式写自己的sql: update [表名] set <字段1> = <值1>
如果需要更新多个字段,可以在更新字段后面添加一个逗号,然后跟在后面,简单实例: update AdditionalService set storeid = 1 , name = '23'
目前所以的更新都是全表更新,当然我们一样可以使用 where来限制。
2.6 删除数据
删除数据的关键字是delete,所以删除的写法是: delete [表名] where <条件>
如果不设置where 条件,则删除的是全表数据。
2.7 删除表
删除表的操作: drop table [表名]
这个操作会把表结构和表里的数据都删除。
3.总结
这一篇大概介绍了SQL的基本用法,开发过程中的SQL基本够用了。后续会随着文章内容逐步填补未介绍的部分。 更多内容烦请关注 我的博客《高先生小屋》
软件开发
2020-06-01 08:20:00
昨天晚上在知乎上看到一个网友问题,我做了一个详细的回答,收到了许多测试人的喜欢与点赞,我把我的回答贴出来分享一下。


既然问题问的这么官方,那我来做一个科普?后面再来解答你的问题。
软件测试(Software Testing),描述一种用来促进鉴定软件的正确性、完整性、安全性和质量的过程。换句话说,软件测试是一种实际输出与预期输出之间的审核或者比较过程。软件测试的经典定义是:在规定的条件下对程序进行操作,以发现程序错误,衡量软件质量,并对其是否能满足设计要求进行评估的过程。
学习软件测试从哪里入手?
我认为分为初级、中级和高级三个阶段,不足之处欢迎朋友们指出,我会及时改正。
初级阶段
初级阶段需要掌握四个方面的内容:
一、软件测试的基础知识,编写测试用例的方法及测试流程
二、掌握禅道、SVN等必要工具,及缺陷定义和测试计划编写方法
三、web测试与app测试的方式方法与协议
四、接口测试postman工具的操作使用,前端基础知识H5及CSS
中级阶段
中级阶段需要掌握六个方面的内容,从中级开始就是涉及到一些工具的使用
一、QTP自动化工具的环境搭建
二、loadrunner性能工具的环境搭建
三、jmeter性能工具的环境搭建及接口压力测试
四、jmeter脚本增强,app/web性能测试
五、fiddler抓包工具的操作使用、Jenkins自动化部署工具
六、数据库MySQL、SQL语句
高级阶段
鉴于问题的角度,我的观点是实践出真知,当你到了中高级阶段,也许你比我的体会更加深刻,就留一个白。正好昨天在编辑一个软件测试全栈思维导图,有需要的可以私信我。贡献我的微薄之力,做好了也会发出来,记得关注我!
随着互联网IT产业的蓬勃发展,软件测试的行业也日趋火热,软件测试基础入门知识都是固定的那一些,要想成为一个优秀的软件测试工程师要学的并不少。回到你的问题,软件测试是不是很简单?什么人都可以学?我想你心里已经有了答案。
我说你只要学一个月就可以学成,入门到放弃啊!滑稽狗头
零基础容易从自学到放弃,太多个这样的例子了,一个人的精力有限,还会涉及到每个人自制力,白天上班下班了很多都是只想呆着不动葛优瘫一会,你需要鼓励需要有人给你加油打气!遇到一些自己解决不了的技术难题,容易诱发焦虑迷茫等不利于学习软件测试的情绪,我深有体会,因此建了一个软件测试交流群,有什么问题可以直接在群里问,(718897738)欢迎来学习交流,免费学习资料可供下载)
那么怎样成为一个好的测试工程师?
作为一名软件工程师,需要的能力并不多,但是要成为一名优秀的软件测试工程师,需要的能力就比较多了,自己整理出来8个方面,每个方面都会分成很多细小的方便并进行举例说明。

知乎是一个开放性的平台,有问题上知乎!那我就再为软件测试人或者在路上的测试人说一下优秀的软件测试工程师必备的8个能力。正所谓,做一行爱一行,既然选择了测试,就要把他做好!
《优秀的软件测试工程师必备的“8个能力”》
一、业务分析能力
1.分析整体业务流程
不了解整个公司的业务,根本就没办法进行测试
2.分析被测业务数据
了解整个业务里面所需的数据有哪些?哪些是需要用户提供的?哪些是自己提供的?有哪些可以是假数据?有哪些必须是真数据?添加数据的时候可以用哪个库?
明白了整个软件的数据库架构,才能知道哪一个数据是从哪一个表里头带出来的,它的逻辑是什么,有没有连带关系。
3.分析被测系统架构
用什么语言开发的?用的是什么服务器?测试它的话需要用什么样的环境进行测试?整体的测试环境是什么样的?
如果缺少了,需要进行环境搭建,架构搭建。一般去一家新公司之后,架构是搭建好的,了解它即可,熟悉之前的这些老员工们使用什么样的架构发表去做的。
4.分析被测业务模块
整个软件有哪些模块,比如说首页面、注册页面、登录页面、会员页面、商品详情页面、优惠券页面等等
明白有多少个模块需要测试,每个模块之间的连带关系,进而怎样进行人员分工
5.分析测试所需资源
我需要几台计算机,需要几部手机,手机需要什么样的系统,什么样的型号。
比如测一个网站的性能的时候,电脑的配置达不到测试并发5000人的标准,要么升级电脑的硬件配置,要么多机联合,多机联合时需要几台电脑,都需要提前筹划。
6.分析测试完成目标
我的性能目标是什么样的?我的功能目标是什么样的?我要上线达到的上线标准是什么样的?性能目标,比如我要达到并发5000人的时候,CPU占用率不能高于70%,内存占用率不能高于60%,响应时间不能超过5秒功能目标,比如整体的业务流程都跑通,所有的分支流程都没有问题,所有的接口都能够互相调用,整体的UI界面没有问题,兼容性没有问题等
把这些问题都弄清楚,测试的思路会非常的清晰
二、缺陷洞察能力
1.一般缺陷的发现能力
至少你要满足一般缺陷的发现能力,这个是最基本的,如果要连最简单的一般的缺陷都发现不了的话,别说优秀测试工程师了,你说你是测试我都不信
2.隐性问题的发现能力
在软件的测试过程当中有一些缺陷藏的比较深,有的是性能方面的问题,有的是功能方面的问题,它需要有一些设定特定的条件的情况下才会出现这样的问题。
比如说买双鞋必须选择的是什么品牌,必须选择是红颜色,必须选择44号,而且必须选择用特定的支付方式才会出现这样的bug的时候,那么这种就属于特别隐性的bug,对于这样的问题的发现能力一定要比别人更强,要找到一些别人可能发现不了的bug。
3.发现连带问题的能力
当发现了一个缺陷之后,能够想到通过这个缺陷可能会引发其他哪个地方出现问题,这就叫做连带的问题。而不是说发现这一个bug之后提了这一个就算完了,一定要有一个察觉,可能其他地方也存在这样的问题。
^_^
最后:
凌晨码字不容易哇~点个赞同是对我最大的支持!其实我也就晚上闲下来才有时间来知乎回答一些我认为有价值的问题。帮助到更多想入行或者已经在路上的测试人。软件测试学习群,欢迎来学习交流,免费学习资料可供下载。
晚安 测试人
如果对python自动化测试、web自动化、接口自动化、移动端自动化、面试经验交流等等感兴趣的测试人,可以关注我。加入我们免费获取更多软件测试进阶资料!
软件开发
2020-06-03 10:43:00
作者:ksfzhaohui https://my.oschina.net/OutOfMemory/blog/3117737
最近有个需求解析一个订单文件,并且说明文件可达到千万条数据,每条数据大概在20个字段左右,每个字段使用逗号分隔,需要尽量在半小时内入库。
思路
1.估算文件大小
因为告诉文件有千万条,同时每条记录大概在20个字段左右,所以可以大致估算一下整个订单文件的大小,方法也很简单使用 FileWriter 往文件中插入一千万条数据,查看文件大小,经测试大概在1.5G左右;
2.如何批量插入
由上可知文件比较大,一次性读取内存肯定不行,方法是每次从当前订单文件中截取一部分数据,然后进行批量插入,如何批次插入可以使用**insert(...)values(...),(...)**的方式,经测试这种方式效率还是挺高的; 怎么快速插入 100 条数据,用时最短 ,这篇看下。
3.数据的完整性
截取数据的时候需要注意,需要保证数据的完整性,每条记录最后都是一个换行符,需要根据这个标识保证每次截取都是整条数,不要出现半条数据这种情况;
4.数据库是否支持批次数据
因为需要进行批次数据的插入,数据库是否支持大量数据写入,比如这边使用的mysql,可以通过设置 max_allowed_packet 来保证批次提交的数据量;
5.中途出错的情况
因为是大文件解析,如果中途出现错误,比如数据刚好插入到900w的时候,数据库连接失败,这种情况不可能重新来插一遍,所有需要记录每次插入数据的位置,并且需要保证和批次插入的数据在同一个事务中,这样恢复之后可以从记录的位置开始继续插入。
实现
1.准备数据表
这里需要准备两张表分别是:订单状态位置信息表,订单表; CREATE TABLE `file_analysis` (   `id` bigint(20) NOT NULL AUTO_INCREMENT,   `file_type` varchar(255) NOT NULL COMMENT '文件类型 01:类型1,02:类型2',   `file_name` varchar(255) NOT NULL COMMENT '文件名称',   `file_path` varchar(255) NOT NULL COMMENT '文件路径',   `status` varchar(255) NOT NULL COMMENT '文件状态 0初始化;1成功;2失败:3处理中',   `position` bigint(20) NOT NULL COMMENT '上一次处理完成的位置',   `crt_time` datetime NOT NULL COMMENT '创建时间',   `upd_time` datetime NOT NULL COMMENT '更新时间',   PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 CREATE TABLE `file_order` (   `id` bigint(20) NOT NULL AUTO_INCREMENT,   `file_id` bigint(20) DEFAULT NULL,   `field1` varchar(255) DEFAULT NULL,   `field2` varchar(255) DEFAULT NULL,   `field3` varchar(255) DEFAULT NULL,   `field4` varchar(255) DEFAULT NULL,   `field5` varchar(255) DEFAULT NULL,   `field6` varchar(255) DEFAULT NULL,   `field7` varchar(255) DEFAULT NULL,   `field8` varchar(255) DEFAULT NULL,   `field9` varchar(255) DEFAULT NULL,   `field10` varchar(255) DEFAULT NULL,   `field11` varchar(255) DEFAULT NULL,   `field12` varchar(255) DEFAULT NULL,   `field13` varchar(255) DEFAULT NULL,   `field14` varchar(255) DEFAULT NULL,   `field15` varchar(255) DEFAULT NULL,   `field16` varchar(255) DEFAULT NULL,   `field17` varchar(255) DEFAULT NULL,   `field18` varchar(255) DEFAULT NULL,   `crt_time` datetime NOT NULL COMMENT '创建时间',   `upd_time` datetime NOT NULL COMMENT '更新时间',   PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=10000024 DEFAULT CHARSET=utf8
2.配置数据库包大小 mysql> show VARIABLES like '%max_allowed_packet%'; +--------------------------+------------+ | Variable_name | Value | +--------------------------+------------+ | max_allowed_packet | 1048576 | | slave_max_allowed_packet | 1073741824 | +--------------------------+------------+ 2 rows in set mysql> set global max_allowed_packet = 1024*1024*10; Query OK, 0 rows affected
通过设置max_allowed_packet,保证数据库能够接收批次插入的数据包大小;不然会出现如下错误: Caused by: com.mysql.jdbc.PacketTooBigException: Packet for query is too large (4980577 > 1048576). You can change this value on the server by setting the max_allowed_packet' variable.     at com.mysql.jdbc.MysqlIO.send(MysqlIO.java:3915)     at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:2598)     at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2778)     at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2834)
3.准备测试数据 public static void main(String[] args) throws IOException {   FileWriter out = new FileWriter(new File("D://xxxxxxx//orders.txt"));   for (int i = 0; i < 10000000; i++) {     out.write(         "vaule1,vaule2,vaule3,vaule4,vaule5,vaule6,vaule7,vaule8,vaule9,vaule10,vaule11,vaule12,vaule13,vaule14,vaule15,vaule16,vaule17,vaule18");     out.write(System.getProperty("line.separator"));   }   out.close(); }
使用FileWriter遍历往一个文件里插入1000w条数据即可,这个速度还是很快的,不要忘了在每条数据的后面添加 换行符(\n\r) ;
4.截取数据的完整性
除了需要设置每次读取文件的大小,同时还需要设置一个参数,用来每次获取一小部分数据,从这小部分数据中获取 换行符(\n\r) ,如果获取不到一直累加直接获取为止,这个值设置大小大致同每条数据的大小差不多合适,部分实现如下: ByteBuffer byteBuffer = ByteBuffer.allocate(buffSize); // 申请一个缓存区 long endPosition = batchFileSize + startPosition - buffSize;// 子文件结束位置 long startTime, endTime; for (int i = 0; i < count; i++) {     startTime = System.currentTimeMillis();     if (i + 1 != count) {         int read = inputChannel.read(byteBuffer, endPosition);// 读取数据         readW: while (read != -1) {             byteBuffer.flip();// 切换读模式             byte[] array = byteBuffer.array();             for (int j = 0; j < array.length; j++) {                 byte b = array[j];                 if (b == 10 || b == 13) { // 判断\n\r                     endPosition += j;                     break readW;                 }             }             endPosition += buffSize;             byteBuffer.clear(); // 重置缓存块指针             read = inputChannel.read(byteBuffer, endPosition);         }     } else {         endPosition = fileSize; // 最后一个文件直接指向文件末尾     }     ...省略,更多可以查看Github完整代码... }
如上代码所示开辟了一个缓冲区,根据每行数据大小来定大概在200字节左右,然后通过遍历查找 换行符(\n\r) ,找到以后将当前的位置加到之前的结束位置上,保证了数据的完整性;
5.批次插入数据
通过**insert(...)values(...),(...)**的方式批次插入数据,部分代码如下: // 保存订单和解析位置保证在一个事务中 SqlSession session = sqlSessionFactory.openSession(); try {   long startTime = System.currentTimeMillis();   FielAnalysisMapper fielAnalysisMapper = session.getMapper(FielAnalysisMapper.class);   FileOrderMapper fileOrderMapper = session.getMapper(FileOrderMapper.class);   fileOrderMapper.batchInsert(orderList);   // 更新上次解析到的位置,同时指定更新时间   fileAnalysis.setPosition(endPosition + 1);   fileAnalysis.setStatus("3");   fileAnalysis.setUpdTime(new Date());   fielAnalysisMapper.updateFileAnalysis(fileAnalysis);   session.commit();   long endTime = System.currentTimeMillis();   System.out.println("===插入数据花费:" + (endTime - startTime) + "ms==="); } catch (Exception e) {   session.rollback(); } finally {   session.close(); } ...省略,更多可以查看Github完整代码...
如上代码在一个事务中同时保存批次订单数据和文件解析位置信息,batchInsert通过使用mybatis的**< foreach >**标签来遍历订单列表,生成values数据;
总结
以上展示了部分代码,完整的代码可以查看 Github 地址中的batchInsert模块,本地设置每次截取的文件大小为2M。
经测试1000w条数据(大小1.5G左右)插入mysql数据库中,大概花费时间在20分钟左右,当然可以通过设置截取的文件大小,花费的时间也会相应的改变。
推荐去我的博客阅读更多:
1. Java JVM、集合、多线程、新特性系列教程
2. Spring MVC、Spring Boot、Spring Cloud 系列教程
3. Maven、Git、Eclipse、Intellij IDEA 系列工具教程
4. Java、后端、架构、阿里巴巴等大厂最新面试题
觉得不错,别忘了点赞+转发哦!
软件开发
2020-06-03 09:49:00
[TOC] mybatis运行分为两部分,第一部分读取配置文件缓存到Configuration对象中。用以创建SqlSessionFactory,第二部分是SqlSession的执行过程。
Mybatis基本认识
动态代理 之前我们知道Mapper仅仅是一个接口,而不是一个逻辑实现类。但是在Java中接口是无法执行逻辑的。这里Mybatis就是通过动态代理实现的。关于动态代理我们常用的有Jdk动态代理和cglib动态代理。两种却别这里不做赘述。关于CGLIB代理在框架中使用的比较多。 关于动态代理就是所有的请求有一个入口,由这个入口进行分发。在开发领域的一个用途就是【负载均衡】 关于Mybatis的动态代理是使用了两种的结合。 下面看看JDK和cglib两种实现
JDK实现 首先我们需要提供一个接口 , 这个接口是对我们程序员的一个抽象。 拥有编码和改BUG的本领 public interface Developer { /** * 编码 */ void code(); /** * 解决问题 */ void debug(); } 关于这两种本领每个人处理方式不同。这里我们需要一个具体的实例对象 public class JavaDeveloper implements Developer { @Override public void code() { System.out.println("java code"); } @Override public void debug() { System.out.println("java debug"); } } 我们传统的调用方式是通过java提供的new 机制创造一个JavaDeveloper对象出来。而通过动态代理是通过 java.lang.reflect.Proxy 对象创建对象调用实际方法的。 通过 newProxyInstance 方法获取接口对象的。而这个方法需要三个参数 ClassLoader loader : 通过实际接口实例对象获取ClassLoader Class[] interfaces : 我们抽象的接口 InvocationHandler h : 对我们接口对象方法的调用。在调用节点我们可以进行我们的业务拦截 JavaDeveloper jDeveloper = new JavaDeveloper(); Developer developer = (Developer) Proxy.newProxyInstance(jDeveloper.getClass().getClassLoader(), jDeveloper.getClass().getInterfaces(), (proxy, method, params) -> { if (method.getName().equals("code")) { System.out.println("我是一个特殊的人,code之前先分析问题"); return method.invoke(jDeveloper, params); } if (method.getName().equals("debug")) { System.out.println("我没有bug"); } return null; }); developer.code(); developer.debug();
CGLIB动态代理 cglib动态代理优点在于他不需要我们提前准备接口。他代理的实际的对象。这对于我们开发来说就很方便了。 public class HelloService { public HelloService() { System.out.println("HelloService构造"); } final public String sayHello(String name) { System.out.println("HelloService:sayOthers>>"+name); return null; } public void sayHello() { System.out.println("HelloService:sayHello"); } } 下面我们只需要实现cglib提供的MethodInterceptor接口,在初始化设置cglib的时候加载这个实例化对象就可以了 public class MyMethodInterceptor implements MethodInterceptor { @Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { System.out.println("======插入前置通知======"); Object object = methodProxy.invokeSuper(o, objects); System.out.println("======插入后者通知======"); return object; } } 下面我们就来初始化设置cglib public static void main(String[] args) { //代理类class文件存入本地磁盘方便我们反编译查看源代码 System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "/root/code"); //通过CGLIB动态代理获取代理对象过程 Enhancer enhancer = new Enhancer(); //设置enhancer对象的父类 enhancer.setSuperclass(HelloService.class); // 设置enhancer的回调对象 enhancer.setCallback(new MyMethodInterceptor()); //创建代理对象 HelloService helloService = (HelloService) enhancer.create(); //通过代理对象调用目标方法 helloService.sayHello(); } 仔细看看cglib和spring的aop特别像。针对切点进行切面拦截控制。
总结 通过对比两种动态代理我们很容易发现,mybatis就是通过JDK代理实现Mapper调用的。我们Mapper接口实现通过代理到xml中对应的sql执行逻辑
反射 相信有一定经验的Java工程师都对反射或多或少有一定了解。其实从思想上看不惯哪种语言都是有反射的机制的。 通过反射我们就摆脱了对象的限制我们调用方法不再需要通过对象调用了。可以通过Class对象获取方法对象。从而通过invoke方法进行方法的调用了。
Configuration对象作用 Configuration对象存储了所有Mybatis的配置。主要初始化一下参数 properties settings typeAliases typeHandler ObjectFactory plugins environment DatabaseIdProvider Mapper映射器
映射器结构
BoundSql提供三个主要的属性 parameterMappings 、parameterObject、sql parameterObject参数本身。我们可以传递java基本类型、POJO、Map或者@Param标注的参数。 当我们传递的是java基本类型mybatis会转换成对应的包装对象 int -> Integer 如果我们传递POJO、Map。就是对象本身 我们传递多个参数且没有@Param指定变量名则parameterObject 类似 {"1":p1,"2":p2,"param1":p1,"param2":p2} 我们传递多个参数且@Param指定变量名 则parameterObject类似 {"key1":p1,"key2":p2,"param1":p1,"param2":p2} parameterMapping 是记录属性、名称、表达式、javaType,jdbcType、typeHandler这些信息 sql 属性就是我们映射器中的一条sql. 正常我们在常见中对sql进行校验。正常不需要修改sql。
sqlsession执行流程(源码跟踪) 首先我们看看我们平时开发的Mapper接口是如何动态代理的。这就需要提到 MapperProxyFactory 这个类了。该类中的 newInstance 方法 protected T newInstance(MapperProxy mapperProxy) { return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); } 通过上满代码及上述对jdk动态代理的表述。我们可以知道mapperProxy是我们代理的重点。 MapperProxy是InvocationHandler的实现类。他重写的invoke方法就是代理对象执行的方法入口。 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, args); } else if (isDefaultMethod(method)) { return invokeDefaultMethod(proxy, method, args); } } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } final MapperMethod mapperMethod = cachedMapperMethod(method); return mapperMethod.execute(sqlSession, args); } private boolean isDefaultMethod(Method method) { return (method.getModifiers() & (Modifier.ABSTRACT | Modifier.PUBLIC | Modifier.STATIC)) == Modifier.PUBLIC && method.getDeclaringClass().isInterface(); } 通过源码发现。invoke内部首先判断对象是否是类 。 通过打断点发现最终会走到cacheMapperMethod这个方法去创建MapperMethod对象。 继续查看MapperMethod中execute方法我们可以了解到内部实现其实是一个命令行模式开发。通过判断命令从而执行不同的语句。判断到具体执行语句然后将参数传递给sqlsession进行sql调用并获取结果。到了sqlsession就和正常jdbc开发sql进行关联了。sqlsession中 Executor 、 StatementHandler 、 ParameterHandler 、 Resulthandler 四大天王
Executor 顾名思义他就是一个执行器。将java提供的sql提交到数据库。Mybatis提供了三种执行器。 Configuration.class 中 newExecutor 源码
根据uml我们不难看出mybatis中提供了三类执行器分别SimpleExecutor、ReuseExecutor、BatchExecutor public SqlSession openSession() { return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false); } private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) { Transaction tx = null; try { // 得到configuration 中的environment final Environment environment = configuration.getEnvironment(); // 得到configuration 中的事务工厂 final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment); tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit); // 获取执行器 final Executor executor = configuration.newExecutor(tx, execType); // 返回默认的SqlSession return new DefaultSqlSession(configuration, executor, autoCommit); } catch (Exception e) { closeTransaction(tx); // may have fetched a connection so lets call close() throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } } 通过上述源码我们知道在sqlsession获取一个数据库session对象时我们或根据我们的settings配置加载一个Executor对象。在settings中配置也很简单 我们也可以通过java代码设置 factory.openSession(ExecutorType.BATCH);
StatementHandler 顾名思义,StatementHandler就是专门处理数据库回话的。这个对象的创建还是在Configuration中管理的。 public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql); statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler); return statementHandler; } 很明显Mybatis中StatementHandler使用的是RoutingStatementHandler这个class
关于StatementHandler和RoutingStatementHandler之间的关系我们通过源码可以看出这里和Executor一样都是适配器模式。采用这种模式的好处是方便我们对这些对象进行代理。这里读者可以猜测一下是使用了哪种动态代理。给点提示 这里使用了接口哦
在查看BaseStatementHandler结构我们会发现和Executor一模一样。同样的Mybatis在构造RoutingStatementHandler的时候会根据setting中配置来加载不同的具体子类。这些子类都是继承了BaseStatementHandler. 前一节我们跟踪了Executor。 我们知道Mybatis默认的是SimpleExecutor。 StatementHandler我们跟踪了Mybaits默认的是PrePareStatementHandler。在SimpleExecutor执行查询的源码如下
我们发现在executor查询钱会先让statementHandler构建一个Statement对象。最终就是StatementHandler中prepare方法。这个方法在抽象类BaseStatmentHandler中已经封装好了。
这个方法的逻辑是初始化statement和设置连接超时等一些辅助作用 然后就是设置一些参数等设置。最后就走到了执行器executor的doquery
PrepareStatement在我们jdbc开发时是常见的一个类 。 这个方法执行execute前我们需要设置sql语句,设置参数进行编译。这一系列步骤就是刚才我们说的流程也是PrepareStatementHandler.prepareStatement帮我们做的事情。那么剩下的我们也很容易想到就是我们对数据结果的封装。正如代码所示下马就是resultSetHandler帮我们做事情了。
结果处理器(ResultSetHandler) @Override public List handleResultSets(Statement stmt) throws SQLException { ErrorContext.instance().activity("handling results").object(mappedStatement.getId()); final List multipleResults = new ArrayList<>(); int resultSetCount = 0; ResultSetWrapper rsw = getFirstResultSet(stmt); List resultMaps = mappedStatement.getResultMaps(); int resultMapCount = resultMaps.size(); validateResultMapsCount(rsw, resultMapCount); while (rsw != null && resultMapCount > resultSetCount) { ResultMap resultMap = resultMaps.get(resultSetCount); handleResultSet(rsw, resultMap, multipleResults, null); rsw = getNextResultSet(stmt); cleanUpAfterHandlingResultSet(); resultSetCount++; } String[] resultSets = mappedStatement.getResultSets(); if (resultSets != null) { while (rsw != null && resultSetCount < resultSets.length) { ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]); if (parentMapping != null) { String nestedResultMapId = parentMapping.getNestedResultMapId(); ResultMap resultMap = configuration.getResultMap(nestedResultMapId); handleResultSet(rsw, resultMap, null, parentMapping); } rsw = getNextResultSet(stmt); cleanUpAfterHandlingResultSet(); resultSetCount++; } } return collapseSingleResultList(multipleResults); } 这个方法我们可以导出来是结果xml中标签配置对结果的一个封装。
总结 SqlSession在一个查询开启的时候会先通过CacheExecutor查询缓存。击穿缓存后会通过BaseExector子类的SimpleExecutor创建StatementHandler。PrepareStatementHandler会基于PrepareStament执行数据库操作。并针对返回结果通过ResultSetHandler返回结果数据
主题
软件开发
2020-06-03 09:30:06
六月福利
2020年6月公众号码农小胖哥原创文章转发第一名将送全新《 Spring Boot实战 》实体书一本,该书是学习热门框架 Spring Boot 的经典之作。 你不再需要依靠运气,而是勤奋 。截止统计日期2020年6月30日,统计数据以官方公众号工具为准,运营人员不参加活动,本次活动图书由掘金社区赞助。
1. 前言
前几天讲了 设计模式中的命令模式 ,今天来看看另一个模式。移动支付目前在国内已经是非常普及了,连楼下早餐摊的七十多岁大妈也使用支付宝和微信支付卖鸡蛋饼。如果让你做一个App你肯定要考虑多个渠道支付,以保证获客渠道。如果让你来接入多种支付渠道你会怎么设计?
2. 通常写法
一般下面这种写法很容易被创造出来: public boolean pay(BigDecimal amount){ boolean ret =false; if (alipay){ //todo 支付宝的逻辑 }else if (wechatpay){ //todo 微信支付的逻辑 }else if (ooxx){ // …… } return ret; }
如果集成了四五种支付,这个代码就没法看了少说几千行,而且改动某个支付的逻辑很容易改了其它支付的逻辑。因此需要合理的设计来避免这种风险。
3. 策略模式
大部分的支付可以简化为这个流程:
中间的 发起支付前逻辑 和 支付后处理逻辑 是客户端的自定义业务逻辑,向支付服务器发送的请求只会携带对应支付服务器特定要求的参数调用不同的支付 SDK 。所以我们分别建立对应支付方式的策略来隔离区分它们,降低它们的耦合度。当准备支付时我们只需要选择对应的策略就可以了。
这就用到了设计模式中的策略模式:
结合上面的类图,我们就来结合着需求来聊聊策略模式中的主要几个角色。 Strategy 接口。这个接口用来声明每一种方式的独立执行策略,用来封装具体策略的特有算法逻辑。 ConcreteStrategy 是 Strategy 的实现类。实现了不同策略的算法逻辑。比如每种支付的调用细节等等。 Context 上下文。它通过策略接口来引用了具体的策略并使用具体的策略来执行逻辑,同时所有策略的共性也可以在该类中进行统一处理。在聚合支付需求中我们传入一个策略,先执行支付前的逻辑,然后使用策略,策略执行完毕后,再执行后置的共性逻辑。 Client 客户端。创建策略对象并传递给上下文 Context ,然后由上下文运行具体的策略。
结合业务逻辑是这样的: 请求到达客户端,客户端根据请求中包含的支付渠道来构建对应的策略对象并把它交给上下文对象去执行支付流程。
然后我们就可以分别为支付宝、微信支付、银联支付构造三个策略对象 AliPayStrategy 、 WechatPayStrategy 、 UnionPayStrategy ,我们来模拟一下执行策略: public class Client { public static void main(String[] args) { // 获取请求中的支付渠道标识 String code = "p01"; PayStrategy payStrategy = null; PayRequest request = null; if (PayType.ALI.getCode().equals(code)) { //组装为支付宝支付策略 payStrategy = new AliPayStrategy(); // 构造支付宝请求参数 request = new AliPayRequest(); } if (PayType.WECHAT.getCode().equals(code)) { //组装为微信支付策略 payStrategy = new AliPayStrategy(); // 构造微信支付请求参数 request = new WechatPayRequest(); } if (PayType.UNION.getCode().equals(code)) { //组装为银联支付策略 payStrategy = new UnionPayStrategy(); // 构造银联支付请求参数 request = new UnionPayRequest(); } if (Objects.nonNull(payStrategy)) { PayContext payContext = new PayContext(); payContext.setPayStrategy(payStrategy); payContext.pay(request); } } }
我们拿到请求中的支付标识,然后初始化对应的支付策略,封装指定的请求参数,交给上下文对象 PayContext 来执行请求。如果你再增加什么 ApplePay 之类的去实现 ApplePayStrategy 就行了。
4. 优缺点
策略模式并不都带来正面的作用。
4.1 优点 我们将算法的实现和算法的使用进行了隔离,算法实现只关心算法逻辑,使用算法只关心什么条件下使用什么算法。 开闭原则,无需修改上下文对象,想扩展只需要引入新的策略。 运行时根据条件动态的去切换算法。 适应个性的同时也可以兼容共性。
4.2 缺点 客户端必须明确知道激活策略的条件。 引入太多的策略类。 可被一些函数式接口所代替。伪代码 Pay.request(data).strategy(data->{ }) 。
5. 总结
策略模式也是很常见而且有着广泛使用场景的设计模式。今天我们从聚合支付来学习了策略模式,对它的优缺点也进行了一个分析。随着 函数式编程 的普及,策略模式开始被逐渐的代替,但是它依然值得我们去学习。感谢你的阅读,多多关注: 码农小胖哥 ,更多编程干货奉上。
关注公众号:Felordcn获取更多资讯
个人博客:https://felord.cn
软件开发
2020-06-03 08:56:00
JavaFX初探(菜单)
本节我们介绍如何创建菜单、菜单栏、增加菜单项、为菜单分类,创建子菜单、设置菜单上下文。你可以使用下面的类来创建菜单。 MenuBar MenuItem Menu CheckMenuItem RadioMenuItem CustomMenuItem SeparatorMenuItem ContextMenu
下图是一个典型的菜单的使用:
在应用中构建菜单
一个菜单就是一系列可操作的项目,可以根据用户的需要来表现。当一个菜单可见的时候,用户可以在某一时刻选中其中一个,在用户选中某一项时,这个菜单变成隐藏模式。通过使用菜单,我们可以节省用户界面的空间,因为有一些功能某些时间并不是总要现实出来的。
菜单在菜单栏中被分组,你需要使用下面的菜单项类,当你构建一个菜单的时候。 MenuItem 创建可选项 Menu 创建子菜单 RadioButtonItem 创建一个单选项 CheckMenuItem 这个菜单项可以在选择被无选择之间转换。
为了给菜单分类,可以使用SeparatorMenuItem 类。
菜单通常在窗口的顶部,并且这些菜单是隐藏的,我们可以通过鼠标点击上下文来打开菜单。
创建菜单栏
尽管菜单栏可以放在用户界面的任何地方,但是一般情况我们放到窗口的顶部。并且菜单栏可已自动的改变自己的大小。默认情况下,每一个菜单栏中的菜单像一个按钮一样呈现出来。
想想一个这样的一个应用,他显示植物的名称,图片,以及简单的描述信息。我们创建3个菜单项,:File,Edit,View.并给这三项添加菜单项。代码如下所示: import java.util.AbstractMap.SimpleEntry; import java.util.Map.Entry; import javafx.application.Application; import javafx.scene.Scene; import javafx.scene.control.*; import javafx.scene.effect.DropShadow; import javafx.scene.effect.Effect; import javafx.scene.effect.Glow; import javafx.scene.effect.SepiaTone; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.VBox; import javafx.stage.Stage; public class MenuSample extends Application { final PageData[] pages = new PageData[] { new PageData( "Apple" , "The apple is the pomaceous fruit of the apple tree, species Malus " + "domestica in the rose family (Rosaceae). It is one of the most " + "widely cultivated tree fruits, and the most widely known of " + "the many members of genus Malus that are used by humans. " + "The tree originated in Western Asia, where its wild ancestor, " + "the Alma, is still found today." , "Malus domestica" ), new PageData( "Hawthorn" , "The hawthorn is a large genus of shrubs and trees in the rose " + "family, Rosaceae, native to temperate regions of the Northern " + "Hemisphere in Europe, Asia and North America. " + "The name hawthorn was " + "originally applied to the species native to northern Europe, " + "especially the Common Hawthorn C. monogyna, and the unmodified " + "name is often so used in Britain and Ireland." , "Crataegus monogyna" ), new PageData( "Ivy" , "The ivy is a flowering plant in the grape family (Vitaceae) native to" + " eastern Asia in Japan, Korea, and northern and eastern China. " + "It is a deciduous woody vine growing to 30 m tall or more given " + "suitable support, attaching itself by means of numerous small " + "branched tendrils tipped with sticky disks." , "Parthenocissus tricuspidata" ), new PageData( "Quince" , "The quince is the sole member of the genus Cydonia and is native to " + "warm-temperate southwest Asia in the Caucasus region. The " + "immature fruit is green with dense grey-white pubescence, most " + "of which rubs off before maturity in late autumn when the fruit " + "changes color to yellow with hard, strongly perfumed flesh." , "Cydonia oblonga" ) }; final String[] viewOptions = new String[] { "Title" , "Binomial name" , "Picture" , "Description" }; final Entry[] effects = new Entry[] { new SimpleEntry<>( "Sepia Tone" , new SepiaTone()), new SimpleEntry<>( "Glow" , new Glow()), new SimpleEntry<>( "Shadow" , new DropShadow()) }; final ImageView pic = new ImageView(); final Label name = new Label(); final Label binName = new Label(); final Label description = new Label(); public static void main (String[] args) { launch(args); } @Override public void start (Stage stage) { stage.setTitle( "Menu Sample" ); Scene scene = new Scene( new VBox(), 400 , 350 ); MenuBar menuBar = new MenuBar(); // --- Menu File Menu menuFile = new Menu( "File" ); // --- Menu Edit Menu menuEdit = new Menu( "Edit" ); // --- Menu View Menu menuView = new Menu( "View" ); menuBar.getMenus().addAll(menuFile, menuEdit, menuView); ((VBox) scene.getRoot()).getChildren().addAll(menuBar); stage.setScene(scene); stage.show(); } private class PageData { public String name; public String description; public String binNames; public Image image; public PageData (String name, String description, String binNames) { this .name = name; this .description = description; this .binNames = binNames; image = new Image(getClass().getResourceAsStream(name + ".jpg" )); } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112
和其他的UI控件不同,Menu类和其他的扩展MenuItem的类都不是扩展自Node类。所以他们不能直接添加到场景中,需要先添加到MenuBar中然后在添加到场景中。运行如下图所示:
你可以使用键盘的方向键来浏览菜单,然而当你选中一个菜单的时候,什么都没有发生,那是因为我们还没有指定行为。
添加菜单项
为文件菜单添加功能。 Shuffle 加载植物的参考信息 Clear 删除参考信息并清空场景 Separator 分离菜单项 Exit 退出应用
使用MenuItem创建了一个Shuffle菜单,并添加了一个图片组件。我们可以为MenuItem指定文本和图片。我们可以通过setAction方法来指定该菜单被点击时候的事件,这个Button是一样的。代码如下: import java.util.AbstractMap.SimpleEntry; import java.util.Map.Entry; import javafx.application.Application; import javafx.event.ActionEvent; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Scene; import javafx.scene.control.*; import javafx.scene.effect.DropShadow; import javafx.scene.effect.Effect; import javafx.scene.effect.Glow; import javafx.scene.effect.SepiaTone; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.VBox; import javafx.scene.paint.Color; import javafx.scene.text.Font; import javafx.scene.text.TextAlignment; import javafx.stage.Stage; public class MenuSample extends Application { final PageData[] pages = new PageData[] { new PageData( "Apple" , "The apple is the pomaceous fruit of the apple tree, species Malus " + "domestica in the rose family (Rosaceae). It is one of the most " + "widely cultivated tree fruits, and the most widely known of " + "the many members of genus Malus that are used by humans. " + "The tree originated in Western Asia, where its wild ancestor, " + "the Alma, is still found today." , "Malus domestica" ), new PageData( "Hawthorn" , "The hawthorn is a large genus of shrubs and trees in the rose " + "family, Rosaceae, native to temperate regions of the Northern " + "Hemisphere in Europe, Asia and North America. " + "The name hawthorn was " + "originally applied to the species native to northern Europe, " + "especially the Common Hawthorn C. monogyna, and the unmodified " + "name is often so used in Britain and Ireland." , "Crataegus monogyna" ), new PageData( "Ivy" , "The ivy is a flowering plant in the grape family (Vitaceae) native" + " to eastern Asia in Japan, Korea, and northern and eastern China." + " It is a deciduous woody vine growing to 30 m tall or more given " + "suitable support, attaching itself by means of numerous small " + "branched tendrils tipped with sticky disks." , "Parthenocissus tricuspidata" ), new PageData( "Quince" , "The quince is the sole member of the genus Cydonia and is native" + " to warm-temperate southwest Asia in the Caucasus region. The " + "immature fruit is green with dense grey-white pubescence, most " + "of which rubs off before maturity in late autumn when the fruit " + "changes color to yellow with hard, strongly perfumed flesh." , "Cydonia oblonga" ) }; final String[] viewOptions = new String[] { "Title" , "Binomial name" , "Picture" , "Description" }; final Entry[] effects = new Entry[] { new SimpleEntry<>( "Sepia Tone" , new SepiaTone()), new SimpleEntry<>( "Glow" , new Glow()), new SimpleEntry<>( "Shadow" , new DropShadow()) }; final ImageView pic = new ImageView(); final Label name = new Label(); final Label binName = new Label(); final Label description = new Label(); private int currentIndex = - 1 ; public static void main (String[] args) { launch(args); } @Override public void start (Stage stage) { stage.setTitle( "Menu Sample" ); Scene scene = new Scene( new VBox(), 400 , 350 ); scene.setFill(Color.OLDLACE); name.setFont( new Font( "Verdana Bold" , 22 )); binName.setFont( new Font( "Arial Italic" , 10 )); pic.setFitHeight( 150 ); pic.setPreserveRatio( true ); description.setWrapText( true ); description.setTextAlignment(TextAlignment.JUSTIFY); shuffle(); MenuBar menuBar = new MenuBar(); final VBox vbox = new VBox(); vbox.setAlignment(Pos.CENTER); vbox.setSpacing( 10 ); vbox.setPadding( new Insets( 0 , 10 , 0 , 10 )); vbox.getChildren().addAll(name, binName, pic, description); // --- Menu File Menu menuFile = new Menu( "File" ); MenuItem add = new MenuItem( "Shuffle" , new ImageView( new Image( "menusample/new.png" ))); add.setOnAction((ActionEvent t) -> { shuffle(); vbox.setVisible( true ); }); menuFile.getItems().addAll(add); // --- Menu Edit Menu menuEdit = new Menu( "Edit" ); // --- Menu View Menu menuView = new Menu( "View" ); menuBar.getMenus().addAll(menuFile, menuEdit, menuView); ((VBox) scene.getRoot()).getChildren().addAll(menuBar, vbox); stage.setScene(scene); stage.show(); } private void shuffle () { int i = currentIndex; while (i == currentIndex) { i = ( int ) (Math.random() * pages.length); } pic.setImage(pages[i].image); name.setText(pages[i].name); binName.setText( "(" + pages[i].binNames + ")" ); description.setText(pages[i].description); currentIndex = i; } private class PageData { public String name; public String description; public String binNames; public Image image; public PageData (String name, String description, String binNames) { this .name = name; this .description = description; this .binNames = binNames; image = new Image(getClass().getResourceAsStream(name + ".jpg" )); } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152
当用户选择Shuffle菜单的时候,会就调用我们指定的setOnAction方法,显示出植物的名字、图片和简单描述。
Clear菜单用来抹除场景中的内容。我们可以使用下面的方法: MenuItem clear = new MenuItem( "Clear" ); clear.setAccelerator(KeyCombination.keyCombination( "Ctrl+X" )); clear.setOnAction ((ActionEvent t) -> { vbox.setVisible( false ); }) ; 1 2 3 4 5 6
Exit菜单实现如下: MenuItem exit = new MenuItem( "Exit" ); exit.setOnAction ((ActionEvent t) -> { System.exit( 0 ); }) ; 1 2 3 4 5
我们使用getItems方法来添加这些菜单项,如下: menuFile .getItems () .addAll ( add , clear, new SeparatorMenuItem(), exit) ; 1 2
编译运行应用,如下图所示:
View菜单可以来用指定,显示植物的部分信息, // --- Creating four check menu items within the start method CheckMenuItem titleView = createMenuItem ( "Title" , name); CheckMenuItem binNameView = createMenuItem ( "Binomial name" , binName); CheckMenuItem picView = createMenuItem ( "Picture" , pic); CheckMenuItem descriptionView = createMenuItem ( "Description" , description); menuView.getItems().addAll(titleView, binNameView, picView, descriptionView); ... // The createMenuItem method private static CheckMenuItem createMenuItem (String title, final Node node){ CheckMenuItem cmi = new CheckMenuItem(title); cmi.setSelected(true); cmi.selectedProperty().addListener( (ObservableValue ov, Boolean old_val, Boolean new_val) -> { node.setVisible(new_val); }); return cmi; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
CheckMenuItem 是MenuItem类的扩展,他可以选中和非选中,如果被选中了,会显示出一个标记。编译运行如下图所示:
创建子菜单
Edit菜单中定义了两个菜单项:图片效果和无效果。图片效果菜单项有子菜单项。
使用RadioMenuItem 来创建子菜单,代码如下: // Picture Effect menu Menu menuEffect = new Menu( "Picture Effect" ); final ToggleGroup groupEffect = new ToggleGroup(); for (Entry effect : effects) { RadioMenuItem itemEffect = new RadioMenuItem(effect.getKey()); itemEffect.setUserData(effect.getValue()); itemEffect.setToggleGroup(groupEffect); menuEffect.getItems().add(itemEffect); } // No Effects menu final MenuItem noEffects = new MenuItem( "No Effects" ); noEffects.setOnAction ((ActionEvent t) -> { pic.setEffect( null ); groupEffect.getSelectedToggle().setSelected( false ); }) ; // Processing menu item selection groupEffect . selectedToggleProperty () . addListener ( new ChangeListener() { public void changed(ObservableValue ov, Toggle old_toggle, Toggle new_toggle) { if (groupEffect.getSelectedToggle() != null ) { Effect effect = (Effect) groupEffect.getSelectedToggle().getUserData(); pic.setEffect(effect); } } }) ; groupEffect . selectedToggleProperty () . addListener ( (ObservableValue ov, Toggle old_toggle, Toggle new_toggle) -> { if (groupEffect.getSelectedToggle() != null ) { Effect effect = (Effect) groupEffect.getSelectedToggle().getUserData(); pic.setEffect(effect); } }) ; // Adding items to the Edit menu menuEdit . getItems () . addAll (menuEffect, noEffects) ; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
如下所示:
我们可以使用setDisable来禁用某一个菜单。 Menu menuEffect = new Menu( "Picture Effect" ); final ToggleGroup groupEffect = new ToggleGroup(); for (Entry effect : effects) { RadioMenuItem itemEffect = new RadioMenuItem(effect.getKey()); itemEffect.setUserData(effect.getValue()); itemEffect.setToggleGroup(groupEffect); menuEffect.getItems().add(itemEffect); } final MenuItem noEffects = new MenuItem( "No Effects" ); noEffects.setDisable( true ); noEffects.setOnAction ((ActionEvent t) -> { pic.setEffect( null ); groupEffect.getSelectedToggle().setSelected( false ); noEffects.setDisable( true ); }) ; groupEffect . selectedToggleProperty () . addListener ( (ObservableValue ov, Toggle old_toggle, Toggle new_toggle) -> { if (groupEffect.getSelectedToggle() != null ) { Effect effect = (Effect) groupEffect.getSelectedToggle().getUserData(); pic.setEffect(effect); noEffects.setDisable( false ); } else { noEffects.setDisable( true ); } }) ; menuEdit . getItems () . addAll (menuEffect, noEffects) ; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
添加上下文菜单
如果你的界面中没有为菜单的控件,那么你可以使用上下文菜单。当点击鼠标的时候,弹出上下文菜单。 final ContextMenu cm = new ContextMenu(); MenuItem cmItem1 = new MenuItem( "Copy Image" ); cmItem1.setOnAction ((ActionEvent e) -> { Clipboard clipboard = Clipboard.getSystemClipboard(); ClipboardContent content = new ClipboardContent(); content.putImage(pic.getImage()); clipboard.setContent(content); }) ; cm . getItems () . add (cmItem1) ; pic . addEventHandler (MouseEvent.MOUSE_CLICKED, (MouseEvent e) -> { if (e.getButton() == MouseButton.SECONDARY) cm.show(pic, e.getScreenX(), e.getScreenY()); }) ; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
软件开发
2020-06-03 08:19:00