待学习iOS面试那些事iOS

02-面试iOS工程师常被问到的20个基础问题

2016-08-20  本文已影响263人  Smy

1.什么是 ARC ? ( ARC 是为了解决什么问题诞生的 ? )####

自动引用计数 ( ARCAutomatic Reference Counting ): 是指内存管理中对引用采取自动计数的技术,在Objective-C 中采用 ARC 机制,让编译器来进行内存管理. 在新一代 Apple LLVM编译器中设置 ARC 为有效状态,就无需再次键入 retainrelease 代码,这在降低程序崩溃、内存泄漏等风险的同时,很大程度上减少了开发程序的工作量.编译器完全清楚目标对象,并能立刻释放那些不再被使用的对象.

ARC 是为了解决什么问题诞生的:需追溯到 MRC 手动内存管理时代.MRC 内存管理存在的问题:


2. 请解释以下 Keywords 的区别: assign 和 weak,block 和 weak?####

assignweak

__block__weak


参考文章:
正确使用Block避免Cycle Retain和Crash - Cooper's Blog
参考书籍:《 Objective-C 高级编程 iOS 与 OS X 多线程和内存管理 - 第2章 Blocks 》


3. __block 在 ARC 和非 ARC 下含义一样吗 ?####

不一样,标记为 __block 的变量,在 block中使用,在 ARC 下会 retain,在非 ARC下则不会 retain.


参考链接:iOS: ARC和非ARC下使用Block属性的问题


4.使用 atomic 一定是线程安全的吗 ?####

nonatomic 的内存管理语义是非原子的,非原子的操作本来就是线程不安全的,而 atomic 的操作是原子的,但是并不意味着它是线程安全的,它会增加正确的几率,能够更好的避免线程的错误,但它仍然是线程不安全的.
当使用 nonatomic的时候,属性的 settergetter 操作是非原子的,所以当多个线程同时对某一属性进行读和写的操作,属性的最终结果是不能预测的.

当使用 atomic 时,虽然对属性的读和写是原子的,但是仍然可能出现线程错误:当线程 A 进行写操作,这时其他线程的读或写操作会因为该操作的进行而等待,当 A 线程的写操作结束后,B 线程进行写操作,然后当 A 线程进行读操作时,却获得了在 B 线程中的值,这就破坏了线程安全,如果有线程 C 在 A 线程读操作前 release 了该属性,那么还会导致程序崩溃.所以仅仅使用 atomic 并不会使得线程安全,我们还需要为线程添加lock来确保线程的安全.

注意atomic 所说的线程安全只是保证了gettersetter 存取方法的线程安全,并不能保证整个对象是线程安全的。如下列所示:@property(atomic,strong)NSMutableArray *arr; 如果一个线程循环的读数据,一个线程循环写数据,那么肯定会产生内存问题,因为这和settergetter 没有关系,如使用 [ self.arrobjectAtIndex:index ] 就不是线程安全的. 好的解决方案就是加锁


参考链接:Objective-C 之 atomic 一定是线程安全的吗?


5.描述一个你遇到过的 retain cycle 例子####

typedef void ( ^blk_t )( void );
@interface MyObject : NSObject
{
     blk_t blk;
     id  obj_;
}
@implementation MyObject
- ( id ) init
{
     self = [ super init ];
     blk_ = ^ { NSLog ( @" obj_ = %@ " ,** obj_ **); }
     return self;
}

虽然 Block 内没有使用self也同样截获了 self,引起了循环引用,即Block语法内使用的obj_实际上截获了self,对编译器来说,obj_只不过是对象用结构体的成员变量.相当于blk_ = ^ { NSLog( @" obj_ = %@ " , self -> obj_ ); }
为了避免循环引用,声明附有 __weak 修饰符的变量并赋值 obj_使用,也可安全的使用__unsafe__unretained修饰符.

- ( id ) init
{
     self = [ super init ];
     id __weak obj = obj_;
     blk_ = ^ { NSLog ( @" obj_ = %@ " , **obj** ); }
     return self;
}

6. +(void)load; 和 +(void)initialize; 有什么用处 ?####

**Objective-C 中绝大多数类都继承自 NSObject,该类有两个方法,+load 和 +initialize,用来实现类的初始化操作. **

** +load() : **该方法是在类或者分类被添加到 Objective-C runtime时被调用的,而且只调用一次. 当包含类或分类的程序库载入系统时,就会执行此方法,这通常就是指应用程序启动的时候,load方法务必实现的精简一些,也就是要减少其所执行的操作,因为整个应用程序在执行 load方法时都会阻塞.
注意:

+initialize()该方法是在类或它的子类收到第一条消息之前被调用的,这里的消息包括实例方法和类方法的调用,也就是说 +initialize方法是以懒加载的方式被调用的,如果程序一直没有给某个类或它的子类发送消息,那么这个类的 initialize 方法永远不会被调用,这样可以节省系统资源,避免浪费.
注意:


参考链接:
Objective-C +load vs +initialize - 雷纯锋的技术博客
Objective C类方法load和initialize的区别 - Ider - 博客园

参考书籍:Effective Objective-C 2.0 编写高质量的 iOS 与 OS X 代码的52个有效方法 (第51条)


7.为什么其他语言里叫函数调用,OC 中则是给对象发送消息(或者谈下对 runtime 的理解).

首先,OC是一门动态语言,可以在运行的时候动态决定调用哪个方法实现,甚至添加、替换方法的具体实现,而这些都归功于Objective-C的运行时runtime系统.

消息与函数之间的关键区别在于:使用消息结构的语言,其运行时所应执行的代码有运行环境来决定,使用函数调用的语言,则有编译器决定;

[receiver message] //向 receiver 对象发送 message 消息
会被编译器转化为:objc_msgSend (receiver, selector);如果消息含有参数,转化为objc_msgSend ( receiver, selector,arg1,arg2,... );如果消息的接收者能够找到对应的selector,那么就相当于直接执行了接收者这个对象的特定方法;否则,消息要么被转发,或是临时向接收者动态添加这个selector对应的实现内容,要么就干脆玩完崩溃掉.
可以看出[receiver message]真的不是一个简简单单的方法调用,因为这只是在编译阶段确定了要向接收者发送message这条消息,而receive将要如何响应这条消息,那就要看运行时发生的情况来决定了.

Objc Runtime使得 C 具有了面向对象能力,在程序运行时创建,检查,修改类、对象和它们的方法,可以使用runtime的一系列方法实现; 顺便附上 OC 中一个类的数据结构 /usr/include/objc/runtime.h

struct objc_class {
    //isa指针指向Meta Class,因为Objc的类的本身也是一个Object,为了处理这个关系,runtime就创造了Meta Class,当给类发送[NSObject alloc]这样消息时,实际上是把这个消息发给了Class Object
    Class isa  OBJC_ISA_AVAILABILITY;

    #if !__OBJC2__
    // 父类
    Class super_class                                        OBJC2_UNAVAILABLE;
    // 类名
    const char *name                                         OBJC2_UNAVAILABLE;
    //类的版本号,默认为0
    long version                                             OBJC2_UNAVAILABLE;
    // 类信息,供运行期使用的一些位标识 
    long info                                                OBJC2_UNAVAILABLE;
    // 该类的实例变量大小
    long instance_size                                       OBJC2_UNAVAILABLE;
    // 该类的成员变量链表
    struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;
    // 方法定义的链表
    struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;
    // 方法缓存,对象接到一个消息会根据isa指针查找消息对象,这时会在methodLists中遍历,如果cache了,常用的方法调用时就能够提高调用的效率。
    struct objc_cache *cache                                 OBJC2_UNAVAILABLE;
    // 协议链表 
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;

    #endif

} OBJC2_UNAVAILABLE;

object发送消息时,Runtime库会根据objectisa指针找到这个实例object所属于的类,然后在类的方法列表以及父类方法列表寻找对应的方法运行,id是一个objc_object结构类型的指针,这个类型的对象能够转换成任何一种对象.
然后再来看看消息发送的函数:objc_msgSend函数,看起来objc_msgSend像是返回了数据,其实objc_msgSend从不返回数据而是你的方法被调用后返回了数据.下面详细叙述下消息发送步骤:

参考文章:
刨根问底Objective-C Runtime


8.什么是 Method Swizzling ?####

Method Sswizzling指的是改变一个已存在的选择器对应的实现过程,它依赖于Objective-C中方法的调用能够在运行时改变,在运行时通过修改类的分发表dispatch tableselector对应的函数,来修改方法的实现.

理解这些概念之间关系最好的方式是:一个类Class维护一张调度表dispatch table,用于解析运行时发送的消息;调度表中的每个实体entry都是一个方法Method,其中key值是一个唯一的名字——选择器SEL,它对应到一个实现IMP——实际上就是指向标准C函数的指针(注:个人觉得一个Method包含了方法名SEL和方法实现IMP,不知道这样理解对不对

#import <objc/runtime.h> 
 
@implementation UIViewController (Tracking) 
 
+ (void)load { 
    static dispatch_once_t onceToken; 
    dispatch_once(&onceToken, ^{ 
        Class class = [self class]; 
 
        // When swizzling a class method, use the following: 
        // Class class = object_getClass((id)self); 
 
        SEL originalSelector = @selector(viewWillAppear:); 
        SEL swizzledSelector = @selector(xxx_viewWillAppear:); 
 
        Method originalMethod = class_getInstanceMethod(class, originalSelector); 
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector); 
 
        BOOL didAddMethod = 
            class_addMethod(class, 
                originalSelector, 
                method_getImplementation(swizzledMethod), 
                method_getTypeEncoding(swizzledMethod)); 
 
        if (didAddMethod) { 
            class_replaceMethod(class, 
                swizzledSelector, 
                method_getImplementation(originalMethod), 
                method_getTypeEncoding(originalMethod)); 
        } else { 
            method_exchangeImplementations(originalMethod, swizzledMethod); 
        } 
    }); 
} 
 
#pragma mark - Method Swizzling 
 
- (void)xxx_viewWillAppear:(BOOL)animated { 
    [self xxx_viewWillAppear:animated]; 
    NSLog(@"viewWillAppear: %@", self); 
} 
//这段代码看起来像是会导致一个死循环,但其实并没有,在Swizzling的过程中,xxx_viewWillAppear:会被重新分配给UIViewController的-viewWillAppear:的原始实现
//反而,如果我们在这个方法中调用viewWillAppear:才会真的导致死循环,因为这个方法的实现会在运行时被swizzle到viewWillAppear:的选择器
@end 

现在,当UIViewController或它子类的任何实例触发viewWillAppear:方法都会打印一条log日志;Method Swizzling就是改变类的调度表让消息解析时从一个选择器对应到另外一个的实现,同时将原始的方法实现混淆到一个新的选择器;

参考文章:
Objective-C Method Swizzling 的最佳实践
Objective-C的hook方案(一): Method Swizzling
Method Swizzling


9. UIView 和 CALayer 是什么关系 ?####

首先应该明确:

10.如何高性能的给 UIImageView 加个圆角 ?(不准说 layer.cornerRadius ).####

圆角是一种很常见的视图效果,相比于直角它更加柔和优美,易于接受;当然,设置圆角会带来一定的性能损耗,如何提高性能是另一个需要重点讨论的话题.

UIImageView 的圆角通过直接截取图片实现,其它视图的圆角可以通过 Core Graphics 画出圆角矩形实现,在下文会讲到;

Off-Screen Rending
离屏渲染,指的是图形处理器GPU(Graphic Processing Unit)在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作,视图和圆角的大小对帧率并没有什么影响,设置圆角的数量才是影响帧率的原因;离屏渲染耗时是发生在离屏这个动作上面,而不是渲染;为什么离屏这个操作耗时?原因主要有缓冲区和上下文切换,创建新的缓冲区代价都不算大,付出最大代价的是上下文切换,不管实在GPU渲染过程中,还是进程切换,上下文切换都是一个相当耗时的操作;首先我要保存当前屏幕渲染环境,然后切换到一个新的绘制环境,申请绘制资源,初始化环境,然后开始一个绘制,绘制完毕后销毁这个绘制环境,如需要切换到On-Screen Rendering或者再开始一个新的离屏渲染,重复之前的操作;

self.layer.shouldRasterize = YES;
self.layer.rasterizationScale = [UIScreen mainScreen].scale;

这样大部分情况下可以马上挽救你的帧数在55帧每秒以上,shouldRasterize = YES会使视图渲染内容被缓存起来,下次绘制的时候可以直接显示缓存,当然要在视图内容不改变的情况下;参考文章iOS图片圆角设置的正确姿势

}
```

 具体使用:
 ```

// 获得的就是一个圆形的图片
UIImage *placeHolder = [[UIImage imageNamed:@"defaultUserIcon"] circleImage];
```

参考文章:


圆角卡顿刨根问底
iOS 高效添加圆角效果实战讲解
iOS view圆角化的四种方法
开源高性能处理圆角
ZYCornerRadius 一句代码,圆角风雨无阻


11.使用 drawRect 有什么影响 ?(可深可浅,至少得用过)####

drawRect方法依赖Core Graphics框架来进行自定义的绘制,但这种方法主要的缺点就是它处理touch事件的方式:每次按钮被点击后,都会用setNeddsDisplay进行强制重绘;而且不止一次,每次单点事件触发两次执行,这样的话从性能的角度来说,对CPU和内存来说都是欠佳的;特别是如果在我们的界面上有多个这样的UIButton实例.


参考文章:
内存恶鬼drawRect
iOS重绘机制drawRect
UIView的layoutSubviews和drawRect

12.ASIHttpRequest 或者 SDWebImage 里面给 UIImageView 加载图片的逻辑是什么样的 ?(把 UIImageView 放到 UITableViewCell 里面问更赞)####

13.麻烦你设计一个简单的图片内存缓存器( 移除策略是一定要说的 ).####

14.讲讲你用 Instrument 优化动画性能的经历吧(别问我什么是 Instrument )####

15. loadView 是干嘛用的 ?####

当你访问一个ViewControllerview属性时,如果此时view的值是nil,那么,ViewController就会自动调用loadView这个方法;这个方法就会加载或者创建一个view对象,赋值给view属性;

loadView默认做的事情是:如果此ViewController存在一个对应的nib文件,那么就加载这个nib;否则,就创建一个UIView对象;

如果你用Interface Builder来创建界面,那么不应该重载这个方法;

如果你想自己创建view对象,那么可以重载这个方法;此时你需要自己给view属性赋值;你自定义的方法不应该调用super;如果你需要对view做一些其他的定制操作,在viewDidLoad里面去做;

16. viewWillLayoutSubView 讲一讲####

  • frame改变之前会调用viewWillLayoutSubviews,如横竖屏切换的时候,系统会响应一些函数,其中 包括viewWillLayoutSubviewsviewDidLayoutSubviews;

17. GCD 里边有哪几种 Queue ? 你自己建立过串行 Queue 吗?背后的线程模型是什么样的 ?####

18.用过 Coredata 或者 sqlite 吗 ?读写是分线程的吗 ?遇到过死锁吗 ?咋解决的 ?####

19. Http 的 post 和 get 的区别 ?(挺多的)####

20. 什么是 BST ?算法的复杂度是多少 ?####

上一篇 下一篇

猜你喜欢

热点阅读