OC面试题 一、OC语法

434 阅读42分钟

OC对象的本质

static和const的区别?

1. static

规定作用域和存储方式。 只在当前模块可见,不能通过extern来引用。

修饰局部变量:

  1. 让局部变量只初始化一次
  2. 局部变量在程序中只生成一份内存
  3. 延长局部变量的生命周期,程序结束才会销毁。

修饰全局变量:

只能在本文件中访问,作用域仅限于当前文件。

2. const

被const修饰的变量是只读的,不可以修改,只在声明中使用。

image.png 开发中经常用到static和const一起使用的情况,如 定义一个只能在当前文件访问的全局常量

static  类型   const   常量名  = 初始化值

例如:static NSString * const test = @"abc";

你对多态的理解?

多态:子类重写父类的方法,父类指针指向子类。 有一个tableView,它有多种cell,cell的UI差异较大,但是它们的model类型又都是一样的。

由于这几种cell都具有相同类型的model,那么你肯定会先建一个基类cell,如:

@interface BaseCell : UITableViewCell

@property (nonatomic, strong) Model *model;

@end

然后各种cell继承自这个基类cell:

image.png

@interface RedCell : BaseCell

@end

子类cell重写BaseCell的setModel:方法:

// 重写父类的setModel:方法
- (void)setModel:(Model *)model {
    // 调用父类的setModel:方法
    super.model = model;
    
    // do something...
}

在controller中:

// cell复用ID array
- (NSArray *)cellReuseIdArray {
    if (!_cellReuseIdArray) {
        _cellReuseIdArray = @[RedCellReuseID, GreenCellReuseID, BlueCellReuseID];
    }
    return _cellReuseIdArray;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *cellResueID = nil;
    cellResueID = self.cellReuseIdArray[indexPath.section];
    // 父类
    BaseCell *cell = [tableView dequeueReusableCellWithIdentifier:cellResueID];
    // 创建不同的子类
    if (!cell) {
        switch (indexPath.section) {
            case 0: // 红
            {
                // 父类指针指向子类
                cell = [[RedCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellResueID];
            }
                break;
                
            case 1: // 绿
            {
                // 父类指针指向子类
                cell = [[GreenCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellResueID];
            }
                break;
                
            case 2: // 蓝
            {
                // 父类指针指向子类
                cell = [[BlueCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellResueID];
            }
                break;
        }
    }
    // 这里会调用各个子类的setModel:方法
    cell.model = self.dataArray[indexPath.row];
    return cell;
}

不出意外,类似于上面的代码我们都写过,其实这里就运用到了类的多态性。

多态的三个条件:

  • 继承:各种cell继承自BaseCell
  • 重写:子类cell重写BaseCell的setModel:方法
  • 指向:父类cell指针指向子类cell

以上,就是多态在实际开发中的体现。

合理运用类的多态性可以降低代码的耦合度让代码更易扩展。

把这些概念性的东西跟日常实际开发相结合,你就能秒懂且难忘。

面试题:一个NSObject对象占用多少内存?

  • 系统分配了16个字节给NSObject对象(通过malloc_size函数获得)
  • 但NSObject对象内部只使用了8个字节的空间(64bit环境下,可以通过class_getInstanceSize函数获得)

isa指针是什么?

isa(is a kind of)是一个Class类型的指针,而classstruct objc_class *的别名,所以isa实际上是指向objc_class类型结构体的指针。

面试题:对象的isa指针指向哪里?

  • instance对象的isa指向class对象
  • classs对象的isa指向meta-class对象
  • meta-class对象的isa指向基类的meta-class对象

面试题:OC的类信息存放在哪里?

  • 成员变量的具体值,存放在instance对象中
  • 对象方法、属性、成员变量、协议信息,存放在类对象中
  • 类方法,存储在meta-class对象中

面试题:KVO的本质是什么?

KVO(Key-Value Observing)的本质是通过 动态创建子类并重写setter方法 来实现对属性变化的监听和通知机制。它允许对象监听其他对象的指定属性,并在这些属性的值发生变化时接收到通知。以下是关于KVO本质的详细解释:

一、KVO的基本概念

  • 定义:KVO,即键值监听,是一种机制,它允许将其他对象的指定属性的更改通知给对象。
  • 应用场景:常用于监听数据模型的变化,实现视图组件和数据模型的分离,以及两个类间的解耦合。

二、KVO的实现原理

  • 动态子类创建:当 注册一个观察者 时,系统会 动态地创建一个新的子类 (如 NSKVONotifying_A),并将 被观察对象的isa指针指向这个新的子类 。这个 新子类重写了被观察属性的setter方法
  • setter方法重写:在新子类的setter方法中,除了 执行原来的属性赋值操作外,还会调用Foundation框架中的相关函数(如_NSSetXXXValueAndNotify) ,这个函数会负责通知观察者属性值的更改。
  • 通知观察者:当 属性值发生变化时,新的子类setter方法会触发通知机制 ,调用观察者的observeValueForKeyPath:ofObject:change:context:方法,将属性变化的详细信息(如旧值、新值等)传递给观察者。

三、KVO的优点和注意事项

  • 优点

    • 实现了视图组件和数据模型的分离,降低了类之间的耦合度。
    • 简化了属性变化通知的发送过程,开发者 无需手动编写通知代码
  • 注意事项

    • 在使用KVO时,必须确保在观察者被销毁之前移除对其的监听,否则可能会导致野指针崩溃等问题。
    • KVO依赖于Runtime机制,因此在某些情况下(如属性为C结构体或标量时),可能需要额外的包装处理。

面试题:KVC底层原理

iOS中的KVC(Key-Value Coding)是一种强大的特性,允许开发者通过字符串键来访问对象的属性。其底层原理涉及多个方面,以下是对其的详细解释:

一、KVC的核心概念

  • 键值编码:KVC通过字符串键来间接访问对象的属性,而无需直接调用属性的getter或setter方法。
  • 动态消息发送:Objective-C的运行时系统支持在运行时解析和发送消息,这使得KVC能够在不知道对象属性具体类型的情况下动态访问这些属性。

二、KVC的底层实现流程

  1. 查找方法

    • 当通过KVC访问属性时,系统会首先尝试在对象的类对象的方法列表中查找对应的getter或setter方法。例如,对于键"name",系统会查找"name"、"isName"、"_name"、"_isName"等方法。
    • 如果找到了相应的方法,系统就会直接调用该方法来访问或修改属性的值。
  2. 访问成员变量

    • 如果在方法列表中未找到对应的方法,系统会调用类的+ (BOOL)accessInstanceVariablesDirectly方法来检查是否允许直接访问类的成员变量。
    • 如果该方法返回YES,系统会按照"_key"、"isKey"、"key"、"isKey"的顺序在类对象的成员属性列表中查找对应的成员变量,并直接访问或修改其值。
    • 如果未找到对应的成员变量或accessInstanceVariablesDirectly方法返回NO,系统会抛出异常。
  3. 键路径支持

    • KVC还支持键路径(Key Paths),允许访问嵌套属性。例如,对于一个对象的属性是另一个对象的属性,可以通过"property1.property2"的形式访问。
    • 系统会解析键路径中的每个部分,依次查找每个属性,直到找到最终要访问的属性。

三、KVC的性能优化

  • 缓存机制:为了提高性能,KVC会缓存一些常用的键值对访问结果,减少重复查找的开销。
  • 内部缓存结构:KVC使用一个称为_kvcCache的内部结构来存储属性的访问结果。这个缓存通常会在对象的类中以字典形式实现,键是属性名或键路径,值是对应的属性值。
  • 缓存更新:当对象的属性值发生改变时,KVC会自动清除或更新相关的缓存项,以确保缓存中的值是最新的。

四、KVC与KVO的关系

  • KVO(Key-Value Observing) :是一种监听回调机制,允许对象观察另一个对象的属性变化。
  • KVO依赖KVC:KVO通过KVC的机制来访问和设置属性值,并自动注册和通知观察者属性的变化。

五、KVC的使用示例

	@interface Person : NSObject  

	@property (nonatomic, strong) NSString *name;  

	@end  

	  

	@implementation Person  

	@end  

	  

	Person *person = [[Person alloc] init];  

	[person setValue:@"John" forKey:@"name"];  

	NSString *name = [person valueForKey:@"name"];  

	NSLog(@"Name: %@", name); // 输出: Name: John

在这个示例中,setValue:forKey:valueForKey:方法通过KVC动态访问name属性。

综上所述,iOS中的KVC是一种强大的特性,其底层原理涉及动态消息发送、方法查找、成员变量访问、键路径支持以及性能优化等多个方面。了解KVC的底层原理有助于开发者更好地利用这一特性,并在设计对象模型时考虑其影响。

面试题:strong, weak, assign, copy 的区别

  1. strong

  • 强引用,只可以修饰对象,属性的默认修饰符,其修饰的对象引用计数增加1

  1. weak

  • 弱引用,只可以修饰对象指向但不拥有对象,其修饰的对象引用计数不增加,可以避免循环引用,weak修饰的对象释放后,指针会被系统置为nil,此时向对象发送消息不会奔溃

  1. assign

  • 可以修饰对象和基本数据类型,如果修饰对象,其修饰的对象引用计数不增加,可以避免循环引用,但assign修饰的对象释放后,指针不会被系统置为nil,这会产生野指针的问题,此时向对象发送消息会崩溃。所以assign通常用于基本数据类型,如int ,float, NSInteger, CGFloat ,这是因为基本数据类型放在栈区,先进先出,基本数据类型出栈后,assign修饰的变量就不存在了,不用担心指针的问题

  1. copy

  • 引用,修饰不可变的对象,比如NSString, NSArray, NSDictionary。copy和strong类似,不同之处在于,copy修饰的对象会先在内存中拷贝一个新的对象,copy会指向那个新的对象的内存地址,这样避免了多个指针指向同一对象,而导致的其中一个指针改变了对象,其他指针指向的对象跟着改变。 copy的原则就是,把一个对象赋值给一个属性变量,当这个对象变化了,如果希望属性变量变化就使用strong属性,如果希望属性变量不跟着变化,就使用copy属性

简述readwrite,readonly,assign,retain,copy,nonatomic属性的作用?

在Objective-C中,readwritereadonlyassignretaincopynonatomic等属性关键字在类的属性声明中起着重要作用,它们各自的作用如下:

  1. readwrite

    • 作用:修饰对象属性为 可读可写,即 同时生成getter和setter方法的声明和实现
    • 使用场景:默认情况下,属性是readwrite的,如果需要明确指定属性为可读可写,可以使用此关键字。
  2. readonly

    • 作用:修饰对象属性为 只读,即**只生成getter方法的声明和实现,不生成setter方法**。
    • 使用场景:当希望外界只能读取某个属性的值而不能修改时,可以使用此关键字。通常,在.h文件中用readonly修饰,而在.m文件里面仍然可以用readwrite修饰,以便类内部可以修改该属性的值。
  3. assign

    • 作用:修饰基本数据类型(如NSInteger、CGFloat)和C数据类型(如int、float、double、char等)。它表示 对属性进行简单赋值,不改变所赋值的引用计数
    • 使用场景:由于基本数据类型和C数据类型不存在内存管理问题,因此使用assign进行赋值即可。
  4. retain

    • 作用:修饰对象类型,表示持有特性。setter方法将传入参数先保留(即增加其引用计数),再赋值。
    • 使用场景:在启用ARC(自动引用计数)机制之前,retain常用于管理对象的生命周期。但在ARC机制下,由于编译器会自动管理对象的引用计数,因此较少使用retain。不过,在某些需要手动管理内存的场景下(如与Core Foundation框架交互时),retain仍然可能用到。
  5. copy

    • 作用:表示拷贝特性。setter方法将传入的对象复制一份新的副本,并释放旧对象(如果旧对象存在的话)。新副本的引用计数为1,与旧对象无直接联系。
    • 使用场景:常用于修饰字符串(如NSString)或其他需要拷贝的对象。当不希望外界直接修改传入的对象时,可以使用copy来创建一个新的副本。
  6. nonatomic

    • 作用:表示非原子操作。它决定编译器生成的getter和setter方法是否为原子操作。
    • 使用场景:如果对象的属性 无需考虑多线程并发访问的安全性问题,可以使用nonatomic来提高性能。因为nonatomic不会生成互斥加锁代码,所以执行效率更高。但需要注意的是,使用nonatomic时,需要自行保证属性的线程安全性。

比较synthesize & dynamic

  1. 通过@synthesize指令告诉编译器在编译期间产生getter/setter方法。
  2. 通过@dynamic指令,自己实现方法。 有些存取是在运行时动态创建的,如在CoreData的NSManagedObject类使用的某些。如果你想这些情况下,声明和使用属性,但要避免缺少方法在编译时的警告,你可以使用@dynamic动态指令,而不是@synthesize合成指令。

delegate和block区别

  1. 从源头上理解和区别block和delegate

  • delegate运行成本低,block的运行成本高。

  • block出栈需要将使用的数据从栈内存拷贝到堆内存,当然对象的话就是加计数,使用完或者block置nil后才消除。delegate只是保存了一个对象指针,直接回调,没有额外消耗。就像C的函数指针,只多做了一个查表动作。

  1. 从使用场景区别block和delegate

  • 有多个相关方法。假如每个方法都设置一个block, 这样会更麻烦。而 delegate 让多个方法分成一组,只需要设置一次,就可以多次回调。当多于 3 个方法时就应该优先采用 delegate。当1,2个回调时,则使用block

  1. 安全性

  • delegate更安全些,比如: 避免循环引用。使用 block 时稍微不注意就形成循环引用,导致对象释放不了。这种循环引用,一旦出现就比较难检查出来。而 delegate 的方法是分离开的,并不会引用上下文,因此会更安全些

代理Delegate、Block、通知的优缺点?

  1. 代理 优点:

  • 代理语法清晰,可读性高,易于维护
  • 它减少代码耦合性,使事件监听与事件处理分离
  • 一个控制器可以实现多个代理,满足自定义开发需求,灵活性较高

缺点:

  • 实现代理的过程较繁琐

  • 跨层传值时加大代码的耦合性,并且程序的层次结构也变得混乱

  • 当多个对象同时传值时不易区分,导致代理易用性大大降低

  1. Block 优点:

  • 语法简洁,代码可读性和维护性较高

  • 配合GCD优秀的解决多线程问题 缺点:

  • Block中的代码将自动进行一次retain操作,容易造成内存泄漏

  • Block内默认引用为强引用,容易造成循环应用

  1. 通知 优点:

  • 使用简单,代码精简
  • 支持一对多,解决同时向多个对象监听的问题
  • 传值方便快捷,context自带携带相应的内容 缺点:
  • 通知使用完毕后需要注销,否则会造成意外崩溃
  • key不够安全,编译器不会检测到是否被通知中心处理
  • 调试时难以跟踪
  • 当使用者向通知中心发送通知的时候,并不能获得任何反馈信息
  • 需要一个第三方的对象来做监听与被监听者的中介

UIView和CALayer之间的关系?

  1. UIView可以响应事件,CALayer不可以响应事件

    • UIView继承自UIResponder,在UIResponder中定义了处理各种事件和事件传递的接口,可以响应系统传递过来的事件并进行处理。

    • CALayer继承自NSObject,并没有相应的处理事件的接口。因此图层本身无法响应用户操作事件却拥有事件响应链相似的判断方法。

  2. UIView是CALayer的delegate

  3. UIView主要处理事件,CALayer负责绘制

  4. 每个UIView内部都有一个CALayer在背后提供内容的绘制和显示,并且UIView的尺寸样式都由内部的Layer所提供。两者都有树状层级结构,layer内部有subLayers,View内部有SubViews。但是Layer比View多了个AnchorPoint

  5. 我们在修改UIView的界面属性的时间其实是修改了这个UIView对应的layer的属性

  6. CALayer可以独立于UIView之外显示在屏幕上,如CAShapelayer。 NSString、NSArray、NSDictionary在Objective-C中经常使用copy关键字,这是由它们的特性和使用场景决定的。下面将详细解释为什么这些类型经常使用copy关键字,以及使用strong可能造成的问题。

简述NSString,NSArray,NSDictionary经常使用copy关键字,为什么?如果使用Strong可能造成什么问题

为什么使用copy关键字

  1. 保护封装性

    • NSString、NSArray、NSDictionary都有对应的可变类型:NSMutableString、NSMutableArray、NSMutableDictionary。
    • 当一个对象的属性被声明为这些类型的指针时,如果外部传入一个可变对象(如NSMutableString)并对其进行修改,那么持有该属性的对象中的值也可能会不经意间被改变。
    • 使用 copy 关键字可以确保在设置新属性值时, 拷贝一份不可变的副本(对于NSString)或深拷贝一份集合(对于NSArray和NSDictionary) ,从而保护对象的封装性,防止外部修改影响内部状态
  2. 内存管理

    • 在ARC(自动引用计数)环境下,copy特质会自动处理内存管理,确保拷贝后的对象在适当的时候被释放。
  3. 线程安全

    • 虽然copy本身不直接提供线程安全,但使用它可以帮助避免在多线程环境下因共享可变对象而导致的竞争条件(race conditions)。

使用strong可能造成的问题

  1. 属性被外部修改

    • 如果使用 strong 关键字,那么属性可能指向一个 可变对象。如果这个可变对象在外部被修改了(例如,添加、删除或修改NSArray中的元素),那么持有该属性的对象中的值也会被相应地修改。
    • 这可能导致不可预测的行为和难以调试的错误。
  2. 违反设计原则

    • 使用copy通常更符合不可变性的设计原则,特别是在需要保证对象状态不被外部改变的情况下。
    • 使用strong可能会让其他开发者误以为可以安全地修改该属性指向的对象,从而导致潜在的错误。
  3. 内存泄漏和循环引用

    • 虽然这与copystrong的直接选择关系不大,但在使用这些关键字时需要注意内存泄漏和循环引用的问题。
    • 特别是在复杂的数据结构和引用关系中,需要谨慎地管理内存和引用计数。

综上所述,NSString、NSArray、NSDictionary经常使用copy关键字是为了保护封装性、确保内存管理的正确性以及避免潜在的多线程问题。而使用strong可能会造成属性被外部修改、违反设计原则以及潜在的内存泄漏和循环引用问题。因此,在大多数情况下,对于这些类型的属性声明,使用copy是更安全和更合适的选择。

frame和bounds的区别

  • frame不管对于位置还是大小,改变的都是自己本身。
  • frame的位置是以父视图的坐标系为参照,从而确定当前视图在父视图中的位置。
  • frame的大小改变时,当前视图的左上角位置不会发生改变,只是大小发生改变。
  • bounds改变位置时,改变的是子视图的位置,自身没有影响;其实就是改变了本身的坐标系原点,默认本身坐标系的原点是左上角。
  • bounds的大小改变时,当前视图的中心点不会发生改变,当前视图的大小发生改变,看起来效果就像缩放一样。

NSProxy & NSObject

  • NSObject: NSObject协议组对所有的Objective - C下的objects都生效。如果objects遵从该协议,就会被看作是first - class objects (一级类)。另外,遵从该协议的objects的retain,release,autorelease等方法也服从objects的管理和在Foundation中定义的释放方法。一些容器中的对象也可以管理这些objects,比如说NSArrayNSDictionary定义的对象。Cocoa的根类也遵循该协议,所以所有继承NSObjects的objects都有遵循该协议的特性。
  • NSProxy: NSProxy是一个虚基类,它为一些表现的像是其它对象替身或者并不存在的对象定义一套API。一般的,发送给代理的消息被转发给一个真实的对象或者代理本身load(或者将本身转换成)一个真实的对象。NSProxy的基类可以被用来透明的转发消息或者耗费巨大的对象的lazy初始化

Category

Category的使用场合是什么?

  • 扩展类的方法
  • Category编译之后的底层结构是struct category_t,里面存储着分类的对象方法、类方法、属性、协议信息
  • 在程序运行的时候,runtime会将Category的数据,合并到类信息中(类对象、元类对象中)

Category和Class Extension的区别是什么?

  • Class Extension编译的时候,它的数据就已经包含在类信息中
  • Category是在运行时,才会将数据合并到类信息中

Category能否添加成员变量?如果可以,如何给Category添加成员变量?

  • 不能直接给Category添加成员变量,但是可以通过runtime运行时和关联函数间接实现Category有成员变量的效果

  • 关联对象提供了以下API

  1. 添加关联对象 void objc_setAssociatedObject(id object, const void * key,id value, objc_AssociationPolicy policy)

  2. 获得关联对象 id objc_getAssociatedObject(id object, const void * key)

  3. 移除所有的关联对象 void objc_removeAssociatedObjects(id object)

关联函数原理

  1. 关联对象并不是存储在被关联对象本身内存中
  2. 关联对象存储在全局的统一的一个AssociationsManager
  3. 设置关联对象为nil,就相当于是移除关联对象。

关联对象的原理

实现关联对象技术的核心对象有

  • AssociationsManager
  • AssociationsHashMap
  • ObjectAssociationMap
  • ObjcAssociation

image.png

image.png

Category中有load方法吗?load方法是什么时候调用的?load 方法能继承吗?

  1. 有load方法
  2. load方法在runtime加载类、分类的时候调用
  3. load方法可以继承,但是一般情况下不会主动去调用load方法,都是让系统自动调用

load、initialize方法的区别什么?它们在category中的调用的顺序?以及出现继承时他们之间的调用过程?

一. 区别:

  1. 调用方式

  • load是根据函数地址直接调用

  • initialize是通过objc_msgSend调用(消息发送机制)

  1. 调用时刻

  • load是runtime加载类、分类的时候调用(只会调用一次)

  • initialize是类第一次接收到消息的时候调用, 每一个类只会initialize一次(如果子类没有实现initialize方法, 会调用父类的initialize方法, 所以父类的initialize方法可能会调用多次) 二. 调用顺序:

  1. load:

  • 先调用类的load, 再调用分类的load

  • 先编译的类, 优先调用load, 调用子类的load之前, 会先调用父类的load

  • 先编译的分类, 优先调用load

  1. initialize

  • 先初始化分类, 后初始化子类
  • 通过消息机制调用, 当子类没有initialize方法时, 会调用父类的initialize方法, 所以父类的initialize方法会调用多次

tableView的优化

  1. 减轻CPU负荷

  • 提前计算好cell的高度,缓存在相应的数据源模型中

  • 尽可能的降低storyboard,xib等使用度

  • 滑动过程中尽量减少重新布局

  • 圆角性能优化 服务器传圆角图片、贝塞尔切割控件layer, 先下载图片,再圆角处理,再在cell上显示

  1. 不要阻塞主线程

  • 减少离屏渲染
  • 减少栅格化

OC消息机制

讲一下OC的消息机制

  • OC中的方法调用其实都是转成了objc_msgSend函数的调用,给receiver(方法调用者)发送了一条消息(selector方法名

  • objc_msgSend底层有3大阶段

  1. 消息发送(当前类、父类中查找)、动态方法解析、消息转发

消息发送机制

  • receiver(方法调用者)通过isa指针找到receiverClass

  • 从receiverClass的cache中查找方法

  • 从receiverClass的class_rw_t中查找方法

  • receiverClass通过superclass指针找到superClass

  • 父类查找(沿着继承链向父类查找)

  • 动态解析 image.png

动态方法解析

  1. 没有动态解析过,开发者可以实现以下方法,来动态添加方法实现

    • (BOOL)resolveInstanceMethod:(SEL)selector
    • (BOOL)resolveClassMethod:(SEL)selector

  1. 标记为动态解析
  2. 重走消息发送机制 image.png

消息转发机制

  • (id)forwardingTargetForSelector:(SEL)selector
  • methodSignatureForSelector:
  • (void)forwardInvocation:(NSInvocation *)invocation
  • doesNotRecognizeSelector:

image.png

事件传递和响应机制

  1. 事件的产生

发生触摸事件后,系统会将该事件加入到一个由UIApplication管理的事件队列中,为什么是队列而不是栈?因为队列的特点是FIFO,即先进先出,先产生的事件先处理才符合常理,所以把事件添加到队列。

UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常,先发送事件给应用程序的主窗口(keyWindow)。

主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件,这也是整个事件处理过程的第一步。

找到合适的视图控件后,就会调用视图控件的touches方法来作具体的事件处理。

  1. 事件的传递 UIApplication队列->keywindow->处理事件最合适的view 触摸事件的传递是从父控件传递到子控件 也就是UIApplication->window->寻找处理事件最合适的view 注意: 如果父控件不能接受触摸事件,那么子控件就不可能接收到触摸事件 3. 应用如何找到最合适的控件来处理事件?

  • 首先判断主窗口(keyWindow)自己是否能接受触摸事件

  • 判断触摸点是否在自己身上

  • 子控件数组中从后往前遍历子控件,重复前面的两个步骤(所谓从后往前遍历子控件,就是首先查找子控件数组中最后一个元素,然后执行1、2步骤)

  • 如果没有符合条件的子控件,那么就认为自己最合适处理这个事件,也就是自己是最合适的view。

  1. 事件的响应

4.1 触摸事件处理的整体过程 视图->上一响应者(nextResponder)->顶级视图->window->UIApplication->丢弃

  • 用户点击屏幕后产生的一个触摸事件,经过一系列的传递过程后,会找到最合适的视图控件来处理这个事件
  • 找到最合适的视图控件后,就会调用控件的touches方法来作具体的事件处理touchesBegan…touchesMoved…touchedEnded…
  • 这些touches方法的默认做法是将事件顺着响应者链条向上传递(也就是touch方法默认不处理事件,只传递事件),将事件交给上一个响应者进行处理

4.2 响应者链的事件传递过程

  • 如果当前view是控制器的view,那么控制器就是上一个响应者,事件就传递给控制器;如果当前view不是控制器的view,那么父视图就是当前view的上一个响应者,事件就传递给它的父视图
  • 在视图层次结构的最顶级视图,如果也不能处理收到的事件或消息,则其将事件或消息传递给window对象进行处理
  • 如果window对象也不处理,则其将事件或消息传递给UIApplication对象
  • 如果UIApplication也不能处理该事件或消息,则将其丢弃

swift实现一个单例

class ShareInstance {
   static let instance = ShareInstance()
}

swift实现一个静态类方法

class ShareInstance {
    static func a() {
    }
}

关联函数为分类添加属性

@interface Person (Extention)

@property (nonatomic, copy) NSString *name;

@end

#import "Person+Extention.h"

#import <objc/runtime.h>

@implementation Person (Extention)

- (void)setName:(NSString *)name {
    objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)name {
    return objc_getAssociatedObject(self, _cmd);
}

MethodSwizzing

#import "UIViewController+Extention.h"

#import <objc/runtime.h>

@implementation UIViewController (Extention)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self swizzleInstanceMethod:[self class] original:@selector(viewDidLoad) swizzled:@selector(swizzle_viewDidLoad)];
    });
}

/// UIViewController 某个分类
+ (void)swizzleInstanceMethod:(Class)target original:(SEL)originalSelector swizzled:(SEL)swizzledSelector {
    Method originMethod = class_getInstanceMethod(target, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(target, swizzledSelector);
    BOOL success = class_addMethod(target, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
    if (success) {
        class_replaceMethod(target, swizzledSelector, method_getImplementation(originMethod), method_getTypeEncoding(originMethod));
    } else {
  method_exchangeImplementations(originMethod, swizzledMethod);
    }
}

/// hook
- (void)swizzle_viewDidLoad {
    [self swizzle_viewDidLoad];
}
@end
#import "UITableView+Extention.h"

#import <objc/runtime.h>

@implementation UITableView (Extention)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self swizzleInstanceMethod:[self class] original:@selector(initWithFrame:style:) swizzled:@selector(swizzle_initWithFrame:style:)];
        [self swizzleInstanceMethod:[self class] original:@selector(init) swizzled:@selector(swizzle_init)];
    });
}

/// UIViewController 某个分类
+ (void)swizzleInstanceMethod:(Class)target original:(SEL)originalSelector swizzled:(SEL)swizzledSelector {
    Method originMethod = class_getInstanceMethod(target, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(target, swizzledSelector);
    BOOL success = class_addMethod(target, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
    if (success) {
        class_replaceMethod(target, swizzledSelector, method_getImplementation(originMethod), method_getTypeEncoding(originMethod));
    } else {
   method_exchangeImplementations(originMethod, swizzledMethod);
    }
}

- (instancetype)swizzle_initWithFrame:(CGRect)frame style:(UITableViewStyle)style {
    UITableView * tableView = [self swizzle_initWithFrame:frame style:style];
#if __IPHONE_OS_VERSION_MAX_ALLOWED > __IPHONE_14_2
#if __IPHONE_OS_VERSION_MAX_ALLOWED > __IPHONE_14_3
#if __IPHONE_OS_VERSION_MAX_ALLOWED > __IPHONE_14_5
        if (@available(iOS 15.0, *)) {
            tableView.sectionHeaderTopPadding = 0;
        }
#endif
#endif
#endif
    return  tableView;
}

- (instancetype)swizzle_init {
    UITableView *tableView = [self swizzle_init];
#if __IPHONE_OS_VERSION_MAX_ALLOWED > __IPHONE_14_2
#if __IPHONE_OS_VERSION_MAX_ALLOWED > __IPHONE_14_3
#if __IPHONE_OS_VERSION_MAX_ALLOWED > __IPHONE_14_5
        if (@available(iOS 15.0, *)) {
            tableView.sectionHeaderTopPadding = 0;
        }
#endif
#endif
#endif
    return tableView;
}

@end

响应链: 如果 Swizzle 了 父 View 的 touchBegin 的方法, 会对子 View 造成什么影响?

#import "FatherView.h"

#import <objc/runtime.h>

@implementation FatherView

- (void)swizzleTouchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
     NSLog(@"哈哈哈");
}

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
            [self swizzleInstanceMethod:[self class] original:@selector(touchesBegan:withEvent:) swizzled:@selector(swizzleTouchesBegan:withEvent:)];
    });
}

/// UIViewController 某个分类
+ (void)swizzleInstanceMethod:(Class)target original:(SEL)originalSelector swizzled:(SEL)swizzledSelector {
    Method originMethod = class_getInstanceMethod(target, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(target, swizzledSelector);
    BOOL success = class_addMethod(target, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
    if (success) {
        class_replaceMethod(target, swizzledSelector, method_getImplementation(originMethod), method_getTypeEncoding(originMethod));
    } else {
  method_exchangeImplementations(originMethod, swizzledMethod);
    }
}
@end
SonView *sonView = [SonView new];
sonView.frame = CGRectMake(100, 100, 100, 100);
sonView.backgroundColor = UIColor.redColor;
[self.view addSubview:sonView];

输出结果:哈哈哈,事件被拦截,需要子类重写方法。

两个按钮同时响应问题?

  1. 第一种方法,在AppDelegate中,添加如下: [[UIButton appearance] setExclusiveTouch:YES];

  2. 第二种方法,为button写一个分类,设置属性button.exclusiveTouch = YES;

虽然exclusiveTouch是UIView的属性,但是如果是UIImageView使用UITapGestureRecognizer的方式添加点击事件的情况下,这个字段是无效的

简单区分UIResponder与UIControl

  • UIResponder主要是响应某个动作,执行某个行为,处理交互事件
  • UIControl在继承了前者的属性基础上,还能够响应某个动作,为某个对象,添加动作

已经上线的项目,用FMDB存储user表,现在要给user表新加字段,如何做

  • 判读数据表中该字段是否存在 columnExists inTableWithName
  • 不存在就执行sqlite的插入字段语句 executeUpdate

layoutSubviews调用时机

  1. addSubview
  2. 更改父视图的大小
  3. UIScrollView滚动的时候
  4. 旋转屏幕

响应者链、事件的传递

1.响应者链

响应链是从最合适的view开始传递,处理事件传递给下一个响应者,响应者链的传递方法是事件传递的反方法,如果所有响应者都不处理事件,则事件被丢弃。我们通常用响应者链来获取上几级响应者,方法是UIResponder的nextResponder方法
2. Hit-Test机制(事件传递)

  • 当iOS程序中 发生触摸事件 后,系统会将事件加入到 UIApplication管理的一个任务队列中
  • UIApplication将处于任务队列最前端的事件向下分发。即 UIWindow
  • UIWindow将事件向下分发,即 UIView
  • UIView首先看自己是否能处理事件,触摸点是否在自己身上。如果能,那么继续寻找子视图
  • 遍历子控件,重复以上两步。
  • 如果没有找到,那么自己就是事件处理者
  • 如果自己不能处理,那么不做任何处理。 当用户触摸(Touch)屏幕进行交互时,系统首先要找到响应者(Responder)。系统检测到手指触摸(Touch)操作时,将Touch以UIEvent的方式加入UIApplication事件队列中。UIApplication从事件队列中取出最新的触摸事件进行分发传递到UIWindow进行处理。UIWindow会通过hitTest:withEvent:方法寻找触碰点所在的视图,这个过程称之为hit-test View。
UIApplication -> UIWindow -> Root View -> ... -> subview

顶级视图(Root View)上调用pointInside:withEvent:方法判断触摸点是否在当前视图内;

如果返回NO,那么hitTest:withEvent:返回nil;

如果返回YES,那么它会向当前视图的所有子视图发送hitTest:withEvent:消息,所有子视图的遍历顺序是从最顶层视图一直到到最底层视图,即从subviews数组的末尾向前遍历,直到有子视图返回非空对象或者全部子视图遍历完毕。

如果有subview的hitTest:withEvent:返回非空对象则返回此对象,处理结束(注意这个过程,子视图也是根据pointInside:withEvent:的返回值来确定是返回空还是当前子视图对象的。并且这个过程中如果子视图的hidden=YES、userInteractionEnabled=NO或者alpha小于0.1都会并忽略);

如果所有subview遍历结束仍然没有返回非空对象,则hitTest:withEvent:返回self;

系统就是这样通过hit test找到触碰到的视图(Initial View)进行响应。

NSMutable属性声明时为什么不能使用copy

NSMutable类并没有重写非可变类的copy方法,给属性赋值时,调用的是父类的copy方法,得到的对象是一个非可变的对象

  1. 为什么不能使用copy 众所周知,所有的可变类都是继承于非可变类的,属于可变类的子类,拿NSMutableString类来举例,大家进入到NSMutableString类的.h文件可以看到它是继承于NSString类的,而且NSMutableString并没有重写NSString类的copy方法,所以我们如果声明NSMutableString类属性时使用了copy进行标识,在我们对这个属性进行赋值时,调用的其实是NSString类的copy方法拿到的实例对象其实是一个NSString的实例而不是一个NSMutableString的实例,下面使用代码给大家验证一下:
#import "ViewController.h"

@interface ViewController ()

@property (nonatomic, copy) NSMutableString *cpStr;
@property (nonatomic, strong) NSMutableString *stStr;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    NSMutableString *tempStr = [[NSMutableString alloc]initWithFormat:@"可变对象"];
    self.cpStr = tempStr;
    self.stStr = tempStr;
    NSLog(@"\n tempStr: %p -> %@, \n stStr: %p -> %@, \n cpStr: %p -> %@", tempStr, [tempStr classForCoder], self.stStr, [self.stStr classForCoder], self.cpStr, [self.cpStr classForCoder]);
}

/**
 tempStr: 0x600001724960 -> NSMutableString,
 stStr: 0x600001724960 -> NSMutableString,
 cpStr: 0x600001973220 -> NSString
 */
@end

从输出结果可以看到tempStr是一个NSMutableString对象,使用Strong修饰声明的属性stStr只是对tempStr对象添加了一个引用计数,并没有产生新的对象实例,所以tempStr和stStr的class方法其实调用的是同一个对象的方法,所以输出的结果是一样的。而使用copy修饰声明的属性cpStr在被赋值时,会调用tempStr对象的copy方法产生一个新的对象,而且从输出结果可以看到这个对象是NSString对象。

综上所述,NSMutable属性声明不能使用copy进行修饰是因为NSMutable类并没有重写非可变类的copy方法,给属性赋值时,调用的是父类的copy方法,得到的对象是一个非可变的对象

  1. 使用了copy会怎样:

由于给对象赋值时得到的对象是非可变对象,所以我们使用该属性调用可变对象的特有方法时程序会崩溃(因为对象根本响应不了该方法),下面我们同样适用代码给大家验证一下: image.png tempStr对象和stStr属性的replaceCharactersInRange方法均执行成功,但是程序运行到[self.cpStr replaceCharactersInRange:NSMakeRange(0, 1) withString:@""]这句代码时崩溃了,所以这个问题对程序的影响还是很大的,而且这个的bug很难被找出来,所以在声明NSMutable属性时一定要多加注意。

静态库和动态库的区别?

1、linux中静态库和动态库区别:

静态库:这类库的名字一般是libxxx.a;利用静态函数库编译成的文件比较大,因为整个函数库的所有数据都会被整合进目标代码中,他的优点就显而易见了,即编译后的执行程序不需要外部的函数库支持,因为所有使用的函数都已经被编译进去了。当然这也会成为他的缺点,因为如果静态函数库改变了,那么你的程序必须重新编译

动态库:这类库的名字一般是libxxx.so;相对于静态函数库,动态函数库在编译的时候 并没有被编译进目标代码中,你的程序执行到相关函数时才调用该函数库里的相应函数,因此动态函数库所产生的可执行文件比较小。由于函数库没有被整合进你的程序,而是程序运行时动态的申请并调用,所以程序的运行环境中必须提供相应的库动态函数库的改变并不影响你的程序,所以动态函数库的升级比较方便

  1. iOS开发中静态库和动态库区别:

静态库和动态库是相对编译期和运行期的:静态库在程序编译时会被链接到目标代码中,程序运行时将不再需要改静态库;而动态库在程序编译时并不会被链接到目标代码中,只是在程序运行时才被载入,因为在程序运行期间还需要动态库的存在。

静态库 好处

  1. 模块化,分工合作,提高了代码的复用及核心技术的保密程度
  2. 避免少量改动经常导致大量的重复编译连接
  3. 也可以重用,注意不是共享使用

动态库 好处:

  1. 使用动态库,可以将最终可执行文件体积缩小,将整个应用程序分模块,团队合作,进行分工,影响比较小
  2. 使用动态库,多个应用程序共享内存中得同一份库文件,节省资源
  3. 使用动态库,可以不重新编译连接可执行程序的前提下,更新动态库文件达到更新应用程序的目的。
  4. 应用插件化
  5. 软件版本实时模块升级

NSSet和NSArray的区别

  1. NSSetNSArray两者功能性质一样,用于存储对象,属于集合。但是和NSArray不一样的是它属于"无序集合",在内存中存储方式是不连续的,而NSArray有序集合,它在内存中存储位置是连续的
  2. NSSet的效率确实是比NSArray高,因为它主要用的是hash算法
  3. 时间复杂度上来看,找出集合NSSet是否包含某个元素,运行时间为O(1),而NSArray的运行时间需要为O(n)

ios中nil、Nil、Null、[NSNull null]区别和联系

不管是NULL、nil还是Nil,它们本质上都是一样的,都是(void *)0,只是写法不同。这样做的意义是为了区分不同的数据类型,比如你一看到用到了NULL就知道这是个C指针,看到nil就知道这是个Objective-C对象,看到Nil就知道这是个Class类型的数据。

nil 就是空对象。把一个对象置成nil之后,就不能对其进行retain, copy等引用计数相关的操作了

在iOS中,Nil完全等同于nil。

NUll就是C语言中的一个空指针,在Objective-C中也可以使用。

[NSNull null]是值为空的对象,nil是一个空对象,已经完全从内存中消失了,而如果我们想表达“我们需要有这样一个对象,但这个对象里什么也没有”的观念时,就需要[NSNull null]这个对象了。OC中数组、字典等对象中插入nil会crash,但是我们可以插入值为空的对象[NSNull null]

OC中的方法重载?

OC中没有严格的方法重载,原因是不允许方法名相同,OC也没有运算符重载一说。

objc/runtime中SEL、IMP和method动态定义

  • SEL selector的简写,俗称方法选择器,实质存储的是方法的名称
  • IMP implement的简写,俗称方法实现,看源码得知它就是一个函数指针
  • Method 对上述两者的一个包装结构

class 的底层结构是什么样的?

image.png

method_t 里包含什么?

  • method_t 是对方法/函数的封装
struct method_t {
    // "big" method传统意义上的方法(用selector、类型和实现的三指针结构体)
    struct big {
        SEL name;// 函数名
        const char *types;// 编码(返回值类型、参数类型)
        MethodListIMP imp;// 指向函数的指针
    };
private:
    // "small" method是用名称、类型和实现3个相对偏移指针
    struct small {
        // 名称字段指向一个selector(共享缓存中),或者指向一个selref(在其他地方)
        RelativePointer<const void *> name;
        RelativePointer<const char *> types;
        RelativePointer imp;
        ...
    };

public:
    // 与方法列表一起使用的指针修饰符。当方法列表中包含小方法时,设置指针的底部位。
    // 我们在其他地方使用底部位来区分大方法和小方法。
    struct pointer_modifier {
        template 
        static method_t *modify(const ListType &list, method_t *ptr) {...}
    };
    ...
};
  • SEL代表方法/函数名,一般叫选择器(selector),底层结构类似char*

不同类中相同名字的方法,所对应的selector都相同且唯一。可以通过@Selector()和sel_registerName()获得

  • Type Encoding

iOS文档中提供了@encode()指令,可以将具体的类型表示成字符串编码

对self和super的理解

@implementation Son : Father
- (id)init
{
self = [super init];
    if(self) {
        NSLog(@"%@", NSStringFromClass([self class]));
        NSLog(@"%@", NSStringFromClass([super class]));
}
returnself;
}

答案:都输出 Son

self 是类的隐藏参数,指向当前调用方法的这个类的实例。而 super 是一个 Magic Keyword, 它本质是一个编译器标示符和 self 是指向的同一个消息接受者。 上面的例子不管调用[self class]还是[super class],接受消息的对象都是当前 Son *xxx 这个对象。而不同的是,super是告诉编译器,调用 class 这个方法时,要去父类的方法,而不是本类里的。
当使用 self 调用方法时,会从当前类的方法列表中开始找,如果没有,就从父类中再找;而当使用 super 时,则从父类的方法列表中开始找。然后调用父类的这个方法。

当调用 [self class] 时,实际先调用的是 objc_msgSend函数,第一个参数是 Son当前的这个实例,然后在 Son 这个类里面去找 - (Class)class这个方法,没有,去父类 Father里找,也没有,最后在 NSObject类中发现这个方法。而 - (Class)class的实现就是返回self的类别,故上述输出结果为 Son。

而当调用 [super class]时,会转换成objc_msgSendSuper函数。第一步先构造 objc_super 结构体,结构体第一个成员就是 self 。 第二个成员是 (id)class_getSuperclass(objc_getClass(“Son”)) , 实际该函数输出结果为 Father。 第二步是去 Father这个类里去找 - (Class)class,没有,然后去NSObject类去找,找到了。最后内部是使用 objc_msgSend(objc_super->receiver, @selector(class))去调用, 此时已经和[self class]调用相同了,故上述输出结果仍然返回 Son

ViewController的生命周期

image.png

ViewController查找与其关联的view

  1. 子类 是否重写了loadView 是则直接调用。然后ViewDidLoad完成View的加载。
  2. 如果子类没有重写的loadView,则 ViewController会从StoryBoards中找或者调用其默认的loadView ,默认的loadView返回一个空白的UIView对象
  3. 外部通过调用initWithNibName:bundle 指定nib文件名,ViewController加载此nib来创建View。
  4. 若外部initWithNibName:bundle 的name参数为nil即未指定nib文件名,则ViewController会通过以下两个步骤找到与其关联的nib:

a): 如果类名包含Controller,例如ViewController的类名是MyViewController,则 查找是否存在MyView.nib

b): 找跟ViewController类名一样的文件,例如MyViewController,则 查找是否存在MyViewController.nib

  1. 如果子类没有重写的loadView,则ViewController会从StroyBoards中找或者调用其默认的loadView,默认的loadView 返回一个空白的UIView对象

什么是数据持久化,一般你使用什么方式实现?

数据持久化就是将文件保存在本地的 硬盘 中,使得 应用程序或者机器重启后可以继续访问以前保留的数据

  1. NSUserDefaults
  2. plist
  3. Keychain(钥匙串)
  4. 归档
  5. 沙盒写入
  6. 数据库

结构体和类的区别?

结构体
封装属性属性、方法
分配
空间
访问效率
赋值直接赋值对象地址
  1. 结构体只能封装属性,类却不仅可以封装属性也可以封装方法。如果一个封装的数据有属性也有行为,就只能用类了。

  2. 结构体变量分配在,而OC对象分配在,栈的空间相对于堆来说是比较小的,但是存储在栈中的数据访问效率相对于堆而言是比较高

  3. 堆的存储空间比较大,存储在堆中的数据访问效率相对于栈而言是比较低的

  4. 如果定义一个结构体,这个结构体中有很多属性,那么这个时候结构体变量在栈中会占据很多空间,这样的话就会降低效率

5、我们使用结构体的时候最好是属性比较少的结构体对象如果属性较多的话就要使用类了。

6、结构体赋值的话是直接赋值,对象的指针,赋值的是对象的地址。

NSDictionary的实现原理是什么?

NSDictionary(字典)是使用 hash表 来实现key和value之间的映射和存储的,hash函数设计的好坏影响着数据的查找访问效率。

OC与JS交互方式有哪些?

  1. 通过 拦截URL
  2. 使用 MessageHandler(WKWebView)
  3. JavaScriptCore(UIWebView)
  4. 使用三方库 WebViewJavascriptBridge,可提供JS调OC,以及OC调JS

遇到过BAD_ACCESS的错误吗?你是怎样调试的?

BAD_ACCESS 报错属于 内存访问错误,会导致 程序崩溃,错误的原因是 访问了野指针(悬挂指针)

  1. 设置 全局断点快速定位问题代码所在行
  2. 开启 僵尸对象诊断
  3. Analyze分析
  4. 重写object的respondsToSelector方法,显示出现EXEC_BAD_ACCESS前 访问的最后一个object对象
  5. Xcode 7 已经集成了BAD_ACCESS捕获功能:Address Sanitizer。 用法如下:在配置中勾选✅Enable Address Sanitizer

位运算的好处?

位运算是一种极为高效乃至可以说最为高效的计算方式,虽然现代程序开发中编译器已经为我们做了大量的优化,但是合理的使用位运算 可以提高代码的可读性以及执行效率

iOS矢量图后缀

矢量图片的扩展名要看是用哪个矢量画图软件生成的,目前比较常见的矢量作图软件有Adobe Illstrator和Corel Draw ,它们生成的文件的扩展名分别是.eps.cdr

.bmp是标准的位图图片格式,bmp就是bit map的简写。

无痕埋点(自动埋点)解决方案

技术原理:Method-Swizzling

对于一个给定的事件,UIControl 会调用 sendAction:to:forEvent: 来将行为消息转发到 UIApplication对象 ,再由 UIApplication对象 调用其 sendAction:to:fromSender:forEvent: 方法来将消息分发到 指定的target上 ,那么,我们写一个 UIControl的类别 ,通过替换它的 sendAction:to:forEvent: 方法 ,结合本地配置的埋点json或者plist文件(若埋点需要额外的参数,需要给UIControl的类别通过Runtime添加属性),便可以实现自动埋点的功能

说一下iOS 中的APNS,远程推送原理?

Apple push Notification Service,简称 APNS ,是苹果的远程消息推送,原理如下:

  1. iOS系统向 APNS服务器 请求 手机端的deviceToken
  2. App 接收到手机端的 deviceToken,然后传给 App对应的服务器
  3. App服务端需要发送推送消息时, 需要先通过 APNS服务器
  4. 然后 根据对应的 deviceToken 发送给对应的手机