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

实验从两个方面来评价: 

  1. 分别使用有 100 个对象和 1000000 个对象的 NSArray,只取对象,不执行操作,测试遍历速度 
  2. 使用有 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

倒序遍历

NSArrayNSOrderedSet都支持使用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 类基本分析完毕。

posted @ 2016-08-03 15:52  鳄鱼不怕牙医不怕  阅读(453)  评论(0编辑  收藏  举报