这篇文章主要内容是整理了部分iOS开发基础知识和收集了一些iOS开发中的小技巧。由于平时自己没有养成做笔记的习惯,现在想做整理的时候,才发现是一件多么坑爹的事情啊😭。其实自己做iOS开发也很久了,遇到的问题也很多,但是,就是不喜欢做笔记(这个习惯一定得改💪),所以现在趁着闲的时候好好把以前遇到的问题好好整理下。喜欢的童鞋可以持续关注下哦,有空的时候会更新的。(写完才发现简书不支持markdown
页面内跳转,目录无法成功跳转😭😭)
目录
Foundation
<a id="f1"></a>1. iOS多线程
在说多线程之前我们必须先弄懂两个概念:进程
和线程
进程
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。
简单来说,进程是指在系统中正在运行的一个应用程序,每一个程序都是一个进程,并且进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内。
线程
线程是程序执行流的最小单元线程是程序中一个单一的顺序控制流程。是进程内一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU的基本单位指运行中的程序的调度单位。
1个进程要想执行任务必须得有线程。线程中任务的执行是串行的,一个线程中的任务只能一个一个地按顺序执行,也就是说在同一时间内,1个线程只能执行1个任务。
线程和进程的区别在于,子进程和父进程有不同的代码和数据空间,而多个线程则共享数据空间,每个线程有自己的执行堆栈和程序计数器为其执行上下文.多线程主要是为了节约CPU时间,发挥利用,根据具体情况而定. 线程的运行中需要使用计算机的内存资源和CPU。
多线程
多线程(英语:multithreading),是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。
所谓多线程,就是在单个程序中同时运行多个线程完成不同的工作
注意,多线程是为了同步完成多项任务,不是为了提高运行效率,而是为了提高资源使用效率来提高系统的效率。线程是在同一时间需要完成多项任务的时候实现的。
GCD
在说GCD之前我们得先弄懂4个比较容易混淆的术语:同步
、异步
、 并发
、 串行
同步和异步主要影响:能不能开启新的线程
-
同步
:只是在当前线程中执行任务,不具备开启新线程的能力 -
异步
:可以在新的线程中执行任务,具备开启新线程的能力
并行和串行主要影响:任务的执行方式 -
并发
:多个任务并发(同时)执行 -
串行
:一个任务执行完毕后,再执行下一个任务
GCD是最常用的管理并行代码和执行异步操作的Unix系统层的API。GCD构造和管理队列中的任务。首先,让我们看看队列是什么。
队列是什么?
队列是按先进先出(FIFO)
管理对象的数据结构。队列类似电影院的售票窗口,票的销售是谁先到谁先服务。在等待线前面的人先去买他们的门票,在其余的后抵达的人之前。
调度队列
调度队列是一种简单的同步和异步任务的方法。任务以block
的形式被提交到其中。系统有两种调度队列:串行队列
和并行队列
。任务分配给这两个队列都是在单独的线程执行的,而不是在创建任务的线程上。换句话说,你创建任务(block)再提交到主线程的调度队列,但所有这些任务任务将运行在单独的线程而不是主线程。
串行队列
当你创建一个串行队列,队列一次只能执行一个任务。同一队列中的任务将按着顺序依次执行,然而它们并不关心任务是不是在单独的线程,所以你可以通过使用多个串行队列来并行地执行任务。例如,你可以创建两个串行队列,每个队列一次只执行一个任务,不过多达两个任务仍可并行执行。
使用串行队列的优点:
- 保证序列化访问共享资源,避免竞争条件。
- 任务的执行顺序是可预测的。当你提交任务到一个串行调度队列,它们将按插入的顺序执行。
- 你可以创建任意数量的串行队列。
并行队列
并行队列可以并行执行多个任务。任务按添加到队列的顺序开始,但它们的执行会同时发生,不会相互等待。并行队列保证任务开始的顺序,但你不知道执行的顺序。
使用队列
默认情况下,系统为每个应用提供了一个串行队列和四个并行队列。主调度队列是全局可用的串行队列,它在应用的主线程执行任务,主要用来更新UI,同时只有一个任务执行。
除了主队列,系统提供了4个并行队列,称之为全局调度队列。这些队列对于应用是全局的,区别只在于它们的优先级。使用dispatch_get_global_queue
可以获取到一个全局队列,它有以下四个优先级:
DISPATCH_QUEUE_PRIORITY_HIGH
DISPATCH_QUEUE_PRIORITY_DEFAULT
DISPATCH_QUEUE_PRIORITY_LOW
DISPATCH_QUEUE_PRIORITY_BACKGROUND
以上优先级由高到低,所有你可以根据任务的优先级决定你使用的队列。不过,你也可以创建任意数量的串行或并行队列。
任务
即操作,你想要干什么,说白了就是一段代码,在 GCD 中就是一个 Block
,所以添加任务十分方便。任务有两种执行方式: 同步执行
和 异步执行
,他们之间的区别是在于会不会阻塞当前线程,直到 Block
中的任务执行完毕!
下面举几个栗子:
func GCD1() {
print("task 1");
dispatch_sync(dispatch_get_main_queue()) { //会阻塞当前线程,task 2不会执行
print("task 2")
}
}
运行会发现控制台打印出task 1
,因为主线程被阻塞了,task2不会执行。
func GCD2() {
print("task 1");
let queue = dispatch_queue_create("come.jewelez.serial", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue) {
print("task 2 \(NSThread.currentThread())")
dispatch_sync(queue, { () -> Void in //会阻塞当前线程,task 3不会执行
print("task 3 \(NSThread.currentThread())")
})
print("task 4 \(NSThread.currentThread())")
}
print("task 5")
}
在这个例子中,你会发现控制台只会打印task1, task5, task2
, 而task3
和task4
不会执行。首先我们创建了一个串行队列,然后以异步的方式提交了任务,所以task2
可以执行,在任务中又以同步的方式向队列中提交了一个新的任务,由于是同步方式所以会阻塞当前线程,task3
不会执行,因为是串行队列,当前线程又线程阻塞了,所以task4
也不会执行。
func GCD3() {
print("task 1")
let queue = dispatch_queue_create("come.jewelez.serial", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue) {
print("task 2 \(NSThread.currentThread())")
dispatch_sync(dispatch_get_main_queue(), { () -> Void in //异步遇到同步回主线程, task 3执行完后才会执行task 4
print("task 3 \(NSThread.currentThread())")
})
print("task 4 \(NSThread.currentThread())")
}
print("task 5 \(NSThread.currentThread())")
}
这个就很容易理解了,不过要注意的一点是异步遇到同步回主线程, task 3
执行完后才会执行task 4
,控制台打印如下:
task 1
task 2 <NSThread: 0x7fe290e05d30>{number = 2, name = (null)}
task 5 <NSThread: 0x7fe290c045b0>{number = 1, name = main}
task 3 <NSThread: 0x7fe290c045b0>{number = 1, name = main}
task 4 <NSThread: 0x7fe290e05d30>{number = 2, name = (null)}
来看最后一个例子:
func GCD4() {
print("task 1")
let queue = dispatch_queue_create("come.jewelez.serial", DISPATCH_QUEUE_SERIAL)
dispatch_async(queue) {
print("task 2 \(NSThread.currentThread())")
dispatch_async(dispatch_get_main_queue(), { () -> Void in
for i in 0..<1000 {
print("i: \(i)")
}
print("task 3 \(NSThread.currentThread())")
})
print("task 4 \(NSThread.currentThread())")
}
print("task 5 \(NSThread.currentThread())")
}
这个例子与上个不同在于,主线程中的任务也是以异步的方式执行的,所以task 4
不用等到task 3
执行完才执行。
NSOperationQueue
NSOperationQueue
有两种不同类型的队列:主队列和自定义队列。主队列运行在主线程之上,而自定义队列在后台执行。在两种类型中,这些队列所处理的任务都使用 NSOperation 的子类来表述。
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue]; //主队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init]; //自定义队列
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
//任务执行
}];
[queue addOperation:operation];
我们可以通过设置 maxConcurrentOperationCount 属性来控制并发任务的数量,当设置为 1 时, 那么它就是一个串行队列。主对列默认是串行队列,这一点和 dispatch_queue_t 是相似的。
NSOperation
你可以使用系统提供的一些现成的NSOperation
的子类, 如 NSBlockOperation
、 NSInvocationOperation
。你也可以实现自己的子类, 通过重写 main
或者 start
方法 来定义自己的 operations 。
使用 main
方法非常简单,开发者不需要管理一些状态属性(例如 isExecuting
和 isFinished
),当 main
方法返回的时候,这个 operation 就结束了。这种方式使用起来非常简单,但是灵活性相对重写 start
来说要少一些, 因为main
方法执行完就认为operation结束了,所以一般可以用来执行同步任务。
如果你希望拥有更多的控制权,或者想在一个操作中可以执行异步任务,那么就重写 start
方法, 但是注意:这种情况下,你必须手动管理操作的状态, 只有当发送 isFinished
的 KVO 消息时,才认为是 operation 结束.
@implementation YourOperation
- (void)start
{
self.isExecuting = YES;
// 任务代码 ...
}
- (void)finish //异步回调
{
self.isExecuting = NO;
self.isFinished = YES;
}
@end
当实现了start
方法时,默认会执行start
方法,而不执行main
方法
为了让操作队列能够捕获到操作的改变,需要将状态的属性以配合 KVO 的方式进行实现。如果你不使用它们默认的 setter
来进行设置的话,你就需要在合适的时候发送合适的 KVO 消息。
需要手动管理的状态有:
-
isExecuting
代表任务正在执行中 -
isFinished
代表任务已经执行完成 -
isCancelled
代表任务已经取消执行
手动的发送 KVO 消息, 通知状态更改如下 :
[self willChangeValueForKey:@"isCancelled"];
_isCancelled = YES;
[self didChangeValueForKey:@"isCancelled"];
为了能使用操作队列所提供的取消功能,你需要在长时间操作中时不时地检查 isCancelled
属性
UI
<a id="ui1"></a>1. AutoLayout
如果你的项目中的所有界面都是使用xib
或storyboard
的话,那么你使用AutoLayout布局的话就很简单了,所以我这里并不打算展开来讲,免得浪费大家的时间,因为确实是很简单,唯一麻烦一点的是如何做scrollView
的布局,这个我在后面会讲到。废话不多说,咱先来看看如何用代码来做自动布局吧。
我们可以通过下面这个API来创建一个约束
+(instancetype)constraintWithItem:(id)view1 attribute:(NSLayoutAttribute)attr1 relatedBy:(NSLayoutRelation)relation toItem:(nullable id)view2 attribute:(NSLayoutAttribute)attr2 multiplier:(CGFloat)multiplier constant:(CGFloat)c;
- 第一个参数
view1
是要设置约束的视图 - 第二个参数
attr1
是view1
要设置的属性,例如左边、右边、顶部、底部啥的,你按住command
键自己点进去看,都看的懂。 - 第三个参数
relation
是view1
和view2
的指定属性之间的关系,是一个枚举值,就三个值,<=
,=
,>=
, 自己点进去看看😯 - 第四个参数
view2
是要参照的视图 - 第五个参数
attr2
是参照视图的属性,意义同参数2 - 第六个参数
multiplier
是view1
的指定属性是参照view2
指定属性的多少倍 - 第七个参数
c
是view1
的指定属性需要加的浮点数
说了这么多,总结为一句话:
view1.attr1 [= , >= , <=] view2.attr2 * multiplier + c;
理解了这一句话那么你的自动布局就差不多都搞定了。
- (void)viewDidLoad {
[super viewDidLoad];
UIView *redView = [UIView new];
redView.translatesAutoresizingMaskIntoConstraints = NO; //很重要,一定不能少
redView.backgroundColor = [UIColor redColor];
_redVew = redView;
[self.view addSubview:redView];
[self addConstraint];
UIView *blueView = [UIView new];
blueView.translatesAutoresizingMaskIntoConstraints = NO;
blueView.backgroundColor = [UIColor blueColor];
[self.view addSubview:blueView];
}
- (void)addConstraint {
NSLayoutConstraint *leading = [NSLayoutConstraint constraintWithItem:_redVew attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeLeading multiplier:1 constant:20];
[self.view addConstraint:leading];
NSLayoutConstraint *tariling = [NSLayoutConstraint constraintWithItem:_redVew attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTrailing multiplier:1 constant:-20];
[self.view addConstraint:tariling];
NSLayoutConstraint *heigh = [NSLayoutConstraint constraintWithItem:_redVew attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeHeight multiplier:0.2 constant:0];
[self.view addConstraint:heigh];
}
在自己手动添加约束时,有两个地方需要注意:
- 给自己添加约束的view设置
translatesAutoresizingMaskIntoConstraints = NO
- 创建的约束一定要添加在设置约束的视图的父视图上
Virtual Format Language
这是苹果推出的一种在代码中使用Autolayout的语法,使用特定的string来创建Constraint,如@"H:[button]-[textField]"
代表button
和textField
水平间隔标准间距(默认为8).
看栗子:
NSArray *constraint1 = [NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[blueView]-|"
options:0
metrics:nil
views:NSDictionaryOfVariableBindings(blueView)];
NSArray *constraint2 = [NSLayoutConstraint constraintsWithVisualFormat:@"V:[redView]-20-[blueView(==200)]"
options:0
metrics:nil
views:NSDictionaryOfVariableBindings(blueView, redView)];
[self.view addConstraints:constraint1];
[self.view addConstraints:constraint2];
在constraint1中@"H:|-[blueView]-
表示buleView在水平方向距离父视图两边距离为20(默认为20,可以自己设置),constraint2中@"V:[redView]-20-[blueView(==200)]"
表示在垂直方向上blueView与redView的间隔为20,同时指定blueView的高度为200.
constraint2也可以这样写,效果是一样的
NSArray *constraints3 = [NSLayoutConstraint constraintsWithVisualFormat:@"V:[redView]-[blueView(==height)]"
options:0
metrics:@{@"height":@200}
views:NSDictionaryOfVariableBindings(blueView, redView)];
如果要实现多个视图等宽等高呢,你可以这样写:
NSArray *constraints4 = [NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[blueView(redView)]"
options:0
metrics:nil
views:NSDictionaryOfVariableBindings(blueView,redView)];
NSArray *constraints5 = [NSLayoutConstraint constraintsWithVisualFormat:@"V:[redView]-[blueView(redView)]"
options:0
metrics:nil
views:NSDictionaryOfVariableBindings(blueView,redView)];
如果你有需要,你的高度值(或者其他同类型的)可以使用>=
,==
,<=
来表示,甚至你可以组合来用,像上面的200,你可以指定一个区别,比如: (>=200,<=400)
,这同样也是可以的。如果你想表达他的优先级别,可以使用@"V: |-20-[blueView(==200@1000)]"
,这个 @1000
,就是他的级别了。
Masonry
大名鼎鼎,不用我多说了吧,传送门
Content Compression Resistance Priofity & Content Hugging Priority
Content Compression Resistance Priorit:也叫内容压缩阻力优先级(小名:别挤我),该优先级越高,则越晚轮到被压缩。
Content Hugging Priority:也叫内容紧靠优先级(小名:别扯我),该优先级越高,这越晚轮到被拉伸。
因此,在父view大小不够布局子label时,我们可以通过增加某个label的Content Compression Resistance Priority(内容压缩阻力优先级)来防止被压缩。当然降低自身则可以让自己被压缩。
同理,在父view大小太大时,我们可以通过增加label的Content Hugging Priority(内容紧靠优先级)来防止被拉伸。降低则可以达到被拉伸的目的。
在UIView的API中也可以看到这几个方法:
- (UILayoutPriority)contentHuggingPriorityForAxis:(UILayoutConstraintAxis)axis NS_AVAILABLE_IOS(6_0);
- (void)setContentHuggingPriority:(UILayoutPriority)priority forAxis:(UILayoutConstraintAxis)axis NS_AVAILABLE_IOS(6_0);
- (UILayoutPriority)contentCompressionResistancePriorityForAxis:(UILayoutConstraintAxis)axis NS_AVAILABLE_IOS(6_0);
- (void)setContentCompressionResistancePriority:(UILayoutPriority)priority forAxis:(UILayoutConstraintAxis)axis NS_AVAILABLE_IOS(6_0);
<a id="ui3"></a>2. 给UIScrollView设置约束
不管你是使用xib
、storyboard
还是用代码给UIScrollView设置约束都是一件蛋疼的事,不过当你看了下面的内容后,你会发现其实也不难。
我们一步一步来:
- 像添加普通控件一样,将
scrollView
添加到控制器的view
中,并设置上下左右的约束(我这儿设置的是0)。这个时候scrollView
的x
,y
坐标和宽高都有了(报错不要管)。 - 将要添加的控件添加到
scrollView
中,设置约束。这里我添加了三个view
, 前两个view
约束如图
第三个是这样的
有些童鞋可能会觉得奇怪,为什么最后一个view
明明已经设置了高度为400,怎么还要设置距离底部的距离为10(这个值随便哈)呢?其实我这么做的目的就是为了确定scrollView
的contentSize
,而我们以平常的方式给scrollView
设置约束行不通,就是因为无法确定scrollView
的contentSize
。通过我的设置,scrollView
的contentSize
的高度是:
(0+128) + (0+128) + (10+400+10) = 676
所以如果最后不给view3
添加距离scrollView
底部的约束,那么虽然所有的子控件都可以定位了,但是scrollView
的contentSize
的高度还是无法确定的,因为scrollView
的contentSize
是由它的子视图撑起来的,子视图的宽高无法确定那又怎么能确定scrollView
的contentSize
呢。所以咱们最后来确定scrollView
的contentSize
的宽度,也就是确定它子视图的宽度,这里我给view1
添加width = scrollView.widht
的约束,这样是可行的,因为在第一步中scrollView
的宽高就已经确定了。这时scrollView
的contentSize
的宽度就是:
0 + scrollView.width + 0 //scrollView.width是确定值,我这里就是屏幕宽度
现在再看看,那可恶的红色就没有了吧。在给scrollView
设置约束时记住一点,scrollView
的contentSize
的宽等于水平方向所有的边距+宽度,高等于垂直方向所有的边距+高度。在例子中的3个子view
的高度我是定死的,不过在实际开发工程中,它们的高度可以由其子视图撑起来。
<a id="ui3"></a>3. 设置UITableViewCell分割线
如果自定义Cell的话只需要设置 tableview
或者 cell的 separatorInset = UIEdgeInsetsZero
以及cell的layoutMargins = UIEdgeInsetsZero
这简单的两步就可以了!!!
如果使用系统的是UITableviewCell
类的话就再多加上preservesSuperviewLayoutMargins = NO
这句就可以了.
如果要去掉cell分割线,可以这样写:
cell.separatorInset = UIEdgeInsetsMake(0, screenW*0.5, 0, screenW*0.5)
<a id="ui4"></a> 4. 如何自动适应cell的高度
如果你的App是支持iOS8及以上版本的,那么你就根本没必要看了,不过你仍然可以从这里查看如何在iOS8下做自适应高度的cell。
我们可以通过systemLayoutSizeFittingSize:
方法来获取计算出来cell的size.
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
CGFloat cellHeight = [self cellHeightAtIndexPath:indexPath];
if (cellHeight > 0) {
return cellHeight;
} else {
NSString *identifier = [Cell1 identifier];
Cell1 *cell1 = self.offScreenCells[identifier];
if (!cell1) {
cell1 = [[[NSBundle mainBundle] loadNibNamed:@"Cell1" owner:nil options:nil] lastObject];
self.offScreenCells[identifier] = cell1;
}
cell1.textLab.text = self.data[indexPath.row];
[cell1 setNeedsUpdateConstraints];
[cell1 updateConstraintsIfNeeded];
CGFloat height = [cell1.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
//计算完后保存,避免多次重复计算*
[self saveCellHeight:height forIndexPath:indexPath];
return height + 1;
}
}
不过有几点需要注意:
- cell中的多行UILabel,如果其width不是固定的话(比如屏幕尺寸不同,width不同),要手动设置其preferredMaxLayoutWidth。 因为计算UILabel的intrinsicContentSize需要预先确定其width才行。
- 要保证垂直方向上的Constraints理论上可以计算出cell的高度,另:最好调低其中一个Constraint的优先级,避免约束发生冲突
- tableView的
dequeueReusableCellWithIdentifier
取出来的cell如果未用于tableView, 那么它就leak了. 因此用于计算高度的cell不要从此方法获取。
未完待续...
网友评论