这节课的主要内容包括:
1.UI元素,比如UITabBarController以及UINavigationItem(就是自定义navigation controller里面的view controller的样子的property);
2.然后就是Blocks,Blocks是一种语言特性,这非常重要,我们所有在ios的多线程的东西都要用到Blocks,因为主线程也就是UI线程,时常要跟用户交流,这是永远都不能被阻塞的。
UITabBarController基本上就是你在iphone或其他ios应用里看到的那些在底部的黑色的button,比如时钟app,可以看到从左边点到右边,就会出现不同的view controller。
通过在xcode里面拖拽的方式来创建TabBarController,然后通过control+拖拽的方式来把view controller和TabBarController联系起来,当做这些control+拖拽动作的时候,就是设置了一个叫做view controller的UITabBarController的property,这是一个UIViewController的NSArray数组,基本上只需要设置这个。
怎么来控制这些tab呢?默认情况下,tab上的名字就是你的UIViewController的标题,UIViewController有个NSString类型的property叫做title,需要设置它。在tab bar上点击编辑,设置好标题和图片,这些都是存储在每个UIViewController里面,而不是UITabBarController本身。UIViewController有个property叫做tabBarItem,它是一个UITabBarItem *,指向UITabBarItem的指针,UITabBarItem有很多property,比如title、image还有badgeValue,可以通过调用self.tabBarItem.badgeValue =来设置它:
- (void)somethingHappenedToCauseUsToNeedToShowABadgeValue { self.tabBarItem.badgeValue = @“R”; }
超过4个button,就会有More button,就在最右下角。点击了More,然后就会有一片显示了想要在底部显示的所有东西的区域出现,这是自动完成的。
如果把UINavigationController和UITabBarController结合起来会怎么样?navigationController总是在tabBarController里面,当你在xcode里把它们拖拽出来的时候,会从tabBarController进行control+拖拽到navigationController来设置navigationController成为它的tab之一。
如果是UISplitViewController和UITabBarController会怎么样?这两个也可以组合起来用,tabBarController会在splitViewController里面,要么在master,要么在detail,要么在这两个里面。
UIViewController有个property叫navigationItem,一个UINavigationItem *对象,这个对象有一些property能在显示你的UIViewController的时候控制UINavigationController的外观,这是UIViewController的property而不是UINavigationController的,除了backButton被UINavigationController控制。navigationItem里面有些方法比如leftBarButtonItem,只能在rootViewController时设置它,因为通常这是backButton所在的地方。
如果想在navigationController上面有toolbarButton,可以在xcode里通过开关打开,这些toolbarButton也是由同时被显示的UIViewController来控制,不过它不是由navigationItem property控制,而是一个叫toolbarItem的property,是一个UIBarButtonItem的NSArray。如果给viewController设置一个toolbarItem数组,然后不论viewController什么时候进入navigationController,这些buttons都会在底部显示,假设navigationController显示底部barButton的话。
UINavigationController和UITabBarController相似,也采用了相同的机制:使用一个在被其显示的viewController里的对象来控制自身的部分显示。
block是一个有序列的指令代码块,通常在代码中间花括号括起来的objective-c代码,但是它能被传递和被分配局部变量,然后作为一个参数来传递,基本上可以把花括号中间的代码保存到数据结构里。看起来就像这样子:
[aDictionary enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) { NSLog(@“value for key %@ is %@”, key, value); if ([@“ENOUGH” isEqualToString:key]) { *stop = YES; } }];
NSDictionary有一个方法叫enumerateKeysAndObjectsUsingBlock,它就一个参数就是block,这个block携带了三个参数,这将要把dictionary里面的key和value每次一组传递到block,enumerateKeysAndObjectsUsingBlock会遍历dictionary并把里面所有的key和value一组一组的展示给你,每组都会执行这个block。这其实就是传递一个block到另一个方法,在这个例子里它会带着特定参数被反复调用,直到找到一个ENOUGH的key,然后就会通过重新赋值那个BOOL *stop来停止运行,停止遍历同时停止调用block。
^表示这是一个block。在enumerateKeysAndObjectsUsingBlock之前定义一个局部变量,可以在block里使用,但它们都是只读的。
BOOL stoppedEarly = NO; double stopValue = 53.5; [aDictionary enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) { NSLog(@“value for key %@ is %@”, key, value); if ([@“ENOUGH” isEqualToString:key] || ([value doubleValue] == stopValue)) { *stop = YES; stoppedEarly = YES; // ILLEGAL } }];
不过如果你在前面加上_block,就会变成可读写了,然后在block执行完后它会保留被赋的值。
__block BOOL stoppedEarly = NO; double stopValue = 53.5; [aDictionary enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) { NSLog(@“value for key %@ is %@”, key, value); if ([@“ENOUGH” isEqualToString:key] || ([value doubleValue] == stopValue)) { *stop = YES; stoppedEarly = YES; // this is legal now } }]; if (stoppedEarly) NSLog(@“I stopped logging dictionary values early!”);
在block里面用了一个实例变量,出来后也是有效的,而且不是只读的。
typedef一个block:
typedef double (^unary_operation_t)(double op);
这行代码的意思就是定义一个新类型,它本质上是一个block,它接受一个类型为double的参数,返回值的类型也是double的,新类型的名字叫unary_operation_t。
已经定义好了这个类型,现在用它声明一个变量,变量名叫square,接下来给它赋值:
unary_operation_t square; square = ^(double operand) { // the value of the square variable is a block return operand * operand; }
现在有了一个类型为unary_operation_t的变量square,怎么用呢?就像用一个C语言函数一样:
double squareOfFive = square(5.0);
把5.0作为参数传给block,5.0就是block里面的操作数,它进行平方操作,得到25。
这有一行代码可以不用typedef直接声明square:
double (^square)(double op) = ^(double op) { return op * op; };
此处的square是一个变量,而不是一个类型。
一旦定义好unary_operation_t,这种类型实际上是一个block,就可以用它进行函数声明了,把它作为函数的参数,就是声明一个参数为unary_operation_t类型的方法,比如addUnaryOperation:whichExecutesBlock:,第二个参数是block,可以用unary_operation_t来声明这个方法。首先,加入一个property:
@property (nonatomic, strong) NSMutableDictionary *unaryOperations;
它就是一个用来存储所有unaryOperations的dictionary,用来记录key值的dictionary。
然后实现addUnaryOperation:whichExecutesBlock:
typedef double (^unary_operation_t)(double op); - (void)addUnaryOperation:(NSString *)op whichExecutesBlock:(unary_operation_t)opBlock { [self.unaryOperations setObject:opBlock forKey:op]; }
这个方法的第二个参数是一个block,unary_operation_t指明了block的类型。把block看成一个对象,通过给定的key加入到dictionary里。block用起来像类一样,实际不是,只是有些相似,不能给它传递消息,可以像安置id一样安置它们。
ARC负责block的内存管理,由于它本身并不是一个对象,它靠ARC进行内存管理,当block执行完之后,它的内存是自动释放的。
然后我们就有了一个字典,字典的key是代表操作的字符串,值是block。接着可以这么做:
- (double)performOperation:(NSString *)operation { unary_operation_t unaryOp = [self.unaryOperations objectForKey:operation]; if (unaryOp) { self.operand = unaryOp(self.operand); } . . . }
大多数不用typedef来声明一个方法,比如
- (void)enumerateKeysAndObjectsUsingBlock:(void (^)(id key, id obj, BOOL *stop))block;
没有返回值所以是void,注意(^),在括号中间没有其他东西,是因为没有定义type。后面就是block需要的参数,然后就是block。
block很常见,实际上有简便方法,然后有两件事情需要去做:第一,不必去声明那些能被block里面引用的返回的type,如果它能够在block里被推断出来,比如
NSNumber *secret = [NSNumber numberWithDouble:42.0]; [brain addUnaryOperation:@“MoLtUaE” whichExecutesBlock:^(double operand) { return operand * [secret doubleValue]; }];
另外一个就是如果一个block没有参数的话,不必写这个(),比如
[UIView animateWithDuration:5.0 animations:^{ view.opacity = 0.5; }];
没有参数,没有返回值,仅仅是花括号。
如果类里面有个property,这个property是里面是block的NSArray,然后尝试在实现里面添加一个对象,这个对象的实现就是一个block引用到self。
@property (nonatomic, strong) NSArray *myBlocks; // array of blocks
现在我在做的就是给自己的property添加一个block,block里面引用到我自己,这样会创建一个Memory Cycles,因为block会有一个strong指针指向self。
[self.myBlocks addObject:^() {
[self doSomething];
}];
block里面的任何对象都需要一个strong指针,同时有一个strong指针指向block,因为有一个指针指向这个数组,然后这个数组又有一个strong指针指向这个block,所以现在有一对strong指针指向对方,谁也不可能离开heap了。因为永远不会有这么一种情况,其中一个没有strong指针指向另一个,所以这不好。这就是Memory Cycles,它将会泄露内存,这两个对象的内存会一直占用着。
怎么避免这种情况呢?幸亏有_weak,就是说这个局部变量是weak。
__weak MyClass *weakSelf = self; [self.myBlocks addObject:^() { [weakSelf doSomething]; }];
然后就有了一个weak指针跟self一样指向heap中的同一个地方,但是它是weak指针指向内存的那个地方,然后在block里用这个weakSelf。现在这个block没有一个strong指针,只有一个weak指针指向self。一旦我们离开这个block的heap,block也可以离开了。
什么时候用到block呢?用来枚举;view的动画;用来排序,给一个根据在这个array里面执行block来决定数组里面的排序;用来弄notification;当某些事情发生的时候执行block,用来处理错误,如果错误发生的时候执行block;用来做完成某些事情后的处理。
最重要应用的地方就是多线程,利用C API,不是一个对象,C API调用Grand Central Dispatch。
Grand Central Dispatch的理念就是你有一系列的操作队列,我们放置一个block的队列,这些block会出队列,然后在另外一个线程里面执行。
但是也有主队列,主队列就是UIKit的事情发生的地方,所有的绘图工作,这个队列我们要保证不放任何与UI无关的东西在上面,因为我们始终想要这个队列处在无压力处理手势或其他类似的事情状态。所以我们搞多线程就是把这些需要长时间运行的block放到队列里面,让它们在另外的线程里面运行,然后那些线程再回主线程更新UI。这样我们就能在另外的线程里面做那些block的事情,比如访问网络或一些很费资源的事情。
在这个API里面有什么重要的函数?
dispatch_queue_t dispatch_queue_create(const char *label, NULL); // serial queue
这创建一个队列,一个将要放block进去的队列。
typedef void (^dispatch_block_t)(void); void dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
将block放到队列里,然后在未来某个不确定的时间,会有人把这个block移出队列到另外一个线程为我们执行。
有两个有趣的方法去得到当前的队列,有时会有人用block调用你,请求你去做些事情,然后你想要在另外的线程里面做一部分,所以你会想要知道这个调用的人所在的队列。因为你可能想要在一个线程里面执行一些事情之后发送消息回这个调用你的队列。
dispatch_queue_t dispatch_get_current_queue(); void dispatch_queue_retain(dispatch_queue_t); // keep it in the heap until dispatch_release
但更重要的是想要得到主队列,就是UIKit的队列,这是要放置任何会涉及UI的代码的地方,如果你发布到除主线程以外的其他线程的队列运行UIKit的代码,会出现问题。
dispatch_queue_t dispatch_get_main_queue();
如果你在另外的线程里面做点事情,然后你需要做些关于UIKit的东西,你可以放一个block来传回到主线程里面执行。
这看起来是什么样子呢?假设有个方法,它去访问网络然后取回一些内容,比如图片的URL,然后我利用这个数据创建了一个UIImage,然后我把它设置到一个image view里,我放入一个scrollView,然后会显示的。这个URL的数据内容可能会花费这个方法很长一段时间去取,不想让这段代码在主队列中运行,因为这样的话,在viewWillAppear的过程中UI会完全没有反应。所以仅仅需要创建一个队列,一个下载队列,我会调用它。dispatch_queue_create的第一个参数是const char *,这是纯C API,不能是NSString,你会在debugger里面看到那个线程,这个参数就是做这个用。这个NULL就是标识这是否是一个连续的队列或者是并发的队列,NULL意味着连续队列。
- (void)viewWillAppear:(BOOL)animated { dispatch_queue_t downloadQueue = dispatch_queue_create(“image downloader”, NULL); dispatch_async(downloadQueue, ^{ NSData *imageData = [NSData dataWithContentsOfURL:networkURL]; dispatch_async(dispatch_get_main_queue(), ^{ UIImage *image = [UIImage imageWithData:imageData]; self.imageView.image = image; self.imageView.frame = CGRectMake(0, 0, image.size.width, image.size.height); self.scrollView.contentSize = image.size; }); }); dispatch_release(downloadQueue); }
创建了这个队列,然后是dispatch_async,里面是想要在另外一个线程里面做的block,有几个难点要注意:一,UIKit的调用只能发生在主线程里面,移动UIKit相关的代码回主队列,如果有些东西需要回到另外的队列上去执行,只要把它们发布到另外的队列上就行,按时间线性执行就好,被执行的东西会在队列和队列之间来回传递,只要保证它们在正确的队列上执行就可以了。正确的队列就是,关于UI的东西都放在主线程上,别的东西都要放在其他线程上。二,关于download线程的内存管理问题,这些线程不是对象,而是C语言代码,所以必须对downloadQueue调用dispatch_release方法,意思是只要队列里不再有block了,就可以release。
downloadQueue是一个独立的线程,并不会锁住主线程。一旦进行了dispatch_async,主线程就会保持执行,直到我向它请求执行一些代码,这样主线程就不会锁。当我想执行一些imageView设置的代码时,就会把这个block放到主线程执行,如果此时主线程正在处理手势,当这些手势执行完,它会询问有block要执行吗?真的有,这样就会显示图片。先把它们加到线程队列上,然后在它们准备执行时,再把它们出队。
一旦把某个东西放到队列上,就不能剔除它。而且一旦队列上的某个进程开始执行了,不能打断它。
这个demo要做的是从Flicker上下载一些图片,用一个列表来展示这些图片,然后会调用GCD,在另外一个线程中开启新的下载任务,所以UI并不会被锁住。
新建名为Shutterbug的工程,删除其中的viewController文件及storyboard中的控制器,从library中拖出一个tableViewController,并创建一个UITableViewController的子类名为FlickrPhotoTableViewController。选中tableViewController,将其嵌入到navigationController中,这样tableViewController就有了toolbar,从library中拖出一个barButtonItem到toolbar并用control+拖拽的方法在FlickrPhotoTableViewController中创建一个动作。
FlickrPhotoTableViewController.h文件的代码:
#import <UIKit/UIKit.h> @interface FlickrPhotoTableViewController : UITableViewController @property (nonatomic, strong) NSArray *photos; // of Flickr photo dictionaries @end
FlickrPhotoTableViewController.m文件的代码:
#import "FlickrPhotoTableViewController.h" #import "FlickrFetcher.h" @implementation FlickrPhotoTableViewController @synthesize photos = _photos; - (IBAction)refresh:(id)sender { // might want to use introspection to be sure sender is UIBarButtonItem // (if not, it can skip the spinner) // that way this method can be a little more generic UIActivityIndicatorView *spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]; [spinner startAnimating]; self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:spinner]; dispatch_queue_t downloadQueue = dispatch_queue_create("flickr downloader", NULL); dispatch_async(downloadQueue, ^{ NSArray *photos = [FlickrFetcher recentGeoreferencedPhotos]; dispatch_async(dispatch_get_main_queue(), ^{ self.navigationItem.rightBarButtonItem = sender; self.photos = photos; }); }); dispatch_release(downloadQueue); } - (void)setPhotos:(NSArray *)photos { if (_photos != photos) { _photos = photos; // Model changed, so update our View (the table) if (self.tableView.window) [self.tableView reloadData]; } } - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation { return YES; } #pragma mark - UITableViewDataSource - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return [self.photos count]; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @"Flickr Photo"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier]; } // Configure the cell... NSDictionary *photo = [self.photos objectAtIndex:indexPath.row]; cell.textLabel.text = [photo objectForKey:FLICKR_PHOTO_TITLE]; cell.detailTextLabel.text = [photo objectForKey:FLICKR_PHOTO_OWNER]; return cell; } @end