关于CTNetwokring的改造记录

前言

在读这篇文章之前,大家先看一下作者Casa的文章,以便对CTNetwokring的整体设计以及使用方式有个大体的了解。

其次这次改造,主要由需求驱动,因为没有以下这些问题,我也不会做这样的尝试:

  • 公司的网络库使用虽然很方便,依赖性太强,不方便测试,属于集约型的做法,导致公司不太可能写单元测试,所以没有写单元测试,又由于公司后台经常私底下改接口,导致线上版本经常出现问题。
  • CTNetworking刚出来的时候我就关注了,苦于放出来的版本,适合学习,并不合适即插即用,我一直在等有人来改造,结果两年过去了。
  • 由于那是早年安居客的脱敏版,所以设计上面就或多或少有一些依赖业务的东西,建立在它们业务上,的确是最合适方便的,但也由于太合适自身业务,那么也导致无法通用化,其他公司想拿来用就必须改,对于这一点,我在这里要建议强烈批评一下Casa,开源这么好的东西出来,怎么就不弄得更方便一点呢,像CTPersistance(安利一下)那样子插上就能用多好。
  • 我想要开始走向开源社区(不要脸地说,我想涨点粉),在写这个文章前,由于公司项目API一两百个,真是没有余力做短期替换,所以我所提供的那部分代码还没有真正的得到真实项目的考量,另一方面基于我自己不会写后端接口,公司内容涉密问题也导致了没法给出更多完整的使用方式,本文给出的将是我的一套YY方案,欢迎评论区拍砖。

改造篇

如上面提到的,原来的CTNetworking之所以不能做到即插即用,主要有以下几点:

  1. 有旧业务的依赖,比如token的拼接,url组装啊。
  2. 涉及面略广,作为一个网络封装层,提供了CTUDIDGenerator等UUID生成器,CTAPPContext致使引入一些不必要的业务。
  3. ServiceFactory需要硬编码,他需要知道详细的业务Service,而业务的Service硬编码无疑就使得他变得不通用。

谈到要解决上面的问题,那么就有必要来讲解一下CTNetworking的基本组成设计了,这是Casa没有在他文章里面讲的东西,打个比方,比如有这么一个接口,使用CTNetworking的视角来说,它是下面这样的:

1
2
3
4
5
6
7
8
9
https://xxx.com/method?username=corotata => 完整接口
拆解:
https://xxx.com => CTNetworking中的CTService
/method => CTAPIBaseManager的子类提供
username=corotata => CTAPIBaseManager的子类所对应的CTAPIManagerParamSource

所以CTNetworking所做的事情,就是将上面的内容进行拆解工作,将完整的API拼接拆解开成为一个个小部件,再通过CTApiProxy、CTRequestGenerator以及CTServiceFactory做为胶水,将他们组合起来,而通过CTURLResponse做为回调输出。


首先是CTService,它主要是以域名头的形式存在,很多网络库封装都会直接忽略掉这一层,相信有不少朋友也是有过以下的经验吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static NSString *const kBaseUrl = @"https://abc.com";//正式服
NetworkRequestDataModel *dataModel = [[NetworkRequestDataModel alloc] init];
dataModel.interfaceFlag = kLoginUrl;
dataModel.url = [NSString stringWithFormat:@"%@%@",kBaseUrl,kLoginUrl];
dataModel.params = loginDic;
dataModel.isRequireLogin = NO;
dataModel.success = ^(id responseObject) {
DLogInfo(@"登录成功");
};
dataModel.failure = ^(NSError *error,NSInteger httpStatusCode) {
DDLogInfo(@"登录失败");
};
[LGHttpManager dataTaskWithDataModel:dataModel];

问题1 :

基本拼接的方式,渗透到每一个调用者身上,让调用者来提供URL的拼接方式,而往往同一服务器团队,提供的API接口都会是同样的规格,以后如果接口拼接方式改动了,那无疑将会是噩梦,可能有的小伙伴会说,你不知道可以全局替换吗?可以的,无疑有点丑了点,但如果一个APP里面同时有几个服务器团队给你提供的接口呢,而且忽然说要改了拼接方式,全局改起来还是有一点难度吧,又或者是有需求加通用参数或者通用的HTTPHeader啊,那时侯我们该怎么办?程序维护,少不了就是变动。所以为了应对这样的问题,我做了Service层的改造,主要提供了以下接口,交由子类去处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
//CTServiceProtocol
//为某些Service需要拼凑额外字段到URL处
- (NSDictionary *)extraParmas;
//为某些Service需要拼凑额外的HTTPToken,如accessToken
- (NSDictionary *)extraHttpHeadParmasWithMethodName:(NSString *)method;
//提供URL的拼接方式
- (NSString *)urlGeneratingRuleByMethodName:(NSString *)method;
//提供拦截器集中处理Service错误问题,比如token失效要抛通知等,返回值用来做拦截特殊错误时通知上层是否还要继续回调
- (BOOL)shouldCallBackByFailedOnCallingAPI:(CTAPIBaseManager *)apiManager;

看源码时,就具体看下CTRequestGenerator,他主要做请求的组装加工操作,以NSURLRequest返回给CTApiProxy进行网络请求,我贴下核心的代码:

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
- (NSURLRequest *)generateRequestWithServiceIdentifier:(NSString *)serviceIdentifier requestParams:(NSDictionary *)requestParams methodName:(NSString *)methodName requestWithMethod:(NSString *)method {
CTService *service = [[CTServiceFactory sharedInstance] serviceWithIdentifier:serviceIdentifier];
NSString *urlString = [service urlGeneratingRuleByMethodName:methodName];
NSDictionary *totalRequestParams = [self totalRequestParamsByService:service requestParams:requestParams];
NSMutableURLRequest *request = [self.httpRequestSerializer requestWithMethod:method URLString:urlString parameters:totalRequestParams error:NULL];
if (![method isEqualToString:@"GET"] && [CTNetworkingConfigurationManager sharedInstance].shouldSetParamsInHTTPBodyButGET) {
request.HTTPBody = [NSJSONSerialization dataWithJSONObject:requestParams options:0 error:NULL];
}
if ([service.child respondsToSelector:@selector(extraHttpHeadParmasWithMethodName:)]) {
NSDictionary *dict = [service.child extraHttpHeadParmasWithMethodName:methodName];
if (dict) {
[dict enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
[request setValue:obj forHTTPHeaderField:key];
}];
}
}
request.requestParams = totalRequestParams;
return request;
}
#pragma mark - private method
//根据Service拼接额外参数
- (NSDictionary *)totalRequestParamsByService:(CTService *)service requestParams:(NSDictionary *)requestParams {
NSMutableDictionary *totalRequestParams = [NSMutableDictionary dictionaryWithDictionary:requestParams];
if ([service.child respondsToSelector:@selector(extraParmas)]) {
if ([service.child extraParmas]) {
[[service.child extraParmas] enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
[totalRequestParams setObject:obj forKey:key];
}];
}
}
return [totalRequestParams copy];
}

内容也比较简单,方法名有相关注释我也就不知道怎么解释了,至此问题1,提到的内容得到解决。

问题2 :

关于问题2,我本来是不想写的,但由于CTNetworking的Star有快1000了,所以对于删文件这种的重大举措,就有必要提一下了,否则下次人家下了个包,就会怒喷上个版本还提供,怎么这个版本就没了,虽然是我干的,但大家@Casa吧,下面就简单聊下我删除内容的想法以及原本CTNetworking的变动内容:

  1. 作为网络组件,涉及的内容应该在这个范围内,否则它应该是一个APP,而不是组件了,对于过多的功能,最简单的方式就是删,将不是必要的内容删除就可以了。
  2. CTAPPContext提供太多不是CTNetworking应该提供的功能,砍掉了。
  3. CTUDIDGenerator作为一个唯一ID生成器,我觉得它应该以一个通用的组件出现,原生主要是CTAPPContext在提供服务,由于CTAPPContext已经被砍掉了,所以它和框架就完全没有依赖了,所以也可以砍掉了。
  4. 原来的一个叫CTNetworkingConfiguration.h被砍掉了,转而增加了CTNetworkingConfigurationManager单例,提供一些默认的配置以及原生CTAPPContext提供的部分必要服务
  5. 将来可能还需要砍掉的如CTSignatureGenerator,CTCommonParamsGenerator,CTLocationManager等,由于有些是不是为了后期预留的暂不知道,所以就没有动了。
  6. 调整了大部分分类名称及方法,统一前缀CT_。

问题3 :

针对问题3,由于CTServiceFactory是单例,而CTServiceFactory与CTService之间的关系其实就只差一个映射字典,所以只要为它提供@{identifier1: @”GDMapService1”,identifier2: @”GDMapService2”},在这里GDMapService1即为相应的Service的字符串,CTServiceFactory使用时,则通过反映直接生成对象,这样就不用去添加不必要的引用了。

在这边我是让CTServiceFactory提供一个数据源,即CTServiceFactoryDataSource,其实也可以直接提供一个属性的,但不晓得当时为什么这么做的,忘记了,可能是脑抽了吧,所以使用起来,大致是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[CTServiceFactory sharedInstance].dataSource = self;
return YES;
}
#pragma mark - CTServiceFactoryDataSource
- (NSDictionary<NSString *,NSString *> *)servicesKindsOfServiceFactory {
return @{kCTServiceGDMapV3: @"GDMapService"};
}

至此,所有改造的内容已经讲完了。

扩展篇

现市面上,大部分APP都采用OAUTH授权登录的方式,登录完成后,会获得相应的accessToken、expireTime及refreshToken,基本上调用其他接口使用,都会在请求头里面拼接相应的accessToken,这部分上面有提到应该交由Service去处理,让Service负责拼接的工作。

网络请求时,token失效是会收到类型expired_access_token,而token非法时,会收到illegal_access_token,当然还有更多的情况,由于错误信息error description可能各种公司的会不一致,所以在Service层预留的shouldCallBackByFailedOnCallingAPI就是为了做这样的事情,当网络请求失败时,到service决定是否拦截并做相应处理。

关于组件化使用私有Pods的一些记录

最近在做项目的组件化,其中遇到一些问题,经常遇到自己解决过的问题又完全想不起来,于是每次都要反复查阅,很影响效率,所以打算把自己遇到的问题都记录下来,利人利己,下面我会以案例的方式,来讲述一些问题:

案例1

项目中图片缓存层是基于SDWebImage做的二次封装,在提交私有Pod时,出现了以下问题:

1
2
3
4
5
-> LGWebImage (1)
- ERROR | [iOS] xcodebuild: Returned an unsuccessful exit code.
- ERROR | xcodebuild: LGWebImage/LGWebImage/LGWebImage/UIButton+LGWebCache.m:10:9: error: 'UIButton+WebCache.h' file not found with <angled> include; use "quotes" instead
- ERROR | xcodebuild: LGWebImage/LGWebImage/LGWebImage/UIImageView+LGWebCache.m:10:9: error: 'UIImageView+WebCache.h' file not found with <angled> include; use "quotes" instead
>

封装库中使用了SDWebImage中的UIImageView+WebCache和UIButton+WebCache,虽然在PodSpec文件里面添加了SDWebImage的依赖,但在打Pod时,并没有发现SDWebImage的文件头,所以代码层面就编译不过了,这时候只需要在PodSpec添加暴露SDWebImage头文件的配置即可解决问题,如下所示:

1
s.xcconfig = { "HEADER_SEARCH_PATHS" => "$(SRCROOT)/SDWebImage" }

Swift中小知识点手札

Xcode快捷键

键位 含义
Command + shift +j 快速定位到目录

guard

Swift中的常用语法,可以有效解决可选绑定形成的{}(大括号)嵌套,提高代码的可读性,语法如下:

1
2
3
4
5
6
7
guard 条件表达式 else{
//条件为false时,才会执行内容的内容
return
}
........
........

try

try是Swift中的关键字,用于处理异常

字段类型 含义
try 正常处理异常,需要通过添加do{} catch{}来处理
try! 告诉系统一定不会有异常,可以不添加do{} catch{},但需要注意,开发中不推荐这样写,一旦发生异常程序就会崩溃,如果没有异常,那么会返回一个确定的值
try? 告诉系统异常发生存在可能性,可以不添加do{} catch{},如果没有异常,系统会自动将结果包装成为一个可选类型,如果有异常则返回nil

public、internal、private

字段 含义
public 最大权限,可以在当前framework和其他framework中访问
internal 默认权限,可以在当前framework中任意使用
private 私有权限,只能在当前文件中访问

以上的权限可以修性属性/方法/类

在开发中,需要严格控制权限,特别是在Swift中,不像OC,可以将部分方法或者属性,只在.m文件中声明,来防止外部调用,Swift中则要用private来修性对应的方法或者属性

@obj

Swift中所有的东西都是编译时确定的,所以Swift不像OC支持动态派发,如果想让其支持,将需要在相应方法前面添加@obj做修饰,一般会使用在给方法添加private时配套使用

Swift中自定义Log

用于输出的常用函数

函数名 含义
#function 获取当前输出所在的文件路径
#line 获取当前输出所在的行号
#file 获取当前输出的文件名

所以利用上面的内容,我们可以定义以下函数做为输出:

1
2
3
4
5
func CRLog<T>(content: T, fileName:String = #file, methodName: String = #function, lineNumber: Int = #line){
let tempFileName = (#file as NSString).pathComponents.last! as NSString
tempFileName
print(tempFileName.stringByDeletingPathExtension + "." + methodName + "___" + "\(lineNumber)")
}

其中后面几个值有默认值,当不输入时,则会自动获得相应内容打印.

如果我们有需求,当在DEBUG模式下,就打印输出,在release模式下就不打印,那么我们需要改写我们的代码如下:

1
2
3
4
5
6
7
8
func CRLog<T>(content: T, fileName:String = #file, methodName: String = #function, lineNumber: Int = #line){
#if DEBUG
let tempFileName = (#file as NSString).pathComponents.last! as NSString
tempFileName
print(tempFileName.stringByDeletingPathExtension + "." + methodName + "___" + "\(lineNumber)")
#endif
}

其中DEBUG可为任意内容,由于Swift中没有像OC中自身定义了DEBUG宏,所以此处的DEBUG可以为任意值,你可以写成你的名字等等,写完后,我们需要去配置一下这个值,于是我们需要来到:
工程目录->Targets->Build Settings->Swift Compiler -> Custom Flags -> Other Swift Flags -> Debug,然后在里面填写 -D DEBUG ,此处的DEBUG就是你上面定义的值,与代码中的那个值保持一致就可以了,至此,你可以切换成DEBUG模式和Release模式各自尝试一下输出效果。

QorumLogs

##QorumLogs
Swift开发中,在面临Bug时,我们常常采用的方式就是暴力调试,今天我就介绍一款Swift中的Log日志,开源项目名为:QorumLogs,它能配合XcodeColors输出彩色的Log日志

###基本用法

1
QorumLogs.enabled = true

开启日志系统

1
QorumLogs.test()

输出测试日志,这时,我们就能看到五颜色的输出日志

1
QorumLogs.minimumLogLevelShown = 1

设置日志输出级别,他总共有四个级别,分别为Debug,Info,Warn和Error,通过设置,我们可以通过设置输出级别来过滤掉不想输出的日志

1
QorumLogs.onlyShowThisFile("ViewController")

设置只输出,ViewController这个文件的日志

1
QorumLogs.onlyShowTheseFiles("ViewController","AppDelegate")

设置指定多文件输出,值得注意的是,这里是大小写敏感的

1
2
3
4
QL1("Debug")
QL2("Info")
QL3("Warn")
QL4("Error")

QorumLogs内部定义的基本四种Tag输出

循环引用

今天记录一下OC里面另一个常见的问题,那就是循环引用,总的来说现在我会想到的只有三种,后续如果有其他再类似的我会再一一补上:

  1. 类的循环引用
  2. MRC retain循环引用
  3. ARC strong对象的循环引用(其实和2是一样的,只是关键字不同而已)

OC内存管理

现在是求职季,OC的面试,肯定少不了内存管理,尽管现在基本都是ARC的项目,应该没有谁还在折腾MRC吧,不过这两天貌似有看到一个开源LeagueofLegends,还真是MRC,那会我看着,我只想说,哥们,你怎么如此蛋疼,没有别的意思,纯属闲扯,好吧,进入今天的话题。

KVC

KVC,即是指 NSKeyValueCoding,一个非正式的 Protocol,提供一种机制来间接访问对象的属性。功能有点类似于我们平常使用的get和set方法,直接从例子来看看比较直观吧:

2016-2-20 Swift和Objective-C中的单例

Objective-C中单例的使用姿势:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
+(instancetype)sharedXMPPManager{
static XMPPManager *_instance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_instance = [[self alloc] init];
});
return _instance;
}
+ (instancetype)allocWithZone:(struct _NSZone *)zone{
static XMPPManager *_instance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_instance = [super allocWithZone:zone];
});
return _instance;
}

那么Swift中该如何写呢,首先给出一种模仿OC的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import UIKit
class XMPPManager: NSObject {
static var onceToken :dispatch_once_t = 0
static var instance :XMPPManager?
class func sharedXMPPManager()->XMPPManager {
dispatch_once(&onceToken) { () -> Void in
instance = XMPPManager()
}
return instance!
}
}

由于let在Swift中是线程安全的,所以又有以下这种写法,也应该是通用写法了:

1
2
3
4
5
6
7
8
9
10
11
import UIKit
class XMPPManager: NSObject {
static let instance : XMPPManager = XMPPManager()
class func sharedXMPPManager()->XMPPManager {
return instance
}
}
调用方式:XMPPManager.sharedXMPPManager()

而后,我又在Alamofire中发现另外一种写法,效果类似于上面,不过他是以属性的方式提供,

1
2
3
4
5
6
7
8
import UIKit
class XMPPManager: NSObject {
internal static let sharedInstance:XMPPManager = XMPPManager()
}
调用方式:XMPPManager.sharedXMPPManager

大概就这样了,暂做记录。


注:版权声明:本文为博主原创文章,未经博主允许不得转载。

在swift中使用CocoaPods

在swift中使用CocoaPods和Objective-C中其实没啥区别,
在工程路径下使用,pod init 创建Profile文件
打开Profile文件,以下以添加SnapKit和Alamofire为例,在文件中写入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Uncomment this line to define a global platform for your project
# platform :ios, '8.0'
# Uncomment this line if you're using Swift
# use_frameworks!
target 'CRWeiBo' do
pod 'SnapKit', '0.18.0'
pod 'Alamofire', '3.1.3'
end
target 'CRWeiBoTests' do
end
target 'CRWeiBoUITests' do
end

如profile前面提示的,如果在swift环境下添加use_frameworks!,因为我们是直接生成的Profile文件,所以直接取消上面那行注释就可以了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Uncomment this line to define a global platform for your project
# platform :ios, '8.0'
# Uncomment this line if you're using Swift
use_frameworks!
target 'CRWeiBo' do
pod 'SnapKit', '0.18.0'
pod 'Alamofire', '3.1.3'
end
target 'CRWeiBoTests' do
end
target 'CRWeiBoUITests' do
end

如果是自己手动创建的,则需要自己补上


注:版权声明:本文为博主原创文章,未经博主允许不得转载。