GitHub Repo:coderZsq.project.ios
Follow: coderZsq · GitHub
Resume: https://coderzsq.github.io/coderZsq.practice.web/
日常扯淡
emm~~ 最近有好多大厂疯狂招人
, 也有很多朋友抛出了橄榄枝要来内推
我, 在此我表示万分感谢, 但以小弟的简历… 真的连简历都过不了… 最近在学习Python
, 真的学了Python
才感受到了编程的乐趣啊, 爬虫
, 自动化
, 数据分析
, 机器学习
, 区块链
真的是感觉掌控了先进科技了… 我把所有学习Python
的代码放在了这里–> 仓库, 会逐步更新的, 希望能够共同学习和进步!
诶… 一直在自学Python
, iOS
都不太会敲了, 想想之前看了函数式编程
的书, 又想学着写写如何设计一个网络框架, 就有了今天这篇文章, 虽然也没啥技术含量….
对于iOS
, Apple
市值破万亿, 说明当初选择这个方向还是不错的, 但是现在的趋势真的是好多在职的iOS
都在往大数据
和人工智能
方向上转, 真的, 我感觉iOS
各个都是全栈, 什么都会真的牛逼, 我也要跟随大佬的步伐, 继续我的学习之旅了~~
网络优化
说实话, 网络优化
基本是根据你的项目来的, 不同的需求会有不同的方案
, 但思想上是想通的, 大体就是, 缓存, 缓存, 缓存… 总结为如下几点:
- 减少, 压缩网络数据
- 如果多次请求结果是相同的, 经量使用缓存
- 使用断点续传, 否则网络不稳定的时候可能多次传输相同的内容
- 网络不可用的时候, 不要尝试执行网络请求
- 让用户可以取消长时间运行或者速度很慢的网络操作, 设置合适的超时时间
- 批量传输,比如,下载视频流时,不要传输很小的数据包,直接下载整个文件或者一大块一大块地下载。如果下载广告,一次性多下载一些,然后再慢慢展示。如果下载电子邮件,一次下载多封,不要一封一封地下载.
HTTP
说起网络, 肯定要说到HTTP
了, 这个真的绕不过去, 对于HTTP
可以看看我学习实践的代码–> 仓库
当然这个是用JS
写的, 而且这个需要服务端的支持, 简单来说就是设置一些头信息, cache-control
这个字段就非常虫咬了, 是做一些缓存处理的, 设置max-age
就可以设定超时时间. accept
设置为gzip
之类的就可以进行网络数据的压缩..
这种都是常规操作, 就不过多赘述了…
准备工作
这个在上一篇iOS 界面性能优化浅析
的基础上进行, 在我们server.js
中添加代码.
if (decodeURI(req.url).indexOf("/get") != -1) {
console.log(req.url);
res.writeHead(200, {
'Content-Type': 'application/json'
});
res.end(JSON.stringify({
data: "GET Method",
status: 'Success'
}));
}
if (decodeURI(req.url).indexOf("/post") != -1) {
console.log(req.url);
let body = "";
req.on('data', function (chunk) {
body += chunk;
});
req.on('end', function () {
body = querystring.parse(body);
console.log(body);
});
res.writeHead(200, {
'Content-Type': 'application/json'
});
res.end(JSON.stringify({
data: "POST Method",
status: 'Success'
}));
}
if (decodeURI(req.url).indexOf("/image") != -1) {
let path = __dirname + '/contents/Castie!.jpg';
fs.readFile(path, 'binary', (err, file) => {
if (err) {
console.log(err);
return;
} else {
res.writeHead(200, {
'Content-Type': 'image/jpeg'
});
res.write(file, 'binary');
res.end();
return;
}
})
}
我们就简单的开了三个接口来模拟get请求
, post请求
和图片请求
这些基本上覆盖了我们80%
的需求了吧.
如何设计
一个网络框架如何设计? 说实话, 我也不知道, 看了AFNetworking
和SDWebImage
的源码, 就会发现侧重点
都是不同的. 做为开发者, 当然不能生搬硬套
别人的思想, 所以我就按自己的理解来进行设计了.
我在写代码的时候, 有自顶向下
设计的习惯, 首先把接口写出来, 关键是要让别人用的爽
, 然后再进行具体的实现.
所以我们先看下之前AFN
我们是怎么用的.
#import "Service.h"
#import <AFNetworking.h>
@implementation Service
- (void)fetchMockDataWithParam:(NSDictionary *)parameter completion:(RequestCompletionBlock)completion {
AFHTTPSessionManager * manager = [AFHTTPSessionManager manager];
[manager GET:@"http://localhost:8080/fetchMockData" parameters:parameter progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
if ([responseObject[@"status"] isEqualToString: @"success"]) {
completion(responseObject[@"data"], nil);
}
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
completion(nil, error);
}];
}
@end
这个是我之前使用AFN
的时候的常规操作, 再看我想要达到的效果:
#import "Service.h"
#import "SQFetcher.h"
#import <UIKit/UIKit.h>
@implementation Service
- (void)fetchMockDataWithParam:(NSDictionary *)parameter completion:(RequestCompletionBlock)completion {
[SQFetchManager managerWithState:SQFetchSerialState]
.GET(@"http://localhost:8090/fetchMockData", nil, ^(NSDictionary *responseObject){
if ([responseObject[@"status"] isEqualToString: @"success"]) {
completion(responseObject[@"data"], nil);
}
NSLog(@"EXCUTE: http://localhost:8090/fetchMockData");
}, nil)
.GET(@"http://localhost:8090/get", @{@"username": @"Castie!",
@"passwd" : @"01234",
@"param0" : @"0.0",
@"param1" : @"1.1",
@"param2" : @"2.2",
@"param3" : @"3.3",
@"param4" : @"4.4",
}, ^(NSDictionary *responseObject){
NSLog(@"EXCUTE: http://localhost:8090/get %@", responseObject);
}, nil)
.POST(@"http://localhost:8090/post", @{@"username": @"Castie!",
@"passwd" : @"56789",
@"param5" : @"5.5",
@"param6" : @"6.6",
@"param7" : @"7.7",
@"param8" : @"8.8",
@"param9" : @"9.9",
}, ^(NSDictionary *responseObject){
NSLog(@"EXCUTE: http://localhost:8090/post %@", responseObject);
},nil)
.GET(@"http://localhost:8090/image", nil, ^(UIImage *responseObject){
NSLog(@"EXCUTE: http://localhost:8090/image %@", responseObject);
},nil);
}
@end
这里的#import <UIKit/UIKit.h>
是不必要的, 只是想表达请求图片也可以的意思.
可以看到, 这种调用, 我们从视觉的角度就非常清晰舒爽了~
刚才的代码, 细心的同学可以看到有一个状态的判断, 这个值可以操作下面链式调用
是串行还是并行, 是不是很神奇~ 妈妈再也不用担心回调地狱
的问题了~
如何实现
那如何实现这个链式调用及控制呢, 来看看我是怎么实现的.
#import <Foundation/Foundation.h>
typedef NS_ENUM(NSInteger, SQFetchState) {
SQFetchSerialState,
SQFetchConcurrentState
};
@interface SQFetchManager : NSObject
@property (nonatomic,strong) SQFetchManager * (^GET)(NSString * url, NSDictionary * parameters, void(^success)(id), void(^failure)(NSError *));
@property (nonatomic,strong) SQFetchManager * (^POST)(NSString * url, NSDictionary * parameters, void(^success)(id), void(^failure)(NSError *));
+ (SQFetchManager *)managerWithState:(SQFetchState)state;
@end
看看头文件, 就能够发现, 我这个属性是不是和你们用的都不太一样, 这是一个Block
, 不要和我说Block
要用copy
修饰, 想想现在ARC
已经多少时间了…. 当然copy
是通用的…
#import "SQFetchManager.h"
#import "SQFetchSerialization.h"
#import "SQFetchReachability.h"
#import "SQFetchCache.h"
typedef NS_ENUM(NSInteger, SQHTTPMethod) {
SQGETMethod,
SQPOSTMethod
};
@interface SQFetchManager()
@property (nonatomic,strong) NSOperation * preOperation;
@property (nonatomic,strong) NSOperationQueue * operationQueue;
@property (nonatomic,assign) SQFetchState state;
@end
@implementation SQFetchManager
- (NSOperationQueue *)operationQueue {
if (!_operationQueue) {
_operationQueue = [NSOperationQueue new];
_operationQueue.maxConcurrentOperationCount = 6;
}
return _operationQueue;
}
- (SQFetchManager *(^)(NSString *, NSDictionary *, void(^)(id), void(^)(NSError *)))GET {
return ^(NSString * URLString, NSDictionary * parameters, void(^success)(NSDictionary *), void(^failure)(NSError *)){
NSBlockOperation * operation = [NSBlockOperation blockOperationWithBlock:^{
dispatch_semaphore_t semaphore = NULL;
if (self.state == SQFetchSerialState)
semaphore = dispatch_semaphore_create(0);
[[self __dataTaskWithHTTPMethod:SQGETMethod
URLString:URLString
parameters:parameters
success:success
failure:failure
semaphore:semaphore] resume];
if (self.state == SQFetchSerialState)
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}];
if (self.preOperation) {
[operation addDependency:self.preOperation];
}
[self.operationQueue addOperation:operation];
self.preOperation = operation;
return self;
};
}
- (SQFetchManager *(^)(NSString *, NSDictionary *, void (^)(id), void (^)(NSError *)))POST {
return ^(NSString * URLString, NSDictionary * parameters, void(^success)(NSDictionary *), void(^failure)(NSError *)){
NSBlockOperation * operation = [NSBlockOperation blockOperationWithBlock:^{
dispatch_semaphore_t semaphore = NULL;
if (self.state == SQFetchSerialState)
semaphore = dispatch_semaphore_create(0);
[[self __dataTaskWithHTTPMethod:SQPOSTMethod
URLString:URLString
parameters:parameters
success:success
failure:failure
semaphore:semaphore] resume];
if (self.state == SQFetchSerialState)
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}];
if (self.preOperation)
[operation addDependency:self.preOperation];
[self.operationQueue addOperation:operation];
self.preOperation = operation;
return self;
};
}
- (NSURLSessionDataTask *)__dataTaskWithHTTPMethod:(SQHTTPMethod)method
URLString:(NSString *)URLString
parameters:(NSDictionary *)parameters
success:(void (^)(id))success
failure:(void (^)(NSError *))failure
semaphore:(dispatch_semaphore_t)semaphore {
SQFetchReachability * reachability = [SQFetchReachability sharedInstance];
if (reachability.status == NotReachable) {
return nil;
}
id cache = [SQFetchCache getCacheFromKey:URLString];
if (cache) {
success(cache);
return nil;
}
if (method == SQGETMethod) {
URLString = [URLString stringByAppendingString:
[SQFetchSerialization getMethodSerializationWithParameters:parameters]];
}
NSURL * url = [NSURL URLWithString:URLString];
NSURLSession * session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL:url];
if (method == SQPOSTMethod) {
request.HTTPMethod = @"POST";
request.HTTPBody = [SQFetchSerialization postMethodSerializationWithParameters:parameters];
}
NSURLSessionDataTask * task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSError * err;
id responseObject = [SQFetchSerialization serializationWithContentType:response.MIMEType
data:data error:err];
[SQFetchCache setCache:responseObject key:URLString];
if(err) {
failure(err);
if (self.state == SQFetchSerialState)
dispatch_semaphore_signal(semaphore);
} else {
success(responseObject);
if (self.state == SQFetchSerialState)
dispatch_semaphore_signal(semaphore);
}
}];
return task;
}
+ (SQFetchManager *)managerWithState:(SQFetchState)state {
SQFetchManager * manager = [SQFetchManager new];
manager.state = state;
return manager;
}
@end
这个是整个实现文件, 可以看到, 这是如何使用函数式编程
了, 用起来真是神清气爽, 但不知道如果别人用了会是什么感受…
如何实现同步依赖, 主要是使用了信号量
, 配合队列依赖
就可以实现啦~
序列化
序列化就是对不同的请求
和响应
做不同的解析.
#import <Foundation/Foundation.h>
@interface SQFetchSerialization : NSObject
+ (NSString *)getMethodSerializationWithParameters:(NSDictionary *)parameters;
+ (NSData *)postMethodSerializationWithParameters:(NSDictionary *)parameters;
+ (id)serializationWithContentType:(NSString *)contentType data:(NSData *)data error:(NSError *)error;
@end
看头文件也是简洁明了吧…
#import "SQFetchSerialization.h"
#import <UIKit/UIKit.h>
@implementation SQFetchSerialization
+ (NSString *)getMethodSerializationWithParameters:(NSDictionary *)parameters {
NSMutableString * parameterString = [NSMutableString string];
[parameters enumerateKeysAndObjectsUsingBlock:^(NSString * key, id _Nonnull obj, BOOL * _Nonnull stop) {
NSString * value = (NSString *)obj;
[parameterString appendFormat:@"%@=%@&", key, value];
}];
if (parameterString.length) {
[parameterString insertString:@"?" atIndex:0];
[parameterString deleteCharactersInRange:NSMakeRange(parameterString.length - 1, 1)];
}
return parameterString;
}
+ (NSData *)postMethodSerializationWithParameters:(NSDictionary *)parameters {
NSMutableString * parameterString = [NSMutableString string];
[parameters enumerateKeysAndObjectsUsingBlock:^(NSString * key, id _Nonnull obj, BOOL * _Nonnull stop) {
NSString * value = (NSString *)obj;
[parameterString appendFormat:@"%@=%@&", key, value];
}];
if (parameterString.length) {
[parameterString deleteCharactersInRange:NSMakeRange(parameterString.length - 1, 1)];
}
return [parameterString dataUsingEncoding:NSUTF8StringEncoding];
}
+ (id)serializationWithContentType:(NSString *)contentType data:(NSData *)data error:(NSError *)error {
id responseObject;
if ([contentType isEqualToString:@"application/json"]) {
responseObject = [NSJSONSerialization JSONObjectWithData:data
options:NSJSONReadingMutableContainers error:&error];
} else if ([contentType isEqualToString:@"image/jpeg"]) {
CGImageRef cgImage = [UIImage imageWithData:data].CGImage;
CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(cgImage) & kCGBitmapAlphaInfoMask;
BOOL hasAlpha = NO;
if (alphaInfo == kCGImageAlphaPremultipliedLast ||
alphaInfo == kCGImageAlphaPremultipliedFirst ||
alphaInfo == kCGImageAlphaLast ||
alphaInfo == kCGImageAlphaFirst) {
hasAlpha = YES;
}
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
size_t width = CGImageGetWidth(cgImage);
size_t height = CGImageGetHeight(cgImage);
CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 0, colorSpace, bitmapInfo);
CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage);
cgImage = CGBitmapContextCreateImage(context);
UIImage * image = [UIImage imageWithCGImage:cgImage];
CGColorSpaceRelease(colorSpace);
CGContextRelease(context);
CGImageRelease(cgImage);
responseObject = image;
}
return responseObject;
}
@end
相关实现也是对请求参数进行拼接
, 以及对响应的数据进行分类处理
…
网络监测
这个依赖了Apple
的Reachability
就是作用网络不可用的时候, 不要尝试执行网络请求.
#import "SQFetchReachability.h"
@interface SQFetchReachability()
@property (nonatomic, strong) Reachability * hostReachability;
@property (nonatomic, strong) Reachability * routerReachability;
@end
@implementation SQFetchReachability
+ (void)load {
[SQFetchReachability sharedInstance];
}
+ (instancetype)sharedInstance {
static SQFetchReachability * instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[super allocWithZone:NULL] init];
});
return instance;
}
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
return [self sharedInstance];
}
- (instancetype)init {
self = [super init];
if (self) {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(appReachabilityChanged:)
name:kReachabilityChangedNotification
object:nil];
NSString * remoteHostName = @"www.baidu.com";
self.hostReachability = [Reachability reachabilityWithHostName:remoteHostName];
[self.hostReachability startNotifier];
self.routerReachability = [Reachability reachabilityForInternetConnection];
[self.routerReachability startNotifier];
}
return self;
}
- (void)appReachabilityChanged:(NSNotification *)notification{
Reachability * reach = [notification object];
if([reach isKindOfClass:[Reachability class]]){
NetworkStatus status = [reach currentReachabilityStatus];
self.status = status;
if (reach == self.routerReachability) {
if (status == NotReachable) {
NSLog(@"routerReachability NotReachable");
} else if (status == ReachableViaWiFi) {
NSLog(@"routerReachability ReachableViaWiFi");
} else if (status == ReachableViaWWAN) {
NSLog(@"routerReachability ReachableViaWWAN");
}
}
if (reach == self.hostReachability) {
NSLog(@"hostReachability");
if ([reach currentReachabilityStatus] == NotReachable) {
NSLog(@"hostReachability failed");
} else if (status == ReachableViaWiFi) {
NSLog(@"hostReachability ReachableViaWiFi");
} else if (status == ReachableViaWWAN) {
NSLog(@"hostReachability ReachableViaWWAN");
}
}
}
}
@end
二级缓存
对于这块, 看过SD
的同学肯定非常了解, 内存缓存
和磁盘缓存
.
#import "SQFetchCache.h"
#import <CommonCrypto/CommonDigest.h>
#import <UIKit/UIKit.h>
@interface SQFetchCache()
@property (nonatomic, strong) NSMutableDictionary * memoryCache;
@end
@implementation SQFetchCache
- (NSMutableDictionary *)memoryCache {
if (!_memoryCache) {
_memoryCache = @{}.mutableCopy;
}
return _memoryCache;
}
+ (instancetype)sharedInstance {
static SQFetchCache * instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[super allocWithZone:NULL] init];
});
return instance;
}
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
return [self sharedInstance];
}
+ (void)setCache:(id)data key:(NSString *)key {
if ([data isKindOfClass:[UIImage class]]) {
[SQFetchCache sharedInstance].memoryCache[[self __sha1:key]] = data;
[SQFetchCache __detectionDiskCache];
NSString * homePath = NSHomeDirectory();
NSString * path = [homePath stringByAppendingPathComponent:@"Library/Caches"];
NSString * filePath = [path stringByAppendingPathComponent:[self __sha1:key]];
[UIImagePNGRepresentation(data) writeToFile:filePath atomically:YES];
}
}
+ (id)getCacheFromKey:(NSString *)key {
id cache = [SQFetchCache sharedInstance].memoryCache[[self __sha1:key]];
if (!cache) {
NSString * cachePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
NSString * file = [NSString stringWithFormat:@"%@/%@", cachePath, [self __sha1:key]];
cache = [[UIImage alloc]initWithContentsOfFile:file];
}
return cache;
}
+ (void)__detectionDiskCache {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSFileManager * fileManager = [NSFileManager defaultManager];
NSString * cachePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
NSDirectoryEnumerator * direnum = [fileManager enumeratorAtPath:cachePath];
NSError * error = nil;
NSString * filename;
while (filename = [direnum nextObject]) {
NSDictionary * fileAttributes = [fileManager attributesOfItemAtPath:[NSString stringWithFormat:@"%@/%@",cachePath, filename] error:&error];
if (fileAttributes != nil) {
NSNumber * fileSize = [fileAttributes objectForKey:NSFileSize];
NSString * fileOwner = [fileAttributes objectForKey:NSFileOwnerAccountName];
NSDate * fileModDate = [fileAttributes objectForKey:NSFileModificationDate];
NSDate * fileCreateDate = [fileAttributes objectForKey:NSFileCreationDate];
if (fileSize) {
NSLog(@"File size: %qi\n", [fileSize unsignedLongLongValue]);
}
if (fileOwner) {
NSLog(@"Owner: %@\n", fileOwner);
}
if (fileModDate) {
NSLog(@"Modification date: %@\n", fileModDate);
}
if (fileCreateDate) {
NSLog(@"create date:%@\n", fileModDate);
}
}
else {
NSLog(@"Path (%@) is invalid.", cachePath);
}
}
});
}
+ (void)clearDiskCache {
NSFileManager *fileManager = [NSFileManager defaultManager];
NSString * cachePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
NSLog(@"%@",cachePath);
NSDirectoryEnumerator * fileEnumerator = [fileManager enumeratorAtPath:cachePath];
for (NSString * fileName in fileEnumerator) {
NSString * filePath = [cachePath stringByAppendingPathComponent:fileName];
[fileManager removeItemAtPath:filePath error:nil];
}
}
+ (NSString *)__sha1:(NSString *)inputString {
NSData * data = [inputString dataUsingEncoding:NSUTF8StringEncoding];
uint8_t digest[CC_SHA1_DIGEST_LENGTH];
CC_SHA1(data.bytes,(unsigned int)data.length,digest);
NSMutableString *outputString = [NSMutableString stringWithCapacity:CC_SHA1_DIGEST_LENGTH];
for (int i = 0; i < CC_SHA1_DIGEST_LENGTH; i++) {
[outputString appendFormat:@"%02x",digest[i]];
}
return [outputString lowercaseString];
}
@end
如何优化就是对空间和时间两方面
, 这里可以关注__detectionDiskCache
这个方法中进行判断处理.. 可以策略性的进行删除..(内存缓存
也是相同的思路)
#import "UIImageView+SQFetcher.h"
#import "SQFetcher.h"
@implementation UIImageView (SQFetcher)
- (void)sq_imageWithURLString:(NSString *)URLString {
[SQFetchManager managerWithState:SQFetchConcurrentState]
.GET(URLString, nil, ^(UIImage *responseObject){
dispatch_async(dispatch_get_main_queue(), ^{
self.image = responseObject;
});
},nil);
}
@end
分类使用的实现也是非常简单的, 我么使用起来也是非常方便.
最后
这个仅仅是一个最小可用
的设计, 当然还有很多值得优化的点, 比如配置请求头
, 请求复用
, 哈希桶扩张
, 管道
, 对于需求, 还要安全验证
等, 写这个东西其实就是了解下一个网络框架需要了解点什么, 平时使用的时候AFN
和SD
肯定是你首选的方案!! 但有了这些思路, 你也可以实现一个自己定制的网络框架!
最后 本文中所有的源码都可以在github上找到 -> SQPerformance
GitHub Repo:coderZsq.project.ios
Follow: coderZsq · GitHub
Resume: https://coderzsq.github.io/coderZsq.practice.web/