1. 什么是 ARC? (ARC 是为了解决什么问题而诞生的?)
ARC 是 Automatic Reference Counting 的缩写, 即自动引用计数. 这是苹果在 iOS5 中引入的内存管理机制. Objective-C 和 Swift 使用 ARC 追踪和管理应用的内存使用. 这一机制使得开发者无需键入retain 和 release, 这不仅能够降低程序崩溃和内存泄露的风险, 而且可以减少开发者的工作量, 能够大幅度提升程序的流畅性和可预测性. 但是 ARC 不适用于 Core Foundation 框架中, 仍然需要手动管理内存.CF是一组C语言接口,它们为iOS应用程序提供基本数据管理和服务功能.Core Foundation框架和Foundation框架紧密相关,它们为相同功能提供接口,但Foundation框架提供Objective-C接口。
我们先来看一下ARC无效的时候,我们写id类型转void*类型的写法:
id obj = [[NSObject alloc] init];
void *p = obj;
反过来,当把void*对象变回id类型时,只是简单地如下来写,
id obj = p;
[obj release];
但是上面的代码在ARC有效时,就有了下面的错误:
error: implicit conversion of an Objective-C pointer
to ’void *’ is disallowed with ARC
void *p = obj;
^error: implicit conversion of a non-Objective-C pointer
type ’void *’ to ’id’ is disallowed with ARC
id o = p;
__bridge
为了解决这一问题,我们使用 __bridge 关键字来实现id类型与void*类型的相互转换。看下面的例子。
id obj = [[NSObject alloc] init];
void *p = (__bridge void *)obj;
id o = (__bridge id)p;
将Objective-C的对象类型用 __bridge 转换为 void* 类型和使用 __unsafe_unretained 关键字修饰的变量是一样的。被代入对象的所有者需要明确对象生命周期的管理,不要出现异常访问的问题。
2. 以下 keywords 有什么区别: assign vs weak, __block vs __weak
assign 和 weak 是用于在声明属性时, 为属性指定内存管理的语义.
assign用于简单的赋值, 不改变属性的引用计数, 用于 Objective-C 中的NSInteger,CGFloat以及 C 语言中int,float,double等数据类型.weak用于对象类型, 由于weak同样不改变对象的引用计数且不持有对象实例, 当该对象废弃时, 该弱引用自动失效并且被赋值为nil, 所以它可以用于避免两个强引用产生的循环引用导致内存无法释放的问题.
__block 和 __weak 之间的却是确实极大的, 不过它们都用于修饰变量.
- 前者用于指明当前声明的变量在被 block 捕获之后, 可以在 block 中改变变量的值. (因为在 block 声明的同时会截获该 block 所使用的全部自动变量的值, 而这些值只在 block 中只具有"使用权"而不具有"修改权”). 而
__block说明符就为 block 提供了变量的修改权. - 后者是所有权修饰符, 什么是所有权修饰符? 这里涉及到另一个问题, 因为在 ARC 有效时, id 类型和对象类型同 C 语言中的其他类型不同, 必须附加所有权修饰符. 所有权修饰符一种有 4 种:
- __strong
- __weak
- __unsafe_unretained
- __autorelease
__weak与weak的区别只在于, 前者用于变量的声明, 而后者用于属性的声明.
3. __block 在 ARC 和非 ARC 下含义一样吗?
__block 在 ARC 下捕获的变量会被 block retain, 这样可能导致循环引用, 所以必须要使用弱引用才能解决该问题. 而在非 ARC 下, 可以直接使用 __block 说明符修饰变量, 因为在非 ARC 下, block 不会 retain 捕获的变量.
4. 使用 nonatomic 一定是线程安全的吗?
nonatomic 的内存管理语义是非原子的, 非原子的操作本来就是线程不安全的, 而 atomic 的操作是原子的, 但是并不意味着它是线程安全的, 它会增加正确的几率, 能够更好的避免线程的错误, 但是它仍然是线程不安全的.
当使用 nonatomic 的时候, 属性的 setter 和 getter 操作是非原子的, 所以当多个线程同时对某一属性进行读和写的操作, 属性的最终结果是不能预测的.
当使用 atomic 时, 虽然对属性的读和写是原子的, 但是仍然可能出现线程错误: 当线程 A 进行写操作, 这时其他线程的读或写操作会因为该操作的进行而等待. 当 A 线程的写操作结束后, B 线程进行写操作, 然后当 A 线程进行读操作时, 却获得了在 B 线程中的值, 这就破坏了线程安全, 如果有线程 C 在 A 线程读操作前 release 了该属性, 那么还会导致程序崩溃. 所以仅仅使用 atomic 并不会使得线程安全, 我们还需要为线程添加 lock 来确保线程的安全.
atomic 都不是一定线程安全的, nonatomic 就更不必多说了.
6. + (void)load; 和 + (void)initialize; 有什么用处?
load: 1. 当类对象被引入项目时, runtime 会向每一个类对象发送 load 消息. 2. load 方法还是非常的神奇的, 因为它会在每一个类甚至分类被引入时仅调用一次, 调用的顺序是父类优先于子类, 子类优先于分类.
3. 而且 load 方法不会被类自动继承, 每一个类中的 load 方法都不需要像 viewDidLoad 方法一样调用父类的方法.
4. 由于 load 方法会在类被 import 时调用一次, 而这时往往是改变类的行为的最佳时机. 我在 DKNightVersion 中使用 method swizlling 来修改原有的方法时, 就是在分类 load 中实现的.
initialize 方法和 load 方法有一些不同,
1. 它虽然也会在整个 runtime 过程中调用一次, 但是它是在该类的第一个方法执行之前调用, 也就是说 initialize 的调用是惰性的, 它的实现也与我们在平时使用的惰性初始化属性时基本相同. 我在实际的项目中并没有遇到过必须使用这个方法的情况, 在该方法中主要做静态变量的设置并用于确保在实例初始化前某些条件必须满足.
7. 为什么其他语言里叫函数调用, Objective-C 中是给对象发送消息 (谈下对 runtime 的理解)
我们在其他语言中 函数调用是在编译期就已经决定了会调用哪个函数(方法), 编译器在编译期就能检查出函数的执行是否正确.
然而 Objective-C(ObjC) 是一门动态的语言, 整个 ObjC 语言都是尽可能的将所有的工作推迟到运行时才决定. 它基于 runtime 来工作, runtime 就是 ObjC 的灵魂, 其核心就是消息发送objc_msgSend .
What makes Objective-C truly powerful is its runtime.
所有的消息都会在运行时才会确定, [obj message] 在运行时会被转化为 objc_msgSend(id self, SEL cmd, ...) 来执行,
它会在运行时从选择子表中寻找对应的选择子并将选择子与实现进行绑定. 而如果没有找到对应的实现, 就会进入类似黑魔法的消息转发流程.
( 三次补救消息转发。 1、调用 + (BOOL)resolveInstanceMethod:(SEL)aSelector 方法, 我们可以在这个方法中为类动态地生成方法. 2、 3、)
我们几乎可以使用 runtime 魔改 Objective-C 中的一切: class property object ivar method protocol, 而下面就是它的主要应用:
- 内省
- 为分类动态的添加属性
- 使用方法调剂修改原有的方法实现
- KVC
- …
8. 什么是 Method Swizzling? 方法交换
method swizzling 实际上就是一种在运行时动态修改原有方法实现的技术, 它实际上是基于 ObjC runtime 的特性, 而 method swizzling 的核心方法就是 method_exchangeImplementations(SEL origin, SEL swizzle). 使用这个方法就可以在运行时动态地改变原有的方法实现,
方法的调用时机就是在上面提到的 load 方法中, 不在 initialize 方法中改变方法实现的原因是 initialize 可能会被子类所继承并重新执行最终导致无限递归, 而 load 并不会被继承.
主要涉及以下三个函数:
- class_addMethod
- class_replaceMethod
- method_exchangeImplementations
9. Method Swizzling 实用中需要注意什么?
- 避免交换父类方法
问题: 如果当前类未实现被交换的方法而父类实现了的情况下,此时父类的实现会被交换,若此父类的多个继承者都在交换时会导致方法被交换多次而混乱,同时当调用父类的方法时会因为找不到而发生崩溃。
解决:所以在交换前都应该先尝试为当前类添加被交换的函数的新的实现IMP,如果添加成功则说明类没有实现被交换的方法,则只需要替代分类交换方法的实现为原方法的实现,如果添加失败,则原类中实现了被交换的方法,则可以直接进行交换。 - 交换方法应在+load方法
方法交换应当在调用前完成交换,+load方法发生在运行时初始化过程中类被加载的时候调用,且每个类被加载的时候只会调用一次load方法,调用的顺序是父类、类、分类,且他们之间是相互独立的,不会存在覆盖的关系,所以放在+load方法中可以确保在使用时已经完成交换。
- 交换方法应该放到dispatch_once中执行
在第2点已经写到,+load方法在类被加载的时候调用,且只会调用一次,那为什么还需要dispatch_once呢?这是为了防止手动调用+load方法而导致反复的被交换,因为这是存在可能的。
- 交换的分类方法应该添加自定义前缀,避免冲突
这个毫无疑问,因为分类的方法会覆盖类中同名的方法,这样会导致无法预估的后果 - 交换的分类方法应调用原实现
很多情况我们不清楚被交换的的方法具体做了什么内部逻辑,而且很多被交换的方法都是系统封装的方法,所以为了保证其逻辑性都应该在分类的交换方法中去调用原被交换方法,注意:调用时方法交换已经完成,在分类方法中应该调用分类方法本身才正确。
在每次的方法交换时都应该注意以上几点,最终我们可以将其封装到NSObject的分类中,方便在调用:
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
@interface NSObject (Swizzling)
+ (void)methodSwizzlingWithOriginalSelector:(SEL)originalSelector
swizzledSelector:(SEL)swizzledSelector;
@end#import "NSObject+Swizzling.h"
@implementation NSObject (Swizzling)
+ (void)methodSwizzlingWithOriginalSelector:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector{
Class class = [self class];
//原有被交换方法
Method originalMethod = class_getInstanceMethod(class, originalSelector);
//要交换的分类新方法
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
//避免交换到父类的方法,先尝试添加被交换方法的新实现IMP
BOOL didAddMethod = class_addMethod(class,originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {//添加成功:则表明没有实现IMP,将源SEL的IMP替换到交换SEL的IMP
class_replaceMethod(class,swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {//添加失败:表明已实现,则可以直接交换实现IMP
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
@end10. UIView 和 CALayer 有什么关系?
1 UIView 的身后对应一个 Core Animation 框架中的 CALayer.2 每一个 UIView 都是 CALayer 的代理.
3. 在 iOS 上当你处理一个又一个的
UIView 时, 实际上是在操作 CALayer. 尽管有的时候你并不知道 (直接操作 CALayer 并不会对效率有着显著的提升).4. UIView 实际上就是对 CALayer 的轻量级的封装. UIView 继承自 UIResponder, 用来处理来自用户的事件; CALayer 继承自 NSObject 主要用于处理图层的渲染和动画. 这么设计有以下几个原因:- 你可以通过操作
UIView在一个更高的层级上处理与用户的交互, 触摸, 点击, 拖拽等事件, 这些都是在UIKit这个层级上完成的. UIView和NSView(AppKit)的实现极其不同, 而使用Core Animation可以实现底层代码地重用, 在 Mac 和 iOS 平台上都使用着近乎相同的Core Animation代码, 这样我们可以对这个层级进行抽象在两种平台上产生UIKit和AppKit用于不同平台的框架.
CALayer 的唯一原因大概是便于移植到不同的平台, 如果仅仅使用 Core Animation 层级进行开发, 处理用户的交互时间需要写更多的代码.11. 如何获取移动中的CALayer的位置信息?
要了解一下基础位置参数UIView有frame、bounds和center三个属性,CALayer分别为frame、bounds、position、anchorPoint。区别在 position、anchorPoint是什么呢?
anchorPoint(锚点) 它的值是用一种相对bounds的比例值来确定的。左上角(0,0),中心点(0.5,0.5), 右下角(1,1).
position 就是anchorPoint在superLayer中的位置,position是layer中的anchorPoint点在superLayer中的位置坐标。因此可以说, position点是相对superLayer的,anchorPoint点是相对layer的,两者是相对不同的坐标空间的一个重合点。
position的原始定义: The layer’s position in its superlayer’s coordinate space。
中文可以理解成为position是layer相对superLayer坐标空间的位置,很显然,这里的位置是根据anchorPoint来确定的。
position.x = frame.origin.x + 0.5 * bounds.size.width;
position.y = frame.origin.y + 0.5 * bounds.size.height;
里面的0.5是因为anchorPoint取默认值,更通用的公式应该是下面的:
position.x = frame.origin.x + anchorPoint.x * bounds.size.width;
position.y = frame.origin.y + anchorPoint.y * bounds.size.height;
但是移动中的layer位置 却涉及到的是动画的实现和响应链
首先我们要明白,UIView 隐式动画与 Core Animation 的显示动画的一点点区别,Core Animation 动画不改变 UIView 的 frame,它改变的只是 layer 的一些属性。而 UIView 动画作用在 frame 上的话,直接改变的是 UIView 的 frame。
也就是说,一旦你在 UIView animation 里面设置了 View 的 frame,那么该 View 的 frame 将会立即生效 (即使该 View 视觉上还在移动中)。喜欢动手的同学可以在上面代码的 redView.frame.origin.y += (UIScreen.main.bounds.height - 140) 下一行打印一下 redView 的 frame, 一定是改变之后的 frame 大小。
既然使用 UIView animtion 改变 frame 立即生效的话, 那么点击移动中的该 View 时间一定不会传递到该 View 中的, 因为在该响应链中你点击的位置是不在该 View 的 bounds 范围内的。不过在该 View 即将要移动到终点的时候,还是可以获取到点击时间的,此时该 View 的 bounds 与最终的 frame 有交叉,肯定可以捕捉到该点击事件,(建议感兴趣的同学可以动手试下)。这里涉及到了响应链的传递等,
既然点击事件添加到该 View 上无法识别,我们可以将该点击事件加在其静止的父 View 上,然后根据获取到的点击位置来判断该移动的 View 是否包含该位置。
可能这里又会有一个新的疑问:如何检测移动的 View 是否包含该位置呢? 其实细心的同学可以早已经见过 CALayer 里面提供了 presentation(), 它在苹果文档里面的注释如下:
Returns a copy of the presentation layer object that represents the state of the layer as it currently appears onscreen.
大致意思就是说,该方法会返回该 layer 在显示在屏幕上的状态的拷贝。 请抓住关键词:拷贝、显示、屏幕。拷贝是指返回的还是一个 layer 啊,显示和屏幕是指当前屏幕显示的样子啊。所以我觉得在这里将其理解为原当前移动状态中 layer 的一份拷贝,既然这样,那就可以很轻松获取到移动状态下的 bounds 了啊。然后手动去判断是否包含点击的位置即可。
好了,既然解决方案都已经出来了,那么就请看看解决之后的效果吧。
思路如上所示, 具体的代码如下所示:
/// 添加手势
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(tapGestureAction(_:)))
view.addGestureRecognizer(tapGesture)
/// 处理手势
@objc private func tapGestureAction(_ gesture: UITapGestureRecognizer) {
let location = gesture.location(in: view)
guard let layer = redView.layer.presentation() else { return }
if (layer.hitTest(location) != nil) {
/// 此时表明已经击中了该移动中的视图。可以在这里处理事情了。
}
}