iOS开发-面试

IOS面试:拼多多、滴滴、招商等面试实录及解答

2020-07-09  本文已影响0人  时光啊混蛋_97boy

原创:面试经验型文章
无私奉献,为国为民,创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
温馨提示:由于简书不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容

Demo在我的Github上,欢迎下载。
IOSInterviewDemo

目录

笔者的面试经历

1、招商银行三面压轴题:把数字金额转为中文大写。

本来以为很简单的,直到见到完整的代码:

- (NSString *)getAmountInWords:(NSString *)money{
    if (money.length == 0) {
        return @"";
    }
    if (money.floatValue == 0) {
        return @"零圆整";
    }
    //大写数字
    NSArray *upperArray = @[ @"零",@"壹",@"贰",@"叁",@"肆",@"伍",@"陆",@"柒",@"捌",@"玖" ];
    /** 整数部分的单位 */
    NSArray *measureArray = @[ @"", @"拾", @"佰", @"仟"];
    /** 整数部分的单位 */
    NSArray *intUnit = @[@"圆", @"万", @"亿"];
    /** 小数部分的单位 */
    NSArray *floatUnitArray = @[ @"角", @"分" ];
    
    NSString *upIntNum = [NSString string];
    NSString *upFloatNum = [NSString string];
    NSArray *numArray = [money componentsSeparatedByString:@"."];
    
    NSString *str1 = [numArray objectAtIndex:0];
    NSInteger num1 = str1.integerValue;
    for (int i = 0; i < intUnit.count && num1 > 0; i++) {//这一部分就是单纯的转化
        NSString *temp = @"";
        int tempNum = num1%10000;
        if (tempNum != 0 || i == 0) {
            for (int j = 0; j < measureArray.count && num1 > 0; j++) {
                temp = [NSString stringWithFormat:@"%@%@%@", [upperArray objectAtIndex:num1%10], [measureArray objectAtIndex:j],temp];//每次转化最后一位数
                num1 = num1/10;//数字除以10
            }
            upIntNum = [[temp stringByAppendingString:[intUnit objectAtIndex:i]] stringByAppendingString:upIntNum];
        } else {
            num1 /= 10000;
            temp = @"零";
            upIntNum = [temp stringByAppendingString:upIntNum];
        }
        
    }
    
    for (int m = 1; m < measureArray.count; m++) { //把零佰零仟这种情况转为零
        NSString *lingUnit = [@"零" stringByAppendingString:[measureArray objectAtIndex:m]];
        upIntNum = [upIntNum stringByReplacingOccurrencesOfString:lingUnit withString:@"零"];
    }
    
    while ([upIntNum rangeOfString:@"零零"].location != NSNotFound) {//多个零相邻的保留一个零
        upIntNum = [upIntNum stringByReplacingOccurrencesOfString:@"零零" withString:@"零"];
    }
    for (int k = 0; k < intUnit.count * 2; k++) { //零万、零亿这种情况转化为万零
        NSString *unit = [intUnit objectAtIndex:k%intUnit.count];
        NSString *lingUnit = [@"零" stringByAppendingString:unit];
        upIntNum = [upIntNum stringByReplacingOccurrencesOfString:lingUnit withString:[unit stringByAppendingString:@"零"]];
    }
    
    if (numArray.count == 2) {//小数部分转化
        NSString *floatStr = [numArray objectAtIndex:1];
        for (NSInteger i = floatStr.length; i > 0; i--) {
            NSString *temp = [floatStr substringWithRange:NSMakeRange(floatStr.length - i, 1)];
            NSInteger tempNum = temp.integerValue;
            if (tempNum == 0) continue;
            NSString *upNum = [upperArray objectAtIndex:tempNum];
            NSString *unit = [floatUnitArray objectAtIndex:floatStr.length - I];
            if (i < floatStr.length && upFloatNum.length == 0 && upIntNum.length > 0) {
                upFloatNum = @"零";
            }
            upFloatNum = [NSString stringWithFormat:@"%@%@%@", upFloatNum, upNum, unit];
        }
    }
    if (upFloatNum.length == 0) {
        upFloatNum = @"整";
    }
    
    NSString *amountInWords = [NSString stringWithFormat:@"%@%@", upIntNum, upFloatNum];
    
    while ([amountInWords rangeOfString:@"零零"].location != NSNotFound) {//再次除去多余的零
        amountInWords = [amountInWords stringByReplacingOccurrencesOfString:@"零零" withString:@"零"];
    }
    
    if ([amountInWords rangeOfString:@"零整"].location != NSNotFound) {
        amountInWords = [amountInWords stringByReplacingOccurrencesOfString:@"零整" withString:@"整"];
    }
    
    return amountInWords;
    
}

2、拼多多二面:相同 URL重复请求问题,比如用户重复点击一个按钮会发送多次请求或者3个头像相同URL一起发起请求。

解决方案有好多种......罗列下:

法一:客户端网络请求方法中过滤

一个网络请求包含两部分:url和参数,因此我们可以在网络请求方类里面增加一个NSMutableArray,用户url和参数通过md5进行一次加密作为key,发送之前我们可以对其值赋值为任意固定值,当服务器返回结果的时候我们可以将这个键值对移除。每次发送网络请求前,先从这个字典中查看本次请求的md5值是否存在,如果存在表明本次请求已经发送但是尚未收到响应,此时应该return,不再进行网络请求,否则就是收到响应了或者该请求是第一次发出。

注意:有的时候参数包含了时间戳,这样计算永远会不相同的,md5加密之前要清除参数中的时间戳或者随机字段。

法二:交给服务器解决

服务器把每次把收到的请求进行MD5加密,作为一个字典的键,值可以设置任意,然后查找数据库,查找回来以后通过适当的形式返回客户端,在查找数据期间,收到请求先从字典查找键是否存在,如果已经存在就不作出响应。

法三:在客户端形成一个遮罩

在网络请求发起到网络响应收到的这段时间在客户端形成一个遮罩,可以用来阻止用户点击UI进行操作,防止某些意外的请求产生。

优点:解决了用户重复点击多次发送请求的问题,同时防止了在某些条件不具备的情况进行其他操作引发客户端出现问题的出现。

缺点:有的时候不人性化,比如用户进入某个界面就是网速不好,一直请求数据,等了好长时间都没有结果,这个时候用户一般都会下意识点击返回按钮,但是这种情况下,返回按钮的点击事件也是不起作用的。

法四:利用运行时设置相应按钮点击间隔

优点:有效解决了用户双击UI造成事件触发两次的情况(不仅仅局限网络请求)/

缺点 :在网络不好的情况下,很可能在m秒内确实没有收到服务器响应。如果用户一直点击按钮,很可能触发重复点击。而且可能和系统以及存在的事件冲突,有的时候会产生莫名其妙的错误。比如我加入这个类扩展后,项目中选择照片时候进行拍照上传的时候,本来需要点击一下拍摄按钮就可以成功的事情,确需要长时间触摸才能生效

UIControl进行扩展:

@interface UIControl (delay)
@property (nonatomic, assign) NSTimeInterval uxy_acceptEventInterval;   // 可以用这个给重复点击加间隔
@end
#import "UIControl+delay.h"
#import <objc/runtime.h>

//增加两个属性
static const char *UIControl_acceptEventInterval = "UIControl_acceptEventInterval";
static const char *UIControl_ignoreEvent = "UIControl_ignoreEvent";

@implementation UIControl (delay)
//时间间隔
- (NSTimeInterval)uxy_acceptEventInterval
{
    return [objc_getAssociatedObject(self, UIControl_acceptEventInterval) doubleValue];
}
- (void)setUxy_acceptEventInterval:(NSTimeInterval)uxy_acceptEventInterval
{
    objc_setAssociatedObject(self, UIControl_acceptEventInterval, @(uxy_acceptEventInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
//是否响应事件的标志位
-(BOOL)uxy_ignoreEvent
{
    return [objc_getAssociatedObject(self, UIControl_ignoreEvent) boolValue];
}
-(void)setUxy_ignoreEvent:(BOOL)uxy_ignoreEvent
{
    objc_setAssociatedObject(self, UIControl_ignoreEvent, @(uxy_ignoreEvent), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
+(void)load
{
    //将系统的sendAction方法和自己实现的方法进行互换
    Method a=class_getInstanceMethod(self,@selector(sendAction:to:forEvent:));
    Method b = class_getInstanceMethod(self,@selector(__uxy_sendAction:to:forEvent:));
    method_exchangeImplementations(a,b);
}
//点击后会先进入这里
- (void)__uxy_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event
{
    if (self.uxy_ignoreEvent)//根据状态判断是否继续执行
        return;
    if (self.uxy_acceptEventInterval > 0)
    {
        self.uxy_ignoreEvent = YES;
        //周期性清空标志位
        [self performSelector:@selector(setUxy_ignoreEvent:) withObject:@(NO) afterDelay:self.uxy_acceptEventInterval];
    }
    //这里其实是系统的原来的sendAction to方法。
    [self __uxy_sendAction:action to:target forEvent:event];
}

@end

或者对UIButton进行扩展:

@implementation UIButton (delay)

// 因category不能添加属性,只能通过关联对象的方式。
static const char *UIControl_acceptEventInterval = "UIControl_acceptEventInterval";

- (NSTimeInterval)cs_acceptEventInterval {
    return  [objc_getAssociatedObject(self, UIControl_acceptEventInterval) doubleValue];
}

- (void)setCs_acceptEventInterval:(NSTimeInterval)cs_acceptEventInterval {
    objc_setAssociatedObject(self, UIControl_acceptEventInterval, @(cs_acceptEventInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

static const char *UIControl_acceptEventTime = "UIControl_acceptEventTime";

- (NSTimeInterval)cs_acceptEventTime {
    return  [objc_getAssociatedObject(self, UIControl_acceptEventTime) doubleValue];
}

- (void)setCs_acceptEventTime:(NSTimeInterval)cs_acceptEventTime {
    objc_setAssociatedObject(self, UIControl_acceptEventTime, @(cs_acceptEventTime), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

// 在load时执行hook
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        //分别获取
        SEL beforeSelector = @selector(sendAction:to:forEvent:);
        SEL afterSelector = @selector(cs_sendAction:to:forEvent:);
        
        Method beforeMethod = class_getInstanceMethod(class, beforeSelector);
        Method afterMethod = class_getInstanceMethod(class, afterSelector);
        //先尝试给原来的方法添加实现,如果原来的方法不存在就可以添加成功。返回为YES,否则
        //返回为NO。
        //UIButton 真的没有sendAction方法的实现,这是继承了UIControl的而已,UIControl才真正的实现了。
        BOOL didAddMethod =
        class_addMethod(class,
                        beforeSelector,
                        method_getImplementation(afterMethod),
                        method_getTypeEncoding(afterMethod));
        NSLog(@"%d",didAddMethod);
        if (didAddMethod) {
            // 如果之前不存在,但是添加成功了,此时添加成功的是cs_sendAction方法的实现
            // 这里只需要方法替换
            class_replaceMethod(class,
                                afterSelector,
                                method_getImplementation(beforeMethod),
                                method_getTypeEncoding(beforeMethod));
        } else {
            //本来如果存在就进行交换
            method_exchangeImplementations(afterMethod, beforeMethod);
        }
    });
}
- (void)cs_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
    if ([NSDate date].timeIntervalSince1970 - self.cs_acceptEventTime < self.cs_acceptEventInterval) {
        return;
    }
    if (self.cs_acceptEventInterval > 0) {
        self.cs_acceptEventTime = [NSDate date].timeIntervalSince1970;
    }
    [self cs_sendAction:action to:target forEvent:event];
    
}
@end
法五:多个线程发起相同请求,加上锁避免重复
 @synchronized (self) {//加锁,避免数组重复创建添加等问题
         static NSMutableArray * successBlocks;//用数组保存回调
         static NSMutableArray * failureBlocks;
         static dispatch_once_t onceToken;
         dispatch_once(&onceToken, ^{//仅创建一次数组
            successBlocks = [NSMutableArray new];
            failureBlocks = [NSMutableArray new];
         });
         if (success) {//每调用一次此函数,就把回调加进数组中
            [successBlocks addObject:success];
         }
         if (failure) {
            [failureBlocks addObject:failure];
         }
         
         static BOOL isProcessing = NO;
         if (isProcessing == YES) {//如果已经在请求了,就不再发出新的请求
            return;
         }
         isProcessing = YES;
         [self callerPostTransactionId:transactionId parameters:dic showActivityIndicator:showActivityIndicator showErrorAlterView:showErrorAlterView success:^(id responseObject) {
            @synchronized (self) {//网络请求的回调也要加锁,这里是另一个线程了
               for (successBlock eachSuccess in successBlocks) { //遍历回调数组,把结果发给每个调用者
                  eachSuccess(responseObject);
               }
               [successBlocks removeAllObjects];
               [failureBlocks removeAllObjects];
               isProcessing = NO;
            }
         } failure:^(id data) {
            @synchronized (self) {
               for (failureBlock eachFailure in failureBlocks) {
                  eachFailure(data);
               }
               [successBlocks removeAllObjects];
               [failureBlocks removeAllObjects];
               isProcessing = NO;
            }
         }];
      }

3、网易一面:队列引起的循环等待

主队列:

- (void)viewDidLoad {
    NSLog(@"1");
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"2");
    });
    NSLog(@"3");
}
死锁原因

同步串行:

- (void)viewDidLoad {
    NSLog(@"1");
    dispatch_sync(serialQueue, ^{
        NSLog(@"2");
    });
    NSLog(@"3");
}
自定义队列

同步并发:

- (void)viewDidLoad {
    NSLog(@"1");
    dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"2");
        dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            NSLog(@"3");
        });
        NSLog(@"4");
    });
    NSLog(@"5");
}

输出12345,原因如下:

异步串行:

- (void)viewDidLoad {
    dispatch_async(dispatch_get_main_queue(), ^{
        NSLog("最常见的方式,回到主队列更新UI");
    });
}

异步并发:

- (void)viewDidLoad {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"1");
        [self performSelector:@selector(printTwo) withObject:nil afterDelay:0];
        NSLog(@"3");
    });
}

- (void)printTwo {
    NSLog(@"2");
}

输出13,原因是:dispatch_asyncGCD开辟的线程执行,无runloop,忽视调performSelector方法。

4、字节跳动一面:子线程AutoRelease对象何时释放

见我的文章:IOS内存管理,看这一篇文章就够了

5、网易二面:@synchronized内部如何实现的

见我的文章:IOS多线程,看这一篇文章就够了

6、58同城一面:关联对象的实现原理

见我的文章:IOS运行时,看这一篇文章就够了

7、58同城一面:NSTimer循环引用的原因以及解决办法

见我的文章:IOS内存管理,看这一篇文章就够了

8、如何将一张内存极大的图片可以像地图一样的加载出来

见我的文章:IOS 解决问题:微博长图分割显示Demo

9、子线程中是如何进行内存管理的
见我的文章:IOS多线程,看这一篇文章就够了

实录一面

1、Bigo:控件的点击事件和添加在上边的手势谁先响应,并说明原因

手势谁先响应
- (void)viewDidLoad
{
    [super viewDidLoad];
    
    UIButton *btn = [[UIButton alloc] initWithFrame:CGRectMake(0, 100, 100, 100)];
    btn.backgroundColor = UIColor.redColor;
    [btn addTarget:self action:@selector(click) forControlEvents:UIControlEventTouchUpInside];
    
    UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] init];
    [tap addTarget:self action:@selector(tap)];
    [btn addGestureRecognizer:tap];
   
    [self.view addSubview:btn];
}

- (void)click
{
    NSLog(@"控件的点击事件");
}

- (void)tap
{
    NSLog(@"手势");
}

输出结果为:

2020-07-21 17:59:15.797539+0800 Demo[72815:21154355] 手势

手势优先响应。为什么呢?

原因

因为单击手势优先于UIView的事件响应,如果手势识别成功,就会直接取消事件的响应链传递。如果手势识别失败了,触摸事件会继续走传递链,传递给响应链处理。但是手势识别是需要时间的,在possible状态的时候,单击事件也可能已经传递给响应链了,如果手势识别器识别出触摸手势,会将传递给响应链的事件取消掉。

手势在触碰事件处理流程中,处于观察者的角色,其不是view层级结构的一部分,所以不参与响应者链。在将触摸事件发送给hit-test view之前,系统会先将触碰事件发送到view绑定的Gesture Recognizer上。

cancelsTouchesInView:YES时,表示当Gesture Recognizers识别到手势后,会向hit-test view发送touchesCancelled:消息以取消hit-test view对触碰序列的处理,这样只要Gesture Recognizer响应此次触碰,响应者链的view不再响应。如果为NO,则不发送touchesCancelled:消息,这样Gesture Recognizerview同时响应触碰事件。默认值是YES

delaysTouchesBegan:NO时表示触碰序列已经开始而手势识别还未识别出此手势时,touch事件会同时发给hit-test view。为YES时,延迟发送touchesEnded:消息,手势失败时才发送。默认值是YES

didSelectRowAtIndexPath失效问题的解决方案
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch  
{  
     // 若为UITableViewCellContentView(即点击了tableViewCell),
    if ([NSStringFromClass([touch.view class]) isEqualToString:@"UITableViewCellContentView"]) {  
        // cell 不需要响应 父视图的手势,保证didselect 可以正常
        return NO;  
    }  
    //默认都需要响应
    return  YES;  
}
UIView用户交互相关的属性
UIView的触碰响应

当手指触碰到屏幕,无论是单点还是多点触碰,事件都会开始,直到用户所有的手指都离开屏幕。期间所有的UITouch对象都被封装在UIEvent事件对象中。

响应者对象就是可以响应事件并对事件作出处理的对象。在iOS中UIResponder类定义了响应者对象的所有方法。UIApplicationUIWindowUIViewControllerUIView以及UIKit中继承自UIView的控件都间接或直接继承自UIResponder类,这些类都可以当做响应者。

响应者链表示一系列响应者对象组成的事件传递的链条。当确定了第一响应者后,事件交由第一响应者处理,如果第一响应者不处理事件沿着响应者链传递,交给下一个响应者。一般来说,第一响应者是UIView对象或者UIView的子类对象,当其被触摸后事件交由它处理,如果它不处理,事件就会交给它的UIViewController处理(如果存在),然后是它的superview父视图对象,以此类推,直到顶层视图。如果顶层视图不处理则交给UIWindow对象处理,再到UIApplication对象。如果整个响应者链都不响应这个事件则该事件被丢弃。

UIView类继承了UIResponder类,要对事件作出处理还需要重写UIResponder类中定义的事件处理函数。根据不同的触碰状态,程序会调用相应的处理函数,这些函数包括:

//触摸事件:对应了UITouch类中的phase属性的4个枚举值
//Touches表示触碰产生的所有的UITouch对象,event表示事件
//因为UIEvent包含了整个触碰过程中所有的触碰对象,所以可以调用allTouches 方法获取该事件内所有触碰对象
//也可以调用touchesForView;  或者touchesForWindows;  取出特定视图或者窗口上的触碰对象
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;

//设备移动事件
- (void)motionBegan:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event 
- (void)motionEnded:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event 
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event 

//远程控制事件,音乐后台播放控制的时候会用到
- (void)remoteControlReceivedWithEvent:(nullable UIEvent *)event 

//第一响应者,如操作UITextField来控制键盘的现隐藏
- (BOOL)canBecomeFirstResponder
- (BOOL)becomeFirstResponder
- (BOOL)canResignFirstResponder    
- (BOOL)resignFirstResponder;
- (BOOL)isFirstResponder
UIView的手势识别
NSArray *gestureRecognizers// 可以通过这个属性获取当前UIView的所有手势对象

// 添加手势
-(void)addGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer//增加一个手势。
-(void)removeGestureRecognizer:(UIGestureRecognizer *)getureRecognizer//删除一个手势。
-(BOOL)gestureRecognizerShouldBegan:(UIGestureRecognizer *)gestureRecognizer//询问是否开始执行该手势,默认返回YES。

// 手势种类
UITabGestureRecognizer// 轻击手势
  numberOfTapsRequired// 点击次数
  numberOfTouchesRequired// 手指个数

UIPinchGestureRecognizer// 捏合手势
  scale// 初始值为1,两手指距离减少则scale不断变小;两个手指重合则变为0
  velocity// 初始值为0,手指移动的相对速度,两手指距离减少为负数,速度越快数值越少;两手指距离变大为整数,速度越快数值越大

UIRotationGestureRecognizer// 旋转手势
  rotation// 初始值为0,两手指的旋转弧度,顺时针旋转为正数,逆时针旋转为负数
  velocity// 初始值为0手指一动的相对速度,顺时针为正数越快值越大;逆时针为负越快越小。

UISwipeGestureRecognizer// 轻扫手势
  numberOfTouchesRequired// 手指个数
  direction// 手势方向,如UISwipeGestureRecognizerDirectionRight向右

UIPanGestureRecognizer// 拖拽手势
  mininumNumberOfTouches// 默认值为1,最少手指数量
  maxnumNumberOfTouches// 最大手指数量

UILongPressGestrueRecognizer// 长按手势
  numberOfTapsRequired// 默认值为0,轻击的次数。
  numberOfTouchesRequired// 默认值是1,手指数量。
  mininumPressDuration// 默认值为0.5,单位是秒。
  allowableMovement:默认值为10,单位是像素pixels。

// 调用的方法
-(id) initWithTarget:action:// 初始化方法
-(void)addTarget:action:   
-(void)removeTarget:action: 

// 手势识别当前所处状态
UIGestureRecognizerStatePossibel// 未识别状态
UIGestureRecognizerStateBegan// 手势开始
UIGestureRecognizerStateChanged// 手势改变
UIGestureRecognizerStateEnded// 手势结束
UIGestureRecognizerStateFailured // 手势失败,被其他事件中断。当把手势state设为这个值得时候相当于取消了这个手势。

// 多手势兼容问题
[panRecognizer requireGestureRecognizerToFail:swipeRecognizer];// 捏合手势失败后才会触发拖拽手势。如果捏合手势成功则拖拽手势永远不会被触发
[rotationGestureRecognizer canBePreventedByGestureRecognizer:pinchGestureRecognizer];// 如果rotation手势重载了canBePreventedByGestureRecognizer方法并且返回YES。则旋转手势被捏合手势阻止
[rotationGestureRecognizer canPreventGestureRecognizer:pinchGestureRecognizer];// 如果rotation手势重载了canBePreventedByGestureRecognizer方法并且返回YES。则旋转手势阻止了捏合手势。

// UIGestureRecognizerDelegate
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer// 此方法在gesture recognizer视图传出UIGestureRecognizerStatePossible状态时调用,如果返回NO,则转换成UIGestureRecognizerStateFailed;如果返回YES,则继续识别。默认返回YES
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch// 此方法在window对象有触碰事件发生时,touchesBegan:withEvent:方法之前调用。如果返回NO,则GestureRecognizer忽略此触碰事件。默认返回YES。可以用于禁止某个区域的手势。
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer;// 如果有多个手势接收到了同一个消息,该回调方法决定当前手势是否要响应该事件,如果返回YES则该事件被响应,如果返回NO该事件将被忽略

2、Bigo:介绍编译的过程和原理

见我的文章:IOS系统架构,看这一篇文章就够了

3、美团:Block和函数指针的区别?

见我的文章:IOS通知、Block、Delegate、KVO与KVC,看这一篇文章就够了

4、字节跳动:如何自己设计json转model

见我的文章:IOS常用框架,看这一篇文章就够了

5、腾讯:断点续传怎么实现?需要设置什么?

见我的文章:IOS进阶:(网络篇)断点续传和后台下载(基于AFNetworking)


实录二面

1、小米:如果现在做一个新的网络层框架,有哪些需要考虑的地方。

见我的文章:IOS常用框架,看这一篇文章就够了

2、百度:判断一个字符串是不是 ipv6 地址(要求尽全力的考虑所有异常的情况)

见我的文章:IOS系统架构,看这一篇文章就够了

3、Bigo:JSBridge 是如何实现的,以及和原生的调用关系

见我的文章:IOS进阶:(网络篇)IOS和JS的交互

4、阿里:相似照片算法是怎么样的一个过程?

见我的文章:IOS 解决问题:相似、截屏照片清理和图片压缩Demo

5、字节跳动:bitmap的结构

见我的文章:IOS绘图与动画,看这一篇文章就够了

6、字节跳动:可变数组的实现原理

Demo尝试
#import <UIKit/UIKit.h>
#import "AppDelegate.h"

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
        
        NSArray *placeHoldler = [NSArray alloc];
        NSArray *arr1 = [[NSArray alloc] init];
        NSArray *arr2 = [[NSArray alloc] initWithObjects:@0, nil];
        NSArray *arr3 = [[NSArray alloc] initWithObjects:@0, @1, nil];
        NSArray *arr4 = [[NSArray alloc] initWithObjects:@0, @1, @2, nil];
        NSLog(@"placeHoldler:%s",object_getClassName(placeHoldler));
        NSLog(@"arr1:%s",object_getClassName(arr1));
        NSLog(@"arr2:%s",object_getClassName(arr2));
        NSLog(@"arr3:%s",object_getClassName(arr3));
        NSLog(@"arr4:%s",object_getClassName(arr4));
        
        NSMutableArray *mutablePlaceHoldler = [NSMutableArray alloc];
        NSMutableArray *mutableArr1 = [[NSMutableArray alloc] init];
        NSMutableArray *mutableArr2 = [[NSMutableArray alloc] initWithObjects:@0, nil];
        NSMutableArray *mutableArr3 = [[NSMutableArray alloc] initWithObjects:@0, @1, nil];
        NSMutableArray *mutableArr4 = [[NSMutableArray alloc] initWithObjects:@0, @1, @2, nil];
        NSLog(@"placeHoldler:%s",object_getClassName(mutablePlaceHoldler));
        NSLog(@"mutableArr1:%s",object_getClassName(mutableArr1));
        NSLog(@"mutableArr2:%s",object_getClassName(mutableArr2));
        NSLog(@"mutableArr3:%s",object_getClassName(mutableArr3));
        NSLog(@"mutableArr4:%s",object_getClassName(mutableArr4));

        NSLog(@"placeHoldler地址:%p",placeHoldler);
        NSArray *anotherPlaceHoldler = [NSArray alloc];
        NSLog(@"anotherPlaceHoldler地址:%p",anotherPlaceHoldler);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

输出结果为:

2020-07-23 17:52:33.886193+0800 Demo[76977:21942936] placeHoldler:__NSPlaceholderArray
2020-07-23 17:52:33.886604+0800 Demo[76977:21942936] arr1:__NSArray0
2020-07-23 17:52:33.886698+0800 Demo[76977:21942936] arr2:__NSSingleObjectArrayI
2020-07-23 17:52:33.886760+0800 Demo[76977:21942936] arr3:__NSArrayI
2020-07-23 17:52:33.886829+0800 Demo[76977:21942936] arr4:__NSArrayI
2020-07-23 17:52:33.886917+0800 Demo[76977:21942936] placeHoldler:__NSPlaceholderArray
2020-07-23 17:52:33.886975+0800 Demo[76977:21942936] mutableArr1:__NSArrayM
2020-07-23 17:52:33.887035+0800 Demo[76977:21942936] mutableArr2:__NSArrayM
2020-07-23 17:52:33.887205+0800 Demo[76977:21942936] mutableArr3:__NSArrayM
2020-07-23 17:52:33.887332+0800 Demo[76977:21942936] mutableArr4:__NSArrayM
2020-07-23 18:12:14.146565+0800 Demo[77022:21953656] placeHoldler地址:0x10dd56738
2020-07-23 18:12:14.146780+0800 Demo[77022:21953656] anotherPlaceHoldler地址:0x10dd56738

不管是NSArray,还是NSMutableArrayalloc之后的得到都是__NSPlacrholderArray。当我们NSArray一个空数组,得到的是__NSArray0NSArray只有一个元素时,得到的是__NSSingleObjectArrayI。当NSArraycount > 1时, 得到 __NSArrayINSMutablearray 返回的都是__NSArrayMplaceHolderanotherPlaceHoldler的内存地址一样,说明是一个单例,该类内部只有一个isa指针,init后被新的实例换掉了。

CFArrayCoreFoundation中的,和Foundation中的NSArray相对应,他们是Toll-Free-Briaged,用的环形缓冲区实现的。

数组的数据结构
__NSArrayI
{
  NSInterger _userd; //数组的元素个数,调用[array count]时,返回的就是_userd的值。
  id_list[0]; //当做id_list来用,即一个存储id对象的buff.由于__NSArrayI的不可变,所以_list一旦分配,释放之前都不会再有移动删除操作了。
}

__NSSingleObjectArrayI
{
  id object; //因为只有在创建只包含一个对象的不可变数组时,才会得到__NSSingleObjectArrayI对象,所以其内部结构更加简单
}

__NSArrayM  
{ 
  NSUInterger _used; //当前对象数目 [nsmutablearray count]
  NSUInterger _offset; //对象数组的起始偏移
  int_size: 28; //已分配的_list大小,能存储的对象个数,不是字节数
  int_unused: 4;
  uint32_t _mutations;//修改标记,每次对__NSArrayM的修改操作都会使_mutations +1 "Collection <__NSArrayM:   0x1002076b0> was mutated while being enumerated" 这个异常就是通过对_mutations的识别来引发的。
  id *_list;//是个循环数组,并且在增删操作时会动态地重新分配以符合当前的存储需求
}
数组插入删除的原理

连续的内存空间, 在下标0处插入一个元素时, 移动其后面所有的元素,即memmove原理。

插入一个元素

同样的移除第一个元素,需要进行相同的动作

移除第一个元素

当数组非常大时,就有问题了。NSMutableArray使用环形缓冲区,这个数据结构相对简单,只是比常规数组/缓冲区复杂点。环形缓冲区的内容能在到达任意一段时绕向另一端。在删除的时候不会清除指针,如果我们在中间进行插入和删除,只会移动最少的一边元素。

删除元素

__NSArrayM_list是个循环数组,它由_offset标识,如果_list还没有构成循环,第一次就获得了全部元素,跟__NSArrayI一样。但是如果_list构成了循环,就需要两次,第一次获取_offset_list末端的元素,第二次获取存放在_list起始处的剩余元素。

插入元素
数组遍历的原理

forin速度最快的原因是遵从了NSFastEnumertation协议,基于快速枚举实现的。外层调用快速枚举方法批量获取元素,内层通过c数组取得一批元素中的每一个,并且在每次获取元素前,检查是否对数组对象进行了变更操作,如果是,则抛出异常。对于可变数组来说,最多只需要两次就可以获取全部数据。如果数组没有构成循环,第一次就获得了全部元素,跟不可变数组一样,如果数组构成了循环,那么就需要两次,第一次获取对象数组的起始偏移到循环数组末端的元素,第二次获取存放在循环数组起始处的剩余元素。如果我们每次遍历不需要知道下标,选择for in

for循环之所以慢一点,是每次都要调用objectAtIndex:,添加@autoreleasepool,可以提高效率。

NSEnumerationConcurrent+Block的方式耗时最大,因为它采用多线程,多线程的优势并不在遍历多快,而是它的回调在各个子线程。系统已经帮我们加了@autoreleasepool,其他的循环也可以通过@autoreleasepool来优化。

7、字节跳动:如何避免if else

比如处理网络请求时后端传来的接口错误编码:

- (void)needDoSomethingWithErrorCode:(NSInteger)errorCode{
    if (errorCode == 0) {
        //任务1
    }else if (errorCode == 2){
        //任务2
    }else if (errorCode == 3){
        //任务3
    }..
}
    

这种嵌套循环就像是盗梦空间一样扰人清梦。换一种写法:

- (void)needDoSomethingWithErrorCode:(NSInteger)errorCode{
    switch (errorCode) {
        case 0:
            //任务1
            break;
        case 1:
            //任务2
            break;
        case 2:
            break;
            //任务3
            ...
        default:
            break;
    }
}

这样写有问题吗?满足正常的开发是没有问题的。但是当需要加一个分支逻辑就必须得去if else结构中改代码,这样不利于程序扩展,同时也非常难维护,如果业务复杂到一定的程度这块代码可能没法去重构了。

这个问题应该是问我们一种架构思想。

- (void)needDoSomethingWithErrorCode:(NSInteger)errorCode{
    [self dealWithErrorCode:errorCode];
}

- (void)dealWithErrorCode:(NSInteger)errorCode{
    NSString *methodString = @"dealWithErrorCode";
    methodString = [methodString stringByAppendingPathExtension:[NSString stringWithFormat:@"%0.0ld",(long)errorCode]];
    SEL method = NSSelectorFromString(methodString);
    if ([self respondsToSelector:method]) {
        [self performSelector:method withObject:nil];
    }else{
        [self dealWithNoDefineErrorCode:errorCode];
    }
}

//处理错误类型0
- (void)dealWithErrorCode0{
    NSLog(@"//处理错误类型0");
}
//处理错误类型1
- (void)dealWithErrorCode1{
    NSLog(@"//处理错误类型1");
}
//未定义的方法类型
- (void)dealWithNoDefineErrorCode:(NSInteger)errorCode{
    NSLog(@"//未定义的方法类型");
}

这样的写法的好处就是之后可以直接新增新的方法,不需要去处理needDoSomethingWithErrorCode这个函数。
通过反射和简单的工厂模式,或者是策略模式去设计我们的代码,可以让我们避免使用一些臃肿的if else

8、百度:UIViewController 只alloc而没用到的时候,UIViewController 的view是否加载了?

ViewController的生命周期中各方法执行流程:init->loadView->viewDidLoad->viewWillAppear->viewDidAppear->viewWillDisAppear->viewDidDisappear->dealloc

UIViewControlleralloc而没用到的时候,UIViewControllerview还没有加载。因为:当allocinit一个Controller时这个Controller还没有创建viewControllerview是使用lazy init方式创建,就是说你调用的view属性的getter方法:[self view]。在getter方法里会判断view是否创建,如果没有创建那么会调用loadview来创建viewloadview完成后会继续调用viewdidload

init里不要出现创建view的代码,也不要调用self.view,在init里应该只有相关数据的初始化,而且这些数据都是比较关键的数据。

在init方法里面,设置背景颜色,会生效吗 会生效。为什么会?

@implementation RootViewController

- (instancetype)init
{
    self = [super init];
    if (self) {
        self.view.backgroundColor = [UIColor blueColor];
    }
    return self;
}

@end

会生效。

9、百度:直接用UILabel和自己用DrawRect画UILabel,哪个性能好?为什么?哪个占用的内存少?为什么?

如果你想在一个图层里面显示文字,完全可以借助图层代理直接将字符串使用CoreGraphics写入图层的内容(这就是UILabel的精髓)。如果越过寄宿于图层的视图,直接在图层上操作,那其实相当繁琐。你要为每一个显示文字的图层创建一个能像图层代理一样工作的类,还要判断哪个图层需要显示哪个字符串,更别提还要记录不同的字体,颜色等一系列乱七八糟的东西。

万幸的是这些都是不必要的,Core Animation提供了一个CALayer的子类CATextLayer ,它以图层的形式包含了UILabel几乎所有的绘制特性,并且额外提供了一些新的特性。同样,CATextLayer 也要比UILabel渲染得快得多。用UILabel 实现绘制会造成当有很多文字的时候就会有极大的性能压力,而CATextLayer使用了Core text,渲染得非常快。

10、百度:Images.xcassets和直接用图片有什么不一样?

实录三面

1、Bigo:谈对于组件化的理解和市面上常见的组件化方案
见我的文章:IOS的Git与CocoaPods,看这一篇文章就够了

实录四面

参考文献

iOS客户端防止发送重复点击发请求
iOS 金额转大写
操作系统面试题集
带着问题看源码----子线程AutoRelease对象何时释放
iOS UIView用户事件响应(exclusiveTouch,触摸响应,手势)
iOS 数组的实现原理
如何避免if else

上一篇下一篇

猜你喜欢

热点阅读