iOS面试题整理分析

引言

之所以要整理这份面试题,也是希望能对更多的知识进行巩固,希望大家通过看面试题来加深自己对于iOS知识的理解。

面试题:

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

首先解释ARC: automatic reference counting - 自动引用计数

ARC几个要点:

  • 在对象被创建时 retain count +1,在对象被release时 retain count -1.当retain count 为0 时,销毁对象。
  • Xcode编译器会在程序中,合适的位置,自动加上autorelease方法,如果该对象引用计数为0,则销毁。

那么ARC是为了解决什么问题诞生的呢?这个得追溯到MRC手动内存管理时代说起。

MRC下内存管理的缺点:

  • 当我们要释放一个堆内存时,首先要确定指向这个堆空间的指针都被release了。(避免提前释放)
  • 释放指针指向的堆空间,首先要确定哪些指针指向同一个堆,这些指针只能释放一次。(MRC下即谁创建,谁释放,避免重复释放)
  • 模块化操作时,对象可能被多个模块创建和使用,不能确定最后由谁去释放。
  • 多线程操作时,不确定哪个线程最后使用完毕

为了解决上面MRC这些问题,所以引进的ARC。

2. +(void)load; +(void)initialize;有什么用处?

答案:

在Objective-C中,runtime会自动调用每个类的两个方法。

  • +load会在类初始加载时调用
  • initialize会在第一次调用类的类方法或实例方法之前被调用
  • 这两个方法是可选的,且只有在实现了它们时才会被调用
  • 共同点:两个方法都只会被调用一次。

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

答案:
先来看看怎么理解发送消息的含义:
​ 很多人在使用Objective-C 时把[receiver message]这样的形式,当成简单的方法调用,也就是receiver 调用了message方法,但是,实际上[receiver message]会被编译器转化为:

1
objc_msgSend(receiver, selector)

如果消息含有参数,则为:

1
objc_msgSend(receiver, selector, arg1, arg2, ...)

如果消息的接收者能够找到对应的selector,那么就相当于直接执行了接收者这个对象的特定方法;
否则,消息要么被转发
或是临时向接收者动态添加这个selector对应的实现内容,
要么就干脆玩完崩溃掉。

​ 现在可以看出[receiver message]真的不是一个简简单单的方法调用。因为这只是在编译阶段确定了要向接收者发送message这条消息,而receive将要如何响应这条消息,那就要看运行时发生的情况来决定了。

总结:Objc Runtime使得C具有了面向对象能力,在程序运行时创建,检查,修改类、对象和它们的方法。可以使用runtime的一系列方法实现。

4. CALayer 和 UIView的区别和联系

答案:

  1. 首先UIView可以响应事件,Layer不可以.

    UIKit使用UIResponder作为响应对象,来响应系统传递过来的事件并进行处理。

    UIApplication、UIViewController、UIView、和所有从UIView派生出来的UIKit类(包括UIWindow)都直接或间接地继承自UIResponder类。

    在 UIResponder中定义了处理各种事件和事件传递的接口, 而 CALayer直接继承 NSObject,并没有相应的处理事件的接口。

  2. View和CALayer的Frame映射及View如何创建CALayer.
    一个 Layer 的 frame 是由它的 anchorPoint,position,bounds,和 transform 共同决定的,而一个 View 的 frame 只是简单的返回 Layer的 frame,同样 View 的 center和 bounds 也是返回 Layer 的一些属性。

    在 [view initWithFrame] 的时候调用私有方法【UIView _createLayerWithFrame】去创建 CALayer。
    然后我在创建 View 的时候,在 Layer 和 View 中Frame 相关的所有方法中都加上断点,可以看到大致如下的调用顺序如下

    1
    2
    3
    4
    5
    6
    [UIView _createLayerWithFrame]
    [Layer setBounds:bounds]
    [UIView setFrame:Frame]
    [Layer setFrame:frame]
    [Layer setPosition:position]
    [Layer setBounds:bounds]

    我发现在创建的过程只有调用了 Layer 的设置尺寸和位置的然而并没有调用View 的 SetCenter 和 SetBounds 方法。

    然后我发现当我修改了 view的 bounds.size 或者 bounds.origin 的时候也只会调用上边 Layer的一些方法。

    所以我认为,View 的 Center 和 Bounds 只是直接返回layer 对应的 Position 和 Bounds.

  3. UIView主要是对显示内容的管理而 CALayer 主要侧重显示内容的绘制。

  1. 在做 iOS 动画的时候,修改非 RootLayer的属性(譬如位置、背景色等)会默认产生隐式动画,而修改UIView则不会。

    对于每一个 UIView 都有一个 layer,把这个 layer 且称作RootLayer,而不是 View 的根 Layer的叫做 非 RootLayer。

    我们对UIView的属性修改时时不会产生默认动画,而对单独 layer属性直接修改会,这个默认动画的时间缺省值是0.25s.

    在 Core Animation 编程指南的 “How to Animate Layer-Backed Views” 中,对为什么会这样做出了一个解释:

    1
    UIView 默认情况下禁止了 layer 动画,但是在 animation block 中又重新启用了它们

    是因为任何可动画的 layer 属性改变时,layer 都会寻找并运行合适的 ‘action’ 来实行这个改变。在 Core Animation 的专业术语中就把这样的动画统称为动作 (action,或者 CAAction)。

    layer 通过向它的 delegate 发送 actionForLayer:forKey: 消息来询问提供一个对应属性变化的 action。

    delegate 可以通过返回以下三者之一来进行响应:

    • 它可以返回一个动作对象,这种情况下 layer 将使用这个动作。
    • 它可以返回一个 nil, 这样 layer 就会到其他地方继续寻找。
    • 它可以返回一个 NSNull 对象,告诉 layer 这里不需要执行一个动作,搜索也会就此停止。

    当 layer 在背后支持一个 view 的时候,view 就是它的 delegate;


上面这些属于研究型的答案,有一部分分析内容,如果面试的时候,可以参考下面的答案来回答:

  1. UIView是iOS系统中界面元素的基础,所有的界面元素都是继承自它。它本身完全是由CoreAnimation来实现的。

    它真正的绘图部分,是由一个CALayer类来管理。

    UIView本身更像是一个CALayer的管理器,访问它的跟绘图和跟坐标有关的属性,例如frame,bounds等,实际上内部都是在访问它所包含的CALayer的相关属性。

  2. UIView有个重要属性layer,可以返回它的主CALayer实例。

    1
    2
    // 要访问层,读取UIView实例的layer属性
    CALayer *layer = myView.layer

    所有从UIView继承来的对象都继承了这个属性。

    这意味着你可以转换、缩放、旋转,甚至可以在Navigation bars,Tables,Text boxes等其它的View类上增加动画。

    每个UIView都有一个层,控制着各自的内容最终被显示在屏幕上的方式。

    UIView的layerClass方法,可以返回主layer所使用的类,UIView的子类可以通过重载这个方法,来让UIView使用不同的CALayer来显示。代码示例:

    1
    2
    3
    - (class)layerClass {
    return ([CAEAGLLayer class]);
    }

    上述代码使得某个UIView的子类使用GL来进行绘制。

  3. UIView的CALayer类似UIView的子View树形结构,也可以向它的layer上添加子layer,来完成某些特殊的表示。

    即CALayer层是可以嵌套的。示例代码:

    1
    2
    3
    grayCover = [[CALayer alloc] init];
    grayCover.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.2] CGColor];
    [self.layer addSubLayer:grayCover];

    上述代码会在目标View上敷上一层黑色透明薄膜的效果。

  4. UIView的layer树形在系统内部,被维护着三份copy。

    • 分别是逻辑树,这里是代码可以操纵的;
    • 动画树,是一个中间层,系统就在这一层上更改属性,进行各种渲染操作;
    • 显示树,其内容就是当前正被显示在屏幕上得内容。
  5. 动画的运作:对UIView的subLayer(非主Layer)属性进行更改,系统将自动进行动画生成,动画持续时间的缺省值是0.25秒。

  6. 坐标系统:CALayer的坐标系统比UIView多了一个anchorPoint属性,使用CGPoint结构表示,值域是0~1,是个比例值。

    这个点是各种图形变换的坐标原点,同时会更改layer的position的位置,它的缺省值是{0.5,0.5},即在layer的中央。

    layer可以设置圆角显示(cornerRadius),也可以设置阴影(shadowColor)。
    但是如果layer树中某个layer设置了圆角,树种所有layer的阴影效果都将不显示了。

    因此若是要有圆角又要阴影,变通方法只能做两个重叠的UIView,一个的layer显示圆角,一个layer显示阴影……

  7. 渲染:当更新层,改变不能立即显示在屏幕上。当所有的层都准备好时,可以调用setNeedsDisplay方法来重绘显示。

    1
    [gameLayer setNeedsDisplay];

    若要重绘部分屏幕区域,请使用setNeedsDisplayInRect:方法,通过在CGRect结构的区域更新:

    1
    [gameLayer setNeedsDisplayInRect:CGRectMake(150.0,100.0,50.0,75.0)];

    如果是用的Core Graphics框架来执行渲染的话,可以直接渲染Core Graphics的内容。用renderInContext:来做这个事。

    1
    [gameLayer renderInContext:UIGraphicsGetCurrentContext()];
  8. 变换:要在一个层中添加一个3D或仿射变换,可以分别设置层的transform或affineTransform属性。

    1
    2
    3
    4
    5
    characterView.layer.transform = CATransform3DMakeScale(-1.0,-1.0,1.0);
    CGAffineTransform transform = CGAffineTransformMakeRotation(45.0);
    backgroundView.layer.affineTransform = transform;

5. 如何高性能的给UIImageView加个圆角?

答案:
​ 一般情况下给 UIImageView 或者说 UIKit 的控件添加圆角都是改变 clipsToBounds 和 layer.cornerRadius,
​ 这样大约两行代码就可以解决这个问题. 但是, 这样使用这样的方法会强制 Core Animation 提前渲染屏幕的离屏绘制, 而离屏绘制就会为性能带来负面影响.

​ 从APP性能优化角度考虑,我们也可以使用另一种比较复杂的方式来为图片添加圆角, 就是通过画布的方式,进行裁剪,

使用Quartz2D直接绘制图片

步骤:
  a、创建目标大小(cropWidth,cropHeight)的画布。

  b、使用UIImage的drawInRect方法进行绘制的时候,指定rect为(-x,-y,width,height)。

  c、从画布中得到裁剪后的图像。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (UIImage*)cropImageWithRect:(CGRect)cropRect
{
CGRect drawRect = CGRectMake(-cropRect.origin.x , -cropRect.origin.y, self.size.width * self.scale, self.size.height * self.scale);
UIGraphicsBeginImageContext(cropRect.size);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextClearRect(context, CGRectMake(0, 0, cropRect.size.width, cropRect.size.height));
[self drawInRect:drawRect];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
@end

这里再介绍一种用贝塞尔曲线的方式.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
imageView.center = CGPointMake(200, 300);
UIImage *anotherImage = [UIImage imageNamed:@"image"];
UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, NO, 1.0);
[[UIBezierPath bezierPathWithRoundedRect:imageView.bounds cornerRadius:50] addClip];
[anotherImage drawInRect:imageView.bounds];
imageView.image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
[self.view addSubview:imageView];

这两种都方式都可以达到我们的需求。

6.layoutSubviews 和 drawRect的调用

答案:

layoutSubviews在以下情况下会被调用

  1. init初始化不会触发layoutSubviews。
  2. addSubview会触发layoutSubviews。
  3. 设置view的Frame会触发layoutSubviews,当然前提是frame的值设置前后发生了变化。
  4. 滚动一个UIScrollView会触发layoutSubviews。
  5. 旋转Screen会触发父UIView上的layoutSubviews事件。
  6. 改变一个UIView大小的时候也会触发父UIView上的layoutSubviews事件。
  7. 直接调用setLayoutSubviews。

    drawRect在以下情况下会被调用:

  8. 如果在UIView初始化时没有设置rect大小,将直接导致drawRect不被自动调用。drawRect 掉用是在Controller->loadView,?Controller->viewDidLoad?两方法之后掉用的.所以不用担心在 控制器中,这些View的drawRect就开始画了.这样可以在控制器中设置一些值给View(如果这些View?draw的时候需要用到某些变量值).

  9. 该方法在调用sizeToFit后被调用,所以可以先调用sizeToFit计算出size。然后系统自动调用drawRect:方法。
  10. 通过设置contentMode属性值为UIViewContentModeRedraw。那么将在每次设置或更改frame的时候自动调用drawRect:。
  11. 直接调用setNeedsDisplay,或者setNeedsDisplayInRect:触发drawRect:,但是有个前提条件是rect不能为0。

以上1,2推荐;而3,4不提倡

drawRect方法使用注意点:

  1. 若使用UIView绘图,只能在drawRect:方法中获取相应的contextRef并绘图。如果在其他方法中获取将获取到一个invalidate 的ref并且不能用于画图。drawRect:方法不能手动显示调用,必须通过调用setNeedsDisplay?或 者?setNeedsDisplayInRect,让系统自动调该方法。
  2. 若使用CALayer绘图,只能在drawInContext:?中(类似于drawRect)绘制,或者在delegate中的相应方法绘制。同样也是调用setNeedDisplay等间接调用以上方法
  3. 若要实时画图,不能使用gestureRecognizer,只能使用touchbegan等方法来调用setNeedsDisplay实时刷新屏幕

7. post和get的区别?

答案:

  1. get是从服务器上获取数据,post是向服务器传送数据。
  2. get是把参数数据队列加到提交表单的ACTION属性所指的URL中,值和表单内各个字段一一对应,在URL中可以看到。post是通过HTTP post机制,将表单内各个字段与其内容放置在HTML HEADER内一起传送到ACTION属性所指的URL地址。用户看不到这个过程。
  3. 对于get方式,服务器端用Request.QueryString获取变量的值,对于post方式,服务器端用Request.Form获取提交的数据。
  4. get传送的数据量较小,不能大于2KB。post传送的数据量较大,一般被默认为不受限制。但理论上,IIS4中最大量为80KB,IIS5中为100KB。
  5. get安全性非常低,post安全性较高。但是执行效率却比Post方法好。

本作品采用 署名-非商业性使用-相同方式共享 2.5 中国大陆 (CC BY-NC-SA 2.5)协议 进行许可,欢迎转载,但转载请注明来自SarielTang,并保持转载后文章内容的完整。本人保留所有版权相关权利。

本文永久链接:http://sarieltang.github.io/2016/10/26/面试题总结/面试题1/index/