iOS DeveloperiOS精品文章-面试iOS

iOS面试题( 一 )

2017-07-21  本文已影响77人  胡小夜大叔

准备刷一波面试题,来巩固一下自己的基础知识,暂时题源来自这里,刷完这波以后遇到新的会再更。

第一波,这的 https://github.com/lzyy/iOS-Developer-Interview-Questions


1.什么是响应链,它是怎么工作的?

写了一篇来解释这个,传送门


2.如何访问并修改一个类的私有属性?

例子:
有这样一个类 TestClassA:

@interface TestClassA ()

@property (nonatomic, assign) NSInteger priviteNum;
@property (nonatomic, strong) UIView *pView;

@end

@implementation TestClassA

-(void)showProperty
{
    NSLog(@"priviteNum = %@", @(_priviteNum));
    NSLog(@"pView = %@", _pView);
}

@end

我们在外部改完后,调用 showProperty 方法去查看这两个私有属性的值。

1.KVC:

TestClassA *classA = [[TestClassA alloc] init];
[classA setValue:@(4) forKey:@"_priviteNum"];
[classA setValue:self.view forKey:@"_pView"];
[classA showProperty];

控制台打印:

priviteNum = 4
pView = <UIView: 0x7fd575c06ef0; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x60800002a020>>

2.通过runtime动态改变

TestClassA *classA = [[TestClassA alloc] init];

unsigned int outCount = 0;
Ivar *ivars = class_copyIvarList([TestClassA class], &outCount);

for (int i = 0; i < outCount; i ++) {
    Ivar ivar = ivars[i];
    
    const char *ivarName = ivar_getName(ivar);
    
    //这里要注意ARC下, 这个会报错
    if (strcmp(ivarName, "_priviteNum") == 0) {
        object_setIvar(classA, ivar, 22);
    }
    
    if (strcmp(ivarName, "_pView") == 0) {
        object_setIvar(classA, ivar, self.view);
    }
}

[classA showProperty];

控制台打印:

priviteNum = 22
pView = <UIView: 0x7ff5cc509640; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x60000002a0e0>>

这里要注意下:
在修改NSInteger型变量的时候,ARC下,编译器不允许你将NSInteger类型的值赋值给id,在buildsetting中将Objective-C Automatic Reference Counting修改为No即可。但是这样工程就会变成MRC,所以,如果是非对象类型就不建议用object_setIvar这样的方法去修改了。

3.msg_send() 去修改 (适用私有属性,不适用私有变量)
既然是私有属性了,必然有setter方法, 那我们动态调用一下。

TestClassA *classA = [[TestClassA alloc] init];
((void (*)(id, SEL, int))(void *) objc_msgSend)((id)classA, @selector(setPriviteNum:) , 33);
((void (*)(id, SEL, id))(void *) objc_msgSend)((id)classA, @selector(setPView:) , self.view);
[classA showProperty];

控制台打印:

priviteNum = 33
pView = <UIView: 0x7ffc9ec09500; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x60800002ed80>>

3.iOS Extension 是什么?能列举几个常用的 Extension 么?

类扩展,其实Extension我们经常用,但是我们可能并不知道这种用法还有Extension这样一个名字。

那我们具体看一下:
Extension和Category一样有独立的文件,当我们新建Objective-C File 的时候,会发现下面这样:


54AEFA1C-AF43-444B-94D3-10C62AA79E09.png

我们大部分时候都在建立Category文件或Protocol文件,这次我们建个Extension文件,于是我们得到一个这样命名的文件:

Paste_Image.png

并且文件内部是这样的:

Paste_Image.png

是不是很熟悉,我们经常会在很多.m文件中,给很多类做一个这样的东西,然后在里面声明一些变量或属性,作为私有的看待,而这种写法就是类的Extension.


4.如何把一个包含自定义对象的数组序列化到磁盘?

先给自定义的对象实现 NSCoding 协议:

@interface CodingClass()
@property (nonatomic, assign) NSInteger objectID;
@end

@implementation CodingClass

-(instancetype)initWithCoder:(NSCoder *)aDecoder{
    self = [super init];
    if (self) {
        self.objectID = [[aDecoder decodeObjectForKey:@"objectID"] integerValue];
    }
    return self;
}

-(void)encodeWithCoder:(NSCoder *)aCoder{
    [aCoder encodeObject:@(self.objectID) forKey:@"objectID"];
}
@end

然后就可以在外部给这个自定义类归档了:

CodingClass *codingA = [[CodingClass alloc] init];
CodingClass *codingB = [[CodingClass alloc] init];

NSArray *array = [NSArray arrayWithObjects:codingA, codingB, nil];

[NSKeyedArchiver archiveRootObject:array toFile:[NSString stringWithFormat:@"%@/%@",NSHomeDirectory(), @"archiver.dat"]];

5.iOS 的沙盒目录结构是怎样的? App Bundle 里面都有什么?

6.iOS 的签名机制大概是怎样的?

确实不是太了解,看这个学习了:
关于iOS签名机制的理解


7.iOS 7的多任务添加了哪两个新的 API? 各自的使用场景是什么?

后台获取(Background Fetch):后台获取使用场景是用户打开应用之前就使app有机会执行代码来获取数据,刷新UI。这样在用户打开应用的时候,最新的内容将已然呈现在用户眼前,而省去了所有的加载过程。

推送唤醒(Remote Notifications):使用场景是使设备在接收到远端推送后让系统唤醒设备和我们的后台应用,并先执行一段代码来准备数据和UI,然后再提示用户有推送。这时用户如果解锁设备进入应用后将不会再有任何加载过程,新的内容将直接得到呈现。


8.Objective-C 的 class 是如何实现的?

OC中的Class其实是一个 objc_class 结构体指针:

/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

我们看一下 objc_class 结构体:

struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class super_class                                        OBJC2_UNAVAILABLE;
    const char *name                                         OBJC2_UNAVAILABLE;
    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;
    struct objc_cache *cache                                 OBJC2_UNAVAILABLE;
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */

每个Class其实也是对象,看下几个重要的属性:


9.Selector 是如何被转化为 C 语言的函数调用的?

首先看下SEL的含义:

typedef struct objc_selector *SEL;

是一个 objc_selector结构体指针,但是objc_selector结构体具体是什么,我们就查不到了,但是我们试着打印一下:

-(void)viewATap:(UITapGestureRecognizer *)tap
{
    NSLog(@"viewA tap~");
}

NSLog(@"sel = %s", @selector(viewATap:));

却发现是可以打印出来的:

sel = viewATap:

所以,SEL 表示的就是那个方法的名字。

然后我们再研究一下 seleter 在方法转发中起的作用
先看一下类中method的结构:

struct objc_method {
   SEL method_name                                          OBJC2_UNAVAILABLE;
   char *method_types                                       OBJC2_UNAVAILABLE;
   IMP method_imp                                           OBJC2_UNAVAILABLE;
} 

SEL method_name :方法名/方法标识
char *method_types :方法返回类型
IMP method_imp :方法具体的实现地址

然后方法调用简单过程,举个例子:

TestClassA *classA = [[TestClassA alloc] init];
[classA showProperty];

编译器会将方法调用转换为发消息的模式,由 objc_msgSend进行消息发送,转换为:

objc_msgSend(classA, @selector(showProperty))

收到消息后,classA 会从 methodLists 中找到 SEL 为 showProperty的 objc_method,然后找到对应的 IMP ,并执行。


10.Objective-C 如何对已有的方法,添加自己的功能代码以实现类似记录日志这样的功能?

用 method swizzling 。
譬如在viewDidLoad加入记录日志功能

+(void)load
{
    [super load];
    Method fromMethod = class_getInstanceMethod([self class], @selector(viewDidLoad));
    Method toMethod = class_getInstanceMethod([self class], @selector(swizzlingViewDidLoad));

    method_exchangeImplementations(fromMethod, toMethod);
}

- (void)swizzlingViewDidLoad {
    [self swizzlingViewDidLoad];
    //自己的记录日志代码
    [self saveLog];
}

11.+load 和 +initialize 的区别是什么?

12.如何让 @protocol 和 Category 支持属性?

我们知道在类里定义一个属性时,会在该类的ivar_list添加ivar描述,会在method_list添加该属性的seter和geter方法描述,在property_list中添加一个属性描述。

但在@protocol 和 category 中如果定义一个 @property 时,只会在property_list中添加一个属性描述,method_list 和 ivar_list中都不会有相关的描述添加。

所以要想@protocol 和 category 中的 @property 有意义,就必须在类中或category中,实现这个@property的setter和getter。

但是 ivar_list 是没有该属性对应的 ivar, 所以 setter 中你是没法赋值的, 这时候就要用到 objc_setAssociatedObject 了,在 setter 中用 objc_setAssociatedObject 赋值,并在 getter 中用 objc_getAssociatedObject 取值,这样就变相完成了@protocol 和 category 中属性的设置


13.NSOperation 相比于 GCD 有哪些优势?

简单说,NSOperation有着更好的封装性,具有面向对象编程的思想,在线程管理上有这GCD无法比拟的优势。
我们结合场景来说这两者:

我们一般在工程中使用GCD都是一些零散的地方,对线程只具有简单的操作,需要用线程去跑一些代码,GCD简洁,高效,易用,在这些场景有这很好的表现。

而一些具有定向功能的任务模块或组件,需要很好的管理的场景,则更多用NSOperation,譬如我们最常用的AFNetworking组件、SDWebImage里的下载器等,这些场景都需要对多线程任务状态有很好的管理,而NSOperation则能更好的完成这种场景的任务。

但大部分状态下,NSOperation封装的线程任务中,也会出现GCD的身影,混用,结合两者的优势。


14.strong / weak / unsafe_unretained 的区别?

15.如何为 Class 定义一个对外只读对内可读写的属性?

用 readonly 修饰, 这样的话走 setter 的赋值就都不可用了,所以外部就都是只读了,内部可以用 成员变量直接赋值, 如:

@property (nonatomic, strong, readonly) NSString *str1;

赋值时:

_str1 = @"str1";

打印可见,成功赋值。


16.Objective-C 中,meta-class 指的是什么?

meta-class :元类,说白了就是 类对象 的类, 就是 类对象 的 isa 指向

我们知道当我们向一个对象发消息的时候,会从这个对象的 Class对象 中去找到对应消息的实现方法并执行,但是我们也经常调用一个类的类方法,我们知道 Class 其实也是一个对象,而存放类方法的就是 该Class对象 的元类对象,所以就是:

一张老图,说明各种关系:

虚线走向是 isa 指针关系走向, 实线是 superclass 关系走向:

meta-class.png

可以看到 对象 的 isa 指向他的 类对象 , 类对象 的 isa 指向他的 元类对象, 元类对象 的 isa 指向 root class 的 元类对象 ,但是 root class 的 元类对象
isa 指向的是他自己, 所以,我们现在大部分 NSObject 为 root class 的体系下,可以认为所有 元类对象 的 isa 都指向 NSObject元类对象, 包括他自己。

在看下 元类对象 的父类关系, 各 元类对象父类, 均为其类对象父类元类对象, 最后到了 root class 的 元类对象, 而 root class 的 元类对象父类是 root class 的 类对象, 这就导致 所有 类对象元类对象基类 都是 root class。


17.UIView 和 CALayer 之间的关系?

17.+[UIView animateWithDuration:animations:completion:] 内部大概是如何实现的?

不知道...


18.什么时候会发生「隐式动画」?

首先知道下什么叫 "隐式动画" :

Core Animation基于一个假设,说屏幕上的任何东西都可以(或者可能)做动画。动画并不需要你在Core Animation中手动打开,相反需要明确地关闭,否则他会一直存在。

当你改变CALayer的一个可做动画的属性,它并不能立刻在屏幕上体现出来。相反,它是从先前的值平滑过渡到新的值。这一切都是默认的行为,你不需要做额外的操作。这其实就是所谓的隐式动画

之所以叫隐式是因为我们并没有指定任何动画的类型。我们仅仅改变了一个属性,然后Core Animation来决定如何并且何时去做动画。

举个例子:

@interface ViewController ()
@property (nonatomic, strong) CALayer *showLayer;
@end

- (void)viewDidLoad {

    self.showLayer = [CALayer layer];
    _showLayer.frame = CGRectMake(0.0f, 20.0f, 100.0f, 100.0f);
    _showLayer.backgroundColor = [UIColor redColor].CGColor;;

    [self.view.layer addSublayer:_showLayer];
}

然后加个button,点击事件改变其frame:

-(void)layerChange
{
    _showLayer.frame = CGRectMake(200.0f, 20.0f, 100.0f, 100.0f);
}

大概就是这样:

点击前.png
点击后,位移发生改变,但是我们会发现layer的位移变化并不是直接去了新的位置,而是 动画平移 过去的,这就是隐式动画的功劳,隐式动画的动画时间为0.25秒。 点击后.png

所以对layer操作是,发生了隐式动画。

但是还有一种情况,当我们直接修改 view.layer 的时候,隐式动画却被view给禁掉了,这是为什么呢?

我们知道Core Animation通常对CALayer的所有属性(可动画的属性)做动画,但是UIView把它关联的图层的这个特性关闭了。为了更好说明这一点,我们需要知道隐式动画是如何实现的。

我们把改变属性时CALayer自动应用的动画称作行为,当CALayer的属性被修改时候,它会调用-actionForKey:方法,传递属性的名称。剩下的操作都在CALayer的头文件中有详细的说明,实质上是如下几步:

所以一轮完整的搜索结束之后,-actionForKey:要么返回空(这种情况下将不会有动画发生),要么是CAAction协议对应的对象,最后CALayer拿这个结果去对先前和当前的值做动画。

于是这就解释了UIKit是如何禁用隐式动画的:每个UIView对它关联的图层都扮演了一个委托,并且提供了-actionForLayer:forKey的实现方法。当不在一个动画块的实现中,UIView对所有图层行为返回nil,但是在动画block范围之内,它就返回了一个非空值。

关于显示\隐式动画的东西看这个,很详细 :http://blog.csdn.net/catsmen/article/details/46546897


19:frame 和 bounds 的区别是什么?

参考坐标系不一样,frame的参考坐标系是其superview, bounds 的参考坐标系是其本身


20:如何把一张大图缩小为1/4大小的缩略图?

UIImageJPEGRepresentation(image, 0.25)


21:一个 App 会处于哪些状态?
  1. Not running:应用还没有启动,或者应用正在运行但是途中被系统停止。
  1. Inactive:当前应用正在前台运行,但是并不接收事件(当前或许正在执行其它代码)。一般每当应用要从一个状态切换到另一个不同的状态时,中途过渡会短暂停留在此状态。唯一在此状态停留时间比较长的情况是:当用户锁屏时,或者系统提示用户去响应某些(诸如电话来电、有未读短信等)事件的时候。
  1. Active:当前应用正在前台运行,并且接收事件。这是应用正在前台运行时所处的正常状态。
  1. Background:应用处在后台,并且还在执行代码。大多数将要进入Suspended状态的应用,会先短暂进入此状态。然而,对于请求需要额外的执行时间的应用,会在此状态保持更长一段时间。另外,如果一个应用要求启动时直接进入后台运行,这样的应用会直接从Not running状态进入Background状态,中途不会经过Inactive状态。比如没有界面的应用。注此处并不特指没有界面的应用,其实也可以是有界面的应用,只是如果要直接进入background状态的话,该应用界面不会被显示。
  1. Suspended:应用处在后台,并且已停止执行代码。系统自动的将应用移入此状态,且在此举之前不会对应用做任何通知。当处在此状态时,应用依然驻留内存但不执行任何程序代码。当系统发生低内存告警时,系统将会将处于Suspended状态的应用清除出内存以为正在前台运行的应用提供足够的内存。

22:Push Notification 是如何工作的?

23:什么是 Runloop?

iOS系统的一种运行机制,本身一个循环,实现合理对事件接受和分发,也是线程的基本架构部分。runloop保证了所在线程能在忙碌的时候合理调配任务,在不忙碌的时候休眠,以节省cpu资源。

之后写一篇详细介绍runloop的东西吧,东西其实还不少。


24:Toll-Free Bridging 是什么?什么情况下会使用?

有一些数据类型是可以在 Core Foundation Framework 和 Foundation Framework 之间互相转换的,意思就是这些数据类型只要做一个转换就可以在这两个框架使用,这就是 Toll-Free Bridging ,而转换的方法就是 用 __bridge 关键字。

什么情况下使用
其实就是当需要转换的时候用的,那么什么时候需要转换,举个例子:
我们一般情况下都是使用 Foundation Framework 的方法较多,但是当我们在写代码的过程中突然需要用 Core Foundation Framework 的函数,但是 Core Foundation Framework 函数需要的参数的类型 Foundation Framework 的类型肯定是不能直接用的,所以就需要用 __bridge 转一下,其实这种情况我们经常遇到。

再附一个 Toll-Free Bridging 的类型转换对应表
再附一个 Toll-Free Bridging 原理


25:当系统出现内存警告时会发生什么?

首先身为一个使用者,会发现机器很卡,然后甚至会闪退。

而作为系统,在内存警告时,会尽可能得释放没必要的内存,比如未显示的view。对于非界面的数据,需要具体分析,可以恢复的数据可以释放,不能恢复的数据就不要释放。


26:什么是 Protocol,Delegate 一般是怎么用的?

A protocol declares a programmatic interface that any class may choose to implement. Protocols make it possible for two classes distantly related by inheritance to communicate with each other to accomplish a certain goal. They thus offer an alternative to subclassing. Any class that can provide behavior useful to other classes may declare a programmatic interface for vending that behavior anonymously. Any other class may choose to adopt the protocol and implement one or more of its methods, thereby making use of the behavior. The class that declares a protocol is expected to call the methods in the protocol if they are implemented by the protocol adopter.

协议声明了一个任何类都可以去选择实现的程序接口,协议使两个没有继承关系的类可以互相沟通并实现特定目标。因此,协议其实是提供了另外一个替代子类化的方式。任何可以为其他类提供有用的行为的类,都可以声明一个编程接口去以匿名的方式提供这种有用行为。而其他的类则可以选择一个协议,并选择实现协议里的一个或多个接口来使用这种行为。如果遵守协议者实现了协议内的方法,那么声明协议者,将会调用遵守协议者实现的协议方法。

而Delegate一般会成为 “遵守协议者” 和 “声明协议者” 的 "桥梁", 因为 Delegate 一般会指向 “遵守协议者” ,这样才能在 “声明协议者”内部通过 Delegate 调用到协议方法。


26:autorelease 对象在什么情况下会被释放?

有两种情况:


27:UIWebView 有哪些性能问题?有没有可替代的方案。

没研究过,看看这个吧: 传送门


28:为什么 NotificationCenter 要 removeObserver? 如何实现自动 remove?

为什么 NotificationCenter 要 removeObserver?

想要实现自动remove,可以通过 method swizzling 在 dealloc 中统一加上删除observer 的操作。当然有些业务需求并不一定是要在dealloc中删除,根据业务作出灵活变动。


29:当 TableView 的 Cell 改变时,如何让这些改变以动画的形式呈现?
[tableView beginUpdates];
[tableView endUpdates];

关于beginUpdates/endUpdates 我们需要知道几点:


30:什么是 Method Swizzle,什么情况下会使用?

被称为iOS的黑科技,原理是利用iOS的运行时机制,进行方法的hook,以达到动态修改一些系统方法的目的。
简单举个例子,hook一个 viewController 的 viewDidload 方法:

+(void)load
{
    [super load];
    Method fromMethod = class_getInstanceMethod([self class], @selector(viewDidLoad));
    Method toMethod = class_getInstanceMethod([self class], @selector(swizzlingViewDidLoad));

    method_exchangeImplementations(fromMethod, toMethod);
}

- (void)swizzlingViewDidLoad {
     [super swizzlingViewDidLoad];
     NSLog(@"my swizzling viewDidLoad");
}

31:为什么 UIScrollView 的滚动会导致 NSTimer 失效?

主线程的 RunLoop 里有两个预置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。这两个 Mode 都已经被标记为"Common"属性。DefaultMode 是 App 平时所处的状态,TrackingRunLoopMode 是追踪 ScrollView 滑动时的状态。当你创建一个 Timer 并加到 DefaultMode 时,Timer 会得到重复回调,但此时滑动一个TableView时,RunLoop 会将 mode 切换为 TrackingRunLoopMode,这时 Timer 就不会被回调,并且也不会影响到滑动操作。

有时你需要一个 Timer,在两个 Mode 中都能得到回调,一种办法就是将这个 Timer 分别加入这两个 Mode。还有一种方式,就是将 Timer 加入到顶层的 RunLoop 的 "commonModeItems" 中。"commonModeItems" 被 RunLoop 自动更新到所有具有"Common"属性的 Mode 里去。

上一篇下一篇

猜你喜欢

热点阅读