面试题

作者: 纳兰沫 | 来源:发表于2019-01-14 16:41 被阅读16次

    非作者原著 来自摘抄

    yahoouchen

    设计模式

    • MVC模式
    • MVVM模式
    • 单例模式: 通过static关键字 声明全局变量 在整个进程运行期间只会被赋值一次
    • 观察者模式: KVO是典型的通知模式 观察某个属性的状态 状态发生变化时通知观察者
    • 委托模式: 代理+协议的组合 实现1对1的反向值操作
    • 工厂模式: 通过一个类方法 批量的根据已有模板生产对象
      重写一个类的方式用分类比较好 因为用Category去重写类的方法 仅对本Category有效 不会影响到其他类与原有类的关系

    什么情况下使用weak关键字 相比assign有什么不同

    1.在ARC中 在有可能出现循环引用的时候 往往要通过让其中一端使用weak来解决
    2.自身已经对它进行一次强引用 没有必须再强引用一次 此时也会使用weak 自定义IBOutlet 控件属性一般也使用weak
    assign 可以用非 OC 对象,而 weak 必须用于 OC 对象。
    weak 表明该属性定义了一种“非拥有关系”。在属性所指的对象销毁时,属性值会自动清空(nil)。
    id 声明的对象具有运行时的特性 即可以指向任意类型的object-C对象

    ViewController 生命周期

    按照执行顺序排列:

    1. initWithCoder:通过nib文件初始化时触发。
    2. awakeFromNib:nib文件被加载的时候,会发生一个awakeFromNib的消息到nib文件中的每个对象。      
    3. loadView:开始加载视图控制器自带的view。
    4. viewDidLoad:视图控制器的view被加载完成。  
    5. viewWillAppear:视图控制器的view将要显示在window上。
    6. updateViewConstraints:视图控制器的view开始更新AutoLayout约束。
    7. viewWillLayoutSubviews:视图控制器的view将要更新内容视图的位置。
    8. viewDidLayoutSubviews:视图控制器的view已经更新视图的位置。
    9. viewDidAppear:视图控制器的view已经展示到window上。 
    10. viewWillDisappear:视图控制器的view将要从window上消失。
    11. viewDidDisappear:视图控制器的view已经从window上消失。
    

    OC中的反射机制

    1.class反射
      通过类名的字符串形式实例化对象
          Class class = NSClassFromString(@"student");
          Student *stu = [[class alloc] init];
      将类名变为字符串
          Class class = [Student class];
          NSString *className = NSStringFromClass(class);
    2.SEL的反射
      通过方法的字符串形式化实例化方法
          SEL selector = NSSelectorFromString(@"setName");
          [stu perfromSelector:selector withObject:@"Mike"];
      将方法变成字符串
          NSStringFromSelector(@selector*(setName:));
    
    松耦合,类与类之间不需要太多依赖  构建灵活
    不利于维护。使用反射模糊了程序内部实际发生的事情,隐藏了程序的逻辑。这种绕过源
    码的方式比直接代码更为复杂,增加了维护成本。
    
    @public 任何地方都能访问
    @protected 该类和子类中访问 是默认的
    @private 只能在本类中访问
    @package 本包内使用 跨报不可用
    

    什么是谓词

    谓词就是通过NSPredicate 给定的逻辑条件作为约束条件 完成对数据的筛选
    //定义谓词对象 谓词对象中包含了过滤条件
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"age<%d",30];
    //使用谓词条件过滤数组中的元素 过滤之后返回查询的结果
    NSArray *array = [persons filteredArrayUsingPredicate:predicate];
    

    BEGINSWITH:是否以指定字符串开头;
    ENDSWITH:是否以指定字符串结尾;
    CONTAINS:是否包含指定字符串;
    LIKE:是否匹配指定字符串模版;
    MATCHES:是否匹配指定的正则表达式;

    ANY、SOME:集合中任意一个元素满足条件,就返回YES。
    ALL:集合中所有的元素都满足条件,才返回YES。
    NONE:集合中没有任何元素满足条件就返回YES。
    IN:等价于SQL语句中的IN运算符。

    NSArray *array = @[@1, @2, @3, @4, @5, @6, @7];
    NSArray *filterArray = @[@1, @4, @8];
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"not (self in %@)", filterArray];
    NSArray *resultArray = [array filteredArrayUsingPredicate:predicate];
    //resultArray is @[@2, @3, @5, @6, @7];
    

    %K:用于动态传入属性名
    %@:用于动态设置属性值

    NSPredicate *pred = [NSPredicate predicateWithFormat:@"SELF CONTAINS $VALUE"];
    

    VALUE是一个可以动态变化的值,它其实最后是在字典中的一个key,所以可以根据你的需要写不同的值,但是必须有开头,随着程序改变$VALUE这个谓词表达式的比较条件就可以动态改变。

    NSArray *array = @[[ZLPersonModel personWithName:@"Jack" age:20 sex:ZLPersonSexMale],
                         [ZLPersonModel personWithName:@"Rose" age:22 sex:ZLPersonSexFamale],
                         [ZLPersonModel personWithName:@"Jackson" age:30 sex:ZLPersonSexMale],
                         [ZLPersonModel personWithName:@"Johnson" age:35 sex:ZLPersonSexMale]];
      //  定义一个property来存放属性名,定义一个value来存放值
      NSString *property = @"name";
      NSString *value = @"Jack";
      //  该谓词的作用是如果元素中property属性含有值value时就取出放入新的数组内,这里是name包含Jack
      NSPredicate *pred = [NSPredicate predicateWithFormat:@"%K CONTAINS %@", property, value];
      NSArray *newArray = [array filteredArrayUsingPredicate:pred];
      NSLog(@"newArray:%@", newArray);
       
      //  创建谓词,属性名改为age,要求这个age包含$VALUE字符串
      NSPredicate *predTemp = [NSPredicate predicateWithFormat:@"%K > $VALUE", @"age"];
      // 指定$SUBSTR的值为 25    这里注释中的$SUBSTR改为$VALUE
      NSPredicate *pred1 = [predTemp predicateWithSubstitutionVariables:@{@"VALUE" : @25}];
      NSArray *newArray1 = [array filteredArrayUsingPredicate:pred1];
      NSLog(@"newArray1:%@", newArray1);
       
      //  修改 $SUBSTR的值为32,  这里注释中的SUBSTR改为$VALUE
      NSPredicate *pred2 = [predTemp predicateWithSubstitutionVariables:@{@"VALUE" : @32}];
      NSArray *newArray2 = [array filteredArrayUsingPredicate:pred2];
      NSLog(@"newArray2:%@", newArray2);
    

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

    1.通过KVC获取
    2.通过runtime访问并修改私有属性

    下面的代码输出什么不太理解

    @implementation Son : Father
    - (id)init {
       if (self = [super init]) {
           NSLog(@"%@", NSStringFromClass([self class])); // Son
           NSLog(@"%@", NSStringFromClass([super class])); // Son
       }
       return self;
    }
    @end
    //解析
    self  是类的隐藏函数  指向当前调用方法的那个类的实例
    super 本质是一个编译器标示符  和self是指向的同一个消息接受者
    不同的是 super会告诉编译器 调用class这个方法时 要去父类的方法 而不是在本类里的 
    上面的例子不管调用[self class]还是[super class],接受消息的对象都是当前 Son *obj 这个对象。
    

    常用的控制台调试命令

    1). p 输出基本类型。是打印命令,需要指定类型。是print的简写
        p (int)[[[self view] subviews] count]
    2). po 打印对象,会调用对象description方法。是print-object的简写
        po [self view]
    3). expr 可以在调试时动态执行指定表达式,并将结果打印出来。常用于在调试过程中修改变量的值。
    4). bt:打印调用堆栈,是thread backtrace的简写,加all可打印所有thread的堆栈
    5). br l:是breakpoint list的简写
    

    Runtime 的实现机制是什么 怎么用 不太理解

    1). 使用时需要导入的头文件 <objc/message.h> <objc/runtime.h>
    2). Runtime 运行时机制,它是一套C语言库。
    3). 实际上我们编写的所有OC代码,最终都是转成了runtime库的东西。
        比如:
            类转成了 Runtime 库里面的结构体等数据类型,
            方法转成了 Runtime 库里面的C语言函数,
            平时调方法都是转成了 objc_msgSend 函数(所以说OC有个消息发送机制)
        // OC是动态语言,每个方法在运行时会被动态转为消息发送,即:objc_msgSend(receiver, selector)。
        // [stu show];  在objc动态编译时,会被转意为:objc_msgSend(stu, @selector(show));    
    4). 因此,可以说 Runtime 是OC的底层实现,是OC的幕后执行者。
    

    有了Runtime库,能做什么事情呢?
    Runtime库里面包含了跟类、成员变量、方法相关的API。
    比如:
    (1)获取类里面的所有成员变量。
    (2)为类动态添加成员变量。
    (3)动态改变类的方法实现。
    (4)为类动态添加新的方法等。
    因此,有了Runtime,想怎么改就怎么改。

    什么是 Method Swizzle(黑魔法),什么情况下会使用?不太理解

    1). 在没有一个类的实现源码的情况下,想改变其中一个方法的实现,除了继承它重写、和借助类别重名方法暴力抢先之外,还有更加灵活的方法 Method Swizzle。
    2). Method Swizzle 指的是改变一个已存在的选择器对应的实现的过程。OC中方法的调用能够在运行时通过改变,通过改变类的调度表中选择器到最终函数间的映射关系。
    3). 在OC中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是selector的名字。利用OC的动态特性,可以实现在运行时偷换selector对应的方法实现。
    4). 每个类都有一个方法列表,存放着selector的名字和方法实现的映射关系。IMP有点类似函数指针,指向具体的方法实现。
    5). 我们可以利用 method_exchangeImplementations 来交换2个方法中的IMP。
    6). 我们可以利用 class_replaceMethod 来修改类。
    7). 我们可以利用 method_setImplementation 来直接设置某个方法的IMP。
    8). 归根结底,都是偷换了selector的IMP。
    

    _objc_msgForward 函数是做什么的,直接调用它将会发生什么?

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

    TCP UDP

    TCP 传输控制协议 面向连接的 建立连接需要经历三次握手 是可靠的传输层协议
    UDP 用户数据协议 是面向无连接的 数据传输是不可靠的 它只管发 不管收不收得到
    TCP注重数据安全 UDP数据传输快点 但安全性一般

    通信底层原理

    OSI采用了分层的结构化技术  分七层
        物理层 数据链路层  网络层  传输层 会话层 表示层 应用层
    

    OC中创建线程的方法是什么?如果在主线程中执行代码,方法是什么?

    // 创建线程的方法
    - [NSThread detachNewThreadSelector:nil toTarget:nil withObject:nil]
    - [self performSelectorInBackground:nil withObject:nil];
    - [[NSThread alloc] initWithTarget:nil selector:nil object:nil];
    - dispatch_async(dispatch_get_global_queue(0, 0), ^{});
    - [[NSOperationQueue new] addOperation:nil];
    
    // 主线程中执行代码的方法
    - [self performSelectorOnMainThread:nil withObject:nil waitUntilDone:YES];
    - dispatch_async(dispatch_get_main_queue(), ^{});
    - [[NSOperationQueue mainQueue] addOperation:nil];
    

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

    - (UIImage *)circleImage {
        // NO代表透明
        UIGraphicsBeginImageContextWithOptions(self.size, NO, 0.0);
        // 获得上下文
        CGContextRef ctx = UIGraphicsGetCurrentContext();
        // 添加一个圆
        CGRect rect = CGRectMake(0, 0, self.size.width, self.size.height);
        CGContextAddEllipseInRect(ctx, rect);
        // 裁剪
        CGContextClip(ctx);
        // 将图片画上去
        [self drawInRect:rect];
        UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
        // 关闭上下文
        UIGraphicsEndImageContext();
        return image;
    }
    

    贝塞尔曲线切割

    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];
    

    简单介绍下APNS发送系统消息的机制

    APNS优势  杜绝了类似安卓那种为了接受通知不停在后台唤醒程序保持长连接的
    行为 由ios系统和APNS进行长连接替代
    APNS原理  
            - 应用在通知中心注册 由ios系统向APNS请求返回设备令牌
              (device Token)
            - 应用程序接受到设备令牌并发送给自己的后台服务器
            - 服务器把要推送的内容和设备发送给APNS
            - APNS根据设备令牌找到设备 再由ios根据APPID把推送内容展示
    

    第三方框架

    AFNetworking 底层原理分析

    AFNetworking 主要是对NSURLSession和NSURLConnection的封装
    - `AFHTTPRequestOperationManager` 内部封装的是NSURLConnection 
    负责发送网络请求(3.0废弃)
    - `AFHTTPSessionManager` 内部封装的是NSURLSession 负责发送网络请求 
    使用最多的一个类
    - `AFNetworkReachabilityManager`  实时监测网络状态的工具类 当前的网
    络环境发生改变之后  可以检测到
    - `AFSecurityPolicy` 网络安全的工具类 主要针对HTTPS服务
    - `AFURLRequestSerialization`  序列化工具类 基类 上传的数据转换成
    JSON格式
    - `AFURLResponseSerialization` 反序列化工具类 基类
    - `AFJSONResponseSerializer`   JSON解析器 默认解析器
    - `AFHTTPResponseSerializer`    万能解析器   JSON和XML之外的数据
    类型 直接返回二进制数据 对服务器返回的数据不做任何处理
    - `AFXMLParserResponseSerializer`  XML解析器
    

    描述下SDWebImage里面给UIImageView加载图片的逻辑

    SDWebImage 中为UIImageView提供了一个分类 UIImageView+WebCache 这个
    分类中有一个最常用的接口 sd_setImageWithURL:placeholderImage: 会在
    真实图片出现前先显示站位图 当真实图片被加载出来后 再替换站位图
    加载图片的过程大致如下
        - 首先会在SDWebImageCache 中寻找图片是否有对应的缓存 它会以url作
    为数据的索引先在内存中寻找是否有对应的缓存
        -  如果缓存未找到就会利用通过MD5处理过的key来继续在磁盘中查询对应
    的数据 如果找到了 就会把磁盘中的数据加载到内存中  并将图片显示出来
        - 如果在内存和磁盘缓存中都没有找到 就会向远程服务器发送请求 开
    始下载图片
        - 下载后的图片会加入到缓存中  并写入磁盘中
        - 整个获取图片的过程都是在子线程中执行 获取到图片后回到主线程把图
    片显示出来
    SDWebImage原理
    调用类别的方法
        - 从内存(字典)中找图片 (当这个图片在本次使用程序的过程中已经被加
    载过)  找到直接使用
        - 从沙盒中找 (当这个图片在之前使用程序的过程中被加载过)  找到使用
     缓存到内存中
        - 从网络上获取 使用 缓存到内存 缓存到沙盒
    

    排序算法

    1. 选择排序将已排序部分定义在左端,然后选择未排序部分的最小元素和未排
    序部分的第一个元素交换。
    2. 冒泡排序将已排序部分定义在右端,在遍历未排序部分的过程执行交换,
    将最大元素交换到最右端。
    3. 插入排序将已排序部分定义在左端,将未排序部分元的第一个元素插入到已
    排序部分合适的位置。
    

    选择排序

    /** 
     *  【选择排序】:最值出现在起始端
     *  
     *  第1趟:在n个数中找到最小(大)数与第一个数交换位置
     *  第2趟:在剩下n-1个数中找到最小(大)数与第二个数交换位置
     *  重复这样的操作...依次与第三个、第四个...数交换位置
     *  第n-1趟,最终可实现数据的升序(降序)排列。
     *
     */
    void selectSort(int *arr, int length) {
        for (int i = 0; i < length - 1; i++) { //趟数
            for (int j = i + 1; j < length; j++) { //比较次数
                if (arr[i] > arr[j]) {
                    int temp = arr[i];
                    arr[i] = arr[j];
                    arr[j] = temp;
                }
            }
        }
    }
    

    冒泡排序

    /** 
     *  【冒泡排序】:相邻元素两两比较,比较完一趟,最值出现在末尾
     *  第1趟:依次比较相邻的两个数,不断交换(小数放前,大数放后)逐个
    推进,最值最后出现在第n个元素位置
     *  第2趟:依次比较相邻的两个数,不断交换(小数放前,大数放后)逐个
    推进,最值最后出现在第n-1个元素位置
     *   ……   ……
     *  第n-1趟:依次比较相邻的两个数,不断交换(小数放前,大数放后)逐个
    推进,最值最后出现在第2个元素位置 
     */
    void bublleSort(int *arr, int length) {
        for(int i = 0; i < length - 1; i++) { //趟数
            for(int j = 0; j < length - i - 1; j++) { //比较次数
                if(arr[j] > arr[j+1]) {
                    int temp = arr[j];
                    arr[j] = arr[j+1];
                    arr[j+1] = temp;
                }
            } 
        }
    }
    

    折半查找

    /**
     *  折半查找:优化查找时间(不用遍历全部数据)
     *
     *  折半查找的原理:
     *   1> 数组必须是有序的
     *   2> 必须已知min和max(知道范围)
     *   3> 动态计算mid的值,取出mid对应的值进行比较
     *   4> 如果mid对应的值大于要查找的值,那么max要变小为mid-1
     *   5> 如果mid对应的值小于要查找的值,那么min要变大为mid+1
     *
     */ 
    
    // 已知一个有序数组, 和一个key, 要求从数组中找到key对应的索引位置 
    int findKey(int *arr, int length, int key) {
        int min = 0, max = length - 1, mid;
        while (min <= max) {
            mid = (min + max) / 2; //计算中间值
            if (key > arr[mid]) {
                min = mid + 1;
            } else if (key < arr[mid]) {
                max = mid - 1;
            } else {
                return mid;
            }
        }
        return -1;
    }
    

    编码格式

    • 在OC中 enum建议使用NS_ENUM和NS_OPTIONS宏来定义枚举类型
    //定义一个枚举(比较严密)
    typedef NS_ENUM(NSInteger, BRUserGender) {
        BRUserGenderUnknown,    // 未知
        BRUserGenderMale,       // 男性
        BRUserGenderFemale,     // 女性
        BRUserGenderNeuter      // 无性
    };
    
    @interface BRUser : NSObject<NSCopying>
    
    @property (nonatomic, readonly, copy) NSString *name;
    @property (nonatomic, readonly, assign) NSUInteger age;
    @property (nonatomic, readonly, assign) BRUserGender gender;
    
    - (instancetype)initWithName:(NSString *)name age:
    (NSUInteger)age gender:(BRUserGender)gender;
    
    @end
    
    //说明:
    //既然该类中已经有一个“初始化方法” ,用于设置 name、age 和 gender 的
    //初始值: 那么在设计对应 @property 时就应该尽量使用不可变的对象:其三
    //个属性都应该设为“只读”。用初始化方法设置好属性值之后,就不能再改变了。
    //属性的参数应该按照下面的顺序排列: (原子性,读写,内存管理)
    
    • 避免使用C语言中的基本数据类型 建议使用Foundation数据类型
    • UITableView的优化
    - 正确的复用cell
    - 设计统一格式的cell
    - 提前计算并缓存好高度(布局) 因为heightForRowAtIndexPath:是调用
    最频繁的方法
    - 异步绘制 遇到负责界面 遇到性能瓶颈
    - 滑动时按需加载 
    - 减少子视图的层级关系
    - 进来使所有的视图不透明化以及做切圆操作
    - 不要动态的add 或者remove 子控件  最好在初始化的时候就添加
    通过hidden来控制是否显示
    - 使用调试工具分析问题
    
    • 如何让计时器调用一个类方法
    计时器只能调用实例方法 但是可以在这个实例方法里面调用静态方法
    使用计时器需要注意 计时器一定要加入RunLoop中 并且选好model才能运行
    scheduledTimerWithTimeInterval方法创建一个计时器并加入到RunLoop中
    所以可以直接使用
    如果计时器的repeats选择YES说明这个计时器会重复执行 一定要在合适的时机
    调用计时器的invalid 不能再dealloc中调用 因为一旦设置repeats为YES
    计时器会强持有self 导致dealloc永远不会调用 这个类就永远无法被释放
    比如可以在viewDidDisappear中调用 这样当类需要被回收的时候就可以正常
    进入dealloc中了
    [NSTimer scheduledTimerWithTimeInterval: 1 target: self 
    target:self selector:@selector(timerMethod) userInfo:nil 
    repeats:YES];
    -(void)timerMethod
    {
    //调用类方法
        [[self class] staticMethod];
    }
    
    -(void)invalid
    {
        [timer invalid];
        timer = nil;
    }
    
    • 如何重写类方法
    1、在子类中实现一个同基类名字一样的静态方法
    2、在调用的时候不要使用类名调用,而是使用[self class]的方式调用。原理
    用类名调用是早绑定,在编译期绑定,用[self class]是晚绑定,在运行
    时决定调用哪个方法。
    
    • id和NSObject *的区别
    id 是一个objc_object  结构体指针 定义是typedef struct 
    objc_object *id
    id 可以理解为指向对象的指针 所有OC的对象 id都可以指向 编译器不会
    做类型检查 id 调用任何存在的方法都不会在编译阶段报错 当然如果这个id
    指向的对象没有这个方法 该崩溃还是会崩溃的
    NSObject * 指向的必须是NSObject的子类 调用的也只能是NSObject里面的
    方法否则就要做强制类型转换
    不是所有的OC对象都是NSObject的子类,还有一些继承自NSProxy。
    NSObject *可指向的类型是id的子集。
    
    • 你用过NSOperationQueue么?如果用过或者了解的话,你为什么要使用NSOperationQueue,实现了什么?请描述它和G.C.D的区别和类似的地方(提示:可以从两者的实现机制和适用范围来描述)
    使用NSOperationQueue用来管理子类化的NSOperation对象,控制
    其线程并发数目。GCD和NSOperation都可以实现对线程的管理,区别
    是 NSOperation和NSOperationQueue是多线程的面向对象抽象。项目中
    使用NSOperation的优点是NSOperation是对线程的高度抽象,在项目中
    使用它,会使项目的程序结构更好,子类化NSOperation的设计思路,
    是具有面向对象的优点(复用、封装),使得实现是多线程支持,而接
    口简单,建议在复杂项目中使用。
    项目中使用GCD的优点是GCD本身非常简单、易用,对于不复杂的多线
    程操作,会节省代码量,而Block参数的使用,会是代码更为易读,建议
    在简单项目中使用。
    
    • 对于Objective-C,你认为它最大的优点和最大的不足是什么?对于不足之处,现在有没有可用的方法绕过这些不足来实现需求。如果可以的话,你有没有考虑或者实践过重新实现OC的一些功能,如果有,具体会如何做?
    最大的优点是它的运行时特性,不足是没有命名空间,对于命名冲
     突,可以使用长命名法或特殊前缀解决,如果是引入的第三方库之间的
    命名冲突,可以使用link命令及flag解决冲突
    
    • 你实现过一个框架或者库以供别人使用么?如果有,请谈一谈构建框架或者库时候的经验;如果没有,请设想和设计框架的public的API,并指出大概需要如何做、需要注意一些什么方面,来使别人容易地使用你的框架
    抽象和封装,方便使用。首先是对问题有充分的了解,比如构建一
    个文件解压压缩框架,从使用者的角度出发,只需关注发送给框架一个
    解压请求,框架完成复杂文件的解压操作,并且在适当的时候通知给使用者,
    如解压完成、解压出错等。在框架内部去构建对象的关系,
    通过抽象让其更为健壮、便于更改。其次是API的说明文档
    

    相关文章

      网友评论

          本文标题:面试题

          本文链接:https://www.haomeiwen.com/subject/notddqtx.html