美文网首页
iOS面试题积累总结

iOS面试题积累总结

作者: 安心做个笨男孩 | 来源:发表于2020-12-03 13:42 被阅读0次

    iOS基础题

    1. 分类和扩展有什么区别?可以分别用来做什么?分类有哪些局限性?分类的结构体里面有哪些成员?

    • 区别
    * 分类:
        在程序运行过程中,通过runtime动态将分类的方法合并到类对象、元类对象中。
        分类中属性,只能生成getter/setter方法的声明,不会生成getter/setter方法的实现
        分类可以为类添加方法的实现
        不能添加实例变量(特殊的成员变量---> 成员变量 = 实例变量 + 基本数据类型变量)
    
    * 类扩展:
        给类扩充属性,方法声明,成员变量,在编译的时候这些就合并到类信息中。
        类扩展都会
        类扩展只能声明方法,不能实现
        能添加实例变量
    
    • 作用
    分类:为类添加方法,协议等
    类扩展:可以将一些本来是公开声明的属性,方法私有化。(写在.h中,写到.m去了)
    
    
    • 分类的局限性
    不能添加实例变量
    添加的属性不能生成getter/setter方法
    多个分类,如果方法重名的话,就调用最后编译的分类的方法
    
    • 分类的结构体
    struct _category_t {
        const char *name; 类名
        struct _class_t *cls;
        const struct _method_list_t *instance_methods; 实例方法列表
        const struct _method_list_t *class_methods; 类方法列表
        const struct _protocol_list_t *protocols; 协议列表
        const struct _prop_list_t *properties; 属性列表
    };
    

    2、Category中load方法什么时候调用,load方法能继承吗,load与initialize区别

    • 调用
    程序的运行过程中只调用一次。在runtime加载类或分类的时候调用。
    load:根据函数地址直接调用,在runtime加载类或分类的时候调,只调用一次
    initialize:通过objc_msgSend调用,第一次接收消息时候调,每一个类只会initialize一次,(但是父类initialize可能会调用多次。)
    
    • 继承
    1、类的load方法不会被分类的load方法覆盖。
    2、调用子类的load方法之前要先调用父类的load方法
    
    
    • initialize
    initialize第一次接收消息时候调用,消息机制msg_send调用
    initialize先初始化父类再初始化子类,如果子类没有initialize,则调用initialize方法。所以父类initialize可能会调用多次。
    如果分类实现initialize方法,会覆盖掉类的initialize方法
    
    

    3、讲一下atomic的实现机制;为什么不能保证绝对的线程安全(最好可以结合场景来说)?

    • 机制
    atomic对property进行修饰,原子性,相比与noatomic具备线程安全(但是否真的安全还要打问号),在读写操作时进行加锁解锁操作,相比之下安全
    但是带来的问题是处理速度变慢,消耗性能,一个程序大量使用get/set方法。
    
    atomic只对getter/setter方法进行加锁,只能保证代码进入setter/getter时候安全,不能保证多线程访问时候安全,一旦出了 getter 和 setter 方法,其线程安全就要由程序员自己来把握,所以atomic属性和线程安全并没有必然联系。
    
    • 场景
    线程打印:同在主线程中进行异步打印,发现打印结果出现AB数字相同情况
    @property (atomic, assign) NSInteger number;
    
    - (void)atomicTest {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            for (int i = 0; i < 10000; i ++) {
                self.number = self.number + 1;
                NSLog(@"A-self.number is %ld",self.number);
            }
        });
    
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            for (int i = 0; i < 10000; i ++) {
                self.number = self.number + 1;
                NSLog(@"B-self.number is %ld",self.number);
            }
        });
    }
    

    4. 关联对象有什么应用,系统如何管理关联对象?其被释放的时候需要手动将所有的关联对象的指针置空么?

    • 应用
    给分类添加实例变量
    
    分类中重写实例变量的set/get方法
    setter方法中实现:objc_setAssociatedObject(self, @selector(xxx), xxx, OBJC_ASSOCIATION_COPY_NONATOMIC);
    getter方法中实现:objc_getAssociatedObject(self, @selector(xxx))
    
    • 系统管理
    1、分析objc_setAssociatedObject(id object, const void * key, id value, objc_AssociationPolicy policy);
    AssociationsManager 内部有 AssociationsHashMap(key,value对应)
    AssociationsHashMap 中 object(参数object) = key,value = ObjectAssociationMap
    ObjectAssociationMap 中 void *(参数key) = key, value = ObjectAssocitaion
    ObjectAssocitaion 存储得  policy(参数policy) + value(参数value)
    
    2、_object_remove_assocations
    移除所有的关联对象,这个方法直接将AssociationsHashMap中的key置空。
    

    5、KVO的底层实现?如何取消系统默认的KVO并手动触发(给KVO的触发设定条件:改变的值符合某个条件时再触发KVO)?

    • KVO的底层实现
    xxx类的isa指向全新的类NSKVONotifying_xxx(runtime动态生成)
    
    
    修改对象属性时候,调用_NSSetObjectValueAndNotify方法,内部实现逻辑
    [self willChangeValueForKey:@"age"]
    [super setAge:age]
    [self didChangeValueForKey:@"age"]
    
    内部触发
    - (void)didChangeValueForKey:(NSString *)key
    {
        [observer observeValueForKeyPath:key ofObject:self change:nil context:nil]
    }
    
    
    • 取消系统默认的KVO并手动触发
    从willChangeValueForKey 和 didChangeValueForKey进行拦截
    - (void)setAge:(int)age {
        if (age > 18) {
            [self willChangeValueForKey:@"age"]
            _age = age
            [self didChangeValueForKey:@"age"]
        } else {
            _age = age
        }
    }
    手动调用willChangeValueForKey和didChangeValueForKey即可自动触发kvo
    
    

    6. 为什么在block外部使用__weak修饰的同时需要在内部使用__strong修饰?

    • __weak修饰
    weak的修饰符来修饰一个变量,防止其在block中被循环引用。
    用__weak修饰之后block不会对该对象进行retain,只是持有了weak指针,在block执行之前或执行的过程时,随时都有可能被释放。
    
    • 内部用__strong修饰
    因为在block中随时会被释放,可能会导致block还没执行结束,而对象已经被释放掉了,从而产生意想不到的错误。
    在内部通过__strong修饰,那么block内部的对象在block结束前不会被释放,而执行结束后,会自动释放掉。
    
    
    • 举例
    SampleObject* sample = [[SampleObject alloc]init];
     
    self->sample= sample;
     
    __weak SampleObject* weaksample = self->sample;
     
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
     
        NSIntegercount =0;
     
        __strong SampleObject* strongsample = weaksample;
     
        while(count<10) {
     
            count++;
     
            NSLog(@"aaa %@",weaksample);
     
            sleep(1);
     
        }
     
    });
     
    // 3秒后外部将self释放。
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3*NSEC_PER_SEC)),dispatch_get_main_queue(), ^{
     
        self->sample=nil;
    }
     
    例子中可以发现,如果没有__strong 修饰,block在执行过程中就会释放,不会出现3秒后的答应
    有__strong 修饰,self被释放,但block内部的self还没有被释放,能继续3秒后的执行。
    从而对该变量在block中的使用起到了保护作用。
    

    7. GCD简介

    • 同步+并发 : 未开辟子线程,按顺序执行。
    - (void)test2 {
        NSLog(@"currentThread----%@",[NSThread currentThread]);
        NSLog(@"syncConcurrent---begin");
        dispatch_queue_t queue = dispatch_queue_create("syncConcurrent", DISPATCH_QUEUE_CONCURRENT);
        
        dispatch_sync(queue, ^{
            [NSThread sleepForTimeInterval:2];
            NSLog(@"1-------%@",[NSThread currentThread]);
        });
        dispatch_sync(queue, ^{
            [NSThread sleepForTimeInterval:2];
            NSLog(@"2-------%@",[NSThread currentThread]);
        });
        dispatch_sync(queue, ^{
            [NSThread sleepForTimeInterval:2];
            NSLog(@"3-------%@",[NSThread currentThread]);
        });
        NSLog(@"syncConcurrent---end");
    }
    
    • 异步+并发: 无序,可开辟子线程。
    - (void)test3 {
        NSLog(@"currentThread----%@",[NSThread currentThread]);
        NSLog(@"asyncConcurrent---begin");
        
        dispatch_queue_t queue = dispatch_queue_create("asyncConcurrent", DISPATCH_QUEUE_CONCURRENT);
        
        dispatch_async(queue, ^{
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
        });
        dispatch_async(queue, ^{
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
        });
        dispatch_async(queue, ^{
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
        });
        NSLog(@"asyncConcurrent---end");
    }
    
    • 异步+串行: 可开辟子线程,有序执行。
    - (void)asyncSerial {
        NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
        NSLog(@"asyncSerial---begin");
        
        dispatch_queue_t queue = dispatch_queue_create("asyncSerial", DISPATCH_QUEUE_SERIAL);
        
        dispatch_async(queue, ^{
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
        });
        dispatch_async(queue, ^{
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
        });
        dispatch_async(queue, ^{
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
        });
        
        NSLog(@"asyncSerial---end");
    }
    
    • 同步+主队列 : 崩溃

    崩溃原因:主队列是一个特殊的同步执行串行队列,在main_queuez中,它需要顺序执行,执行结束后,这个队列任务才算完成。中间如果插上一个同步队列,这个时候主队列要执行完,同步队列也要先执行,卡住了。死锁了。
    比喻:老贾要先吃饭后给钱,老板要先给钱才给做饭,都很固执,都不干,卡住了,老板没赚到钱,老贾没吃到饭。

    - (void)syncMain {
        NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
        NSLog(@"syncMain---begin");
            
        dispatch_queue_t queue = dispatch_get_main_queue();
            
        dispatch_sync(queue, ^{
            // 追加任务 1
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
        });
            
        dispatch_sync(queue, ^{
            // 追加任务 2
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
        });
            
        dispatch_sync(queue, ^{
            // 追加任务 3
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
        });
            
        NSLog(@"syncMain---end");
    
    }
    
    • 异步+主: 先把主线程任务执行完成,未开辟子线程,顺序执行
    - (void)asyncMain {
        NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
        NSLog(@"asyncMain---begin");
            
        dispatch_queue_t queue = dispatch_get_main_queue();
            
        dispatch_async(queue, ^{
            // 追加任务 1
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
        });
            
        dispatch_async(queue, ^{
            // 追加任务 2
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
        });
            
        dispatch_async(queue, ^{
            // 追加任务 3
            [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
            NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
        });
            
        NSLog(@"asyncMain---end");
    }
    
    • 常见其他GCD高级函数
    dispatch_once------>单例模式
    栅栏函数:dispatch_barrier_async------->控制队列执行顺序
    延迟操作:dispatch_after------>延迟执行线程中的方法,一些延迟跳转等功能
    队列组:dispatch_group_t------>控制执行队列执行顺序
    

    8、KVC的底层实现?

    当一个对象调用setValue方法时,方法内部会做以下操作:
    1. 检查是否存在相应的key的set方法,如果存在,就调用set方法。
    2. 如果set方法不存在,就会查找与key相同名称并且带下划线的成员变量,如果有,则直接给成员变量属性赋值。
    3. 如果没有找到_key,就会查找相同名称的属性key,如果有就直接赋值。
    4. 如果还没有找到,则调用valueForUndefinedKey:和setValue:forUndefinedKey:方法。
    这些方法的默认实现都是抛出异常,我们可以根据需要重写它们。
    

    9、属性关键字 readwrite,readonly,assign,retain,copy,nonatomic 各是什么作用,在那种情况下用?

    1. readwrite 是可读可写特性。需要生成getter方法和setter方法。
    2. readonly 是只读特性。只会生成getter方法,不会生成setter方法,不希望属性在类外改变。
    3. assign 是赋值特性。setter方法将传入参数赋值给实例变量;仅设置变量时,assign用于基本数据类型。
    4. retain(MRC)/strong(ARC) 表示持有特性。setter方法将传入参数先保留,再赋值,传入参数的retaincount会+1。
    5. copy 表示拷贝特性。setter方法将传入对象复制一份,需要完全一份新的变量时。
        - 深copy和浅copy:    
        No1:可变对象的copy和mutableCopy方法都是深拷贝(区别完全深拷贝与单层深拷贝)。
        No2:不可变对象的copy方法是浅拷贝,mutableCopy方法是深拷贝。
        No3:copy方法返回的对象都是不可变对象
    6. nonatomic 非原子操作。决定编译器生成的setter和getter方法是否是原子操作,atomic表示多线程安全,一般使用nonatomic,效率高。
    

    10、被weak修饰的对象在被释放的时候会发生什么?是如何实现的?知道sideTable么?里面的结构可以画出来么?

    • 发生
    weak修饰的对象,被销毁了,会把weak指针自动置空,所以打印对象会发现对象为nil。相对安全。
    
    unsafe_unretained,同样没有强应用,但是对象被销毁再打印对象发现直接坏指针错误。不安全
    
    
    • week指针的底层实现
    将weak修饰的额对象放在哈希表中,去weak修饰的对象的内存地址作为key,weak指针为值。
    
    当需要清空的时候,找到sideTable中的弱引用表weak_table,传递weak_table和对象的内存地址。通过地址找到哈希表中所指对象的weak指针,然后弱引用表存储的所有weak指针置为nil。
    
    
    
    • sideTable
    struct SideTable {
        spinlock_t slock;//操作SideTable时用到的锁
        RefcountMap refcnts;//引用计数器的值---散列表
        weak_table_t weak_table;//存放weak指针的哈希表
    };
    

    iOS实战题

    1. App无痕埋点的思路了解么?你认为理想的无痕埋点系统应该具备哪些特点?(知道多少说多少))

    • 思路
    1、利用runtime,方法替换方法(class_getInstanceMethod)进行埋点
    
    
    #import "SMHook.h"
    #import <objc/runtime.h>
    
    @implementation SMHook
    
    + (void)hookClass:(Class)classObject fromSelector:(SEL)fromSelector toSelector:(SEL)toSelector {
        Class class = classObject;
        // 得到被替换类的实例方法
        Method fromMethod = class_getInstanceMethod(class, fromSelector);
        // 得到替换类的实例方法
        Method toMethod = class_getInstanceMethod(class, toSelector);
        
        // class_addMethod 返回成功表示被替换的方法没实现,然后会通过 class_addMethod 方法先实现;返回失败则表示被替换方法已存在,可以直接进行 IMP 指针交换 
        if(class_addMethod(class, fromSelector, method_getImplementation(toMethod), method_getTypeEncoding(toMethod))) {
          // 进行方法的替换
            class_replaceMethod(class, toSelector, method_getImplementation(fromMethod), method_getTypeEncoding(fromMethod));
        } else {
          // 交换 IMP 指针
            method_exchangeImplementations(fromMethod, toMethod);
        }
    
    }
    
    @end
    
    2、在load方法中,进行方法替换,一般是给UIViewController添加一个分类,替换viewWillAppear方法。可以在替换的方法中[self class]拿到控制器名称。后台管理系统通过特定的控制器名称拿到改界面访问次数等信息。
    (这种方法的缺陷是无法传递某页面特定的参数。有一定局限性,需要后台辅助。)
    
    3、在按钮点击时,一定会调用sendAction:to:forEvent:,可以在load方法中hook sendAction:to:forEvent:方法,可以拿到
    NSString *actionString = NSStringFromSelector(action);
    NSString *targetName = NSStringFromClass([target class]);
    (同样的,局限性是无法针对点击后的某些必要参数进行传递,例如点击商品,部分埋点需要传递商品id这些精确参数没法传递给服务器)。
    
    
    
    • 特点
    不侵入业务代码
    统计尽可能多的事件
    自动生成唯一标识
    要能统计到控件在但不同状态意义不同的情况
    需要某些机制能够提供业务数据
    在合适的时机去上传数据
    

    2. 你知道有哪些情况会导致app卡顿,分别可以用什么方法来避免?(知道多少说多少))

    * 主线程中进化IO或其他耗时操作,解决:把耗时操作放到子线程中操作
    * GCD并发队列短时间内创建大量任务,解决:使用线程池
    * 文本计算,解决:把计算放在子线程中避免阻塞主线程
    * 大量图像的绘制,解决:在子线程中对图片进行解码之后再展示
    * 高清图片的展示,解法:可在子线程中进行下采样处理之后再展示
    * collectionviewcell中嵌套collectionview,刷新collectionview出现卡顿。
    原因:UICollecitonView reload时候,是整个collecitonView刷新。
    解决:在setModel方法中进行判断,一般滑动过程中数据不在改变,setModel中判断是否已经进行model赋值,再考虑是否需要次次刷新子collectionview。(尽量少用这种嵌套)
    

    3、AppDelegate如何瘦身?

    • AppDelegate臃肿的主要原因
    AppDelegate作为项目的入口,需要做到:初始化根控制器以及首屏渲染,管理推送,初始化第三方SDK。
    
    • 瘦身方案
    创建AppDelegate分类,例如推送一个分类,初始化第三方SDK(分享)一个分类。便于维护。
    

    4、你知道有哪些情况会导致app崩溃,分别可以用什么方法拦截并化解?(知道多少说多少))

    • unrecognized selector sent to instance 方法找不到错误
    1、问题出现原因:消息转发机制objc_msgSend
    
    消息发送:
    receiver是否为空--(receiver为空)-->
    在类的缓存中查找(如果缓存中没有类的class_rw_t 方法列表查找)--(类中没有)-->
    在父类的缓存中查找(如果缓存中没有父类的class_rw_t 方法列表查找)--(都没有)-->
    进入动态方法解析
    
    动态方法解析:
    主要方法:resolveInstanceMethod(对象方法) / resolveClassMethod(类方法)
    
    
    消息转发
    当未实现动态方法解析,会到消息转发,
    forwardingTargetForSelector:--(nil)-->
    methodSignatureForSelector:--(nil)-->
    doesNotRecognizeSelector:报unrecognized selector sent to instance的错误。
    
    forwardingTargetForSelector:--(不为nil)-->objc_msgSend(返回值,SEL)
    methodSignatureForSelector:--(不为nil)-->调用forwardingInvocation:方法
    
    
    2、解决
    在动态方法解析过程中hook --- resolveInstanceMethod(对象方法) / resolveClassMethod(类方法),
    因为forwardInvocation会被调用多次,开销很大不适合hook。
    
    简要步骤:
    * 创建一个NSObject分类,
    * 在分类的load方法中方法替换forwordingTagetForSelector:.
    * 新的NewForwordingTagetForSelector:中,写防止崩溃的代码(判断方法是否存在,不存在则通过runtime进行添加class_addMethod),以及收集崩溃日志。
    
    
    • kvo crash
    1、崩溃原因
    ---> 添加了observer,但是没有实现observeValueForKeyPath: 方法
    ---> 重复removeObserver或者重复addObserver
    
    2、解决
    利用runtime方法替换
    方案一:@try-catch 替换的removerObserver方法。
    方案二:利用observationInfo私有属性"_observances",这个属性中存储有属性的监听者,通知者,还有监听的keyPath,等等KVO相关的属性。
    
    // 进行检索获取Key
    
    - (BOOL)observerKeyPath:(NSString *)key
    {
        id info = self.observationInfo;
        // 获取observationInfo中私有属性_observances
        NSArray *array = [info valueForKey:@"_observances"];
        for (id objc in array) {
            id Properties = [objc valueForKeyPath:@"_property"];
            NSString *keyPath = [Properties valueForKeyPath:@"_keyPath"];
            if ([key isEqualToString:keyPath]) {
                return YES;
            }
        }
        return NO;
    }
    
    
    

    iOS 技巧篇

    1、xcode单元测试

    • 作用
    快速测试某功能
    比如:
    测试分享功能,未使用单元测试,需要将程序run起来,然后点到指定位置测试功能或者新修改的代码。
    使用单元测试,不需要将程序run,而且可以针对性方法进行测试,精确到你修改的某方法。
    
    代码之间的耦合度很小,那么可以将其分解成多个小部件,从而更易于测试。
    
    • 使用方法
    1、创建单元测试文件。
    2、介绍单元测试文件中方法特性
    
    /**
     * 用于在测试前设置好测试的方法,在测试方法调用之前调用
     */
    - (void)setUp {
        // Put setup code here. This method is called before the invocation of each test method in the class.
        [super setUp];
        self.viewController = [[ViewController alloc]init];//初始化
    }
    
    /**
     * 用于在测试后将设置好的要测试的方法拆卸掉,释放资源
     */
    - (void)tearDown {
        // Put teardown code here. This method is called after the invocation of each test method in the class.
        self.viewController = nil;//释放
        
        [super tearDown];
    }
    
    /**
     * 测试示例,一定要以test开头
     * 比如,你可以创建, - (void)testMyProject{}
     */
    - (void)testMyBoolFuc {
        BOOL result = [self.viewController getBoolValue];
        XCTAssertEqual(result, NO,@"测试没通过");
    }
    
    /**
     * 性能测试示例
     */
    - (void)testPerformanceExample {
        // This is an example of a performance test case.
        [self measureBlock:^{
            // Put the code you want to measure the time of here.
            [self.viewController getBoolValue];
        }];
    }
    
    

    2、iOS中常用的几种设计模式

    • 适配器模式
    1、简述
    ---- 适配器模式(有时候也称包装样式或者包装)将一个类的接口适配成用户所期待的。一个适配允许通常因为接口不兼容而不能在一起工作的类工作在一起,做法是将类自己的接口包裹在一个已存在的类中。(巨枯燥看不懂)
    
    举例子
    ---- 手机要用usb数据线充电,但是家里的插座啊都是两个控,三个控这种,咋办。这时候我们就用了转换器(这个转换器就是适配器)。类似这种生活情况,放在编程当中就是适配器模式了(包装了下接口)
    
    2、实现
    * 创建一个文件PlayerAdapter,在这文件内部将原来的接口内部实现重写。
    * 外部在调用的时候,就直接调用PlayerAdapter文件的方法。
    
    • MVC模式:Model View Control,把模型 视图 控制器 层进行解耦合编写。

    • MVVM模式:Model View ViewModel 把模型 视图 业务逻辑 层进行解耦和编写。

    • 单例模式:通过static关键词,声明全局变量。在整个进程运行期间只会被赋值一次。

    // 创建静态对象 防止外部访问
    static Tools *_instance;
    +(instancetype)allocWithZone:(struct _NSZone *)zone
    {
        // 也可以使用一次性代码
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            if (_instance == nil) {
                _instance = [super allocWithZone:zone];
            }
        });
        return _instance;
    }
    // 为了使实例易于外界访问 我们一般提供一个类方法
    // 类方法命名规范 share类名|default类名|类名
    +(instancetype)shareTools
    {
        //return _instance;
        // 最好用self 用Tools他的子类调用时会出现错误
        return [[self alloc]init];
    }
    // 为了严谨,也要重写copyWithZone 和 mutableCopyWithZone
    -(id)copyWithZone:(NSZone *)zone
    {
        return _instance;
    }
    -(id)mutableCopyWithZone:(NSZone *)zone
    {
        return _instance;
    }
    
    1. 观察者模式:KVO是典型的通知模式,观察某个属性的状态,状态发生变化时通知观察者。

    分为两种:通知和KVO

    • 通知(Notification)

    接受方(观察者)

    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(notice:) name:@"tongzhi" object:nil];
    
    -(void)notice:(id)sender{  
      NSLog(@"%@",sender);
    }
    

    发送方

    //创建通知对象
    NSNotification *notification = [NSNotification notificationWithName:@"tongzhi" object:nil];
     //Name是通知的名称 object是通知的发布者(是谁要发布通知,也就是对象) userInfo是一些额外的信息(通知发布者传递给通知接收者的信息内容,字典格式)
    //    [NSNotification notificationWithName:@"tongzhi" object:nil userInfo:nil];
    //发送通知
     [[NSNotificationCenter defaultCenter] postNotification:notification];
    

    在dealloc里面移除观察者

    - (void)dealloc {
      //删除根据name和对象,如果object对象设置为nil,则删除所有叫name的,否则便删除对应的
        [[NSNotificationCenter defaultCenter] removeObserver:self name:@"tongzhi" object:nil];
    }
    
    • KVO:就是一种观察者模式用于监听属性的变化

    例子:

    //自定义MyTimer类,在.h文件中定义一个属性name
    @property (nonatomic, strong) NSString *name;
    //自定义ViewController,在controller的.h文件中也定义一个属性myView
    @property (nonatomic, strong) UIView *myView;
    

    在Viewcontroller的.m文件中定义一个button,设置点击事件,在该事件中分别调用上面定义的两个属性

    int i = 5;
    int sum = 15;
    - (void)viewDidLoad {
        [super viewDidLoad];
        self.view.backgroundColor = [UIColor whiteColor];
            UIButton *btn = [UIButton buttonWithType:UIButtonTypeSystem];
            btn.frame = CGRectMake(100, 100, 100, 30);
            [btn setTitle:@"点击" forState:UIControlStateNormal];
            [btn addTarget:self action:@selector(handleTimer:) forControlEvents:UIControlEventTouchUpInside];
            [self.view addSubview:btn];
        
        
        _label = [[UILabel alloc ] initWithFrame:CGRectMake(100, 200, 180, 30)];
        _label.text = @"当前年龄15岁";
        [self.view addSubview:_label];
        
        //创建Mytimer对象
        _ourTimer = [[MyTimer alloc ] init];
        //观察属性name
        [_ourTimer addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew || NSKeyValueChangeOldKey context:nil];
        //观察属性myView
        [self addObserver:self forKeyPath:@"myView" options:NSKeyValueObservingOptionNew || NSKeyValueChangeOldKey context:nil];
        
    }
    
    //点击事件,分别调用属性
    - (void)handleTimer:(UIButton *)btn {
        _ourTimer.name = @"小明";
        self.myView = nil;
        NSLog(@"第一次设置名字");
    }
    
    //一旦属性被操作了,这里会自动响应(上面设置观察的属性才会在这响应)
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
        if ([keyPath isEqualToString:@"name"]) {
            NSLog(@"名字发生了改变");
            _label.text = [NSString stringWithFormat:@"当前年龄%d岁", i + sum];
            sum = i + sum;
        } else if ([keyPath isEqualToString:@"myView"]) {
            NSLog(@"我的视图");
        }
    }
    
    //移除
    - (void)dealloc {
        [_ourTimer removeObserver:self forKeyPath:@"name"];
        [self removeObserver:self forKeyPath:@"myView"];
    }
    
    1. 委托模式:代理+协议的组合,block。实现1对1的反向传值操作。
    • 代理
    #import <Foundation/Foundation.h>
     
    @protocol MissionDelegate <NSObject>
     // 代理方法
    -(void)draftDocuments;
    -(void)tellPhone;
     
    @end
     
    @interface Boss : NSObject
    
    @property(nonatomic, assign)id<MissionDelegate> delegate;
     
    @end
    
    • block
    
    #import <UIKit/UIKit.h>
    
    typedef void (^TimeisoverBlock)(void);
    typedef void (^AgainBuyvipBlock)(NSString *string);
    
    @interface HIstoryVipNewTableViewCell : UITableViewCell
    
    @property (nonatomic, copy) TimeisoverBlock timeisoverBlock;
    @property (nonatomic, copy) AgainBuyvipBlock againBuyvipBlock;
    
    @end
    
    

    ==注:主工程调用代理先将对象设置为遵从其代理,block用copy修饰源自RAC,提醒开发者编辑器自动对block进行copy操作。==

    1. 工厂模式:通过一个类方法,批量的根据已有模板生产对象。(感觉比较类似于继承)

    3、#import跟 #include 有什么区别,@class呢,#import<> 跟 #import“”有什么区别?

    1. #import是Objective-C导入头文件的关键字,#include是C/C++导入头文件的关键字,使用#import头文件会自动只导入一次,不会重复导入。
    2. @class告诉编译器某个类的声明,当执行时,才去查看类的实现文件,可以解决头文件的相互包含。
    ==注:使用@class可以避免文件的循环引用,同时提交编译效率。==
    3. #import<>用来包含系统的头文件,#import""用来包含用户头创建的文件。
    

    4、frame 和 bounds 有什么不同?

    1. frame指的是:该view在父view坐标系统中的位置和大小。(参照点是父view的坐标系统) 
    2. bounds指的是:该view在本身坐标系统中的位置和大小。(参照点是本身坐标系统)
    

    5、你是否接触过OC中的反射机制?简单聊一下概念和使用

    1. class反射
      通过类名的字符串形式实例化对象。
    Class class = NSClassFromString(@"student");
    Student *stu = [[class alloc] init];
    

    将类名变为字符串。

    Class class =[Student class];
    NSString className = NSStringFromClass(class);
    
    1. SEL的反射
      通过方法的字符串形式实例化方法。
    SEL selector = NSSelectorFromString(@"setName");
    [stu performSelector:selector withObject:@"Mike"];
    

    将方法变成字符串。

    NSStringFromSelector(@selector(setName:));
    

    6、ipa包瘦身

    • 删除未使用的图片资源(LSUnusedResource)
    • 编译器优化
    • 未用到的类(AppCode)

    7、谈谈3D展示想法

    • 方案一:采用SeneKit展示3D模型

    SceneKit是一个高性能的渲染游戏引擎,它能够将3D模型文件用简单的方式渲染出来。

    优点:
    1. 3D显示商品的流畅度更高,能实现的功能及效果更好。
    2. SeneKit是苹果封装的一个专门实现3D效果的框架。项目冲突问题相对少。
    
    缺点:
    1. 因为SeneKit使用,本质上还是需要读取.dae包,需要专门做模型。如果大批量商品都需要3D动画效果,需要更多的.dae文件。这方面需要一些成本投入。
    2. 大批量商品都采用3D展示,过多.dae文件,会导致App越用越大,所以需要一个清除缓存的功能。可以再本地存储一个加载3D的模型动画。在展示动画的过程中,后台下载需要展示的3D模型数据。另外再下次进入时读取缓存,比对是否是需要展示的3D模型,做到先判断选择清空数据重新下载,还是读取缓存数据。保证本地只有两个.dae文件,一个是加载动画,一个是最新需要展示的3D模型。
    
    • 方案二:使用CATransform3D实现3D动画效果

    CATransform3D是苹果提供的核心动画中展示3D效果的框架。

    优点:
    1. 不需要模型文件。纯代码实现3D动画效果。
    
    缺点:
    1. 拼接3D商品:
        -  3D显示与用户手势交互方面,没有方案一那么灵活。
        -  商品的立体图形,需要程序员手动使用代码拼接,需要商品的6个面的图片组合起来。
            - 类似:骰子,6个面(1,2,3,4,5,6),将6个面组合成一个六面体。
        -  正规六面体的拼接还可以做,但是商品不正规的六面挺难组合,组合出的效果也不是很好。
    
    2. 不拼接3D商品:
        - 能够上下左右各种翻转图片,但是无法实现立体感,体验大打折扣。
    
    • 方案三:h5实现3D效果,iOS与WKWebView交互

    网络

    1、


    算法

    1、二叉搜索树简单实现?

    element 是可比较的数。二叉搜索树的时间复杂度最多O(log n);

    • node结构
    node<E> {
        E element;
        Node<E> left;
        Node<E> rigth;
        Node<E> parent;
        public Node(E element, Node<E> parent) {
            this.element = element;
            this.parent = parent;
        }
    }
    
    • add(E element)
    public void add(E element) {
        if(root == null) {
            root = new Node<>(element, null);
            size++;
            return;
        }
        
        // 找到父节点
        Node<E> node = root;
        Node<E> parent = root;
        int cmp = 0;
        while(node != null) {
            // 自定义比较方法
            cmp = compare(element, node.element);
            parent = node;
        
            if (cmp > 0) {
                node = node.right;
            } else if(cmp < 0) {
                node = node.left;
            } else {
                return;
            }
        }
        
        // 插入父节点的位置
        Node<E> newNode = new Node<>(elemnt, parent);
        if (cmp > 0) {
            parent.right = newNode;
        } else {
            parent.left = newNode;
        }
        size++;
    }
    
    • remove(E element)
    // 叶子节点直接删除。 
    // 度=1的节点(子节点替代原节点的位置)。
    // 度=2的节点(左子树找前驱节点,右子树找后驱节点)
    
    
    // 根据value找节点
    private Node<E> node(E element) {
        Node<E> node = root;
        while (node != null) {
            int cmp = compare(element, node.element);
            if (cmp == 0) return node;
            if (cmp > 0) {
                node = node.right;
            } else {
                node = node.left;
            }
        }
        return null;
    }
    
    // 删除
    public void remove(Node<E> node) {
        if (node == null) return;
        
        size--;
        
        if (node.hasTwoChildren) {// 度==2
            Node<E> s = predecessor(node); // 找前驱节点
            // 前驱节点的值覆盖2的节点的值
            node.element = s.element;
            // 现在需要删除的就是s。
            node = s
        }
        // 删除node节点
        Node<E> replacement = node.left != null ?? node.left : node.right;
        
        if (replacement != null) { // 度==1
            replacement.parent = node.parent;
            if (node.parent == null) {// node的度==1 且是 root节点
                root = replacement;
            } else if (node == node.parent.left) {
                node.parent.left = replacement
            } else if (node == node.parent.right) {
                node.parent.right = replacement
            } 
        } else if (node.parent == null) { // node是叶子节点 且是 root节点
            root = null;
        } else {
            if (node == node.parent.left) {
                node.parent.left = null;
            } else {
                node.parent.right = null;
            }
        }
    }
    
    
    

    2、前序遍历,中序遍历,后序遍历,层序遍历 二叉搜索树

    • 前序遍历

    根节点,前序遍历左子树,前序遍历右子树。

    方案一:递归
    public void preorderTraversal() {
        preorderTraversal(root);
    }
    
    public void preorderTraversal(Node<E> node) {
        if (node == null) return;
        
        System.out.printIn(node.element);
        preorderTraversal(node.left);
        preorderTraversal(node.right);
    }
    
    • 中序遍历

    中序遍历左子树,根节点,中序遍历右子树。

    可以发现从小到大排列.

    方案一:递归
    public void inorderTraversal() {
        inorderTraversal(root);
    }
    
    public void inorderTraversal(Node<E> node) {
        if (node == null) return;
        
        inorderTraversal(node.left);
        System.out.printIn(node.element);
        inorderTraversal(node.right);
    }
    
    • 后序遍历

    后序遍历左子树,后序遍历右子树,根节点。

    方案一:递归
    public void postorderTraversal() {
        postorderTraversal(root);
    }
    
    public void postorderTraversal(Node<E> node) {
        if (node == null) return;
        
        postorderTraversal(node.left);
        postorderTraversal(node.right);
        System.out.printIn(node.element);
    }
    
    • 层序遍历

    从上到下,从左到右依次访问每个节点。

    // 对列方式处理
    public void levelOrderTraversal() {
        if (root == null) return;
        
        // 队列进出队
        Queue<Node<E>> queue = new LinkedList<>();
        queue.offer(root);
        
        while (!queue.isEmpty()) {
            Node<E> node = queue.poll();
            System.out.printIn(node.element);
            
            if (node.left != null) {
                queue.offer(node.left);
            }
            
            if (node.right != null) {
                queue.offer(node.right);
            }
        }
    }
    
    // 层序遍历计算树高度
    // ----访问完一层,然后访问下一层,
    // 用levelSize记录每一层的元素数量,
    // 当数量为0的时候访问下一层,height++
    public int height() {
        if (root == null) return 0;
        
        int height = 0;
        // 存储每一层的元素数量
        int levelSize = 1;
        Queue<Node<E>> queue = new LinkedList<>();
        queue.offer(root);
        
        while (!queue.isEmpty()) {
            Node<E> node = queue.poll();
            
            levelSize--;
            
            if (node.left != null) {
                queue.offer(node.left);
            }
            
            if (node.right != null) {
                queue.offer(node.right);
            }
            
            if (levelSize == 0) { // 意味着即将访问下一层
                levelSize = queue.size();
                height++;
            }
            
            return height;
        }
    }
    
    ---
    
    /// 是不是完全二叉树(什么是完全二叉树,完全二叉树满足条件)
    // 是否是叶子节点
    public boolean isLeaf() {
        return left == null && right == null;
    }
    // 是否有两个子节点
    public boolean hasTwoChildren() {
        return left != null && right != null;
    }
    // 是否是完全二叉树
    publick boolean isComplete() {
        if (root == null) return false;
        
        Queue<Node<E>> queue = new LinkedList<>();
        queue.offer(root);
        
        boolean leaf = false;
        while (!queue.isEmpty()) {
            Node<E> node = queue.poll();
            if (leaf && !node.isLeaf()) return false;// 要求是叶子,但实际上不是叶子
            
            if (node.hasTwoChildren()) {
                queue.offer(node.left);
                queue.offer(node.right);
            } else if (node.left == null && node.right != null) {// 只有右节点没有左节点
                return false;
            } else { // 后面遍历的都必须是叶子节点
                leaf = true;
            }
        }
        return ture;
    }
    

    3、中序遍历前驱节点

    前驱节点在左子树中。前驱节点在祖节点中。

    prvite Node<E> predecessor(Node<E> node) {
        if (node == null) return null;
        
        // 前驱节点在左子树中(left.right.right.right...)
        Node<E> p = node.left;
        if (p != null) {
            while (p.right != null) {
                p = p.right;
            }
            return p;
        }
        
        // 从父节点,祖节点找前驱节点
        while (node.parent != null && node == node.parent.left) {
            node = node.parent;
        }
        
        // node.parent == null , node == node.parent.right
        
        return node.parent;
    }
    

    3、实现AVL树

    特殊的二叉搜索树,再二叉搜索树得基础上添加了一个平衡因子,|平衡因子|<=1。

    LL-右旋转,RR-左旋转,LR-RR+LL,RL-LL+RR

    4、链表和动态数组区别

    链表是一种链式存储的线性表,所有元素内存地址不一定是连续的。优点:需要多少就申请多少内存。

    • 分为单向链表(增删改查均O(n))和双向链表(增删改查均O(n/2)),双向链表是单向链表算法上的优化。主要体现在增删改查上。

    动态数组的元素的内存地址是连续的。

    • 动态数组在添加和删除元素时,时间复杂度O(n)【内存地址是连续的所以每次添加和删除其实本质上都是元素直接的挪动过程】,但是在get,set元素时O(1)【可以直接通过下标去修改和获取。】

    5、队列

    队列特殊的线性表,头尾两端操作,先进先出(FIFO)

    可以利用双向链表去实现队列

    链表,队列,二叉搜索树,AVL树...持续更新中...

    相关文章

      网友评论

          本文标题:iOS面试题积累总结

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