NSObject:
UIResponder:
SKNode
UIApplication
UIView
UIViewController
UIResponder
给对象定义了响应和处理事件的接口。
它是UIApplication
, UIView
的父类,是UIWindow
的爷类,(~LOL)。
通常的两种事件:触摸事件(touch events
)和运动事件(motion events
)。
触摸事件的方法:
touchesBegan:withEvent:
touchesMoved:withEvent:
touchesEnded:withEvent:
touchesCancelled:withEvent:
这些方法的参数与触摸事件紧密相连,例如刚开始触摸或者触摸发生改变,因此对象可以跟踪和处理这些事件。任何时间点,当你触摸屏幕,滑动屏幕,或者从屏幕挪开手指,都会生成一个UIEvent
。这个事件对象包含了你对屏幕所做的任何事情,定义在UITouch
类中。
运动事件的方法:
iOS 3.0:
motionBegan:withEvent:
motionEnded:withEvent:
motionCancelled:withEvent:
canPerformAction:withSender:
iOS 4.0:
remoteControlReceivedWithEvent:
在iOS 3.0苹果提供了对运动事件的处理方法,例如摇晃设备。
第一响应者(First responder)指的是当前接受触摸的响应者对象(通常是一个 UIView 对象),即表示当前该对象正在与用户交互,它是响应者链的开端。响应者链和事件分发的使命都是找出第一响应者。
iOS 系统检测到手指触摸 (Touch) 操作时会将其打包成一个 UIEvent 对象,并放入当前活动 Application 的事件队列,单例的 UIApplication 会从事件队列中取出触摸事件并传递给单例的 UIWindow 来处理,UIWindow 对象首先会使用 hitTest:withEvent:
方法寻找此次 Touch 操作初始点所在的视图(View),即需要将触摸事件传递给其处理的视图,这个过程称之为 hit-test view。
hitTest:withEvent:
方法的处理流程如下:
hitTest:withEvent:
返回 nil,若返回 YES, 则向当前视图的所有子视图 (subviews) 发送 hitTest:withEvent:
消息,所有子视图的遍历顺序是从最顶层视图一直到到最底层视图,即从 subviews 数组的末尾向前遍历,直到有子视图返回非空对象或者全部子视图遍历完毕;示例性代码:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
UIView *touchView = self;
if ([self pointInside:point withEvent:event] &&
(!self.hidden) &&
self.userInteractionEnabled &&
(self.alpha >= 0.01f)) {
for (UIView *subView in self.subviews) {
[subview convertPoint:point fromView:self];
UIView *subTouchView = [subView hitTest:subPoint withEvent:event];
if (subTouchView) {
touchView = subTouchView;
break;
}
}
}else{
touchView = nil;
}
return touchView;
}
说明:
hitTest:withEvent:
方法将会忽略隐藏 (hidden=YES) 的视图,禁止用户操作 (userInteractionEnabled=NO)
的视图,以及 alpha 级别小于 0.01(alpha<0.01)的视图。如果一个子视图的区域超过父视图的 bound 区域(父视图的 clipsToBounds
属性为 NO,这样超过父视图 bound 区域的子视图内容也会显示),那么正常情况下对子视图在父视图之外区域的触摸操作不会被识别, 因为父视图的 pointInside:withEvent:
方法会返回 NO, 这样就不会继续向下遍历子视图了。当然,也可以重写 pointInside:withEvent:
方法来处理这种情况。hitTest:withEvent:
来达到某些特定的目的。- nextResponder
- isFirstResponder
- canBecomeFirstResponder
- becomeFirstResponder
- canResignFirstResponder
- resignFirstResponder
inputView (Property)
inputViewController (Property)
inputAccessoryView (Property)
inputAccessoryViewController (Property)
- reloadInputViews
- touchesBegan:withEvent:
- touchesMoved:withEvent:
- touchesEnded:withEvent:
- touchesCancelled:withEvent:
- touchesEstimatedPropertiesUpdated:
- motionBegan:withEvent:
- motionEnded:withEvent:
- motionCancelled:withEvent:
- pressesBegan:withEvent:
- pressesCancelled:withEvent:
- pressesChanged:withEvent:
- pressesEnded:withEvent:
- remoteControlReceivedWithEvent:
程序运行期间UIApplication
完成集中控制和协调的工作。
每个app都必须有且仅有一个UIApplication
的实例。
当app开始运行时,系统会调用UIApplicationMain
方法来启动,它会创建一个UIApplication
的单例。
UIApplication 的一个主要工作是处理用户事件,它会把所有用户事件都放入一个队列中,逐个处理;在处理的时候,它会将事件发送到一个合适的目标控件。
此外,UIApplication 实例还维护一个在本应用中打开的 window 列表(UIWindow 实例),这样它就可以接触应用中的任何一个 UIView 对象。
UIApplication 实例会被赋予一个代理对象,以处理应用程序的生命周期事件(比如程序启动和关闭)、系统事件(比如来电、记事项警告)等等。
状态 | 说明 |
---|---|
Not running | 程序没有启动 |
Inactive | 前台运行,但无事件响应 |
Active | 前台运行,且有事件响应 |
Background | 后台运行,可以处理一些事件 |
Suspended | 在后台不能处理事件,内存不够app就会被清除 |
(void)applicationWillResignActive:(UIApplication *)application
当应用程序将要入非活动状态执行,在此期间,应用程序不接收消息或事件,比如来电话了
(void)applicationDidBecomeActive:(UIApplication *)application
当应用程序入活动状态执行,这个刚好跟上面那个方法相反
(void)applicationDidEnterBackground:(UIApplication *)application
当程序被推送到后台的时候调用。所以要设置后台继续运行,则在这个函数里面设置即可
(void)applicationWillEnterForeground:(UIApplication *)application
当程序从后台将要重新回到前台时候调用,这个刚好跟上面的那个方法相反。
(void)applicationWillTerminate:(UIApplication *)application
当程序将要退出是被调用,通常是用来保存数据和一些退出前的清理工作。这个需要设置 UIApplicationExitsOnSuspend 的键值。
(void)applicationDidReceiveMemoryWarning:(UIApplication *)application
iPhone 设备只有有限的内存,如果为应用程序分配了太多内存操作系统会终止应用程序的运行,在终止前会执行这个方法,通常可以在这里进行内存清理工作防止程序被终止.
(void)applicationSignificantTimeChange:(UIApplication*)application
当系统时间发生改变时执行
(void)applicationDidFinishLaunching:(UIApplication*)application
当程序载入后执行
App生命周期示意图:
UIView 在屏幕上定义了一个矩形区域,并且为如何管理此矩形区域创造了接口。 它在iOS App中占有绝对重要的地位,因为iOS中几乎所有可视化控件都是由UIView或其子类构成。
UIView 的任务:
UIView 是按需绘制的,当整个视图或者视图的一部分由于布局变化,变成可变的,系统会要求视图进行绘制。对于那些需要使用UIKit或者CoreGraphics进行自定义绘制的视图,系统会调用drawRect:
方法进行绘制。
当视图内容发生变化,需要调用setNeedsDisplay
或者setNeedsDisplayInRect:
方法,告诉系统该重新绘制这个视图。调用此方法之后,系统会在下一个绘制周期更新这个视图的内容。由于系统要等到下一个绘制周期才真正进行绘制,可以一次性对多个视图调用setNeedsDisplay
,它们会同时被更新。
视图有 frame, center, bounds 等几个基本几何属性,其中:
(0, 0, width, height)
, bounds 的含义可以认为是当前view被允许绘制的范围。视图在初次绘制完成后,系统会对绘制结果进行快照,之后尽可能的使用快照,避免重新绘制。如果视图的几何属性发生变化,系统会根据视图的contentMode来决定如何改变显示效果。
默认的contentMode是UIViewContenModeScaleToFill
,系统会拉伸当前的快照,使其符合新的frame尺寸。大部分contentMode都会对当前的快照进行拉伸或者移动等操作。如果需要重新绘制,可以把contentMode设置为UIVeiwContentModeRedraw
,强制视图在改变大小之类的操作时调用drawRect:
重绘。
可以以动画的形式改变视图的下面这些属性,只需要告诉系统动画开始和结束时的数值,系统会自动处理中间的过渡程度。
除了提供视图本身内容之外,一个视图也可以表现的想一个容器。当一个视图包含其他视图时,两个视图之间就创建了一个父子关系。在这个关系中子视图被称作subView,父视图被称作superView。一个视图可以包含多个子视图,它们被存放在这个视图的subViews数组里。添加,删除,以及调动操作这些子视图的函数如下:
addSubview:
insertSubview:...
bringSubviewToFront:
sendSubviewToBack:
exchangeSubviewAtIndex: withSubviewAtIndex:
removeFromSuperView
当一个视图的大小改变时,它的子视图的位置和大小也需要相应地改变。UIView支持自动布局,也可以手动对子视图进行布局。
当下列这些事件发生时,需要进行布局操作:
setNeedsLayout
或layoutIfNeeded
方法setNeedsLayout
方法视图的autoresizesSubviews
属性决定了在视图大小发生变化时,如何自动调节子视图。
掩码如下:
可以通过位运算符组合他们,例如:UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth
Constraint 时另一种用于自动布局的方法,本质上Constraint就是对UIView之间两个属性之间的一个约束:
attribute1 == multiplier * attribute2 + constant
其中方程两边不一定时等于,也可以时大于等于之类的关系。
Constraint比AutoResizing更加灵活和强大,可以实现复杂的子视图布局。
UIView 中提供了一个layoutSubviews
函数,UIView的子类可以重载这个函数,以实现更加复杂和精细的子View布局。
苹果文档专门强调了,应该只在上面提到的 Autoresizing 和 Constraint 机制不能实现所需要的效果时,才使用 layoutSubviews。而且,layoutSubviews 方法只能被系统触发调用,程序员不能手动直接调用该方法。
UIView时UIResponder的子类,可以响应触控事件。
通常可以使用addGestureRecongnizer:
添加手势识别器来响应触控事件,如果需要手动处理,则按需要重载下面的函数:
touchesBegan: withEvent:
touchesMoved: withEvent:
touchesEnded: withEvent:
touchesCancelled: withEvent:
UIViewController(视图控制器)是 iOS Apps 管理视图的基本工具。它是MVC设计模式中的C部分。UIViewController在UIKit中主要功能是用于控制画面的切换,其中的view
属性(UIView类型)管理整个画面的外观。
UIViewController生命周期的第一步是初始化。不过具体调用的方法在SB和纯代码中还有所不同,这里只讲代码。代码中我们可以使用init:
函数进行初始化,init:
函数在实现过程中还会调用initWithNibName:bundle:
。我们应该尽量避免在VC外部调用initWithNibName:bundle:
,而是把它放在VC的内部。原因如下:
苹果官方要求
假如我们这样:
FCViewController *fcVC = [[FCViewController alloc] initWithNibName:@"Myview" bundle:nil];
那么我可以告诉你,这是不规范的,违反了类的封装原则。
我们应该这样写:
FCViewController *fcVC = [[FCViewController alloc] init];
在 FCViewController
里:
- (id)init
{
self = [super initWithNibName:@"Myview" bundle:nil];
if (self != nil)
{
// Further initialization if needed
}
return self;
}
- (id)initWithNibName:(NSString *)nibName bundle:(NSBundle *)bundle
{
NSAssert(NO, @"Initialize with -init");
return nil;
}
这样就会将主权交还给对象,防止破坏封装原则。如果你修改了NIB的名字,就不需要在每个实例的初始化地方修改,只需要在一个VC里修改就可以了。
初始化完成后,VC的生命周期会经过下面几个函数:
(void)loadView
(void)viewDidLoad
(void)viewWillAppear
(void)viewWillLayoutSubviews
(void)viewDidLayoutSubviews
(void)viewDidAppear
(void)viewWillDisappear
(void)viewDidDisappear
假设现在有一个 AViewController(简称 Avc) 和 BViewController (简称 Bvc),通过 navigationController 的 push 实现 Avc 到 Bvc 的跳转,下面是各个方法的执行执行顺序:
1. A viewDidLoad
2. A viewWillAppear
3. A viewDidAppear
4. B viewDidLoad
5. A viewWillDisappear
6. B viewWillAppear
7. A viewDidDisappear
8. B viewDidAppear
如果再从 Bvc 跳回 Avc,会产生下面的执行顺序:
1. B viewWillDisappear
2. A viewWillAppear
3. B viewDidDisappear
4. A viewDidAppear
可见 viewDidLoad 只会调用一次,再第二次跳回 Avc 的时候,AViewController 仍然存在于内存中,也就不需要 load 了。
iOS 中的多线程,是 Cocoa 框架下的多线程,通过 Cocoa 的封装,可以让我们更为方便的进行多线程编程。
Cocoa 中封装了 NSThread, NSOperation, GCD 三种多线程编程方式。
NSThread 是一个控制线程执行的对象,通过它我们可以方便的得到一个线程并控制它。NSThread 的线程之间的并发控制,是需要我们自己来控制的,可以通过 NSCondition 实现。它的缺点是需要自己维护线程的生命周期和线程的同步和互斥等,优点是轻量,灵活。
NSOperation 是一个抽象类,它封装了线程的细节实现,不需要自己管理线程的生命周期和线程的同步和互斥等。只是需要关注自己的业务逻辑处理,需要和 NSOperationQueue 一起使用。使用 NSOperation 时,你可以很方便的设置线程之间的依赖关系。这在略微复杂的业务需求中尤为重要。
GCD(Grand Central Dispatch) 是 Apple 开发的一个多核编程的解决方法。在 iOS4.0 开始之后才能使用。GCD 是一个可以替代 NSThread 的很高效和强大的技术。当实现简单的需求时,GCD 是一个不错的选择。
而 NSThread 大多被淘汰了,苹果推荐使用 GCD 和 NSOperation。
理解 iOS 开发中 GCD 相关的同步(synchronization)\ 异步(asynchronization),串行(serial)\ 并行(concurrency)概念
关于死锁问题,可以看我简书的一篇。
NSOperation 和 NSOperationQueue 并不一定要一起使用。 NSOperation 本身是可以单独使用的,不过单独使用的话并不能体现出 NSOperation 的强大之处(从下面的部分你就能看出单独用 NSOperation 真的是做不了什么事情),通常还是使用 NSOperationQueue 来执行 NSOperation。
NSOperation 是一个抽象类,我们需要继承它并且实现我们的子类。
这是面试中经常会问到的一点,这两个都很常用,也都很强大。对比它们可以从下面几个角度来说:
underlyingQueue
来把 operation 放到已有的 dispatch queue 中。苹果为了安全,将每个应用以及数据放到了沙盒(Sandbox)里,应用只能访问自己沙盒目录下的内容和网络资源等。当然也可以被授权访问系统的相机、照片和通讯录等。
MyApp.app 包含了应用程序本身的数据,程序打包时的文件,可执行文件,plist等。
Documents 这个目录用来存放关键数据。
Library
用来存放一些配置文件和其他一些文件。
其中使用NSUserDefaults
写的数据会保存到Library/Preferences
下的一个plist中。
tmp 存放临时文件。
NSHomeDirectory() // 沙盒目录
[[NSBundle mainBundle] bundlePath] // MyApp.app
NSTemporaryDirectory() // tmp
NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0] // Documents
NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES)[0] // Library
单例模式在开发中经常使用。 一个简单的单利模式示例代码:
/* UserInfo.h */
#import <Foundation/Foundation.h>
#define kSharedUserInfo [UserInfo sharedUserInfo]
@interface UserInfo : NSObject
@property (copy, nonatomic) NSString *userName;
@property (copy, nonatomic) NSString *userPassword;
+ (UserInfo *)sharedUserInfo;
- (void)logIn;
- (void)logOff;
- (void)saveUserName;
- (void)saveUserPassword;
@end
/* UserInfo.m */
#import "UserInfo.h"
@implementation UserInfo
static UserInfo *sharedUserInfoInstance = nil;
- (id)init {
if (self = [super init]) {
_userName = @"fcName";
_userPassword = @"fcPassword";
}
return self;
}
+ (UserInfo *)sharedUserInfo {
@synchronized(self) {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedUserInfoInstance = [[UserInfo alloc] init];
});
}
return sharedUserInfoInstance;
}
+ (id)allocWithZone:(NSZone *)zone {
@synchronized(self) {
if(sharedUserInfoInstance == nil){
sharedUserInfoInstance = [super allocWithZone:zone];
return sharedUserInfoInstance;
}
}
return nil;
}
- (id)copyWithZone:(NSZone *)zone {
return self;
}
- (void)logIn {
NSLog(@"%s",__func__);
}
- (void)logOff {
NSLog(@"%s",__func__);
}
- (void)saveUserName {
NSLog(@"%s",__func__);
}
- (void)saveUserPassword {
NSLog(@"%s",__func__);
}
@end
/* 使用 */
#import ...
#import "UserInfo.h"
...
- (void)foo {
[kSharedUserInfo logIn];
}
经常需要编写“只需执行一次的线程安全代码”。通过GCD所提供的dispatch_once
函数,很容易实现此功能。
工厂模式可以简化类的初始化过程。
/* FCEmployee.h */
#import <Foundation/Foundation.h>
typedef NS_ENUM(NSUInteger, FCEmployeeType) {
FCEmployeeTypeDeveloper,
FCEmployeeTypeDesigner,
FCEmployeeTypeFinance,
};
@interface FCEmployee : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic) NSUInteger salary;
+ (FCEmployee *)employeeWithType:(FCEmployeeType)type;
- (void)doADaysWork;
@end
/* FCEmployee.m */
#import "FCEmployee.h"
#import "FCEmployeeDeveloper.h"
@implementation FCEmployee
+ (FCEmployee *)employeeWithType:(FCEmployeeType)type {
switch (type) {
case FCEmployeeTypeDeveloper:
return [[FCEmployeeDeveloper alloc] init];
break;
case FCEmployeeTypeDesigner:
return [[FCEmployeeDeveloper alloc] init];
break;
case FCEmployeeTypeFinance:
return [[FCEmployeeDeveloper alloc] init];
break;
}
}
- (void)doADaysWork {
// in it's subclass
}
@end
/* FCEmployeeDeveloper.h */
#import "FCEmployee.h"
@interface FCEmployeeDeveloper : FCEmployee
@end
/* FCEmployeeDeveloper.m */
#import "FCEmployeeDeveloper.h"
@implementation FCEmployeeDeveloper
- (void)doADaysWork{
// 子类其工作的实现细节
[self writeCode];
}
- (void)writeCode{
NSLog(@"writeCode");
}
@end
/* main.m */
...
main() {
FCEmployee *developer = [FCEmployee employeeWithType:FCEmployeeTypeDeveloper];
NSLog(@"%@",[developer class]);
}
委托模式可以实现对象间的通信。
优点:数据与业务逻辑解耦。
示例代码:
@protocol PrintDelegate <NSObject>
- (void)print;
@end
@interface AClass : NSObject<PrintDelegate>
@property id<PrintDelegate> delegate;
@end
@implementation AClass
-(void)sayHello {
[self.delegate print];
}
-(void)print {
NSLog(@"Do Print");
}
@end
// 使用 AClass
int main(int argc, const char * argv[]) {
@autoreleasepool {
AClass * a = [AClass new];
a.delegate = a;
[a sayHello];
}
return 0;
}
Cocoa 中提供了两种用于实现观察者模式的办法,一直是使用NSNotification
,另一种是KVO
(Key Value Observing)。
KVO的实现依赖于 Objective-C 本身强大的 KVC(Key Value Coding) 特性,可以实现对于某个属性变化的动态监测。
示例代码:
// Book类
@interface Book : NSObject
@property NSString *name;
@property CGFloat price;
@end
// AClass类
@class Book;
@interface AClass : NSObject
@property (strong) Book *book;
@end
@implementation AClass
- (id)init:(Book *)theBook {
if(self = [super init]){
self.book = theBook;
[self.book addObserver:self forKeyPath:@"price" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];
}
return self;
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context{
if([keyPath isEqual:@"price"]){
NSLog(@"------price is changed------");
NSLog(@"old price is %@",[change objectForKey:@"old"]);
NSLog(@"new price is %@",[change objectForKey:@"new"]);
}
}
- (void)dealloc{
[self.book removeObserver:self forKeyPath:@"price"];
}
@end
// 使用 KVO
int main(int argc, const char * argv[]) {
@autoreleasepool {
Book *aBook = [Book new];
aBook.price = 10.9;
AClass * a = [[AClass alloc] init:aBook];
aBook.price = 11; // 输出 price is changed
}
return 0;
}
KVO进阶请看这里
NSNotification
基于 Cocoa 自己的消息中心组件 NSNotificationCenter
实现。
观察者需要统一在消息中心注册,说明自己要观察哪些值的变化。观察者通过类似下面的函数来进行注册:
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(printName:)
name: @"messageName"
object:nil];
上面的函数表明把自身注册成 “messageName” 消息的观察者,当有消息时,会调用自己的 printName 方法。
消息发送者使用类似下面的函数发送消息:
[[NSNotificationCenter defaultCenter] postNotificationName:@"messageName"
object:nil
userInfo:nil];
当然最后别忘在dealloc
中移除
[[NSNotificationCenter defaultCenter] removeObserver:self];