iOS面试了20几家总结出来的面试题(一)

[TOC]前端

##iOS面试题ios

本面试题为我的使用版本,如后续流传出去,请转发的朋友务必注释一下,答案正确性有待商榷,本人的答案不表明权威,仅仅是我的理解。 文章内部有写混乱,将就着看吧。另外大部分图片加载不出来,,MARKDown格式也不太统一(各平台不同),因为博主太懒不想改,不过不影响最终效果,程序员

1、硬技术篇

###1.对象方法和类方法的区别?面试

  • 对象方法能个访问成员变量。
  • 类方法中不能直接调用对象方法,想要调用对象方法,必须建立或者传入对象。
  • 类方法能够和对象方法重名。

引申1. 若是在类方法中调用self 会有什么问题?

  • 在 实例方法中self不能够调用类方法,此时的self不是Class。
  • 在类方法中self能够调用其余类方法。
  • 在类方法中self不能够调用实例方法。
  • 总结:类方法中的self,是class/ 实例方法中self是对象的首地址。

引伸2. 讲一下对象,类对象,元类,跟元类结构体的组成以及他们是如何相关联的?

  • 对象的结构体当中存放着isa指针和成员变量,isa指针指向类对象
  • 类对象的isa指针指向元类,元类的isa指针指向NSObject的元类
  • 类对象和元类的结构体有isa,superClass,cache等等

引伸3. 为何对象方法中没有保存对象结构体里面,而是保存在类对象的结构体里面?

  • 方法是每一个对象相互能够共用的,若是每一个对象都存储一份方法列表太浪费内存,因为对象的isa是指向类对象的,当调用的时候, 直接去类对象中去查找就能够了,节约了不少内存空间。

引伸4. 类方法存在哪里? 为何要有元类的存在?

  • 全部的类自身也是一个对象,咱们能够向这个对象发送消息(即调用类方法)。 为了调用类方法,这个类的isa指针必须指向一个包含这些类方法的一个objc_class结构体。这就引出了meta-class的概念,元类中保存了建立类对象以及类方法所需的全部信息。

引伸5. 什么是野指针?

  • 野指针就是指向一个被释放或者被收回的对象,可是对指向该对象的指针没有作任何修改,以致于该指针让指向已经回收后的内存地址。
  • 其中访问野指针是没有问题的,使用野指针的时候会出现崩溃Crash!样例以下
__unsafe_unretained UIView *testObj = [[UIView alloc] init];
   NSLog(@"testObj 指针指向的地址:%p 指针自己的地址:%p", testObj, &testObj);
   [testObj setNeedsLayout];
   // 能够看到NSlog打印不会闪退,调用[testObj setNeedsLayout];会闪退
复制代码

引伸6. 如何检测野指针?

这是网友总结的,有兴趣的能够看下:www.jianshu.com/p/9fd4dc046… 本人,也就是看看乐呵,其原理啥的,见仁见智吧。开发行业太j8难了!数据库

引伸7. 致使Crash的缘由有哪些?

一、找不到方法的实现unrecognized selector sent to instance 二、KVC形成的crash 三、EXC_BAD_ACCESS 四、KVO引发的崩溃 五、集合类相关崩溃 六、多线程中的崩溃 七、Socket长链接,进入后台没有关闭 八、Watch Dog超时形成的crash 九、后台返回NSNull致使的崩溃,多见于Java作后台服务器开发语言数组

引伸8. 不使用第三方,如何知道已经上线的App崩溃问题, 具体到哪个类的哪个方法的?

大体实现方式以下。浏览器

  • 使用NSSetUncaughtExceptionHandler能够统计闪退的信息。
  • 将统计到的信息以data的形式 利用网络请求发给后台
  • 在后台收集信息,进行排查
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
        // Override point for customization after application launch.
        
        NSSetUncaughtExceptionHandler(&my_uncaught_exception_handler);
        return YES;
    }

    static void my_uncaught_exception_handler (NSException *exception) {
        //这里能够取到 NSException 信息
        NSLog(@"***********************************************");
        NSLog(@"%@",exception);
        NSLog(@"%@",exception.callStackReturnAddresses);
        NSLog(@"%@",exception.callStackSymbols);
        NSLog(@"***********************************************");
    }
复制代码

实现方式如: blog.csdn.net/u013896628/…缓存

iOS中内省的几个方法?

  • isMemberOfClass //对象是不是某个类型的对象
  • isKindOfClass //对象是不是某个类型或某个类型子类的对象
  • isSubclassOfClass //某个类对象是不是另外一个类型的子类
  • isAncestorOfObject //某个类对象是不是另外一个类型的父类
  • respondsToSelector //是否能响应某个方法
  • conformsToProtocol //是否遵循某个协议

引伸 2. ==、 isEqualToString、isEqual区别?

  • == ,比较的是两个指针的值 (内存地址是否相同)。
  • isEqualToString, 比较的是两个字符串是否相等。
  • isEqual 判断两个对象在类型和值上是否都同样。

引伸 3. class方法和object_getClass方法有什么区别?

  • 实例class方法直接返回object_getClass(self)
  • 类class直接返回self
  • 而object_getClass(类对象),则返回的是元类

3.深拷贝和浅拷贝

  • 所谓深浅指的是是否建立了一个新的对象(开辟了新的内存地址)仍是仅仅作了指针的复制。
  • copy和mutableCopy针对的是可变和不可变,凡涉及copy结果均变成不可变,mutableCopy均变成可变。
  • mutableCopy均是深复制。
  • copy操做不可变的是浅复制,操做可变的是深赋值。

4.NSString类型为何要用copy修饰 ?

  • 主要是防止NSString被修改,若是没有修改的说法用Strong也行。
  • 当NSString的赋值来源是NSString时,strong和copy做用相同。
  • 当NSString的赋值来源是NSMutableString,copy会作深拷贝,从新生成一个新的对象,修改赋值来源不会影响NSString的值。

5.iOS中block 捕获外部局部变量实际上发生了什么?__block 中又作了什么?

  • block 捕获的是当前在block内部执行的外部局部变量的瞬时值, 为何说瞬时值呢? 看一下C++源码中得知, 其内部代码在捕获的同时安全

  • 其实block底层生成了一个和外部变量相同名称的属性值若是内部修改值,其实修改的是捕获以前的值,其捕获的内部的值因代码只作了一次捕获,并无作再一次的捕获,因此block里面不能够修改值。bash

  • 若是当前捕获的为对象类型,其block内部能够认为从新建立了一个指向当前对象内存地址的指针(堆),操控内部操做的东西均为同一块内存地址,因此能够修改当前内部的对象里面的属性,可是不能直接修改当前的指针(没法直接修改栈中的内容)(即从新生成一个新的内存地址)。其原理和捕获基本数据类型一致。

  • 说白了, block内部能够修改的是堆中的内容, 但不能直接修改栈中的任何东西。


  • 若是加上__block 在运行时建立了一个外部变量的“副本”属性,把栈中的内存地址放到了堆中进而在block内部也能修改外部变量的值。

6.iOS Block为何用copy修饰?

  • block 是一个对象
  • MRC的时候 block 在建立的时候,它的内存比较奇葩,非得分配到栈上,而不是在传统的堆上,它自己的做用于就属于建立的时候(见光死,夭折),一旦在建立时候的做用于外面调用它会致使崩溃。
  • 因此,利用copy把本来在栈上的复制到堆里面,就保住了它。
  • **ARC的时候 因为ARC中已经看不到栈中的block了。用strong和copy 同样 随意, 用copy是遵循其传统, **

7. 为何分类中不能建立属性Property(runtime除外)?

  • 分类的实现原理是将category中的方法,属性,协议数据放在category_t结构体中,而后将结构体内的方法列表拷贝到类对象的方法列表中。 Category能够添加属性,可是并不会自动生成成员变量及set/get方法。由于category_t结构体中并不存在成员变量。经过以前对对象的分析咱们知道成员变量是存放在实例对象中的,而且编译的那一刻就已经决定好了。而分类是在运行时才去加载的。那么咱们就没法再程序运行时将分类的成员变量中添加到实例对象的结构体中。所以分类中不能够添加成员变量。

  • 在往深一点的回答就是 类在内存中的位置是编译时期决定的, 以后再修改代码也不会改变内存中的位置,class_ro_t 的属性在运行期间就不能再改变了, 再添加方法是会修改class_rw_t 的methods 而不是class_ro_t 中的 baseMethods

引申:分类能够添加那些内容?
  • 实例方法,类方法,协议,属性
引申:Category 的实现原理?
  • Category 在刚刚编译完成的时候, 和原来的类是分开的,只有在程序运行起来的时候, 经过runtime合并在一块儿。
引伸 使用runtime Associate方法关联的对象,须要在主对象dealloc的时候释放么?
  • 不须要,被关联的对象的生命周期内要比对象自己释放晚不少, 它们会在被 NSObject -dealloc 调用的 object_dispose() 方法中释放。
引伸 可否向编译后获得的类中增长实例变量, 可否向运行时建立的类中添加实力变量?
  • 不能再编译后获得的类中增长实例变量。由于编译后的类已经注册在runtime中, 类结构体中objc_ivar_list 实例变量的链表和objc_ivar_list 实例变量的内存大小已经肯定,因此不能向存在的类中添加实例变量
  • 能在运行时建立的类中添加实力变量。调用class_addIvar 函数
引伸 主类执行了foo方法,分类也执行了foo方法,在执行的地方执行了foo方法,主类的foo会被覆盖么? 若是想只想执行主类的foo方法,如何去作?
  • 主类的方法被分类的foo覆盖了,其实分类并无覆盖主类的foo方法,只是分类的方法排在方法列表前面,主类的方法列表被挤到了后面, 调用的时候会首先找到第一次出现的方法。
  • 若是想要只是执行主类的方法,可逆序遍历方法列表,第一次遍历到的foo方法就是主类的方法
- (void)foo{   
  [类 invokeOriginalMethod:self selector:_cmd];
}

+ (void)invokeOriginalMethod:(id)target selector:(SEL)selector {
    uint count;
    Method *list = class_copyMethodList([target class], &count);
    for ( int i = count - 1 ; i >= 0; i--) {
        Method method = list[i];
        SEL name = method_getName(method);
        IMP imp = method_getImplementation(method);
        if (name == selector) {
            ((void (*)(id, SEL))imp)(target, name);
            break;
        }
    }
    free(list);
}
复制代码

8. load 和 initilze 的调用状况,以及子类的调用顺序问题?

  • initialize 这个方法是第一次给某给类发送消息的时候调用,而且只会调用一次。 若是某一个类一直没有被用到,此方法也不会执行。
  • initialize先初始化父类, 在初始化子类,子类的initialize 会覆盖父类的方法。
  • 分类中实现了initialize会覆盖原本的initialize方法,若是多个分类都执行了initialize ,那么只是执行最后编译的那个。

  • load当程序被加载的时候就会调用, 其加载顺序为, 若是子类实现类load 先执行父类 -> 在执行子类,而分类的在最后执行。
  • 若是子类不实现load,父类的load就不会被执行。
  • load是线程安全的,其内部使用了锁,因此咱们应该避免在load方法中线程阻塞。
  • load在分类中,重写了load方法, 不会影响其主类的方法。即不会覆盖本类的load方法
  • 当有多个类的时候,每一个类的load的执行顺序和编译顺序一致。

###9. 什么是线程安全?

  • 多条线程同时访问一段代码,不会形成数据混乱的状况

###10. 你接触到的项目,哪些场景运用到了线程安全?

答: 举例说明,12306 同一列火车的车票, 同一时间段多人抢票! 如何解决 互斥锁使用格式

synchronized(锁对象) { // 须要锁定的代码 }
注意:锁定1份代码只用1把锁,用多把锁是无效的

Tips: 互斥锁的优缺点
优势:能有效防止因多线程抢夺资源形成的数据安全问题
缺点:须要消耗大量的CPU资源
 
互斥锁的使用前提:多条线程抢夺同一块资源 
相关专业术语:线程同步,多条线程按顺序地执行任务
互斥锁,就是使用了线程同步技术
 
Objective-C中的原子和非原子属性
OC在定义属性时有nonatomic和atomic两种选择
atomic:原子属性,为setter/getter方法都加锁(默认就是atomic)
nonatomic:非原子属性,不加锁
 
atomic加锁原理:
property (assign, atomic) int age;
 - (void)setAge:(int)age
{ 
    @synchronized(self) {  
       _age = age;
    }
}

- (int)age {
	int age1 = 0;
	@synchronized(self) {
		age1 = _age;
	}
}


原子和非原子属性的选择
nonatomic和atomic对比
atomic:线程安全,须要消耗大量的资源
nonatomic:非线程安全,适合内存小的移动设备
 
iOS开发的建议
全部属性都声明为nonatomic
尽可能避免多线程抢夺同一块资源
尽可能将加锁、资源抢夺的业务逻辑交给服务器端处理,减少移动客户端的压力

atomic就必定能保证线程安全么?
不能,还须要更深层的锁定机制才能够,由于一个线程在连续屡次读取某条属性值的时候,与此同时别的线程也在改写值,这样仍是会读取到不一样的属性值!  或者 一个线程在获取当前属性的值, 另一个线程把这个属性释放调了, 有可能形成崩溃

复制代码

###11. 你实现过单例模式么? 你能用几种实现方案?

1. 运用GCD:
import "Manager.h"
implementation Manager
+ (Manager *)sharedManager {
  static dispatch_once_t onceToken;
  static Manager * sharedManager;
  dispatch_once(&onceToken, ^{
    sharedManager=[[Manager alloc] init];
  });
  return sharedManager;
}
end
注明:dispatch_once这个函数,它能够保证整个应用程序生命周期中某段代码只被执行一次!


2. 不使用GCD的方式:
static Manager *manager;
implementation Manager
+ (Manager *)defaultManager {
    if(!manager)
        manager=[[self allocWithZone:NULL] init];
    return  manager;
}
end

3. 正常的完整版本
+(id)shareInstance{
     static dispatch_once_t onceToken;
      dispatch_once(&onceToken, ^{
      if(_instance == nil)
            _instance = [MyClass alloc] init]; 
    });
     return _instance;
}

//重写allocWithZone,里面实现跟方法一,方法二一致就行.
+(id)allocWithZone:(struct _NSZone *)zone{
   return [self shareInstance];
} 

//保证copy时相同
-(id)copyWithZone:(NSZone *)zone{  
    return _instance;  
} 
// 方法3建立的目的的是 为了方式开发者在调用单例的时候并无用shareInstance方法来建立 而是用的alloc 或者copy的形式建立形成单例不一致的状况

// 
复制代码

引伸1. 单例是怎么销毁的?

//必须把static dispatch_once_t onceToken; 这个拿到函数体外,成为全局的.
+ (void)attempDealloc {
    onceToken = 0; // 只有置成0,GCD才会认为它从未执行过.它默认为0,这样才能保证下次再次调用shareInstance的时候,再次建立对象.
    _sharedInstance = nil;
}

dispatch_once_t 的工做原理是,static修饰会默认将其初始化为0, 当且仅当其为0的时候dispatch_once(&onceToken, ^{})这个函数才能被调用, 若是执行了这个函数  这个dispatch_once_t 静态变成- 1了  就永远不会被调用

复制代码

引伸2. 不使用dispatch_once 如何 实现单例

1.第一种方式,重写+allocWithZone:方法;
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
    static id instance = nil;
    @synchronized (self) { // 互斥锁
        if (instance == nil) {
            instance = [super allocWithZone:zone];
        }
    }
    return instance;
}

2.第二种方式,不用重写+allocWithZone:方法,而是直接用@synchronized 来保证线程安全,其它与上面这个方法同样;
+ (instancetype)sharedSingleton {
    static id instance = nil;
    @synchronized (self) {
        if (!instance) {
            instance = [[self alloc] init];
        }
    }
    return instance;
}
复制代码

###12. 项目开发中,你用单例都作了什么?

答 :整个程序公用一份资源的时候 例如 :

  • 设置单例类访问应用的配置信息
  • 用户的我的信息登陆后用的NSUserDefaults存储,对登陆类进一步采用单例封装方便全局访问
  • 防止一个单例对 应用 多处 对赞成本地数据库存进行操做

13.APNS的基本原理

  • 基本
  • 第一阶段:应用程序的服务器端把要发送的消息、目的iPhone的标识打包,发给APNS。
  • 第二阶段:APNS在自身的已注册Push服务的iPhone列表中,查找有相应标识的iPhone,并把消息发送到iPhone。
  • 第三阶段:iPhone把发来的消息传递给相应的应用程序,而且按照设定弹出Push通知。
  • 详细说明

首先是注册

  • Device(设备)链接APNs服务器并携带设备序列号(UUID)
  • 链接成功,APNs通过打包和处理产生devicetoken并返回给注册的Device(设备)
  • Device(设备)携带获取的devicetoken发送到咱们本身的应用服务器
  • 完成须要被推送的Device(设备)在APNs服务器和咱们本身的应用服务器的注册

推送过程

  • 一、首先手机装有当前的app,而且保持有网络的状况下,APNs服务器会验证devicetoken,成功那个以后会处于一个长链接。 (这里会有面试问? 若是app也注册成功了, 也下载了,也赞成了打开推送功能, 这个时候在把App删除了, 还能接受推送了么? )
  • 二、当咱们推送消息的时候,咱们的服务器按照指定格式进行打包,结合devicetoken 一块儿发送给APNs服务器,
  • 三、APNs 服务器将新消息推送到iOS 设备上,而后在设备屏幕上显示出推送的消息。
  • 四、iOS设备收到推送消息后, 会通知给咱们的应用程序并给予提示

// 推送过程以下图

Alt text

###14. RunLoop的基础知识

  • RunLoop模式有哪些?

答 : iOS中有五种RunLoop模式

NSDefaultRunLoopMode (默认模式,有事件响应的时候,会阻塞旧事件)
NSRunLoopCommonModes (普通模式,不会影响任何事件)
UITrackingRunLoopMode (只能是有事件的时候才会响应的模式)

还有两种系统级别的模式
一个是app刚启动的时候会执行一次
另一个是系统检测app各类事件的模式

复制代码
  • RunLoop的基本执行原理

答 : 本来系统就有一个runloop在检测App内部的行为或事件,当输入源(用户的直接或者间接的操做)有“执行操做”的时候, 系统的runloop会监听输入源的状态, 进而在系统内部作一些对应的相应操做。 处理完成后,会自动回到睡眠状态, 等待下一次被唤醒,

  • RunLoop和线程的关系

  • RunLoop的做用就是用来管理线程的, 当线程的RunLoop开启以后,线程就会在执行完成任务后,进入休眠状态,随时等待接收新的任务,而不是退出。

  • 为何只有主线程的runloop是开启的

  • 程序开启以后,要一直运行,不会退出。 说白了就是为了让程序不死


如何保证一个线程永远不死(常驻线程)

// 先建立一个线程用于测试
	 NSThread *thread = [[NSThread alloc]  initWithTarget:self selector:@selector(play) object:nil];
    [thread start];

    // 保证一个线程永远不死
    [[NSRunLoop currentRunLoop] addPort:[NSPort port] -forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] run];
    
    
    // 在合适的地方处理线程的事件处理
    [self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:NO];
    
复制代码

###15. weak属性?

1. 说说你理解weak属性?
复制代码
1.实现weak后,为何对象释放后会自动为nil?

runtime 对注册的类, 会进行布局,
对于 weak 对象会放入一个 hash 表中。 
用 weak 指向的对象内存地址做为 key,
Value是weak指针的地址数组。
当释放的时候,其内部会经过当前的key找到全部的weak指针指向的数组
而后遍历这个数组把其中的数据设置为nil。


稍微详细的说:在内部底层源码也同时和当前对象相关联得SideTable, 其内部有三个属性, 一个是一把自旋锁,一个是引用计数器相关,一个是维护weak生命得属性得表
**SideTable**这个结构体同样的东西,能够花半个小时看一眼。
复制代码

延伸

  • objc中向一个nil对象发送消息将会发生什么? 首先 在寻找对象化的isa指针时就是0地址返回了, 因此不会有任何错误, 也不会错误

  • objc在向一个对象发送消息时,发生了什么?

- 首先是经过obj 的isa指针找到对应的class
  - 先去操做对象中的缓存方法列表中objc_cache中去寻找 当前方法,若是找到就直接实现对应IMP
  - 若是在缓存中找不到,则在class中找到对用的Method list中对用foo
  - 若是class中没有找到对应的foo, 就会去superClass中去找
  - 若是找到了对应的foo, 就会实现foo对应的IMP

  缓存方法列表, 就是每次执行这个方法的时候都会作如此繁琐的操做这样太过于消耗性能,因此出现了一个objc_cache,这个会把当前调用过的类中的方法作一个缓存, 当前method_name做为key, method_IMP做为Value,当再一次接收到消息的时候,直接经过objc_cache去找到对应的foo的IMP便可, 避免每一次都去遍历objc_method_list

若是一直没有找到方法, 就会专用消息转发机制,机制以下

// 动态方法解析和转发
上面的例子若是foo函数一直没有被找到,一般状况下,会出现报错,可是在报错以前,OC的运行时给了咱们三次补救的机会

- Method resolution
- Fast forwarding
- Normal forwarding
 
 
1. Runtime 会发送 +resolveInstanceMethod: 或者 +resolveClassMethod: 尝试去 resolve(重启) 这个消息;
2. 若是 resolve 方法返回 NO,Runtime 就发送 -forwardingTargetForSelector: 容许你把这个消息转发给另外一个对象;
3. 若是没有新的目标对象返回, Runtime 就会发送 -methodSignatureForSelector: 和 -forwardInvocation: 消息。你能够发送 -invokeWithTarget: 消息来手动转发消息或者发送 -doesNotRecognizeSelector: 抛出异常。

复制代码

16.UIView和CALayer是什么关系?

- 二者最明显的区别是 View能够接受并处理事件,而 Layer 不能够;
 	- 每一个 UIView 内部都有一个 CALayer 在背后提供内容的绘制和显示,而且 UIView 的尺寸样式都由内部的 Layer 所提供。二者都有树状层级结构,layer 内部有 SubLayers,View 内部有 SubViews.可是 Layer 比 View 多了个AnchorPoint
	- 在 View显示的时候,UIView 作为 Layer 的 CALayerDelegate,View 的显示内容由内部的 CALayer 的 display 
CALayer 是默认修改属性支持隐式动画的,在给 UIView 的 Layer 作动画的时候,View 做为 Layer 的代理,Layer 经过 actionForLayer:forKey:向 View请求相应的 action(动画行为)
	- layer 内部维护着三分 layer tree,分别是 presentLayer Tree(动画树),modeLayer Tree(模型树), Render Tree (渲染树),在作 iOS动画的时候,咱们修改动画的属性,在动画的实际上是 Layer 的 presentLayer的属性值,而最终展现在界面上的实际上是提供 View的modelLayer
复制代码

16. @synthesize 和 @dynamic 分别有什么做用

- @property有两个对应的词,一个是 @synthesize,一个是 @dynamic。若是 @synthesize和 @dynamic都没写,那么默认的就是@syntheszie var = _var;
- @synthesize 的语义是若是你没有手动实现 setter 方法和 getter 方法,那么编译器会自动为你加上这两个方法。

- @dynamic 告诉编译器:属性的 setter 与 getter 方法由用户本身实现,不自动生成。(固然对于 readonly 的属性只需提供 getter 便可)。假如一个属性被声明为 @dynamic var,而后你没有提供 @setter方法和 @getter 方法,编译的时候没问题,可是当程序运行到 instance.var = someVar,因为缺 setter 方法会致使程序崩溃;或者当运行到 someVar = var 时,因为缺 getter 方法一样会致使崩溃。编译时没问题,运行时才执行相应的方法,这就是所谓的动态绑定。
复制代码

17. static有什么做用?

static关键字能够修饰函数和变量,做用以下:

**隐藏**

经过static修饰的函数或者变量,在该文件中,全部位于这条语句以后的函数均可以访问,而其余文件中的方法和函数则不行

**静态变量**

类方法不能够访问实例变量(函数),经过static修饰的实例变量(函数),能够被类	方法访问;

**持久**

static修饰的变量,能且只能被初始化一次;

**默认初始化**

static修饰的变量,默认初始化为0;
复制代码

18. objc在向一个对象发送消息时,发生了什么?

- objc_msgSend(recicver, selecter..)
复制代码

19. runloop是来作什么的?runloop和线程有什么关系?主线程默认开启了runloop么?子线程呢?

1. runloop与线程是一一对应的,一个runloop对应一个核心的线程,为何说是核心的,是由于runloop是能够嵌套的,可是核心的只能有一个,他们的关系保存在一个全局的字典里。
2. runloop是来管理线程的,当线程的runloop被开启后,线程会在执行完任务后进入休眠状态,有了任务就会被唤醒去执行任务。runloop在第一次获取时被建立,在线程结束时被销毁。
3. 对于主线程来讲,runloop在程序一启动就默认建立好了。
4. 对于子线程来讲, runloop是懒加载的,只有当咱们使用的时候才会建立,因此在子线程用定时器要注意:确保子线程的runloop被开启,否则定时器不会回调。	
复制代码

20. 如何手动触发一个value的KVO

键值观察通知依赖于 NSObject 的两个方法: willChangeValueForKey: 和 didChangevlueForKey: 。在一个被观察属性发生改变以前, willChangeValueForKey: 必定会被调用,这就 会记录旧的值。而当改变发生后, didChangeValueForKey: 会被调用,继而 observeValueForKey:ofObject:change:context: 也会被调用。若是能够手动实现这些调用,就能够实现“手动触发”了。

引伸 0 如何给系统KVO设置筛选条件?

  • 举例:取消Person类age属性的默认KVO,设置age大于18时,手动触发KVO
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    if ([key isEqualToString:@"age"]) {
        return NO;
    }
    return [super automaticallyNotifiesObserversForKey:key];
}

- (void)setAge:(NSInteger)age {
    if (age >= 18) {
        [self willChangeValueForKey:@"age"];
        _age = age;
        [self didChangeValueForKey:@"age"];
    }else {
        _age = age;
    }
}
复制代码

引伸 1.经过KVC修改属性会触发KVO么?直接修改为员变量呢 ?

  • 会触发KVO。即便没有声明属性,只有成员变量,只要accessInstanceVariablesDirectly返回的是YES,容许访问其成员变量,那么无论有没有调用setter方法,经过KVC修改为员变量的值,都能触发KVO。这也说明经过KVC内部实现了willChangeValueForKey:方法和didChangeValueForKey:方法
  • 直接修改为员变量不会触发KVO。直接修改为员变量内部并无作处理只是单纯的赋值,因此不会触发。

引伸 kvc的底层实现?

  • 赋值方法setValue:forKey:的原理

(1)首先会按照顺序依次查找setKey:方法和_setKey:方法,只要找到这两个方法当中的任何一个就直接传递参数,调用方法;

(2)若是没有找到setKey:和_setKey:方法,那么这个时候会查看accessInstanceVariablesDirectly方法的返回值,若是返回的是NO(也就是不容许直接访问成员变量),那么会调用setValue:forUndefineKey:方法,并抛出异常“NSUnknownKeyException”;

(3)若是accessInstanceVariablesDirectly方法返回的是YES,也就是说能够访问其成员变量,那么就会按照顺序依次查找 _key、_isKey、key、isKey这四个成员变量,若是查找到了,就直接赋值;若是依然没有查到,那么会调用setValue:forUndefineKey:方法,并抛出异常“NSUnknownKeyException”。

  • 取值方法valueForKey:的原理

(1)首先会按照顺序依次查找getKey:、key、isKey、_key:这四个方法,只要找到这四个方法当中的任何一个就直接调用该方法;

(2)若是没有找到,那么这个时候会查看accessInstanceVariablesDirectly方法的返回值,若是返回的是NO(也就是不容许直接访问成员变量),那么会调用valueforUndefineKey:方法,并抛出异常“NSUnknownKeyException”;

(3)若是accessInstanceVariablesDirectly方法返回的是YES,也就是说能够访问其成员变量,那么就会按照顺序依次查找 _key、_isKey、key、isKey这四个成员变量,若是找到了,就直接取值;若是依然没有找到成员变量,那么会调用valueforUndefineKey方法,并抛出异常“NSUnknownKeyException”。

21. 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上消失。
复制代码

###22.网络协议

  • TCP三次握手和四次挥手?

三次握手

**1.**客户端向服务端发起请求连接,首先发送SYN报文,SYN=1,seq=x,而且客户端进入SYN_SENT状态 **2.**服务端收到请求连接,服务端向客户端进行回复,并发送响应报文,SYN=1,seq=y,ACK=1,ack=x+1,而且服务端进入到SYN_RCVD状态 **3.**客户端收到确认报文后,向服务端发送确认报文,ACK=1,ack=y+1,此时客户端进入到ESTABLISHED,服务端收到用户端发送过来的确认报文后,也进入到ESTABLISHED状态,此时连接建立成功

- 哎!
- 嗯
- 给你 
复制代码

为何须要三次握手: 为了防止已失效的链接请求报文段忽然又传送到了服务端,于是产生错误。假设这是一个早已失效的报文段,但server收到此失效的链接请求报文段后,就误认为是client再次发出的一个新的链接请求。因而就向client发出确认报文段,赞成创建链接。假设不采用“三次握手”,那么只要server发出确认,新的链接就创建了。因为如今client并无发出创建链接的请求,所以不会理睬server的确认,也不会向server发送数据。但server却觉得新的运输链接已经创建,并一直等待client发来数据。这样,server的不少资源就白白浪费掉了。

四次挥手

**1.**客户端向服务端发起关闭连接,并中止发送数据 **2.**服务端收到关闭连接的请求时,向客户端发送回应,我知道了,而后中止接收数据 **3.**当服务端发送数据结束以后,向客户端发起关闭连接,并中止发送数据 **4.**客户端收到关闭连接的请求时,向服务端发送回应,我知道了,而后中止接收数据

- 哎!
- 嗯
- 关了
- 好的
复制代码

为何须要四次挥手: 由于TCP是全双工通讯的,在接收到客户端的关闭请求时,还可能在向客户端发送着数据,所以不能再回应关闭连接的请求时,同时发送关闭连接的请求

引伸

  1. HTTP和HTTPS有什么区别?

    • HTTP协议是一种使用明文数据传输的网络协议。
    • HTTPS协议能够理解为HTTP协议的升级,就是在HTTP的基础上增长了数据加密。在数据进行传输以前,对数据进行加密,而后再发送到服务器。这样,就算数据被第三者所截获,可是因为数据是加密的,因此你的我的信息让然是安全的。这就是HTTP和HTTPS的最大区别。
  2. HTTPS的加密方式?

    • Https采用对称加密和非对称加密结合的方式来进行通讯。

    • Https不是应用层的新协议,而是Http通讯接口用SSL和TLS来增强加密和认证机制。

      • 对称加密: 加密和解密都是同一个钥匙
      • 非对称加密:密钥承兑出现,分为公钥和私钥,公钥加密须要私钥解密,私钥加密须要公钥解密

HTTP和HTTPS的创建链接的过程?

HTTP

  • 创建连接完毕之后客户端会发送响应给服务器
  • 服务端接受请求而且作出响应发送给客户端
  • 客户端收到响应而且解析响应给客户

HTTPS

  • 在使用HTTPS是须要保证服务端配置了正确的对应的安全证书
  • 客户端发送请求到服务器
  • 服务端返回公钥和证书到客户端
  • 客户端接受后,会验证证书的安全性,若是经过则会随机生成一个随机数,用公钥对其解密, 发送到服务端
  • 服务端接受到这个加密后的随机数后,会用私钥对其进行揭秘,获得真正的随机数,而后调用这个随机数看成私钥对须要发送的数据进行对称加密。
  • 客户端接收到加密后的数据使用私钥(以前生成的随机值)对数据进行解密,而且解析数据呈现给客户

HTTP协议中GET和POST的区别

  • GET在特定的浏览器和服务器对URL的长度是有限制的。 可是理论上是没有限制的

  • POST不是经过URL进行传值,理论上不受限制。

  • GET会把请求参数拼接到URL后面, 不安全,

  • POST把参数放到请求体里面, 会比GET相对安全一点, 可是因为能够窥探数据, 因此也不安全, 想更安全用加密。

  • GET比POST的请求速度快。缘由:Post请求的过程, 会现将请求头发送给服务器确认,而后才真正的发送数据, 而Get请求 过程会在连接创建后会将请求头和数据一块儿发送给服务器。 中间少了一步。 因此get比post 快

  • post的请求过程

  • 三次握手以后 第三次会把post请求头发送

  • 服务器返回100 continue响应

  • 浏览器开始发送数据

  • 服务器返回200 ok响应


  • get请求过程
  • 三次握手以后 第三次会发送get请求头和数据
  • 服务器返回200 ok响应

###23. 有没有使用过performSelector?

  • 这题主要是想问的是有没有动态添加过方法
  • 话很少说上代码
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    Person *p = [[Person alloc] init];

    // 默认person,没有实现eat方法,能够经过performSelector调用,可是会报错。
    // 动态添加方法就不会报错
    [p performSelector:@selector(eat)];
}

@end


@implementation Person

// **这里真是奇葩, 实在想不到何时才有这种使用场景, 我再外面找不到方法, 我再当前类里面直接在写一个方法就好咯,干吗要在这里写这个玩意, 还要写一个C语言的东西, 既然面试想问, 那咱就要会!**

// void(*)()
// 默认方法都有两个隐式参数,
void eat(id self,SEL sel)
{
    NSLog(@"%@ %@",self,NSStringFromSelector(sel));
}

// 当一个对象调用未实现的方法,会调用这个方法处理,而且会把对应的方法列表传过来.
// 恰好能够用来判断,未实现的方法是否是咱们想要动态添加的方法
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    if (sel == @selector(eat)) {
        // 动态添加eat方法

        // 第一个参数:给哪一个类添加方法
        // 第二个参数:添加方法的方法编号
        // 第三个参数:添加方法的函数实现(函数地址)
        // 第四个参数:函数的类型,(返回值+参数类型) v:void @:对象->self :表示SEL->_cmd
        class_addMethod(self, @selector(eat), eat, "v@:");
    }
    return [super resolveInstanceMethod:sel];
}
@end
复制代码
  • 固然面试的时候也可能问你这个
// 延时操做 和GCD的after 一个效果
[p performSelector:@selector(eat) withObject:nil afterDelay:4];
复制代码
  • 你觉得完了? 错了,大几率面试官会问你,*** 上面这段代码放在子线程中 是什么样子的?为何?**

    —首先 上面这个方法其实就是内部建立了一个NSTimer定时器,而后这个定时器会添加在当前的RunLoop中因此上面代码放到子线程中不会有任何定时器相关方法被执行,若是想要执行,开启当前线程便可 即

[[NSRunLoop currentRunLoop] run];
复制代码
// 完整调用
 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(queue, ^{
        // [[NSRunLoop currentRunLoop] run]; 放在上面执行时不能够的,由于当前只是开启了runloop 里面没有任何事件(source,timer,observer)也是开启失败的
         [self performSelector:@selector(test) withObject:nil afterDelay:2];
         [[NSRunLoop currentRunLoop] run];
});

// 由此我自行又作了一个测试, 把 
[self performSelector:@selector(test)];
在子线程调用,是没有任何问题的。

// 我又测试了一下,
 [self performSelector:@selector(test) withObject:nil afterDelay:2];
 这个方法在主线程执行  打印线程是1

在子线程中调用打印线程 非1
复制代码
  • 而后面试官开始飘了, 开始问你关于NSTimer相关问题?怎么办? 答: 搞他!

引伸 NSTimer在子线程执行?

  • NSTimer直接在在子线程是不会被调用的, 想要执行请开启当前的Runloop 。具体开启方案上面题有说,不赘述。

引伸 为何说NSTimer不许确?

  • NSTimer的触发时间到的时候,runloop若是在阻塞状态,触发时间就会推迟到下一个runloop周期 减小偏差的方法 代码以下
// 在子线程中开启NStimer,或者更改当前Runloop的Mode 为NSRunLoopCommonModes
[[NSRunLoop mainRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];

// 利用CADisplayLink (iOS设备的屏幕刷新频率是固定的,CADisplayLink在正常状况下会在每次刷新结束都被调用,精确度至关高)
CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(logInfo)];
[displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];

// 利用GCD
NSTimeInterval interval = 1.0;
_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0));
dispatch_source_set_timer(_timer, dispatch_walltime(NULL, 0), interval * NSEC_PER_SEC, 0);
dispatch_source_set_event_handler(_timer, ^{
    NSLog(@"GCD timer test");
});
dispatch_resume(_timer);
复制代码

###24. 为何AFN3.0中须要设置self.operationQueue.maxConcurrentOperationCount = 1;而AF2.0却不须要?

  • 功能不同, 2.x是基于NSURLConnection的,其内部实现要在异步并发,因此不能设置1。 3.0 是基于NSURLSession其内部是须要串行的鉴于一些多线程数据访问的安全性考虑, 设置这个达到串行回调的效果。

AFNetworking 2.0 和3.0 的区别?

  • AFN3.0剔除了全部的NSURLConnection请求的API
  • AFN3.0使用NSOperationQueue代替AFN2.0的常驻线程

2.x版本常驻线程的分析

  • 在请求完成后咱们须要对数据进行一些序列化处理,或者错误处理。若是咱们在主线中处理这些事情很明显是不合理的。不只会致使UI的卡顿,甚至受到默认的RunLoopModel的影响,咱们在滑动tableview的时候,会致使时间的处理中止。

  • 这里时候咱们就须要一个子线程来处理事件和网络请求的回调了。可是,子线程在处理完事件后就会自动结束生命周期,这个时候后面的一些网络请求得回调咱们就没法接收了。因此咱们就须要开启子线程的RunLoop来保存线程的常驻。

  • 固然咱们能够每次发起一个请求就开启一条子线程,可是这个想一下就知道开销有多大了。因此这个时候保活一条线程来对请求得回调处理是比较好的一个方案。

3.x版本不在常驻线程的分析?

  • 在3.x的AFN版本中使用的是NSURLSession进行封装。对比于NSURLConnection,NSURLSession不须要在当前的线程等待网络回调,而是可让开发者本身设定须要回调的队列。

  • 因此在3.x版本中AFN使用了NSOperationQueue对网络回调的管理,而且设置maxConcurrentOperationCount为1,保证了最大的并发数为1,也就是说让网络请求串行执行。避免了多线程环境下的资源抢夺问题。

###25. autoreleasePool 在什么时候被释放?

  • ARC中全部的新生对象都是 自动加autorelese的, @atuorelesepool 大部分时候解决了瞬时内存暴增的问题 。
  • MRC中的状况 关键词变了NSAutoreleasePool。
//来自Apple文档,见参考
NSArray *urls = <# An array of file URLs #>;
for (NSURL *url in urls) { 
  @autoreleasepool { 
		NSError *error;
		NSString *fileContents = [NSString stringWithContentsOfURL:urlencoding:NSUTF8StringEncoding error:&error]; 
}

// 若是循环次数很是多,并且循环体里面的对象都是临时建立使用的,就能够用@autoreleasepool 包起来,让每次循环结束时,能够及时释放临时对象的内存

// for 和 for in 里面是没有自动包装@autoreleasepool着的,而下面的方法是由@autoreleasepool自动包围的
[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
    // 这里被一个局部@autoreleasepool包围着
}];
复制代码
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSString* str = [[[NSString alloc] initWithString:@"666"] autorelease];
[pool drain];

// 其做用于为drain 和 init 之间
复制代码
  • 回归正题@autoReleasePool什么时间释放?
    • 一个被autoreleasepool包裹生成得对象,都会在其建立生成以后自动添加autorelease, 而后被autorelease对象得释放时机 就是在当前runloop循环结束的时候自动释放的
    • 参考连接:blog.sunnyxx.com/2014/10/15/…

子线程中的autorelease变量何时释放?

  • 子线程中会默认包裹一个autoreleasepool的, 释放时机是当前线程退出的时候。

autoreleasepool是如何实现的?

  • @autoreleasepool{} 本质上是一个结构体:
  • autoreleasepool会被转换成__AtAutoreleasePool
  • __AtAutoreleasePool 里面有两个函数objc_autoreleasePoolPush(),objc_autoreleasePoolPop().,其实一些列下来以后实际上调用得是AutoreleasePoolPage类中得push 和 pop两个类方法
  • push就是压栈操做,
  • pop就是出栈操做于此同时对其对象发送release消息进行释放

###26. iOS界面渲染机制? [这是很大的一个模块,里面牵扯不少东西, 耐心看下去]

  • 先简单解释一下渲染机制

首先iOS渲染视图的核心是Core Animation,其渲染层次依次为:图层树->呈现树->渲染树

  • 一共三个阶段

  • CPU阶段(进行Frame布局,准备视图和图层之间的层级关系)

  • OpenGL ES阶段(iOS8之后改为Metal), (渲染服务把上面提供的图层上色,生成各类帧)

  • GPU阶段 (把上面操做的东西进行一些列的操做,最终展现到屏幕上面)

  • 稍微详细说明

  • 首先一个视图由CPU进行Frame布局,准备视图和图层的层及关系。

  • CUP会将处理视图和图层的层级关系打包,经过IPC(进程间的通讯)通道提交给渲染服务(OpenGL和GPU)

  • 渲染服务首先将图层交给OpenGL进行纹理生成和着色,生成先后帧缓存,再根据硬件的刷新帧率,通常以设备的VSync信号和CADisplayLink(相似一个刷新UI专用的定时器)为标准,进行先后帧缓存的切换

  • 最后,将最终 要显示在画面上的后帧缓存交给GPU,进行采集图片和形状,运行变换, 应用纹理混合,最终显示在屏幕上。

程序卡顿的缘由?

  • 正常渲染流程
  • CPU计算完成以后交给GPU,来个同步信号Vsync 将内容渲染到屏幕上
  • 非正常(卡顿/掉帧)的流程
  • CPU计算时间正常或者慢,GPU渲染时间长了, 这时候Vsync信号, 因为没有绘制彻底,CUP开始计算下一帧,当下一帧正常绘制成功以后,把当前没有绘制完成的帧丢弃, 显示了下一帧,因而这样就形成了卡顿。 须要注意的是:Vsync时间间隔是固定的, 好比60帧率大的Vsync 是每16ms就执行一个一次,相似定时器同样

这里会出现一个面试题!!! 题目以下:

  • 从第一次打开App到彻底开始展示出UI,中间发生了什么? 或者App是怎么渲染某一个View的?
  • 回答就是上面的稍微详细说明,若是要求更详细, 能够继续深究一下。

在科普一下 1.Core Animation Core Animation 在 RunLoop 中注册了一个 Observer,监听了 BeforeWaiting 和 Exit 事件。这个 Observer 的优先级是 2000000,低于常见的其余 Observer。当一个触摸事件到来时,RunLoop 被唤醒,App 中的代码会执行一些操做,好比建立和调整视图层级、设置 UIView 的 frame、修改 CALayer 的透明度、为视图添加一个动画;这些操做最终都会被 CALayer 捕获,并经过 CATransaction 提交到一个中间状态去(CATransaction 的文档略有提到这些内容,但并不完整)。当上面全部操做结束后,RunLoop 即将进入休眠(或者退出)时,关注该事件的 Observer 都会获得通知。这时 CA 注册的那个 Observer 就会在回调中,把全部的中间状态合并提交到 GPU 去显示;若是此处有动画,CA 会经过 DisplayLink 等机制屡次触发相关流程。

2.CPU渲染职能

  • 布局计算:若是视图层级过于复杂,当试图呈现或者修改的时候,计算图层帧率就会消耗一部分时间,
  • 视图懒加载: iOS只会当视图控制器的视图显示到屏幕上才会加载它,这对内存使用和程序启动时间颇有好处,可是当呈现到屏幕以前,按下按钮致使的许多工做都不会被及时响应。好比,控制器从数据局中获取数据, 或者视图从一个xib加载,或者涉及iO图片显示都会比CPU正常操做慢得多。
  • 解压图片:PNG或者JPEG压缩以后的图片文件会比同质量的位图小得多。可是在图片绘制到屏幕上以前,必须把它扩展成完整的未解压的尺寸(一般等同于图片宽 x 长 x 4个字节)。为了节省内存,iOS一般直到真正绘制的时候才去解码图片。根据你加载图片的方式,第一次对 图层内容赋值的时候(直接或者间接使用 UIImageView )或者把它绘制到 Core Graphics中,都须要对它解压,这样的话,对于一个较大的图片,都会占用必定的时间。
  • Core Graphics绘制:若是对视图实现了drawRect:或drawLayer:inContext:方法,或者 CALayerDelegate 的 方法,那么在绘制任何东 西以前都会产生一个巨大的性能开销。为了支持对图层内容的任意绘制,Core Animation必须建立一个内存中等大小的寄宿图片。而后一旦绘制结束以后, 必须把图片数据经过IPC传到渲染服务器。在此基础上,Core Graphics绘制就会变得十分缓慢,因此在一个对性能十分挑剔的场景下这样作十分很差。
  • 图层打包:当图层被成功打包,发送到渲染服务器以后,CPU仍然要作以下工做:为了显示 屏幕上的图层,Core Animation必须对渲染树种的每一个可见图层经过OpenGL循环 转换成纹理三角板。因为GPU并不知晓Core Animation图层的任何结构,因此必须 要由CPU作这些事情。这里CPU涉及的工做和图层个数成正比,因此若是在你的层 级关系中有太多的图层,就会致使CPU没一帧的渲染,即便这些事情不是你的应用 程序可控的。

3.GPU渲染职能 GPU会根据生成的先后帧缓存数据,根据实际状况进行合成,其中形成GPU渲染负担的通常是:离屏渲染,图层混合,延迟加载。

这里又会出现一个面试题!!! 一个UIImageView添加到视图上之后,内部如何渲染到手机上的?

图片显示分为三个步骤: 加载、解码、渲染、 一般,咱们程序员的操做只是加载,至于解码和渲染是由UIKit内部进行的。 例如:UIImageView显示在屏幕上的时候须要UIImage对象进行数据源的赋值。而UIImage持有的数据是未解码的压缩数据,当赋值的时候,图像数据会被解码变成RGB颜色数据,最终渲染到屏幕上。


看完上面的又来问题了! 关于UITableView优化的问题?(真他妈子子孙孙无穷尽也~) 先说形成UITableView滚动时候卡顿的的缘由有哪些?

  • 隐式绘制 CGContext
  • 文本CATextLayer 和 UILabel
  • 光栅化 shouldRasterize
  • 离屏渲染
  • 可伸缩图片
  • shadowPath
  • 混合和过分绘制
  • 减小图层数量
  • 裁切
  • 对象回收
  • Core Graphics绘制
  • -renderInContext: 方法

在说关于UITableView的优化问题!

基础的

  • 重用机制(缓存池)
  • 少用有透明度的View
  • 尽可能避免使用xib
  • 尽可能避免过多的层级结构
  • iOS8之后出的预估高度
  • 减小离屏渲染操做(圆角、阴影啥的)

  • **** 解释一下为何减小离屏渲染操做?****

  • 须要建立新的缓冲区

  • 整个过程须要屡次切换上下文环境, 显示从当前的屏幕切换到离屏,等待离屏渲染结束后,将离屏缓冲区的渲染结果 显示到屏幕有上, 又要将上下文环境从离屏切换到当前屏幕,

  • 那些操做会触发离屏渲染?

  • 光栅化 layer.shouldRasterize = YES

  • 遮罩layer.mask

  • 圆角layer.maskToBounds = Yes,Layer.cornerRadis 大于0

  • 阴影layer.shadowXXX

进阶的

  • 缓存cell的高度(提早计算好cell的高度,缓存进当前的模型里面)
  • 异步绘制
  • 滑动的时候,按需加载

高阶的

  • 你想不到 居然不推荐用UILabel。哈哈哈~ 至于为何 看下面的连接吧

至于上面的那些基础的,涉及到渲染级别的本身说的时候悠着点,面试官若是想搞你的话,考一考你最上面的那些,CUP和GUP,以及openGL相关, 在考一下你进程通讯IPC,以及VSync信号啥的, 这些东西太鸡儿高深了,没点匠心 这东西还真搞不了,要想研究能够看看YYKit的做者写的一篇关于页面流畅的文章:blog.ibireme.com/2015/11/12/…

卡顿检测的方法

  • 卡顿就是主线程阻塞的时间问题,能够添加Observer到主线程Runloop中,经过监听Runloop状态切换的耗时,以达到监听卡顿的目的

继续

既然都是图形绘制了,那就再研究一下事件响应链&原理

传统的问法来了:UIView和CALayer的区别? 一般咱们这样回答:UIView能够响应用户事件,而CALayer不能处理事件


回答这个以前, 先回顾一下另一个经典面试题:事件响应链和事件传递?

基本概念:

  • 响应链: 是由连接在一块儿的响应者(UIResponse子类)组成的,通常为第一响应着到application对象以及中间全部响应者一块儿组成的。

  • 事件传递: 获取响应链以后, 将事件由第一响应者网application的传递过程

  • enter image description here

  • enter image description here

  • 事件的分发和传递

  • 当程序中发生触摸事件以后,系统会将事件添加到UIApplication管理的一个队列当中

  • UIApplication将处于任务队列最前端的事件向下分发 即UIWindow

  • UIWindow将事件向下分发,即UIView或者UIViewController

  • UIView首先看本身可否处理这个事件,触摸点是否在本身身上,本身的透明度是否大于0,01,userInteractionEnabled 是不是YES, Hidden实际是NO,若是这些都知足,那么继续寻找其子视图

  • 遍历子控件,重复上面步骤

  • 若是没有找到,那么本身就是改事件的处理者

  • 若是本身不能处理,那么就不作任何处理 即视为没有合适的View能接收处理当前事件,则改事件会被废弃。

  • *** 怎么寻找当前触摸的是哪个View?*** 下面中两个方法

// 此方法返回的View是本次点击事件须要的最佳View
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event

// 判断一个点是否落在范围内
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
复制代码

事件传递给控件以后, 就会调用hitTest:withEvent方法去寻找更合适的View,若是当前View存在子控件,则在子控件继续调用hitTest:withEvent方法判断是不是合适的View, 若是还不是就一直遍历寻找, 找不到的话直接废弃掉。

// 由于全部的视图类都是继承BaseView
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
   // 1.判断当前控件可否接收事件
   if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
   // 2. 判断点在不在当前控件
   if ([self pointInside:point withEvent:event] == NO) return nil;
   // 3.从后往前遍历本身的子控件
   NSInteger count = self.subviews.count;
   for (NSInteger i = count - 1; i >= 0; i--) {
      UIView *childView = self.subviews[I];
       // 把当前控件上的坐标系转换成子控件上的坐标系
	  CGPoint childP = [self convertPoint:point toView:childView];
      UIView *fitView = [childView hitTest:childP withEvent:event];
       if (fitView) { // 寻找到最合适的view
           return fitView;
       }
   }
   // 循环结束,表示没有比本身更合适的view
   return self;
   
}
复制代码
  • 判断触摸点是否在视图内?
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
复制代码
  • tableView 加一个tap的手势, 点击当前cell的位置 哪一个事件被响应 为何?
  • tap事件被响应, 由于tap事件添加以后,默认是取消当前tap之外的全部事件的, 也就是说, tap事件处于当前响应者链的最顶端, 解决的办法执行tap的delagete, 实现
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch  
{  
    if([touch.view isKindOfClass:[XXXXcell class]])  
    {  
        return NO;  
    }  
    return YES;  
}

复制代码
相关文章
相关标签/搜索