基于Docker+Jenkins+Gitlab搭建持续集成环境

随着DevOps理念和敏捷理念的发展,我们希望通过自动化技术,加快项目的迭代。尤其是当使用微服务方案后,面临在大量的项目构建和部署工作,借助于jenkins的持续集成,可以快速把应用打包成docker镜像,实现自动部署。

持续集成.png

如图演示了以下的场景:

  • 开发者向自己的gitlab网站提交了代码
  • jenkins通过定时任务检测到了代码有变成,执行自动化构建过程
  • jenkins在自动化构建脚本中调用docker命令将构建好的镜像push 私有镜像注册中心
  • 同时,jenkins也可以直接执行remote shell启动构建好的容器
  • 构建失败或者成功,可以及时将结果推送给相关人员,比如测试人员,安排测试
  • 服务端可以手动通过docker命令,从镜像仓库中心拉取镜像,进行手动部署

我搭建的环境都是在本地,gitlab、jenkins、docker私有仓库都部署在本地,以下是操作步骤:

搭建docker私有仓库

使用docker拉取registry镜像,然后启动容器

docker run -d -p 5000:5000 -v ~/docker-registry:/tmp/registry registry

这样就可以在本地运行一个私有镜像注册中心,通过镜像名称前缀127.0.0.1:5000可以将镜像推送到这个地址

搭建gitlab

拉取gitlab镜像,并启动

docker pull gitlab/gitlab-ce
sudo docker run --detach \
--hostname gitlab.bill.com \
--publish 443:443 --publish 80:80 --publish 22:22 \
--name gitlab \
--restart always \
--volume ~/gitlab/config:/etc/gitlab \
--volume ~/gitlab/logs:/var/log/gitlab \
--volume ~/gitlab/data:/var/opt/gitlab \
gitlab/gitlab-ce:latest

因为部署在本地,又指定了gitlab.bill.com作为域名,所以在/etc/hosts配置下,这样可以通过域名访问gitlab。

127.0.0.1 gitlab.bill.com

初次登录可以修改密码,或者重新注册一个用户,我注册了一新用户,并在gitlab中创建了一个demo项目

demo.png

创建了一个空项目后,然后根据github上的提示,将本地的git项目推送到gitlab

Gitlab_http.png

上传完后Gitlab项目如下:

Demo sourcecode.png

搭建jenkins

安装jenkins
使用docker 下载jenkins镜像 jenkinsci/jenkins,并启动

docker run -d \
-p 8080:8080 \
-p 50000:50000 \
--name jenkins \
--link gitlab:gitlab.bill.com \
-u root \
-v ~/jenkins:/var/jenkins_home  \
-v /usr/share/maven:/usr/local/maven \
-v /usr/lib/jvm/jdk1.8:/usr/local/jdk 
jenkinsci/jenkins:latest

8080端口是jenkins的端口,5000端口是master和slave通信端口(没错,jenkins可以部署集群,在本次中没有配置)
并将宿主机maven和jdk映射到对应的容器目录上,同时通过配置–link连接gitlab,因为要从gitlab下代码。

初次启动的时候,可以通过docker logs -f jenkins查看控制台的密码,通过这个密码登录系统,执行创建用户等操作

安装插件
启动项目后,下载所需的插件(尤其要下载Git Plugin和Maven Intergration Plugin插件),如果缺少Maven Integration Plugin插件,在创建job时,不会有新建一个maven项目选项

jenkins plugin.png

注意:在Global Tool Configuration中配置Maven和Jdk,按道理可以配置通过配置JAVA_HOME 和MAVEN_HOME,分别指向/usr/local/jdk,/usr/local/maven,但是通过测试JAVA_HOME生效了,但MAVEN_HOME没成效,提示说没有找到maven,所以又通过自动化安装重新配置了maven

自动安装maven.png

配置项目
新建一个项目,输入项目名称demo,并选择Maven project,然后在配置中做如下配置:

General Info.png
Source Code Management.png
Build Triggers.png

每隔5检查是否有新代码发布,如果有则自动执行构建

Pre Steps.png
Build Settings.png

点开项目,点击左侧菜单build now则立马开始一次构建,点开当前构建信息,左侧Console Output中,可查看构建的明细

Console Output.png

集成docker部署

在上述的命令中,基本完成集成,但是还没有使用docker,构建的jar包也无法推送到测试环境或者生产环境,我们可以通过如下思路解决:

在项目生成jar包之后,调用项目中Dockerfile文件,使用docker构建镜像,将镜像推送到私有镜像中心的同时,使用该镜像启动一个容器(启动前删除同名容器)

代码如下所示:

# 定义变量
API_NAME="demo"
API_VERSION="0.0.1"
API_PORT=58080
IMAGE_NAME="127.0.0.1:5000/billjiang/$API_NAME:$BUILD_NUMBER"
CONTAINER_NAME=$API_NAME-$API_VERSION

# 进入target 目录复制Dockerfile 文件
cd $WORKSPACE/target
cp classes/Dockerfile .

#构建docker 镜像
docker build -t $IMAGE_NAME .

#推送docker镜像
docker push $IMAGE_NAME

#删除同名docker容器
cid=$(docker ps | grep "$CONTAINER_NAME" | awk '{print $1}')
if [ "$cid" != "" ]; then
   docker rm -f $cid
fi

#启动docker 容器
docker run -d -p $API_PORT:8080 --name $CONTAINER_NAME $IMAGE_NAME

#删除 Dockerfile 文件
rm -f Dockerfile

以上未声明的变量$WORKSPACE为jenkins的变量

Post Steps中通过配置以上Execute shell命令,并不能成功执行,发现在Console Output中出现docker not found 的错误,是因为jenkins 的容器没有安装docker。所以解决这个问题要么jenkins不使用docker容器安装,要自己安装一个带有docker的jenkins容器,当然还有docker in docker的类似解决方案,本人觉得独立安装jenkins也许算的上一种比较好的方案。

 

MAC删除开机自启动程序

一些程序在安装后会设置开机启动,如果不希望这些程序开机启动,可以通过以下办法禁止掉

打开「系统偏好」选择「用户与群组」,选择当前用户,并在右侧选择登录项标签,开机启动的程序会在这里列出来,想禁止哪一个,选中,并点击下方的➖按钮即可。

一般通过以上方法即可禁止许多应用,但也有一些应用并未出现用户的登陆项列表里,此时就需要别的办法禁止其开机启动。

部分应用在安装时会生成几个plist文件,用以设置开机启动。这些文件大多存在于一下几个位置

/Library/LaunchDaemons/
/Library/LaunchAgents/
~/Library/LaunchAgents/

其中~开头的是用户目录,/开头的是系统目录。在这些目录下存在着以程序名命名的plist文件,或者包含程序名。
找到某个程序对应的plist文件后,可通过下面的命令来取消其开机启动

launchctl unload com.razer.rzupdater.plist # 禁止razer程序的开机启动

Ansible之roles介绍

Ansible之roles介绍

本节内容:

  • 什么场景下会用roles?
  • roles示例

一、什么场景下会用roles?

假如我们现在有3个被管理主机,第一个要配置成httpd,第二个要配置成php服务器,第三个要配置成MySQL服务器。我们如何来定义playbook?

第一个play用到第一个主机上,用来构建httpd,第二个play用到第二个主机上,用来构建php,第三个play用到第三个主机上,用来构建MySQL。这些个play定义在playbook中比较麻烦,将来也不利于模块化调用,不利于多次调。比如说后来又加进来一个主机,这个第4个主机既是httpd服务器,又是php服务器,我们只能写第4个play,上面写上安装httpd和php。这样playbook中的代码就重复了。

为了避免代码重复,roles能够实现代码重复被调用。定义一个角色叫websrvs,第二个角色叫phpappsrvs,第三个角色叫dbsrvs。那么调用时如下来调用:

hosts: host1
role:
- websrvs

hosts: host2
role:
- phpappsrvs
        
hosts: host3
role:
- dbsrvs
        
hosts: host4
role:
- websrvs
- phpappsrvs
这样代码就可以重复利用了,每个角色可以被独立重复调用。下面举例说明使用方式。

二、roles示例

假设有3台主机,172.16.7.151主机上安装MySQL,172.16.7.152上安装httpd,172.16.7.153上安装MySQL和httpd。我们建立两个角色websrvs和dbsrvs,然后应用到这几个主机上。

1. 创建roles的必需目录 

[root@node1 opt]# mkdir -pv ansible_playbooks/roles/{websrvs,dbsrvs}/{tasks,files,templates,meta,handlers,vars}

每个role下面有个目录叫meta,在里面可以新建文件main.yml,在文件中可以设置该role和其它role之前的关联关系。

2. 配置角色

(1)配置角色websrvs

[root@node1 opt]# cd ansible_playbooks/roles/
[root@node1 roles]# cd websrvs/
[root@node1 websrvs]# ls
files  handlers  meta  tasks  templates  vars

a. 将httpd配置文件上传到files目录下,我这里假设httpd.conf每台主机都是一样的,实际上应该用模板,先用一样的配置文件举例

[root@node1 websrvs]# cp /etc/httpd/conf/httpd.conf files/

直接复制的静态文件都放在files目录下。打算用模板文件的都放在templates目录下。

b.编写任务列表tasks

[root@node1 websrvs]# vim tasks/main.yml
- name: install httpd package
  yum: name=httpd
- name: install configuration file
  copy: src=httpd.conf dest=/etc/httpd/conf
  tags:
  - conf
  notify:
  - restart httpd
- name: start httpd
  service: name=httpd state=started

c.由于上面的tasks中定义了notify,所以要定义handlers

[root@node1 websrvs]# vim handlers/main.yml
- name: restart httpd
  service: name=httpd state=restarted

如果需要定义变量,则在vars目录下创建main.yml文件,在文件中写入变量,以key:value的形式定义,比如:

http_port: 8080

(2)配置角色dbsrvs

[root@node1 roles]# cd dbsrvs/
[root@node1 dbsrvs]# ls
files  handlers  meta  tasks  templates  vars

a.将MySQL配置文件上传到files目录下。

b.编写任务列表tasks

[root@node1 dbsrvs]# vim tasks/main.yml
- name: install mysql-server package
  yum: name=mysql-server state=latest
- name: install configuration file
  copy: src=my.cnf dest/etc/my.cnf
  tags:
  - conf
  notify:
  - restart mysqld
- name:
  service: name=mysqld enabled=true state=started
c.定义handlers
[root@node1 dbsrvs]# vim handlers/main.yml
- name: restart mysqld
  service: name=mysqld state=restarted

(3)定义playbook

【注意】:要在roles目录同级创建playbook。

[root@node1 ansible_playbooks]# vim web.yml
- hosts: 172.16.7.152
  roles:
  - websrvs 

[root@node1 ansible_playbooks]# vim db.yml
- hosts: 172.16.7.151
  roles:
  - dbsrvs 

[root@node1 ansible_playbooks]# vim site.yml
- hosts: 172.16.7.153
  roles:
  - websrvs
  - dbsrvs
运行:
[root@node1 ansible_playbooks]# ansible-playbook web.yml
[root@node1 ansible_playbooks]# ansible-playbook db.yml
[root@node1 ansible_playbooks]# ansible-playbook site.yml

当然也可以把这些内容写入同一个playbook中。playbook的名字可以自定义。

cordova–入门 webview_url修改,打开web时跳出app

cordova–入门 webview_url修改,打开web时跳出app

若url为http打头的,需要做如下修改:

修改web的网址:

cordova初始化:
安装cordova cli
1、下载安装node.js(js的运行环境),安装时勾选npm(js包的管理工具)。
2、下载安装git(配置好环境变量)。
3、使用npm安装cordova。

Swift – 封装一个正则表达式工具类(附:正则替换、正则匹配样例)

之前我写过一篇文章介绍如何使用正则表达式来验证用户名、邮箱、URL 等格式是否正确(点击查看)。除了验证数据外,我们还可以使用正则表达式进行文字替换、或者提取工作。下面通过样例进行演示。

一、封装一个正则工具类(Regex.swift)

由于 NSRegularExpression 使用起来十分繁琐,为方便使用,我们首先对它进行封装。增加一些常用的正则处理方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
import Foundation
/// 基于NSRegularExpression api 的正则处理工具类
public struct Regex {
    private let regularExpression: NSRegularExpression
    
    //使用正则表达式进行初始化
    public init(_ pattern: String, options: Options = []) throws {
        regularExpression = try NSRegularExpression(
            pattern: pattern,
            options: options.toNSRegularExpressionOptions
        )
    }
    
    //正则匹配验证(true表示匹配成功)
    public func matches(_ string: String) -> Bool {
        return firstMatch(in: string) != nil
    }
    
    //获取第一个匹配结果
    public func firstMatch(in string: String) -> Match? {
        let firstMatch = regularExpression
            .firstMatch(in: string, options: [],
                        range: NSRange(location: 0, length: string.utf16.count))
            .map { Match(result: $0, in: string) }
        return firstMatch
    }
    
    //获取所有的匹配结果
    public func matches(in string: String) -> [Match] {
        let matches = regularExpression
            .matches(in: string, options: [],
                     range: NSRange(location: 0, length: string.utf16.count))
            .map { Match(result: $0, in: string) }
        return matches
    }
    
    //正则替换
    public func replacingMatches(in input: String, with template: String,
                                 count: Int? = nil) -> String {
        var output = input
        let matches = self.matches(in: input)
        let rangedMatches = Array(matches[0..<min(matches.count, count ?? .max)])
        for match in rangedMatches.reversed() {
            let replacement = match.string(applyingTemplate: template)
            output.replaceSubrange(match.range, with: replacement)
        }
        
        return output
    }
}
//正则匹配可选项
extension Regex {
    /// Options 定义了正则表达式匹配时的行为
    public struct Options: OptionSet {
        
        //忽略字母
        public static let ignoreCase = Options(rawValue: 1)
        
        //忽略元字符
        public static let ignoreMetacharacters = Options(rawValue: 1 << 1)
        
        //默认情况下,“^”匹配字符串的开始和结束的“$”匹配字符串,无视任何换行。
        //使用这个配置,“^”将匹配的每一行的开始,和“$”将匹配的每一行的结束。
        public static let anchorsMatchLines = Options(rawValue: 1 << 2)
        
        ///默认情况下,"."匹配除换行符(\n)之外的所有字符。使用这个配置,选项将允许“.”匹配换行符
        public static let dotMatchesLineSeparators = Options(rawValue: 1 << 3)
        
        //OptionSet的 raw value
        public let rawValue: Int
        
        //将Regex.Options 转换成对应的 NSRegularExpression.Options
        var toNSRegularExpressionOptions: NSRegularExpression.Options {
            var options = NSRegularExpression.Options()
            if contains(.ignoreCase) { options.insert(.caseInsensitive) }
            if contains(.ignoreMetacharacters) {
                options.insert(.ignoreMetacharacters) }
            if contains(.anchorsMatchLines) { options.insert(.anchorsMatchLines) }
            if contains(.dotMatchesLineSeparators) {
                options.insert(.dotMatchesLineSeparators) }
            return options
        }
        
        //OptionSet 初始化
        public init(rawValue: Int) {
            self.rawValue = rawValue
        }
    }
}
//正则匹配结果
extension Regex {
    // Match 封装有单个匹配结果
    public class Match: CustomStringConvertible {
        //匹配的字符串
        public lazy var string: String = {
            return String(describing: self.baseString[self.range])
        }()
        
        //匹配的字符范围
        public lazy var range: Range<String.Index> = {
            return Range(self.result.range, in: self.baseString)!
        }()
        
        //正则表达式中每个捕获组匹配的字符串
        public lazy var captures: [String?] = {
            let captureRanges = stride(from: 0, to: result.numberOfRanges, by: 1)
                .map(result.range)
                .dropFirst()
                .map { [unowned self] in
                    Range($0, in: self.baseString)
            }
            
            return captureRanges.map { [unowned self] captureRange in
                if let captureRange = captureRange {
                    return String(describing: self.baseString[captureRange])
                }
                
                return nil
            }
        }()
        
        private let result: NSTextCheckingResult
        
        private let baseString: String
        
        //初始化
        internal init(result: NSTextCheckingResult, in string: String) {
            precondition(
                result.regularExpression != nil,
                "NSTextCheckingResult必需使用正则表达式"
            )
            
            self.result = result
            self.baseString = string
        }
        
        //返回一个新字符串,根据“模板”替换匹配的字符串。
        public func string(applyingTemplate template: String) -> String {
            let replacement = result.regularExpression!.replacementString(
                for: result,
                in: baseString,
                offset: 0,
                template: template
            )
            
            return replacement
        }
        
        //藐视信息
        public var description: String {
            return "Match<\"\(string)\">"
        }
    }
}

 

二、使用样例

1,验证字符串格式

下面样例验证一个邮箱地址的格式是否正确。
1
2
3
4
5
6
7
8
9
10
11
//初始化正则工具类
let pattern = "^([a-z0-9_\\.-]+)@([\\da-z\\.-]+)\\.([a-z\\.]{2,6})$"
let regex = try! Regex(pattern)
//验证邮箱地址
let mailAddress = "admin@hangge.com"
if regex.matches(mailAddress) {
    print("邮箱地址格式正确")
}else{
    print("邮箱地址格式有误")
}

 

2,提取字符串

(1)获取第一个匹配结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//初始化正则工具类
let pattern = "([\\u4e00-\\u9fa5]+):([\\d]+)"
let regex = try! Regex(pattern)
//原始字符串
let str = "王大锤:123456,李子明:23457,李洛克:110"
//获取第一个匹配对象
if let first = regex.firstMatch(in: str) {
    print("--- 第一个匹配结果  ---")
    print(first)
    print("匹配字符串:", first.string)
    print("捕获组:", first.captures[0]!, first.captures[1]!)
    print("匹配范围:", first.range)
}

(2)获取所有的匹配结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//初始化正则工具类
let pattern = "([\\u4e00-\\u9fa5]+):([\\d]+)"
let regex = try! Regex(pattern)
//原始字符串
let str = "王大锤:123456,李子明:23457,李洛克:110"
//获取第一个匹配对象
for match in regex.matches(in: str) {
    print("\n--- 匹配结果  ---")
    print(match)
    print("匹配字符串:", match.string)
    print("捕获组:", match.captures[0]!, match.captures[1]!)
    print("匹配范围:", match.range)
}

3,字符串替换

(1)简单的替换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//初始化正则工具类
let pattern = "([\\u4e00-\\u9fa5]+):([\\d]+)"
let regex = try! Regex(pattern)
//原始字符串
let str = "王大锤:123456,李子明:23457,李洛克:110"
//只替换第1个匹配项
let out1 = regex.replacingMatches(in: str, with: "***", count: 1)
//替换所有匹配项
let out2 = regex.replacingMatches(in: str, with: "***")
  
//输出结果
print("原始的字符串:", str)
print("替换第1个匹配项:", out1)
print("替换所有匹配项:", out2)

(2)捕获组替换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//初始化正则工具类
let pattern = "([\\u4e00-\\u9fa5]+):([\\d]+)"
let regex = try! Regex(pattern)
//原始字符串
let str = "王大锤:123456,李子明:23457,李洛克:110"
//只替换第1个匹配项
let out1 = regex.replacingMatches(in: str, with: "$1的电话是$2", count: 1)
//替换所有匹配项
let out2 = regex.replacingMatches(in: str, with: "$1的电话是$2")
  
//输出结果
print("原始的字符串:", str)
print("替换第1个匹配项:", out1)
print("替换所有匹配项:", out2)

 

Swift脚本编程

用命令启动REPL时,使用的就是Bash Shell.

#!/usr/bin/swift

import Foundation

class Execution {

    class func execute(path: String, arguments: [String]? = nil) -> Int {

        let task = Process()

        task.launchPath = path

        if arguments != nil {

            task.arguments = arguments!

        }

        task.launch()

        task.waitUntilExit()

        return Int(task.terminationStatus)

    }

}

var status : Int = 0

status = Execution.execute(path: “/bin/ls”)

print(“Status = \(status)”)

status = Execution.execute(path: “/bin/ls”, arguments: [“/”])

print(“Status = \(status)”)

将此文件保存为:swiftScript3.sh

chmod +x swiftScript3.sh

./swiftScript3.sh

几种常见的程序命名规则

正确并形象地给函数、变量命名,不仅可以增加程序的可读性,也是程序员编程风格的一种反映。较好的命名习惯,可以有效的提高程序的可维护性。以下介绍几种常用的变量命名规则。

一、匈牙利命名法【Hungarian】:广泛应用于Microsoft Windows这类环境中。

这种命名技术是由一位能干的 Microsoft 程序员查尔斯·西蒙尼(Charles Simonyi) 提出的。匈牙利命名法通过在变量名前面加上相应的小写字母的符号标识作为前缀,标识出变量的作用域,类型等。这些符号可以多个同时使用,顺序是先m_(成员变 量),再指针,再简单数据类型,再其他。例如:m_lpszStr, 表示指向一个以0字符结尾的字符串的长指针成员变量。

匈牙利命名法关键是:标识符的名字以一个或者多个小写字母开头作为前缀;前缀之后的是首字母大写的一个单词或多个单词组合,该单词要指明变量的用途。

例如:bEnable, nLength, hWnd。

匈牙利命名法中常用的小写字母的前缀:

前缀 类型 描述
a Array 数组
b BOOL 布尔
by BYTE 无符号字符
c char 字符
cb Count of bytes 字节数
cr Color reference value 颜色值
cx,cy Count of x,y(short) 长度
dw DWORD 双字(无符号长整形)
f Flags 标志
fn Function 函数
g_ Global 全局的
h HANDLE 句柄
i Integer(int) 整数
l Long(long) 长整数
lp Long point 长指针
m_ Data member of a class 类的数据成员
n Short(short) 短整型
np Near point 短指针
p Point 指针
s String 字符串
sz Zero terminated string 以0结尾的字符串
tm Text metric 文本规则
u Unsigned int 无符号整数
ul Unsigned long(ULONG) 无符号长整数
w WORD 无符号短整数
x,y x,y coordinates(short) 坐标
v Void

有关项目的全局变量用g_开始,类成员变量用m_。

前缀 类型 例子
C CDocument, CPrintInfo
m_ 成员变量 m_pDoc, m_nCustomers
g_ 全局变量 g_Servers

二、驼峰命名法【camelCase】:近年来越来越流行。

驼峰命名法,正如它的名称所表示的那样,指的是混合使用大小写字母来构成标识符的名字。其中第一个单词首字母小写,余下的单词首字母大写。
例如:

printEmployeePaychecks();

函数名中每一个逻辑断点都有一个大写字母来标记。

三、帕斯卡(Pascal)命名法:与驼峰命名法类似。
只不过驼峰命名法是第一个单词首字母小写,而帕斯卡命名法则是第一个单词首字母大写。因此这种命名法也有人称之为“大驼峰命名法”。
例如:

DisplayInfo();

UserName

都是采用了帕斯卡命名法。
在C#中,以帕斯卡命名法和骆驼命名法居多。
事实上,很多程序设计者在实际命名时会将驼峰命名法和帕斯卡结合使用,例如变量名采用驼峰命名法,而函数采用帕斯卡命名法。

四、下划线命名法。

下划线法是随着C语言的出现流行起来的,在UNIX/LIUNX这样的环境,以及GNU代码中使用非常普遍。
4.1 函数的命名
函数名使用下划线分割小写字母的方式命名:
设备名_操作名();

操作名一般采用:谓语(此时设备名作为宾语或者标明操作所属的模块)或者谓语+宾语/表语(此时设备名作为主语或者标明操作所属的模块) 等形式,如:
tic_init();
adc_is_busy();
uart_tx_char();

中断函数的命名直接使用 设备名_isr() 的形式命名,如:
timer2_isr();

4.2 变量的命名
变量的命名也采用下划线分割小写字母的方式命名。命名应当准确,不引起歧义,且长度适中。如:
int length;
uint32 test_offset;

单字符的名字也是常用的,如i, j, k等,它们通常可用作函数内的局部变量。tmp常用做临时变量名。
局部静态变量,应加s_词冠(表示static),如:
static int s_lastw;

全局变量(尤其是供外部访问的全局变量),应加g_词冠(表示global),如:
void (* g_capture_hook)(void);

4.3 常量及宏的命名
采用下划线分割大写字母的方式命名,一般应以设备名作为前缀,
防止模块间命名的重复。如:
#define TIMER0_MODE_RELOAD                2
#define TIMER2_COUNT_RETRIEVE(val)        ((uint16)(65536 – (val)))

  当然,看作接口的宏可以按照函数的命名方法命名,例如:
#define timer2_clear()                      (TF2 = 0)
#define timer0_is_expired()                (TF0)
  据考察,没有一种命名规则可以让所有的程序员赞同,程序设计教科书一般都不指定命名规则。命名规则对软件产品而言并不是“成败悠关”的事,我们不要化太多精力试图发明世界上最好的命名规则,而应当制定一种令大多数项目成员满意的命名规则,并在项目中贯彻实施。

 

Swift中安全优雅的使用UserDefaults

纳尼? 如此简单的 UserDefaults 怎么去优雅的使用? 这么简单的还能玩出花来? 没毛病吧?

嗯, 没毛病!


Objective-C 中的 NSUserDefaults 我们并不陌生, 通常作为数据持久化的一种方式, 一般用来存储用户信息和基础配置信息. Swift 中使用 UserDefaults 来替代 NSUserDefaults, 两者的使用基本相同.

let defaults = UserDefaults.standard
defaults.set(123, forKey: "defaultKey")
defaults.integer(forKey: "defaultKey")

Objective-C中需要调用 synchronize 方法进行同步, 但是在Swift中已经废弃了该方法, 所以不需要手动去调用.
-synchronize is deprecated and will be marked with the NS_DEPRECATED macro in a future release.

上面的用法是最基本的用法, 也是我们平常开发中使用频率最高的用法, 但也是最危险的用法, 为什么呢?

  1. 在应用内部我们可以随意地覆盖和删除存储的值, 直接使用字符串来作为存储数据的 key 是非常危险的, 容易导致存数据时使用的 key 和取数据的时候使用的 key 不一致.
  2. UserDefaults.standard 是一个全局的单例, 如果需要存储账户信息(AccountInfo), 配置信息(SettingInfo), 此时按照最基本的使用方式, 简单的使用 key 来存取数据, 那么 key 值会随着存储的数据越来越多, 到时候不管是新接手的小伙伴还是我们自己都很难明白每个 key 值对应的意义. 也就是说我们不能根据方法调用的上下文明确知道我存取数据的具体含义, 代码的可读性和可维护性就不高.所以我们要利用 Swift 强大的灵活性来让我们使用 UserDefaults 存取数据的时候更加便捷和安全.

所以要想把 UserDefaults 玩出花来就得解决下面两个问题:

  • 一致性
  • 上下文

一致性

使用 UserDefaults 存取数据时使用的 key 值不同就会导致存在一致性问题. 原因就在于通常我们在存取数据的时候, 手动键入 key 或者复制粘贴 key 可能会出错, 输入的时候也很麻烦. 那我们的目的就比较明确了, 就是为了让存取的 key 一致, 即使改了其中一个另外一个也随之更改.

解决办法:

  • 常量保存
  • 分组存储

常量保存字符串

既然涉及到两个重复使用的字符串, 很容易就想到用常量保存字符串, 只有在初始化的时候设置 key 值, 存取的时候拿来用即可, 简单粗暴的方式.

let defaultStand = UserDefaults.standard
let defaultKey = "defaultKey"
defaultStand.set(123, forKey: defaultKey)
defaultStand.integer(forKey: defaultKey)

是不是感觉有点换汤不换药? 上面使用常量存储 key 值, 虽然能够保证存取的时候 key 值相同, 但是在设置 key 值的时候稍显麻烦.
最重要的一点就是如果需要存很多账户信息或者配置信息的时候, 按照这种方式都写在同一处地方就稍微欠妥, 比如下面这个场景, 在 app 启动后, 需要存储用户信息和登录信息, 用户信息里面包含: userName, avatar, password, gender等, 登录信息里包含: token, userId, timeStamp等等, 也就说需要存两类不同的信息, 那么此时这种方式就不合时宜了, 我们就会想办法把同类的信息归为一组, 进行分组存取.

分组存储

分组存储 key 可以把存储数据按不同类别区分开, 代码的可读性和可维护性大大提升. 我们可以采用类class, 结构体struct, 枚举enum来进行分组存储 key, 下面使用结构体来示例.

// 账户信息
struct AccountInfo {
    let userName = "userName"
    let avatar = "avatar"
    let password = "password"
    let gender = "gender"
    let age = "age"
    
}
// 登录信息
struct LoginInfo {
    let token = "token"
    let userId = "userId"
}
// 配置信息
struct SettingInfo {
    let font = "font"
    let backgroundImage = "backgroundImage"
}

存取数据:

let defaultStand = UserDefaults.standard
// 账户信息
defaultStand.set("Chilli Cheng", forKey: AccountInfo().avatar)
defaultStand.set(18, forKey: AccountInfo().age)
// 登录信息
defaultStand.set("achj167", forKey: LoginInfo().token)
// 配置信息
defaultStand.set(24, forKey: SettingInfo().font)
        
let userName = defaultStand.string(forKey: AccountInfo().avatar)
let age = defaultStand.integer(forKey: AccountInfo().age)
let token = defaultStand.string(forKey: LoginInfo().token)
let font = defaultStand.integer(forKey: SettingInfo().font)

上下文

上面这种方式是不是比直接使用常量的效果更好? 但是仍然有个问题, 账户信息, 登录信息, 配置信息都是属于要存储的信息, 那我们就可以把这三类信息归到一个大类里, 在这个大类中有这三个小类, 三个小类作为大类的属性, 既能解决一致性问题, 又能解决上下文的问题, 需要存储到 UserDefaults 里面的数据, 我只需要去特定的类中找到对应分组里面的属性即可. 示例:

struct UserDefaultKeys {
    // 账户信息
    struct AccountInfo {
        let userName = "userName"
        let avatar = "avatar"
        let password = "password"
        let gender = "gender"
        let age = "age"
    }
    // 登录信息
    struct LoginInfo {
        let token = "token"
        let userId = "userId"
    }
    // 配置信息
    struct SettingInfo {
        let font = "font"
        let backgroundImage = "backgroundImage"
    }
}

存取数据:

let defaultStand = UserDefaults.standard
// 账户信息
defaultStand.set("Chilli Cheng", forKey:UserDefaultKeys.AccountInfo().userName)
defaultStand.string(forKey: UserDefaultKeys.AccountInfo().userName)

上面的代码看起来可读性好了很多, 不仅是为了新接手的小伙伴能看懂, 更是为了我们自己过段时间能看懂. 我亲眼见过自己写的代码看不懂反而要进行重构的小伙伴.

避免初始化

但是上面的代码存在一个明显的缺陷, 每次存取值的时候需要初始化 struct 出一个实例, 再访问这个实例的属性获取 key 值, 其实是不必要的, 怎么才能做到不初始化实例就能访问属性呢? 可以使用静态变量, 直接通过类型名字访问属性的值.

struct AccountInfo {
    static let userName = "userName"
    static let avatar = "avatar"
    static let password = "password"
    static let gender = "gender"
    static let age = "age"
}

存取的时候:

defaultStand.set("Chilli Cheng", forKey: UserDefaultKeys.AccountInfo.userName)
defaultStand.string(forKey: UserDefaultKeys.AccountInfo.userName)

枚举分组存储

上面的方法虽然能基本满足要求, 但是仍然不完美, 我们依然需要手动去设置 key, 当 key 值很多的时候, 需要一个个的设置, 那有没有可以一劳永逸的办法呢? 不需要我们自己设置 key 的值, 让系统默认给我们设置好 key 的初始值, 我们直接拿 key 去进行存取数据. Swift这么好的语言当然可以实现, 即用枚举的方式, 枚举不仅可以分组设置 key, 还能默认设置 key 的原始值. 前提是我们需要遵守 String 协议, 不设置 rawValue 的时候, 系统会默认给我们的枚举 case 设置跟成员名字相同的原始值(rawValue), 我们就可以拿这个 rawValue 来作为存取数据的 key.

struct UserDefaultKeys {
    // 账户信息
    enum AccountInfo: String {
        case userName
        case age
    }
}

// 存账户信息
defaultStand.set("Chilli Cheng", forKey: UserDefaultKeys.AccountInfo.userName.rawValue)
defaultStand.set(18, forKey: UserDefaultKeys.AccountInfo.age.rawValue)

// 取存账户信息
defaultStand.string(forKey: UserDefaultKeys.AccountInfo.userName.rawValue)
defaultStand.integer(forKey: UserDefaultKeys.AccountInfo.age.rawValue)

吼吼, 是不是感觉很方便, Swift 太棒了!
上面基本就能达到我们的目的, 既解决了一致性问题, 又有上下文知道我存取数据使用的 key 的含义. 但是代码看起来很冗余, 我不就需要一个key 嘛, 干嘛非要链式调用那么多层呢? 还有就是为啥我非要写 rawValue 呢? 如果新来的小伙伴不知道 rawValue 是什么鬼肯定懵逼.

优化 key 值路径

虽然上面的代码能很好的达到目的, 但是写法和使用上还是欠妥, 我们仍需要继续改进, 上面的代码主要存在两个问题:

  • key 值路径太长
  • rawValue 没必要写

我们先分析一下为什么会出现这个两个问题:
key值的路径长是因为我们想分组存储 key, 让key具有上下文, 可读性更改,
rawValue 的作用是因为我们使用枚举来存储 key, 就不需要去手动设置 key 的初始值.

看起来简直是”鱼和熊掌不能兼得”, 有什么办法能解决”鱼和熊掌”的问题呢?
那就是”砍掉抓着鱼的熊掌”. 也就是说我们必须先解决一个问题(先让熊抓鱼), 再想法”砍熊掌”.

有了上面的一系列步骤, 解决第一个问题并不像刚开始一样使用简单的字符串, 而必须是使用枚举, 在这个前提下去”抓鱼”. 也就是我能不能直接传枚举成员值进去, 先利用枚举的 rawValue 解决第一个问题,例如这样使用:

defaultStand.set("Chilli Cheng", forKey: .userName)
defaultStand.string(forKey: .userName)

很明显能够实现, 只要给 userDefaults 扩展自定义方法即可, 在自定义方法中调用系统的方法进行存取, 为了使用方便我们扩展类方法.示例:

extension UserDefaults {
    enum AccountKeys: String {
        case userName
        case age
    }
    
    static func set(value: String, forKey key: AccountKeys) {
        let key = key.rawValue
        UserDefaults.standard.set(value, forKey: key)
    }

    static func string(forKey key: AccountKeys) -> String? {
        let key = key.rawValue
        return UserDefaults.standard.string(forKey: key)
    }
}

// 存取数据
UserDefaults.set(value: "chilli cheng", forKey: .userName)
UserDefaults.string(forKey: .userName)

前置上下文

能实现上面的目的之一, 但是没有上下文, 既然在 key 那里不能加, 换一个思路, 那就在前面加, 例如:

UserDefaults.AccountInfo.set(value: "chilli cheng", forKey: .userName)
UserDefaults.AccountInfo.string(forKey: .userName)

要实现上面的实现方式, 需要扩展 UserDefaults, 添加 AccountInfo 属性, 再调用 AccountInfo 的方法, key值由 AccountInfo 来提供, 因为AccountInfo 提供分组的 key, 由于是自定义的一个分组信息, 需要实现既定方法, 必然想到用协议呀, 毕竟 Swift 的协议很强大, Swift 就是面向协议编程的.
那我们先把自定义的方法抽取到协议中, 额, 但是协议不是只能提供方法声明, 不提供方法实现吗? 谁说的? 站出来我保证不打死他! Swift 中可以对协议 protocol 进行扩展, 提供协议方法的默认实现, 如果遵守协议的类/结构体/枚举实现了该方法, 就会覆盖掉默认的方法.
我们来试着实现一下, 先写一个协议, 提供默认的方法实现:

protocol UserDefaultsSettable {
    
}

extension UserDefaultsSettable {
    static func set(value: String, forKey key: AccountKeys) {
        let key = key.rawValue
        UserDefaults.standard.set(value, forKey: key)
    }
    static func string(forKey key: AccountKeys) -> String? {
        let key = key.rawValue
        return UserDefaults.standard.string(forKey: key)
    }
}

只要我的 AccountInfo 类/结构体/枚举遵守这个协议, 就能调用存取方法了, 但是, 现在问题来了, 也是至关重要的问题, AccountKeys 从哪儿来? 我们上面是把 AccountKeys 写在UserDefaults扩展里面的, 在协议里面如何知道这个变量是什么类型呢? 而且还使用到了 rawValue, 为了通用性, 那就需要在协议里关联类型, 而且传入的值能拿到 rawValue, 那么这个关联类型需要遵守 RawRepresentable 协议, 这个很关键!!!

protocol UserDefaultsSettable {
    associatedtype defaultKeys: RawRepresentable
}

extension UserDefaultsSettable where defaultKeys.RawValue==String {
    static func set(value: String?, forKey key: defaultKeys) {
        let aKey = key.rawValue
        UserDefaults.standard.set(value, forKey: aKey)
    }
    static func string(forKey key: defaultKeys) -> String? {
        let aKey = key.rawValue
        return UserDefaults.standard.string(forKey: aKey)
    }
}

必须在扩展中使用 where 子语句限制关联类型是字符串类型, 因为 UserDefaults 的 key 就是字符串类型.
where defaultKeys.RawValue==String

在 UserDefaults 的扩展中定义分组 key:

extension UserDefaults {
    // 账户信息
    struct AccountInfo: UserDefaultsSettable {
        enum defaultKeys: String {
            case userName
            case age
        }
    }
    
    // 登录信息
    struct LoginInfo: UserDefaultsSettable {
        enum defaultKeys: String {
            case token
            case userId
        }
    }
}

存取数据:

UserDefaults.AccountInfo.set(value: "chilli cheng", forKey: .userName)
UserDefaults.AccountInfo.string(forKey: .userName)
        
UserDefaults.LoginInfo.set(value: "ahdsjhad", forKey: .token)
UserDefaults.LoginInfo.string(forKey: .token)

打完收工, 既没有手动去写 key, 避免了写错的问题, 实现了key的一致性, 又实现了上下文, 能够直接明白 key 的含义.
如果还有需要存储的分类数据, 同样在 UserDefaults extension 中添加一个结构体, 遵守 UserDefaultsSettable 协议, 实现 defaultKeys 枚举属性, 在枚举中设置该分类存储数据所需要的 key.

注意: UserDefaultsSettable 协议中只实现了存取 string 类型的数据, 可以自行在 UserDefaultsSettable 协议中添加 Int, Bool等类型方法. 虽然这种用法前期比较费劲, 但是不失为一种管理 UserDefaults 的比较好的方式.
如果大家有更好的方式, 欢迎交流.