Coding源码学习第三部分(EaseStartView.m)
首先接上篇的要做一个NSEnumerator 类的延展阅读。
枚举(NSEnumerator)
(1)依附于集合类(NSArray,NSSet,NSDictionary),没有用来创建实例的接口。
(2)NSEnumerator的nextObject方法可以遍历每个集合元素,结束返回nil,通过与while结合使用可遍历集合中所有项
(3)对可变数组进行枚举操作时,不能通过添加或删除对象这种类方式来改变数组容器。
遍历数组每个索引处的对象,你可以编写一个0到[array count]的循环,而NSEnumerator用来描述这种集合迭代运算的方式。
通过objectEnumerator向集合类请求枚举器,如果想从后向前浏览集合,可使用reverseObjectEnumerator方法。在获得枚举器后,可以开始一个while循环,每次循环都向这个枚举器请求它的下一个对象:nextObject。nextObject返回nil值时,循环结束。
创建枚举:向容器类对象请求枚举器
- (NSEnumerator *)objectEnumerator;
- (NSEnumerator<ObjectType> *)reverseObjectEnumerator;
浏览集合类对象的全部元素
- (id)nextObject;
1 UIView *selectedView; 2 NSEnumerator *frontToBackWindowsOne = [[UIApplication sharedApplication].windows objectEnumerator]; 3 UIWindow *textWindow; 4 while (textWindow = [frontToBackWindowsOne nextObject]) { 5 NSLog(@"testWindows Level = %f", textWindow.windowLevel); 6 } 7 NSEnumerator *frontToBackWindows = [UIApplication.sharedApplication.windows reverseObjectEnumerator]; // 按照索引号从大到小访问数组的元素,而不是从小到大访问数组的元素,即逆序遍历数组。下面会做一个延伸阅读。 8 for (UIWindow *window in frontToBackWindows) { // 从windows 数组里面获取application 的当前的Window 9 BOOL windowOnMainScreen = window.screen == UIScreen.mainScreen; 10 BOOL windowIsVisible = !window.hidden && window.alpha > 0; 11 BOOL windowLevelNormal = window.windowLevel == UIWindowLevelNormal; 12 13 if (windowOnMainScreen && windowIsVisible && windowLevelNormal) { 14 selectedView = window; 15 break; 16 } 17 }
对于可变数组进行枚举操作时,主要不要添加或删除数组中的对象。
快速枚举forin 比NSEnumerator 更简洁快捷。
iOS 中集合遍历方法的比较和技巧(本来是只放链接的,因为作者很厉害,还是把原文再看一下)
前言:集合的遍历操作是开发中最常见的操作之一,从C语言经典的for 循环到利用多核cpu 的优势进行遍历,开发中iOS 有若干集合遍历方法,通过研究和测试比较了各个操作方法的效率和优略势,并总结几个使用集合遍历时的小技巧。
ios中常用的遍历运算方法
遍历的目的是获取集合中的某个对象或执行某个操作,所以能满足这个条件的方法都可以作为备选:
- 经典for循环
- for in (NSFastEnumeration),若不熟悉可以参考《nshipster介绍NSFastEnumeration的文章》
- makeObjectsPerformSelector
- kvc集合运算符
- enumerateObjectsUsingBlock
- enumerateObjectsWithOptions(NSEnumerationConcurrent)
- dispatch_apply
实验
实验条件
测试类如下:
1 @interface Sark : NSObject 2 @property (nonatomic) NSInteger number; 3 - (void)doSomethingSlow; // sleep(0.01) 4 @end
实验从两个方面来评价:
- 分别使用有 100 个对象和 1000000 个对象的 NSArray,只取对象,不执行操作,测试遍历速度
- 使用有 100 个对象的 NSArray 遍历执行
doSomethingSlow
方法,测试遍历中多任务运行速度
实验使用CFAbsoluteTimeGetCurrent()
记录时间戳来计算运行时间,单位秒。
运行在 iPhone5 真机(双核cpu)
实验数据
100对象遍历操作:
1 经典for循环 - 0.001355 2 for in (NSFastEnumeration) - 0.002308 3 makeObjectsPerformSelector - 0.001120 4 kvc集合运算符(@sum.number) - 0.004272 5 enumerateObjectsUsingBlock - 0.001145 6 enumerateObjectsWithOptions(NSEnumerationConcurrent) - 0.001605 7 dispatch_apply(Concurrent) - 0.00138
1000000对象遍历操作:
1 经典for循环 - 1.246721 2 for in (NSFastEnumeration) - 0.025955 3 makeObjectsPerformSelector - 0.068234 4 kvc集合运算符(@sum.number) - 21.677246 5 enumerateObjectsUsingBlock - 0.586034 6 enumerateObjectsWithOptions(NSEnumerationConcurrent) - 0.722548 7 dispatch_apply(Concurrent) - 0.607100
100对象遍历执行一个很费时的操作:
1 经典for循环 - 1.106567 2 for in (NSFastEnumeration) - 1.102643 3 makeObjectsPerformSelector - 1.103965 4 kvc集合运算符(@sum.number) - N/A 5 enumerateObjectsUsingBlock - 1.104888 6 enumerateObjectsWithOptions(NSEnumerationConcurrent) - 0.554670 7 dispatch_apply(Concurrent) - 0.554858
值得注意的
- 对于集合中对象数很多的情况下,
for in (NSFastEnumeration)
的遍历速度非常之快,但小规模的遍历并不明显(还没普通for循环快) - 使用
kvc集合运算符
运算很大规模的集合时,效率明显下降(100万的数组离谱的21秒多),同时占用了大量内存和cpu enumerateObjectsWithOptions(NSEnumerationConcurrent)
和dispatch_apply(Concurrent)
的遍历执行可以利用到多核cpu的优势(实验中在双核cpu上效率基本上x2)
遍历实践Tips
倒序遍历
NSArray
和NSOrderedSet
都支持使用reverseObjectEnumerator
倒序遍历,如:
1 NSArray *strings = @[@"1", @"2", @"3"]; 2 for (NSString *string in [strings reverseObjectEnumerator]) { 3 NSLog(@"%@", string); 4 }
这个方法只在循环第一次被调用,所以也不必担心循环每次计算的问题。
同时,使用enumerateObjectsWithOptions:NSEnumerationReverse
也可以实现倒序遍历:
1 [array enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(Sark *sark, NSUInteger idx, BOOL *stop) { 2 [sark doSomething]; 3 }];
使用block同时遍历字典key,value
block版本的字典遍历可以同时取key和value(forin只能取key再手动取value),如:
1 NSDictionary *dict = @{@"a": @"1", @"b": @"2"}; 2 [dict enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { 3 NSLog(@"key: %@, value: %@", key, obj); 4 }];
对于耗时且顺序无关的遍历,使用并发版本
1 [array enumerateObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(Sark *sark, NSUInteger idx, BOOL *stop) { 2 [sark doSomethingSlow]; 3 }];
遍历执行block会分配在多核cpu上执行(底层很可能就是gcd的并发queue),对于耗时的任务来说是很值得这么做的,而且在以后cpu升级成更多核心后不用改代码也可以享受带来的好处。同时,对于遍历的外部是保持同步的(遍历都完成后才继续执行下一行),猜想内部大概是gcd的dispatch_group或者信号量控制。
代码可读性和效率的权衡
虽然说上面的测试结果表明,在集合内元素不多时,经典for循环的效率要比forin要高,但是从代码可读性上来看,就远不如forin看着更顺畅;同样的还有kvc的集合运算符,一些内置的操作以keypath
的方式声明,相比自己用for循环实现,一行代码就能搞定,清楚明了,还省去了重复工作;在framework中增加了集合遍历的block支持后,对于需要index的遍历再也不需要经典for循环的写法了。
参考链接:NSEnumerator 容器类对象:枚举NSEnumerator NSEnumerator使用
ios中集合遍历方法的比较和技巧 Objective-C 高性能的循环 iOS中数组遍历的方法及比较
本篇主要分析EaseStartView.m。
目前已知的只有有这三张不同的启动图片,另外还有一张中秋节特制的图片,当然这三张图片都是由
wallpapers 接口返回的启动图的数据信息里面的 图片的URL 获取到的三张图片,后台把图片换了以后app 也会同步更新,另外还有一张固定的图片,在没有替换图片时默认显示固定图片。
。app在每次启动时会随机获取一张图片,且和上次一定不同,因为每次显示了哪张图片都会在本地做一个记录,图片在手机网络是WiFi 的时候会后台下载好并保存在本地指定的目录。
首先白色的CODING 是logo 图片, 底部的引号里面是图片的名字,®后面是作者的名字。
EaseStartView.h 有一个类方法+ (instancetype) startView; 用于初始化创建一个EaseStarView 对象。和一个实例方法
- (void)startAnimationWithCompletionBlock:(void(^)(EaseStartView *easeStartView))completionHandler; 执行启动页的动画,动画执行完成completionHandler block 执行一些后续的操作。
app 启动时该启动图会缓慢向左滑出屏幕,然后显示 [UIApplication sharedApplication].keyWindow.rootViewController 视 [Login isLogin] 的返回结果而定是 RootTabViewController 或者 IntroductionViewController。
1 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 2 { 3 ...... 4 EaseStartView *startView = [EaseStartView startView]; // 设置正常启动的view 和启动动画 这里面有app 的第一个网络请求,先获取到了启动图的信息,再判断如果是WiFi 的话根据启动图的信息下载启动图片 5 @weakify(self); 6 // startView 开始动画和动画完成后的block 实现 7 [startView startAnimationWithCompletionBlock:^(EaseStartView *easeStartView) { 8 @strongify(self); 9 /** 10 * 启动动画执行完毕后执行下面的方法 11 */ 12 [self completionStartAnimationWithOptions:launchOptions]; 13 }]; 14 ...... 15 }
这是AppDelegate.m 里面,EaseStarView 类相关操作,接上面的[FunctionIntroManager showIntroPage];
首先看创建EaseStartView *startView = [EaseStartView startView];
下面进入EaseStartView.m 内部:
1 // 获得启动的EaseStartView 2 + (instancetype)startView{ 3 UIImage *logoIcon = [UIImage imageNamed:@"logo_coding_top"]; // logoIcon 小猴子的图片 4 StartImage *st = [[StartImagesManager shareManager] randomImage]; // 获得启动时的整个屏幕的大图 5 return [[self alloc] initWithBgImage:st.image logoIcon:logoIcon descriptionStr:st.descriptionStr]; // alloc init 6 }
创建Logo UIImage 。下面的StartImage 类和StartImagesManager 类(这两个类写在了同一个.h 和.m 文件里面)共同处理启动图片的事件,主要包括下载图片,保存图片等操作。
获取StartImagesManager 单例:
1 // 单例 2 + (instancetype)shareManager{ 3 static StartImagesManager *shared_manager = nil; 4 static dispatch_once_t pred; 5 dispatch_once(&pred, ^{ 6 shared_manager = [[self alloc] init]; 7 }); 8 return shared_manager; 9 }
StarImagesManager 类的初始化操作:
1 // 初始化 2 - (instancetype)init 3 { 4 self = [super init]; 5 if (self) { 6 [self createFolder:[self downloadPath]]; // 创建图片下载文件夹 7 [self loadStartImages]; // 获取本地图片信息 8 } 9 return self; 10 }
[self downloadPath]; 首先获得本地图片路径的字符串:
1 // app 启动时显示的图片的本地路径 2 - (NSString *)downloadPath{ 3 NSString *documentPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject]; 4 NSString *downloadPath = [documentPath stringByAppendingPathComponent:@"Coding_StartImages"]; 5 return downloadPath; 6 }
然后把该路径当做参数传递,创建实际文件夹,首先判断该文件夹是否存在防止重复创建:
1 // 根据路径创建存储启动时显示图片的文件夹 2 - (BOOL)createFolder:(NSString *)path{ 3 BOOL isDir = NO; 4 NSFileManager *fileManager = [NSFileManager defaultManager]; 5 BOOL existed = [fileManager fileExistsAtPath:path isDirectory:&isDir]; 6 BOOL isCreated = NO; 7 if (!(isDir == YES && existed == YES)){ 8 // 创建路劲 9 isCreated = [fileManager createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:nil]; 10 }else{ 11 isCreated = YES; 12 } 13 if (isCreated) { 14 // 路径存在后 15 // 做标记,不做备份处理 16 [NSURL addSkipBackupAttributeToItemAtURL:[NSURL fileURLWithPath:path isDirectory:YES]]; 17 } 18 return isCreated; 19 }
如果路径存在后,这里用到了NSURL 的category :
1 +(BOOL)addSkipBackupAttributeToItemAtURL:(NSURL *)URL 2 { 3 if ([[NSFileManager defaultManager] fileExistsAtPath: [URL path]]) 4 { 5 if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"5.1")) { // 系统版本小于等于5.1 6 NSError *error = nil; 7 BOOL success = [URL setResourceValue: [NSNumber numberWithBool: YES] forKey: NSURLIsExcludedFromBackupKey error: &error]; // 做标记,不做备份处理 http://www.th7.cn/Program/IOS/201406/218830.shtml 8 if(error){ 9 DebugLog(@"addSkipBackupAttributeToItemAtURL: %@, error: %@", [URL lastPathComponent], error); 10 } 11 return success; 12 } 13 14 if (SYSTEM_VERSION_GREATER_THAN(@"5.0")) { // 系统版本大于5.0 15 const char* filePath = [[URL path] fileSystemRepresentation]; // fileSystemRepresentation 将这个字符串转换成文件系统的规范形式然后用 UTF-8 编码 http://blog.csdn.net/apple_0611/article/details/38304409 16 const char* attrName = "com.apple.MobileBackup"; 17 u_int8_t attrValue = 1; 18 /** 19 * 20 3.设置扩展属性 21 setxattr(path, //路径 22 key, //扩展属性的名字 23 value, // 扩展属性的值 24 size, //扩展属性的长度 25 flags); //标识 26 * 27 * @param filePath 路径 28 * @param attrName 扩展属性的名字 29 * @param attrValue 扩展属性的值 30 * @param attrValue 扩展属性的长度 31 * 32 * @return 33 */ 34 int result = setxattr(filePath, attrName, &attrValue, sizeof(attrValue), 0, 0); // 设置扩展属性 http://blog.csdn.net/lin_fs/article/details/7162903 35 return result == 0; 36 } 37 } 38 return NO; 39 }
同时在NSURL+Common.m 里面定了三个比较系统版本的宏:
1 #define SYSTEM_VERSION_EQUAL_TO(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedSame) 2 #define SYSTEM_VERSION_GREATER_THAN(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedDescending) 3 #define SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] != NSOrderedAscending)
还有NSURL 三个比较有意思的属性:
1 @property (nullable, readonly, copy) NSString *lastPathComponent NS_AVAILABLE(10_6, 4_0); // 从路径中获得完整的文件名(带后缀) 2 @property (nullable, readonly, copy) NSString *pathExtension NS_AVAILABLE(10_6, 4_0); // 获得文件的扩展类型(不带'.')
看一下NSURL 类各个属性的意义:
NSURL其实就是我们在浏览器上看到的网站地址,这不就是一个字符串么,为什么还要在写一个NSURL呢,主要是因为网站地址的字符串都比较复杂,包括很多请求参数,这样在请求过程中需要解析出来每个部门,所以封装一个NSURL,操作很方便:
1 NSURL *url = [NSURL URLWithString:@"https://www.baidu.com/s/b/test?tn=baiduhome_pg&bs=NSRUL&f=8&rsv_bp=1&rsv_spt=1&wd=NSurl&inputT=2709"]; 2 NSLog(@"Scheme: %@", [url scheme]); // URL 的头: http 或者 https 3 NSLog(@"Host: %@", [url host]); // www.baidu.com (类似首页域名) 4 NSLog(@"Port: %@", [url port]); // null 5 NSLog(@"Path: %@", [url path]); // /s/b/test 域名和?之间的内容 6 NSLog(@"Relative path: %@", [url relativePath]); // /s/b/test 7 NSLog(@"Path components as array: %@", [url pathComponents]); // "/", s, b, test 8 NSLog(@"Parameter string: %@", [url parameterString]); // null 9 NSLog(@"Query: %@", [url query]); // tn=baiduhome_pg&bs=NSRUL&f=8&rsv_bp=1&rsv_spt=1&wd=NSurl&inputT=2709 10 NSLog(@"Fragment: %@", [url fragment]); // null 11 NSLog(@"User: %@", [url user]); // null 12 NSLog(@"Password: %@", [url password]); // null
下面继续看init 方法里面的[self loadStartImages];
1 // 初始化的时候 loadStartImages 2 - (void)loadStartImages{ 3 NSArray *plistArray = [NSArray arrayWithContentsOfFile:[self pathOfSTPlist]]; 4 plistArray = [NSObject arrayFromJSON:plistArray ofObjects:@"StartImage"]; // 从本地读文件并转化为模型 - (void)refreshImagesPlist{} 方法请求所有的图片信息返回后会存在本地plist里面 5 6 NSMutableArray *imageLoadedArray = [[NSMutableArray alloc] init]; 7 NSFileManager *fm = [NSFileManager defaultManager]; 8 9 for (StartImage *curST in plistArray) { 10 if ([fm fileExistsAtPath:curST.pathDisk]) { // 如果磁盘里面存在该图片就把他添加进数组里面 11 [imageLoadedArray addObject:curST]; 12 } 13 } 14 // 上一次显示的图片,这次就应该把它换掉 15 NSString *preDisplayImageName = [self getDisplayImageName]; // 获得上一次显示的图片的名字 16 if (preDisplayImageName && preDisplayImageName.length > 0) { 17 NSUInteger index = [imageLoadedArray indexOfObjectPassingTest:^BOOL(id obj, NSUInteger idx, BOOL *stop) { // http://www.cnblogs.com/xiaobajiu/p/4116503.html 18 if ([[(StartImage *)obj fileName] isEqualToString:preDisplayImageName]) { 19 *stop = YES; 20 return YES; 21 } 22 return NO; 23 }]; 24 if (index != NSNotFound && imageLoadedArray.count > 1) {//imageLoadedArray.count > 1 是因为,如果一共就一张图片,那么即便上次显示了这张图片,也应该再次显示它 25 [imageLoadedArray removeObjectAtIndex:index]; 26 } 27 } 28 self.imageLoadedArray = imageLoadedArray; 29 }
1 // STARTIMAGE.plist 的路径 2 - (NSString *)pathOfSTPlist{ 3 return [[self downloadPath] stringByAppendingPathComponent:@"STARTIMAGE.plist"]; 4 }
在[self downloadPath]; 路径后面拼接文件名,返回完整的STARTIMAGE.plist 文件路径。
首先获取该路径下面的plist 文件里面的数组数据,然后根据数据和类名创建StartImage 对象并放进数组plistArray里面。
创建imageLoadedArray 数组,forin plistArray数组,判断每一个StartImage 对象的pathDisk 路径,如果该路径下存在文件就把该对象装进imageLoadedArray 数组里面。
从本地[NSUserDefaults standardUserDefaults] 读取kStartImageName 的值,即上一次app 启动显示的启动图片的名字preDisplayImageName。如果preDisplayImageName 存在且长度大于 0,然后遍历imageLoadedArray 数组, 如果imageLoadedArray中有对象的fileName 和preDisplayImageName 相等的话,停止遍历,并把该对象的index 返回,如果index 不等于NSNotFound 且imageLoadedArray的count 大于 1 ,把该index 下的对象移除。 即移除fileName属性值 和上次显示的图片的名字的一样的StartImage 对象。并把imageLoadedArray 赋值给self.imageLoadedArray 属性。
StartImage *st = [[StartImagesManager shareManager] randomImage]; // 获得启动时的整个屏幕的大图
当执行完shareManger 方法init 一个StartImagesManager 对象后开始执行 randomImage方法:
1 // 获得随机的StartImage 2 - (StartImage *)randomImage{ 3 if ([NSDate isDuringMidAutumn]) { // 判断是不是中秋 4 _startImage = [StartImage midAutumnImage]; // 获得中秋节的StartImage 5 }else{ 6 NSUInteger count = _imageLoadedArray.count; // _imageLoadedArray 该数组里面出存放所有的StartImage 7 if (count > 0) { 8 NSUInteger index = arc4random()%count; // 随机获得一个小于count 的下标 9 _startImage = [_imageLoadedArray objectAtIndex:index]; // 从_imageLoadedArray 里面随机获得一个_startImage 10 }else{ 11 _startImage = [StartImage defautImage]; // 如果_imageLoadedArray 里面没有数据,则使用默认的StartImage 12 } 13 } 14 15 [self saveDisplayImageName:_startImage.fileName]; // 把显示的图片的名字存进本地plist 里面,用来记录本次启动的启动图的名字,下次启动的时候要更换图片 16 [self refreshImagesPlist]; // 刷新Plist 17 return _startImage; 18 }
如果是中秋节的话会获取中秋节的_startImage
1 // 中秋节特制的启动图片 2 + (StartImage *)midAutumnImage{ 3 StartImage *st = [[StartImage alloc] init]; 4 st.descriptionStr = @"\"中秋快乐\" © Mango"; 5 st.fileName = @"MIDAUTUMNIMAGE.jpg"; 6 st.pathDisk = [[NSBundle mainBundle] pathForResource:@"MIDAUTUMNIMAGE" ofType:@"jpg"]; 7 return st; 8 }
@property (strong, nonatomic) NSString *fileName, *descriptionStr, *pathDisk; StartImage 的三个属性的赋值,pathDisk 属性的赋值特别重要,他并不是从网络返回的,是本地添加的。
然后判断_imageLoadArray.count ,在startImagesManager 对象init 方法里面,根据本地存储的数据情况对_imageLoadArray 进行了赋值。如果它的count 不大于0 的话,则获取默认的StartImage 对象:
1 // 默认StartImage 2 + (StartImage *)defautImage{ 3 StartImage *st = [[StartImage alloc] init]; 4 st.descriptionStr = @"\"Light Returning\" © 十一步"; 5 st.fileName = @"STARTIMAGE.jpg"; 6 st.pathDisk = [[NSBundle mainBundle] pathForResource:@"STARTIMAGE" ofType:@"jpg"]; 7 return st; 8 }
当然如果_imageLoadedArray 的count 大于0 ,则随机获取该数组里面的一个StarImage 对象。
最终获得一个_startImage 对象,执行[self saveDisplayImageName:_startImage.fileName]; // 把该选中的对象的图片的名字存进本地plist 里面,用来记录本次启动的启动图的名字,下次启动的时候要更换图片
下面:[self refreshImagesPlist]; // 从后台获取启动图片的数据信息,并保存在本地,方便每次app 启动的时候使用。
1 // 刷新ImagesPlist 2 - (void)refreshImagesPlist{ 3 NSString *aPath = @"api/wallpaper/wallpapers"; 4 NSDictionary *params = @{@"type" : @"3"}; 5 [[CodingNetAPIClient sharedJsonClient] GET:aPath parameters:params success:^(AFHTTPRequestOperation *operation, id responseObject) { 6 DebugLog(@"\n===========response===========\n%@:\n%@", aPath, responseObject); // 打印数据 7 id error = [self handleResponse:responseObject]; // error 信息处理 8 if (!error) { 9 NSArray *resultA = [responseObject valueForKey:@"data"]; 10 if ([self createFolder:[self downloadPath]]) { // 路径存在 11 if ([resultA writeToFile:[self pathOfSTPlist] atomically:YES]) { 12 [[StartImagesManager shareManager] startDownloadImages]; // 开始下载图片 13 } 14 } 15 } 16 } failure:^(AFHTTPRequestOperation *operation, NSError *error) { // 请求错误信息 17 DebugLog(@"\n===========response===========\n%@:\n%@", aPath, error); 18 }]; 19 }
CodingNetAPIClient 是继承自AFHTTPRequestOperationManager 的类,他做一个单例实现并封装了数据请求、上传图片、上传声音等方法。
完整的URL 是 https://coding.net/api/wallpaper/wallpapers ,参数是{@"type": @"3"},当正确数据返回后,执行success 的block,把返回的数据"data" 对应的value 值存进[self pathofSTPlist] 路径下面。在执行[[StartImagesManager shareManager] startDownloadImages]; // 开始下载图片
1 // 开始下载图片 2 - (void)startDownloadImages{ 3 // 如果是WiFi 就下载图片 4 if (![AFNetworkReachabilityManager sharedManager].reachableViaWiFi) { 5 return; 6 } 7 8 NSArray *plistArray = [NSArray arrayWithContentsOfFile:[self pathOfSTPlist]]; 9 plistArray = [NSObject arrayFromJSON:plistArray ofObjects:@"StartImage"]; // JSON 转模型 10 11 NSMutableArray *needToDownloadArray = [NSMutableArray array]; 12 NSFileManager *fm = [NSFileManager defaultManager]; 13 for (StartImage *curST in plistArray) { 14 if (![fm fileExistsAtPath:curST.pathDisk]) { // 判断磁盘有没有这些图片 15 [needToDownloadArray addObject:curST]; 16 } 17 } 18 19 for (StartImage *curST in needToDownloadArray) { // 循环下载图片 20 [curST startDownloadImage]; 21 } 22 }
首先判断是不是WiFi 网络,如果不是直接return 了。
先从刚才写进本地plist 里面读取数据并把数据转化为 StartImage 对象保存进数组plistArray 里面。已知的当前是包含4 个元素。(这个传递数据和模型名转化模型的操作是NSObject+ObjectMap 类目做的,后面会讲解)
接着创建needToDownloadArray 数组准备接收需要下载图片的StartImage 对象。
遍历plistArray 数组,根据每个StarImage 对象的pathDisk 判断该路径下本地是否存在文件,如果不存在就把StartImage 对象添加进needToDownloadArray 数组。然后遍历needToDownloadArrat 数组循环下载图片:[curST startDownloadImage];
1 // 下载图片 2 - (void)startDownloadImage{ 3 NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; 4 AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:configuration]; 5 NSURL *URL = [NSURL URLWithString:self.url]; 6 NSURLRequest *request = [NSURLRequest requestWithURL:URL]; 7 NSURLSessionDownloadTask *downloadTask = [manager downloadTaskWithRequest:request progress:nil destination:^NSURL *(NSURL *targetPath, NSURLResponse *response) { 8 NSString *documentPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject]; 9 NSString *pathDisk = [[documentPath stringByAppendingPathComponent:@"Coding_StartImages"] stringByAppendingPathComponent:[response suggestedFilename]]; // http://www.xuebuyuan.com/1378139.html 从NSURLResponse 里面取文件名 10 return [NSURL fileURLWithPath:pathDisk]; // 返回文件路径 11 } completionHandler:^(NSURLResponse *response, NSURL *filePath, NSError *error) { 12 DebugLog(@"downloaded file_path is to: %@", filePath); 13 }]; 14 [downloadTask resume]; 15 }
destination 需要返回一个NSURL 的block。
1 // pathDisk get方法 返回文件路径 2 - (NSString *)pathDisk{ 3 if (!_pathDisk && _url) { 4 NSString *documentPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject]; 5 _pathDisk = [[documentPath 6 stringByAppendingPathComponent:@"Coding_StartImages"] 7 stringByAppendingPathComponent:[[_url componentsSeparatedByString:@"/"] lastObject]]; 8 } 9 return _pathDisk; 10 }
这一段必须注意的StarImage 的pathDisk 重写了get方法,该属性的赋值都是本地字符串拼接的并不是随请求数据返回的。且fileName (是图片的名字)descriptonStr (是作者的名字和图片名字的拼接在一起的字符串在启动View的底部显示)的get 方法都进行了重写, 都是本地根据网络数据返回拼接而成。都需要注意一下。
到此StartImagesManager 类和 StartImage 类基本完毕。
下面接着看EaseStartView 类:
return [[self alloc] initWithBgImage:st.image logoIcon:logoIcon descriptionStr:st.descriptionStr]; // alloc init
st.image 是上面的StartImage.image logoIcon logo 图片Image st.descriptionStr 上面的StartImage.descriptionStr 。
1 // 初始化EaseStartView 2 - (instancetype)initWithBgImage:(UIImage *)bgImage logoIcon:(UIImage *)logoIcon descriptionStr:(NSString *)descriptionStr{ 3 self = [super initWithFrame:kScreen_Bounds]; 4 if (self) { 5 //add custom code 6 UIColor *blackColor = [UIColor blackColor]; 7 self.backgroundColor = blackColor; 8 // 背景View 9 _bgImageView = [[UIImageView alloc] initWithFrame:kScreen_Bounds]; 10 _bgImageView.contentMode = UIViewContentModeScaleAspectFill; 11 _bgImageView.alpha = 0.0; 12 [self addSubview:_bgImageView]; 13 if (![NSDate isDuringMidAutumn]) { // 如果是中秋节 14 [self addGradientLayerWithColors:@[(id)[blackColor colorWithAlphaComponent:0.4].CGColor, (id)[blackColor colorWithAlphaComponent:0.0].CGColor] locations:nil startPoint:CGPointMake(0.5, 0.0) endPoint:CGPointMake(0.5, 0.4)]; // 处理颜色渐变 15 } 16 // logoIcon ImageView 17 _logoIconView = [[UIImageView alloc] init]; 18 _logoIconView.contentMode = UIViewContentModeScaleAspectFit; 19 [self addSubview:_logoIconView]; 20 // descriptionStrLabel 21 _descriptionStrLabel = [[UILabel alloc] init]; 22 _descriptionStrLabel.font = [UIFont systemFontOfSize:10]; 23 _descriptionStrLabel.textColor = [UIColor colorWithWhite:1.0 alpha:0.5]; 24 _descriptionStrLabel.textAlignment = NSTextAlignmentCenter; 25 _descriptionStrLabel.alpha = 0.0; 26 [self addSubview:_descriptionStrLabel]; 27 // descriptionStrLabel 约束 28 [_descriptionStrLabel mas_makeConstraints:^(MASConstraintMaker *make) { 29 make.centerX.equalTo(@[self, _logoIconView]); 30 make.height.mas_equalTo(10); 31 make.bottom.equalTo(self.mas_bottom).offset(-15); 32 make.left.equalTo(self.mas_left).offset(20); 33 make.right.equalTo(self.mas_right).offset(-20); 34 }]; 35 // logoIconView 约束 36 [_logoIconView mas_makeConstraints:^(MASConstraintMaker *make) { 37 make.centerX.equalTo(self); 38 make.top.mas_equalTo(kScreen_Height/7); 39 make.width.mas_equalTo(kScreen_Width *2/3); 40 make.height.mas_equalTo(kScreen_Width/4 *2/3); 41 }]; 42 43 [self configWithBgImage:bgImage logoIcon:logoIcon descriptionStr:descriptionStr]; 44 } 45 return self; 46 }
我们先看一下他的属性有哪些:
@property (strong, nonatomic) UIImageView *bgImageView, *logoIconView;
@property (strong, nonatomic) UILabel *descriptionStrLabel;
bgImageView 就是背景的UIImageView , logoIconView 就是展示的Coding 的logo 的UIImageView descriptionStrLabel 是启动View 底部的一个UILabel 主要显示bgImageView 的图片的名字和作者的名字拼接的字符串,启动图片都是有版权的。
首先看_bgImageView 没什么特别全屏大小图片各个方向充满透明度是0添加到self。
下面的一个如果不是中秋节的时候:[self addGradientLayerWithColors:@[(id)[blackColor colorWithAlphaComponent:0.4].CGColor, (id)[blackColor colorWithAlphaComponent:0.0].CGColor] locations:nil startPoint:CGPointMake(0.5, 0.0) endPoint:CGPointMake(0.5, 0.4)]; // 处理颜色渐变
1 // 处理颜色渐变 2 - (void)addGradientLayerWithColors:(NSArray *)cgColorArray locations:(NSArray *)floatNumArray startPoint:(CGPoint )startPoint endPoint:(CGPoint)endPoint{ 3 CAGradientLayer *layer = [CAGradientLayer layer]; 4 layer.frame = self.bounds; 5 if (cgColorArray && [cgColorArray count] > 0) { 6 layer.colors = cgColorArray; 7 }else{ 8 return; 9 } 10 if (floatNumArray && [floatNumArray count] == [cgColorArray count]) { // locations 渐变颜色的区间分布,locations的数组长度和colors一致,这个值一般不用管它,默认是nil,会平均分布 11 layer.locations = floatNumArray; 12 } 13 layer.startPoint = startPoint; // 映射locations中第一个位置,用单位向量表示,比如(0,0)表示从左上角开始变化。默认值是(0.5,0.0) 14 layer.endPoint = endPoint; // 映射locations中最后一个位置,用单位向量表示,比如(1,1)表示到右下角变化结束。默认值是(0.5,1.0) type 默认值是kCAGradientLayerAxial,表示按像素均匀变化。除了默认值也无其它选项 15 [self.layer addSublayer:layer]; 16 }
该方法在UIView+Common 类目里面,给View 添加一个CAGradientLayer 渐变色的layer 。
下面分别是创建_logoIconView 和_descriptionStrLabel 并添加到self.
下面引入了一个功能强大的第三方库Masonry ,用来添加约束做自动布局。我准备在下篇开始的时候在对它做一个详细的分析。
下面是开启启动动画并使用一个EaseStartView 做参数返回值为void 的completionHandler block 做启动动画完毕的处理。
1 // 开始动画 2 - (void)startAnimationWithCompletionBlock:(void(^)(EaseStartView *easeStartView))completionHandler{ 3 [kKeyWindow addSubview:self]; 4 [kKeyWindow bringSubviewToFront:self]; // 把EaseStartView 添加到当前window 的最前面 5 _bgImageView.alpha = 0.0; 6 _descriptionStrLabel.alpha = 0.0; 7 8 @weakify(self); 9 [UIView animateWithDuration:2.0 animations:^{ 10 @strongify(self); 11 self.bgImageView.alpha = 1.0; 12 self.descriptionStrLabel.alpha = 1.0; 13 } completion:^(BOOL finished) { 14 [UIView animateWithDuration:0.6 delay:0.3 options:UIViewAnimationOptionCurveEaseIn animations:^{ 15 @strongify(self); 16 [self setX:-kScreen_Width]; 17 } completion:^(BOOL finished) { 18 @strongify(self); 19 [self removeFromSuperview]; // 移除EaseStartView 20 if (completionHandler) { 21 completionHandler(self); 22 } 23 }]; 24 }]; 25 }
#define kKeyWindow [UIApplication sharedApplication].keyWindow
先把self 即EaseStartView 添加到当前Window 上,并把它提到window 的最前面。并设置_bgImageView 和_descriptionStrLabel 的透明度是0,然后在2 秒内做一个透明度渐变为 1 的操作。
@weakify(self);
@strongify(self); 是另外一个功能强大的框架ReactiveCocoa 会在下篇做讲解。
这里主要是为了防止block 内部循环引用造成内存泄露。
在第一个block 动画执行完毕后是第二个block 动画,在0.6 秒内设置[self setX:-kScreen_Width];,并在动画完毕后[self removeFromSuperview]; // 从Window 移除EaseStartView
if (completionHandler) {
completionHandler(self);
}
如果CompletionHandler 存在,则执行completionHandler block实现。到此EaseStartView 类基本分析完毕。