Redis的三种集群方案简介(Master, Slave, Sentinel, Cluster)

 redis  Redis的三种集群方案简介(Master, Slave, Sentinel, Cluster)已关闭评论
7月 292021
 

重温下redis集群的三种集群方案,分享下网络上的一篇文章,写的很好, 原文链接见文末。

 

在开发测试环境中,我们一般搭建Redis的单实例来应对开发测试需求,但是在生产环境,如果对可用性、可靠性要求较高,则需要引入Redis的集群方案。虽然现在各大云平台有提供缓存服务可以直接使用,但了解一下其背后的实现与原理总还是有些必要(比如面试), 本文就一起来学习一下Redis的几种集群方案。

Redis支持三种集群方案

  • 主从复制模式
  • Sentinel(哨兵)模式
  • Cluster模式

主从复制模式

1. 基本原理

主从复制模式中包含一个主数据库实例(master)与一个或多个从数据库实例(slave),如下图

redis-master-slave

客户端可对主数据库进行读写操作,对从数据库进行读操作,主数据库写入的数据会实时自动同步给从数据库。

具体工作机制为:

  1. slave启动后,向master发送SYNC命令,master接收到SYNC命令后通过bgsave保存快照(即上文所介绍的RDB持久化),并使用缓冲区记录保存快照这段时间内执行的写命令
  2. master将保存的快照文件发送给slave,并继续记录执行的写命令
  3. slave接收到快照文件后,加载快照文件,载入数据
  4. master快照发送完后开始向slave发送缓冲区的写命令,slave接收命令并执行,完成复制初始化
  5. 此后master每次执行一个写命令都会同步发送给slave,保持master与slave之间数据的一致性

2. 部署示例

本示例基于Redis 5.0.3版。

redis.conf的主要配置

###网络相关###
# bind 127.0.0.1 # 绑定监听的网卡IP,注释掉或配置成0.0.0.0可使任意IP均可访问
protected-mode no # 关闭保护模式,使用密码访问
port 6379  # 设置监听端口,建议生产环境均使用自定义端口
timeout 30 # 客户端连接空闲多久后断开连接,单位秒,0表示禁用

###通用配置###
daemonize yes # 在后台运行
pidfile /var/run/redis_6379.pid  # pid进程文件名
logfile /usr/local/redis/logs/redis.log # 日志文件的位置

###RDB持久化配置###
save 900 1 # 900s内至少一次写操作则执行bgsave进行RDB持久化
save 300 10
save 60 10000 
# 如果禁用RDB持久化,可在这里添加 save ""
rdbcompression yes #是否对RDB文件进行压缩,建议设置为no,以(磁盘)空间换(CPU)时间
dbfilename dump.rdb # RDB文件名称
dir /usr/local/redis/datas # RDB文件保存路径,AOF文件也保存在这里

###AOF配置###
appendonly yes # 默认值是no,表示不使用AOF增量持久化的方式,使用RDB全量持久化的方式
appendfsync everysec # 可选值 always, everysec,no,建议设置为everysec

###设置密码###
requirepass 123456 # 设置复杂一点的密码

部署主从复制模式只需稍微调整slave的配置,在redis.conf中添加

replicaof 127.0.0.1 6379 # master的ip,port
masterauth 123456 # master的密码
replica-serve-stale-data no # 如果slave无法与master同步,设置成slave不可读,方便监控脚本发现问题

本示例在单台服务器上配置master端口6379,两个slave端口分别为7001,7002,启动master,再启动两个slave

[[email protected] master-slave]# redis-server master.conf
[[email protected] master-slave]# redis-server slave1.conf
[[email protected] master-slave]# redis-server slave2.conf

进入master数据库,写入一个数据,再进入一个slave数据库,立即便可访问刚才写入master数据库的数据。如下所示

[[email protected] master-slave]# redis-cli 
127.0.0.1:6379> auth 123456
OK
127.0.0.1:6379> set site blog.jboost.cn
OK
127.0.0.1:6379> get site
"blog.jboost.cn"
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:2
slave0:ip=127.0.0.1,port=7001,state=online,offset=13364738,lag=1
slave1:ip=127.0.0.1,port=7002,state=online,offset=13364738,lag=0
...
127.0.0.1:6379> exit

[[email protected] master-slave]# redis-cli -p 7001
127.0.0.1:7001> auth 123456
OK
127.0.0.1:7001> get site
"blog.jboost.cn"

执行info replication命令可以查看连接该数据库的其它库的信息,如上可看到有两个slave连接到master

3. 主从复制的优缺点

优点:

  1. master能自动将数据同步到slave,可以进行读写分离,分担master的读压力
  2. master、slave之间的同步是以非阻塞的方式进行的,同步期间,客户端仍然可以提交查询或更新请求

缺点:

  1. 不具备自动容错与恢复功能,master或slave的宕机都可能导致客户端请求失败,需要等待机器重启或手动切换客户端IP才能恢复
  2. master宕机,如果宕机前数据没有同步完,则切换IP后会存在数据不一致的问题
  3. 难以支持在线扩容,Redis的容量受限于单机配置

Sentinel(哨兵)模式

1. 基本原理

哨兵模式基于主从复制模式,只是引入了哨兵来监控与自动处理故障。如图

redis-sentinel

哨兵顾名思义,就是来为Redis集群站哨的,一旦发现问题能做出相应的应对处理。其功能包括

  1. 监控master、slave是否正常运行
  2. 当master出现故障时,能自动将一个slave转换为master(大哥挂了,选一个小弟上位)
  3. 多个哨兵可以监控同一个Redis,哨兵之间也会自动监控

哨兵模式的具体工作机制:

在配置文件中通过 sentinel monitor <master-name> <ip> <redis-port> <quorum> 来定位master的IP、端口,一个哨兵可以监控多个master数据库,只需要提供多个该配置项即可。哨兵启动后,会与要监控的master建立两条连接:

  1. 一条连接用来订阅master的_sentinel_:hello频道与获取其他监控该master的哨兵节点信息
  2. 另一条连接定期向master发送INFO等命令获取master本身的信息

与master建立连接后,哨兵会执行三个操作:

  1. 定期(一般10s一次,当master被标记为主观下线时,改为1s一次)向master和slave发送INFO命令
  2. 定期向master和slave的_sentinel_:hello频道发送自己的信息
  3. 定期(1s一次)向master、slave和其他哨兵发送PING命令

发送INFO命令可以获取当前数据库的相关信息从而实现新节点的自动发现。所以说哨兵只需要配置master数据库信息就可以自动发现其slave信息。获取到slave信息后,哨兵也会与slave建立两条连接执行监控。通过INFO命令,哨兵可以获取主从数据库的最新信息,并进行相应的操作,比如角色变更等。

接下来哨兵向主从数据库的_sentinel_:hello频道发送信息与同样监控这些数据库的哨兵共享自己的信息,发送内容为哨兵的ip端口、运行id、配置版本、master名字、master的ip端口还有master的配置版本。这些信息有以下用处:

  1. 其他哨兵可以通过该信息判断发送者是否是新发现的哨兵,如果是的话会创建一个到该哨兵的连接用于发送PING命令。
  2. 其他哨兵通过该信息可以判断master的版本,如果该版本高于直接记录的版本,将会更新
  3. 当实现了自动发现slave和其他哨兵节点后,哨兵就可以通过定期发送PING命令定时监控这些数据库和节点有没有停止服务。

如果被PING的数据库或者节点超时(通过 sentinel down-after-milliseconds master-name milliseconds 配置)未回复,哨兵认为其主观下线(sdown,s就是Subjectively —— 主观地)。如果下线的是master,哨兵会向其它哨兵发送命令询问它们是否也认为该master主观下线,如果达到一定数目(即配置文件中的quorum)投票,哨兵会认为该master已经客观下线(odown,o就是Objectively —— 客观地),并选举领头的哨兵节点对主从系统发起故障恢复。若没有足够的sentinel进程同意master下线,master的客观下线状态会被移除,若master重新向sentinel进程发送的PING命令返回有效回复,master的主观下线状态就会被移除

哨兵认为master客观下线后,故障恢复的操作需要由选举的领头哨兵来执行,选举采用Raft算法:

  1. 发现master下线的哨兵节点(我们称他为A)向每个哨兵发送命令,要求对方选自己为领头哨兵
  2. 如果目标哨兵节点没有选过其他人,则会同意选举A为领头哨兵
  3. 如果有超过一半的哨兵同意选举A为领头,则A当选
  4. 如果有多个哨兵节点同时参选领头,此时有可能存在一轮投票无竞选者胜出,此时每个参选的节点等待一个随机时间后再次发起参选请求,进行下一轮投票竞选,直至选举出领头哨兵

选出领头哨兵后,领头者开始对系统进行故障恢复,从出现故障的master的从数据库中挑选一个来当选新的master,选择规则如下:

  1. 所有在线的slave中选择优先级最高的,优先级可以通过slave-priority配置
  2. 如果有多个最高优先级的slave,则选取复制偏移量最大(即复制越完整)的当选
  3. 如果以上条件都一样,选取id最小的slave

挑选出需要继任的slave后,领头哨兵向该数据库发送命令使其升格为master,然后再向其他slave发送命令接受新的master,最后更新数据。将已经停止的旧的master更新为新的master的从数据库,使其恢复服务后以slave的身份继续运行。

2. 部署演示

本示例基于Redis 5.0.3版。

哨兵模式基于前文的主从复制模式。哨兵的配置文件为sentinel.conf,在文件中添加

sentinel monitor mymaster 127.0.0.1 6379 1 # mymaster定义一个master数据库的名称,后面是master的ip, port,1表示至少需要一个Sentinel进程同意才能将master判断为失效,如果不满足这个条件,则自动故障转移(failover)不会执行
sentinel auth-pass mymaster 123456 # master的密码

sentinel down-after-milliseconds mymaster 5000 # 5s未回复PING,则认为master主观下线,默认为30s
sentinel parallel-syncs mymaster 2  # 指定在执行故障转移时,最多可以有多少个slave实例在同步新的master实例,在slave实例较多的情况下这个数字越小,同步的时间越长,完成故障转移所需的时间就越长
sentinel failover-timeout mymaster 300000 # 如果在该时间(ms)内未能完成故障转移操作,则认为故障转移失败,生产环境需要根据数据量设置该值

一个哨兵可以监控多个master数据库,只需按上述配置添加多套

分别以26379,36379,46379端口启动三个sentinel

[[email protected] sentinel]# redis-server sentinel1.conf --sentinel
[[email protected] sentinel]# redis-server sentinel2.conf --sentinel
[[email protected] sentinel]# redis-server sentinel3.conf --sentinel

也可以使用redis-sentinel sentinel1.conf 命令启动。此时集群包含一个master、两个slave、三个sentinel,如图,

redis-cluster-instance

我们来模拟master挂掉的场景,执行 kill -9 3017 将master进程干掉,进入slave中执行 info replication查看,

[[email protected] sentinel]# redis-cli -p 7001
127.0.0.1:7001> auth 123456
OK
127.0.0.1:7001> info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:7002
master_link_status:up
master_last_io_seconds_ago:1
master_sync_in_progress:0
# 省略
127.0.0.1:7001> exit
[[email protected] sentinel]# redis-cli -p 7002
127.0.0.1:7002> auth 123456
OK
127.0.0.1:7002> info replication
# Replication
role:master
connected_slaves:1
slave0:ip=127.0.0.1,port=7001,state=online,offset=13642721,lag=1
# 省略

可以看到slave 7002已经成功上位晋升为master(role:master),接收一个slave 7001的连接。此时查看slave2.conf配置文件,发现replicaof的配置已经被移除了,slave1.conf的配置文件里replicaof 127.0.0.1 6379 被改为 replicaof 127.0.0.1 7002。重新启动master,也可以看到master.conf配置文件中添加了replicaof 127.0.0.1 7002的配置项,可见大哥(master)下位后,再出来混就只能当当小弟(slave)了,三十年河东三十年河西。

3. 哨兵模式的优缺点

优点:

  1. 哨兵模式基于主从复制模式,所以主从复制模式有的优点,哨兵模式也有
  2. 哨兵模式下,master挂掉可以自动进行切换,系统可用性更高

缺点:

  1. 同样也继承了主从模式难以在线扩容的缺点,Redis的容量受限于单机配置
  2. 需要额外的资源来启动sentinel进程,实现相对复杂一点,同时slave节点作为备份节点不提供服务

Cluster模式

1. 基本原理

哨兵模式解决了主从复制不能自动故障转移,达不到高可用的问题,但还是存在难以在线扩容,Redis容量受限于单机配置的问题。Cluster模式实现了Redis的分布式存储,即每台节点存储不同的内容,来解决在线扩容的问题。如图

redis-cluster

Cluster采用无中心结构,它的特点如下:

  1. 所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽
  2. 节点的fail是通过集群中超过半数的节点检测失效时才生效
  3. 客户端与redis节点直连,不需要中间代理层.客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可

Cluster模式的具体工作机制:

  1. 在Redis的每个节点上,都有一个插槽(slot),取值范围为0-16383
  2. 当我们存取key的时候,Redis会根据CRC16的算法得出一个结果,然后把结果对16384求余数,这样每个key都会对应一个编号在0-16383之间的哈希槽,通过这个值,去找到对应的插槽所对应的节点,然后直接自动跳转到这个对应的节点上进行存取操作
  3. 为了保证高可用,Cluster模式也引入主从复制模式,一个主节点对应一个或者多个从节点,当主节点宕机的时候,就会启用从节点
  4. 当其它主节点ping一个主节点A时,如果半数以上的主节点与A通信超时,那么认为主节点A宕机了。如果主节点A和它的从节点都宕机了,那么该集群就无法再提供服务了

Cluster模式集群节点最小配置6个节点(3主3从,因为需要半数以上),其中主节点提供读写操作,从节点作为备用节点,不提供请求,只作为故障转移使用。

2. 部署演示

本示例基于Redis 5.0.3版。

Cluster模式的部署比较简单,首先在redis.conf中

port 7100 # 本示例6个节点端口分别为7100,7200,7300,7400,7500,7600 
daemonize yes # r后台运行 
pidfile /var/run/redis_7100.pid # pidfile文件对应7100,7200,7300,7400,7500,7600 
cluster-enabled yes # 开启集群模式 
masterauth passw0rd # 如果设置了密码,需要指定master密码
cluster-config-file nodes_7100.conf # 集群的配置文件,同样对应7100,7200等六个节点
cluster-node-timeout 15000 # 请求超时 默认15秒,可自行设置 

分别以端口7100,7200,7300,7400,7500,7600 启动六个实例(如果是每个服务器一个实例则配置可一样)

[[email protected] cluster]# redis-server redis_7100.conf
[[email protected] cluster]# redis-server redis_7200.conf
...

然后通过命令将这个6个实例组成一个3主节点3从节点的集群,

redis-cli --cluster create --cluster-replicas 1 127.0.0.1:7100 127.0.0.1:7200 127.0.0.1:7300 127.0.0.1:7400 127.0.0.1:7500 127.0.0.1:7600 -a passw0rd

执行结果如图

redis-cluster-deploy

可以看到 7100, 7200, 7300 作为3个主节点,分配的slot分别为 0-5460, 5461-10922, 10923-16383, 7600作为7100的slave, 7500作为7300的slave,7400作为7200的slave。

我们连接7100设置一个值

[[email protected] cluster]# redis-cli -p 7100 -c -a passw0rd
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
127.0.0.1:7100> set site blog.jboost.cn
-> Redirected to slot [9421] located at 127.0.0.1:7200
OK
127.0.0.1:7200> get site
"blog.jboost.cn"
127.0.0.1:7200>

注意添加 -c 参数表示以集群模式,否则报 (error) MOVED 9421 127.0.0.1:7200 错误, 以 -a 参数指定密码,否则报(error) NOAUTH Authentication required错误。

从上面命令看到key为site算出的slot为9421,落在7200节点上,所以有Redirected to slot [9421] located at 127.0.0.1:7200,集群会自动进行跳转。因此客户端可以连接任何一个节点来进行数据的存取。

通过cluster nodes可查看集群的节点信息

127.0.0.1:7200> cluster nodes
eb28aaf090ed1b6b05033335e3d90a202b422d6c 127.0.0.1:[email protected] slave c1047de2a1b5d5fa4666d554376ca8960895a955 0 1584165266071 5 connected
4cc0463878ae00e5dcf0b36c4345182e021932bc 127.0.0.1:[email protected] slave 5544aa5ff20f14c4c3665476de6e537d76316b4a 0 1584165267074 4 connected
dbbb6420d64db22f35a9b6fa460b0878c172a2fb 127.0.0.1:[email protected] master - 0 1584165266000 1 connected 0-5460
d4b434f5829e73e7e779147e905eea6247ffa5a2 127.0.0.1:[email protected] slave dbbb6420d64db22f35a9b6fa460b0878c172a2fb 0 1584165265000 6 connected
5544aa5ff20f14c4c3665476de6e537d76316b4a 127.0.0.1:[email protected] myself,master - 0 1584165267000 2 connected 5461-10922
c1047de2a1b5d5fa4666d554376ca8960895a955 127.0.0.1:[email protected] master - 0 1584165268076 3 connected 10923-16383

我们将7200通过 kill -9 pid杀死进程来验证集群的高可用,重新进入集群执行cluster nodes可以看到7200 fail了,但是7400成了master,重新启动7200,可以看到此时7200已经变成了slave。

3. Cluster模式的优缺点

优点:

  1. 无中心架构,数据按照slot分布在多个节点。
  2. 集群中的每个节点都是平等的关系,每个节点都保存各自的数据和整个集群的状态。每个节点都和其他所有节点连接,而且这些连接保持活跃,这样就保证了我们只需要连接集群中的任意一个节点,就可以获取到其他节点的数据。
  3. 可线性扩展到1000多个节点,节点可动态添加或删除
  4. 能够实现自动故障转移,节点之间通过gossip协议交换状态信息,用投票机制完成slave到master的角色转换

缺点:

  1. 客户端实现复杂,驱动要求实现Smart Client,缓存slots mapping信息并及时更新,提高了开发难度。目前仅JedisCluster相对成熟,异常处理还不完善,比如常见的“max redirect exception”
  2. 节点会因为某些原因发生阻塞(阻塞时间大于 cluster-node-timeout)被判断下线,这种failover是没有必要的
  3. 数据通过异步复制,不保证数据的强一致性
  4. slave充当“冷备”,不能缓解读压力
  5. 批量操作限制,目前只支持具有相同slot值的key执行批量操作,对mset、mget、sunion等操作支持不友好
  6. key事务操作支持有线,只支持多key在同一节点的事务操作,多key分布不同节点时无法使用事务功能
  7. 不支持多数据库空间,单机redis可以支持16个db,集群模式下只能使用一个,即db 0

Redis Cluster模式不建议使用pipeline和multi-keys操作,减少max redirect产生的场景。

总结

本文介绍了Redis集群方案的三种模式,其中主从复制模式能实现读写分离,但是不能自动故障转移;哨兵模式基于主从复制模式,能实现自动故障转移,达到高可用,但与主从复制模式一样,不能在线扩容,容量受限于单机的配置;Cluster模式通过无中心化架构,实现分布式存储,可进行线性扩展,也能高可用,但对于像批量操作、事务操作等的支持性不够好。三种模式各有优缺点,可根据实际场景进行选择。

 

转自:https://segmentfault.com/a/1190000022028642

RelativeLayout/ 相对布局简介、使用、说明

 android  RelativeLayout/ 相对布局简介、使用、说明已关闭评论
7月 012021
 

网上找到的一篇对relativeLayout使用非常好的文章,分享下, 原文地址见文件末尾


在上一节中我们对LinearLayout进行了详细的解析,LinearLayout也是我们 用的比较多的一个布局,我们更多的时候更钟情于他的weight(权重)属性,等比例划分,对屏幕适配还是 帮助蛮大的;但是使用LinearLayout的时候也有一个问题,就是当界面比较复杂的时候,需要嵌套多层的 LinearLayout,这样就会降低UI Render的效率(渲染速度),而且如果是listview或者GridView上的 item,效率会更低,另外太多层LinearLayout嵌套会占用更多的系统资源,还有可能引发stackoverflow; 但是如果我们使用RelativeLayout的话,可能仅仅需要一层就可以完成了,以父容器或者兄弟组件参考+margin +padding就可以设置组件的显示位置,是比较方便的!当然,也不是绝对的,具体问题具体分析吧! 总结就是:尽量使用RelativeLayout + LinearLayout的weight属性搭配使用吧!


1.核心属性图


2.父容器定位属性示意图


3.根据兄弟组件定位

恩,先说下什么是兄弟组件吧,所谓的兄弟组件就是处于同一层次容器的组件,如图

图中的组件1,2就是兄弟组件了,而组件3与组件1或组件2并不是兄弟组件,所以组件3不能通过 组件1或2来进行定位,比如layout_toleftof = “组件1″这样是会报错的!切记! 关于这个兄弟组件定位的最经典例子就是”梅花布局”了,下面代码实现下:

运行效果图:

实现代码:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"    
    xmlns:tools="http://schemas.android.com/tools"    
    android:id="@+id/RelativeLayout1"    
    android:layout_width="match_parent"    
    android:layout_height="match_parent" >    
    
    <!-- 这个是在容器中央的 -->    
        
    <ImageView    
        android:id="@+id/img1"     
        android:layout_width="80dp"    
        android:layout_height="80dp"    
        android:layout_centerInParent="true"    
        android:src="@drawable/pic1"/>    
        
    <!-- 在中间图片的左边 -->    
    <ImageView    
        android:id="@+id/img2"     
        android:layout_width="80dp"    
        android:layout_height="80dp"    
        android:layout_toLeftOf="@id/img1"    
        android:layout_centerVertical="true"    
        android:src="@drawable/pic2"/>    
        
    <!-- 在中间图片的右边 -->    
    <ImageView    
        android:id="@+id/img3"     
        android:layout_width="80dp"    
        android:layout_height="80dp"    
        android:layout_toRightOf="@id/img1"    
        android:layout_centerVertical="true"    
        android:src="@drawable/pic3"/>    
        
    <!-- 在中间图片的上面-->    
    <ImageView    
        android:id="@+id/img4"     
        android:layout_width="80dp"    
        android:layout_height="80dp"    
        android:layout_above="@id/img1"    
        android:layout_centerHorizontal="true"    
        android:src="@drawable/pic4"/>    
        
    <!-- 在中间图片的下面 -->    
    <ImageView    
        android:id="@+id/img5"     
        android:layout_width="80dp"    
        android:layout_height="80dp"    
        android:layout_below="@id/img1"    
        android:layout_centerHorizontal="true"    
        android:src="@drawable/pic5"/>    
    
</RelativeLayout>

4.margin与padding的区别

初学者对于这两个属性可能会有一点混淆,这里区分下: 首先margin代表的是偏移,比如marginleft = “5dp”表示组件离容器左边缘偏移5dp; 而padding代表的则是填充,而填充的对象针对的是组件中的元素,比如TextView中的文字 比如为TextView设置paddingleft = “5dp”,则是在组件里的元素的左边填充5dp的空间! margin针对的是容器中的组件,而padding针对的是组件中的元素,要区分开来! 下面通过简单的代码演示两者的区别:

比较示例代码如下:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"    
    xmlns:tools="http://schemas.android.com/tools"    
    android:layout_width="match_parent"    
    android:layout_height="match_parent"    
    android:paddingBottom="@dimen/activity_vertical_margin"    
    android:paddingLeft="@dimen/activity_horizontal_margin"    
    android:paddingRight="@dimen/activity_horizontal_margin"    
    android:paddingTop="@dimen/activity_vertical_margin"    
    tools:context=".MainActivity" >    
    
    <Button    
        android:id="@+id/btn1"     
        android:layout_height="wrap_content"    
        android:layout_width="wrap_content"    
        android:text="Button"/>    
    <Button    
        android:paddingLeft="100dp"     
        android:layout_height="wrap_content"    
        android:layout_width="wrap_content"    
        android:text="Button"    
        android:layout_toRightOf="@id/btn1"/>    
        
    <Button    
        android:id="@+id/btn2"     
        android:layout_height="wrap_content"    
        android:layout_width="wrap_content"    
        android:text="Button"    
        android:layout_alignParentBottom="true"/>    
    <Button    
        android:layout_marginLeft="100dp"     
        android:layout_height="wrap_content"    
        android:layout_width="wrap_content"    
        android:text="Button"    
        android:layout_toRightOf="@id/btn2"     
        android:layout_alignParentBottom="true"/>    
        
</RelativeLayout>

运行效果图比较:


5.很常用的一点:margin可以设置为负数

相信很多朋友都不知道一点吧,平时我们设置margin的时候都习惯了是正数的, 其实是可以用负数的,下面写个简单的程序演示下吧,模拟进入软件后,弹出广告 页面的,右上角的cancle按钮的margin则是使用负数的!

贴出的广告Activity的布局代码吧,当然,如果你对这个有兴趣的话可以下下demo, 因为仅仅是实现效果,所以代码会有些粗糙!

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"  
    xmlns:tools="http://schemas.android.com/tools"  
    android:layout_width="match_parent"  
    android:layout_height="match_parent"  
    tools:context="com.jay.example.relativelayoutdemo.MainActivity"   
    android:background="#00CCCCFF">  
  
    <ImageView  
        android:id="@+id/imgBack"  
        android:layout_width="200dp"  
        android:layout_height="200dp"  
        android:layout_centerInParent="true"  
        android:background="@drawable/myicon" />  
  
    <ImageView  
        android:id="@+id/imgCancle"  
        android:layout_width="28dp"  
        android:layout_height="28dp"  
        android:layout_alignRight="@id/imgBack"  
        android:layout_alignTop="@id/imgBack"  
        android:background="@drawable/cancel"  
        android:layout_marginTop="-15dp"  
        android:layout_marginRight="-10dp" />  
  
</RelativeLayout>

来自:https://www.runoob.com/w3cnote/android-tutorial-relativelayout.html

文章二: 使用ssh实现内网穿透及SSH端口转发详解(转)

 ssh  文章二: 使用ssh实现内网穿透及SSH端口转发详解(转)已关闭评论
2月 202021
 

以下内容是关于SSH端口转发的详解使用,文章通俗易懂,比前面的文章一写的更好,强烈推荐!原文地址见文末。

 

SSH有三种端口转发模式,本地端口转发(Local Port Forwarding)远程端口转发(Remote Port Forwarding)以及动态端口转发(Dynamic Port Forwarding)。对于本地/远程端口转发,两者的方向恰好相反。动态端口转发则可以用于科学上网。

SSH端口转发也被称作SSH隧道(SSH Tunnel),因为它们都是通过SSH登陆之后,在SSH客户端SSH服务端之间建立了一个隧道,从而进行通信。SSH隧道是非常安全的,因为SSH是通过加密传输数据的(SSH全称为Secure Shell)。

在本文所有示例中,本地主机A1为SSH客户端,远程云主机B1为SSH服务端。从A1主机通过SSH登陆B1主机,指定不同的端口转发选项(-L、-R和-D),即可在A1与B1之间建立SSH隧道,从而进行不同的端口转发。

本地端口转发

应用场景:

远程云主机B1运行了一个服务,端口为3000,本地主机A1需要访问这个服务。

示例为一个简单的Node.js服务:

var http = require('http');

var server = http.createServer(function(request, response)
{
    response.writeHead(200,
    {
        "Content-Type": "text/plain"
    });
    response.end("Hello O-u-u.com\n");
});

server.listen(3000);

假设云主机B1的IP为103.59.22.17,则该服务的访问地址为:http://103.59.22.17:3000

为啥需要本地端口转发呢?

一般来讲,云主机的防火墙默认只打开了22端口,如果需要访问3000端口的话,需要修改防火墙。为了保证安全,防火墙需要配置允许访问的IP地址。但是,本地公网IP通常是网络提供商动态分配的,是不断变化的。这样的话,防火墙配置需要经常修改,就会很麻烦。

什么是本地端口转发?

所谓本地端口转发,就是将发送到本地端口的请求,转发到目标端口。这样,就可以通过访问本地端口,来访问目标端口的服务。使用-L属性,就可以指定需要转发的端口,语法是这样的:

-L 本地网卡地址:本地端口:目标地址:目标端口

通过本地端口转发,可以将发送到本地主机A1端口2000的请求,转发到远程云主机B1的3000端口。

# 在本地主机A1登陆远程云主机B1,并进行本地端口转发
ssh -L localhost:2000:localhost:3000 [email protected]

这样,在本地主机A1上可以通过访问http://localhost:2000来访问远程云主机B1上的Node.js服务。

# 在本地主机A1访问远程云主机B1上的Node.js服务
curl http://localhost:2000
Hello O-u-u.com

实际上,-L选项中的本地网卡地址是可以省略的,这时表示2000端口绑定了本地主机A1的所有网卡:

# 在本地主机A1登陆远程云主机B1,并进行本地端口转发。2000端口绑定本地所有网卡
ssh -L 2000:localhost:3000 [email protected]

若本地主机A2能够访问A1,则A2也可以通过A1访问远程远程云主机B1上的Node.js服务。

另外,-L选项中的目标地址也可以是其他主机的地址。假设远程云主机B2的局域网IP地址为192.168.59.100,则可以这样进行端口转发:

# 在本地主机A1登陆远程云主机B1,并进行本地端口转发。请求被转发到远程云主机B2上
ssh -L 2000:192.168.59.100:3000 [email protected]

若将Node.js服务运行在远程云主机B2上,则发送到A1主机2000端口的请求,都会被转发到B2主机上。

远程端口转发

应用场景:

本地主机A1运行了一个服务,端口为3000,远程云主机B1需要访问这个服务。

将前文的Node.js服务运行在本地,在本地就可以通过http://localhost:3000访问该服务。

为啥需要远程端口转发呢?

通常,本地主机是没有独立的公网IP的,它与同一网络中的主机共享一个IP。没有公网IP,云主机是无法访问本地主机上的服务的。

什么是远程端口转发?

所谓远程端口转发,就是将发送到远程端口的请求,转发到目标端口。这样,就可以通过访问远程端口,来访问目标端口的服务。使用-R属性,就可以指定需要转发的端口,语法是这样的:

-R 远程网卡地址:远程端口:目标地址:目标端口

这时,通过远程端口转发,可以将发送到远程云主机B1端口2000的请求,转发到本地主机A1端口3000。

# 在本地主机A1登陆远程云主机B1,并进行远程端口转发
ssh -R localhost:2000:localhost:3000 [email protected]

这样,在远程云主机A1可以通过访问http://localhost:2000来访问本地主机的服务。

# 在远程云主机B1访问本地主机A1上的Node.js服务
curl http://localhost:2000
Hello O-u-u.com

同理,远程网卡地址可以省略,目标地址也可以是其他主机地址。假设本地主机A2的局域网IP地址为192.168.0.100。

# 在本地主机A1登陆远程云主机B1,并进行远程端口转发
ssh -R 2000:192.168.0.100:3000 [email protected]

若将Node.js服务运行在本地主机A2上,则发送到远程云主机A1端口2000的请求,都会被转发到A2主机上。

动态端口转发

应用场景:

远程云主机B1运行了多个服务,分别使用了不同端口,本地主机A1需要访问这些服务。

为啥需要动态端口转发呢?

一方面,由于防火墙限制,本地主机A1并不能直接访问远程云主机B1上的服务,因此需要进行端口转发;另一方面,为每个端口分别创建本地端口转发非常麻烦。

什么是动态端口转发?

对于本地端口转发远程端口转发,都存在两个一一对应的端口,分别位于SSH的客户端和服务端,而动态端口转发则只是绑定了一个本地端口,而目标地址:目标端口则是不固定的。目标地址:目标端口是由发起的请求决定的,比如,请求地址为192.168.1.100:3000,则通过SSH转发的请求地址也是192.168.1.100:3000

-D 本地网卡地址:本地端口

这时,通过动态端口转发,可以将在本地主机A1发起的请求,转发到远程主机B1,而由B1去真正地发起请求。

# 在本地主机A1登陆远程云主机B1,并进行动态端口转发
ssh -D localhost:2000 [email protected]

而在本地发起的请求,需要由Socket代理(Socket Proxy)转发到SSH绑定的2000端口。以Firefox浏览器为例,配置Socket代理需要找到首选项>高级>网络>连接->设置:

这样的话,Firefox浏览器发起的请求都会转发到2000端口,然后通过SSH转发到真正地请求地址。若Node.js服务运行在远程云主机B1上,则在Firefox中访问localhost:3000即可以访问。如果主机B1能够访问外网的话,则可以科学上网……

链式端口转发

本地端口转发远程端口转发结合起来使用,可以进行链式转发。假设A主机在公司,B主机在家,C主机为远程云主机。A主机上运行了前文的Node.js服务,需要在B主机上访问该服务。由于A和B不在同一个网络,且A主机没有独立公共IP地址,所以无法直接访问服务。

通过本地端口转发,将发送到B主机3000端口的请求,转发到远程云主机C的2000端口。

# 在B主机登陆远程云主机C,并进行本地端口转发
ssh -L localhost:3000:localhost:2000 [email protected]

通过远程端口转发,将发送到远程云主机C端口2000的请求,转发到A主机的3000端口。

# 在A主机登陆远程云主机C,并进行远程端口转发
ssh -R localhost:2000:localhost:3000 [email protected]

这样,在主机B可以通过访问http://localhost:3000来访问主机A上的服务。

# 在主机B访问主机A上的服务
curl http://localhost:3000
Hello O-u-u.com

 

转自:https://blog.fundebug.com/2017/04/24/ssh-port-forwarding/

文章一:使用ssh实现内网穿透入门及应用(附autossh方法)(转)

 ssh  文章一:使用ssh实现内网穿透入门及应用(附autossh方法)(转)已关闭评论
2月 142021
 

转发一篇关于使用ssh实现内网穿透的文章,写的通俗易懂,原链接见文章末尾。

 

“世界上最遥远的距离就是你在外网请求,我在内网测试。”

这句话的内容,对于开发人员来说,特别容易理解。很多情况下,我们的开发及测试环境在单位的内网下,只能通过位于内网的机器来连接操作,位于外网的机器是连不到内网环境的。比如说,如果我们周末在家工作,而家里的机器又不在单位内网环境下,那该如何连接内网的环境呢?难不成我们还要大周末的跑到单位去加班吗?

答案是否定的。这是种普遍又迫切的需求,叫“内网穿透”。这里我们使用SSH端口转发的技术,解决这种问题。

SSH本地端口转发

假设,host1和host2位于内网,host3位于外网,host3可以连接host1和host2,但host1不能连接host3和host2。我们要做的是,通过位于外网的host3,让host1来连接host2。

具体步骤

首先,在host1上进行如下操作:


ssh -L 2222:host2:22 [email protected]

其中,-L参数指定了“本地主机端口:目标主机:目标主机端口”。这表示,让host1作为sshd服务端,监听它自己的2222端口,然后将所有数据经由host3,转发到host2的22端口。

这种情况下,host1不能连接host3,但由于host1的配置,使得从host1到host3建立了一条“SSH隧道”

然后,在host1上进行如下操作:


ssh -p 2222 [email protected]

其中,-p参数指定了ssh连接的端口,默认为22,这里指定了2222端口。这表示,让host1作为ssh客户端,连接它自己的2222端口,相当于连接host2的22端口。

一般情况下,host2与host3为一台主机,换句话说,我们只要实现连接host3,那么再连接host2也不成问题。

这时,命令分别转换为:


ssh -L 2222:localhost:22 [email protected]
ssh -p 2222 [email protected]

SSH本地端口转发的本质

本质上,SSH本地端口转发,主要是实现以下两个方面:


1. 将本地主机的端口,转发到目标主机的端口
2. 在本地主机上,连接本地主机的端口,相当于连接目标主机的端口

SSH远程端口转发

假设,host1和host2位于内网,host3位于外网,host1可以连接host3和host2,但host3不能连接host1和host2。我们要做的是,通过位于内网的host1,让host3来连接host2,也就是实现所谓的“内网穿透”

具体步骤

首先,在host1上进行如下操作:


ssh -R 2222:host2:22 [email protected]

其中,-R参数指定了“远程主机端口:目标主机:目标主机端口”。这表示,让host3作为sshd服务端,监听它自己的2222端口,然后将所有数据经由host1,转发到host2的22端口。

这种情况下,host3不能连接host1,但由于host1的配置,使得从host1到host3建立了一条“SSH反向隧道”

然后,在host3上进行如下操作:


ssh -p 2222 [email protected]

其中,-p参数指定了ssh连接的端口,默认为22,这里指定了2222端口。这表示,让host3作为ssh客户端,连接它自己的2222端口,相当于连接host2的22端口。

一般情况下,host2与host1为一台主机,换句话说,我们只要实现连接host1,那么再连接host2也不成问题。

这时,命令分别转换为:


ssh -R 2222:localhost:22 [email protected]
ssh -p 2222 [email protected]

SSH远程端口转发的本质

本质上,SSH远程端口转发,主要是实现以下两个方面:


1. 将远程主机的端口,转发到目标主机的端口
2. 在远程主机上,连接远程主机的端口,相当于连接目标主机的端口

SSH实现内网穿透

内网穿透,简单来说就是,利用位于外网的主机,来连接位于内网的主机,这符合SSH远程端口转发的情况。但由于实际情况中,SSH连接经常由于这样那样的问题,导致连接断开,因此我们不得不重新去在内网主机上建立与外网主机的连接,也就是维持这条“SSH反向隧道”,autossh能实现连接断开之后自动重连功能。

自动重连

autossh与ssh用法类似,只要将ssh命令替换成autossh命令即可,如下所示:


autossh -M 2345 -NTR 2222:localhost:22 [email protected]

其中,-M参数指定了autossh监听的端口,注意这里与其转发的端口要区分开。

另外,-N表示禁止执行远程命令,-T表示禁止分配伪终端,这两个参数结合起来表示SSH连接不允许用户交互执行远程操作,只能用来传数据,从而保证了远程主机的安全。

自动登录

每次重新建立连接,autossh都需要确认一下登录身份。要保证自动重连,前提就是要实现自动登录

一种常见的做法,就是使用公钥登录进行免密登录,将host1上的公钥传送至host3上。这样,每次在进行SSH登录的时候,host3都会向host1发送一段随机字符串,host1用自己的私钥加密后将数据返回,然后host3用事先存好的公钥对返回的数据进行解密,如果成功,则证明host1的身份可信,允许直接登录,不再要求密码。

还有一种做法,就是利用sshpass将密码明文传输给autossh,如下所示:


sshpass -p "xxxxxx" autossh -M 2345 -NTR 2222:localhost:22 [email protected]

其中,-p参数指定了登录的密码。除了命令行输入密码的形式,sshpass还包含-f、-e等参数,分别支持文件输入密码及系统环境变量输入密码等形式,如图所示。

其他端口

实现内网穿透,除了转发22端口外,我们也可以转发其他应用的端口,如web服务的80端口、mysql的3306端口等,这里就不一一细说了。

 

来自:http://www.lining0806.com/ssh%E7%AB%AF%E5%8F%A3%E8%BD%AC%E5%8F%91%E5%AE%9E%E7%8E%B0%E5%86%85%E7%BD%91%E7%A9%BF%E9%80%8F/

ssh解析、原理、入门、运用(转)

 linux, ssh  ssh解析、原理、入门、运用(转)已关闭评论
1月 252021
 

转自网上的一篇文章, 描述ssh通俗易懂,原链接见文章末尾。

 

SSH是每一台Linux电脑的标准配置。

随着Linux设备从电脑逐渐扩展到手机、外设和家用电器,SSH的使用范围也越来越广。不仅程序员离不开它,很多普通用户也每天使用。

SSH具备多种功能,可以用于很多场合。有些事情,没有它就是办不成。本文是我的学习笔记,总结和解释了SSH的常见用法,希望对大家有用。

虽然本文内容只涉及初级应用,较为简单,但是需要读者具备最基本的”Shell知识”和了解”公钥加密”的概念。

 

一、什么是SSH?

简单说,SSH是一种网络协议,用于计算机之间的加密登录。

如果一个用户从本地计算机,使用SSH协议登录另一台远程计算机,我们就可以认为,这种登录是安全的,即使被中途截获,密码也不会泄露。

最早的时候,互联网通信都是明文通信,一旦被截获,内容就暴露无疑。1995年,芬兰学者Tatu Ylonen设计了SSH协议,将登录信息全部加密,成为互联网安全的一个基本解决方案,迅速在全世界获得推广,目前已经成为Linux系统的标准配置。

需要指出的是,SSH只是一种协议,存在多种实现,既有商业实现,也有开源实现。本文针对的实现是OpenSSH,它是自由软件,应用非常广泛。

此外,本文只讨论SSH在Linux Shell中的用法。如果要在Windows系统中使用SSH,会用到另一种软件PuTTY,这需要另文介绍。

二、最基本的用法

SSH主要用于远程登录。假定你要以用户名user,登录远程主机host,只要一条简单命令就可以了。

  $ ssh [email protected]

如果本地用户名与远程用户名一致,登录时可以省略用户名。

  $ ssh host

SSH的默认端口是22,也就是说,你的登录请求会送进远程主机的22端口。使用p参数,可以修改这个端口。

  $ ssh -p 2222 [email protected]

上面这条命令表示,ssh直接连接远程主机的2222端口。

三、中间人攻击

SSH之所以能够保证安全,原因在于它采用了公钥加密。

整个过程是这样的:(1)远程主机收到用户的登录请求,把自己的公钥发给用户。(2)用户使用这个公钥,将登录密码加密后,发送回来。(3)远程主机用自己的私钥,解密登录密码,如果密码正确,就同意用户登录。

这个过程本身是安全的,但是实施的时候存在一个风险:如果有人截获了登录请求,然后冒充远程主机,将伪造的公钥发给用户,那么用户很难辨别真伪。因为不像https协议,SSH协议的公钥是没有证书中心(CA)公证的,也就是说,都是自己签发的。

可以设想,如果攻击者插在用户与远程主机之间(比如在公共的wifi区域),用伪造的公钥,获取用户的登录密码。再用这个密码登录远程主机,那么SSH的安全机制就荡然无存了。这种风险就是著名的“中间人攻击”(Man-in-the-middle attack)。

SSH协议是如何应对的呢?

四、口令登录

如果你是第一次登录对方主机,系统会出现下面的提示:

  $ ssh [email protected]

The authenticity of host ‘host (12.18.429.21)’ can’t be established.

RSA key fingerprint is 98:2e:d7:e0:de:9f:ac:67:28:c2:42:2d:37:16:58:4d.

Are you sure you want to continue connecting (yes/no)?

这段话的意思是,无法确认host主机的真实性,只知道它的公钥指纹,问你还想继续连接吗?

所谓”公钥指纹”,是指公钥长度较长(这里采用RSA算法,长达1024位),很难比对,所以对其进行MD5计算,将它变成一个128位的指纹。上例中是98:2e:d7:e0:de:9f:ac:67:28:c2:42:2d:37:16:58:4d,再进行比较,就容易多了。

很自然的一个问题就是,用户怎么知道远程主机的公钥指纹应该是多少?回答是没有好办法,远程主机必须在自己的网站上贴出公钥指纹,以便用户自行核对。

假定经过风险衡量以后,用户决定接受这个远程主机的公钥。

  Are you sure you want to continue connecting (yes/no)? yes

系统会出现一句提示,表示host主机已经得到认可。

  Warning: Permanently added ‘host,12.18.429.21’ (RSA) to the list of known hosts.

然后,会要求输入密码。

  Password: (enter password)

如果密码正确,就可以登录了。

当远程主机的公钥被接受以后,它就会被保存在文件$HOME/.ssh/known_hosts之中。下次再连接这台主机,系统就会认出它的公钥已经保存在本地了,从而跳过警告部分,直接提示输入密码。

每个SSH用户都有自己的known_hosts文件,此外系统也有一个这样的文件,通常是/etc/ssh/ssh_known_hosts,保存一些对所有用户都可信赖的远程主机的公钥。

五、公钥登录

使用密码登录,每次都必须输入密码,非常麻烦。好在SSH还提供了公钥登录,可以省去输入密码的步骤。

所谓”公钥登录”,原理很简单,就是用户将自己的公钥储存在远程主机上。登录的时候,远程主机会向用户发送一段随机字符串,用户用自己的私钥加密后,再发回来。远程主机用事先储存的公钥进行解密,如果成功,就证明用户是可信的,直接允许登录shell,不再要求密码。

这种方法要求用户必须提供自己的公钥。如果没有现成的,可以直接用ssh-keygen生成一个:

  $ ssh-keygen

运行上面的命令以后,系统会出现一系列提示,可以一路回车。其中有一个问题是,要不要对私钥设置口令(passphrase),如果担心私钥的安全,这里可以设置一个。

运行结束以后,在$HOME/.ssh/目录下,会新生成两个文件:id_rsa.pub和id_rsa。前者是你的公钥,后者是你的私钥。

这时再输入下面的命令,将公钥传送到远程主机host上面:

  $ ssh-copy-id [email protected]

好了,从此你再登录,就不需要输入密码了。

如果还是不行,就打开远程主机的/etc/ssh/sshd_config这个文件,检查下面几行前面”#”注释是否取掉。

  RSAAuthentication yes
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys

然后,重启远程主机的ssh服务。

  // ubuntu系统
service ssh restart

// debian系统
/etc/init.d/ssh restart

六、authorized_keys文件

远程主机将用户的公钥,保存在登录后的用户主目录的$HOME/.ssh/authorized_keys文件中。公钥就是一段字符串,只要把它追加在authorized_keys文件的末尾就行了。

这里不使用上面的ssh-copy-id命令,改用下面的命令,解释公钥的保存过程:

  $ ssh [email protected] ‘mkdir -p .ssh && cat >> .ssh/authorized_keys’ < ~/.ssh/id_rsa.pub

这条命令由多个语句组成,依次分解开来看:(1)”$ ssh [email protected]”,表示登录远程主机;(2)单引号中的mkdir .ssh && cat >> .ssh/authorized_keys,表示登录后在远程shell上执行的命令:(3)”$ mkdir -p .ssh”的作用是,如果用户主目录中的.ssh目录不存在,就创建一个;(4)’cat >> .ssh/authorized_keys’ < ~/.ssh/id_rsa.pub的作用是,将本地的公钥文件~/.ssh/id_rsa.pub,重定向追加到远程文件authorized_keys的末尾。

写入authorized_keys文件后,公钥登录的设置就完成了。

 

转自阮一峰的文章:http://www.ruanyifeng.com/blog/2011/12/ssh_remote_login.html

python使用timeit测试/统计小段代码运行时间

 python  python使用timeit测试/统计小段代码运行时间已关闭评论
12月 232020
 

python下有个小而美的测试代码片段时间的模块timeit,使用方法如下:

比如:  “-“.join(str(n) for n in range(100))语句执行3趟,每趟10000次的时间统计如下测试:

一、 终端命令行方式:
python -m timeit -n 10000 -r 3 -v ‘”-“.join(str(n) for n in range(100))’
raw times: 0.345 0.351 0.329
10000 loops, best of 3: 32.9 usec per loop
每次执行了10000次,共执行3次,最好的1次,平均每loop是32.9 usec(32.9微秒,10000 loops,best of 3的意思是一共repeat了3次,每一次10000 loops,取最好的那一次来平均)。32.9 usec就是这一行python表达式的执行时间。

二、python模块内方式:
>>> import timeit
>>> timeit.timeit(‘”-“.join(str(n) for n in range(100))’, number = 10000)
0.3615691661834717
>>> timeit.repeat(‘”-“.join(str(n) for n in range(100))’, number = 10000, repeat=3)[0.37982702255249023, 0.3650989532470703, 0.3783681392669678]

三、jupyter note使用方式
%timeit -n 1000 -r 3 “-“.join(str(n) for n in range(100))

 

附录资料:

1. 命令行界面

命令格式: python -m timeit [-n N] [-r N] [-u U] [-s S] [-t] [-c] [-h] [语句 ...]

参数:

-n:执行次数
-r:计时器重复次数
-s:执行环境配置(通常该语句只被执行一次)
-v:打印原始时间
-h:帮助

 

2. 多行语句执行方式(注意空格)

python -m timeit ‘try:’ ‘  str.__bool__’ ‘except AttributeError:’ ‘  pass’

1000000 loops, best of 3: 1.2 usec per loop

android中SpannableStringBuilder的使用(转)

 android  android中SpannableStringBuilder的使用(转)已关闭评论
10月 132020
 
关于SpannableStringBuilder使用的文章,分享下
1.简介

SpannableStringBuilder和SpannableString也可以用来存储字符串,它们俩都有SetSpan()方法,可以对字符串设置背景色,字体大小颜色,下划线,删除线,粗体斜体等。
SpannableStringBuilder和SpannableString的区别:
SpannableString在构造对象的时候必须一次传入,之后无法再更换String,
SpannableStringBuilder可以使用append方法不断的拼接多个String。
因为Spannable等最终都实现了CharSequence接口,所以可以直接把SpannableString和SpannableStringBuilder通过TextView.setText()设置给TextView。

2.setSpan()

 /*给特定范围的字符串设定Span样式
 * Falg参数标识了当在所标记范围前和标记范围后紧贴着插入新字符时的动作
 * what 对应的各种Span 
 * start Span指定的开始位置,索引从0开始
 * end  Span指定的结束的位置,不包括尾,()包含头不包含尾
 * flags   用来指定范围前后输入新的字符时,会不会应用效果的
  ----Spannable.SPAN_EXCLUSIVE_EXCLUSIVE:前后都不包括,即在指定范围的前面和后面插入新字符都不会应用新样式 
  ----Spannable.SPAN_EXCLUSIVE_INCLUSIVE:前面不包括,后面包括。即仅在范围字符的后面插入新字符时会应用新样式
  ----Spannable.SPAN_INCLUSIVE_EXCLUSIVE:前面包括,后面不包括。
  ----Spannable.SPAN_INCLUSIVE_INCLUSIVE:前后都包括。
 */

void setSpan(Object what, int start, int end, int flags)

演示

 EditText et = (EditText) findViewById(R.id.edittext);
        SpannableStringBuilder stringBuilder =new SpannableStringBuilder("我来测试flags属性");
        ForegroundColorSpan span = new ForegroundColorSpan(Color.RED);
        stringBuilder.setSpan(span,1,3, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        et.setText(stringBuilder);

这里用的是 flag : Spanned.SPAN_INCLUSIVE_EXCLUSIVE,在前面插入新字符会改变字体颜色,但范围后面插入的新字符字体颜色不变。

spanablestring_01.png

插入新字符后效果

spanstring_02.png
改变字体大小

AbsoluteSizeSpan span = new AbsoluteSizeSpan(16);

spanstring_text_size.png
改变字体背景颜色

BackgroundColorSpan span = new BackgroundColorSpan(Color.BLUE);

spanstring_text_background.png
改变字体为粗体斜体

StyleSpan span = new StyleSpan(Typeface.BOLD_ITALIC);//粗体斜体

spanstring_bold.png
删除线

StrikethroughSpan span = new StrikethroughSpan();//删除线

spanstring_strikethrough.png
下划线

UnderlineSpan span = new UnderlineSpan();//下划线

spanstring_underline.png
文字换为图片

Drawable d = getResources().getDrawable(R.mipmap.ic_launcher);
d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
ImageSpan span = new ImageSpan(d, ImageSpan.ALIGN_BASELINE);

spanstring_text_to_img.png

示例 (区间价格的显示)
生成效果

spannablestringbuilder_price.png

TextView tvPrice =(TextView)findViewById(R.id.tv_price);
tvPrice.setText(formatPriceString(this,"100.00","230.87","件"));

public static SpannableStringBuilder formatPriceString(Context context, String minPrice, String maxPrice, String unit) {
        try {
            while (minPrice.contains(".") && (minPrice.endsWith("0") || minPrice.endsWith("."))) {
                minPrice = minPrice.subSequence(0, minPrice.length() - 1).toString();
            }
            while (maxPrice.contains(".") && (maxPrice.endsWith("0") || maxPrice.endsWith("."))) {
                maxPrice = maxPrice.subSequence(0, maxPrice.length() - 1).toString();
            }

            Float minPriceFloat = Float.valueOf(minPrice);//100.0
            Float maxPriceFloat = Float.valueOf(maxPrice);//230.87
            DecimalFormat decimalFormat = new DecimalFormat("#0.00");
            boolean isBig = false;
            if (minPriceFloat > tenThousand || maxPriceFloat > tenThousand) {
                isBig = true;
                minPriceFloat = minPriceFloat / 10000;
                maxPriceFloat = maxPriceFloat / 10000;
                minPrice = decimalFormat.format(minPriceFloat);
                maxPrice = decimalFormat.format(maxPriceFloat);
            }
            SpannableStringBuilder builder = new SpannableStringBuilder("价格:¥");
            int dp12 = getPixByDp(12, context);
            int length = builder.length();
            builder.setSpan(new AbsoluteSizeSpan(dp12), 0, length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);//字体大小
            builder.setSpan(new ForegroundColorSpan(Color.parseColor("#919191")), 0, length - 1, Spannable.SPAN_INCLUSIVE_INCLUSIVE);//字体颜色
            builder.setSpan(new ForegroundColorSpan(Color.parseColor("#eb413d")), length - 1, length, Spannable.SPAN_INCLUSIVE_INCLUSIVE);

            SpannableString priceSpan = new SpannableString(minPrice + "~" + maxPrice);
            int dp16 = getPixByDp(16, context);
            int lengthPrice = priceSpan.length();
            priceSpan.setSpan(new AbsoluteSizeSpan(dp16), 0, lengthPrice, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            priceSpan.setSpan(new ForegroundColorSpan(Color.parseColor("#eb413d")), 0, lengthPrice, Spannable.SPAN_INCLUSIVE_INCLUSIVE);
            builder.append(priceSpan);
            SpannableString unitSpan;
            if (!isBig) {
                unitSpan = new SpannableString("元/" + unit);
            } else {
                unitSpan = new SpannableString("万元/" + unit);
            }
            int lengthUnit = unitSpan.length();
            unitSpan.setSpan(new AbsoluteSizeSpan(dp12), 0, lengthUnit, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            unitSpan.setSpan(new ForegroundColorSpan(Color.parseColor("#919191")), 0, lengthUnit, Spannable.SPAN_INCLUSIVE_INCLUSIVE);
            builder.append(unitSpan);
            return builder;
        } catch (Exception e) {

        }
        return new SpannableStringBuilder("");
    }

链接:https://www.jianshu.com/p/a157cd9297b5

android开源EventBus框架使用介绍

 android  android开源EventBus框架使用介绍已关闭评论
10月 102020
 

网上一篇关于android eventBus框架使用的文章,分享下,来自:https://blog.csdn.net/sdsxtianshi/article/details/80449804

 

1. EventBus 框架

    EventBus,顾名思义即事件总线,是针对Android跨进程、线程通信的优化方案,在一定程度上可以代替Handle、Intent、Brodcast等实现通信;如下图所示即EventBus的运行框架。
这里写图片描述
     在EventBus中主要有以下三个成员

  • Event:事件,可以自定义为任意对象,类似Message类的作用;
  • Publisher:事件发布者,可以在任意线程、任意位置发布Event,已发布的Evnet则由EventBus进行分发;
  • Subscriber:事件订阅者,接收并处理事件,需要通过register(this)进行注册,而在类销毁时要使用unregister(this)方法解注册。每个Subscriber可以定义一个或多个事件处理方法,其方法名可以自定义,但需要添加@Subscribe的注解,并指明ThreadMode(不写默认为Posting)。
1.1 五种ThreadMode
  • Posting:直接在事件发布者所在线程执行事件处理方法;
  • Main:直接在主线程中执行事件处理方法(即UI线程),如果发布事件的线程也是主线程,那么事件处理方法会直接被调用,并且未避免ANR,该方法应避免进行耗时操作;
  • MainOrdered:也是直接在主线程中执行事件处理方法,但与Main方式不同的是,不论发布者所在线程是不是主线程,发布的事件都会进入队列按事件串行顺序依次执行;
  • BACKGROUND:事件处理方法将在后台线程中被调用。如果发布事件的线程不是主线程,那么事件处理方法将直接在该线程中被调用。如果发布事件的线程是主线程,那么将使用一个单独的后台线程,该线程将按顺序发送所有的事件。
  • Async:不管发布者的线程是不是主线程,都会开启一个新的线程来执行事件处理方法。如果事件处理方法的执行需要一些时间,例如网络访问,那么就应该使用该模式。为避免触发大量的长时间运行的事件处理方法,EventBus使用了一个线程池来有效地重用已经完成调用订阅者方法的线程以限制并发线程的数量。
    后面会通过代码展示五种ThreadMode的工作方式。

2. EventBus的使用流程

1. build.gradle 中添加EventBus的依赖:

dependencies {
    ...
    compile 'org.greenrobot:eventbus:3.1.1'
}

2. 定义Event事件类:

public class myEvent {
    private String mMessage;
            ...
}

3. 添加订阅事件:

EventBus.getDefault().register(this);

4. 定义事件处理方法并添加注解,参数为定义的事件类:

@Subscribe(threadMode = ThreadMode.MAIN)
public void onEvent(myEvent me){
            ...
}

5. 发布事件以触发事件处理方法:

EventBus.getDefault().post(new myEvent("this is first message !"));

3. EventBus应用

以下Demo以5中不同的ThreadMode定义了5个事件处理方法,并新开启一个线程作为事件发布者。

3.1 定义Event时间类
/**
 * Created by wise on 2018/5/16.
 */
/**Event事件类**/
public class myEvent {
    private String mMessage;
    public myEvent(String message){
        this.mMessage = message;
    }

    public void setMessage(String message){
        this.mMessage = message;
    }

    public String getmessage(){
        return this.mMessage;
    }

}
3.2 EventBus功能实现
public class EventBusActivity extends AppCompatActivity {
    private static final String TAG = "EventBusActivity";
    private TextView tv_Event;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_event_bus);
        /**注册事件**/
        EventBus.getDefault().register(this);
        Thread thread1 = new Thread(new myThread1());
        thread1.start();
    }
    /**事件处理方法**/
    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onEventMain(myEvent me){
        Log.d(TAG,me.getmessage() + " onEventMain" + " thread:" + android.os.Process.myTid());
    }
    /**事件处理方法**/
    @Subscribe(threadMode = ThreadMode.MAIN_ORDERED)
    public void onEventMainOrdered(myEvent me){
        Log.d(TAG,me.getmessage() + " onEventMainOrdered" + " thread:" + android.os.Process.myTid());
    }
    /**事件处理方法**/
    @Subscribe(threadMode = ThreadMode.POSTING)
    public void onEventPosting(myEvent te){
      Log.d(TAG,te.getmessage() + " onEventPosting" + " thread:" + android.os.Process.myTid());
    }
    /**事件处理方法**/
    @Subscribe(threadMode = ThreadMode.BACKGROUND)
    public void onEventBackground(myEvent me){
        Log.d(TAG,me.getmessage() + " onEventBackground" + " thread:" + android.os.Process.myTid());
    }
    /**事件处理方法**/
    @Subscribe(threadMode = ThreadMode.ASYNC)
    public void onEventAsync(myEvent me){
        Log.d(TAG,me.getmessage() + " onEventAsync" + " thread:" + android.os.Process.myTid());
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        /**解注册**/
        EventBus.getDefault().unregister(this);
    }

    public class myThread1 implements Runnable {

        @Override
        public void run() {
            Log.d(TAG,"" + android.os.Process.myTid());
            try {
                Thread.sleep(1000);
                /**事件发布**/
                EventBus.getDefault().post(new myEvent("this is first message !"));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

 

执行结果如下:

05-24 17:17:47.335 24447 24478 D EventBusActivity: 24478
05-24 17:17:48.341 24447 24478 D EventBusActivity: this is first message ! onEventBackground thread:24478
05-24 17:17:48.341 24447 24498 D EventBusActivity: this is first message ! onEventAsync thread:24498
05-24 17:17:48.342 24447 24447 D EventBusActivity: this is first message ! onEventMain thread:24447
05-24 17:17:48.342 24447 24447 D EventBusActivity: this is first message ! onEventMainOrdered thread:24447
05-24 17:17:48.344 24447 24478 D EventBusActivity: this is first message ! onEventPosting thread:24478

 

主线程Tid为24447,子线程Tid为24478,可以看到每种ThreadMode的运行方式。另外我们可以通过定义不同的事件类作为post的参数来执行不同的事件执行方法,后一篇会分析源码解释调用事件处理方法的逻辑。

4. EventBus的粘性事件

以上的Demo中,事件订阅者的注册必须在发布事件之前,否则发布之后,订阅者无法接受到事件,而粘性事件则避免了这一问题,粘性事件的发布使用postSticky()方法即可,并在注解中配置sticky参数。

// 订阅粘性事件
@Subscribe(sticky = true)
public void onMessageEvent(myEvent me) {
    ...
}

// 粘性事件发布
EventBus.getDefault().postSticky(new myEvent ("This is sticky event"));

发布一个粘性事件之后,EventBus将在内存中缓存该粘性事件。当有订阅者订阅了该粘性事件,订阅者将接收到该事件。需要注意的是在未移除粘性事件之前,它会一直缓存在内存中,因此在处理完该事件后要及时移除该事件的缓存,移除粘性事件的方法如下:

// 移除指定的粘性事件
removeStickyEvent(Object event);

// 移除指定类型的粘性事件
removeStickyEvent(Class<T> eventType);

// 移除所有的粘性事件
removeAllStickyEvents();

5. EventBus中的优先级

在定义事件处理方法时,还可以在注解中设置该方法的优先级:

@Subscribe(priority = 1)
public void onEvent(myEvent me) {
        ...
}

 

默认情况下,订阅者方法的事件传递优先级为0。数值越大,优先级越高。在相同的线程模式下,更高优先级的订阅者方法将优先接收到事件。注意:优先级只有在相同的线程模式下才有效。

Mac 上使用多点触控手势(含鼠标手势)

 mac  Mac 上使用多点触控手势(含鼠标手势)已关闭评论
8月 072020
 

在 Mac 上使用多点触控手势

使用多点触控触控板或妙控鼠标,您可以通过轻点、轻扫、捏合或开合一根或多根手指进行有用的操作。

触控板手势

有关这些手势的更多信息,请选取苹果菜单 () >“系统偏好设置”,然后点按“触控板”。您可以关闭某个手势,更改手势类型,以及了解哪些手势可在您的 Mac 上使用。

触控板手势要求使用妙控板或内建的多点触控触控板。如果您的触控板支持力度触控,您还可以进行“用力点按”操作并获得触感反馈

轻点来点按
用单指轻点来进行点按。

辅助点按(右键点按)
用双指点按或轻点。

智能缩放
用双指轻点两下可放大网页或 PDF,或缩小回原来的大小。

滚动
双指向上或向下滑动可滚动。1

放大或缩小
双指捏合或张开可放大或缩小。

旋转
双指互相以对方为中心移动,可旋转照片或其他项目。

在页面之间轻扫
双指向左或向右轻扫,可显示上一页或下一页。

打开“通知中心”
用双指从右边缘向左轻扫,可显示“通知中心”。

三指拖移
用三根手指拖移屏幕上的项目,然后点按或轻点以放下。可在“辅助功能”偏好设置中开启此功能2

 

查找和数据检测器
用三根手指轻点可查找字词,或者对日期、地址、电话号码和其他数据采取相关操作。

显示桌面
将拇指和另外三根手指同时展开,可显示桌面。

 

“启动台”
将拇指和另外三根手指合拢到一起,可显示“启动台”。

“调度中心”
用四根手指向上轻扫3,可打开“调度中心”。

应用 Exposé
用四根手指向下轻扫3,可查看正在使用的应用的所有窗口。

在全屏应用之间轻扫
用四根手指向左或向右轻扫3,可在桌面与全屏应用之间移动。

 

鼠标手势

有关这些手势的更多信息,请选取苹果菜单 () >“系统偏好设置”,然后点按“鼠标”。您可以从中关闭某个手势,更改手势类型,以及了解哪些手势可在您的 Mac 上使用。鼠标手势要求使用妙控鼠标

辅助点按(右键点按)
点按鼠标的右侧。

滚动
单指向上或向下滑动可滚动。1

智能缩放
用单指轻点两下可放大网页或 PDF,或缩小回原来的大小。

“调度中心”
用双指轻点两下,可打开“调度中心”。

在全屏应用之间轻扫
用双指向左或向右轻扫,可在桌面与全屏应用之间移动。

在页面之间轻扫
用单指向左或向右轻扫,可显示上一页或下一页。

1. 您可以在“辅助功能”偏好设置中关闭触控板滚动功能:选取苹果菜单 >“系统偏好设置”,然后点按“辅助功能”。在“鼠标与触控板”部分中,点按“触控板选项”,然后取消选择“滚动”复选框。

2.“辅助功能”偏好设置还包含单指拖移的选项:选取苹果菜单 >“系统偏好设置”,然后点按“辅助功能”。在“鼠标与触控板”部分中,点按“触控板选项”。选择“启用拖移”,然后从弹出式菜单中选取一个“拖移锁定”选项。点按问号按钮可了解有关每个选项的更多信息。

3. 在某些版本的 macOS中,这个手势使用的是三根手指,而不是四根。

 

转自苹果官网:https://support.apple.com/zh-cn/HT204895

PendingIntent的基本理解和使用

 android  PendingIntent的基本理解和使用已关闭评论
7月 032020
 

简书上看到的一篇PendingIntent的文章,收藏起来,链接:https://www.jianshu.com/p/a37f0ce2da2e

 

PendingIntent可以看作是对Intent的一个封装,但它不是立刻执行某个行为,而是满足某些条件或触发某些事件后才执行指定的行为(启动特定Service,Activity,BrcastReceive)。

我们可以把Pending Intent交给其他程序,其他程序按照PendingIntent进行操作。

Alarm定时器与Notification通知中都使用了PendingIntent

1.获得PendingIntent类内部静态方法获得PendingIntent实例:

//获得一个用于启动特定Activity的PendingIntent

public static PendingIntent getActivity(Context context, int requestCode,Intent intent, int flags)

//获得一个用于启动特定Service的PendingIntent

public static PendingIntent getService(Context context, int requestCode,Intent intent, int flags)

//获得一个用于发送特定Broadcast的PendingIntent

public static PendingIntent getBroadcast(Context context, int requestCode,Intent intent, int flags)

参数说明:

context:上下文对象。

requstCode:请求码,发件人的私人请求代码(当前未使用)。

intent:请求意图。用于要指明要启动的类以及数据的传递;

flags:这是一个关键的标志位:

主要常量

FLAG_CANCEL_CURRENT:如果当前系统中已经存在一个相同的PendingIntent对象,那么就将先将已有的PendingIntent取消,然后重新生成一个PendingIntent对象。

FLAG_NO_CREATE:如果当前系统中不存在相同的PendingIntent对象,系统将不会创建该PendingIntent对象而是直接返回null。

FLAG_ONE_SHOT:该PendingIntent只作用一次。在该PendingIntent对象通过send()方法触发过后,PendingIntent将自动调用cancel()进行销毁,那么如果你再调用send()方法的话,系统将会返回一个SendIntentException。

FLAG_UPDATE_CURRENT:如果系统中有一个和你描述的PendingIntent对等的PendingInent,那么系统将使用该PendingIntent对象,但是会使用新的Intent来更新之前PendingIntent中的Intent对象数据,例如更新Intent中的Extras。

注意:两个PendingIntent对等是指它们的operation一样, 且其它们的Intent的action, data, categories, components和flags都一样。但是它们的Intent的Extra可以不一样。

 

Android下使用Picasso图片加载库详解

 android  Android下使用Picasso图片加载库详解已关闭评论
7月 012020
 

网上看到的一篇关于Android下 使用 Picasso 图片加载库的好文章,虽然有点老,但还是值得分享 !来自:https://www.jianshu.com/p/c68a3b9ca07a

写在前面

Android 中有几个比较有名的图片加载框架,Universal ImageLoader、Picasso、Glide和Fresco。它们各有优点,以前一直用的是ImageLoader 做项目中的图片加载,由于作者宣布ImageLoader 不会在更新了,因此新的项目打算换一个图片加载框架-Picasso, Picasso 是Square 公司开源的Android 端的图片加载和缓存框架。Square 真是一家良心公司啊,为我们Android开发者贡献了很多优秀的开源项目有木有!像什么Rerefoit 、OkHttp、LeakCanary、Picasso等等都是非常火的开源项目。扯远了,回到正题,除了使用简单方便,Picasso还能自动帮我们做以下事情:

  • 处理Adapter 中ImageView的回收和取消下载。
  • 使用最小的内存 来做复杂的图片变换。比如高斯模糊,圆角、圆形等处理。
  • 自动帮我们缓存图片。内存和磁盘缓存。

以上只是列出了Picasso 比较核心的几点,其实它的优点远远不止这些,接下来就看一下如何使用Picasso。

Picasso-Android.png

本文目录

0,添加依赖
1, 加载显示图片
2,Placeholder & noPlaceholder & noFade
3, 设置图片尺寸(Resize)、缩放(Scale)和裁剪(Crop)
4,图片旋转Rotation()
5, 转换器Transformation
6,请求优先级
7,Tag管理请求
8,同步/异步加载图片
9,缓存(Disk 和 Memory)
10,Debug 和日志
11,Picasso 扩展

正文

0. 添加依赖

要使用Picasso,首先我们要添加版本依赖,去官网或者Github 看一下当前的最新版本(截止本文最新版本为2.5.2),然后在build.gradle中添加依赖:

   compile 'com.squareup.picasso:picasso:2.5.2'

1. 加载显示图片

将Picasso添加到项目之后,我们就可以用它来加载图片了,使用方法非常简单:

 Picasso.with(this)
        .load("http://ww3.sinaimg.cn/large/610dc034jw1fasakfvqe1j20u00mhgn2.jpg")
        .into(mImageView);

只需要一行代码就完成了加载图片到显示的整个过程,链式调用,非常简洁,其实有三步,一次调用了三个方法:

  • with(Context) 获取一个Picasso单例,参数是一个Context上下文
  • load(String) 调用load 方法加载图片
  • into (ImageView) 将图片显示在对应的View上,可以是ImageView,也可以是实现了Target j接口的自定义View。

上面演示了加载一张网络图片,它还支持其它形式的图片加载,加载文件图片,加载本地资源图片,加载一个Uri 路径给的图片,提供了几个重载的方法:

1, load(Uri uri) 加载一个以Uri路径给的图片

Uri uri = Uri.parse(ANDROID_RESOURCE + context.getPackageName() + FOREWARD_SLASH + resourceId)

Picasso.with(this).load(uri).into(mImageView);

** 2,load(File file) 加载File中的图片**

 Picasso.with(this).load(file).into(mImageView);

3, load(int resourceId) 加载本地资源图片

Picasso.with(this).load(R.mipmap.ic_launcher).into(mImageView);

提醒:上面介绍了load的几个重载方法,加载不同资源的图片,另外提醒注意一下load(String path)接受String 参数的这个方法,参数String 可以是一个网络图片url,也可以是file 路径、content资源 和Android Resource。看一下源码的注释。

/**
   * Start an image request using the specified path. This is a convenience method for calling
   * {@link #load(Uri)}.
   * <p>
   * This path may be a remote URL, file resource (prefixed with {@code file:}), content resource
   * (prefixed with {@code content:}), or android resource (prefixed with {@code
   * android.resource:}.
   * <p>
   * Passing {@code null} as a {@code path} will not trigger any request but will set a
   * placeholder, if one is specified.
   *
   * @see #load(Uri)
   * @see #load(File)
   * @see #load(int)
   * @throws IllegalArgumentException if {@code path} is empty or blank string.
   */
  public RequestCreator load(String path) {
    if (path == null) {
      return new RequestCreator(this, null, 0);
    }
    if (path.trim().length() == 0) {
      throw new IllegalArgumentException("Path must not be empty.");
    }
    return load(Uri.parse(path));
  }

要使用string 参数加载上面的几种资源,除了网络url,其它几种需要加上对应前缀,file文件路径前缀:file: , content 添加前缀:content: ,Android Resource 添加:android.resource:

2. placeholder& error & noPlaceholder & noFade

通过上面的第一步我们就可以通过Picasso 加载图片了,我们的项目中通常最常用的就是加载网络图片,但是由于网络环境的差异,有时侯加载网络图片的过程有点慢,这样界面上就会显示空ImageView什么也看不见,用户体验非常不好。其实以前用过ImageLoader的同学都知道,ImageLoader 是可以设置加载中显示默认图片的,Picasso当然也给我们提供了这个功能,这就是我们要说的placeholder(占位图)。

1,placeholder
placeholder提供一张在网络请求还没有完成时显示的图片,它必须是本地图片,代码如下:

 Picasso.with(this).load(URL)
                .placeholder(R.drawable.default_bg)
                .into(mImageView);

设置placeholder之后,在加载图片的时候,就可以显示设置的默认图了,提升用户体验。
2, error
和placeholder 的用法一样,error 提供一张在加载图片出错的情况下显示的默认图


        Picasso.with(this).load(URL)
                .placeholder(R.drawable.default_bg)
                .error(R.drawable.error_iamge)
                .into(mImageView);

3,noPlaceholder
这个方法的意思就是:在调用into的时候明确告诉你没有占位图设置。根据这个方法签名的解释是阻止View被回收的时候Picasso清空target或者设置一个应用的占位图。需要注意的是placeholder和noPlaceholder 不能同时应用在同一个请求上,会抛异常。


        Picasso.with(this).load(URL)
                .noPlaceholder()
                .error(R.drawable.error_iamge)
                .into(mImageView);

4,noFade
无论你是否设置了占位图,Picasso 从磁盘或者网络加载图片时,into 显示到ImageView 都会有一个简单的渐入过度效果,让你的UI视觉效果更柔顺丝滑一点,如果你不要这个渐入的效果(没有这么坑爹的需求吧!!!),就调用noFade方法。

 Picasso.with(this).load(URL)
                .placeholder(R.drawable.default_bg)
                .error(R.drawable.error_iamge)
                .noFade()
                .into(mImageView);

3. 设置图片尺寸(Resize)、缩放(Scale)和裁剪(Crop)

1, Resize(int w,int h)
在项目中,为了带宽、内存使用和下载速度等考虑,服务端给我们的图片的size 应该和我们View 实际的size一样的,但是实际情况并非如此,服务端可能给我们一些奇怪的尺寸的图片,我们可以使用resize(int w,int hei) 来重新设置尺寸。

 Picasso.with(this).load(URL)
                .placeholder(R.drawable.default_bg)
                .error(R.drawable.error_iamge)
                .resize(400,200)
                .into(mImageView);

resize()方法接受的参数的单位是pixels,还有一个可以设置dp单位的方法,将你的尺寸写在dimens.xml文件中,然后用resizeDimen(int targetWidthResId, int targetHeightResId)方法

 <dimen name="image_width">300dp</dimen>
 <dimen name="image_height">200dp</dimen>

 Picasso.with(this).load(URL)
                .placeholder(R.drawable.default_bg)
                .error(R.drawable.error_iamge)
                .resizeDimen(R.dimen.image_width,R.dimen.image_height)
                .into(mImageView);

2,onlyScaleDown
当调用了resize 方法重新设置图片尺寸的时候,调用onlyScaleDown 方法,只有当原始图片的尺寸大于我们指定的尺寸时,resize才起作用,如:

  Picasso.with(this).load(URL)
                .placeholder(R.drawable.default_bg)
                .error(R.drawable.error_iamge)
                .resize(4000,2000)
                .onlyScaleDown()
                .into(mImageView);

只有当原来的图片尺寸大于4000 x 2000的时候,resize 才起作用。
3,图片裁剪 centerCrop()
这个属性应该不陌生吧!ImageView 的ScaleType 就有这个属性。当我们使用resize 来重新设置图片的尺寸的时候,你会发现有些图片拉伸或者扭曲了(使用ImageView的时候碰到过吧),我要避免这种情况,Picasso 同样给我们提供了一个方法,centerCrop,充满ImageView 的边界,居中裁剪

 Picasso.with(this).load(URL)
                .placeholder(R.drawable.default_bg)
                .error(R.drawable.error_iamge)
                .resize(400,200)
                .centerCrop()
                .into(mImageView);

4,centerInside
上面的centerCrop是可能看不到全部图片的,如果你想让View将图片展示完全,可以用centerInside,但是如果图片尺寸小于View尺寸的话,是不能充满View边界的。

 Picasso.with(this).load(URL)
                .placeholder(R.drawable.default_bg)
                .error(R.drawable.error_iamge)
                .resize(400,200)
                .centerInside()
                .into(mImageView);

5,fit
fit 是干什的呢?上面我们需要用resize()来指定我们需要的图片的尺寸,那就是说在程序中需要我们计算我们需要的尺寸(固定大小的除外),这样很麻烦,fit 方法就帮我们解决了这个问题。fit 它会自动测量我们的View的大小,然后内部调用reszie方法把图片裁剪到View的大小,这就帮我们做了计算size和调用resize 这2步。非常方便。代码如下:

Picasso.with(this).load(URL)
                .placeholder(R.drawable.default_bg)
                .error(R.drawable.error_iamge)
                .fit()
                .into(mImageView);

使用fit 还是会出现拉伸扭曲的情况,因此最好配合前面的centerCrop使用,代码如下:

 Picasso.with(this).load(URL)
                .placeholder(R.drawable.default_bg)
                .error(R.drawable.error_iamge)
                .fit()
                .centerCrop()
                .into(mImageView);

看一下对比图:
fit(会拉伸):

image_fit.png

fit & centerCrop (不会拉伸):

fit_centerCrop.png

注意:特别注意,
1,fit 只对ImageView 有效
2,使用fit时,ImageView 宽和高不能为wrap_content,很好理解,因为它要测量宽高。

4. 图片旋转Rotation()

在图片显示到ImageView 之前,还可以对图片做一些旋转操作,调用rotate(int degree)方法

Picasso.with(this).load(URL)
                .placeholder(R.drawable.default_bg)
                .error(R.drawable.error_iamge)
                .rotate(180)
                .into(mImageView);

这个方法它是以(0,0)点旋转,但是有些时候我们并不想以(0,0)点旋转,还提供了另外一个方法可以指定原点:

  • rotate(float degrees, float pivotX, float pivotY) 以(pivotX, pivotY)为原点旋转

 Picasso.with(this).load(URL)
                .placeholder(R.drawable.default_bg)
                .error(R.drawable.error_iamge)
                .rotate(180,200,100)
                .into(mImageView);

5. 转换器Transformation

Transformation 这就是Picasso的一个非常强大的功能了,它允许你在load图片 -> into ImageView 中间这个过成对图片做一系列的变换。比如你要做图片高斯模糊、添加圆角、做度灰处理、圆形图片等等都可以通过Transformation来完成。

来看一个高斯模糊的例子:

1,首先定义一个转换器继承 Transformation

 public static class BlurTransformation implements Transformation{

        RenderScript rs;

        public BlurTransformation(Context context) {
            super();
            rs = RenderScript.create(context);
        }

        @Override
        public Bitmap transform(Bitmap bitmap) {
            // Create another bitmap that will hold the results of the filter.
            Bitmap blurredBitmap = bitmap.copy(Bitmap.Config.ARGB_8888, true);

            // Allocate memory for Renderscript to work with
            Allocation input = Allocation.createFromBitmap(rs, blurredBitmap, Allocation.MipmapControl.MIPMAP_FULL, Allocation.USAGE_SHARED);
            Allocation output = Allocation.createTyped(rs, input.getType());

            // Load up an instance of the specific script that we want to use.
            ScriptIntrinsicBlur script = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs));
            script.setInput(input);

            // Set the blur radius
            script.setRadius(25);

            // Start the ScriptIntrinisicBlur
            script.forEach(output);

            // Copy the output to the blurred bitmap
            output.copyTo(blurredBitmap);

            bitmap.recycle();

            return blurredBitmap;
        }

        @Override
        public String key() {
            return "blur";
        }
    }

2, 加载图片的时候,在into 方法前面调用 transform方法 应用Transformation

  Picasso.with(this).load(URL)
                .placeholder(R.drawable.default_bg)
                .error(R.drawable.error_iamge)
                .transform(new BlurTransformation(this))
                .into(mBlurImage);

看一下效果图:

transformation.png

上面为原图,下面为高斯模糊图

是不是很强大,任何复杂的变换都可以通过Transformation 来做。

还不止于此,还有更强大的功能。可以在一个请求上应用多个Transformation

比如:我想先做个度灰处理然后在做一个高斯模糊图:

1, 度灰的Transformation

 public static class GrayTransformation implements Transformation{

        @Override
        public Bitmap transform(Bitmap source) {
            int width, height;
            height = source.getHeight();
            width = source.getWidth();

            Bitmap bmpGrayscale = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);
            Canvas c = new Canvas(bmpGrayscale);
            Paint paint = new Paint();
            ColorMatrix cm = new ColorMatrix();
            cm.setSaturation(0);
            ColorMatrixColorFilter f = new ColorMatrixColorFilter(cm);
            paint.setColorFilter(f);
            c.drawBitmap(source, 0, 0, paint);

            if(source!=null && source!=bmpGrayscale){
                source.recycle();
            }
            return bmpGrayscale;
        }

        @Override
        public String key() {
            return "gray";
        }
    }

2, 如果是多个Transformation操作,有2种方式应用
方式一:直接调用多次transform 方法,不会覆盖的。它只是保存到了一个List 里面

 Picasso.with(this).load(URL)
                .placeholder(R.drawable.default_bg)
                .error(R.drawable.error_iamge)
                .fit()
                .centerCrop()
                .transform(new GrayTransformation())//度灰处理
                .transform(new BlurTransformation(this))//高斯模糊
                .into(mBlurImage);

需要注意调用的顺序
方式二:接受一个List,将Transformation 放大list 里

        List<Transformation> transformations = new ArrayList<>();
        transformations.add(new GrayTransformation());
        transformations.add(new BlurTransformation(this));

        Picasso.with(this).load(URL)
                .placeholder(R.drawable.default_bg)
                .error(R.drawable.error_iamge)
                .fit()
                .centerCrop()
                .transform(transformations)
                .into(mBlurImage);

效果图:

gray_blur.png

如上图,第一张为度灰操作,第二张为 度灰+高斯模糊

另外发现了一个开源库,专门写了很多好玩的Transformation,有兴趣的可以看一下:
picasso-transformations

6. 请求优先级

Picasso 为请求设置有优先级,有三种优先级,LOW、NORMAL、HIGH。默认情况下都是NORMAL,除了调用fetch 方法,fetch 方法的优先级是LOW。

public enum Priority {
    LOW,
    NORMAL,
    HIGH
  }

可以通过priority方法设置请求的优先级,这会影响请求的执行顺序,但是这是不能保证的,它只会往高的优先级靠拢。代码如下:

 Picasso.with(this).load(URL)
                .placeholder(R.drawable.default_bg)
                .error(R.drawable.error_iamge)
                .priority(Picasso.Priority.HIGH)
               // .priority(Picasso.Priority.LOW)
                .into(mImageView);

7. Tag管理请求

Picasso 允许我们为一个请求设置tag来管理请求,看一下对应的几个方法:
下面3个方法是Picasso这个类的:

  • cancelTag(Object tag) 取消设置了给定tag的所有请求
  • pauseTag(Object tag) 暂停设置了给定tag 的所有请求
  • resumeTag(Object tag) resume 被暂停的给定tag的所有请求

还有一个方法是RequestCreator的:

  • tag(Object tag) 为请求设置tag

几个方法的意思也很明确,就是我们可以暂停、resume、和取消请求,可以用在哪些场景呢?

场景一: 比如一个照片流列表,当我们快速滑动列表浏览照片的时候,后台会一直发起请求加载照片的,这可能会导致卡顿,那么我们就可以为每个请求设置一个相同的Tag,在快速滑动的时候,调用pauseTag暂停请求,当滑动停止的时候,调用resumeTag恢复请求,这样的体验是不是就会更好一些呢。

Adapter中添加如下代码:

Picasso.with(this).load(mData.get(position))
                .placeholder(R.drawable.default_bg)
                .error(R.drawable.error_iamge)
                .tag("PhotoTag")
                .into(holder.mImageView);

Activity中为RecyclerView添加滑动监听:

mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                final Picasso picasso = Picasso.with(MainActivity.this);

                if (newState == SCROLL_STATE_IDLE) {
                    picasso.resumeTag("PhotoTag");
                } else {
                    picasso.pauseTag("PhotoTag");
                }
            }
        });

场景二: 比如一个照片流列表界面,在弱网环境下,加载很慢,退出这个界面时可能会有很多请求没有完成,这个时候我们就可 以通过tag 来取消请求了。

 @Override
    protected void onDestroy() {
        super.onDestroy();
        Picasso.with(this).cancelTag("PhotoTag");
    }

8. 同步/异步加载图片

Picasso 加载图片也有同步/异步两种方式
**1,get() 同步 **
很简单,同步加载使用get() 方法,返回一个Bitmap 对象,代码如下:

 try {
           Bitmap bitmap =  Picasso.with(this).load(URL).get();
        } catch (IOException e) {
            e.printStackTrace();
        }

注意:使用同步方式加载,不能放在主线程来做。

2,异步的方式加载图片,fetch()
一般直接加载图片通过into显示到ImageView 是异步的方式,除此之外,还提供了2个异步的方法:

  • fetch() 异步方式加载图片
  • fetch(Callback callback) 异步方式加载图片并给一个回调接口。

  Picasso.with(this).load(URL).fetch(new Callback() {
            @Override
            public void onSuccess() {
                //加载成功
            }

            @Override
            public void onError() {
              //加载失败
            }
        });

这里就要吐槽一下接口设计了,回调并没有返回Bitmap, 不知道作者是怎么考虑的,只是一个通知效果,知道请求失败还是成功。
**fetch 方法异步加载图片并没有返回Bitmap,这个方法在请求成功之后,将结果存到了缓存,包括磁盘和内存缓存。所以使用这种方式加载图片适用于这种场景:知道稍后会加载图片,使用fetch 先加载缓存,起到一个预加载的效果。 **

9. 缓存(Disk 和 Memory)

Picasso 有内存缓存(Memory)和磁盘缓存( Disk), 首先来看一下源码中对于缓存的介绍:

  • LRU memory cache of 15% the available application RAM
  • Disk cache of 2% storage space up to 50MB but no less than 5MB. (Note: this is only
    available on API 14+ <em>or</em> if you are using a standalone library that provides a disk cache on all API levels like OkHttp)
  • Three download threads for disk and network access.

可以看出,内存缓存是使用的LRU 策略的缓存实现,它的大小是内存大小的15%,可以自定义它的大小,最后在扩展那一章节再讲,磁盘缓存是磁盘容量的2%但是不超过50M,不少于5M。处理一个请求的时候,按照这个顺讯检查:memory->disk->network 。先检查有木有内存缓存,如果命中,直接返回结果,否则检查磁盘缓存,命中则返回结果,没有命中则从网上获取。

默认情况下,Picasso 内存缓存和磁盘缓存都开启了的,也就是加载图片的时候,内存和磁盘都缓存了,但是有些时候,我们并不需要缓存,比如说:加载一张大图片的时候,如果再内存中保存一份,很容易造成OOM,这时候我们只希望有磁盘缓存,而不希望缓存到内存,因此就需要我们设置缓存策略了。Picasso 提供了这样的方法。

1,memoryPolicy 设置内存缓存策略
就像上面所说的,有时候我们不希望有内存缓存,我们可以通过 memoryPolicy 来设置。MemoryPolicy是一个枚举,有两个值

NO_CACHE:表示处理请求的时候跳过检查内存缓存
**NO_STORE: ** 表示请求成功之后,不将最终的结果存到内存。

示例代码如下:

with(this).load(URL)
                .placeholder(R.drawable.default_bg)
                .error(R.drawable.error_iamge)
                .memoryPolicy(MemoryPolicy.NO_CACHE,MemoryPolicy.NO_STORE) //静止内存缓存
                .into(mBlurImage);

2,networkPolicy 设置磁盘缓存策略
和内存缓存一样,加载一张图片的时候,你也可以跳过磁盘缓存,和内存缓存策略的控制方式一样,磁盘缓存调用方法networkPolicy(NetworkPolicy policy, NetworkPolicy... additional) , NetworkPolicy是一个枚举类型,有三个值:

NO_CACHE: 表示处理请求的时候跳过处理磁盘缓存
** NO_STORE:** 表示请求成功后,不将结果缓存到Disk,但是这个只对OkHttp有效。
OFFLINE: 这个就跟 上面两个不一样了,如果networkPolicy方法用的是这个参数,那么Picasso会强制这次请求从缓存中获取结果,不会发起网络请求,不管缓存中能否获取到结果。

使用示例:

 with(this).load(URL)
                .placeholder(R.drawable.default_bg)
                .error(R.drawable.error_iamge)
                .memoryPolicy(MemoryPolicy.NO_CACHE,MemoryPolicy.NO_STORE)//跳过内存缓存
                .networkPolicy(NetworkPolicy.NO_CACHE)//跳过磁盘缓存
                .into(mBlurImage);

强制从缓存获取:

 with(this).load(URL)
                .placeholder(R.drawable.default_bg)
                .error(R.drawable.error_iamge)
                .networkPolicy(NetworkPolicy.OFFLINE)//强制从缓存获取结果
                .into(mBlurImage);

10. Debug 和日志

1,缓存指示器

上一节说了,Picasso 有内存缓存和磁盘缓存,先从内存获取,没有再去磁盘缓存获取,都有就从网络加载,网络加载是比较昂贵和耗时的。因此,作为一个开发者,我们往往需要加载的图片是从哪儿来的(内存、Disk还是网络),Picasso让我们很容易就实现了。只需要调用一个方法setIndicatorsEnabled(boolean)就可以了,它会在图片的左上角出现一个带色块的三角形标示,有3种颜色,绿色表示从内存加载、蓝色表示从磁盘加载、红色表示从网络加载。

 Picasso.with(this)
        .setIndicatorsEnabled(true);//显示指示器

效果图:

cache_indicator.png

如上图所示,第一张图从网络获取,第二张从磁盘获取,第三张图从内存获取。

看一下源码中定义指示器的颜色:

 /** Describes where the image was loaded from. */
  public enum LoadedFrom {
    MEMORY(Color.GREEN),
    DISK(Color.BLUE),
    NETWORK(Color.RED);

    final int debugColor;

    private LoadedFrom(int debugColor) {
      this.debugColor = debugColor;
    }
  }

可以很清楚看出,对应三种颜色代表着图片的来源。

2,日志
上面的指示器能够很好的帮助我们看出图片的来源,但是有时候我们需要更详细的信息,Picasso,可以打印一些日志,比如一些关键方法的执行时间等等,我们只需要调用setLoggingEnabled(true)方法,然后App在加载图片的过程中,我们就可以从logcat 看到一些关键的日志信息。

   Picasso.with(this)
          .setLoggingEnabled(true);//开启日志打印

11. Picasso 扩展

到目前为止,Picasso的基本使用已经讲得差不多了,但是在实际项目中我们这可能还满足不了我们的需求,我们需要对它做一些自己的扩展,比如我们需要换缓存的位置、我们需要扩大缓存、自定义线程池、自定义下载器等等。这些都是可以的,接下来我们来看一下可以做哪些方面的扩展。

1,用Builder 自己构造一个Picasso Instance
我们来回顾一下前面是怎么用Picasso 加载图片的:

Picasso.with(this)
       .load("http://ww3.sinaimg.cn/large/610dc034jw1fasakfvqe1j20u00mhgn2.jpg")
       .into(mImageView);

总共3步:
1,用with方法获取一个Picasso 示例
2,用load方法加载图片
3,用into 放法显示图片

首先Picasso是一个单例模式,我们每一次获取的示例都是默认提供给我们的实例。但是也可以不用它给的Instance,我们直接用builder来构造一个Picasso:

       Picasso.Builder builder = new Picasso.Builder(this);
        //构造一个Picasso
        Picasso picasso = builder.build();
        //加载图片
        picasso.load(URL)
                .into(mImageView);

这样我们就构造了一个局部的Picasso实例,当然了,我们直接用new 了一个builder,然后build()生成了一个Picasso。这跟默认的通过with方法获取的实例是一样的。那么现在我们就可以配置一些自定义的功能了。
2, 配置自定义下载器 downLoader
如果我们不想用默认提供的Downloader,那么我们可以自定义一个下载器然后配置进去。举个例子:

(1) 先自定义一个Downloader(只是举个例子,并没有实现):

/**
 * Created by zhouwei on 17/2/26.
 */

public class CustomDownloader implements Downloader {

    @Override
    public Response load(Uri uri, int networkPolicy) throws IOException {
        return null;
    }

    @Override
    public void shutdown() {

    }
}

(2) 然后通过builder 配置:

        //配置下载器
        builder.downloader(new CustomDownloader());
        //构造一个Picasso
        Picasso picasso = builder.build();

这样配置后,我们用build()生成的Picasso 实例来加载图片就会使用自定义的下载器来下载图片了。

** 3, 配置缓存**
前面说过,内存缓存是用的LRU Cahce ,大小是手机内存的15% ,如果你想缓存大小更大一点或者更小一点,可以自定义,然后配置。

        //配置缓存
        LruCache cache = new LruCache(5*1024*1024);// 设置缓存大小
        builder.memoryCache(cache);

上面只是一个简单的举例,当然了你可以自定义,也可以使用LRUCache,改变大小,改变存储路径等等。

提示: 很遗憾,好像没有提供改变磁盘缓存的接口,那就只能用默认的了。

4, 配置线程池
Picasso 默认的线程池的核心线程数为3,如果你觉得不够用的话,可以配置自己需要的线程池,举个列子:

        //配置线程池
        ExecutorService executorService = Executors.newFixedThreadPool(8);
        builder.executor(executorService);

**5, 配置全局的 Picasso Instance **
上面说的这些自定义配置项目都是应用在一个局部的Picasso instance 上的,我们不可能每一次使用都要重新配置一下,这样就太麻烦了。我们希望我们的这些自定义配置能在整个项目都应用上,并且只配置一次。其实Picasso 给我们提供了这样的方法。可以调用setSingletonInstance(Picasso picasso)就可以了,看一下这个方法的源码:

 /**
   * Set the global instance returned from {@link #with}.
   * <p>
   * This method must be called before any calls to {@link #with} and may only be called once.
   */
  public static void setSingletonInstance(Picasso picasso) {
    synchronized (Picasso.class) {
      if (singleton != null) {
        throw new IllegalStateException("Singleton instance already exists.");
      }
      singleton = picasso;
    }
  }

设置一个通过with方法返回的全局instance。我们只希望配置一次,所以,我们应该在Application 的onCreate方法中做全局配置就可以了。app一启动就配置好,然后直接和前面的使用方法一样,调用with方法获取Picasso instance 加载图片就OK了。

因此在Application 中添加如下代码:

 @Override
    public void onCreate() {
        super.onCreate();
        
        // 配置全局的Picasso instance 
        
        Picasso.Builder builder = new Picasso.Builder(this);
        //配置下载器
        builder.downloader(new CustomDownloader());
        //配置缓存
        LruCache cache = new LruCache(5*1024*1024);// 设置缓存大小
        builder.memoryCache(cache);
        //配置线程池
        ExecutorService executorService = Executors.newFixedThreadPool(8);
        builder.executor(executorService);

        //构造一个Picasso
        Picasso picasso = builder.build();
        // 设置全局单列instance 
        Picasso.setSingletonInstance(picasso);
        
        
    }

然后应用这些自定义配置加载图片

Picasso.with(this).load("http://ww3.sinaimg.cn/large/610dc034jw1fasakfvqe1j20u00mhgn2.jpg").into(mImageView);

用法和以前的一样,但是我们已经将我们的自定义配置应用上了。

结尾

以上就是对Picasso 用法的全部总结,如有什么问题,欢迎留言指正。Picasso真的是一个强大的图片加载缓存库,API 简单好用,而且是链式调用的(这点我特别喜欢)。官方文档写的比较简单,很多用法都要看源码和注释才知道。希望本文能给才开始使用Picasso 的同学一点帮助。

参考

Picasso官网
Picasso — Getting Started

springboot jar包解压、重新压缩打包方法

 jar, spring-boot  springboot jar包解压、重新压缩打包方法已关闭评论
5月 112020
 

springboot在产生jar包后,有时需要修改其中的文件并重新打包,比如上传到服务器后,发现配置文件里某个配置没改,可以使用下面的方法:

以一个测试springboot 包  spingboot-ouu-test.jar 为例

因为有重新打包的需求,建议jar包放在一个空的文件夹下执行下面的命令:

  • 解压spingboot-ouu-test.jar

jar -xvf spingboot-ouu-test.jar

常见的配置文件如:application.properties在BOOT-INF/classes/目录下.

  • 修改文件后, 重新打包spingboot-ouu-test.jar,

jar  -cvfM0 spingboot-ouu-test.jar .           //(M后面是数字0,如果只使用参数cf,java -jar spingboot-ouu-test.jar启动springboot jar项目时将提示“没有主清单属性”)

 

附:

jar -tvf spingboot-ouu-test.jar   //查看jar包文件结构

jar命令常用参数:

c :创建一个 jar 包
t :显示 jar 包里面的内容
x :解压 jar 包
u :添加文件到 jar包
f :指定 jar 包的文件名
v :在 CMD 显示详细执行过程(报告)
m :指定 manufest.mf 文件(该文件可以对jar包及其内容做设置)
0 :打包 jar包 时不压缩
M :不产生 manufest.mf 文件,覆盖 m 参数的设置
i :为打包的 jar包 创建索引文件
c :进入某目录后再执行 jar 命令,相当于 cd 进入目录然后不带 c 参数执行 jar命令

 

Linux里使用shell中的$(( ))、$( )、“与${ }及区别

 linux, shell  Linux里使用shell中的$(( ))、$( )、“与${ }及区别已关闭评论
4月 072020
 
转一篇linux下关于“$(( ))、$( )、“与${ }”使用及区别的好文章。来自:https://blog.csdn.net/number_0_0/article/details/73291182
命令替换

在bash中,$( )` `(反引号)都是用来作命令替换的。
命令替换与变量替换差不多,都是用来重组命令行的,先完成引号里的命令行,然后将其结果替换出来,再重组成新的命令行。

exp 1

  1. $ echo today is $(date “+%Y-%m-%d”)
  2. today is 2014-07-01
$( )与``

在操作上,这两者都是达到相应的效果,但是建议使用$( ),理由如下:

  • ``很容易与”搞混乱,尤其对初学者来说。
  • 在多层次的复合替换中,``必须要额外的跳脱处理(反斜线),而$( )比较直观。
  • 最后,$( )的弊端是,并不是所有的类unix系统都支持这种方式,但反引号是肯定支持的。

exp 2

  1. # 将cmd1执行结果作为cmd2参数,再将cmd2结果作为cmd3的参数
  2. cmd3 $(cmd2 $(cmd1))
  3. # 如果是用反引号,直接引用是不行的,还需要作跳脱处理
  4. cmd3 `cmd2 \`cmd1\“
${ }变量替换

一般情况下,$var${var}是没有区别的,但是用${ }会比较精确的界定变量名称的范围

  1. $ A=B
  2. $ echo ${A}B
  3. BB

取路径、文件名、后缀
先赋值一个变量为一个路径,如下:
file=/dir1/dir2/dir3/my.file.txt

命令 解释 结果
${file#*/} 拿掉第一条 / 及其左边的字符串 dir1/dir2/dir3/my.file.txt
${file##*/} 拿掉最后一条 / 及其左边的字符串 my.file.txt
${file#*.} 拿掉第一个 . 及其左边的字符串 file.txt
${file##*.} 拿掉最后一个 . 及其左边的字符串 txt
${file%/*} 拿掉最后一条 / 及其右边的字符串 /dir1/dir2/dir3
${file%%/*} 拿掉第一条 / 及其右边的字符串 (空值)
${file%.*} 拿掉最后一个 . 及其右边的字符串 /dir1/dir2/dir3/my.file
${file%%.*} 拿掉第一个 . 及其右边的字符串 /dir1/dir2/dir3/my

记忆方法如下:

  • # 是去掉左边(在键盘上 # 在 $ 之左边)
  • % 是去掉右边(在键盘上 % 在 $ 之右边)
  • 单一符号是最小匹配;两个符号是最大匹配
  • *是用来匹配不要的字符,也就是想要去掉的那部分
  • 还有指定字符分隔号,与*配合,决定取哪部分

取子串及替换

命令 解释 结果
${file:0:5} 提取最左边的 5 个字节 /dir1
${file:5:5} 提取第 5 个字节右边的连续 5 个字节 /dir2
${file/dir/path} 将第一个 dir 提换为 path /path1/dir2/dir3/my.file.txt
${file//dir/path} 将全部 dir 提换为 path /path1/path2/path3/my.file.txt
${#file} 获取变量长度 27

根据状态为变量赋值

命令 解释 备注
${file-my.file.txt} 若 $file 没设定,则使用 my.file.txt 作传回值 空值及非空值不作处理
${file:-my.file.txt} 若 $file 没有设定或为空值,则使用 my.file.txt 作传回值 非空值时不作处理
${file+my.file.txt} 若$file 设为空值或非空值,均使用my.file.txt作传回值 没设定时不作处理
${file:+my.file.txt} 若 $file 为非空值,则使用 my.file.txt 作传回值 没设定及空值不作处理
${file=txt} 若 $file 没设定,则回传 txt ,并将 $file 赋值为 txt 空值及非空值不作处理
${file:=txt} 若 $file 没设定或空值,则回传 txt ,将 $file 赋值为txt 非空值时不作处理
${file?my.file.txt} 若 $file 没设定,则将 my.file.txt 输出至 STDERR 空值及非空值不作处理
${file:?my.file.txt} 若 $file没设定或空值,则将my.file.txt输出至STDERR 非空值时不作处理

tips:
以上的理解在于, 你一定要分清楚 unset 与 null 及 non-null 这三种赋值状态. 一般而言, : 与 null 有关, 若不带 : 的话, null 不受影响, 若带 : 则连 null 也受影响.

数组

  1. A=“a b c def” # 定义字符串
  2. A=(a b c def) # 定义字符数组
命令 解释 结果
${A[@]} 返回数组全部元素 a b c def
${A[*]} 同上 a b c def
${A[0]} 返回数组第一个元素 a
${#A[@]} 返回数组元素总个数 4
${#A[*]} 同上 4
${#A[3]} 返回第四个元素的长度,即def的长度 3
A[3]=xyz 则是将第四个组数重新定义为 xyz
$(( ))与整数运算

bash中整数运算符号

符号 功能
+ – * / 分别为加、减、乘、除
% 余数运算
& | ^ ! 分别为“AND、OR、XOR、NOT”

在 $(( )) 中的变量名称,可于其前面加 $ 符号来替换,也可以不用。

  1. $ a=5;b=7;c=2
  2. $ echo $((a+b*c))
  3. 19
  4. $ echo $(($a+$b*$c))
  5. 19

进制转换
$(( ))可以将其他进制转成十进制数显示出来。用法如下:
echo $((N#xx))
其中,N为进制,xx为该进制下某个数值,命令执行后可以得到该进制数转成十进制后的值。

  1. $ echo $((2#110)) # 二进制转十进制
  2. 6
  3. $ echo $((16#2a)) # 十六进制转十进制
  4. 42
  5. $ echo $((8#11)) # 八进制转十进制
  6. 9

(( ))重定义变量值

  1. $ a=5;b=7
  2. $ ((a++));echo $a
  3. 6
  4. $ ((a–));echo $a
  5. 5
$ ((a<b));echo $? 0

使用(( ))作整数测试时,不要跟[ ]的整数测试搞混乱了。

swift下类和结构体的比较(相同点与不同点,什么情况下使用)

 swift  swift下类和结构体的比较(相同点与不同点,什么情况下使用)已关闭评论
3月 122020
 

网上看到的一篇好文章,清楚明了的解释了swift下类与结构体的相同点和不同点,并说明了该在什么情况下使用,推荐! 来自: https://juejin.im/post/5b57e75ef265da0f87593513

结构体是构建代码所用的一种通用且灵活的构造体。我们可以使用完全相同的语法规则来为类和结构体定义属性(变量,常量)和添加方法。从而扩展类和结构体的功能。

与其他编程语言所不同的是,Swift并不要求你为自定义类和结构去创建独立的接口和实现文件。你所要做的是在一个单一文件中定义一个类或者结构体,系统将会自动生成面向其他代码的外部接口。

注: 通常一个的势力被称为对象。在Swift中,类和结构体的关系要比在其他语言中更加密切,本章中所讨论的部分功能都可以在类和结构体上。因此主要使用实例。内容包含类和结构体对比结构体和枚举是值类型类是引用类型类和结构体的选择字符串、数组和字典类型的赋值和复制行为


类和结构体对比

Swift中的类和结构体有很多共同点:

  • 定义属性用于储存值
  • 定义方法用于提供功能
  • 定义下标操作通过下标语法可以访问它们的值
  • 定义构造器用于生成初始化值
  • 通过扩展以增加默认实现的功能
  • 遵循协议以提供某种标准功能

与结构体相比,类还有如下的附加功能:

  • 继承允许一个类继承另一个类的特征
  • 类型转换允许在运行时检查和解释一个类实例的类型
  • 析构器允许一个类实例释放任何其所被分配的资源
  • 引用计数允许对一个类的多次引用

注: 结构体总是通过被复制的方式在代码中传递,不使用引用计数。

定义语法

类和结构体有着类似的定义方式。通过关键字classstruct来分别表示类和结构体,并在一对大括号中定义它们的具体内容:

class SomeClass {
	// 在这里定义类
}
struct SomeStructure {
	// 在这里定义结构体
} 

注: 在每次定义一个新类或者结构体的时候,实际上是定义了一个新的Swift类型。因此请使用UpperCamelCase这种命名方式命名(如SomeClassSomeStructure等),已便符合标准Swift类型的大写命名风格(如StringIntBool)。相反的,请使用lowerCamelCase这种方式为属性和方法命名(如:framerateincrementCount),以便和类型名区分。

以下是定义结构体和定义类的示例:

struct Resolution {
	var width = 0
	var height = 0
}
class VideoMode {
	var resolution = Resolution()
	var interlaced = false
	var frameRate = 0.0
	var name: String?
} 

在上面的示例中我们定义了一个名为Resolution的结构体,用来描述一个显示器的像素分辨率。这个结构体包含了两个名为widthheight的存储属性。存储属性是被捆绑和存储在类或结构体中的常量或变量。这两个属性被初始化为整数0的时候,它们会被推断为Int类型。

在上面的示例中我们还定义了一个名为VideoMode的类,用来描述一个视频显示器的特定模式。这个类包含了四个变量存储属性。第一个是分辨率,它被初始化为一个新的Resolution结构体的实例,属性类型被推断为Resolution。新的VideoMode实例同时还会初始化其他三个属性,它们分别是,初始化为falseinterlaced,初始值为0.0frameRate,以及可选值为Stringnamename属性会被自定赋值nil,为可选类型。

类和结构体实例

Resolution结构体和VideoMode类的定义仅描述了什么是ResolutionVideoMode。它们并没有描述一个特定的分辨率(resolution)和视频模式(video mode)。为了描述一个特定的分辨率或者视频模式,我们需要生成一个它们的实例。

let someResolution = Resolution()
let someVideoMode = VideoMode() 

结构体和类都使用构造器语法来生成新的实例。构造器语法的最简单形式是在结构体或者类的类型名称后跟随一对空括号,如Resolution()VideoMode()。通过这种方式所创建的类或者结构体实例,其属性均会被初始化为默认值。

属性访问

通过使用点语法,你可以访问实例的属性。其语法规则则是,实例名后面紧跟属性名:

print("The width of someResolution is \(someResolution.width)")
// 打印 "The width of someResolution is 0"

print("The width of someVideoMode is \(someVideoMode.resolution.width)")
// 打印 "The width of someVideoMode is 0"

someVideoMode.resolution.width = 1280
print("The width of someVideoMode is now \(someVideoMode.resolution.width)")
// 打印 "The width of someVideoMode is now 1280" 

注: 与OC语言不通的是,Swift允许直接设置结构体属性的子属性。上面的最后一个例子,就是直接设置了someVideoModeresolution属性的width这个子属性,以上操作并不需要重新为整个resolution属性设置新值。

结构体类型的成员逐一构造器

所有结构体都有一个自动生成的成员逐一构造器,用于初始化新结构体实例中成员的属性。新实例中各个属性的初始值可以通过属性的名称传递到成员逐一构造器之中,如下:

let vga = Resolution(width: 640, height: 480) 

与结构体不同,类实例没有默认的成员逐一构造器

结构体和枚举是值类型

值类型被赋予给一个变量、常量或者传递给一个函数的时候,其值会被拷贝

在Swift中,所有的基本类型:整数,浮点数,布尔值,字符串,数组,字典都是值类型,并且在底层都是以结构体的形式所实现。所有的结构体和枚举类型都是值类型。这意味着他们的实例,以及实例中所包含的任何值类型属性,在代码中传递都会被复制。

let hd = Resolution(width: 1920, height: 1080)
var cinema = hd 

在以上示例中,声明了一个名为hd的常量,其值为一个初始化为全高清视频分辨率(1920 像素宽,1080 像素高)的Resolution的实例。

下面,为了符合数码影院的放映需求(2048 像素宽,1080 像素高),cinemawidth 属性需要作如下修改:

cinema.width = 2048 

这里,将会显示 cinemawidth 属性确已改为了 2048

print("cinema is now  \(cinema.width) pixels wide")
// 打印 "cinema is now 2048 pixels wide" 

然而,初始的 hd 实例中 width 属性还是 1920

print("hd is still \(hd.width) pixels wide")
// 打印 "hd is still 1920 pixels wide" 

证明将hd赋值给cinema的时候,实际是将hd中所有储存的值进行拷贝,然后将拷贝的数据存储到新的cinema实例中。由于两者相互独立,因此将 cinemawidth 修改为 2048 并不会影响 hd 中的 width 的值。

枚举也遵循相同的行为准则:

enum CompassPoint {
	case North, South, East, West
}
var currentDirection = CompassPoint.West
let rememberedDirection = currentDirection
currentDirection = .East
if rememberedDirection == .West {
	print("The remembered direction is still .West")
}
// 打印 "The remembered direction is still .West" 

类是引用类型

与值类型不同,引用类型在被赋予到一个变量、常量挥着被传递到一个函数时,其值不会被拷贝。因此,引用的是已存在的实例本身而不是其拷贝。使用VideoMode举例如下:

let tenEighty = VideoMode()
tenEighty.resolution = hd
tenEighty.interlaced = true
tenEighty.name = "1080i"
tenEighty.frameRate = 25.0

let alsoTenEighty = tenEighty
alsoTenEighty.frameRate = 30.0

print("The frameRate property of tenEighty is now \(tenEighty.frameRate)")
// 打印 "The frameRate property of theEighty is now 30.0" 

以上示例中,声明了一个名为tenEighty的常量,其引用了一个VideoMode了的新实例。在之前的示例中,这个视频模式被赋予了HD分辨率(1920*1080)的一个拷贝(即 hd 实例)。同时设置为interlaced,命名为“1080i”。最为帧率为25.0帧每秒。然后,tenEighty 被赋予名为 alsoTenEighty 的新常量,同时对 alsoTenEighty 的帧率进行修改。

因为类是引用类型,所以 tenEightalsoTenEight 实际上引用的是相同的 VideoMode 实例。换句话说,它们是同一个实例的两种叫法。

通过查看 tenEightyframeRate 属性,我们会发现它正确的显示了所引用的 VideoMode 实例的新帧率,其值为 30.0

注: tenEightyalsoTenEighty 被声明为常量而不是变量。然而你依然可以改变 tenEighty.frameRatealsoTenEighty.frameRate,因为 tenEightyalsoTenEighty 这两个常量的值并未改变。它们并不“存储”这个 VideoMode 实例,而仅仅是对 VideoMode 实例的引用。所以,改变的是被引用的 VideoModeframeRate 属性,而不是引用 VideoMode 的常量的值。

恒等于运算符

因为类是引用类型,有可能有多个常量和变量在幕后同时引用同一个类实例。(对于结构体和枚举来说,这并不成立。因为它们作为值类型,在二笔赋予到常量、变量或者传递到函数时,其值总是被拷贝。)

Swift里有两个恒等于符号:等价于(===不等价于!== 来判定两个常量是否引用同一个类实例。

if tenEighty === alsoTenEighty {
	print("tenEighty and alsoTenEighty refer to the same Resolution instance.")
}
//打印 "tenEighty and alsoTenEighty refer to the same Resolution instance." 

注: 等于(==等价于(===)意义不同:

等于表示两个实例的值相等相同 等价于表示两个类类型的常量或者变量引用同一个类实例。

指针

在OC中,指针是来引用内存中的地址。一个引用某个引用类型实例的Swift常量或者变量,与OC的指针类似,但并不直接指向某个内存地址,也不要求使用(*)来表明你在创建一个引用。Swift中的这些引用于其他的常量或者变量的定义相同。

类和结构体的选择

在我们的代码中,我们可以使用类和结构体来定义我们的自定义数据类型。

然而,结构体实例总是通过值传递,类实例总是通过引用传递。这意味两者使用不同的任务。当你在考虑一个工程项目的数据和功能的时候,你需要决定每个数据结构是定义成类还是结构体。

按照通用准则,当符合一条或多条一下条件时,可以考虑结构体:

  • 该数据结构的主要目的是用来封装少量相关简单数据值
  • 有理由预计该数据结构的实例在被赋值或传递时,封装的数据将会被拷贝而不是被引用。
  • 该数据结构中存储的值类型属性,也应该被拷贝,而不是被引用。
  • 该数据结构不需要去继承另一个既有类型的属性或者行为。

字符串、数组、字典类型的赋值与复制行为

Swift中,许多基本类型如StringArrayDictionary类型均以结构体的形式实现。这意味着被赋值给新的常量,或者被传入函数或方法中时,它们的值会被拷贝。

OC中NSStringNSArrayNSDictionary类型均以类的形式实现,而并非结构体。它们在被赋值或者被传入函数或者方法时,不会发生值拷贝,而是传递现有实例的引用。

 

Swift自动布局SnapKit的使用介绍

 swift  Swift自动布局SnapKit的使用介绍已关闭评论
3月 042020
 
网上看到的一篇介绍snapkit使用的文章,推荐:https://www.jianshu.com/p/2bad53a2a180, 转发下:

简介

SnapKit,一个经典的Swift版的第三方库,专门用于项目的自动布局,目前在github上的stars就高达9340颗星,这是一个不小的数字,亦足以证明它存在的非凡意义和作用。作者认为,在iOS开发(swift)中,它是用于项目最优秀的自动布局的必选库之一。它的作者仍然是写Objective-C的第三方库Masonry的大牛 – @Robert Payne,开门见山,本文将详细介绍介绍SnapKit的详细使用和安装,相信对于初入门该库的开发者或许会有一定的帮助。

Snapkit的安装(CocoaPods)

环境配置要求:

  • iOS 8.0 / Mac OS X 10.11+
  • Xcode 8.0+
  • Swift 3.0+

安装

在已经安装CocoaPods的前提下, 即可以进行下列步骤。

  • 在你的项目工程里的Podfile文件里面添加

 

source 'https://github.com/CocoaPods/Specs.git'

platform :ios, '10.0'

use_frameworks!

target '这里是你的工程名称' do
    pod 'SnapKit', '~> 3.0'
end
  • 老生常谈,运行CocoaPods的如下命令

 

pod install

到此,不出意外的话,你已经将SnapKit集成到你的项目中了。然后,就开始讲怎么使用它了。

Snapkit的布局使用

1、 实现一个宽高为100,居于当前视图的中心的视图布局,示例代码如下

 

import UIKit
import SnapKit

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
     
        let testView = UIView()
        testView.backgroundColor = UIColor.cyan
        view.addSubview(testView)
        testView.snp.makeConstraints { (make) in
            make.width.equalTo(100)         // 宽为100
            make.height.equalTo(100)        // 高为100
            make.center.equalTo(view)       // 位于当前视图的中心
        }
        
    }
}

更简洁的写法可以

 

import UIKit
import SnapKit

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
     
        let testView = UIView()
        testView.backgroundColor = UIColor.cyan
        view.addSubview(testView)
        testView.snp.makeConstraints { (make) in
            make.width.height.equalTo(100)    // 链式语法直接定义宽高
            make.center.equalToSuperview()    // 直接在父视图居中
        }
        
    }
}

效果图

TestPicture1

2、View2位于View1内, view2位于View1的中心, 并且距离View的边距的距离都为20

 

import UIKit
import SnapKit

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
     
         // 黑色视图作为父视图
         let view1 = UIView()
         view1.frame = CGRect(x: 0, y: 0, width: 300, height: 300)
         view1.center = view.center
         view1.backgroundColor = UIColor.black
         view.addSubview(view1)
         
         // 测试视图
         let view2 = UIView()
         view2.backgroundColor = UIColor.magenta
         view1.addSubview(view2)
         view2.snp.makeConstraints { (make) in
              make.top.equalToSuperview().offset(20)      // 当前视图的顶部距离父视图的顶部:20(父视图顶部+20)
              make.left.equalToSuperview().offset(20)     // 当前视图的左边距离父视图的左边:20(父视图左边+20)
              make.bottom.equalToSuperview().offset(-20)  // 当前视图的底部距离父视图的底部:-20(父视图底部-20)
              make.right.equalToSuperview().offset(-20)   // 当前视图的右边距离父视图的右边:-20(父视图右边-20)
         }
        
    }
}

更简洁的写法

 

import UIKit
import SnapKit

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
     
         // 黑色视图作为父视图
         let view1 = UIView()
         view1.frame = CGRect(x: 0, y: 0, width: 300, height: 300)
         view1.center = view.center
         view1.backgroundColor = UIColor.black
         view.addSubview(view1)
         
         // 测试视图
         let view2 = UIView()
         view2.backgroundColor = UIColor.magenta
         view1.addSubview(view2)
         view2.snp.makeConstraints { (make) in
            make.edges.equalToSuperview().inset(UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20))
         }
        
    }
}

效果图

TestPicture2

3、布局一个视图view2, 让它的水平中心线小于等于另一个视图view2的左边,可以这样布局

 

import UIKit
import SnapKit

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
     
         // 黑色视图作为父视图
         let view1 = UIView()
         view1.frame = CGRect(x: 0, y: 0, width: 300, height: 300)
         view1.center = view.center
         view1.backgroundColor = UIColor.black
         view.addSubview(view1)
        
         // 测试视图
         let view2 = UIView()
         view2.backgroundColor = UIColor.magenta
         view1.addSubview(view2)
         view2.snp.makeConstraints { (make) in
            // 让顶部距离view1的底部为10的距离
            make.top.equalTo(view1.snp.bottom).offset(10)
            // 设置宽、高
            make.width.height.equalTo(100)
            // 水平中心线<=view1的左边
            make.centerX.lessThanOrEqualTo(view1.snp.leading)
         }
        
    }
}

效果图

TestPicture

视图的属性说明

通过上面的大致简单布局我们对SnapKit有了一个基本的了解,那么, 它的布局属性是怎么来的呢?和原生的布局类有什么关联? 下面看一个SnapKit的布局属性表

从表中,我们知道,Snapkit的布局属性都是源自于系统的NSLayoutAttribute,那么,NSLayoutAttribute是个什么呢?其实,它在swift中是一个枚举,内部列举了很多布局属性诸如top、left、leading、centerX等,Snapkit的布局属性与它们都存在一一的对应关系。

Snapkit 的 greaterThanOrEqualTo 属性

如果想让视图View2的左边>=父视图View1的左边, 这时我们就可以用到greaterThanOrEqualTo

 

import UIKit
import SnapKit

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
         // 黑色视图作为父视图
         let view1 = UIView()
         view1.frame = CGRect(x: 0, y: 0, width: 300, height: 300)
         view1.center = view.center
         view1.backgroundColor = UIColor.black
         view.addSubview(view1)
        
         // 测试视图
         let view2 = UIView()
         view2.backgroundColor = UIColor.magenta
         view1.addSubview(view2)
         view2.snp.makeConstraints { (make) in
            // 让顶部距离view1的底部为10的距离
            make.top.equalTo(view1.snp.bottom).offset(10)
            // 设置宽、高
            make.width.height.equalTo(100)
            // 水平中心线<=view1的左边
            make.left.greaterThanOrEqualTo(view1)
            
            // 或者, 和上面一行代码一样的效果
//            make.left.greaterThanOrEqualTo(view1.snp.left)
         }
    }
}

效果图

TestPicture

其实,greaterThanOrEqualTo这个属性有点多余,比如上面这行代码 make.left.greaterThanOrEqualTo(view1) , 我们可以换成 make.left.equalToSuperview()make.left.equalTo(view1.snp.left), 效果是一样的,也就是说,一般情况下 >=<= 我们都可以直接用equalTo来代替!

SnapKit的greaterThanOrEqualTo和lessThanOrEqualTo联合使用

当我们想要让某个视图的width或height大于等于某个特定的值,小于等于某个特定的值的时候,一般而言,Snapkit会以greaterThanOrEqualTo为准,这里举一个width的例子,为了方便,这里指贴出了viewDidLoad中的代码(其他没必要)

 

 // 黑色视图作为父视图
 let view1 = UIView()
 view1.frame = CGRect(x: 0, y: 0, width: 300, height: 300)
 view1.center = view.center
 view1.backgroundColor = UIColor.black
 view.addSubview(view1)
    
 // 测试视图
 let view2 = UIView()
 view2.backgroundColor = UIColor.magenta
 view1.addSubview(view2)
 view2.snp.makeConstraints { (make) in
    make.width.lessThanOrEqualTo(300)
    make.width.greaterThanOrEqualTo(200)
    make.height.equalTo(100)
    make.center.equalToSuperview()
 }

接着,我们来看一下效果图

Test6

很明显,最后的宽度是以make.width.greaterThanOrEqualTo(200)为标准的,也可以这样的,在同时使用两者的情况下,greaterThanOrEqualTo的优先级略比lessThanOrEqualTo的优先级高。值得一提的是, 在上面的例子中,如果我们只设置make.width.lessThanOrEqualTo(300),那么view2是不会显示出来的,因为view2不知道你要表达的是要显示多少,小于等于300,到底是100还是200呢?(这里指针对width和height)所以它不能确定这个约束的值,但是,如果我们单独设置make.width.greaterThanOrEqualTo(200),那么就和上面的效果一样,因为它会以200为标准布局约束!

lessThanOrEqualTo的用于上、下、左、右

如果我们想要视图view2的左边 <= view1.left + 10, 那么就可以直接用到lessThanOrEqualTo布局了,看下面这个例子

 

 // 黑色视图作为父视图
 let view1 = UIView()
 view1.frame = CGRect(x: 0, y: 0, width: 300, height: 300)
 view1.center = view.center
 view1.backgroundColor = UIColor.black
 view.addSubview(view1)
    
 // 测试视图
 let view2 = UIView()
 view2.backgroundColor = UIColor.magenta
 view1.addSubview(view2)
 view2.snp.makeConstraints { (make) in
    make.left.lessThanOrEqualTo(20)     // <= 父视图的左边+20
    make.right.equalTo(-40)             // = 父视图的右边-40
    make.height.equalTo(100)
    make.center.equalToSuperview()
 }

效果图

Test7

Snapkit布局的灵活性

  • Snapkit布局灵活性很强, 我们看下面的例子, 他们的效果是一样的

 

make.left.equalToSuperview().offset(10)
make.left.equalTo(10)
make.left.equalTo(view1.snp.left).offset(10)
  • 设置视图的大小(width,height),他们效果是一样的

 

make.width.height.equalTo(100)
或
make.width.equalTo(100)
make.height.equalTo(100)
或
make.size.equalTo(CGSize(width: 100, height: 100))
  • 优先级(priority)

我们来看一下Snapkit的优先级设置, 优先级都是附加在约束链的末尾处,看下面的使用方法

 

// 黑色视图作为父视图
let view1 = UIView()
view1.frame = CGRect(x: 0, y: 0, width: 300, height: 300)
view1.center = view.center
view1.backgroundColor = UIColor.black
view.addSubview(view1)
    
// 测试视图
let view2 = UIView()
view2.backgroundColor = UIColor.magenta
view1.addSubview(view2)
view2.snp.makeConstraints { (make) in
    make.width.equalTo(100).priority(666)
    make.width.equalTo(250).priority(999)
    make.height.equalTo(111)
    make.center.equalToSuperview()
}

效果图

priorityimage

从上面我们可以知道, 我们设置了两个优先级:make.width.equalTo(100).priority(666) make.width.equalTo(250).priority(999), 那运行结果是一个哪个为准呢?显然是以优先级为 999的为准,因为 priority(999)>priotity(666), 所以在使用Snapkit的过程中,有时我们可以使用优先级priority来设置我们的约束, 另外,值得一提的是,SnapKit的优先级最大值只能是 1000, 如果优先级的数值超过1000,则运行时就会Crash!这里要尤其注意。

</br>

  • 更新约束(引用约束)

</br>

我们可以通过保存某一个约束布局来更新相应的约束,或者保存一组约束布局到一个数组中更新约束, 具体看下面代码

 

// 保存约束(引用约束)
var updateConstraint: Constraint?
    
override func viewDidLoad() {
    super.viewDidLoad()
    
    // 黑色视图作为父视图
    let view1 = UIView()
    view1.frame = CGRect(x: 0, y: 0, width: 300, height: 300)
    view1.center = view.center
    view1.backgroundColor = UIColor.black
    view.addSubview(view1)

    // 测试视图
    let view2 = UIView()
    view2.backgroundColor = UIColor.magenta
    view1.addSubview(view2)
    view2.snp.makeConstraints { (make) in
        make.width.height.equalTo(100)  // 宽高为100
        self.updateConstraint = make.top.left.equalTo(10).constraint   // 距离父视图上、左为10
    }
    
    let updateButton = UIButton(type: .custom)
    updateButton.backgroundColor = UIColor.brown
    updateButton.frame = CGRect(x: 100, y: 80, width: 50, height: 30)
    updateButton.setTitle("更新", for: .normal)
    updateButton.addTarget(self, action: #selector(updateConstraintMethod), for: .touchUpInside)
    view.addSubview(updateButton)
}
    
// 更新约束
func updateConstraintMethod() {
    self.updateConstraint?.update(offset: 50)   // 更新距离父视图上、左为50
}
testgif1
  • 更新约束(snp.updateConstraints)

说起这个updateConstraints, 我也懵逼过,那么它到底有何作用呢?又怎么用呢?它和一开始就使用的makeConstraints又有什么明确的区别呢?请继续往下看

说明1:如果你这是更新某个约束或某几个约束的常量值,你就可以使用updateConstraints而不是makeConstraints

说明2:这个也是苹果推荐用来添加或更新约束的方式

说明3:这个方法可以调用多次,会相应setNeedsUpdateConstraints, 在控制器中,可以写在override func updateViewConstraints()方法里面(当然也可以写在你想要什么时候更新的点击事件里面)

 

import UIKit
import SnapKit

class ViewController: UIViewController {

    lazy var blackView = UIView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        blackView.backgroundColor = UIColor.black
        view.addSubview(blackView)
        blackView.snp.makeConstraints { (make) in
            
            // 四个约束确定位置和大小
            make.width.equalTo(100)
            make.height.equalTo(150)
            make.top.equalTo(10)
            make.centerX.equalToSuperview()
        }
 
    }
    
    override func updateViewConstraints() {
        
        blackView.snp.updateConstraints { (make) in
            // 更新距离父视图顶部的约束(从 10 ---> 300 )
            make.top.equalTo(300)
        }
        
        // 根据苹果,调用父类应该放在末尾
        super.updateViewConstraints()
    }
}

注意: 从上面的代码中我们很明确地知道, blackView通过widthheighttopcenterX确定了它本身的大小和位置, 但是, 在 run 出来之后,它的top改变了距离, 从 10 变成了 300,其他三个约束保持不变, 见下图效果:

test10

显而易见, 除了top约束, 其他都没有改变! 也就是说,约束被更新(相当于系统升级一样,是一个道理)

现在,我们通过UIButton的点击事件来证明一下制作约束makeConstraintsupdateConstraints具体的区别在哪里?

 

lazy var blackView = UIView()
    
override func viewDidLoad() {
    super.viewDidLoad()
    
    blackView.backgroundColor = UIColor.black
    view.addSubview(blackView)
    blackView.snp.makeConstraints { (make) in
        
        make.width.equalTo(100)
        make.height.equalTo(150)
        make.top.equalTo(50)
        make.centerX.equalToSuperview()
    }
    
    let btn = UIButton(type: .custom)
    btn.backgroundColor = UIColor.brown
    btn.frame = CGRect(x: 100, y: 200, width: 60, height: 30)
    btn.addTarget(self, action: #selector(buttonAction), for: .touchUpInside)
    view.addSubview(btn)
 
}
    
// 点击更新/制作约束
func buttonAction() {
    
    blackView.snp.makeConstraints { (make) in
        make.width.height.equalTo(20)
        make.top.equalTo(300)
    }
    
}

先看效果图

点击事件发生前(图1):

test11

点击事件发生后(图2)

test12

图3

test14

图4

test13

上面,我们知道,视图 blackView一开始是由四个约束确定位置和大小,在点击事情响应后,我们又给 blackView 制作(记住,是制作,而不是重做,两者有明确的区别)了3个约束,分别是 widthheighttop, 那么这样做问题出现在哪里呢? 第一, 点击事情发生前(图1), 在点击事件发生后(见图2), 我们发现,blackViewwidthheight约束改变了,但是 top却没有改变,还是原来的距离父视图顶部 50 的距离, 原因在于,我们在原来的约束基础上又添加了多余的约束, 也就是说,约束从4个变成了7个(见图3左边constraints), 这样就产生了约束不明确,进而导致snapkit的警告(见图4), 这样布局显然是不可取的,在项目中这样做极其危险,甚至可能会导致异常奔溃!!!!

现在, 我们该将点击事件中的约束布局从makeConstraints改变成updateConstraints来试试两者有什么区别(下面只添加了点击事件的代码,其他事重复的就不多此一举了)

 

func buttonAction() {
    // 注意这里是updateConstraints, 而不是makeConstraints
    blackView.snp.updateConstraints { (make) in
        make.width.height.equalTo(20)
        make.top.equalTo(300)
    }
    print("这里试试snapkit有没有报警告")
}

接着看点击事件后的效果图

图5

test5

图6

test6

图7

test7

发现没有,在将makeConstraints改变成updateConstraints之后,约束还是4个,snapkit没有报警告,点击事件中的widthheighttop全部起了作用,而这就是两者的本质区别makeConstraints是制作约束,在原来的基础上再添加另外的约束,也就是画蛇添足,约束增加,视图布局就有不确定性,从而有些约束起作用,有些不起作用(如上面的top),snapkit报警告!!!而updateConstraints是更新约束,改变原有约束,约束不会增加,没经过updateConstraints处理的保持原有约束,经过处理就更新约束,约束不会减少,snapkit不会产生警告,这是正常标准的更新约束的正确方式!!!

</br>

  • 重做约束(remakeConstraints)

重做约束的本质就是:去掉已有的所有约束, 重新做约束,记住,是做约束, 也就是说, 使用了remakeConstraints后,重做的约束必须要能确定相应视图的大小位置, 之前makeConstraints的约束已经不会存在了,完全销毁!!!

 

// 点击更新/制作约束
func buttonAction() {
    
    // 注意这里是 remakeConstraints
    blackView.snp.remakeConstraints { (make) in
        make.width.height.equalTo(20)
        make.top.equalTo(300)
    }
    
    print("这里试试snapkit有没有报警告")
}

效果图

图(1)

test18

图(2)

test19

图(3)

test20

我们看到, blackView重做了约束, 之前的约束不起任何作用,由于它在重做约束后只有 3 个约束分别是 widthheighttop, 但是这里有一个问题,就是这 3 个约束只能确定大小,无法确定视图的位置, 所以在水平方向上或者左右缺少一个布局条件, 故 blackView整体视图的x紧靠左边(默认)! 另外我们发现, 在图(3)中,右上角出现了一个感叹号“!”, 那是因为告诉你缺少了一个约束条件:x-xcode-debug-views://7f81fcbc7900: runtime: Layout Issues: Horizontal position is ambiguous for UIView.

小结

通过以上学习,我们或深或浅地学习了布局三方库SnapKit的使用, 我相信,只要将上述布局会使用,并且懂得布局的原则和道理,基本上就可以“高枕无忧”了,至于涉及动态布局、动画布局等知识,后续有时间会更新文档。

</br>