Lyddwn

生活不只是代码


  • Home

  • Tags

  • Categories

  • Archives

  • Commonweal 404

iPadOS新功能

Posted on 2019-06-10 | In iPad

iPadOS

苹果基于iOS系统为iPad打造了全新的iPadOS系统。不仅增加了悬浮框功能,还让分屏显示功能更加便捷。极大提高了同屏浏览信息量。同时,新增了手势可以进行复制、粘贴、删除和撤销操作,提高了iPad生产力。

Widget

可以将Widget固定在HOME页面。只需要一个向右的拖拽手势。

多窗口

iPad在iOS11就支持了多窗口。可以同时使用两个App。

iPadOS将支持多窗口堆叠显示,比如统一应用可以开启两个窗口,然后并排显示。

这意味着你可以同时打开两个备忘录,或者同时打开两个Word窗口,页面与页面之间的拖拽功能也得到了优化。

第三方App也可以: Word

更多的切换手势功能

与Mac交互

搭配最新版MacOS Catelina,iPad可以变成Mac的外接屏幕和数位板。用户可以把电脑上打开的App直接拖拽到iPad上,并进行触控/笔触操作。

文件功能

原本支持的文件管理App在iPadOS上有了重大功能提升,现在加入分栏视图,支持相机和U盘导入文件。

新的手势

新增的强大的手势交互。可以直接使用三指捏合和松开的手势完成复制、粘贴,而三指滑动即可实现撤销操作。

选择

复制|粘贴

取消

Apple Pencil

更多的第三方App可以使用苹果PencilKit API,让笔触交互更加自然和敏捷。输入延迟也从20ms降低到9ms。在任何App环境下用笔从屏幕角落向中间滑动,可以开启截图笔记模式。

鼠标

Safari

Safari 桌面级的浏览器

支持下载管理

支持多达30个快捷键

字体

下载管理你喜爱的字体,在App中使用。

iOS Runtime

Posted on 2019-05-28 | In iOS

介绍

Objective-C 扩展了 C 语言,并加入了面向对象特性和 Smalltalk 式的消息传递机制。而这个扩展的核心是一个用 C 和 编译语言 写的 Runtime 库。它是 Objective-C 面向对象和动态机制的基石。

Objective-C 是一个动态语言,这意味着它不仅需要一个编译器,也需要一个运行时系统来动态得创建类和对象、进行消息传递和转发。理解 Objective-C 的 Runtime 机制可以帮我们更好的了解这个语言,适当的时候还能对语言进行扩展,从系统层面解决项目中的一些设计或技术问题。了解 Runtime ,要先了解它的核心 - 消息传递 (Messaging)。

Runtime有两个版本: “modern” 和 “legacy”。

我们现在用的 Objective-C 2.0采用的是现行 (Modern) 版的 Runtime系统,只能运行在 iOS和 macOS 10.5之后的 64位程序中。而 macOS较老的32位程序仍采用 Objective-C 1中的(早期)Legacy版本的 Runtime系统。这两个版本最大的区别在于当你更改一个类的实例变量的布局时,在早期版本中你需要重新编译它的子类,而现行版就不需要。

Runtime基本是用 C和汇编写的,可见苹果为了动态系统的高效而作出的努力。你可以在这里下到苹果维护的开源代码。苹果和GNU各自维护一个开源的 runtime版本,这两个版本之间都在努力的保持一致。

平时的业务中主要是使用官方Api,解决我们框架性的需求。

高级编程语言想要成为可执行文件需要先编译为汇编语言再汇编为机器语言,机器语言也是计算机能够识别的唯一语言,但是OC并不能直接编译为汇编语言,而是要先转写为纯C语言再进行编译和汇编的操作,从OC到C语言的过渡就是由runtime来实现的。然而我们使用OC进行面向对象开发,而C语言更多的是面向过程开发,这就需要将面向对象的类转变为面向过程的结构体。

消息传递

调用一个对象的方法像这样

1
[obj foo]

编译器转成消息发送

1
objc_msgSend(obj, foo)

Runtime时执行的流程是这样的:

  • 首先,通过obj的isa指针找到它的 class;
  • 在 class的 method list找 foo;
  • 如果 class中没到 foo,继续往它的 superclass中找 ;
  • 一旦找到 foo这个函数,就去执行它的实现IMP。

但这种实现有个问题,效率低。

因为一个class往往只有 20%的函数会被经常调用,可能占总调用次数的 80%。每个消息都需要遍历一次objc_method_list并不合理。

如果把经常被调用的函数缓存下来,那可以大大提高函数查询的效率。

这也就是objc_class中另一个重要成员objc_cache做的事情。

再找到foo之后,把foo的method_name作为key,method_imp作为value给存起来。当再次收到foo消息的时候,可以直接在cache里找到,避免去遍历objc_method_list。从前面的源代码可以看到objc_cache是存在objc_class结构体中的。

objec_msgSend的方法定义如下:

1
OBJC_EXPORT id objc_msgSend(id self, SEL op, ...)

那消息传递是怎么实现的呢?我们看看对象(object),类(class),方法(method)这几个的结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//对象
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
//类
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
//方法列表
struct objc_method_list {
struct objc_method_list *obsolete OBJC2_UNAVAILABLE;
int method_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_method method_list[1] OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;
//方法
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
}
  1. 系统首先找到消息的接收对象,然后通过对象的isa找到它的类。
  2. 在它的类中查找method_list,是否有selector方法。
  3. 没有则查找父类的method_list。
  4. 找到对应的method,执行它的IMP。
  5. 转发IMP的return值。

消息传递涉及到的一些概念:

  • 类对象(objc_class)
  • 实例(objc_object)
  • 元类(Meta Class)
  • Method(objc_method)
  • SEL(objc_selector)
  • IMP
  • 类缓存(objc_cache)
  • Category(objc_category)

类对象(objc_class)

Objective-C类是由Class类型来表示的,它实际上是一个指向objc_class结构体的指针。

1
typedef struct objc_class *Class;

查看objc/runtime.h中objc_class结构体的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;


#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif


} OBJC2_UNAVAILABLE;

struct objc_class结构体定义了很多变量,通过命名不难发现, 结构体里保存了指向父类的指针、类的名字、版本、实例大小、实例变量列表、方法列表、缓存、遵守的协议列表等, 一个类包含的信息也不就正是这些吗?没错,类对象就是一个结构体struct objc_class,这个结构体存放的数据称为元数据(metadata), 该结构体的第一个成员变量也是isa指针,这就说明了Class本身其实也是一个对象,因此我们称之为类对象,类对象在编译期产生用于创建实例对象,是单例。

实例(objc_object)

1
2
3
4
5
6
7
8
/// Represents an instance of a class.
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};


/// A pointer to an instance of a class.
typedef struct objc_object *id;

类对象中的元数据存储的都是如何创建一个实例的相关信息,那么类对象和类方法应该从哪里创建呢? 就是从isa指针指向的结构体创建,类对象的isa指针指向的我们称之为元类(metaclass), 元类中保存了创建类对象以及类方法所需的所有信息,因此整个结构应该如下图所示:

元类(Meta Class)

通过上图我们可以看出整个体系构成了一个自闭环,struct objc_object结构体实例它的isa指针指向类对象, 类对象的isa指针指向了元类,super_class指针指向了父类的类对象, 而元类的super_class指针指向了父类的元类,那元类的isa指针又指向了自己。

元类(Meta Class)是一个类对象的类。 在上面我们提到,所有的类自身也是一个对象,我们可以向这个对象发送消息(即调用类方法)。 为了调用类方法,这个类的isa指针必须指向一个包含这些类方法的一个objc_class结构体。这就引出了meta-class的概念,元类中保存了创建类对象以及类方法所需的所有信息。 任何NSObject继承体系下的meta-class都使用NSObject的meta-class作为自己的所属类,而基类的meta-class的isa指针是指向它自己。

Method(objc_method)

先看下定义

1
2
3
4
5
6
7
8
/// An opaque type that represents a method in a class definition.代表类定义中一个方法的不透明类型
typedef struct objc_method *Method;
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
...
}

Method和我们平时理解的函数是一致的,就是表示能够独立完成一个功能的一段代码,比如:

1
2
3
4
- (void)logName
{
NSLog(@"name");
}

这段代码,就是一个函数。

我们来看下objc_method这个结构体的内容:

  • SEL method_name 方法名
  • char *method_types 方法类型
  • IMP method_imp 方法实现

在这个结构体重,我们已经看到了SEL和IMP,说明SEL和IMP其实都是Method的属性。
我们接着来看SEL。

SEL(objc_selector)

先看下定义

1
2
3
Objc.h
/// An opaque type that represents a method selector.代表一个方法的不透明类型
typedef struct objc_selector *SEL;

objc_msgSend函数第二个参数类型为SEL,它是selector在Objective-C中的表示类型(Swift中是Selector类)。selector是方法选择器,可以理解为区分方法的 ID,而这个 ID的数据结构是SEL:

1
@property SEL selector;

可以看到selector是SEL的一个实例。

A method selector is a C string that has been registered (or “mapped“) with the Objective-C runtime. Selectors generated by the compiler are automatically mapped by the runtime when the class is loaded.

其实selector就是个映射到方法的C字符串,你可以用 Objective-C编译器命令@selector()或者 Runtime系统的sel_registerName函数来获得一个 SEL类型的方法选择器。

selector既然是一个string,我觉得应该是类似className+method的组合,命名规则有两条:

  • 同一个类,selector不能重复
  • 不同的类,selector可以重复

这也带来了一个弊端,我们在写C代码的时候,经常会用到函数重载,就是函数名相同,参数不同,但是这在Objective-C中是行不通的,因为selector只记了method的name,没有参数,所以没法区分不同的method。

比如:

1
2
- (void)caculate(NSInteger)num;
- (void)caculate(CGFloat)num;

是会报错的。

我们只能通过命名来区别:

1
2
- (void)caculateWithInt(NSInteger)num;
- (void)caculateWithFloat(CGFloat)num;

在不同类中相同名字的方法所对应的方法选择器是相同的,即使方法名字相同而变量类型不同也会导致它们具有相同的方法选择器。

IMP

看下IMP的定义

1
2
3
/// A pointer to the function of a method implementation.  指向一个方法实现的指针
typedef id (*IMP)(id, SEL, ...);
#endif

就是指向最终实现程序的内存地址的指针。

在iOS的Runtime中,Method通过selector和IMP两个属性,实现了快速查询方法及实现,相对提高了性能,又保持了灵活性。

类缓存(objc_cache)

当Objective-C运行时通过跟踪它的isa指针检查对象时,它可以找到一个实现许多方法的对象。然而,你可能只调用它们的一小部分,并且每次查找时,搜索所有选择器的类分派表没有意义。所以类实现一个缓存,每当你搜索一个类分派表,并找到相应的选择器,它把它放入它的缓存。所以当objc_msgSend查找一个类的选择器,它首先搜索类缓存。这是基于这样的理论:如果你在类上调用一个消息,你可能以后再次调用该消息。

为了加速消息分发, 系统会对方法和对应的地址进行缓存,就放在上述的objc_cache,所以在实际运行中,大部分常用的方法都是会被缓存起来的,Runtime系统实际上非常快,接近直接执行内存地址的程序速度。

Category(objc_category)

Category是表示一个指向分类的结构体的指针,其定义如下:

1
2
3
4
5
6
7
8
struct category_t { 
const char *name;
classref_t cls;
struct method_list_t *instanceMethods;
struct method_list_t *classMethods;
struct protocol_list_t *protocols;
struct property_list_t *instanceProperties;
};
  • name:是指 class_name 而不是 category_name。
  • cls:要扩展的类对象,编译期间是不会定义的,而是在Runtime阶段通过name对 应到对应的类对象。
  • instanceMethods:category中所有给类添加的实例方法的列表。
  • classMethods:category中所有添加的类方法的列表。
  • protocols:category实现的所有协议的列表。
  • instanceProperties:表示Category里所有的properties,这就是我们可以通过objc_setAssociatedObject和objc_getAssociatedObject增加实例变量的原因,不过这个和一般的实例变量是不一样的。

从上面的category_t的结构体中可以看出,分类中可以添加实例方法,类方法,甚至可以实现协议,添加属性,不可以添加成员变量。

消息转发

前文介绍了进行一次发送消息会在相关的类对象中搜索方法列表,如果找不到则会沿着继承树向上一直搜索知道继承树根部(通常为NSObject),如果还是找不到并且消息转发都失败了就回执行doesNotRecognizeSelector:方法报unrecognized selector错。

那么消息转发到底是什么呢?接下来将会逐一介绍最后的三次机会。

  • 动态方法解析
  • 备用接收者
  • 完整消息转发

动态方法解析

首先,Objective-C运行时会调用 +resolveInstanceMethod:或者 +resolveClassMethod:,让你有机会提供一个函数实现。如果你添加了函数并返回YES, 那运行时系统就会重新启动一次消息发送的过程。

实现一个动态方法解析的例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
//执行foo函数
[self performSelector:@selector(foo:)];
}


+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(foo:)) {//如果是执行foo函数,就动态解析,指定新的IMP
class_addMethod([self class], sel, (IMP)fooMethod, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}


void fooMethod(id obj, SEL _cmd) {
NSLog(@"Doing foo");//新的foo函数
}
1
打印结果: Doing foo

可以看到虽然没有实现foo:这个函数,但是我们通过class_addMethod动态添加fooMethod函数,并执行fooMethod这个函数的IMP。

从打印结果看,成功实现了。

如果resolve方法返回 NO,运行时就会移到下一步:forwardingTargetForSelector。

备用接收者

如果目标对象实现了-forwardingTargetForSelector:,Runtime这时就会调用这个方法,给你把这个消息转发给其他对象的机会。

实现一个备用接收者的例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#import "ViewController.h"
#import "objc/runtime.h"


@interface Person: NSObject


@end


@implementation Person


- (void)foo {
NSLog(@"Doing foo");//Person的foo函数
}


@end


@interface ViewController ()


@end


@implementation ViewController


- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
//执行foo函数
[self performSelector:@selector(foo)];
}


+ (BOOL)resolveInstanceMethod:(SEL)sel {
return YES;//返回YES,进入下一步转发
}


- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(foo)) {
return [Person new];//返回Person对象,让Person对象接收这个消息
}

return [super forwardingTargetForSelector:aSelector];
}


@end
1
打印结果: Doing foo

可以看到我们通过forwardingTargetForSelector把当前ViewController的方法转发给了Person去执行了。打印结果也证明我们成功实现了转发。

完整消息转发

如果在上一步还不能处理未知消息,则唯一能做的就是启用完整的消息转发机制了。 首先它会发送-methodSignatureForSelector:消息获得函数的参数和返回值类型。如果-methodSignatureForSelector:返回nil,Runtime则会发出 -doesNotRecognizeSelector:消息,程序这时也就挂掉了。如果返回了一个函数签名,Runtime就会创建一个NSInvocation对象并发送 -forwardInvocation:消息给目标对象。

实现一个完整转发的例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#import "ViewController.h"
#import "objc/runtime.h"


@interface Person: NSObject


@end


@implementation Person


- (void)foo {
NSLog(@"Doing foo");//Person的foo函数
}


@end


@interface ViewController ()


@end


@implementation ViewController


- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
//执行foo函数
[self performSelector:@selector(foo)];
}


+ (BOOL)resolveInstanceMethod:(SEL)sel {
return YES;//返回YES,进入下一步转发
}


- (id)forwardingTargetForSelector:(SEL)aSelector {
return nil;//返回nil,进入下一步转发
}


- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if ([NSStringFromSelector(aSelector) isEqualToString:@"foo"]) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];//签名,进入forwardInvocation
}

return [super methodSignatureForSelector:aSelector];
}


- (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL sel = anInvocation.selector;


Person *p = [Person new];
if([p respondsToSelector:sel]) {
[anInvocation invokeWithTarget:p];
}
else {
[self doesNotRecognizeSelector:sel];
}


}


@end
1
打印结果:Doing foo

从打印结果来看,我们实现了完整的转发。通过签名,Runtime生成了一个对象anInvocation,发送给了forwardInvocation,我们在forwardInvocation方法里面让Person对象去执行了foo函数。签名参数v@:怎么解释呢,这里苹果文档Type Encodings有详细的解释。

应用

  • Method Swizzling
  • KVO
  • MJExtension
  • 模拟多继承

ios NSObject从创建到销毁

Posted on 2019-05-28 | In iOS

TODO

iOS NSObject对象模型

Posted on 2019-05-27 | In iOS

前言

我们编写的Objective-C代码,底层通过C/C++代码实现。

Objective-C -> C/C++ -> 汇编语言 -> 机器语言

那么一个Objective-C类、对象是如何通过C/C++实现、储存的呢?

源码探究

ISA

我们通过以下命令,将.m文件转化为C/C++代码。

1
clang -rewrite-objc **.m

可以获取 **.cpp文件。

在7700多行,可以获取如下结构体

1
2
3
struct NSObject_IMPL {
Class isa;
};

在objc.h头文件中,我们可以发现如下代码

1
2
3
4
5
6
7
8
#if !OBJC_TYPES_DEFINED
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};

通过注释我们得知:

  • 结构体objc_class表示一个Objective-C的类。
  • 结构体objc_object表示一个Objective-C Class的实例。

那么objc_class又是什么呢?

在runtime.h文件中,我们可以发现如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */

我们可以得知:在Objective-C中,每个对象都是一个结构体,都有一个isa指针,类对象Class也是一个对象。在运行时,可以通过isa指针查找该对象是什么类。


那么isa到底是什么?为什么需要isa呢?

我们可以通过objc4源码获取一些信息

objc4 的源码不能直接编译,需要配置相关环境才能运行。可以在这里下载可调式的源码。
objc 运行时源码的入口在 void _objc_init(void) 函数。

objc-private.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct objc_object {
private:
//isa是一个union联合体,包含这个对象所属类的信息
isa_t isa;

public:

// ISA() assumes this is NOT a tagged pointer object
Class ISA();

// getIsa() allows this to be a tagged pointer object
Class getIsa();
...
}

objc-runtime-new.h

1
2
3
4
5
6
7
8
9
10
11
12
13
struct objc_class : objc_object {
// Class ISA; (ISA继承于objc_object)
Class superclass; //当前类父类
cache_t cache; // formerly cache pointer and vtable 缓存指针和vtable,提高方法调用效率
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags 存储类的方法、属性、协议等信息

// 针对 class_data_bits_t 的 data() 函数的封装,最终返回一个 class_rw_t 类型的结构体变量
// Objective-C 类中的属性、方法还有遵循的协议等信息都保存在 class_rw_t 中
class_rw_t *data() {
return bits.data();
}
...
}

通过源码可以得知,objc_class继承于objc_object,所以二者都存在isa成员变量,类型为:isa_t。isa_t是一个union。

我们回到objc-private.h中

1
2
3
4
5
6
7
8
9
10
11
12
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
//所属类
Class cls;
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
#endif
};

isa_t包含一个成员变量 cls。

每个objc_object通过自己持有的isa查找到自己所属的类。而对于objc_class来说,可以通过isa找到自己的mate class,即元类。

对于IAS_BITFIELD,定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//__arm64__ 
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t shiftcls : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; \
uintptr_t deallocating : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 19
  • nonpointer

表示 isa_t 的类型,0 表示 raw isa,也就是没有结构体的部分,访问对象的 isa 会直接返回一个指向 cls 的指针,也就是在 iPhone 迁移到 64 位系统之前时 isa 的类型。1 表示当前 isa 不是指针,但是其中也有 cls 的信息,只是其中关于类的指针都是保存在 shiftcls 中。

  • has_assoc

对象含有或者曾经含有关联引用(category相关),没有关联引用的可以更快地释放内存(object dealloc相关,其他文章会说到)。

  • has_cxx_dtor

表示当前对象有 C++ 或者 ObjC 的析构器(destructor),如果没有析构器就会快速释放内存(object dealloc相关)。

  • shiftcls

见nonpointer

  • magic

用于调试器判断当前对象是真的对象还是没有初始化的空间

  • weakly_referenced

对象被指向或者曾经指向一个 ARC 的弱变量,没有弱引用的对象可以更快释放

  • deallocating

对象正在释放内存

  • has_sidetable_rc

对象的引用计数太大了,存不下

  • extra_rc

对象的引用计数超过 1,会存在这个这个里面,如果引用计数为 10,extra_rc 的值就为 9。


isa初始化

我们可以通过isa初始化方法,来了解这64位bits作用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
inline void 
objc_object::initIsa(Class cls)
{
initIsa(cls, false, false);
}

inline void
objc_object::initClassIsa(Class cls)
{
if (DisableNonpointerIsa || cls->instancesRequireRawIsa()) {
initIsa(cls, false/*not nonpointer*/, false);
} else {
initIsa(cls, true/*nonpointer*/, false);
}
}

inline void
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
assert(!cls->instancesRequireRawIsa());
assert(hasCxxDtor == cls->hasCxxDtor());

initIsa(cls, true, hasCxxDtor);
}

inline void
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)
{
assert(!isTaggedPointer());

if (!nonpointer) {
isa.cls = cls;
} else {
assert(!DisableNonpointerIsa);
assert(!cls->instancesRequireRawIsa());

isa_t newisa(0);

#if SUPPORT_INDEXED_ISA
assert(cls->classArrayIndex() > 0);
newisa.bits = ISA_INDEX_MAGIC_VALUE;
// isa.magic is part of ISA_MAGIC_VALUE
// isa.nonpointer is part of ISA_MAGIC_VALUE
newisa.has_cxx_dtor = hasCxxDtor;
newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
newisa.bits = ISA_MAGIC_VALUE;
// isa.magic is part of ISA_MAGIC_VALUE
// isa.nonpointer is part of ISA_MAGIC_VALUE
newisa.has_cxx_dtor = hasCxxDtor;
newisa.shiftcls = (uintptr_t)cls >> 3;
#endif

// This write must be performed in a single store in some cases
// (for example when realizing a class because other threads
// may simultaneously try to use the class).
// fixme use atomics here to guarantee single-store and to
// guarantee memory order w.r.t. the class index table
// ...but not too atomic because we don't want to hurt instantiation
isa = newisa;
}
}

当我们初始化一个Objective-C对象,为其分配内存时,调用栈中包含了 initInstanceIsa与initIsa这2个方法。

initInstanceIsa方法,传入的nonpointer为true。所以initIsa方法可以简化为(ARM_ARCH_7K应该是watch指令集,所以我们只看else部分):

1
2
3
4
5
6
7
8
inline void 
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) {
isa_t newisa(0);
newisa.bits = ISA_MAGIC_VALUE;
newisa.has_cxx_dtor = hasCxxDtor;
newisa.shiftcls = (uintptr_t)cls >> 3;
isa = newisa;
}

先将 newisa 的 bits 赋值为常量 ISA_MAGIC_VALUE

1
#define ISA_MAGIC_VALUE 0x000001a000000001ULL

里面包括了 magic 和 nonpointer 的值。然后将是否有 C++ 析构函数标示上,最后将位移(shift)后的 cls 存入 shiftcls。

1
newisa.shiftcls = (uintptr_t)cls >> 3;

将当前地址右移三位的主要原因是用于将 Class 指针中无用的后三位清除减小内存的消耗,因为类的指针要按照字节(8 bits)对齐内存,其指针后三位都是没有意义的 0。

绝大多数机器的架构都是 byte-addressable 的,但是对象的内存地址必须对齐到字节的倍数,这样可以提高代码运行的性能,在 iPhone5s 中虚拟地址为 33 位,所以用于对齐的最后三位比特为 000,我们只会用其中的 30 位来表示对象的地址。

最后将 isa = newisa,工作就结束了。

方法、属性、协议

在Objective-C中,对象的方法储存在类中,而非实例对象中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint32_t version;

const class_ro_t *ro;

method_array_t methods;
property_array_t properties;
protocol_array_t protocols;

Class firstSubclass;
Class nextSiblingClass;

char *demangledName;

#if SUPPORT_INDEXED_ISA
uint32_t index;
#endif

void setFlags(uint32_t set)
{
OSAtomicOr32Barrier(set, &flags);
}

void clearFlags(uint32_t clear)
{
OSAtomicXor32Barrier(clear, &flags);
}

// set and clear must not overlap
void changeFlags(uint32_t set, uint32_t clear)
{
assert((set & clear) == 0);

uint32_t oldf, newf;
do {
oldf = flags;
newf = (oldf | set) & ~clear;
} while (!OSAtomicCompareAndSwap32Barrier(oldf, newf, (volatile int32_t *)&flags));
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 用于存储一个 Objective-C 类在编译期就已经确定的属性、方法以及遵循的协议
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif

const uint8_t * ivarLayout;

const char * name;
method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;

const uint8_t * weakIvarLayout;
property_list_t *baseProperties;

method_list_t *baseMethods() const {
return baseMethodList;
}
};

当一个对象的实例方法被调用时,需要先通过持有的isa指针查找相应的类,然后在类的class_data_bits_t 结构体中查找对应方法的实现(每个对象可以通过 cls->data()-> methods 来访问所属类的方法)。同时,每一个 objc_class 也有一个指向自己的父类的指针 super_class 用来查找继承的方法。

而当一个类的类方法被调用时,通过类的isa在元类中获取方法的实现。如上图。

结论

isa用于查找对象(或类对象)所属类(或元类)的信息,比如方法列表、属性、协议等。

ivar_t

protocol_t

method_t

字符集和字符编码

Posted on 2019-05-24 | In Coding

前言

对于计算机来说,它无法处理文本,只能与0,1数字打交道。为了可以在计算机中用数字表示文本,我们指定了一个从字符到数字的映射。这个映射叫做编码(encoding)。反之,我们将计算机中数字解析显示为字符,成为解码(decoding)。

字符集(Charset)

是一个系统支持的所有抽象字符的集合。字符是各种文字和符号的总称,包括各国家文字、标点符号、图形符号、数字等。

*

字符编码(Character Encoding)

是一套法则,将符号集合转换为计算机可以理解的数字。


字符集

  • 常见的字符集

ASCII
GB2312
GB18030
BIG5
Unicode

ASCII

ASCII(American Standard Code for Information Interchange,美国信息交换标准代码)是基于拉丁字母的一套电脑编码系统。它主要用于显示现代英语,而其扩展版本EASCII则可以可以部分支持其他西欧语言,并等同于国际标准ISO/IEC 646。

ASCII 码是 7 位的,它将英文字母,数字 0-9 以及一些标点符号和控制字符映射为 0-127 这些整型。

ASCII的最大缺点是只能显示26个基本拉丁字母、阿拉伯数目字和英式标点符号,因此只能用于显示现代美国英语(而且在处理英语当中的外来词如naïve、café、élite等等时,所有重音符号都不得不去掉,即使这样做会违反拼写规则)。而EASCII虽然解决了部份西欧语言的显示问题,但对更多其他语言依然无能为力。因此现在的苹果电脑已经抛弃ASCII而转用Unicode。

GB2312

GB 2312 或 GB 2312–80 是中华人民共和国国家标准简体中文字符集,全称《信息交换用汉字编码字符集·基本集》,通常簡稱GB,又稱GB0,由中国国家标准总局发布,1981年5月1日实施。GB 2312编码通行于中国大陆;新加坡等地也采用此编码。中国大陆几乎所有的中文系统和国际化的软件都支持GB 2312。

GB 2312标准共收录6763个汉字,其中一级汉字3755个,二级汉字3008个;同时收录了包括拉丁字母、希腊字母、日文平假名及片假名字母、俄语西里尔字母在内的682个字符。

GB 2312的出现,基本满足了汉字的计算机处理需要,它所收录的汉字已经覆盖中国大陆99.75%的使用频率。但对于人名、古汉语等方面出现的罕用字和繁體字,GB 2312不能处理,因此后来GBK及GB 18030汉字字符集相继出现以解決這些問題。

GB18030

GB 18030,全称《信息技术 中文编码字符集》,是中华人民共和国国家标准所规定的变长多字节字符集。其对GB 2312-1980完全向后兼容,与GBK基本向后兼容,并支持Unicode(GB 13000)的所有码位。GB 18030共收录汉字70,244个。

  • GB 18030主要有以下特点

采用变长多字节编码,每个字可以由1个、2个或4个字节组成。
编码空间庞大,最多可定义161万个字元。
完全支持Unicode,无需动用造字区即可支持中国国內少数民族文字、中日韩和繁体汉字以及emoji等字符。

BIG5

Big5,又称为大五码或五大码,是使用繁体中文(正体中文)社区中最常用的电脑汉字字符集标准,共收录13,060个汉字。

中文码分为内码及交换码两类,Big5属中文内码,知名的中文交换码有CCCII、CNS11643。Big5虽普及于台湾、香港与澳门等繁体中文通行区,但长期以来并非当地的国家标准,而只是业界标准。倚天中文系统、Windows等主要系统的字符集都是以Big5为基准,但厂商又各自增加不同的造字与造字区,派生成多种不同版本。

2003年,Big5被收录到CNS11643中文标准交换码的附录当中,取得了较正式的地位。这个最新版本被称为Big5-2003。

不同国家、不同语言在网络上交流时,如果都实现类似GB2312/GB18030/BIG5的编码方案,各自出一套方案,相互不兼容,就非常容易出现乱码。

Unicode

为了解决这个问题,Unicode(统一码、万国码、单一码、标准万国码)应运而生。Unicode为世界上几乎所有的书写系统里所使用的每一个字符或符号定义了一个唯一的数字,这个数字叫做码点(code points)。它使用4字节的数字来表达每个字母、符号,或者表意文字(ideograph)。

最初,Unicode 编码是被设计为 16 位的,提供了 65,536 个字符的空间。当时人们认为这已经大到足够编码世界上现代文本里所有的文字和字符了。

后来,考虑到要编码历史上的文字以及一些很少使用的日本汉字和中国汉字,Unicode 编码扩展到了 21 位(从 U+0000 到 U+10FFFF)。Unicode 不是 16 位的编码!它是 21 位的。这 21 位提供了 1,114,112 个码点,其中,只有大概 10% 正在使用,所以还有相当大的扩充空间。

简单来说:Unicode是一个统一了地球上所有语言的字符集。

编码

字符和码点之间的映射已经有了,但还需要定义另一种编码来确定码点在内存和硬盘中要如何表示。

Unicode 标准为此定义了几种映射,叫做「Unicode 转换格式」(Unicode Transformation Formats,简称 UTF)。

日常工作中,人们就直接把它们叫做「编码」—— 因为按照定义,如果是用 UTF 编码的,那么就要使用 Unicode,所以也就没必要明确区分这两个步骤了。

UTF-32

最清楚明了的一个 UTF 就是 UTF-32:它在每个码点上使用整 32 位。32 大于 21,因此每一个 UTF-32 值都可以直接表示对应的码点。

尽管简单,UTF-32却几乎从来不在实际中使用,因为每个字符占用 4 字节太浪费空间了。比如:表述一个字母A(只需要7位一个字节),前3字节空间被浪费了。

UTF-16

尽管有Unicode字符非常多,但是实际上大多数人不会用到超过前65535个以外的字符。因此,就有了另外一种Unicode编码方式,叫做UTF-16。

UTF-16 本身是一种长度可变的编码。基本多文种平面(BMP)中的每一个码点都直接与一个码元相映射。鉴于 BMP 几乎囊括了所有常见字符,UTF-16 一般只需要 UTF-32 一半的空间。其它平面里很少使用的码点都是用两个 16 位的码元来编码的,这两个合起来表示一个码点的码元就叫做代理对(surrogate pair)。

和所有多字节长度的编码系统一样,UTF-16(以及 UTF-32)还得解决字节顺序的问题。在内存里存储字符串时,大多数实现方式自然都采用自己运行平台的 CPU 的字节序(endianness);而在硬盘里存储或者通过网络传输字符串时,UTF-16 允许在字符串的开头插入一个「字节顺序标记」(Byte Order Mask,简称 BOM)。

字节顺序标记是一个值为 U+FEFF 的码元,通过检查文件的头两个字节,解码器就可以识别出其字节顺序。字节顺序标记不是必须的,Unicode 标准把高字节顺序(big-endian byte order)定为默认情况。UTF-16 需要指明字节顺序,这也是为什么 UTF-16 在文件格式和网络传输方面不受欢迎的一个原因,不过微软和苹果都在自己的操作系统内部使用它。

UTF-8

UTF-16 还是在常用的英文和西欧文本上浪费了大量的空间:每个 16 位的码点的高 8 位的值都会是 0。

UTF-8(8-bit Unicode Transformation Format)是一种针对Unicode的可变长度字符编码。它可以用来表示Unicode标准中的任何字符,且其编码中的第一个字节仍与ASCII兼容,这使得原来处理ASCII字符的软件无须或只须做少部份修改,即可继续使用。因此,它逐渐成为电子邮件、网页及其他存储或传送文字的应用中,优先采用的编码。互联网工程工作小组(IETF)要求所有互联网协议都必须支持UTF-8编码。

UTF-8使用一至四个字节为每个字符编码:

  • 128个US-ASCII字符只需一个字节编码(Unicode范围由U+0000至U+007F)。
  • 带有附加符号的拉丁文、希腊文、西里尔字母、亚美尼亚语、希伯来文、阿拉伯文、叙利亚文及它拿字母则需要二个字节编码(Unicode范围由U+0080至U+07FF)。
  • 其他基本多文种平面(BMP)中的字符(这包含了大部分常用字)使用三个字节编码。
  • 其他极少使用的Unicode辅助平面的字符使用四字节编码。

iOS相关

NSString和Unicode

NSString文档

An NSString object encodes a Unicode-compliant text string, represented as a sequence of UTF–16 code units. All lengths, character indexes, and ranges are expressed in terms of UTF–16 code units, with index values starting at 0. The length property of an NSString returns the number of UTF-16 code units in an NSString, and the characterAtIndex: method retrieves a specific UTF-16 code unit. These two “primitive” methods provide basic access to the contents of a string object.

我们已经了解了 Unicode 是一种 21 位的编码方案。

就想文档所说,NSString 代表的是用 UTF-16 编码的文本,长度、索引和范围都基于 UTF-16 的码元。除非你知道字符串的内容,或者你提前有所防范,不然 NSString 类里的方法都是基于上述概念的,无法给你提供可靠的信息。每当文档提及「字符」(character)或者 unichar 时,它其实都说的是码元。

NSString 对象代表的是用 UTF-16 编码的码元组成的数组。相应地,length 方法的返回值也是字符串包含的码元个数(而不是字符个数)。

length

The number of UTF-16 code units in the receiver.

1
2
Declaration
@property(readonly) NSUInteger length;

Discussion

This number includes the individual characters of composed character sequences, so you cannot use this property to determine if a string will be visible when printed or how long it will appear.

结语

文本很复杂,程序员还是需要了解其中的运作机制以便能正确处理它。

iOS测试代码运行效率-diapatch_benchmark

Posted on 2019-05-23 | In iOS

前言

在日常开发过程中,我们经常会遇到这样的疑问:

这段代码相对效率是多少?方法A与方法B哪个更快?

打时间戳

逻辑很简单:代码运行前记录一次时间,运行后记录一次,然后比较时间差即可。

1
2
3
4
5
6
CFTimeInterval startTime = CACurrentMediaTime();

//your test code goes here

CFTimeInterval endTime = CACurrentMediaTime();
NSLog(@"Total Runtime: %g s", endTime - startTime);

diapatch_benchmark

dispatch_benchmark 是 libdispatch (Grand Central Dispatch) 的一部分。但这个方法并没有被公开声明,所以你必须要自己声明:

1
extern uint64_t dispatch_benchmark(size_t count, void (^block)(void));

因为没有公开的函数定义, dispatch_benchmark 在 Xcode 中也没有公开的文档。但幸运的是有 man 页面

dispatch_benchmark(3)

The dispatch_benchmark function executes the given block multiple times according to the count variable and then returns the average number of nanoseconds per execution. This function is for debugging and performance analysis work. For the best results, pass a high count value to dispatch_benchmark.
Please look for inflection points with various data sets and keep the following facts in mind:

  • Code bound by computational bandwidth may be inferred by proportional changes in performance as concurrency is increased.
  • Code bound by memory bandwidth may be inferred by negligible changes in performance as concurrency is increased.
  • Code bound by critical sections may be inferred by retrograde changes in performance as concurrency is increased.
    • Intentional: locks, mutexes, and condition variables.
    • Accidental: unrelated and frequently modified data on the same cache-line.
dispatch_benchmark是一个根据传入的count参数多次执行给定block然后返回平均执行时间(纳秒)的方法。这个方法用于调试和测试分析代码性能。为了获取跟好的效果,传入的一个较大的count值
使用Sample

对于iOS中NSMutableArray,它对首尾插入/删除有较高的效率,近乎常数。

下面我们对比下,首尾删除与在中间删除有多少差距。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//测试函数
- (void)benchmark {

NSUInteger iterations = 100;
NSMutableArray *iterationArray = [NSMutableArray arrayWithArray:_testArray];
NSMutableArray *iterationArray2 = [NSMutableArray arrayWithArray:_testArray];;

uint64_t t_0 = dispatch_benchmark(iterations, ^{
for (int i = 0; i < self->_count; i ++) {
NSUInteger index = iterationArray.count/2;
if (index < iterationArray.count) {
[iterationArray removeObjectAtIndex:index];
}
}
});
NSLog(@"Remove Array At Middle Avg. Runtime: %llu ms", t_0/1000);
uint64_t t_1 = dispatch_benchmark(iterations, ^{
for (int i = 0; i < self->_count; i ++) {
NSUInteger index = iterationArray2.count/2;
if (index < iterationArray2.count) {
[iterationArray2 removeObjectAtIndex:0];
}
}
});
NSLog(@"Remove Array From Begining Avg. Runtime: %llu ms", t_1/1000);
}
1
2
3
4
5
6
7
8
9
//调用
{
_count = 200000;
_testArray = [NSMutableArray arrayWithCapacity:_count];
for (int i = 0; i < _count; i ++) {
[_testArray addObject:@(i)];
}
[self benchmark];
}

上面代码可以看出,初始化了一个数量为200000的可变数组,分别在中间、首尾移除数组元素,执行100次。

执行结果

1
2
Remove Array At Middle Avg. Runtime: 21851 ms
Remove Array From Begining Avg. Runtime: 1553 ms

Xcode开发效率-快捷键&代码块

Posted on 2019-05-23 | In Xcode

前言

程序员使用最多的还是键盘,我们可以通过快捷键的使用来方便的、快捷的访问Xcode很多功能。从而提高开发效率。

Keys

  • Shift + Command + O(字母O,而非0)弹出快速查找文件窗口。
  • Shift +Commond + 0(数字0,而非字母O)快速打开官方文档。
  • Shift + Comand + j 定位到文件所在目录,配合第一条快捷键使用。
  • Control + Command + 上\下 切换 .m 和.h。
  • Command + t 新建一个Tab(这个很实用,我平时一般都会建3,4个Tab)。
  • Command + w 关闭Tab。
  • Command + ` 切换同一个应用多个窗口。可以用来在多个Xcode窗口中切换。
  • Control + Command + e 可以批量修改光标所在位置的变量,像这样:

  • Shift + Command + f 打开全局搜索。(可以加个 Any 正则,就可以搜出如图中的这种)

  • Command + f 在类中搜索 (enter 匹配下一个 Shift + enter 匹配上一个)。
  • Command + 上\下\左\右 光标切换到类首/类尾/行首/行尾。
  • Shift + Command + 上\下\左\右 从当前光标所在选中到类首/类尾/行首/行尾的文本。
  • alt + 左\右 光标左右移动一个单词。
  • Command + delete 删除光标到行首的内容,同理alt + delete 删除光标前的一个单词,另外可以先切换到到行尾 用Command + delete删除一整行内容。
  • Control + i 自动缩进代码。
  • Command + \ 当前行加断点。
  • alt + Command + \,新建一个symbolic breakpoint。
  • Command + n新建文件 ,Shift + Command + n 新建工程。
  • alt + Command + 左\右 折叠\显示当前块。
  • Shift + alt + Command + 左\右 折叠\显示当前文件中的块。
  • alt + Command + [ 上移,如果没有选中,默认上移当前行,alt + Command + ] 下移。
  • Command + ] 向右缩进,支持多行,Command + [ 向左缩进。(类似Tab与shift+tab)。
  • Shift + Command + k product 清理;Command + r Run;Command + b 编译。

Xcode窗口控制

  • Shift + Command + Y 隐藏\显示 console 区。
  • Shift + Command + C 显示 console 区,且直接聚焦。
  • Command + k console清屏。
  • Control + 1 (没用过>,<)。

  • Control + 6 查看当前类的方法列表(可以用 pragma mark 来合理分块,查看更直观)。
  • Command + (1~9)切换左边窗体.Command + 0 显示 \ 隐藏左边窗体。
  • alt + Command + 0 显示 \ 隐藏右边窗体,同理alt + Command + 1,2等也可以切换。
  • Command + , 弹出 Perferences ,可以用 Command + w 隐藏。
  • Command + +(加号)/-(减号),放大/缩小整体页面。与Web端一致。

Code Snippet

选中编写好的代码块,右击选择 Create Code Snippet

参数说明:
  • Title: Code Snippet的名字,将作为主标题显示在列表中。
  • Summary: 说明,将作为副标题显示在列表中。
  • Platform: 平台,有All,iOS,watchOS,macOS和tvOS五个选项。
  • Language: 语言,有Objective-C,Swift等选项。
  • Completion Shortcut: 快捷键。
  • Completion Scopes: 匹配范围,比如说OC中一个类的代码块不该在另一个类中匹配出来,这样可以更加精确地匹配,这个选项可以多选。
  • 代码: 这就不用多说,直接在代码模块修改,但是这里相当于纯文本,建议验证后再保存。

Command + shift + L 打开所有代码块。

1
Tips: 代码段中可以使用<#placeHolder#>用于占位

补充Code Snippet示例

1
2
//property
@property (nonatomic, readwrite, strong) <#expression#> *<#expression#>;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//UICollectionView 
#pragma mark - UICollectionViewDataSource
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {
return <#number#>;
}

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
return <#number#>;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{

UICollectionViewCell *cell = (UICollectionViewCell *)[collectionView dequeueReusableCellWithReuseIdentifier:@"" forIndexPath:indexPath];
return cell;
}

- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
return CGSizeMake(<#number#>, <#number#>);
}

- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section
{
return UIEdgeInsetsMake(<#number#>, <#number#>, <#number#>, <#number#>);
}

#pragma mark - UICollectionViewDelegate
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//attributeLabel
UILabel *attributedLabel =[[UILabel alloc] init];
attributedLabel.numberOfLines = 0;
attributedLabel.preferredMaxLayoutWidth = <#preferredMaxLayoutWidth#>;
attributedLabel.backgroundColor = [UIColor clearColor];
NSString *text = <#text#>;
NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc] init];
style.lineSpacing = <#lineSpacing#>;
NSDictionary *attr = @{
NSFontAttributeName: [UIFont <#font#>],
NSParagraphStyleAttributeName: style,
NSForegroundColorAttributeName: [UIColor <#color#>]
};
attributedLabel.attributedText = [[NSAttributedString alloc] initWithString:text attributes:attr];
[<#view#> addSubview:attributedLabel];

Xcode插件

略(待补充)

Simlulater

略(待补充)

修改.gitignoe文件后如何生效

Posted on 2019-05-23 | In git

方法

1
2
3
git rm -r --cached .  #清除缓存  
git add . #重新trace file
git commit -m "update .gitignore" #提交和注释

总来来说,移除本地缓存,重新添加trace文件。

使用->访问实例对象成员变量Crash问题

Posted on 2019-05-22 | In iOS

前言

最近线上发生了一个Crash。根据栈信息定位到如下代码
1
2
EXC_BAD_ACCESS (SIGBUS)
Attempted to dereference garbage pointer 0x10. Originated at or in a subcall of WebViewJavascriptBridge_js
1
2
3
if (instance->ivar) {
...
}
就是访问成员变量时Crash。
难道instance可能为空,但是为空应该也不会Crash吧?因为在我的固有的思维里面,对nil发送消息并不会crash。
但是又仔细想了想,访问成员变量时,是通过当前类的实例对象偏移量再加上此变量在类中的偏移量获取的。与发送消息无关(不是属性,没有写get方法)。如果当前实例对象如果为空(obj的offet=0),为什么还会继续呢?百思不得解。
写了一个demo,使用clang命令转换为cpp文件后,才知道缘由。
大体代码如下:
1
2
3
4
5
6
7
8

//MyObject.h
@interface MyObject : NSObject {
@public
NSString *_strObject;
NSArray *_arrList;
}
@end
1
2
3
4
5
6
7
8
9
10
11
12
13
//Test.h
@implementation Test

- (void)helloworld {
MyObject *obj = nil;
if (condition) {
obj = [MyObject new];
}
NSString *str = obj->_strObject;
NSLog(@"%@", str);
}

@end
1
clang -rewrite-objc Test.m
通过clang命令获取Test.cpp文件(10万行左右)
在后面看到如下片段
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#pragma clang assume_nonnull end
// @implementation Test


static void _I_Test_helloworld(Test * self, SEL _cmd) {
MyObject *obj = __null;
if (condition) {
obj = ((MyObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("MyObject"), sel_registerName("new"));
}
NSString *str = (*(NSString **)((char *)obj + OBJC_IVAR_$_MyObject$_strObject));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_hs_pl2pwmlj27g9xt0wlb9mbdbh0000gn_T_Test_2546e4_mi_0, str);
}

// @end
主要下这行代码
1
NSString *str = (*(NSString **)((char *)obj + OBJC_IVAR_$_MyObject$_strObject));
这段代码就是获取_strObject偏移量,OBJC_IVAR_$_MyObject$_strObject是一个在编译器已经决定了的固定的偏移量。obj就是实例对象的首地址。通过2个值获取到_strObject地址。如果obj==nil;这个偏移地址就是一个固定的值0,加上变量固定偏移量,值就类似于0x10(这也和crash信息里相匹配)。

结论

通过->访问成员变量要判断当前变量是否为空。通过offset寻址成员变量时并不会多一层为空判断逻辑。

Swift学习笔记(一)

Posted on 2019-05-21 | In swift

简述

Swift是一种支持多编程范式、安全、快速和互动的跨平台编译式编程语言。且编译器对性能进行了优化,编程语言对开发进行了优化,两者互不干扰,鱼与熊掌兼得。它支持代码预览(playgrounds),这个革命性的特性可以允许程序员在不编译和运行应用程序的前提下运行 Swift 代码并实时查看结果。

历史

  • 2010年7月,苹果开发者工具部门总监克里斯·拉特纳开始着手 Swift 编程语言的设计工作,以一年时间,完成基本架构后,他领导了一个设计团队大力参与其中。
  • 2014年6月发表, Swift大约历经4年的开发期。苹果宣称Swift的特点是:快速、现代、安全、互动,而且明显优于Objective-C语言。Swift以LLVM编译,可以使用现有的Cocoa和Cocoa Touch框架。Xcode Playgrounds功能是Swift为苹果开发工具带来的最大创新,该功能提供强大的互动效果,能让Swift源代码在撰写过程中能即时显示出其运行结果。拉特纳本人强调,Playgrounds很大程度是受到布雷特·维克多理念的启发。
  • 2015年6月8日,苹果于WWDC2015上宣布,Swift将开放源代码,包括编译器和标准库。
  • 2015年12月3日,苹果宣布开源swift,并支持Linux,苹果在新网站swift.org和托管网站Github上开源了swift,但苹果的app store并不支持开源的swift,只支持苹果官方的swift版本,官方版本会在新网站swift.org上定期与开源版本同步。

Swift历史版本

  • 2019-01-24 Swift 5.0 更新
  • 2018-09-17 Swift 4.2 更新
  • 2018-03-29 Swift 4.1 更新
  • 2017-12-04 Swift 4.0.3 更新
  • 2017-09-19 Swift 4.0 更新
  • 2017-03-27 Swift 3.1 更新
  • 2016-10-27 Swift 3.0.1 更新
  • 2016-09-13 Swift 3.0 更新
  • 2016-03-21 Swift 2.2 更新
  • 2015-10-20 Swift 2.1 更新
  • 2015-09-16 Swift 2.0 更新
  • 2015-4-8 Swift 1.2 更新
  • 2014-10-16 Swift 1.1 更新
  • 2014-08-18 Swift 1.0 更新

Swift 是一门开发 iOS, macOS, watchOS 和 tvOS 应用的新语言。然而,如果你有 C 或者 Objective-C 开发经验的话,你会发现 Swift 的很多内容都是你熟悉的。

下面通过Swift与Objective-C/C对比来介绍这门语言。

基础部分

基本数值类型(numeric types)

Swift 包含了 C 和 Objective-C 上所有基础数据类型

Int 表示整型值; Double 和 Float 表示浮点型值; Bool 是布尔型值;String 是文本型数据。

Swift 还提供了三个基本的集合类型,Array、Set 和 Dictionary。类似OC中NSArray、NSMutableArray、NSSet、NSMutableSet、NSDictionary、NSMutableDictionary。

值类型(Value Type)&引用类型(Reference Type)

内存(RAM)中有两个区域,栈区(stack)和堆区(heap)。在 Swift 中,值类型,存放在栈区;引用类型,存放在堆区。

在 Swift 中,典型的有 struct,enum,以及 tuples(元组) 都是值类型。而平时使用的 Int, Double,Float,String,Array,Dictionary,Set 其实都是用结构体实现的,也是值类型。详见下面代码

在 Swift 中,class 和闭包是引用类型。

而在Objective-C中,除了基本数据类型、结构体等,其他都是引用类型。

*

注意

标准库定义的集合,例如数组,字典和字符串,都对复制进行了优化以降低性能成本。新集合不会立即复制,而是跟原集合共享同一份内存,共享同样的元素。在集合的某个副本要被修改前,才会复制它的元素。而你在代码中看起来就像是立即发生了复制。即Copy On Write。

1
2
3
public struct Int : FixedWidthInteger, SignedInteger {
...
}

元组(tuples)

元组(tuples)把多个值组合成一个复合值。元组内的值可以是任意类型,并不要求是相同类型。这在Objective-C中是没有的。

1
2
3
4
5
6
7
let http404Error = (404, "Not Found")
// http404Error 的类型是 (Int, String),值是 (404, "Not Found")
let (statusCode, statusMessage) = http404Error
print("The status code is \(statusCode)")
// 输出“The status code is 404”
print("The status message is \(statusMessage)")
// 输出“The status message is Not Found”

(404, “Not Found”) 元组把一个 Int 值和一个 String 值组合起来表示 HTTP 状态码的两个部分:一个数字和一个人类可读的描述。这个元组可以被描述为“一个类型为 (Int, String) 的元组”。 如果函数需要返回多个值,是一个不错的应用场景。

类型安全

Swift 是一门类型安全的语言,这意味着 Swift 可以让你清楚地知道值的类型。如果你的代码需要一个 String ,类型安全会阻止你不小心传入一个 Int 。而在OC中则不然。

常量&变量

var - 声明变量。
let - 声明常量。

类型标注

1
2
3
4
5
//swift
var welcomeMessage: String = "hello world!";
var welcomeMessage = "hello world!";
//Objective-C
NSString *welcomeMessage = @"hello world";

声明一个类型为 String ,名字为 welcomeMessage 的变量。

如果你在声明常量或者变量的时候赋了一个初始值,Swift 可以推断出这个常量或者变量的类型。

常量和变量的命名

1
2
3
4
var x = 0.0, y = 0.0;
let z = 0.0;
let 你好 = "hello";
let 🐶🐮 = "emoj-dog&cat";

上面例子可以看出,常量和变量名可以包含任何字符,包括 Unicode 字符。

输出常量和变量

1
2
3
4
5
//swift
// 输出“The current value of friendlyWelcome is hello world!”
print("The current value of welcomeMessage is \(welcomeMessage)")
//iOS
NSLog("The current value of welcomeMessage is %@", welcomeMessage);

可选类型 | nil | 强制解析

使用可选类型(optionals)来处理值可能缺失的情况。可选类型表示两种可能: 或者有值, 你可以解析可选类型访问这个值, 或者根本没有值。

注意

C 和 Objective-C 中并没有可选类型这个概念。最接近的是 Objective-C 中的一个特性,一个方法要不返回一个对象要不返回 nil,nil 表示“缺少一个合法的对象”。然而,这只对对象起作用——对于结构体,基本的 C 类型或者枚举类型不起作用。对于这些类型,Objective-C 方法一般会返回一个特殊值(比如 NSNotFound)来暗示值缺失。这种方法假设方法的调用者知道并记得对特殊值进行判断。然而,Swift 的可选类型可以让你暗示任意类型的值缺失,并不需要一个特殊值。

1
2
3
let possibleNumber = "123"
let convertedNumber = Int(possibleNumber)
// convertedNumber 被推测为类型 "Int?", 或者类型 "optional Int"

因为该构造器可能会失败,所以它返回一个可选类型(optional)Int,而不是一个 Int。

nil:你可以给可选变量赋值为 nil 来表示它没有值:

1
2
3
4
5
6
var serverResponseCode: Int? = 404
// serverResponseCode 包含一个可选的 Int 值 404
serverResponseCode = nil
// serverResponseCode 现在不包含值
var surveyAnswer: String?
// surveyAnswer 被自动设置为 nil

注意

nil 不能用于非可选的常量和变量。如果你的代码中有常量或者变量需要处理值缺失的情况,请把它们声明成对应的可选类型。

Swift 的 nil 和 Objective-C 中的 nil 并不一样。在 Objective-C 中,nil 是一个指向不存在对象的指针。在 Swift 中,nil 不是指针——它是一个确定的值,用来表示值缺失。任何类型的可选状态都可以被设置为 nil,不只是对象类型。

可选绑定

1
2
3
4
5
6
7
//如果 Int(possibleNumber) 返回的可选 Int 包含一个值,创建一个叫做 actualNumber 的新常量并将可选包含的值赋给它。
if let actualNumber = Int(possibleNumber) {
print("\'\(possibleNumber)\' has an integer value of \(actualNumber)")
} else {
print("\'\(possibleNumber)\' could not be converted to an integer")
}
// 输出“'123' has an integer value of 123”

强制解析

有时候在程序架构中,第一次被赋值之后,可以确定一个可选类型总会有值。在这种情况下,每次都要判断和解析可选值是非常低效的,因为可以确定它总会有值。

1
2
3
4
5
let possibleString: String? = "An optional string."
let forcedString: String = possibleString! // 需要感叹号来获取值

let assumedString: String! = "An implicitly unwrapped optional string."
let implicitString: String = assumedString // 不需要感叹号

注意

如果你在隐式解析可选类型没有值的时候尝试取值,会触发运行时错误。和你在没有值的普通可选类型后面加一个惊叹号一样。


基本运算符

这里只介绍OC中没有的运算符

区间运算符

1
2
3
4
5
6
7
8
//[a,b] a< b
(a...b)
//[a,b) a<b
(a..<b)

for index in 1...5 {
print("\(index) * 5 = \(index * 5)")
}

运算符重载(略)

字符串和字符

Swift 的 String 类型与 Foundation NSString 类进行了无缝桥接。Foundation 还对 String 进行扩展使其可以访问 NSString 类型中定义的方法。这意味着调用那些 NSString 的方法,你无需进行任何类型转换。

操作

增删改、下标访问等

1
2
3
4
5
6
7
8
9
10
11
//连接字符串
let string1 = "hello"
let string2 = " there"
var welcome = string1 + string2
// welcome 现在等于 "hello there"

var welcome = "hello"
welcome.insert("!", at: welcome.endIndex)

welcome.remove(at: welcome.index(before: welcome.endIndex))
// welcome 现在等于 "hello there"

编码方式

Objective-C(NSString)

NSString对象是用 UTF-16 编码的码元组成的数组。相应地,length 方法的返回值也是字符串包含的码元个数(而不是字符个数)。

Swift(String)

每一个字符串都是由编码无关的 Unicode 字符组成。Swift 中通过多种 “View” 来区分处理字符串的方式,UTF8View 让你以 UTF-8 编码的方式来处理字符串,而 CharacterView 则让你以字符为单位处理字符串而不必考虑编码的问题。

1
2
3
4
5
for codeUnit in dogString.utf8 {
print("\(codeUnit) ", terminator: "")
}
print("")
// 68 111 103 226 128 188 240 159 144 182

1
2
3
4
5
for codeUnit in dogString.utf16 {
print("\(codeUnit) ", terminator: "")
}
print("")
// 68 111 103 8252 55357 56374

集合

Swift 语言中的 Arrays、Sets 和 Dictionaries 中存储的数据值类型必须明确,即被现实为泛型集合。而Objective-C没有这样的要求(只要求存储的为对象)。

数组(Arrays)

创建数组

1
2
3
4
5
6
//swift 
var shoppingList = [String]()
var shoppingList: [String] = ["Eggs", "Milk"]
//...其他方法
//OC 类似泛型的功能
NSMutableArray <NSString *> *shoppingList = [NSMutableArray arrayWithArray:@[@"Eggs", @"Milk"]];

修改数组

1
2
3
4
5
6
7
//添加元素
//swift
shoppingList.insert("Maple Syrup", at: 0)
shoppingList.append("Flour")
//OC
[shoppingList addObject:@"Flour"];
[shoppingList addObject:@"Flour" atIndex:0];
1
2
3
4
5
//添加数组
//swift
shoppingList += ["Chocolate Spread", "Cheese", "Butter"];
//OC
[shoppingList addObjectFromArray:["Chocolate Spread", "Cheese", "Butter"]];
1
2
3
4
5
6
//修改数组
//swift
shoppingList[0] = "Six eggs"
shoppingList[4...6] = ["Bananas", "Apples"]
//OC
shoppingList[0] = @"Six eggs"
1
2
3
4
5
//移除元素
//swift
shoppingList.remove(at:0);
//OC
[shoppingList removeObjectAtIndex:0];

数组遍历

如果我们同时需要每个数据项的值和索引值,可以使用 enumerated() 方法来进行数组遍历。enumerated() 返回一个由每一个数据项索引值和数据值组成的元组。我们可以把这个元组分解成临时常量或者变量来进行遍历:

1
2
3
for (index, value) in shoppingList.enumerated() {
print("Item \(String(index + 1)): \(value)")
}

集合(Sets)

Q:集合和数组的区别?

集合(Set)用来存储相同类型并且没有确定顺序的值。当集合元素顺序不重要时或者希望确保每个元素只出现一次时可以使用集合而不是数组。

存储在集合中的类型,必须是可哈希化的。遵循协议Hashable。

集合操作

使用 intersection(:) 方法根据两个集合中都包含的值创建的一个新的集合。
使用 symmetricDifference(
:) 方法根据在一个集合中但不在两个集合中的值创建一个新的集合。
使用 union(:) 方法根据两个集合的值创建一个新的集合。
使用 subtracting(
:) 方法根据不在该集合中的值创建一个新的集合。

字典

Swift 的字典使用 Dictionary<Key, Value> 定义,其中 Key 是字典中键的数据类型,Value 是字典中对应于这些键所存储值的数据类型。

一个字典的 Key 类型必须遵循 Hashable 协议,就像 Set 的值类型。

Objective-C中NSDictionary的key必须是字符串。
Objective-C中NSDictionary对存储的Value类型一致性没有要求。但只能存储对象。

字典创建

1
2
3
4
5
//swift
var namesOfIntegers = [Int: String]()
// namesOfIntegers 是一个空的 [Int: String] 字典
//OC
NSDictionary *namesOfIntegers = [NSDictionary dictionary];

访问&修改字典

基本一致

字典遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
//swift
for (key, value) in namesOfIntegers {
print("\(key): \(value)")
}
//OC
for (key in namesOfIntegers.allKeys) {
//key is NSString object
...
}

for (value in namesOfIntegers.allValues) {
...
}

控制流

for-in

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let names = ["Anna", "Alex", "Brian", "Jack"]
for name in names {
print("Hello, \(name)!")
}
// Hello, Anna!
// Hello, Alex!
// Hello, Brian!
// Hello, Jack!

for index in 1...5 {
print("\(index) times 5 is \(index * 5)")
}

//忽略变量值
let base = 3
let power = 10
var answer = 1
for _ in 1...power {
answer *= base
}
print("\(base) to the power of \(power) is \(answer)")
// 输出“3 to the power of 10 is 59049”

While

1
2
3
while condition {
statements
}

Repeat-While

1
2
3
repeat {
statements
} while condition

if条件语句

1
2
3
4
5
if condition <= 0 {
print("It's very cold. Consider wearing a scarf.")
} else {
print("It's not that cold. Wear a t-shirt.")
}

Switch

1
2
3
4
5
6
7
8
9
switch some value to consider {
case value 1:
respond to value 1
case value 2,
value 3:
respond to value 2 or 3
default:
otherwise, do something else
}

不存在隐式的贯穿

与 C 和 Objective-C 中的 switch 语句不同,在 Swift 中,当匹配的 case 分支中的代码执行完毕后,程序会终止 switch 语句,而不会继续执行下一个 case 分支。这也就是说,不需要在 case 分支中显式地使用 break 语句。这使得 switch 语句更安全、更易用,也避免了漏写 break 语句导致多个语言被执行的错误。

注意 : 如果想要显式贯穿 case 分支,请使用 fallthrough 语句。

区间匹配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let approximateCount = 62
let countedThings = "moons orbiting Saturn"
let naturalCount: String
switch approximateCount {
case 0:
naturalCount = "no"
case 1..<5:
naturalCount = "a few"
case 5..<12:
naturalCount = "several"
case 12..<100:
naturalCount = "dozens of"
case 100..<1000:
naturalCount = "hundreds of"
default:
naturalCount = "many"
}
print("There are \(naturalCount) \(countedThings).")

函数

Swift拥有比Objective-C更强大的函数功能。

  • 多重返回值
  • 默认参数值
  • 可变参数
  • 使用函数作为参数
  • 使用函数作为返回类型

函数定义

1
2
3
4
func greet(person: String) -> String {
let greeting = "Hello, " + person + "!"
return greeting
}

函数调用

1
let greeting = greet(person: "Anna");

多重返回值 && 可选返回类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14

func minMax(array: [Int]) -> (min: Int, max: Int)? {
if array.isEmpty { return nil }
var currentMin = array[0]
var currentMax = array[0]
for value in array[1..<array.count] {
if value < currentMin {
currentMin = value
} else if value > currentMax {
currentMax = value
}
}
return (currentMin, currentMax)
}

OC 可以通过字典返、结构体、Model回多重值。

*

函数返回一个包含两个 Int 值的元组,这些值被标记为 min 和 max ,以便查询函数的返回值时可以通过名字访问它们。

指定/忽略参数标签

可以在参数名称前指定它的参数标签,中间以空格分隔:

1
2
3
func someFunction(argumentLabel parameterName: Int) {
// 在函数体内,parameterName 代表参数值
}
1
2
3
4
5
func greet(person: String, from hometown: String) -> String {
return "Hello \(person)! Glad you could visit from \(hometown)."
}
print(greet(person: "Bill", from: "Cupertino"))
// 打印“Hello Bill! Glad you could visit from Cupertino.”

OC 没有可比性

如果你不希望为某个参数添加一个标签,可以使用一个下划线(_)来代替一个明确的参数标签。

1
2
3
4
func someFunction(_ firstParameterName: Int, secondParameterName: Int) {
// 在函数体内,firstParameterName 和 secondParameterName 代表参数中的第一个和第二个参数值
}
someFunction(1, secondParameterName: 2)

默认参数

你可以在函数体中通过给参数赋值来为任意一个参数定义默认值(Deafult Value)。当默认值被定义后,调用这个函数时可以忽略这个参数。

1
2
3
4
5
func someFunction(parameterWithoutDefault: Int, parameterWithDefault: Int = 12) {
// 如果你在调用时候不传第二个参数,parameterWithDefault 会值为 12 传入到函数体中。
}
someFunction(parameterWithoutDefault: 3, parameterWithDefault: 6) // parameterWithDefault = 6
someFunction(parameterWithoutDefault: 4) // parameterWithDefault = 12

OC 不支持

可变参数

一个可变参数(variadic parameter)可以接受零个或多个值。函数调用时,你可以用可变参数来指定函数参数可以被传入不确定数量的输入值。通过在变量类型名后面加入(…)的方式来定义可变参数。

1
2
3
4
5
6
7
8
9
10
11
func arithmeticMean(_ numbers: Double...) -> Double {
var total: Double = 0
for number in numbers {
total += number
}
return total / Double(numbers.count)
}
arithmeticMean(1, 2, 3, 4, 5)
// 返回 3.0, 是这 5 个数的平均数。
arithmeticMean(3, 8.25, 18.75)
// 返回 10.0, 是这 3 个数的平均数。

OC 通过数组实现

函数类型作为参数类型

你可以用 (Int, Int) -> Int 这样的函数类型作为另一个函数的参数类型。这样你可以将函数的一部分实现留给函数的调用者来提供。

1
2
3
4
5
func printMathResult(_ mathFunction: (Int, Int) -> Int, _ a: Int, _ b: Int) {
print("Result: \(mathFunction(a, b))")
}
printMathResult(addTwoInts, 3, 5)
// 打印“Result: 8”

函数类型作为返回类型

1
2
3
4
5
6
7
8
9
func stepForward(_ input: Int) -> Int {
return input + 1
}
func stepBackward(_ input: Int) -> Int {
return input - 1
}
func chooseStepFunction(backward: Bool) -> (Int) -> Int {
return backward ? stepBackward : stepForward
}
1
2
3
var currentValue = 3
let moveNearerToZero = chooseStepFunction(backward: currentValue > 0)
// moveNearerToZero 现在指向 stepBackward() 函数。

函数功能和其他函数式编程差不多,比如JS。

嵌套函数

1
2
3
4
5
6
7
8
9
10
11
12
13
func chooseStepFunction(backward: Bool) -> (Int) -> Int {
func stepForward(input: Int) -> Int { return input + 1 }
func stepBackward(input: Int) -> Int { return input - 1 }
return backward ? stepBackward : stepForward
}
var currentValue = -4
let moveNearerToZero = chooseStepFunction(backward: currentValue > 0)
// moveNearerToZero now refers to the nested stepForward() function
while currentValue != 0 {
print("\(currentValue)... ")
currentValue = moveNearerToZero(currentValue)
}
print("zero!")

闭包

枚举

类和结构体

与其他编程语言所不同的是,Swift 并不要求你为自定义的结构体和类的接口与实现代码分别创建文件。

在iOS开发中,类和结构体差距还是蛮大的,我们经常使用结构体来封装一些属性来组成新的类型,简化运算。

而在Swift中,结构体(值类型)和类(引用类型)有很多共同点。

  • 定义属性用于存储值
  • 定义方法用于提供功能
  • 定义下标操作用于通过下标语法访问它们的值
  • 定义构造器用于设置初始值
  • 通过扩展以增加默认实现之外的功能
  • 遵循协议以提供某种标准功能

与结构体相比,类还有如下的附加功能:

  • 继承允许一个类继承另一个类的特征
  • 类型转换允许在运行时检查和解释一个类实例的类型
  • 析构器允许一个类实例释放任何其所被分配的资源
  • 引用计数允许对一个类的多次引用

定义语法

1
2
3
4
5
6
struct SomeStructure {
// 在这里定义结构体
}
class SomeClass {
// 在这里定义类
}

Swift命名注意

每当你定义一个新的结构体或者类时,你都是定义了一个新的 Swift 类型。请使用 UpperCamelCase 这种方式来命名类型(如这里的 SomeClass 和 SomeStructure),以便符合标准 Swift 类型的大写命名风格(如 String,Int 和 Bool)。请使用 lowerCamelCase 这种方式来命名属性和方法(如 framerate 和 incrementCount),以便和类型名区分。

实例化

1
2
let someClass = SomeClass()
let someStructure = SomeStructure()

属性访问

你可以通过使用点语法访问实例的属性。其语法规则是,实例名后面紧跟属性名,两者以点号(.)分隔,不带空格,与iOS一致。

恒等运算符

判定两个常量或者变量是否引用同一个类实例有时很有用。为了达到这个目的,Swift 提供了两个恒等运算符:

  • 相同(===)
  • 不相同(!==)

“==”表示两个实例的值“相等”或“等价”,判定时要遵照设计者定义的评判标准。

1
2
3
4
5
extension Vector2D: Equatable {
static func == (left: Vector2D, right: Vector2D) -> Bool {
return (left.x == right.x) && (left.y == right.y)
}
}

属性

存储属性

一个存储属性就是存储在特定类或结构体实例里的一个常量或变量。

1
2
3
4
5
6
7
8
struct FixedLengthRange {
var firstValue: Int
let length: Int
}
var rangeOfThreeItems = FixedLengthRange(firstValue: 0, length: 3)
// 该区间表示整数 0,1,2
rangeOfThreeItems.firstValue = 6
// 该区间现在表示整数 6,7,8

计算属性

除存储属性外,类、结构体和枚举可以定义计算属性。计算属性不直接存储值,而是提供一个 getter 和一个可选的 setter,来间接获取和设置其他属性或变量的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
struct Point {
var x = 0.0, y = 0.0
}
struct Size {
var width = 0.0, height = 0.0
}
struct Rect {
var origin = Point()
var size = Size()
var center: Point {
get {
let centerX = origin.x + (size.width / 2)
let centerY = origin.y + (size.height / 2)
return Point(x: centerX, y: centerY)
}
set(newCenter) {
origin.x = newCenter.x - (size.width / 2)
origin.y = newCenter.y - (size.height / 2)
}
}
}
var square = Rect(origin: Point(x: 0.0, y: 0.0),
size: Size(width: 10.0, height: 10.0))
let initialSquareCenter = square.center
square.center = Point(x: 15.0, y: 15.0)
print("square.origin is now at (\(square.origin.x), \(square.origin.y))")
// 打印“square.origin is now at (10.0, 10.0)”

只读计算属性

只有 getter 没有 setter 的计算属性叫只读计算属性。只读计算属性总是返回一个值,可以通过点运算符访问,但不能设置新的值。

iOS中通过readOnly修饰@property,来实现只读属性。

1
@property (nonatomic, readonly) NSString *varString;

注意

必须使用 var 关键字定义计算属性,包括只读计算属性,因为它们的值不是固定的。let 关键字只用来声明常量属性,表示初始化后再也无法修改的值。

属性观察器

属性观察器监控和响应属性值的变化,每次属性被设置值的时候都会调用属性观察器,即使新值和当前值相同的时候也不例外。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class StepCounter {
var totalSteps: Int = 0 {
willSet(newTotalSteps) {
print("将 totalSteps 的值设置为 \(newTotalSteps)")
}
didSet {
if totalSteps > oldValue {
print("增加了 \(totalSteps - oldValue) 步")
}
}
}
}
let stepCounter = StepCounter()
stepCounter.totalSteps = 200
// 将 totalSteps 的值设置为 200
// 增加了 200 步
stepCounter.totalSteps = 360
// 将 totalSteps 的值设置为 360
// 增加了 160 步
stepCounter.totalSteps = 896
// 将 totalSteps 的值设置为 896
// 增加了 536 步

iOS通过KVO来实现属性观察。

类型属性

实例属性属于一个特定类型的实例,每创建一个实例,实例都拥有属于自己的一套属性值,实例之间的属性相互独立。

你也可以为类型本身定义属性,无论创建了多少个该类型的实例,这些属性都只有唯一一份。这种属性就是类型属性。

使用关键字 static 来定义类型属性。在为类定义计算型类型属性时,可以改用关键字 class 来支持子类对父类的实现进行重写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct SomeStructure {
static var storedTypeProperty = "Some value."
static var computedTypeProperty: Int {
return 1
}
}
enum SomeEnumeration {
static var storedTypeProperty = "Some value."
static var computedTypeProperty: Int {
return 6
}
}
class SomeClass {
static var storedTypeProperty = "Some value."
static var computedTypeProperty: Int {
return 27
}
class var overrideableComputedTypeProperty: Int {
return 107
}
}

iOS中通过 class来修饰property。

1
@property (nonatomic, class) NSString *classString;

方法

实例方法(Instance Methods)

实例方法是属于某个特定类、结构体或者枚举类型实例的方法。实例方法提供访问和修改实例属性的方法或提供与实例目的相关的功能,并以此来支撑实例的功能。

实例方法中修改值类型

结构体和枚举是值类型。默认情况下,值类型的属性不能在它的实例方法中被修改。

但是,如果你确实需要在某个特定的方法中修改结构体或者枚举的属性,你可以为这个方法选择 可变(mutating)行为,然后就可以从其方法内部改变它的属性;并且这个方法做的任何改变都会在方法执行结束时写回到原始结构中。方法还可以给它隐含的 self 属性赋予一个全新的实例,这个新实例在方法结束时会替换现存实例。

1
2
3
4
5
6
7
8
9
10
11
struct Point {
var x = 0.0, y = 0.0
mutating func moveBy(x deltaX: Double, y deltaY: Double) {
x += deltaX
y += deltaY
}
}
var somePoint = Point(x: 1.0, y: 1.0)
somePoint.moveBy(x: 2.0, y: 3.0)
print("The point is now at (\(somePoint.x), \(somePoint.y))")
// 打印“The point is now at (3.0, 4.0)”

类型方法

实例方法是被某个类型的实例调用的方法。你也可以定义在类型本身上调用的方法,这种方法就叫做类型方法。在方法的 func 关键字之前加上关键字 static,来指定类型方法。类还可以用关键字 class 来允许子类重写父类的方法实现。

注意

在 Objective-C 中,你只能为 Objective-C 的类类型(classes)定义类型方法(type-level methods)。在 Swift 中,你可以为所有的类、结构体和枚举定义类型方法。每一个类型方法都被它所支持的类型显式包含。

1
2
3
4
5
6
class SomeClass {
class func someTypeMethod() {
// 在这里实现类型方法
}
}
SomeClass.someTypeMethod()

继承

为了指明某个类的超类,将超类名写在子类名的后面,用冒号分隔:

1
2
3
class SomeClass: SomeSuperclass {
// 这里是子类的定义
}

重写

子类可以为继承来的实例方法,类方法,实例属性,类属性,或下标提供自己定制的实现。我们把这种行为叫重写。

如果要重写某个特性,你需要在重写定义的前面加上 override 关键字。

重写方法

1
2
3
4
5
class Train: Vehicle {
override func makeNoise() {
print("Choo Choo")
}
}

重写属性

1
2
3
4
5
6
class Car: Vehicle {
var gear = 1
override var description: String {
return super.description + " in gear \(gear)"
}
}

重写属性观察器

1
2
3
4
5
6
7
class AutomaticCar: Car {
override var currentSpeed: Double {
didSet {
gear = Int(currentSpeed / 10.0) + 1
}
}
}

防止重写/继承

你可以通过把方法,属性或下标标记为 final 来防止它们被重写,只需要在声明关键字前加上 final 修饰符即可(例如:final var、final func、final class func 以及 final subscript)。

任何试图对带有 final 标记的方法、属性或下标进行重写的代码,都会在编译时会报错。在类扩展中的方法,属性或下标也可以在扩展的定义里标记为 final。

可以通过在关键字 class 前添加 final 修饰符(final class)来将整个类标记为 final 。这样的类是不可被继承的,试图继承这样的类会导致编译报错。

Extension 扩展

扩展可以给一个现有的类,结构体,枚举,还有协议添加新的功能。它还拥有不需要访问被扩展类型源代码就能完成扩展的能力(即逆向建模)。扩展和 Objective-C 的分类很相似。

Swift 中的扩展可以:

  • 添加计算型实例属性和计算型类属性
  • 定义实例方法和类方法
  • 提供新的构造器
  • 定义下标
  • 定义和使用新的嵌套类型
  • 使已经存在的类型遵循(conform)一个协议

注意

扩展可以给一个类型添加新的功能,但是不能重写已经存在的功能(与OC不同)。

扩展语法

1
2
3
extension SomeType {
// 在这里给 SomeType 添加新的功能
}

计算型属性

1
2
3
4
5
6
7
8
9
10
11
12
13
extension Double {
var km: Double { return self * 1_000.0 }
var m: Double { return self }
var cm: Double { return self / 100.0 }
var mm: Double { return self / 1_000.0 }
var ft: Double { return self / 3.28084 }
}
let oneInch = 25.4.mm
print("One inch is \(oneInch) meters")
// 打印“One inch is 0.0254 meters”
let threeFeet = 3.ft
print("Three feet is \(threeFeet) meters")
// 打印“Three feet is 0.914399970739201 meters”

注意

扩展可以添加新的计算属性,但是它们不能添加存储属性,或向现有的属性添加属性观察者。

构造器

扩展可以给现有的类型添加新的构造器。它使你可以把自定义类型作为参数来供其他类型的构造器使用,或者在类型的原始实现上添加额外的构造选项。

方法

扩展可以给现有类型添加新的实例方法和类方法。

在下面的例子中,给 Int 类型添加了一个新的实例方法叫做 repetitions:

1
2
3
4
5
6
7
extension Int {
func repetitions(task: () -> Void) {
for _ in 0..<self {
task()
}
}
}
1
2
3
4
5
6
3.repetitions {
print("Hello!")
}
// Hello!
// Hello!
// Hello!

可变实例方法

通过扩展添加的实例方法同样也可以修改(或 mutating(改变))实例本身。结构体和枚举的方法,若是可以修改 self 或者它自己的属性,则必须将这个实例方法标记为 mutating,就像是改变了方法的原始实现。

下标

扩展可以给现有的类型添加新的下标。下面的例子中,对 Swift 的 Int 类型添加了一个整数类型的下标。下标 [n] 从数字右侧开始,返回小数点后的第 n 位:

123456789[0] 返回 9

123456789[1] 返回 8

……以此类推:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
extension Int {
subscript(digitIndex: Int) -> Int {
var decimalBase = 1
for _ in 0..<digitIndex {
decimalBase *= 10
}
return (self / decimalBase) % 10
}
}
746381295[0]
// 返回 5
746381295[1]
// 返回 9
746381295[2]
// 返回 2
746381295[8]
// 返回 7

协议

协议 定义了一个蓝图,规定了用来实现某一特定任务或者功能的方法、属性,以及其他需要的东西。类、结构体或枚举都可以遵循协议,并为协议定义的这些要求提供具体实现。某个类型能够满足某个协议的要求,就可以说该类型遵循这个协议。

除了遵循协议的类型必须实现的要求外,还可以对协议进行扩展,通过扩展来实现一部分要求或者实现一些附加功能,这样遵循协议的类型就能够使用这些功能。

Objective-C中协议只能定义公用的一套接口,但不能提供具体的实现方法。也就是说,它只告诉你要做什么,但具体怎么做,它不关心。

协议语法

协议的定义方式与类、结构体和枚举的定义非常相似:

1
2
3
protocol SomeProtocol {
// 这里是协议的定义部分
}

要让自定义类型遵循某个协议,在定义类型时,需要在类型名称后加上协议名称,中间以冒号(:)分隔。遵循多个协议时,各协议之间用逗号(,)分隔:

1
2
3
struct SomeStructure: FirstProtocol, AnotherProtocol {
// 这里是结构体的定义部分
}

若一个拥有父类的类在遵循协议时,应该将父类名放在协议名之前,以逗号分隔:

1
2
3
class SomeClass: SomeSuperClass, FirstProtocol, AnotherProtocol {
// 这里是类的定义部分
}

属性要求

协议可以要求遵循协议的类型提供特定名称和类型的实例属性或类型属性。协议不指定属性是存储属性还是计算属性,它只指定属性的名称和类型。此外,协议还指定属性是可读的还是可读可写的。

如果协议要求属性是可读可写的,那么该属性不能是常量属性或只读的计算型属性。如果协议只要求属性是可读的,那么该属性不仅可以是可读的,如果代码需要的话,还可以是可写的。

协议总是用 var 关键字来声明变量属性,在类型声明后加上 { set get } 来表示属性是可读可写的,可读属性则用 { get } 来表示:

1
2
3
4
protocol SomeProtocol {
var mustBeSettable: Int { get set }
var doesNotNeedToBeSettable: Int { get }
}

在协议中定义类型属性时,总是使用 static 关键字作为前缀。当类类型遵循协议时,除了 static 关键字,还可以使用 class 关键字来声明类型属性:

1
2
3
protocol AnotherProtocol {
static var someTypeProperty: Int { get set }
}

方法要求

协议可以要求遵循协议的类型实现某些指定的实例方法或类方法。这些方法作为协议的一部分,像普通方法一样放在协议的定义中,但是不需要大括号和方法体。可以在协议中定义具有可变参数的方法,和普通方法的定义方式相同。但是,不支持为协议中的方法提供默认参数。

泛型

泛型是 Swift 最强大的特性之一,Swift 的 Array 和 Dictionary 都是泛型集合。你可以创建一个 Int 类型数组,也可创建一个 String 类型数组。

Objective-C中,泛型支持较弱。

1
2
3
4
//swift
var stringArray:[String] = ["Jack", "Rose"]
//oc
NSMutableArray <NSString *>*stringArray = [NSMutableArray new];

可解决的问题

Q:编写一个函数,实现交换输入的2个参数的值

我们编写一个函数用来交换两个 Int 值 swapTwoInts(_:_:):

1
2
3
4
5
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
let temporaryA = a
a = b
b = temporaryA
}

调用:

1
2
3
4
5
var someInt = 3
var anotherInt = 107
swapTwoInts(&someInt, &anotherInt)
print("someInt is now \(someInt), and anotherInt is now \(anotherInt)")
// 打印“someInt is now 107, and anotherInt is now 3”

swapTwoInts(_::) 函数很实用,但它只能作用于 Int 类型。如果你想交换两个 String 类型值,或者 Double 类型值,你必须编写对应的函数,类似下面 swapTwoStrings(::) 和 swapTwoDoubles(:_:) 函数:

1
2
3
4
5
6
7
8
9
10
11
func swapTwoStrings(_ a: inout String, _ b: inout String) {
let temporaryA = a
a = b
b = temporaryA
}

func swapTwoDoubles(_ a: inout Double, _ b: inout Double) {
let temporaryA = a
a = b
b = temporaryA
}

泛型函数

而泛型函数可适用于任意类型。我们命名新的函数为 swapTwoValues(_:_:):

1
2
3
4
5
func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
let temporaryA = a
a = b
b = temporaryA
}

泛型版本的函数使用占位符类型名(这里叫做 T ),而不是 实际类型名(例如 Int、String 或 Double),占位符类型名并不关心 T 具体的类型,但它要求 a 和b 必须是相同的类型,T 的实际类型由每次调用 swapTwoValues(_:_:) 来决定。

泛型函数和非泛型函数的另外一个不同之处在于这个泛型函数名(swapTwoValues(_::))后面跟着占位类型名(T),并用尖括号括起来()。这个尖括号告诉 Swift 那个 T 是 swapTwoValues(:_:) 函数定义内的一个占位类型名,因此 Swift 不会去查找名为 T的实际类型。

swapTwoValues(_::) 函数现在可以像 swapTwoInts(::) 那样调用,不同的是它能接受两个任意类型的值,条件是这两个值有着相同的类型。swapTwoValues(:_:) 函数被调用时,T 所代表的类型都会由传入的值的类型推断出来。

1
2
3
4
5
6
7
8
9
var someInt = 3
var anotherInt = 107
swapTwoValues(&someInt, &anotherInt)
// someInt 现在是 107,anotherInt 现在是 3

var someString = "hello"
var anotherString = "world"
swapTwoValues(&someString, &anotherString)
// someString 现在是“world”,anotherString 现在是“hello”

你可提供多个类型参数,将它们都写在尖括号中,用逗号分开。

泛型类型

除了泛型函数,Swift 还允许自定义泛型类型。这些自定义类、结构体和枚举可以适用于任意类型,类似于 Array 和 Dictionary。

编写一个名为 Stack(栈)的泛型集合类型。栈是值的有序集合,和数组类似,但比数组有更严格的操作限制。数组允许在其中任意位置插入或是删除元素。而栈只允许在集合的末端添加新的元素(称之为入栈)。类似的,栈也只能从末端移除元素(称之为出栈)。

Int型栈:

1
2
3
4
5
6
7
8
9
struct IntStack {
var items = [Int]()
mutating func push(_ item: Int) {
items.append(item)
}
mutating func pop() -> Int {
return items.removeLast()
}
}

泛型支持:

1
2
3
4
5
6
7
8
9
struct Stack<Element> {
var items = [Element]()
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element {
return items.removeLast()
}
}

Stack 基本上和 IntStack 相同,只是用占位类型参数 Element 代替了实际的 Int 类型。这个类型参数包裹在紧随结构体名的一对尖括号里()。

调用:

1
2
3
4
5
6
var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
stackOfStrings.push("cuatro")
// 栈中现在有 4 个字符串

泛型扩展

当对泛型类型进行扩展时,你并不需要提供类型参数列表作为定义的一部分。原始类型定义中声明的类型参数列表在扩展中可以直接使用,并且这些来自原始类型中的参数名称会被用作原始定义中类型参数的引用。

1
2
3
4
5
extension Stack {
var topItem: Element? {
return items.isEmpty ? nil : items[items.count - 1]
}
}

类型约束

例如,Swift 的 Dictionary 类型对字典的键的类型做了些限制,字典键的类型必须是可哈希(hashable)的。

类型约束语法

1
2
3
func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
// 这里是泛型函数的函数体部分
}

findIndex 用于查找任何符合 Equatable 的类型是否存在数组中,若存在,返回其索引。

1
2
3
4
5
6
7
8
func findIndex<T: Equatable>(of valueToFind: T, in array:[T]) -> Int? {
for (index, value) in array.enumerated() {
if value == valueToFind {
return index
}
}
return nil
}

关联类型

定义一个协议时,声明一个或多个关联类型作为协议定义的一部分将会非常有用。关联类型为协议中的某个类型提供了一个占位符名称,其代表的实际类型在协议被遵循时才会被指定。关联类型通过 associatedtype 关键字来指定。

下面例子定义了一个 Container 协议,该协议定义了一个关联类型 Item:

1
2
3
4
5
6
7
protocol Container {
//给关联类型添加约束
associatedtype Item: Equatable
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}

Container 协议定义了三个任何遵循该协议的类型(即容器)必须提供的功能:

  • 必须可以通过 append(_:) 方法添加一个新元素到容器里。
  • 必须可以通过 count 属性获取容器中元素的数量,并返回一个 Int 值。
  • 必须可以通过索引值类型为 Int 的下标检索到容器中的每一个元素。

泛型 Where 语句

类型约束让你能够为泛型函数、下标、类型的类型参数定义一些强制要求。

对关联类型添加约束通常是非常有用的。你可以通过定义一个泛型 where 子句来实现。通过泛型 where 子句让关联类型遵从某个特定的协议,以及某个特定的类型参数和关联类型必须类型相同。你可以通过将 where 关键字紧跟在类型参数列表后面来定义 where 子句,where 子句后跟一个或者多个针对关联类型的约束,以及一个或多个类型参数和关联类型间的相等关系。你可以在函数体或者类型的大括号之前添加 where 子句。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func allItemsMatch<C1: Container, C2: Container>
(_ someContainer: C1, _ anotherContainer: C2) -> Bool
where C1.Item == C2.Item, C1.Item: Equatable {

// 检查两个容器含有相同数量的元素
if someContainer.count != anotherContainer.count {
return false
}

// 检查每一对元素是否相等
for i in 0..<someContainer.count {
if someContainer[i] != anotherContainer[i] {
return false
}
}

// 所有元素都匹配,返回 true
return true
}

上面的例子定义了一个名为 allItemsMatch 的泛型函数,用来检查两个 Container 实例是否包含相同顺序的相同元素。如果所有的元素能够匹配,那么返回 true,否则返回 false。

ARC

Swift 使用自动引用计数(ARC)机制来跟踪和管理你的应用程序的内存。通常情况下,Swift 内存管理机制会一直起作用,你无须自己来考虑内存的管理。ARC 会在类的实例不再被使用时,自动释放其占用的内存。

注意

引用计数仅仅应用于类的实例。结构体和枚举类型是值类型,不是引用类型,也不是通过引用的方式存储和传递。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var reference1: Person?
var reference2: Person?
var reference3: Person?

reference1 = Person(name: "John Appleseed")
//initialized

reference2 = reference1
reference3 = reference1

reference1 = nil
reference2 = nil

reference3 = nil
//dealloc

解决2个类间循环引用问题

Swift 提供了两种办法用来解决你在使用类的属性时所遇到的循环强引用问题:弱引用(weak reference)和无主引用(unowned reference)。

闭包循环引用

在定义闭包时同时定义捕获列表作为闭包的一部分,通过这种方式可以解决闭包和类实例之间的循环强引用。捕获列表定义了闭包体内捕获一个或者多个引用类型的规则。跟解决两个类实例间的循环强引用一样,声明每个捕获的引用为弱引用或无主引用,而不是强引用。应当根据代码关系来决定使用弱引用还是无主引用。

1
2
3
4
lazy var someClosure: () -> String = {
[unowned self, weak delegate = self.delegate!] in
// 这里是闭包的函数体
}

访问控制

访问控制可以限定其它源文件或模块中的代码对你的代码的访问级别。这个特性可以让我们隐藏代码的一些实现细节,并且可以为其他人可以访问和使用的代码提供接口。

访问级别

Swift 为代码中的实体提供了五种不同的访问级别。这些访问级别不仅与源文件中定义的实体相关,同时也与源文件所属的模块相关。

  • Open 和 Public 级别可以让实体被同一模块源文件中的所有实体访问,在模块外也可以通过导入该模块来访问源文件里的所有实体。通常情况下,你会使用 Open 或 Public 级别来指定框架的外部接口。Open 和 Public 的区别在后面会提到。
  • Internal 级别让实体被同一模块源文件中的任何实体访问,但是不能被模块外的实体访问。通常情况下,如果某个接口只在应用程序或框架内部使用,就可以将其设置为 Internal 级别。
  • File-private 限制实体只能在其定义的文件内部访问。如果功能的部分细节只需要在文件内使用时,可以使用 File-private 来将其隐藏。
  • Private 限制实体只能在其定义的作用域,以及同一文件内的 extension 访问。如果功能的部分细节只需要在当前作用域内使用时,可以使用 Private 来将其隐藏。

Open 为最高访问级别(限制最少),Private 为最低访问级别(限制最多)。

Open 只能作用于类和类的成员,它和 Public 的区别如下:

  • Public 或者其它更严访问级别的类,只能在其定义的模块内部被继承。
  • Public 或者其它更严访问级别的类成员,只能在其定义的模块内部的子类中重写。
  • Open 的类,可以在其定义的模块中被继承,也可以在引用它的模块中被继承。
  • Open 的类成员,可以在其定义的模块中子类中重写,也可以在引用它的模块中的子类重写。

把一个类标记为 open,明确的表示你已经充分考虑过外部模块使用此类作为父类的影响,并且设计好了你的类的代码了。

默认访问级别

如果你没有为代码中的实体显式指定访问级别,那么它们默认为 internal 级别(有一些例外情况,稍后会进行说明)。因此,在大多数情况下,我们不需要显式指定实体的访问级别。

12

垂杨小梳雨

None

14 posts
6 categories
35 tags
© 2019 垂杨小梳雨
Powered by Hexo
|
Theme — NexT.Muse v5.1.4