1. load
可以说我们在日常开发中可以接触到的调用时间最靠前的方法,在主函数运行之前,load
方法就会调用
- 它只是一个在整个文件被加载到运行时,在 main 函数调用之前被 ObjC 运行时调用的钩子方法
- 父类先于子类调用
- 类先于分类调用
- 它的调用不是惰性的,且其只会在程序调用期间调用一次
- 如果在类与分类中都实现了
load
方法,它们都会被调用,不像其它的在分类中实现的方法会被覆盖,这就使load
方法成为了方法调剂的绝佳时机 -
load
方法的运行时间过早,所以这里可能不是一个理想的环境,因为某些类可能需要在在其它类之前加载,但是这是我们无法保证的。不过在这个时间点,所有的 framework 都已经加载到了运行时中,所以调用 framework 中的方法都是安全的
2. initialize
作用也非常局限,一般我们只会在 initialize
方法中进行一些常量的初始化
-
initialize
调用是惰性的,它会在第一次调用当前类的方法时被调用 -
initialize
方法是在alloc
方法之前调用的,alloc
的调用导致了前者的执行 - 始终要保证父类的初始化方法要在子类之前调用
- 与
load
不同,initialize
方法调用时,所有的类都已经加载到了内存中 - 子类会继承父类的
initialize
方法 -
initialize
的运行是线程安全的
3. UI
UI架构主要关注三大模块:界面布局管理、渲染及动画、事件响应
- 离屏渲染:一般情况下,OpenGL 会将应用提交到 Render Server 的动画直接渲染显示(基本的 Tile-Based 渲染流程),但对于一些复杂的图像动画的渲染并不能直接渲染叠加显示,而是需要根据 Command Buffer 分通道进行渲染之后再组合,这一组合过程中,就有些渲染通道是不会直接显示的;对比基本渲染通道流程和 Masking 渲染通道流程图,我们可以看到到 Masking 渲染需要更多渲染通道和合并的步骤;而这些没有直接显示在屏幕的上的通道(如上图的 Pass 1 和 Pass 2)就是 Offscreen Rendering Pass。
正常渲染通道 : 首先,OpenGL 提交一个命令到 Command Buffer,随后 GPU 开始渲染,渲染结果放到 Render Buffer 中,这是正常的渲染流程。但是有一些复杂的效果无法直接渲染出结果,它需要分步渲染最后再组合起来,比如添加一个蒙版(mask):
离屏渲染 :在前两个渲染通道中,GPU 分别得到了纹理(texture,也就是那个相机图标)和 layer (蓝色的蒙版)的渲染结果。但这两个渲染结果没有直接放入Render Buffer中,也就表示这是离屏渲染。直到第三个渲染通道,才把两者组合起来放入 Render Buffer 中。离屏渲染意味着把渲染结果临时保存,等用到时再取出,因此相对于普通渲染更占用资源。离屏渲染的最后一步是把此前的多个路径组合起来。
离屏渲染可能会自动触发,也可以手动触发。以下情况可能会导致触发离屏渲染:
- 重写 drawRect 方法
- 有 mask 或者是阴影 (layer.masksToBounds, layer.shadow),模糊效果也是一种 mask
- layer.shouldRasterize = true
优化滑动性能主要涉及方面 :
- 避免图层混合
- 确保控件的 opaque 属性设置为 true,确保 backgroundColor 和父视图颜色一致且不透明
- 如无特殊需要,不要设置低于 1 的 alpha 值
- 确保 UIImage 没有 alpha 通道
- 避免临时转换
- 确保图片大小和 frame 一致,不要在滑动时缩放图片
- 确保图片颜色格式被 GPU 支持,避免劳烦 CPU 转换
- 慎用离屏渲染
- 绝大多数时候离屏渲染会影响性能
- 重写 drawRect 方法,设置圆角、阴影、模糊效果,光栅化都会导致离屏渲染
- 设置阴影效果时加上阴影路径
- 滑动时若需要圆角效果,开启光栅化
-
ViewController 的要做的事:
- View Management:管理 View
- Data Marshalling:管理数据
- User Interactions:响应用户交互
- Resource Management:管理资源
- Adaptivity:适配不同的屏幕尺寸空间的变化
常见的 UI 结构 VC 例子
- MVC、MVP、MVVM
- MVC
- MVP
- MVVM
- 苹果官方给的MVC的设计模式图
- Controller 里面就只应该存放这些不能复用的代码,这些代码包括:
- 在初始化时,构造相应的 View 和 Model。
- 监听 Model 层的事件,将 Model 层的数据传递到 View 层。
- 监听 View 层的事件,并且将 View 层的事件转发到 Model 层。
- VC 的瘦身 : 通过代码的抽取,我们可以将原本的 MVC 设计模式中的 ViewController 进一步拆分,构造出 网络请求层、ViewModel 层、Service 层、Storage 层等其它类,来配合 Controller 工作,从而使 Controller 更加简单,我们的 App 更容易维护。
4. Runtime
- 方法调用
首先,在相应操作的对象中的缓存方法列表中找调用的方法,如果找到,转向相应实现并执行
如果没找到,在相应操作的对象中的方法列表中找调用的方法,如果找到,转向相应实现执行
如果没找到,去父类指针所指向的对象中执行1,2
以此类推,如果一直到根类还没找到,转向拦截调用
如果没有重写拦截调用的方法,程序报错
obj_msgSend : 这个方法先去查找 self 这个对象或者其父类是否响应
@selector(xxx:
),如果从这个类的方法分发表或者 cache 里面找到了,就调用它对应的函数指针。如果找不到,那就会执行一些其他的东西。步骤如下:
- 检测这个 selector 是不是要忽略的。比如 Mac OS X 开发,有了垃圾回收就不理会 retain, release 这些函数了。
- 检测这个 target 是不是 nil 对象。ObjC 的特性是允许对一个 nil 对象执行任何一个方法不会 Crash,因为会被忽略掉。
- 如果上面两个都过了,那就开始查找这个类的 IMP,先从 cache 里面找,完了找得到就跳到对应的函数去执行。
- 如果 cache 找不到就找一下方法分发表。
- 如果还找不到就要开始消息转发逻辑了。
-
拦截调用 : 查找该类及其父类的 cahce 和方法分发表,在找不到调用的方法程序崩溃之前,有机会通过重写 NSObject 的四个方法来处理
// 第一个方法是当你调用一个不存在的类方法的时候,会调用这个方法,默认返回 NO,你可以加上自己的处理然后返回 YES, 可以告诉 runtime 在找不到该方法的情况下执行什么方法。 + (BOOL)resolveClassMethod:(SEL)sel; // 第二个方法和第一个方法相似,只不过处理的是实例方法 + (BOOL)resolveInstanceMethod:(SEL)sel; //后两个方法需要转发到其他的类处理 // 第三个方法是将你调用的不存在的方法重定向到一个其他声明了这个方法的类,只需要你返回一个有这个方法的 target,没办法在自己的类里面找到替代方法,你就重载这个方法,然后把消息转给其他的 Object。这样你就可以把消息转给别人了。当然这里你不能 return self,不然就死循环了。 - (id)forwardingTargetForSelector:(SEL)aSelector; // 第四个方法是将你调用的不存在的方法打包成 NSInvocation 传给你。做完你自己的处理后,调用 invokeWithTarget: 方法让某个 target 触发这个方法。NSInvocation 其实就是一条消息的封装。如果你能拿到 NSInvocation,那你就能修改这条消息的 target, selector 和 arguments。 - (void)forwardInvocation:(NSInvocation *)anInvocation;
什么是Objective-C runtime?
简单来说,Objective-C runtime 是一个实现 Objective-C 语言的 C 库。对象可以用C语言中的结构体表示,而方法(methods)可以用 C 函数实现。事实上,他们 差不多也是这么干了,另外再加上了一些额外的特性。这些结构体和函数被 runtime 函数封装后,Objective-C 程序员可以在程序运行时创建,检查,修改类、对象和它们的方法。除了封装,Objective-C runtime 库也负责找出方法的最终执行代码。当程序执行 [object doSomething] 时,不会直接找到方法并调用。相反,一条消息(message)会发送给对象(在这儿,我们通常叫它接收者)。runtime库给次机会让对象根据消息决定该作出什么样的反应。
ObjC Runtime 其实是一个 Runtime 库,基本上用 C 和汇编写的,这个库使得 C 语言有了面向对象的能力。这个库做的事前就是加载类的信息,进行方法的分发和转发之类的。
- Objective-C 是一个面向运行时的语言
什么是一个运行时语言?一个运行时语言就是在应用程序运行的时候来决定函数内部实现什么以及做出其它决定的语言。
ObjC 是一种面向 runtime (运行时)的语言,也就是说,它会尽可能地把代码执行的决策从编译和链接的时候,推迟到运行时。这给程序员写代码带来很大的灵活性,比如说你可以把消息转发给你想要的对象,或者随意交换一个方法的实现之类的。这就要求 runtime 能检测一个对象是否能对一个方法进行响应,然后再把这个方法分发到对应的对象去。
-
Class
typedef struct objc_class *Class; typedef struct objc_object { Class isa; } *id;
这里有两个结构体,一个类结构体一个对象结构体。所有的 objc_object 对象结构体都有一个 isa 指针,这个 isa 指向它所属的类,在运行时就靠这个指针来检测这个对象是否可以响应一个 selector。完了我们看到最后有一个 id 指针。这个指针其实就只是用来代表一个 ObjC 对象,有点类似于 C++ 的泛型。当你拿到一个 id 指针之后,就可以获取这个对象的类,并且可以检测其是否响应一个 selector。这就是对一个 delegate 常用的调用方式。
-
IMP (Method Implementations)
typedef id (*IMP)(id self,SEL _cmd,...);
一个 IMP 就是一个函数指针,这是由编译器生成的,当你发起一个 ObjC 消息之后,最终它会执行的那个代码,就是由这个函数指针指定的。
Class 的方法列表其实是一个字典,key 为 selectors ,IMPs 为value 。一个 IMP 是指向方法在内存中的实现。很重要的一点是,selector 和 IMP 之间的关系是在运行时才决定的,而不是编译时。IMP 通常是指向方法的指针。
- Selector
一个 Selector 事实上是一个 C 的结构体,表示的是一个方法。定义是:
typedef struct objc_selector *SEL;
使用起来就是:
SEL aSel = @selector(movieTitle);
这样可以直接取一个selector,如果是传递消息(类似于C的方法调用)就是:
[target getMovieTitleForObject:obj];
- Objective-C Classes
看看一个 ObjC 的类
@interface MyClass : NSObject {
//vars
NSInteger counter;
}
//methods
-(void)doFoo;
@end
定义一个类我们可以写成如上代码,而在运行时,一个类就不仅仅是上面看到的这些东西了:
#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
可以看到运行时一个类还关联了它的父类指针,类名,成员变量,方法,cache 还有附属的 protocol。
- 类定义了对象并且自己也是个对象?
一个 ObjC 类同时也是一个对象,为了处理类和对象的关系,runtime 库创建了一种叫做 元类(Meta Class)的东西。当你发出一个消息的时候,比方说
[NSObject alloc];
你事实上是把这个消息发给了一个类对象(Class Object),这个类对象必须是一个 Meta Class 的实例,而这个 Meta Class 同时也是一个根 MetaClass 的实例。当你继承了 NSObject 成为其子类的时候,你的类指针就会指向 NSObject 为其父类。但是 Meta Class 不太一样,所有的 Meta Class 都指向根 Meta Class 为其父类。一个 Meta Class 持有所有能响应的方法。所以当 [NSObject alloc] 这条消息发出的时候,objc_msgSend() 这个方法会去 NSObject 它的 Meta Class 里面去查找是否有响应这个 selector 的方法,然后对 NSObject 这个类对象执行方法调用。
在 Objective-C 中,Classes 本身也是 Objects ,也可以处理消息,这也是为什么会有类方法和实例方法。具体来说,Objective-C 中的 Object 是一个结构体(struct),第一个成员是 isa,指向自己的Class。这是在 objc/objc.h 中定义的:
typedef struct objc_object { Class isa;} *id;
Object 的 Class 保存了方法列表,还有指向父类的指针。但 Classes 也是 Objects,也会有 isa 变量,那么它又指向哪儿呢?这里就引出了第三个类型: Meta class。一个 meta class 被指向 class,class 被指向 object。它保存了所有实现的方法列表,以及父类的 meta class。
因为类也是一个对象,那它也必须是另一个类的实例,这个类就是元类(metaclass
)。元类保存了类方法的列表。当一个类方法被调用时,元类会首先查找它本身是否有该类方法的实现,如果没有,则该元类会向它的父类查找该方法,直到一直找到继承链的头。
元类 (metaclass) 也是一个对象,那么元类的 isa 指针又指向哪里呢?为了设计上的完整,所有的元类的 isa 指针都会指向一个根元类 (root metaclass
)。根元类 (root metaclass) 本身的 isa 指针指向自己,这样就行成了一个闭环。上面提到,一个对象能够接收的消息列表是保存在它所对应的类中的。在实际编程中,我们几乎不会遇到向元类发消息的情况,那它的 isa 指针在实际上很少用到。不过这么设计保证了面向对象的干净,即所有事物都是对象,都有 isa 指针。
-
class 与 object 的定义:
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
/// Represents an instance of a class.
struct objc_object {
Class isa;
};/// A pointer to an instance of a class. typedef struct objc_object *id;
由此可见,Class是一个指向 objc_class 结构体的指针,而 id 是一个指向 objc_object 结构体的指针,其中的成员 isa 是一个指向 objec_class 结构体的指针。
-
关于objc_class的定义:
struct objc_class { Class isa; // 指向metaclass Class super_class ; // 指向父类 const char *name ; // 类名 long version ; // 类的版本信息,初始化默认为0,可以通过runtime函数class_setVersion或者class_getVersion进行修改、读取 long info; // 一些标识信息,如CLS_CLASS (0x1L) 表示该类为普通 class ,其中包含实例方法和变量;CLS_META (0x2L) 表示该类为 metaclass,其中包含类方法; long instance_size ; // 该类的实例变量大小(包括从父类继承下来的实例变量); struct objc_ivar_list *ivars; // 用于存储每个成员变量的地址 struct objc_method_list **methodLists ; // 与 info 的一些标志位有关,如CLS_CLASS (0x1L),则存储实例方法,如CLS_META (0x2L),则存储类方法; struct objc_cache *cache; // 指向最近使用的方法的指针,用于提升效率; struct objc_protocol_list *protocols; // 存储该类声明遵守的协议 }
-
OC中类与对象的继承层次关系
当我们调用某个对象的实例方法时,它会首先在自身 isa 指针指向的类(class)methodLists 中查找该方法,如果找不到则会通过 class 的 super_class 指针找到父类的类对象结构体,然后从 methodLists 中查找该方法,如果仍然找不到,则继续通过 super_class 向上一级父类结构体中查找,直至根 class;
当我们调用某个类方法时,它会首先通过自己的 isa 指针找到 metaclass,并从其中 methodLists 中查找该类方法,如果找不到则会通过 metaclass 的 super_class 指针找到父类的 metaclass 对象结构体,然后从 methodLists 中查找该方法,如果仍然找不到,则继续通过 super_class 向上一级父类结构体中查找,直至根 metaclass;
runtime 可以做什么:
实现多继承:从 forwardingTargetForSelector: 方法就能知道,一个类可以做到继承多个类的效果,只需要在这一步将消息转发给正确的类对象就可以模拟多继承的效果。
-
交换两个方法的实现:
Method m1 = class_getInstanceMethod([M1 class], @selector(hello1)); Method m2 = class_getInstanceMethod([M2 class], @selector(hello2)); method_exchangeImplementations(m2, m1);
关联对象:
通过下面两个方法,可以给 category 实现添加成员变量的效果。
objc_setAssociatedObject
objc_getAssociatedObject动态添加类和方法:
objc_allocateClassPair 函数与 objc_registerClassPair 函数可以完成一个新类的添加,class_addMethod 给类添加方法,class_addIvar 添加成员变量,objc_registerClassPair 来注册类,其中成员变量的添加必须在类注册之前,类注册后就可以创建该类的对象了,而再添加成员变量就会破坏创建的对象的内存结构。将 json 转换为 model:
用到了 Runtime 获取某一个类的全部属性的名字,以及 Runtime 获取属性的类型。能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?
不能向编译后得到的类中增加实例变量;
能向运行时创建的类中添加实例变量;
因为编译后的类已经注册在 runtime 中,类结构体中的 objc_ivar_list 实例变量的链表和 instance_size 实例变量的内存大小已经确定,同时 runtime 会调用 class_setIvarLayout 或 class_setWeakIvarLayout 来处理 strong weak 引用。所以不能向存在的类中添加实例变量;
运行时创建的类是可以添加实例变量,调用 class_addIvar 函数。但是得在调用 objc_allocateClassPair 之后,objc_registerClassPair 之前,原因同上。
5. 单例示例
+ (instancetype)shareObject
{
static SNObject *object;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
object = [[SNObject alloc] init];
});
return object;
}
6. AutoreleasePool
-
main
函数内部@autoreleasepool
的实质 :int main(int argc, const char * argv[]) { { void * atautoreleasepoolobj = objc_autoreleasePoolPush(); // do whatever you want objc_autoreleasePoolPop(atautoreleasepoolobj); } return 0; }
自动释放池是由
AutoreleasePoolPage
以双向链表的方式实现的当对象调用
autorelease
方法时,会将对象加入AutoreleasePoolPage
的栈中调用
AutoreleasePoolPage::pop
方法会向栈中的对象发送release
消息
7. 添加圆角
- 如果能够只用
cornerRadius
解决问题,就不用优化。 - 如果必须设置
masksToBounds
,可以参考圆角视图的数量,如果数量较少(一页只有几个)也可以考虑不用优化。 -
UIImageView
的圆角通过直接截取图片实现,其它视图的圆角可以通过Core Graphics
画出圆角矩形实现。 - 一般情况下给 UIImageView 或者说 UIKit 的控件添加圆角都是改变 clipsToBounds 和 layer.cornerRadius, 这样大约两行代码就可以解决这个问题. 但是, 这样使用这样的方法会强制 Core Animation 提前渲染屏幕的离屏绘制, 而离屏绘制就会为性能带来负面影响.
我们也可以使用另一种比较复杂的方式来为图片添加圆角, 这里就用到了贝塞尔曲线.
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
imageView.center = CGPointMake(200, 300);
UIImage *anotherImage = [UIImage imageNamed:@"image"];
UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, NO, 1.0);
[[UIBezierPath bezierPathWithRoundedRect:imageView.bounds cornerRadius:50] addClip];
[anotherImage drawInRect:imageView.bounds];
imageView.image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
[self.view addSubview:imageView];
在这里使用了贝塞尔曲线"切割"个这个图片, 给 UIImageView 添加了的圆角.
8. 主线程中也不绝对安全的 UI 操作
在苹果的
MapKit
框架中,有一个叫做addOverlay
的方法,它在底层实现的时候,不仅仅要求代码执行在主线程上,还要求执行在 GCD 的主队列上。队列和线程的区别,他们之间并没有“拥有关系(ownership)”,当我们同步的提交一个任务时,首先会阻塞当前队列,然后等到下一次 runloop 时再在合适的线程中执行 block。
寻找线程的规则是: 任何提交到主队列的 block 都会在主线程中执行,在不违背此规则的前提下,文档还告诉我们系统会自动进行优化,尽可能的在当前线程执行 block。
即使是在主线程中执行的代码,也很可能不是运行在主队列中(反之则必然)。如果我们在子队列中调用
MapKit
的addOverlay
方法,即使当前处于主线程,也会导致 bug 的产生,因为这个方法的底层实现判断的是主队列而非主线程。解决方法 :
由于提交到主队列的 block 一定在主线程运行,并且在 GCD 中线程切换通常都是由指定某个队列引起的,我们可以做一个更加严格的判断,即用判断是否处于主队列来代替是否处于主线程。
GCD 没有提供 API 来进行相应的判断,但我们可以另辟蹊径,利用 dispatch_queue_set_specific
和 dispatch_get_specific
这一组方法为主队列打上标记:
+ (BOOL)isMainQueue {
static const void* mainQueueKey = @"mainQueue";
static void* mainQueueContext = @"mainQueue";
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
dispatch_queue_set_specific(dispatch_get_main_queue(), mainQueueKey, mainQueueContext, nil);
});
return dispatch_get_specific(mainQueueKey) == mainQueueContext;
}
用 isMainQueue 方法代替 [NSThread isMainThread] 即可获得更好的安全性。
9. HTTPS
HTTPS 从最终的数据解析的角度,与 HTTP 没有任何的区别,HTTPS 就是将 HTTP 协议数据包放到 SSL/TSL 层加密后,在 TCP/IP 层组成 IP 数据报去传输,以此保证传输数据的安全;而对于接收端,在 SSL/TSL 将接收的数据包解密之后,将数据传给 HTTP 协议层,就是普通的 HTTP 数据。HTTP 和 SSL/TSL 都处于 OSI 模型的应用层。
SSL/TSL : 通过四次握手,主要交换三个信息:
数字证书
三个随机数
加密通信协议
10. SDWebImage
- UIImageView + WebCache
- SDWebImageManager
- SDImageCache
- SDWebImageDownloader
- SDWebImageDownloaderOperation
SDWebImage 如何为 UIImageView 添加图片(面试回答)
SDWebImage 中为 UIImageView 提供了一个分类叫做 WebCache, 这个分类中有一个最常用的接口, sd_setImageWithURL:placeholderImage:
, 这个分类同时提供了很多类似的方法, 这些方法最终会调用一个同时具有 option
progressBlock
completionBlock
的方法, 而在这个类最终被调用的方法首先会检查是否传入了 placeholderImage
以及对应的参数, 并设置 placeholderImage
.
然后会获取 SDWebImageManager
中的单例调用一个 downloadImageWithURL:...
的方法来获取图片, 而这个 manager
获取图片的过程有大体上分为两部分, 它首先会在 SDImageCache
中寻找图片是否有对应的缓存, 它会以 url 作为数据的索引先在内存中寻找是否有对应的缓存, 如果缓存未命中就会在磁盘中利用 MD5 处理过的 key 来继续查询对应的数据, 如果找到了, 就会把磁盘中的缓存备份到内存中.
然而, 假设我们在内存和磁盘缓存中都没有命中, 那么 manager 就会调用它持有的一个 SDWebImageDownloader
对象的方法 downloadImageWithURL:...
来下载图片, 这个方法会在执行的过程中调用另一个方法 addProgressCallback:andCompletedBlock:forURL:createCallback:
来存储下载过程中和下载完成的回调, 当回调块是第一次添加的时候, 方法会实例化一个 NSMutableURLRequest
和 SDWebImageDownloaderOperation
, 并将后者加入 downloader
持有的下载队列开始图片的异步下载.
而在图片下载完成之后, 就会在主线程设置 image
属性, 完成整个图像的异步下载和配置.
SDWebImage 的图片加载过程其实很符合我们的直觉:
- 查看缓存
- 缓存命中
- 返回图片
- 更新 UIImageView
- 缓存未命中
- 异步下载图片
- 加入缓存
- 更新 UIImageView
10. @synthesize 和 @dynamic 分别有什么作用?
@property有两个对应的词,一个是 @synthesize,一个是 @dynamic。如果 @synthesize和 @dynamic都没写,那么默认的就是@syntheszie var = _var;
@synthesize 的语义是如果你没有手动实现 setter 方法和 getter 方法,那么编译器会自动为你加上这两个方法。
@dynamic 告诉编译器:属性的 setter 与 getter 方法由用户自己实现,不自动生成。(当然对于 readonly 的属性只需提供 getter 即可)。假如一个属性被声明为 @dynamic var,然后你没有提供 @setter方法和 @getter 方法,编译的时候没问题,但是当程序运行到 instance.var = someVar,由于缺 setter 方法会导致程序崩溃;或者当运行到 someVar = var 时,由于缺 getter 方法同样会导致崩溃。编译时没问题,运行时才执行相应的方法,这就是所谓的动态绑定。
当你同时重写了 setter 和 getter 时,系统就不会生成 ivar(实例变量/成员变量)。这时候有两种选择:
手动创建 ivar
使用@synthesize foo = _foo; ,关联 @property 与 ivar。
11. objc中的类方法和实例方法有什么本质区别和联系?
类方法:
类方法是属于类对象的
类方法只能通过类对象调用
类方法中的self是类对象
类方法可以调用其他的类方法
类方法中不能访问成员变量
类方法中不能直接调用对象方法
实例方法:
实例方法是属于实例对象的
实例方法只能通过实例对象调用
实例方法中的self是实例对象
实例方法中可以访问成员变量
实例方法中直接调用实例方法
实例方法中也可以调用类方法(通过类名)
12. BAD_ACCESS在什么情况下出现?
访问了野指针,比如对一个已经释放的对象执行了 release、访问已经释放对象的成员变量或者发消息,死循环
13. KVO
addObserver:forKeyPath:options:context:
各个参数的作用分别是什么,observer中需要实现哪个方法才能获得KVO回调?
// 添加键值观察
/*
1 观察者,负责处理监听事件的对象
2 观察的属性
3 观察的选项
4 上下文
*/
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"Person Name"];
observer
中需要实现以下方法:
// 所有的 kvo 监听到事件,都会调用此方法
/*
1. 观察的属性
2. 观察的对象
3. change 属性变化字典(新/旧)
4. 上下文,与监听的时候传递的一致
*/
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context;
- 如何手动触发一个 value 的 KVO
所谓的“手动触发”是区别于“自动触发”:
自动触发是指类似这种场景:在注册 KVO 之前设置一个初始值,注册之后,设置一个不一样的值,就可以触发了。
想知道如何手动触发,必须知道自动触发 KVO 的原理:
键值观察通知依赖于 NSObject 的两个方法: willChangeValueForKey:
和 didChangevlueForKey:
。在一个被观察属性发生改变之前, willChangeValueForKey:
一定会被调用,这就 会记录旧的值。而当改变发生后, observeValueForKey:ofObject:change:context:
会被调用,继而 didChangeValueForKey:
也会被调用。如果可以手动实现这些调用,就可以实现“手动触发”了。
那么“手动触发”的使用场景是什么?一般我们只在希望能控制“回调的调用时机”时才会这么做。
具体做法如下:
如果这个 value 是 表示时间的 self.now ,那么代码如下:最后两行代码缺一不可。
相关代码已放在仓库里。
// .m文件
// 手动触发 value 的KVO,最后两行代码缺一不可。
//@property (nonatomic, strong) NSDate *now;
- (void)viewDidLoad {
[super viewDidLoad];
_now = [NSDate date];
[self addObserver:self forKeyPath:@"now" options:NSKeyValueObservingOptionNew context:nil];
NSLog(@"1");
[self willChangeValueForKey:@"now"]; // “手动触发self.now的KVO”,必写。
NSLog(@"2");
[self didChangeValueForKey:@"now"]; // “手动触发self.now的KVO”,必写。
NSLog(@"4");
}
但是平时我们一般不会这么干,我们都是等系统去“自动触发”。“自动触发”的实现原理:
比如调用 setNow:
时,系统还会以某种方式在中间插入 wilChangeValueForKey:
、 didChangeValueForKey:
和 observeValueForKeyPath:ofObject:change:context:
的调用。
- apple 用什么方式实现对一个对象的 KVO?
当你观察一个对象时,一个新的类会被动态创建。这个类继承自该对象的原本的类,并重写了被观察属性的 setter 方法。重写的 setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象:值的更改。最后通过 isa 混写(isa-swizzling) 把这个对象的 isa 指针 ( isa 指针告诉 Runtime 系统这个对象的类是什么 ) 指向这个新创建的子类,对象就神奇的变成了新创建的子类的实例。
14. IBOutlet 连出来的视图属性为什么可以被设置成 weak?
既然有外链那么视图在 xib 或者 storyboard 中肯定存在,视图已经对它有一个强引用了。
不过这个回答漏了个重要知识,使用 storyboard(xib不行)创建的 vc,会有一个叫_topLevelObjectsToKeepAliveFromStoryboard
的私有数组强引用所有 top level 的对象,所以这时即便 outlet 声明成 weak 也没关系
15. UIView 和 CALayer 有什么关系?
每一个 UIView 的身后对应一个 Core Animation 框架中的 CALayer. 每一个 UIView 都是 CALayer 的代理.
Many of the methods you call on UIView simply delegate to the layer
在 iOS 上当你处理一个又一个的 UIView 时, 实际上是在操作 CALayer. 尽管有的时候你并不知道 (直接操作 CALayer 并不会对效率有着显著的提升).
UIView 实际上就是对 CALayer 的轻量级的封装. UIView 继承自 UIResponder, 用来处理来自用户的事件; CALayer 继承自 NSObject 主要用于处理图层的渲染和动画. 这么设计有以下几个原因:
你可以通过操作 UIView 在一个更高的层级上处理与用户的交互, 触摸, 点击, 拖拽等事件, 这些都是在 UIKit 这个层级上完成的.
UIView 和 NSView(AppKit) 的实现极其不同, 而使用 Core Animation 可以实现底层代码地重用, 在 Mac 和 iOS 平台上都使用着近乎相同的 Core Animation 代码, 这样我们可以对这个层级进行抽象在两种平台上产生 UIKit 和 AppKit 用于不同平台的框架.
使用 CALayer 的唯一原因大概是便于移植到不同的平台, 如果仅仅使用 Core Animation 层级进行开发, 处理用户的交互时间需要写更多的代码.
15. OS X 和 iOS 中的并发编程
pthread 、 NSThread 、GCD 、NSOperationQueue,以及 NSRunLoop。
建议采纳的安全模式是这样的:从主线程中提取出要使用到的数据,并利用一个操作队列在后台处理相关的数据,最后回到主队列中来发送你在后台队列中得到的结果。使用这种方式,你不需要自己做任何锁操作,这也就大大减少了犯错误的几率。