《ios编程第四版》随感,随想
oc的对象可以看作一个指挥中心,控制着内存的各个部分。如同一张网一样。采用的是链表的结构。
对象会直接保存非指针类型的实例变量。
向super发送消息,其实是向self发送消息,但是要求系统在查找方法时跳过当前对象的类,从父类开始查询。
在初始化方法中,应直接访问实例变量。而在其他方法中,要使用存取方法。
任何一个类,无论有多少初始化方法,,都必须选定其中一个作为指定初始化方法。指定初始化方法要确保对象的每一个实例变量都处在一个有效的状态。
串联使用初始化方法的机制可以减少错误,也更容易维护代码。在创建类时,需要先确定指定初始化方法,然后旨在指定初始化方法中编写初始化的核心代码,其他初始化方法只需要调用指定初始化方法(直接或间接)并传入默认值即可。
初始化方法总结
- 类会继承父类所有的初始化方法,也可以为类加入任意数量的初始化方法。
- 每个类都要选定一个指定初始化方法。
- 在执行其他初始化工作之前,必须先用指定初始化方法调用父类的指定初始化方法。(直接或间接)
- 其他初始化方法要调用指定初始化方法(直接或间接)。
- 如果某个类的指定初始化方法与其父类的不同,就必须覆盖父类的指定初始化方法并调用新的指定初始化方法(直接或间接)。
头文件的声明顺序 实例变量声明应该写在最前面,然后是类方法,接下来是初始化方法,最后是其他方法。
如果某个类方法的返回类型是这个类的对象,就可以将这种类方法称为便捷方法(convenience method)。
任何属性都有三个特性:
- 多线程特性(nonatomic,atomic),默认是atomic;
- 读/写特性(readwrite, readonly),默认是readwrite;
- 内存管理特性(strong,weak,copy,unsafe_unretained),默认是strong,非对象属性默认值是unsafe_unretained;
通常情况下,当某个属性是指向其他对象的指针,而且该对象的类有可修改的子类(如NSString/NSMutableString)时,应该将属性的内存管理特性设置为copy。原因如下:
- 如果属性指向的对象的类有可修改的子类,那么该属性可能会指向该子类。该子类对象有可能在拥有者不知情的情况下被修改。因此,最好先复制该对象。
- 向不可变对象发送copy时,会返回一个指向自己的指针。不会造成内存空间浪费。
当为声明的属性重写了存方法或取方法时,编译器便不会自动生成该方法和实例变量。
视图控制器
视图控制器通过两种方式创建视图层次结构:
- 代码方式:覆盖UIViewController中的loadView
- Nib文件方式:使用Interface Builder创建一个Xib文件,然后加入所需的视图层次结构,最后视图控制器会在运行时加载由该xib文件编译而成的nib文件
将插座变量声明为弱引用是一种编程约定。当系统的可用内存偏少时,视图控制器会自动释放其视图并在之后需要显示时在创建。以便在释放view时同时释放view的所有子视图,从而避免内存泄漏。
指向XIB文件中的顶层对象的插座变量必须声明为强引用,相反,当插座变量指向顶层对象所拥有的对象(例如顶层对象的子视图)时,应该使用弱引用。
视图的延迟加载 为了实现视图延迟加载,在initWithNibName:bundle:
中不应该访问view货view的任何子视图。凡是和view或view的子视图相关的初始化代码,都应该在viewDidLoad
方法中实现,避免加载不需要在屏幕上显示的视图。
访问视图 应该在以下两个方法中访问xib文件中创建的视图:
viewDidLoad viewWillAppear
区别是: 如果你只需在应用启动后设置一次视图对象,就选择viewDidLoad
。 而如果用户每次看到的视图控制器的view时都需要对其进行设置,则选择viewWillAppear
。
与控制器及其视图进行交互
application:didFinishLaunchingWithOptions:
只在应用启动完毕后调用一次。initWithNibName:bundle:
该方法是UIViewController的指定初始化方法,创建视图控制器时,就会调用该方法。loadView:
可以覆盖该方法,使用代码方式设置视图控制器的view属性。viewDidLoad
可以覆盖该方法,设置使用NIB文件创建的视图对象,该方法会在视图控制器加载完毕后被调用。viewWillAppear: viewDidAppear: viewWillDisappear: 和viewDidDisappear:
这些方法会被调用很多次。
UITableViewController
MVC
- Model(模型):负责存储数据,与用户界面无关。
- View (视图):负责显示界面,与模型对象无关。
- Controller(控制器):负责确保视图对象和模型对象的数据保持一致。
@class
当某个文件只需要使用某个类的声明,无需知道具体实现细节时,可以使用该@class
指令。 使用该指令的好处: 当某个类的头文件发生变化时,对那些通过@class
指令声明该类的其他文件,编译器可以不用重新编译,这样就可以节省编译时间。
而在另一些文件中,程序会向声明的类或对象发送消息时,就必须导入该类的头文件,是编译器知道所有的实现细节。
重用UITableViewCell
如果针对每条记录创建相应的UITableViewCell对象,就会很快耗尽ios设备的内存资源。 为了解决该问题,需要重用UITableViewCell对象。该机制是: 滚动时,部分UITableViewCell对象会移出窗口,UITableView将移出窗口的cell对象放入UITableViewCell对象池,等待重用。
按照约定,应该将UITableViewCell或UITableViewCell子类
的类名用作reuseIdentifier
。
视图的责任是将模型对象中的数据呈现给用户,只更新视图而不更新模型对象就会发生数据不一致的错误。
相机
NSDictionary
NSDictionary
非常有用,其中最常见的用法是可变数据结构和查询表。
可变数据结构: 为了在代码中描述一个模型对象,常见的做法是创建一个NSObject
的子类,然后添加模型对象的相关属性。**使用NSDictionary与NSObject子类的区别是,**NSObject的子类要求事先明确定义好该类的各项属性,并且之后无法修改,添加和删除。而使用NSDictionary就可以。
查询表: 当需要编写包含大连if-else或switch语句的代码时,通常应该考虑替换为NSDictionary。例如:
- (void)changeCharacterClass:(id)sender {
NSString *enteredText = textField.text;
CharaterClass *cc = nil;
if ([enteredText isEqualToString:@"Warrior"]) {
cc = knignt;
} else if ([enteredText isEqualToString:@"Mage"]) {
cc = wizard;
}
character.characterClass = cc;
}
复制代码
以上代码应替换为:
NSMutableDictionary *lookup = [[NSMutableDictionary alloc] init];
[lookup setObject:knight forKey:@"Warrior"];
[lookup setObject:wizard forKey:@"Mage"];
- (void)changeCharacterClass:(id)sender {
character.characterClass = [lookup objectForKey:textField.text];
}
复制代码
处理触摸事件并创建线条对象
使用valueWithNonretainedObject:方法将UITouch对象的内存地址封装为NSValue对象。使用内存地址分辨UITouch对象的原因是,在触摸事件开始,移动,结束的整个过程中,其内存地址是不会改变的,内存地址相同的UITouch对象一定是同一个对象。
为什么UITouch对象自身不能用做NSMutableDictionary的键?这是由于NSDictionary及其子类NSMutableDictionary的键必须遵守NSCopying协议--键必须可以复制(可以响应copy消息)。UITouch对象并不遵守NSCopying协议,因为每个触摸事件都是唯一的,不应该被复制。
响应对象链:
[图片上传失败...(image-4919fc-1509616086734)]
如果没有为某个UIResponder对象覆盖特定的事件处理方法,那么该对象的nextResponder会尝试处理相应的触摸事件。最终传递给UIApplication,如果UIApplication也无法对其进行处理,系统就会丢弃该事件。
自动布局入门
- 如果视图都是视图控制器的一级子视图,那么遍历一次就可以找出全部视图。
- (void)viewDidLayoutSubviews {
for (UIView *subView in self.view.subviews) {
if (subView hasAmbiguousLayout) {
NSLog(@"%@", subView);
}
}
}
复制代码
- 如果这些视图中又包含复杂的视图层次结构,就应该使用另一种方法:
在viewWillAppear
中加断点,然后在控制台输入:
po [[UIWindow keyWindow] _autolayoutTrace]
如果应用界面布局跟期望不一致,同时也无法确定原因,可以使用该方法。
视图控制器需要针对iPhone和iPad显示完全不同的界面,通过创建两个独立的XIB文件来实现
针对iPhone和iPad的XIB文件需要在类名后加上对应的后缀:
CZViewController~iphone.xib
CZViewController~ipad.xib
复制代码
[图片上传失败...(image-c2d487-1509616086734)]
[图片上传失败...(image-d4d689-1509616086734)]
#在代码中使用自动布局
通常,若要为UIViewController
创建视图,如果是创建整个视图的层次结构及所有视图约束,就覆盖loadView
方法;如果只是通过NIB文件创建的视图层次结构中添加一个视图或约束,就覆盖viewDidLoad
Visual Format Language (视觉化格式语言)
根据Apple的命名规范,应该使用属性的名称或实例变量的名称作为视图对象的键。视觉化格式字符串将使用字典中的键表示相应的视图对象。
XIB文件中创建并添加约束只需一步就可以完成,但是在代码中,创建和添加约束需要分为两个不同的步骤。
NSDictionary *nameMap = @{@"imageView" : self.imageView,
@"valueField" : self.valueField,
@"toolBar" : self.toolBar
};
NSArray *horizontalConstraints = [NSLayoutConstraint constraintsWithVisualFormat:@"H:|-0-[imageView]-0-|"
options:0
metrics:nil
views:nameMap];
NSArray *verticalConstraints = [NSLayoutConstraint constraintsWithVisualFormat:@"V:[valueField]-[imageView]-[toolBar]"
options:0
metrics:nil
views:nameMap];
[self.view addConstraints:horizontalConstraints];
[self.view addConstraints:verticalConstraints];
复制代码
###如何判断约束应该添加到哪个视图中,以下是判定法则:
- 如果约束 同时对 多个父视图相同的 视图起作用,那么约束应该添加到它们的父视图中。(例如下图中的约束A)
- 如果约束 同时对 多个父视图不同的 视图起作用,但是这些视图在层次结构中有共同的祖先视图,那么约束应该添加到它们最近以及的祖先视图中。(例如下图中的约束C)
- 如果约束只对某个视图自身起作用,那么约束应该添加到该视图中。(例如下图中的约束B)
- 如果约束同时对某个视图及其父视图起作用,那么约束应该添加到其父视图中。
[图片上传失败...(image-8181a6-1509616086734)]
(Intrinsic Content Size)固有内容大小
固有内容大小的含义:视图要显示的实际内容区域大小。
自动布局系统会根据固有内容大小为视图添加相应的约束,与其他约束不同,这类约束有两个优先级属性,分别是内容放大优先级(content hugging priority)和内容缩小优先级(content compression resistance priority)。
content hugging priority: 表示视图固有内容大小的放大优先级,如果该优先级为1000,表示不允许自动布局系统基于固有内容放大视图尺寸,相反,如果该优先级小于1000,自动布局系统会在必要时放大视图的尺寸。 content compression resistance priority:表示视图固有内容大小的缩小优先级,如果该优先级为1000,表示不允许自动布局系统基于固有内容大小缩小视图尺寸;相反,如果该优先级小于1000,自动布局系统会在必要时缩小视图的尺寸。
视图与控制器之间的关系
父子关系
当使用视图控制器容器时,就会产生拥有父子关系的视图控制器。通过viewControllers
来保存一组视图控制器。
视图控制器容器的特性:
- 容器对象会将viewControllers中的视图作为子视图加入自己的视图。
- 容器视图对象通常都会有自己的默认外观。
处在同一个父子关系下的视图控制器形成一个族系:
-
任何容器对象都可以通过viewControllers访问其子对象。
-
子对象可以通过UIViewController对象的四个特定属性来访问其容器对象:
- 先介绍四个特定属性的前三个:
navigationController,tabBarController,splitViewController
。当某个视图控制器收到这三个消息中的某一个,就会沿着族系向上查找,直到找到类型匹配的视图控制器。如果没找到,返回nil。 - 第四个属性:
parentViewController
,该属性会指向族系中“最近的”那个容器对象。
- 先介绍四个特定属性的前三个:
显示与被显示关系(presenting-presenter relationship)
当某个视图控制器以模态(modal)形式显示另一个视图控制器时,就会产生拥有这种关系的视图控制器。当某个视图控制器A以模态形式显示另一个视图控制器B时, B的视图会覆盖A的视图。这和之前介绍的父子关系不同,父子关系的子视图只会在容器对象的视图内显示。 当某个视图控制器A以模态形式显示另外一个视图控制器B时,A的presentedViewController
会指向B,而B的presentingViewController
属性会指向A。
[图片上传失败...(image-80af6a-1509616086734)]
在显示与被显示关系中,位于关系的两头的视图控制器不会处于同一个族系中。被显示的视图控制器会有自己的族系。
如上图所示,凡是针对父子关系的属性,其指向的对象都会在当前族系的范围内。因此,向族系2中的视图控制器发送UITabBarController对象,会返回nil。
不同族系中的视图控制器关系: 当应用以模态形式显示某个视图控制器时,负责显示该视图控制器的将是相关族系中的顶部视图控制器(以上图为例,族系2中的视图控制器的presentingViewController的属性指向的都是UITabBarController对象)。
通过编写代码,可以改变这种“顶部对象负责以模态形式显示其他视图控制器”的行为(仅限iPad)。这样就可以限定视图的显示位置。
为此,UIViewController
提供了definesPresentationContext
属性,其默认值是NO,当为NO时,会将“显示权”传递给父视图控制器,并沿族系依次向上传递,直到最顶层视图控制器。相反,如果在传递过程中,如果某个视图控制器的definesPresentationContext是YES,该视图控制器就不会再将“显示权”传递给父视图控制器,而是由自己负责新的视图控制器。此外,对于这种情况,必须将需要显示的视图控制器的modalPresentationStyle
属性设置为UIModalPresentationCurrentContext
。
保存,读取与应用状态
应用沙盒
应用沙盒会包含以下多个目录:
- 应用程序包(application bundle):包含应用可执行文件,例如NIB文件。
- Documents/ :存放应用运行时生成的并且需要保留的数据。同步设备时会备份该目录。
- Library/Caches/ :存放应用运行时生成的需要保留的数据。不会同步。
- Library/Caches/ :存放所有的偏好设置。使用
NSUserDefaults
类,可以通过Library/Perferences目录中的某个特定文件以键值对的形式保存数据。同步设备时会备份该目录。 - tmp/ :存放应用运行时所需的临时数据。不会同步。可通过
NSTemporaryDirectory
函数得到tmp/目录的全路径。
应用状态与切换
[图片上传失败...(image-314295-1509616086734)]
处于未运行状态的应用,不会执行任何代码,也不会占用RAM。 当应用处于激活状态时:
- 被某个系统事件打断,临时进入未激活状态。这类事件包括收到短信,收到推送,来电等。
- 按下顶部锁定按钮,切换至未激活状态,并且保留未激活状态直到设备解锁。
- 按下home键,或进入多任务界面,或通过某种途径切换至另一个应用时,状态切换至未激活状态,停留极短的时间,然后进入后台运行状态。默认情况下,进入后台状态的应用大约有10秒的时间,然后进入挂起状态。
应用的各种状态:
状态 | 界面是否可见 | 是否能接收事件 | 是否能执行代码 |
---|---|---|---|
未运行状态 | 否 | 否 | 否 |
激活状态 | 是 | 是 | 是 |
未激活状态 | 大部分 | 否 | 是 |
后台运行状态 | 否 | 否 | 是 |
挂起状态 | 否 | 否 | 否 |
模型
标准的模型-视图-控制器设计模式要求控制对象负责模型对象的保存和读取。但这样做的效果并不是很好。控制对象主要的任务是处理模型对象和视图对象之间的交互,如果还要负责实现所有的存取细节,则可能会不堪重负。为此,将模型对象的存取逻辑移入另一类对象:存储对象。
保存和读取模型对象的实现细节全部由存储对象负责。存储对象通过以下方式来创建和保存模型对象:
- 通过指定文件夹来创建和保存。
- 通过数据库
- 通过Web服务
- 其他
这种设计模式为:模型-视图-控制器-存储(Model-View-Controller-Store)。
这种设计模式的好处:
- 简化控制器类
- 不用修改控制器对象或应用的其他部分,就能修改存储对象的工作方式。因此,无论应用有多少个需要存取数据的控制对象,都只需要修改相应的存储对象即可。
NSException和NSError
NSException和NSError的使用场景不同。
NSException 如果需要指出程序员的编码错误,则应该使用NSException
。例如:一个方法只能接受奇数作为参数,但是程序员在调用该方法时传入了偶数,这时应该抛出异常,以方便程序员解决代码错误。
NSError 对于预期错误,如用户错误和设备环境错误,应该使用NSError
。例如:一个方法需要读取用户照片,但是没有访问用户相册的权限,这时应该向方法调用者返回一个NSError
对象,指出不能执行本次操作的原因。
文档系统的读取和写入
和NSString
类似,NSDictionary
和NSArray
也有writeToFile:
和initWithContentsOfFile:
。只有当容器对象包含可序列化(property list serializable)对象时,才能通过writeToFile:
这类方法将数据写入。
可序列化对象包括: NSString, NSNumber, NSData, NSArray, NSDictionary
。NSArray和NSDictionary
这类对象的文件写入方法生成的都是XML格式的property list
文件。
创建UITableViewCell子类
block对象应被声明为copy
将block对象声明为copy的原因是: 系统对block对象和其他对象的内存管理方式不同,block对象是在栈中创建的,而其他对象是在堆中创建的。这意味着,即使应用针对新创建的block对象保留了强引用类型的指针,一旦创建该对象的方法返回,新创建的block对象也会被立即释放。为了在声明block对象的方法返回后仍然保留该对象,必须向其发送copy
消息。拷贝某个block对象时,应用会在堆中创建该对象的备份。
Web 服务
NSURL NSURLRequest NSURLSessionTask NSURLSession
NSURL
对象负责以URL的格式保存Web应用的位置。对大多数Web服务,URL将包含基地址,Web应用名和需要传送的参数。NSURLRequest
对象负责保存需要传送给Web服务器的全部数据。这些数据包括:一个NSURL对象,缓存方案,等待Web服务器响应的最长时间和需要通过http协议传送的额外信息(NSMutableURLRequest是NSURLRequest的子类
)。- 每一个
NSURLSessionTask
都表示一个NSURLRequest
的生命周期,NSURLSessionTask
可以跟踪NSURLRequest
的状态,还可以对NSURLRequest
执行取消,暂停,继续操作。NSURLSessionTask
有多种不同功能的子类,包括NSURLSessionDataTask,NSURLSessionUploadTask,NSURLSessionDownloadTask
。 NSURLSession
对象可以看作是一个生产NSURLSessionTask
对象的工厂。可以设置其生产出的NSURLSessionTask
对象的通用属性。例如请求头的内容,是否允许在蜂窝网下发送请求等。NSURLSession
还有一个功能强大的委托,可以跟踪NSURLSessionTask
对象的状态,处理服务器的认证要求等。
服务器端根据功能将Web服务分配到不同的路径,然后要求客户端以路径作为参数请求不同的Web服务。
URL字符串必须是URL安全的(URL-safe)。例如,在URL中不允许出现空格字符和双引号,必须使用转义序列(escape-sequence)来替换这些字符: NSString *search = @"Play some \"Abba\""; NSString *escaped = [search stirngByAddingPercentEscapsUsingEncoding:NSUTF8StringEncoding]; // 转义后的字符串是“Play%20some%20%22Abba%22”
解除转义:stringByRemovingPercentEncoding
##解析JSON数据 Apple提供了专门用于解析JSON数据的类:NSJSONSerialization
.可以讲JSON数据中的对象转换为对应的OC对象。
默认情况下,NSURLSessionDataTask
是在后台线程中执行completionHandler
的。 [图片上传失败...(image-b5deb4-1509616086734)]
HTTP请求的主体是传送给服务器的数据,这些数据通常是XML格式,JSON格式或Base-64编码后的数据。如果某个请求包含主体,就必须包含Content-Length头。NSURLRequest会计算主体的大小并自动添加Content-Length头。
Core Data
固化机制: 使用固化机制的最大缺点是数据必须“整存整取”,要访问固化文件中的任何数据,必须先解固整个文件;要保存数据的任何改动,必须重写整个文件。
Core Data: Core Data没有这样的缺点。Core Data可以只读取已存对象中的一小部分。如果取出的对象发生了变化,也只要更新相应的部件。如果某个应用要在文件系统和RAM之间传送大量模型对象,那么Core Data的这种增量读取,更新,删除和插入的特性可以大幅提高性能。
Core Data可以存储transformable类型,CoreData会在存储或恢复transformable类型的实体属性时首先将其转换为NSData,然后再存入文件系统或恢复为OC对象。为了向Core Data描述转换过程,需要创建NSValueTransformer的子类。
Core Data 框架中的NSManagedObjectContext
负责应用和数据库之间的交互工作。通过NSManagedObjectContext
对象所使用的NSPersistentStoreCoordinator
对象,可以指定文件路径并打开相应的SQLite数据库。NSPersistentStoreCoordinator
对象需要配合某个模型文件才能工作(NSManagedObjectModel对象可以代表模型文件)。
启用状态恢复
如果应用启用了状态恢复,系统在终止应用之前会遍历应用树中的每一个节点,记录每个节点的状态信息,例如,对象的唯一标识,类和需要保存的状态数据。在终止应用之后,系统会将这些信息存储到文件系统中。
其中对象的唯一标识又称为对象的恢复标识(restoration identifier)。通常与对象的类名相同;类称为恢复类(restoration class),通常与该对象的isa指针指向的类相同;状态数据则保存了对象的状态信息,例如,UITabBarController
的状态数据包括当前选中的是哪一个标签项。
当应用重新启动后,系统会读取之前保存的状态信息,重新创建应用树,依次恢复树中的每一个节点:
- 系统通过节点的恢复类为该节点创建一个新的视图控制器。
- 将一组恢复标识赋给新节点:包括该节点的恢复标识及其所有祖先节点的恢复标识。数组中第一个标识是根节点的恢复标识,最后一个标识是新节点的恢复标识。
- 将对应的状态数据赋给新节点。状态数据保存在NSCoder对象中。