IOS面试题

Posted 四叔

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了IOS面试题相关的知识,希望对你有一定的参考价值。

_objc_msgForward是 IMP 类型,用于消息转发的:当向一个对象发送一条消息,但它并没有实现的时候,_objc_msgForward会尝试做消息转发。

我们可以这样创建一个_objc_msgForward对象:

1
IMP msgForwardIMP = _objc_msgForward;

上篇中的《objc中向一个对象发送消息[obj foo]和objc_msgSend()函数之间有什么关系?》曾提到objc_msgSend在“消息传递”中的作用。在“消息传递”过程中,objc_msgSend的动作比较清晰:首先在 Class 中的缓存查找 IMP (没缓存则初始化缓存),如果没找到,则向父类的 Class 查找。如果一直查找到根类仍旧没有实现,则用_objc_msgForward函数指针代替 IMP 。最后,执行这个 IMP 。

Objective-C运行时是开源的,所以我们可以看到它的实现。打开 Apple Open Source 里Mac代码里的obj包 下载一个最新版本,找到 objc-runtime-new.mm,进入之后搜索_objc_msgForward。

技术分享

里面有对_objc_msgForward的功能解释:

技术分享

1
2
3
4
5
6
7
8
9
10
11
12
/***********************************************************************
* lookUpImpOrForward.
* The standard IMP lookup. 
* initialize==NO tries to avoid +initialize (but sometimes fails)
* cache==NO skips optimistic unlocked lookup (but uses cache elsewhere)
* Most callers should use initialize==YES and cache==YES.
* inst is an instance of cls or a subclass thereof, or nil if none is known. 
*   If cls is an un-initialized metaclass then a non-nil inst is faster.
* May return _objc_msgForward_impcache. IMPs destined for external use 
*   must be converted to _objc_msgForward or _objc_msgForward_stret.
*   If you don‘t want forwarding at all, use lookUpImpOrNil() instead.
**********************************************************************/

对 objc-runtime-new.mm文件里与_objc_msgForward有关的三个函数使用伪代码展示下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//  objc-runtime-new.mm 文件里与 _objc_msgForward 有关的三个函数使用伪代码展示
//  Copyright (c)  微博@ios程序犭袁(http://weibo.com/luohanchenyilong/). All rights reserved.
//  同时,这也是 obj_msgSend 的实现过程
 
id objc_msgSend(id self, SEL op, ...) {
    if (!self) return nil;
    IMP imp = class_getMethodImplementation(self->isa, SEL op);
    imp(self, op, ...); //调用这个函数,伪代码...
}
 
//查找IMP
IMP class_getMethodImplementation(Class cls, SEL sel) {
    if (!cls || !sel) return nil;
    IMP imp = lookUpImpOrNil(cls, sel);
    if (!imp) return _objc_msgForward; //_objc_msgForward 用于消息转发
    return imp;
}
 
IMP lookUpImpOrNil(Class cls, SEL sel) {
    if (!cls->initialize()) {
        _class_initialize(cls);
    }
 
    Class curClass = cls;
    IMP imp = nil;
    do //先查缓存,缓存没有时重建,仍旧没有则向父类查询
        if (!curClass) break;
        if (!curClass->cache) fill_cache(cls, curClass);
        imp = cache_getImp(curClass, sel);
        if (imp) break;
    while (curClass = curClass->superclass);
 
    return imp;
}

虽然Apple没有公开_objc_msgForward的实现源码,但是我们还是能得出结论:

_objc_msgForward是一个函数指针(和 IMP 的类型一样),是用于消息转发的:当向一个对象发送一条消息,但它并没有实现的时候,_objc_msgForward会尝试做消息转发。

上篇中的《objc中向一个对象发送消息[obj foo]和objc_msgSend()函数之间有什么关系?》曾提到objc_msgSend在“消息传递”中的作用。在“消息传递”过程中,objc_msgSend的动作比较清晰:首先在 Class 中的缓存查找 IMP (没缓存则初始化缓存),如果没找到,则向父类的 Class 查找。如果一直查找到根类仍旧没有实现,则用_objc_msgForward函数指针代替 IMP 。最后,执行这个 IMP 。

为了展示消息转发的具体动作,这里尝试向一个对象发送一条错误的消息,并查看一下_objc_msgForward是如何进行转发的。

首先开启调试模式、打印出所有运行时发送的消息: 可以在代码里执行下面的方法:

1
(void)instrumentObjcMessageSends(YES);

或者断点暂停程序运行,并在 gdb 中输入下面的命令:

1
call (void)instrumentObjcMessageSends(YES)

以第二种为例,操作如下所示:

技术分享

之后,运行时发送的所有消息都会打印到/tmp/msgSend-xxxx文件里了。

终端中输入命令前往:

1
open /private/tmp

技术分享

可能看到有多条,找到最新生成的,双击打开

在模拟器上执行执行以下语句(这一套调试方案仅适用于模拟器,真机不可用,关于该调试方案的拓展链接: Can the messages sent to an object in Objective-C be monitored or printed out? ),向一个对象发送一条错误的消息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//
//  main.m
//  CYLObjcMsgForwardTest
//
//  Copyright (c) 2015年 微博@iOS程序犭袁. All rights reserved.
//
 
#import #import "AppDelegate.h"
#import "CYLTest.h"
 
int main(int argc, char * argv[]) {
    @autoreleasepool {
        CYLTest *test = [[CYLTest alloc] init];
        [test performSelector:(@selector(iOS程序犭袁))];
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

技术分享

你可以在/tmp/msgSend-xxxx(我这一次是/tmp/msgSend-9805)文件里,看到打印出来:

技术分享

1
2
3
4
5
6
7
8
9
10
11
12
13
14
+ CYLTest NSObject initialize
+ CYLTest NSObject alloc
- CYLTest NSObject init
- CYLTest NSObject performSelector:
+ CYLTest NSObject resolveInstanceMethod:
+ CYLTest NSObject resolveInstanceMethod:
- CYLTest NSObject forwardingTargetForSelector:
- CYLTest NSObject forwardingTargetForSelector:
- CYLTest NSObject methodSignatureForSelector:
- CYLTest NSObject methodSignatureForSelector:
- CYLTest NSObject class
- CYLTest NSObject doesNotRecognizeSelector:
- CYLTest NSObject doesNotRecognizeSelector:
- CYLTest NSObject class

结合《NSObject官方文档》,排除掉 NSObject 做的事,剩下的就是_objc_msgForward消息转发做的几件事:

  1. 调用resolveInstanceMethod:方法 (或 resolveClassMethod:)。允许用户在此时为该 Class 动态添加实现。如果有实现了,则调用并返回YES,那么重新开始objc_msgSend流程。这一次对象会响应这个选择器,一般是因为它已经调用过class_addMethod。如果仍没实现,继续下面的动作。

  2. 调用forwardingTargetForSelector:方法,尝试找到一个能响应该消息的对象。如果获取到,则直接把消息转发给它,返回非 nil 对象。否则返回 nil ,继续下面的动作。注意,这里不要返回 self ,否则会形成死循环。

  3. 调用methodSignatureForSelector:方法,尝试获得一个方法签名。如果获取不到,则直接调用doesNotRecognizeSelector抛出异常。如果能获取,则返回非nil:创建一个 NSlnvocation 并传给forwardInvocation:。

  4. 调用forwardInvocation:方法,将第3步获取到的方法签名包装成 Invocation 传入,如何处理就在这里面了,并返回非ni。

  5. 调用doesNotRecognizeSelector: ,默认的实现是抛出异常。如果第3步没能获得一个方法签名,执行该步骤。

上面前4个方法均是模板方法,开发者可以override,由 runtime 来调用。最常见的实现消息转发:就是重写方法3和4,吞掉一个消息或者代理给其他对象都是没问题的

也就是说_objc_msgForward在进行消息转发的过程中会涉及以下这几个方法:

  1. resolveInstanceMethod:方法 (或 resolveClassMethod:)。

  2. forwardingTargetForSelector:方法

  3. methodSignatureForSelector:方法

  4. forwardInvocation:方法

  5. doesNotRecognizeSelector: 方法

下面回答下第二个问题“直接_objc_msgForward调用它将会发生什么?”

直接调用_objc_msgForward是非常危险的事,如果用不好会直接导致程序Crash,但是如果用得好,能做很多非常酷的事。

就好像跑酷,干得好,叫“耍酷”,干不好就叫“作死”。

正如前文所说:

_objc_msgForward是 IMP 类型,用于消息转发的:当向一个对象发送一条消息,但它并没有实现的时候,_objc_msgForward会尝试做消息转发。

如何调用_objc_msgForward? _objc_msgForward隶属 C 语言,有三个参数 :

技术分享

首先了解下如何调用 IMP 类型的方法,IMP类型是如下格式:

为了直观,我们可以通过如下方式定义一个 IMP类型 :

1
typedef void (*voidIMP)(id, SEL, ...)

一旦调用_objc_msgForward,将跳过查找 IMP 的过程,直接触发“消息转发”,

如果调用了_objc_msgForward,即使这个对象确实已经实现了这个方法,你也会告诉objc_msgSend:

“我没有在这个对象里找到这个方法的实现”

想象下objc_msgSend会怎么做?通常情况下,下面这张图就是你正常走objc_msgSend过程,和直接调用_objc_msgForward的前后差别:

技术分享

有哪些场景需要直接调用_objc_msgForward?最常见的场景是:你想获取某方法所对应的NSInvocation对象。举例说明:

JSPatch (Github 链接)就是直接调用_objc_msgForward来实现其核心功能的:

JSPatch 以小巧的体积做到了让JS调用/替换任意OC方法,让iOS APP具备热更新的能力。

作者的博文《JSPatch实现原理详解》详细记录了实现原理,有兴趣可以看下。

26. runtime如何实现weak变量的自动置nil?

runtime 对注册的类, 会进行布局,对于 weak 对象会放入一个 hash 表中。 用 weak 指向的对象内存地址作为 key,当此对象的引用计数为0的时候会 dealloc,假如 weak 指向的对象内存地址是a,那么就会以a为键, 在这个 weak 表中搜索,找到所有以a为键的 weak 对象,从而设置为 nil。

上篇中的《runtime 如何实现 weak 属性》有论述。(注:在上篇的《使用runtime Associate方法关联的对象,需要在主对象dealloc的时候释放么?》里给出的“对象的内存销毁时间表”也提到__weak引用的解除时间。)

我们可以设计一个函数(伪代码)来表示上述机制:

objc_storeWeak(&a, b)函数:

objc_storeWeak函数把第二个参数--赋值对象(b)的内存地址作为键值key,将第一个参数--weak修饰的属性变量(a)的内存地址(&a)作为value,注册到 weak 表中。如果第二个参数(b)为0(nil),那么把变量(a)的内存地址(&a)从weak表中删除,

你可以把objc_storeWeak(&a, b)理解为:objc_storeWeak(value, key),并且当key变nil,将value置nil。

在b非nil时,a和b指向同一个内存地址,在b变nil时,a变nil。此时向a发送消息不会崩溃:在Objective-C中向nil发送消息是安全的。

而如果a是由assign修饰的,则: 在b非nil时,a和b指向同一个内存地址,在b变nil时,a还是指向该内存地址,变野指针。此时向a发送消息极易崩溃。

下面我们将基于objc_storeWeak(&a, b)函数,使用伪代码模拟“runtime如何实现weak属性”:

1
2
3
4
5
6
7
8
// 使用伪代码模拟:runtime如何实现weak属性
 
 id obj1;
 objc_initWeak(&obj1, obj);
/*obj引用计数变为0,变量作用域结束*/
 objc_destroyWeak(&obj1);

下面对用到的两个方法objc_initWeak和objc_destroyWeak做下解释:

总体说来,作用是: 通过objc_initWeak函数初始化“附有weak修饰符的变量(obj1)”,在变量作用域结束时通过objc_destoryWeak函数释放该变量(obj1)。

下面分别介绍下方法的内部实现:

objc_initWeak函数的实现是这样的:在将“附有weak修饰符的变量(obj1)”初始化为0(nil)后,会将“赋值对象”(obj)作为参数,调用objc_storeWeak函数。

1
2
obj1 = 0;
obj_storeWeak(&obj1, obj);

也就是说:

weak 修饰的指针默认值是 nil (在Objective-C中向nil发送消息是安全的)

然后obj_destroyWeak函数将0(nil)作为参数,调用objc_storeWeak函数。

1
objc_storeWeak(&obj1, 0);

前面的源代码与下列源代码相同。

1
2
3
4
5
6
7
8
9
// 使用伪代码模拟:runtime如何实现weak属性
 
id obj1;
obj1 = 0;
objc_storeWeak(&obj1, obj);
/* ... obj的引用计数变为0,被置nil ... */
objc_storeWeak(&obj1, 0);

objc_storeWeak函数把第二个参数--赋值对象(obj)的内存地址作为键值,将第一个参数--weak修饰的属性变量(obj1)的内存地址注册到 weak 表中。如果第二个参数(obj)为0(nil),那么把变量(obj1)的地址从weak表中删除。

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

  • 不能向编译后得到的类中增加实例变量;

  • 能向运行时创建的类中添加实例变量;

解释下:

  • 因为编译后的类已经注册在 runtime 中,类结构体中的 objc_ivar_list 实例变量的链表 和 instance_size 实例变量的内存大小已经确定,同时runtime 会调用 class_setIvarLayout 或 class_setWeakIvarLayout 来处理 strong weak 引用。所以不能向存在的类中添加实例变量;

  • 运行时创建的类是可以添加实例变量,调用 class_addIvar 函数。但是得在调用 objc_allocateClassPair 之后,objc_registerClassPair 之前,原因同上。

28. runloop和线程有什么关系?

总的说来,Run loop,正如其名,loop表示某种循环,和run放在一起就表示一直在运行着的循环。实际上,run loop和线程是紧密相连的,可以这样说run loop是为了线程而生,没有线程,它就没有存在的必要。Run loops是线程的基础架构部分, Cocoa 和 CoreFundation 都提供了 run loop 对象方便配置和管理线程的 run loop (以下都以 Cocoa 为例)。每个线程,包括程序的主线程( main thread )都有与之相应的 run loop 对象。

runloop 和线程的关系:

1. 主线程的run loop默认是启动的。

iOS的应用程序里面,程序启动后会有一个如下的main()函数

1
2
3
4
int main(int argc, char * argv[]) {
@autoreleasepool {    return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

重点是UIApplicationMain()函数,这个方法会为main thread设置一个NSRunLoop对象,这就解释了:为什么我们的应用可以在无人操作的时候休息,需要让它干活的时候又能立马响应。

2. 对其它线程来说,run loop默认是没有启动的,如果你需要更多的线程交互则可以手动配置和启动,如果线程只是去执行一个长时间的已确定的任务则不需要。

3. 在任何一个 Cocoa 程序的线程中,都可以通过以下代码来获取到当前线程的 run loop 。

1
NSRunLoop *runloop = [NSRunLoop currentRunLoop];

参考链接:《Objective-C之run loop详解》

29. runloop的mode作用是什么?

model 主要是用来指定事件在运行循环中的优先级的,分为:

  • NSDefaultRunLoopMode(kCFRunLoopDefaultMode):默认,空闲状态

  • UITrackingRunLoopMode:ScrollView滑动时

  • UIInitializationRunLoopMode:启动时

  • NSRunLoopCommonModes(kCFRunLoopCommonModes):Mode集合

苹果公开提供的 Mode 有两个:

  1. NSDefaultRunLoopMode(kCFRunLoopDefaultMode)

  2. NSRunLoopCommonModes(kCFRunLoopCommonModes)

30. 以+ scheduledTimerWithTimeInterval...的方式触发的timer,在滑动页面上的列表时,timer会暂定回调,为什么?如何解决?

RunLoop只能运行在一种mode下,如果要换mode,当前的loop也需要停下重启成新的。利用这个机制,ScrollView滚动过程中NSDefaultRunLoopMode(kCFRunLoopDefaultMode)的mode会切换到UITrackingRunLoopMode来保证ScrollView的流畅滑动:只能在NSDefaultRunLoopMode模式下处理的事件会影响scrllView的滑动。

如果我们把一个NSTimer对象以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)添加到主运行循环中的时候, ScrollView滚动过程中会因为mode的切换,而导致NSTimer将不再被调度。

同时因为mode还是可定制的,所以:

Timer计时会被scrollView的滑动影响的问题可以通过将timer添加到NSRunLoopCommonModes(kCFRunLoopCommonModes)来解决。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 
// http://weibo.com/luohanchenyilong/ (微博@iOS程序犭袁)
 
//将timer添加到NSDefaultRunLoopMode中
[NSTimer scheduledTimerWithTimeInterval:1.0
     target:self
     selector:@selector(timerTick:)
     userInfo:nil
     repeats:YES];
//然后再添加到NSRunLoopCommonModes里
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0
     target:self
     selector:@selector(timerTick:)
     userInfo:nil
     repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

31. 猜想runloop内部是如何实现的?

一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出。如果我们需要一个机制,让线程能随时处理事件但并不退出,通常的代码逻辑 是这样的:

1
2
3
4
5
6
7
function loop() {
    initialize();
    do {
        var message = get_next_message();
        process_message(message);
    while (message != quit);
}

或使用伪代码来展示下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 
// http://weibo.com/luohanchenyilong/ (微博@iOS程序犭袁)
int main(int argc, char * argv[]) {
 //程序一直运行状态
 while (AppIsRunning) {
      //睡眠状态,等待唤醒事件
      id whoWakesMe = SleepForWakingUp();
      //得到唤醒事件
      id event = GetEvent(whoWakesMe);
      //开始处理事件
      HandleEvent(event);
 }
 return 0;
}

参考链接:

  1. 《深入理解RunLoop》

  2. 摘自博文CFRunLoop,原作者是微博@我就叫Sunny怎么了

32. objc使用什么机制管理对象内存?

通过 retainCount 的机制来决定对象是否需要释放。 每次 runloop 的时候,都会检查对象的 retainCount,如果retainCount 为 0,说明该对象没有地方需要继续使用了,可以释放掉了。

33. ARC通过什么方式帮助开发者管理内存?

编译时根据代码上下文,插入 retain/release

34. 不手动指定autoreleasepool的前提下,一个autorealese对象在什么时刻释放?(比如在一个vc的viewDidLoad中创建)

分两种情况:手动干预释放时机、系统自动去释放。

  1. 手动干预释放时机--指定autoreleasepool 就是所谓的:当前作用域大括号结束时释放。

  2. 系统自动去释放--不手动指定autoreleasepool

    Autorelease对象会在当前的 runloop 迭代结束时释放。

    如果在一个vc的viewDidLoad中创建一个 Autorelease对象,那么该对象会在 viewDidAppear 方法执行前就被销毁了。

参考链接:《黑幕背后的Autorelease》

35. BAD_ACCESS在什么情况下出现?

访问了野指针,比如对一个已经释放的对象执行了release、访问已经释放对象的成员变量或者发消息。 死循环

36. 苹果是如何实现autoreleasepool的?

autoreleasepool以一个队列数组的形式实现,主要通过下列三个函数完成.

  1. objc_autoreleasepoolPush

  2. objc_autoreleasepoolPop

  3. objc_aurorelease

看函数名就可以知道,对autorelease分别执行push,和pop操作。销毁对象时执行release操作。

37. 使用block时什么情况会发生引用循环,如何解决?

一个对象中强引用了block,在block中又使用了该对象,就会发射循环引用。 解决方法是将该对象使用__weak或者__block修饰符修饰之后再在block中使用。

  1. id weak weakSelf = self; 或者 weak __typeof(&*self)weakSelf = self该方法可以设置宏

  2. id __block weakSelf = self;

38. 在block内如何修改block外部变量?

默认情况下,在block中访问的外部变量是复制过去的,即:写操作不对原变量生效。但是你可以加上__block来让其写操作生效,示例代码如下:

1
2
3
4
5
6
__block int a = 0;
void  (^foo)(void) = ^{ 
    a = 1; 
}
f00(); 
//这里,a的值被修改为1

参考链接:微博@唐巧_boy的著作《iOS开发进阶》中的第11.2.3章节

39. 使用系统的某些block api(如UIView的block版本写动画时),是否也考虑引用循环问题?

系统的某些block api中,UIView的block版本写动画时不需要考虑,但也有一些api 需要考虑:

所谓“引用循环”是指双向的强引用,所以那些“单向的强引用”(block 强引用 self )没有问题,比如这些:

1
2
3
4
5
6
[UIView animateWithDuration:duration animations:^{ [self.superview layoutIfNeeded]; }]; 
[[NSOperationQueue mainQueue] addOperationWithBlock:^{ self.someProperty = xyz; }]; 
[[NSNotificationCenter defaultCenter] addObserverForName:@"someNotification" 
                                                  object:nil 
                           queue:[NSOperationQueue mainQueue]                                              usingBlock:^(NSNotification * notification) {
                                                    self.someProperty = xyz; }];

这些情况不需要考虑“引用循环”。

但如果你使用一些参数中可能含有 ivar 的系统 api ,如 GCD 、NSNotificationCenter就要小心一点:比如GCD 内部如果引用了 self,而且 GCD 的其他参数是 ivar,则要考虑到循环引用:

1
2
3
4
5
6
7
__weak __typeof__(self) weakSelf = self;
dispatch_group_async(_operationsGroup, _operationsQueue, ^
{
__typeof__(self) strongSelf = weakSelf;
[strongSelf doSomething];
[strongSelf doSomethingElse];
} );

类似的:

1
2
3
4
5
6
7
8
__weak __typeof__(self) weakSelf = self;
  _observer = [[NSNotificationCenter defaultCenter] addObserverForName:@"testKey"
                                                                object:nil
                                                                 queue:nil
                                                            usingBlock:^(NSNotification *note) {
      __typeof__(self) strongSelf = weakSelf;
      [strongSelf dismissModalViewControllerAnimated:YES];
  }];

self --> _observer --> block --> self 显然这也是一个循环引用。

40. GCD的队列(dispatch_queue_t)分哪两种类型?

  1. 串行队列Serial Dispatch Queue

  2. 并行队列Concurrent Dispatch Queue

41. 如何用GCD同步若干个异步调用?(如根据若干个url异步加载多张图片,然后在都下载完成后合成一张整图)

使用Dispatch Group追加block到Global Group Queue,这些block如果全部执行完毕,就会执行Main Dispatch Queue中的结束处理的block。

1
2
3
4
5
6
7
8
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{ /*加载图片1 */ });
dispatch_group_async(group, queue, ^{ /*加载图片2 */ });
dispatch_group_async(group, queue, ^{ /*加载图片3 */ }); 
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        // 合并图片
});

42. dispatch_barrier_async的作用是什么?

在并行队列中,为了保持某些任务的顺序,需要等待一些任务完成后才能继续进行,使用 barrier 来等待之前任务完成,避免数据竞争等问题。 dispatch_barrier_async 函数会等待追加到Concurrent Dispatch Queue并行队列中的操作全部执行完之后,然后再执行 dispatch_barrier_async 函数追加的处理,等 dispatch_barrier_async 追加的处理执行结束之后,Concurrent Dispatch Queue才恢复之前的动作继续执行。

打个比方:比如你们公司周末跟团旅游,高速休息站上,司机说:大家都去上厕所,

以上是关于IOS面试题的主要内容,如果未能解决你的问题,请参考以下文章

Java进阶之光!2021必看-Java高级面试题总结

iOS面试--虎牙最新iOS开发面试题

国内在线旅游巨头iOS面试题

经验总结:Java高级工程师面试题-字节跳动,成功跳槽阿里!

可能碰到的iOS笔试面试题--C语言

一道简单的IOS面试题-b