iOS-OC高级面试题基础篇

1. 在OC 中 Category(分类)和Extension(扩展)的区别? 分别可以用来做什么? 分类有哪些局限性? 分类的结构体里面有哪些成员?
区别:

1).分类(category)有名字, 扩展(extension)没有名字, 扩展是一种特殊的分类;
2).分类只能扩展方法(分类里面的属性仅仅只是声明属性的get set方法, 被没有真正的实现)
3).Extension 可以扩展方法、属性、成员变量;

Category功能:

1).对SDK提供类的扩展;
2).不想生成新的子类的情况下,例如对NSArray 的扩展;
3).方便做项目管理, 可以将一份源码在多个地方共享或者做方法版本管理、多人写作开发、用本地版本替换公共版本实现;

Extension功能:

1).一般类扩展写到.m 文件中; 一般私有属性写到类扩展中;
2). 当需要声明一个属性, 他对外只读, 但是在内部可以修改, 可以通过Extension 实现;

分类的局限性:

1).分类的优先级比较高可以覆盖原类的方法;
2).分类方法中不可以调用super 方法;
3).分类方法也可能回覆盖同一个类的其他分类中的方法, 也可能被覆盖,因为无法预知他们的加载顺序,通常会在编译时报错;
4).分类理论上不能添加成员变量, 但是也可以使用@dyanmic(runtime)来弥补;

分类用结构体成员 category_t包含:

1)、类的名字(name)
2)、类(cls)
3)、category中所有给类添加的实例方法的列表(instanceMethods)
4)、category中所有添加的类方法的列表(classMethods)
5)、category实现的所有协议的列表(protocols)
6)、category中添加的所有属性(instanceProperties)

typedef struct category_t  {
    const char *name;
    classref_t cls;
    struct method_list_t *instanceMethods;
    struct method_list_t *classMethods;
    struct protocol_list_t *protocols;
    struct property_list_t *instanceProperties;
} category_t;
2. atomic(原子属性) 的实现机制? 为什么不能保证绝对的线程安全?
// atomic setter 方法
-(void)setName:(NSString *) nane  {
@synchronized (self)  {
if (_name = name)  {
[_name release];
[_name retain];
     }
  }
}
// getter 方法
-(NSString *)name  {
@synchronized(self) {
 return _name;
      }
}

1.atomic 的setter、 getter 方法多出了@synchronized 代码块儿;
@synchronized所做的事情跟(lock)类似(防止不同的线程执行同一段代码),( 官方文档强调: 防止不同的线程同事获取相同的锁);
2.对于atomic的属性, 系统生成getter、setter 方法会保证 get set 操作的完整性, 不受其他线程影响;(比如,线程A的getter方法运行到一半,线程B调用了setter方法,那么线程A的getter还是能得到一个完好无损的对象);
3.一般情况下,iOS程序的所有属性都声明为nonatomic,是因为。在iOS中使用同步锁的开销比较大,这会带来性能问题,并且atomic并不能保证线程安全。想要实现线程安全,还需要采用更为深层的锁定机制。
其实属性无论是否是原子性的,只是针对于getter和setter而言。比如用atomic去操作一个NSMutableArray ,如果一个线程循环读数据,一个线程循环写数据,肯定会产生内存问题,这个就跟getter和setter没有关系了。

3. atomic 为什么不是线程绝对安全的?

1).当使用atomic时,虽然对属性的读和写是原子性的,但是仍然可能出现线程错误:
2).当线程A进行属性写操作,这时其他线程的读或者写操作会因为A操作而等待;
3).当A线程的属性写操作结束后,B线程进行此属性写操作,然后当A线程需要读操作时,却获得了在B线程中此属性的值,这就破坏了线程安全,如果有线程C在A线程读操作前release了该属性,那么还会导致程序崩溃;
4).所以仅仅使用atomic并不会使得线程安全,我们还要为线程添加lock来确保线程的安全;

4. 多线程数据为什么不安全?

1). 多条线程同时工作的情况下,通过运用线程锁、原子性等方法避免多条线程同时访问一块内存造成的数据错误或者冲突

2).每条线程都有自己独立的栈空间, 但是他们共用了堆, 所以他们可能同时访问了同一块内存空间.
3).解决线程安全的方法, 线程锁, 原子锁;

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

历史: runtime 维护了一个weak 表, 用于存储某个对象的所有weak指针; weak表其实是一个hash(哈希)表,Key是所指对象的地址,Value是weak指针的地址数组;

1). 初始化: runtime 会调用objc_initWeak()函数, 初始化一个新的weak 指针指向对象的地址;
2).添加引用时: objc_initWeak() 函数会调用objc_storeWeak()函数, objc_storeWeak() 的作用是更新指针指向, 创建对应的弱引用表;
3). 释放时: 调用clearDeallocating() 函数, clearDeallocating()函数首先根据对象的地址获取所有weak指针地址的数组, 然后遍历数组把其中的数据设置为nil, 最后把这个entry从weak 表中删除, 最后清理对象记录;

SideTable: 主要用于管理对象的引用计数和 weak 表, 在NSObject.mm 中声明其数据结构:

struct SideTable {
// 保证原子操作的自旋锁
spinlock t slock;
// 引用计数的hash 表
RefcountMap refcnts;
//  weak 引用全局hash 表
weak_table_t weak _table;
}

weak_table_t 结构体构成:

struct weak_table_t  {
weak entry_t  *weak_entries;
size_t  num_entries;
uintptr_t  mask;
uintptr_t max_hash_displacement;
}

这是一个全局弱引用hash表; 使用不定类型对象的地址作为 key ,用 weak_entry_t 类型结构体对象作为 value 。其中的 weak_entries 成员,从字面意思上看,即为弱引用表入口;

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

1).关联对象是指某一个OC 对象通过一个唯一的key 连接到一个类的实例上;
2).给NSArray添加一个属性,就使用runtime关联对象;(就是添加系统级的公共属性)

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

1.KVO基于runtime实现.
2.当一个类(A)的属性被观察时,系统会通过runtime动态的创建一个A的派生类(B),B类继承于A类.
3.将A类的isa(通过isa-swapping交换)指针指向B类.
4.在B类中重写被观察属性的setter方法,重写的setter方法会调用父类的setter方法之前和之后,通知观察者对象值得变更情况.

使用取消系统默认KVO的方法:

+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    if ([key isEqualToString:@"name"]) {
        return NO;
    } else {
        return [super automaticallyNotifiesObserversForKey:key];
    }
}

-(void)setName:(NSString *)name {
    if (_name!=name) {
        [self willChangeValueForKey:@"name"];
        _name=name;
        [self didChangeValueForKey:@"name"];
    }
}
8.KVC 的底层实现原理?
  1. 检查是否存在对应key的set方法,如果存在就调用set方法.
  2. 如果set方法不存在, 就会查找与key相同名称并且带下划线的成员变量,如果有,则直接给成员变量属性赋值.
  3. 如果没找到_key,就会查找相同名称的属性key,如果有就赋值.
  4. 如果还没找到,就调用valueForUndefinedKey: 和setValue:forUndefinedKey:方法.
9. Autoreleasepool的实现原理?

1). 在运行循环开始前,系统会自动创建一个autoreleasepool(一个autoreleasepool会存在多个AutoreleasePoolPage),此时会调用一次objc_autoreleasePoolPush函数,runtime会向当前的AutoreleasePoolPage中add进一个POOL_SENTINEL(哨兵对象,值为0,也就是个nil,代表autoreleasepool的起始边界),并返回此哨兵对象的内存地址poolToken
2). 在运行循环结束时,autoreleasepool会被drain掉,此时会调用objc_autoreleasePoolPop(poolToken)函数,入参是之前产生的POOL_SENTINEL的内存地址poolToken,对在POOL_SENTINEL之后添加的所有autoreleased对象调用一次release,可以向前跨越若干个page,直到哨兵对象所在的page,并向回移动next指针到哨兵对象所在位置
3). 中间{...}所产生的autoreleased对象都会被插入到最近的autoreleasepool中(因为autoreleasepool存在嵌套的情况)

10.Autoreleasepool所使用的数据结构是什么?AutoreleasePoolPage结构体了解么?

1). autoreleasepool本质上就是一个指针堆栈
2). 指针堆栈中存放的是autoreleased对象的内存地址 或者 POOL_SENTINEL的内存地址
3). 内部结构是由若干个以page为结点的双向链表组成,系统会在需要的时候动态地增加或删除page节点,这里说的page就是下面即将说到的AutoreleasePoolPage对象

  1. magic用来校验AutoreleasePoolPage的结构是否完整;
  2. next指向下一个即将产生的autoreleased对象的存放位置(当next == begin()时,表示AutoreleasePoolPage为空;当next == end()时,表示AutoreleasePoolPage已满)
  3. thread指向当前线程,一个AutoreleasePoolPage只会对应一个线程,但一个线程可以对应多个AutoreleasePoolPage;
  4. parent指向父结点,第一个结点的 parent 值为 nil;
  5. child指向子结点,最后一个结点的 child 值为 nil;
  6. depth代表深度,第一个page的depth为0,往后每递增一个7. page,depth会加1;
  7. hiwat代表 high water mark

总结:
前面所说的autoreleasepool的内部结构是由若干个以AutoreleasePoolPage为结点的双向链表组成,这个双向链表就是通过上述结构中的parent指针和child指针连接起来的

每个AutoreleasePoolPage对象会开辟4KB内存(也就是虚拟内存一页的大小),除了上面的实例变量所占空间,剩下的空间全部用来储存autoreleased对象的内存地址

一个page的空间被占满时,会新建一个page,通过parent指针和child指针连接链表,之后的autoreleased对象会加入到新建的page中。

11.讲一下对象,类对象,元类,根元类结构体的组成以及他们是如何相关联的?为什么对象方法没有保存在对象结构体里,而是保存在类对象的结构体里?
12. NSThread、NSRunLoop 和 NSAutoreleasePool三者之间的关系?
  1. NSThread 和 NSRunLoop是一一对应的关系
  2. 在NSRunLoop对象的每个运行循环(event loop)开始前,系统会自动创建一个autoreleasepool,并在运行循环(event loop)结束时drain掉这个pool,同时释放所有autoreleased对象
  3. Autoreleasepool 只会对应一个线程,每个线程可能会对应多个Autoreleasepool,比如 autoreleasepool 嵌套的情况
13. class_ro_t 和 class_rw_t 的区别?
  1. Objc 类中的属性、方法、遵循的协议等信息都保存在class_rw_t中.
  2. 还有一个指向常量的指针ro,其中储存了当前类在编译期就已经确定的属性、方法、遵循的协议.

class_rw_t结构体内有一个指向class_ro_t结构体的指针。

每个类都对应有一个class_ro_t结构体和一个class_rw_t结构体。在编译期间,class_ro_t结构体就已经确定,objc_class中的bits的data部分存放着该结构体的地址。在runtime运行之后,具体说来是在运行runtime的realizeClass 方法时,会生成class_rw_t结构体,该结构体包含了class_ro_t,并且更新data部分,换成class_rw_t结构体的地址。

14. iOS 中内省的几个方法?

对象在运行时获取其类型的能力称为内省.
OC在runtime 内省的四个方法:

// 判断对象类型:
-(BOOL) isKindOfClass: 判断是否是这个类或者这个类的子类的实例
-(BOOL) isMemberOfClass: 判断是否是这个类的实例

// 判断对象or类是否有这个方法
-(BOOL) respondsToSelector: 判断实例是否有这样方法
+(BOOL) instancesRespondToSelector: 判断类是否有这个方法
15. class方法和objc_getClass方法有什么区别?
  1. objc_getClass参数是类名的字符串,返回的就是这个类的类对象.
  2. object_getClass参数是id类型,它返回的是这个id的isa指针所指向的Class,如果传参是Class,则返回该Class的metaClass.
16.在运行时创建类的方法objc_allocateClassPair的方法名尾部为什么是pair(成对的意思)?
17.一个int变量被__block修饰与否的区别?

1.在没有修饰的情况下,被block捕获,是值拷贝.
2.在使用__block修饰的时候,会生成一个结构体,复制int的引用地址,达到修改数据的目的.

18.为什么在block外部使用weakSelf修饰的同时需要在内部使用strongSelf修饰?
__weak __typeof(self)weakSelf = self;
AFNetworkReachabilityStatusBlock callback = ^(AFNetworkReachabilityStatus status) {
    __strong __typeof(weakSelf)strongSelf = weakSelf;
    strongSelf.networkReachabilityStatus = status;
    if (strongSelf.networkReachabilityStatusBlock) {
        strongSelf.networkReachabilityStatusBlock(status);
    }
};
  1. 外部使用weakSelf 修饰是为了防止强循环引用,产生不必要的内存泄漏.
  2. 内部使用strongSelf是因为在block的内部weakSelf有可能为self或者为nil(当前正在网络加载,而此时用户关闭了该界面,会导致crash),为了不让self 不为nil, 在block内部将weakSelf转换成strongSelf,当block结束时,该strongSelf变量也会被自动释放,既避免了循环引用,又让self在block 内部不为nil.
19.RunLoop的作用是什么?它的内部工作机制了解么?(最好结合线程和内存管理来说)
  1. runloop 与线程是一一对应的, 一个runloop 对应一个核心线程.
  2. runloop 是管理线程的, 当线程的runloop被开启后, 线程会在执行完任务后进入休眠状态, 有任务后会被唤醒去执行任务;
  3. runloop 在第一次获取时被创建, 在线程结束时被销毁;
  4. 对于主线程来说,runloop 在程序启动的时候就被创建好了, 对于子线程来说,runloop 是懒加载的, 只有当我们使用的时候才会创建, 所以在子线程使用定时器的时候需要注意, 确保子线程的runloop 被创建, 不然定时器不会回调;
20.比较OC 引用计数关键词 strong、 weak 、assign、 copy的不同?

retain: 表示持有特性, setter 方法将传入的参数先保留, 在赋值,传入参数的retaincount会加一;
strong: 表示指向并拥有该对象, 其修饰的对象引用计数加一,该对象只要是引用计数不为零则不会被销毁,当然强行将其设置为nil 则可以销毁它;
weak: 表示指向但不拥有该对象,其修饰的对象引用计数不会加一,无需手动设置,该对象会自行在内存中销毁;
assign: 主要修饰数据类型, 比如: NSInteger 、CGFloat, 这些数值主要存在于栈上;
weak: 一般用来修饰对象
assign: 一般用来修饰基本数据类型, 原因是assign修饰对象被释放后,指针的地址依然存在, 会造成野指针,在堆上容易崩溃, 而栈上的内存系统会自动释放,不会造成野指针;
copy 与strong 类似, 不同之处在于strong的复制是多个指针指向同一个地址copy: 的复制每次会在内存中拷贝一份对象, 指针指向不同地址, copy 一般用于修饰有可变类型对应的不可变对象上, 如 NSString NSArray NSdictionary.

21. @synthesize和@dynamic分别有什么作用?

1). @property有两个对应的词,一个是 @synthesize,一个是 @dynamic。如果 @synthesize和 @dynamic都没写,那么默认的就是@syntheszie var = _var;
2). @synthesize 的语义是如果你没有手动实现 setter 方法和 getter 方法,那么编译器会自动为你加上这两个方法。
3). @dynamic 告诉编译器:属性的 setter 与 getter 方法由用户自己实现,不自动生成。(当然对于 readonly 的属性只需提供 getter 即可).

假如一个属性被声明为 @dynamic var,然后你没有提供 @setter方法和 @getter 方法,编译的时候没问题,但是当程序运行到 instance.var = someVar,由于缺 setter 方法会导致程序崩溃;或者当运行到 someVar = var 时,由于缺 getter 方法同样会导致崩溃。编译时没问题,运行时才执行相应的方法,这就是所谓的动态绑定。

16.block 的内部实现原理?

block在内部会作为一个指向结构体的指针,当调用block的时候其实就是根据block对应的指针找到相应的函数,进而进行调用,并传入自身.

struct __Block_byref_num_0 {
  void *__isa;  // isa指针
__Block_byref_num_0 *__forwarding;  // 实例本身
 int __flags; 
 int __size;
 int num;
22.被weak修饰的对象在被释放的时候会发生什么?是如何实现的?知道sideTable么?里面的结构可以画出来么?

被weak修饰的对象在被释放时候会置为nil,不同于assign;

Runtime维护了一个weak表,用于存储指向某个对象的所有weak指针。weak表其实是一个hash(哈希)表,Key是所指对象的地址,Value是weak指针的地址(这个地址的值是所指对象指针的地址)数组。

1)、初始化时:runtime会调用objc_initWeak函数,初始化一个新的weak指针指向对象的地址。
2)、添加引用时:objc_initWeak函数会调用 objc_storeWeak() 函数, objc_storeWeak() 的作用是更新指针指向,创建对应的弱引用表。
3)、释放时,调用clearDeallocating函数。clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录.
sideTable:

struct SideTable {
    // 保证原子操作的自旋锁
    spinlock_t slock;
    // 引用计数的 hash 表
    RefcountMap refcnts;
    // weak 引用全局 hash 表
    weak_table_t weak_table;
}

struct weak_table_t {
    // 保存了所有指向指定对象的 weak 指针
    weak_entry_t *weak_entries;
    // 存储空间
    size_t    num_entries;
    // 参与判断引用计数辅助量
    uintptr_t mask;
    // hash key 最大偏移值
    uintptr_t max_hash_displacement;
};

23.内存管理的范围?

只有OC 对象需要进行内存管理, 非OC对象类型譬如基本数据类型是需要在栈内存进行管理的,栈内存会自动释放;

23.1.内存管理的本质?

OC 的对象在内存中是以堆的方式分配空间的, 并且堆内存是谁管理谁释放, 堆里面的内存是动态分配的,就需要添加内存,回收内存;

23.2.内存分配及管理方式?

堆是动态分配内存和回收内存的, 没有静态分配的堆;
栈有两种分配方式, 静态分配和动态分配, 静态分配是系统编译器完成的, 譬如局部变量的分配, 动态分配是是有alloc函数进行分配,但是栈的动态分配和堆不同,他的动态分配也是有编译器进行释放的;

堆:是大家共有的空间,分全局堆和局部堆。全局堆就是所有没有分配的空间,局部堆就是用户分配的空间。堆在操作系统对进程初始化的时候分配,运行过程中也可以向系统要额外的堆,但是记得用完了要还给操作系统,要不然就是内存泄漏。堆里面一般放的是静态数据,比如static的数据和字符串常量等,资源加载后一般也放在堆里面。一个进程的所有线程共有这些堆 ,所以对堆的操作要考虑同步和互斥的问题。程序里面编译后的数据段都是堆的一部分;

栈:是个线程独有的,保存其运行状态和局部自动变量的。栈在线程开始的时候初始化,每个线程的栈互相独立,因此 ,栈是 thread safe的。每个c++对象的数据成员也存在在栈中,每个函数都有自己的栈,栈被用来在函数之间传递参数。操作系统在切换线程的时候会自动的切换栈,就是切换ss/esp寄存器。栈空间不需要在高级语言里面显式的分配 和释放;
堆:由程序员分配和释放,如果不释放可能会引起内存泄漏
栈:由编译器自动分配和释放,一般存放参数值,局部变量.

UI 篇

1.哪些场景可以触发离屏渲染?

CPU渲染机制:
cpu 计算好显示数据提交给gpu, gpu渲染完成后将渲染结果放入帧缓冲区, 随后视频控制器会按照V-Sync(垂直同步) 信号逐行读取帧缓冲区的数据, 经过模数模转换传递给显示器显示;

GPU屏幕渲染有以下两种方式:
On-Screen Rendering
意为当前屏幕渲染,指的是GPU的渲染操作是在当前用于显示的屏幕缓冲区中进行;
Off-Screen Rendering
意为离屏渲染,指的是GPU在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作;

特殊的离屏渲染:
如果GPU不在的当前屏幕缓冲区中进行的渲染都称为离屏渲染,那么就还有另一种特殊的“离屏渲染”方式: CPU渲染;
如果我们重写了drawRect方法,并且使用任何Core Graphics的技术进行了绘制操作,就涉及到了CPU渲; 整个渲染过程由CPU在App内同步地
完成,渲染得到的bitmap最后再交由GPU用于显示;

备注:CoreGraphic通常是线程安全的,所以可以进行异步绘制,显示的时候再放回主线程,一个简单的异步绘制过程大致如下:

- (void)display {
     dispatch_async(backgroundQueue, ^{
         CGContextRef ctx = CGBitmapContextCreate(...);
         // draw in context...
         CGImageRef img = CGBitmapContextCreateImage(ctx);
         CFRelease(ctx);
         dispatch_async(mainQueue, ^{
             layer.contents = img;
         });
     });
  }
离屏渲染的触发方式:

1.shouldRasterize(光栅化): 将图像转换为一个个格栅组成的图像, 使其每个元素对应帧缓冲区的一个像素, shouldRasterize = YES在其他属性触发离屏渲染的同时,会将光栅化后的内容缓存起来,如果对应的layer及其sublayers没有发生改变,在下一帧的时候可以直接复用。shouldRasterize = YES,这将隐式的创建一个位图,各种阴影遮罩等效果也会保存到位图中并缓存起来,从而减少渲染的频度(不是矢量图), 光栅化相当于是把GPU 的操作转换到CPU上, 生成位图缓存, 直接读取复用;

光栅化例子:

我们日程经常打交道的TableViewCell,因为TableViewCell的重绘是很频繁的(因为Cell的复用),如果Cell的内容不断变化,则Cell需要不断重绘,如果此时设置了cell.layer可光栅化,则会造成大量的离屏渲染,降低图形性能.
2.masks(遮罩);
3.shadows(阴影);
4.edge antialiasing(抗锯齿);
5.group opacity(不透明);
6.圆角;
7.渐变;

你可能感兴趣的:(iOS-OC高级面试题基础篇)