【iOS】iOS面试知识点学习(Objective-C语言特性,KVC,属性,2021.1.13更新)

前言

这篇学习文章的框架草稿其实一早打好了,但是工作上接二连三有些琐碎的事情打乱了节奏,中途也稍微又有些泄气,不过最后还是希望坚持一下


全篇图文基本自网络,属于知识总结,如有错漏欢迎指出


分类/类别/Category

1.适用范围

对于一个已经封装好的类(比如系统类、第三方库),不想/不能改动这个类,但是想在类中增加一个方法,这时候只需要给原来的类增加一个分类。

其实分类不应该理解成“把东西分成几类”,而应该理解为和“主类”相对应的“分类”,是对主类的扩展。

2.语法格式

文件名:主类名+分类名 比如 NSString+Safe.h/NSString+Safe.m

文件中的语法:

@interface 主类类名(分类类名)
//一般来说不能定义成员变量
@end

@implementation 主类类名(分类类名)

@end

分类中可以添加实例方法、类方法、协议、属性(这里的属性是指添加getter和setter方法,并没有实例变量)

typedef struct category_t {
    const char *name;  /* 分类的名字 */
    classref_t cls;   /* 类 */
    struct method_list_t *instanceMethods;  /* 分类中所有给类添加的实例方法的列表 */
    struct method_list_t *classMethods;  /* 分类中所有给类添加的类方法的列表 */
    struct protocol_list_t *protocols;  /* 实现的所有协议的列表 */
    struct property_list_t *instanceProperties;  /* 添加的所有属性 */
} category_t;

3.注意事项(考点)

  1. 分类是运行时特性。在运行时阶段,将分类中的示例方法列表、协议列表、属性列表添加到主类中,然后会递归调用所有类的load方法,这一切在main方法之前执行。
  2. 分类方法可以访问主类的成员变量
  3. 分类的执行优先级:3.1.在本类和分类有相同的方法时,优先调用分类方法再调用主类方法,其实分类没有完全替换原来类的同命方法,只是放在了方法列表前面;3.2.如果两个分类都实现了相同的方法,执行顺序可以通过Targets->Build Phases->Compile Source进行调节,执行顺序从上到下
  4. 分类不允许为已有的类添加新的属性或者成员变量。常见的办法是通过runtime.h中objc_getAssociatedObject / objc_setAssociatedObject方法来访问和生成关联对象,模拟生成属性。
    //NSObject+SpecialName.h
    @interface NSObject (SpecialName)
    @property (nonatomic, copy) NSString *specialName;
    @end
    
    //NSObject+SpecialName.m
    #import "NSObject+Extension.h"
    #import 
    static const void *SpecialNameKey = &SpecialNameKey;    
    @implementation NSObject (SpecialName)
    @dynamic specialName;
    
    - (NSString *)specialName {
        //如果属性值是非id类型,可以通过属性值先构造OC的id对象,再通过对象获取非id类型属性
        return objc_getAssociatedObject(self, SpecialNameKey);
    }
    
    - (void)setSpecialName:(NSString *)specialName{
        //如果属性值是非id类型,可以通过属性值先构造OC的id对象,再通过对象获取非id类型属性
        objc_setAssociatedObject(self, SpecialNameKey, specialName, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 
    } 
    
    @end
  5. 分类方法的引用问题:5.1.类引用分类中未重写的扩展方法,需要引入分类的声明文件(.h),如果类引用的是自己的分类,就只能在.m中引用,其他情况下在.h或.m文件中引用即可。5.2.类引用分类中的重写方法,不用引入分类声明文件
  6. 分类的影响向下有效:会影响到主类以及主类的子类,而不会影响到父类。子类如果没有引用父类的分类声明文件,子类就不会继承父类的非重写分类方法。
  7. +load方法,先执行类,再执行分类,但是分类方法会“覆盖”类方法。

扩展/Extension

在网上的资料说的扩展大多是指分类,这里的扩展可以理解为一种匿名的分类。

扩展的作用是为某个类附加额外的属性、成员变量、声明方法(声明方法的意义不大)

扩展一般直接写在扩展类的.m文件中,也可以写成单一的.h文件,命名为"类名_类别名.h"(比如BaseViewController_Extension.h)

#import "类名.h"

@interface 类名 ()
// 在此添加私有成员变量、属性、声明方法
@end

扩展的限制

  1. 扩展添加的都是私有属性和方法,其他类不能调用,也不能被子类继承。可以通过扩展实现一个@property对外只读,对内是可读写的
  2. 扩展写在扩展类的.m中时,必须写在@implementation上方
  3. 扩展定义的方法需要在类实现中进行实现
  4. 留心扩展与类别的区别:分类一般不能添加属性,扩展需要有类的源码才能添加

代理/Delegate

协议Protocol是多个类之间协商的一个公共接口,提供一系列方法的声明给类使用,而代理Delegate是协议的一个典型应用机制。先看一下协议

协议/Protocol

协议是由一系列方法声明组成的,一个类只要遵守了协议,就相当于拥有了协议的所有方法声明

协议具有“继承”特性,一个协议遵守了其他协议,就用了其他协议的所有方法声明

遵守了协议的类在.m中实现协议方法,也就是说不同类对协议方法的实现可以是不同的。

类是否需要实现协议中的方法取决于方法在协议中的声明方式,默认方法为@required

@required //@required之下的方法是必须实现的,编译器会warning
- (void)eat;

@optional //@optional之下的方法是可选实现的
- (void)watch;

协议的声明方法:

@protocol Eat//@protocol Eat<其他协议>
@required
- (void)eat;
@optional
- (void)watch;
@end

NSObject在这里表示为一个基协议,所有协议都继承自基协议。

NSObject协议中声明了很多基本方法,比如description、retain、release等。

遵循协议的类的声明方法:

#import "协议名称.h"

@interface 类名 : 父类 <协议名称1, 协议名称2,…>
// 这里不要再声明协议里已经声明过的方法
@end

之后即可在.m中实现协议方法。

协议的限制对象类型作用:假设有协议MyProtocol,则声明一个id obj/NSObject *obj的对象则表明,该对象必须遵守MyProtocol协议,如果给该对象赋值的对象没有遵守协议就会报错

协议和代理

个人理解:一个类不想/不能实现协议的方法,他就找一个遵循协议的代理类来实现,类在恰当的时机就可以调起代理类实现特定功能

【iOS】iOS面试知识点学习(Objective-C语言特性,KVC,属性,2021.1.13更新)_第1张图片

图片来源:https://www.jianshu.com/p/e70bac443cf2

举例步骤:

1.创建了一个列表类协议UITableViewDelegate,视图类ListController遵循这个协议;

2.UITableView声明一个代理属性:@property (nonatomic, weak) id<代理协议> delegate;(UITableView本身应该不用遵循UITableViewDelegate协议)

3.ListController实现UITableViewDelegate的方法numberOfSectionsInTableView

4.设置代理:UITableView.delegate = ListController

5.当UITableView初始化时调用代理ListController的numberOfSectionsInTableView来获取section的数量

6.ListController执行代理方法numberOfSectionsInTableView,返回NSInteger告知UITableView结果

看到第二步,UITableView的代理属性使用weak关键词避免循环引用

解释一下循环引用的情况:如果delegate为强引用(strong),假设UITableView其实是ListController中的一个属性(很普遍的情况),那么UITableView持有了一个ListController对象强引用,外部持有了一个ListController的引用,同时ListController持有了一个UITableView强应用,造成了UITableView和ListController的循环引用

如果delegate采用assign修饰,没有引用计数操作,但当页面销毁时这个对应的代理地址并没置为nil,从而导致成为了一个野指针,然后在调用方法时便会产生崩溃

调用代理的语法:

if ([obj.delegate respondsToSelector:@selector(doSth)]) {
    [obj.delegate doSth];
}

题外话:感觉代理的特性和我平时block的部分用法有点异曲同工?


通知

通知中心NSNotificationCenter

通知中心负责发布通知和监听通知,每一个应用程序有一个通知中心实例,负责不同对象之间的消息通信。

任何一个对象都可以向通知中心发布通知NSNotification,描述自己在做什么。其他感兴趣的对象Observer可以申请在某个特定通知发布时(或在某个特定的对象发布通知时)收到这个通知

通知的封装NSNotification

通知的属性和初始化

- (NSString *)name; // 通知的名称 
- (id)object; // 通知发布者(是谁要发布通知)
- (NSDictionary *)userInfo; // 一些额外的信息(通知发布者传递给通知接收者的信息内容)

//初始化一个通知(NSNotification)对象
+ (instancetype)notificationWithName:(NSString *)aName object: (id)anObject;
+ (instancetype)notificationWithName:(NSString *)aName object: (id)anObject userInfo:(NSDictionary *)aUserInfo; 
- (instancetype)initWithName:(NSString *)name object:(id)object userInfo:(NSDictionary *)userInfo;

通知的发布

//通知中心的方法
- (void)postNotification:(NSNotification *)notification;
 发布一个notification通知,可在notification对象中设置通知的名称、通知发布者、额外信息等

- (void)postNotificationName:(NSString *)aName object:(id)anObject;
发布一个名称为aName的通知,anObject为这个通知的发布者

- (void)postNotificationName:(NSString *)aName object:(id)anObject userInfo:(NSDictionary *)aUserInfo;
 发布一个名称为aName的通知,anObject为这个通知的发布者, aUserInfo为额外信息```

通知的发送与处理时同步的,在某个地方发送一个通知时,会等到所有观察者对象执行完处理操作后,才回到发送通知的地方,继续执行后面的代码。

通知的监听

注册监听通知的监听器

-(void)addObserver:(id)observer selector:(SEL)aSelector name:(NSString )aName object:(id)anObject;
//observer:监听器,即谁要接收这个通知
//aSelector:收到通知后,回调监听器的这个方法,并且把通知对象当做参数传入*
//aName:通知的名称。如果为nil,那么无论通知的名称是什么,监听器都能收到这个通知
//anObject:通知发布者。如果为anObject和aName都为nil,监听器都收到所有的通知

- (id)addObserverForName:(NSNotificationName)name object:(id)obj queue:(NSOperationQueue *)queue usingBlock:(void (^)(NSNotification *note))block;
//name:通知的名称
//obj:通知发布者
//block:收到对应的通知时,会回调这个block
//queue:决定了block在哪个操作队列中执行,如果传nil,默认在当前操作队列中同步执行

第一个方法是常用的方法,让observer监听anObject发送的aName通知,接受通知后执行aSelector方法

第二个方法不太常见,创建了一个匿名对象作为观察者(返回的id)接受obj发送的name通知并且在queue线程执行block。

addObserverForName方法简析:

  1. name和obj和第一个方法的aName,anObject是相同的
  2. 如果queue为nil,则接受通知的线程和发送通知的线程相同
  3. block块会被通知中心拷贝copy一份,维护block对象知道观察者从通知中心中移除-》所以block中需要避免对象循环引用
  4. 如果给定通知出发了多个block,这些操作会在各自的queue中被并发执行,不能预期其顺序

对于3.所说情况网上有很多例子,可以自行百度,这里不再复述

取消注册(移除)监听器

-(void)removeObserver:(id)observer;
-(void)removeObserver:(id)observer name:(NSString *)aName object:(id)anObject;

前一个方法将observer从通知中心移除,而后一个会根据三个参数来移除相应的观察者。

注意对于用addObserverForName添加的监视器,正确的observer应该是addObserverForName返回的匿名对象,而不是注册通知所在类。

  1. 通知中心会维护一个观察者的unsafe_unretained引用,所以在释放对象时需要确保移除对象所有监听的通知,否则通知中心依然可以发送通知给观察者,产生不可预期的问题
  2. 第一个方法适合在类的dealloc方法中调用(注意出现循环引用时不会进入dealloc),确保将对象从通知中心移除;第二个方法适合在类的viewWillDisappear:中调用,这是为了避免移除父类/系统类的通知

总结来说通知的注册和移除有这样的对应

viewDidLoad->dealloc

viewWillAppear->viewWillDisappear

按照这个作者说法实际上根据系统版本可能可以不严格对应,因为

https://www.jianshu.com/p/26323f5b823d

不过还是严格对应最安全吧


KVO/Key-Value Observing

键值观察是是使用“获取其他对象的特定属性变化”的通知机制。可以使用KVO来检测对象属性的变化,快速做出响应,为开发强交互、响应式应用以及实现视图和模型的双向绑定时提供大量的帮助。

KVO的使用

使用KVO,需要注册成为某个对象属性的观察者,在合适的时间点将观察者移除,同时重写一个方法,在方法中判断是否为需要观察的属性发生变化。

这里假设Fizz类持有一个@property (nonatomic, strong) NSNumber *number 属性

//class ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.fizz = [[Fizz alloc] init];
    [self.fizz addObserver:self
                forKeyPath:@"number"
                   options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
                   context:nil];//注册成为某个对象属性的观察者
    self.fizz.number = @2;
}


- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    //重写一个方法,在方法中判断是否为需要观察的属性发生变化
    if ([keyPath isEqualToString:@"number"]) {
        NSLog(@"%@", change);
    }
}

- (void)dealloc {
    [self.fizz removeObserver:self forKeyPath:@"number"];//在合适的时间点将观察者移除
}

上述代码可以对应到前面所讲的使用KVO步骤。

第三个方法在大多数情况下只需要对比 keyPath ,就可以知道到底监控的是哪个对象,但是在更复杂的业务场景下,使用 context 上下文以及其它辅助手段才能够更加精准地确定被观测的对象

需要注意的情况:

  1. 在上述例子中,如果ViewController不持有Fizz对象,那么在viewDidLoad方法结束时Fizz就会被销毁,但是由于Fizz内并没有移除观察者的逻辑,就会造成crash,这种情况下的解决方法之一:Fizz持有观察者对象,并在自己的dealloc中移除观察者(的确不优雅,因为观察者的代码出现在了两个类中)
    //class ViewController
    Fizz *fizz = [[Fizz alloc] init];
    fizz.observer = self;
    
    //clas Fizz
    - (void)dealloc {
        [self removeObserver:self.observer forKeyPath:@"number"];
    }
    
  2. 注册观察者的代码和事件发生处的代码上下文不同,通过void *指针传递上下文

本小节参考:https://draveness.me/kvocontroller 作者并不推荐使用原生KVO

KVO实现机制

KVO的实现依赖于OC的Runtime运行时机制。

为一个对象注册观察者时,一个新的类会被动态创建,这个类继承自该对象原本的类,并重写了被观察属性的setter方法。重写的setter方法会负责在调用原setter方法之前和之后通知所有观察对象属性值的更改。最后再把对象的isa指针指向这个新创建的子类,对象就可以成为新创建的子类的实例。同时系统还重写了-class方法,这样就显得对象还是原来的类没有改变。

具体实现可以参考这篇Blog:https://tech.glowing.com/cn/implement-kvo/ 这里先不深入研究了


KVC/Key-Value Coding

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

KVC的使用

最重要的四个方法

- (nullable id)valueForKey:(NSString *)key;                          //直接通过Key来取值
- (void)setValue:(nullable id)value forKey:(NSString *)key;          //通过Key来设值
- (nullable id)valueForKeyPath:(NSString *)keyPath;                  //通过KeyPath来取值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;  //通过KeyPath来设值

具体使用这里就不细说了。

KVC的原理

参考:https://www.jianshu.com/p/45cbd324ea65

设值(基础Setter搜索模式)

调用setValue:forKey:时,底层执行机制:(这里以setValue:@"123" forKey:@"name"为例进行说明)

1.先调用setName:@"123" 方法,通过setter方法完成设置

2.如果没有找到setName:方法,KVC机制检查accessInstanceVariablesDirectly方法返回值,默认返回YES。如果重写该方法让其返回NO,则KVC执行setValue:forUndefined:方法(即不让该类实现KVC,找不到setName:就直接到5.)

3.如果accessInstanceVariablesDirectly方法返回YES,就会搜索该类里面有没有名为_name的成员变量,无论该变量是在类接口处定义,还是在类实现处,也无论用了什么样的访问修饰符,只要存在以_name命名的变量,KVC都可以对该成员变量赋值

4.如果上述操作都没有找到对应变量,接着搜索_isName的成员变量,之后再搜索name和isName成员变量。只要找到对应变量就给他们赋值

5.如果上述方法都找不到成员变量,系统会执行该对象的setValue:forUndefinedKey:方法,默认抛出异常。

取值(基础getter搜索模式)

调用valueForKey:时,KVC的搜索不同于setValue:forKey:(这里以valueForKey:@"name"为例进行说明)

1.首先按getName,name,isName的顺序方法查找getter方法,找到则直接调用。如果是bool,int等值类型,会将其包装成一个NSNumber对象

2.如果上面的方法没有找到,KVC查找countOfName,objectInNameAtIndex或nameAtIndexes方法,如果这三个方法中的countOfName和另外两个方法中的一个被找到,那就返回一个可以响应NSArray所有方法的代理集合NSKeyValueArray(NSArray子类),调用这个代理集合的方法,就会以countOfName,objectInNameAtIndex或nameAtIndexes这几个方法组合的形式调用。还有一个getName:range:方法可选,通过添加(重写)这些方法可以重新定义KVC的一些功能

3.如果没有找到上述方法,再查找countOfName,enumeratorOfName,memberOfName方法。如果三个方法都找到,返回一个可以响应NSSet所有方法的代理集合

2.的条件是countOfName+objectInNameAtIndex或者countOfName+nameAtIndexes,把对象当作NSArray

3.的条件是countOfName+enumeratorOfName+memberOfName,把对象当作NSSet

4.如果没有找到找到,检查类方法accessInstanceVariablesDirectly,如果返回YES,按_Name,_isName,name,isName的顺序搜索成员变量名。

5.如果accessInstanceVariablesDirectly返回NO或者没有搜索到变量名,则调用valueForUndefinedKey:方法并抛出异常。

KVC中使用keyPath

- (nullable id)valueForKeyPath:(NSString *)keyPath;                  //通过KeyPath来取值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;  //通过KeyPath来设值

回到这两个方法上,主要解决了一个类成员变量是复杂类型,需要多次使用valueForKey/setValue获取属性的问题。

keyPath是用小数点拼接的key,因此这两个方法的搜索机制的第一步是分割keyPath为key,然后像普通key一样搜索下去。

//TODO


属性/属性关键字

参考:OC知识--成员变量(属性,实例变量)的相关知识

参考:OC - 属性关键字和所有权修饰符

参考:@property详解,@property修饰符以及各个修饰符区别

成员变量(属性、实例变量)

成员变量一般是指在类中声明的变量,如下

@interface Iphone : NSObject
{
    // 成员变量声明
    int _cpu;            // cup   0
    int _size;     // 尺寸  0
    int _color;          // 颜色  0

    // 其中_cpu、_size、_color 就是 Iphone  类的成员变量
}

成员变量无法从外界被直接访问,因此通过getter-setter方法为调用者提供对成员变量的访问、赋值等操作

如果给成员变量提供了getter-setter方法,就可以通过点语法来访问成员变量

点语法的本质就是调用getter-setter方法,因此不能对没有getter-setter方法的成员变量使用点语,也不能在getter-setter方法中使用本属性的点语法,因为会造成死循环

成员变量可以使用修饰符来确定作用域:@public/@protected/@private/@package。@package是框架级别的,作用域介于私有和公开之间,只要处于同一框架就相当于@public,在框架外部相当于@private

在@interface@end之间声明的成员变量如果不做特别说明默认是为protected

@property和@synthesize

@property是声明属性方法(getter-setter)的语法

@interface
@property int size;
@end
    |
    V
- (int)size; 
- (void)setSize:(int)size;

@synthesize是实现属性方法的语法

@synthesize size;
    |
    V
- (int)size{

}

- (void)setSize:(int)size{

}

//注意:@synthesize size; 并没有告诉setter和getter 把size赋值给谁,返回谁

@synthesize size= _size;
    |
    V

- (int)size{

    return _size;

}

- (void)setSize:(int)size{

    _size = size;

}

 

按照上图代码,如果成员变量_size不存在,就会自动生成一个@private的成员变量_size

另外还有一个@dynamic,告诉编译器属性的setter和getter方法由用户自己实现不自动生成。如果一个属性被声明为@dynamic但没有提供setter/getter方法,编译不会出错,但在执行get/set操作时会程序崩溃

自Xcode4.4以后,apple对@property进行了一个增强,以后不用再写@synthesize了,只用一个@property就可以同时生成setter/getter方法的声明和实现。

默认@property会将传入的属性赋值给_开头的对应成员变量,如果没有则自动生成一个私有的_开头的成员变量

如果重写了setter/getter方法,那么@property只会生成getter/setter方法

如果同时重写了setter和getter方法,那么@property不会自动生成_开头的成员变量

@property修饰符

  • 原子性——atomic/nonatomic(默认atomic)

atomic在setter/getter方法内加锁(自动生成自旋锁代码),避免多线程下对一个属性进行setter/getter操作造成数据混乱,耗费系统资源。而且setter和getter之间并不会加锁,因此不能真正保证线程安全

  • 读写权限——readwrite/readonly(默认readwrite)

readonly只生成getter方法

  • 给setter/getter方法起别名

指定生成的setter方法名,setter = xxx:

指定生成的getter方法名,getter = xxx

  • 内存管理相关
  1. assign:默认关键字,setter方法的实现是直接赋值,一般用于基本数据类型。
    修饰基本数据类型、枚举、结构体等
    修饰对象类型时不增加其引用计数,assign修饰的对象被释放后,如果不手动置为nil,指针仍然指向原对象地址造成悬垂指针(野指针)
  2. weak:和assign类似,但是weak只能修饰OC对象,如果指向的对象消失,会自动置为nil,不会产生野指针。
  3. copy:先看一下深浅拷贝的总结:【iOS】iOS面试知识点学习(Objective-C语言特性,KVC,属性,2021.1.13更新)_第2张图片可以看到关键点在mutable身上,同时NSNumber算是个特例,可以理解为是常量所以做深拷贝吧。接下来比较copy和strong,对于字符串来说二者赋值都是浅拷贝,但是对于可变字符串对象(NSMutableString)来说,用strong修饰的字符串依旧进行了浅拷贝,而copy字符串进行了深拷贝。因此如果对于一个NSMutable对象进行了赋值,同时对原对象进行了更改,那么用strong修饰的对象也会变化,而copy对象则不会变化。Ex:block也使用copy修饰,主要是在MRC下block会在栈区,随时可能被销毁,进行copy后会放在堆区。不过ARC的block都会放在堆上
  4. strong/retain:针对对象类型进行内存管理,setter方法先将旧对象属性release,再将新对象赋值给属性并且retain,增加对象引用计数。strong用于ARC,retain用于一般MRC
  • 可空性

//TODO

 

 

你可能感兴趣的:(iOS学习,Objective-C,iOS,面试)