美文网首页
ios 面试题

ios 面试题

作者: 成长的船 | 来源:发表于2018-05-09 17:45 被阅读0次

    1.Foundation对象与Core Foundation对象有什么区别

    1. Foundation对象是OC的,Core Foundation对象是C对象
    2. 数据类型之间的转换
    • ARC:__bridge_retained、__bridge_transfer
    • 非ARC: __bridge
    Snip20180509_1.png

    2.KVO内部实现原理

    • 1.KVO是基于runtime机制实现的
    • 2.当某个类的对象第一次被观察时, 系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的 setter 方法。 派生类在被重写的 setter 方法实现真正的通知机制(NSKVONotifying_Dog)。我们注册监听的时候,底层找方法的的isa指针就变成指向新创建的子类对象也就是NSKVONotifying_Dog这个新类。


      Snip20180509_4.png
    派生出的问题:
    1.为什么用的是子类而不是分类?

    答:因为如果用分类的话,那么Dog的子类就不走了,肯定不行。

    2.你可否自己实现kvo?

    答:kvo是给予runtime机制的。具体的请点击
    https://blog.csdn.net/u014247354/article/details/78403567

    3.kvo的局限性?

    答:kvo只能监听属性的改变;再一个,运行时时创建派生类比较耗性能。


    3.手写单例

    #import "MMTool.h"
    
    @implementation MMTool
    static MMTool *_mmTool = nil;
    +(instancetype)allocWithZone:(struct _NSZone *)zone{
        if (_mmTool == nil) {
            static dispatch_once_t onceToken;
            dispatch_once(&onceToken, ^{
                _mmTool = [super allocWithZone:zone];
            });
        }
      
        return _mmTool;
    }
    +(instancetype)sharedMMTool{
        return [[self alloc]init];
    }
    

    4.runtime

    1.必备常识
    • Ivar : 成员变量
    • Method : 成员方法
    • objc_msgSend : 给对象发送消息
    • class_copyMethodList : 遍历某个类所有的方法
    • class_copyIvarList : 遍历某个类所有的成员变量

    5.关于block

    下面是面试题:

      1.
        int a = 10;
        void (^block)() =^{
            NSLog(@"打印结果是 %d",a);   
        };
        a = 20;
        block(); //打印结果是 10
    ----------------------------------------------------------------------------
     2.
       static int a = 10;
        void (^block)() =^{
            NSLog(@"打印结果是 %d",a); 
        };
        a = 20;
        block(); //打印结果是 20
    ----------------------------------------------------------------------------
    3.
     __block int a = 10;
        void (^block)() =^{
            NSLog(@"打印结果是 %d",a); 
        };
        a = 20;
        block(); //打印结果是 20
    ----------------------------------------------------------------------------
    4.
     int a = 10;
    - (void)tes{
        void (^block)() =^{
            NSLog(@"打印结果是 %d",a); 
        };
        a = 20;
        block(); //打印结果是 20   
    }
    除了第一种是值传递之外,剩下的只要有修饰(__block、static)、或者全局变量修饰的都是指针传递。
    
    用static修饰、和一个全局变量,本质是一份内存,所以是指针传递,也就是内存地址传递。前后的内存地址是一样的
    用__block修饰,其本质是:会把“外部变量”在栈中内存地址放到堆中。前后的内存地址不一样
    除了第一种不能在block中修改a的值,剩下的三种都是可以修改a的值。

    1.在用__block修饰的时候,是把“外部变量”在栈中内存地址放到堆中,所以当我们修改a的值的时候,其实是在栈去进行修改。
    2.而不用__block修饰的时候,再去修改a的值,那就是在栈中进行修改,然后就会报错。

    所以:栈中的数据不能修改,内存由系统释放。堆中的数据可以修改,所以想修改,就用__block修饰。


    关于block的循环引用问题:

    block用从copy修饰。那么内存就在堆区,copy一次,就会retain,引用对象的计数器就会加1. 所以用block就用__block(MRC);
    ARC :id weak weakSelf = self; 或者 weak __typeof(&*self)weakSelf = self该方法可以设置宏。
    就可以保证对象在block中引用的时候,引用计数器不会增加。

    关于block【RAC中的一个例子】

    + (RACSignal *)createSignal:(RACDisposable * (^)(id<RACSubscriber> subscriber))didSubscribe {
        RACDynamicSignal *signal = [[self alloc] init];
        signal->_didSubscribe = [didSubscribe copy];
        return [signal setNameWithFormat:@"+createSignal:"];
    }
    (RACDisposable * (^)(id<RACSubscriber> subscriber))didSubscribe 
    
    RACDisposable * :是返回值类型  
    id<RACSubscriber> subscriber:指的是传入的参数
    didSubscribe : block起的别名
    
    
    

    6.关于MRC开发

    原则:
    • 有alloc\init\copy的,都要对应的使用release或者autorelease
    • 有retain的都要使用一次release或者autorelease。
    .h文件:
    @property (retain, nonatomic) Dog * dog;
    @end
    @implementation Person
    - (void)setDog:(Dog *)dog{
        if (_dog != dog) {
            [_dog release]; //开始指向空指针,给空指针发消息是OK的
            [_dog retain]; //引用计数加一,然后_dog = dog
        }
    }
    
    - (void)dealloc{
        [self.dog release];
        self.dog = nil;
        [super dealloc];
    }
    @end
    
    在dealloc方法里面为什么要进行release? 在set为什么要判断_dog!=dog?然后再release再retain?

    答:在dealloc里面release 是因为,对dog对象进行了retain,所以要release。

    解释set方法

    如果没有if判断

    p.dog = dog1;
    p.dog = dog1;
    [d release]
    p.dog = dog1;  //这句就会崩溃,因为dog1的引用计数为0。
    加了判断后,意思就是:如果是同一对象,那么就不管。如果是新对象,那么就释放旧对象然后对新对象retain一次。
    

    7.多线程的底层实现?

    1.首先弄清楚什么是线程,什么是多线程。
    2.Mach是第一个以多线程方式处理任务的系统,因此多线程的底层实现机制是基于Mach的线程
    3.开发中很少用Mach级的线程,因为Mach级的线程没有提供多线程的基本特征,线程之间是独立的

    开发中实现多线程的方案

    1.C语言的POSIX接口:#include <pthread.h>
    2.OC的NSThread
    3.C语言的GCD接口(性能最好,代码更精简)
    4.OC的NSOperation和NSOperationQueue(基于GCD)

    线程间怎么通信?

    1.performSelector:onThread:withObject:waitUntilDone:
    2.NSMachPort

    8.网络图片处理问题中怎么解决一个相同的网络地址重复请求的问题?

    利用字典(图片地址为key,下载操作为value)

    /**定义字典保存下载好的图片key 图片的地址 value 图片UIImage*/
    @property (nonatomic, strong) NSMutableDictionary *imagesDic;
    
    /**定义字典保存下载操作key 图片地址 value 下载操作*/
    @property (nonatomic, strong) NSMutableDictionary *operationsDic;
    
    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
    {
    static NSString *identifier = @"cell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
    if (cell == nil) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
    }
    
    // 1.根据下载地址去字典中取图片
    UIImage *image = self.imagesDic[@"http://www.baidu/example.png""];
    
    // 2.判断字典中是否有图片(是否已经下载过图片)
    if (image == nil) {
        // 没有下载过(1.从来没有下载过, 2.没有下载完,正在下载中)
        
        NSOperation *operation =self.operationsDic[@"http://www.baidu/example.png"];
        if (operation == nil) {
            // 1.从来没有下载过
            // 创建操作
            operation = [NSBlockOperation blockOperationWithBlock:^{
                NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:@"http://www.baidu/example.png"]];
                UIImage *image = [UIImage imageWithData:imageData];
                
                // 将下载好的图片存储到字典中
                self.imagesDic[@"http://www.baidu/example.png""] = image;
                
                // 将操作从字典中移除
                self.operationsDic[@"http://www.baidu/example.png""] = nil;
                
                // 回到主线程更新UI
                dispatch_async(dispatch_get_main_queue(), ^{
                    cell.imageView.image = image;
                });
            }];
            // 将操作放入队列,会自动开辟一个线程
            [self.queue addOperation:operation];
     
            // 将操作添加到字典中,这里可以用来判断是不是正在下载
            self.operationsDic[@"http://www.baidu/example.png"] = operation;
            
        }else
        {
            // 2.没有下载完,正在下载中
            cell.imageView.image = [UIImage imageNamed:@"占位图片"];
        }
    }else
    {
        // 已经下载过
        cell.imageView.image = image;
    }
    return cell;
    }
    
    思路
    Snip20180511_1.png
    9.你用过NSOperationQueue么?如果用过或者了解的话,你为什么要使用NSOperationQueue,实现了什么?请描述它和GCD的区别和类似的地方(提示:可以从两者的实现机制和适用范围来描述)。

    1.GCD是纯C语言的API,NSOperationQueue是基于GCD的OC版本封装
    2.GCD只支持FIFO的队列,NSOperationQueue可以很方便地调整执行顺序、设置最大并发数量、依赖、栅栏
    3.NSOperationQueue可以在轻松在Operation间设置依赖关系,而GCD需要写很多的代码才能实现
    4.NSOperationQueue支持KVO,可以监测operation是否正在执行(isExecuted)、是否结束(isFinished),是否取消(isCanceld)
    5.GCD的执行速度比NSOperationQueue快

    • 任务之间不太互相依赖:GCD
    • 任务之间有依赖\或者要监听任务的执行情况:NSOperationQueue
    10.使用 [UIImage imageNamed:@"example.png"];会有怎样的影响?

    imageNamed加载图片
    1.用 imageNamed:方法加载的图片不会随着UIImageView一起销毁,而是依旧在内存中。【用instrument实时看的】
    2.加载时耗的内存比较大
    3.相同的图片只会加载一份到内存中,如果使用多次或者同时使用,使用的是同一个对象。

    imageWithContentsOfFile加载图片
    1.用 imageWithContentsOfFile:方法加载的图片会随着UIImageView一起销毁
    2.加载时耗的内存相对于imageNamed方法要小
    3相同的图片会多次加载到内存中 ,如果同时使用,使用的是不同的对象。

    • 总结:imageNamed:如果一些图片在多个界面会使用,并且图片较小、使用频率高。例如:图标和小的背景图。
    • imageWithContentsOfFile:只在一个地方用、并且图片较大、使用频率不高。

    11.关于self和super关键字

    Self关键字
    1.如果self在对象方法中,那么self就代表调用当前对象方法的那个对象
    2.如果self在类方法中,那么self就代表调用当前类方法的那个类。
    总结:我们只关注self 在哪个方法中。
    Super关键字
    1.如果想在子类方法中调用父类的方法,就可以用super。
    2. super 是一个 Magic Keyword, 它本质是一个编译器标示符,super 会告诉编译器,调用 class 这个方法时,要去父类的方法,而不是本类里的。

    • 一个面试题
     下面的代码输出什么?
    @implementation Son : Father
    - (id)init
    {
        self = [super init];
        if (self) {
            NSLog(@"%@", NSStringFromClass([self class]));
            NSLog(@"%@", NSStringFromClass([super class]));
        }
        return self;
    }
    @end
    都输出 Son
    NSStringFromClass([self class]) = Son
    NSStringFromClass([super class]) = Son
    
    解释:https://www.jianshu.com/p/10b65df06e44

    1.[self class]的意思是先去子类中找class方法;[super class]的意思是先去Father父类中找class方法;
    2.[self class]没有找到,最后找到NSObject里面找到class方法,而 - (Class)class的实现就是返回self的类别,故上述输出结果为 Son。[super class]也是一样,最后也在NSObject里面找到class方法...

    • 如果改成这样
    @implementation Son : Father
    - (id)init
    {
        self = [super init];
        if (self) {
            NSLog(@"%@", NSStringFromClass([self class]));
            NSLog(@"%@", NSStringFromClass([super class]));
        }
        return self;
    }
    @end
    -----------------------------------------
    @implementation Father
    - (Class)class{
        [super class];
        return  [Father class];
    }
    @end
    都输出 Father
    NSStringFromClass([self class]) = Father
    NSStringFromClass([super class]) = Father
    

    但如果改成这样子:

    @implementation Son
    - (id)init
    {
        self = [super init];
        if (self) {
            NSLog(@"%@", NSStringFromClass([self class]));
            NSLog(@"%@", NSStringFromClass([super class]));
        }
        return self;
    }
    - (Class)class{
        [super class];
        return  [Son class];
    }
    @implementation Father
    //空的
    @end
    都输出 Son
    NSStringFromClass([self class]) = Son
    NSStringFromClass([super class]) = Son
    

    12.New实现原理

    New做了三件事情

    1.开辟存储空间 +alloc方法
    2.初始化所有的属性(成员变量)- init 方法
    3.返回对象的地址

    
    

    如图:


    Snip20180516_8.png

    13.什么是构造方法?什么是类构造方法?;instancetype和id的区别?

    构造方法
    - (instancetype)init{
        if (self = [super init]) {
            
        }
    类构造方法
    +(instancetype)person{
        return [[self alloc]init]; //这里一定要用self不要用类名。
    }
    //id 是动态类型,在运行的时候才会确定具体的类型;instancetype是静态类型,在编译时就确定具体的类型。
    有了id动态类型,便于多态的使用。
    

    14.类的本质和存储细节?

    Snip20180517_3.png

    15.类的启动过程?

    1.先调用+ (void)load{}
    只要程序启动就会将类的代码加载到内存中,放到代码区【只要程序启动就加载而不是用到的时候加载】load方法会在当前类被加载到内存的时候调用,有且仅会调用一次
    2.再调用+ (void)initialize{ },在当前类第一次被使用时就会调用(创建类对象的时候,类对象只会创建一次)。这个方法在整个程序中只调用一次。

    注意点:
    1.如果有继承关系,则先调用父类的load方法
    2.如果有继承关系,则先调用父类的initialize方法

    f#16.空指针和野指针以及僵尸对象?

    image.png

    即指向僵尸对象的指针就是野指针

    @autoreleasepool {
            Person * p = [[Person alloc]init];
            [p release];//0 这个时候堆类的实例对象就是一个僵尸对象
    
            [p release]; //p 这个时候调用release的时候,实例对象已经被销毁,这个时候会崩溃。这个时候p就是野指针【即指向僵尸对象的指针就是野指针】
            [p release];
            [p release];
        }
    上面代码会崩溃
    改成
    @autoreleasepool {
            Person * p = [[Person alloc]init];
            [p release];
            p = nil; //让p指向空指针,给空指针发消息是OK的
            [p release]; 
            [p release];
            [p release];
        }
    

    17.关于@class和import, 以及@class避免的循环拷贝?

    1.import是一个预编译指令,它会将“.h”文件拷贝到import所在的位置,并且import有一个特点,只要".h"中的文件发生了变化,那么importj就会重新拷贝一次(更新操作)。【如果A中import了B类,B中import了C类,C类一动,B和A类都会改动】
    2.@class仅仅只会告诉编译器,@class后面是一个类,所以编译器不知到这个类中有什么方法和参数。所以在.m文件里面需要import,这个时候如果改变导入的类,只会更新引用的这个类,而不是很多类。


    image.png

    18.关于循环retain

    image.png
        Person * p = [[Person alloc]init];
        Dog * d = [[Dog alloc]init];
    
        p.dog = d; //2
        d.ownner = p; //2
    
        [p release];//1
        [d release]; //1
    解决就让某一方不要进行retain,改成
    '@property (strong, assign)Person  * owner;'改成assign。
    

    19.关于autorelease和autoreleasepool

    1. [p autorelease] 引用计数是不变的。
    这句话的意思是:系统会创建一个自动释放池。当自动释放池销毁的时候会给自动释放池里面的对象发送一次release。相当于延迟release

        @autoreleasepool {
        Person * p = [[Person alloc]init];
        [p run];
        [p release];
        [p run]; //这句话崩溃
    }
    
        @autoreleasepool {
        Person * p = [[Person alloc]init];
        [p run];
        [p autorelease]; //这句话的好处是,我们不用关心什么时候释放。会在最后统一发一次release
        [p run]; 但是这个不会。
    }
    
    注意点:

    1.一定要在 @autoreleasepool {
    }里面进行autorelease。
    2.在@autoreleasepool {
    }不要进行消耗内存的操作
    3.自动释放池是可以嵌套的。

    20.关于Copy的深浅拷贝和copy的内存管理;以及property和copy;以及block为什么用copy修饰;以及自定义类实现copy。

    原则:

    • copy会产生一个新的副本,修改副本不会影响原来的值,修改原本也不会影响副本的值。
    浅拷贝:就是指针拷贝。
    深拷贝:就是创建新对象。

    如果前后两个对象是不可变的,就是浅拷贝。其他的都是深拷贝。
    举例:

     NSString * str = @"qwe";
     NSString * strCopy  =[str copy];  //浅拷贝。要满足我们的原则。
    
    总结:浅拷贝不生成新的对象。深拷贝会产生新的对象

    所以浅拷贝会进行一次retain


    property和copy

    防止外界修改内部的值

    @property (nonatomic, copy) NSString * str;
    @end
    - (void)viewDidLoad {
        [super viewDidLoad];
      
       NSMutableString * s = [NSMutableString stringWithFormat:@"hello"];
        _str = s;
        [s appendString:@"world"];
        NSLog(@"str = %@  , s = %@",_str,s);
    }
    打印结果:str = helloworld  , s = helloworld
    原因是copy执行的是深拷贝。生成了一个新的str对象。
    所以字符串就用copy修饰
    

    block为什么用copy修饰

    因为block默认在栈中,就不会对外界对象进行retain ,若在block中访问对象,但是对象在访问前就被释放了就会崩溃。所以用copy,将block从栈中移到堆中,这样就可以retain一次,保要使用对象的命。

    自定义类实现copy

    1.遵守NSCopying协议
    2.实现- (id)copyWithZone:(NSZone *)zone{
    }

    21.关于网络?

    1.什么是URL?

    URL的全称是Uniform Resource Locator(统一资源定位符)
    通过1个URL,能找到互联网上唯一的1个资源
    URL就是资源的地址、位置,互联网上的每个资源都有一个唯一的URL.

    URL的基本格式 = 协议://主机地址/路径
    http://202.108.22.5/img/bdlogo.gif

    2.什么是请求头、请求体,什么是响应头、响应体 ?

    请求头:包含了对客户端的环境描述、客户端请求信息等

    image.png
    image.png

    22.小知识点

    [UINavigationBar appearance];
    [UITabBarItem appearance];
    通过appearance统一设置所有UITabBarItem的文字属性
    后面带有UI_APPEARANCE_SELECTOR的方法, 都可以通过appearance对象来统一设置

    23.内存泄漏的种类,以及你是如何避免的?

    参考文章1
    参考文章2
    1.网络请求管理者。保持是一个管理者,如果多个管理者就有多个任务队列。
    2.Block的循环引用。
    3.delegate的循环引用
    4.#import的循环引用
    5.无线for循环 最后内存飙升,APP崩溃
    6.NSTimer不清除会内存泄露。手机发烫,内存飙升app崩溃。
    7.局部变量是控制器,它被释放后,其View还在,这个时候也是内存泄露。

    避免:
    1.运用单例
    2.__weak typeof(self) weakSelf = self;
    3.weak 或者assign
    4.用@class
    5.用instruments看内存增长的情况
    6.dealloc的时候清楚NSTimer
    7.运用父子控制器,再一个用strong修饰,不用局部变量。

    24.谈谈上拉下拉刷新的细节处理?

    1.这里主要涉及的是多次请求问题
    2.请求失败情况下的问题。
    3.刷新控件的关闭和显示问题
    4.分页处理的问题,page参数的恢复问题。

    25.一些开发经验?

    1.如果图片设置的frame与预设的不一样,要想到autoresizing
    2.减轻第三方框架的风险。对第三方框架进行一次封装,面向自己的类开发。

    26.frame和bounds,为什么要有bounds?

    bounds的有以下两个特点,可以从bounds自身的来剖析:bounds是一个Rectangle,前半部分是point后半部分是view的size。这两个属性也预示着两个特点:

    bounds像是浮于frame之上的。frame是一个框架,bounds是显示子view的东西,下面总结bounds的两个特征:
    

    第一、 对于bound的point:它不会改变frame的原点,改变的是bounds自己的原点,进而影响到“子view”的显示位置。这个作用更像是移动bounds原点的意思。
    第二、 对于bound的size:它可以改变的frame。如果bounds的size比frame的size大。那么frame也会跟着变大,那么frame的原点也会变,但是center不会变。这个作用更像边界的意思。
    可以推测一下,setBound第一个特性可以用于view的滑动,手势动作,因为可以影响子view的显示位置。
    工程中经常修改tablewview的contentInset值。该技巧常用于屏幕两边,上下头部的“留白”。修改contentInset的时候其实修改的也是bounds。

    27.ARC的实现机制到底是什么?

    ARC 的工作原理大致是这样:当我们编译源码的时候,编译器会分析源码中每个对象的生命周期,然后基于这些对象的生命周期,来添加相应的引用计数操作代码。所以,ARC 是工作在编译期的一种技术方案,这样的好处是:

    编译之后,ARC 与非 ARC 代码是没有什么差别的,所以二者可以在源码中共存。实际上,你可以通过编译参数 -fno-objc-arc 来关闭部分源代码的 ARC 特性。

    28.SDWebimage的缓存策略是什么?

    29.iOS开发中#import、#include和@class的区别解析

    1. 一般来说,导入objective c的头文件时用#import,包含c/c++头文件时用#include。
    2. ,#import 确定一个文件只能被导入一次,这使你在递归包含中不会出现问题。<标记>
      所以,#import比起#include的好处就是不会引起交叉编译。

    ,#import && #class:

    1. import会包含这个类的所有信息,包括实体变量和方法(.h文件中),而@class只是告诉编译器,其后面声明的名称是类的名称,至于这些类是如何定义的,后面会再告诉你。
    2. 在头文件中, 一般只需要知道被引用的类的名称就可以了。 不需要知道其内部的实体变量和方法,所以在头文件中一般使用@class来声明这个名称是类的名称。 而在实现类里面,因为会用到这个引用类的内部的实体变量和方法,所以需要使用#import来包含这个被引用类的头文件。
      备注:#import 就是把被引用类的头文件走一遍,即把.h文件里的变量和方法包含进来一次,且仅一次,而@class不用,所以后者编译效率更高。
    3. 在编译效率方面考虑,如果你有100个头文件都#import了同一个头文件,或者这些文件是依次引用的,如A–>B, B–>C, C–>D这样的引用关系。当最开始的那个头文件有变化的话,后面所有引用它的类都需要重新编译,如果你的类有很多的话,这将耗费大量的时间。而是用@class则不会。
    4. 如果有循环依赖关系,如:A–>B, B–>A这样的相互依赖关系,如果使用#import来相互包含,那么就会出现编译错误,如果使用@class在两个类的头文件中相互声明,则不会有编译错误出现。
      备注:实践证明,A,B相互#import不会出现编译错误。因为<标记>处已经说明#import时文件只被导入一次,所以此条不成立。
      总结:
    5. 如果不是c/c++,尽量用#import。
    6. 能在实现文件中#import,就不在头文件中#import。
    7. 能在头文件中@class+实现文件中#import,就不在头文件中#import。

    30.NSDictionary 的底层数据结构是什么?

    NSDictionary(字典)是使用 hash表来实现key和value之间的映射和存储的, hash函数设计的好坏影响着数据的查找访问效率。数据在hash表中分布的越均匀,其访问效率越高。而在Objective-C中,通常都是利用NSString 来作为键值,其内部使用的hash函数也是通过使用 NSString对象作为键值来保证数据的各个节点在hash表中均匀分布。
    参考文章

    相关文章

      网友评论

          本文标题:ios 面试题

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