iOS 面试题-2019.上

  1. UIViewCALayer是什么关系
  • UIView继承自UIResponder类,能够响应事件
  • CALayer直接继承自NSObject类,不能够响应事件
  • UIViewCALayerdelegate(CALayerDelegate)
  • UIView主要处理事件,CALayer负责绘制
  • 每一个UIView内部都有一个CALayer在背后提供内容的绘制和显示,而且UIView的尺寸样式都由内部的Layer所提供。二者都有树状层级结构,Layer内部有SubLayersView内部有SubViews,可是LayerView多了个AnchorPoint
  1. NSCacheNSMutableDictionary的相同点与区别

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

  1. atomic的实现机制;为何不能保证绝对的线程安全(最好能够结合场景来讲)
  • atomic会对属性的setter/getter方法进行加锁,这仅仅只能保证在操做setter/getter方法是安全的。不能保证其余线程的安全
  • 例如:线程1调用了某一属性的setter方法并进行到了一半,线程2调用其getter方法,那么会执行完setter操做后,再执行getter操做,线程2会获取到线程1setter后的完整的值;当几个线程同时调用同一属性的setter、getter方法时,会获取到一个完整的值,但获取到的值不可控
  1. iOS 中内省的几个方法

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

  • 判断对象类型:
-(BOOL) isKindOfClass:            // 判断是不是这个类或者这个类的子类的实例
-(BOOL) isMemberOfClass:      // 判断是不是这个类的实例
复制代码
  • 判断对象/类是否有这个方法
-(BOOL) respondsToSelector:                      // 判断实例是否有这样方法
+(BOOL) instancesRespondToSelector:      // 判断类是否有这个方法
复制代码
  1. objc在向一个对象发送消息时,发生了什么

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

  1. 你是否接触过OC中的反射机制?简单聊一下概念和使用
  • class反射
  • 经过类名的字符串形式实例化对象
Class class = NSClassFromString(@"student"); 
Student *stu = [[class alloc] init];
复制代码
  • 将类名变为字符串
Class class = [Student class];
NSString *className = NSStringFromClass(class);
复制代码
  • SEL的反射
  • 经过方法的字符串形式实例化方法
SEL selector = NSSelectorFromString(@"setName");
[stu performSelector:selector withObject:@"Mike"];
复制代码
  • 将方法变成字符串 NSStringFromSelector(@selector(setName:));
  1. 这个写法会出什么问题@property (nonatomic, copy) NSMutableArray *arr;

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

  1. 如何让本身的类用copy修饰符

若想令本身所写的对象具备拷贝功能,则需实现NSCopying协议。若是自定义的对象分为可变版本与不可变版本,那么就要同时实现NSCopyingNSMutableCopying协议。 具体步骤: 1.需声明该类听从NSCopying协议 2.实现NSCopying协议的方法,具体区别戳这里缓存

  • NSCopying协议方法为:
- (id)copyWithZone:(NSZone *)zone {
  MyObject *copy = [[[self class] allocWithZone: zone] init];
  copy.username = self.username;
  return copy;
}
复制代码
  1. 为何assign不能用于修饰对象

首先咱们须要明确,对象的内存通常被分配到堆上,基本数据类型和oc数据类型的内存通常被分配在栈上 若是用assign修饰对象,当对象被释放后,指针的地址仍是存在的,也就是说指针并无被置为nil,从而形成了野指针。由于对象是分配在堆上的,堆上的内存由程序员分配释放。而由于指针没有被置为nil,若是后续的内存分配中,恰好分配到了这块内存,就会形成崩溃 而assign修饰基本数据类型或oc数据类型,由于基本数据类型是分配在栈上的,由系统分配和释放,因此不会形成野指针安全

  1. 请写出如下代码输出
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,对应的值就是5bash

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

第一种:若是是点击某个按钮后,才会刷新它的值,其它不用修改,那么不用引用任何按钮,直接在回调时,就已经将接收响应的按钮给传过来了,直接经过它修改便可 第二种:点击某个按钮后,全部与之同类型的按钮都要修改值,那么能够经过在建立按钮时将按钮存入到数组中,在须要的时候遍历查找并发

  1. UIViewControllerviewDidUnload、viewDidLoadloadView分别何时调用?UIViewdrawRectlayoutSubviews分别起什么做用

第一个问题: 在控制器被销毁前会调用viewDidUnloadMRC下才会调用) 在控制器没有任何view时,会调用loadViewview加载完成时,会调用viewDidLoad 第二个问题: 在调用setNeedsDisplay后,会调用drawRect方法,咱们经过在此方法中能够获取到context(设置上下文),就能够实现绘图 在调用setNeedsLayout后,会调用layoutSubviews方法,咱们能够经过在此方法去调整UI。固然能引发layoutSubviews调用的方式有不少种的,好比添加子视图、滚动scrollview、修改视图的frame异步

  1. 自动释放池工做原理

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

  1. 苹果是如何实现autoreleasepool

autoreleasepool是由AutoreleasePoolPage以双向链表的方式实现的,主要经过下列三个函数完成:

  • objc_autoreleasePoolPush做为自动释放池做用域的第一个函数
  • 使用objc_autorelease将对象加入自动释放池
  • objc_autoreleasePoolPop做为自动释放池做用域的最后一个函数
  1. 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);
 } // 出了这里,就会去遍历该自动释放池了
}
复制代码
  1. 简述内存管理基本原则

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

  1. 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

  1. 什么是离屏渲染?什么状况下会触发?离屏渲染消耗性能的缘由

离屏渲染就是在当前屏幕缓冲区之外,新开辟一个缓冲区进行操做 离屏渲染触发的场景有如下:

  • 圆角(同时设置layer.masksToBounds = YES、layer.cornerRadius大于0)
  • 图层蒙版
  • 阴影,layer.shadowXXX,若是设置了layer.shadowPath就不会产生离屏渲染
  • 遮罩,layer.mask
  • 光栅化,layer.shouldRasterize = YES

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

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

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

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

类方法:

  • 类方法是属于类对象的
  • 类方法只能经过类对象调用
  • 类方法中的 self 是类对象
  • 类方法能够调用其余的类方法
  • 类方法中不能访问成员变量
  • 类方法中不能直接调用对象方法

实例方法:

  • 实例方法是属于实例对象的
  • 实例方法只能经过实例对象调用
  • 实例方法中的 self 是实例对象
  • 实例方法中能够访问成员变量
  • 实例方法中直接调用实例方法
  • 实例方法中也能够调用类方法(经过类名)
  1. 可否向编译后获得的类中增长实例变量?可否向运行时建立的类中添加实例变量?为何?
  • 不能向编译后获得的类中增长实例变量
  • 能向运行时建立的类中添加实例变量
  • 由于编译后的类已经注册在runtime中,类结构体中的objc_ivar_list实例变量的链表和instance_size实例变量的内存大小已经肯定,同时runtime会调用class_setIvarLayoutclass_setWeakIvarLayout来处理strong weak引用,因此不能向存在的类中添加实例变量 运行时建立的类是能够添加实例变量,调用class_addIvar函数。可是得在调用objc_allocateClassPair以后,objc_registerClassPair以前,缘由同上
  1. runtime如何经过selector找到对应的IMP地址(分别考虑实例方法和类方法)Selector、Method 和 IMP的有什么区别与联系

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

  1. objc_msgSend、_objc_msgForward都是作什么的?OC 中的消息调用流程是怎样的
  • objc_msgSend是用来作消息发送的。在OC中,对方法的调用都会被转换成内部的消息发送执行
  • _objc_msgForwardIMP类型(函数指针)用于消息转发的:当向一个对象发送一条消息,但它并无实现的时候,_objc_msgForward会尝试作消息转发
  • 在消息调用的过程当中,objc_msgSend的动做比较清晰:首先在Class中的缓存查找IMP(没缓存则初始化缓存)若是没找到,则向父类的Class查找。若是一直查找到根类仍旧没有实现,则用_objc_msgForward函数指针代替IMP。最后,执行这个IMP。当调用一个NSObject对象不存在的方法时,并不会立刻抛出异常,而是会通过多层转发,层层调用对象的-resolveInstanceMethod:、-forwardingTargetForSelector:、-methodSignatureForSelector:、-forwardInvocation:等方法。其中最后-forwardInvocation:是会有一个NSInvocation对象,这个NSInvocation对象保存了这个方法调用的全部信息,包括Selector名,参数和返回值类型,能够从这个NSInvocation对象里拿到调用的全部参数值
  1. class方法和objc_getClass方法有什么区别

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

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

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

  1. _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

  1. 何时会报unrecognized selector的异常
  • 当调用该对象上某个方法,而该对象上没有实现这个方法的时候。能够经过消息转发进行解决,流程见下图
  • OC在向一个对象发送消息时,runtime库会根据对象的isa指针找到该对象实际所属的类,而后在该类中的方法列表以及其父类方法列表中寻找方法运行,若是在最顶层的父类中依然找不到相应的方法时,程序在运行时会挂掉并抛出异常unrecognized selector sent to XXX 可是在这以前,OC的运行时会给出三次拯救程序崩溃的机会
  • Method resolution(消息动态解析) OC运行时会调用+resolveInstanceMethod:或者+resolveClassMethod:,让你有机会提供一个函数实现。若是你添加了函数,那运行时系统就会从新启动一次消息发送的过程,不然,运行时就会移到下一步,消息转发(Message Forwarding
// 重写 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);
}
复制代码
  • Fast forwarding(消息接受者重定向) 若是目标对象实现了-forwardingTargetForSelector:Runtime这时就会调用这个方法,给你把这个消息转发给其余对象的机会。只要这个方法返回的不是nilself,整个消息发送的过程就会被重启,固然发送的对象会变成你返回的那个对象。不然,就会继续Normal Fowarding。 这里叫Fast,只是为了区别下一步的转发机制。由于这一步不会建立任何新的对象,但下一步转发会建立一个NSInvocation对象,因此相对更快点
// 消息接受者重定向
- (id)forwardingTargetForSelector:(SEL)aSelector{
    if (aSelector == @selector(run:)) {
        return [[Person alloc] init];
        // 返回 Person 对象,让 Person 对象接收这个消息
    }
    return [super forwardingTargetForSelector:aSelector];
}
复制代码
  • Normal forwarding(消息重定向) 这一步是Runtime最后一次给你挽救的机会。首先它会发送-methodSignatureForSelector:消息得到函数的参数和返回值类型。若是-methodSignatureForSelector:返回nilRuntime则会发出-doesNotRecognizeSelector:消息,程序这时也就挂掉了。若是返回了一个函数签名,Runtime就会建立一个NSInvocation对象并发送-forwardInvocation:消息给目标对象
// 获取函数的参数和返回值类型,返回签名
- (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,参数等。将消息转发给多个对象

  1. iOS layoutSubviews何时会被调用
  • init方法不会调用layoutSubviews,可是是用initWithFrame进行初始化时,当rect的值不为CGRectZero时,会触发
  • addSubview会触发layoutSubviews方法
  • setFrame只有当设置的frame的参数的size与原来的size不一样,才会触发其viewlayoutSubviews方法
  • 滑动UIScrollView会调用scrollviewscrollview上的viewlayoutSubviews方法
  • 旋转设备只会调用VCviewlayoutSubviews方法
  • 直接调用[self setNeedsLayout];(这个在上面苹果官方文档里有说明) -layoutSubviews方法:这个方法默认没有作任何事情,须要子类进行重写 -setNeedsLayout方法:标记为须要从新布局,异步调用layoutIfNeeded刷新布局,不当即刷新,但layoutSubviews必定会被调用 -layoutIfNeeded方法:若是有须要刷新的标记,当即调用layoutSubviews进行布局(若是没有标记,不会调用layoutSubviews) 若是要当即刷新,要先调用[view setNeedsLayout],把标记设为须要布局,而后立刻调用[view layoutIfNeeded],实现布局 在视图第一次显示以前,标记老是须要刷新的,能够直接调用[view layoutIfNeeded]
  1. 下面代码会发生什么问题
@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时,旧值就会被过分释放,致使向已释放内存的对象发送消息而崩溃

  • 追问:怎么修改这段代码变为不崩溃呢

一、使用串行队列 将set方法改为在串行队列中执行就行,这样即便异步,但全部block操做追加在队列最后依次执行 二、使用atomic atomic关键字至关于在setter方法加锁,这样每次执行setter都是线程安全的,但这只是单独针对setter方法而言的狭义的线程安全 三、使用weak关键字 weaksetter没有保留新值的操做,因此不会引起重复释放。固然这个时候要看具体状况可否使用weak,可能值并非所须要的值 四、使用互斥锁,保证数据访问的惟一性@synchronized (self) {self.str = [NSString stringWithFormat:@"changzifuchaung:%d",i];} 五、使用Tagged Pointer Tagged Pointer是苹果在64位系统引入的内存技术。简单来讲就是对于NSString(内存小于60位的字符串)或NSNumber(小于2^31),64位的指针有8个字节,彻底能够直接用这个空间来直接表示值,这样的话其实会将NSStringNSNumber对象由一个指针转换成一个值类型,而值类型的setter和getter又是原子的,从而线程安全

  • 发散:下面代码会crash
@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,因此拥有极快的读取和建立速度

附:个人博客地址

相关文章
相关标签/搜索