在前面我们生成了所有kubernetes相关的TLS证书,kubernetes集群自身所有配置相关信息都存储在etcd之中,而flannel也将网络子网网段注册到etcd之中并为集群中节点的pod提供了加入同一局域网的能力。因此接下来我们安装部署etcd集群。
因为flannel插件也依赖于etcd存储信息,所以我们首先需要安装etcd集群,使之实现高可用。
在开始之前请确保在上一篇文章中生成的TLS证书都分发到需要部署的所有机器节点
的以下位置:
/etc/kubernetes/ssl/etcd.pem
/etc/kubernetes/ssl/etcd-key.pem
/etc/kubernetes/ssl/ca.pem
我们采用纯二进制安装etcd,因此不使用默认的包管理器中的安装文件。在每台需要部署的etcd的节点上,通过官方仓库下载你需要的版本的etcd二进制安装包:
目前最新版本是v3.3.4(截止到我写这篇文章的时候),而kubernetes v1.10验证过的版本为3.1.12
,如果没有特殊需求请尽量使用验证版本(如果你是不升级不舒服斯基当我没说)找到对应的系统架构并直接下载: https://github.com/coreos/etcd/releases/download/v3.1.12/etcd-v3.1.12-linux-amd64.tar.gz
在所有需要安装etcd节点执行以下命令来安装etcd和etcdctl
(注意:etcd目前不支持降级,如果你初始安装版本过高,后续像降级到验证版是比较麻烦的):
1 | wget https://github.com/coreos/etcd/releases/download/v3.1.12/etcd-v3.1.12-linux-amd64.tar.gz |
接着,我们需要编辑对应的systemd unit service文件,我们需要新建一个etcd.service
文件并放置于以下路径:/usr/lib/systemd/system/etcd.service
并键入以下内容:
1 | [Unit] |
WorkingDirectory
:指定 etcd 的工作目录和数据目录为 /var/lib/etcd
,需在启动服务前创建这个目录。--initial-cluster-state
值为 new
时,--name
的参数值必须位于 --initial-cluster
列表中。--peer-xxx
前缀的配置为etcd与其它etcd节点通信的相关配置,不带有的该前缀的则为客户端(例如:etcdctl)与etcd节点(作为服务器)通信的相关配置。对应的,我们在/etc/etcd/etcd.conf
路径中新建一个etcd.conf
文件并键入以下内容:
1 | # [member] |
ETCD_NAME
为对应的node1、node2、node3,并将ETCD_INITIAL_ADVERTISE_PEER_URLS
和ETCD_INITIAL_ADVERTISE_PEER_URLS
修改为对应的节点的ip
。ETCD_CLUSTER_NODE_LIST
中的ip必须在生成etcd TLS证书时在etcd-csr.json
中的hosts
字段中指定(Subject Alternative Name(SAN)),否则可能会得到(error "remote error: tls: bad certificate", ServerName "")
这样的错误。ETCD_CLUSTER_NODE_LIST
中指定,并正确配置其ETCD_NAME
。在所有的节点上完成了上述两步之后,我们分别执行以下命令来启动etcd(初始可能会阻塞一段时间):
1 | sudo systemctl daemon-reload |
如果配置正确,那么上述命令执行结果应该是任何输出的。如果结果有错,请参照上述配置和环境变量文件检查配置。一旦我们顺利启动etcd
服务,我们还需要正确检查我们的etcd
集群是否可用,在etcd
集群中任一节点中执行以下命令:
1 | etcdctl --endpoint https://127.0.0.1:2379 \ |
在一切正常情况下,你会得到类似如下的输出结果:
1 | member 245a74588a3e85d0 is healthy: got healthy result from https://xxx.xxx.xxx.xxx:2379 |
需要特别说明的是:etcd
集群是否和kubernetes
部署在同样的服务器节点上是可选的
。也就是说etcd
集群可以脱离kubernetes
部署的集群而单独部署在其他单独的服务器上,且并不需要和kubernetes
节点数对应。经过我的实践如果有条件的话请务必:
至此,我们的etcd
集群已经顺利安装完成。接下来安装flannel插件。
Kubernetes是Google开源的容器化集群管理系统,其提供的应用部署、扩展、服务发现等机制对于微服务化架构应用有着十分重要的作用。
本系列文章基于以下版本来讲述如何使用二进制方式安装Kubernetes集群顺便讲述下踩坑的心路历程:
v1.10
CentOS Linux 7
Linux 3.10.0
Kubernetes系统的各个组件需要使用TLS证书对其通信加密以及授权认证,所以在部署之前我们需要先生成相关的TLS证书以便后续操作能够顺利进行。
在后续安装部署中,将不使用kube-apiserver的HTTP非安全端口,所有组件都启用TLS双向认证通信。因此TLS证书配置是在安装配置Kubernetes系统中最容易出错和难于排查问题的一步,所以请务必耐心仔细。
在开始前,为了模拟集群节点,我们假定需要在以下三台Linux主机上部署Kubernetes:
10.138.148.161
:作为master
节点10.138.196.180
:作为Node
节点10.138.212.68
:作为Node
节点同一台主机上可以同时部署master和Node节点相关组件,即同时作为控制节点和工作节点,不过这么做可能导致master节点负载过高而失去响应进而导致整个集群出现无法预知的问题。
CFSSL
证书生成工具我们将使用Cloudflare
的PKI工具集cloudflare/cfssl来生成集群所需要的各种TLS
证书。
执行以下命令直接下载二进制文件进行安装:
1 | wget https://pkg.cfssl.org/R1.2/cfssl_linux-amd64 -O cfssl |
CA(Certificate Authority)是自签名的根证书,用来签名后续创建的其它 TLS 证书;
确认CFSSL
工具安装成功之后,我们先通过CFSSL
工具来创建模版配置json文件:
1 | cfssl print-defaults config > config.json |
这将生成两个模版json文件,后续CFSSL
将读取json文件内容并生成对应的pem
文件。我们先复制config.json
为ca-config.json
文件并做如下修改:
1 | { |
profiles
:可以定义多个profiles,分别指定不同的过期时间、使用场景等参数;后续在签名证书时使用某个特定的profile。
signing
:表示该证书可用于签名(签发)其它证书,生成的 ca.pem 证书中 CA=TRUE。
server auth
:表示`client可以用该 CA(生成的ca.pem) 对server提供的证书进行验证。
client auth
:表示server可以用该CA(生成的ca.pem)对client提供的证书进行验证。
我们复制csr.json
为ca-csr.json
并做以下修改:
1 | { |
CN
(Common Name
):后续kube-apiserver
组件将从证书中提取该字段作为请求的用户名;
O
(Organtzation
):后续kube-apiserver
组件将从证书中提取该字段作为请求的用户所属的用户组;
执行以下命令来生成CA证书和私钥:
1 | cfssl gencert -initca ca-csr.json | cfssljson -bare ca |
这样,我们就生成了CA证书和私钥了,因为我们需要双向TLS
认证,所以需要拷贝ca-key.pem
和ca.pem
到所有要部署的机器的/etc/kubernetes/ssl
目录下备用。
因为我们准备部署的kubernetes组件是使用TLS
双向认证的,包括kube-apiserver
不打算使用HTTP端口,因此,我们需要生成以下的证书以供后续组件部署的时候备用:
etcd
证书:etcd集群之间通信加密使用的TLS
证书。kube-apiserver
证书:配置kube-apiserver
组件的证书。kube-controller-manager
证书:用于和kube-apiserver
通信认证的证书。kube-scheduler
证书:用于和kube-apiserver
通信认证的证书。kubelet
证书【可选,非必需】:用于和kube-apiserver
通信认证的证书,如果使用TLS Bootstarp
认证方式,将没有必要配置。kube-proxy
证书【可选,非必需】:用于和kube-apiserver
通信认证的证书,如果使用TLS Bootstarp
认证方式,将没有必要配置。下面我们将逐个创建对应的TLS
证书,并做相应的简短说明:
etcd
证书:首选我们创建etcd
证书签名请求(CSR),拷贝csr.json
为etcd-csr.json
并做以下修改:
1 | { |
此处需要指定host
字段的值,该值为所有需要部署etcd节点的ip 域名 或者 hostname
,etcd需要使用Subject Alternative Name(SAN)
来校验集群以及防止滥用。如果你不清楚应该使用哪个ip,默认情况下使用ip a
查看eth0
即可。此处指定的ip
与后续指定的etcd的systemd
配置initial-cluster
相关。
相关阅读: Option to accept TLS client certificates even if they lack correct Subject Alternative Names
生成etcd
证书和私钥:
1 | cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=kubernetes etcd-csr.json | cfssljson -bare etcd |
将生成的etch-key.pem
和etcd.pem
拷贝到所有需要部署etcd
集群的服务器/etc/etcd/ssl
目录下备用。
kube-apiserver
证书创建kube-apiserver
证书签名请求配置文件,拷贝csr.json
为kubernetes-csr.json
并做以下修改:
1 | { |
此处指定了host
字段来表示授权使用该证书的ip或域名
列表,因此上述配置文件指定了要部署的kubernetes三台服务器ip(实际上只需要指定打算部署master节点的ip即可)以及kube-apiserver
注册的名为kubernetes
服务的服务ip(一般默认为后续配置kube-apiserve
组件的时候指定的—service-cluster-ip-range
网段的第一个ip。)如果你不清楚怎么操作,可以留空host
字段。
如果你指定了host
字段,这里如果有 VIP
的,也是需要填写的。
生成kube-apiserver
证书和私钥:
1 | cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=kubernetes kubernetes-csr.json | cfssljson -bare apiserver |
我们将该证书拷贝到需要部署到master
节点上的/etc/kubernetes/ssl
上备用。
因为我们master节点的组件之间的通信使用非HTTP
的安全端口,所以同样也需要TLS
认证授权,因此我们也需要配置kube-controller-manager
和kube-scheduler
的证书来供这两个组件访问kube-apiserver
.如果你的集群master节点组件使用HTTP非安全端口通信,那么可以不需要配置这两个证书。
kube-controller-manager
证书复制car.json
为kube-controller-manager-csr.json
并做以下修改:
1 | { |
上述的配置中,kube-apiserver
将提取CN
作为客户端组件(kube-controller-manager)的用户名(system:kube-controller-manager),kube-apiserver
预定义的RBAC使用ClusterRoleBinding system:kube-controller-manager
将用户system:kube-controller-manager
与ClusterRole system:kube-controller-manager
绑定。
生成kube-controller-manager
证书和私钥:
1 | cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=kubernetes kube-controller-manager-csr.json | cfssljson -bare controller-manager |
将证书拷贝到需要部署kube-controller-manager
的master节点/etc/kubernetes/ssl
上备用。
与kube-controller-manager
一样,kube-scheduler
同样也需要TLS
证书来访问kube-apiserver
。此处不再赘述。直接上kube-scheduler-csr.json
文件内容:
1 | { |
kube-scheduler
将提取
CN作为客户端的用户名,这里是
system:kube-scheduler。 kube-apiserver 预定义的 RBAC 使用的 ClusterRoleBindings
system:kube-scheduler 将
用户system:kube-scheduler 与
ClusterRole system:kube-scheduler `绑定。
生成kube-scheduler
证书以及私钥:
1 | cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=kubernetes kube-scheduler-csr.json | cfssljson -bare scheduler |
将证书拷贝到需要部署kube-scheduler
的master节点/etc/kubernetes/ssl
上备用。
至此,master
节点上的证书生成就全部完成了,接下来是生成worker
节点的证书,需要注意的是:生成worker
证书是可选的,如果你使用TLS Bootstarpping
那么你可以跳过以下步骤worker
证书生成工作。直接转到部署的实际操作环节。关于TLS
证书和TLS Bootstarpping
认证方式的区别,后续考虑单独写一遍文章展开来讲。
kubelet
证书拷贝car.json
为kubelet-csr.json
并做以下修改:
1 | { |
O
为用户组,kubernetes RBAC定义了ClusterRoleBinding将Group system:nodes和CLusterRole system:node关联起来。
注意:在kubernetes v1.8+
以上版本,将不会自动创建binding
,因此我们后续需要手动创建绑定关系。
生成kubelet
证书和私钥:
1 | cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=kubernetes kubelet-csr.json | cfssljson -bare kubelet |
将生成的证书和秘钥拷贝到所有需要部署的worker节点上的/etc/kubernetes/ssl
下备用。
kube-proxy
证书拷贝car.json
为kube-proxy-csr.json
并做以下修改:
1 | { |
CN
指定该证书的 User为 system:kube-proxy。Kubernetes RBAC定义了ClusterRoleBinding将system:kube-proxy用户
与system:node-proxier 角色
绑定。system:node-proxier具有kube-proxy组件访问ApiServer的相关权限。
生成kube-proxy
证书和私钥:
1 | cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=kubernetes kube-proxy-csr.json | cfssljson -bare kube-proxy |
将生成的证书和私钥拷贝到所有需要部署worker
节点的/etc/kubernetes/ssl
下备用。
在完成证书分发之后,这样我们的证书相关的生成工作就完成了。接下来开始配置各个组件。
参考资料:
]]>1 | [error] 7#7: *12030 lua entry thread aborted: runtime error: /data/share/apps/lua/access_check.lua:133: bad argument #1 to 'decode' (string expected, got userdata) |
该段代码的主要作用是在openresty
中lua
读取redis
中数据并解码为json
:
1 | local access_token = redis_client:read_by_key(token_key) |
通过查询资料得知原因:lua
读取redis
数据返回结果为空时,返回的结果不是nil
而是userdata
类型的ngx.null
。
因为
nil
在lua
中有特殊的意义,如果一个变量被设置为nil
相当于告知该变量未定义
(不存在)一样,如果把redis
查询的结果为空设置为nil
,而该查询的key
对应在redis
中又是存在的,就无法把查询为空
和未定义
区分开来了,这样显然是不合理的。所以必须使用一个userdata
类型的值来表示这个查询记录为空,但是又不等同于未定义变量
(ngx.null)。
因此,代码做如下修改即可:
1 | local access_token = redis_client:read_by_key(token_key) |
后端的数据持久化使用的是 Mybatis ,在做高并发下账户增减余额的时候,打算使用乐观锁来解决这个问题。在获取update操作的返回值时遇到了一个问题,似乎 Mybatis 进行 update 操作得到的 int
返回值并不是影响的行数。这下就尴尬了。
一般而言,我们知道当我们使用 Mybatis 在 mapper
接口中定义 insert
delete
等操作,定义一个 int
类型的返回值,通过该值是否为 0 来判断数据库中受影响的行数进而判断操作是否成功。
到底 update
返回值代表什么呢?我们来验证一下便知道了,假设有如下一张表以及两条数据:
我们来编写一个简单的单元测试用例来验证下,首先使用 mybatis 简单的写个 mapper 进行更新操作,其中 xml 中的内容为:
数据库连接配置为:
接来下,我们来编写一个简单的单元测试来验证下:update 的返回值是不是受影响的记录的条数,对应的单元测试代码如下:
由单元测试代码可以得知,我们将要把数据库中两条记录的 phone
字段的值由 12345678
修改为 66666666
,正常情况下,resultCode
将会返回 2 。因为 update
操作影响到数据库中这 2 条记录,这和我们期望 2 是相符合的。那么一切正常的情况下,这次单元测试将会通过,那么我们运行看看结果:
单元测试通过了,再查看数据库中的记录:
这说明 mybatis 的 update 更新操作返回值的确是返回受影响的行数……真的是这样吗?
我们知道,当数据库中的记录被修改之后,再次执行重复的 update
操作将不会影响到新的行数,为了验证我说的话,我们试试:
那么,按照这个逻辑:我们再次执行这个单元测试应该是,resultCode
返回的应该是 0
,和我们的期望的数字 2
不一致,将会导致测试不通过。再次运行单元测试:
居然还是 passed
,看到这里聪明的你已经看出来了,默认情况下,mybatis 的 update
操作返回值是记录的 matched
的条数,并不是影响的记录条数。
严格意义上来将,这并不是 mybatis 的返回值,mybatis 仅仅只是返回的数据库连接驱动(通常是 JDBC
)的返回值,也就是说,如果驱动告知更新 2 条记录受影响,那么我们将得到 mybatis 的返回值就会是 2 和 mybatis 本身是没有关系的。
道理我都懂,如果我们非得要 mybatis 的 update
操作明确的返回受影响的记录条数,有没有什么办法呢?
当然是有的。
通过对 JDBC
URL 显式的指定 useAffectedRows
选项,我们将可以得到受影响的记录的条数:
1 | jdbc:mysql://${jdbc.host}/${jdbc.db}?useAffectedRows=true |
我们对我们的数据库连接配置稍做修改,添加 useAffectedRows
字段:
此时,mybatis 的 update 操作返回的应该是受影响的条数了,我们再次运行单元测试试试看:
update
操作返回的是受影响的记录条数,我们知道为 0
和我们预期的 2
不一致,自然而然单元测试不通过咯~。
在开发Android项目的时候,使用的是Gradle
构建工具,喜欢它的灵活和方便,在转向Java后端开发的时候更多时候使用的是Maven
构建工具,然而看着漫天的尖括号,心里实在是难受。虽然只是一个构建工具,本着折腾的心,我还是更认可和看好Gradle
。然而很多时候你的队友并没有习惯去使用或者快速熟悉Gradle
构建工具,那么这个时候就需要将Gradle
项目转换为Maven项目了,或者将Maven项目转换为Gradle
项目了。
首先是安装构建工具,这个没啥好说的。
打开Powershell或者Cmder执行以下命令完成安装:
1 | choco install gradle |
choco
为windows下的一款包管理工具,可以方便安装管理配置一些常见的软件包,如果你没有安装choco
的话,请移步:https://chocolatey.org/
打开Terminal,执行以下命令安装:
1 | brew install gradle |
需要特别说明的是,Gradle
对Maven
的支持是比较完善的,因此,转换也是非常的简单,在pom.xml
文件所在的目录下执行:
1 | gradle init # 根据pom.xml内容生成对应的gradle配置 |
Gradle
项目转Maven
项目需要借助一个Gradle插件,在项目的module
的build.gradle
文件中加入以下配置即可:
1 | apply plugin: 'maven' |
通过双击Idea
的Gradle Tasks GUI:
或者执行命令来完成转换:
1 | gradle install |
完成之后,将会在当前Module项目的build
目录下的poms
文件夹下生成pom-default.xml
,将其拷贝到项目的根目录下即可。
通过实际测试,这样的生成的pom-default.xml
文件是不能用于直接maven
构建的,因为生成的pom-default.xml
文件中的groupId
还需要我们手动指定下。这样显然是不清真的,于是我们可以在build.gradle
文件中将其事先定义好,这样生成的pom文件就不用我们再手动更改了:
然而这样我们还是觉得麻烦,毕竟需要手动复制到项目根目录,再重新命名。我们还可以通过Hook Gradle中Maven插件的install
Task来完成自动的复制和命名,编辑build.gradle
:
1 | task convert2Maven { |
此时,再执行gradle install
这个task就可以看到gradle已经自动为我们在项目的根目录下生成好了pom.xml
文件啦。
Docker Engine的Deamon进程是以root权限运行的,如果是普通用户要与之交互,需要使用sudo
命令来提权与之交互。之前使用Docker官方的安装脚本安装完成之后,会给出一个提示将当前非root用户添加到doker组之中,以避免每次都需要输入sudo
的麻烦。
然而随着Docker版本的迭代和官网的安装方式的更改,现在官方给出的安装方式是添加仓库源地址,然后使用默认的apt
或者yum
包管理工具来完成后安装。并不再提示用户添加非root用户到组。
默认情况下,完成Docker Engine的安装之后,Docker将会自动创建一个名为docker
的用户组,所以root
用户和在docker
组中的用户都可以免去sudo
来与Docker Engine交互。知道原理之后就简单了:
1 | sudo usermod -aG docker ${whoami} #添加当前用户到docker组 |
在做Android开发的时候,对于Api接口的对接有着深刻的体会:后端通过Markdown或者Word写好Api文档,然后通过类似Samba或者Dropbox这样的服务与移动端实现文档共享。有的时候因为接口出了问题,中间还得来回修改对接,效率低下不说,要是后端手抖写错参数而没有意识到,移动端埋头一顿调试。。。说多了都是泪。
为了避免同时维护代码和文档来保持两者之间的同步而带来的额外负担,同事推荐了ApiDoc
来生成文档,虽然生成的文档界面比较清爽然而前提是必须得按照规定的语法写上详细的注释,才能生成对应的文档,虽然写注释本身是一件好事,不过有能够自动生成的方法为啥不使用呢?
与Apidoc
类似,Swagger
也是一个用来文档化Resetful Api的项目,不过开源社区的支持应该是所有类似项目中最为完善的,因此除了可以使用Swagger Editor来编写Api文档之外,你还可以使用其它对应的自动化生成工具,以此来避免同时维护文档和代码的麻烦:
这篇文章将从头创建一个Spring Boot项目并使用Springfox来生成对应的接口文档,来说明使用Springfox是多么的简单。首先创建Spring Boot项目:
如果你是使用Eclipse的话,那么:
我们使用IDEA的Spring initializr
向导来简化初始化创建项目,如图所示:
点击下一步根据个人的喜好来配置喜欢的JVM语言和构建工具,此处我选择Kotlin
和Gradle
,一切都是为了爽:
点击下一步选择需要集成的依赖项,此处我们简单演示下Resetful Api文档生成,所以选择Web即可,如图:
点击Next直至完成。这样,我们就完成了Spring Boot项目的创建了。
编辑根目录下的build.gradle
文件,修改以下内容:
1 | dependencies { |
Springfox通过Docket
对象来定义生成的Api的一些属性,因此我们来创建一个Configure类来专门做Springfox的配置。创建一个Swagger2Configure.kt
文件,并添加以下内容:
1 |
|
上述示例只演示了最基本的配置,如果想查看完整的示例解释,请移步Configuration explained,至此,Springfox的配置就完成了。就是这么简单。
我们创建一个简单的UserController来模拟获取用户信息,UserController.kt
:
1 |
|
对应的ModelUser.kt
:
1 | data class User(val id: Int, var name: String, var age: Int) |
至此就完成了简单的接口,接着我们启动项目并访问http://localhost:8080/swagger-ui.html ,一切正常的话,你将会看到以下页面:
一般来说这样已经能够满足我们的基本需要了,如果还需要更为详细的文档,Springfox也提供的注解来简化配置过程,我们接下来稍微修改下UserController.kt
:
1 | ) |
我们重启项目查看下:
可以发现文档添加了对应的中文,要查看全部可用的注解以及其作用,请移步官方文档:
简单集成使用到这里👌咯,后续再写一写生成静态文档相关的内容吧。Just for Fun!
]]>repo
建立关联的之后,进行pull
操作会出现类似于下面的这种错误:1 | * branch master -> FETCH_HEAD |
通过查阅资料显示,GIt从版本2.9.0
开始,预设行为不允许合并没有共同祖先的分支,需要加上--allow-unrelated-histories
选项进行pull操作才不会出现此类错误信息:
1 | git pull origin master --allow-unrelated-histories |
相关参考:
]]>在Docker中,容器之间的链接是一种很常见的操作:它提供了访问其中的某个容器的网络服务而不需要将所需的端口暴露给Docker Host主机的功能。Docker Compose中对该特性的支持同样是很方便的。然而,如果需要链接的容器没有定义在同一个docker-compose.yml
中的时候,这个时候就稍微麻烦复杂了点。
在不使用Docker Compose的时候,将两个容器链接起来使用—link
参数,相对来说比较简单,以nginx
镜像为例子:
1 | docker run --rm --name test1 -d nginx #开启一个实例test1 |
这样,test2
与test1
便建立了链接,就可以在test2
中使用访问test1
中的服务了。
如果使用Docker Compose,那么这个事情就更简单了,还是以上面的nginx
镜像为例子,编辑docker-compose.yml
文件为:
1 | version: "3" |
最终效果与使用普通的Docker命令docker run xxxx
建立的链接并无区别。这只是一种最为理想的情况。
docker-compose.yml
文件中,应该如何链接它们呢?docker-compose.yml
文件中的容器需要与docker run xxx
启动的容器链接,需要如何处理?针对这两种典型的情况,下面给出我个人测试可行的办法:
我们还是使用nginx镜像来模拟这样的一个情景:假设我们需要将两个使用Docker Compose管理的nignx容器(test1
和test2
)链接起来,使得test2
能够访问test1
中提供的服务,这里我们以能ping通为准。
首先,我们定义容器test1
的docker-compose.yml
文件内容为:
1 | version: "3" |
容器test2
内容与test1
基本一样,只是多了一个external_links
,需要特别说明的是:最近发布的Docker版本已经不需要使用external_links来链接容器,容器的DNS服务可以正确的作出判断,因此如果你你需要兼容较老版本的Docker的话,那么容器test2
的docker-compose.yml
文件内容为:
1 | version: "3" |
否则的话,test2
的docker-compose.yml
和test1
的定义完全一致,不需要额外多指定一个external_links
。相关的问题请参见stackoverflow上的相关问题:docker-compose + external container
正如你看到的那样,这里两个容器的定义里都使用了同一个外部网络app_net
,因此,我们需要在启动这两个容器之前通过以下命令再创建外部网络:
1 | docker network create app_net |
之后,通过docker-compose up -d
命令启动这两个容器,然后执行docker exec -it test2 ping test1
,你将会看到如下的输出:
1 | docker exec -it test2 ping test1 |
证明这两个容器是成功链接了,反过来在test1
中pingtest2
也是能够正常ping通的。
如果我们通过docker run --rm --name test3 -d nginx
这种方式来先启动了一个容器(test3
)并且没有指定它所属的外部网络,而需要将其与test1
或者test2
链接的话,这个时候手动链接外部网络即可:
1 | docker network connect app_net test3 |
这样,三个容器都可以相互访问了。
通过更改你想要相互链接的容器的网络模式为bridge
,并指定需要链接的外部容器(external_links
)即可。与同属外部网络的容器可以相互访问的链接方式一不同,这种方式的访问是单向的。
还是以nginx容器镜像为例子,如果容器实例nginx1
需要访问容器实例nginx2
,那么nginx2
的doker-compose.yml
定义为:
1 | version: "3" |
与其对应的,nginx1
的docker-compose.yml
定义为:
1 | version: "3" |
需要特别说明的是,这里的
external_links
是不能省略的,而且nginx1
的启动必须要在nginx2
之后,否则可能会报找不到容器nginx2
的错误。
接着我们使用ping来测试下连通性:
1 | $ docker exec -it nginx1 ping nginx2 # nginx1 to nginx2 |
以上也能充分证明这种方式是属于单向联通的。
在实际应用中根据自己的需要灵活的选择这两种链接方式,如果想偷懒的话,大可选择第二种。不过我更推荐第一种,不难看出无论是联通性还是灵活性,较为更改网络模式的第二种都更为友好。
]]>最近需要使用Django写点东西,由于自己的macbook上没有也不打算安装MySQL而是以Docker的MySQL镜像替代,Django文档提供了三种MySQL驱动供选择,官方推荐的是mysqlclient
,由于我本地没有安装MySQL,所以是没有Native Driver的以至于在安装MySQL驱动的时候遇到了点小问题,在此记录下。
安装mysqlclient
:
1 | pip install mysqlclient |
然而得到错误信息如下:
1 | Collecting mysqlclient |
因为没有安装MySQL,所以在安装mysqlclient
之前还需要安装Connector,如下:
1 | brew install mysql-connector-c |
之后安装再安装mysqlclient
:
1 | pip install mysqlclient |
然后又就报错了,错误信息如下:
1 | Collecting mysqlclient |
通过查找资料得出可能的结论是通过brew安装的mysql-connector-c
配置可能不正确,打开/usr/local/bin/mysql_config
脚本修改其中的部分内容:
1 | # Create options |
修改为:
1 | Create options |
保存,再次安装mysqlclient
应该就会正常安装了。接着就可以使用Django和运行在Docker中的MySQL愉快的Coding了~
在日常的Android NDK开发中,会不可避免的用到C与Java代码相互调用的情况。Java调用C的方法还好,C调用Java的方法就比较麻烦了。需要编写看着就头疼的Java方法描述符才能正确的调用Java方法。
其中常见的Java方法域和描述符如下表所示:
Java类型 | 签名 |
---|---|
Boolean | Z |
Byte | B |
Char | C |
Short | S |
Int | I |
Long | J |
Float | F |
Double | D |
Fully-qualified-class | Lfully-qualified-class |
type[] | [type |
Method type | (arg-type)ret-type |
通过上述对照表,我们可以通过C代码查找一个为String
类型的Java静态字段,例如:
1 | jfieldID staticJavaFieldId; |
借助javap
我们可以很方便的得知一个class
文件其中包含对应的描述符。如下:
1 | $ javap -s -p com.xiamo.test.Message |
但是每次需要查看对应类的方法描述符的时候都需要手动敲一次命令,这样显然不够清真。好在Android Studio
为我们提供了External Tools
。我们可以用它来自定义这个操作简化我们的双手。
打开Android Studio
的设置页面,在Tools
选项卡中选中External Tools
,如下图所示:
点击右侧区域的+
新增一个Tools
,在选卡中填入如下图所示的参数:
Name
为你要设置的External Tools
的名字,便于你自己标识就行,此处我设置为JNI Descriptor Generator
Program
为Tools
执行的命令的路径,如果你需要替换为你自己JDK中的javap
修改这个值就行,此处使用Android Studio
自带的JDK
路径,填入$JDKPath$/bin/javap
Parameters
为命令执行的参数,我们要获取方法描述符,所以设置为:-s -p $FileClass$
Working directory
为上述设置好的工具执行的目录,设置为$ModuleFileDir$/build/intermediates/classes/debug
点击保存,我们的External Tools
就设置好啦。这个时候在Tools
—>External Tools
中就可以看到我们设置好的Tools
了。需要注意的是这个时候点击改工具查看当前我们选中的Java源文件的文件操作符,是可能会报错找不到指定的class文件。
这是因为我们指定的Working directory
中还没有生成class文件,选择Build
选项中的Make Project
,等待make完成,再次点击Tools
—>External Tools—>
JNI Descriptor Generator
即可生成对应Java源文件的文件描述符了。这样我们就可以愉快的调用使用C调用Java中的方法咯。
14.0.3770861
下载完成FFmpeg源码之后,先对源码根目录中的configure
文件进行修改以适应Android平台。因为默认编译出来的动态库文件版本号在.so
之后,例如:libavcodec.so.56.60.100
。Android平台对这种格式不能很好的识别(如果你不介意一个一个修改文件名的话)。通过Vim
或者其他文本编辑器打开configure
文件的第2934
行(如果你下载的FFmpeg版本和我的一样的话)将:
1 | SLIBNAME_WITH_MAJOR='$(SLIBNAME).$(LIBMAJOR)' |
修改为:
1 | SLIBNAME_WITH_MAJOR='$(SLIBPREF)$(FULLNAME)-$(LIBMAJOR)$(SLIBSUF)' |
后保存。
]]>command
+space
调出Spotlight搜索
键入screen Sharing.app
即可。]]>由于Git默认是大小写不敏感的,导致我这边虽然修改了,然而队友那边还是老样子,可能会导致后续的提交出现问题让项目无法编译通过,虽然不是什么大问题,稍微小改一下就行。然而鉴于这种问题出现了几次,每次都手动修改还是挺闹心的。我们还是来动手让Git区分大小写吧。
如果只是想应用于当前项目,那么在当前项目中使用执行以下Git命令
:
1 | git config core.ignorecase false |
当然,如果想一劳永逸的话,推荐还是做一个全局配置:
1 | git config --global core.ignorecase false |
这样之后的项目都不用担心大小写都问题闹心了。
]]>嗯,相关的源码和基本的部署说明丢在Github上了,感兴趣的可以戳此查看。
目前保证基本的功能能够稳定的运行,后续再添加一些其它的功能吧。嗯,详细的配置说明先挖个坑,改天再写。懒癌犯了,先休息~
]]>为了说明上述问题,我们来简单模拟这样一个过程:
APP –> ActivityA –> ActivityB –>ActivityC –> Pressed Home
假设APP在Activity C页面用户按下Home
键应用退到后台运行。这个时候启动DDMS,选中该APP的进程,Kill。然后我们从运行APP历史列表中选中该APP并将其置于前台,这个时候回到该应用的界面还是Activity C。再点击返回按钮回到ActivityB,在某些性能比较差一点的机器上可能会出现短暂的黑屏然后才会显示出ActivityB。这是因为该Activity实例其实在Kill该APP进程的时候已经被销毁了,但是Android系统虽然销毁了Activity实例,却并没有销毁该APP的Activity栈。因此我们点击返回按钮还是会回到ActivityB。但是需要重新构建该ActivityB的实例。
这样看貌似并没有什么问题,然而事情并不会这么简单(废话,不然我写这篇博客干嘛。。),如果ActivityB中引用了静态变量并尝试获取其值的时候,这个时候是会出现NPE的。
我们简单来总结下上述过程:
当应用在后台被Kill,整个APP进程都被销毁,所有变量都被清空,包括Application的实例。
虽然所有变量和实例都被销毁,但是Activity栈并没有被清空,所以我们回到应用还能得知页面的打开顺序。
当应用被强杀时,会自动调用onSaveInstance
方法去保存一些核心变量,然而这在面对N多的页面的时候显然不是一件省心的事情,而且你也不能保证你的队友也会这么做。。
在某些性能比较低或者页面逻辑比较复杂的页面会黑屏是因为需要重建ActivityB的实例,也就是需要重走Activity的声明周期OnCreate
,性能差点的机器上自然就会有短暂的黑屏了。
如果APP中没有静态变量的引用,那就不会出现NEP,但是一旦引用了静态变量,这个时候可能就比较危险了。(静态变量包括全局的登录状态,全局的用户配置、标志位之类的数据)当然了,如果你能将所有的静态变量修改到单例中去,并将其持久化,为NULL的时候再去取的话,原则上来说这样也可以避免NPE。然而要是这样做的话很大程度上会减缓开发的进度,而且指不定哪个队友就给你挖坑了。然后你怎么挂的都不知道。。
为了一劳永逸的解决这个问题,我们需要冷静下来思考一下:既然APP被强杀了,为啥还要回到原先的页面中去而不是重走启动APP的流程?
我们虽然不能阻止Android系统销毁实例却保存Activity栈,也不想多写那些持久化或者Cache静态变量代码(这将是一件费力而且不太讨好的事情)所以我们唯一能做的就是检测应用是否被强杀,并且在被强杀之后重走启动流程而不是回到原先的逻辑当中。
以下给出我的一种实现方式,如果你有更好的想法,欢迎和我交流:
1 |
|
我们实例化上述场景,给出一种对应关系:
ActivityA –> 启动页
ActivityB –> 主页
ActivityC –> 详情页
其中将ActivityB的launchMode
设置为singleTask
,并且在BaseActivity中使用静态变量FLAG进行判断当前应用是否被强杀,如果被强杀则利用ActivityB的launchMode
特性清空栈并重新初始化即可。
在Android Stduio中系统默认内置了一个签名文件debug.keystore
,用于我们在debug下的默认App签名。如果没有在Gradle文件中特殊指定,那么Android Studio将自动使用默认的debug.keystore
文件为项目App生成Debug版本的签名。
在Mac/Linux系统中,debug.keystore
文件默认储存在~/.android/
路径下。
在Windows系统中,debug.keystore
文件将默认存储在C:\Users\{USERNAME}\.android\
路径下。
SHA-1
知道了Android Stduio 默认的debug.keystore
之后,下一步我们将是要获取其指纹信息,以便于在第三方服务配置中填入Debug指纹信息。
1 | keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android |
1 | keytool -list -v -keystore "%USERPROFILE%\.android\debug.keystore" -alias androiddebugkey -storepass android -keypass android |
回车执行之后,你将会看到类似下面的debug.keystore
输出提示:
1 | 别名: androiddebugkey |
我们将其中的证书指纹填入到第三方服务DEBUG配置中即可。当然了,有的时候出于这样或者那样的原因考虑,我们并不想使用系统默认的KeyStore或者就想自己生成一个新的KeyStore,Debug环境与Release环境都使用同一个来减少配置的麻烦。这个时候我们就需要创建一个新的KeyStore文件了。
我们使用JDK自带的Keytool命令行工具即可完成KeyStore密钥库文件的创建,此处需要说明的是,Android Stduio中自带的图形化界面KeyStore
生成工具生成的.jks
文件与Keytool生成的.keystore
文件在使用上没有任何区别。
在终端中键入以下命令:
1 | keytool -genkey -v -keystore {FILENAME.keystore} -alias {ALIAS} -keyalg RSA -validity {DURATION} |
{FILENAME.keystore}
为生成的KeyStore的文件名{ALIAS}
为生成的KeyStore文件的别名{DURATION}
为该KeyStore文件的过期时间下面将以生成一个test.keystore文件为示例:
1 | keytool -genkey -v -keystore test.keystore -alias test -keyalg RSA -validity 365 |
键入以上命令将生成一个以RSA算法加密的有效期365天的名为test.keystore
的文件,该KeyStore文件的alias为 test。回车确认执行该命令之后,将会要求输入密钥库口令以及一些基本的信息,根据提示输入无误之后将会在当前终端所在目录生成指定的KeyStore文件。完整的示例如下所示:
1 |
|
这样我们就有了一个全新的KeyStore文件可以用于Android的App签名,有了KeyStore文件下一步当然就是获取我们生成的KeyStore文件的指纹信息咯~
与获取默认的debug.keystore
文件指纹信息类似,我们在终端中键入以下命令:
1 | keytool -v -list -keystore test.keystore -alias test -keypass android -storepass android |
即可获取到我们生成的KeyStore指纹信息,有的同学已经看出来了,只要将上述命令中的几个参数替换下,即可查看任意KeyStore的指纹信息:
1 | keytool -v -list -keystore {FILENAME.keystore} -alias {ALIAS} -keypass {KEYPASSWD} -storepass {STOREPASSWD} |
{FILENAME.keystore}
为keystore文件名{ALIAS}
为KeyStore的别名{KEYPASSWD}
为KeyStore的密钥口令{STOREPASSWD}
为KeyStore的密钥库口令前面我们忙活了大半天生成了KeyStore文件,并查看其指纹信息。但是如果我们不使用到我们的项目中,毕竟还是不会对我们的项目生效的~我们还需要在Gradle脚本中对其进行配置,我们的项目才会应用其KeyStore文件。
其中我们有两种较为普遍的方式在项目中配置我们的KeyStore文件,第一种比较简单粗暴,直接在gradle构建脚本中写入KeyStore信息,第二种则将KeyStore信息配置在一个单独的配置文件中,在gradle构建时动态读取。
在Android Stduio中打开主moudle的build.gradle
文件,在其中的android
闭包中键入如下内容:
1 | signingConfigs { |
其中声明的release闭包中包含了keyAlias
、keyPassword
、storeFile
、storePassword
四个Property。其中含义分别为:
keyAlias
keystore的alias
keyPassword
KeyStore的密钥口令storeFile
为KeyStore的文件存放路径,可以为相对或者绝对路径,此处使用的为相对路径storePassword
为KeyStore的密钥库口令以上的Gradle DSL将会作用于我们的项目的Release版本,当我们在终端中个输入:
1 | gradlew assembleRelease |
项目将会使用我们上面定义的test.keystore
密钥库文件签名打包项目为Release发布版。
同样,如果我们不想使用默认的debug.keystore
签名项目的Debug版本,我们亦可以重新生成一个KeyStore文件或者使用Release版本的签名该文件,放入debug闭包中即可:
1 | signingConfigs { |
细心的同学可能发现了,虽然上面的把签名信息写入gradle脚本中比较方便省事,但是却在密钥文件的密钥密码泄露问题,任何能够看到此Moudle的build.gradle脚本的人都可以拿到KeyStore文件及其对应的密钥口令,可能会导致一些安全风险。因此在一些开源项目或者比较敏感的项目中,可能会存在类似的gradle配置:
在主moudle的build.gradle脚本的android闭包中:
1 | applicationVariants.all { |
其中Variants
翻译中文为变种
,applicationVariants.all
属性含义为app plugin
下所有的Variant
的配置信息,可以将其看作为一个总览,可以方便的访问所有对象。
相关延伸阅读Gradle Plugin User Guide,我们在其中通过project.hasProperty
读取项目中的配置,并将其动态的赋值给signingConfigs.release
下的相关属性。
然后我们通过在gradle.properties
或者其它项目中能够被gradle的文件中定义以上属性并赋值即可:
1 | storeFile=./keystore/test.keystore |
这样我们在项目团队协作时,将gradle.properties
文件忽略即可。
Enjoy IT!
]]>RxJava
配合Retrofit
能够大大简化Android项目中的网络请求代码量,使得逻辑更清晰,当然也可能会遇到一些问题。下面给出一种问题的解决方案。一个基本的RxJava配合Retrofit以及Lambda的网络调用看起来像这个样子的:
1 |
|
当Retrofit
中的网络请求返回码状态码为200
时,执行do Something
中的逻辑处理正常的
业务流程,但是当服务器返回状态码为非200
时,将会执行Ops Error
中的业务流程而不会
执行do Something
中的业务逻辑。
这样本没有什么问题,一般我们会在错误处理逻辑中在UI中给出错误提示,像这样:
1 | Log.e("Ops", "Error:" + throwable.getMessage()); |
但是这样的话我们只能获取到一服务器的错误响应码以及对应的简短的响应码错误说明,一般情况下我们服务器
都会包装错误信息为一个JSON,客户端解析错误信息必要的时候动态展示在UI上以提示用户。如果我们要拿到这样的JSON,使用throwable.getMessage()
这样做显然是不行的。是不是使用RxJava配合Retrofit只能拿到这样的错误Throwable信息呢?
显然不是的,其实服务器返回的错误信息非200
响应码的Response Body
JSON对象包含在这个throwable
对象中,我们可以这样将其解析出来:
1 | throwable -> { |
在Parse to JSON Obj
中将errorBody
解析为JSON对象进行相应的处理即可。
然而,你不能让我每个地方都加上这样的一段代码吧,既然我们使用的是RxJava
,我们可以让这种处理稍微看起来优雅点。以下以Jackson
为例:
由于RxJava
的错误异常处理接受一个参数,并且没有返回值,因此我们可以定义一个Action1
来替代默认的Error Action:
1 | public abstract class ErrorAction implements Action1<Throwable> { |
其中ErrorMessage
为我们定义好的错误消息Model:
1 | true) public class ResponseError { (ignoreUnknown = |
这样,我们就完成了一个自定义Action1
了,接下来我们便可以这样使用了:
1 |
|
其中在Do Error
中拿到ErrorMessage
对象,进行相应的对象操作即可~
Enjoy IT!
]]>由于最近在折腾Android项目,需要用到一些与服务器交互、以及数据存储的相关功能,然后发现了LeanCloud这家服务提供商,使用下来还感觉还挺靠谱的(请给我广告费)。正好发现他们服务提供了JavaScript SDK,于是就想着尝试着实现Hexo博客文章的浏览数统计功能,之前虽然在使用不蒜子,但是不蒜子不能够在主页展示文章阅读量啊!对于博主这种有强迫症又想装X的人来说果断不能忍啊!
本方法理论上对Hexo博客通用,由于博主使用的是NexT主题,所以当然针对NexT来说咯。NexT主题目前已经合并这个Feature,因此如果你使用的是NexT主题,可以直接使用不用修改主题模版而直接在_config.yml
中配置即可,请直接跳转查看配置LeanCloud
_config.yml
文件打开NexT主题的根目录下的_config.yml
文件,在任意位置添加以下内容:
1 | leancloud_visitors: |
lean-analytics.swig
文件在主题的layout\_scripts
路径下,新建一个lean-analytics.swig
文件,并向里面添加以下内容
1 | <!-- custom analytics part create by xiamo --> |
post.swig
文件在主题的layout\_macro
路径下,打开post.swig
文件,找到以下内容(大概88行):
1 | {% if not is_index and theme.facebook_sdk.enable and theme.facebook_sdk.like_button %} |
在其后面添加如下内容:
1 | {% if theme.leancloud_visitors.enable %} |
添加完毕之后,文件内容像这个样子:
1 | {% if not is_index and theme.facebook_sdk.enable and theme.facebook_sdk.like_button %} |
layout.swig
文件在NexT根目录的layout
路径下,打开 _layout.swig
文件,在</body>
上方添加如下内容:1
2
3{% if theme.leancloud_visitors.enable %}
{% include '_scripts/lean-analytics.swig' %}
{% endif %}
添加完成之后,文件内容像这个样子:
1 | {# LazyLoad #} |
zh-Hans.yml
文件在NexT目录的languages
路径下的zh-Hans.yml
文件,在post:
结点下添加visitors: 阅读次数
,像这个样子:1
2
3
4
5
6
7
8post:
posted: 发表于
visitors: 阅读次数
updated: 更新于
in: 分类于
read_more: 阅读全文
untitled: 未命名
toc_empty: 此文章未包含目录
如果你使用的是其它NexT的语言,请相应的添加该字段即可。
至此NexT的修改工作就完成了,但是现在还是不能够使用文章阅读量这个统计功能的。这个功能依赖于LeanCloud提供后端数据存取,因此我们需要注册一个LeanCloud帐号才能继续使用这个功能,点我快速注册.
在注册完成LeanCloud帐号并验证邮箱之后,我们就可以登录我们的LeanCloud帐号,进行一番配置之后拿到AppID
以及AppKey
这两个参数即可正常使用文章阅读量统计的功能了。
创建应用
:创建Class
来新建Class用来专门保存我们博客的文章访问量等数据:创建Class
之后,Counter
:无限制
。创建完成之后,左侧数据栏应该会多出一栏名为Counter
的栏目,这个时候我们点击顶部的设置,切换到test应用的操作界面:
在弹出的界面中,选择左侧的应用Key
选项,即可发现我们创建应用的AppID
以及AppKey
,有了它,我们就有权限能够通过主题中配置好的Javascript代码与这个应用的Counter表进行数据存取操作了:
复制AppID
以及AppKey
并在NexT主题的_config.yml
文件中我们相应的位置填入即可,正确配置之后文件内容像这个样子:
1 | leancloud_visitors: |
这个时候重新生成部署Hexo博客,应该就可以正常使用文章阅读量统计的功能了。需要特别说明的是:记录文章访问量的唯一标识符是文章的发布日期
以及文章的标题
,因此请确保这两个数值组合的唯一性,如果你更改了这两个数值,会造成文章阅读数值的清零重计。
当你配置部分完成之后,初始的文章统计量显示为0,但是这个时候我们LeanCloud对应的应用的Counter
表中并没有相应的记录,只是单纯的显示为0而已,当博客文章在配置好阅读量统计服务之后第一次打开时,便会自动向服务器发送数据来创建一条数据,该数据会被记录在对应的应用的Counter
表中。
我们可以修改其中的time
字段的数值来达到修改某一篇文章的访问量的目的(博客文章访问量快递提升人气的装逼利器)。双击具体的数值,修改之后回车即可保存。
url
字段被当作唯一ID
来使用,因此如果你不知道带来的后果的话请不要修改。title
字段显示的是博客文章的标题,用于后台管理的时候区分文章之用,没有什么实际作用。因为AppID以及AppKey是暴露在外的,因此如果一些别用用心之人知道了之后用于其它目的是得不偿失的,为了确保只用于我们自己的博客,建议开启Web安全选项,这样就只能通过我们自己的域名才有权访问后台的数据了,可以进一步提升安全性。
选择应用的设置的安全中心
选项卡:
在Web 安全域名
中填入我们自己的博客域名,来确保数据调用的安全:
如果你不知道怎么填写安全域名而或者填写完成之后发现博客文章访问量显示不正常,打开浏览器调试模式,发现如下图的输出:
这说明你的安全域名填写错误,导致服务器拒绝了数据交互的请求,你可以更改为正确的安全域名或者你不知道如何修改请在本博文中留言或者放弃设置Web安全域名。
Enjoy it!
]]> 由于最近做的一个Android项目需要用到用户的输入一些字符,常规的输入法输入非常的不方便。因此有必要自定义一个输入法来完成这个过程。此处给出一个简单的输入法Demo
来帮助理解自定义输入法的一些实现过程。
磨刀不误砍柴工,创建一个Android是需要一点点准备工作的,选择一个好的IDE能够提高我们编码的效率:
本博文的编写环境为Android Studio,如果你还在使用Eclipse的话,转到Android Studio上来吧!如果对上述IDE的下载感到茫然的话,推荐一个国内Android开发的好网站:http://www.androiddevtools.cn。
SimpleKeyboard
,如下图:Add No Activity
:点击Finish,这样就完成了我们Android Keyboard项目的创建了。这个等待Gradle构建完成,因为我们这个项目是没有Activity的,因此我们需要按照如下图稍微配置一下:
键盘在Android系统中被识别为一个输入法编辑器(IME),IME作为一个Service运行。只有在AndroidManifest.xml文件中通过android.permission.BIND_INPUT_METHOD
权限声明的Service并且响应android.view.im
这个元数据动作才能够被Android系统正确的识别为IME。因此,我们在AndroidManifest.xml
文件中的application标签对中添加如下代码:
1 | <service |
在上面的Service声明中,meta-data标签声明引用了一个叫做
method.xml
的文件,如果没有这个文件,那么Android系统将不能够识别我们的Service为一个有效的IME Service。这个文件包含了有关输入法及其子类的详细信息。
在我们的Demo中,我们定义一个subtyoe
来声明输入法的显示名称以及其语言环境:(如果没有res/xml目录的话,创建一个并将下面的内容添加到该目录的method.xml文件中)
1 |
|
如果你对label中直接写死字符串这种做法比较有强迫症的话,可以将其抽取到Strings.xml文件中。
我们的键盘布局比较简单,仅仅包含了一个KeyboardView,因此在layout
目录中创建一个keyboard.xml
文件,并添加一下内容:
1 |
|
其中需要说明的是:
layout_alignParentBottom
属性设置为true
来保证键盘会在设备屏幕的底端弹出而不是在其它什么地方弹出。keyPreviewLayout
属性用于按下按键时暂短的按键预览。这里我们由于图方便省事,就用一个TextView来预览吧~在layout
目录下创建一个preview.xml文件,并在其中添加如下内容:
1 |
|
键盘按键所代表的键值以及其位置等详细信息都被指定在一个xml文件中,每个独立的键盘按键至少都必须包含一下两个属性:
属性名 | 作用 |
---|---|
keyLabel | 决定这个按键上显示的字符信息 |
codes | 决定这个按键的对应的字符信息所代表的键值 |
例如:定义一个字母A的按键那么它的codes
属性值应该为:97,keyLabel
属性值应该为A。
如果一个按键关联了多个键值,那么点击该按键输出的字符依赖于敲击该按键的次数。
例如:如果一个按键拥有三个键值:63、33、58:
敲击该按键的次数 | 输出的字符 |
---|---|
1次 | ? |
2次 | ! |
3次 | : |
当然了,除了上述属性之外,一个按键还有一些其他的属性:
属性名 | 作用 |
---|---|
keyEdgeFlags | 可以设置的值有:right或者left,该属性通常用在一行最左边或者最右边的按键上用于表示按键的排布 |
keyWidth | 定义一个按键的宽度,通常该宽度值被定义为一个百分比值 |
isRepeatable | 如果这个属性被设置为true的话,长按被设置为该属性的按键将会在长按这段时间中多次重复该按键的动作。通过,在删除键或者空格键中设置该属性为true |
键盘上的按键通过Row标识为一组按键,比较推荐的做法是限制每组中最多10枚按键,这样的话,每个按键的宽度等于键盘宽度的10%。在本Demo中,按键高度被设置为60dp,这个数值可以任意调整。但是推荐不要低于48dp。
在res/xml
目录中,创建一个qwerty.xml
文件,并添加以下内容完成按键的定义:
1 |
|
创建一个java类,命名为SimpleIME.java
(与AndroidManifest.xml文件中的定义向对应):
SimpleIME
类应该继承至InputMethodService
类。SimpleIME
类应该实现OnKeyboardActionListener
接口,该接口包含了键盘被点击或者被按下时回调的一些函数。创建完成之后,将以下内容添加到该文件中:
1 | package tk.xiamo.notes.simplekeyboard; |
需要特别说明的是:一旦用户在键盘上按下了一个按键,onKey方法将会带着被按下的按键的所代表的键值参数而被调用,基于键值的不同,将会执行以下动作:
键值 | 指定的动作 |
---|---|
KEYCODE_DELETE | 将会使用deleteSurroundingText 方法删除光标左侧的一个字符。 |
KEYCODE_DONE | 将会激发一个KEYCODE_ENTER 事件。 |
KEYCODE_SHIFT | caps变量的值将会被改变并且通过setShifted方法更新键盘的状态。整个键盘都会被重新绘制来保证状态改变之后按键的label标签可以被更新。其中:invalidateAllKeys 方法将可以重绘所有按键。 |
普通键值 | 将会被简单的转换为一个字符然后发送到文本输入域中,如果caps变量被设置为true,那么按键字符都将被转换为大写。 |
编译运行,在手机上运行的效果如下图:
]]>