美文网首页
iOS 面试题

iOS 面试题

作者: milk_powder | 来源:发表于2016-12-27 21:17 被阅读80次

    一、网络层

    1.简介 TCP 和 UDP 区别,他们位于哪一层?
    TCP和UDP同属于OSI七层网络模型的第四层传输层。
    TCP是传输控制协议,特点:面向连接的,可靠的,点对点的通信,传输数据多,速度较慢。
    UDP是用户数据报协议,特点:非连接,不可靠,多对多的通信,传输数据少,速度快。
    2.描述TCP 协议三次握手,四次释放的过程。
    -------------三次握手----------
    (1)第一次握手:建立连接时,客户端A发送SYN包(SYN=j)到服务器B,并进入SYN_SEND状态,等待服务器B确认。
    (2)第二次握手:服务器B收到SYN包,必须确认客户A的SYN(ACK=j+1),同时自己也发送一个SYN包(SYN=k),即SYN+ACK包,此时服务器B进入SYN_RECV状态。
    (3)第三次握手:客户端A收到服务器B的SYN+ACK包,向服务器B发送确认包ACK(ACK=k+1),此包发送完毕,客户端A和服务器B进入ESTABLISHED状态,完成三次握手。
    -------------四次挥手----------
    (1)客户端A发送一个FIN,用来关闭客户A到服务器B的数据传送。
    (2)服务器B收到这个FIN,它发回一个ACK,确认序号为收到的序号加1。和SYN一样,一个FIN将占用一个序号。
    (3)服务器B关闭与客户端A的连接,发送一个FIN给客户端A。
    (4)客户端A发回ACK报文确认,并将确认序号设置为收到序号加1。
    3.HTTP 请求中的 GET 和 POST 的区别,Session 和 Cookie 的区别。
    GET,请求数据会附加在URL之后,请求的数据会暴露在地址栏,传输数据会受到URL长度的限制,安全性较低,一般用于获取服务器数据。
    POST,请求数据放到请求体中,理论上是不会受限制的,但是实际上各个服务器会规定对POST提交数据大小进行限制,安全性低。 上传请求均可以。
    cookie数据存放在客户的浏览器上,不安全,存放其他信息。
    session数据放在服务器上,相对安全,存放登录信息。
    4.TCP 协议是如何进行流量控制,拥塞控制的?
    滑动窗口机制实现

    二、操作系统与编译

    1.堆和栈的区别

    • 按管理方式,
      对于栈来讲,是由系统编译器自动管理,不需要程序员手动管理;
      对于堆来讲,释放工作由程序员手动管理,不及时回收容易产生内存泄露。
    • 按分配方式分,
      栈有两种分配方式:静态分配和动态分配;
      堆是动态分配和回收内存的,没有静态分配的堆。
      按申请大小,
      栈空间小,堆空间大。
    • 按数据存储,
      栈一般存储基本类型对象的地址,
      堆一般存放对象本身,block的copy等。

    三、内存管理

    1.内存管理修饰关键字
    nonatomic: 非原子的,禁止多线程,变量保护,提高性能.
    assign:普通赋值,常用于基本数据类型,也可以用来修饰对象,但是,被assign修饰的对象在释放后,指针的地址还是存在的,也就是说指针并没有被置为nil,成为野指针。也可以修饰非OC对象 比如C数据类型(int, float,double, char, 等等)
    retain:获得对象所有权,引用计数+1
    weak:弱引用,相当于MRC的assign,当指向的对象销毁后,weak会将变量置为nil,防止野指针,用于代理,UI控件。
    strong:强引用,相当于MRC的retain,
    copy:赋值方式 将新值复制一份赋覆盖原值,用于NSString、block等类型。不可变对象使用copy相当于retain,只是引用计数+1
    注意:strong和weak只能修饰对象
    2.用@property声明的NSString(或NSArray,NSDictionary)经常使用copy关键字,为什么?如果改用strong关键字,可能造成什么问题?
    (1) 因为父类指针可以指向子类对象,使用copy的目的是为了让本对象的属性不受外界影响,使用copy无论给我传入是一个可变对象还是不可对象,我本身持有的就是一个不可变的副本.
    (2) 如果我们使用是strong,那么这个属性就有可能指向一个可变对象,如果这个可变对象在外部被修改了,那么会影响该属性.
    3._ block和 _weak的区别
    __block:在ARC和MRC下都可用,可修饰对象,也可以修饰基本数据类型。__block对象可以在block被重新赋值,__weak不可以。
    __weak:只在ARC中使用,只能修饰对象,不能修饰基本数据类型,在ARC下,要避免block出现循环引用,经常会:__weak typedof(self) weakSelf = self;
    4.野指针
    (1)出现场景

    1.访问一个僵尸对象,访问僵尸对象的成员变量或者向其发消息
    2.死循环
    

    (2)如何调试

    开启僵尸对象调试:Edit Scheme--Enable Zombie Objects
    

    四、设计模式(对象通信和交互)

    1.代理
    delegate实质上是一种回调,主要用于两个对象之间的通信交互,优点是解耦合,注重回调结果。比如:tableViewDelegate,但需要声明协议,定义代理属性,设置代理,步骤较多。
    2.通知
    通知特点一对多,通信对象之间不需要建立联系,只负责发送通知不关心结果,缺点,代码可读性差,一般步骤 注册通知中心-发通知-接通知执行方法-移除通知。
    3.KVO
    键值观察,用来监听对象的属性值的变化,步骤跟通知类似。

    键值观察通知依赖于NSObject 的两个方法: willChangeValueForKey: 和 didChangevlueForKey:;
    在一个被观察属性发生改变之前, willChangeValueForKey: 一定会被调用,这就会记录旧的值。
    而当改变发生后,didChangeValueForKey: 会被调用,继而 observeValueForKey:ofObject:change:context:
    也会被调用。
    
    • KVO 底层实现原理--runtime
      当观察者对象 A ,KVO 机制动态的创建一个对象 A 当前的子类,并为这个新的子类重写了被观察者的属性 keyPath 的 setter 方法。随后 setter 方法就负责通知观察对象属性的改变状况。
    • 如何手动触发一个value的KVO
      自动触发的场景:在注册KVO之前设置一个初始值,注册之后,设置一个不一样的值,就可以触发了。
      手动触发演示:
    @property (nonatomic, strong) NSDate *now;
    - (void)viewDidLoad{ 
      [super viewDidLoad];
     [self willChangeValueForKey:@"now"]; // “手动触发self.now的KVO”,必写。
     [self didChangeValueForKey:@"now"]; // “手动触发self.now的KVO”,必写。
    }
    

    4.单利
    确保一个类只有一个对象,所有类都可以访问和设置单利对象中的属性数据,通常采用懒加载创建。可以节约性能。
    5.block(下面单独讲)

    五、数据持久----

    iOS 之 FMDB、CoreData、Plist、NSUserDefault详解
    1.属性列表
    涉及的类:NSUserDefaults,一般[NSUserDefaults standardUserDefaults]
    示例:

    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];//单例
    [defaults setValue:@"yfyfyfyfyfyfyfy"forKey:@"username"];
    [defaults setValue:@"123"forKey:@"password"];```
    //注意:UserDefaults设置数据时,不是立即写入,而是根据时间戳定时地把缓存中的数据写入本地磁盘。所以调用了set方法之后数据有可能还没有写入磁盘应用程序就终止了。出现以上问题,可以通过调用synchornize方法强制写入到文件中
    ```[defaults synchronize];```
    

    //读取
    NSString *name = [defaults valueForKey:@"username"];
    NSString *pwd = [defaults valueForKey:@"password"];```
    2.对象归档
    要使用对象归档,对象必须实现NSCoding协议.大部分Object C对象都符合NSCoding协议,也可以在自定义对象中实现NSCoding协议,要实现NSCoding协议,实现两个方法
    --进行编码--

    - (void)encodeWithCoder:(NSCoder*)coder
    {
    //[super encodeWithCode:coder];如果父类也遵守了NSCoding协议,确保继承的实例变
    量也能被编码,即也能被归档
    [coder encodeObject:self.nameforKey:@"name"];
    [coder encodeInteger:self.ageforKey:@"age"];
    [coder encodeObject:self.genderforKey:@"gender"];
    }```
    --进行解码--
    
    • (id)initWithCoder:(NSCoder*)aDecoder
      {
      //self = [super initWithCoder:aDecoder];确保继承的实例变量也能
      被解码,即也能被恢复
      self= [super init];
      if(self) {
      self.name= [aDecoder decodeObjectForKey:@"name"];
      self.gender= [aDecoder decodeObjectForKey:@"gender"];
      self.age= [aDecoder decodeIntegerForKey:@"age"];
      }
      return self;
      }
      @end```
      3.SQLite3
      (1)常用 SQLite 语句 及 SQLite3 的使用
    /*
    使用数据库的准备工作
    1.导入libsqlite3.dylib静态库
    2.导入sqlite3.h头文件         #import <sqlite3.h>
    
    使用数据库注意事项
    1.使用数据库时,要保证数据库文件存在
    2.使用数据库前,要保证数据库是打开状态
    3.使用数据库后,要关闭数据库
     */
    #import "ViewController.h"
    #import <sqlite3.h>
    @interface ViewController ()
    {
        sqlite3 *_db;
    }
    @property (retain, nonatomic) IBOutlet UITextField *nameText;
    @property (retain, nonatomic) IBOutlet UITextField *ageTextField;
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad
    {
        [super viewDidLoad];
        NSLog(@"%@",NSHomeDirectory());
    }
    
    - (void)dealloc {
        [_nameText release];
        [_ageTextField release];
        [super dealloc];
    }
    #pragma mark -- 创建数据库文件并打开数据库
    -(BOOL)openDatabase
    {
        //数据库文件路径
        NSString *filePath = [NSHomeDirectory()stringByAppendingPathComponent:@"Documents/student.sqlite"];
        //UTF8String把oc的字符串转化成c的字符串
        const char *filename = [filePath UTF8String];
        // sqlite3_open 打开数据库的方法; 如果有数据库文件就直接打开,如果没有数据库文件系统会自动创建一个数据库文件,并打开
        //参数1.数据库文件的路径 2.数据库对象,要打开的数据库
        //返回值表示操作的状态码
        int result = sqlite3_open(filename, &_db);
        if (result == SQLITE_OK)
        {
            NSLog(@"数据库打开成功");
            return YES;
        }
        else
        {
            NSLog(@"数据库打开失败");
            //sqlite3_close 关闭数据库的方法
            sqlite3_close(_db);
            return NO;
        }
    }
    #pragma mark --创建表
    - (IBAction)createTable:(id)sender
    {
        //1.打开数据库
        if (![self openDatabase])
        {
            return;
        }
        //2.创建表
        NSString *str = @"CREATE TABLE IF NOT EXISTS student (student_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT , age INTEGER)";
        char *error = nil;
        //sqlite3_exec对数据库的操作:创建表,增删改,都是用这个方法
        //参数 1.数据库对象 2.sql语句 3.回调函数nil 4.回调函数的参数nil
        if (sqlite3_exec(_db, [str UTF8String], nil, nil, &error)==SQLITE_OK)
        {
            NSLog(@"创建表成功");
        }
        else
        {
            NSLog(@"创建表失败:%s",error);
        }
        //3.关闭数据库
        sqlite3_close(_db);
    }
    #pragma mark -- 添加数据
    - (IBAction)insertInto:(id)sender
    {
        //1.打开数据库2.执行增删改查的操作3.关闭数据库
        if (![self openDatabase])
        {
            return;
        }
        NSString *str = [NSString stringWithFormat:@"insert into student (name , age) values ('%@',%d)",_nameText.text,_ageTextField.text.intValue];
        char *error = nil;
        if (sqlite3_exec(_db, [str UTF8String], nil, nil, &error)==SQLITE_OK)
        {
            NSLog(@"插入数据成功");
        }
        else
        {
            NSLog(@"插入数据失败:%s",error);
        }
        sqlite3_close(_db);
    }
    #pragma mark -- 删除数据
    - (IBAction)delete:(id)sender
    {
        if (![self openDatabase])
        {
            return;
        }
        NSString *str =@"delete from student where student_id = 6";
        char *error = nil;
        if (sqlite3_exec(_db, [str UTF8String], nil, nil, &error)==SQLITE_OK)
        {
            NSLog(@"删除成功");
        }
        else
        {
            NSLog(@"删除失败:%s",error);
        }
        sqlite3_close(_db);
    }
    #pragma mark -- 修改数据
    - (IBAction)upData:(id)sender
    {
        if (![self openDatabase])
        {
            return;
        }
        NSString *str = [NSString stringWithFormat:@"update student set name = '%@' where student_id =%d",@"马云",2];
        if (sqlite3_exec(_db, [str UTF8String], nil, nil, nil)==SQLITE_OK)
        {
            NSLog(@"修改数据成功");
        }
        else
        {
            NSLog(@"修改数据失败");
        }
        sqlite3_close(_db);
    }
    #pragma mark --查询数据
    - (IBAction)select:(id)sender
    {
        //先打开数据库,查询,关闭数据库
        if (![self openDatabase])
        {
            return;
        }
        NSString *str = @"select * from student";
        //缓存区
        sqlite3_stmt *stmt = nil;
        //sqlite3_prepare 把sql语句放入缓存区
        //参数1.数据库对象2.sql语句 3.sql语句的长度,-1全部的sql语句 4.缓存区 5.剩余部分的sql语句
        if (sqlite3_prepare(_db, [str UTF8String], -1, &stmt, nil)==SQLITE_OK)
        {
            //sqlite3_step 单步查询数据,一次只查询一条数据
            //SQLITE_ROW表示数据库中还有数据继续下一个查询
            //SQLITE_DONE 表示数据查询完毕
            while (sqlite3_step(stmt) == SQLITE_ROW)
            {
                //sqlite3_column_text 查询字符串的方法
                //参数1.缓存区 2.字段所在的列号
                char *cname = (char *)sqlite3_column_text(stmt, 1);
                int age = sqlite3_column_int(stmt, 2);
                //stringWithUTF8String 把c的字符串转化成oc的字符串
                NSString *name = [NSString stringWithUTF8String:cname];
                NSLog(@"%@---%d",name,age);
            }
            //查询结束,释放缓存区
            sqlite3_finalize(stmt);
        }
        sqlite3_close(_db);
    }
    -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
    {
        [self.view endEditing:YES];
    }
    @end
    

    4.FMDB
    建议不直接操作SQLite库,而是采用一些开源的第三方库来进行操作。比如:FMDB:https://github.com/ccgus/fmdb.git

    FMDB有三个主要的类:
    FMDatabase,一个FMDatabase对象就代表一个单独的SQLite数据库,用来执行SQL语句。
    FMResultSet,使用FMDatabase执行查询后的结果集。
    FMDatabaseQueue,用于在多线程中执行多个查询或更新,它是线程安全的。
    

    5.Core Data
    (1)Core Data本质上是对SQLite的封装,但是它不需要编写任何SQL语句。
    (2)要使用Core Data,需要在Xcode中的数据模型编辑器中设计好各个实体以及定义好他们的属性和关系。之后,通过操作这些对象,结合Core Data完成数据的持久化。

    六、OC关键字解释(略)

    七、Block详解

    1.说说你对block的理解
    (1)block就是代码块,类似于“闭包”,block的实现是基于指针和函数指针.
    (2)默认情况下block是存储在栈中的,如果对block进行一个copy操作,block会转移到堆中。
    (3)block底层基于C语言,执行效率高---(多核心CPU可直接处理Block指令)
    2.block 3种类型

    1、 _NSConcreteStackBlock ,栈块,如果一个block引用了外部变量,那它就是一个栈块。但是它不会持有外部对象;
    2、_NSConcreteGlobalBlock,全局块,如果一个block没有引用外部变量,它就是全局块;
    3、_NSConcreteMallocBlock,堆块,如果一个block被copy了,那么它就是堆块。
    

    3.循环引用
    因为block会给内部的强指针对象进行一次强引用,比如常见的传入block中的self进行强引用,下面是解释:

    A对象持有B,那么B的retain count 为1,而此时A因为被其他变量
    持有,所有A的retain Count为1,而B又要引用A,那么A的retain 
    count又要加一,这样等到持有A的那个对象释放A的时候,就算A
    的retain count减一,但还是为1,所以无法释放,而因为A无法释
    放,导致B也无法成功释放。```
    如果方法中的一些参数是 成员变量,那么可以造成循环引用,如 GCD 、NSNotificationCenter,比如 GCD 内部如果引用了 self,而且 GCD 的参数是 成员变量,则要考虑到循环引用示例:
    

    GCD
    __weak typeof(self) weakSelf = self;
    dispatch_group_async(_operationsGroup, _operationsQueue, ^{
    [weakSelf doSomething];
    [weakSelf doSomethingElse];
    } );
    通知
    __weak typeof(self) weakSelf = self;
    _observer = [[NSNotificationCenter defaultCenter]
    addObserverForName:@"testKey"
    object:nil
    queue:nil
    usingBlock:^(NSNotification *note){
    [weakSelf dismissModalViewControllerAnimated:YES];
    }];

    解决方法:
    

    __weak typeof(self) weakSelf = self;

    **注意:block对于参数形式传进来的对象,不会强引用。**
    系统的某些block api(如UIView的block版本写动画时),不用考虑循环引用问题。当 block 本身不被 self 持有,而被别的对象持有,同时不产生循环引用的时候,就不需要使用 weak self 了。
    

    //UIView 的某个负责动画的对象持有了 block,block 持有了 self
    //因为 self 并不持有 block,所以就没有循环引用产生,因为就不需要使用 weak self 了。
    [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;
    }];

    **思考:有没有这样一个需求场景,block会产生循环引用,但是业务又需要你不能使用 weak self? 如果有,请举一个例子并且解释这种情况下如何解决循环引用问题。**
    
    >比如说block是一个post请求的回调,这个block被self持有,业务需求需要回调后调用self的一个方法,必须调用,这个时候得保证block调用前self不被释放,就不能weak化self,就有循环引用。解决方法应该是block回调里调用完这个方法后执行下self.block=nil打破循环链就好.
    
    
    #八、多线程(重点说GCD,FIFO)
    - **队列**
    

    GCD 3种队列类型
    1.串行队列,一般用dispatch_queue_create,并指定队列类型DISPATCH_QUEUE_SERIAL;
    2.并行队列,一般指的就是全局队列,调用dispatch_get_global_queue函数传入优先级
    来访问队列。当然也可以用dispatch_queue_create,并指定队列类型DISPATCH_QUEUE_
    CONCURRENT,来自己创建一个并发队列。
    3.主队列,调用dispatch_get_main_queue(),

    - **任务**
    

    同步:使用dispatch_sync将任务加入队列。将同步任务加入串行队列,会顺序执行,一般不
    这样做并且在一个任务未结束时调起其它同步任务会死锁。将同步任务加入并行队列,会顺序执
    行,但是也没什么意义。
    异步:使用dispatch_async将任务加入队列。将异步任务加入串行队列,会顺序执行,并且不
    会出现死锁问题。将异步任务加入并行队列,会并行执行多个任务,这也是我们最常用的一种
    方式。

    **1.Object C中创建线程的方法是什么?如果在主线程中执行代码,方法是什么?如果想延时执行代码,方法又是什么?**
    线程创建有三种方法:使用NSThread创建、使用GCD的dispatch、使用子类化的NSOperation,然后将其加入NSOperationQueue;
    在主线程执行代码,方法是
    

    performSelectorOnMainThread

    如果想延时执行代码可以用
    

    performSelector:onThread:withObject: afterDelay:

    或者使用GCD的函数:
    

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW,(int64_t)(2.0*
    NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    // 2秒后异步执行这里的代码...
    });

    **2.GCD优点**
    (1)GCD自动利用更多的CPU内核
    (2)GCD会自动管理线程的生命周期
    (3)使用简单 任务放到队列中。
    (4)dispatch_queue是线程安全的,可以随意添加任务。
    **3.GCD常用函数**
    (1)dispatch_async 
    

    dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(globalQueue, ^{
    // 一个异步的任务,例如网络请求,耗时的文件操作等等
    ...
    dispatch_async(dispatch_get_main_queue(), ^{
    // UI刷新
    ...
    });
    });
    应用场景:这种用法非常常见,比如开启一个异步的网络请求,待数据返回后返回主队列刷新UI;
    又比如请求图片,待图片返回刷新UI等等。

    (2)dispatch_after
    

    dispatch_queue_t queue= dispatch_get_main_queue();
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC)), queue, ^{
    // 在queue里面延迟执行的一段代码
    ...
    });
    应用场景:这为我们提供了一个简单的延迟执行的方式,比如在view加载结束延迟执行一个动画。

    (3)dispatch_once
    

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
    // 只执行一次的任务
    ...
    });
    应用场景:可以使用其创建一个单例,也可以做一些其他只执行一次的代码,比如做一个只能点一
    次的button。

    (4)dispatch_group
    
    

    //创建队列组
    dispatch_group_t group = dispatch_group_create();
    //获取全局并发队列
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    //往队列组中添加耗时操作
    dispatch_group_async(group, queue, ^{
    // 异步任务1
    });

    dispatch_group_async(group, queue, ^{
    // 异步任务2
    });

    // 等待group中多个异步任务执行完毕,做一些事情,介绍两种方式

    // 方式1(不好,会卡住当前线程)
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    ...

    // 方式2(比较好)
    dispatch_group_notify(group, mainQueue, ^{
    // 如果这里还有基于上面两个任务的结果继续执行一些代码,建议还是放到子线程中,等
    代码执行完毕后在回到主线程
    // 回到主线程
    dispatch_async(group, dispatch_get_main_queue(), ^{
    // 执行相关代码...
    });
    });
    应用场景:上述的一种方式,可以适用于自己维护的一些异步任务的同步问题;但是对于已经封
    装好的一些库,比如AFNetworking等,我们不获取其异步任务的队列,这里可以通过一种计数
    的方式控制任务间同步,下面为解决单界面多接口的一种方式。

    // 两个请求和参数为我项目里面的不用在意。

    // 计数+1
    dispatch_group_enter(group);
    [JDApiService getActivityDetailWithActivityId:self.activityId Location:stockAddressId SuccessBlock:^(NSDictionary *userInfo) {
    // 数据返回后一些处理
    ...

    // 计数-1
    dispatch_group_leave(group);
    

    } FailureBlock:^(NSError *error) {
    // 数据返回后一些处理
    ...

    // 计数-1
    dispatch_group_leave(group);
    

    }];

    // 计数+1
    dispatch_group_enter(group);
    [JDApiService getAllCommentWithActivityId:self.activityId PageSize:3 PageNum:self.commentCurrentPage SuccessBlock:^(NSDictionary *userInfo) {
    // 数据返回后一些处理
    ...

    // 计数-1
    dispatch_group_leave(group);
    

    } FailureBlock:^(NSError *error) {
    // 数据返回后一些处理
    ...

    // 计数-1
    dispatch_group_leave(group);
    

    }];

    // 其实用计数的说法可能不太对,但是就这么理解吧。会在计数为0的时候执行dispatch_group_notify的任务。
    dispatch_group_notify(group, mainQueue, ^{
    // 一般为回主队列刷新UI
    ...
    });

    (5) dispatch_barrier_async
    

    // dispatch_barrier_async的作用可以用一个词概括--承上启下,它保证此前的任务都
    先于自己执行,此后的任务也迟于自己执行。本例中,任务4会在任务1、2、3都执行完之后执行,
    而任务5、6会等待任务4执行完后执行。

    dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{ // 任务1 ...});
    dispatch_async(queue, ^{ // 任务2 ...});
    dispatch_async(queue, ^{ // 任务3 ...});
    dispatch_barrier_async(queue, ^{ // 任务4 ...});
    dispatch_async(queue, ^{ // 任务5 ...});
    dispatch_async(queue, ^{ // 任务6 ...});

    应用场景:和dispatch_group类似,dispatch_barrier也是异步任务间的一种同步方式,可
    以在比如文件的读写操作时使用,保证读操作的准确性.
    注意:
    1.必须是并发队列,要是串行队列,(Global)并发队列、串行队列上,效果跟dispatch_(a)sync效果一样。
    2.这个函数的第一个参数queue不能是全局的并发队列
    3.作用:在它前面的任务执行结束后它才执行,在它后面的任务等它执行完成后才会执在全局

    (6)dispatch_apply
    

    // for循环做一些事情,输出0123456789
    for (int i = 0; i < 10; i ++) {
    NSLog(@"%d", i);
    }

    // dispatch_apply替换(当且仅当处理顺序对处理结果无影响环境),输出顺序不定,比如1098673452
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    /*! dispatch_apply函数说明

    • @brief dispatch_apply函数是dispatch_sync函数和Dispatch Group的关联API
    • 该函数按指定的次数将指定的Block追加到指定的Dispatch Queue中,并等到全部的处理执
      行结束
    • @param 10 指定重复次数 指定10次
    • @param queue 追加对象的Dispatch Queue
    • @param index 带有参数的Block, index的作用是为了按执行的顺序区分各个Block

    */
    dispatch_apply(10, queue, ^(size_t index) {
    NSLog(@"%zu", index);
    });
    应用场景:那么,dispatch_apply有什么用呢,因为dispatch_apply并行的运行机制,
    效率一般快于for循环的类串行机制(在for一次循环中的处理任务很多时差距比较大)。比如这
    可以用来拉取网络数据后提前算出各个控件的大小,防止绘制时计算,提高表单滑动流畅性,如
    果用for循环,耗时较多,并且每个表单的数据没有依赖关系,所以用dispatch_apply比较好。

    **4.死锁**
    - 主线程死锁示例
    
    
    • (void)viewDidLoad{
      [super viewDidLoad];
      NSLog(@"1");
      dispatch_sync(dispatch_get_main_queue(), ^{
      NSLog(@"2");
      });
      NSLog(@"3");
      }
    **(1)dispatch_sync**
    

    // 假设这段代码执行于主队列
    dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);
    dispatch_queue_t mainQueue = dispatch_get_main_queue();

    // 在主队列添加同步任务
    dispatch_sync(mainQueue, ^{
    // 任务 ...
    });
    // 在串行队列添加同步任务
    dispatch_sync(serialQueue, ^{
    // 任务
    ...
    dispatch_sync(serialQueue, ^{
    // 任务
    ...
    });
    };

    **(2)dispatch_apply**
    

    // 因为dispatch_apply会卡住当前线程,内部的dispatch_apply会等待外部,外部的等待
    内部,所以死锁。

    dispatch_apply(10, queue, ^(size_t) {
    // 任务
    ...
    dispatch_apply(10, queue, ^(size_t) {
    // 任务
    ...
    });
    });

    
    **(3)dispatch_barrier**
    

    dispatch_barrier_sync在串行队列和全局并行队列里面和dispatch_sync同样的效果,
    所以需考虑同dispatch_sync一样的死锁问题。

    #九、UI相关
    **1.UIView和CALayer是什么区别和联系?**
    (1)每个 UIView 内部都有一个 CALayer 在背后提供内容的绘制和显示,并且 UIView 的尺寸样式都由内部的 Layer 所提供。
    (2)两者最明显的区别是 View可以接受并处理事件,而 Layer 不可以,UIView继承自UIResponder,可以响应用户事件。
    (3)UIView主要是对显示内容的管理,而 CALayer 主要侧重显示内容的绘制。
    (4)在做 iOS 动画的时候,修改非 RootLayer的属性(譬如位置、背景色等)会默认产生隐式动画,而修改UIView则不会。
    **2.如何高性能的给 UIImageView 加个圆角?**
    使用下面的方式会强制Core Animation提前渲染屏幕的离屏绘制, 而离屏绘制就会给性能带来负面影响,会有卡顿的现象出现.
    

    self.view.layer.cornerRadius = 5;
    self.view.layer.masksToBounds = YES;

    ######正确的解决方案:
    **(1)使用绘图技术**
    
    • (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;
      }
    **(2)使用贝塞尔曲线"切割"个这个图片, 给UIImageView 添加了的圆角,其实也是通过绘图技术来实现的。**
    

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

    **3.如何实现类似QQ的三角形头像**
    (1)Quartz2D
    

    //Quatrz 2D的绘图功能的三个核心概念是什么并简述其作用
    上下文:主要用于描述图形写入哪里;
    路径:是在图层上绘制的内容;
    状态:用于保存配置变换的值、填充和轮廓, alpha 值等。

    (2)使用coreGraphics裁剪出一个三角形
    **4.简述UITableView的复用机制?**
    (1)每次创建cell的时候通过dequeueReusableCellWithIdentifier:方法创建cell,它先到缓存池中找指定标识的cell,如果没有就直接返回nil;
     (2)如果没有找到指定标识的cell,那么会通过
    initWithStyle:reuseIdentifier:创建一个cell;
    (3)当cell离开界面就会被放到缓存池中,以供下次复用。
    #十、开源框架(第三方库)
    **1.描述下SDWebImage里面给UIImageView加载图片的逻辑**
    

    SDWebImage 中为 UIImageView 提供了一个分类UIImageView+WebCache.h, 这个分类
    中有一个最常用的接口sd_setImageWithURL:placeholderImage:,会在真实图片出现前
    会先显示占位图片,当真实图片被加载出来后在替换占位图片.

    加载图片的过程大致如下:
    (1)首先会在 SDWebImageCache 中寻找图片是否有对应的缓存, 它会以url 作为数据的索引
    先在内存中寻找是否有对应的缓存;
    (2)如果缓存未找到就会利用通过MD5处理过的key来继续在磁盘中查询对应的数据, 如果找到了,
    就会把磁盘中的数据加载到内存中,并将图片显示出来;
    (3)如果在内存和磁盘缓存中都没有找到,就会向远程服务器发送请求,开始下载图片;
    (4)下载后的图片会加入缓存中,并写入磁盘中;
    (5)整个获取图片的过程都是在子线程中执行,获取到图片后回到主线程将图片显示出来。

    #十一、屏幕适配(略)
    #十二、TableView滑动优化
    **1.cell复用
    2.如果是cell动态行高,可以计算高度后缓存。
    3.如果不是必要,减少刷新全部cell,只刷新单行。
    4.尽量使用不透明的视图,cell中尽量少使用动画。
    5.加载图片采用异步加载,并且设置图片加载的并发数,(滑动时不加载图片,停止滑动时加载)
    6.文字图片切圆角,可以直接使用drawInRect绘制。**
    #十三、RunTime(运行时)
    **1.简述Objective-C中调用方法的过程(runtime)?**
    

    Objective-C是动态语言,每个方法在运行时会被动态转为消息发送,
    即:objc_msgSend(receiver, selector)。

    过程如下:
    

    1.objc在向一个对象发送消息时,runtime库会根据对象的isa指针找到该对象实际所属的类.
    2.然后在该类中的方法列表以及其父类方法列表中寻找方法运行.
    3.如果,在最顶层的父类(一般也就NSObject)中依然找不到相应的方法时,转向拦截调用。
    4.如果没有写拦截调用的方法,程序报错,抛出异常。

    **2.拦截调用,三次拯救程序崩溃的机会。**
    - Method resolution
    

    1.objc运行时会调用+resolveInstanceMethod:或者 +resolveClassMethod:,让你
    有机会提供一个函数实现。
    2.如果你添加了函数并返回 YES,那运行时系统就会重新启动一次消息发送的过程
    3.如果 resolve 方法返回 NO ,运行时就会移到下一步,消息转发

    - Fast forwarding
    

    1.如果目标对象实现了-forwardingTargetForSelector:,Runtime 这时就会调用这个
    方法,给你把这个消息转发给其他对象的机会
    2.只要这个方法返回的不是nil和self,整个消息发送的过程就会被重启,当然发送的对象会
    变成你返回的那个对象。
    3.否则,就会继续Normal Fowarding。
    4.这里叫Fast,只是为了区别下一步的转发机制。因为这一步不会创建任何新的对象,但
    Normal forwarding转发会创建一个NSInvocation对象,相对Normal forwarding转
    发更快点,所以这里叫Fast forwarding.

    - Normal forwarding
    

    1.这一步是Runtime最后一次给你挽救的机会。
    2.首先它会发送-methodSignatureForSelector:消息获得函数的参数和返回值类型。
    3.如果-methodSignatureForSelector:返回nil,Runtime则会发出
    -doesNotRecognizeSelector:消息,程序这时也就挂掉了。
    4.如果返回了一个函数签名,Runtime就会创建一个NSInvocation对象并发送
    -forwardInvocation:消息给目标对象.

    
    **3.objc中向一个nil对象发送消息将会发生什么?**
    (1) 在Objective-C中向nil发送消息是完全有效的——只是在运行时不会有任何作用.
    (2)如果一个方法的返回值是一个对象/整型变量/结构体/指针类型,那么发送给nil的消息返回0(nil).否则,返回值将是未定义的。
    原因如下:
    

    1.objc是动态语言,每个方法在运行时会被动态转为消息发送,
    即:objc_msgSend(receiver, selector).
    2.objc在向一个对象发送消息时,runtime库会根据对象的isa指针找到该对象实际所属的
    类,然后在该类中的方法列表以及其父类方法列表中寻找方法运行,然后再发送消息的时候,
    objc_msgSend方法不会返回值,所谓的返回内容都是具体调用时执行的。
    3.如果向一个nil对象发送消息,首先在寻找对象的isa指针时就是0地址返回了,所以不会出
    现任何错误.

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

    //weak的特点
    weak策略表明该属性定义了一种“非拥有关系” (nonowning relationship)。为这种属性
    设置新值时,设置方法既不保留新值,也不释放旧值。此特质同assign类似;然而在属性所指
    的对象遭到摧毁时,属性值也会清空(nil out)。

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

    **5.一个Objective-C对象如何进行内存布局?(考虑有父类的情况)**
    

    1.所有父类的成员变量和自己的成员变量都会存放在该对象所对应的存储空间中
    2.父类的方法和自己的方法都会缓存在类对象的方法缓存中,类方法是缓存在元类对象中.
    3.每一个对象内部都有一个isa指针,指向他的类对象,类对象中存放着本对象的如下信息:
    对象方法列表
    成员变量的列表
    属性列表
    4.类对象中也有一个isa指针指向它的元类(meta class),即类对象是元类的实例。元类内
    部存放的是类方法列表,根元类的isa指针指向自己,superclass指针指向NSObject类.
    5.根类对象就是NSObject,它的super class指针指向nil.

    #十四、RunLoop
    [深入理解RunLoop(运行循环)](http://www.jianshu.com/p/1b091c97c0af)
    #十五、app打包上架和SVN/Git协作开发(代码冲突)
    (1)一种版本控制工具。
    (2)优点:集中管理,保证安全;缺点:需要网络支持,容易冲突。
    (3)避免冲突:下载--更新--提交--避免不通人修改同一处代码。
    #十六、即时通讯
    1.[iOS之环信即时通讯实现步骤](http://www.jianshu.com/p/85d3dc78be0d)
    2.[iOS开发融云即时通讯集成详细步骤](http://www.jianshu.com/p/713dbbaef2f8)
    3.[阿里百川SDK集成详细步骤](http://www.jianshu.com/p/7a877fed766b)
    #十七、推送通知
    (1)APP客户端申请推送服务,同时设备向苹果APNS服务器发送请求。
    (2)APNS服务器接受请求,返回deviceToken给你设备上的程序。
    (3)客户端将deviceToken发送给极光后台,后台接受储存,同时向APNS服务器发送通知信息
    (4)APNS服务器将消息发送给deviceToken对应的设备的应用程序。
    #十八、支付
    商户签约 ( 商户ID和账号ID )--下载公私钥(加密签名用)--支付宝SDK--生成订单--调用支付宝客户端--支付宝客户端和内部服务器--支付完毕,返回结果给客户端和服务器
    #十九、JS和OC相互调用
    - [JS和OC的交互(基于UIWebView)](http://www.jianshu.com/p/c3baa6ea60b8)
    - [js oc相互调用的三种方法](http://www.jianshu.com/p/dbddfc0eaa26)
    - [IOS中 使用JavaScriptCore 实现OC与JS的交互](http://www.jianshu.com/p/cdaf9bc3d65d)
    - [UIWebView与JS简单交互(删除标签和获取某个标签的内容)](http://www.jianshu.com/p/e584eaccb3a6)
    
    #二十、核心动画。
    **1.如何使用核心动画?**
      (1)创建
      (2)设置相关属性
      (3)添加到CALayer上,会自动执行动画
    #二十一、图文混排
    - [iOS图文混排](http://www.jianshu.com/p/a8c98e684918)
    - [TextKit 探究](http://www.jianshu.com/p/3f445d7f44d6)
    - [NSAttributedString(图文混排)](http://www.jianshu.com/p/6ec86db11bc1)
    
    #二十二、自定义控件
    **你是怎么封装一个view的?**
    可以通过纯代码或者xib的方式来封装子控件,建立一个跟view相关的模型,然后将模型数据传给view,通过模型上的数据给view的子控件赋值.
    
    

    // 纯代码初始化控件时一定会走这个方法

    • (instancetype)initWithFrame:(CGRect)frame
      {
      if(self = [super initWithFrame:frame])
      {
      [self setup];
      }
      return self;
      }
      // 通过xib初始化控件时一定会走这个方法
    • (id)initWithCoder:(NSCoder *)aDecoder
      {
      if(self = [super initWithCoder:aDecoder])
      {
      [self setup];
      }
      return self;
      }
    • (void)setup
      {
      // 初始化代码
      }
    #二十三、内存泄漏,代码调试
    **1.代码调试**
    log--控制台po--全局断点--条件断点--开启僵尸对象--分析层级结构
    **开启僵尸对象:首先打开“Edit Scheme”,然后选择Diagnostics选项卡,勾选Enable NSZombie Objects选项。**
    #二十四、MVC和MVVM
    [iOS MVVM与MVC](http://www.jianshu.com/p/9a99a93201e3)
    #二十五、响应链和生命周期。
    **1.响应链**
    

    响应者链通常是由视图(UIView)构成的,一个视图的下一个响应者是它的视图控制器
    (如果有的话),然后再传递给它的父视图视图控制器(如果有的话)的下一个响应者为其
    管理的视图的父视图。在视图层次结构的最顶级视图,如果也不能处理收到的事件或者
    消息,就会把事件传递给UIWindow对象进行处理如果window对象也不处理,则将事件
    或消息传递给UIApplication对象,如果UIApplication也不处理则丢弃。
    补充:如何判断上一个响应者?
    如果当前这个view是控制器的view,那么控制器就是上一个响应者
    如果当前这个view不是控制器的view,那么父控件就是上一个响应者

    **2.触摸事件的传递**
    

    触摸事件的传递是从父控件传递到子控件
    如果父控件不能接收触摸事件,那么子控件就不可能接收到触摸事件
    不能接受触摸事件的四种情况
    不接收用户交互,即:userInteractionEnabled = NO
    隐藏,即:hidden = YES
    透明,即:alpha <= 0.01
    未启用,即:enabled = NO
    提示:UIImageView的userInteractionEnabled默认就是NO,因此UIImageView以及它的
    子控件默认是不能接收触摸事件的

    如何找到最合适处理事件的控件:
    首先,判断自己能否接收触摸事件
    可以通过重写hitTest:withEvent:方法验证
    其次,判断触摸点是否在自己身上
    对应方法pointInside:withEvent:
    从后往前(先遍历最后添加的子控件)遍历子控件,重复前面的两个步骤
    如果没有符合条件的子控件,那么就自己处理

    **3.事件分发**
    

    iOS系统检测到手指触摸(Touch)操作时会将其打包成一个UIEvent对象,并放入当前活动
    Application的事件队列,单例的UIApplication会从事件队列中取出触摸事件并传递给
    单例的UIWindow来处理,UIWindow对象首先会使用hitTest:withEvent:方法寻找此次
    Touch操作初始点所在的视图(View),即需要将触摸事件传递给其处理的视图,这个过程称
    之为hit-test view。

    **4.生命周期**
    

    // 自定义控制器view,这个方法只有实现了才会执行

    • (void)loadView
      {
      self.view = [[UIView alloc] init];
      self.view.backgroundColor = [UIColor orangeColor];
      }
      // view是懒加载,只要view加载完毕就调用这个方法
    • (void)viewDidLoad
      {
      [super viewDidLoad];
      NSLog(@"%s",func);
      }
      // view即将显示
    • (void)viewWillAppear:(BOOL)animated
      {
      [super viewWillAppear:animated];
      NSLog(@"%s",func);
      }
      // view即将开始布局子控件
    • (void)viewWillLayoutSubviews
      {
      [super viewWillLayoutSubviews];
      NSLog(@"%s",func);
      }
      // view已经完成子控件的布局
    • (void)viewDidLayoutSubviews
      {
      [super viewDidLayoutSubviews];
      NSLog(@"%s",func);
      }
      // view已经出现
    • (void)viewDidAppear:(BOOL)animated
      {
      [super viewDidAppear:animated];
      NSLog(@"%s",func);
      }
      // view即将消失
    • (void)viewWillDisappear:(BOOL)animated
      {
      [super viewWillDisappear:animated];
      NSLog(@"%s",func);
      }
      // view已经消失
    • (void)viewDidDisappear:(BOOL)animated
      {
      [super viewDidDisappear:animated];
      NSLog(@"%s",func);
      }
      // 收到内存警告
    • (void)didReceiveMemoryWarning
      {
      [super didReceiveMemoryWarning];
      NSLog(@"%s",func);
      }
      // 方法已过期,即将销毁view
    • (void)viewWillUnload
      {
      }
      // 方法已过期,已经销毁view
    • (void)viewDidUnload
      {
      }

    相关文章

      网友评论

          本文标题:iOS 面试题

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