1.转场动画
iOS7开始苹果新增了一些转场动画给我们,这给我们做动画提供了便利。
UIViewControllerAnimatedTransitioning
UIViewControllerContextTransitioning
UIPercentDrivenInteractiveTransition
这个经常在导航控制器push或pop的时候自己可以添加自定义的转场动画来实现自己想要的结果。
这个也可以实现右滑操作。
- (nullable id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
animationControllerForOperation:(UINavigationControllerOperation)operation
fromViewController:(UIViewController *)fromVC
toViewController:(UIViewController *)toVC
{
if (operation == UINavigationControllerOperationPush) {
return [AnimateInOut new];
}
else if (operation == UINavigationControllerOperationPop) {
return [AnimateTransition new];
}
return nil;
}
- (nullable id <UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController
interactionControllerForAnimationController:(id <UIViewControllerAnimatedTransitioning>) animationController {
return nil;
}
2.C语言中的二级指针
int main(void) {
int tcount = 7;
int *pcount = &tcount;
increaseValue(&pcount);
printf("count = %d\n", tcount);
printf("pcount = %p\n", pcount);
int tcu = 8;
increase(&tcu);
printf("count = %d\n", tcu);
printf("tcu = %d\n", tcu);
return 0;
}
void increaseValue(int **ptr)
{
**ptr = **ptr + 1;
*ptr = NULL;
}
void increase(int *ptr)
{
*ptr = *ptr + 1;
ptr = NULL;
}
结果:
ount = 8
pcount = 0x0或NULL
count = 9
tcu = 9
一级指针变量,也是一个普通变量,只不过这变量的值是一个内存单元的地址而已。tcu在传递给increase之前,被copy到一个临时变量中,这个临时变量的值是一个地址,可以改变这个地址所在内存单元的值,但是无法改变外部的tcu。
从这个结果可以得出一个结论:一级指针作为参数传递,可以改变外部变量的值,即一级指针所指向的内容,但是却无法改变指针本身(如tcu)。
对于指针操作,有两个概念:
引用:对应于C语言中的&取地址操作
Reference
解引用:在C语言中,对应于->操作。
Dereference operator
对于一个普通变量,引用操作,得到的是一级指针。一级指针传递到函数内部,虽然这个一级指针的值会copy一份到临时变量,但是这个临时变量的内容是一个指针,通过->解引用一个地址可以修改该地址所指向的内存单元的值。
对于一个一级指针,引用操作,得到一个二级指针。相反,对于一个二级指针解引用得到一级指针,对于一个一级指针解引用得到原始变量。一级指针和二级指针的值都是指向一个内存单元,一级指针指向的内存单元存放的是源变量的值,二级指针指向的内存单元存放的是一级指针的地址。
二级指针一般用在需要修改函数外部指针的情况。因为函数外部的指针变量,只有通过二级指针解引用得到外部指针变量在内存单元的地址,修改这个地址所指向的内容即可。
总结
首先,指针变量,它也是一个变量,在内存单元中也要占用内存空间。一级指针变量指向的内容是普通变量的值,二级指针变量指向的内容是一级指针变量的地址。
3.KVO的底层实现。
这是怎么实现的呢?其实这都是通过Objective-C强大的运行时(runtime)实现的。当你第一次观察某个object时,runtime会创建一个新的继承原先class的subclass,命名为NSKVONotifying_CustomClass(CustomClass为自己定义的类)。在这个新的class中,它重写了所有被观察的key,然后将object的isa指针指向新创建的class(这个指针告诉Objective-C运行时某个object到底是哪种类型的object)。所以object神奇地变成了新的子类的实例。
这些被重写的方法实现了如何通知观察者们。当改变一个key时,会触发setKey方法,但这个方法被重写了,并且在内部添加了发送通知机制。(当然也可以不走setXXX方法,比如直接修改iVar,但不推荐这么做)。
有意思的是:苹果不希望这个机制暴露在外部。除了setters,这个动态生成的子类同时也重写了-class方法,依旧返回原先的class!如果不仔细看的话,被KVO过的object看起来和原先的object没什么两样。
来自博客http://limboy.me/ios/2013/08/05/internal-implementation-of-kvo.html。
KVO和NSNotificationCenter简述
#import <Foundation/Foundation.h>
@interface Food : NSObject
@property (nonatomic, copy) NSString *name;
@end
控制器主要代码
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(listen:) name:@"NOTIFICATION" object:nil];
_f = [Food new];
_f.name = @"马铃薯";
[_f addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
// Printing description of _f->isa:
// NSKVONotifying_Food
_f.name = @"番薯";
- (void)listen:(NSNotification *)note {
NSLog(@"%@", [NSThread currentThread]);
NSLog(@"%@", note.userInfo[@"new"]);
}
- (void)dealloc {
[_f removeObserver:self forKeyPath:@"name" context:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:@"NOTIFICATION" object:nil];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
{
if ([keyPath isEqualToString:@"name"]) {
NSLog(@"%@", change[NSKeyValueChangeNewKey]);
// NSLog(@"%@", [NSThread currentThread]);
dispatch_queue_t queue = dispatch_queue_create("com.quart2DDemo.serialQueue", 0);
dispatch_async(queue, ^{
NSLog(@"%@", [NSThread currentThread]);
[[NSNotificationCenter defaultCenter] postNotificationName:@"NOTIFICATION" object:nil userInfo:change];
});
}
else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
这里主要是想说明KVO的底层原理,简述KVO在监听对象的时候如果监听对象的属性发生改变,KVO会动态的生成一个新的子类,用isa指向它,然后在新的子类里重写属性的set方法,在方法里调用父类的set方法,并通过调用observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context来通知属性的改变。
还有一点就是通知中心的问题,这里说明了在哪个线程发通知,就在哪个线程回调通知的信息。就是说如果在主线程(子线程)发通知,那么回调就在主线程(子线程)。
4.NSRunLoop的原理。
以下内容来自RyanJIN http://www.jianshu.com/p/ebb3e42049fd的简书。
关于NSRunLoop推荐看一下来自百度工程师的分享视频:http://v.youku.com/v_show/id_XODgxODkzODI0.html
RunLoop就是跑圈, 保证程序一直在执行. App运行起来之后, 即使你什么都不做, 放在那儿它也不会退出, 而是一直在"跑圈", 这就是RunLoop干的事. 主线程会自动创建一个RunLoop来保证程序一直运行. 但子线程默认不创建NSRunLoop, 所以子线程的任务一旦返回, 线程就over了.
上面的并发operation当start函数返回后子线程就退出了, 当NSURLConnection的delegate回调时, 线程已经木有了, 所以你也就收不到回调了. 为了保证子线程持续live(等待connection回调), 你需要在子线程中加入RunLoop, 来保证它不会被kill掉.
RunLoop在某一时刻只能在一种模式下运行, 更换模式时需要暂停当前的Loop, 然后重启新的Loop. RunLoop主要有下面几个模式:
NSDefalutRunLoopMode : 默认Mode, 通常主线程在这个模式下运行
UITrackingRunLoopMode : 滑动ScrollView是会切换到这个模式
NSRunLoopCommonModes: 包括上面两个模式
这边需要特别注意的是, 在滑动ScrollView的情况下, 系统会自动把RunLoop模式切换成UITrackingRunLoopMode来保证ScrollView的流畅性.
[NSTimer scheduledTimerWithTimeInterval:1.f
target:self
selector:@selector(timerAction:)
userInfo:nil
reports:YES];
当你在滑动ScrollView的时候上面的timer会失效, 原因是Timer是默认加在NSDefalutRunLoopMode上的, 而滑动ScrollView后系统把RunLoop切换为UITrackingRunLoopMode, 所以timer就不会执行了. 解决方法是把该Timer加到NSRunLoopCommonModes下, 这样即使滑动ScrollView也不会影响timer了.
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
另外还有一个trick是当tableview的cell从网络异步加载图片, 加载完成后在主线程刷新显示图片, 这时滑动tableview会造成卡顿. 通常的思路是tableview滑动的时候延迟加载图片, 等停止滑动时再显示图片. 这里我们可以通过RunLoop来实现.
[self.cellImageView performSelector:@sector(setImage:)
withObject:downloadedImage
afterDelay:0
inModes:@[NSDefaultRunLoopMode]];
当NSRunLoop为NSDefaultRunLoopMode的时候tableview肯定停止滑动了, why? 因为如果还在滑动中, RunLoop的mode应该是UITrackingRunLoopMode.
呼叫NSURLConnection的异步回调
现在解决方案已经很清晰了, 就是利用RunLoop来监督线程, 让它一直等待delegate的回调. 上面已经说到Main Thread是默认创建了一个RunLoop的, 所以我们的Option 1是让start函数在主线程运行(即使[operation start]是在子线程调用的).
- (void)start
{
if (![NSThread isMainThread]) {
[self performSelectorOnMainThread:@selector(start)
withObject:nil
waitUntilDone:NO];
return;
}
// set up NSURLConnection...
}
或者这样:
- (void)start
{
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
self.connection = [NSURLConnection connectionWithRequest:self.request delegate:self];
}];
}
这样我们可以简单直接的使用main run loop, 因为数据delivery是非常快滴. 然后我们就可以将处理incoming data的操作放到子线程去...
Option 2是让operation的start函数在子线程运行, 但是我们为它创建一个RunLoop. 然后把URL connection schedule到上面去. 我们先来瞅瞅AFNetworking是怎么做滴:
+ (void)networkRequestThreadEntryPoint:(id)__unused object
{
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
+ (NSThread *)networkRequestThread
{
static NSThread *_networkRequestThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
[_networkRequestThread start];
});
return _networkRequestThread;
}
- (void)start
{
[self.lock lock];
if ([self isCancelled]) {
[self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
} else if ([self isReady]) {
self.state = AFOperationExecutingState;
[self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
}
[self.lock unlock];
}
AFNetworking创建了一个新的子线程(在子线程中调用NSRunLoop *runloop = [NSRunLoop currentRunLoop]; 获取RunLoop对象的时候, 就会创建RunLoop), 然后把它加到RunLoop里面来保证它一直运行.
这边我们可以简单的判断下当前start()的线程是子线程还是主线程, 如果是子线程则调用[NSRunLoop currentRunLoop]创新RunLoop, 否则就直接调用[NSRunLoop mainRunLoop], 当然在主线程下就没必要调用[runLoop run]了, 因为它本来就是一直run的.
P.S. 我们还可以使用CFRunLoop来启动和停止RunLoop, 像下面这样:
[self.connection scheduleInRunLoop:[NSRunLoop currentRunLoop]
forMode:NSRunLoopCommonModes];
CFRunLoopRun();
等到该Operation结束的时候, 一定要记得调用CFRunLoopStop()停止当前线程的RunLoop, 让当前线程在operation finished之后可以退出.
多线程。
上下文切换
容易混淆的几个概念,之前学习计算机操作系统课程的时候老是搞混了。
1). 串行(Serial) VS. 并行(Concurrent)
串行和并行描述的是任务和任务之间的执行方式. 串行是任务A执行完了任务B才能执行, 它们俩只能顺序执行. 并行则是任务A和任务B可以同时执行.
2). 同步(Synchronous) VS. 异步(Asynchronous)
同步和异步描述的其实就是函数什么时候返回. 比如用来下载图片的函数A: {download image}, 同步函数只有在image下载结束之后才返回, 下载的这段时间函数A只能搬个小板凳在那儿坐等... 而异步函数, 立即返回. 图片会去下载, 但函数A不会去等它完成. So, 异步函数不会堵塞当前线程去执行下一个函数!
3). 并发(Concurrency) VS. 并行(Parallelism)
这个更容易混淆了, 先用Ray大神的示意图和说明来解释一下: 并发是程序的属性(property of the program), 而并行是计算机的属性(property of the machine).
图片省略。。。
Concurrency_vs_Parallelism.png
还是很抽象? 那我再来解释一下, 并行和并发都是用来让不同的任务可以"同时执行", 只是并发是伪同时, 而并行是真同时. 假设你有任务T1和任务T2(这里的任务可以是进程也可以是线程):
a. 首先如果你的CPU是单核的, 为了实现"同时"执行T1和T2, 那只能分时执行, CPU执行一会儿T1后马上再去执行T2, 切换的速度非常快(这里的切换也是需要消耗资源的, context switch), 以至于你以为T1和T2是同时执行了(但其实同一时刻只有一个任务占有着CPU).
b. 如果你是多核CPU, 那么恭喜你, 你可以真正同时执行T1和T2了, 在同一时刻CPU的核心core1执行着T1, 然后core2执行着T2, great!
其实我们平常说的并发编程包括狭义上的"并行"和"并发", 你不能保证你的代码会被并行执行, 但你可以以并发的方式设计你的代码. 系统会判断在某一个时刻是否有可用的core(多核CPU核心), 如果有就并行(parallelism)执行, 否则就用context switch来分时并发(concurrency)执行. 最后再以Ray大神的话结尾: Parallelism requires Concurrency, but Concurrency does not guarantee Parallelism!
NSOperation实验课
下面我们进入实验课啦, 要想真正了解某个东东, 还是需要打开Xcode, 写上几行代码, 然后Commard+R. 为了帮Apple提升Xcode的使用率:-D, 我会给出几个case, 童鞋们可以自己编写test code来验证:
1). 创建两个operation, 然后直接[operation start], 在NSOperation并发设计和非并发设计的情况下, 查看这两个operation是否同时执行了(最简单的打log看是不是交替打印).
2). 在主线程和子线程下分别调用[operation start], 看看执行情况.
主线程2016-01-19 18:19:05.916 Demo[5482:2300666] <NSThread: 0x7ff80ac04b20>{number = 1, name = main}, 耗时下载
子线程 2016-01-19 18:19:20.005 Demo[5482:2301611] <NSThread: 0x7ff80ad10e20>{number = 5, name = (null)}, 耗时下载2
3). 创建operation并放到NSOperationQueue里面执行, 分别看看mainQueue和非mainQueue下的执行情况.
4). maxConcurrentOperationCount设置后的执行情况.
最多并发的个数。
5). 试试NSOperation的依赖关系设置, [operationB addDependency:operationA].
operationB依赖于operationA,等待operationA的操作完成后才执行operationB的操作。
6). 写个完整的demo吧, 比如简单的HTTP Downloader.
5.iOS 7中UITableViewCell的变化
iOS 7的UITableViewCell内部与iOS 6有些不同,cell与contentView之间多了一层UITableViewCellScrollView。
下面简单作个比较:
iOS 6中UITableViewCell的View层级
(lldb) po [cell recursiveDescription]
<UITableViewCell: 0xba72e50; frame = (0 0; 320 44); layer = <CALayer: 0xba72fd0>>
| <UITableViewCellContentView: 0xba6f980; frame = (0 0; 320 44); gestureRecognizers = <NSArray: 0xba70960>; layer = <CALayer: 0xba6fb70>>
iOS 7中UITableViewCell的View层级
(lldb) po [cell recursiveDescription]
<UITableViewCell: 0xd984fc0; frame = (0 0; 320 44); layer = <CALayer: 0xd982750>>
| <UITableViewCellScrollView: 0xcca9790; frame = (0 0; 320 44); clipsToBounds = YES; autoresize = W+H; gestureRecognizers = <NSArray: 0xcc54ec0>; layer = <CALayer: 0xccac430>; contentOffset: {0, 0}>
| | <UITableViewCellContentView: 0xdb582a0; frame = (0 0; 320 44); gestureRecognizers = <NSArray: 0xdb49c40>; layer = <CALayer: 0xdb54050>>