乐虎游戏|乐虎国际登录|欢迎你

iOS 面试题一

日期:2020-04-15编辑作者:计算机资讯

出处:先是程序员,然后才是iOS程序员 — 写给广大非科班iOS开发者的一篇面试总结如果让你实现属性的weak,如何实现的?如果让你来实现属性的atomic,如何实现?KVO为什么要创建一个子类来实现?类结构体的组成,isa指针指向了什么?(这里应该将元类和根元类也说一下)RunLoop有几种事件源?有几种模式?方法列表的数据结构是什么?分类是如何实现的?它为什么会覆盖掉原来的方法?为什么分类不能添加实例变量

属性

属性与成员变量之间的关系

  • 属性对成员变量扩充了存储方法
  • 属性默认会生成带下划线的成员变量
  • 声明了成员变量不会生成属性

成员变量地址可以根据实例的内存地址偏移寻址。而属性的读写都需要函数调用,相对更慢。self对应类的成员变量的首地址

1.weak和assign区别

修饰变量类型的区别:

weak 只可以修饰对象。如果修饰基本数据类型,编译器会报错-“Property with ‘weak’ attribute must be of object type”。

assign 可修饰对象和基本数据类型。当需要修饰对象类型时,MRC时代使用unsafe_unretained。当然,unsafe_unretained也可能产生野指针,所以它名字是"unsafe_”。

是否产生野指针的区别:

weak 不会产生野指针问题。因为weak修饰的对象释放后(引用计数器值为0),指针会自动被置nil,之后再向该对象发消息也不会崩溃。 weak是安全的。

assign 如果修饰对象,会产生野指针问题;如果修饰基本数据类型则是安全的。修饰的对象释放后,指针不会自动被置空,此时向对象发消息会崩溃。

总结:

assign 适用于基本数据类型如int,float,struct等值类型,不适用于引用类型。因为值类型会被放入栈中,遵循先进后出原则,由系统负责管理栈内存。而引用类型会被放入堆中,需要我们自己手动管理内存或通过ARC管理。

weak 适用于delegate和block等引用类型,不会导致野指针问题,也不会循环引用,非常安全。

值类型会被放入栈中,遵循先进后出原则,由系统负责管理栈内存。而引用类型会被放入堆中,需要我们自己手动管理内存或通过ARC管理。

一. 如果让你实现属性的weak,如何实现的?

  • 要实现weak属性,首先要搞清楚weak属性的特点:

weak 此特质表明该属性定义了一种“非拥有关系”,为这种属性所修饰的值设置新值时,设置方法既不保留新值,也不释放旧值。此特质同assign类似,然而在属性所指对象遭到摧毁时,属性值也会清空。

先看下runtime 里源码实现:

/*** The internal structure stored in the weak references table. * It maintains and stores* a hash set of weak references pointing to an object.* If out_of_line==0, the set is instead a small inline array.*/#define WEAK_INLINE_COUNT 4struct weak_entry_t { DisguisedPtr<objc_object> referent; union { struct { weak_referrer_t *referrers; uintptr_t out_of_line : 1; uintptr_t num_refs : PTR_MINUS_1; uintptr_t mask; uintptr_t max_hash_displacement; }; struct { // out_of_line=0 is LSB of one of these (don't care which) weak_referrer_t inline_referrers[WEAK_INLINE_COUNT]; }; };};/*** The global weak references table. Stores object ids as keys,* and weak_entry_t structs as their values.*/struct weak_table_t { weak_entry_t *weak_entries; size_t num_entries; uintptr_t mask; uintptr_t max_hash_displacement;};

我们可以设计一个函数来表示上述机制:

objc_storeWeak(&a, b)函数:

objc_storeWeak函数把第二个参数--赋值对象的内存地址作为键值`key`,将第一个参数--`weak`修饰的属性变量的内存地址作为value,注册到 `weak`表中。如果第二个参数为0,那么把变量的内存地址`从weak`表中删除,

你可以把objc_storeWeak(&a, b)理解为:objc_storeWeak(value, key),并且当keynil,将valuenil

bnil时,ab指向同一个内存地址,在bnil时,anil。此时向a发送消息不会崩溃:在Objective-C中向nil发送消息是安全的。

而如果a是由 assign 修饰的,则: 在 bnil 时,ab 指向同一个内存地址,在 bnil 时,a 还是指向该内存地址,变野指针。此时向 a 发送消息极易崩溃。

下面我们将基于objc_storeWeak(&a, b)函数,使用伪代码模拟“runtime如何实现weak属性”:

// 使用伪代码模拟:runtime如何实现weak属性// http://weibo.com/luohanchenyilong/// https://github.com/ChenYilong id obj1; objc_initWeak(&obj1, obj);/*obj引用计数变为0,变量作用域结束*/ objc_destroyWeak(&obj1);

下面对用到的两个方法objc_initWeakobjc_destroyWeak做下解释:

总体说来,作用是: 通过objc_initWeak函数初始化“附有weak修饰符的变量”,在变量作用域结束时通过`objc_destoryWeak`函数释放该变量

下面分别介绍下方法的内部实现:

objc_initWeak函数的实现是这样的:在将“附有weak修饰符的变量”初始化为`0`后,会将“赋值对象”作为参数,调用objc_storeWeak函数。

obj1 = 0;obj_storeWeak(&obj1, obj);

也就是说:

weak 修饰的指针默认值是 nil (在Objective-C中向nil发送消息是安全的)

然后obj_destroyWeak函数将0作为参数,调用objc_storeWeak函数。

objc_storeWeak(&obj1, 0);

前面的源代码与下列源代码相同。

// 使用伪代码模拟:runtime如何实现weak属性// http://weibo.com/luohanchenyilong/// https://github.com/ChenYilongid obj1;obj1 = 0;objc_storeWeak(&obj1, obj);/* ... obj的引用计数变为0,被置nil ... */objc_storeWeak(&obj1, 0);

objc_storeWeak 函数把第二个参数--赋值对象的内存地址作为键值,将第一个参数--`weak`修饰的属性变量的内存地址注册到 weak表中。如果第二个参数为`0`,那么把变量的地址从 weak 表中删除。

使用伪代码是为了方便理解,下面我们“真枪实弹”地实现下:

如何让不使用weak修饰的@property,拥有weak的效果。

我们从setter方法入手:

(注意以下的cyl_runAtDealloc方法实现仅仅用于模拟原理,如果想用于项目中,还需要考虑更复杂的场景,想在实际项目使用的话,可以使用我写的一个小库,可以使用 CocoaPods在项目中使用:CYLDeallocBlockExecutor)

- setObject:(NSObject *)object{ objc_setAssociatedObject(self, "object", object, OBJC_ASSOCIATION_ASSIGN); [object cyl_runAtDealloc:^{ _object = nil; }];}

也就是有两个步骤:

  1. setter方法中做如下设置:
 objc_setAssociatedObject(self, "object", object, OBJC_ASSOCIATION_ASSIGN);
  1. 在属性所指的对象遭到摧毁时,属性值也会清空`。做到这点,同样要借助runtime`:
//要销毁的目标对象id objectToBeDeallocated;//可以理解为一个“事件”:当上面的目标对象销毁时,同时要发生的“事件”。id objectWeWantToBeReleasedWhenThatHappens;objc_setAssociatedObject(objectToBeDeallocted, someUniqueKey, objectWeWantToBeReleasedWhenThatHappens, OBJC_ASSOCIATION_RETAIN);

知道了思路,我们就开始实现cyl_runAtDealloc方法,实现过程分两部分:

第一部分:创建一个类,可以理解为一个“事件”:当目标对象销毁时,同时要发生的“事件”。借助block执行“事件”。

.h文件

// .h文件// http://weibo.com/luohanchenyilong/// https://github.com/ChenYilong// 这个类,可以理解为一个“事件”:当目标对象销毁时,同时要发生的“事件”。借助block执行“事件”。typedef void (^voidBlock);@interface CYLBlockExecutor : NSObject- initWithBlock:(voidBlock)block;@end

.m文件

// .m文件// http://weibo.com/luohanchenyilong/// https://github.com/ChenYilong// 这个类,可以理解为一个“事件”:当目标对象销毁时,同时要发生的“事件”。借助`block`执行“事件”。#import "CYLBlockExecutor.h"@interface CYLBlockExecutor() { voidBlock _block;}@implementation CYLBlockExecutor- initWithBlock:(voidBlock)aBlock{ self = [super init]; if  { _block = [aBlock copy]; } return self;}- dealloc{ _block ? _block() : nil;}@end

第二部分:核心代码:利用runtime实现cyl_runAtDealloc方法

// CYLNSObject+RunAtDealloc.h文件// http://weibo.com/luohanchenyilong/// https://github.com/ChenYilong// 利用runtime实现cyl_runAtDealloc方法#import "CYLBlockExecutor.h"const void *runAtDeallocBlockKey = &runAtDeallocBlockKey;@interface NSObject (CYLRunAtDealloc)- cyl_runAtDealloc:(voidBlock)block;@end// CYLNSObject+RunAtDealloc.m文件// http://weibo.com/luohanchenyilong/// https://github.com/ChenYilong// 利用runtime实现cyl_runAtDealloc方法#import "CYLNSObject+RunAtDealloc.h"#import "CYLBlockExecutor.h"@implementation NSObject (CYLRunAtDealloc)- cyl_runAtDealloc:(voidBlock)block{ if  { CYLBlockExecutor *executor = [[CYLBlockExecutor alloc] initWithBlock:block]; objc_setAssociatedObject(self, runAtDeallocBlockKey, executor, OBJC_ASSOCIATION_RETAIN); }}@end

使用方法: 导入

 #import "CYLNSObject+RunAtDealloc.h"

然后就可以使用了:

NSObject *foo = [[NSObject alloc] init];[foo cyl_runAtDealloc:^{ NSLog(@"正在释放foo!");}];

如果对cyl_runAtDealloc的实现原理有兴趣,可以看下我写的一个小库,可以使用 CocoaPods 在项目中使用:CYLDeallocBlockExecutor

具体详见:《招聘一个靠谱的iOS》

类别

利用OC的动态运行时为现有类添加方法,您可以在先有类中添加属性,但是不能添加成员变量,但是我们的编译器会把属性自动生成存储方法和带下划线的成员变量,所以我们的声明的属性必须是@dynamic类型的,及需要重写存储方法。

#import "Person.h"

@interface Person (Ex)

@property(nonatomic, strong)    NSString *name;

- (void)setName:(NSString *)name; //***
- (NSString *)name;//***

- (void)method_one;
@end

分类方法实现中可以访问原来类中声明的成员变量。

类别的优缺点

  • 不破坏原有的类的基础之上扩充方法。
  • 实现代码之间的解偶

缺点

  • 无法在类别中添加新的实例变量,类别中没有空间容纳实例变量
  • 命名冲突
    类别无法添加实例变量的原因,从Runtime角度,我们发现的确没有空间容纳实力变量,为什么可以添加属性,原因是属性的本质是实例变量+ getter方法 + setter方法;所以我们重写了set/get方法

同名方法调用的优先级为 分类 > 本类 > 父类

/* 
 *  Category Template //类别模版
 */
typedef struct objc_category *Category;

struct objc_category {
    char *category_name;
    char *class_name;
    struct objc_method_list *instance_methods;
    struct objc_method_list *class_methods;
    struct objc_protocol_list *protocols;
};

2.对象的生命周期

在对象的创建和初始化之后,只要对象的retainCount的值比0大,那么它就会一直存在在内存中。通过向一个对象发送retain消息,或者进行copy操作。其他的对象可以引用并持有该对象的所有权。同时,移除引用的时候要发送release消息。

图片 1

object_lifecycle@2x.png

二. 如果让你来实现属性的atomic,如何实现?

atomic特点:

系统生成getter/setter方法会保证get、set操作的完整性,不受其他线程的影响。同时atomic是默认属性,会有一定的系统开销。

但是atomic所说的线程安全只是保证了gettersetter存取方法的线程安全,并不能保证整个对象是线程安全的。

假设有一个 atomic 的属性 name,如果线程 A[self setName:@"A"]线程 B[self setName:@"B"]线程 C[self name],那么所有这些不同线程上的操作都将依次顺序执行——也就是说,如果一个线程正在执行getter/setter,其他线程就得等待。因此,属性 name 是读/写安全的。

但是,如果有另一个线程 D 同时在调[name release],那可能就会crash,因为 release 不受 getter/setter 操作的限制。也就是说,这个属性只能说是读/写安全的,但并不是线程安全的,因为别的线程还能进行读写之外的其他操作。线程安全需要开发者自己来保证。

如果 name 属性是nonatomic 的,那么上面例子里的所有线程 A、B、C、D 都可以同时执行,可能导致无法预料的结果。如果是 atomic 的,那么 A、B、C 会串行,而D 还是并行的。

实现automic属性:

//@property(automic, retain) UITextField *userName;//系统生成的代码如下:- (UITextField *) userName { @synchronized { return _userName; }}-  setUserName:(UITextField *)userName { @synchronized { if(userName != _userName) { [_userName release]; _userName = [userName_ retain]; } }}

nonatomic 实现:

//@property(nonatomic, retain) UITextField *userName;//系统生成的代码如下:- (UITextField *) userName { return _userName;}-  setUserName:(UITextField *)userName { if(userName != _userName) { [_userName release]; _userName = [userName_ retain]; }}

详见: [爆栈热门 iOS 问题] atomic 和 nonatomic 有什么区别?

扩展

没有名字的类别

类别中创建的方法和属性都是私有的,只有这个类对象可以使用

优点

  • 可以添加实例变量
  • 可以更改读写权限(但是更改的权限的存储方法只能是私有的)
#import <Foundation/Foundation.h>

@interface Person : NSObject

@property (nonatomic,readonly)  NSString *name; //只有getter方法,是公有的

@end


#import "Person.h"

@interface Person ()

@property (nonatomic, readwrite) NSString *name; //更改name的权限,但是setter方法是私有的不能被外部访问

@end

@implementation Person

@end

类别和扩展的区别

就category和extension的区别来看,我们可以推导出一个明显的事实,extension可以添加实例变量,而category是无法添加实例变量的(因为在运行期,对象的内存布局已经确定,如果添加实例变量就会破坏类的内部布局,这对编译型语言来说是灾难性的)extension在编译期决议,它就是类的一部分,但是category则完全不一样,它是在运行期决议的。extension在编译期和头文件里的@interface以及实现文件里的@implement一起形成一个完整的类,它、extension伴随类的产生而产生,亦随之一起消亡。

  • extension一般用来隐藏类的私有信息,你必须有一个类的源码才能为一个类添加extension,所以你无法为系统的类比如NSString添加extension,除非创建子类再添加extension。而category不需要有类的源码,我们可以给系统提供的类添加category。
  • extension可以添加实例变量,而category不可以。
  • extension和category都可以添加属性,但是category的属性不能生成成员变量和getter、setter方法的实现。
    Extension
    在编译器决议,是类的一部分,在编译器和头文件的@interface和实现文件里的@implement一起形成了一个完整的类。
    伴随着类的产生而产生,也随着类的消失而消失。
    Extension一般用来隐藏类的私有消息,你必须有一个类的源码才能添加一个类的Extension,所以对于系统一些类,如NSString,就无法添加类扩展
    Category
    是运行期决议的
    类扩展可以添加实例变量,分类不能添加实例变量
    原因:因为在运行期,对象的内存布局已经确定,如果添加实例变量会破坏类的内部布局,这对编译性语言是灾难性的。

3.对象是如何初始化的

整个对象的初始化过程其实只是为一个分配内存空间,并且初始化 isa_t 结构体的过程

alloc的实现:

直接调用了另一个私有方法 id _objc_rootAlloc(Class cls)。id _objc_rootAlloc(Class cls)调用了callAlloc。

init方法:

init 方法只是调用了 _objc_rootInit 并返回了当前对象。

三. KVO为什么要创建一个子类来实现?

基本的原理:

当观察某对象A时,KVO机制动态创建一个对象A当前类的子类,并为这个新的子类重写了被观察属性keyPathsetter 方法。setter方法随后负责通知观察对象属性的改变状况。同时子类的class方法也会重写为返回父类的class。深入剖析:

Apple 使用了isa 混写(isa-swizzling)来实现KVO。当观察对象A时,KVO机制动态创建一个新的名为: NSKVONotifying_A的新类,该类继承自对象A的本类,且KVONSKVONotifying_A重写观察属性的setter 方法,setter 方法会负责在调用原setter 方法之前和之后,通知所有观察对象属性值的更改情况。

(备注: isa 混写(isa-swizzling)isa:is a kind of ; swizzling:混合,搅合;)

NSKVONotifying_A类剖析:在这个过程,被观察对象的 isa 指针从指向原来的A类,被KVO机制修改为指向系统新创建的子类 NSKVONotifying_A类,来实现当前类属性值改变的监听;

所以当我们从应用层面上看来,完全没有意识到有新的类出现,这是系统“隐瞒”了对KVO的底层实现过程,让我们误以为还是原来的类。但是此时如果我们创建一个新的名为“NSKVONotifying_A”的类(),就会发现系统运行到注册KVO的那段代码时程序就崩溃,因为系统在注册监听的时候动态创建了名为NSKVONotifying_A的中间类,并指向这个中间类了。

isa 指针的作用:每个对象都有isa指针,指向该对象的类,它告诉Runtime系统这个对象的类是什么。所以对象注册为观察者时,isa指针指向新子类,那么这个被观察的对象就神奇地变成新子类的对象 因而在该对象上对 setter的调用就会调用已重写的 setter,从而激活键值通知机制。

②子类setter方法剖析:KVO的键值观察通知依赖于 NSObject的两个方法:willChangeValueForKey:didChangevlueForKey:,在存取数值的前后分别调用2个方法:

被观察属性发生改变之前,willChangeValueForKey:被调用,通知系统该 keyPath 的属性值即将变更;当改变发生后, didChangeValueForKey:被调用,通知系统该keyPath的属性值已经变更;之后,observeValueForKey:ofObject:change:context:也会被调用。且重写观察属性的setter 方法这种继承方式的注入是在运行时而不是编译时实现的。

-setName:(NSString *)newName { [self willChangeValueForKey:@"name"]; //KVO在调用存取方法之前总调用 [super setValue:newName forKey:@"name"]; //调用父类的存取方法 [self didChangeValueForKey:@"name"]; //KVO在调用存取方法之后总调用}

既然是重写,就有两种选择: 改变本类和改变子类

  • 改变本类,就会污染到本类的所有其他对象的方法,显然这种做法是不可取的
  • 改变子类, 只针对被添加KVO监听的类创建子类,同时对该子类的setter和class方法的进行重写,这样就不需要担心影响到本类的其他对象,会因为方法的修改而导致bug.

具体详见: iOS--KVO的实现原理与具体应用

非正式协议

创建一个NSObject的类别称为"非正式协议"

4.一个objc对象的isa指针指向什么?有什么作用

图片 2

isa_@2x.png

对象的isa指向类,类的isa指向元类(meta class),元类isa指向元类的根类。

isa:是一个Class 类型的指针. 每个实例对象有个isa的指针,他指向对象的类,而Class里也有个isa的指针, 指向meteClass(元类)。元类保存了类方法的列表。当类方法被调用时,先会从本身查找类方法的实现,如果没有,元类会向他父类查找该方法。同时注意的是:元类(meteClass)也是类,它也是对象。元类也有isa指针,它的isa指针最终指向的是一个根元类(root meteClass).根元类的isa指针指向本身,这样形成了一个封闭的内循环。

四. 类结构体的组成,isa指针指向了什么?(这里应该将元类和根元类也说一下)

  • classobject 的定义
typedef struct objc_class *Class;typedef struct objc_object *id;@interface Object { Class isa; }@interface NSObject <NSObject> { Class isa OBJC_ISA_AVAILABILITY;}struct objc_object {private: isa_t isa;}struct objc_class : objc_object { // Class ISA; Class superclass; cache_t cache; // formerly cache pointer and vtable class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags}union isa_t { isa_t() { } isa_t(uintptr_t value) : bits { } Class cls; uintptr_t bits;}

把源码的定义转化成类图,如下:

图片 3image.png

从源码中可以看出,Objective-C对象都是C语言结构体实现的,在·objc2.0·中,所有的对象都会包含一个·isa_t·类型的结构体。

objc_object被源码typedefid类型,这也就是平常所用的id类型,这个结构体中只包含一个isa_t类型的结构体。

objc_class继承自objc_object。所以在objc_class中也会包含isa_t类型的结构体isa。至此,可以得出: Objective-C中类也是一个对象。在objc_class中,除了isa之外,还有3个成员变量,一个是父类的指针,一个是方法缓存,最后一个是这个类的实例方法链表。

  • isa指针指向

当一个对象的实例方法被调用的时候,会通过isa找到相对应的类,然后在该类的class_data_bits_t中去查找方法。class_data_bits_t是指向了类对象的数据区域。在该数据区域内查找相应方法的对应实现。

同样当我们调用类方法的时候,类对象的isa里面是什么呢?这里为了和对象查找方法的机制一致,遂引入了元类(meta-class)的概念。

在引入元类之后,类对象和对象查找方法的机制就完全统一了。

对象的实例方法调用时,通过对象的 isa 在类中获取方法的实现。类对象的类方法调用时,通过类的 isa 在元类中获取方法的实现。

meta-class之所以重要,是因为它存储着一个类的所有类方法。每个类都会有一个单独的meta-class,因为每个类的类方法基本不可能完全相同。

对象,类,元类之间的关系图如下:

图片 4image.png

图中实线是super_class指针,虚线是isa指针。

  1. 根类Root class其实就是NSObject,NSObject是没有超类的,所以根类Root classsuperclass指向nil

  2. 每个类Class都有一个isa指针指向唯一的元类(Meta class)

  3. 根元类Root class(meta)的superclass指向Root class,也就是NSObject,形成一个回路。

  4. 每个元类Meta classisa指针都指向Root class

具体详见神经病院Objective-C Runtime入院第一天——isa和Class

正式协议

正式协议中可以方法,同时协议也是可以继承的

@protocal MySuperDuberProtocol <MyParentProtocol>

@optional //可选

@required //必须要实现

@end

5.runtime怎么添加属性、方法等

  • ivar表示成员变量
  • class_addIvar
  • class_addMethod
  • class_addProperty
  • class_addProtocol
  • class_replaceProperty

五. RunLoop有几种事件源?有几种模式?

  • RunLoop的事件源CoreFoundation 里面关于 RunLoop5个类:
CFRunLoopRef - 获得当前RunLoop和主RunLoopCFRunLoopModeRef RunLoop - 运行模式,只能选择一种,在不同模式中做不同的操作CFRunLoopSourceRef - 事件源,输入源CFRunLoopTimerRef - 定时器时间CFRunLoopObserverRef - 观察者

其中 CFRunLoopModeRef 类并没有对外暴露,只是通过 CFRunLoopRef 的接口进行了封装。他们的关系如下:

图片 5image.png

一个RunLoop包含若干个 Mode,每个 Mode又包含若干个 Source/Timer/Observer。每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。如果需要切换 Mode,只能退出Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。

CFRunLoopSourceRef 是事件产生的地方。Source有两个版本:Source0Source1

Source0 只包含了一个回调,它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal,将这个Source 标记为待处理,然后手动调用 CFRunLoopWakeUp 来唤醒 RunLoop,让其处理这个事件。

Source1 包含了一个 mach_port 和一个回调,被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程,其原理在下面会讲到。

CFRunLoopTimerRef是基于时间的触发器,它和 NSTimertoll-free bridged 的,可以混用。其包含一个时间长度和一个回调。当其加入到RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。

CFRunLoopObserverRef是观察者,每个 Observer 都包含了一个回调,当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。可以观测的时间点有以下几个:

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { kCFRunLoopEntry = (1UL << 0), // 即将进入Loop kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠 kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒 kCFRunLoopExit = (1UL << 7), // 即将退出Loop};

上面的 Source/Timer/Observer 被统称为 mode item,一个 item可以被同时加入多个 mode。但一个item 被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。

  • RunLoopModel

系统默认注册了5Mode:1.kCFRunLoopDefaultMode: App的默认 Mode,通常主线程是在这个 Mode 下运行的。

  1. UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView追踪触摸滑动,保证界面滑动时不受其他 Mode影响。
  2. UIInitializationRunLoopMode: 在刚启动App 时第进入的第一个 Mode,启动完成后就不再使用。4: GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到。5: kCFRunLoopCommonModes: 这是一个占位的 Mode,作为标记kCFRunLoopDefaultModeUITrackingRunLoopMode用,并不是一种真正的Mode

详见:深入理解RunLoop

委托

委托就是某个对象指定另一个对象处理某些特定事物的设计模式

代理主要由三部分组成:

  • 协议:用来指定代理双方可以做什么,必须做什么。
  • 代理:根据指定的协议,完成委托方需要实现的功能。
  • 委托:根据指定的协议,指定代理去完成什么功能。

这里用一张图来阐述一下三方之间的关系:

图片 6

代理使用原理

在iOS中代理的本质就是代理对象内存的传递和操作,我们在委托类设置代理对象后,实际上只是用一个id类型的指针将代理对象进行了一个弱引用。委托方让代理方执行操作,实际上是在委托类中向这个id类型指针指向的对象发送消息,而这个id类型指针指向的对象,就是代理对象。

图片 7

代理原理

代理内存管理

为什么我们设置代理属性都使用weak呢?

我们定义的指针默认都是__strong类型的,而属性本质上也是一个成员变量和set、get方法构成的,strong类型的指针会造成强引用,必定会影响一个对象的生命周期,这也就会形成循环引用。

图片 8

6.runtime 如何实现 weak 属性

weak策略表明该属性定义了一种“非拥有关系” (nonowning relationship)。为这种属性设置新值时,设置方法既不保留新值,也不释放旧值。此特质同assign类似;然而在属性所指的对象遭到摧毁时,属性值也会清空(nil out)。

六. 方法列表的数据结构是什么?

struct objc_method_list { /* 这个变量用来链接另一个单独的方法链表 */ struct objc_method_list * _Nullable 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;} 

Block

原理:

//最基础的结构体实现
 void (^Blk)(void) = ^(void) {
     };

    Blk();

clang -rewrite-objc main.m后得到的结果

/*
// __block_impl 是 block 实现的结构体
struct __block_impl
{
    void *isa; //说明block是一个对象来实现的
    int Flags; //按位承载 block 的附加信息;
    int Reserved; //保留变量
    void *FuncPtr; //函数指针,指向Block需要执行的函数
};
*/

// __main_block_impl_0 是 block 实现的结构体,也是 block 实现的入口
struct __main_block_impl_0 {
  struct __block_impl impl; //实现的结构体变量及__block_impl
  struct __main_block_desc_0* Desc; //描述结构体变量
  //结构体的构造函数,初始化结构体变量impl、Desc
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

//__main_block_func_0最终需要执行的函数代码块
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

     }

// __main_block_desc_0 是 block 的描述信息结构体
static struct __main_block_desc_0 {
  size_t reserved; //结构体信息保留字段
  size_t Block_size; //结构体大小
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)}; //定义一个结构体变量,初始化结构体,计算结构体大小

int main(int argc, const char * argv[]) {

    void (*Blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

    ((void (*)(__block_impl *))((__block_impl *)Blk)->FuncPtr)((__block_impl *)Blk);
}

isa 指向实例对象,表明 block 本身也是一个 Objective-C 对象。block 的三种类型:_NSConcreteStackBlock、_NSConcreteGlobalBlock、_NSConcreteMallocBlock,即当代码执行时,isa 有三种值

  • impl.isa = &_NSConcreteStackBlock;
  • impl.isa = &_NSConcreteMallocBlock;
  • impl.isa = &_NSConcreteGlobalBlock;
  1. NSConcreteGlobalBlock 全局的静态 block,不会访问任何外部变量。
  2. _NSConcreteStackBlock 保存在栈中的 block,当函数返回时会被销毁。
  3. _NSConcreteMallocBlock 保存在堆中的 block,当引用计数为 0 时会被销毁。

block 实现的执行流程

main() >> 调用__main_block_impl_0构造函数初始化结构体__main__block_impl_0(__main_block_func_0, __main_block_desc_0_DATA) >> 得到的__main_block_impl_0类型变量赋值给Blk >> 执行Blk->FuncPtr()函数 >> END

带参数的Block

    int intValue = 1;

    void (^Blk)(void) = ^(void) {
        NSLog(@"%d",intValue);
     };

    Blk();

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int intValue;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _intValue, int flags=0) : intValue(_intValue) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int intValue = __cself->intValue; // bound by copy

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_r__t4y1n4fj5xlgntt308jvn7c80000gn_T_main_be769b_mi_0,intValue);
     }

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(int argc, const char * argv[]) {

    int intValue = 1;

    void (*Blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, intValue));

    ((void (*)(__block_impl *))((__block_impl *)Blk)->FuncPtr)((__block_impl *)Blk);
}

原来 block 通过参数值传递获取到 intValue 变量,通过函数

__main_block_impl_0 (void *fp, struct __main_block_desc_0 *desc, int _intValue, int flags=0) : intValue(_intValue)

保存到 __main_block_impl_0 结构体的同名变量 intValue,通过代码 int intValue = __cself->intValue; 取出 intValue,打印出来。

构造函数 __main_block_impl_0 冒号后的表达式 intValue(_intValue) 的意思是,用 _intValue 初始化结构体成员变量 intValue。

有四种情况下应该使用初始化表达式来初始化成员:

  1. 初始化const成员
  2. 初始化引用成员
  3. 当调用基类的构造函数,而它拥有一组参数时
  4. 当调用成员类的构造函数,而它拥有一组参数时

那么runtime如何实现weak变量的自动置nil

runtime对注册的类会进行布局,会将 weak 对象放入一个 hash 表中。用 weak 指向的对象内存地址作为 key,当此对象的引用计数为0的时候会调用对象的 dealloc 方法,假设 weak 指向的对象内存地址是a,那么就会以a为key,在这个 weak hash表中搜索,找到所有以a为key的 weak 对象,从而设置为 nil。

七. 分类是如何实现的?它为什么会覆盖掉原来的方法?为什么分类不能添加实例变量

分类是如何实现的:

  • 在程序启动的入口函数_objc_init中通过如下调用顺序
void _objc_init └──const char *map_2_images └──const char *map_images_nolock └──void _read_images(header_info **hList, uint32_t hCount)

_read_images中进行分类的加载,主要做了这两件事:

  1. category的实例方法、协议以及属性添加到类上

  2. category的类方法和协议添加到类的metaclass上相关代码如下:

 category_t **catlist = _getObjc2CategoryList(hi, &count); bool hasClassProperties = hi->info()->hasCategoryClassProperties(); for (i = 0; i < count; i++) { category_t *cat = catlist[i]; Class cls = remapClass(cat->cls); if  { // Category's target class is missing (probably weak-linked). // Disavow any knowledge of this category. catlist[i] = nil; if (PrintConnecting) { _objc_inform("CLASS: IGNORING category ??? %p with " "missing weak-linked target class", cat->name, cat); } continue; } // Process this category. // First, register the category with its target class. // Then, rebuild the class's method lists  if // the class is realized. bool classExists = NO; if (cat->instanceMethods || cat->protocols || cat->instanceProperties) { addUnattachedCategoryForClass(cat, cls, hi); if (cls->isRealized { remethodizeClass; classExists = YES; } if (PrintConnecting) { _objc_inform("CLASS: found category -%s %s", cls->nameForLogging(), cat->name, classExists ? "on existing class" : ""); } } if (cat->classMethods || cat->protocols || (hasClassProperties && cat->_classProperties)) { addUnattachedCategoryForClass(cat, cls->ISA; if (cls->ISA()->isRealized { remethodizeClass(cls->ISA; } if (PrintConnecting) { _objc_inform("CLASS: found category +%s", cls->nameForLogging(), cat->name); } } }

这里 addUnattachedCategoryForClass(cat, cls->ISA;主要是为类添加添加未依附的分类。

static void addUnattachedCategoryForClass(category_t *cat, Class cls, header_info *catHeader){ runtimeLock.assertWriting(); // DO NOT use cat->cls! cls may be cat->cls->isa instead NXMapTable *cats = unattachedCategories(); category_list *list; list = (category_list *)NXMapGet(cats, cls); if  { list = (category_list *) calloc(sizeof + sizeof(list->list[0]), 1); } else { list = (category_list *) realloc(list, sizeof + sizeof(list->list[0]) * (list->count + 1)); } list->list[list->count++] = (locstamped_category_t){cat, catHeader}; NXMapInsert(cats, cls, list);}

执行过程伪代码:1.取得存储所有 unattached 分类的列表

NXMapTable *cats = unattachedCategories(); 

2.从 cats 列表中找倒 cls 对应的 unattached 分类的列表

category_list *list;list = (category_list *)NXMapGet(cats, cls);

3.将新来的分类 cat 添加刚刚开辟的位置上

list->list[list->count++] = (locstamped_category_t){cat, catHeader};

4.将新的 list 重新插入 cats 中,会覆盖老的 list

NXMapInsert(cats, cls, list);

执行完这个过程,系统将分类放到一个该类cls对应的unattached分类的list中。

接着执行remethodizeClass

static void remethodizeClass(Class cls){ category_list *cats; bool isMeta; runtimeLock.assertWriting(); isMeta = cls->isMetaClass(); // Re-methodizing: check for more categories if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) { if (PrintConnecting) { _objc_inform("CLASS: attaching categories to class '%s' %s", cls->nameForLogging(), isMeta ? "" : ""); } attachCategories(cls, cats, true /*flush caches*/); free; }}

执行过程伪代码:

1.取得 cls类的unattached 的分类列表

category_list *cats = unattachedCategoriesForClass(cls, false/*not realizing*/)

2.将 unattached 的分类列表 attachcls 类上

attachCategories(cls, cats, true /* 清空方法缓存 flush caches*/);

执行完上述过程后,系统就把category的实例方法、协议以及属性添加到类上。

attachCategories(cls, cats, true /* 清空方法缓存 flush caches*/)函数内部:

1.在堆上创建方法、属性、协议数组,用来存储分类的方法、属性、协议

 // fixme rearrange to remove these intermediate allocations method_list_t **mlists = (method_list_t **) malloc(cats->count * sizeof; property_list_t **proplists = (property_list_t **) malloc(cats->count * sizeof(*proplists)); protocol_list_t **protolists = (protocol_list_t **) malloc(cats->count * sizeof(*protolists));

2.遍历 cats ,取出各个分类的方法、属性、协议,并填充到上述代码创建的数组中

int mcount = 0; // 记录方法的数量int propcount = 0; // 记录属性的数量int protocount = 0; // 记录协议的数量int i = cats->count; // 从后开始,保证先取最新的分类bool fromBundle = NO; // 记录是否是从 bundle 中取的while  { // 从后往前遍历 auto& entry = cats->list[i]; // 分类,locstamped_category_t 类型 // 取出分类中的方法列表;如果是元类,取得的是类方法列表;否则取得的是实例方法列表 method_list_t *mlist = entry.cat->methodsForMeta; if  { mlists[mcount++] = mlist; // 将方法列表放入 mlists 方法列表数组中 fromBundle |= entry.hi->isBundle(); // 分类的头部信息中存储了是否是 bundle,将其记住 } // 取出分类中的属性列表,如果是元类,取得是nil property_list_t *proplist = entry.cat->propertiesForMeta; if  { proplists[propcount++] = proplist; // 将属性列表放入 proplists 属性列表数组中 } // 取出分类中遵循的协议列表 protocol_list_t *protolist = entry.cat->protocols; if (protolist) { protolists[protocount++] = protolist; // 将协议列表放入 protolists 协议列表数组中 }}
  1. 取出 clsclass_rw_t 数据
auto rw = cls->data();

4.存储方法、属性、协议数组到 rw

// 准备 mlists 中的方法prepareMethodLists(cls, mlists, mcount, NO, fromBundle);// 将新方法列表添加到 rw 中的方法列表数组中并释放mlists rw->methods.attachLists(mlists, mcount); free; if (flush_caches && mcount > 0) flushCaches;// 将新属性列表添加到 rw 中的属性列表数组中并释放proplists rw->properties.attachLists(proplists, propcount); free(proplists);// 将新协议列表添加到 rw 中的协议列表数组中并释放protolists rw->protocols.attachLists(protolists, protocount); free(protolists);

其中 rw->methods.attachLists是用来合并category中的方法:

void attachLists(List* const * addedLists, uint32_t addedCount) { if (addedCount == 0) return; uint32_t oldCount = array()->count; uint32_t newCount = oldCount + addedCount; setArray((array_t *)realloc, array_t::byteSize)); array()->count = newCount; memmove->lists + addedCount, array()->lists, oldCount * sizeof->lists[0])); memcpy->lists, addedLists, addedCount * sizeof->lists[0]));}

这段代码就是先调用 realloc()函数将原来的空间拓展,然后把原来的数组复制到后面,最后再把新数组复制到前面。这就是为什么类别中的方法会在类中的方法前面的原因。

它为什么会覆盖掉原来的方法?我们来看下 runtime 在查找方法时的逻辑:

static method_t *getMethodNoSuper_nolock(Class cls, SEL sel){ for (auto mlists = cls->data()->methods.beginLists(), end = cls->data()->methods.endLists(); mlists != end; ++mlists) { method_t *m = search_method_list(*mlists, sel); if  return m; } return nil;}static method_t *search_method_list(const method_list_t *mlist, SEL sel) { for (auto& meth : *mlist) { if (meth.name == sel) return &meth; }}

可见搜索的过程是按照从前向后的顺序进行的,一旦找到了就会停止循环。由于, category中的方法在类中方法的前面,因此 category 中定义的同名方法不会替换类中原有的方法,但是对原方法的调用实际上会调用 category 中的方法。

为什么分类不能添加实例变量:

图片 9image.png

因为一个类的实例变量在编译阶段,就会在在objc_classclass_ro_t这里进行存储和布局,而category是在运行时才进行加载的,然后在加载 ObjC 运行时的过程中在 realizeClass 方法中:

// 从 `class_data_bits_t `调用 `data` 方法,将结果从 `class_rw_t `强制转换为 `class_ro_t `指针const class_ro_t *ro = (const class_ro_t *)cls->data();// 初始化一个 `class_rw_t` 结构体class_rw_t *rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);// 设置`结构体 ro` 的值以及 `flag`rw->ro = ro;// 最后设置正确的` data`。rw->flags = RW_REALIZED|RW_REALIZING;cls->setData;

运行时加载的时候class_ro_t里面的方法、协议、属性等内容赋值给class_rw_t,而class_rw_t里面没有用来存储相关变量的数组,这样的结构是不是也就注定实例变量是无法在运行期进行填充.

图片 10image.png

具体详见:iOS分类底层实现原理小记 结合 category 工作原理分析 OC2.0 中的 runtime

KVC、KVO

KVC/KVO是观察者模式的一种实现,在Cocoa中是以被万物之源NSObject类实现的NSKeyValueCoding/NSKeyValueObserving非正式协议的形式被定义为基础框架的一部分。从协议的角度来说,KVC/KVO本质上是定义了一套让我们去遵守和实现的方法.当然,KVC/KVO实现的根本是Objective-C的动态性和runtime

KVC定义了一种按名称(字符串)访问对象的机制,而不是访问器

weak属性需要在dealloc中置nil么

在ARC环境无论是强指针还是弱指针都无需在 dealloc 设置为 nil , ARC 会自动帮我们处理
即便是编译器不帮我们做这些,weak也不需要在dealloc中置nil
在属性所指的对象遭到摧毁时,属性值也会清空。

KVC的实现细节

-(void)setValue:(id)value forKey:(NSString *)key;

  1. 首先搜索set方法,有就直接赋值
  2. 如果上面的 setter 方法没有找到,再检查类方法+ (BOOL)accessInstanceVariablesDirectly
    • 返回 NO,则执行setValue:forUNdefinedKey:
    • 返回 YES,则按<key>,<isKey>,<key>,<isKey>的顺序搜索成员
  3. 还没有找到的话,就调用setValue:forUndefinedKey:
 // 允许直接访问实例变量,默认返回YES。如果某个类重写了这个方法,且返回NO,则KVC不可以访问该类。
 + (BOOL)accessInstanceVariablesDirectly

// 如果Key不存在,且没有KVC无法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常
- (id)valueForUndefinedKey:(NSString *)key;
// 如果Key不存在,且没有KVC无法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常
- (id)valueForUndefinedKey:(NSString *)key;

-(id)valueForKey:(NSString *)key;

  1. 首先查找 getter 方法,找到直接调用。如果是 bool、int、float 等基本数据类型,会做 NSNumber 的转换。

  2. 如果没查到,再检查类方法+ (BOOL)accessInstanceVariablesDirectly

    • 返回 NO,则执行valueForUndefinedKey:
    • 返回 YES,则按<key>,<isKey>,<key>,<isKey>的顺序搜索成员
  3. 还没有找到的话,调用valueForUndefinedKey:

KVC 与点语法比较

用 KVC 访问属性和用点语法访问属性的区别:

  • 用点语法编译器会做预编译检查,访问不存在的属性编译器会报错,但是用 KVC 方式编译器无法做检查,如果有错误只能运行的时候才能发现(crash)。

  • 相比点语法用 KVC 方式 KVC 的效率会稍低一点,但是灵活,可以在程序运行时决定访问哪些属性。

  • 用 KVC 可以访问对象的私有成员变量。

7.runtime如何通过selector找到对应的IMP地址?(分别考虑类方法和实例方法)

  • 每一个类对象中都一个对象方法列表(对象方法缓存)
  • 类方法列表是存放在类对象中isa指针指向的元类对象中(类方法缓存)
  • 方法列表中每个方法结构体中记录着方法的名称,方法实现,以及参数类型,其实selector本质就是方法名称,通过这个方法名称就可以在方法列表中找到对应的方法实现.
  • 当我们发送一个消息给一个NSObject对象时,这条消息会在对象的类对象方法列表里查找
  • 当我们发送一个消息给一个类时,这条消息会在类的Meta Class对象的方法列表里查找

KVO 实现原理

当某个类的对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的 setter 方法。 派生类在被重写的 setter 方法实现真正的通知机制,就如前面手动实现键值观察那样。这么做是基于设置属性会调用 setter 方法,而通过重写就获得了 KVO 需要的通知机制。当然前提是要通过遵循 KVO 的属性设置方式来变更属性值,如果仅是直接修改属性对应的成员变量,是无法实现 KVO 的。 同时派生类还重写了 class 方法以“欺骗”外部调用者它就是起初的那个类。然后系统将这个对象的 isa 指针指向这个新诞生的派生类,因此这个对象就成为该派生类的对象了,因而在该对象上对 setter 的调用就会调用重写的 setter,从而激活键值通知机制。此外,派生类还重写了 dealloc 方法来释放资源。

派生类 NSKVONotifying_Person 剖析:

在这个过程,被观察对象的 isa 指针从指向原来的 Person 类,被 KVO 机制修改为指向系统新创建的子类 NSKVONotifying_Person 类,来实现当前类属性值改变的监听。

所以当我们从应用层面上看来,完全没有意识到有新的类出现,这是系统“隐瞒”了对 KVO 的底层实现过程,让我们误以为还是原来的类。但是此时如果我们创建一个新的名为 NSKVONotifying_Person 的类(),就会发现系统运行到注册 KVO 的那段代码时程序就崩溃,因为系统在注册监听的时候动态创建了名为 NSKVONotifying_Person 的中间类,并指向这个中间类了。

因而在该对象上对 setter 的调用就会调用已重写的 setter,从而激活键值通知机制。这也是 KVO 回调机制,为什么都俗称 KVO 技术为黑魔法的原因之一吧:内部神秘、外观简洁。

子类 setter 方法剖析:

KVO 在调用存取方法之前总是调用 willChangeValueForKey:,通知系统该 keyPath 的属性值即将变更。 当改变发生后,didChangeValueForKey: 被调用,通知系统该 keyPath 的属性值已经变更。 之后,observeValueForKey:ofObject:change:context: 也会被调用。

重写观察属性的 setter 方法这种方式是在运行时而不是编译时实现的。 KVO 为子类的观察者属性重写调用存取方法的工作原理在代码中相当于:

- (void)setName:(NSString *)newName
{
    [self willChangeValueForKey:@"name"];    // KVO在调用存取方法之前总调用
    [super setValue:newName forKey:@"name"]; // 调用父类的存取方法
    [self didChangeValueForKey:@"name"];     // KVO在调用存取方法之后总调用
}

总结: KVO 的本质就是监听对象的属性进行赋值的时候有没有调用 setter 方法

系统会动态创建一个继承于 Person 的 NSKVONotifying_Person

person 的 isa 指针指向的类 Person 变成 NSKVONotifying_Person,所以接下来的 person.age = newAge 的时候,他调用的不是 Person 的 setter 方法,而是 NSKVONotifying_Person(子类)的 setter 方法

重写NSKVONotifying_Person的setter方法:[super setName:newName]

#import "ViewController.h"
#import "Student.h"


@interface ViewController ()
{
    Student             *_student;
}

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    _student = [[Student alloc] init];
    _student.stuName = @"oldName_hu";

    // 1.给student对象的添加观察者,观察其stuName属性
    [_student addObserver:self forKeyPath:@"stuName" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];

    // 此时,stuName发生了变化
    _student.stuName = @"newName_wang";
}

// stuName发生变化后,观察者(self)立马得到通知。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
{
    // 最好判断目标对象object和属性路径keyPath
    if(object == _student && [keyPath isEqualToString:@"stuName"])
    {
        NSLog(@"----old:%@----new:%@",change[@"old"],change[@"new"]);
    }else
    {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}


- (void)dealloc
{
    // 移除观察者
    [_student removeObserver:self forKeyPath:@"stuName"];
}

@end

@interface Target : NSObject
{
    int age;
}

// for manual KVO - age
- (int) age;
- (void) setAge:(int)theAge;

@end

@implementation Target

- (id) init
{
    self = [super init];
    if (nil != self)
    {
        age = 10;
    }

    return self;
}

// for manual KVO - age
- (int) age
{
    return age;
}

- (void) setAge:(int)theAge
{
    [self willChangeValueForKey:@"age"];
    age = theAge;
    [self didChangeValueForKey:@"age"];
}

+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key {
    if ([key isEqualToString:@"age"]) {
        return NO;
    }

    return [super automaticallyNotifiesObserversForKey:key];
}

@end

///
/**
 *  添加观察者
 *
 *  @param observer 观察者
 *  @param keyPath  被观察的属性名称
 *  @param options  观察属性的新值、旧值等的一些配置(枚举值,可以根据需要设置,例如这里可以使用两项)
 *  @param context  上下文,可以为nil。
 */
[person addObserver: self
             forKeyPath: @"age"
                options: NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
                context: nil];


- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    Person *per = object;
    NSLog(@"keyPath: %@ object: %ld",keyPath, (long)per.age);
}

8.使用runtime Associate方法关联的对象,需要在主对象dealloc的时候释放么?

无论在MRC下还是ARC下均不需要,被关联的对象在生命周期内要比对象本身释放的晚很多,它们会在被 NSObject -dealloc 调用的object_dispose()方法中释放。

补充:对象内存销毁时间表,分四个步骤

  • 调用 -release :引用计数变为零

    • 对象正在被销毁,生命周期即将结束

    • 不能再有新的 __weak 弱引用,否则将指向 nil

    • 调用 [self dealloc]

  • 父类调用 -dealloc

    • 继承关系中最直接继承的父类再调用 -dealloc
    • 如果是 MRC 代码 则会手动释放实例变量们(iVars)
    • 继承关系中每一层的父类 都再调用 -dealloc
  • NSObject 调 -dealloc

    • 只做一件事:调用 Objective-C runtime 中object_dispose() 方法
  • 调用 object_dispose()

    • 为 C++ 的实例变量们(iVars)调用 destructors
    • 为 ARC 状态下的 实例变量们(iVars) 调用 -release
    • 解除所有使用 runtime Associate方法关联的对象
    • 解除所有 __weak 引用
    • 调用 free()

9._objc_msgForward函数是做什么的?直接调用它将会发生什么?

_objc_msgForward是IMP类型,用于消息转发的:当向一个对象发送一条消息,但它并没有实现的时候,_objc_msgForward会尝试做消息转发。直接调用_objc_msgForward是非常危险的事,这是把双刃刀,如果用不好会直接导致程序Crash,但是如果用得好,能做很多非常酷的事。

JSPatch就是直接调用_objc_msgForward来实现其核心功能的。

10.能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?

  • 不能向编译后得到的类中增加实例变量
  • 能向运行时创建的类中添加实例变量

分析:

  • 因为编译后的类已经注册在runtime中,类结构体中的objc_ivar_list 实例变量的链表和instance_size实例变量的内存大小已经确定,同时runtime 会调用class_setIvarLayout 或 class_setWeakIvarLayout来处理strong weak引用,所以不能向存在的类中添加实例变量。
  • 运行时创建的类是可以添加实例变量,调用 class_addIvar函数,但是得在调用objc_allocateClassPair之后,objc_registerClassPair之前,原因同上。

11.简述下Objective-C中调用方法的过程(runtime)

  • Objective-C是动态语言,每个方法在运行时会被动态转为消息发送,即:objc_msgSend(receiver, selector),整个过程介绍如下:

    • objc在向一个对象发送消息时,runtime库会根据对象的isa指针找到该对象实际所属的类
    • 然后在该类中的方法列表以及其父类方法列表中寻找方法运行
    • 如果,在最顶层的父类(一般也就NSObject)中依然找不到相应的方法时,程序在运行时会挂掉并抛出异常unrecognized selector sent to XXX
    • 但是在这之前,objc的运行时会给出三次拯救程序崩溃的机会,这三次拯救程序奔溃的说明见问题《什么时候会报unrecognized selector的异常》中的说明
  • 补充说明:Runtime 铸就了Objective-C 是动态语言的特性,使得C语言具备了面向对象的特性,在程序运行期创建,检查,修改类、对象及其对应的方法,这些操作都可以使用runtime中的对应方法实现。

12.什么是method swizzling(俗称黑魔法)

  • 简单说就是进行方法交换

  • 在Objective-C中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是selector的名字。利用Objective-C的动态特性,可以实现在运行时偷换selector对应的方法实现,达到给方法挂钩的目的

  • 每个类都有一个方法列表,存放着方法的名字和方法实现的映射关系,selector的本质其实就是方法名,IMP有点类似函数指针,指向具体的Method实现,通过selector就可以找到对应的IMP

  • 交换方法的几种实现方式

    • 利用 method_exchangeImplementations 交换两个方法的实现
    • 利用 class_replaceMethod 替换方法的实现
    • 利用 method_setImplementation 来直接设置某个方法的IMP

13.对象如何找到对应的方法去调用

  • 根据对象的isa去对应的类查找方法,isa:判断去哪个类查找对应的方法 指向方法调用的类
  • 根据传入的方法编号SEL,里面有个哈希列表,在列表中找到对应方法Method(方法名)
  • 根据方法名(函数入口)找到函数实现,函数实现在方法区

14.内存的分区

  • 栈区: 局部变量和方法实参

  • 堆区:OC中使用new方法创建的对象,被创建对象的所有成员变量保存在堆区中

  • BSS段(也叫静态区):

    • 教科书:未被初始化的全局变量和静态变量
    • Xcode8中: 全局变量和静态变量,不管有没有被初始化,都存放在BSS段中
  • 常量区(也叫数据段):

    • 教科书: 存储已经初始化的全局变量,静态变量,常量
    • xcode8: 存储常量
  • 代码段: 程序的代码

![](https://upload-images.jianshu.io/upload_images/5628907-bebe0e706dd6a19b.png)

memory_1@2x.png

图片 11

memory_2@2x.png

15.runtime消息转发机制

  • 动态解析方法

对象在收到无法解读的消息后, 首先调用其所属类的这个类方法 :
+ (BOOL)resolveInstanceMethod:(SEL)selector
selector : 那个未知的选择子

返回YES则结束消息转发

返回NO则进入备胎

假如尚未实现的方法不是实例方法而是类方法, 则会调用另一个方法resolveClassMethod:

  • 备胎

动态方法解析失败, 则调用这个方法
- (id)forwardingTargetForSelector:(SEL)selector
selector : 那个未知的消息
返回一个能响应该未知选择子的备胎对象

  • 消息签名

备胎搞不定, 这个方法就准备要被包装成一个NSInvocation对象, 在这里要先返回一个方法签名

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector

NSMethodSignature : 该selector对应的方法签名

  • 完整的消息转发

给接收者最后一次机会把这个方法处理了, 搞不定就直接程序崩溃

- (void)forwardInvocation:(NSInvocation *)invocation
invocation : 封装了与那条尚未处理的消息相关的所有细节的对象

在这里能做的比较现实的事就是 : 在触发消息前, 先以某种方式改变消息内容, 比如追加另外一个参数, 或是改变消息等等. 实现此方法时, 如果发现某调用操作不应该由本类处理, 可以调用超类的同名方法. 则继承体系中的每个类都有机会处理该请求, 直到NSObject. 如果NSObject搞不定, 则还会调用doesNotRecognizeSelector:来抛出异常, 此时你就会在控制台看到那熟悉的unrecognized selector sent to instance..

16.iOS中__block 关键字的底层实现原理

Block不允许修改外部变量的值,这里所说的外部变量的值,指的是栈中指针的内存地址。__block 所起到的作用就是只要观察到该变量被 block 所持有,就将“外部变量”在栈中的内存地址放到了堆中。进而在block内部也可以修改外部变量的值。

  • 那么如何证明“block内部”打印的是堆地址?

把三个16进制的内存地址转成10进制就是:

定义后前:6171559672

block内部:5732708296

定义后后:5732708296

中间相差438851376个字节,也就是 418.5M 的空间,因为堆地址要小于栈地址,又因为iOS中一个进程的栈区内存只有1M,Mac也只有8M,显然a已经是在堆区了。

这也证实了:a 在定义前是栈区,但只要进入了 block 区域,就变成了堆区。这才是 __block 关键字的真正作用。

17.HTTP协议的8种请求类型

  • OPTIONS:返回服务器针对特定资源所支持的HTTP请求方法。也可以利用向Web服务器发送'*'的请求来测试服务器的功能性。
  • HEAD:向服务器索要与GET请求相一致的响应,只不过响应体将不会被返回。这一方法可以在不必传输整个响应内容的情况下,就可以获取包含在响应消息头中的元信息。
  • GET:向特定的资源发出请求。
  • POST:向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST请求可能会导致新的资源的创建和/或已有资源的修改。
  • PUT:向指定资源位置上传其最新内容。
  • DELETE:请求服务器删除Request-URI所标识的资源。
  • TRACE:回显服务器收到的请求,主要用于测试或诊断。
  • CONNECT:HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器。

18.列举几种容易造成tableview卡顿的原因,并简单描述解决办法

  • cell中控件的数量太多

    • 尽量让cell的布局大致相同,不用风格的cell可以使用不同的重用标识符
  • 使用clearColor,透明度,图片圆角

    • 渲染耗时比较长,图片圆角可以用xxxx
  • 无用cell加载

    • 只加载范围内的cell
  • 频繁计算高度

    • 缓存行高
  • 图片没异步加载

    • 异步加载图片
  • 经常add和remove

    • 最好在初始化时就添加完

19.时间复杂度

  • 快速排序:O(nlogn)
  • 归并排序:O(nlogn)
  • 冒泡排序:O(n2)

20.iOS开发中数据持久化有哪几种

  • 偏好设置
  • plist文件
  • 数据库
  • CoreData
  • 归档解档

21.继承之后的类,子类里面[self class]、[self superClass]、[super class]

  • 注:super:编译修饰符,不是指针,只是指向父类标志。本质还是拿到当前对象去调用父类的方法。super并不是拿到父类对象去调用父类方法
  • 子类
  • 父类
  • 子类

22.通知是否可以在子线程中执行

苹果采取通知中心在同一线程中post和转发同一消息策略。我们可以再多线程环境下使用同一个NSNOtificationCenter对象而不需要加锁。但是delloc方法和-postNotificationName:方法不在同一线程中运行时,会出现BAD_ACCESS。例如:sleep(1).

23.串行、并发和同步、异步

串行:每次只有一个任务被执行
并发:同一时间可以有多个任务被执行
同步:只有在完成了塔预定的任务之后才返回
异步:立即返回

24.线程间通信

NSThread:performselector

GCD:dispatch_async(dispatch_get_main_queue(),^{
})

NSOperation:[NSOperaionQueue mainqueue]

本文由乐虎游戏发布于计算机资讯,转载请注明出处:iOS 面试题一

关键词:

计算机在iOS开垦中应用Protobuf

Protobuf简介 protocolbuffer 是google的一种数据交换的格式,它独立于语言,独立于平台。google提供了多种语言的实现:j...

详细>>

Android 滚轮选择器的实现详解

之前项目中需要实现这样一个功能,效果如图所示: 简介 最近用一个日期选择控件,感觉官方的DatePicker操作有点复杂...

详细>>

iOS开发 之 依赖管理

Carthage 最近把线上的项目依赖包管理工具从CocoaPods切换到Carthage上来,使用了一段时间有一些体会.其实谈到iOS依赖包...

详细>>

深入剖析Objective-C中的Swizzle

method_name是函数的选择器,method_type是参数和返回值类型编码的c字符串,method_imp是指向实际函数的函数指针。可以通过下...

详细>>