2019--iOS 面试集锦

2019-06-26  本文已影响0人  Crics

一、iOS基础

1、#import、#include、@class有什么区别,#import<>和#import“”有什么区别

1、#import是OC对#include()的改进,能确保引用的文件只会被引用一次,不会陷入递归包含问题中。

2、#import和@class的区别:

            1)#import会链入头文件的全部信息,包括实体变量和方法等。

            2)@class只是告诉编译器,这只是个类名,至于这个类是如何定义的,暂不考虑。

            3)@class可以解决循环包含问题。

            在头文件中,一般只需要知道所引用类的名称,不需要知道其内部细节,所以在头文件中使用@class+类名;在类的实现文件里,需要用到引用类的实体变量和方法等,所以需要使用#import+类名来包含这个引用类的头文件。

3、#import<>:用来包含系统自带的文件或者第三方SDK,#import“”用来包含本项目自定义的文件。

2、属性有哪些关键字及其用法?

属性可以按拥有的特性分为五类:

1、原子性    -    atomic、nonatomic。

2、读/写权限    -    readwrite、readonly。

3、内存管用    -    assign、strong、retain、weak、unsafe_unretained、copy。

4、方法名    -    getter=<name>、setter=<name>。

5、不常用:nonnull、null_resettable、nullable。

********************************************************

1、readwrite:可读可写特性,同时生产getter方法和setter方法。

2、readonly:只读特性,只会生成getter方法;不希望属性在类外改变。

3、assign:赋值特性,setter方法的实现是直接赋值,用于基本数据类型;仅设置变量时。

        修饰对象类型时,不改变其引用计数,会产生悬垂指针,修饰的对象在被释放后,assign指针仍然指向原对象内存地址,如果继续访问原对象的话,就可能会导致内存泄漏或者程序异常。

4、retain(MRC)/strong(ARC):持有特性,set方法将传入参数先保留,再赋值,传入参数的retainCount会+1。

5、copy:拷贝特性,set方法的实现是release旧值,copy新值,用于NSString、Block等类型。

        浅拷贝:对内存地址的复制,让目标对象指针和原对象指向同一片内存空间,会增加引用计数。

        深拷贝:对对象内容的复制,开辟新的内存空间。

        可变对象的copy和mutableCopy都是深拷贝。

        不可变对象的copy是浅拷贝,mutableCopy是深拷贝。

        copy返回的都是不可变对象。

6、atomic、nonatomic:(非)原子特性,决定编译器生成的setter、getter是否是原子操。atomic表示多线程安全,非绝对安全,比如如果修饰的是数组,那么对数组的读写是安全,但如果是操作数组进行添加、更改、删除其中对象时,就不能保证安全了;一般使用nonatomic(更高效)。

3、frame和bounds有什么不同?

1、frame:该view在父view坐标系中的位置和大小(参照点是父view的坐标系统)。

2、bounds:该view在本身坐标系中的位置和大小(参照点是本身的坐标系统)。

4、@property的本质是什么?ivar、getter、setter是如何生成并添加到这个类中的?

1、@property的本质:@property = ivar + getter + setter。

2、属性(property)有两大概念:ivar(实例变量)、getter+setter(存取方法)。

3、属性作为OC的一项特性,主要的作用就在于封装对象中的数据。OC对象通常会把其所需要的数据保存为各种实例变量。实例变量一般通过“存取方法”来访问。其中,getter用于读取变量值,setter用于写入变量值。

5、什么情况使用weak,相比assign有什么不同?

1、在ARC中,在有可能出现循环引用的时候,往往要通过让其中一端使用weak来解决,比如delegate。

2、自身已经对它进行一次强引用,没必要再强引用一次,也使用weak,比如:自定义IBOutlet空间属性一般也使用weak(因为父控件的subViews属性已经对它有一个强引用),当然也可以使用strong。

3、assign可以用于非OC对象,weak必须用于OC对象。weak表明该属性定义了一种“非拥有关系”,在属性所指的对象销毁时,属性值会自动清空(nil)。

6、被weak修饰的对象在被释放的时候会发生什么?如何实现的?画出sideTable内部结构?

详细讲解: 浅谈iOS之weak底层实现原理 - 简书

1、weak其实是系统通过一个hash表来实现的弱引用。

2、Runtime维护了一个weak表,用于存储指向某个对象的所有weak指针。weak表其实是一个hash表,Key是所指对象的地址,Value是weak指针的地址(这个地址的值是所指对象的地址)数组。

原理:

1、初始化时:runtime会调用 objc_initWeak(),初始化一个新weak指针指向对象的地址。

2、添加引用时:objc_initWeak() 会调用 objc_storeWeak(),objc_storeWeak() 的作用是更新指针指向,创建对应的弱引用表。

3、释放时:调用 clearDeallocating()。clearDeallocating() 首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个 entry 从 weak表中删除,最后清理对象的记录。

步骤:

1、调用 objc_release。

2、因为对象的引用计数为0,所以执行dealloc。

3、在dealloc中,调用了 _objc_rootDealloc函数。

4、在 _objc_rootDealloc中,调用了 object_dispose函数。

5、调用了 objc_destructInstance。

6、最后调用 objc_clear_deallocating,详细过程:

    1)从weak表中获取废弃对象的地址为键值的记录。

    2)将包含在记录中的所有附有 weak修饰符变量的地址,赋值为nil。

    3)将weak表中该记录删除。

    4)从引用计数表中删除对象的地址为键值的记录。

7、怎么使用copy关键字?

1、NSString、NSArray、NSDictionary等经常使用copy关键字,因为他们有对应的可变类型:NSMutableString、NSMutableArray、NSMutableDictionary。

2、block也经常使用copy关键字。

block使用copy是从MRC遗留下来的传统,在MRC中,方法内部的block是在栈区的,使用copy可以把它放到堆区。在ARC中写不写都行。

8、用@property声明的NSString、NSArray、NSDictionary经常使用copy关键字,为什么?如果改用strong关键字,可能造成什么问题?

1、因为他们有对应的可变类型,他们之间可能进行赋值操作(就是把可变的赋值给不可变的),为确保对象的属性值不会无意间变动,应该在设置属性值时拷贝一份。

2、使用copy是为了让对象的属性不受外界影响。使用copy,无论传入是一个可变还是不可变对象,我本身持有的就是一个不可变副本。

3、如果使用strong,那么这个属性就有可能指向一个可变对象,当这个可变对象在外部被修改了,就会影响到该属性。

9、浅拷贝和深拷贝的区别?

1、浅拷贝:只复制指向对象的指针,而不是复制引用对象本身。

2、深拷贝:复制引用对象本身,内存中存在两份独立对象本身,当修改A时,A_copy不变。

10、如何让自己的类用copy修饰符?如何重写copy关键字的setter?

1、声明该类遵从NSCopying协议(NSMutableCopying协议)。

2、实现NSCopying协议的方法。

-(id)copyWithZone:(NSZone *)zone;

11、这个写法有什么问题:@property (nonatomic, copy) NSMutableArray *arr?

1、添加,删除,修改数组内元素的时候,程序会因为找不到对应的方法而崩溃

2、copy复制的是一个不可变 NSArray 的对象,不能 NSArray 对象进行 添加、修改、删除

12、写一个setter方法用于完成 @property (nonatomic, retain) NSString *name,写一个setter方法完成 @property (nonatomic, copy) NSString *name。

// retain

- (void)setName:(NSString *)name {

        [name retain];

        [_name release];

        _name = name;

}

// copy

- (void)setName:(NSString *)name {

        id str = [name copy];

        [_name release];

        _name = str;

}


13、@synthesize和@dynamic分别有什么用?

@property有两个对应的词,一个是@synthsize(合成实例变量),一个是@dynamic。

如果@synthsize和@dynamic都没有写,那么默认的就是 @synthsize var = _var;

// 在类的实现代码里通过@synthsize语法可以指定实例变量的名字 @synthsize var = _newVar;;

1、@synthsize的语义是如果你没有手动实现setter和getter方法,那么编译器会自动为你加上这两个方法。

2、@dynamic告诉编译器,属性的setter和getter方法由用户自己实现,不会自动生成(@dynamic var)

14、常见OC数据类型有哪些?和C的基本数据类型有什么区别?

OC的数据类型:NSString、NSNumber、NSArray、NSDictionary、NSData等等,这些都是class,创建后便是对象;而C语言的基本类型:int、float等,只是一定字节的内存空间,用于存放数值;NSInteger是基本数据类型,并不是NSNumber的子类,当然也不是NSObject的子类。NSInteger是基本数据类型Int或者Long的别名(typedef long NSInteger),它的区别在于,NSInteger会根据系统是32位还是64位来决定其本身是int还是long。

15、id声明的对象有什么特性?

id声明的对象具有运行时的特性,即可以指向任意类型的OC对象。

16、OC的内存管理?

1、OC的内存管理主要有三种方式:ARC、MRC、内存池

2、自动内存计数ARC:有Xcode自动在App编译阶段,在代码中添加内存管理代码。

3、手动内存计数MRC:遵循内存谁申请,谁释放;谁添加,谁释放的原则。

4、内存释放池Release Pool:把需要释放的内存统一放在一个池子中,当池子被抽干后(drain),池子中所有内存空间也就被自动释放掉。内存池的释放分为自动和手动。自动释放受Runloop机制影响。

17、Category(类别)、Extension(扩展)和继承的区别?

1、分类有名字,扩展没有分类名字,是一种特殊的分类。

2、分类只能扩展方法(属性仅仅是声明,并不能实现),扩展可以扩展属性、成员变量和方法。

3、继承可以拯救、修改、删除方法,并可以增加属性。

详细:

# 分类:

1、分类的作用:声明私有方法,分解体积大的类文件,把framework的私有方法公开。

2、分类的特点:运行时决议,可以为系统类添加分类。在运行时期,将Category中的实例方法列表、协议列表、属性列表添加到主类中后(所以Category中的方法在方法列表中的位置是在主类的同名方法之前),然后会递归调用所以累的load方法,者一切都是在main函数之前执行的。

3、分类可以添加哪些内容:实例方法,类方法,协议,属性(添加getter和setter方法,并没有实例变量,添加实例变量需要用关联对象)。

4、如果工程里有两个分类A和B,两个分类中有一个同名的方法,哪个最终会生效:取决于分类的编译顺序,最后编译的那个分类的同名方法最终生效,而之前的都会被覆盖掉(并不是真正的覆盖,因为其与方法任然存在,只是访问不到。因为在动态添加类的方法的时候是倒序遍历方法列表的,而最后编译的分类的方法会放在方法列表前面,访问的时候就会被优先访问。同理,如果声明了一个和原类方法名的方法,也会覆盖掉原类的方法)。

5、如果声明了两个同名的分类会怎么样:会报错,所以第三方的分类,一般都带有命名前缀。

6、分类能添加成员变量吗:不能。只能通过关联对象(objc_setAssociatedObject)来模拟实现成员变量,但其实质是关联内容,所有对象的关联内容都放在同一个全局容器hash表中:AssociationsHashMap,有AssociationsManager统一管理。

# 扩展

1、扩展的作用:声明私有属性,声明方法(没什么意义),声明私有成员变量

2、扩展的特点:编译时决议,只能以声明的形式存在,多数情况下寄生在宿主类的.m中,不能为系统类添加扩展。

18、什么时候用代理,什么时候用通知?

    1、代理:是一种设计模式。以@protocol形式体现,一般是一对一传递。一般以weak关键词来避免循环引用。

    2、通知:使用观察者模式来实现的用于跨层传递消息的机制。传递方式是一对多。

19、KVO(Key - Value - Observing)

    * KVO是观察者模式的另一种实现。使用了isa混写(isa-swizzling)来实现KVO。

    * 使用sette方法改变值KVO会生效,使用setValue:forKey即KVC改变值KVO也会生效,因为KVC回去调用setter方法。

- (void)setValue:(id)value {

        [self willChangeValueForKey:@"key"];

        [super setValue:value];

        [self didChangeValueForKey:@"key"];

}

    * 通过直接赋值成员变量会触发KVO吗?

        不糊,因为不会调用setter方法,需要加上willChangeValueForKey和didChnageValueForKey方法来手动触发才行。

20、KVC(Key - Value - Key)

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

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

    * KVC:在iOS开发中,可以允许开发者通过Key名直接访问对象的属性,或者给对象的属性赋值,而不需要调用明确的存取方法。这样可以在运行时动态的访问和修改对象的属性,而不需要在编译时确定,这也是iOS开发黑魔法之一。

    * 当调用setValue:value forKey:@"xxx"代码时,底层执行机制如下:

1、程序优先调用set<Key>:属性值方法,代码通过setter方法完成设置。这里的<key>是指成员变量名,首字母大小写要符合KVC的命名规则。

2、如果没找到setName:方法,KVC机制会检查 + (BOOL)accessInstanceVariablesDirectly方法有没有返回YES,默认该方法会返回YES,如果你重写了该方法让其返回NO的话,那么进一步KVC会执行setValue:forUndefinedKey:方法。所以KVC机制会搜索该类里面有没有名为<key>的成员变量,无论该变量是在类接口处定义,还是在类实现处定义,也无论用了什么样的访问修饰符,只要存在以<key>命名的变量,KVC都可以对该成员变量赋值。

3、如果该类既没有set<Key>:方法,也没有_<key>成员变量,KVC机制会搜索_is<Key>的成员变量。

4、如果该类既没有set<Key>:方法,也没有_<key>和_is<Key>成员变量,KVC机制会再继续搜索<key>和is<Key>的成员变量,再给它们赋值。

5、如果上面列出的方法或者成员变量都不存在,系统将会执行该对象的setValue:forUndefinedKey:方法,默认是抛出异常。

即如果没有找到set<Key>:方法时,会按照_key,_isKey,key,isKey的顺序搜索成员并进行赋值操作。

如果想让这个类禁用KVC,那么重写+(BOOL)accessInstanceVariablesDirectly方法,让其返回NO即可,这样的话如果KVC没找到set<Key>:时,会直接调用setValue:forUndefinedKey:方法。

    * 当调用valueForKey:@"xxx"代码时,KVC对key的搜索方式不同于setValue:value forKey:@"xxx",其搜索方式如下:

1、首先按get<Key>,<key>,is<key>的顺序方式查找getter方法,找到合适会直接调用。如果是BOLL或者Int等值类型,会将其包装成一个NSNumber对象。

2、如果上面的getter没有找到,KVC则会查找countOf<Key>,objectIn<Key>AtIndex或<Key>AtIndex格式的方法。如果countOf<Key>方法和另外两个方法中的一个被找到,那么就会返回一个可以响应NSArray所有方法的代理集合(NSKeyValueArray,是NSArray的子集),调用这个代理结合方法,或者说给这个代理结合发送属于NSArray的方法,就会以countOf<Key>,objectIn<Key>AtIndex或<Key>AtIndex这几种方式组合的形式调用。还有一个可选的get<Key>:range:方法。想重新定义KVC的一些功能,可以添加这些方法,需要注意的是方法名要符合KVC的标准命名方法,包括方法签名。

3、如果上面的方法没有找到,那么会同时查找countOf<Key>,enumeratorOf<Key>,memberOf<Key>格式的方法。如果这三个方法都找到,那么就返回一个可以响应NSSet所有方法的代理集合,和上面一样,给这个代理集合发NSSet的消息,就会以countOf<Key>,enumeratorOf<Key>,memberOf<Key>组合的形式调用。

4、如果还没有找到,再检查类方法+(BOOL)accessInstanceVariablesDirectly,如果返回YES(默认行为),那么和先前的设置一样,会按_<key>,_is<Key>,<key>,is<Key>的顺序搜索成员变量名,这里不推荐这么做,因为这样直接访问实例变量破坏了封装性,是代码更脆弱。如果重写了类方法+(BOOL)accessInstanceVariablesDirectly返回NO的话,那么会直接调用valueForUndefinedKey:方法,默认是抛出异常。

21、内存布局?


1、栈(stack):方法调用,局部变量等,是连续的,高地址往低地址扩展。

2、堆(heap):通过alloc等分配的对象,是离散的,低地址往高地址扩展,需要我们手动控制。

3、未初始化数据(bss):未初始化的全局变量等。

4、已初始化数据(data):已初始化的全局变量等。

5、代码段(text):程序代码。

22、long和char* 在64bit和32bit所占字节数

char:1个字节(ASCII 2^8 = 256个字符)。

char* (指针变量):4个字节(32位的寻址空间是2^32,即32个bit,也就是4个字节,同理64位编译器为8个字节)。

short int:2个字节,范围:- 2^16  ~>  2^16,即 -32768 ~> 32767。

int:4个字节,范围:-2147483648 ~> 2147483647

unsigned int:4个字节

long:4个字节,范围和int一样,64位下8个字节,范围:-9223372036854775808 ~> 9223372036854775807。

long long:8个字节,范围:-9223372036854775808 ~> 9223372036854775807。

unsigned long long:8个字节,最大值:1844674407370955161。

float:4个字节。

double:8个字节。

23、static、const、sizeof关键字

static:一是用于修饰存储类型使之成为静态存储类型,二是用于修饰连接属性使之成为内部连接属性。

1、静态存储类型:

        *在函数内定义的静态局部变量,该变量存在内存的静态区,所以即使该函数运行结束,静态变量的值也不会被销毁,函数下次运行时仍能使用这个值。

        *在函数外定义的静态变量 ——静态全局变量,该变量的作用域只能在定义该变量的文件中,不能被其他文件通过extern引用。

2、内部连接属性:静态函数只能在声明它的源文件中使用。

const关键字:

1、声明常变量,使得指定的变量不能被修改。

const int a = 3; // a的值一直为5,不能被修改

const int b; b = 10; // b的值被赋值为10后,不能被修改

const int *ptr; // ptr为指向整型常量的指针,ptr的值可以修改,但是不能修改器所指向的值

int  *const ptr; // ptr为指向整型的常量指针,ptr的值不能改变,但可以修改其所指向的值

const int *const ptr; //ptr为指向整型常量的常量指针,ptr及其指向的值都不能修改

2、修饰函数形参,使得形参在函数内不能被修改,表示输入参数。

int fun(const int a); int fun(const char *str); // 。。。

3、修饰函数返回值,使得函数的返回值不能被修改。

const char *getStr(void); 使用:const *str = getStr();

const int getInt(void); 使用:const int a = getInt();

sizeof关键字:

sizeof是在编译阶段处理,且不能被编译为机器码。sizeof的结果等于对象或类型所占用的内存字节数。sizeof的返回值类型为size_t。

1、变量:int a; sizeof(a)为4;

2、指针:int *p; sizeof(p)为4;

3、数组:int b[10];sizeof(b)为数组的大小, 4*10;int c[0];sizeof(c)等于0。

4、结构体:struct(int a, char ch)s1;sizeof(s1)为8,与结构体字节对齐有关。

        对结构体求sizeof时,有两个原则:

        1)展开后的结构体的第一个成员的偏移量应当是被展开的结构体中最大的成员的整数倍。

        2)结构体大小必须是所有成员大小的整数倍,这里的所有成员,计算的是展开后的成员,而不是将嵌套的结构体当做一个整体。

5、不能对结构体中的位域成员使用sizeof

6、sizeof(void)等于1;sizeof(void *)等于4。

24、自动释放池?

1、在当次runloop将要结束的时候调用 objc_autoreleasePoolPop,并push进来一个新的AutoreleasePool。

2、AutoreleasePoolPage是以栈为节点通过双向链表的形式结合而成,是和线程一一对应的。内部属性有parent、child对应前后两个节点,thread对应线程,next指针指向栈中下一个可填充的位置。

3、AutoreleasePool实现原理:

        1、编译器会将@autoreleasepool{}改写为

                void *ctx = objc_autoreleasePoolPush;

                objc_autoreleasePoolPop(ctx);

        2、objc_autoreleasePoolPush:把当前next位置置为nil,即哨兵对象,然后next指针指向下一个可入栈位置。AutoreleasePool的多层嵌套,即每次 objc_autoreleasePoolPush,实际上是不断地向栈中插入哨兵对象。

        3、objc_autoreleasePoolPop:根据传入的哨兵对象找到对应位置。给上次push操作之后添加的对象依次发送release消息。回退next指针到正确位置。

25、Runloop

* RunLoop 概念

RunLoop是通过内部维护的 事件循环(Event Loop)来对 事件/消息进行管理 的一个对象。

    1)没有消息处理时,休眠已避免资源占用,由用户态切换到内核态(CPU-内核态和用户态 - 简书)。

    2)有消息需要处理时,立刻被唤醒,由内核态切换到用户态。

为什么main函数不会退出?

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

        @autoreleasepool {

            return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));

        }

}

UIApplicationMain内部默认开启了主线程的RunLoop,并执行了一段无限循环的代码(不是简单的for循环或while循环)。

// 无限循环(伪代码)

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

        BOOL running = YES;

        do {

                // 执行各种任务,处理各种事件

        }while(running);

        return;

}

UIApplicationMain函数一直没有返回,而是不断地接受处理消息以及等待休眠,所以运行程序之后会保持持续运行状态。

* RunLoop的数据结构

NSRunLoop(Foundation)是 CFRunLoop(CoreFoundation)的封装,提供了面向对象的API。

RunLoop相关的主要涉及五个类:

CFRunLoop:RunLoop对象。

CFRunLoopMode:运行模式。

CFRunLoopSource:输入源/事件源。

CFRunLoopTimer:定时源。

CFRunLoopObserver:观察者。

1)CFRunLoop

由 pthread(线程对象,说明RunLoop和线程是一一对应的)、currentMode(当前所处的运行模式)、modes(多个运行模式的集合)、commonModes(模式名称字符串集合)commonModelItems(Observer,Timer,Source集合)构成。

2)CFRunLoopMode

由name、source0、source1、observes、times构成。

3)CFRunLoopSource

1、source0:即非基于port的,也就是用户触发的实践。需要手动唤醒线程,将当前线程从内核态切换到用户态。

2、source1:基于port的,包含一个 mach_port和一个回调,可监听系统端口和通过内核和其他线程发送的消息,能主动唤醒RunLoop,接受分发系统事件。具备唤醒线程的能力。

4)CFRunLoopTimer

基于事件的触发器,基本上说的就是NSTimer。在预设的时间点唤醒RunLoop执行回调。因为他是基于RunLoop的,因此它不是实时的(就是NSTimer是不准确的。因为RunLoop只负责分发源的消息。如果线程当前正在处理繁重的任务,就有可能导致Timer本次延时,或者少执行一次)。

5)CFRunLoopObserver

监听以下时间点:CFRunLoopActivity。

1、kCFRunLoopEntry:RunLoop准备启动。

2、kCFRunLoopBeforeTimers:RunLoop将要处理一些Timer相关事件。

3、kCFRunLoopBeforeSources:RunLoop将要处理一些Source事件。

4、kCFRunLoopBeforeWaiting:RunLoop将要进行休眠状态,即将由用户态切换到内核态。

5、kCFRunLoopAfterWaiting:RunLoop被唤醒,即从内核态切换到用户态后。

6、kCFRunLoopExit:RunLoop退出。

7、kCFRunLoopAllActivities:监听所有状态。

6)各数据结构之间的联系

线程和RunLoop一一对应,RunLoop和Mode是一对多的,Mode和source、timer、observer也是一对多的。


* RunLoop的Mode

关于Mode,首先要知道一个RunLoop对象中可能包含多个Mode,且每次调用RunLoop的主函数时,只能指定其中一个Mode(CurrentMode)。切换Mode,需要重新指定一个Mode。主要为了分割开不用的Source、Timer、Observer,让他们之间互不影响。


当RunLoop运行在Mode1上时,是无法接受处理Mode2或Mode3上的Source、Timer、Observer事件的。

共有五种CFRunLoopMode:

1、kCFRunLoopDefaultMode:默认模式,主线程是在这个模式下运行。

2、UITrackingRunLoopMode:跟踪用户交互事件(用于ScrollView追踪触摸滑动,保证界面滑动时不受其他Mode影响)。

3、UIInitializationRunLoopMode:在刚启动App时进入的第一个Mode,启动完成后就不再使用。

4、GSEventReceiveRunLoopMode:接受系统内部事件,通常用不到。

5、kCFRunLoopCommonModes:伪模式,不是一种真正的运行模式,是同步Source/Timer/Observer到多个Mode中的一种解决方案。

* RunLoop的实现机制



对于RunLoop而言,最核心的事情就是保证线程在没有消息的时候休眠,在有消息时唤醒,以提高程序性能。RunLoop这个机制是依靠系统内核来完成的(苹果操作系统核心组件Darwin中的Mach)。


RunLoop通过 mach_msg()函数接收、发送消息。它本质是调用函数 mach_msg_trap(),相当于是一个系统调用,会触发内核状态切换。在用户态调用 mach_msg_trap()时会切换到内核态;内核态中内核实现的 mach_msg()函数会完成实际的工作。即基于port的source1,监听端口,端口有消息就会触发回调;而source(),要手动标记为待处理和手动唤醒RunLoop。

Mach消息发送机制:

1、通知观察者RunLoop即将启动。

2、通知观察者即将要处理Timer事件。

3、通知观察者即将要处理source()事件。

4、处理source()事件。

5、如果基于端口的源(Source1)准备好了并处于等待状态,进入步骤9。

6、通知观察者线程即将进入休眠状态。

7、将线程置于休眠状态,由用户态切换到内核态,直到下面的任一事件发生才唤醒线程。

        1)一个基于 port 的 Source1 的事件。

        2)一个 Timer 到时间了。

        3)RunLoop自身的超时时间到了。

        4)被其他调用者手动唤醒。

8、通知观察者线程将被唤醒。

9、处理唤醒时收到的事件。

        1)如果用户定义的定时器启动,处理定时器事件并重启RunLoop,进入步骤2。

        2)如果输入源启动,传递响应的消息。

        3)如果RunLoop被显示唤醒而且事件还没超时,重启RunLoop,进入步骤2。

10、通知观察者RunLoop结束。

* RunLoop与NSTimer

一个比较常见的问题:滑动tableView,定时器还会生效吗?

默认情况下RunLoop运行在kCFRunLoopDefaultMode下,而当滑动tableView时,RunLoop切换到UITrackingRunLoopMode,而Timer是在kCFRunLoopDefaultMode下的,就无法接受处理Timer的事件。

怎么去解决这个问题?把Timer添加到UITrackingRunLoopMode上并不能解决问题,因为这样在morning情况下就无法接收定时器事件了。

所以我们需要把Timer同时添加到UITrackingRunLoopMode和kCFRunLoopDefaultMode上。

那么如何把timer同时添加到多个mode上呢?就要用到NSRunLoopCommonModes了。

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

Timer就被添加到多个mode上,这样即使RunLoop由KCFRunLoopDefaultMode切换到UITrackingRunLoopMode下,也不会影响接收Timer事件。

* RunLoop和线程

1、线程和RunLoop是一一对应的,其映射关系保存在一个全局的Dictionary里。

2、自己创建的线程默认是没有开启RunLoop的

1)如何创建一个常驻线程?

    1> 为当前线程开启一个RunLoop(第一次调用[NSRunLoop currentRunLoop]方法时,实际是会先去创建一个RunLoop)。

    2> 向当前RunLoop中添加一个Port/Source等维持RunLoop的事件循环(如果RunLoop的mode中一个item都没有,RunLoop会退出)。

    3> 启动该RunLoop

   @autoreleasepool {

        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];

        [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];

        [runLoop run];

2)输出下面代码的执行顺序

NSLog(@"1");

dispatch_async(dispatch_get_global_queue(0, 0), ^{

        NSLog(@"2");

        [self performSelector:@selector(test) withObject:nil afterDelay:10];

        NSLog(@"3");

});

NSLog(@"4");

- (void)test {

        NSLog(@"5");

}

答案是:1423,test昂发并不会执行。

原因是如果是带afterDelay的延时函数,会在内部创建一个NSTimer,然后添加到当前线程的RunLoop中。也就是如果当前线程没有开启RunLoop,该方法会失效。

那么我们改成:

dispatch_async(dispatch_get_global_queue(0, 0), ^{

        NSLog(@"2");

        [[NSRunLoop currentRunLoop] run];

        [self performSelector:@selector(test) withObject:nil afterDelay:10];

        NSLog(@"3");

});

然而test方法依然不执行。

原因是如果RunLoop的mode中一个item都没有,RunLoop会退出。即在调用RunLoop的run方法后,由于某mode中没有添加任何item去维持RunLoop的时间循环,RunLoop随机还是会退出。

所以我们自己启动RunLoop,一定要在后面添加item

dispatch_async(dispatch_get_global_queue(0, 0) , ^{

        NSLog(@"2");

        [self performSelector:@selector(test) withObject:nil afterDelay:10];

        [[NSRunLoop currentRunLoop] run];

        NSLog(@"3");

});

3)怎样保证子线程数据回来后更新UI的时候不打断用户的滑动操作?

当我们在子线程请求数据的同时滑动浏览当前页面,如果数据请求成功要切回主线程更新UI,那么就会影响当前正在滑动的体验。

我们可以将更新UI事件放在主线程的NSDefaultRunLoopMode上执行即可,这样就会等用户不再滑动页面,主线程RunLoop由UITrackingRunLoopMode切换到NSDefaultRunLoopMode时再去更新UI。

[self performSelectorOnMainThread:@selector(reloadData) withObject:nil waitUntilDone:NO mode:@[NSDefaultRunLoopMode]];

26、多线程

1、进程

        1)进程是一个具有一定独立功能的程序关于某次数据集合的一次运行活动,它是操作系统分配资源的基本单元。

        2)进程是指在系统中正在运行的一个应用程序,就是一段程序的执行过程,可以理解为手机上的一个app。

        3)每个进程之间是独立的,每个进程均运行在某专用且受保护的内存空间内,拥有独立运行所需的全部资源。

2、线程

        1)程序执行流的最小单元,线程是进程中的一个实体。

        2)一个进程要想执行任务,必须至少有一条线程,应用程序启动的时候,系统会默认开启一条进程,也就是主线程。

3、进程和线程的关系

        1)线程是进程的执行单元,进程的所有任务都在线程中执行。

        2)线程是CPU分配资源和调度的最小单位

        3)一个程序可以对应多个进程(多进程),一个进程中可有多个线程,但至少要有一条线程。

        4)同一个进程内的线程共享进程资源。

4、多进程

        打开Mac的活动监视器,可以卡到很多个进程同时运行。

        1)进程是程序在计算机上的一次执行活动。当你运行一个程序,你就启动了一个进程。显然,程序是死的(静态的),进程是活的(动态的)。

        2)进程可以分为系统进程和用户进程。凡是用于完成操作系统的各种功能的进程就是系统进程,它们是处于运行状态下的操作系统本身。所有由用户启动的进程都是用户进程,进程是操作系统进行资源分配的单位。

        3)进程又被细化为线程,也就是一个进程下有多个能独立运行的更小的单位。在同一个时间里,同一台计算机系统中如果允许两个或两个以上的进程处于运行状态,这便是多进程。

5、多线程

        1)同一时间,CPU只能处理1条线程,只有1条线程在执行。多线程并发执行,其实就是CPU快速地在多条线程之间调度(切换)。如果CPU调度线程的实践足够快,就造成了多线程并发执行的假象。

        2)如果线程非常非常多,CPU会在N多线程之间调度,消耗大量CPU资源,每条线程被调度执行的频次会降低(线程的执行效率降低)。

        3)多线程的优点:

                能适当提高程序的执行效率。

                能适当提高资源利用率(CPU、内存利用率)。

        4)多线程的缺点:

                开启线程需要占用一定的内存空间(默认情况下,主线程占用1M,子线程占用512KB),如果开启大量的线程,会占用大量的内存空间,降低程序的性能。线程越多,CPU在调度线程上的开销就越大。

                程序设计更加复杂:比如线程之间的通信、多线程的数据共享等。

6、任务

        就是执行操作的意思,也就是在线程中执行的那段代码。在GCD中是放在block中的。执行任务有两种方式:同步执行(sync)和异步执行(async)。

        1)同步(Sync):同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任务完成之后再继续执行,即会阻塞线程。只能在当前线程中执行任务(是当前线程,不一定是主线程),不具备开启新线程的能力。    

        2)异步(Async):线程会立即返回,无需等待就会继续执行下面的任务,不阻塞当前线程。可以在新的线程中执行任务,具备开启新线程的negligence(并不一定开启新线程)。如果不是添加到主队列上,异步会在子线程中执行任务。

7、队列

        队列(Dispatch Queue):这里的队列指执行任务的等待队列,即用来存放任务的队列。队列是一种特殊的线性表,采用FIFO(先进先出)的原则,即新任务总是被插入到队列的末尾,而读取任务的时候总是从队列的头部开始读取。每读取一个任务,则从队列中释放一个任务。

        在GCD中有两个队列:串行队列和并发队列。两者都符合FIFO(先进先出)的原则。

        两者的主要区别是:执行顺序不同,以及开启线程数不同。

        1)串行队列:(Serial Despatch Queue):

                同一时间,队列中只能执行一个任务,只有当前的任务执行完成之后,才能执行下一个任务。(只开启一个线程,一个任务执行完毕后,再执行下一个任务)。主队列是主线程上的一个串行队列,是系统自动为我们创建的。

        2)并发队列(Concurrent Dispatch Queue):

                同时允许多个任务并发执行。(可以开启多个线程,并且同时执行任务)。并发队列的并发功能只有在异步(dispatch_async)函数下才有效


iOS多线程:NSThread、NSOperationQueue、GCD

1、NSThread:清凉级别的多线程技术

        是我们自己手动开辟的子线程,如果使用的是初始化方式就需要我们自己启动,如果使用的是构造方式它就会自动启动。只需要我们手动开辟的线程,都需要我们自己管理该线程,不只是启动,还有该线程使用完毕后的资源回收。

NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(testThread:) object@"手动创建"];

// 当使用初始化方法出来的线程需要start启动

[thread start];

// 可以为开辟的线程起名字

thread.name = @"自己的线程";

// 调整Thread的权限,线程权限的范围是0~1。越大权限越高,先执行的概率就会越高,由于概率高,所以并不能很准确的实现我们想要的执行顺序,默认值时0.5

thread.threadPriority = 1;

// 取消当前已经启动的线程

[thread cancel];

// 通过遍历构造器开辟线程

[NSThread detachNewThreadSelector:@selector(testThread:) toTarget:self withObject:@"构造器方法"];

performSelector...只要是NSObject的子类或者对象都可以通过调用此方法进入子线程和主线程,其实这些方法所开辟的子线程也是NSThread的另一种体现方式。

在编译阶段并不会去检查方法是否有效存在,如果不存在,只会给出警告。

// 在当前线程,延迟1s执行。响应了OC语言的动态性:延迟到运行时才绑定方法

[self performSelector:@selector(test) withObject:nil afterDelay:1];

// 回到主线程。waitUntilDone:是否将该回调方法执行完再执行后面的代码。如果是YES:就必须等回调方法执行完成之后才能执行后面的代码,会阻塞当前的线程;如果是NO:就是不等回调方法结束,不会阻塞当前线程

[self performSelectorOnMainThread:@selector(test) withObject:nil waitUntilDone:YES];

// 开辟子线程

[self performSelectorInBackground:@selector(test) withObject:nil];

// 在指定线程执行

[self performSelector:@selector(test) onThread:[NSThread currentThread] withObject:nil waitUntilDone:YES];

需要注意的是:如果是带有afterDelay的延时函数,会在内部创建一个Timer,然后添加到当前线程的RunLoop中,也就是如果当前线程没有开启runloop,该方法会失效。在子线程中,需要启动runloop(注意调用顺序)。

[self performSelector:@selector(test) withObject:nil afterDelay:1];

[[NSRunLoop currentRunLoop] run];

而performSelector: withObject: 只是一个单纯的消息发送,和时间没有一点关系。所以不需要添加到子线程的RunLoop中也能执行。

2、GCD和NSOperationQueue

GCD是面向底层的C语言的API,NSOperationQueue是用GCD构建封装的,是GCD的高级抽象。

1、GCD执行效率更高,而却由于队列中执行的是block构成的任务,这是一个轻量级的数据结构,写起来更方便。

2、GCD只支持FIFI的队列,而NSOperationQueue可以通过设置最大并发数,设置优先级,添加依赖关系等调整执行顺序。

3、NSOperationQueue甚至可以跨队列设置依赖关系,但是GCD只能通过设置串行队列,或者在队列内添加barrier(dispatch_barrier_async)任务,才能控制执行顺序,较为复杂。

4、NSOperationQueue是面向对象,所以支持KVO,可以监听operation是否正在执行(isExecuted)、是否结束(isFinished)、是否取消(isCanceld)。

        1)实际项目开发中,很多时候只会用到异步操作,不会有特别复杂的线程关系管理,所以苹果推崇的且优化完善、运行快速的GCD是首选。

        2)如果考虑异步操作之间的事务性,顺序性,依赖关系,比如多线程并发下载,GCD需要自己写更多的代码来实现,而NSOperationQueue已经内建了这些支持

        3)不论是GCD还是NSOperationQueue,我么接触的都是任务和队列,都没有直接接触到线程,事实上线程管理也的确不需要我么操心,系统对于线程的创建,调度管理和释放都做得很好。而NSThread需要我们自己去管理线程的生命周期,还要考虑线程头部、加锁问题,造成一些性能上的开销。

3、死锁

死锁就是队列引起的循环等待

1)一个比较常见的死锁:主队列同步   

 - (void)viewDidLoad {

        [super viewDidLoad];

        dispatch_sync(dispatch_get_main_queue(), ^{  

                NSLog(@"1");

        });

        NSLog(@"2");

}

在主线程中运用主队列同步,也就是把任务放到了主线程的队列中。

同步对于任务是立刻执行的,那么当吧任务放到主队列时,它就会立马执行,只有执行完这个任务,viewDidLoad才会继续向下执行。

而viewDidLoad和任务都是在主队列上的,由于队列的FIFO(先进先出)原则,任务又需等待viewDidLoad执行完毕后才能继续执行,viewDidLoad和这个任务就形成了相互循环等待,就造成了死锁。

想避免这种死锁,可以将同步改成异步dispatch_async或者将dispatch_get_main_queue换成其他串行或并行队列,都可以解决

2)下面的代码也会造成死锁:

dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);

dispatch_async(serialQueue, ^{

        dispatch_sync(serialQueue, ^{

                NSLog(@"111");

        });

    NSLog(@"222");

});

外面的函数无论是同步还是异步都会造成死锁。

这是因为里面的任务和外面的任务都是在同一个serialQueue队列内,又是同步,这就和上面主队列同步的例子一样造成了死锁。

解决方案和上面的一样,将里面的同步改成异步dispatch_async,或者将serialQueue换成其他串行或者并行队列,都可以解决。

dispatch_queue_t serialQueue1 = dispatch_queue_create("test",  DISPATCH_QUEUE_SERIAL);

dispatch_queue_t serialQueue2 = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);

dispatch_async(serialQueue1, ^{

        dispatch_sync(serialQueue2, ^{

                NSLog(@"111");

        }) ;

        NSLog(@"222");

});

这样是不会死锁的,并且serialQueue1和serialQueue2是在同一个线程中。

4、GCD —— 队列

GCD有三种队列类型:

1、main queue:通过dispatch_get_main_queue()获得,这是一个与主线程相关的串行队列。

2、global queue:全局队列,是并发队列,由整个进程共享。存放着高、中、低三种优先级的全局队列。调用dispatch_get_global_queue并传入优先级来访问队列。

3、自定义队列:通过函数dispatch_queue_create创建的队列。

1)串行队列先异步后同步

dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);

NSLog(@"1");

dispatch_async(serialQueue, ^{

        NSLog(@"2");

});

NSLog(@"3");

dispatch_sync(serialQueue, ^{

        NSLog(@"4");

});

NSLog(@"5");

打印顺序:13245。

原因:首先先打印1;接下来将任务2添加到串行队列上,由于任务2是异步,不会阻塞线程,继续向下执行,打印3;然后是任务4,将任务4添加到串行队列上,因为任务4和任务2在同一个串行队列,根据队列FIFI原则,任务4必须等待任务2执行完后才能执行,有因为任务4是同步任务,会阻塞线程,只有执行完任务4才能继续向下执行打印5。

这里的任务4在主线程中执行,而任务2在子线程中执行。如果任务4是添加到另一个串行队列或者并行队列,则任务2和任务4无序执行。

2)performSelector

dispatch_async(dispatch_get_global_queue(0 ,0 ) ^{

            [self performSelector:@selector(test) withObject:nil afterDelay:0];

});

这里的test方法不会去执行,原因是

- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;

这个方法要创建任务提交到runloop上,而gcd框架继承的线程默认没有开启对应runloop的,所以这个方法会失效。

而如果将dispatch_get_global_queue改成主队列,由于主队列所在的主线程是默认开启了runloop的,就会去执行。

将dispatch_async改成同步,因为同步是在当前线程执行,那么如果当前线程是主线程,test方法也会去执行。

在performSelector 下面添加[[NSRunLoop currentRunLoop] run]; 开启当前线程,test也会去执行

3)dispatch_barrier_async

(1)怎么用GCD实现多读单写?

多度单写的意思就是:可以多个读者同时读取数据,而在读的时候,不能取写入数据。并且,在写的过程中,不能有其他写者去写,即读者之间是并发的,写者与读者或者其他者时互斥的。


 处理


这里的写处理可以用 dispatch_barrier_sync(栅栏函数)去实现

(2)dispatch_barrier_async

dispatch_queue_t concurrentQueue = dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT);

for (NSInteger i = 0; i < 10; I++) {

        dispatch_sync(concurrentQueue, ^{

                NSLog(@"%zd", i);

        });

}

dispatch_barrier_sync(concurrentQueue, ^{

        NSLog(@"barrier");

});

for (NSInteger i = 10; i < 20; i++) {

        dispatch_sync(concurrentQueue, ^{

                NSLog(@"%zd", i);

        });

}

这里的dispatch_barrier_sync上的队列要和需要阻塞的任务在同一队列上,否则是无效的。

从打印上看,任务0-9和热舞10-19因为是异步并发的原因,彼此是无序的。而由于栅栏函数的存在,导致顺序必然是先执行任务0-9,再执行栅栏函数,再执行任务10-19.

dispatch_barrier_sync和dispatch_barrier_async的区别在于会不会阻塞当前的线程。

比如,上述代码如果在dispatch_barrier_async后随便加一条打印,则会先去执行该打印,再去执行任务0-9和栅栏函数;额如果是dispatch_barrier_sync,则会在任务0-9和栅栏函数执行完后再去执行这条打印。

(3)设计多读单写

- (id)readDataForKey:(NSString *)key {

        __block id result;

        dispatch_sync(_concurrentQueue, ^ {

                result = [self valueForKey:key];

        });

        return result;

}

- (void)writeData:(id)data forKey:(NSString *)key {

        dispatch_barrier_async(_concurrentQueue, ^{

                [self setValue:data forKey:key];

        });

}

4)dispatch_group_async

场景:在n个耗时并发任务都完成后再去执行接下来的任务。比如,在n个网络请求完成后刷新UI页面。

dispatch_queue_t concurrentQueue = dispatch_queue_create("test", DISPATCH_QUQUE_CONCURRENT);

dispatch_group_t group  = dispatch_group_create();

for (NSInteger i = 0; i < 10; i++) {

        dispatch_group_async(group, concurrentQueue, ^{

                sleep(1);

                NSLog(@"%zd:网络请求", i);

        });

}

dispatch_group_notify(group, dispatch_get_main_queue(), ^{

        NSLog(@"刷新页面");

});

5)Dispatch Semaphore

GCD中的信号量是指:Dispatch Semaphore,是持有计数的信号。

Dispatch Semaphore提供了三个函数

1、dispatch_semaphore_create:创建一个Semaphore并初始化信号的总量。

2、dispatch_semaphore_signal:发送一个信号,让信号总量加1。

3、dispatch_semaphore_wait:可以使总信号量减1,当信号总量为0时就会一直等待(阻塞所在线程),否则就可以正常执行。

Dispatch Semaphore在开发中主要用于:

(1)、保存线程同步,将异步执行的任务转换成同步执行的任务。

dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

__block NSInteger number = 0;

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

        number = 100;

        dispatch_semaphore_signal(semaphore);

});

dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

NSLog(@"semaphore-- end, number = %zd", number);

dispatch_semaphore_wait加锁阻塞了当前线程,dispatch_semaphore_signal解锁后,当前线程才能继续执行。

(2)、保证线程安全,为线程加锁。

在线程安全中可以将dispatch_semaphore_wait看作是加锁,而dispatch_semaphore_signal看作是解锁。// 首先创建全局变量

_semaphore = dispatch_semaphore_create(1);

__block NSInteger count = 0;

// 这里出事话信号量是1。

- (void)asyncTask {

        dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);

        count++;

        sleep(1);

        NSLog(@"执行任务:%zd", count);

        dispatch_semaphore_signal(_semaphore);

}

// 异步并发调用asyncTask

for (NSInteger i = 0; i < 100; i++) {       

        dispatch_async(dispatch_get_global_queue(0, 0), ^{                  

              [self asyncTask];         

        });

}

打印的是任务从1顺序执行到100,没有发生两个任务同时执行的情况。

原因:在子线程中并发执行asyncTask,那么第一个添加到并发队列里的,会将信号量减1,此时信号量等于0,可以执行接下来的任务。而并发队列中的其他任务,由于此时信号量不等于0,必须等当前正在执行的任务执行完毕后调用dispatch_semaphore_signal将信号量加1,才可以继续执行接下来的任务,以此类推,从而达到线程加锁的目的。

6)延时函数(dispatch_after)

dispatch_after能让我么添加进队列的任务延时执行,该函数并不是在指定时间后执行处理,而只是在指定时间追加处理到dispatch_queue

// 第一个参数是time,第二个参数是dispatch_queue,第三个参数是要执行的block

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

        NSLog(@"dispatch_after");

});

由于其内部使用的是dispatch_time_t管理时间,而不是NSTimer。所有如果在子线程中调用,相比于performSelector: afterDelay,不用关心runloop是否开启

7)单例(dispatch_once)

static id _instance;

+ (instancetype)shareInstance {

        static dispatch_once_t  onceToken;

        dispatch_once(&onceToken, ^{

                _instance = [[self alloc] init];

        });

        return _instance;

}

+ (id)allocWithZone:(struct _NSZone *)zone {

        static dispatch_one_t onceToken;

        dispatch_once(&onceToken, ^{

                _instance = [super allocWithZone:zone];

        });

        return _instance;

}

// 如果遵守了NSCopying协议

- (id)copyWithZone:(nullable NSZone *)zone {

        return _instance;

}






1、 设计模式是什么?你知道的设计模式有哪些?

设计模式是一种编码经验,用比较成熟的逻辑去处理某一种类型的事情。

1、MVC模式:Model - View - Control,把模型-视图-控制器 进行解耦合

2、MVVM模式:Model - View - ViewModel,把模型-视图-业务逻辑 进行解耦合

3、单例模式:通过static关键词,声明全局变量。在整个进程运行期间只会被赋值一次

4、观察者模式:KVO是典型的观察模式,观察某个属性的状态,状态发生改变时通知观察者

5、委托模式:代理+协议的祝贺。实现一对一的反向传值操作

6、工厂模式:通过一个类方法,批量生产已有模板的对象

7、策略模式:定义算法簇,封装起来,使它们之间可以互相替换

3、

3、


1、KVO实现原理?

    基本原理:

        1、KVO是基于runtime机制实现的

        2、当某个类的属性对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的setter方法。派生类在被重写的setter方法内实现真正的通知机制。

        3、如果原类为Person,那么生成的派生类名为NSKVONotifying_Person。

        4、每个类对象中都有一个isa指针指向当前类,当一个类对象第一次被观察时,系统会偷偷将isa指针指向动态生成的派生类,从而在给被监听属性赋值时执行的是派生类的setter方法。

        5、键值观察通知依赖于NSObject的两个方法:willChangeValueForKey:和didChangeValueForKey:,在一个被观察属性发生改变之前,willChangeValueForKey:会被调用,这里会记录旧的值;当发生改变后,didChangeValueForKey:会被调用,这里会更新旧值到新值,最后调用observeValueForKey:ofObject:change:context。

    深入原理:

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

        2、NSKVONotify_A类剖析:在这个过程中,被观察对象的isa指针从指向A类到被KVO机制修改的NSKVONotifying_A类,来实现当前类属性值改变的监听。

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

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

        5、子类setter方法剖析:KVO的键值观察通知依赖于NSObject的两个方法:willChangeValueForKey:和didChangeValueForKey:,在存取数值的前后分别调用这2个方法,被观察属性即将发生改变前,willChangeValueForKey:被调用,通知系统该keyPath的属性值即将改变;当改变发生之后,didChangeValueForKey:被调用,通知系统该keyPath的属性值已经改变;之后 observeValueForKey:ofObject:change:context:被调用。重写观察属性的setter方法这种继承方式的注入是在运行时而不是编译时实现的。

2、


未完待续!

欢迎补充!

上一篇 下一篇

猜你喜欢

热点阅读