iOS-进阶题目

直击大厂——2020最全面面试题

2020-04-01  本文已影响0人  lp_lp

本文为群内一位小伙伴3月份7次面试而整理出来的面试题合集

1. UIViewCALayer是什么关系

> *   `UIView`继承自`UIResponder`类,可以响应事件
> *   `CALayer`直接继承自`NSObject`类,不可以响应事件
> *   `UIView`是`CALayer`的`delegate`(`CALayerDelegate`)
> *   `UIView`主要处理事件,`CALayer`负责绘制
> *   每个`UIView`内部都有一个`CALayer`在背后提供内容的绘制和显示,并且`UIView`的尺寸样式都由内部的`Layer`所提供。两者都有树状层级结构,`Layer`内部有`SubLayers`,`View`内部有`SubViews`,但是`Layer`比`View`多了个`AnchorPoint`

2. NSCacheNSMutableDictionary的相同点与区别

> 相同点:
> `NSCache`和`NSMutableDictionary`功能用法基本是相同的
> 区别:
> `NSCache`是线程安全的,`NSMutableDictionary`线程不安全,`Mutable开发的类`一般都是线程不安全的
> 当内存不足时`NSCache`会自动释放内存(所以从缓存中取数据的时候总要判断是否为空)
> `NSCache`可以指定缓存的限额,当缓存超出限额自动释放内存
> `NSCache`的`Key`只是对对象进行了`Strong`引用,而非拷贝,所以不需要实现`NSCopying`协议

3. atomic的实现机制;为什么不能保证绝对的线程安全(最好可以结合场景来说)

> *   `atomic`会对属性的`setter/getter`方法进行加锁,这仅仅只能保证在操作`setter/getter`方法是安全的。不能保证其他线程的安全
> *   例如:线程1调用了某一属性的`setter`方法并进行到了一半,线程2调用其`getter`方法,那么会执行完`setter`操作后,再执行`getter`操作,线程2会获取到线程1`setter`后的完整的值;当几个线程同时调用同一属性的`setter、getter`方法时,会获取到一个完整的值,但获取到的值不可控

4. iOS 中内省的几个方法

对象在运行时获取其类型的能力称为内省。内省可以有多种方法实现
OC运行时内省的4个方法:

-(BOOL) isKindOfClass:            // 判断是否是这个类或者这个类的子类的实例
-(BOOL) isMemberOfClass:      // 判断是否是这个类的实例

-(BOOL) respondsToSelector:                      // 判断实例是否有这样方法
+(BOOL) instancesRespondToSelector:      // 判断类是否有这个方法

5. objc在向一个对象发送消息时,发生了什么

> 根据对象的isa指针找到该对象所属的类,去objc的对应的类中找方法
> 1.首先,在相应操作的对象中的缓存方法列表中找调用的方法,如果找到,转向相应实现并执行
> 2.如果没找到,在相应操作的对象中的方法列表中找调用的方法,如果找到,转向相应实现执行
> 3.如果没找到,去父类指针所指向的对象中执行1,2.
> 4.以此类推,如果一直到根类还没找到,转向拦截调用,走消息转发机制
> 5.如果没有重写拦截调用的方法,程序报错

6. 你是否接触过OC中的反射机制?简单聊一下概念和使用

Class class = NSClassFromString(@"student"); 
Student *stu = [[class alloc] init];

Class class = [Student class];
NSString *className = NSStringFromClass(class);

SEL selector = NSSelectorFromString(@"setName");
[stu performSelector:selector withObject:@"Mike"];

7. 这个写法会出什么问题@property (nonatomic, copy) NSMutableArray *arr;

> 添加,删除,修改数组内元素的时候,程序会因为找不到对应的方法而崩溃。原因:是因为`copy`就是复制一个不可变`NSArray`的对象,不能对`NSArray`对象进行添加/修改

小编这呢,给大家推荐一个优秀的iOS交流学习圈子,圈子里的伙伴们都是非常优秀的iOS开发人员,我们专注于技术的分享、学习和交流,大家可以在圈子里讨论技术,交流学习。欢迎大家的加入(761407670密码123)点击进群

另外还在金三银四为大家准备了一套iOS面试题,可以进群自行下载

8. 如何让自己的类用copy修饰符

> 若想令自己所写的对象具有拷贝功能,则需实现`NSCopying`协议。如果自定义的对象分为可变版本与不可变版本,那么就要同时实现`NSCopying`与`NSMutableCopying`协议。
> 具体步骤:
> 1.需声明该类遵从`NSCopying`协议
> 2.实现`NSCopying`协议的方法,具体区别[戳这里](https://www.jianshu.com/p/f84803356cbb)
- (id)copyWithZone:(NSZone *)zone {
  MyObject *copy = [[[self class] allocWithZone: zone] init];
  copy.username = self.username;
  return copy;
}

9. 为什么assign不能用于修饰对象

> 首先我们需要明确,对象的内存一般被分配到堆上,基本数据类型和oc数据类型的内存一般被分配在栈上
> 如果用`assign`修饰对象,当对象被释放后,指针的地址还是存在的,也就是说指针并没有被置为`nil`,从而造成了野指针。因为对象是分配在堆上的,堆上的内存由程序员分配释放。而因为指针没有被置为`nil`,如果后续的内存分配中,刚好分配到了这块内存,就会造成崩溃
> 而`assign`修饰基本数据类型或oc数据类型,因为基本数据类型是分配在栈上的,由系统分配和释放,所以不会造成野指针

10. 请写出以下代码输出

int a[5] = {1, 2, 3, 4, 5};
int *ptr = (int *)(&a + 1);
printf("%d, %d", *(a + 1), *(ptr + 1));

参考答案:2,随机值
分析:
a代表有5个元素的数组的首地址,a[5]的元素分别是1,2,3,4,5。接下来,a + 1表示数据首地址加1,那么就是a[1],也就是对应于值为2,但是,这里是&a + 1,因为a代表的是整个数组,它的空间大小为5 * sizeof(int),因此&a + 1就是a + 5a是个常量指针,指向当前数组的首地址,指针+1就是移动sizeof(int)个字节
因此,ptr是指向int *类型的指针,而ptr指向的就是a + 5,那么ptr + 1也相当于a + 6,所以最后的*(ptr + 1)就是一个随机值了。而*(ptr – 1)就相当于a + 4,对应的值就是5

11. 一个view已经初始化完毕,view上面添加了n个button(可能使用循环创建),除用viewtag之外,还可以采用什么办法来找到自己想要的button来修改Button的值

> 第一种:如果是点击某个按钮后,才会刷新它的值,其它不用修改,那么不用引用任何按钮,直接在回调时,就已经将接收响应的按钮给传过来了,直接通过它修改即可
> 第二种:点击某个按钮后,所有与之同类型的按钮都要修改值,那么可以通过在创建按钮时将按钮存入到数组中,在需要的时候遍历查找

12. UIViewControllerviewDidUnload、viewDidLoadloadView分别什么时候调用?UIViewdrawRectlayoutSubviews分别起什么作用

> 第一个问题:
> 在控制器被销毁前会调用`viewDidUnload`(`MRC`下才会调用)
> 在控制器没有任何`view`时,会调用`loadView`
> 在`view`加载完成时,会调用`viewDidLoad`
> 第二个问题:
> 在调用`setNeedsDisplay`后,会调用`drawRect`方法,我们通过在此方法中可以获取到`context`(设置上下文),就可以实现绘图
> 在调用`setNeedsLayout`后,会调用`layoutSubviews`方法,我们可以通过在此方法去调整UI。当然能引起`layoutSubviews`调用的方式有很多种的,比如添加子视图、滚动`scrollview`、修改视图的`frame`等

13. 自动释放池工作原理

> 自动释放池是`NSAutorelease`类的一个实例,当向一个对象发送`autorelease`消息时,该对象会自动入池,待池销毁时,将会向池中所有对象发送一条`release`消息,释放对象
> `[pool release]、[pool drain]`表示的是池本身不会销毁,而是池子中的临时对象都被发送`release`,从而将对象销毁

14. 苹果是如何实现autoreleasepool

> `autoreleasepool`是由`AutoreleasePoolPage`以双向链表的方式实现的,主要通过下列三个函数完成:
> 
> *   由`objc_autoreleasePoolPush`作为自动释放池作用域的第一个函数
> *   使用`objc_autorelease`将对象加入自动释放池
> *   由`objc_autoreleasePoolPop`作为自动释放池作用域的最后一个函数

15. autorelease的对象何时被释放

RunLoop在每个事件循环结束后会去自动释放池将所有自动释放对象的引用计数减一,若引用计数变成了0,则会将对象真正销毁掉,回收内存。
在没有手动添加Autorelease Pool的情况下,autorelease的对象是在每个事件循环结束后,自动释放池才会对所有自动释放的对象的引用计数减一,若引用计数变成了0,则释放对象,回收内存。因此,若想要早一点释放掉autorelease对象,那么我们可以在对象外加一个自动释放池。比如,在循环处理数据时,临时变量要快速释放,就应该采用这种方式:

// 通过alloc创建的对象,直接加入@autoreleasepool没有作用,需在创建对象后面显式添加autorelease
// 通过类方法创建的对象不需要显式添加autorelease,原因是类方法创建的对象系统会自动添加autorelease
for (int i = 0; i < 1000000; i++) {
  @autoreleasepool {
    NSString *str = @"Abc";
    str = [str lowercaseString];
    str = [str stringByAppendingString:@"xyz"];
    NSLog(@"%@", str);
  } // 出了这里,就会去遍历该自动释放池了
}

16. 简述内存管理基本原则

> OC内存管理遵循`谁创建,谁释放,谁引用,谁管理`的机制,当使用`alloc、copy(mutableCopy)或者retian`一个对象时,你就有义务向它发送一条`release或者autorelease`消息释放该对象,其他方法创建的对象,不需要由你来管理内存,当对象引用计数为0时,系统将释放该对象,这是OC的手动管理机制(`MRC`)
> 向一个对象发送一条`autorelease`消息,这个对象并不会立即销毁,而是将这个对象放入了自动释放池,待池子释放时,它会向池中每一个对象发送一条`release`消息,以此来释放对象
> 向一个对象发送`release`消息,并不意味着这个对象被销毁了,而是当这个对象的引用计数为0时,系统才会调用`dealloc`方法释放该对象和对象本身所拥有的实例

17. sizeof关键字

> `sizeof`是在编译阶段处理,且不能被编译为机器码。`sizeof`的结果等于对象或类型所占的内存字节数。`sizeof`的返回值类型为`size_t`
> 变量:`int a; sizeof(a)`为4;
> 指针:`int *p; sizeof(p)`为4;
> 数组:`int b[10]; sizeof(b)`为数组的大小4*10;`int c[0]; sizeof(c)`等于0
> `sizeof(void)`等于1
> `sizeof(void *)`等于4

18. 什么是离屏渲染?什么情况下会触发?离屏渲染消耗性能的原因

> 离屏渲染就是在当前屏幕缓冲区以外,新开辟一个缓冲区进行操作
> 离屏渲染触发的场景有以下:
> 
> *   圆角(同时设置`layer.masksToBounds = YES、layer.cornerRadius`大于0)
> *   图层蒙版
> *   阴影,`layer.shadowXXX`,如果设置了`layer.shadowPath`就不会产生离屏渲染
> *   遮罩,`layer.mask`
> *   光栅化,`layer.shouldRasterize = YES`

离屏渲染消耗性能的原因
需要创建新的缓冲区,离屏渲染的整个过程,需要多次切换上下文环境,先是从当前屏幕(On-Screen)切换到离屏(Off-Screen)等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上,又需要将上下文环境从离屏切换到当前屏幕

19. ARC 下,不显式指定任何属性关键字时,默认的关键字都有哪些

> 基本数据类型默认关键字是:`atomic, readwrite, assign`
> 普通`Objective-C`对象默认关键字是:`atomic, readwrite, strong`

20. OC中的类方法和实例方法有什么本质区别和联系

> 类方法:
> 
> *   类方法是属于类对象的
> *   类方法只能通过类对象调用
> *   类方法中的 self 是类对象
> *   类方法可以调用其他的类方法
> *   类方法中不能访问成员变量
> *   类方法中不能直接调用对象方法

实例方法:

  • 实例方法是属于实例对象的
  • 实例方法只能通过实例对象调用
  • 实例方法中的 self 是实例对象
  • 实例方法中可以访问成员变量
  • 实例方法中直接调用实例方法
  • 实例方法中也可以调用类方法(通过类名)

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

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

22. runtime如何通过selector找到对应的IMP地址(分别考虑实例方法和类方法)Selector、Method 和 IMP的有什么区别与联系

> 对于实例方法,每个实例的`isa`指针指向着对应类对象,而每一个类对象中都有一个对象方法列表。对于类方法,每个类对象的`isa`指针都指向着对应的元类对象,而每一个元类对象中都有一个类方法列表。方法列表中记录着方法的名称,方法实现,以及参数类型,其实`selector`本质就是方法名称,通过这个方法名称就可以在方法列表中找到对应的方法实现
> `Selector、Method 和 IMP`的关系可以这样描述:在运行期分发消息,方法列表中的每一个实体都是一个方法(`Method`)它的名字叫做选择器(`SEL`)对应着一种方法实现(`IMP`)

23. objc_msgSend、_objc_msgForward都是做什么的?OC 中的消息调用流程是怎样的

> *   `objc_msgSend`是用来做消息发送的。在`OC`中,对方法的调用都会被转换成内部的消息发送执行
> *   `_objc_msgForward`是`IMP`类型(函数指针)用于消息转发的:当向一个对象发送一条消息,但它并没有实现的时候,`_objc_msgForward`会尝试做消息转发
> *   在消息调用的过程中,`objc_msgSend`的动作比较清晰:首先在`Class`中的缓存查找`IMP`(没缓存则初始化缓存)如果没找到,则向父类的`Class`查找。如果一直查找到根类仍旧没有实现,则用`_objc_msgForward`函数指针代替`IMP`。最后,执行这个`IMP`。当调用一个`NSObject`对象不存在的方法时,并不会马上抛出异常,而是会经过多层转发,层层调用对象的`-resolveInstanceMethod:、-forwardingTargetForSelector:、-methodSignatureForSelector:、-forwardInvocation:`等方法。其中最后`-forwardInvocation:`是会有一个`NSInvocation`对象,这个`NSInvocation`对象保存了这个方法调用的所有信息,包括`Selector名,参数和返回值类型`,可以从这个`NSInvocation`对象里拿到调用的所有参数值
>     ![image](https://img.haomeiwen.com/i1948913/f236e31f679fe683.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

24. class方法和objc_getClass方法有什么区别

> `object_getClass(obj)`返回的是`obj`中的`isa`指针,即指向类对象的指针;而`[obj class]`则分两种情况:一是当`obj`为实例对象时,`[obj class]`中`class`是实例方法,返回的是`obj`对象中的`isa`指针;二是当`obj`为类对象(包括元类和根类以及根元类)时,调用的是类方法,返回的结果为其本身

25. OC中向一个nil对象发送消息将会发生什么

> 在`OC`中向`nil`发送消息是完全有效的,只是在运行时不会有任何作用;向一个`nil`对象发送消息,首先在寻找对象的`isa`指针时就是`0地址`返回了,所以不会出现任何错误,也不会崩溃

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

> `_objc_msgForward`是一个函数指针(和`IMP`的类型一样)用于消息转发;当向一个对象发送一条消息,但它并没有实现的时候,`_objc_msgForward`会尝试做消息转发
> `objc_msgSend`在`消息传递`中的作用。在`消息传递`过程中,`objc_msgSend`的动作比较清晰:首先在`Class`中的缓存查找`IMP`(`没有缓存则初始化缓存`)如果没找到,则向`父类的Class`查找。如果一直查找到`根类`仍旧没有实现,则用`_objc_msgForward`函数指针代替`IMP`,最后执行这个`IMP`
> 一旦调用了`_objc_msgForward`,将跳过查找`IMP`的过程,直接触发`消息转发`,如果调用了`_objc_msgForward`,即使这个对象确实已经实现了这个方法,你也会告诉`objc_msgSend`,我没有在这个对象里找到这个方法的实现,如果用不好会直接导致程序`Crash`

27. 什么时候会报unrecognized selector的异常

// 重写 resolveInstanceMethod: 添加对象方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    // 如果是执行 run 函数,就动态解析,指定新的 IMP
    if (sel == NSSelectorFromString(@"run:")) {
        // class: 给哪个类添加方法
        // SEL: 添加哪个方法
        // IMP: 方法实现 => 函数 => 函数入口 => 函数名
        // type: 方法类型:void用v来表示,id参数用@来表示,SEL用:来表示
        class_addMethod(self, sel, (IMP)runMethod, "v@:@");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

//新的 run 函数
void runMethod(id self, SEL _cmd, NSNumber *meter) {
    NSLog(@"跑了%@", meter);
}

// 消息接受者重定向
- (id)forwardingTargetForSelector:(SEL)aSelector{
    if (aSelector == @selector(run:)) {
        return [[Person alloc] init];
        // 返回 Person 对象,让 Person 对象接收这个消息
    }
    return [super forwardingTargetForSelector:aSelector];
}

// 获取函数的参数和返回值类型,返回签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if ([NSStringFromSelector(aSelector) isEqualToString:@"run:"]) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
    }
    return [super methodSignatureForSelector:aSelector];
}

// 消息重定向
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    // 从 anInvocation 中获取消息
    SEL sel = anInvocation.selector;
    if (sel == NSSelectorFromString(@"run:")) {
        // 1\. 指定当前类的一个方法作为IMP
        // anInvocation.selector = @selector(readBook:);
        // [anInvocation invoke];

        // 2\. 指定其他类来执行这个IMP
        Person *p = [[Person alloc] init];
        // 判断 Person 对象方法是否可以响应 sel
        if([p respondsToSelector:sel]) {
            // 若可以响应,则将消息转发给其他对象处理
            [anInvocation invokeWithTarget:p];
        } else {
            // 若仍然无法响应,则报错:找不到响应方法
            [self doesNotRecognizeSelector:sel];
        }
    }else{
        [super forwardInvocation:anInvocation];
    }
}

- (void)doesNotRecognizeSelector:(SEL)aSelector {
    [super doesNotRecognizeSelector:aSelector];
}

既然-forwardingTargetForSelector:-forwardInvocation:都可以将消息转发给其他对象处理,那么两者的区别在哪?
区别就在于-forwardingTargetForSelector:只能将消息转发给一个对象。而-forwardInvocation:可以把消息存储,在你觉得合适的时机转发出去,或者不处理这个消息。修改消息的target,selector,参数等。将消息转发给多个对象

28. iOS layoutSubviews什么时候会被调用

> *   `init`方法不会调用`layoutSubviews`,但是是用`initWithFrame`进行初始化时,当`rect`的值不为`CGRectZero`时,会触发
> *   `addSubview`会触发`layoutSubviews`方法
> *   `setFrame`只有当设置的`frame`的参数的`size`与原来的`size`不同,才会触发其`view`的`layoutSubviews`方法
> *   滑动`UIScrollView`会调用`scrollview`及`scrollview`上的`view`的`layoutSubviews`方法
> *   旋转设备只会调用`VC`的`view`的`layoutSubviews`方法
> *   直接调用`[self setNeedsLayout];`(这个在上面苹果官方文档里有说明)
>     `-layoutSubviews`方法:这个方法默认没有做任何事情,需要子类进行重写
>     `-setNeedsLayout`方法:标记为需要重新布局,异步调用`layoutIfNeeded`刷新布局,不立即刷新,但`layoutSubviews`一定会被调用
>     `-layoutIfNeeded`方法:如果有需要刷新的标记,立即调用`layoutSubviews`进行布局(如果没有标记,不会调用`layoutSubviews`)
>     如果要立即刷新,要先调用`[view setNeedsLayout]`,把标记设为需要布局,然后马上调用`[view layoutIfNeeded]`,实现布局
>     在视图第一次显示之前,标记总是`需要刷新`的,可以直接调用`[view layoutIfNeeded]`

29. 下面代码会发生什么问题

@property (nonatomic, strong) NSString *str;

dispatch_queue_t queue = dispatch_queue_create("parallel", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 1000000 ; i++) {
    dispatch_async(queue, ^{
        self.str = [NSString stringWithFormat:@"changzifuchaung:%d",i];
    });
}

crash。因为在并行队列DISPATCH_QUEUE_CONCURRENT中异步dispatch_asyncstr属性进行赋值,就会导致str已经被release了,还会执行release。这就是向已释放内存的对象发送消息而发生crash
详细解析:对str属性strong修饰进行赋值,相当与MRC中的

- (void)setStr:(NSString *)str{
    if (str == _str) return;
    id pre = _str;
    [str retain];//1.先保留新值
    _str = str;//2.再进行赋值
    [pre release];//3.释放旧值
}

那么假如并发队列里调度的线程A执行到步骤1,还没到步骤2时,线程B执行到步骤3,那么当线程A再执行步骤3时,旧值就会被过度释放,导致向已释放内存的对象发送消息而崩溃

@property (nonatomic, strong) NSString *str;

dispatch_queue_t queue = dispatch_queue_create("parallel", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 1000000 ; i++) {
    dispatch_async(queue, ^{
        // 相比上面,仅字符串变短了
        self.str = [NSString stringWithFormat:@"%d",i];
        NSLog(@"%d, %s, %p", i, object_getClassName(self.str), self.str);
    });
}

不会crash。而且发现str这个字符串类型是NSTaggedPointerString
Tagged Pointer是一个能够提升性能、节省内存的有趣的技术
Tagged Pointer专门用来存储小的对象,例如NSNumberNSDate(后来可以存储小字符串)
Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已
它的内存并不存储在中,也不需要malloc和free,所以拥有极快的读取和创建速度


说在后面的话:

2019年iOS就已经很低迷了,2020这一场黑天鹅事件更是让本就低迷的iOS行业更是雪上加霜,我也相信大家也能感觉到,工作更加不好找了。 但这个社会本就是残酷的,物竞天择,适者生存,优胜劣汰,你只有不断地变强,不断地提升自己,才能站在顶端,才能不被淘汰!

加油!逆风而行的iOS开发者们!!!

密码123
上一篇下一篇

猜你喜欢

热点阅读