CTMediator 组件完整性检查工具

公司项目组件化使用 CTMediator 作为路由, 实施过程中存在一个问题

业务路由中所定义的函数, 对应组件未在target中实现, 导致业务异常的问题

核心思想是遍历 CTMediator 的方法列表, 模拟真实业务下的函数调用, hook CTMediator

NoTargetActionResponseWithTargetString:selectorString:originParams:

safePerformAction:target:params:

performTarget:action:params:shouldCacheTarget:

记录未实现路由的 targetsel 定位组件

.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//
// BTComponentCheckerTool.h
// BTComponentChecker
//
// Created by 张强 on 2023/4/13.
//

#import <CTMediator/CTMediator.h>

NS_ASSUME_NONNULL_BEGIN

@interface BTComponentCheckerTool: NSObject


/// 检查所有 `Target` 是否实现了路由
+ (void)checkUnImpRouterFuncs;

@end

NS_ASSUME_NONNULL_END

.m

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
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
//
// BTComponentCheckerTool.m
// BTComponentChecker
//
// Created by 张强 on 2023/4/13.
//

#import "BTComponentCheckerTool.h"

#import <objc/runtime.h>
#import <objc/message.h>

#import <BTBaseKit/BTWeakProxy.h>


// 是否正在循环
BOOL isLoop = NO;
/// 记录所有组件未实现的路由
static NSMutableArray<NSMutableDictionary<NSString*, NSString*> *> *__unImpRouterFuncs;
/// 扫描白名单
static NSArray<NSString *> *__whiteFuncsArray;

@implementation BTComponentCheckerTool

+ (void)checkUnImpRouterFuncs {
#if DEBUG
isLoop = YES;
if (!__unImpRouterFuncs) {
__unImpRouterFuncs = [NSMutableArray array];
}
/// 配置扫描白名单
[self configWhiteFuncs];
/// 交换方法
[self exchangeNoSelectorMethods];
[self exchangeSafePerformMethods];
[self exchangePerformMethods];
/// 模拟路由调用
unsigned int count;
Method *m_list = class_copyMethodList(CTMediator.class, &count);
for (int i = 0; i < count; i++) {
Method m = m_list[i];

const char *m_name = sel_getName(method_getName(m));
NSString *name = [NSString stringWithCString:m_name encoding:NSUTF8StringEncoding];

SEL action = NSSelectorFromString(name);
NSMethodSignature* methodSig = [[CTMediator sharedInstance] methodSignatureForSelector:action];

if ([self unRouterWithMethodName:name]) continue;

/// 无参方法
if (methodSig.numberOfArguments == 2) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
[self invocationWith:invocation methodName:name];
} else {
// 根据具体类型进行参数判断调用
[self invokeMethodSignature:methodSig methodName:name];
}
}

free(m_list);

isLoop = NO;
if (__unImpRouterFuncs.count > 0) {
/// 打印未实现的路由
NSLog(@"未实现的路由 %@", __unImpRouterFuncs);
/// 更加严厉的处理, 如直接结束进程
// exit(1);
}
#endif
}


/// 模拟方法参数 - 调用
/// - Parameters:
/// - methodSignature: 方法签名
/// - methodName: 方法名
+ (void)invokeMethodSignature:(NSMethodSignature *)methodSignature methodName:(NSString *)methodName {
if(methodSignature == nil) return;
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];

id target = [NSObject new];
BTWeakProxy *obj = [BTWeakProxy proxyWithTarget:target];

unsigned int u = 0;
double d = 1.0;
void (^sampleBlock)(void) = ^() { };
BOOL b = YES;

/// 设置方法参数
if (methodSignature.numberOfArguments > 2) {
for (int i = 0; i < methodSignature.numberOfArguments - 2; i++) {
NSInteger realArgumentIndex = i + 2;
const char * argumentType = [methodSignature getArgumentTypeAtIndex:realArgumentIndex];

/// BOOL 参数
if (*argumentType == 'B') {
[invocation setArgument:&b atIndex:realArgumentIndex];
continue;
}
/// UInt64
if (*argumentType == 'Q' || *argumentType == 'q') {
[invocation setArgument:&u atIndex:realArgumentIndex];
continue;
}
/// double
if (*argumentType == 'd') {
[invocation setArgument:&d atIndex:realArgumentIndex];
continue;
}
/// object or block
if (*argumentType == '@') {
size_t len = strlen(argumentType);

if (len == 0) {
[invocation setArgument:&obj atIndex:realArgumentIndex];
continue;
}

if (len == 0) {
[invocation setArgument:&obj atIndex:realArgumentIndex];
continue;
}
// block
if (len == 2 && *(argumentType + 1) == '?') {
[invocation setArgument:&sampleBlock atIndex:realArgumentIndex];
} else {
[invocation setArgument:&obj atIndex:realArgumentIndex];
}
}
}
}

[self invocationWith:invocation methodName:methodName];
}


/// 调用方法
/// - Parameters:
/// - invocation: invocation description
/// - methodName: methodName description
+ (void)invocationWith:(NSInvocation *)invocation methodName:(NSString *)methodName {
SEL action = NSSelectorFromString(methodName);
[invocation setSelector:action];
[invocation setTarget:[CTMediator sharedInstance]];
[invocation invoke];
}

#pragma mark: - 交换 `CTMeditor` 的实现
void (*origin_NoTargetActionResponseWithTargetString)(id _self, SEL __cmd, NSString *targetString, NSString *selectorString, NSDictionary *originParams, BOOL shouldCacheTarget);
void swizzled_NoTargetActionResponseWithTargetString(id _self, SEL __cmd, NSString *targetString, NSString *selectorString, NSDictionary *originParams, BOOL shouldCacheTarget) {

// 记录未实现路由及 `target`
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
dict[@"target"] = targetString;
dict[@"sel"] = selectorString;
[__unImpRouterFuncs addObject:dict];

if (isLoop) { return; }
origin_NoTargetActionResponseWithTargetString(_self, __cmd, targetString, selectorString, originParams, shouldCacheTarget);
}


id (*origin_safePerformAction)(id _self, SEL __cmd, SEL action, NSObject *target, NSDictionary *params);
id swizzled_safePerformAction(id _self, SEL __cmd, SEL action, NSObject *target, NSDictionary *params) {
if (isLoop) { return nil; }
return origin_safePerformAction(_self, __cmd, action, target, params);
}


id (*origin_performAction)(id _self, SEL __cmd, NSString *targetName, NSObject *actionName, NSDictionary *params, BOOL shouldCacheTarget);
id swizzled_performAction(id _self, SEL __cmd, NSString *targetName, NSObject *actionName, NSDictionary *params, BOOL shouldCacheTarget) {
if (isLoop) {
if ([params isKindOfClass:[BTWeakProxy class]]) {
params = @{};
}
}
return origin_performAction(_self, __cmd, targetName, actionName, params, shouldCacheTarget);
}

+ (void)exchangeNoSelectorMethods {
SEL noTargetSel = NSSelectorFromString(@"NoTargetActionResponseWithTargetString:selectorString:originParams:");
Method m1 = class_getInstanceMethod(CTMediator.class, noTargetSel);
origin_NoTargetActionResponseWithTargetString = method_setImplementation(m1, (IMP)swizzled_NoTargetActionResponseWithTargetString);
}

+ (void)exchangeSafePerformMethods {
SEL safePerform = NSSelectorFromString(@"safePerformAction:target:params:");
Method m1 = class_getInstanceMethod(CTMediator.class, safePerform);
origin_safePerformAction = method_setImplementation(m1, (IMP)swizzled_safePerformAction);
}

+ (void)exchangePerformMethods {
SEL perform = NSSelectorFromString(@"performTarget:action:params:shouldCacheTarget:");
Method m1 = class_getInstanceMethod(CTMediator.class, perform);
origin_performAction = method_setImplementation(m1, (IMP)swizzled_performAction);
}


/// 判断方法是否需要模拟调用
/// - Parameter methodName: 方法名
+ (BOOL)unRouterWithMethodName:(NSString *)methodName {
__block BOOL isWhite = NO;
[__whiteFuncsArray enumerateObjectsUsingBlock:^(NSString * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
if ([methodName hasPrefix:obj]) {
isWhite = YES;
*stop = YES;
}
}];
return isWhite;
}

/// 白名单可以按需修改
+ (void)configWhiteFuncs {
if (!__whiteFuncsArray) {
__whiteFuncsArray = @[
@"register",
@"performActionWithUrl",
@"performTarget",
@"releaseCachedTargetWithFullTargetName",
@"NoTargetActionResponseWithTargetString",
@"safePerformAction",
@"safeFetchCachedTarget",
@"safeSetCachedTarget",
@"setCachedTarget",
@"presentViewController",
@"pushViewController",
@"cachedTarget",
@"swizzled_performTarget",
@"__get",
@"integerValue",
@"setCountryListIndicatorConfig",
@"getLoginThemeConfig",
];
}
}

@end

自用 CI 环境搭建

自用 CI 环境搭建

所需工具及环境

Jenkins 官网 Jenkins

Nodejs 环境安装 Nodejs参考这里

Fastlane 官方文档 Fastlane 官方文档

Tomcat + Jenkins 参考

mySql 环境安装 环境安装

STS 套件安装 STS

相关操作

对 Fastlane CI 项目进行 修改

阅读前, 请确保已经搭建好相关环境, 或阅读过

Jenkins一键发布ipa到蒲公英

进入 构建项目的 设置 构建环境 Execute shell 修改为 fastlane test

运行测试

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
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
Started by user ooops
Running as SYSTEM
Building in workspace /Users/xxxxxx/.jenkins/workspace/JenkinsTest
using credential 80ba6ebd-6283-4f44-9f24-a220389c234e
> git rev-parse --is-inside-work-tree # timeout=10
Fetching changes from the remote Git repository
> git config remote.origin.url git@gitlab.xxxxxx.com.cn:ooops/Jenkins.git # timeout=10
Fetching upstream changes from git@gitlab.xxxxxx.com.cn:ooops/Jenkins.git
> git --version # timeout=10
using GIT_SSH to set credentials
> git fetch --tags --force --progress git@gitlab.xxxxxx.com.cn:ooops/Jenkins.git +refs/heads/*:refs/remotes/origin/*
> git rev-parse refs/remotes/origin/master^{commit} # timeout=10
> git rev-parse refs/remotes/origin/origin/master^{commit} # timeout=10
Checking out Revision afe1f9d46d15bba769daa46552ccc11c598b9642 (refs/remotes/origin/master)
> git config core.sparsecheckout # timeout=10
> git checkout -f afe1f9d46d15bba769daa46552ccc11c598b9642
Commit message: "init project"
> git rev-list --no-walk afe1f9d46d15bba769daa46552ccc11c598b9642 # timeout=10
[JenkinsTest] $ /bin/sh -xe /Users/xxxxxx/Library/Tomcat/temp/jenkins7807814047229248875.sh
+ fastlane test
[18:20:15]: fastlane detected a Gemfile in the current directory
[18:20:15]: however it seems like you don't use `bundle exec`
[18:20:15]: to launch fastlane faster, please use
[18:20:15]:
[18:20:15]: $ bundle exec fastlane test
[18:20:15]:
[18:20:15]: Get started using a Gemfile for fastlane https://docs.fastlane.tools/getting-started/ios/setup/#use-a-gemfile
+-----------------------+---------+--------+
| Used plugins |
+-----------------------+---------+--------+
| Plugin | Version | Action |
+-----------------------+---------+--------+
| fastlane-plugin-pgyer | 0.2.2 | pgyer |
+-----------------------+---------+--------+

[18:20:16]: ----------------------------------------
[18:20:16]: --- Step: Verifying fastlane version ---
[18:20:16]: ----------------------------------------
[18:20:16]: Your fastlane version 2.135.2 matches the minimum requirement of 2.134.0 ✅
[18:20:16]: ------------------------------
[18:20:16]: --- Step: default_platform ---
[18:20:16]: ------------------------------
[18:20:16]: Driving the lane 'ios test' 🚀
[18:20:16]: -----------------------
[18:20:16]: --- Step: cocoapods ---
[18:20:16]: -----------------------
[18:20:16]: Using deprecated option: '--clean' (true)
[18:20:16]: $ cd '.' && pod install
[18:20:17]: ▸ Analyzing dependencies
[18:20:17]: ▸ Downloading dependencies
[18:20:17]: ▸ Using Masonry (1.1.0)
[18:20:17]: ▸ Generating Pods project
[18:20:17]: ▸ Integrating client project
[18:20:17]: ▸ Sending stats
[18:20:17]: ▸ Pod installation complete! There is 1 dependency from the Podfile and 1 total pod installed.
[18:20:17]: ▸ [!] Automatically assigning platform `ios` with version `13.1` on target `Jenkins` because no platform was specified. Please specify a platform for this target in your Podfile. See `https://guides.cocoapods.org/syntax/podfile.html#platform`.
[18:20:18]: 请输入版本描述:
[18:20:18]: -----------------------------
[18:20:18]: --- Step: last_git_commit ---
[18:20:18]: -----------------------------
[18:20:18]:
Ad-Hoc - init project
&#10 short_hash:afe1f9d
[18:20:18]: --------------------------------
[18:20:18]: --- Step: get_version_number ---
[18:20:18]: --------------------------------
[18:20:18]:

+------------------------+---------+----------------------------+
packing
+--------------------+-----------------------+------------------+
| schemeName export_method configuration
+--------------------+-----------------------+------------------+
| Jenkins development Test-Debug
+--------------------+-----------------------+------------------+
[18:20:18]: -----------------
[18:20:18]: --- Step: gym ---
[18:20:18]: -----------------
[18:20:18]: $ xcodebuild -showBuildSettings -workspace ./Jenkins.xcworkspace -scheme Jenkins -configuration Test-Debug
[18:20:20]: Couldn't find specified configuration 'Test-Debug'.

+------------------------------------------+---------------------------------------------------------------+
| Summary for gym 2.135.2 |
+------------------------------------------+---------------------------------------------------------------+
| scheme | Jenkins |
| output_name | Jenkins |
| clean | true |
| output_directory | /Users/xxxxxx/Library/Tomcat/webapps/app/ipa/afe1f9d |
| export_options.method | development |
| export_options.manifest.appURL | https://ios.xxxxxx.com.cn/app/ipa/afe1f9d/Jenkins.ipa |
| export_options.manifest.displayImageURL | https://ios.xxxxxx.com.cn/app/a.png |
| export_options.manifest.fullSizeImageURL | https://ios.xxxxxx.com.cn/app/b.png |
| workspace | ./Jenkins.xcworkspace |
| destination | generic/platform=iOS |
| build_path | /Users/xxxxxx/Library/Developer/Xcode/Archives/2019-11-12 |
| silent | false |
| skip_package_ipa | false |
| result_bundle | false |
| buildlog_path | ~/Library/Logs/gym |
| skip_profile_detection | false |
| xcode_path | /Applications/Xcode.app |
+------------------------------------------+---------------------------------------------------------------+

[18:20:20]: $ set -o pipefail && xcodebuild -workspace ./Jenkins.xcworkspace -scheme Jenkins -destination 'generic/platform=iOS' -archivePath /Users/xxxxxx/Library/Developer/Xcode/Archives/2019-11-12/Jenkins\ 2019-11-12\ 18.20.20.xcarchive clean archive | tee /Users/xxxxxx/Library/Logs/gym/Jenkins-Jenkins.log | xcpretty
[18:20:21]: ▸ Clean Succeeded
[18:20:22]: ▸ Copying ViewController+MASAdditions.h
[18:20:22]: ▸ Copying Masonry-umbrella.h
[18:20:22]: ▸ Copying View+MASShorthandAdditions.h
[18:20:22]: ▸ Processing Masonry-Info.plist
[18:20:22]: ▸ Copying View+MASAdditions.h
[18:20:22]: ▸ Copying NSLayoutConstraint+MASDebugAdditions.h
[18:20:22]: ▸ Copying NSArray+MASShorthandAdditions.h
[18:20:22]: ▸ Copying NSArray+MASAdditions.h
[18:20:22]: ▸ Copying Masonry.h
[18:20:22]: ▸ Copying MASViewConstraint.h
[18:20:22]: ▸ Copying MASViewAttribute.h
[18:20:22]: ▸ Copying MASUtilities.h
[18:20:22]: ▸ Copying MASLayoutConstraint.h
[18:20:22]: ▸ Copying MASConstraintMaker.h
[18:20:22]: ▸ Copying MASConstraint.h
[18:20:22]: ▸ Copying MASConstraint+Private.h
[18:20:22]: ▸ Copying MASCompositeConstraint.h
[18:20:22]: ▸ Compiling ViewController+MASAdditions.m
[18:20:24]: ▸ Compiling View+MASAdditions.m
[18:20:24]: ▸ ⚠️ /Users/xxxxxx/.jenkins/workspace/JenkinsTest/Pods/Masonry/Masonry/View+MASAdditions.m:14:65: parameter of overriding method should be annotated with __attribute__((noescape)) [-Wmissing-noescape]
[18:20:24]: ▸ - (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
[18:20:24]: ▸  ^
[18:20:24]: ▸ ⚠️ /Users/xxxxxx/.jenkins/workspace/JenkinsTest/Pods/Masonry/Masonry/View+MASAdditions.m:21:67: parameter of overriding method should be annotated with __attribute__((noescape)) [-Wmissing-noescape]
[18:20:24]: ▸ - (NSArray *)mas_updateConstraints:(void(^)(MASConstraintMaker *))block {
[18:20:24]: ▸  ^
[18:20:24]: ▸ ⚠️ /Users/xxxxxx/.jenkins/workspace/JenkinsTest/Pods/Masonry/Masonry/View+MASAdditions.m:29:71: parameter of overriding method should be annotated with __attribute__((noescape)) [-Wmissing-noescape]
[18:20:24]: ▸ - (NSArray *)mas_remakeConstraints:(void(^)(MASConstraintMaker *make))block {
[18:20:24]: ▸  ^
[18:20:24]: ▸ Compiling NSLayoutConstraint+MASDebugAdditions.m
[18:20:24]: ▸ Compiling NSArray+MASAdditions.m
[18:20:24]: ▸ ⚠️ /Users/xxxxxx/.jenkins/workspace/JenkinsTest/Pods/Masonry/Masonry/NSArray+MASAdditions.m:14:69: parameter of overriding method should be annotated with __attribute__((noescape)) [-Wmissing-noescape]
[18:20:24]: ▸ - (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *make))block {
[18:20:24]: ▸  ^
[18:20:24]: ▸ ⚠️ /Users/xxxxxx/.jenkins/workspace/JenkinsTest/Pods/Masonry/Masonry/NSArray+MASAdditions.m:23:71: parameter of overriding method should be annotated with __attribute__((noescape)) [-Wmissing-noescape]
[18:20:24]: ▸ - (NSArray *)mas_updateConstraints:(void(^)(MASConstraintMaker *make))block {
[18:20:24]: ▸  ^
[18:20:24]: ▸ ⚠️ /Users/xxxxxx/.jenkins/workspace/JenkinsTest/Pods/Masonry/Masonry/NSArray+MASAdditions.m:32:71: parameter of overriding method should be annotated with __attribute__((noescape)) [-Wmissing-noescape]
[18:20:24]: ▸ - (NSArray *)mas_remakeConstraints:(void(^)(MASConstraintMaker *make))block {
[18:20:24]: ▸  ^
[18:20:24]: ▸ Compiling Masonry_vers.c
[18:20:24]: ▸ Compiling Masonry-dummy.m
[18:20:24]: ▸ Compiling MASViewConstraint.m
[18:20:24]: ▸ Compiling MASViewAttribute.m
[18:20:24]: ▸ Compiling MASLayoutConstraint.m
[18:20:24]: ▸ Compiling MASConstraintMaker.m
[18:20:24]: ▸ Compiling MASConstraint.m
[18:20:25]: ▸ Compiling MASCompositeConstraint.m
[18:20:25]: ▸ Compiling ViewController+MASAdditions.m
[18:20:27]: ▸ Compiling View+MASAdditions.m
[18:20:27]: ▸ ⚠️ /Users/xxxxxx/.jenkins/workspace/JenkinsTest/Pods/Masonry/Masonry/View+MASAdditions.m:14:65: parameter of overriding method should be annotated with __attribute__((noescape)) [-Wmissing-noescape]
[18:20:27]: ▸ - (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
[18:20:27]: ▸  ^
[18:20:27]: ▸ ⚠️ /Users/xxxxxx/.jenkins/workspace/JenkinsTest/Pods/Masonry/Masonry/View+MASAdditions.m:21:67: parameter of overriding method should be annotated with __attribute__((noescape)) [-Wmissing-noescape]
[18:20:27]: ▸ - (NSArray *)mas_updateConstraints:(void(^)(MASConstraintMaker *))block {
[18:20:27]: ▸  ^
[18:20:27]: ▸ ⚠️ /Users/xxxxxx/.jenkins/workspace/JenkinsTest/Pods/Masonry/Masonry/View+MASAdditions.m:29:71: parameter of overriding method should be annotated with __attribute__((noescape)) [-Wmissing-noescape]
[18:20:27]: ▸ - (NSArray *)mas_remakeConstraints:(void(^)(MASConstraintMaker *make))block {
[18:20:27]: ▸  ^
[18:20:27]: ▸ Compiling NSLayoutConstraint+MASDebugAdditions.m
[18:20:27]: ▸ Compiling NSArray+MASAdditions.m
[18:20:27]: ▸ ⚠️ /Users/xxxxxx/.jenkins/workspace/JenkinsTest/Pods/Masonry/Masonry/NSArray+MASAdditions.m:14:69: parameter of overriding method should be annotated with __attribute__((noescape)) [-Wmissing-noescape]
[18:20:27]: ▸ - (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *make))block {
[18:20:27]: ▸  ^
[18:20:27]: ▸ ⚠️ /Users/xxxxxx/.jenkins/workspace/JenkinsTest/Pods/Masonry/Masonry/NSArray+MASAdditions.m:23:71: parameter of overriding method should be annotated with __attribute__((noescape)) [-Wmissing-noescape]
[18:20:27]: ▸ - (NSArray *)mas_updateConstraints:(void(^)(MASConstraintMaker *make))block {
[18:20:27]: ▸  ^
[18:20:27]: ▸ ⚠️ /Users/xxxxxx/.jenkins/workspace/JenkinsTest/Pods/Masonry/Masonry/NSArray+MASAdditions.m:32:71: parameter of overriding method should be annotated with __attribute__((noescape)) [-Wmissing-noescape]
[18:20:27]: ▸ - (NSArray *)mas_remakeConstraints:(void(^)(MASConstraintMaker *make))block {
[18:20:27]: ▸  ^
[18:20:27]: ▸ Linking Masonry
[18:20:27]: ▸ Compiling Masonry_vers.c
[18:20:27]: ▸ Compiling Masonry-dummy.m
[18:20:27]: ▸ Compiling MASViewConstraint.m
[18:20:27]: ▸ Compiling MASViewAttribute.m
[18:20:27]: ▸ Compiling MASLayoutConstraint.m
[18:20:27]: ▸ Compiling MASConstraintMaker.m
[18:20:27]: ▸ Compiling MASConstraint.m
[18:20:27]: ▸ Compiling MASCompositeConstraint.m
[18:20:27]: ▸ Linking Masonry
[18:20:27]: ▸ Generating 'Masonry.framework.dSYM'
[18:20:28]: ▸ Touching Masonry.framework (in target 'Masonry' from project 'Pods')
[18:20:28]: ▸ Processing Pods-Jenkins-Info.plist
[18:20:28]: ▸ Copying Pods-Jenkins-umbrella.h
[18:20:28]: ▸ Compiling Pods-Jenkins-dummy.m
[18:20:29]: ▸ Compiling Pods_Jenkins_vers.c
[18:20:29]: ▸ Touching Pods_Jenkins.framework (in target 'Pods-Jenkins' from project 'Pods')
[18:20:29]: ▸ Running script '[CP] Check Pods Manifest.lock'
[18:20:29]: ▸ Compiling main.m
[18:20:31]: ▸ Compiling SceneDelegate.m
[18:20:31]: ▸ Compiling ViewController.m
[18:20:31]: ▸ Compiling AppDelegate.m
[18:20:31]: ▸ Linking Jenkins
[18:20:32]: ▸ Compiling Main.storyboard
[18:20:33]: ▸ Compiling LaunchScreen.storyboard
[18:20:33]: ▸ Processing Info.plist
[18:20:33]: ▸ Generating 'Jenkins.app.dSYM'
[18:20:33]: ▸ Running script '[CP] Embed Pods Frameworks'
[18:20:34]: ▸ Touching Jenkins.app (in target 'Jenkins' from project 'Jenkins')
[18:20:34]: ▸ Archive Succeeded
[18:20:34]: Generated plist file with the following values:
[18:20:34]: ▸ -----------------------------------------
[18:20:34]: ▸ {
[18:20:34]: ▸  "method": "development",
[18:20:34]: ▸  "manifest": {
[18:20:34]: ▸  "appURL": "https://ios.xxxxxx.com.cn/app/ipa/afe1f9d/Jenkins.ipa",
[18:20:34]: ▸  "displayImageURL": "https://ios.xxxxxx.com.cn/app/a.png",
[18:20:34]: ▸  "fullSizeImageURL": "https://ios.xxxxxx.com.cn/app/b.png"
[18:20:34]: ▸  }
[18:20:34]: ▸ }
[18:20:34]: ▸ -----------------------------------------
[18:20:34]: $ /usr/bin/xcrun /Library/Ruby/Gems/2.3.0/gems/fastlane-2.135.2/gym/lib/assets/wrap_xcodebuild/xcbuild-safe.sh -exportArchive -exportOptionsPlist '/var/folders/4j/q9rj1lpd13lgz9t9fd60bs_h0000gn/T/gym_config20191112-12790-1s8sdqx.plist' -archivePath /Users/xxxxxx/Library/Developer/Xcode/Archives/2019-11-12/Jenkins\ 2019-11-12\ 18.20.20.xcarchive -exportPath '/var/folders/4j/q9rj1lpd13lgz9t9fd60bs_h0000gn/T/gym_output20191112-12790-ngi2js' 
[18:20:37]: Mapping dSYM(s) using generated BCSymbolMaps
[18:20:37]: $ dsymutil --symbol-map /Users/xxxxxx/Library/Developer/Xcode/Archives/2019-11-12/Jenkins\ 2019-11-12\ 18.20.20.xcarchive/BCSymbolMaps/6D75071B-0037-384F-926F-8B1C1361029B.bcsymbolmap /Users/xxxxxx/Library/Developer/Xcode/Archives/2019-11-12/Jenkins\ 2019-11-12\ 18.20.20.xcarchive/dSYMs/Masonry.framework.dSYM
[18:20:37]: $ dsymutil --symbol-map /Users/xxxxxx/Library/Developer/Xcode/Archives/2019-11-12/Jenkins\ 2019-11-12\ 18.20.20.xcarchive/BCSymbolMaps/767280BF-E897-396A-89C4-C434CC6D4567.bcsymbolmap /Users/xxxxxx/Library/Developer/Xcode/Archives/2019-11-12/Jenkins\ 2019-11-12\ 18.20.20.xcarchive/dSYMs/Masonry.framework.dSYM
[18:20:38]: $ dsymutil --symbol-map /Users/xxxxxx/Library/Developer/Xcode/Archives/2019-11-12/Jenkins\ 2019-11-12\ 18.20.20.xcarchive/BCSymbolMaps/23A90BD7-3B62-3230-81A9-88F573B9B154.bcsymbolmap /Users/xxxxxx/Library/Developer/Xcode/Archives/2019-11-12/Jenkins\ 2019-11-12\ 18.20.20.xcarchive/dSYMs/23A90BD7-3B62-3230-81A9-88F573B9B154.dSYM
warning: /Users/xxxxxx/Library/Developer/Xcode/Archives/2019-11-12/Jenkins 2019-11-12 18.20.20.xcarchive/BCSymbolMaps/23A90BD7-3B62-3230-81A9-88F573B9B154.bcsymbolmap: No such file or directory: not unobfuscating.
[18:20:38]: $ dsymutil --symbol-map /Users/xxxxxx/Library/Developer/Xcode/Archives/2019-11-12/Jenkins\ 2019-11-12\ 18.20.20.xcarchive/BCSymbolMaps/B4A69CB4-BB3F-3714-BB41-23CB1697F350.bcsymbolmap /Users/xxxxxx/Library/Developer/Xcode/Archives/2019-11-12/Jenkins\ 2019-11-12\ 18.20.20.xcarchive/dSYMs/Jenkins.app.dSYM
[18:20:38]: $ dsymutil --symbol-map /Users/xxxxxx/Library/Developer/Xcode/Archives/2019-11-12/Jenkins\ 2019-11-12\ 18.20.20.xcarchive/BCSymbolMaps/C5642B3B-9FDD-318D-A3AF-3D618762769A.bcsymbolmap /Users/xxxxxx/Library/Developer/Xcode/Archives/2019-11-12/Jenkins\ 2019-11-12\ 18.20.20.xcarchive/dSYMs/C5642B3B-9FDD-318D-A3AF-3D618762769A.dSYM
warning: /Users/xxxxxx/Library/Developer/Xcode/Archives/2019-11-12/Jenkins 2019-11-12 18.20.20.xcarchive/BCSymbolMaps/C5642B3B-9FDD-318D-A3AF-3D618762769A.bcsymbolmap: No such file or directory: not unobfuscating.
[18:20:38]: Compressing 4 dSYM(s)
[18:20:38]: $ cd '/Users/xxxxxx/Library/Developer/Xcode/Archives/2019-11-12/Jenkins 2019-11-12 18.20.20.xcarchive/dSYMs' && zip -r '/Users/xxxxxx/Library/Tomcat/webapps/app/ipa/afe1f9d/Jenkins.app.dSYM.zip' *.dSYM
[18:20:38]: ▸  adding: 23A90BD7-3B62-3230-81A9-88F573B9B154.dSYM/ (stored 0%)
[18:20:38]: ▸  adding: 23A90BD7-3B62-3230-81A9-88F573B9B154.dSYM/Contents/ (stored 0%)
[18:20:38]: ▸  adding: 23A90BD7-3B62-3230-81A9-88F573B9B154.dSYM/Contents/Resources/ (stored 0%)
[18:20:38]: ▸  adding: 23A90BD7-3B62-3230-81A9-88F573B9B154.dSYM/Contents/Resources/DWARF/ (stored 0%)
[18:20:38]: ▸  adding: 23A90BD7-3B62-3230-81A9-88F573B9B154.dSYM/Contents/Resources/DWARF/Jenkins (deflated 82%)
[18:20:38]: ▸  adding: 23A90BD7-3B62-3230-81A9-88F573B9B154.dSYM/Contents/Resources/23A90BD7-3B62-3230-81A9-88F573B9B154.plist (deflated 21%)
[18:20:38]: ▸  adding: 23A90BD7-3B62-3230-81A9-88F573B9B154.dSYM/Contents/Info.plist (deflated 52%)
[18:20:38]: ▸  adding: C5642B3B-9FDD-318D-A3AF-3D618762769A.dSYM/ (stored 0%)
[18:20:38]: ▸  adding: C5642B3B-9FDD-318D-A3AF-3D618762769A.dSYM/Contents/ (stored 0%)
[18:20:38]: ▸  adding: C5642B3B-9FDD-318D-A3AF-3D618762769A.dSYM/Contents/Resources/ (stored 0%)
[18:20:38]: ▸  adding: C5642B3B-9FDD-318D-A3AF-3D618762769A.dSYM/Contents/Resources/DWARF/ (stored 0%)
[18:20:38]: ▸  adding: C5642B3B-9FDD-318D-A3AF-3D618762769A.dSYM/Contents/Resources/DWARF/Masonry (deflated 68%)
[18:20:38]: ▸  adding: C5642B3B-9FDD-318D-A3AF-3D618762769A.dSYM/Contents/Resources/C5642B3B-9FDD-318D-A3AF-3D618762769A.plist (deflated 21%)
[18:20:38]: ▸  adding: C5642B3B-9FDD-318D-A3AF-3D618762769A.dSYM/Contents/Info.plist (deflated 52%)
[18:20:38]: ▸  adding: Jenkins.app.dSYM/ (stored 0%)
[18:20:38]: ▸  adding: Jenkins.app.dSYM/Contents/ (stored 0%)
[18:20:38]: ▸  adding: Jenkins.app.dSYM/Contents/Resources/ (stored 0%)
[18:20:38]: ▸  adding: Jenkins.app.dSYM/Contents/Resources/DWARF/ (stored 0%)
[18:20:38]: ▸  adding: Jenkins.app.dSYM/Contents/Resources/DWARF/Jenkins (deflated 61%)
[18:20:38]: ▸  adding: Jenkins.app.dSYM/Contents/Info.plist (deflated 52%)
[18:20:38]: ▸  adding: Masonry.framework.dSYM/ (stored 0%)
[18:20:38]: ▸  adding: Masonry.framework.dSYM/Contents/ (stored 0%)
[18:20:38]: ▸  adding: Masonry.framework.dSYM/Contents/Resources/ (stored 0%)
[18:20:38]: ▸  adding: Masonry.framework.dSYM/Contents/Resources/DWARF/ (stored 0%)
[18:20:38]: ▸  adding: Masonry.framework.dSYM/Contents/Resources/DWARF/Masonry (deflated 63%)
[18:20:38]: ▸  adding: Masonry.framework.dSYM/Contents/Info.plist (deflated 52%)

[18:20:38]: Successfully exported and compressed dSYM file
[18:20:38]: Successfully exported and signed the ipa file:
[18:20:38]: /Users/xxxxxx/Library/Tomcat/webapps/app/ipa/afe1f9d/Jenkins.ipa
[18:20:38]: Successfully exported the manifest.plist file:
[18:20:38]: /Users/xxxxxx/Library/Tomcat/webapps/app/ipa/afe1f9d/manifest.plist
[18:20:38]: --------------------------------------------------------------------
[18:20:38]: Step: node /Users/xxxxxx/ipa/ios.js 'APPName App - R' 'https://ios.xxxxxx.com.cn/app/ipa/afe1f9d/Jenkins.ipa' 'Ad-Hoc - init project
&#10 short_hash:afe1f9d' '1.0' 'https://ios.xxxxxx.com.cn/app/ipa/afe1f9d/manifest.plist'
[18:20:38]: --------------------------------------------------------------------
[18:20:38]: $ node /Users/xxxxxx/ipa/ios.js 'APPName App - R' 'https://ios.xxxxxx.com.cn/app/ipa/afe1f9d/Jenkins.ipa' 'Ad-Hoc - init project
&#10 short_hash:afe1f9d' '1.0' 'https://ios.xxxxxx.com.cn/app/ipa/afe1f9d/manifest.plist'
[18:20:38]: ▸ --------------------------INSERT----------------------------
[18:20:38]: ▸ INSERT ID: OkPacket {
[18:20:38]: ▸ fieldCount: 0,
[18:20:38]: ▸ affectedRows: 1,
[18:20:38]: ▸ insertId: 65,
[18:20:38]: ▸ serverStatus: 2,
[18:20:38]: ▸ warningCount: 0,
[18:20:38]: ▸ message: '',
[18:20:38]: ▸ protocol41: true,
[18:20:38]: ▸ changedRows: 0 }
[18:20:38]: ▸ -----------------------------------------------------------------

+------+------------------------------------------------------------------+-------------+
| fastlane summary |
+------+------------------------------------------------------------------+-------------+
| Step | Action | Time (in s) |
+------+------------------------------------------------------------------+-------------+
| 1 | Verifying fastlane version | 0 |
| 2 | default_platform | 0 |
| 3 | cocoapods | 1 |
| 4 | last_git_commit | 0 |
| 5 | get_version_number | 0 |
| 6 | gym | 19 |
| 7 | node /Users/xxxxxx/ipa/ios.js 'APPName App - R' 'https://ios. | 0 |
+------+------------------------------------------------------------------+-------------+

[18:20:38]: fastlane.tools finished successfully 🎉
Finished: SUCCESS

DataBase

数据接口

以下内容纯属现学现卖, low bee 的地方, 轻喷.

依赖工具

STS

JDK

STS 套件中新建一个 Maven spring 项目

pom.xml 配置如下

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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.7.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>consuming-rest</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>consuming-rest</name>
<description>Demo project for Spring Boot</description>

<properties>
<java.version>1.8</java.version>
</properties>

<dependencies>
<!--spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

</dependencies>
<build>
<!-- jar 包的名字 -->
<finalName>api</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

创建数据类 IpaInfo

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
package com.example.consumingrest.api;


public class IpaInfo {

private String id;
private String title;
private String ver;
private String url;
private String des;
private String mainfest;
private String createTime;

public IpaInfo(String id, String title, String ver, String url, String des, String mainfest, String createTime) {
this.id = id;
this.title = title;
this.ver = ver;
this.url = url;
this.des = des;
this.mainfest = mainfest;
this.createTime = createTime;
}

public IpaInfo(String id, String title, String ver) {
this.id = id;
this.title = title;
this.ver = ver;
}




public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

public String getTitle() {
return title;
}

public void setTitle(String title) {
this.title = title;
}

public String getVer() {
return ver;
}

public void setVer(String ver) {
this.ver = ver;
}

public String getUrl() {
return url;
}

public void setUrl(String url) {
this.url = url;
}

public String getDes() {
return des;
}

public void setDes(String des) {
this.des = des;
}

public String getMainfest() {
return mainfest;
}

public void setMainfest(String mainfest) {
this.mainfest = mainfest;
}

@Override
public String toString() {
return String.format("Customer[id=%s, title='%s', des='%s']", id, title, des);
}

public String getCreateTime() {
return createTime;
}

public void setCreateTime(String createTime) {
this.createTime = createTime;
}

}

创建分页类对象 PackageInfo

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
package com.example.consumingrest.api;

import java.util.List;

import com.example.consumingrest.api.IpaInfo;

public class PackageInfo {

private Integer currentPage;

private Integer totalPage;

private Integer totalRows;

private List<IpaInfo> ipaList;

public Integer getCurrentPage() {
return currentPage;
}

public void setCurrentPage(Integer currentPage) {
this.currentPage = currentPage;
}

public Integer getTotalPage() {
return totalPage;
}

public void setTotalPage(Integer totalPage) {
this.totalPage = totalPage;
}

public Integer getTotalRows() {
return totalRows;
}

public void setTotalRows(Integer totalRows) {
this.totalRows = totalRows;
}

public List<IpaInfo> getIpaList() {
return ipaList;
}

public void setIpaList(List<IpaInfo> ipaList) {
this.ipaList = ipaList;
}

}

接口请求参数类对象 PageObject

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
package com.example.consumingrest.api;

public class PageObject {
public Integer page;
public Integer pageSize;
/// 1 - ios, 2 - android
public Integer platform;

public Integer getPage() {
return page;
}
public void setPage(Integer page) {
this.page = page;
}
public Integer getPageSize() {
return pageSize;
}
public void setPageSize(Integer pageSize) {
this.pageSize = pageSize;
}
public Integer getPlatform() {
return platform;
}
public void setPlatform(Integer platform) {
this.platform = platform;
}
}

com.example.consumingrest.api 的包中创建 ApiContorller, 添加方法如下

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
package com.example.consumingrest.api;
import java.util.Properties;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.mysql.cj.jdbc.Driver;
import org.springframework.http.MediaType;
import java.util.ArrayList;
import java.util.List;

@CrossOrigin
@RestController
public class ApiContorller {
@RequestMapping(value = "/list",consumes = MediaType.APPLICATION_JSON_VALUE)
@PostMapping(value = "/list",consumes = MediaType.APPLICATION_JSON_VALUE)
public PackageInfo ipa(@RequestBody PageObject obj) throws SQLException {

PackageInfo pa = new PackageInfo();

Integer page = obj.page;

Integer size = obj.pageSize;

String limite = String.valueOf(page * size);

String offset = String.valueOf(size * (page - 1));

String tableName = (obj.platform == 1) ? "ipaInfo" : "apkInfo";

List<IpaInfo> ipaList = new ArrayList<IpaInfo>();

Driver driver = new com.mysql.cj.jdbc.Driver();

String url = "jdbc:mysql://localhost:3306/testFlight?useSSL=false&serverTimezone=UTC";
Properties info = new Properties();
info.put("user", "root");
info.put("password", "password");

Connection conn = driver.connect(url, info);


if(!conn.isClosed())
System.out.println("Succeeded connecting to the Database!");

String sql = String.format("SELECT id, title, url, ver, des, mainfest, createTime FROM %s ORDER BY id DESC limit %s offset %s", tableName, limite, offset);

System.out.println(String.format("sql ; %s", sql));

PreparedStatement psta = conn.prepareStatement(sql);
ResultSet rs = psta.executeQuery();
while(rs.next()){
String id = rs.getString("id");
String appName = rs.getString("title");
String appUrl = rs.getString("url");
String ver = rs.getString("ver");
String description = rs.getString("des");
String mainfest = rs.getString("mainfest");
String createTime = rs.getString("createTime");

IpaInfo info2 = new IpaInfo(id, appName, ver, appUrl, description, mainfest, createTime);

ipaList.add(info2);
}

pa.setCurrentPage(page);

pa.setTotalPage(getTotalPage(conn, size, tableName));

pa.setIpaList(ipaList);

pa.setTotalRows(totalRows(conn, size, tableName));

rs.close();
psta.close();
conn.close();

return pa;
}

/// 总页数
public int getTotalPage(Connection conn, Integer pageSize, String tableName) throws SQLException {
int count = totalRows(conn, pageSize, tableName);
//计算总页数公式
int totalPage = count / pageSize + (count % pageSize > 0 ? 1 : 0);

return totalPage;
}

/// 总记录数
private int totalRows(Connection conn, Integer pageSize, String tableName) throws SQLException {
PreparedStatement psta = null;
ResultSet rs = null;
String strSql = null;
int count = 0;
try {
strSql = String.format("SELECT count(id) FROM %S", tableName);
psta = conn.prepareStatement(strSql);
rs = psta.executeQuery();
if(rs.next()) {
count =rs.getInt(1);
}
System.out.println("查询数据成功" + count);
} catch (Exception e) {
e.printStackTrace();
} finally {
// rs.close();
// psta.close();
// conn.close();
}

return count;
}
}

修改 application.propertiesapplication.yml

内容修改为

1
2
server:
port: 9000
直接运行项目

STS 内部会自动部署一个 Tomcat

生成 jar

通过 右键 项目 Run As Maven install 打包生成 jar

项目 target 文件夹下, 找到 pom.xmlfinalName 配置的 finalName.jar

放入想要存储的文件夹

通过命令行启动 jar

1
$ java -jar api.jar
数据访问

通过浏览器访问 http://localhost:9000/list

1
2
3
4
5
This application has no explicit mapping for /error, so you are seeing this as a fallback.

Tue Nov 12 17:07:17 CST 2019
There was an unexpected error (type=Unsupported Media Type, status=415).
Content type '' not supported

显然是拒绝的, 没有编写 GET 访问

通过 postman 访问

请求参数

1
2
3
4
5
{
"page": 1,
"pageSize": 20,
"platform": 1
}

返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"currentPage": 1,
"totalPage": 1,
"totalRows": 1,
"ipaList": [
{
"id": "1",
"title": "APPName",
"ver": "v0.0.1",
"url": "https://.../xxxx.ipa",
"des": "下单问题修改",
"mainfest": "https://..../manifest.plist",
"createTime": "2019-11-08 12:54:20"
},
{...},
{...}
]
}

由于自己部署的 Tomcat 下的 webapp 已经被运维同学, 通过 Nginx 转发为 HTTPS,

并绑定了 9000 端口号.所以这里修改 STS 下的默认端口号为 9000, 以便数据接口能够通过绑定的域名进行访问

因此 POST 访问的地址可以变更为 https://xxxxxxx.com/list

Fastlane

Fastlane 的安装

Fastlane 官方文档 Fastlane 官方文档

1
2
3
4
5
$ xcode-select --install

出现
xcode-select: error: command line tools are already installed, use "Software Update" to install updates
表示已经安装过 xcode command line tools

继续执行

1
2
3
4
5
$ sudo gem install -n /usr/local/bin fastlane -NV

......
Successfully installed fastlane-2.130.0
1 gem installed

安装 Fastlane

1
2
3
4
$ brew install fastlane
.......
After doing so close the terminal session and restart it to start using fastlane ?
? fastlane was successfully installed!
对 Jenkins 构建项目实行 fastlane 化构建
1
$ cd /Users/your_user_name/.jenkins/workspace/your project 
1
$ fastlane init
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[✔] Looking for iOS and Android projects in current directory...
[15:03:11]: Created new folder './fastlane'.
[15:03:11]: Detected an iOS/macOS project in the current directory: 'Jenkins.xcworkspace'
[15:03:11]: -----------------------------
[15:03:11]: --- Welcome to fastlane 🚀 ---
[15:03:11]: -----------------------------
[15:03:11]: fastlane can help you with all kinds of automation for your mobile app
[15:03:11]: We recommend automating one task first, and then gradually automating more over time
[15:03:11]: What would you like to use fastlane for?
1. 📸 Automate screenshots
2. 👩‍✈️ Automate beta distribution to TestFlight
3. 🚀 Automate App Store distribution
4. 🛠 Manual setup - manually setup your project to automate your tasks
? 4
[15:03:22]: ------------------------------------------------------------
[15:03:22]: --- Setting up fastlane so you can manually configure it ---
[15:03:22]: ------------------------------------------------------------
[15:03:22]: Installing dependencies for you...
[15:03:22]: $ bundle update

这里可以去喝个咖啡 / 抽根烟啥的. 还可以 更换 gem 源.
如果 Fastlane bundle update 卡住解决方案

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
[Jenkins:Jenkins ooops$ fastlane init
[✔] 🚀
[✔] Looking for iOS and Android projects in current directory...
[15:03:11]: Created new folder './fastlane'.
[15:03:11]: Detected an iOS/macOS project in the current directory: 'Jenkins.xcworkspace'
[15:03:11]: -----------------------------
[15:03:11]: --- Welcome to fastlane 🚀 ---
[15:03:11]: -----------------------------
[15:03:11]: fastlane can help you with all kinds of automation for your mobile app
[15:03:11]: We recommend automating one task first, and then gradually automating more over time
[15:03:11]: What would you like to use fastlane for?
1. 📸 Automate screenshots
2. 👩‍✈️ Automate beta distribution to TestFlight
3. 🚀 Automate App Store distribution
4. 🛠 Manual setup - manually setup your project to automate your tasks
? 4
[15:03:22]: ------------------------------------------------------------
[15:03:22]: --- Setting up fastlane so you can manually configure it ---
[15:03:22]: ------------------------------------------------------------
[15:03:22]: Installing dependencies for you...
[15:03:22]: $ bundle update
^C^C
Cancelled... use --verbose to show the stack trace
Jenkins:Jenkins ooops$
Jenkins:Jenkins ooops$
Jenkins:Jenkins ooops$ gem sources --add https://gems.ruby-china.com/ --remove https://rubygems.org/
source https://gems.ruby-china.com/ already present in the cache
source https://rubygems.org/ not present in cache
Jenkins:Jenkins ooops$ fastlane init
[✔] 🚀
[15:10:58]: ------------------------------
[15:10:58]: --- Step: default_platform ---
[15:10:58]: ------------------------------

--------- ios---------
----- fastlane ios custom_lane
Description of what the lane does

Execute using `fastlane [lane_name]`
[15:10:58]: ------------------
[15:10:58]: fastlane is already set up at path `./fastlane/`, see the available lanes above
[15:10:58]:
[15:10:58]: ------------------------------
[15:10:58]: --- Where to go from here? ---
[15:10:58]: ------------------------------
[15:10:58]: 📸 Learn more about how to automatically generate localized App Store screenshots:
[15:10:58]: https://docs.fastlane.tools/getting-started/ios/screenshots/
[15:10:58]: 👩‍✈️ Learn more about distribution to beta testing services:
[15:10:58]: https://docs.fastlane.tools/getting-started/ios/beta-deployment/
[15:10:58]: 🚀 Learn more about how to automate the App Store release process:
[15:10:58]: https://docs.fastlane.tools/getting-started/ios/appstore-deployment/
[15:10:58]: 👩‍⚕️ Learn more about how to setup code signing with fastlane
[15:10:58]: https://docs.fastlane.tools/codesigning/getting-started/
Jenkins:Jenkins ooops$ fastlane init
[✔] 🚀
[✔] Looking for iOS and Android projects in current directory...
[15:40:42]: Created new folder './fastlane'.
[15:40:42]: Detected an iOS/macOS project in the current directory: 'Jenkins.xcworkspace'
[15:40:42]: -----------------------------
[15:40:42]: --- Welcome to fastlane 🚀 ---
[15:40:42]: -----------------------------
[15:40:42]: fastlane can help you with all kinds of automation for your mobile app
[15:40:42]: We recommend automating one task first, and then gradually automating more over time
[15:40:42]: What would you like to use fastlane for?
1. 📸 Automate screenshots
2. 👩‍✈️ Automate beta distribution to TestFlight
3. 🚀 Automate App Store distribution
4. 🛠 Manual setup - manually setup your project to automate your tasks
? 4
[15:40:45]: ------------------------------------------------------------
[15:40:45]: --- Setting up fastlane so you can manually configure it ---
[15:40:45]: ------------------------------------------------------------
[15:40:45]: --------------------------------------------------------
[15:40:45]: --- ✅ Successfully generated fastlane configuration ---
[15:40:45]: --------------------------------------------------------
[15:40:45]: Generated Fastfile at path `./fastlane/Fastfile`
[15:40:45]: Generated Appfile at path `./fastlane/Appfile`
[15:40:45]: Gemfile and Gemfile.lock at path `Gemfile`
[15:40:45]: Please check the newly generated configuration files into git along with your project
[15:40:45]: This way everyone in your team can benefit from your fastlane setup
[15:40:45]: Continue by pressing Enter ⏎

[15:40:47]: fastlane will collect the number of errors for each action to detect integration issues
[15:40:47]: No sensitive/private information will be uploaded, more information: https://docs.fastlane.tools/#metrics
[15:40:47]: ----------------------
[15:40:47]: --- fastlane lanes ---
[15:40:47]: ----------------------
[15:40:47]: fastlane uses a `Fastfile` to store the automation configuration
[15:40:47]: Within that, you'll see different lanes.
[15:40:47]: Each is there to automate a different task, like screenshots, code signing, or pushing new releases
[15:40:47]: Continue by pressing Enter ⏎

[15:40:49]: --------------------------------------
[15:40:49]: --- How to customize your Fastfile ---
[15:40:49]: --------------------------------------
[15:40:49]: Use a text editor of your choice to open the newly created Fastfile and take a look
[15:40:49]: You can now edit the available lanes and actions to customize the setup to fit your needs
[15:40:49]: To get a list of all the available actions, open https://docs.fastlane.tools/actions
[15:40:49]: Continue by pressing Enter ⏎

[15:40:51]: ------------------------------
[15:40:51]: --- Where to go from here? ---
[15:40:51]: ------------------------------
[15:40:51]: 📸 Learn more about how to automatically generate localized App Store screenshots:
[15:40:51]: https://docs.fastlane.tools/getting-started/ios/screenshots/
[15:40:51]: 👩‍✈️ Learn more about distribution to beta testing services:
[15:40:51]: https://docs.fastlane.tools/getting-started/ios/beta-deployment/
[15:40:51]: 🚀 Learn more about how to automate the App Store release process:
[15:40:51]: https://docs.fastlane.tools/getting-started/ios/appstore-deployment/
[15:40:51]: 👩‍⚕️ Learn more about how to setup code signing with fastlane
[15:40:51]: https://docs.fastlane.tools/codesigning/getting-started/
[15:40:51]:
[15:40:51]: To try your new fastlane setup, just enter and run
[15:40:51]: $ fastlane custom_lane

执行 fastlane custom_lane

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Jenkins:Jenkins ooops$ fastlane custom_lane
[✔] 🚀
[15:41:10]: fastlane detected a Gemfile in the current directory
[15:41:10]: however it seems like you don't use `bundle exec`
[15:41:10]: to launch fastlane faster, please use
[15:41:10]:
[15:41:10]: $ bundle exec fastlane custom_lane
[15:41:10]:
[15:41:10]: Get started using a Gemfile for fastlane https://docs.fastlane.tools/getting-started/ios/setup/#use-a-gemfile
[15:41:12]: ------------------------------
[15:41:12]: --- Step: default_platform ---
[15:41:12]: ------------------------------
[15:41:12]: Driving the lane 'ios custom_lane' 🚀

+------+------------------+-------------+
| fastlane summary |
+------+------------------+-------------+
| Step | Action | Time (in s) |
+------+------------------+-------------+
| 1 | default_platform | 0 |
+------+------------------+-------------+

[15:41:12]: fastlane.tools finished successfully 🎉
Jenkins:Jenkins ooops$

ipa 可以内测分发的条件

1 - 配置必要的打包证书

2 - 支持 HTTPS 访问的服务器

3 - 一个用来提供下载 ipa 文件的网页

fastlane 脚本编写

修改 Fastfile 文件

初始脚本如下

1
2
3
4
5
6
7
default_platform(:ios)

platform :ios do
desc "Description of what the lane does"
lane :custom_lane do
end
end

修改 lane 自定义命令

1
2
3
4
5
6
7
default_platform(:ios)

platform :ios do
desc "Description of what the lane does"
lane :test do
end
end

最终脚本如下

更多语法可以参考 Fastlane 官方文档

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
159
160
161
162
163
164
# This file contains the fastlane.tools configuration
# You can find the documentation at https://docs.fastlane.tools
#
# For a list of all available actions, check out
#
# https://docs.fastlane.tools/actions
#
# For a list of all available plugins, check out
#
# https://docs.fastlane.tools/plugins/available-plugins
#

# Uncomment the line if you want fastlane to automatically update itself
# update_fastlane

# 定义打包平台
default_platform :ios

# 蒲公英api_key和user_key
# 蒲公英账号
# api_key="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxXXX"
# user_key="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxXXX"

# 任务脚本 -----------------------------------
platform :ios do
lane :test do|options|
branch = options[:branch]


# cocoapods 支持
cocoapods(
clean: true,
use_bundle_exec: false,
podfile: "./Podfile"
)


# puts "\n
# +------------------------+---------+------------------+
# | \033[1;32mchoice scheme\033[0m |
# +---------------+--------+---------+------------------+
# | scheme | Index | version | Description |
# +---------------+--------+---------+------------------+
# | development | 1 | Test | Test-Package |
# +---------------+--------+---------+------------------+
# | ad-hoc | 2 | Release | ad-hoc-Package |
# +---------------+--------+---------+------------------+
# | app-store | 3 | Release | app-store-Package|
# +---------------+--------+---------+------------------+
# "

#自选scheme
# schemeName 输出的ipa名称的前缀, 也是 xcode 中 scheme 的名字
# export_method 指定打包所使用的输出方式,目前支持app-store, package, ad-hoc, enterprise, development
# configuration 指定打包方式,Release 或者 Debug , Test-Debug
scheme="1"#STDIN.gets

if scheme == "1"

schemeName = "Jenkins"
export_method = "development"
configuration = "Test-Debug"

elsif scheme == "2"

schemeName = "Jenkins"
export_method = "ad-hoc"
configuration = "Release"

elsif scheme == "3"

schemeName = "Jenkins"
export_method = "ad-hoc"
configuration = "Release"

else

schemeName = "Jenkins"
export_method = "app-store"
configuration = "Release"

end


# puts "请输入版本描述:#{version}"

puts "请输入版本描述:"
commit = last_git_commit
message = commit[:message]
author = commit[:author] # author of the commit
author_email = commit[:author_email] # email of the author of the commit
hash = commit[:commit_hash] # long sha of commit
short_hash = commit[:abbreviated_commit_hash] # short sha of commit
# desc = "commit:#{commit}\nauthor:#{author}\nauthor_email:#{author_email}\nhash:#{hash}\nshort_hash:#{short_hash}"#STDIN.gets
desc = "#{message} &#10 short_hash:#{short_hash}"

puts "\n#{desc}"

version = get_version_number(xcodeproj: "Jenkins.xcodeproj")
# 下面的为 多 Target 的写法
# version = get_version_number(xcodeproj: "Jenkins.xcodeproj", target: "Jenkins")
# build_number = get_build_number(xcodeproj: "Jenkins.xcodeproj")

puts "\n
+------------------------+---------+----------------------------+
\033[1;32mpacking\033[0m
+--------------------+-----------------------+------------------+
| schemeName export_method configuration
+--------------------+-----------------------+------------------+
| #{schemeName} #{export_method} #{configuration}
+--------------------+-----------------------+------------------+"

#last_component_path = "#{version}#{build_number}"
# 最后一级 url 路径 - 这里取每次 commit 的 short_hash 值
last_component_path = "#{short_hash}"
# 用来提供 ipa 内测包安装地址
url_file_path = "https://ios.xxxxx.com.cn/app"
# 执行 ipa 包输出在磁盘上的目录
output_dir = "/Users/your_user_name/Library/Tomcat/webapps/app/ipa/#{last_component_path}"

# 开始打包
gym(
# 指定scheme
scheme: "#{schemeName}",
# 输出的ipa名称
output_name:"#{schemeName}",
# 是否清空以前的编译信息 true:是
clean:true,
# 指定打包方式,Release 或者 Debug , Test-Debug
configuration:"#{configuration}",
# 指定输出文件夹
output_directory:"#{output_dir}",
# 包输出配置
export_options: {
# 指定打包所使用的输出方式,目前支持app-store, package, ad-hoc, enterprise, development
method:"#{export_method}",
manifest: {
appURL: "#{url_file_path}/ipa/#{last_component_path}/#{schemeName}.ipa",
displayImageURL: "#{url_file_path}/a.png",
fullSizeImageURL: "#{url_file_path}/b.png"
},
provisioningProfiles: { # 此两处配置 为 打 Ad-Hoc 包使用, 需要配置 打包证书 / 描述文件 上面的 `scheme` 应该改为 3
"your app bundle id" => "Jenkins_Ad_Hoc (此处文件为评估官网下载的描述文件), 存放于对应的项目 `fastlane` 文件夹下"
},
codesigning_identity: "通过钥匙串查看, 打包证书(development / distribution) 证书的常用名称"
}
)

sh "node /Users/your_user_name/ipa/ios.js 'your_app_name' '#{url_file_path}/ipa/#{last_component_path}/#{schemeName}.ipa' '#{desc}' '#{version}' '#{url_file_path}/ipa/#{last_component_path}/manifest.plist'"

# `output_dir` 中的 `app/ipa` 为 `Tomcat` 下 `webapp` 文件夹下的子文件夹 `app/ipa` 用来存储 `ipa` 文件包

# `url_file_path` 为 本地 `Tomcat` 下的网站, 通过 `Nginx` 转发出去的内网服务器地址

# 由于蒲公英地址被封禁, 暂时停用
# puts "\n
# \033[1;32m-------------------------------------------\033[0m
# \033[1;32m--------- 开始上传蒲公英 ---------\033[0m
# \033[1;32m-------------------------------------------\033[0m"
# # 开始上传蒲公英
# pgyer(update_description:"#{desc}", api_key: "#{api_key}", user_key: "#{user_key}")

end
end

H5

H5 展示CI, 并提供 APP 分发功能

编辑工具

Sublime Text 3

H5 页面搭建依赖

bootstrap

jQuery

jquery.qrcode.js

jquery.qrcode.js

bootstrap-table.js

bootstrap-paginator.js

bootstrap-table-zh-CN.js 多语言

数据接口 STS

一台支持 HTTPS 访问的服务器, 此项为必须, ipa 包下发需要依赖

参见 数据接口

iOS 部署参考

H5 页面结构
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
<html>

<head>
<meta charset="UTF-8">
<title>接口测试</title>
<meta content="width=device-width, initial-scale=1,maximum-scale=1,maximum-scale=1, user-scalable=no" name="viewport">
<style type="text/css">

</style>
<script src="js/jquery.js"></script>
<script src="js/qrCode.js" ></script>
<script src="js/jquery.timeago.js" ></script>
<link href="dist/bootstrap-table.css" rel="stylesheet">
<link href="css/ipa.css" rel="stylesheet">
<link href="bootstrap/css/bootstrap.css" rel="stylesheet">
<script src="dist/bootstrap-table.js"></script>
<script src="dist/locale/bootstrap-table-zh-CN.js"></script>
<script src="bootstrap/js/bootstrap-paginator.js"></script>
<!-- 最新版本的 Bootstrap 核心 CSS 文件 -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">

<!-- 最新的 Bootstrap 核心 JavaScript 文件 -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
</head>
<body>
<!-- APP logo 展示 -->
<div class="logo"><img src="https://ios.xxxxxx.com.cn/app/b.png" /></div>
<br/>
<!-- 扫一扫二维码展示 -->
<div class="qrcode"><div id="qrcode"></div></div>
<!-- APP 包 展示 -->
<table class="table table-striped table-bordered table-hover" id="tableL01"></table>
<nav class="text-right"> <!--text-right 右对齐,bootstrap中样式-->
<div class="col-lg-12" align="center">
<!-- 分页控件,标签必须是<ul> -->
<ul id="pageButton"></ul>
</div>
</nav>
</body>
</html>
<!-- 页面二维码展示 -->
<script type="text/javascript">
new QRCode(document.getElementById("qrcode"), "https://ios.xxxxxx.com.cn/app/ios.html");
</script>

读取数据库数据方法

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
function getPageOf(page) {
$.ajax({
//请求方式
type : "POST",
//请求地址
url : "https://ios-api.xxxxxx.com.cn/list",
//数据,json字符串
data : JSON.stringify({
page: page,
pageSize: 20,
platform: 1
}),
contentType: "application/json;charset=utf-8",
dataType: 'json',
//请求成功
success : function(res) {
console.log(res);
var totalPages = res.totalPage;

var result = res.ipaList;

//if(page == 1) {
buildTable(result, totalPages);
//}
},
//请求失败,包含具体的错误信息
error : function(e){
console.log(e.status);
console.log(e.responseText);
}
});
}

表格数据构建方法

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
/// 构建表格数据
function buildTable(result, totalPages, current_page) {
$('#tableL01').bootstrapTable('destroy');
/// 构建分页控件
var element = $('#pageButton');
var options = {
bootstrapMajorVersion : 3,
currentPage : current_page,
numberOfPages : 5,
totalPages : totalPages,
itemTexts : function(type, page, current) {
switch (type) {
case "first":
return "首页";
case "prev":
return "上一页";
case "next":
return "下一页";
case "last":
return "末页";
case "page":
return page;
}
},
// 点击事件,用于通过Ajax来刷新整个list列表
onPageClicked : function(event, originalEvent, type, page) {
getPageOf(page);
}
};

element.bootstrapPaginator(options);

/// 构建表格
$('#tableL01').bootstrapTable({
columns: [
{
checkbox: true,
width:60,// 定义列的宽度,单位为像素px
visible: false
},
{
field:'id',
title:' 编号',
width:20,
visible: false

},
{
field:'createTime',
title:'发布时间',
width:80,
align: 'center',
valign: 'middle',
formatter: function (value, row, index) {
return `<span class="time" title="${row.createTime}"></span>`;
}
},
{
field:'title',
title:'名称',
width:50,
visible: false
},
{
field:'des',
title:'描述',
width:100,
// visible: false
},
{
field:'url',
title:'ipa 包',
visible: false
},
{
field:'mainfest',
title:'mainfest文件',
visible: false
},
{
field:'ver',
title:'版本',
width:40,
},
{
title: "操作",
align: 'center',
valign: 'middle',
width: 85,
formatter: function (value, row, index) {
return `<a class="btn btn-primary btn-lg active" href="itms-services://?action=download-manifest&url=${ row.mainfest }">安装</a >`;
}
}
], //表头
data:result, //通过ajax返回的数据
// width:300,
// height:700,
// method: 'get',
pageSize: 20, //每页3条
pageNumber: 1, //第1页
// cache: false, //不缓存
// striped: true,
pagination: false,
sidePagination: 'client',
search: false,
showRefresh: false,
showExport: false,
showFooter: false,
buttonsAlign:'right',
// exportTypes: ['csv', 'txt', 'xml'],
clickToSelect: true,
});
$(".time").timeago();
}

最终页面结构

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
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
<html>

<head>
<meta charset="UTF-8">
<title>接口测试</title>
<meta content="width=device-width, initial-scale=1,maximum-scale=1,maximum-scale=1, user-scalable=no" name="viewport">
<style type="text/css">

</style>
<script src="js/jquery.js"></script>
<script src="js/qrCode.js" ></script>
<script src="js/jquery.timeago.js" ></script>
<link href="dist/bootstrap-table.css" rel="stylesheet">
<link href="css/ipa.css" rel="stylesheet">
<link href="bootstrap/css/bootstrap.css" rel="stylesheet">
<script src="dist/bootstrap-table.js"></script>
<script src="dist/locale/bootstrap-table-zh-CN.js"></script>
<script src="bootstrap/js/bootstrap-paginator.js"></script>
<!-- 最新版本的 Bootstrap 核心 CSS 文件 -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">

<!-- 最新的 Bootstrap 核心 JavaScript 文件 -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>

<script type="text/javascript">
getPageOf(1);
function getPageOf(page) {
$.ajax({
//请求方式
type : "POST",
//请求地址
url : "https://ios-api.xxxxxx.com.cn/list",
//数据,json字符串
data : JSON.stringify({
page: page,
pageSize: 20,
platform: 1
}),
contentType: "application/json;charset=utf-8",
dataType: 'json',
//请求成功
success : function(res) {
console.log(res);
var totalPages = res.totalPage;

var result = res.ipaList;

//if(page == 1) {
buildTable(result, totalPages);
//}
},
//请求失败,包含具体的错误信息
error : function(e){
console.log(e.status);
console.log(e.responseText);
}
});
}

/// 构建表格数据
function buildTable(result, totalPages, current_page) {
$('#tableL01').bootstrapTable('destroy');
/// 构建分页控件
var element = $('#pageButton');
var options = {
bootstrapMajorVersion : 3,
currentPage : current_page,
numberOfPages : 5,
totalPages : totalPages,
itemTexts : function(type, page, current) {
switch (type) {
case "first":
return "首页";
case "prev":
return "上一页";
case "next":
return "下一页";
case "last":
return "末页";
case "page":
return page;
}
},
// 点击事件,用于通过Ajax来刷新整个list列表
onPageClicked : function(event, originalEvent, type, page) {
getPageOf(page);
}
};

element.bootstrapPaginator(options);

/// 构建表格
$('#tableL01').bootstrapTable({
columns: [
{
checkbox: true,
width:60, // 定义列的宽度,单位为像素px
visible: false
},
{
field:'id',
title:' 编号',
width:20,
visible: false

},
{
field:'createTime',
title:'发布时间',
width:80,
align: 'center',
valign: 'middle',
formatter: function (value, row, index) {
return `<span class="time" title="${row.createTime}"></span>`;
}
},
{
field:'title',
title:'名称',
width:50,
visible: false
},
{
field:'des',
title:'描述',
width:100,
// visible: false
},
{
field:'url',
title:'ipa 包',
visible: false
},
{
field:'mainfest',
title:'mainfest文件',
visible: false
},
{
field:'ver',
title:'版本',
width:40,
},
{
title: "操作",
align: 'center',
valign: 'middle',
width: 85,
formatter: function (value, row, index) {
return `<a class="btn btn-primary btn-lg active" href="itms-services://?action=download-manifest&url=${ row.mainfest }">安装</a >`;
}
}
], //表头
data:result, //通过ajax返回的数据
// width:300,
// height:700,
// method: 'get',
pageSize: 20, //每页3条
pageNumber: 1, //第1页
// cache: false, //不缓存
// striped: true,
pagination: false,
sidePagination: 'client',
search: false,
showRefresh: false,
showExport: false,
showFooter: false,
buttonsAlign:'right',
// exportTypes: ['csv', 'txt', 'xml'],
clickToSelect: true,
});
$(".time").timeago();
}
</script>

</head>

<body>
<!-- APP logo 展示 -->
<div class="logo"><img src="https://ios.xxxxxx.com.cn/app/b.png" /></div>
<br/>
<!-- 扫一扫二维码展示 -->
<div class="qrcode"><div id="qrcode"></div></div>
<!-- APP 包 展示 -->
<table class="table table-striped table-bordered table-hover" id="tableL01"></table>
<nav class="text-right"> <!--text-right 右对齐,bootstrap中样式-->
<div class="col-lg-12" align="center">
<!-- 分页控件,标签必须是<ul> -->
<ul id="pageButton"></ul>
</div>
</nav>
</body>

</html>

<script type="text/javascript">
new QRCode(document.getElementById("qrcode"), "https://ios.xxxxxx.com.cn/app/ios.html");
</script>

MySQL

MySQL 数据库环境配置

工具及环境

Navicat For MySQL

工具为非必须, 其他图形化操作工具也可以, 也可以使用终端环境建库、建表

MySQL 终端环境安装

依赖 Homebrew Homebrew

1
$ brew install mysql

终端环境建库、建表可以看这里MySQL 菜鸟教程

需要建立的表结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- ----------------------------
-- Table structure for ipaInfo
-- ----------------------------
DROP TABLE IF EXISTS `ipaInfo`;
CREATE TABLE `ipaInfo` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(50) DEFAULT NULL,
`des` varchar(500) DEFAULT NULL,
`mainfest` varchar(255) DEFAULT NULL,
`url` varchar(200) DEFAULT NULL,
`ver` varchar(50) DEFAULT NULL,
`createTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
);
Nodejs 插入数据

依赖 Nodejs

编写插入数据的脚本 Nodejs MySQL

脚本如下

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
// 接收命令行传递的参数
var arguments = process.argv.splice(2);
var title = arguments[0]
var url = arguments[1]
var desc = arguments[2]
var version = arguments[3]
var mainfest = arguments[4]

// 插入数据
var mysql = require('mysql');
var connection = mysql.createConnection({
host : 'localhost',
user : 'root',
password : 'password',
database : 'testFlight'
});

connection.connect();
var addSql = 'INSERT INTO ipaInfo(title, url, des, ver, mainfest) VALUES(?, ?, ?, ?, ?)';
var addSqlParams = [title, url, desc, version, mainfest];

connection.query(addSql,addSqlParams,function (err, result) {
if(err){
console.log('[INSERT ERROR] - ',err.message);
return;
}
//console.log('INSERT ID:',result.insertId);
console.log('INSERT ID:',result);
});

connection.end();

存入数据测试, 这里只是做测试数据插入处理, 后面真实数据将由 FastlaneRuby 执行插入.

将脚本保存到 /Users/your_user_name/ipa/ 文件夹下, 备用

1
node /Users/your_user_name/ipa/ios.js 'APPName' 'https://.../xxxx.ipa' '下单问题修改' 'v0.0.1' 'https://..../manifest.plist'

终端输出如下, 表示插入成功

1
2
3
4
5
6
7
8
9
INSERT ID: OkPacket {
fieldCount: 0,
affectedRows: 1,
insertId: 64,
serverStatus: 2,
warningCount: 0,
message: '',
protocol41: true,
changedRows: 0 }

入库数据如下

id title url des ver mainfest createTime
1 APPName https://…/xxxx.ipa 下单问题修改 v0.0.1 https://…./manifest.plist 2019-10-16 16:00:53

Tomcat

本机Tomcat网站目录

以下内容为本机 Tomcat webapp 目录 app 文件夹下, 目录结构

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
.
├── a.png // Fastlane 安装 MD 下, `gym` 配置 `displayImageURL` 的文件地址
├── android.html // `android` 内测分发网页
├── apk // `apk` 包 存储目录
│   ├── 0e216939
│   │   └── android.apk
│   └── 4c8566c2
│   └── android.apk
├── b.png // Fastlane 安装 MD 下, `gym` 配置 `fullSizeImageURL` 的文件地址
├── bootstrap
│   ├── css
│   ├── fonts
│   └── js
├── css
│   └── ipa.css
├── dist
│   ├── extensions
│   ├── locale
│   └── themes
├── ios.html // `ios` 内测分发网页
├── ipa // `ipa` 包 存储目录, Fastlane 安装 MD 下, `gym` 配置 `appURL` 的文件地址
│   ├── 0076cf88 // commit short_hash 值
│   │   ├── xxxxxx-TEST.ipa
│   │   └── manifest.plist
│   ├── 038e1399
│   │   ├── xxxxxx-TEST.ipa
│   │   └── manifest.plist
└── js // jQuery js 插件目录

Jenkins一键发布ipa到蒲公英

什么是 Jenkins

Jenkins 是一个持续集成工具,它可以在设定的某个时间点(或者代码有更新等情况)自动去构建安装包,同时可以将安装包上传到第三方平台,比如:Bugly、蒲公英,这样测试人员可以通过微信、QQ扫一扫直接安装

建议下载 .war文件 配合Tomcat环境使用.

直接下载了.war文件,这个文件是直接放在Tomcatwebapps目录下.

优点:它的权限就是当前用户.

开始安装Tomcat

把下载好的 Tomcat 文件夹 放到你想放的目录

然后把 .war 文件放入Tomcat文件夹下的 webapps 目录下

运行Jenkins

命令行进入Tomcat安装目录.

1
cd /Users/ooops/apache-tomcat-7.0.75/bin

可能会报 -bash: /Users/ooops/apache-tomcat-7.0.75/bin/startup.sh: Permission denied错误,这是因为对该文件没有执行权限造成的. 执行如下命令:

1
chmod +x *.sh

然后执行

1
sh startup.sh

打开浏览器输入 localhost:8080 看到 Tomcat启动页面,表示Tomcat安装成功 进入Tomcat目录下 conf打开 tomcat-users.xml文件,在文件根节点结束之前添加如下配置

1
2
<role rolename="manager-gui"/>
<user username="admin" password="admin" roles="manager-gui"/>

设置Tomcat的登录信息 使用如下命令重启Tomcat

1
2
3
cd /Users/ooops/apache-tomcat-7.0.75/bin
sh shutdown.sh
sh startup.sh

再次访问 localhost:8080 选择 Manager Apps 登录Tomcat,选择列表中Jenkins 输入如下命令查看Jenkins初始密码

1
open /Users/ooops/.jenkins/secrets/initialAdminPassword

粘贴初始密码,点击继续,等待片刻

选择Install suggested plugins 安装推荐的 Jenkins 插件

推荐的插件安装完毕,还有我们自己需要的插件安装

首先进入插件管理器界面,进入方式: 系统管理->管理插件

在可选插件中搜索我们需要的插件,进行安装

GIT 环境下我们需要安装的插件有 三 个

  • Gitlab Hook Plugin 因为我们用的是GitLab来管理源代码,Jenkins本身并没有自带GitLab插件
  • Xcode Xcode环境插件.
  • Keychains and Provisioning Profiles Management 钥匙串和证书配置

配置GIT

私钥文件位置 家目录下的 .ssh 文件夹下

配置 GIT 私钥

钥匙串和证书配置

进入方式: 系统管理->Keychains and Provisioning Profiles Management

钥匙串文件路径 : /Users/用户名(your user name)/Library/Keychains/login.keychain

上传钥匙串 keychain 和 证书描述文件 Provisioning Profile

上传之后的要是串和证书描述文件会被Jenkins

保存在/Users/改成你的(your user name)/.jenkins/kpp_upload目录下

配置对应的钥匙串中的开发(调试) 生产(发布) 证书

首先打开钥匙串,获取证书的常用名称

我们将证书的常用名称点入 钥匙串的配置中,具体配置参考下图

上图目录配置,改为/Users/改成你的(your user name)/.jenkins/kpp_upload.偷个懒 不补图了.

点击确定完成配置.

系统全局配置

Jenkins 主页 -> 系统管理 -> 系统设置 -> 全局属性 -> 勾选Keychains and

Provisioning Profiles Management 填入上传到 Jenkins 的证书和描述文件目录

/Users/ooops(你的用户名)/.jenkins/kpp_upload

下面开始构建项目配置.

创建一个多配置的项目

General 参数

源码管理 参数

构建触发器设置

这里是设置自动化测试的地方。
涉及的内容很多,
暂时我也没有深入研究,这里暂时先不设置。
有自动化测试需求的可以 研究研究这里的设置。

不过这里两个配置还是需要是设置的

Poll SCM (poll source code management) 轮询源码管理

需要设置源码的路径才能起到轮询的效果。

一般设置为类似结果: 0/5 每5分钟轮询一次

Build periodically (定时构建)

一般设置为类似: 00 20 * 每天 20点执行定时构建

格式如下 (具体可以参考后面的小问号??) 嘿嘿

分钟(0-59) 小时(0-23) 日期(1-31) 月(1-12) 周几(0-7,0和7都是周日)

设置之后文本输入域下面会有你填写格式的翻译.

参数配置参考:

https://blog.csdn.net/xueyingqi/article/details/53216506

https://blog.csdn.net/yezicjj/article/details/52763700

构建环境 参数

参考下图设置

请参考 钥匙串和证书配置 中关于钥匙串的配置.

构建 设置

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
# 工程名
APP_NAME="你的项目名称"
# 证书
CODE_SIGN_DISTRIBUTION="iPhone Developer: XXXXXXX"
#CODE_SIGN_DISTRIBUTION="iPhone Distribution: XXXXXXXXXX"
# info.plist路径
project_infoplist_path="./${APP_NAME}/Info.plist"

#取版本号
bundleShortVersion=$(/usr/libexec/PlistBuddy -c "print CFBundleShortVersionString" "${project_infoplist_path}")

#取build值
bundleVersion=$(/usr/libexec/PlistBuddy -c "print CFBundleVersion" "${project_infoplist_path}")

DATE="$(date +%Y%m%d)"
IPANAME="${APP_NAME}_V${bundleShortVersion}_${DATE}.ipa"
#IPANAME="${APP_NAME}.ipa"

#要上传的ipa文件路径
IPA_PATH="$HOME/${IPANAME}"
echo ${IPA_PATH}
echo "${IPA_PATH}">> text.txt

#下面2行是没有Cocopods的用法
echo "=================clean================="
xcodebuild -target "${APP_NAME}" -configuration 'Release' clean

echo "+++++++++++++++++build+++++++++++++++++"
xcodebuild -target "${APP_NAME}" -sdk iphoneos -configuration 'Release' CODE_SIGN_IDENTITY="${CODE_SIGN_DISTRIBUTION}" SYMROOT='$(PWD)'

#//下面2行是集成有Cocopods的用法
#echo "=================clean================="
#xcodebuild -workspace "${APP_NAME}.xcworkspace" -scheme "${APP_NAME}" -configuration 'Release' clean

#echo "+++++++++++++++++build+++++++++++++++++"
#xcodebuild -workspace "${APP_NAME}.xcworkspace" -scheme "${APP_NAME}" -sdk iphoneos -configuration 'Release' CODE_SIGN_IDENTITY="${CODE_SIGN_DISTRIBUTION}" SYMROOT='$(PWD)'

xcrun -sdk iphoneos PackageApplication "./Release-iphoneos/${APP_NAME}.app" -o ~/"${IPANAME}"

#蒲公英上的User Key
uKey="9b0aa78a32a......4e1ca68bdbf"
#蒲公英上的API Key
apiKey="ac75fcf38.....7b9f19f2e4b23"
#要上传的ipa文件路径
IPA_PATH=$(cat text.txt)

rm -rf text.txt

#执行上传至蒲公英的命令
echo "++++++++++++++upload+++++++++++++"
curl -F "file=@${IPA_PATH}" -F "uKey=${uKey}" -F "_api_key=${apiKey}" https://www.pgyer.com/apiv1/app/upload

构建项目

以下是一次成功构建的输出信息

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
Started by user ooops
Building in workspace /Users/ooops/.jenkins/workspace/test
> git rev-parse --is-inside-work-tree # timeout=10
Fetching changes from the remote Git repository //拉取GIT仓库
> git config remote.origin.url git@github.com:xxxxx/jenkinsTest.git # timeout=10
Fetching upstream changes from git@github.com:xxxxx/jenkinsTest.git
> git --version # timeout=10
> git fetch --tags --progress git@github.com:xxxxx/jenkinsTest.git +refs/heads/*:refs/remotes/origin/*
> git rev-parse refs/remotes/origin/master^{commit} # timeout=10
> git rev-parse refs/remotes/origin/origin/master^{commit} # timeout=10
Checking out Revision 1f023f0132273ddc4d3d98a8283e3a15bbe254aa (refs/remotes/origin/master)
> git config core.sparsecheckout # timeout=10
> git checkout -f 1f023f0132273ddc4d3d98a8283e3a15bbe254aa
> git rev-list 1f023f0132273ddc4d3d98a8283e3a15bbe254aa # timeout=10
[test] $ /bin/sh -xe /Users/ooops/apache-tomcat-7.0.75/temp/hudson6997647762219224773.sh
+ APP_NAME=jenkinsTest
+ CODE_SIGN_DISTRIBUTION='iPhone Developer: xxxxx'
+ project_infoplist_path=./jenkinsTest/Info.plist
++ /usr/libexec/PlistBuddy -c 'print CFBundleShortVersionString' ./jenkinsTest/Info.plist
+ bundleShortVersion=1.0
++ /usr/libexec/PlistBuddy -c 'print CFBundleVersion' ./jenkinsTest/Info.plist
+ bundleVersion=1
++ date +%Y%m%d
+ DATE=20170312
+ IPANAME=jenkinsTest_V1.0_20170312.ipa
+ IPA_PATH=/Users/ooops/jenkinsTest_V1.0_20170312.ipa
+ echo /Users/ooops/jenkinsTest_V1.0_20170312.ipa
/Users/ooops/jenkinsTest_V1.0_20170312.ipa
+ echo /Users/ooops/jenkinsTest_V1.0_20170312.ipa
+ echo =================clean=================
=================clean=================
+ xcodebuild -target jenkinsTest -configuration Release clean
2017-03-12 12:56:15.278 xcodebuild[12363:1935879] [MT] PluginLoading: Required plug-in compatibility UUID DAxxxD8-C509-4xB-8xx5-84xxxxxxx701 for plug-in at path '~/Library/Application Support/Developer/Shared/Xcode/Plug-ins/VVDocumenter-Xcode.xcplugin' not present in DVTPlugInCompatibilityUUIDs
2017-03-12 12:56:15.279 xcodebuild[12363:1935879] [MT] PluginLoading: Required plug-in compatibility UUID DAxxxD8-C509-4xB-8xx5-84xxxxxxx701 for plug-in at path '~/Library/Application Support/Developer/Shared/Xcode/Plug-ins/HOStringSense.xcplugin' not present in DVTPlugInCompatibilityUUIDs
2017-03-12 12:56:15.280 xcodebuild[12363:1935879] [MT] PluginLoading: Required plug-in compatibility UUID DAxxxD8-C509-4xB-8xx5-84xxxxxxx701 for plug-in at path '~/Library/Application Support/Developer/Shared/Xcode/Plug-ins/ESJsonFormat.xcplugin' not present in DVTPlugInCompatibilityUUIDs
2017-03-12 12:56:15.280 xcodebuild[12363:1935879] [MT] PluginLoading: Required plug-in compatibility UUID DAxxxD8-C509-4xB-8xx5-84xxxxxxx701 for plug-in at path '~/Library/Application Support/Developer/Shared/Xcode/Plug-ins/Auto-Importer.xcplugin' not present in DVTPlugInCompatibilityUUIDs
=== CLEAN TARGET jenkinsTest OF PROJECT jenkinsTest WITH CONFIGURATION Release ===

Check dependencies

Create product structure
/bin/mkdir -p /Users/ooops/.jenkins/workspace/test/build/Release-iphoneos/jenkinsTest.app

Clean.Remove clean build/jenkinsTest.build/Release-iphoneos/jenkinsTest.build
builtin-rm -rf /Users/ooops/.jenkins/workspace/test/build/jenkinsTest.build/Release-iphoneos/jenkinsTest.build

Clean.Remove clean build/Release-iphoneos/jenkinsTest.app
builtin-rm -rf /Users/ooops/.jenkins/workspace/test/build/Release-iphoneos/jenkinsTest.app

Clean.Remove clean build/Release-iphoneos/jenkinsTest.app.dSYM
builtin-rm -rf /Users/ooops/.jenkins/workspace/test/build/Release-iphoneos/jenkinsTest.app.dSYM

** CLEAN SUCCEEDED **

+ echo +++++++++++++++++build+++++++++++++++++
+++++++++++++++++build+++++++++++++++++
+ xcodebuild -target jenkinsTest -sdk iphoneos -configuration Release 'CODE_SIGN_IDENTITY=iPhone Developer: xxxxxxxxxxxxxxxx' 'SYMROOT=$(PWD)'
2017-03-12 12:56:16.118 xcodebuild[12376:1936105] [MT] PluginLoading: Required plug-in compatibility UUID DAxxxx8-C509-4xxB-8B55-84AxxxxxE701 for plug-in at path '~/Library/Application Support/Developer/Shared/Xcode/Plug-ins/VVDocumenter-Xcode.xcplugin' not present in DVTPlugInCompatibilityUUIDs
2017-03-12 12:56:16.119 xcodebuild[12376:1936105] [MT] PluginLoading: Required plug-in compatibility UUID DAxxxxD8-C509-4D8B-8xxxx-84Axxxx01 for plug-in at path '~/Library/Application Support/Developer/Shared/Xcode/Plug-ins/HOStringSense.xcplugin' not present in DVTPlugInCompatibilityUUIDs
2017-03-12 12:56:16.119 xcodebuild[12376:1936105] [MT] PluginLoading: Required plug-in compatibility UUID DA4xxx8-C509-4Dxx-8xx5-84AxxxE701 for plug-in at path '~/Library/Application Support/Developer/Shared/Xcode/Plug-ins/ESJsonFormat.xcplugin' not present in DVTPlugInCompatibilityUUIDs
2017-03-12 12:56:16.120 xcodebuild[12376:1936105] [MT] PluginLoading: Required plug-in compatibility UUID Dxxxx8-C509-xx-8B55-84xxxxxAE701 for plug-in at path '~/Library/Application Support/Developer/Shared/Xcode/Plug-ins/Auto-Importer.xcplugin' not present in DVTPlugInCompatibilityUUIDs
Build settings from command line:
CODE_SIGN_IDENTITY = iPhone Developer: qiang zhang (36XGYU49EQ)
SDKROOT = iphoneos10.1
SYMROOT = $(PWD)

=== BUILD TARGET jenkinsTest OF PROJECT jenkinsTest WITH CONFIGURATION Release ===

Check dependencies

Validate Release-iphoneos/jenkinsTest.app
cd /Users/ooops/.jenkins/workspace/test
export PATH="/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/usr/bin:/Applications/Xcode.app/Contents/Developer/usr/bin:/Users/ooops/.rvm/gems/ruby-2.2.2/bin:/Users/ooops/.rvm/gems/ruby-2.2.2@global/bin:/Users/ooops/.rvm/rubies/ruby-2.2.2/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Users/ooops/.rvm/bin:/Users/ooops/.rvm/bin"
export PRODUCT_TYPE=com.apple.product-type.application
builtin-validationUtility /Users/ooops/.jenkins/workspace/test/Release-iphoneos/jenkinsTest.app -validate-for-store

** BUILD SUCCEEDED **

+ xcrun -sdk iphoneos PackageApplication ./Release-iphoneos/jenkinsTest.app -o /Users/ooops/jenkinsTest_V1.0_20170312.ipa



warning: PackageApplication is deprecated, use `xcodebuild -exportArchive` instead.

//这里是上传蒲公英的配置.
+ uKey=9b0aa78a32........64e1ca68bdbf
+ apiKey=ac75fcf3..........a47b9f19f2e4b23
++ cat text.txt
+ IPA_PATH=/Users/ooops/jenkinsTest_V1.0_20170312.ipa
+ rm -rf text.txt
+ echo ++++++++++++++upload+++++++++++++
++++++++++++++upload+++++++++++++
+ curl -F file=@/Users/ooops/jenkinsTest_V1.0_20170312.ipa -F uKey=9b0aa78a32...af26..4e1ca68bdbf -F _api_key=ac75fcf38f2.........19f2e4b23 http://www.pgyer.com/apiv1/app/upload
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed

0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0
100 32843 0 0 100 32843 0 21213 0:00:01 0:00:01 --:--:-- 21202
100 32843 0 0 100 32843 0 12862 0:00:02 0:00:02 --:--:-- 12859
100 33438 100 595 100 32843 220 12149 0:00:02 0:00:02 --:--:-- 12146
{"code":0,"message":"","data":{"appKey":"dd580f5...........0305ac671","userKey":"9b0aa78a.........e1ca68bdbf","appType":"1","appIsLastest":"1","appFileSize":"32368","appName":"jenkinsTest","appVersion":"1.0","appVersionNo":"1","appBuildVersion":"2","appIdentifier":"com.ooops.pull","appIcon":"","appDescription":"","appUpdateDescription":"","appScreenshots":"","appShortcutUrl":"nZwg","appCreated":"2017-03-12 12:56:18","appUpdated":"2017-03-12 12:56:18","appQRCodeURL":"http:\/\/static.pgyer.com\/app\/qrcodeHistory\/ea895709d0e57a4a8e4c8f57811838b2ce985322a94f9443e64a0937c6d29e30"}}Finished: SUCCESS

上传到蒲公英

自定义控件-UILabel

开发中经常会遇到 图文混排

这样的界面,当然你可以使用UIButton来实现.

想着折腾一下

使用自定义UILabel的子类来实现一样的效果,代码如下

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
public enum EIconEdgeDirection {
case left
case right
}

public class IconEdgeInsetsLabel: UILabel {
/// UIView 灵活多变
public var iconView: UIView? {
didSet{
if oldIconView != nil && oldIconView!.isKind(of: UIView.self) {
oldIconView?.removeFromSuperview()
}
oldIconView = iconView
iconView!.x = 0.0
iconView!.y = 0.0
addSubview(iconView!)
}
}
public var edgeInsets: UIEdgeInsets = UIEdgeInsets.zero
/// iconView 默认在左边
public var direction: EIconEdgeDirection = .left
/// 用于调整iconView 和 文字 之间的间距
public var gap: CGFloat = 0.0

private var oldIconView: UIView?

private func sizeToFit(_ text: String) {
self.text = text
self.sizeToFit()
}

private func sizeToFit(attributeText: NSAttributedString) {
self.attributedText = attributedText
self.sizeToFit()
}

public override func drawText(in rect: CGRect) {
var insets = self.edgeInsets
if iconView != nil {
if self.direction == .left {
iconView?.left = insets.left
iconView?.centerY = self.middleY
insets = UIEdgeInsets(top: insets.top, left: insets.left + gap + iconView!.width, bottom: insets.bottom, right: insets.right)
}else if self.direction == .right {
iconView?.right = self.width - insets.right
iconView?.centerY = self.middleY
insets = UIEdgeInsets(top: insets.top, left: insets.left, bottom: insets.bottom, right: insets.right + gap + (iconView?.width)!)
}
}
super .drawText(in: UIEdgeInsetsInsetRect(rect, insets))
}

public override func textRect(forBounds bounds: CGRect, limitedToNumberOfLines numberOfLines: Int) -> CGRect {
let insets = self.edgeInsets
var rect = super.textRect(forBounds: UIEdgeInsetsInsetRect(bounds, insets), limitedToNumberOfLines: numberOfLines)

rect.origin.x -= insets.left
rect.origin.y -= insets.top
rect.size.height += (insets.top + insets.bottom)
if iconView != nil && iconView!.isKind(of: UIView.self) {
rect.size.width += (insets.left + insets.right + gap + iconView!.width)
}else{
rect.size.width += (insets.left + insets.right)
}
return rect
}
}

.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#import <UIKit/UIKit.h>

typedef enum : NSUInteger {
kIconAtLeft,
kIconAtRight,

} EIconEdgeDirection;

@interface IconEdgeInsetsLabel : UILabel
@property (nonatomic, strong) UIView *iconView;
@property (nonatomic) UIEdgeInsets edgeInsets;
@property (nonatomic) EIconEdgeDirection direction;
@property (nonatomic) CGFloat gap;
@end

.m

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
#import "IconEdgeInsetsLabel.h"

@interface IconEdgeInsetsLabel ()

@property (nonatomic, weak) UIView *oldIconView;

@end

@implementation IconEdgeInsetsLabel

- (CGRect)textRectForBounds:(CGRect)bounds limitedToNumberOfLines:(NSInteger)numberOfLines {

UIEdgeInsets insets = self.edgeInsets;

CGRect rect = [super textRectForBounds:UIEdgeInsetsInsetRect(bounds, insets) limitedToNumberOfLines:numberOfLines];

rect.origin.x -= insets.left;
rect.origin.y -= insets.top;
rect.size.height += (insets.top + insets.bottom);
_iconView && [_iconView isKindOfClass:[UIView class]] ?
(rect.size.width += (insets.left + insets.right + _gap + _iconView.frame.size.width)) :
(rect.size.width += (insets.left + insets.right));
return rect;
}

- (void)drawTextInRect:(CGRect)rect {

UIEdgeInsets insets = self.edgeInsets;

if (self.iconView) {

if (self.direction == kIconAtLeft) {
_iconView.left = insets.left;
_iconView.centerY = self.middleY;
insets = UIEdgeInsetsMake(insets.top, insets.left + _gap + _iconView.frame.size.width, insets.bottom, insets.right);

} else if (self.direction == kIconAtRight) {

_iconView.right = self.width - insets.right;
_iconView.centerY = self.middleY;
insets = UIEdgeInsetsMake(insets.top, insets.left, insets.bottom, insets.right + _gap + _iconView.frame.size.width);
}
}

[super drawTextInRect:UIEdgeInsetsInsetRect(rect, insets)];
}

- (void)sizeToFitWithText:(NSString *)text {
self.text = text;
[self sizeToFit];
}

- (void)sizeToFitWithAttrText:(NSAttributedString *)text {
self.attributedText = text;
[self sizeToFit];
}

#pragma mark - setter & getter.

@synthesize iconView = _iconView;

- (void)setIconView:(UIView *)iconView {
_oldIconView && [_oldIconView isKindOfClass:[UIView class]] ? ([_oldIconView removeFromSuperview]) : 0;

_iconView = iconView;
_oldIconView = iconView;
iconView.x = 0.f;
iconView.y = 0.f;

[self addSubview:iconView];
}

- (UIView *)iconView {
return _iconView;
}

@end

那么实际中如何使用呢?

swift

1
2
3
4
5
6
7
8
9
10
11
12
13
lazy var vipTypeLabel: IconEdgeInsetsLabel = {
let l: IconEdgeInsetsLabel = IconEdgeInsetsLabel()
l.textColor = UIColor(hex: 0xFF8A00)
l.backgroundColor = .white
l.layer.cornerRadius = 2
l.layer.masksToBounds = true
l.text = "零售价"
l.edgeInsets = UIEdgeInsets(top: 1, left: 2, bottom: 1, right: 2)
l.textAlignment = .center
l.iconView = UIImageView(image: UIImage.init(named: "img_name"))
l.direction = .right
return l
}()

oc

1
2
3
4
5
6
7
8
9
10
11
12
13
- (IconEdgeInsetsLabel *)iconLabel {
if(_iconLabel) return _iconLabel;
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 12, 12)];
imageView.image = [UIImage imageNamed:@"right_arrow_orange"];
_iconLabel = [IconEdgeInsetsLabel new];
_iconLabel.direction = kIconAtRight;
_iconLabel.gap = 4;
_iconLabel.iconView = imageView;
_iconLabel.textColor = HEXCOLOR(0xFF8A00);
_iconLabel.font = SystemFontWithPxSize(14.f);
_iconLabel.textAlignment = NSTextAlignmentCenter;
return _iconLabel;
}

参考

textrectforbounds limitedtonumberoflines
https://developer.apple.com/documentation/uikit/uilabel/1620545-textrectforbounds