本文是为了准备在实习公司新人串讲中部份内容的草稿,主要介绍一些 Objective-C 和 iOS 基础的东西,纯属抛砖引玉~html
接下来跟你们分享一下 Objective-C 和 iOS 开发的基础内容,并且主要会围绕一句普通的代码进行展开:编程
NSObject *obj = [[NSObject alloc] init];
复制代码
其实这部份内容大都是我本身对这行代码冒出的一些的问题和想法进行的解释,并且准备得有些仓促,因此不免会有些不全面和错漏的地方,请多多见谅~swift
咱们先来看看这句代码的基本含义,尝试从 NSObject 这个角度去解读promise
这行代码中写有两个 NSObject ,但他们表示的意思是不同的。安全
等号左边表示:建立了一个 NSObject 类型的指针 obj 。(开辟一个 NSObject 类型大小的内存空间,并用指针变量 obj 指向它) 等号右边表示:调用 NSObject 对象的类方法 alloc 进行内存空间的分配,调用实例方法 init 进行构造工做,如成员变量的初始化等。 等号右边的 NSObject 对象初始化完成以后将内存地址赋值给左边的 obj 。bash
感受使用 Java 的人常常会说 new 一个对象,虽然 Objective-C 也给咱们提供了这个方法,但咱们却不多直接使用 new ,而是使用 alloc init ,为何?微信
使用 new 和 使用 alloc init 均可以建立一个对象,并且在 new 的内部其实也是调用 alloc 和 init 方法,只是 alloc 会在分配内存时使用到 zone ,其实整体来看没啥区别。网络
NSZone 是 Apple 用来处理内存碎片化的优化方式,处理对象的初始化及释放等问题,以提升性能。但听说效果并很差。闭包
使用 new 的好处是什么?并发
使用 alloc init 的好处是什么?
initWithXxx
initWithXxx
,而 swift 可使用 init
?initWithXxx
替代NSObject *obj;
?swift 呢 ?在 Objective-C 中容许只声明一个变量并被使用,编译器不会报错。若是声明的是一个 Objective-C 对象,输出的值是 null ,若是是基本类型,输出的值是 0 ,若是是结构体如 CGRect ,会用 0 填充。
可是在 swift 中,状况就不同了,声明一个变量如 let a : Int
并被使用时,编译器会对这种行为报以错误提示,Variable 'a' used before being initialized
,表示变量 a 在使用前未被初始化。
为何?
其实在 Objective-C 中声明一个变量时,它是会有一个默认值的,但在 swift 中则不会提供默认值,由于 swift 做为一种强类型语言,它老是强制类型定义,而且要求变量的使用要严格符合定义,全部变量都必须先定义,且初始化后才能被使用。
这里有一个例外 -- 可选类型,如 let b : Int?
,可选属性不须要设置初始值,默认的初始值都是 nil ,无论是基础类型仍是对象类型的可选类型属性的初始值都是 nil 。并且可选类型是在设置数值的时候才分配空间,是一种 lazy-evaluation 即延迟计算的行为。
if(self = [super init])
写法日常在构造方法里作一些初始化工做时都会写上这样的代码, self = [super init]
这里先调用父类的构造方法也符合上述的构造顺序问题,但疑惑的是,为何 [super init]
要赋值给 self ?为何须要使用 if 做校验?
Objective-C 方法的调用,会转换成消息发送的代码,如 id objc_msgSend(id self, SEL op, ...);
MyClass *myObject = [[MyClass alloc] initWithString:@"someString"];
复制代码
上述代码会被编译器转换成:
class myClass = objc_getClass("MyClass");
SEL allocSelector = @selector(alloc);
MyClass *myObject1 = objc_msgSend(myClass, allocSelector);
SEL initSelector = @selector(initWithString:);
MyClass *myObject2 = objc_msgSend(myObject1, initSelector, @"someString");
复制代码
能够看到,当调用 objc_msgSend(myObject1, initSelector, @"someString")
时 self 已经有值了,它的值是 myObject1 。
回到 [super init]
这句代码,要注意,它不是被编译器转换成 objc_msgSend(super, @selector(init))
,而是会被转换成 objc_msgSendSuper(self, @selector(init))
。
是的,self 在初始化方法开始执行时已经有值了。
这里的 super 是一个编译器指令,和 self 指向同一个消息接受者,即当前调用方法的实例。他们两个的不一样点在于:super 会告诉编译器,执行
[super xxx]
时转换成objc_msgSendSuper
,即要去父类的方法列表找,而不是本类。
那么为何要将 [super init] 方法的返回值赋值给 self 呢?
来看一段常见的构造方法代码片断:
- (id)initWithString:(NSString *)aString
{
self = [super init];
if (self)
{
instanceString = [aString retain];
}
return self;
}
复制代码
经典解释:执行 [super init]
会产生如下三种结果中的一种:
第一种结果,赋值操做对 self 没有影响,后面的实例变量赋值在了原始对象上。 第三种结果,初始化失败,self 被赋值为 nil ,返回。
至于第二种结果,若是返回的对象不同,那么就须要将 instanceString = [aString retain]
(被转换成 self->instanceString = [aString retain]
)方法实现里的 self 指向新的值。
那么问题来了,[super init]
会返回不一样的对象?
是的!在如下状况会返回不一样的对象(所谓不一样对象,是内存地址的不一样):
[NSNumber numberWithInteger:0]
老是返回全局的 "zero" 对象)如今,根据返回的对象是否不一样,执行 [super init]
产生的结果扩展为如下四种:
能够看到,case 2 和 3 实际上是互斥的,咱们通常没法使用一种途径来知足全部的这四种 case 。
常见的可以知足 case 1,2 和 4 的作法是: self = [super init];
即上面的作法。
这里展现一种可以知足 case 1,3,和 4 的途径,即日常会被问到可否用一个变量替代 self 的作法是:
- (id)initWithString:(NSString *)aString
{
id result = [super init];
if (self == result)
{
instanceString = [aString retain];
}
return result;
}
复制代码
因此类簇,单例和特殊的对象都是 case 3 ,NSManagedObject 是 case 2 。
能够看到 case 3 很是常见,可是在构造方法中知足 case 1,2 和 4 变成了一种 standard (虽然在某些隐藏条件下是错误的作法)。
在这行代码 NSObject *obj = [[NSObject alloc] init];
等号左边的 NSObject 表示的是对象类型,那么在 Objective-C 中常见的基础类型和对象类型有哪些
基础类型:
对象类型:
这里有一个注意点是,有可变与不可变类型的对象,为了安全起见,用 copy 修饰不可变版本,用 strong 修饰可变版本,这样作的缘由是,若是有一个不可变的字符串 str 且用 strong 修饰,这时被赋值了一个可变字符串 mStr ,这样可能会发生这样的状况:一个原本预想中不可变的字符串 str 会因 mStr 的改变而改变。因此这里要仔细考量一下使用 copy 仍是 strong 去修饰。
在 Objective-C 中,BOOl 的定义是这样的:
typedef signed char BOOL;
#define YES (BOOL)1
#define NO (BOOL)0
复制代码
其余相关的布尔型以下:
bool :
C99标准定义了一个新的关键字_Bool,提供了布尔类型
#define bool _Bool
#define true 1
#define false 0
复制代码
Boolean:
typedef unsigned char Boolean;
enum DYLD_BOOL { FALSE, TRUE };
复制代码
上面谈到了 NSObject 做为类型展开的一些内容,如今咱们来看看 NSObject 做为对象来延伸出 Objective-C 对象存储位置相关的内容。
先来看看内存的五大区:
仍是回到最开始的那行代码来进行解释:
NSObject *obj = [[NSObject alloc] init];
复制代码
咱们知道,在 Objective-C 中,对象一般是指一块有特定布局的连续内存区域。这行代码建立了一个 NSObject 类型的指针 obj 和一个 NSObject 类型的对象,obj 指针存储在栈上,而其指向的对象则存储在堆上。
在栈上就不能建立对象吗?
* 不能直接建立,但可经过在结构体中的 isa 来间接建立对象。
那么这里又带来了几个问题,isa 和 block ,这在后面会单独聊。
那么为何 Objective-C 会选择使用堆来存储对象而不是栈,来看看栈对象的优缺点。
优势:
缺点:
- 512 KB (secondary threads)
- 8 MB (OS X main thread)
- 1 MB (iOS main thread)
综上,Objective-C 选择使用堆存储对象。
关于 NSString 的存储位置很是复杂,能够分配在栈区、堆区、常量区,粗略的理解以下:
block 能够存储在栈上,也能够存储在堆上。一般咱们会使用 copy
将一个栈上的 block 复制到堆上。
顺便谈谈关于 block 的其余内容:
block 的意思是拥有自动变量的匿名函数。
这里要注意的是,因为栈对象的有效区域仅限于其所在的块 {} ,即其捕获自动变量的范围也仅限于所在块。
还有一个修改自动变量时的注意点是:
到这儿由 block 联想到了咱们项目中使用到的自动布局框架 Mansory ,好比项目中的一个代码片断:
[self.carousel mas_remakeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(self.headerView);
make.left.mas_equalTo(self.headerView).offset(20);
make.right.mas_equalTo(self.headerView).offset(-20);
make.height.mas_equalTo(CGFLOAT_MIN);
}];
复制代码
一般在使用 block 时都会避免在 block 内部使用 self ,以避免产生循环引用,形成内存泄漏,因此一般会在 block 外部对 self 进行一次弱引用,再在内部进行一次强引用,用这种组合作法来避免产生循环引用现象,这里的循环引用现象多是:self -> block -> self 。
而后咱们经过观察其源码实现来进一步了解:
- (NSArray *)mas_remakeConstraints:(void(^)(MASConstraintMaker *make))block {
self.translatesAutoresizingMaskIntoConstraints = NO;
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
constraintMaker.removeExisting = YES;
block(constraintMaker);
return [constraintMaker install];
}
复制代码
结合前面介绍的关于 block 存储位置的内容,咱们能够知道,虽然 block 内部引用了 self ,但因为这是一个局部的 block ,存储在栈上而不是堆上,于是在出了 block 所在做用域后会被 pop 出栈而自动销毁,因此不存在引用环。
讲完 block 的存储位置,天然会想到它的一些使用场景,特别是选在择使用 block 仍是代理的一些争执吧。
其实我以为如何进行选择更多的是依据我的的一些编码风格和习惯,还有就是要符合原有项目的须要,说到底使用 block 和代理都没问题。但因为一些函数式框架的出现,好比 RAC 、RxSwift 、 promisekit ,里面链式调用 + 闭包的操做实在是很方便,并且也更加符合低耦合高内聚的编程理念,因此可能选择使用 block 又多了一个理由。
下面再聊一下 block 和 delegate 的一些本质区别,这部份内容主要是引述微信技术群里面的一位大佬的解释:
代理的 debug 追踪性确实会比 block 好,可是若是跟 block 在可读性方面比较的话其实算是弱项。
代理和 block 实际上都是函数 imp 的调用,但区别是,代理就等价于 weak 持有一个代理对象,你不写 protocol 不写 delegate ,一股脑把全部方法全写在 header 里,而后把代理对象自己直接传过去给另外一个对象,在另外一个对象中 weak 持有这个代理对象,这种写法和代理是没有区别的。
而 block 是一种还原上下文环境,甚至自动包裹一些自由变量的闭包概念,换句话说,block 的回调代码,和写 block 的代码,是能够同处于一个函数内,在一个可读代码上下文内,即 block 在代码上是一个连续的过程。
代理方法实际上传值传的是一整个对象,你把 a.delegate = self 实际上是把 self 传给了 a 持有,跟通常的属性赋值无异,若是再次传递,彻底能够继续传递 self 给别人。
block 继续传递,其实是把 imp 和上下文环境的自动变量打包进行传递,这个过程当中不必定会传递一个对象。从这个角度看 block 的控制力度更强一些。
这里会涉及到一个安全性方面的考虑,你把 self 传给了一个不知名的三方库,他虽然只是 id 看起来只能调用 protocol 里限定的方法,但其实 OC 这个约束只是骗骗编译器的。若是你把一个 self 传给了一个三方,设定为代理,若是三方有其余意图,他其实能够直接控制你的 self 对象的任意值或者方法。但 block ,你传过去的 block ,他只能操做 block 自己包裹的上下文环境。
扯得有点远了,咱回头看回最开始的那行代码 NSObject *obj = [[NSObject alloc] init];
,在 ARC 下会变成 __strong NSObject *obj = [[NSObject alloc] init];
,这涉及到 iOS 开发的内存管理相关内容。
在早期 macOS 开发中使用 GC 进行内存管理但如今都跟 iOS 开发同样已统一使用引用计数进行内存管理:当对象的引用计数为 0 时会被销毁,当对象被引用时其引用计数会 +1 ,当对象的引用被销毁时引用计数 -1 。
__strong 是一个变量修饰符,但这里不打算列举其余的变量修饰符,而会在下一条聊聊关于内存管理相关的属性修饰符。
内存管理相关的变量修饰符都有相对应的属性修饰符,通常的写法是在属性修饰符前添加两个下划线。
这里列举一下内管管理语义的属性修饰符:
设置上述属性修饰符会在属性自动生成 setter 方法的时候为咱们添加内存管理语义,明确内存管理全部权,若是咱们自定义 setter 访问器,则需手动指定。
标量类型缺省是 assign ,对象类型缺省是 strong 。
咱们知道,属性 = 实例变量(ivar) + setter + getter ,他的具体过程是这样的:
完成属性定义后,编译器会自动编写访问这些属性所需的访问器,此过程称为“自动合成”(autosynthesize)。须要强调的是,这个过程由编译器在编译期执行,因此在编译器中看不到自动生成的源代码。除了生成访问器 getter 、setter 以外,编译器还要自动向类中添加适当类型的实例变量,实例变量名称是在属性名前加下划线,咱们也能够在类的实现代码里经过 @synthsize 语法来指定实例变量的名字,如:
@implementation Person
@synthesize firstName = _myFirstName;
@synthesize lastName = _myLastName;
@end
复制代码
这里有一个注意的地方,@synthesize firstName;
像这样不指定实例变量的名字,那么生成的实例变量名会跟属性名一致,而不会再加下划线。
还有一个要注意的关键字:@dynamic ,他不会在编译阶段自动生成 getter 和 setter 方法,并且使用点语法或者赋值操做在编译阶段仍可以经过,可是该属性的访问器必须在运行时由用户本身实现,不然会 crash 。
在前面咱们有说到 Objective-C 对象一般是一块有特定布局的连续内存区域,因此接下来牵扯的内容可能会扯得比较远。
在计算机网络中有一堆协议,遵照同一个协议,那么他们之间即可以知晓对方的身份,接着愉快的进行通讯。
那么 Objective-C 做为一门面向对象语言,他是怎样判断一个东西是否是对象,又是如何进行对象间的通讯?
Objective-C 中的对象是一个指向 ClassObject 地址的变量: id obj = &ClassObject
这个地址其实就是在最高位的 isa 指针。
而对象的实例变量则是:
void *ivar = isa + offset(N)
因此 isa 就至关于对象之间的一个协议。
Objective-C 面向对象的一个 bug :好比一个 Person 实例,她调用一个 talk 方法,按理说,这个 talk 方法应该直接在该实例里面调用对应的实现,可是实际却不是这样,她会经过实例本身的 isa 指针找到对应的类,而后在类或及其类的继承结构一直往上寻找 talk 方法,该方法被找到后就会调用,这样看来就并非最初的那个实例进行调用。
属性还有一类原子性修饰符,atomic 和 nonatomic ,原子性和非原子性,缺省是原子性的,但在 iOS 开发中,几乎全部属性都会主动声明为 nonatomic 。
缘由有两个:
第一点,是由于使用 atomic 修饰的属性由编译器所合成的方法会经过锁机制(底层使用自旋锁)来确保原子性。 第二点,是由于即使原子操做阻止了属性被多个线程同时进行访问,但这并不表明咱们最终使用它们的代码时时线程安全的,好比并发访问粒度更大的实例中的属性,举个例子:
// Person.h
@property(atomic, copy) NSString *firstName;
@property(atomic, copy) NSString *lastName;
- (void)updateWithFirstName:(NSString *)firstName lastName:(NSString *)lastName delay:(double)t on:(dispatch_queue_t) q;
// Person.m
- (void)updateWithFirstName:(NSString *)firstName lastName:(NSString *)lastName delay:(double)t on:(dispatch_queue_t)q{
if (firstName != nil) {
self.firstName = firstName;
}
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(t * NSEC_PER_SEC)), q, ^{
if (lastName != nil) {
self.lastName = lastName;
}
});
}
// ViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
Person *p = [[Person alloc] init];
p.firstName = @"Holy";
p.lastName = @"H";
dispatch_queue_t queueA = dispatch_queue_create("queueA", 0);
dispatch_queue_t queueB = dispatch_queue_create("queueB", 0);
dispatch_async(queueA, ^{
[p updateWithFirstName:@"John" lastName:@"J" delay:0.0 on:queueA];
});
dispatch_async(queueB, ^{
[p updateWithFirstName:@"Ben" lastName:@"B" delay:0.0 on:queueB];
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@ %@", p.firstName, p.lastName);
});
}
复制代码
如今运行结果可能会出现
Ben J
复制代码
这显然不是咱们最初想要获得的。
到目前为止,Apple 公司的开发者们并无为 swift 的属性提供标记 atomic/nonatomic 的方法,也没有下面提到的 @synchronized 块那样去作互斥操做,而咱们能够经过使用 @synchronized 底层使用到的 objc_sync_enter(obj)
和 objc_sync_exit(obj)
去实现,但由于 objc_sync_xxx
是至关底层的方案,通常不推荐直接使用,而应选择其余高阶的方案。
那么如何解决上述示例的问题?咱们能够经过在修改时添加 @synchroized(obj) {} 块,将原子操做的粒度扩大到 obj 对象的修改域。
还有其余一些常见的同步机制如:NSLock、pthread、OSSpinLock、信号量等。
最后简单介绍一下 iOS 开发中事件的产生、传递和响应链。
事件产生:
系统注册了一个 Source 1(基于 mach port)用来接收系统事件,其回调函数为 _IOHIDEventSystemClientQueueCallback() 。当一个硬件事件(好比触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收,SpringBoard 只接收按键(锁屏/静音等)、触摸、加速、传感器等几种 event ,随后用 mach port 转发给须要的 APP 进程。随后苹果注册的哪一个 Source 1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内部的分发。
_UIApplicationHandleEventQueue() 会以先进先出的顺序把 IOHIDEvent 处理并包装成 UIEvent 进行处理分发,其中包括识别 UIGesture /处理屏幕旋转/发送给 UIWindow 等。一般事件好比 UIButton 点击、touchesBegan/Moved/End/Cancel 等都是在这个回调中完成的。
触摸事件传递,大体是从父控件传递到子控件:
UIApplication -> UIWindow -> UIView (or Gesture recognizer 这时会被当前 vc 截断) -> 寻找处理事件最合适的 view
那么如何寻找处理事件最合适的 view ?步骤:
底层实现主要涉及两个方法:func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView?
和 func point(inside point: CGPoint, with event: UIEvent?) -> Bool
hitTest 方法会根据视图层级结构往上调用 pointInside 方法,肯定可否接收事件。若是 pointInside 返回 true ,则继续调用子视图层级结构,直到在最远的视图找到点击的 point 。若是一个视图没有找到该 point ,则不会继续它往上的视图层级结构。
咱们能够经过调用这个方法来截获和转发事件。
事件响应,大体是从子控件传递到父控件:
过程:
在上述过程当中,若是某个控件实现了 touchesXxx 方法,则这个事件将由该控件接管,若是调用 super 的 touchesXxx ,就会将事件顺着响应者链继续往上传递,接着会调用上一个响应者的 touchesXxx 方法。
通常咱们会选择使用 block 或者 delegate 或者 notification center 去作一些消息事件的传递,而如今咱们也能够利用响应者链的关系来进行消息事件的传递。