iOS之模块化

背景

当团队发展到一定规模,各业务之间相互影响问题剧增(合作成本变高),就开始进行模块化之路。

  • 模块化的说法:
    更准确的说法是模块化,而不是组件化。
  1. 模块化:是横向划分,各业务模块之间有依赖关系,往往通过路由器解耦。
  2. 组件化:是纵向划分,更多的是基础组件,各组件相互独立。

原理

主流都是通过路由器进行解耦,通过CocoaPods、git进行业务模块分割,对应技术框架有Mediator、JLRouter、CRN等。

路由

我们先看看目前的状态,引用一张图说明:

没错,就是相互引用,耦合很严重。我们想做什么?VC都不要依赖其它的VC。引用一张图说明:

我们就希望VC就只引入中间类(比如Router)就好了,不需要知道其它的控制器。这样是不是很爽?让我们看看怎么设计。

我们需要通过中间类跳转,那么A->中间类->B的过程:

  1. A->中间类:必然有参数的传递(商品ID、闭包等),也包含B所对应的标识(类名、路径、其它标识)。
  2. 中间类->B:通过获取前面拿到的信息,我们根据B的标识,对应到正确的控制器,获取到界面所需参数,并通过闭包,回调给A。

预期结果

我用路由就可以模块化了吗?不行,你代码写一块,可以确保不会改到其它业务模块吗?答案是否定的。所以我们需要通过CocoaPods、git进行管理。我不用CocoaPods,可以进行管理吗?也可以,比如Workspace管理+git形式,RN+git形式等都可以。最后通过路由配置进行业务模块的管理(这里并没有写控制器,因为并不是所有控制器都对外)

JLRoutes的二次封装思路

初步的设计思路,所以还有很多没考虑到的地方。仅供参考。

扩展

想深入了解的,还是需要了解一下主流的一些方案。

  1. JLRoutes方案
  2. 在现有工程中实施基于CTMediator的组件化方案
  3. iOS组件化——蘑菇街案例分析

参考资料

  1. iOS 组件化-路由解耦思想 JLRoutes 实战篇(一)App内控制器跳转
  2. iOS组件化——蘑菇街案例分析
  3. JLRoutes
  4. JSDRouterGuide
  5. iOS应用架构谈 组件化方案
  6. 在现有工程中实施基于CTMediator的组件化方案
  7. 手把手教你搭建cocoapods私有仓库

iOS之网络请求拦截与修改

背景

有时候我们会有些特别的想法:

  1. 查看或动态修改网络的请求与返回的参数
  2. 实现自己的缓存规则

原理

NSURLProtocol可以用于数据请求和数据返回的拦截与修改。具体工作流程,如下。

具体工作流程

需要了解一下URL Loading System(ULS)工作原理。每当我们发出一个请求的时候,ULS会询问NSURLProtocol注册的子类是否需要拦截,需要拦截的话,处理完,再扔回ULS询问。为了避免这个问题,每次拦截完之后,需要标记一下已拦截过,下次再询问就不要再拦截了。

注意事项

  1. NSURLSession需要注意:对于基于NSURLSession的网络请求,需要通过配置NSURLSessionConfiguration对象的protocolClasses属性。
    1
    2
    NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
    sessionConfiguration.protocolClasses = @[[NSClassFromString(@"CustomURLProtocol") class]];
  2. NSURLConnection:直接registerClass就可以用

扩展

iOS - 让WKWebView 支持 NSURLProtocol

最后附上您可能用得着的代码

拦截修改请求的一个类

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
//RSURLProtocol.h
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface RSURLProtocol : NSURLProtocol
+ (void)requestConfig;
@end

NS_ASSUME_NONNULL_END
//RSURLProtocol.m
#import "RSURLProtocol.h"
#import "RSDataHandle.h"
#import <objc/runtime.h>
#import <UIKit/UIKit.h>
#import "ConsoleHelper.h"
#import "RSInfo.h"

static NSString * const RSURLConfig = @"http://xxx.com/config";
static NSString * const RSURLProtocolHandledKey = @"RSURLProtocolHandledKey";
@interface RSURLProtocol()<NSURLSessionDelegate>
@property (nonatomic,strong) NSURLSession *session;
@end
@implementation RSURLProtocol


+(void)load{
[RSURLProtocol registerProtocol];
[self injectNSURLSessionConfiguration];
[self requestConfig];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[[ConsoleHelper sharedInstance] show];
});
}



+ (NSString *)finalConfigString{
if ([RSInfo.sharedInstance.networkAppID length]>0) {
return [NSString stringWithFormat:@"%@?appID=%@",RSURLConfig,RSInfo.sharedInstance.networkAppID];
}
return RSURLConfig;
}
+ (void)requestConfig{
RSInfo.sharedInstance.configInit = false;
NSURLSession *session = [NSURLSession sharedSession];

NSURL *url = [NSURL URLWithString:[self finalConfigString]];

NSURLRequest *request = [NSURLRequest requestWithURL:url];

[[session dataTaskWithRequest:request completionHandler:^(NSData*_Nullable data, NSURLResponse*_Nullable response, NSError*_Nullable error) {

if(!error) {

// 数据请求成功
NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:nil];
if (dic && [dic[@"code"] intValue]==0) {
[RSInfo sharedInstance].configs = dic[@"data"];
RSInfo.sharedInstance.configInit = true;
}

} else {

// 数据请求失败

}
}] resume];
}
+ (void)injectNSURLSessionConfiguration{
Class cls = NSClassFromString(@"__NSCFURLSessionConfiguration") ?: NSClassFromString(@"NSURLSessionConfiguration");
Method originalMethod = class_getInstanceMethod(cls, @selector(protocolClasses));
Method stubMethod = class_getInstanceMethod([self class], @selector(rs_protocolClasses));
if (!originalMethod || !stubMethod) {
[NSException raise:NSInternalInconsistencyException format:@"Couldn't load NEURLSessionConfiguration."];
}
method_exchangeImplementations(originalMethod, stubMethod);
}

- (NSArray *)rs_protocolClasses {
return @[[RSURLProtocol class]];
}

+ (void)registerProtocol
{
[NSURLProtocol registerClass:[self class]];
}

+ (void)unregisterProtocol
{
[NSURLProtocol unregisterClass:[self class]];
}
#pragma mark - 拦截处理

+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
if ([NSURLProtocol propertyForKey:RSURLProtocolHandledKey inRequest:request]) {
return NO;
}

// 拦截http、https
NSString * scheme = [[request.URL scheme] lowercaseString];
if ([scheme isEqual:@"http"]||[scheme isEqual:@"https"]) {
return YES;
}
return NO;
}
// 这个方法用来统一处理请求request 对象的,可以修改头信息,或者重定向。没有特殊需要,则直接return request。
//  如果要在这里做重定向以及头信息的时候注意检查是否已经添加,因为这个方法可能被调用多次,也可以在后面的方法中做。
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {

return request;
}
//主要判断两个request是否相同,如果相同的话可以使用缓存数据,通常只需要调用父类的实现。
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b {
return [super requestIsCacheEquivalent:a toRequest:b];
}
//在拦截到网络请求,并且对网络请求进行定制处理以后。我们需要将网络请求重新发送出去,就可以初始化一个NSURLProtocol对象了:
- (id)initWithRequest:(NSURLRequest *)request cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id <NSURLProtocolClient>)client {

return [super initWithRequest:request cachedResponse:cachedResponse client:client];
}


- (void)reStartRequest {
[RSDataHandle handleRequest:[self request]];

NSMutableURLRequest *mutableReqeust = [[self request] mutableCopy];
//标示该request已经处理过了,防止无限循环
[NSURLProtocol setProperty:@(YES) forKey:RSURLProtocolHandledKey inRequest:mutableReqeust];

//使用NSURLSession继续把request发送出去
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
self.session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:mainQueue];
NSURLSessionDataTask *task = [self.session dataTaskWithRequest:mutableReqeust];

[task resume];
}

//实际请求
- (void)startLoading
{

if (RSInfo.sharedInstance.configInit || [[RSURLProtocol finalConfigString] isEqualToString:[self request].URL.absoluteString]) {
[self reStartRequest];
}else{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self startLoading];
});

}

}

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler
{
[self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
completionHandler(NSURLSessionResponseAllow);
}

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
// 打印返回数据
NSString *dataStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
if (dataStr) {
NSLog(@"***截取URL***: %@", dataTask.currentRequest.URL.absoluteString);
NSLog(@"***截取数据***: %@", dataStr);
dataStr = [RSDataHandle handleResponse:dataStr url:dataTask.currentRequest.URL];
if (dataStr) {
[self.client URLProtocol:self didLoadData:[dataStr dataUsingEncoding:NSUTF8StringEncoding]];
return ;
}
}

[self.client URLProtocol:self didLoadData:data];
}

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
if (error) {
[self.client URLProtocol:self didFailWithError:error];
} else {
[self.client URLProtocolDidFinishLoading:self];
}
}
- (void)stopLoading {
[self.session invalidateAndCancel];
self.session = nil;
}

@end

参考资料

NSURLProtocol拦截 HTTP 请求

iOS静态库与动态库集成问题与处理

情况一:第三方静态库,被自己的动态库、App同时集成:

  • 经典警告:
    One of the two will be used. Which one is undefined.
  • 具体情况:
    第三方静态库(比如RSStaticPrint)同时被APP自己的动态库SDK集成,APP又嵌入自己的动态库SDK
  • 分析:
    【现象】存在两份静态库,各自load方法都会执行,根据调用位置各自调用所在位置的第三方静态库(比如RSStaticPrint)。
    【对象情况简单剖析】
    1. 自己的动态库SDK调用的是自己的动态库SDK里面的类对象RSStaticPrint A(即自己的动态库SDK.framework里的代码)
    2. App调用的实际上是类对象RSStaticPrint B(即.app里的二进制代码)
  • 经典应用:
    【无法调起微信登录问题(微信登录通过Pod只能静态库形式集成)】

【问题】
自己的动态库APP同时集成微信登录的静态库,导致Appdelegate的回调无法进行。因为微信初始化和回调在Appdelegate,而微信登录调用位置在自己的动态库SDK,由于Appdelegate(即App的位置)和自己的动态库SDK用的不是同一个类对象,所以由于未初始化,无法调起微信登录

【处理】
动态库直接集成微信登录的静态库,App不要静态集成

情况二:第三方动态库(比如RSStaticPrint)同时被APP、自己的动态库SDK集成嵌入

结论:不会有问题,实际上都是同一份,即*.app/frameworks/*.framework的这份

进一步探究(选看)

Using Firebase from a framework or a library

Bugless异常监控系统 (iOS端)

一、引言

目前部分线上的业务,缺少客户端的异常错误的线上监控、告警与异常数据聚合并沉淀的平台。也无法在多维度进行异常数据的对比,使得收集应用信息和收集崩溃日志变得日益迫切。
Bugless定位于从线上问题追踪的视角出发,检测代码异常,通过回溯问题,从而解决代码本身问题。它的作用如下:

实时监控SDK业务异常
汇总包体崩溃排重与聚合后的数据
统计影响设备数
上报崩溃日志
收集iOS系统向上兼容性问题
监控客户端请求的网络问题

appium 入门参考

前言

游戏发行业务中,对游戏进行测试是保证游戏质量重要的一环。传统人工测试的方法费时费力、容易出错,自动化测试才是更好的解决方案。appium则是自动化测试的优秀方案,新手可以通过官网的开始文档快速入门。
本文定位为:新手阅读完开始文档的第二篇文档。重点介绍了appium方案与其他方案的优缺点对比,以及在环境配置原生控件查找图片识别方面的关键知识和常用问题解决方法。
本文适合只有单一iOS开发或者自动化测试背景的人员,阅读完开始文档,作为辅助文档阅读;不适合作为新手第一篇文档,或者已同时熟练掌握iOS开发、appium自动化测试的人员阅读。

iOS UI测试方案对比

在正式开始appium的相关内容前,我们先看看还有哪些其他选择。
iOS的UI测试的技术方案有两个大的方向:原生方向以及跨平台的方向。简单表格对比如下:

方向 框架 编程语言 原生控件查找 图片识别 更新维护 开发体验
原生 XCTest Objective-C、Swift 最优 没有 最优 最优
跨平台 appium Python、JS等 一般
跨平台 airtest Python 一般(iOS)